diff --git a/AGENTS.md b/AGENTS.md index 64ae9b1..77fa58b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,38 +59,37 @@ castle program add [--name ...] # adopt EXISTIN castle program clone [name] # provision repo: source castle program delete [--source] [-y] castle program run [args...] # declared run command -castle program install|uninstall [name] # activate tools/statics castle program build|test|lint|format|type-check|check [name] # dev verbs # Services — daemons (manager: systemd, no schedule) castle service list|info [--json] castle service create [--program P] [--port N] [--health ...] [--launcher ...] -castle service deploy # generate unit + gateway route -castle service enable|disable # systemd enable/disable (+ boot) -castle service start|stop|restart +castle service restart # imperative bounce castle service logs [-f] [-n 50] -# Jobs — scheduled tasks (manager: systemd + schedule). Same verbs; create takes --schedule +# Jobs — scheduled tasks (manager: systemd + schedule). create takes --schedule castle job create [--program P] --schedule "0 2 * * *" [--launcher ...] -castle job ... +castle job ... # Tools — CLIs on PATH (manager: path) castle tool list [--json] # each tool's executable + description + install state castle tool info [--json] -castle tool install|uninstall # Platform-wide -castle list [--kind ...] [--stack ...] [--json] # all deployments +castle apply [name] [--plan] # converge runtime to config — the workhorse +castle list [--kind ...] [--stack ...] [--json] # all deployments castle status # unified health/status -castle doctor # diagnose setup + runtime, with fix hints -castle deploy [name] # apply config → units + Caddyfile -castle start | stop | restart # all services (+ gateway) -castle gateway start|stop|reload|status # the Caddy gateway +castle doctor # diagnose setup + runtime, with fix hints +castle restart [name] # imperative bounce (one or all) +castle gateway reload|status # the Caddy gateway ``` `castle service`/`job`/`tool` are **views** over the one deployment set, filtered -by derived kind. Bringing everything online is the two honest steps -**`castle deploy && castle start`** (apply config, then start). +by derived kind. Lifecycle is **convergence**: edit config, then **`castle apply`** +renders units + the Caddyfile and reconciles the runtime (activate what's enabled, +restart what changed, deactivate what's disabled). To durably turn a deployment +off, set `enabled: false` and apply — there is no separate start/stop/enable. +`castle restart` is the one imperative bounce. **Dev verbs resolve per-program:** a declared `commands:` entry (or `build:`) overrides the program's stack default, else the stack handler, else the verb is @@ -108,8 +107,7 @@ castle program create my-service --stack python-fastapi --description "Does X" cd /data/repos/my-service && uv sync # implement it castle program test my-service castle service create my-service --program my-service --port 9001 -castle service deploy my-service && castle service enable my-service -castle gateway reload +castle apply my-service # renders the unit + gateway route and starts it ``` The service reads its port/data dir from env vars that `deployments/my-service.yaml` @@ -120,7 +118,7 @@ maps via placeholders (see §6). Stack guide: **`docs/stacks/python-fastapi.md`* ```bash castle program create my-tool --stack python-cli --description "Does Y" cd /data/repos/my-tool && uv sync -castle tool install my-tool # uv tool install → on PATH +castle apply my-tool # installs the path deployment on PATH ``` `castle tool list --json` is the machine-readable tool catalog (each tool's real @@ -134,7 +132,7 @@ A job is a `manager: systemd` deployment with a `schedule` (cron). Generates a ```bash castle job create nightly --program my-tool --schedule "0 2 * * *" --launcher command -castle job deploy nightly && castle job enable nightly +castle apply nightly ``` ### Create a static frontend @@ -143,7 +141,7 @@ castle job deploy nightly && castle job enable nightly # scaffold a Vite/React app under /data/repos/my-frontend (see docs/stacks/react-vite.md) castle program build my-frontend # produces dist/ # deployments/my-frontend.yaml → manager: caddy, root: dist -castle deploy && castle gateway reload # served at my-frontend. +castle apply # served at my-frontend. ``` The gateway serves the build **in place** from `/` — no copy, no @@ -213,7 +211,7 @@ context** (`crypto.subtle`, service workers), which plain-HTTP LAN hosts lack. `acme` operational prerequisites (castle can't do these for you): - **DNS-plugin Caddy** at `/usr/local/bin/caddy` — `./install.sh --with-dns-plugin=cloudflare`. - **Provider token** stored as a secret and mapped into the gateway service env - (`CLOUDFLARE_API_TOKEN`); `castle deploy` warns if missing. + (`CLOUDFLARE_API_TOKEN`); `castle apply` warns if missing. - **Bind :443/:80** — lower the floor once: `net.ipv4.ip_unprivileged_port_start=80` in `/etc/sysctl.d/` (beats `setcap`, which `NoNewPrivileges` would void). - **Stage first**: `CASTLE_ACME_STAGING=1` at deploy, verify issuance, then unset @@ -258,7 +256,7 @@ Roots: **`CASTLE_HOME`** (config/code/artifacts/secrets, default `~/.castle`) an `public: true` (requires `proxy: true`) projects a service to the internet at `.` (a **separate** zone, so internal subdomain names -stay out of public DNS). `castle deploy` generates the cloudflared ingress from the +stay out of public DNS). `castle apply` generates the cloudflared ingress from the set of public services. Needs `gateway.public_domain` + `gateway.tunnel_id` set and the `castle-tunnel` service running. → One-time setup: **`docs/tunnel-setup.md`**. diff --git a/README.md b/README.md index 1716b8c..e908b0f 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,10 @@ A program can have several deployments — a CLI that is both a `tool` on PATH a scheduled `job` — so a program has no single kind of its own; it *has deployments*, each with its own. -Standing everything up is the two honest steps `castle deploy` (regenerate systemd -units and gateway config from your config) then `castle start` (enable/start it). -There is no bundled "up". +Standing everything up is one honest step: `castle apply` renders systemd units and +gateway config from your config, then reconciles the runtime to match — activating +what's enabled, restarting what changed, deactivating what's disabled. Edit config, +`castle apply`; `castle apply --plan` shows the diff first. ## Stacks @@ -112,7 +113,7 @@ git clone ~/castle && cd ~/castle # Postgres), creates ~/.castle, registers Castle's own control plane, builds the UI. ./install.sh -castle deploy && castle start # apply config to the runtime, then bring it up +castle apply # converge the runtime to config (units, routes, run) castle doctor # verify — every check should be green open http://localhost:9000 # the dashboard ``` @@ -124,7 +125,7 @@ looks off — after an install, a deploy, or a config change. ### Exposure: from localhost to your own HTTPS domain Localhost is the first rung; you climb only as far as you need. Each rung is a small -config change plus `castle deploy`, and `castle doctor` tells you what a rung still +config change plus `castle apply`, and `castle doctor` tells you what a rung still needs. | Rung | You get | What it takes | @@ -145,15 +146,15 @@ are still missing, each with its fix. # A service — FastAPI app + a systemd service deployment (health, unit, route) castle program create my-api --stack python-fastapi --description "Does something" castle program test my-api -castle deploy my-api && castle service enable my-api +castle apply my-api # render unit + route, then start it # A tool — a CLI installed on your PATH castle program create my-tool --stack python-cli --description "Does something" -castle tool install my-tool +castle apply my-tool # installs its path deployment on PATH # A static frontend — built once, served by the gateway castle program create my-app --stack react-vite --description "Web interface" -castle program build my-app && castle deploy +castle program build my-app && castle apply # Adopt an existing repo (no stack needed — dev verbs detected or declared) castle program add ~/projects/some-rust-tool @@ -162,28 +163,32 @@ castle program add ~/projects/some-rust-tool ## CLI Operations live under the resource they act on. `program` is the catalog; -`service`, `job`, and `tool` are **views** over the one deployment set; platform -lifecycle is top-level. +`service`, `job`, and `tool` are **views** over the one deployment set. Lifecycle +is convergence — `castle apply` — not a pile of per-kind verbs. ``` # Programs — the software catalog -castle program list|info|create|add|clone|delete|run|install|uninstall +castle program list|info|create|add|clone|delete|run castle program build|test|lint|type-check|check [name] # dev verbs # Deployment lenses (service = systemd, job = systemd + schedule, tool = path) -castle service list|info|create|delete|deploy|enable|disable|start|stop|restart|logs +castle service list|info|create|delete|restart|logs castle job …same verbs; create takes --schedule -castle tool list|info|install|uninstall # CLIs on your PATH +castle tool list|info # CLIs on your PATH # Platform-wide +castle apply [name] [--plan] # converge runtime to config — the workhorse castle list [--kind K] [--stack S] [--json] # catalog + every deployment view castle status # unified runtime status castle doctor # diagnose setup + health, with fix hints -castle deploy [name] # apply config → units + Caddyfile -castle start | stop | restart # all deployments (+ gateway) -castle gateway start|stop|reload|status +castle restart [name] # imperative bounce (one or all) +castle gateway reload|status ``` +To turn a deployment off, set `enabled: false` in its config and `castle apply` — +there's no start/stop/enable/install verb; the manager decides the mechanism +(systemd unit, PATH install, gateway route), the verb is always `apply`. + `castle tool list --json` is the machine-readable tool catalog assistants use to build context — it surfaces each tool's actual **executable** (which can differ from the program name, e.g. `litellm-intent-router` installs `intent-router`), its diff --git a/docs/design.md b/docs/design.md index 82fe24a..244629d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -204,7 +204,7 @@ A service's env is exactly its `defaults.env` — castle injects nothing implicitly. Values may use `${port}`/`${data_dir}`/`${name}`/`${secret:…}` placeholders, which deploy resolves into the registry's flat `env`. -**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle deploy`) — Node config: +**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle apply`) — Node config: ```yaml node: @@ -228,7 +228,7 @@ deployed: ``` The node config says what's deployed *here* and with what concrete -values. `castle deploy` reads the spec from the repo, resolves the +values. `castle apply` reads the spec from the repo, resolves the `defaults.env` placeholders and secrets, resolves binary paths, and writes the registry. Systemd units and Caddyfile are then generated from the registry — never from the spec directly. @@ -280,7 +280,7 @@ programs on a single node and across multiple Castle nodes. **MQTT topics:** - `castle/{hostname}/registry` — retained JSON, full NodeRegistry. - Published on connect and after `castle deploy`. + Published on connect and after `castle apply`. - `castle/{hostname}/status` — `"online"` (retained) / `"offline"` (LWT). LWT ensures nodes are marked offline if they disconnect unexpectedly. @@ -457,7 +457,7 @@ $CASTLE_HOME/ ← Config & artifacts (default ~/.castle) ├── code/ ← Program source (your programs) │ └── / ├── artifacts/ -│ ├── specs/ ← Generated by `castle deploy` +│ ├── specs/ ← Generated by `castle apply` │ │ ├── Caddyfile │ │ └── registry.yaml ← Node config (what's deployed here) │ └── content/ ← Built frontend assets @@ -491,7 +491,7 @@ Castle's architecture parallels Erlang/OTP, mapped onto Unix: | Application | Component (independent, self-contained) | | Application resource file | Component spec in `castle.yaml` | | Release config (sys.config) | Node config in `$CASTLE_HOME/artifacts/specs/registry.yaml` | -| Release assembly | `castle deploy` (spec + node config → runtime) | +| Release assembly | `castle apply` (spec + node config → runtime) | | Supervisor | systemd (restart policies, ordering) | | Process | Running service/worker/job | | Application env | Env vars | @@ -527,12 +527,12 @@ What exists today: - **CLI** — `castle` command, installed via `uv tool install --editable cli/` - **Three packages** — `castle-core` (models, config, generators), `castle-cli` (commands), `castle-api` (HTTP API) -- **Source/runtime split** — `castle.yaml` (spec) → `castle deploy` → +- **Source/runtime split** — `castle.yaml` (spec) → `castle apply` → `$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and Caddyfile generated from registry with fully resolved paths. No repo references in runtime artifacts. - **Explicit env with placeholders** — a deployment's env is exactly its - `defaults.env`; `castle deploy` resolves `${port}`/`${data_dir}`/`${name}`/ + `defaults.env`; `castle apply` resolves `${port}`/`${data_dir}`/`${name}`/ `${secret:…}` into concrete values. No hidden convention injection. - **Gateway** — Caddy on port 9000, Caddyfile generated from registry - **API** — `castle-api` on port 9020, reads from registry (optional diff --git a/docs/developing-castle.md b/docs/developing-castle.md index 288b00f..4cd15c4 100644 --- a/docs/developing-castle.md +++ b/docs/developing-castle.md @@ -63,7 +63,7 @@ for tools/libraries. at `/usr/local/bin/caddy` when `tls: acme`. - Systemd user units at `~/.config/systemd/user/castle-*.service` (+ `.timer`); the unit for program `X` is `castle-X.service`. Use drop-in `*.service.d/*.conf` - for extra env `castle deploy` shouldn't overwrite. + for extra env `castle apply` shouldn't overwrite. - The `container` launcher resolves docker via `shutil.which("docker")` (preferred over rootless podman on this box). - Service data at `$CASTLE_DATA_DIR//`; secrets at `~/.castle/secrets/` diff --git a/docs/dns-and-tls.md b/docs/dns-and-tls.md index e746e8b..34e4cad 100644 --- a/docs/dns-and-tls.md +++ b/docs/dns-and-tls.md @@ -122,7 +122,7 @@ origin — moving a service onto HTTPS changes its origin. (§DNS). Verify: `dig +short .` → the node's IP. 3. **Set `gateway.tls: acme`** (with `domain`/`acme_email`), plus the operational prerequisites (below). -4. **Deploy & reload:** `castle deploy` regenerates the Caddyfile and reloads Caddy. +4. **Deploy & reload:** `castle apply` regenerates the Caddyfile and reloads Caddy. 5. **Update the app's origin allowlist** if it has one (§secure context). ## Operational prerequisites @@ -142,7 +142,7 @@ a DNS token. (`~/.castle/secrets/`, scope: the DNS provider's "edit DNS records" permission for your zone) and map it into the gateway service env in `deployments/castle-gateway.yaml` (`defaults.env`), so Caddy reads it as - `{env.}`. `castle deploy` warns if the domain, env var, or secret is + `{env.}`. `castle apply` warns if the domain, env var, or secret is missing. - **`acme` — stage first.** Set `CASTLE_ACME_STAGING=1` at deploy to use Let's Encrypt's staging CA (generous rate limits) while verifying issuance, then unset diff --git a/docs/registry.md b/docs/registry.md index 12c0ae5..5752f85 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -333,7 +333,7 @@ proxy: true # expose at . `public: true` additionally projects a proxied service to the public internet via a Cloudflare tunnel, at **`.`** (a separate zone, so internal subdomain names stay out of public DNS). Defaults to `false` — public is -explicit — and **requires `proxy: true`**. `castle deploy` generates the cloudflared +explicit — and **requires `proxy: true`**. `castle apply` generates the cloudflared ingress from the set of public services. Needs `gateway.public_domain` + `gateway.tunnel_id` set and the `castle-tunnel` service running; see @docs/tunnel-setup.md for the one-time setup. @@ -467,7 +467,7 @@ Setup (the parts castle can't do for you): env: CLOUDFLARE_API_TOKEN: ${secret:CLOUDFLARE_API_TOKEN} ``` - `castle deploy` warns if the domain, this env var, or the secret is missing. + `castle apply` warns if the domain, this env var, or the secret is missing. - **LAN DNS.** Add a wildcard on your LAN's DNS server (usually the router) pointing `*.` at the gateway's private IP — `address=//` (dnsmasq) or the equivalent A record. The public zone gets no A records, so @@ -493,8 +493,9 @@ manage: systemd: {} ``` -Enables `castle service enable/disable` and `castle service logs`. An empty `{}` -uses defaults (enable=true, restart=on-failure, restart_sec=2). +Marks the deployment systemd-managed, so `castle apply` generates and reconciles +its unit (and `castle service logs` tails it). An empty `{}` uses defaults +(enable=true, restart=on-failure, restart_sec=2). Full options: ```yaml @@ -636,17 +637,17 @@ castle program create my-service --stack python-fastapi # 1. Scaffold + regist cd /data/repos/my-service && uv sync # 2. Install deps # ... implement ... castle program test my-service # 3. Run tests -castle service enable my-service # 4. Generate systemd unit, start -castle gateway reload # 5. Update Caddy routes +castle apply my-service # 4. Render unit + routes and reconcile (start it) ``` -After `service enable`, the service starts automatically on boot and restarts -on failure. Manage with: +`castle apply` renders the systemd unit + Caddy route and starts the service +(which then runs on boot and restarts on failure). Manage with: ```bash castle logs my-service -f # Tail logs castle service run my-service # Run in foreground (for debugging) -castle service disable my-service # Stop and remove systemd unit +castle service restart my-service # Imperative bounce +# To stop it durably: set `enabled: false` in its deployment, then `castle apply` ``` ### Tool lifecycle @@ -656,7 +657,7 @@ castle program create my-tool --stack python-cli # 1. Scaffold + register cd /data/repos/my-tool && uv sync # 2. Install deps # ... implement ... castle program test my-tool # 3. Run tests -uv tool install --editable /data/repos/my-tool/ # 4. Install to PATH +castle apply my-tool # 4. Install the path deployment on PATH ``` ### Job lifecycle @@ -676,7 +677,7 @@ manage: systemd: {} ``` -`castle job enable my-job` generates both a `.service` (Type=oneshot) +`castle apply my-job` generates both a `.service` (Type=oneshot) and a `.timer` file. ## Infrastructure paths diff --git a/docs/stacks/react-vite.md b/docs/stacks/react-vite.md index 102a890..a8e7c7d 100644 --- a/docs/stacks/react-vite.md +++ b/docs/stacks/react-vite.md @@ -171,7 +171,7 @@ See @docs/registry.md for the full registry reference. ## Serving with Caddy For production, the static build output is served by Caddy rather than a Node -process. You do **not** write this block by hand — `castle deploy` generates it. +process. You do **not** write this block by hand — `castle apply` generates it. The flow: 1. You build the frontend with `castle program build ` (deploy does **not** diff --git a/docs/stacks/supabase.md b/docs/stacks/supabase.md index 56da107..ec13212 100644 --- a/docs/stacks/supabase.md +++ b/docs/stacks/supabase.md @@ -119,7 +119,7 @@ leaves the schema intact (and says so). To destroy the data too: castle delete my-app --purge-data # drop schema my_app cascade ``` -`castle deploy` then prunes the schema from `PGRST_DB_SCHEMAS`; **restart the +`castle apply` then prunes the schema from `PGRST_DB_SCHEMAS`; **restart the `supabase` service** for PostgREST to pick up the added/removed schema list. ## Auth, RLS & the three privacy layers @@ -161,7 +161,7 @@ service itself is likewise at `supabase.`.) See castle program create my-app --stack supabase --description "..." # scaffold + register castle program build my-app # apply unapplied migrations to the substrate castle program test my-app # deno test over functions/ (if deno present) -castle deploy && castle gateway reload # serve the static UI at /my-app/ +castle apply # serve the static UI at /my-app/ ``` ## Scaffolding diff --git a/docs/tunnel-setup.md b/docs/tunnel-setup.md index 9d48d61..a328ee9 100644 --- a/docs/tunnel-setup.md +++ b/docs/tunnel-setup.md @@ -17,7 +17,7 @@ internet at `.`, via a Cloudflare Tunnel. Cloudflare (no inbound holes, no public IP needed) and forwards each public hostname to the gateway on `:443`, rewriting the Host header and TLS SNI to the *internal* name so Caddy routes it and its wildcard cert validates. The public - surface is exactly the set of `public: true` services — `castle deploy` generates + surface is exactly the set of `public: true` services — `castle apply` generates the ingress from the registry. - **One kill switch.** Stop `castle-tunnel` → instantly nothing is public. @@ -76,8 +76,8 @@ manage: Bring it online: ```bash -castle deploy # writes cloudflared.yml from public services -castle service enable castle-tunnel # start the tunnel +castle apply # writes cloudflared.yml from public services +castle apply castle-tunnel # start the tunnel ``` ## Using the toggle @@ -92,10 +92,10 @@ public: true # also expose at .pub.payne.io via the tunnel Then just deploy: ```bash -castle deploy +castle apply ``` -`castle deploy` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the +`castle apply` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the current set of public services and restarts `castle-tunnel`. Flip `public` back to `false` (or remove it) and redeploy to un-expose — the hostname drops out of the ingress immediately. @@ -116,14 +116,14 @@ printf %s "" > ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN chmod 600 ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN ``` -With it present, every `castle deploy` **reconciles** the public CNAMEs against the +With it present, every `castle apply` **reconciles** the public CNAMEs against the current public set — creating missing ones and deleting removed ones — and prints a one-line summary. It only ever touches records pointing at *this* tunnel (`.cfargotunnel.com`), so hand-managed records in the same zone are safe. This token is separate from `CLOUDFLARE_API_TOKEN` (which is the ACME token for the internal zone); the public zone is usually a different zone/account. -Without the token, `castle deploy` instead prints the exact command to run per host: +Without the token, `castle apply` instead prints the exact command to run per host: ```bash cloudflared tunnel route dns .pub.payne.io diff --git a/install.sh b/install.sh index f0d9833..105d5c5 100755 --- a/install.sh +++ b/install.sh @@ -22,7 +22,7 @@ CASTLE_HOME="${HOME}/.castle" CASTLE_CONF="${CASTLE_HOME}/infra.conf" # Program data lives on a dedicated volume, decoupled from CASTLE_HOME — must match # castle_core.config._resolve_data_dir (default /data/castle, override CASTLE_DATA_DIR) -# so the data dirs + container mounts created here line up with what castle deploy +# so the data dirs + container mounts created here line up with what castle apply # generates for the mqtt/postgres/neo4j deployments. DATA_DIR="${CASTLE_DATA_DIR:-/data/castle}" # Program source repos (default /data/repos, override CASTLE_REPOS_DIR). @@ -177,7 +177,7 @@ CADDY_DNS_VERSION="${CADDY_DNS_VERSION:-v2.11.4}" # (Let's Encrypt wildcard via DNS-01). Stock apt Caddy has no DNS modules. The # result goes to /usr/local/bin/caddy, which precedes /usr/bin on PATH, so the # gateway (a `command` runner resolving `caddy` via PATH) picks it up on the next -# `castle deploy` with no spec change. Idempotent and opt-in (--with-dns-plugin). +# `castle apply` with no spec change. Idempotent and opt-in (--with-dns-plugin). ensure_caddy_dns_plugin() { local provider="${1:-cloudflare}" local module @@ -211,7 +211,7 @@ ensure_caddy_dns_plugin() { /usr/local/bin/caddy list-modules 2>/dev/null | grep -q "dns.providers.$provider" \ || log_fail "built caddy is missing dns.providers.$provider" - log_info "Built /usr/local/bin/caddy — run 'castle deploy && castle gateway restart' to use it." + log_info "Built /usr/local/bin/caddy — run 'castle apply' to use it." log_ok } @@ -259,7 +259,7 @@ create_directories() { # Program data volume (default /data/castle) lives outside $HOME, so on a fresh # machine its parent may not be user-writable — fall back to sudo + chown so the - # later container mounts (and castle deploy) can write there. + # later container mounts (and castle apply) can write there. if ! mkdir -p "${DATA_DIR}" 2>/dev/null; then log_info "creating ${DATA_DIR} (needs sudo — outside \$HOME)" sudo mkdir -p "${DATA_DIR}" @@ -280,7 +280,7 @@ create_directories() { # --------------------------------------------------------------------------- # Register Castle's own control-plane programs/deployments from bootstrap/ so a -# fresh registry is not empty. Without this, `castle deploy && castle start` +# fresh registry is not empty. Without this, `castle apply` # would bring up nothing. Never clobbers existing entries (idempotent). The # gateway deployment carries a `__SPECS_DIR__` placeholder (the source repo has # no machine-specific paths) that we substitute with this machine's specs dir. @@ -337,7 +337,7 @@ seed_caddyfile() { fi cat > "$caddyfile" << 'EOF' :9000 { - respond "Castle is starting. Run 'castle deploy' to configure." 200 + respond "Castle is starting. Run 'castle apply' to configure." 200 } EOF log_ok @@ -553,8 +553,7 @@ print_summary() { printf "\n" printf "Next steps:\n" - printf " castle deploy # Generate registry, systemd units, Caddyfile\n" - printf " castle start # Start the gateway, API, and all deployments\n" + printf " castle apply # Converge: render units/routes + start everything\n" printf " castle doctor # Verify setup + health (green = good to go)\n" printf " open http://localhost:9000 # the dashboard\n" }