docs: clean up pre-existing staleness across the guides

Sweep of accumulated drift unrelated to the CLI reorg:
- Source paths: ~/.castle/code/<name> → /data/repos/<name> (REPOS_DIR), and
  source: code/X → absolute /data/repos/X. registry.md source-resolution table
  and 'how programs get in' section rewritten for the /data/repos layout.
- Old field name component: → program: in all YAML examples.
- Old term 'component' → 'program' throughout design.md and the stack guides.
- Frontend serving: corrected the copy-to-artifacts/content model to serve-in-
  place from the repo (<source>/<dist>), matching Phase 2b; dropped the dead
  _copy_app_static reference and the 'runtime never references the source tree'
  claim (frontends are the deliberate exception).
- Stale endpoints/commands: /components → /deployments; castle create/add →
  castle program create/add in the create.py/add.py docstrings.

CLAUDE.md was already current. cli 24 green; ruff clean.
This commit is contained in:
2026-06-14 16:32:13 -07:00
parent 5c6d8ec6f1
commit fd562f7468
7 changed files with 98 additions and 101 deletions

View File

@@ -1,6 +1,6 @@
"""castle add — adopt an existing repo as a program (no scaffolding). """castle program add — adopt an existing repo as a program (no scaffolding).
`castle create` makes new code from a stack. `castle add` adopts code that `castle program create` makes new code from a stack. `castle program add` adopts code that
already exists — a local path, or a git URL to clone. It detects sensible dev already exists — a local path, or a git URL to clone. It detects sensible dev
verb commands so a non-castle project becomes usable without writing them by hand. verb commands so a non-castle project becomes usable without writing them by hand.
""" """

View File

@@ -1,4 +1,4 @@
"""castle create - scaffold a new project from templates.""" """castle program create scaffold a new program from templates."""
from __future__ import annotations from __future__ import annotations

View File

@@ -20,7 +20,7 @@ is self-sufficient. The mesh is optional.
Castle service is just a well-behaved Unix daemon that happens to Castle service is just a well-behaved Unix daemon that happens to
be registered in a manifest. be registered in a manifest.
3. **Stack and behavior.** Each component has a *stack* (development 3. **Stack and behavior.** Each program has a *stack* (development
toolchain: python-fastapi, python-cli, react-vite) and a *behavior* toolchain: python-fastapi, python-cli, react-vite) and a *behavior*
(runtime role: daemon, tool, frontend). Scheduling, systemd management, (runtime role: daemon, tool, frontend). Scheduling, systemd management,
and proxying are orthogonal operations — not behaviors. and proxying are orthogonal operations — not behaviors.
@@ -36,7 +36,7 @@ is self-sufficient. The mesh is optional.
source tree. source tree.
6. **AI-manageable.** The CLI and API exist so that AI assistants can 6. **AI-manageable.** The CLI and API exist so that AI assistants can
discover, create, and manage components programmatically. Humans discover, create, and manage programs programmatically. Humans
use the dashboard. Agents use the CLI and API. use the dashboard. Agents use the CLI and API.
7. **Simple until proven otherwise.** Filesystem over databases. HTTP 7. **Simple until proven otherwise.** Filesystem over databases. HTTP
@@ -123,25 +123,25 @@ Castle generates systemd unit files and Caddyfile entries from the
registry. It doesn't run a daemon itself — it configures OS-level registry. It doesn't run a daemon itself — it configures OS-level
infrastructure and gets out of the way. infrastructure and gets out of the way.
Critically, the runtime layer references only standard paths — never Systemd units point to installed binaries (on PATH or in `~/.local/bin/`),
the source tree. Systemd units point to installed binaries (on PATH not to repo subdirectories. Frontends are the deliberate exception: rather
or in `~/.local/bin/`), not to repo subdirectories. Caddy serves than stage a copy, Caddy serves their built assets **in place** from the repo
from `$CASTLE_HOME/artifacts/content/`, not from build output directories in the repo. (`<source>/<dist>/`), with the serve prefix baked into the build via `VITE_BASE`.
### Registry Layer ### Registry Layer
The registry is the central concept in Castle. It tracks what components The registry is the central concept in Castle. It tracks what programs
exist, what they can do, and how they're configured. But it's not a exist, what they can do, and how they're configured. But it's not a
single thing — it's three distinct concepts: single thing — it's three distinct concepts:
**1. Component spec** — what a component *is*. Description, capabilities, **1. Component spec** — what a program *is*. Description, capabilities,
build instructions, default configuration. This is source-level build instructions, default configuration. This is source-level
information, version-controlled in the repo. It answers: "what components information, version-controlled in the repo. It answers: "what programs
could exist?" could exist?"
**2. Node config** — what's *deployed on this machine*, with what concrete **2. Node config** — what's *deployed on this machine*, with what concrete
ports, data paths, and env vars. This is per-machine. Two Castle nodes ports, data paths, and env vars. This is per-machine. Two Castle nodes
might run different subsets of components with different parameters. It might run different subsets of programs with different parameters. It
answers: "what's running here, and how?" answers: "what's running here, and how?"
**3. Runtime state** — what's *actually happening*. PIDs, health, uptime, **3. Runtime state** — what's *actually happening*. PIDs, health, uptime,
@@ -158,11 +158,11 @@ These map to two files:
programs: programs:
central-context: central-context:
description: Content storage API description: Content storage API
source: code/central-context source: /data/repos/central-context
services: services:
central-context: central-context:
component: central-context program: central-context
run: run:
runner: python runner: python
tool: central-context tool: central-context
@@ -178,7 +178,7 @@ services:
jobs: jobs:
backup-collect: backup-collect:
component: backup-collect program: backup-collect
run: run:
runner: command runner: command
argv: [backup-collect] argv: [backup-collect]
@@ -191,7 +191,7 @@ Programs define *what software exists* (identity, source, install, tools).
Services define *how daemons run* (run config, expose, proxy, systemd). Services define *how daemons run* (run config, expose, proxy, systemd).
Jobs define *how scheduled tasks run* (run config, cron schedule, systemd). Jobs define *how scheduled tasks run* (run config, cron schedule, systemd).
Services and jobs can reference a component via `component:` for description Services and jobs can reference a program via `program:` for description
fallthrough and source code linking. They can also exist independently fallthrough and source code linking. They can also exist independently
(e.g., `castle-gateway` runs Caddy — not our software). (e.g., `castle-gateway` runs Caddy — not our software).
@@ -239,7 +239,7 @@ Three interfaces expose the registry:
- **CLI** (`castle`) — For AI agents and terminal users. Structured - **CLI** (`castle`) — For AI agents and terminal users. Structured
output via `--json`. Commands for listing, inspecting, creating, output via `--json`. Commands for listing, inspecting, creating,
and managing components. and managing programs.
- **API** (`castle-api`) — For programmatic access over HTTP. Used by - **API** (`castle-api`) — For programmatic access over HTTP. Used by
the dashboard, other nodes, and remote agents. the dashboard, other nodes, and remote agents.
- **Dashboard** (`castle-app`) — For human discoverability. Visual - **Dashboard** (`castle-app`) — For human discoverability. Visual
@@ -248,7 +248,7 @@ Three interfaces expose the registry:
### Coordination Layer ### Coordination Layer
Coordination handles discovery and communication — both between Coordination handles discovery and communication — both between
components on a single node and across multiple Castle nodes. programs on a single node and across multiple Castle nodes.
**Intra-node coordination:** **Intra-node coordination:**
- Components find each other through the gateway (path-based routing) - Components find each other through the gateway (path-based routing)
@@ -257,7 +257,7 @@ components on a single node and across multiple Castle nodes.
- No service mesh or message broker required for basic operation. - No service mesh or message broker required for basic operation.
**Inter-node coordination:** **Inter-node coordination:**
- Each Castle node runs the API, which exposes its component registry. - Each Castle node runs the API, which exposes its program registry.
- Nodes discover each other via MQTT retained messages and mDNS/DNS-SD - Nodes discover each other via MQTT retained messages and mDNS/DNS-SD
(python-zeroconf) for LAN environments. (python-zeroconf) for LAN environments.
- The gateway on each node can proxy to services on other nodes, - The gateway on each node can proxy to services on other nodes,
@@ -290,9 +290,9 @@ Local paths always take precedence.
**Why MQTT over custom gossip:** **Why MQTT over custom gossip:**
- Standard protocol, every language has a client library. - Standard protocol, every language has a client library.
- Retained messages give new nodes an immediate view of the network. - Retained messages give new nodes an immediate view of the network.
- Topic-based routing maps naturally to `castle/{node}/{component}`. - Topic-based routing maps naturally to `castle/{node}/{program}`.
- Works across networks (not just LAN like mDNS). - Works across networks (not just LAN like mDNS).
- Mosquitto is a single binary, simple to run as a Castle component. - Mosquitto is a single binary, simple to run as a Castle program.
**Why mDNS/DNS-SD as a complement:** **Why mDNS/DNS-SD as a complement:**
- Zero-config LAN discovery via python-zeroconf. - Zero-config LAN discovery via python-zeroconf.
@@ -303,9 +303,9 @@ Local paths always take precedence.
### Dashboard ### Dashboard
The web dashboard (`castle-app`) is a React SPA served by Caddy from The web dashboard (`castle-app`) is a React SPA served by Caddy in place from
`$CASTLE_HOME/artifacts/content/castle-app/`. It talks to `castle-api` via the its repo build output (`<source>/dist/`) at the root `/`. It talks to
gateway proxy at `/api`. `castle-api` via the gateway proxy at `/api`.
**Layout:** **Layout:**
@@ -344,7 +344,7 @@ Components · Software catalog
backup-collect Python / CLI tool 0 2 * * * — backup-collect Python / CLI tool 0 2 * * * —
``` ```
**Key components:** **Key programs:**
- **GatewayPanel** — Route table with live health badges, reload button, - **GatewayPanel** — Route table with live health badges, reload button,
collapsible Caddyfile viewer. Node column appears when multi-node. collapsible Caddyfile viewer. Node column appears when multi-node.
- **MeshPanel** — MQTT connection status (connected/disconnected badge), - **MeshPanel** — MQTT connection status (connected/disconnected badge),
@@ -353,7 +353,7 @@ Components · Software catalog
- **NodeBar** — Horizontal list of discovered nodes. Hidden in single-node - **NodeBar** — Horizontal list of discovered nodes. Hidden in single-node
mode. Each node links to `/node/{hostname}`. mode. Each node links to `/node/{hostname}`.
- **ServiceSection** — Daemon cards in a responsive grid. - **ServiceSection** — Daemon cards in a responsive grid.
- **ComponentTable** — Unified sortable table for all non-daemon components - **ComponentTable** — Unified sortable table for all non-daemon programs
(tools, frontends) with Stack, Behavior, Schedule, and Status columns. (tools, frontends) with Stack, Behavior, Schedule, and Status columns.
**Real-time updates:** **Real-time updates:**
@@ -365,11 +365,11 @@ Components · Software catalog
**Multi-node behavior:** **Multi-node behavior:**
- NodeBar appears when `GET /nodes` returns >1 node. - NodeBar appears when `GET /nodes` returns >1 node.
- GatewayPanel shows a "Node" column when routes span multiple nodes. - GatewayPanel shows a "Node" column when routes span multiple nodes.
- `/node/{hostname}` page shows a specific node's deployed components. - `/node/{hostname}` page shows a specific node's deployed programs.
## Component Contract ## Component Contract
Every Castle component, regardless of language, must satisfy a minimal Every Castle program, regardless of language, must satisfy a minimal
contract. This is what makes the system uniform above the build line. contract. This is what makes the system uniform above the build line.
### Services (long-running daemons) ### Services (long-running daemons)
@@ -421,11 +421,11 @@ Each step is distinct:
2. **Install** — Makes the artifact available on the system. For tools: 2. **Install** — Makes the artifact available on the system. For tools:
`uv tool install` or compiled binary placed in `~/.local/bin/`. For `uv tool install` or compiled binary placed in `~/.local/bin/`. For
services: same — the binary or entry point is on PATH or in a known services: same — the binary or entry point is on PATH or in a known
location. For frontends: built assets copied to location. For frontends: assets built in place under the repo
`$CASTLE_HOME/artifacts/content/`. (`<source>/<dist>/`), served from there — no copy.
3. **Deploy** — Materializes the runtime configuration. Reads the 3. **Deploy** — Materializes the runtime configuration. Reads the
component spec, merges with node config, generates systemd units program spec, merges with node config, generates systemd units
and Caddyfile entries that reference *installed* artifacts — never and Caddyfile entries that reference *installed* artifacts — never
the source tree. Enables and starts services. the source tree. Enables and starts services.
@@ -507,7 +507,7 @@ Key OTP ideas that apply:
can remap these across nodes. can remap these across nodes.
- **Spec vs. config.** In OTP, an application defines its structure - **Spec vs. config.** In OTP, an application defines its structure
(the `.app` file) and a release provides the deployment config (the `.app` file) and a release provides the deployment config
(`sys.config`). Castle mirrors this: the component spec defines (`sys.config`). Castle mirrors this: the program spec defines
structure, the node config provides deployment values. structure, the node config provides deployment values.
## Current State ## Current State
@@ -526,9 +526,9 @@ What exists today:
the manifest. Only non-convention values need `defaults.env`. the manifest. Only non-convention values need `defaults.env`.
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry - **Gateway** — Caddy on port 9000, Caddyfile generated from registry
- **API** — `castle-api` on port 9020, reads from registry (optional - **API** — `castle-api` on port 9020, reads from registry (optional
castle.yaml fallback for non-deployed components) castle.yaml fallback for non-deployed programs)
- **Dashboard** — `castle-app` React/Vite frontend, static assets - **Dashboard** — `castle-app` React/Vite frontend, static assets
served from `$CASTLE_HOME/artifacts/content/castle-app/` served in place from its repo build output (`<source>/dist/`)
- **Services** — central-context (content storage), notification-bridge - **Services** — central-context (content storage), notification-bridge
(desktop notification forwarder) (desktop notification forwarder)
- **Jobs** — protonmail (email sync every 5 min), backup-collect (nightly), - **Jobs** — protonmail (email sync every 5 min), backup-collect (nightly),
@@ -543,7 +543,7 @@ What exists today:
on port 1883, managed by systemd. Config and data in on port 1883, managed by systemd. Config and data in
`$CASTLE_DATA_DIR/castle-mqtt/`. `$CASTLE_DATA_DIR/castle-mqtt/`.
- **Node API** — `GET /mesh/status`, `GET /nodes`, `GET /nodes/{hostname}`. - **Node API** — `GET /mesh/status`, `GET /nodes`, `GET /nodes/{hostname}`.
`GET /components?include_remote=true` for cross-node component listing. `GET /deployments?include_remote=true` for cross-node program listing.
- **Gateway panel** — Dedicated UI showing route table, health per route, - **Gateway panel** — Dedicated UI showing route table, health per route,
reload button, Caddyfile viewer. Cross-node routes shown when multi-node. reload button, Caddyfile viewer. Cross-node routes shown when multi-node.
- **Mesh panel** — Dashboard UI showing MQTT connection status, broker - **Mesh panel** — Dashboard UI showing MQTT connection status, broker
@@ -555,7 +555,7 @@ What exists today:
What doesn't exist yet: What doesn't exist yet:
- **Multi-language support** — Rust and Go components (the abstractions - **Multi-language support** — Rust and Go programs (the abstractions
support them via `command` runner, but no examples exist yet) support them via `command` runner, but no examples exist yet)
- **Build automation** — Castle records build specs but doesn't - **Build automation** — Castle records build specs but doesn't
orchestrate builds (each project builds independently) orchestrate builds (each project builds independently)

View File

@@ -43,14 +43,14 @@ gateway:
programs: programs:
my-tool: my-tool:
description: Does something useful description: Does something useful
source: code/my-tool source: /data/repos/my-tool
stack: python-cli stack: python-cli
behavior: tool behavior: tool
system_dependencies: [pandoc] system_dependencies: [pandoc]
services: services:
my-service: my-service:
component: my-service program: my-service
run: run:
runner: python runner: python
program: my-service program: my-service
@@ -65,7 +65,7 @@ services:
jobs: jobs:
my-job: my-job:
component: my-tool program: my-tool
run: run:
runner: command runner: command
argv: [my-tool, sync] argv: [my-tool, sync]
@@ -82,7 +82,7 @@ jobs:
| `services:` | Long-running daemons — how they run | service | | `services:` | Long-running daemons — how they run | service |
| `jobs:` | Scheduled tasks — when they run | job | | `jobs:` | Scheduled tasks — when they run | job |
Services and jobs can reference a program via `component:` for description Services and jobs can reference a program via `program:` for description
fallthrough and source code linking. They can also exist independently fallthrough and source code linking. They can also exist independently
(e.g., `castle-gateway` runs Caddy — not our software). (e.g., `castle-gateway` runs Caddy — not our software).
@@ -104,7 +104,7 @@ Explicit declaration of how the program is used:
### `source` — Where the source lives ### `source` — Where the source lives
```yaml ```yaml
source: code/my-tool # your programs, under ~/.castle/code/ source: /data/repos/my-tool # your programs, under $CASTLE_REPOS_DIR
source: repo:castle-api # castle's own programs, inside the git repo source: repo:castle-api # castle's own programs, inside the git repo
``` ```
@@ -112,16 +112,15 @@ The `source` path is resolved one of three ways (`core/src/castle_core/config.py
| `source:` value | Resolves to | Used for | | `source:` value | Resolves to | Used for |
|-----------------|-------------|----------| |-----------------|-------------|----------|
| `code/my-tool` *(relative)* | `$CASTLE_HOME/code/my-tool` | Your own programs | | `/data/repos/my-tool` *(absolute)* | as-is | Your own programs (the default) |
| `repo:castle-api` | `<repo>/castle-api` (via the top-level `repo:` field) | Castle's built-in programs | | `repo:castle-api` | `<repo>/castle-api` (via the top-level `repo:` field) | Castle's built-in programs |
| `/abs/path` | as-is | Anything explicitly absolute | | `code/my-tool` *(relative)* | `$CASTLE_HOME/code/my-tool` | Legacy — pre-`/data/repos` layout |
Relative sources resolve against the castle home (`$CASTLE_HOME`, default Programs you create or adopt live under **`$CASTLE_REPOS_DIR`** (default
`~/.castle` — see [Infrastructure paths](#infrastructure-paths)). Most programs `/data/repos`, override with `CASTLE_REPOS_DIR`) and are recorded with an
you create live under **`$CASTLE_HOME/code/`** and are recorded as **absolute** `source:`. Castle's own programs (CLI, core, castle-api, app) live
`source: code/<name>`. Castle's own programs (CLI, core, castle-api, app) live in in the git repo and use the `repo:` prefix. A relative `source:` still resolves
the git repo and use the `repo:` prefix. When `castle deploy` writes `castle.yaml` against `$CASTLE_HOME` for back-compat, but new programs no longer use it.
back out, it rewrites absolute paths into these relative forms.
### `stack` — Development toolchain (optional) ### `stack` — Development toolchain (optional)
@@ -168,7 +167,7 @@ system_dependencies: [pandoc, poppler-utils]
``` ```
System packages that must be installed for the program to work. Displayed System packages that must be installed for the program to work. Displayed
in `castle list --behavior tool` and the dashboard. in `castle program list --behavior tool` and the dashboard.
### `version` — Program version ### `version` — Program version
@@ -287,23 +286,21 @@ Castle generates a systemd `.timer` file alongside the `.service` unit.
Jobs also support `run` (required), `manage`, and `defaults` — same Jobs also support `run` (required), `manage`, and `defaults` — same
semantics as services. semantics as services.
## How programs get into `~/.castle/code/` ## How programs get into `/data/repos/`
Every program's source lives in one place — `~/.castle/code/<name>/`. It can Every program's source lives under `$CASTLE_REPOS_DIR` (default `/data/repos/<name>/`).
arrive there a few ways: It can arrive there a few ways:
1. **Scaffold a new one** with `castle program create` — writes the project into 1. **Scaffold a new one** with `castle program create` — writes the project into
`~/.castle/code/<name>/` and registers it in `castle.yaml` with `/data/repos/<name>/` and registers it in `castle.yaml` with an absolute
`source: code/<name>`. `source: /data/repos/<name>`.
2. **Clone an existing project**`git clone <url> ~/.castle/code/<name>`, 2. **Adopt an existing repo**`castle program add <path|git-url>` registers it
then add a `programs:` entry pointing at `source: code/<name>`. in place (or records its `repo:` URL for `castle program clone`).
3. **Drop files in directly** — a `code/<name>/` directory is just a working 3. **Drop files in directly** — a `/data/repos/<name>/` directory is just a
tree; it doesn't have to be under version control to be registered and run. working tree; it doesn't have to be under version control to be run.
`~/.castle/` and `~/.castle/code/` are **not** themselves git repos, and there `/data/repos/` holds independent repos — each program directory manages its own
are no submodules. Each program directory manages its own version control (or version control (or none); some are standalone git clones, others loose files.
none) independently — some are standalone git clones, others are just loose
files.
Castle's own programs (CLI, core, castle-api, app) are the exception: they live Castle's own programs (CLI, core, castle-api, app) are the exception: they live
inside the castle git repo and are referenced with `source: repo:<name>`. inside the castle git repo and are referenced with `source: repo:<name>`.
@@ -313,16 +310,16 @@ inside the castle git repo and are referenced with `source: repo:<name>`.
### Via `castle program create` (recommended) ### Via `castle program create` (recommended)
```bash ```bash
# Service — scaffolds into ~/.castle/code/, assigns port, registers in castle.yaml # Service — scaffolds into /data/repos/, assigns port, registers in castle.yaml
castle program create my-service --stack python-fastapi --description "Does something" castle program create my-service --stack python-fastapi --description "Does something"
# Tool — scaffolds into ~/.castle/code/ # Tool — scaffolds into /data/repos/
castle program create my-tool --stack python-cli --description "Does something" castle program create my-tool --stack python-cli --description "Does something"
``` ```
### Manually ### Manually
Clone or create the project under `~/.castle/code/`, then add entries to the Clone or create the project under `/data/repos/`, then add entries to the
appropriate sections of `castle.yaml`: appropriate sections of `castle.yaml`:
```yaml ```yaml
@@ -330,7 +327,7 @@ appropriate sections of `castle.yaml`:
programs: programs:
my-tool: my-tool:
description: Does something useful description: Does something useful
source: code/my-tool source: /data/repos/my-tool
stack: python-cli stack: python-cli
behavior: tool behavior: tool
@@ -338,13 +335,13 @@ programs:
programs: programs:
my-service: my-service:
description: Does something useful description: Does something useful
source: code/my-service source: /data/repos/my-service
stack: python-fastapi stack: python-fastapi
behavior: daemon behavior: daemon
services: services:
my-service: my-service:
component: my-service program: my-service
run: run:
runner: python runner: python
program: my-service program: my-service
@@ -364,7 +361,7 @@ services:
```bash ```bash
castle program create my-service --stack python-fastapi # 1. Scaffold + register castle program create my-service --stack python-fastapi # 1. Scaffold + register
cd ~/.castle/code/my-service && uv sync # 2. Install deps cd /data/repos/my-service && uv sync # 2. Install deps
# ... implement ... # ... implement ...
castle program test my-service # 3. Run tests castle program test my-service # 3. Run tests
castle service enable my-service # 4. Generate systemd unit, start castle service enable my-service # 4. Generate systemd unit, start
@@ -384,10 +381,10 @@ castle service disable my-service # Stop and remove systemd unit
```bash ```bash
castle program create my-tool --stack python-cli # 1. Scaffold + register castle program create my-tool --stack python-cli # 1. Scaffold + register
cd ~/.castle/code/my-tool && uv sync # 2. Install deps cd /data/repos/my-tool && uv sync # 2. Install deps
# ... implement ... # ... implement ...
castle program test my-tool # 3. Run tests castle program test my-tool # 3. Run tests
uv tool install --editable ~/.castle/code/my-tool/ # 4. Install to PATH uv tool install --editable /data/repos/my-tool/ # 4. Install to PATH
``` ```
### Job lifecycle ### Job lifecycle
@@ -427,7 +424,7 @@ variable (both expand `~` and resolve relative paths):
| Program source (castle's) | `<repo>/<name>` (via `source: repo:<name>`) | | Program source (castle's) | `<repo>/<name>` (via `source: repo:<name>`) |
| Secrets | `$CASTLE_HOME/secrets/<NAME>` | | Secrets | `$CASTLE_HOME/secrets/<NAME>` |
| Generated Caddyfile | `$CASTLE_HOME/artifacts/specs/Caddyfile` | | Generated Caddyfile | `$CASTLE_HOME/artifacts/specs/Caddyfile` |
| Built frontends | `$CASTLE_HOME/artifacts/content/<name>/` | | Built frontends | served in place from `<source>/<dist>/` (no copy) |
| **Service data** | **`$CASTLE_DATA_DIR/<name>/` (default `/data/castle/<name>/`)** | | **Service data** | **`$CASTLE_DATA_DIR/<name>/` (default `/data/castle/<name>/`)** |
| Systemd units | `~/.config/systemd/user/castle-*.service` | | Systemd units | `~/.config/systemd/user/castle-*.service` |
| Systemd timers | `~/.config/systemd/user/castle-*.timer` | | Systemd timers | `~/.config/systemd/user/castle-*.timer` |

View File

@@ -31,10 +31,10 @@ How to build CLI tools following Unix philosophy.
## Project layout ## Project layout
Each tool is an independent project under `~/.castle/code/` with its own `pyproject.toml`: Each tool is an independent project under `/data/repos/` with its own `pyproject.toml`:
``` ```
~/.castle/code/my-tool/ /data/repos/my-tool/
├── src/my_tool/ ├── src/my_tool/
│ ├── __init__.py │ ├── __init__.py
│ └── main.py # Entry point │ └── main.py # Entry point
@@ -50,7 +50,7 @@ Examples: `code/pdf2md/`, `code/gpt/`, `code/protonmail/`
```bash ```bash
castle program create my-tool --stack python-cli --description "Does something" castle program create my-tool --stack python-cli --description "Does something"
cd ~/.castle/code/my-tool && uv sync cd /data/repos/my-tool && uv sync
``` ```
This scaffolds the project and registers it in `castle.yaml`. This scaffolds the project and registers it in `castle.yaml`.
@@ -324,7 +324,7 @@ uv run ruff format . # Format
programs: programs:
my-tool: my-tool:
description: Does something useful description: Does something useful
source: code/my-tool source: /data/repos/my-tool
stack: python-cli stack: python-cli
behavior: tool behavior: tool
``` ```
@@ -335,13 +335,13 @@ Tools with system dependencies declare them directly on the program:
programs: programs:
pdf2md: pdf2md:
description: Convert PDF files to Markdown description: Convert PDF files to Markdown
source: code/pdf2md source: /data/repos/pdf2md
stack: python-cli stack: python-cli
behavior: tool behavior: tool
system_dependencies: [pandoc, poppler-utils] system_dependencies: [pandoc, poppler-utils]
``` ```
Tools live in the `programs:` section. If a tool also runs on a schedule, Tools live in the `programs:` section. If a tool also runs on a schedule,
add a separate entry in the `jobs:` section referencing the component. add a separate entry in the `jobs:` section referencing the program.
See @docs/registry.md for the full registry reference. See @docs/registry.md for the full registry reference.

View File

@@ -109,13 +109,13 @@ Castle passes config via env vars in castle.yaml:
programs: programs:
my-service: my-service:
description: Does something useful description: Does something useful
source: code/my-service source: /data/repos/my-service
stack: python-fastapi stack: python-fastapi
behavior: daemon behavior: daemon
services: services:
my-service: my-service:
component: my-service program: my-service
run: run:
runner: python runner: python
program: my-service program: my-service

View File

@@ -7,7 +7,7 @@
> stack — it declares its own `commands:`. See @docs/registry.md for > stack — it declares its own `commands:`. See @docs/registry.md for
> `commands:`, `stack:` (optional), and `repo:`. > `commands:`, `stack:` (optional), and `repo:`.
How to build, serve, and manage web frontends as castle components. How to build, serve, and manage web frontends as castle programs.
## Stack ## Stack
@@ -27,7 +27,7 @@ How to build, serve, and manage web frontends as castle components.
```bash ```bash
# Create the project # Create the project
mkdir ~/.castle/code/my-frontend && cd ~/.castle/code/my-frontend mkdir /data/repos/my-frontend && cd /data/repos/my-frontend
pnpm create vite . --template react-ts pnpm create vite . --template react-ts
# Core dependencies # Core dependencies
@@ -122,9 +122,9 @@ pnpm run check # lint + type-check + test
The `build` output is a static SPA in `dist/` — just HTML, JS, and CSS files. The `build` output is a static SPA in `dist/` — just HTML, JS, and CSS files.
## Registering as a castle component ## Registering as a program
A frontend component has a `build` spec (produces static output). Register it A frontend program has a `build` spec (produces static output). Register it
in the `programs:` section of `castle.yaml`. No `run` block needed if Caddy in the `programs:` section of `castle.yaml`. No `run` block needed if Caddy
handles serving directly from the build output. handles serving directly from the build output.
@@ -133,7 +133,7 @@ handles serving directly from the build output.
programs: programs:
my-frontend: my-frontend:
description: Web dashboard description: Web dashboard
source: code/my-frontend source: /data/repos/my-frontend
build: build:
commands: commands:
- ["pnpm", "build"] - ["pnpm", "build"]
@@ -141,16 +141,16 @@ programs:
- dist/ - dist/
``` ```
For production, `castle deploy` copies the build output to For production, Caddy serves the build output **in place** from the program's
`~/.castle/artifacts/content/<name>/` and Caddy serves it from there — no repo (`<source>/<build.outputs[0]>`) — no Node process and no copy into a
Node process needed. See [Serving with Caddy](#serving-with-caddy) below. central directory. See [Serving with Caddy](#serving-with-caddy) below.
For development with Vite's dev server, add a service entry: For development with Vite's dev server, add a service entry:
```yaml ```yaml
services: services:
my-frontend: my-frontend:
component: my-frontend program: my-frontend
run: run:
runner: node runner: node
script: dev script: dev
@@ -170,19 +170,19 @@ 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 deploy` generates it.
The flow: The flow:
1. `castle deploy` runs the program's `build.commands`, then copies each 1. `castle deploy` runs the program's `build.commands` (so `dist/` is current).
`build.outputs` directory from `~/.castle/code/<name>/` into 2. The Caddyfile generator emits a route per `behavior: frontend` program that
`~/.castle/artifacts/content/<name>/` (`core/src/castle_core/deploy.py`, has `build.outputs`, rooted **in place** at `<source>/<build.outputs[0]>`
`_copy_app_static`). no copy. A static frontend mounts at `/<name>/`; `castle-app` is
2. The Caddyfile generator scans `~/.castle/artifacts/content/`; any directory special-cased to serve at the root `/`.
containing an `index.html` is served as a SPA at a path prefix matching its
name. `castle-app` is special-cased to serve at the root `/`.
The generated block looks like this (written to `~/.castle/artifacts/specs/Caddyfile`): The build is run with `VITE_BASE` set to that serve prefix, so a `vite.config`
that reads it (see [Vite config](#vite-config)) emits asset URLs that resolve at
the subpath. The generated block (in `~/.castle/artifacts/specs/Caddyfile`):
```caddyfile ```caddyfile
handle_path /my-frontend/* { handle_path /my-frontend/* {
root * /home/payne/.castle/artifacts/content/my-frontend root * /data/repos/my-frontend/dist
try_files {path} /index.html try_files {path} /index.html
file_server file_server
} }