Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/reference/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,21 @@ keep their exit code (normally 2), as with `list --json`.

```bash
specify preset resolve <name>
specify preset resolve <name> --kind script
specify preset resolve <name> --kind script --output <path>
```

Shows which file will be used for a given name by tracing the full resolution stack. Useful for debugging when multiple presets provide the same file.
Shows which file will be used for a given name by tracing the full resolution
stack. By default, dotted names resolve as commands and other names resolve as
templates. Use `--kind script` to inspect a script artifact, including a
manifest-declared file such as an `.mjs` module.

For a script that uses a composition strategy, the reported layer path is not
the composed output. Pass `--output <path>` to write the effective script to
that location. The command creates missing parent directories and overwrites
an existing destination file. Relative module imports in the written output
resolve from the output file's directory; referenced files are not copied.
`--output` is only valid with `--kind script`.

## Enable / Disable a Preset

Expand Down
68 changes: 65 additions & 3 deletions src/specify_cli/presets/command_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from __future__ import annotations

import re
from pathlib import Path
from typing import Literal

import typer
from rich.markup import escape as _escape_markup
Expand All @@ -16,12 +18,26 @@ def preset_resolve(
template_name: str = typer.Argument(
..., help="Template name to resolve (e.g., spec-template)"
),
kind: Literal["command", "script", "template"] | None = typer.Option(
None,
"--kind",
help="Artifact kind (defaults to command for dotted names, template otherwise)",
),
output: Path | None = typer.Option( # noqa: B008
None,
"--output",
help=(
"Write the effective script to this path "
"(required to materialize composition)"
),
),
):
"""Show which template will be resolved for a given name."""
"""Show which artifact wins, or materialize an effective script."""
from .. import _require_specify_project
from . import PresetResolver

is_command = "." in template_name
template_type = kind or ("command" if "." in template_name else "template")
is_command = template_type == "command"
valid_name = (
re.fullmatch(r"[a-z0-9-]+(?:\.[a-z0-9-]+)+", template_name)
if is_command
Expand All @@ -36,9 +52,49 @@ def preset_resolve(
)
raise typer.Exit(1)

if output is not None and template_type != "script":
console.print("[red]Error:[/red] --output can only be used with --kind script")
raise typer.Exit(1)

project_root = _require_specify_project()
resolver = PresetResolver(project_root)
template_type = "command" if is_command else "template"

if output is not None:
try:
content = resolver.resolve_content(template_name, "script")
Comment on lines +62 to +64
except Exception as exc:
console.print(
f"[red]Error:[/red] Could not resolve script: "
f"{_escape_markup(str(exc))}"
)
raise typer.Exit(1) from exc
if content is None:
console.print(
f"[yellow]{_escape_markup(str(template_name))}[/yellow]: "
"not found or cannot be composed"
)
raise typer.Exit(1)

output_path = output.expanduser()
if not output_path.is_absolute():
output_path = Path.cwd() / output_path
output_path = output_path.resolve()
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content, encoding="utf-8")
except OSError as exc:
console.print(
f"[red]Error:[/red] Could not write script to "
f"{_escape_markup(str(output_path))}: {_escape_markup(str(exc))}"
)
raise typer.Exit(1) from exc

console.print(
f" [bold]{_escape_markup(str(template_name))}[/bold]: "
f"{_escape_markup(str(output_path))}"
)
console.print(" [dim](effective script written to --output path)[/dim]")
return

layers = resolver.collect_all_layers(template_name, template_type)
safe_template_name = _escape_markup(str(template_name))
Expand Down Expand Up @@ -77,6 +133,12 @@ def preset_resolve(
console.print(
" [dim]Final output is composed from multiple preset layers; the path above is the highest-priority contributing layer.[/dim]"
)
if template_type == "script":
console.print(
" [dim]Use --output PATH to write the effective composed "
"script. Relative imports in the output resolve from that "
"file's directory.[/dim]"
)
console.print("\n [bold]Composition chain:[/bold]")
# Compute the effective base: first replace layer scanning from
# highest priority (matching resolve_content top-down logic).
Expand Down
Loading
Loading