Manager abstraction + manager-dispatched lifecycle; frontends create static services

- manifest: `manager_for(runner)` — the single source of truth mapping a runner to
  its manager (systemd | caddy | path | none). deploy's `managed` now derives from it.
- lifecycle: is_active/activate/deactivate dispatch by manager instead of behavior
  (removed the stale `_is_static_frontend`, which broke once frontends became
  services). caddy → gateway reload; path → install/uninstall; none → no-op.
- cli/service: start/stop/restart and enable/disable route through the manager
  (systemctl only for systemd; static reloads the gateway; remote is a no-op);
  platform restart skips non-systemd runners.
- create: a frontend stack (react-vite/supabase) now emits a `runner: static`
  service, not a bare frontend program.
This commit is contained in:
2026-07-01 06:51:31 -07:00
parent 5f6afbf007
commit 57861e58c1
8 changed files with 206 additions and 78 deletions

View File

@@ -15,6 +15,7 @@ from castle_cli.manifest import (
ManageSpec,
ProgramSpec,
RunPython,
RunStatic,
ServiceSpec,
SystemdSpec,
)
@@ -93,11 +94,12 @@ def run_create(args: argparse.Namespace) -> int:
# Initialize a git repo for the new source.
subprocess.run(["git", "init", "-q", str(project_dir)], check=False)
# Frontend stacks with a static output get a build spec so the gateway emits
# an in-place static route at /<name>/ (no service, no process).
# Frontend stacks declare a build output; the program builds it, a `static`
# service serves it in place at <name>.<gateway.domain>.
build = None
if stack in STACK_BUILD_OUTPUTS:
build = BuildSpec(outputs=[STACK_BUILD_OUTPUTS[stack]])
static_root = STACK_BUILD_OUTPUTS.get(stack)
if static_root:
build = BuildSpec(outputs=[static_root])
config.programs[name] = ProgramSpec(
id=name,
@@ -107,7 +109,14 @@ def run_create(args: argparse.Namespace) -> int:
behavior=behavior,
build=build,
)
if behavior == "daemon":
if behavior == "frontend":
# A caddy-managed static service: no systemd unit, served from the build dir.
config.services[name] = ServiceSpec(
id=name,
program=name,
run=RunStatic(runner="static", root=static_root or "dist"),
)
elif behavior == "daemon":
prefix = name.replace("-", "_").upper()
config.services[name] = ServiceSpec(
id=name,