-
Notifications
You must be signed in to change notification settings - Fork 12.2k
feat: add specify artifact introspection
#4305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
nicolehaugen
merged 116 commits into
main
from
nicolehaugen-contribution-ids-and-artifacts
Sep 11, 2026
Merged
Changes from all commits
Commits
Show all changes
116 commits
Select commit
Hold shift + click to select a range
3807522
Add deterministic contribution IDs and stack lookup IDs for resolved …
nicolehaugen 9a441fe
feat: add `specify artifact` command exposing composition stacks as JSON
nicolehaugen c47371e
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen ea2636f
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen b1d6a75
Potential fix for pull request finding 'Unused import'
nicolehaugen 424ca0f
Project preset artifacts by entry type
Copilot ac90181
Represent project override artifact layers
Copilot ed2f3db
Preserve artifact JSON init-dir errors
Copilot 7eb1a52
Canonicalize core script artifacts
Copilot 70965d7
Potential fix for pull request finding
nicolehaugen 70a937e
Fix artifact inventory resolver filtering
Copilot df4afef
Add resolver tests for single-runtime core scripts
Copilot 028fcd8
Cache artifact resolver lookups
Copilot 8e7cf83
Handle artifact resolver failures
Copilot fcb72b1
Document artifact resolution error
Copilot 69baa87
Include convention-based artifacts in inventory
Copilot e4347b0
Restore legacy flat core script lookup
Copilot 2c36d57
Extend convention discovery to presets in artifact inventory
Copilot f6eacd2
Fix manifest path portability and export ArtifactResolutionError
Copilot f601efa
Bound artifact manifest search to project root
Copilot 5410f72
Cover project-root artifact manifests
Copilot 9b68bac
Handle directory artifact manifest lookups
Copilot f9ee35c
Fall back to top-level preset name in artifact stacks
Copilot 95bbd99
Include project-local core artifacts in inventory
Copilot aacb49b
Address inline review feedback on artifact resolver helpers
Copilot fef72da
Reuse manifest/registry APIs in artifact contribution enumeration
Copilot 442cd23
Pass layer explicitly to _iter_pack_contributions instead of inferrin…
Copilot ddadd5f
Fix core command namespacing and validate names for kind-scoped lookups
Copilot 602f042
Skip manifest contributions without a usable identifier
Copilot 63237cf
Hoist test-local imports to module scope in artifact/assets tests
Copilot 67c107f
fix: resolve artifact inventory and validation review regressions
Copilot 2514302
perf: avoid duplicate read in core command inventory
Copilot f93f526
fix: classify dotted override-only artifacts as commands
Copilot 5b08932
fix: accept single-segment artifact commands
Copilot 355ed82
fix: fail closed on corrupt artifact registries
Copilot d0f9f25
fix: trust inventory for artifact info lookups
Copilot b4de317
fix: validate registry before artifact info
Copilot 4857201
fix: resolve artifact description by layer precedence, not enumeratio…
Copilot a20502d
fix: validate subdir before wheel bundle lookup in _locate_core_asset…
Copilot a149714
fix: detect duplicate hooks after command canonicalization
Copilot 9fd43d7
fix: reuse normalized hook entries for duplicate detection
Copilot 4682021
fix: align core command candidate ordering
Copilot c816a29
test: cover manifest-backed artifact parity
Copilot 60050a8
fix: align artifact IDs with resolver identity
Copilot 1c4842f
fix: skip invalid local artifact name components
Copilot a3293eb
fix: filter invalid local artifact IDs from inventory
Copilot 0377861
fix: align artifact preset enumeration with resolver
Copilot 1739a09
test: remove tautological artifact tests and strengthen id assertion
Copilot 866b3a5
fix: preserve documented hook duplicate semantics
Copilot 467ddca
fix: dedupe hook contributions last-wins
Copilot a2fc586
docs: clarify hook identifier deduplication
Copilot f9efe06
docs: remove hook discriminator references
Copilot 9ae2682
style: space identifier declarations
Copilot 7e0e7dc
fix: address unresolved review feedback on PR #4305
Copilot e1a63ce
fix: keep on-disk preset/extension identity separate from lookupId
Copilot fd2b268
fix: remove stale lookupId parsing fallback and tighten malformed loo…
Copilot 1c4319c
fix: route resolver core fallback through shared asset resolver, desc…
Copilot f7b549f
refactor: drop redundant derive_named_id import-visibility assignment
Copilot 083af4c
fix: align artifact inventory and lookup ID validation
nicolehaugen bd77448
fix: fail closed on malformed artifact registries
nicolehaugen 46394a9
Merge remote-tracking branch 'origin/nicolehaugen-contribution-ids-an…
nicolehaugen 436f394
fix: preserve convention artifact descriptions
nicolehaugen 7416247
fix: align artifact override resolution
Copilot 4b8b381
Refactor artifact inventory candidates
Copilot 19beb04
Address artifact inventory review
Copilot dd9ef64
Address artifact inventory review
Copilot 8c29bfe
Address artifact inventory review
Copilot 8678014
source-agnostic artifact IDs; built-in tier recognized by exclusion, …
Copilot 05eaf72
fix: reject malformed artifact layer provenance
Copilot b6f4481
Tighten artifact provenance handling
Copilot cea47c7
Refactor shared asset directory lookup
Copilot 2f3ec02
Document shared asset families
Copilot 38ce9f6
Avoid full artifact content scans
Copilot f881074
Clarify artifact resolution guard
Copilot 9c73550
Restore resolver core provenance
Copilot ef7fca3
Reuse artifact inventory layers
Copilot 2b8ce69
Simplify preset resolve assertion
Copilot 73832c7
Restore source-agnostic artifact provenance
Copilot bf8aeb3
artifact catalog: `id` is the source-agnostic round-trip key; `info` …
Copilot d7babbb
fix: keep layer_kind_from_lookup_id and derive_hook_id in agreement o…
Copilot 6efff92
artifact: reuse shared project resolver, rename handlers, dedupe vali…
Copilot bd483da
fix: align artifact info existence and resolver naming
Copilot e67b1cd
artifact: reuse PresetResolver.templates_dir in _project_core_asset_root
Copilot e7bdc62
fix: guard stale registry entries in artifact convention discovery
Copilot 9977faa
fix: include stack in artifact list json
Copilot d9d1ef7
docs: document artifact list stack records
Copilot 0ba4800
feat: add artifact layer source paths
Copilot ec76b9f
docs: clarify artifact sourcePath provenance
Copilot 48605fd
refactor: clarify sourcePath derivation flow
Copilot f83e95b
refactor: document artifact source path fallback
Copilot b763712
refactor: expose registrar output path helper
Copilot c45c4bf
refactor: centralize registrar skill output check
Copilot 63ec272
fix: only use materialized command output for the active stack row
Copilot 3cc0570
Merge origin/main into nicolehaugen-contribution-ids-and-artifacts
nicolehaugen 26f7fb0
docs(artifacts): cross-link contribution identifier grammar
nicolehaugen 64f6a65
fix(artifacts): enforce identifier grammar and gate manifestPath on d…
nicolehaugen b2dbb7a
chore(tests): remove trailing blank lines
nicolehaugen 842aa87
fix(presets): reuse parsed extension manifest identity
nicolehaugen 778fc06
fix(identifiers): enforce layer source sentinel when parsing
nicolehaugen 3f3206a
docs(identifiers): clarify built-in provenance contract
nicolehaugen c9b23e8
fix(artifacts): preserve preset registry fallback
nicolehaugen 4eefb4b
test(artifacts): drop preset corruption fallback coverage
nicolehaugen c903bb5
chore(changelog): remove manual unreleased entry
nicolehaugen a5b779c
docs(artifacts): clarify layer resolution semantics
nicolehaugen 429579c
docs(artifacts): explain resolver reuse
nicolehaugen 165e9bd
fix(presets): preserve legacy layer resolution
nicolehaugen 2db45ef
fix: preserve README convention resolution
nicolehaugen 1b88a8f
fix: preserve existing script resolution
nicolehaugen bd50670
refactor: isolate artifact provenance
nicolehaugen 7d23dfb
refactor: split artifact catalog modules
nicolehaugen 68b8278
style: normalize artifact resolution EOF
nicolehaugen 820d542
Merge origin/main into artifact branch
nicolehaugen d14b5b3
fix: expose both override artifact kinds
nicolehaugen ff695dd
docs: use Spec Kit product spelling
nicolehaugen 599106f
fix: confine artifact script references
nicolehaugen 6c77ca9
test: cover composing stack visibility
nicolehaugen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Artifacts | ||
|
|
||
| An **artifact** is any command, template, or script Spec Kit exposes in a project, regardless of which layer contributes it — built-in assets, an installed preset, an installed extension, or a project-local override in `.specify/templates/overrides/`. | ||
|
|
||
| The `specify artifact` command group is the read-only introspection surface for that inventory. `specify preset resolve <name>` answers "which file wins for this preset-managed name?"; `specify artifact` answers "what exists at all, and what is the full composition stack behind it?" — including built-in artifacts that no preset touches. | ||
|
|
||
| Both subcommands currently require `--json`. Omitting it exits with code `2` and prints a usage message on stderr; no stdout is produced. Text rendering is deliberately deferred so the JSON shapes below are the only contract, and adding a default text renderer later stays a non-breaking, additive change. | ||
|
|
||
| ## List Artifacts | ||
|
|
||
| ```bash | ||
| specify artifact list --json | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | -------- | -------------------------------------------------------- | | ||
| | `--json` | Required. Emit the inventory as a JSON array on stdout. | | ||
|
|
||
| Prints the full inventory of every visible artifact — one row per `(kind, name)` pair, including its composition `stack` — sorted by kind (`command`, then `template`, then `script`) and then by name. | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "name": "speckit.specify", | ||
| "kind": "command", | ||
| "description": "Create or update the feature specification.", | ||
| "stack": [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": null, | ||
| "lookupId": null, | ||
| "sourcePath": null | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "script:setup-plan", | ||
| "name": "setup-plan", | ||
| "kind": "script", | ||
| "description": "Setup implementation plan for a feature.", | ||
| "stack": [ | ||
| { | ||
| "id": "script:setup-plan", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": null, | ||
| "lookupId": null, | ||
| "sourcePath": null | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| | Field | Description | | ||
| | ------------- | ------------------------------------------------------------------------- | | ||
| | `id` | `{kind}:{name}` — the shorthand `artifact info` accepts as its argument | | ||
| | `name` | Logical artifact name (commands use the `speckit.<stem>` namespace) | | ||
| | `kind` | One of `command`, `template`, `script` | | ||
| | `description` | Description from the highest-precedence layer that declares one, else `""` | | ||
| | `stack` | Composition stack for this artifact, using the same row shape as `artifact info` | | ||
|
|
||
| Built-in artifacts always appear, even when nothing overrides them. Descriptions come from the highest-priority layer that has one — a preset or project override that hides a built-in command reports its own description, not the hidden built-in text. Skills (`.github/skills/**/SKILL.md`) are excluded: they are integration-specific output, not a shipped asset family. | ||
|
|
||
| ## Artifact Info | ||
|
|
||
| ```bash | ||
| specify artifact info <name> --json | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | ---------------- | ------------------------------------------------------------------- | | ||
| | `--json` | Required. Emit the composition stack as a JSON object on stdout. | | ||
| | `--kind <kind>` | Narrow the lookup to `command`, `template`, or `script` | | ||
|
|
||
| `<name>` accepts either a bare name (`speckit.specify`) or the `kind:name` shorthand (`command:speckit.specify`). When both the shorthand and `--kind` are supplied they must agree. | ||
|
|
||
| ```json | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "name": "speckit.specify", | ||
| "kind": "command", | ||
| "description": "Create or update the feature specification.", | ||
| "stack": [ | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": "preset", | ||
| "sourceId": "compliance", | ||
| "presetId": "compliance", | ||
| "presetName": "Compliance Preset", | ||
| "strategy": "replace", | ||
| "active": true, | ||
| "hidden": false, | ||
| "manifestPath": ".specify/presets/compliance/preset.yml", | ||
| "lookupId": "preset:compliance:command:speckit.specify", | ||
| "sourcePath": ".github/skills/speckit-specify/SKILL.md" | ||
| }, | ||
| { | ||
| "id": "command:speckit.specify", | ||
| "layer": null, | ||
| "sourceId": null, | ||
| "presetId": null, | ||
| "presetName": null, | ||
| "strategy": "replace", | ||
| "active": false, | ||
| "hidden": true, | ||
| "manifestPath": null, | ||
| "lookupId": null, | ||
| "sourcePath": null | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| The top-level `id`, `name`, `kind`, `description`, and `stack` fields match the corresponding row on `artifact list --json`. | ||
|
|
||
| ### Stack semantics | ||
|
|
||
| `stack` is ordered by resolution precedence: index `0` is the layer that wins. Each row describes one contributing layer: | ||
|
|
||
| | Field | Description | | ||
| | -------------- | -------------------------------------------------------------------------------- | | ||
| | `id` | `{kind}:{name}` — the source-agnostic round-trip key, identical on every row of the same artifact's stack | | ||
| | `layer` | `project`, `preset`, or `extension`; `null` for built-in layers | | ||
| | `sourceId` | Source component of `lookupId`, or `null` when the layer has no provenance | | ||
| | `presetId` | Preset pack directory id; `null` on built-in, `project`, and `extension` rows | | ||
| | `presetName` | Preset display name when its manifest declares one, else the pack id; `null` when `presetId` is `null` | | ||
| | `strategy` | `replace`, `wrap`, `prepend`, or `append` | | ||
| | `active` | `true` only for index `0` — the layer whose content is served | | ||
| | `hidden` | `true` when a lower-index `replace` layer cuts this layer out of the composition | | ||
| | `manifestPath` | Project-relative path to the declaring manifest, or `null` when none applies | | ||
| | `lookupId` | Deterministic `{layer}:{sourceId}:{kind}:{name}` identifier, or `null` for built-in layers | | ||
| | `sourcePath` | Project-relative POSIX path to the concrete file backing the layer, or `null` for built-in/synthetic layers | | ||
|
|
||
| `active` and `hidden` are independent labels, not opposites. `active` identifies the highest-precedence layer selected by the existing Spec Kit layer-resolution order; it does not validate that the layer content can be read or composed. This preserves the diagnostic behavior of `specify preset resolve`, which reports the discovered layer chain even when content composition later produces a warning. Composing strategies (`wrap`, `prepend`, `append`) keep lower layers in the composed output, so an inactive layer is not necessarily hidden: only layers below the first `replace` layer are marked `hidden`. Built-in rows have no provenance: `layer`, `sourceId`, and `lookupId` are `null` — but `id` is always populated, even on built-in rows. `id` is the round-trip key: `specify artifact info` accepts it as input (for example, `specify artifact info command:speckit.specify --json`), and it resolves the same artifact whether the caller passes the bare name or the `id`. | ||
|
|
||
| Lookup IDs are derived by the artifact command from the resolved layer and its existing preset or extension manifest. Manifest-declared layers use the manifest's `id`; convention-only layers use the installed preset or extension directory id. Project-local overrides carry a synthetic `project:_:{kind}:{name}` ID, while built-in layers have no `lookupId`. These values are artifact-stack provenance, not the round-trip key — use `id` for that. `sourcePath` is populated only when the layer maps to a concrete installed preset/extension file or a tracked agent materialization; core, project-override, and other synthetic rows report `null`. | ||
|
|
||
| ## JSON Errors | ||
|
|
||
| On failure, nothing is written to stdout. A single-key JSON envelope is written to stderr and the process exits with code `1`: | ||
|
|
||
| ```json | ||
| { "error": "unknown artifact command:nope" } | ||
| ``` | ||
|
|
||
| | Message | Cause | | ||
| | --------------------------------------------------- | ---------------------------------------------------------------- | | ||
| | `not a Spec Kit project: no .specify/ directory found` | Run outside an initialized project | | ||
| | `unknown artifact <name>` | No artifact matches the requested name (and kind, when given) | | ||
| | `ambiguous artifact <name>: matches kinds [...]` | The bare name matches more than one kind — re-run with `--kind` | | ||
| | `artifact resolution failed` | The extension registry could not be read, or an error prevented the artifact layer stack from being collected | | ||
|
|
||
| Exit code `2` is reserved for usage errors — a missing `--json` flag or an invalid `--kind` value — and emits a plain-text message on stderr rather than a JSON envelope. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """Public API for artifact inventory and resolution.""" | ||
|
|
||
| from .catalog import ArtifactCatalog | ||
| from .models import ( | ||
| AmbiguousArtifactError, | ||
| Artifact, | ||
| ArtifactError, | ||
| ArtifactKind, | ||
| ArtifactNotFoundError, | ||
| ArtifactResolutionError, | ||
| LayerName, | ||
| NotASpecKitProjectError, | ||
| StackLayer, | ||
| Strategy, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "AmbiguousArtifactError", | ||
| "Artifact", | ||
| "ArtifactCatalog", | ||
| "ArtifactError", | ||
| "ArtifactKind", | ||
| "ArtifactNotFoundError", | ||
| "ArtifactResolutionError", | ||
| "LayerName", | ||
| "NotASpecKitProjectError", | ||
| "StackLayer", | ||
| "Strategy", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| """Typer sub-app for the `specify artifact` command group. | ||
|
|
||
| Kept intentionally thin: the pure logic lives in ``specify_cli.artifacts``. | ||
| This module is only responsible for CLI wiring — argument parsing, JSON | ||
| serialization, exit-code selection, and error-envelope emission on stderr. | ||
|
|
||
| Mirrors the shape used by ``src/specify_cli/presets/_commands.py`` and | ||
| ``src/specify_cli/extensions/_commands.py``: a module-level Typer app plus a | ||
| ``register(app)`` entry point invoked from ``src/specify_cli/__init__.py``. | ||
|
|
||
| The user-facing contract for both subcommands — the ``list``/``info`` JSON | ||
| shapes, stack semantics (``active``/``hidden``, built-in rows, lookup IDs), and | ||
| the JSON error envelope — is documented in ``docs/reference/artifacts.md``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import io | ||
| import json | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| import typer | ||
|
|
||
| from . import ( | ||
| ArtifactCatalog, | ||
| ArtifactError, | ||
| ArtifactKind, | ||
| ArtifactResolutionError, | ||
| NotASpecKitProjectError, | ||
| ) | ||
| from ..presets import PresetError | ||
|
|
||
| artifact_app = typer.Typer( | ||
| name="artifact", | ||
| help="Introspect commands, templates, and scripts Spec Kit exposes.", | ||
| no_args_is_help=True, | ||
| ) | ||
|
|
||
|
|
||
| def _resolve_project_root() -> Path: | ||
| """Return the project root without emitting Rich output on failure. | ||
|
|
||
| Delegates to :func:`specify_cli._require_specify_project` — the same | ||
| resolution chokepoint every other project-scoped subcommand (``preset``, | ||
| ``extension``, ``workflow``, ...) uses, including its ``SPECIFY_INIT_DIR`` | ||
| override handling. That helper prints Rich error output and raises | ||
| ``typer.Exit`` on failure, which would corrupt the strict JSON envelope | ||
| ``specify artifact list --json`` and ``specify artifact info --json`` | ||
| emit on stdout/stderr. The Rich output is suppressed here and the | ||
| failure is re-raised as the module-local :class:`NotASpecKitProjectError` | ||
| for the shared error handler to serialize instead. | ||
| """ | ||
| from .. import _require_specify_project # lazy: avoids circular import | ||
|
|
||
| with contextlib.redirect_stderr(io.StringIO()): | ||
| try: | ||
| return _require_specify_project() | ||
| except typer.Exit: | ||
| raise NotASpecKitProjectError() from None | ||
|
|
||
|
|
||
| def _emit_error_and_exit(exc: ArtifactError) -> None: | ||
| """Write ``{"error": "..."}`` to stderr and exit with code 1. | ||
|
|
||
| The stdout stream is left completely untouched — the contract is that | ||
| machine consumers can rely on an empty stdout when the exit code is | ||
| non-zero, so no partial JSON payload leaks even on a late-stage failure. | ||
| """ | ||
| payload = json.dumps({"error": exc.message}, ensure_ascii=False) | ||
| print(payload, file=sys.stderr) | ||
| raise typer.Exit(code=1) | ||
|
|
||
|
|
||
| def _require_json_flag(json_flag: bool) -> None: | ||
| """Enforce the opt-in ``--json`` contract shared by both subcommands. | ||
|
|
||
| A text-mode formatter is intentionally deferred so the initial release | ||
| can commit to exactly one output shape. Callers that omit ``--json`` | ||
| get a usage error (exit 2) with no stdout output — this makes future | ||
| addition of a default text renderer a purely additive, non-breaking | ||
| change. | ||
| """ | ||
| if json_flag: | ||
| return | ||
| print( | ||
| "specify artifact requires --json for now; text output is not yet implemented.", | ||
| file=sys.stderr, | ||
| ) | ||
| raise typer.Exit(code=2) | ||
|
|
||
|
|
||
| @artifact_app.command("list") | ||
| def artifact_list( | ||
| json_flag: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Emit the inventory as a JSON array on stdout.", | ||
| ), | ||
| ) -> None: | ||
| """List every command, template, and script Spec Kit exposes.""" | ||
| _require_json_flag(json_flag) | ||
| try: | ||
| root = _resolve_project_root() | ||
| catalog = ArtifactCatalog(root) | ||
| rows = catalog.list_artifacts_with_stack() | ||
|
nicolehaugen marked this conversation as resolved.
|
||
| except ArtifactError as exc: | ||
| _emit_error_and_exit(exc) | ||
| return # pragma: no cover — _emit_error_and_exit raises | ||
| except (OSError, PresetError): | ||
| _emit_error_and_exit(ArtifactResolutionError()) | ||
| return # pragma: no cover — _emit_error_and_exit raises | ||
|
|
||
| sys.stdout.write(json.dumps(rows, indent=2, sort_keys=True, ensure_ascii=False)) | ||
| sys.stdout.write("\n") | ||
|
|
||
|
|
||
| @artifact_app.command("info") | ||
| def artifact_info( | ||
| name: str = typer.Argument(..., help="Artifact name, optionally 'kind:name'."), | ||
| json_flag: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Emit the composition stack as a JSON object on stdout.", | ||
| ), | ||
| kind: Optional[str] = typer.Option( | ||
| None, | ||
| "--kind", | ||
| help="Narrow the lookup to one artifact family (command/template/script).", | ||
| ), | ||
| ) -> None: | ||
| """Show one artifact and its full composition stack.""" | ||
| _require_json_flag(json_flag) | ||
|
|
||
| resolved_kind: Optional[ArtifactKind] = None | ||
| if kind is not None: | ||
| if kind not in ("command", "template", "script"): | ||
| print( | ||
| f"invalid --kind {kind!r}: expected one of command, template, script", | ||
| file=sys.stderr, | ||
| ) | ||
| raise typer.Exit(code=2) | ||
| resolved_kind = kind # type: ignore[assignment] | ||
|
|
||
| try: | ||
| root = _resolve_project_root() | ||
| catalog = ArtifactCatalog(root) | ||
| payload = catalog.get_artifact_info(name, kind=resolved_kind) | ||
| except ArtifactError as exc: | ||
| _emit_error_and_exit(exc) | ||
| return # pragma: no cover | ||
| except (OSError, PresetError): | ||
| _emit_error_and_exit(ArtifactResolutionError()) | ||
| return # pragma: no cover | ||
|
|
||
| sys.stdout.write(json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False)) | ||
| sys.stdout.write("\n") | ||
|
|
||
|
|
||
| def register(app: typer.Typer) -> None: | ||
| """Attach the artifact command group to the root Typer app.""" | ||
| app.add_typer(artifact_app, name="artifact") | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.