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.