"""Wild PC CLI entry point — resource-first command surface. Operations live under the resource they act on. `program` is the catalog; `service`, `job`, and `tool` are deployment lenses (systemd services, scheduled timers, and PATH-installed CLIs); `gateway` is infrastructure. Platform-wide lifecycle (`start`/`stop`/`restart`/`status`/`deploy`) and the cross-resource `list` are top-level. Names can collide across resource types (a program and a service may share a name), so the resource is always explicit. """ from __future__ import annotations import argparse import sys from wildpc_core.stacks import available_stacks from wildpc_cli import __version__ DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"] def _add_name(p: argparse.ArgumentParser, help: str = "Name", optional: bool = False) -> None: p.add_argument("name", nargs="?" if optional else None, help=help) def _build_program_group(subparsers: argparse._SubParsersAction) -> None: prog = subparsers.add_parser("program", help="Manage programs (the software catalog)") prog.set_defaults(resource="program") sub = prog.add_subparsers(dest="program_command") p = sub.add_parser("list", help="List programs") p.add_argument( "--kind", choices=["service", "job", "tool", "static", "reference"], help="Filter by derived kind", ) p.add_argument("--stack", help="Filter by stack") p.add_argument("--json", action="store_true", help="Output as JSON") p = sub.add_parser("info", help="Show program details") _add_name(p, "Program name") p.add_argument("--json", action="store_true", help="Output as JSON") p = sub.add_parser("create", help="Scaffold a new program") _add_name(p, "Program name") p.add_argument("--stack", choices=available_stacks(), default=None) p.add_argument("--description", default="", help="Program description") p.add_argument("--port", type=int, help="Port (service deployments only)") p = sub.add_parser("add", help="Adopt an existing repo (path or git URL)") p.add_argument("target", help="Local path or git URL") p.add_argument("--name", help="Program name (default: dir/repo name)") p.add_argument("--description", default="", help="Program description") p = sub.add_parser("clone", help="Clone source for programs with repo:") _add_name(p, "Program to clone (default: all with repo:)", optional=True) p = sub.add_parser("delete", help="Remove a program from wildpc.yaml") _add_name(p, "Program name") p.add_argument("--source", action="store_true", help="Also delete the source directory") p.add_argument( "--purge-data", action="store_true", help="Also destroy the program's persistent data (e.g. a supabase app's DB schema)", ) p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") p = sub.add_parser("run", help="Run a program's declared run command") _add_name(p, "Program name") p.add_argument("extra", nargs=argparse.REMAINDER, help="Extra args passed to the program") for verb in DEV_VERBS: p = sub.add_parser(verb, help=f"Run {verb}") _add_name(p, "Program (default: all)", optional=True) def _build_tool_group(subparsers: argparse._SubParsersAction) -> None: """The `tool` lens — programs installed on PATH (path deployments).""" grp = subparsers.add_parser("tool", help="Tools on your PATH (the tools lens)") grp.set_defaults(resource="tool") sub = grp.add_subparsers(dest="tool_command") fmt_help = "Render each tool_schema in this envelope (default: openai)" fmt_choices = ("openai", "anthropic", "neutral") p = sub.add_parser("list", help="List tools with their executable + description") p.add_argument("--json", action="store_true", help="Machine-readable output") p.add_argument("--format", choices=fmt_choices, default="openai", help=fmt_help) p = sub.add_parser("info", help="Show a tool's executable, description, install state") _add_name(p, "Tool name") p.add_argument("--json", action="store_true", help="Machine-readable output") p.add_argument("--format", choices=fmt_choices, default="openai", help=fmt_help) def _build_stack_group(subparsers: argparse._SubParsersAction) -> None: """The `stack` lens — the toolchains each stack needs + whether they're present.""" grp = subparsers.add_parser("stack", help="Stacks + the toolchains they require") grp.set_defaults(resource="stack") sub = grp.add_subparsers(dest="stack_command") p = sub.add_parser("list", help="List stacks with their toolchain health") p.add_argument("--json", action="store_true", help="Machine-readable output") p = sub.add_parser("info", help="Show a stack's tools, versions, fixes, and users") _add_name(p, "Stack name") p.add_argument("--json", action="store_true", help="Machine-readable output") def _add_service_create(sub: argparse._SubParsersAction, kind: str) -> None: p = sub.add_parser("create", help=f"Create a {kind} in wildpc.yaml") _add_name(p, f"{kind.capitalize()} name") p.add_argument("--program", help="Program this deployment runs (convenience ref)") p.add_argument("--description", default="", help="Description") p.add_argument("--run", help="Console script / command to run (default: --program or name)") p.add_argument("--launcher", choices=["python", "command"], default="python") p.add_argument( "--env", action="append", metavar="KEY=VALUE", help="Env var for the program (repeatable). Use ${port}/${data_dir}/${name} placeholders.", ) if kind == "service": p.add_argument("--port", type=int, help="HTTP port") p.add_argument("--health", default="/health", help="Health path (default: /health)") p.add_argument( "--no-proxy", action="store_true", help="Port-only; don't expose at .", ) else: p.add_argument("--schedule", default="0 2 * * *", help="Cron schedule (default: 0 2 * * *)") def _build_deployment_group(subparsers: argparse._SubParsersAction, kind: str) -> None: """Build the `service` or `job` group (shared verb set).""" grp = subparsers.add_parser(kind, help=f"Manage {kind}s") grp.set_defaults(resource=kind) sub = grp.add_subparsers(dest=f"{kind}_command") p = sub.add_parser("list", help=f"List {kind}s") p.add_argument("--json", action="store_true", help="Output as JSON") p = sub.add_parser("info", help=f"Show {kind} details") _add_name(p, f"{kind.capitalize()} name") p.add_argument("--json", action="store_true", help="Output as JSON") _add_service_create(sub, kind) p = sub.add_parser("delete", help=f"Remove a {kind} from wildpc.yaml") _add_name(p, f"{kind.capitalize()} name") p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") p.add_argument("--source", action="store_true", help=argparse.SUPPRESS) p.add_argument("--purge-data", action="store_true", help=argparse.SUPPRESS) cap = f"{kind.capitalize()} name" # Lifecycle is convergence: `wildpc apply [name]`. `restart` stays as the one # imperative bounce that doesn't change desired state. _add_name(sub.add_parser("restart", help=f"Restart the {kind} (imperative bounce)"), cap) p = sub.add_parser("logs", help=f"View {kind} logs") _add_name(p, f"{kind.capitalize()} name") p.add_argument("-f", "--follow", action="store_true", help="Follow log output") p.add_argument("-n", "--lines", type=int, default=50, help="Lines to show (default: 50)") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="wildpc", description=( "Wild PC platform CLI — programs, deployments " "(services, jobs, tools), and infrastructure" ), ) parser.add_argument("--version", action="version", version=f"wildpc {__version__}") subparsers = parser.add_subparsers(dest="command", help="Available commands") _build_program_group(subparsers) _build_deployment_group(subparsers, "service") _build_deployment_group(subparsers, "job") _build_tool_group(subparsers) _build_stack_group(subparsers) # Gateway (inspection). The gateway is a deployment — start/stop/reload it via # `wildpc apply` / `wildpc restart wildpc-gateway`; this lens just shows routes. gw = subparsers.add_parser("gateway", help="Show the gateway's status + route table") gw_sub = gw.add_subparsers(dest="gateway_command") gw_sub.add_parser("status", help="Show gateway status + routes (the default)") # Mesh — inspect nodes + manage authority-written shared config. mesh = subparsers.add_parser("mesh", help="Inspect the mesh + shared config") mesh_sub = mesh.add_subparsers(dest="mesh_command") mesh_sub.add_parser("status", help="Mesh coordination status (the default)") mesh_sub.add_parser("nodes", help="List mesh nodes (local + remote)") mc = mesh_sub.add_parser("config", help="Shared config (only the authority writes)") mc_sub = mc.add_subparsers(dest="mesh_config_command") mc_sub.add_parser("list", help="List shared-config keys") mc_get = mc_sub.add_parser("get", help="Get a shared-config value") mc_get.add_argument("key") mc_set = mc_sub.add_parser("set", help="Set a shared-config value (authority only)") mc_set.add_argument("key") mc_set.add_argument("value") # TLS material for raw-TCP services (cert cut from the gateway wildcard). tls = subparsers.add_parser( "tls", help="Manage wildpc-materialized TLS certs for raw-TCP services" ) tls_sub = tls.add_subparsers(dest="tls_command") tls_sub.add_parser( "reconcile", help="Refresh materialized certs from the wildcard + reload changed" ) tls_sub.add_parser("status", help="Show each TLS service's cert fingerprint + expiry") # Secrets — read/write the ACTIVE backend (file or openbao), so a value never # gets hand-written to the wrong store (the mistake that silently shadows an # OpenBao fleet's secret with a stray file the vault never reads). sec = subparsers.add_parser("secret", help="Manage secrets in the active backend") sec.set_defaults(resource="secret") sec_sub = sec.add_subparsers(dest="secret_command") sec_sub.add_parser("list", help="List secret names in the active backend") p = sec_sub.add_parser("set", help="Set a secret (prompts if VALUE omitted)") p.add_argument("name", help="Secret name") p.add_argument("value", nargs="?", help="Value (omit to read from a hidden prompt / stdin)") p = sec_sub.add_parser("get", help="Read a secret's value") p.add_argument("name", help="Secret name") p = sec_sub.add_parser("rm", help="Delete a secret from the active backend") p.add_argument("name", help="Secret name") p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") # Convergence — the one lifecycle verb. Renders units/Caddyfile/tunnel, then # reconciles the runtime to match config (activate/restart/deactivate). p = subparsers.add_parser( "apply", help="Converge the running system to match config (render + reconcile)" ) p.add_argument("name", nargs="?", help="Single deployment to converge (default: all)") p.add_argument("--plan", action="store_true", help="Show the diff without changing anything") # Imperative ops (don't change desired state) p = subparsers.add_parser("restart", help="Restart deployment(s) — an imperative bounce") p.add_argument("name", nargs="?", help="Deployment to restart (default: all)") subparsers.add_parser("status", help="Show status across the platform") subparsers.add_parser("doctor", help="Diagnose setup + runtime health, with next-step hints") # Relationship model — repos, requires edges, and derived status. p = subparsers.add_parser( "graph", help="Show how programs/deployments relate (repos, requires, status)" ) p.add_argument("--json", action="store_true", help="Output as JSON") # Cross-resource overview p = subparsers.add_parser("list", help="List programs, services, jobs, and tools") p.add_argument( "--kind", choices=["service", "job", "tool", "static", "reference"], help="Filter by derived kind", ) p.add_argument("--stack", help="Filter by stack") p.add_argument("--json", action="store_true", help="Output as JSON") return parser def _dispatch_program(args: argparse.Namespace) -> int: sub = args.program_command if not sub: verbs = "list|info|create|add|clone|delete|run|" + "|".join(DEV_VERBS) print(f"Usage: wildpc program {{{verbs}}}") return 1 if sub == "list": from wildpc_cli.commands.list_cmd import run_list return run_list(args) if sub == "info": from wildpc_cli.commands.info import run_info return run_info(args) if sub == "create": from wildpc_cli.commands.create import run_create return run_create(args) if sub == "add": from wildpc_cli.commands.add import run_add return run_add(args) if sub == "clone": from wildpc_cli.commands.clone import run_clone return run_clone(args) if sub == "delete": from wildpc_cli.commands.delete import run_delete return run_delete(args) if sub == "run": from wildpc_cli.commands.run_cmd import run_run return run_run(args) if sub in DEV_VERBS: from wildpc_cli.commands.dev import run_verb return run_verb(args, sub) return 1 def _dispatch_tool(args: argparse.Namespace) -> int: sub = args.tool_command if not sub: print("Usage: wildpc tool {list|info} (install/uninstall → edit config + wildpc apply)") return 1 if sub == "list": from wildpc_cli.commands.tool import run_tool_list return run_tool_list(args) if sub == "info": from wildpc_cli.commands.tool import run_tool_info return run_tool_info(args) return 1 def _dispatch_stack(args: argparse.Namespace) -> int: sub = args.stack_command if not sub: print("Usage: wildpc stack {list|info}") return 1 if sub == "list": from wildpc_cli.commands.stack import run_stack_list return run_stack_list(args) if sub == "info": from wildpc_cli.commands.stack import run_stack_info return run_stack_info(args) return 1 def _dispatch_deployment(args: argparse.Namespace, kind: str) -> int: sub = getattr(args, f"{kind}_command") if not sub: verbs = "list|info|create|delete|restart|logs (deploy/enable/... → wildpc apply)" print(f"Usage: wildpc {kind} {{{verbs}}}") return 1 if sub == "list": from wildpc_cli.commands.list_cmd import run_list return run_list(args) if sub == "info": from wildpc_cli.commands.info import run_info return run_info(args) if sub == "create": from wildpc_cli.commands.deploy_create import run_job_create, run_service_create return run_service_create(args) if kind == "service" else run_job_create(args) if sub == "delete": from wildpc_cli.commands.delete import run_delete return run_delete(args) if sub == "restart": from wildpc_cli.commands.service import run_job_cmd, run_service_cmd return run_service_cmd(args) if kind == "service" else run_job_cmd(args) if sub == "logs": from wildpc_cli.commands.logs import run_logs return run_logs(args) return 1 def main() -> int: parser = build_parser() args = parser.parse_args() if not args.command: parser.print_help() return 0 cmd = args.command if cmd == "program": return _dispatch_program(args) if cmd in ("service", "job"): return _dispatch_deployment(args, cmd) if cmd == "tool": return _dispatch_tool(args) if cmd == "stack": return _dispatch_stack(args) if cmd == "gateway": from wildpc_cli.commands.gateway import run_gateway return run_gateway(args) if cmd == "mesh": from wildpc_cli.commands.mesh import run_mesh return run_mesh(args) if cmd == "tls": from wildpc_cli.commands.tls import run_tls return run_tls(args) if cmd == "secret": from wildpc_cli.commands.secret import run_secret return run_secret(args) if cmd == "apply": from wildpc_cli.commands.apply import run_apply return run_apply(args) if cmd == "restart": from wildpc_cli.commands.service import run_restart return run_restart(args) if cmd == "status": from wildpc_cli.commands.service import run_status return run_status(args) if cmd == "doctor": from wildpc_cli.commands.doctor import run_doctor return run_doctor(args) if cmd == "graph": from wildpc_cli.commands.graph import run_graph return run_graph(args) if cmd == "list": from wildpc_cli.commands.list_cmd import run_list return run_list(args) parser.print_help() return 1 def cli() -> None: sys.exit(main()) if __name__ == "__main__": cli()