1- """Typer sub-app for the ` specify artifact` command group .
1+ """Shared infrastructure and registration for `` specify artifact`` commands .
22
3- Kept intentionally thin: the pure logic lives in ``specify_cli.artifacts``.
4- This module is only responsible for CLI wiring — argument parsing, JSON
5- serialization, exit-code selection, and error-envelope emission on stderr.
6-
7- Mirrors the shape used by ``src/specify_cli/presets/_commands.py`` and
8- ``src/specify_cli/extensions/_commands.py``: a module-level Typer app plus a
9- ``register(app)`` entry point invoked from ``src/specify_cli/__init__.py``.
10-
11- The user-facing contract for both subcommands — the ``list``/``info`` JSON
12- shapes, stack semantics (``active``/``hidden``, built-in rows, lookup IDs), and
13- the JSON error envelope — is documented in ``docs/reference/artifacts.md``.
3+ Command handlers live in ``command_*.py`` modules. Domain behavior remains in
4+ Typer-free modules in this package, following ``design/cli.md``.
145"""
156
167from __future__ import annotations
2314
2415import typer
2516
26- from ..presets import PresetError
2717from . import (
28- ArtifactCatalog ,
2918 ArtifactError ,
30- ArtifactKind ,
31- ArtifactResolutionError ,
3219 NotASpecKitProjectError ,
3320)
3421
@@ -74,7 +61,7 @@ def _emit_error_and_exit(exc: ArtifactError) -> None:
7461
7562
7663def _require_json_flag (json_flag : bool ) -> None :
77- """Enforce the opt-in ``--json`` contract shared by both subcommands .
64+ """Enforce the opt-in ``--json`` contract shared by artifact commands .
7865
7966 A text-mode formatter is intentionally deferred so the initial release
8067 can commit to exactly one output shape. Callers that omit ``--json``
@@ -91,111 +78,12 @@ def _require_json_flag(json_flag: bool) -> None:
9178 raise typer .Exit (code = 2 )
9279
9380
94- @artifact_app .command ("list" )
95- def artifact_list (
96- json_flag : bool = typer .Option (
97- False ,
98- "--json" ,
99- help = "Emit the inventory as a JSON array on stdout." ,
100- ),
101- ) -> None :
102- """List every command, template, script, and hook Spec Kit exposes."""
103- _require_json_flag (json_flag )
104- try :
105- root = _resolve_project_root ()
106- catalog = ArtifactCatalog (root )
107- rows = catalog .list_artifacts_with_stack ()
108- except ArtifactError as exc :
109- _emit_error_and_exit (exc )
110- return # pragma: no cover — _emit_error_and_exit raises
111- except (OSError , PresetError ):
112- _emit_error_and_exit (ArtifactResolutionError ())
113- return # pragma: no cover — _emit_error_and_exit raises
114-
115- sys .stdout .write (json .dumps (rows , indent = 2 , sort_keys = True , ensure_ascii = False ))
116- sys .stdout .write ("\n " )
117-
118-
119- @artifact_app .command ("info" )
120- def artifact_info (
121- name : str = typer .Argument (..., help = "Artifact name, optionally 'kind:name'." ),
122- json_flag : bool = typer .Option (
123- False ,
124- "--json" ,
125- help = "Emit the composition stack as a JSON object on stdout." ,
126- ),
127- kind : str | None = typer .Option (
128- None ,
129- "--kind" ,
130- help = "Narrow the lookup to one artifact family (command/template/script/hook)." ,
131- ),
132- ) -> None :
133- """Show one artifact and its full composition stack."""
134- _require_json_flag (json_flag )
135-
136- resolved_kind : ArtifactKind | None = None
137- if kind is not None :
138- if kind not in ("command" , "template" , "script" , "hook" ):
139- print (
140- f"invalid --kind { kind !r} : expected one of command, template, script, hook" ,
141- file = sys .stderr ,
142- )
143- raise typer .Exit (code = 2 )
144- resolved_kind = kind # type: ignore[assignment]
145-
146- try :
147- root = _resolve_project_root ()
148- catalog = ArtifactCatalog (root )
149- payload = catalog .get_artifact_info (name , kind = resolved_kind )
150- except ArtifactError as exc :
151- _emit_error_and_exit (exc )
152- return # pragma: no cover
153- except (OSError , PresetError ):
154- _emit_error_and_exit (ArtifactResolutionError ())
155- return # pragma: no cover
156-
157- sys .stdout .write (json .dumps (payload , indent = 2 , sort_keys = True , ensure_ascii = False ))
158- sys .stdout .write ("\n " )
159-
160-
161- @artifact_app .command ("lookup" )
162- def artifact_lookup (
163- lookup_id : str = typer .Argument (..., help = "Contribution lookupId from an artifact stack." ),
164- json_flag : bool = typer .Option (
165- False ,
166- "--json" ,
167- help = "Emit the validated manifest contribution used by Spec Kit as JSON." ,
168- ),
169- ) -> None :
170- """Resolve a stack lookupId to its effective preset or extension contribution."""
171- _require_json_flag (json_flag )
172- try :
173- root = _resolve_project_root ()
174- payload = ArtifactCatalog (root ).get_contribution_info (lookup_id )
175- except ArtifactError as exc :
176- _emit_error_and_exit (exc )
177- return # pragma: no cover
178- except (OSError , PresetError ):
179- _emit_error_and_exit (ArtifactResolutionError ())
180- return # pragma: no cover
181-
182- try :
183- rendered = json .dumps (
184- payload ,
185- indent = 2 ,
186- sort_keys = True ,
187- ensure_ascii = False ,
188- allow_nan = False ,
189- )
190- rendered .encode ("utf-8" )
191- except (TypeError , ValueError , UnicodeEncodeError ):
192- _emit_error_and_exit (ArtifactResolutionError ())
193- return # pragma: no cover
194-
195- sys .stdout .write (rendered )
196- sys .stdout .write ("\n " )
197-
198-
19981def register (app : typer .Typer ) -> None :
20082 """Attach the artifact command group to the root Typer app."""
83+ # isort: off
84+ from . import command_list # noqa: F401 — registers handler via decorator
85+ from . import command_info # noqa: F401 — registers handler via decorator
86+ from . import command_lookup # noqa: F401 — registers handler via decorator
87+ # isort: on
88+
20189 app .add_typer (artifact_app , name = "artifact" )
0 commit comments