Coverage for smith / delivery / cli.py: 100%
0 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 18:48 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 18:48 +0000
1"""CLI entry point for the smith command-line tool."""
3import argparse
4import sys
5from pathlib import Path
7from smith.application.connect import ConnectUseCase
8from smith.application.disconnect import DisconnectUseCase
9from smith.application.status import StatusUseCase
10from smith.application.update import UpdateUseCase
11from smith.domain.ports import TemplateSourceError
12from smith.domain.value_objects import TemplateSource
14EXIT_SUCCESS = 0
15EXIT_ERROR = 1
18def build_parser() -> argparse.ArgumentParser:
19 """Build and return the argument parser for the smith CLI."""
20 parser = argparse.ArgumentParser(
21 prog="smith", description="Connect AI agent configurations to any project"
22 )
23 parser.add_argument("--version", action="version", version=_get_version())
25 subparsers = parser.add_subparsers(dest="command", help="Available commands")
27 connect_parser = subparsers.add_parser(
28 "connect", help="Connect agentic files to a project"
29 )
30 connect_parser.add_argument(
31 "--from",
32 dest="source",
33 help="Template source (bundled:agents-smith, local path, or URL)",
34 )
35 connect_parser.add_argument(
36 "--overwrite",
37 action="store_true",
38 dest="overwrite",
39 help="Replace existing agentic files without prompting",
40 )
41 connect_parser.set_defaults(func=handle_connect)
43 disconnect_parser = subparsers.add_parser(
44 "disconnect", help="Disconnect agentic files from a project"
45 )
46 disconnect_parser.set_defaults(func=handle_disconnect)
48 update_parser = subparsers.add_parser("update", help="Update agentic files")
49 update_parser.add_argument(
50 "--from",
51 dest="source",
52 help="Template source (bundled:agents-smith, local path, or URL)",
53 )
54 update_parser.set_defaults(func=handle_update)
56 status_parser = subparsers.add_parser("status", help="Show connection status")
57 status_parser.add_argument("--json", action="store_true", help="Output as JSON")
58 status_parser.set_defaults(func=handle_status)
60 return parser
63def _get_version() -> str:
64 """Return the current smith version string."""
65 try:
66 from importlib.metadata import metadata
68 meta = metadata("agents-smith")
69 return f"smith {meta['Version']}"
70 except Exception:
71 return "smith 0.1.0"
74def _parse_source(source_arg: str | None) -> TemplateSource:
75 """Parse a source argument string into a TemplateSource value object."""
76 if source_arg is None:
77 return TemplateSource(kind="bundled", location="agents-smith")
78 if source_arg.startswith("/"):
79 return TemplateSource(kind="local", location=source_arg)
80 if source_arg.startswith("http://") or source_arg.startswith("https://"):
81 return TemplateSource(kind="url", location=source_arg)
82 if ":" in source_arg:
83 kind, _, location = source_arg.partition(":")
84 return TemplateSource(kind=kind, location=location) # type: ignore[arg-type]
85 return TemplateSource(kind="local", location=source_arg)
88def handle_connect(args: argparse.Namespace) -> int:
89 """Handle the ``connect`` sub-command."""
90 project_dir = Path(getattr(args, "project_dir", ".")).resolve()
91 source = _parse_source(getattr(args, "source", None))
92 overwrite = getattr(args, "overwrite", False)
93 try:
94 ConnectUseCase(project_dir=project_dir).execute(
95 source=source, overwrite=overwrite
96 )
97 return EXIT_SUCCESS
98 except TemplateSourceError as e:
99 sys.stderr.write(f"Error: {e}\n")
100 return EXIT_ERROR
101 except Exception as e:
102 sys.stderr.write(f"Error: {e}\n")
103 return EXIT_ERROR
106def handle_disconnect(args: argparse.Namespace) -> int:
107 """Handle the ``disconnect`` sub-command."""
108 project_dir = Path(getattr(args, "project_dir", ".")).resolve()
109 try:
110 DisconnectUseCase(project_dir=project_dir).execute()
111 return EXIT_SUCCESS
112 except Exception as e:
113 sys.stderr.write(f"Error: {e}\n")
114 return EXIT_ERROR
117def handle_update(args: argparse.Namespace) -> int:
118 """Handle the ``update`` sub-command."""
119 project_dir = Path(getattr(args, "project_dir", ".")).resolve()
120 source = _parse_source(getattr(args, "source", None))
121 try:
122 UpdateUseCase(project_dir=project_dir).execute(
123 source=source if args.source else None
124 )
125 return EXIT_SUCCESS
126 except TemplateSourceError as e:
127 sys.stderr.write(f"Error: {e}\n")
128 return EXIT_ERROR
129 except Exception as e:
130 sys.stderr.write(f"Error: {e}\n")
131 return EXIT_ERROR
134def handle_status(args: argparse.Namespace) -> int:
135 """Handle the ``status`` sub-command."""
136 project_dir = Path(getattr(args, "project_dir", ".")).resolve()
137 try:
138 status = StatusUseCase(project_dir=project_dir).execute()
139 if getattr(args, "json", False):
140 import json
142 sys.stdout.write(json.dumps(status.to_dict(), indent=2) + "\n")
143 else:
144 sys.stdout.write(f"State: {status.state.value}\n")
145 if status.source:
146 sys.stdout.write(
147 f"Source: {status.source.kind}:{status.source.location}\n"
148 )
149 if status.present_files:
150 sys.stdout.write("Present files:\n")
151 for f in status.present_files:
152 sys.stdout.write(f" {f}\n")
153 if status.missing_files:
154 sys.stdout.write("Missing files:\n")
155 for f in status.missing_files:
156 sys.stdout.write(f" {f}\n")
157 return EXIT_SUCCESS
158 except Exception as e:
159 sys.stderr.write(f"Error: {e}\n")
160 return EXIT_ERROR
163def main(argv: list[str] | None = None) -> int:
164 """Run the smith CLI and return an exit code."""
165 parser = build_parser()
166 args = parser.parse_args(argv)
167 if not hasattr(args, "func"):
168 parser.print_help()
169 return EXIT_ERROR
170 return args.func(args)