refactor(cli): resource-first command surface

Names collide across resource types (a program and a service can share a name),
so the CLI no longer resolves bare names. Operations live under the resource
they act on; platform lifecycle and the cross-resource overview stay top-level.

  castle program  list|info|create|add|clone|delete|run|install|uninstall|
                  build|test|lint|format|type-check|check
  castle service  list|info|create|delete|deploy|enable|disable|start|stop|restart|logs
  castle job      <same verbs>   (create takes --schedule)
  castle gateway  start|stop|reload|status
  castle list | status | deploy [name]        # cross-cutting
  castle start | stop | restart               # whole platform (systemd verbs)

Key changes:
- info/delete/list/run are resource-scoped (no more cross-section name match).
- delete only removes the named resource's entry; program delete still blocks
  when a deployment references it.
- per-resource start/stop/restart (systemctl on the unit/timer); top-level
  start/stop/restart act on the whole platform.
- expose → 'service create' (more general: --program ref, --runner); gained
  'job create' (the CLI never had one).
- dropped up/down — bringing things online is the honest 'deploy && start'.

Docs (CLAUDE.md, registry.md, stack guides) and user-facing messages updated.
core 94 / cli 24 / api 52 green; ruff clean. Verified live: program/service/job
list+info scope correctly, service create+scoped-delete round-trips.
This commit is contained in:
2026-06-14 16:25:34 -07:00
parent 82f12c9d61
commit 5c6d8ec6f1
16 changed files with 556 additions and 511 deletions

View File

@@ -44,24 +44,32 @@ def _run_program(name: str, extra: list[str]) -> int | None:
def run_run(args: argparse.Namespace) -> int:
"""Run a program (declared run) or a deployed service in the foreground."""
"""Run a program's declared run, or a deployed service, in the foreground.
Scoped by `resource`: `program run` runs the program's declared `run`;
`service run` runs the deployed service's command from the registry.
"""
name = args.name
extra_args = getattr(args, "extra", []) or []
resource = getattr(args, "resource", None)
# 1. Program with a declared run command.
prog_rc = _run_program(name, extra_args)
if prog_rc is not None:
return prog_rc
# Program: declared run command.
if resource in (None, "program"):
prog_rc = _run_program(name, extra_args)
if prog_rc is not None:
return prog_rc
if resource == "program":
print(f"Error: program '{name}' has no declared `run` command.")
return 1
# 2. Deployed service from the registry.
# Service: deployed command from the registry.
if not REGISTRY_PATH.exists():
print("Error: no registry found. Run 'castle deploy' first.")
return 1
registry = load_registry()
if name not in registry.deployed:
print(f"Error: '{name}' is not a runnable program or deployed service.")
print("Declare a `run` command in castle.yaml, or run 'castle deploy'.")
print(f"Error: '{name}' is not a deployed service. Run 'castle deploy' first.")
return 1
deployed = registry.deployed[name]