smith.delivery.cli

CLI entry point for the smith command-line tool.

  1"""CLI entry point for the smith command-line tool."""
  2
  3import argparse
  4import sys
  5from pathlib import Path
  6
  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
 13
 14EXIT_SUCCESS = 0
 15EXIT_ERROR = 1
 16
 17
 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())
 24
 25    subparsers = parser.add_subparsers(dest="command", help="Available commands")
 26
 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)
 42
 43    disconnect_parser = subparsers.add_parser(
 44        "disconnect", help="Disconnect agentic files from a project"
 45    )
 46    disconnect_parser.set_defaults(func=handle_disconnect)
 47
 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)
 55
 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)
 59
 60    return parser
 61
 62
 63def _get_version() -> str:
 64    """Return the current smith version string."""
 65    try:
 66        from importlib.metadata import metadata
 67
 68        meta = metadata("agents-smith")
 69        return f"smith {meta['Version']}"
 70    except Exception:
 71        return "smith 0.1.0"
 72
 73
 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)
 86
 87
 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
104
105
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
115
116
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
132
133
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
141
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
161
162
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)
EXIT_SUCCESS = 0
EXIT_ERROR = 1
def build_parser() -> argparse.ArgumentParser:
19def build_parser() -> argparse.ArgumentParser:
20    """Build and return the argument parser for the smith CLI."""
21    parser = argparse.ArgumentParser(
22        prog="smith", description="Connect AI agent configurations to any project"
23    )
24    parser.add_argument("--version", action="version", version=_get_version())
25
26    subparsers = parser.add_subparsers(dest="command", help="Available commands")
27
28    connect_parser = subparsers.add_parser(
29        "connect", help="Connect agentic files to a project"
30    )
31    connect_parser.add_argument(
32        "--from",
33        dest="source",
34        help="Template source (bundled:agents-smith, local path, or URL)",
35    )
36    connect_parser.add_argument(
37        "--overwrite",
38        action="store_true",
39        dest="overwrite",
40        help="Replace existing agentic files without prompting",
41    )
42    connect_parser.set_defaults(func=handle_connect)
43
44    disconnect_parser = subparsers.add_parser(
45        "disconnect", help="Disconnect agentic files from a project"
46    )
47    disconnect_parser.set_defaults(func=handle_disconnect)
48
49    update_parser = subparsers.add_parser("update", help="Update agentic files")
50    update_parser.add_argument(
51        "--from",
52        dest="source",
53        help="Template source (bundled:agents-smith, local path, or URL)",
54    )
55    update_parser.set_defaults(func=handle_update)
56
57    status_parser = subparsers.add_parser("status", help="Show connection status")
58    status_parser.add_argument("--json", action="store_true", help="Output as JSON")
59    status_parser.set_defaults(func=handle_status)
60
61    return parser

Build and return the argument parser for the smith CLI.

def handle_connect(args: argparse.Namespace) -> int:
 89def handle_connect(args: argparse.Namespace) -> int:
 90    """Handle the ``connect`` sub-command."""
 91    project_dir = Path(getattr(args, "project_dir", ".")).resolve()
 92    source = _parse_source(getattr(args, "source", None))
 93    overwrite = getattr(args, "overwrite", False)
 94    try:
 95        ConnectUseCase(project_dir=project_dir).execute(
 96            source=source, overwrite=overwrite
 97        )
 98        return EXIT_SUCCESS
 99    except TemplateSourceError as e:
100        sys.stderr.write(f"Error: {e}\n")
101        return EXIT_ERROR
102    except Exception as e:
103        sys.stderr.write(f"Error: {e}\n")
104        return EXIT_ERROR

Handle the connect sub-command.

def handle_disconnect(args: argparse.Namespace) -> int:
107def handle_disconnect(args: argparse.Namespace) -> int:
108    """Handle the ``disconnect`` sub-command."""
109    project_dir = Path(getattr(args, "project_dir", ".")).resolve()
110    try:
111        DisconnectUseCase(project_dir=project_dir).execute()
112        return EXIT_SUCCESS
113    except Exception as e:
114        sys.stderr.write(f"Error: {e}\n")
115        return EXIT_ERROR

Handle the disconnect sub-command.

def handle_update(args: argparse.Namespace) -> int:
118def handle_update(args: argparse.Namespace) -> int:
119    """Handle the ``update`` sub-command."""
120    project_dir = Path(getattr(args, "project_dir", ".")).resolve()
121    source = _parse_source(getattr(args, "source", None))
122    try:
123        UpdateUseCase(project_dir=project_dir).execute(
124            source=source if args.source else None
125        )
126        return EXIT_SUCCESS
127    except TemplateSourceError as e:
128        sys.stderr.write(f"Error: {e}\n")
129        return EXIT_ERROR
130    except Exception as e:
131        sys.stderr.write(f"Error: {e}\n")
132        return EXIT_ERROR

Handle the update sub-command.

def handle_status(args: argparse.Namespace) -> int:
135def handle_status(args: argparse.Namespace) -> int:
136    """Handle the ``status`` sub-command."""
137    project_dir = Path(getattr(args, "project_dir", ".")).resolve()
138    try:
139        status = StatusUseCase(project_dir=project_dir).execute()
140        if getattr(args, "json", False):
141            import json
142
143            sys.stdout.write(json.dumps(status.to_dict(), indent=2) + "\n")
144        else:
145            sys.stdout.write(f"State: {status.state.value}\n")
146            if status.source:
147                sys.stdout.write(
148                    f"Source: {status.source.kind}:{status.source.location}\n"
149                )
150            if status.present_files:
151                sys.stdout.write("Present files:\n")
152                for f in status.present_files:
153                    sys.stdout.write(f"  {f}\n")
154            if status.missing_files:
155                sys.stdout.write("Missing files:\n")
156                for f in status.missing_files:
157                    sys.stdout.write(f"  {f}\n")
158        return EXIT_SUCCESS
159    except Exception as e:
160        sys.stderr.write(f"Error: {e}\n")
161        return EXIT_ERROR

Handle the status sub-command.

def main(argv: list[str] | None = None) -> int:
164def main(argv: list[str] | None = None) -> int:
165    """Run the smith CLI and return an exit code."""
166    parser = build_parser()
167    args = parser.parse_args(argv)
168    if not hasattr(args, "func"):
169        parser.print_help()
170        return EXIT_ERROR
171    return args.func(args)

Run the smith CLI and return an exit code.