diff --git a/.gitignore b/.gitignore index a5e4fa6..097ad84 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ __pycache__ .claude node_modules castle.yaml +!bootstrap/**/castle.yaml .working diff --git a/bootstrap/deployments/castle-api.yaml b/bootstrap/deployments/castle-api.yaml new file mode 100644 index 0000000..3c190f0 --- /dev/null +++ b/bootstrap/deployments/castle-api.yaml @@ -0,0 +1,18 @@ +program: castle-api +description: Castle API +manager: systemd +run: + launcher: python + program: castle-api +expose: + http: + internal: + port: 9020 + health_path: /health +proxy: true +manage: + systemd: {} +defaults: + env: + CASTLE_API_PORT: ${port} + CASTLE_API_DATA_DIR: ${data_dir} diff --git a/bootstrap/deployments/castle-gateway.yaml b/bootstrap/deployments/castle-gateway.yaml new file mode 100644 index 0000000..06d2041 --- /dev/null +++ b/bootstrap/deployments/castle-gateway.yaml @@ -0,0 +1,22 @@ +# The Caddy gateway. `__SPECS_DIR__` is substituted with $CASTLE_HOME/artifacts/specs +# at install time (install.sh seed_control_plane). For acme TLS, add a +# CLOUDFLARE_API_TOKEN to defaults.env later — see docs/dns-and-tls.md. +description: Caddy reverse proxy gateway +manager: systemd +run: + launcher: command + argv: + - caddy + - run + - --config + - __SPECS_DIR__/Caddyfile + - --adapter + - caddyfile +expose: + http: + internal: + port: 9000 + health_path: / +manage: + systemd: + exec_reload: caddy reload --config __SPECS_DIR__/Caddyfile --adapter caddyfile diff --git a/bootstrap/deployments/castle.yaml b/bootstrap/deployments/castle.yaml new file mode 100644 index 0000000..d0d0550 --- /dev/null +++ b/bootstrap/deployments/castle.yaml @@ -0,0 +1,3 @@ +program: castle +description: Castle web app +manager: caddy diff --git a/bootstrap/programs/castle-api.yaml b/bootstrap/programs/castle-api.yaml new file mode 100644 index 0000000..ae9a404 --- /dev/null +++ b/bootstrap/programs/castle-api.yaml @@ -0,0 +1,3 @@ +description: Castle API +source: repo:castle-api +stack: python-fastapi diff --git a/bootstrap/programs/castle.yaml b/bootstrap/programs/castle.yaml new file mode 100644 index 0000000..7aef44e --- /dev/null +++ b/bootstrap/programs/castle.yaml @@ -0,0 +1,6 @@ +description: Castle web app +source: repo:app +stack: react-vite +build: + outputs: + - dist/ diff --git a/install.sh b/install.sh index 3533f85..9b69239 100755 --- a/install.sh +++ b/install.sh @@ -215,6 +215,34 @@ ensure_caddy_dns_plugin() { log_ok } +# --------------------------------------------------------------------------- +# Castle CLI +# --------------------------------------------------------------------------- + +ensure_uv() { + log_step "Ensuring uv (Python package manager)" + if cmd_exists uv; then + log_skip "already installed" + return + fi + curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 || log_fail "uv install failed" + # The installer drops uv in ~/.local/bin — make it visible for the rest of this run. + export PATH="${HOME}/.local/bin:${PATH}" + cmd_exists uv || log_fail "uv installed but not on PATH (expected ~/.local/bin)" + log_ok +} + +# Install the `castle` CLI from this repo as an editable uv tool, so a fresh +# clone becomes a working `castle` command. Idempotent — reinstall is cheap and +# keeps the entry point pointed at the current checkout. +install_cli() { + log_step "Installing the castle CLI" + ( cd "$CASTLE_ROOT" && uv tool install --editable ./cli >/dev/null 2>&1 ) \ + || log_fail "uv tool install of ./cli failed" + cmd_exists castle || log_info "NOTE: ensure ~/.local/bin is on your PATH to use 'castle'" + log_ok +} + # --------------------------------------------------------------------------- # Directory structure # --------------------------------------------------------------------------- @@ -247,6 +275,41 @@ create_directories() { fi } +# --------------------------------------------------------------------------- +# Control plane (Castle's own gateway + API + dashboard) +# --------------------------------------------------------------------------- + +# Register Castle's own control-plane programs/deployments from bootstrap/ so a +# fresh registry is not empty. Without this, `castle deploy && castle start` +# 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. +seed_control_plane() { + log_step "Registering Castle's control plane" + local specs="${CASTLE_HOME}/artifacts/specs" + + # The `repo:` field lets `source: repo:` resolve castle's own programs. + if ! grep -q "^repo:" "${CASTLE_HOME}/castle.yaml" 2>/dev/null; then + printf 'repo: %s\n' "$CASTLE_ROOT" >> "${CASTLE_HOME}/castle.yaml" + fi + + local seeded=0 f dst + for f in "${CASTLE_ROOT}"/bootstrap/programs/*.yaml; do + dst="${CASTLE_HOME}/programs/$(basename "$f")" + [ -f "$dst" ] || { cp "$f" "$dst"; seeded=1; } + done + for f in "${CASTLE_ROOT}"/bootstrap/deployments/*.yaml; do + dst="${CASTLE_HOME}/deployments/$(basename "$f")" + [ -f "$dst" ] || { sed "s#__SPECS_DIR__#${specs}#g" "$f" > "$dst"; seeded=1; } + done + + if [ "$seeded" = "1" ]; then + log_ok "castle-gateway, castle-api, castle (dashboard)" + else + log_skip "already registered" + fi +} + # --------------------------------------------------------------------------- # Systemd lingering # --------------------------------------------------------------------------- @@ -435,6 +498,28 @@ maybe_setup_neo4j() { fi } +# --------------------------------------------------------------------------- +# Dashboard build +# --------------------------------------------------------------------------- + +# Build the dashboard SPA so the gateway has something to serve at :9000. The +# `castle` program serves `app/dist/` in place; without a build there's no UI. +# Best-effort: a missing pnpm is a warning (the CLI/API still work), not a failure. +build_dashboard() { + log_step "Building the dashboard" + if [ ! -d "${CASTLE_ROOT}/app" ]; then + log_skip "no app/ directory" + return + fi + if ! cmd_exists pnpm; then + log_skip "pnpm not found — build later with 'castle program build castle'" + return + fi + ( cd "${CASTLE_ROOT}/app" && pnpm install --silent >/dev/null 2>&1 && pnpm build >/dev/null 2>&1 ) \ + || { log_skip "build failed — retry later with 'castle program build castle'"; return; } + log_ok "app/dist/" +} + # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- @@ -468,8 +553,9 @@ print_summary() { printf "\n" printf "Next steps:\n" - printf " castle deploy # Generate registry, systemd units, Caddyfile\n" - printf " castle start # Start all deployments and the gateway\n" + printf " castle deploy # Generate registry, systemd units, Caddyfile\n" + printf " castle start # Start the gateway, API, and all deployments\n" + printf " open http://localhost:9000 # the dashboard\n" } # --------------------------------------------------------------------------- @@ -500,13 +586,17 @@ main() { ensure_docker ensure_caddy [ -n "$WITH_DNS_PLUGIN" ] && ensure_caddy_dns_plugin "$WITH_DNS_PLUGIN" + ensure_uv + install_cli create_directories + seed_control_plane enable_lingering seed_caddyfile migrate_old_containers setup_mqtt setup_postgres maybe_setup_neo4j + build_dashboard print_summary }