From 05b28cb58495f2fcc057d42e26d3c3cc96b3f218 Mon Sep 17 00:00:00 2001
From: Paul Payne
Date: Sat, 18 Jul 2026 22:55:08 -0700
Subject: [PATCH] Rename Castle -> Wild PC across the repo
Repo-side rename only (Phases 1-3 of the migration plan); the live box
(~/.castle, systemd units, /data/castle, domains) is a separate cutover.
- Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api),
dist names, entry point `wildpc = wildpc_cli.main:main`.
- Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*.
- Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml ->
wildpc.yaml, /data/castle -> /data/wildpc.
- Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc.
- Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend.
- Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated;
redundant nested uv.lock files dropped (workspace root lock is authoritative).
Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds.
Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
---
.gitignore | 4 +-
AGENTS.md | 196 +++----
CLAUDE.md | 2 +-
README.md | 150 +++---
app/.env | 2 +-
app/index.html | 2 +-
app/src/components/AddProgramForm.tsx | 4 +-
app/src/components/AssistantDock.tsx | 2 +-
app/src/components/CommandPalette.tsx | 2 +-
app/src/components/ConvergePanel.tsx | 2 +-
app/src/components/GatewaySettings.tsx | 4 +-
app/src/components/HostSwitcher.tsx | 6 +-
app/src/components/Layout.tsx | 4 +-
app/src/components/detail/ConfigPanel.tsx | 8 +-
.../detail/CreateDeploymentForm.tsx | 2 +-
app/src/components/detail/JobFields.tsx | 4 +-
app/src/components/detail/ProgramFields.tsx | 6 +-
app/src/components/detail/ServiceFields.tsx | 6 +-
app/src/components/detail/ToolFields.tsx | 2 +-
app/src/components/detail/fields.tsx | 2 +-
app/src/lib/labels.ts | 2 +-
app/src/pages/Overview.tsx | 2 +-
app/src/pages/ProgramDetail.tsx | 2 +-
app/src/pages/ServiceDetail.tsx | 2 +-
app/src/pages/SystemMap.tsx | 16 +-
app/src/services/api/client.ts | 10 +-
app/src/services/api/hooks.ts | 10 +-
app/src/types/index.ts | 4 +-
app/vite.config.ts | 4 +-
.../{castle-api.yaml => wildpc-api.yaml} | 10 +-
...astle-gateway.yaml => wildpc-gateway.yaml} | 2 +-
bootstrap/deployments/statics/castle.yaml | 3 -
bootstrap/deployments/statics/wildpc.yaml | 3 +
bootstrap/programs/castle-api.yaml | 3 -
bootstrap/programs/wildpc-api.yaml | 3 +
.../programs/{castle.yaml => wildpc.yaml} | 2 +-
castle-api/CLAUDE.md | 31 --
castle-api/uv.lock | 487 ------------------
cli/pyproject.toml | 12 +-
cli/src/castle_cli/__init__.py | 3 -
cli/src/wildpc_cli/__init__.py | 3 +
.../src/wildpc_cli/commands}/__init__.py | 0
.../commands/add.py | 16 +-
.../commands/apply.py | 4 +-
.../commands/clone.py | 4 +-
.../commands/create.py | 30 +-
.../commands/delete.py | 20 +-
.../commands/deploy_create.py | 16 +-
.../commands/dev.py | 16 +-
.../commands/doctor.py | 90 ++--
.../commands/gateway.py | 16 +-
.../commands/graph.py | 6 +-
.../commands/info.py | 10 +-
.../commands/list_cmd.py | 8 +-
.../commands/logs.py | 10 +-
.../commands/mesh.py | 10 +-
.../commands/run_cmd.py | 12 +-
.../commands/secret.py | 12 +-
.../commands/service.py | 40 +-
.../commands/stack.py | 12 +-
.../commands/tls.py | 8 +-
.../commands/tool.py | 22 +-
cli/src/{castle_cli => wildpc_cli}/config.py | 14 +-
cli/src/{castle_cli => wildpc_cli}/main.py | 90 ++--
.../{castle_cli => wildpc_cli}/manifest.py | 6 +-
.../templates}/__init__.py | 0
.../templates/scaffold.py | 34 +-
cli/tests/conftest.py | 20 +-
cli/tests/test_add.py | 36 +-
cli/tests/test_create.py | 52 +-
cli/tests/test_delete.py | 42 +-
cli/tests/test_doctor.py | 66 +--
cli/tests/test_info.py | 62 +--
cli/tests/test_list.py | 44 +-
cli/tests/test_mesh.py | 14 +-
cli/tests/test_secret.py | 8 +-
cli/tests/test_tool.py | 32 +-
cli/uv.lock | 425 ---------------
core/pyproject.toml | 8 +-
core/src/castle_core/__init__.py | 3 -
core/src/wildpc_core/__init__.py | 3 +
.../src/{castle_core => wildpc_core}/adopt.py | 16 +-
.../src/{castle_core => wildpc_core}/audit.py | 12 +-
.../{castle_core => wildpc_core}/config.py | 142 ++---
.../{castle_core => wildpc_core}/deploy.py | 134 ++---
.../generators/__init__.py | 6 +-
.../generators/caddyfile.py | 36 +-
.../generators/dns.py | 10 +-
.../generators/systemd.py | 22 +-
.../generators/tunnel.py | 4 +-
core/src/{castle_core => wildpc_core}/git.py | 8 +-
.../{castle_core => wildpc_core}/lifecycle.py | 34 +-
.../{castle_core => wildpc_core}/manifest.py | 36 +-
.../{castle_core => wildpc_core}/registry.py | 22 +-
.../{castle_core => wildpc_core}/relations.py | 20 +-
.../secret_backends.py | 28 +-
.../stack_status.py | 18 +-
.../{castle_core => wildpc_core}/stacks.py | 38 +-
core/src/{castle_core => wildpc_core}/tls.py | 34 +-
.../tool_schema.py | 18 +-
.../toolchains.py | 14 +-
core/tests/conftest.py | 26 +-
core/tests/test_apply.py | 62 +--
core/tests/test_audit.py | 16 +-
core/tests/test_caddyfile.py | 38 +-
core/tests/test_caddyfile_remote.py | 10 +-
core/tests/test_config.py | 156 +++---
core/tests/test_deploy_prune.py | 32 +-
core/tests/test_deploy_run_cmd.py | 54 +-
core/tests/test_deploy_secret_env.py | 9 +-
core/tests/test_deploy_secret_gate.py | 8 +-
core/tests/test_dns.py | 12 +-
core/tests/test_fleet_role.py | 40 +-
core/tests/test_git.py | 4 +-
core/tests/test_kind_identity.py | 18 +-
core/tests/test_lifecycle.py | 42 +-
core/tests/test_manifest.py | 4 +-
core/tests/test_relations.py | 16 +-
core/tests/test_resolution.py | 4 +-
core/tests/test_secret_backends.py | 18 +-
core/tests/test_systemd.py | 42 +-
core/tests/test_tls.py | 10 +-
core/tests/test_tool_schema.py | 6 +-
core/tests/test_toolchains.py | 8 +-
core/tests/test_tunnel.py | 4 +-
docs/design.md | 196 +++----
...eloping-castle.md => developing-wildpc.md} | 46 +-
docs/dns-and-tls.md | 24 +-
docs/fleet-mesh-plan.md | 124 ++---
docs/registry.md | 202 ++++----
docs/relationships.md | 10 +-
docs/stacks/hugo.md | 26 +-
docs/stacks/python-cli.md | 10 +-
docs/stacks/python-fastapi.md | 32 +-
docs/stacks/react-vite.md | 38 +-
docs/stacks/supabase.md | 38 +-
docs/tcp-exposure.md | 84 +--
docs/tunnel-setup.md | 42 +-
install.sh | 202 ++++----
pyproject.toml | 10 +-
ruff.toml | 2 +-
uv.lock | 224 ++++----
wildpc-api/CLAUDE.md | 31 ++
{castle-api => wildpc-api}/pyproject.toml | 14 +-
.../src/wildpc_api}/__init__.py | 2 +-
.../src/wildpc_api}/agent_registry.py | 16 +-
.../src/wildpc_api}/agent_sessions.py | 28 +-
.../src/wildpc_api}/agents.py | 22 +-
.../src/wildpc_api}/config.py | 24 +-
.../src/wildpc_api}/config_editor.py | 56 +-
.../src/wildpc_api}/deploy_routes.py | 8 +-
.../src/wildpc_api}/graph.py | 6 +-
.../src/wildpc_api}/health.py | 6 +-
.../src/wildpc_api}/llm.py | 14 +-
.../src/wildpc_api}/logs.py | 12 +-
.../src/wildpc_api}/main.py | 44 +-
.../src/wildpc_api}/mdns.py | 32 +-
.../src/wildpc_api}/mesh.py | 2 +-
.../src/wildpc_api}/mesh_gateway.py | 12 +-
.../src/wildpc_api}/mesh_wire.py | 6 +-
.../src/wildpc_api}/models.py | 4 +-
.../src/wildpc_api}/nats_client.py | 24 +-
.../src/wildpc_api}/nodes.py | 8 +-
.../src/wildpc_api}/programs.py | 30 +-
.../src/wildpc_api}/pty_session.py | 4 +-
.../src/wildpc_api}/repos.py | 8 +-
.../src/wildpc_api}/routes.py | 116 ++---
.../src/wildpc_api}/secrets.py | 8 +-
.../src/wildpc_api}/services.py | 18 +-
.../src/wildpc_api}/stream.py | 4 +-
.../tests}/__init__.py | 0
{castle-api => wildpc-api}/tests/conftest.py | 70 +--
.../tests/test_config_delete.py | 4 +-
.../tests/test_deployment_roundtrip.py | 18 +-
.../tests/test_exposure_derivation.py | 16 +-
.../tests/test_graph_repos.py | 8 +-
.../tests/test_health.py | 18 +-
.../tests/test_kind_twins.py | 16 +-
{castle-api => wildpc-api}/tests/test_mesh.py | 4 +-
.../tests/test_mesh_config_api.py | 0
.../tests/test_mesh_wire.py | 12 +-
.../tests/test_nats_client.py | 10 +-
.../tests/test_nats_integration.py | 22 +-
.../tests/test_nodes.py | 10 +-
.../tests/test_programs_commands.py | 0
.../tests/test_programs_git.py | 4 +-
.../tests/test_secrets_overrides.py | 0
.../tests/test_stacks_api.py | 2 +-
.../tests/test_tool_schema_api.py | 40 +-
...le.code-workspace => wildpc.code-workspace | 6 +-
190 files changed, 2428 insertions(+), 3337 deletions(-)
rename bootstrap/deployments/services/{castle-api.yaml => wildpc-api.yaml} (55%)
rename bootstrap/deployments/services/{castle-gateway.yaml => wildpc-gateway.yaml} (88%)
delete mode 100644 bootstrap/deployments/statics/castle.yaml
create mode 100644 bootstrap/deployments/statics/wildpc.yaml
delete mode 100644 bootstrap/programs/castle-api.yaml
create mode 100644 bootstrap/programs/wildpc-api.yaml
rename bootstrap/programs/{castle.yaml => wildpc.yaml} (68%)
delete mode 100644 castle-api/CLAUDE.md
delete mode 100644 castle-api/uv.lock
delete mode 100644 cli/src/castle_cli/__init__.py
create mode 100644 cli/src/wildpc_cli/__init__.py
rename {castle-api/tests => cli/src/wildpc_cli/commands}/__init__.py (100%)
rename cli/src/{castle_cli => wildpc_cli}/commands/add.py (70%)
rename cli/src/{castle_cli => wildpc_cli}/commands/apply.py (96%)
rename cli/src/{castle_cli => wildpc_cli}/commands/clone.py (95%)
rename cli/src/{castle_cli => wildpc_cli}/commands/create.py (89%)
rename cli/src/{castle_cli => wildpc_cli}/commands/delete.py (92%)
rename cli/src/{castle_cli => wildpc_cli}/commands/deploy_create.py (89%)
rename cli/src/{castle_cli => wildpc_cli}/commands/dev.py (88%)
rename cli/src/{castle_cli => wildpc_cli}/commands/doctor.py (86%)
rename cli/src/{castle_cli => wildpc_cli}/commands/gateway.py (73%)
rename cli/src/{castle_cli => wildpc_cli}/commands/graph.py (93%)
rename cli/src/{castle_cli => wildpc_cli}/commands/info.py (96%)
rename cli/src/{castle_cli => wildpc_cli}/commands/list_cmd.py (97%)
rename cli/src/{castle_cli => wildpc_cli}/commands/logs.py (90%)
rename cli/src/{castle_cli => wildpc_cli}/commands/mesh.py (90%)
rename cli/src/{castle_cli => wildpc_cli}/commands/run_cmd.py (91%)
rename cli/src/{castle_cli => wildpc_cli}/commands/secret.py (88%)
rename cli/src/{castle_cli => wildpc_cli}/commands/service.py (87%)
rename cli/src/{castle_cli => wildpc_cli}/commands/stack.py (91%)
rename cli/src/{castle_cli => wildpc_cli}/commands/tls.py (92%)
rename cli/src/{castle_cli => wildpc_cli}/commands/tool.py (88%)
rename cli/src/{castle_cli => wildpc_cli}/config.py (54%)
rename cli/src/{castle_cli => wildpc_cli}/main.py (85%)
rename cli/src/{castle_cli => wildpc_cli}/manifest.py (74%)
rename cli/src/{castle_cli/commands => wildpc_cli/templates}/__init__.py (100%)
rename cli/src/{castle_cli => wildpc_cli}/templates/scaffold.py (95%)
delete mode 100644 cli/uv.lock
delete mode 100644 core/src/castle_core/__init__.py
create mode 100644 core/src/wildpc_core/__init__.py
rename core/src/{castle_core => wildpc_core}/adopt.py (91%)
rename core/src/{castle_core => wildpc_core}/audit.py (92%)
rename core/src/{castle_core => wildpc_core}/config.py (88%)
rename core/src/{castle_core => wildpc_core}/deploy.py (92%)
rename core/src/{castle_core => wildpc_core}/generators/__init__.py (73%)
rename core/src/{castle_core => wildpc_core}/generators/caddyfile.py (94%)
rename core/src/{castle_core => wildpc_core}/generators/dns.py (95%)
rename core/src/{castle_core => wildpc_core}/generators/systemd.py (92%)
rename core/src/{castle_core => wildpc_core}/generators/tunnel.py (97%)
rename core/src/{castle_core => wildpc_core}/git.py (96%)
rename core/src/{castle_core => wildpc_core}/lifecycle.py (92%)
rename core/src/{castle_core => wildpc_core}/manifest.py (94%)
rename core/src/{castle_core => wildpc_core}/registry.py (95%)
rename core/src/{castle_core => wildpc_core}/relations.py (96%)
rename core/src/{castle_core => wildpc_core}/secret_backends.py (88%)
rename core/src/{castle_core => wildpc_core}/stack_status.py (90%)
rename core/src/{castle_core => wildpc_core}/stacks.py (97%)
rename core/src/{castle_core => wildpc_core}/tls.py (89%)
rename core/src/{castle_core => wildpc_core}/tool_schema.py (96%)
rename core/src/{castle_core => wildpc_core}/toolchains.py (92%)
rename docs/{developing-castle.md => developing-wildpc.md} (60%)
create mode 100644 wildpc-api/CLAUDE.md
rename {castle-api => wildpc-api}/pyproject.toml (72%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/__init__.py (54%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/agent_registry.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/agent_sessions.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/agents.py (93%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/config.py (70%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/config_editor.py (93%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/deploy_routes.py (92%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/graph.py (90%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/health.py (95%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/llm.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/logs.py (91%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/main.py (78%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/mdns.py (81%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/mesh.py (98%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/mesh_gateway.py (84%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/mesh_wire.py (96%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/models.py (98%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/nats_client.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/nodes.py (96%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/programs.py (92%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/pty_session.py (97%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/repos.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/routes.py (92%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/secrets.py (94%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/services.py (93%)
rename {castle-api/src/castle_api => wildpc-api/src/wildpc_api}/stream.py (95%)
rename {cli/src/castle_cli/templates => wildpc-api/tests}/__init__.py (100%)
rename {castle-api => wildpc-api}/tests/conftest.py (82%)
rename {castle-api => wildpc-api}/tests/test_config_delete.py (94%)
rename {castle-api => wildpc-api}/tests/test_deployment_roundtrip.py (89%)
rename {castle-api => wildpc-api}/tests/test_exposure_derivation.py (95%)
rename {castle-api => wildpc-api}/tests/test_graph_repos.py (89%)
rename {castle-api => wildpc-api}/tests/test_health.py (95%)
rename {castle-api => wildpc-api}/tests/test_kind_twins.py (90%)
rename {castle-api => wildpc-api}/tests/test_mesh.py (96%)
rename {castle-api => wildpc-api}/tests/test_mesh_config_api.py (100%)
rename {castle-api => wildpc-api}/tests/test_mesh_wire.py (92%)
rename {castle-api => wildpc-api}/tests/test_nats_client.py (68%)
rename {castle-api => wildpc-api}/tests/test_nats_integration.py (84%)
rename {castle-api => wildpc-api}/tests/test_nodes.py (92%)
rename {castle-api => wildpc-api}/tests/test_programs_commands.py (100%)
rename {castle-api => wildpc-api}/tests/test_programs_git.py (95%)
rename {castle-api => wildpc-api}/tests/test_secrets_overrides.py (100%)
rename {castle-api => wildpc-api}/tests/test_stacks_api.py (96%)
rename {castle-api => wildpc-api}/tests/test_tool_schema_api.py (85%)
rename castle.code-workspace => wildpc.code-workspace (94%)
diff --git a/.gitignore b/.gitignore
index 097ad84..175b5b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,6 @@ __pycache__
.pytest_cache
.claude
node_modules
-castle.yaml
-!bootstrap/**/castle.yaml
+wildpc.yaml
+!bootstrap/**/wildpc.yaml
.working
diff --git a/AGENTS.md b/AGENTS.md
index 75d37e0..7744128 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
-# AGENTS.md — Castle
+# AGENTS.md — Wild PC
-You are working in **Castle**, a personal software platform. Castle is a
-monorepo of independent programs managed by the `castle` CLI. From this directory
+You are working in **Wild PC**, a personal software platform. Wild PC is a
+monorepo of independent programs managed by the `wildpc` CLI. From this directory
you can **manage all the software on this box from source** — create programs,
deploy them as services, jobs, tools, or static frontends, route them through the
gateway, expose them over TLS or a public tunnel, and coordinate across nodes.
@@ -13,7 +13,7 @@ section links to a doc under `docs/`. Read those before non-trivial changes.
## 1. Mental model — two layers
-Castle splits every piece of software into **what it is** and **how it runs here**:
+Wild PC splits every piece of software into **what it is** and **how it runs here**:
- **`programs/.yaml`** — the software *catalog*: source, stack, build,
system dependencies. "What software exists."
@@ -32,70 +32,70 @@ stored:
| `none` | — | **reference** | an external service on another node |
A program may have **no** deployment (just source you develop), or one/more
-deployments. Global settings live in **`castle.yaml`** (`gateway`, `repo`,
-`agents`). Config root defaults to `~/.castle/`.
+deployments. Global settings live in **`wildpc.yaml`** (`gateway`, `repo`,
+`agents`). Config root defaults to `~/.wildpc/`.
-**Prime directive:** regular programs must **never depend on castle**. They take
-standard config (data dir, port, URLs) via **env vars**; only castle's own
-programs (CLI, gateway, api) know castle internals. When you scaffold or adopt a
-program, wire it with env vars — not castle imports.
+**Prime directive:** regular programs must **never depend on wildpc**. They take
+standard config (data dir, port, URLs) via **env vars**; only wildpc's own
+programs (CLI, gateway, api) know wildpc internals. When you scaffold or adopt a
+program, wire it with env vars — not wildpc imports.
-→ Full reference: **`docs/registry.md`** (castle.yaml structure, every field,
+→ Full reference: **`docs/registry.md`** (wildpc.yaml structure, every field,
manifest models, lifecycle). Architecture rationale: **`docs/design.md`**.
---
-## 2. The `castle` CLI
+## 2. The `wildpc` CLI
Resource-first: operations live under the resource they act on. Names can collide
across resource types, so the resource is explicit.
```bash
# Programs — the software catalog
-castle program list [--kind service] [--stack python-cli] [--json]
-castle program info [--json]
-castle program create [--stack ...] [--description ...] # scaffold NEW code
-castle program add [--name ...] # adopt EXISTING repo
-castle program clone [name] # provision repo: source
-castle program delete [--source] [-y]
-castle program run [args...] # declared run command
-castle program build|test|lint|format|type-check|check [name] # dev verbs
+wildpc program list [--kind service] [--stack python-cli] [--json]
+wildpc program info [--json]
+wildpc program create [--stack ...] [--description ...] # scaffold NEW code
+wildpc program add [--name ...] # adopt EXISTING repo
+wildpc program clone [name] # provision repo: source
+wildpc program delete [--source] [-y]
+wildpc program run [args...] # declared run command
+wildpc 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 restart # imperative bounce
-castle service logs [-f] [-n 50]
+wildpc service list|info [--json]
+wildpc service create [--program P] [--port N] [--health ...] [--launcher ...]
+wildpc service restart # imperative bounce
+wildpc service logs [-f] [-n 50]
# Jobs — scheduled tasks (manager: systemd + schedule). create takes --schedule
-castle job create [--program P] --schedule "0 2 * * *" [--launcher ...]
-castle job ...
+wildpc job create [--program P] --schedule "0 2 * * *" [--launcher ...]
+wildpc job ...
# Tools — CLIs on PATH (manager: path)
-castle tool list [--json] # each tool's executable + description + install state
-castle tool info [--json]
+wildpc tool list [--json] # each tool's executable + description + install state
+wildpc tool info [--json]
# Secrets — read/write the ACTIVE backend (file or openbao); never hand-write the store
-castle secret list # names in the active backend
-castle secret set [VALUE] # VALUE omitted → hidden prompt / stdin
-castle secret get # print a value
-castle secret rm [-y] # delete
+wildpc secret list # names in the active backend
+wildpc secret set [VALUE] # VALUE omitted → hidden prompt / stdin
+wildpc secret get # print a value
+wildpc secret rm [-y] # delete
# Platform-wide
-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 restart [name] # imperative bounce (one or all)
-castle gateway # gateway status + route table (inspection)
+wildpc apply [name] [--plan] # converge runtime to config — the workhorse
+wildpc list [--kind ...] [--stack ...] [--json] # all deployments
+wildpc status # unified health/status
+wildpc doctor # diagnose setup + runtime, with fix hints
+wildpc restart [name] # imperative bounce (one or all)
+wildpc gateway # gateway status + route table (inspection)
```
-`castle service`/`job`/`tool` are **views** over the one deployment set, filtered
-by derived kind. Lifecycle is **convergence**: edit config, then **`castle apply`**
+`wildpc service`/`job`/`tool` are **views** over the one deployment set, filtered
+by derived kind. Lifecycle is **convergence**: edit config, then **`wildpc 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.
+`wildpc 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
@@ -109,11 +109,11 @@ commands. All projects use **uv** (Python) / **pnpm** (frontends).
### Create a new service (HTTP daemon)
```bash
-castle program create my-service --stack python-fastapi --description "Does X"
+wildpc 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 apply my-service # renders the unit + gateway route and starts it
+wildpc program test my-service
+wildpc service create my-service --program my-service --port 9001
+wildpc 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`
@@ -122,12 +122,12 @@ maps via placeholders (see §6). Stack guide: **`docs/stacks/python-fastapi.md`*
### Create a CLI tool
```bash
-castle program create my-tool --stack python-cli --description "Does Y"
+wildpc program create my-tool --stack python-cli --description "Does Y"
cd /data/repos/my-tool && uv sync
-castle apply my-tool # installs the path deployment on PATH
+wildpc apply my-tool # installs the path deployment on PATH
```
-`castle tool list --json` is the machine-readable tool catalog (each tool's real
+`wildpc tool list --json` is the machine-readable tool catalog (each tool's real
**executable**, which may differ from the program name). Stack:
**`docs/stacks/python-cli.md`**.
@@ -137,17 +137,17 @@ A job is a `manager: systemd` deployment with a `schedule` (cron). Generates a
`.service` (Type=oneshot) + a `.timer`.
```bash
-castle job create nightly --program my-tool --schedule "0 2 * * *" --launcher command
-castle apply nightly
+wildpc job create nightly --program my-tool --schedule "0 2 * * *" --launcher command
+wildpc apply nightly
```
### Create a static frontend
```bash
# scaffold a Vite/React app under /data/repos/my-frontend (see docs/stacks/react-vite.md)
-castle program build my-frontend # produces dist/
+wildpc program build my-frontend # produces dist/
# deployments/my-frontend.yaml → manager: caddy, root: dist
-castle apply # served at my-frontend.
+wildpc apply # served at my-frontend.
```
The gateway serves the build **in place** from `/` — no copy, no
@@ -158,11 +158,11 @@ Supabase substrate: **`docs/stacks/supabase.md`**.
### Adopt an existing repo (no stack needed)
```bash
-castle program add ~/projects/some-rust-tool # local path
-castle program add https://github.com/me/widget.git --name widget
+wildpc program add ~/projects/some-rust-tool # local path
+wildpc program add https://github.com/me/widget.git --name widget
```
-Castle detects dev-verb commands (pyproject→uv/ruff/pytest, Cargo.toml→cargo, …)
+Wild PC detects dev-verb commands (pyproject→uv/ruff/pytest, Cargo.toml→cargo, …)
or you declare them under `commands:` in `programs/.yaml`.
---
@@ -191,19 +191,19 @@ public: true # ALSO expose to the internet via Cloudflare tunnel (requires pro
- There are **no path-prefix routes** — a whole subdomain maps to the backend
root, so root-relative assets and `window.location` WebSocket URLs just work.
-Inspect routes: `castle gateway` / `GET /gateway`. Regenerate routes + reload the
-gateway: `castle apply` (converge). → Field-level detail: **`docs/registry.md`**.
+Inspect routes: `wildpc gateway` / `GET /gateway`. Regenerate routes + reload the
+gateway: `wildpc apply` (converge). → Field-level detail: **`docs/registry.md`**.
---
## 5. DNS & TLS — making names resolve and be trusted
Two orthogonal questions for `https://foo./` to work from a LAN browser:
-**resolve** (DNS) and **trust** (TLS). Castle doesn't run DNS — you add one
+**resolve** (DNS) and **trust** (TLS). Wild PC doesn't run DNS — you add one
**wildcard** record on the LAN's DNS server (usually the router):
`address=//` (dnsmasq) pinned with a DHCP reservation.
-`gateway.tls` in `castle.yaml` picks the trust mode:
+`gateway.tls` in `wildpc.yaml` picks the trust mode:
| `gateway.tls` | serves | client setup | when |
|---------------|--------|--------------|------|
@@ -215,83 +215,83 @@ Two orthogonal questions for `https://foo./` to work from a LAN browser:
the names — the public zone has no A records). HTTPS also unlocks **secure
context** (`crypto.subtle`, service workers), which plain-HTTP LAN hosts lack.
-`acme` operational prerequisites (castle can't do these for you):
+`acme` operational prerequisites (wildpc 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 apply` warns if missing.
+ (`CLOUDFLARE_API_TOKEN`); `wildpc 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
+- **Stage first**: `WILDPC_ACME_STAGING=1` at deploy, verify issuance, then unset
and redeploy for a production cert.
→ Full conceptual + step-by-step guide: **`docs/dns-and-tls.md`**. Read the actual
-values for *this* node in `~/.castle/castle.yaml` (`gateway.domain`, `tls`, etc.).
+values for *this* node in `~/.wildpc/wildpc.yaml` (`gateway.domain`, `tls`, etc.).
---
## 6. Environment, secrets, data, placeholders
`defaults.env` in a deployment is the **single explicit source** of the env a
-service/job runs with — castle injects nothing implicitly. Map the vars your
-program reads to castle's computed values with placeholders:
+service/job runs with — wildpc injects nothing implicitly. Map the vars your
+program reads to wildpc's computed values with placeholders:
```yaml
expose: { http: { internal: { port: 9001 }, health_path: /health } }
defaults:
env:
MY_SERVICE_PORT: ${port} # = expose.http.internal.port
- MY_SERVICE_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/
+ MY_SERVICE_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/
PUBLIC_URL: ${public_url} # gateway origin (CORS/allowlists)
- API_KEY: ${secret:MY_API_KEY} # reads ~/.castle/secrets/MY_API_KEY
+ API_KEY: ${secret:MY_API_KEY} # reads ~/.wildpc/secrets/MY_API_KEY
```
| placeholder | expands to |
|-------------|-----------|
| `${port}` | the service's `expose.http.internal.port` |
-| `${data_dir}` | `$CASTLE_DATA_DIR/` (default `/data/castle/`) |
+| `${data_dir}` | `$WILDPC_DATA_DIR/` (default `/data/wildpc/`) |
| `${name}` | the deployment name |
| `${public_url}` | `https://.` under acme, else `http://localhost:` |
| `${secret:NAME}` | the secret `NAME` from the active backend |
**Secret backend.** `${secret:NAME}` (and `read_secret()` for direct-read code)
-resolve through a backend selected by the **`secrets:` block in `castle.yaml`**
+resolve through a backend selected by the **`secrets:` block in `wildpc.yaml`**
(`core/secret_backends.py`):
```yaml
secrets:
backend: openbao # or `file` (default)
- addr: https://castle-openbao.:8200
- mount: castle # KV-v2 mount
+ addr: https://wildpc-openbao.:8200
+ mount: wildpc # KV-v2 mount
token_secret: OPENBAO_ROOT_TOKEN # vault token, read from a file
node_prefix: nodes/ # optional: per-node overrides
```
-- **file** (default) → `~/.castle/secrets/NAME`.
+- **file** (default) → `~/.wildpc/secrets/NAME`.
- **openbao** → vault KV-v2 `mount/NAME`, no file fallback. The vault **token** is
still a file (`token_secret`, the bootstrap root of trust — it can't live in the
vault it opens). With `node_prefix`, a read tries `mount//NAME`
then shared `mount/NAME`, so one vault serves shared secrets + per-node overrides
- (e.g. each node's postgres password). Env vars (`CASTLE_SECRET_BACKEND`,
- `CASTLE_OPENBAO_*`) override the block (tests/CI force `file`).
+ (e.g. each node's postgres password). Env vars (`WILDPC_SECRET_BACKEND`,
+ `WILDPC_OPENBAO_*`) override the block (tests/CI force `file`).
- **This fleet runs `openbao`**: civil is the authority (root token), primer reads
- via a least-privilege `castle-read` token. Only the bootstrap tier stays as
+ via a least-privilege `wildpc-read` token. Only the bootstrap tier stays as
files (`OPENBAO_ROOT_TOKEN`/`OPENBAO_UNSEAL_KEY`, the `cloudflared` creds dir).
-**Write secrets with `castle secret set NAME`, never by hand.** It targets the
+**Write secrets with `wildpc secret set NAME`, never by hand.** It targets the
*active* backend, so a value can't land in a store the resolver never reads — the
failure mode where a file-written secret is silently ignored on an openbao fleet
and `${secret:NAME}` degrades to a literal `` that a service
-then uses as its credential. `castle apply` **refuses to converge** (exits
+then uses as its credential. `wildpc apply` **refuses to converge** (exits
non-zero, writes nothing) any deployment whose `${secret:…}` refs don't resolve in
the active backend, naming the fix.
-**Never** put secret *values* in `castle.yaml` or project dirs — use `${secret:…}`.
-Roots: **`CASTLE_HOME`** (config/code/artifacts/secrets, default `~/.castle`,
-env-only — it *contains* castle.yaml) and **program data** (base of `${data_dir}`,
-default `/data/castle`) + **repos** (default `/data/repos`). The latter two resolve
-**env > `castle.yaml` > default** — set `data_dir:` / `repos_dir:` in `castle.yaml`
+**Never** put secret *values* in `wildpc.yaml` or project dirs — use `${secret:…}`.
+Roots: **`WILDPC_HOME`** (config/code/artifacts/secrets, default `~/.wildpc`,
+env-only — it *contains* wildpc.yaml) and **program data** (base of `${data_dir}`,
+default `/data/wildpc`) + **repos** (default `/data/repos`). The latter two resolve
+**env > `wildpc.yaml` > default** — set `data_dir:` / `repos_dir:` in `wildpc.yaml`
(the single source of truth both the CLI and the api read), not a per-shell env var
-that only one of them sees. → **`docs/registry.md`** (castle.yaml globals).
+that only one of them sees. → **`docs/registry.md`** (wildpc.yaml globals).
---
@@ -299,9 +299,9 @@ that only one of them sees. → **`docs/registry.md`** (castle.yaml globals).
`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 apply` generates the cloudflared ingress from the
+stay out of public DNS). `wildpc 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`**.
+the `wildpc-tunnel` service running. → One-time setup: **`docs/tunnel-setup.md`**.
Routing only moves bytes — it does **not** supply a backend's own auth. Do not make
a service public unless it authenticates or is meant to be open.
@@ -310,19 +310,19 @@ a service public unless it authenticates or is meant to be open.
## 8. Mesh — multi-node coordination (opt-in)
-Runs on **NATS JetStream** (`castle-nats`, TLS + token). Enable via env on
-`castle-api`: `CASTLE_API_NATS_ENABLED=true`, `CASTLE_API_NATS_URL=tls://castle-nats.:4222`,
-`CASTLE_API_NATS_TOKEN=${secret:NATS_TOKEN}`. Each node publishes its
+Runs on **NATS JetStream** (`wildpc-nats`, TLS + token). Enable via env on
+`wildpc-api`: `WILDPC_API_NATS_ENABLED=true`, `WILDPC_API_NATS_URL=tls://wildpc-nats.:4222`,
+`WILDPC_API_NATS_TOKEN=${secret:NATS_TOKEN}`. Each node publishes its
(secret-stripped) registry to a JetStream **KV** bucket, renews a **presence**
key, and watches for peers; remote deployments surface as `manager: none`
-**reference** kinds. A static **`role`** (`authority`|`follower`, in `castle.yaml`)
+**reference** kinds. A static **`role`** (`authority`|`follower`, in `wildpc.yaml`)
gates who may write the shared-config bucket. A consumed cross-node service
(`requires: - ref: X` satisfied by a peer) is routed by the gateway with a
presence-gated circuit-breaker.
-Inspect + drive from the CLI: **`castle mesh status`** / **`castle mesh nodes`** /
-**`castle mesh config list|get|set`** (or `GET /mesh/status`, `/nodes`,
-`/mesh/config`). Modules: `castle_api.nats_client`, `.mesh`, `.mesh_gateway`,
+Inspect + drive from the CLI: **`wildpc mesh status`** / **`wildpc mesh nodes`** /
+**`wildpc mesh config list|get|set`** (or `GET /mesh/status`, `/nodes`,
+`/mesh/config`). Modules: `wildpc_api.nats_client`, `.mesh`, `.mesh_gateway`,
`.mdns`; secrets via `core` `secret_backends` (file default, OpenBao opt-in).
→ Full history + operations: **`docs/fleet-mesh-plan.md`**.
@@ -332,8 +332,8 @@ Inspect + drive from the CLI: **`castle mesh status`** / **`castle mesh nodes`**
| Topic | Doc |
|-------|-----|
-| Registry model, `castle.yaml`, every field, lifecycle | **`docs/registry.md`** |
-| Why castle is shaped this way | **`docs/design.md`** |
+| Registry model, `wildpc.yaml`, every field, lifecycle | **`docs/registry.md`** |
+| Why wildpc is shaped this way | **`docs/design.md`** |
| DNS resolution + the two TLS modes, acme recipe | **`docs/dns-and-tls.md`** |
| Public exposure (cloudflared) one-time setup | **`docs/tunnel-setup.md`** |
| Writing FastAPI services | **`docs/stacks/python-fastapi.md`** |
@@ -341,9 +341,9 @@ Inspect + drive from the CLI: **`castle mesh status`** / **`castle mesh nodes`**
| Writing React/Vite frontends | **`docs/stacks/react-vite.md`** |
| Writing Hugo static sites | **`docs/stacks/hugo.md`** |
| Database-backed apps (shared Supabase) | **`docs/stacks/supabase.md`** |
-| **Developing Castle itself** (CLI/core/api/app, key files, endpoints) | **`docs/developing-castle.md`** |
+| **Developing Wild PC itself** (CLI/core/api/app, key files, endpoints) | **`docs/developing-wildpc.md`** |
-Castle's own programs live in this repo (`source: repo:` → cli, core,
-castle-api, app). Your programs live under `/data/repos//` with an absolute
+Wild PC's own programs live in this repo (`source: repo:` → cli, core,
+wildpc-api, app). Your programs live under `/data/repos//` with an absolute
`source:`. When in doubt about *this* node's actual config, read
-`~/.castle/castle.yaml` and `castle status`.
+`~/.wildpc/wildpc.yaml` and `wildpc status`.
diff --git a/CLAUDE.md b/CLAUDE.md
index c22dce2..cdfea35 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,5 +1,5 @@
# CLAUDE.md
See @AGENTS.md — the single, agent-agnostic guide for this repo: how to use
-Castle (create/deploy/expose/manage software) and how to develop Castle's own
+Wild PC (create/deploy/expose/manage software) and how to develop Wild PC's own
code. Everything lives there.
diff --git a/README.md b/README.md
index 0cc97a8..efb83b3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# Castle
+# Wild PC
> Standing to author, run, govern, and maintain your own software.
-A personal software platform. Castle manages independent services, tools, and
+A personal software platform. Wild PC manages independent services, tools, and
frontends — and launches your coding agents — from a single CLI, with a unified
gateway, systemd integration, and a web dashboard.
@@ -18,27 +18,27 @@ and configurations of existing tools. Many of these programs do not need a publi
release process, an app store listing, or a conventional distribution channel. They
need a reliable place to run.
-Castle provides that place.
+Wild PC provides that place.
-Castle gives simple applications a consistent local environment for development,
+Wild PC gives simple applications a consistent local environment for development,
registration, deployment, discovery, and operation. Programs remain independent, but
-Castle provides the surrounding structure: process management, routing, metadata,
+Wild PC provides the surrounding structure: process management, routing, metadata,
service lifecycle, logs, and a common interface for running and inspecting the
software in your domain.
-In this sense, Castle is a **personal software estate**: a practical way to organize
+In this sense, Wild PC is a **personal software estate**: a practical way to organize
the software you create and run yourself, without requiring every tool to become a
fully packaged product.
## How it works
-Castle separates *what software exists* from *how it runs*:
+Wild PC separates *what software exists* from *how it runs*:
-- **Programs** — the catalog. A program is a source repo Castle knows how to work
+- **Programs** — the catalog. A program is a source repo Wild PC knows how to work
with (dev verbs, build) and where it lives. One file per program under
- `~/.castle/programs/.yaml`.
+ `~/.wildpc/programs/.yaml`.
- **Deployments** — how a program is realized on this node. One file per deployment
- under `~/.castle/deployments/.yaml`, discriminated by its **manager**:
+ under `~/.wildpc/deployments/.yaml`, discriminated by its **manager**:
| manager | what it is | derived **kind** |
|---------|------------|------------------|
@@ -55,33 +55,33 @@ 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 one honest step: `castle apply` renders systemd units and
+Standing everything up is one honest step: `wildpc 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.
+`wildpc apply`; `wildpc apply --plan` shows the diff first.
## Stacks
-Castle **stacks** are pre-configured development environments that provide starting
-points for building Castle programs. A stack can define the language, framework,
-dependencies, tools, conventions, and Castle integration needed for a particular
+Wild PC **stacks** are pre-configured development environments that provide starting
+points for building Wild PC programs. A stack can define the language, framework,
+dependencies, tools, conventions, and Wild PC integration needed for a particular
kind of application — `python-fastapi`, `python-cli`, `react-vite`, `supabase`.
Stacks are designed to work well with coding assistants. They give assistants a
-consistent target when generating Castle programs, making it easier to produce
+consistent target when generating Wild PC programs, making it easier to produce
applications that are correctly structured, configured, and ready to run under
-Castle. If your coding assistant understands Castle, it can help you create,
+Wild PC. If your coding assistant understands Wild PC, it can help you create,
register, manage, and evolve custom applications more efficiently.
A stack seeds a new program's scaffold and default dev-verb commands, but it's
optional at runtime: a program stands on its own via its declared `commands:` and
-`source:`, so you can adopt any existing repo with `castle program add` — no stack
+`source:`, so you can adopt any existing repo with `wildpc program add` — no stack
required.
## Agents
-Castle can launch your coding agents. Declare them in `castle.yaml` under `agents:`
-— each entry is just a command Castle runs in a terminal:
+Wild PC can launch your coding agents. Declare them in `wildpc.yaml` under `agents:`
+— each entry is just a command Wild PC runs in a terminal:
```yaml
agents:
@@ -96,7 +96,7 @@ agents:
The dashboard has a terminal dock that launches any declared agent in a
pseudo-terminal (over a WebSocket) and manages live sessions (list, resume, kill).
-Castle is **assistant-agnostic** — it only ever runs `command args` in a pty and
+Wild PC is **assistant-agnostic** — it only ever runs `command args` in a pty and
never parses the agent's output, so any interactive CLI works. With no `agents:`
block set, a sensible default set (`claude`, `opencode`, `amplifier`, …) is offered.
@@ -104,28 +104,28 @@ block set, a sensible default set (`claude`, `opencode`, `amplifier`, …) is of
**Prerequisites:** a Debian/Ubuntu-family Linux with `apt` and `sudo`, `systemd`
(user services), and `git`. `install.sh` sets up everything else — Docker, Caddy,
-`uv`, and the `castle` CLI itself.
+`uv`, and the `wildpc` CLI itself.
```bash
-git clone ~/castle && cd ~/castle
+git clone ~/wildpc && cd ~/wildpc
-# One command: installs uv + the castle CLI, sets up infra (Docker, Caddy, MQTT,
-# Postgres), creates ~/.castle, registers Castle's own control plane, builds the UI.
+# One command: installs uv + the wildpc CLI, sets up infra (Docker, Caddy, MQTT,
+# Postgres), creates ~/.wildpc, registers Wild PC's own control plane, builds the UI.
./install.sh
-castle apply # converge the runtime to config (units, routes, run)
-castle doctor # verify — every check should be green
+wildpc apply # converge the runtime to config (units, routes, run)
+wildpc doctor # verify — every check should be green
open http://localhost:9000 # the dashboard
```
-`castle doctor` is your friend at every step: it inspects setup *and* runtime and,
+`wildpc doctor` is your friend at every step: it inspects setup *and* runtime and,
for anything not green, prints the exact next command. Run it any time something
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 apply`, and `castle doctor` tells you what a rung still
+config change plus `wildpc apply`, and `wildpc doctor` tells you what a rung still
needs.
| Rung | You get | What it takes |
@@ -135,86 +135,86 @@ needs.
| **Public** | a chosen service reachable from the internet | `public: true` on the service + a Cloudflare tunnel. → [docs/tunnel-setup.md](docs/tunnel-setup.md) |
The jump to LAN HTTPS is the involved one (DNS + a token + binding `:443`). Set
-`gateway.tls: acme` and run `castle doctor` — it enumerates exactly the pieces that
+`gateway.tls: acme` and run `wildpc doctor` — it enumerates exactly the pieces that
are still missing, each with its fix.
## Creating programs
-`castle program create` scaffolds the source **and** its deployment from a stack:
+`wildpc program create` scaffolds the source **and** its deployment from a stack:
```bash
# 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 apply my-api # render unit + route, then start it
+wildpc program create my-api --stack python-fastapi --description "Does something"
+wildpc program test my-api
+wildpc 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 apply my-tool # installs its path deployment on PATH
+wildpc program create my-tool --stack python-cli --description "Does something"
+wildpc 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 apply
+wildpc program create my-app --stack react-vite --description "Web interface"
+wildpc program build my-app && wildpc apply
# Adopt an existing repo (no stack needed — dev verbs detected or declared)
-castle program add ~/projects/some-rust-tool
+wildpc 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. Lifecycle
-is convergence — `castle apply` — not a pile of per-kind verbs.
+is convergence — `wildpc apply` — not a pile of per-kind verbs.
```
# Programs — the software catalog
-castle program list|info|create|add|clone|delete|run
-castle program build|test|lint|type-check|check [name] # dev verbs
+wildpc program list|info|create|add|clone|delete|run
+wildpc 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|restart|logs
-castle job …same verbs; create takes --schedule
-castle tool list|info # CLIs on your PATH
+wildpc service list|info|create|delete|restart|logs
+wildpc job …same verbs; create takes --schedule
+wildpc 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 restart [name] # imperative bounce (one or all)
-castle gateway # status + route table (inspection)
+wildpc apply [name] [--plan] # converge runtime to config — the workhorse
+wildpc list [--kind K] [--stack S] [--json] # catalog + every deployment view
+wildpc status # unified runtime status
+wildpc doctor # diagnose setup + health, with fix hints
+wildpc restart [name] # imperative bounce (one or all)
+wildpc gateway # status + route table (inspection)
```
-To turn a deployment off, set `enabled: false` in its config and `castle apply` —
+To turn a deployment off, set `enabled: false` in its config and `wildpc 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
+`wildpc 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
description, and whether it's installed.
## Configuration
-The registry lives under `~/.castle/`: a global `castle.yaml` plus one file per
+The registry lives under `~/.wildpc/`: a global `wildpc.yaml` plus one file per
resource under `programs/` and `deployments/`.
```yaml
-# ~/.castle/castle.yaml — globals
+# ~/.wildpc/wildpc.yaml — globals
gateway:
port: 9000
-repo: /data/repos/castle # resolves `source: repo:` for castle's own programs
+repo: /data/repos/wildpc # resolves `source: repo:` for wildpc's own programs
```
```yaml
-# ~/.castle/programs/my-api.yaml — the catalog entry
+# ~/.wildpc/programs/my-api.yaml — the catalog entry
description: Does something useful
source: /data/repos/my-api
stack: python-fastapi
```
```yaml
-# ~/.castle/deployments/my-api.yaml — a systemd service (derived kind: service)
+# ~/.wildpc/deployments/my-api.yaml — a systemd service (derived kind: service)
program: my-api
manager: systemd
run: { launcher: python, program: my-api }
@@ -224,21 +224,21 @@ manage: { systemd: {} }
defaults:
env:
MY_API_PORT: ${port} # = expose.http.internal.port
- MY_API_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/my-api
+ MY_API_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/my-api
API_KEY: ${secret:MY_API_KEY}
```
`defaults.env` is the **single, explicit source** of a deployment's environment —
-Castle injects nothing implicitly. The placeholders `${port}`, `${data_dir}`,
+Wild PC injects nothing implicitly. The placeholders `${port}`, `${data_dir}`,
`${name}`, `${public_url}`, and `${secret:NAME}` map your program's own env var names
-to Castle's computed values. Secrets live in `~/.castle/secrets/` (never in a repo).
+to Wild PC's computed values. Secrets live in `~/.wildpc/secrets/` (never in a repo).
## Gateway, DNS & TLS
Every gateway-exposed deployment gets its own subdomain — `.`
— routed to it by the Caddy gateway (there are no path-prefix routes). Exposure is a
single checkbox: `proxy: true` on a service, while a static deployment is inherently
-served. The dashboard is `castle.` and the API `castle-api.`; on a
+served. The dashboard is `wildpc.` and the API `wildpc-api.`; on a
node with no domain, `:9000` serves the dashboard plus a `/api` proxy.
`gateway.tls` is a per-node choice: `off` (plain HTTP on `:9000`) or `acme` (a real
@@ -250,42 +250,42 @@ public internet is separately opt-in via a Cloudflare tunnel (`public: true`). S
## Layout
```
-~/.castle/ # instance: config, artifacts, secrets
- castle.yaml # globals (gateway, repo, agents)
+~/.wildpc/ # instance: config, artifacts, secrets
+ wildpc.yaml # globals (gateway, repo, agents)
programs/ deployments/ # one file per program / deployment
secrets/ # secret files (mode 700)
artifacts/specs/ # generated Caddyfile, registry.yaml
/data/repos// # your program source (absolute source:)
-/data/castle// # per-deployment data volume
-/ # castle itself: cli/ core/ castle-api/ app/ docs/
+/data/wildpc// # per-deployment data volume
+/ # wildpc itself: cli/ core/ wildpc-api/ app/ docs/
```
-**Independence principle:** your programs never depend on Castle. They accept
-configuration (data dir, port, URLs) via environment variables; only Castle's own
-programs (CLI, API, gateway) know Castle internals.
+**Independence principle:** your programs never depend on Wild PC. They accept
+configuration (data dir, port, URLs) via environment variables; only Wild PC's own
+programs (CLI, API, gateway) know Wild PC internals.
## Dashboard & API
-The **dashboard** (`app/`, served at `castle.` or `http://localhost:9000`)
+The **dashboard** (`app/`, served at `wildpc.` or `http://localhost:9000`)
lists programs and deployments, edits their config, drives lifecycle, shows the
gateway route table and logs, and hosts the agent terminal dock.
-**`castle-api`** (port 9020, proxied at `castle-api.`) is the control plane:
+**`wildpc-api`** (port 9020, proxied at `wildpc-api.`) is the control plane:
`/deployments`, `/programs`, `/services`, `/jobs`, `/gateway`, `/status`, an SSE
`/stream`, config editing under `/config/…`, and the agent session endpoints under
`/agents`. The full endpoint reference is in [CLAUDE.md](CLAUDE.md).
## Mesh (opt-in)
-Castle nodes can discover each other via MQTT + mDNS to form a personal
+Wild PC nodes can discover each other via MQTT + mDNS to form a personal
infrastructure mesh — the gateway can route to services on other nodes and the
dashboard shows discovered nodes and cross-node routes. It's all off by default;
-single-node needs none of it. Enable on `castle-api` via `CASTLE_API_MQTT_ENABLED`
-and `CASTLE_API_MDNS_ENABLED`.
+single-node needs none of it. Enable on `wildpc-api` via `WILDPC_API_MQTT_ENABLED`
+and `WILDPC_API_MDNS_ENABLED`.
## Docs
-- [docs/registry.md](docs/registry.md) — the registry model, `castle.yaml`, deployment fields, lifecycle
+- [docs/registry.md](docs/registry.md) — the registry model, `wildpc.yaml`, deployment fields, lifecycle
- [docs/dns-and-tls.md](docs/dns-and-tls.md) — gateway routing, DNS, the `off` / `acme` TLS modes
- [docs/stacks/](docs/stacks/) — per-stack guides (python-fastapi, python-cli, react-vite, supabase)
- [AGENTS.md](AGENTS.md) — the canonical, assistant-agnostic operator guide (recipes, gateway, tunnel, mesh)
diff --git a/app/.env b/app/.env
index d88544d..49ee1bb 100644
--- a/app/.env
+++ b/app/.env
@@ -1,4 +1,4 @@
# The API base URL is derived at runtime (see src/services/api/client.ts):
-# - dashboard at castle-app. -> castle-api. (cross-origin, CORS)
+# - dashboard at wildpc-app. -> wildpc-api. (cross-origin, CORS)
# - dev / off-mode :9000 (bare host) -> /api (same-origin)
# Set VITE_API_BASE_URL only to force a specific base (rarely needed).
diff --git a/app/index.html b/app/index.html
index e1ab651..9b8cf79 100644
--- a/app/index.html
+++ b/app/index.html
@@ -4,7 +4,7 @@
- Castle
+ Wild PC
diff --git a/app/src/components/AddProgramForm.tsx b/app/src/components/AddProgramForm.tsx
index 8f3427a..ce5aa87 100644
--- a/app/src/components/AddProgramForm.tsx
+++ b/app/src/components/AddProgramForm.tsx
@@ -21,7 +21,7 @@ function splitPath(raw: string): { dir: string | null; filter: string } {
return { dir: raw.slice(0, idx), filter: raw.slice(idx + 1) }
}
-/** Adopt an existing repo as a program (the web `castle program add`). Programs
+/** Adopt an existing repo as a program (the web `wildpc program add`). Programs
* live on the *server's* filesystem, so the picker browses the server's dirs: type
* a path (autocompletes as you go), navigate the listing, or paste a git URL. */
export function AddProgramForm({
@@ -181,7 +181,7 @@ export function AddProgramForm({
{target && (
Registering {target}
- {isGit && " (cloned later via castle clone)"}
+ {isGit && " (cloned later via wildpc clone)"}
)}
diff --git a/app/src/components/AssistantDock.tsx b/app/src/components/AssistantDock.tsx
index ac56103..19be366 100644
--- a/app/src/components/AssistantDock.tsx
+++ b/app/src/components/AssistantDock.tsx
@@ -133,7 +133,7 @@ export function AssistantDock() {
? "border-[var(--primary)] text-[var(--foreground)]"
: pillIdle,
)}
- title="Plain login shell in the castle repo"
+ title="Plain login shell in the wildpc repo"
>
Shell
diff --git a/app/src/components/CommandPalette.tsx b/app/src/components/CommandPalette.tsx
index fe3f72d..c73b51e 100644
--- a/app/src/components/CommandPalette.tsx
+++ b/app/src/components/CommandPalette.tsx
@@ -240,7 +240,7 @@ function PaletteBody({ onClose }: { onClose: () => void }) {
)}
{Object.entries(env).map(([key, val]) => (
diff --git a/app/src/lib/labels.ts b/app/src/lib/labels.ts
index 24feb2c..6bd14b0 100644
--- a/app/src/lib/labels.ts
+++ b/app/src/lib/labels.ts
@@ -82,7 +82,7 @@ export function detailPath(name: string, kind: string): string {
/**
* Full URL for a service exposed at .. The domain is
- * derived from the dashboard's own host (it is served at castle.), so
+ * derived from the dashboard's own host (it is served at wildpc.), so
* this returns null when the dashboard is on a bare host (off mode, no subdomains).
*/
export function subdomainUrl(subdomain: string): string | null {
diff --git a/app/src/pages/Overview.tsx b/app/src/pages/Overview.tsx
index 4a3c7c5..c8a2c00 100644
--- a/app/src/pages/Overview.tsx
+++ b/app/src/pages/Overview.tsx
@@ -78,7 +78,7 @@ export function Overview() {
return (
-
+
{nodes && }
diff --git a/app/src/pages/ProgramDetail.tsx b/app/src/pages/ProgramDetail.tsx
index f70ffa0..f67387d 100644
--- a/app/src/pages/ProgramDetail.tsx
+++ b/app/src/pages/ProgramDetail.tsx
@@ -93,7 +93,7 @@ export function ProgramDetailPage() {
Program Info
- Where the source lives and how castle works with it.
+ Where the source lives and how wildpc works with it.
{deployment.source && (
diff --git a/app/src/pages/ServiceDetail.tsx b/app/src/pages/ServiceDetail.tsx
index 53557fc..88b6135 100644
--- a/app/src/pages/ServiceDetail.tsx
+++ b/app/src/pages/ServiceDetail.tsx
@@ -15,7 +15,7 @@ export function ServiceDetailPage() {
const { data: deployment, isLoading, error, refetch } = useService(name ?? "")
const { data: statusResp } = useStatus()
const health = statusResp?.statuses.find((s) => s.id === name)
- const isGateway = name === "castle-gateway"
+ const isGateway = name === "wildpc-gateway"
const { data: caddyfile } = useCaddyfile(isGateway)
if (isLoading) {
diff --git a/app/src/pages/SystemMap.tsx b/app/src/pages/SystemMap.tsx
index f5cec6f..e79524b 100644
--- a/app/src/pages/SystemMap.tsx
+++ b/app/src/pages/SystemMap.tsx
@@ -84,7 +84,7 @@ const KIND_COLOR: Record = {
// Dependency edges are colored by the protocol of the thing being consumed (the
// target's endpoint). "system" = a socket-less dep (a tool/package) or a target
-// castle doesn't model an endpoint for.
+// wildpc doesn't model an endpoint for.
const PROTO_COLOR: Record = {
http: "#58a6ff",
pg: "#3b82f6",
@@ -100,7 +100,7 @@ const HANDLE = { opacity: 0, width: 8, height: 8 } as const
// User's manual node arrangement. Positions default to the computed lane layout;
// any node the user drags is remembered here and survives refetches + reloads.
-const POS_KEY = "castle-map-positions"
+const POS_KEY = "wildpc-map-positions"
type PosMap = Record
function loadPositions(): PosMap {
try {
@@ -295,7 +295,7 @@ function protoOf(url: string | null): string {
}
}
-// An external resource — a `reference` (manager: none) castle doesn't run. Sits in
+// An external resource — a `reference` (manager: none) wildpc doesn't run. Sits in
// the External zone as a registry entry + a drop target for consumes edges.
function ExternalNode({ data }: NodeProps) {
const d = data as { label: string; host: string; focusDim?: boolean; focused?: boolean }
@@ -325,7 +325,7 @@ function ExternalNode({ data }: NodeProps) {
)
}
-// A deployment on another castle node (mesh-discovered). Read-only — you manage
+// A deployment on another wildpc node (mesh-discovered). Read-only — you manage
// remote deployments from that node.
function RemoteNode({ data }: NodeProps) {
const d = data as {
@@ -1336,7 +1336,7 @@ export function SystemMapPage() {
open={!!confirmDel}
danger
title={`Delete ${confirmDel?.name}?`}
- body={`This removes the ${confirmDel?.kind} deployment from castle.yaml and tears it down. The program's source is kept.`}
+ body={`This removes the ${confirmDel?.kind} deployment from wildpc.yaml and tears it down. The program's source is kept.`}
confirmLabel="Delete"
onConfirm={doDelete}
onCancel={() => setConfirmDel(null)}
@@ -1345,7 +1345,7 @@ export function SystemMapPage() {
)
}
-const LEGEND_KEY = "castle-map-legend-open"
+const LEGEND_KEY = "wildpc-map-legend-open"
function Legend() {
const [open, setOpen] = useState(() => {
@@ -1461,7 +1461,7 @@ function InspectPanel({
)}
- onOpen(info.name, info.kind, info.node)} title="Castle details" className="text-[var(--muted)] hover:text-[var(--card-foreground)]">
+ onOpen(info.name, info.kind, info.node)} title="Wild PC details" className="text-[var(--muted)] hover:text-[var(--card-foreground)]">
@@ -1593,7 +1593,7 @@ function AddExternalModal({
>
Add external resource
- An endpoint castle doesn't run (a SaaS API, a remote service). Draw a consumes edge to
+ An endpoint wildpc doesn't run (a SaaS API, a remote service). Draw a consumes edge to
it from any deployment.
diff --git a/app/src/services/api/client.ts b/app/src/services/api/client.ts
index 5df715d..1ef9dba 100644
--- a/app/src/services/api/client.ts
+++ b/app/src/services/api/client.ts
@@ -1,6 +1,6 @@
-// Resolve the castle-api base URL. The gateway serves each service at its own
-// subdomain (.), so when the dashboard runs at castle.
-// the API lives at castle-api. — a cross-origin call (castle-api allows
+// Resolve the wildpc-api base URL. The gateway serves each service at its own
+// subdomain (.), so when the dashboard runs at wildpc.
+// the API lives at wildpc-api. — a cross-origin call (wildpc-api allows
// CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the
// API is reachable same-origin at /api.
function resolveApiBase(): string {
@@ -10,7 +10,7 @@ function resolveApiBase(): string {
const { protocol, hostname } = window.location
const labels = hostname.split(".")
if (labels.length > 2) {
- return `${protocol}//castle-api.${labels.slice(1).join(".")}`
+ return `${protocol}//wildpc-api.${labels.slice(1).join(".")}`
}
}
return "/api"
@@ -78,7 +78,7 @@ class ApiClient {
}
// Build a ws(s):// URL for a WebSocket endpoint. Handles both the cross-origin
- // base (https://castle-api.) and the same-origin "/api" base.
+ // base (https://wildpc-api.) and the same-origin "/api" base.
wsUrl(path: string): string {
const absolute = this.baseUrl.startsWith("http")
? `${this.baseUrl}${path}`
diff --git a/app/src/services/api/hooks.ts b/app/src/services/api/hooks.ts
index 94d49d1..44ea2dc 100644
--- a/app/src/services/api/hooks.ts
+++ b/app/src/services/api/hooks.ts
@@ -59,7 +59,7 @@ export function useJobs() {
})
}
-// The stacks castle has handlers for — authoritative source for the program
+// The stacks wildpc has handlers for — authoritative source for the program
// stack select, so a new backend stack appears without a frontend change.
export function useStacks() {
return useQuery({
@@ -260,7 +260,7 @@ const REACH_SECTION: Record = {
}
// Create/update an external resource — a `reference` deployment (manager: none)
-// that points at an endpoint castle doesn't run (a SaaS API, a remote service).
+// that points at an endpoint wildpc doesn't run (a SaaS API, a remote service).
// Behind the System Map's "add external resource" authoring. No apply needed —
// a reference has no runtime unit; it just declares the endpoint.
export function useSaveReference() {
@@ -314,7 +314,7 @@ export function useSetReach() {
})
}
-// Delete a deployment from castle.yaml (the kind-scoped removal; keeps the
+// Delete a deployment from wildpc.yaml (the kind-scoped removal; keeps the
// program). Behind the System Map's node deletion.
export function useDeleteDeployment() {
const qc = useQueryClient()
@@ -450,7 +450,7 @@ export interface AdoptResult {
commands: string[]
is_git_url: boolean
}
-// Adopt an existing repo as a program (the web `castle program add`). Refreshes
+// Adopt an existing repo as a program (the web `wildpc program add`). Refreshes
// the catalog + the derived graph/repo views.
export function useAdoptProgram() {
const qc = useQueryClient()
@@ -508,7 +508,7 @@ export function useSuggestions() {
})
}
-// Deployments on other (mesh-discovered) castle nodes, for the multi-node map.
+// Deployments on other (mesh-discovered) wildpc nodes, for the multi-node map.
export function useMeshDeployments() {
return useQuery({
queryKey: ["mesh", "deployments"],
diff --git a/app/src/types/index.ts b/app/src/types/index.ts
index f7981bf..a7d991c 100644
--- a/app/src/types/index.ts
+++ b/app/src/types/index.ts
@@ -163,7 +163,7 @@ export interface GraphSuggestion {
protocol: string
}
-// GET /mesh/deployments — deployments on other (mesh-discovered) castle nodes.
+// GET /mesh/deployments — deployments on other (mesh-discovered) wildpc nodes.
export interface MeshDeployment {
name: string
kind: string
@@ -311,7 +311,7 @@ export interface SSEServiceActionEvent {
export interface NodeSummary {
hostname: string
gateway_port: number
- gateway_domain: string | null // acme domain → dashboard at castle.
+ gateway_domain: string | null // acme domain → dashboard at wildpc.
deployed_count: number
service_count: number
is_local: boolean
diff --git a/app/vite.config.ts b/app/vite.config.ts
index 5c8916e..ac697ad 100644
--- a/app/vite.config.ts
+++ b/app/vite.config.ts
@@ -4,8 +4,8 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
- // Castle sets VITE_BASE to the gateway serve prefix at build time so absolute
- // asset URLs resolve at the subpath (castle-app is the root app → "/").
+ // Wild PC sets VITE_BASE to the gateway serve prefix at build time so absolute
+ // asset URLs resolve at the subpath (wildpc-app is the root app → "/").
base: process.env.VITE_BASE ?? "/",
plugins: [react(), tailwindcss()],
resolve: {
diff --git a/bootstrap/deployments/services/castle-api.yaml b/bootstrap/deployments/services/wildpc-api.yaml
similarity index 55%
rename from bootstrap/deployments/services/castle-api.yaml
rename to bootstrap/deployments/services/wildpc-api.yaml
index ed4cda8..db1f7c8 100644
--- a/bootstrap/deployments/services/castle-api.yaml
+++ b/bootstrap/deployments/services/wildpc-api.yaml
@@ -1,9 +1,9 @@
-program: castle-api
-description: Castle API
+program: wildpc-api
+description: Wild PC API
manager: systemd
run:
launcher: python
- program: castle-api
+ program: wildpc-api
expose:
http:
internal:
@@ -14,5 +14,5 @@ manage:
systemd: {}
defaults:
env:
- CASTLE_API_PORT: ${port}
- CASTLE_API_DATA_DIR: ${data_dir}
+ WILDPC_API_PORT: ${port}
+ WILDPC_API_DATA_DIR: ${data_dir}
diff --git a/bootstrap/deployments/services/castle-gateway.yaml b/bootstrap/deployments/services/wildpc-gateway.yaml
similarity index 88%
rename from bootstrap/deployments/services/castle-gateway.yaml
rename to bootstrap/deployments/services/wildpc-gateway.yaml
index 06d2041..9c922e0 100644
--- a/bootstrap/deployments/services/castle-gateway.yaml
+++ b/bootstrap/deployments/services/wildpc-gateway.yaml
@@ -1,4 +1,4 @@
-# The Caddy gateway. `__SPECS_DIR__` is substituted with $CASTLE_HOME/artifacts/specs
+# The Caddy gateway. `__SPECS_DIR__` is substituted with $WILDPC_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
diff --git a/bootstrap/deployments/statics/castle.yaml b/bootstrap/deployments/statics/castle.yaml
deleted file mode 100644
index d0d0550..0000000
--- a/bootstrap/deployments/statics/castle.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-program: castle
-description: Castle web app
-manager: caddy
diff --git a/bootstrap/deployments/statics/wildpc.yaml b/bootstrap/deployments/statics/wildpc.yaml
new file mode 100644
index 0000000..5dbe026
--- /dev/null
+++ b/bootstrap/deployments/statics/wildpc.yaml
@@ -0,0 +1,3 @@
+program: wildpc
+description: Wild PC web app
+manager: caddy
diff --git a/bootstrap/programs/castle-api.yaml b/bootstrap/programs/castle-api.yaml
deleted file mode 100644
index ae9a404..0000000
--- a/bootstrap/programs/castle-api.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-description: Castle API
-source: repo:castle-api
-stack: python-fastapi
diff --git a/bootstrap/programs/wildpc-api.yaml b/bootstrap/programs/wildpc-api.yaml
new file mode 100644
index 0000000..697ceb3
--- /dev/null
+++ b/bootstrap/programs/wildpc-api.yaml
@@ -0,0 +1,3 @@
+description: Wild PC API
+source: repo:wildpc-api
+stack: python-fastapi
diff --git a/bootstrap/programs/castle.yaml b/bootstrap/programs/wildpc.yaml
similarity index 68%
rename from bootstrap/programs/castle.yaml
rename to bootstrap/programs/wildpc.yaml
index 7aef44e..f925ee4 100644
--- a/bootstrap/programs/castle.yaml
+++ b/bootstrap/programs/wildpc.yaml
@@ -1,4 +1,4 @@
-description: Castle web app
+description: Wild PC web app
source: repo:app
stack: react-vite
build:
diff --git a/castle-api/CLAUDE.md b/castle-api/CLAUDE.md
deleted file mode 100644
index 81b321e..0000000
--- a/castle-api/CLAUDE.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working
-with code in this repository.
-
-## Overview
-
-castle-api is a FastAPI service. Castle API.
-
-## Commands
-
-```bash
-uv sync # Install dependencies
-uv run castle-api # Run service (port 9020)
-uv run pytest tests/ -v # Run tests
-uv run ruff check . # Lint
-uv run ruff format . # Format
-```
-
-## Architecture
-
-- `src/castle_api/config.py` — Settings via pydantic-settings, env prefix `CASTLE_API_`
-- `src/castle_api/main.py` — FastAPI app, lifespan, health endpoint
-- `tests/` — pytest with TestClient fixtures
-
-## Configuration
-
-Environment variables with `CASTLE_API_` prefix:
-- `CASTLE_API_DATA_DIR` — Data directory (default: ./data)
-- `CASTLE_API_HOST` — Bind host (default: 0.0.0.0)
-- `CASTLE_API_PORT` — Port (default: 9020)
diff --git a/castle-api/uv.lock b/castle-api/uv.lock
deleted file mode 100644
index 07cd083..0000000
--- a/castle-api/uv.lock
+++ /dev/null
@@ -1,487 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.13"
-
-[[package]]
-name = "annotated-doc"
-version = "0.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.12.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
-]
-
-[[package]]
-name = "castle-api"
-version = "0.1.0"
-source = { editable = "." }
-dependencies = [
- { name = "castle-cli" },
- { name = "fastapi" },
- { name = "httpx" },
- { name = "pydantic-settings" },
- { name = "uvicorn" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "httpx" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pyyaml" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "castle-cli", editable = "../cli" },
- { name = "fastapi", specifier = ">=0.115.0" },
- { name = "httpx", specifier = ">=0.27.0" },
- { name = "pydantic-settings", specifier = ">=2.0.0" },
- { name = "uvicorn", specifier = ">=0.34.0" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "httpx", specifier = ">=0.27.0" },
- { name = "pytest", specifier = ">=7.0.0" },
- { name = "pytest-asyncio", specifier = ">=0.23.0" },
- { name = "pyyaml", specifier = ">=6.0.0" },
-]
-
-[[package]]
-name = "castle-cli"
-version = "0.1.0"
-source = { editable = "../cli" }
-dependencies = [
- { name = "jinja2" },
- { name = "pydantic" },
- { name = "pyyaml" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "jinja2", specifier = ">=3.1.0" },
- { name = "pydantic", specifier = ">=2.0.0" },
- { name = "pyyaml", specifier = ">=6.0.0" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "pyright", specifier = ">=1.1.0" },
- { name = "pytest", specifier = ">=7.0.0" },
- { name = "ruff", specifier = ">=0.11.0" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.1.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
-]
-
-[[package]]
-name = "click"
-version = "8.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "fastapi"
-version = "0.129.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-doc" },
- { name = "pydantic" },
- { name = "starlette" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/48/47/75f6bea02e797abff1bca968d5997793898032d9923c1935ae2efdece642/fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af", size = 375450, upload-time = "2026-02-12T13:54:52.541Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
- { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
- { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
- { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
- { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
- { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
- { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
- { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
- { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
- { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
- { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
- { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
- { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
- { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
- { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
- { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
- { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
- { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
- { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
- { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
- { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
- { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
- { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
- { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
- { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
- { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
- { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
- { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
- { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
- { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
- { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
- { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
- { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
- { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
- { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
- { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
- { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.12.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.41.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
-]
-
-[[package]]
-name = "pydantic-settings"
-version = "2.13.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
- { name = "python-dotenv" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
-]
-
-[[package]]
-name = "pytest-asyncio"
-version = "1.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
-]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
- { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
- { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
- { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
- { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
- { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
- { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
- { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
- { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
- { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
- { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
-]
-
-[[package]]
-name = "starlette"
-version = "0.52.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
-]
-
-[[package]]
-name = "uvicorn"
-version = "0.41.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
-]
diff --git a/cli/pyproject.toml b/cli/pyproject.toml
index 6f368ac..b245005 100644
--- a/cli/pyproject.toml
+++ b/cli/pyproject.toml
@@ -1,27 +1,27 @@
[project]
-name = "castle-cli"
+name = "wildpc-cli"
version = "0.1.0"
-description = "Castle platform CLI - manage projects, services, and infrastructure"
+description = "Wild PC platform CLI - manage projects, services, and infrastructure"
requires-python = ">=3.11"
dependencies = [
"pyyaml>=6.0.0",
"jinja2>=3.1.0",
"pydantic>=2.0.0",
- "castle-core",
+ "wildpc-core",
]
[tool.uv.sources]
-castle-core = { workspace = true }
+wildpc-core = { workspace = true }
[project.scripts]
-castle = "castle_cli.main:main"
+wildpc = "wildpc_cli.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
-packages = ["src/castle_cli"]
+packages = ["src/wildpc_cli"]
[dependency-groups]
dev = [
diff --git a/cli/src/castle_cli/__init__.py b/cli/src/castle_cli/__init__.py
deleted file mode 100644
index 0851bbd..0000000
--- a/cli/src/castle_cli/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Castle CLI - manage projects, services, and infrastructure."""
-
-__version__ = "0.1.0"
diff --git a/cli/src/wildpc_cli/__init__.py b/cli/src/wildpc_cli/__init__.py
new file mode 100644
index 0000000..a936db3
--- /dev/null
+++ b/cli/src/wildpc_cli/__init__.py
@@ -0,0 +1,3 @@
+"""Wild PC CLI - manage projects, services, and infrastructure."""
+
+__version__ = "0.1.0"
diff --git a/castle-api/tests/__init__.py b/cli/src/wildpc_cli/commands/__init__.py
similarity index 100%
rename from castle-api/tests/__init__.py
rename to cli/src/wildpc_cli/commands/__init__.py
diff --git a/cli/src/castle_cli/commands/add.py b/cli/src/wildpc_cli/commands/add.py
similarity index 70%
rename from cli/src/castle_cli/commands/add.py
rename to cli/src/wildpc_cli/commands/add.py
index 427f186..dd447f4 100644
--- a/cli/src/castle_cli/commands/add.py
+++ b/cli/src/wildpc_cli/commands/add.py
@@ -1,10 +1,10 @@
-"""castle program add — adopt an existing repo as a program (no scaffolding).
+"""wildpc program add — adopt an existing repo as a program (no scaffolding).
-`castle program create` makes new code from a stack. `castle program add` adopts code that
+`wildpc program create` makes new code from a stack. `wildpc program add` adopts code that
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-wildpc project becomes usable without writing them by hand.
-The adopt logic itself lives in ``castle_core.adopt`` so the CLI and the API
+The adopt logic itself lives in ``wildpc_core.adopt`` so the CLI and the API
(`POST /programs/adopt`, the dashboard's "Add program") behave identically.
"""
@@ -12,9 +12,9 @@ from __future__ import annotations
import argparse
-from castle_core.adopt import AdoptError, build_adopted_program
+from wildpc_core.adopt import AdoptError, build_adopted_program
-from castle_cli.config import load_config, save_config
+from wildpc_cli.config import load_config, save_config
def run_add(args: argparse.Namespace) -> int:
@@ -35,11 +35,11 @@ def run_add(args: argparse.Namespace) -> int:
print(f"Adopted '{adopted.name}' as a program.")
print(f" source: {adopted.source}")
if adopted.repo:
- print(f" repo: {adopted.repo} (run 'castle clone {adopted.name}' to fetch it)")
+ print(f" repo: {adopted.repo} (run 'wildpc clone {adopted.name}' to fetch it)")
if adopted.stack:
print(f" stack: {adopted.stack} (verbs inherited from stack defaults)")
elif adopted.commands:
print(f" commands detected: {', '.join(adopted.commands)}")
else:
- print(" no stack/commands detected — declare verbs in castle.yaml as needed")
+ print(" no stack/commands detected — declare verbs in wildpc.yaml as needed")
return 0
diff --git a/cli/src/castle_cli/commands/apply.py b/cli/src/wildpc_cli/commands/apply.py
similarity index 96%
rename from cli/src/castle_cli/commands/apply.py
rename to cli/src/wildpc_cli/commands/apply.py
index 60ee04a..e07876a 100644
--- a/cli/src/castle_cli/commands/apply.py
+++ b/cli/src/wildpc_cli/commands/apply.py
@@ -1,4 +1,4 @@
-"""castle apply — converge the running system to match config.
+"""wildpc apply — converge the running system to match config.
The one workhorse verb: renders systemd units + the Caddyfile + tunnel config,
then reconciles the runtime (activate what's enabled and down, restart what
@@ -28,7 +28,7 @@ def _line(verb_color: str, verb: str, names: list[str]) -> None:
def run_apply(args: argparse.Namespace) -> int:
- from castle_core.deploy import apply
+ from wildpc_core.deploy import apply
target = getattr(args, "name", None)
plan = getattr(args, "plan", False)
diff --git a/cli/src/castle_cli/commands/clone.py b/cli/src/wildpc_cli/commands/clone.py
similarity index 95%
rename from cli/src/castle_cli/commands/clone.py
rename to cli/src/wildpc_cli/commands/clone.py
index a9ab344..f8ac8ae 100644
--- a/cli/src/castle_cli/commands/clone.py
+++ b/cli/src/wildpc_cli/commands/clone.py
@@ -1,4 +1,4 @@
-"""castle clone — clone source for programs that declare a `repo:` URL.
+"""wildpc clone — clone source for programs that declare a `repo:` URL.
Used to provision a fresh machine: every program with a `repo:` and a missing
local `source:` gets cloned. A program whose source already exists is skipped.
@@ -10,7 +10,7 @@ import argparse
import subprocess
from pathlib import Path
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
def _clone_one(name: str, repo: str, source: str | None, ref: str | None, repos_dir: Path) -> bool:
diff --git a/cli/src/castle_cli/commands/create.py b/cli/src/wildpc_cli/commands/create.py
similarity index 89%
rename from cli/src/castle_cli/commands/create.py
rename to cli/src/wildpc_cli/commands/create.py
index 957dfbc..bc88f41 100644
--- a/cli/src/castle_cli/commands/create.py
+++ b/cli/src/wildpc_cli/commands/create.py
@@ -1,12 +1,12 @@
-"""castle program create — scaffold a new program from templates."""
+"""wildpc program create — scaffold a new program from templates."""
from __future__ import annotations
import argparse
import subprocess
-from castle_cli.config import load_config, save_config
-from castle_cli.manifest import (
+from wildpc_cli.config import load_config, save_config
+from wildpc_cli.manifest import (
BuildSpec,
CaddyDeployment,
DefaultsSpec,
@@ -21,7 +21,7 @@ from castle_cli.manifest import (
SystemdDeployment,
SystemdSpec,
)
-from castle_cli.templates.scaffold import scaffold_project
+from wildpc_cli.templates.scaffold import scaffold_project
# Stack determines the default deployment kind + scaffold template.
STACK_DEFAULTS: dict[str, str] = {
@@ -79,7 +79,7 @@ def run_create(args: argparse.Namespace) -> int:
kind = STACK_DEFAULTS.get(stack) if stack else None
if name in config.programs or config.deployments_named(name):
- print(f"Error: '{name}' already exists in castle.yaml")
+ print(f"Error: '{name}' already exists in wildpc.yaml")
return 1
config.repos_dir.mkdir(parents=True, exist_ok=True)
@@ -94,7 +94,7 @@ def run_create(args: argparse.Namespace) -> int:
port = next_available_port(config)
package_name = name.replace("-", "_")
- description = args.description or (f"A castle {stack} program" if stack else f"{name}")
+ description = args.description or (f"A wildpc {stack} program" if stack else f"{name}")
if stack:
scaffold_project(
@@ -168,7 +168,7 @@ def run_create(args: argparse.Namespace) -> int:
requires=seeded_requires,
proxy=True, # expose at .
manage=ManageSpec(systemd=SystemdSpec()),
- # python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's
+ # python-fastapi scaffold reads env_prefix MY_SERVICE_ — map wildpc's
# computed port/data dir to those vars (explicit, no hidden injection).
defaults=DefaultsSpec(
env={f"{prefix}_PORT": "${port}", f"{prefix}_DATA_DIR": "${data_dir}"}
@@ -181,24 +181,24 @@ def run_create(args: argparse.Namespace) -> int:
print(f"Created {label} '{name}' at {project_dir}")
if port:
print(f" Port: {port}")
- print(" Registered in castle.yaml")
+ print(" Registered in wildpc.yaml")
print("\nNext steps:")
print(f" cd {project_dir}")
if stack == "supabase":
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
- print(f" castle program build {name} # apply migrations to the substrate")
- print(f" castle apply # serve at {name}.")
+ print(f" wildpc program build {name} # apply migrations to the substrate")
+ print(f" wildpc apply # serve at {name}.")
elif stack == "hugo":
print(" # edit content/, layouts/ (or add a theme under themes/)")
- print(f" castle program build {name} # hugo --gc --minify -> public/")
- print(f" castle apply {name} # serve at {name}.")
+ print(f" wildpc program build {name} # hugo --gc --minify -> public/")
+ print(f" wildpc apply {name} # serve at {name}.")
elif stack:
print(" uv sync")
if kind == "service":
print(f" uv run {name} # starts on port {port}")
- print(f" castle apply {name}")
- print(f" castle test {name}")
+ print(f" wildpc apply {name}")
+ print(f" wildpc test {name}")
else:
- print(" # add code, then declare commands: in castle.yaml")
+ print(" # add code, then declare commands: in wildpc.yaml")
return 0
diff --git a/cli/src/castle_cli/commands/delete.py b/cli/src/wildpc_cli/commands/delete.py
similarity index 92%
rename from cli/src/castle_cli/commands/delete.py
rename to cli/src/wildpc_cli/commands/delete.py
index 8050d0f..8d4aab0 100644
--- a/cli/src/castle_cli/commands/delete.py
+++ b/cli/src/wildpc_cli/commands/delete.py
@@ -1,4 +1,4 @@
-"""castle delete — remove a program/deployment from the registry AND tear it down.
+"""wildpc delete — remove a program/deployment from the registry AND tear it down.
Deleting cascades: a program's referencing deployments are taken offline (stop +
disable a service/job, uninstall a tool from PATH, drop a static route) before the
@@ -18,7 +18,7 @@ import argparse
import shutil
from pathlib import Path
-from castle_cli.config import load_config, save_config
+from wildpc_cli.config import load_config, save_config
def run_delete(args: argparse.Namespace) -> int:
@@ -35,7 +35,7 @@ def run_delete(args: argparse.Namespace) -> int:
in_deployment = bool(named) and resource in _DEPLOY_RESOURCES
if not (in_programs or in_deployment):
where = f" {resource}" if resource else ""
- print(f"Error: no{where} '{name}' in castle.yaml")
+ print(f"Error: no{where} '{name}' in wildpc.yaml")
return 1
where = [
@@ -60,7 +60,7 @@ def run_delete(args: argparse.Namespace) -> int:
# entry itself — its stack may own persistent data (a DB schema) that survives
# a code delete unless --purge-data drops it via the stack's `teardown`.
public_hosts = _public_hosts(config, deployments_to_remove)
- from castle_core.stacks import get_handler
+ from wildpc_core.stacks import get_handler
program_spec = config.programs.get(name) if in_programs else None
stack = program_spec.stack if program_spec else None
@@ -73,7 +73,7 @@ def run_delete(args: argparse.Namespace) -> int:
source_dir = Path(config.programs[name].source)
purge_data = getattr(args, "purge_data", False)
- print(f"Will remove '{name}' from castle.yaml ({', '.join(where)}).")
+ print(f"Will remove '{name}' from wildpc.yaml ({', '.join(where)}).")
if deployments_to_remove:
print(
"Will tear down deployment(s): "
@@ -100,7 +100,7 @@ def run_delete(args: argparse.Namespace) -> int:
if deployments_to_remove:
import asyncio
- from castle_core.lifecycle import deactivate
+ from wildpc_core.lifecycle import deactivate
for kind, d in deployments_to_remove:
try:
@@ -114,18 +114,18 @@ def run_delete(args: argparse.Namespace) -> int:
if in_programs:
del config.programs[name]
save_config(config)
- print(f"Removed '{name}' from castle.yaml ({', '.join(where)}).")
+ print(f"Removed '{name}' from wildpc.yaml ({', '.join(where)}).")
# Converge the runtime: prune orphan units, regenerate the Caddyfile (dropping
# the static route), reload the gateway + tunnel.
if deployments_to_remove:
- from castle_core.deploy import deploy
+ from wildpc_core.deploy import deploy
try:
deploy()
- print("Reconciled runtime (castle apply).")
+ print("Reconciled runtime (wildpc apply).")
except Exception as e:
- print(f"warning: reconcile failed — run 'castle apply': {e}")
+ print(f"warning: reconcile failed — run 'wildpc apply': {e}")
# Optional: delete the source directory.
if args.source and source_dir:
diff --git a/cli/src/castle_cli/commands/deploy_create.py b/cli/src/wildpc_cli/commands/deploy_create.py
similarity index 89%
rename from cli/src/castle_cli/commands/deploy_create.py
rename to cli/src/wildpc_cli/commands/deploy_create.py
index 7894df5..b47ca6e 100644
--- a/cli/src/castle_cli/commands/deploy_create.py
+++ b/cli/src/wildpc_cli/commands/deploy_create.py
@@ -1,6 +1,6 @@
-"""castle service create / castle job create — declare a deployment.
+"""wildpc service create / wildpc job create — declare a deployment.
-A service or job can run anything (a castle program or not). `--program`
+A service or job can run anything (a wildpc program or not). `--program`
records a convenience reference for description fallthrough; the run target is
the console script (python) or argv (command) to execute.
"""
@@ -9,8 +9,8 @@ from __future__ import annotations
import argparse
-from castle_cli.config import load_config, save_config
-from castle_cli.manifest import (
+from wildpc_cli.config import load_config, save_config
+from wildpc_cli.manifest import (
DefaultsSpec,
ExposeSpec,
HttpExposeSpec,
@@ -49,7 +49,7 @@ def _check_new(config: object, name: str, label: str) -> str | None:
def run_service_create(args: argparse.Namespace) -> int:
- """Create a service entry in castle.yaml."""
+ """Create a service entry in wildpc.yaml."""
config = load_config()
name = args.name
if err := _check_new(config, name, "service"):
@@ -89,12 +89,12 @@ def run_service_create(args: argparse.Namespace) -> int:
print(f" port: {args.port}")
if reach != Reach.OFF:
print(f" subdomain: {name}.")
- print(f"\nNext: castle apply {name}")
+ print(f"\nNext: wildpc apply {name}")
return 0
def run_job_create(args: argparse.Namespace) -> int:
- """Create a job entry in castle.yaml."""
+ """Create a job entry in wildpc.yaml."""
config = load_config()
name = args.name
if err := _check_new(config, name, "job"):
@@ -119,5 +119,5 @@ def run_job_create(args: argparse.Namespace) -> int:
print(f"Created job '{name}'.")
print(f" runs: {args.launcher} ({args.run or args.program or name})")
print(f" schedule: {args.schedule}")
- print(f"\nNext: castle apply {name}")
+ print(f"\nNext: wildpc apply {name}")
return 0
diff --git a/cli/src/castle_cli/commands/dev.py b/cli/src/wildpc_cli/commands/dev.py
similarity index 88%
rename from cli/src/castle_cli/commands/dev.py
rename to cli/src/wildpc_cli/commands/dev.py
index 8737581..4a07119 100644
--- a/cli/src/castle_cli/commands/dev.py
+++ b/cli/src/wildpc_cli/commands/dev.py
@@ -1,4 +1,4 @@
-"""castle build/test/lint/type-check/check/run/install/uninstall — dev verbs.
+"""wildpc build/test/lint/type-check/check/run/install/uninstall — dev verbs.
Verbs resolve per-program: a declared command (manifest `commands:` / `build:`)
overrides the stack default, falling back to the stack handler, else unavailable.
@@ -9,12 +9,12 @@ from __future__ import annotations
import argparse
import asyncio
-from castle_core.stacks import is_available, run_action
+from wildpc_core.stacks import is_available, run_action
-from castle_cli.config import CastleConfig, load_config
+from wildpc_cli.config import WildpcConfig, load_config
-def _run_verb(config: CastleConfig, project_name: str, verb: str) -> bool:
+def _run_verb(config: WildpcConfig, project_name: str, verb: str) -> bool:
"""Run a verb for a single program. Returns True on success."""
if project_name not in config.programs:
print(f"Unknown program: {project_name}")
@@ -39,7 +39,7 @@ def _run_verb(config: CastleConfig, project_name: str, verb: str) -> bool:
return result.status == "ok"
-def _run_verb_all(config: CastleConfig, verb: str) -> bool:
+def _run_verb_all(config: WildpcConfig, verb: str) -> bool:
"""Run a verb across every program that supports it. Returns True if all pass."""
all_passed = True
ran_any = False
@@ -89,10 +89,10 @@ def run_check(args: argparse.Namespace) -> int:
return run_verb(args, "check")
-def _lifecycle(config: CastleConfig, name: str, deactivate: bool) -> bool:
+def _lifecycle(config: WildpcConfig, name: str, deactivate: bool) -> bool:
"""Activate or deactivate a single program. Returns True on success."""
- from castle_core.lifecycle import activate
- from castle_core.lifecycle import deactivate as do_deactivate
+ from wildpc_core.lifecycle import activate
+ from wildpc_core.lifecycle import deactivate as do_deactivate
if name not in config.programs and name not in config.services and name not in config.jobs:
print(f"Unknown program: {name}")
diff --git a/cli/src/castle_cli/commands/doctor.py b/cli/src/wildpc_cli/commands/doctor.py
similarity index 86%
rename from cli/src/castle_cli/commands/doctor.py
rename to cli/src/wildpc_cli/commands/doctor.py
index a7bfda2..8cf3ca4 100644
--- a/cli/src/castle_cli/commands/doctor.py
+++ b/cli/src/wildpc_cli/commands/doctor.py
@@ -1,4 +1,4 @@
-"""castle doctor — diagnose whether this node is set up and running.
+"""wildpc doctor — diagnose whether this node is set up and running.
Read-only. It answers the question the runtime status view can't: *is this node
correctly configured, and if not, what's the exact next command?* Runs a series
@@ -6,7 +6,7 @@ of checks grouped into Environment, Configuration, Runtime, and TLS & exposure;
each check reports ok / warn / fail with a one-line hint when action is needed.
Exit code: 0 when nothing FAILed (warnings are allowed), 1 otherwise — so it
-doubles as a scriptable smoke test after `./install.sh` or `castle apply`.
+doubles as a scriptable smoke test after `./install.sh` or `wildpc apply`.
"""
from __future__ import annotations
@@ -26,9 +26,9 @@ _ICON = {
FAIL: "\033[31m✗\033[0m",
}
-_GATEWAY = "castle-gateway"
-_API = "castle-api"
-_DASHBOARD = "castle"
+_GATEWAY = "wildpc-gateway"
+_API = "wildpc-api"
+_DASHBOARD = "wildpc"
@dataclass
@@ -62,13 +62,13 @@ def _port_open(port: int, host: str = "127.0.0.1") -> bool:
def _check_environment() -> list[Check]:
checks: list[Check] = []
- if shutil.which("castle"):
- checks.append(Check(OK, "castle CLI on PATH"))
+ if shutil.which("wildpc"):
+ checks.append(Check(OK, "wildpc CLI on PATH"))
else:
checks.append(
Check(
WARN,
- "castle CLI not on PATH",
+ "wildpc CLI not on PATH",
hint="ensure ~/.local/bin is on PATH (uv tool install target)",
)
)
@@ -119,7 +119,7 @@ def _check_configuration(config) -> list[Check]:
checks.append(
Check(
OK,
- "castle.yaml loaded",
+ "wildpc.yaml loaded",
detail=f"gateway :{gw.port}, tls={tls}"
+ (f", domain={gw.domain}" if gw.domain else ""),
)
@@ -131,9 +131,9 @@ def _check_configuration(config) -> list[Check]:
checks.append(
Check(
FAIL,
- "repo: not set in castle.yaml",
- detail="source: repo: cannot resolve castle's own programs",
- hint="add 'repo: ' to ~/.castle/castle.yaml",
+ "repo: not set in wildpc.yaml",
+ detail="source: repo: cannot resolve wildpc's own programs",
+ hint="add 'repo: ' to ~/.wildpc/wildpc.yaml",
)
)
@@ -148,22 +148,22 @@ def _check_configuration(config) -> list[Check]:
FAIL,
"data dir missing or not writable",
detail=str(ddir),
- hint=f"set data_dir: in ~/.castle/castle.yaml, or: "
+ hint=f"set data_dir: in ~/.wildpc/wildpc.yaml, or: "
f"sudo mkdir -p {ddir} && sudo chown $(id -un) {ddir}",
)
)
- # Drift guard: castle.yaml is the single source of truth for the roots. An env var
+ # Drift guard: wildpc.yaml is the single source of truth for the roots. An env var
# override is per-process, so it's the one way the CLI and the api service can still
# diverge (env set in your shell, absent in the service unit — the original bug).
- for var in ("CASTLE_DATA_DIR", "CASTLE_REPOS_DIR"):
+ for var in ("WILDPC_DATA_DIR", "WILDPC_REPOS_DIR"):
if var in os.environ:
checks.append(
Check(
WARN,
- f"{var} overrides castle.yaml",
+ f"{var} overrides wildpc.yaml",
detail=f"{var}={os.environ[var]}",
- hint=f"set data_dir:/repos_dir: in castle.yaml and unset {var}, so "
+ hint=f"set data_dir:/repos_dir: in wildpc.yaml and unset {var}, so "
"every process (CLI and api) resolves the same roots",
)
)
@@ -186,7 +186,7 @@ def _check_configuration(config) -> list[Check]:
def _check_dashboard_built(config) -> Check:
- from castle_core.lifecycle import _static_built
+ from wildpc_core.lifecycle import _static_built
if not config.deployments_named(_DASHBOARD):
return Check(WARN, "dashboard not registered", detail="skipping build check")
@@ -196,7 +196,7 @@ def _check_dashboard_built(config) -> Check:
WARN,
"dashboard not built",
detail="gateway has no UI to serve at /",
- hint=f"castle program build {_DASHBOARD}",
+ hint=f"wildpc program build {_DASHBOARD}",
)
@@ -212,8 +212,8 @@ def _deployment_port(config, name: str) -> int | None:
def _check_runtime(config) -> list[Check]:
- from castle_core.config import SPECS_DIR
- from castle_core.lifecycle import is_active
+ from wildpc_core.config import SPECS_DIR
+ from wildpc_core.lifecycle import is_active
checks: list[Check] = []
@@ -228,7 +228,7 @@ def _check_runtime(config) -> list[Check]:
WARN,
"gateway active but not listening",
detail=f":{gw_port} refused",
- hint="castle gateway reload; check 'castle service logs castle-gateway'",
+ hint="wildpc gateway reload; check 'wildpc service logs wildpc-gateway'",
)
)
else:
@@ -236,7 +236,7 @@ def _check_runtime(config) -> list[Check]:
Check(
FAIL,
"gateway not running",
- hint="castle apply",
+ hint="wildpc apply",
)
)
@@ -247,16 +247,16 @@ def _check_runtime(config) -> list[Check]:
checks.append(
Check(
WARN,
- "castle-api active but not listening",
+ "wildpc-api active but not listening",
detail=f":{api_port} refused",
- hint="castle service logs castle-api",
+ hint="wildpc service logs wildpc-api",
)
)
else:
detail = f"listening on :{api_port}" if api_port else ""
- checks.append(Check(OK, "castle-api running", detail=detail))
+ checks.append(Check(OK, "wildpc-api running", detail=detail))
else:
- checks.append(Check(FAIL, "castle-api not running", hint="castle apply"))
+ checks.append(Check(FAIL, "wildpc-api not running", hint="wildpc apply"))
# Generated artifacts.
registry = SPECS_DIR / "registry.yaml"
@@ -270,7 +270,7 @@ def _check_runtime(config) -> list[Check]:
FAIL,
"generated specs missing",
detail=", ".join(missing),
- hint="castle apply",
+ hint="wildpc apply",
)
)
@@ -293,7 +293,7 @@ def _check_tls_exposure(config) -> list[Check]:
Check(
FAIL,
"tls=acme but no domain set",
- hint="add 'domain: ' under gateway: in castle.yaml",
+ hint="add 'domain: ' under gateway: in wildpc.yaml",
)
)
@@ -327,7 +327,7 @@ def _check_tls_exposure(config) -> list[Check]:
token_name = {"cloudflare": "CLOUDFLARE_API_TOKEN"}.get(
provider, f"{provider.upper()}_API_TOKEN"
)
- from castle_core.config import read_secret
+ from wildpc_core.config import read_secret
if read_secret(token_name):
checks.append(Check(OK, "provider token present", detail=token_name))
@@ -348,7 +348,7 @@ def _check_tls_exposure(config) -> list[Check]:
public_specs = [(n, d) for _k, n, d in config.all_deployments() if getattr(d, "public", False)]
public = [n for n, _d in public_specs]
if public:
- from castle_core.lifecycle import is_active
+ from wildpc_core.lifecycle import is_active
# A public deployment gets its public name from its own `public_host`
# override or the node-wide `public_domain`; the default domain is only
@@ -370,13 +370,13 @@ def _check_tls_exposure(config) -> list[Check]:
hint="see docs/tunnel-setup.md",
)
)
- if not is_active("castle-tunnel", "service", config):
+ if not is_active("wildpc-tunnel", "service", config):
checks.append(
Check(
WARN,
- "castle-tunnel not running",
+ "wildpc-tunnel not running",
detail="public routes are down",
- hint="enable castle-tunnel in its deployment, then: castle apply",
+ hint="enable wildpc-tunnel in its deployment, then: wildpc apply",
)
)
checks.append(_check_public_dns(config))
@@ -387,18 +387,18 @@ def _check_tls_exposure(config) -> list[Check]:
def _check_public_dns(config) -> Check:
- """Whether Castle can manage the public CNAMEs itself.
+ """Whether Wild PC can manage the public CNAMEs itself.
Read-only: confirms the token exists and can reach the public zone + its DNS
records. The required permission is a single DNS:Edit (Cloudflare's 'Edit zone
DNS' template), which also grants the zone lookup — so a correctly-scoped token
passes this probe. Write itself isn't exercised (that would mutate); a
- DNS:Read-only token would false-pass here but `castle apply` then surfaces a
+ DNS:Read-only token would false-pass here but `wildpc apply` then surfaces a
403 with the fix. Absent token → WARN (CNAMEs stay manual), not a failure.
"""
import urllib.error
- from castle_core.generators.dns import PUBLIC_DNS_TOKEN, _api, public_dns_token
+ from wildpc_core.generators.dns import PUBLIC_DNS_TOKEN, _api, public_dns_token
gw = config.gateway
token = public_dns_token()
@@ -410,7 +410,7 @@ def _check_public_dns(config) -> Check:
hint=(
f"add a Cloudflare token with DNS:Edit on "
f"{gw.public_domain or 'the public zone(s)'} "
- f"('Edit zone DNS' template) → ~/.castle/secrets/{PUBLIC_DNS_TOKEN} "
+ f"('Edit zone DNS' template) → ~/.wildpc/secrets/{PUBLIC_DNS_TOKEN} "
"(else route each host by hand)"
),
)
@@ -463,7 +463,7 @@ def _check_privileged_ports() -> Check:
"cannot bind :80/:443",
detail=f"unprivileged floor is {val}",
hint="echo 'net.ipv4.ip_unprivileged_port_start=80' | "
- "sudo tee /etc/sysctl.d/50-castle-gateway.conf && sudo sysctl --system",
+ "sudo tee /etc/sysctl.d/50-wildpc-gateway.conf && sudo sysctl --system",
)
@@ -476,7 +476,7 @@ def _check_stacks(config: object) -> list[Check]:
tool for an enabled deployment is a FAIL (its service can't build/run); missing
for a not-yet-enabled program is a WARN. Unused stacks are skipped — no nagging
about pnpm when there are no frontends."""
- from castle_core.stack_status import all_stack_status
+ from wildpc_core.stack_status import all_stack_status
checks: list[Check] = []
for st in all_stack_status(config, with_version=False):
@@ -504,9 +504,9 @@ def _check_stacks(config: object) -> list[Check]:
def run_doctor(args: argparse.Namespace) -> int:
- from castle_core.config import load_config
+ from wildpc_core.config import load_config
- print("\n\033[1mCastle Doctor\033[0m")
+ print("\n\033[1mWild PC Doctor\033[0m")
try:
config = load_config()
@@ -515,9 +515,9 @@ def run_doctor(args: argparse.Namespace) -> int:
_print(
Check(
FAIL,
- "castle.yaml failed to load",
+ "wildpc.yaml failed to load",
detail=str(exc),
- hint="check ~/.castle/castle.yaml — re-run ./install.sh to reseed",
+ hint="check ~/.wildpc/wildpc.yaml — re-run ./install.sh to reseed",
)
)
print()
@@ -544,7 +544,7 @@ def run_doctor(args: argparse.Namespace) -> int:
print(f"\033[31m{fails} problem(s)\033[0m" + (f", {warns} warning(s)" if warns else ""))
return 1
if warns:
- print(f"\033[33m{warns} warning(s)\033[0m — Castle is up; address when convenient")
+ print(f"\033[33m{warns} warning(s)\033[0m — Wild PC is up; address when convenient")
return 0
print("\033[32mAll checks passed.\033[0m")
return 0
diff --git a/cli/src/castle_cli/commands/gateway.py b/cli/src/wildpc_cli/commands/gateway.py
similarity index 73%
rename from cli/src/castle_cli/commands/gateway.py
rename to cli/src/wildpc_cli/commands/gateway.py
index 32faa47..ec75c03 100644
--- a/cli/src/castle_cli/commands/gateway.py
+++ b/cli/src/wildpc_cli/commands/gateway.py
@@ -1,8 +1,8 @@
-"""castle gateway — inspect the Caddy reverse proxy gateway.
+"""wildpc gateway — inspect the Caddy reverse proxy gateway.
-The gateway is itself a deployment (`castle-gateway`): start/stop/reload it the
-same way as anything else — `castle apply` (render routes + reload), `castle
-restart castle-gateway` (bounce), or `enabled: false` + apply (stop). This command
+The gateway is itself a deployment (`wildpc-gateway`): start/stop/reload it the
+same way as anything else — `wildpc apply` (render routes + reload), `wildpc
+restart wildpc-gateway` (bounce), or `enabled: false` + apply (stop). This command
is the read-only inspection lens: is it up, and what's the route table.
"""
@@ -11,9 +11,9 @@ from __future__ import annotations
import argparse
import subprocess
-from castle_core.registry import REGISTRY_PATH, load_registry
+from wildpc_core.registry import REGISTRY_PATH, load_registry
-GATEWAY_UNIT = "castle-castle-gateway.service"
+GATEWAY_UNIT = "wildpc-wildpc-gateway.service"
def run_gateway(args: argparse.Namespace) -> int:
@@ -32,10 +32,10 @@ def _gateway_status() -> int:
print(f"Gateway: {'running' if status == 'active' else status}")
if not REGISTRY_PATH.exists():
- print(" (no registry — run 'castle apply')")
+ print(" (no registry — run 'wildpc apply')")
return 0
- from castle_core.generators.caddyfile import compute_routes
+ from wildpc_core.generators.caddyfile import compute_routes
routes = compute_routes(load_registry())
if not routes:
diff --git a/cli/src/castle_cli/commands/graph.py b/cli/src/wildpc_cli/commands/graph.py
similarity index 93%
rename from cli/src/castle_cli/commands/graph.py
rename to cli/src/wildpc_cli/commands/graph.py
index ba77b67..5387f7a 100644
--- a/cli/src/castle_cli/commands/graph.py
+++ b/cli/src/wildpc_cli/commands/graph.py
@@ -1,4 +1,4 @@
-"""castle graph — the relationship model: repos, `requires` edges, derived status.
+"""wildpc graph — the relationship model: repos, `requires` edges, derived status.
A read-only diagnostic (see docs/relationships.md). Nothing here is stored — repos
come from git, predicates (functional/fresh/deployed) are computed on the fly.
@@ -10,14 +10,14 @@ import argparse
import dataclasses
import json
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
BOLD, DIM, RESET = "\033[1m", "\033[2m", "\033[0m"
GREEN, RED, YELLOW, CYAN = "\033[32m", "\033[31m", "\033[33m", "\033[36m"
def run_graph(args: argparse.Namespace) -> int:
- from castle_core.relations import build_model
+ from wildpc_core.relations import build_model
config = load_config()
model = build_model(config, check=True, freshness=True)
diff --git a/cli/src/castle_cli/commands/info.py b/cli/src/wildpc_cli/commands/info.py
similarity index 96%
rename from cli/src/castle_cli/commands/info.py
rename to cli/src/wildpc_cli/commands/info.py
index 02fed47..76f43ce 100644
--- a/cli/src/castle_cli/commands/info.py
+++ b/cli/src/wildpc_cli/commands/info.py
@@ -1,4 +1,4 @@
-"""castle info - show detailed program information."""
+"""wildpc info - show detailed program information."""
from __future__ import annotations
@@ -6,7 +6,7 @@ import argparse
import json
from pathlib import Path
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
# Terminal colors
BOLD = "\033[1m"
@@ -20,7 +20,7 @@ RED = "\033[91m"
def _load_deployed_program(name: str) -> object | None:
"""Try to load a specific deployed program from registry."""
try:
- from castle_core.registry import load_registry
+ from wildpc_core.registry import load_registry
registry = load_registry()
return next(iter(registry.named(name)), None)
@@ -41,7 +41,7 @@ def run_info(args: argparse.Namespace) -> int:
if not program and not service and not job:
where = f" {resource}" if resource else ""
- print(f"Error: no{where} '{name}' in castle.yaml")
+ print(f"Error: no{where} '{name}' in wildpc.yaml")
return 1
deployed = _load_deployed_program(name)
@@ -153,7 +153,7 @@ def run_info(args: argparse.Namespace) -> int:
if deployed.secret_env_keys:
print(f" {BOLD}secrets{RESET}: {', '.join(deployed.secret_env_keys)}")
else:
- print(f"\n {DIM}not applied (run 'castle apply'){RESET}")
+ print(f"\n {DIM}not applied (run 'wildpc apply'){RESET}")
# Show CLAUDE.md if it exists
source_dir = None
diff --git a/cli/src/castle_cli/commands/list_cmd.py b/cli/src/wildpc_cli/commands/list_cmd.py
similarity index 97%
rename from cli/src/castle_cli/commands/list_cmd.py
rename to cli/src/wildpc_cli/commands/list_cmd.py
index f7fe2c2..20548e5 100644
--- a/cli/src/castle_cli/commands/list_cmd.py
+++ b/cli/src/wildpc_cli/commands/list_cmd.py
@@ -1,4 +1,4 @@
-"""castle list - the program catalog plus every deployment view (services, jobs,
+"""wildpc list - the program catalog plus every deployment view (services, jobs,
tools, static)."""
from __future__ import annotations
@@ -7,7 +7,7 @@ import argparse
import json
import logging
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
log = logging.getLogger(__name__)
@@ -75,7 +75,7 @@ def run_list(args: argparse.Namespace) -> int:
and the **Services**/**Jobs** deployment views. `--kind` filters the catalog
by a program's derived kind (service/job/tool/static/reference).
"""
- from castle_core.lifecycle import is_active
+ from wildpc_core.lifecycle import is_active
config = load_config()
@@ -199,7 +199,7 @@ def _list_json(
filter_stack: str | None,
) -> int:
"""Output JSON: the program catalog (kind-filterable) plus deployments."""
- from castle_core.lifecycle import is_active
+ from wildpc_core.lifecycle import is_active
output = []
diff --git a/cli/src/castle_cli/commands/logs.py b/cli/src/wildpc_cli/commands/logs.py
similarity index 90%
rename from cli/src/castle_cli/commands/logs.py
rename to cli/src/wildpc_cli/commands/logs.py
index 9f29592..7df4fbe 100644
--- a/cli/src/castle_cli/commands/logs.py
+++ b/cli/src/wildpc_cli/commands/logs.py
@@ -1,12 +1,12 @@
-"""castle logs - view component logs."""
+"""wildpc logs - view component logs."""
from __future__ import annotations
import argparse
import subprocess
-from castle_cli.commands.service import UNIT_PREFIX
-from castle_cli.config import load_config
+from wildpc_cli.commands.service import UNIT_PREFIX
+from wildpc_cli.config import load_config
def run_logs(args: argparse.Namespace) -> int:
@@ -51,7 +51,7 @@ def _compose_logs(name: str, svc: object, args: argparse.Namespace) -> int:
import shutil
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
- project = getattr(svc.run, "project_name", None) or f"castle-{name}" # type: ignore[attr-defined]
+ project = getattr(svc.run, "project_name", None) or f"wildpc-{name}" # type: ignore[attr-defined]
cmd = [runtime, "compose", "-p", project, "logs"]
lines = getattr(args, "lines", 50)
@@ -70,7 +70,7 @@ def _container_logs(name: str, args: argparse.Namespace) -> int:
import shutil
runtime = shutil.which("podman") or shutil.which("docker") or "podman"
- container_name = f"castle-{name}"
+ container_name = f"wildpc-{name}"
cmd = [runtime, "logs"]
lines = getattr(args, "lines", 50)
diff --git a/cli/src/castle_cli/commands/mesh.py b/cli/src/wildpc_cli/commands/mesh.py
similarity index 90%
rename from cli/src/castle_cli/commands/mesh.py
rename to cli/src/wildpc_cli/commands/mesh.py
index 01d64ad..48b75a6 100644
--- a/cli/src/castle_cli/commands/mesh.py
+++ b/cli/src/wildpc_cli/commands/mesh.py
@@ -1,6 +1,6 @@
-"""castle mesh — inspect the NATS mesh and manage shared config.
+"""wildpc mesh — inspect the NATS mesh and manage shared config.
-The mesh lives in the running castle-api (it holds the live peer state), so this
+The mesh lives in the running wildpc-api (it holds the live peer state), so this
command talks to the local API over HTTP rather than reading files.
"""
@@ -15,10 +15,10 @@ import urllib.request
def _api_base() -> str:
port = None
try:
- from castle_core.config import load_config
+ from wildpc_core.config import load_config
config = load_config()
- dep = next((d for _k, d in config.deployments_named("castle-api")), None)
+ dep = next((d for _k, d in config.deployments_named("wildpc-api")), None)
internal = getattr(getattr(getattr(dep, "expose", None), "http", None), "internal", None)
port = getattr(internal, "port", None)
except Exception:
@@ -56,7 +56,7 @@ def run_mesh(args: argparse.Namespace) -> int:
print(f"error: HTTP {e.code} — {detail}")
return 1
except urllib.error.URLError as e:
- print(f"castle-api not reachable ({e.reason}). Is it running + mesh enabled?")
+ print(f"wildpc-api not reachable ({e.reason}). Is it running + mesh enabled?")
return 1
print(f"unknown mesh command: {sub}")
return 2
diff --git a/cli/src/castle_cli/commands/run_cmd.py b/cli/src/wildpc_cli/commands/run_cmd.py
similarity index 91%
rename from cli/src/castle_cli/commands/run_cmd.py
rename to cli/src/wildpc_cli/commands/run_cmd.py
index f88a644..b3aa561 100644
--- a/cli/src/castle_cli/commands/run_cmd.py
+++ b/cli/src/wildpc_cli/commands/run_cmd.py
@@ -1,4 +1,4 @@
-"""castle run - run a program or service in the foreground.
+"""wildpc run - run a program or service in the foreground.
Unified: if the target is a program with a declared `run` command, run that in
the foreground (dev-run). Otherwise fall back to the deployed-service run from
@@ -12,10 +12,10 @@ import os
import subprocess
from pathlib import Path
-from castle_core.generators.systemd import secret_env_path
-from castle_core.registry import REGISTRY_PATH, load_registry
+from wildpc_core.generators.systemd import secret_env_path
+from wildpc_core.registry import REGISTRY_PATH, load_registry
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
def _load_secret_env(name: str) -> dict[str, str]:
@@ -82,13 +82,13 @@ def run_run(args: argparse.Namespace) -> int:
# Service: deployed command from the registry.
if not REGISTRY_PATH.exists():
- print("Error: no registry found. Run 'castle apply' first.")
+ print("Error: no registry found. Run 'wildpc apply' first.")
return 1
registry = load_registry()
matches = registry.named(name)
if not matches:
- print(f"Error: '{name}' is not a deployed service. Run 'castle apply' first.")
+ print(f"Error: '{name}' is not a deployed service. Run 'wildpc apply' first.")
return 1
# A name may span kinds; run the one that has a run command (service/job).
diff --git a/cli/src/castle_cli/commands/secret.py b/cli/src/wildpc_cli/commands/secret.py
similarity index 88%
rename from cli/src/castle_cli/commands/secret.py
rename to cli/src/wildpc_cli/commands/secret.py
index 3ee5bf9..02cc798 100644
--- a/cli/src/castle_cli/commands/secret.py
+++ b/cli/src/wildpc_cli/commands/secret.py
@@ -1,14 +1,14 @@
-"""castle secret — read/write secrets in the *active* backend.
+"""wildpc secret — read/write secrets in the *active* backend.
The active backend (file or openbao) is selected by the ``secrets:`` block of
-castle.yaml (env overrides). Writing a secret by hand means knowing that choice
+wildpc.yaml (env overrides). Writing a secret by hand means knowing that choice
and the backend's storage layout — get it wrong and the value lands somewhere the
resolver never reads, so ``${secret:NAME}`` silently degrades to a
```` placeholder that a service then uses as if it were the
real credential (exactly how immich's DB password went to a file on an OpenBao
fleet). This command routes every read/write through
-:func:`castle_core.config.active_secret_backend`, so there's no wrong store to
-pick. ``castle apply`` refuses to converge a deployment whose secrets don't
+:func:`wildpc_core.config.active_secret_backend`, so there's no wrong store to
+pick. ``wildpc apply`` refuses to converge a deployment whose secrets don't
resolve here.
"""
@@ -18,13 +18,13 @@ import argparse
import getpass
import sys
-from castle_core.config import active_backend_name, active_secret_backend
+from wildpc_core.config import active_backend_name, active_secret_backend
def run_secret(args: argparse.Namespace) -> int:
sub = getattr(args, "secret_command", None)
if not sub:
- print("Usage: castle secret {list|set|get|rm}")
+ print("Usage: wildpc secret {list|set|get|rm}")
return 1
if sub == "list":
return _list()
diff --git a/cli/src/castle_cli/commands/service.py b/cli/src/wildpc_cli/commands/service.py
similarity index 87%
rename from cli/src/castle_cli/commands/service.py
rename to cli/src/wildpc_cli/commands/service.py
index c31db2d..642681b 100644
--- a/cli/src/castle_cli/commands/service.py
+++ b/cli/src/wildpc_cli/commands/service.py
@@ -1,23 +1,23 @@
-"""castle service / castle job — manage systemd service & timer units."""
+"""wildpc service / wildpc job — manage systemd service & timer units."""
from __future__ import annotations
import argparse
import subprocess
-from castle_core.generators.systemd import (
+from wildpc_core.generators.systemd import (
SYSTEMD_USER_DIR,
timer_name,
unit_name,
)
-from castle_cli.config import (
- CastleConfig,
+from wildpc_cli.config import (
+ WildpcConfig,
load_config,
)
# Re-export for use by other commands
-UNIT_PREFIX = "castle-"
+UNIT_PREFIX = "wildpc-"
def _install_unit(uname: str, content: str) -> None:
@@ -40,25 +40,25 @@ _PAST = {"start": "started", "stop": "stopped", "restart": "restarted"}
def run_service_cmd(args: argparse.Namespace) -> int:
- """`castle service restart ` — the imperative bounce (only verb left).
+ """`wildpc service restart ` — the imperative bounce (only verb left).
- Lifecycle (deploy/enable/disable/start/stop) is now convergence: `castle apply`.
+ Lifecycle (deploy/enable/disable/start/stop) is now convergence: `wildpc apply`.
"""
config = load_config()
return _unit_action(config, args.name, "restart", "service")
def run_job_cmd(args: argparse.Namespace) -> int:
- """`castle job restart ` — bounce the job's timer."""
+ """`wildpc job restart ` — bounce the job's timer."""
config = load_config()
return _unit_action(config, args.name, "restart", "job")
def run_restart(args: argparse.Namespace) -> int:
- """Top-level `castle restart [name]` — bounce one deployment, or all of them.
+ """Top-level `wildpc restart [name]` — bounce one deployment, or all of them.
An imperative op: it re-actualizes current desired state, it does not change it
- (that's `castle apply`). A bare name bounces every kind sharing it.
+ (that's `wildpc apply`). A bare name bounces every kind sharing it.
"""
config = load_config()
name = getattr(args, "name", None)
@@ -74,10 +74,10 @@ def run_restart(args: argparse.Namespace) -> int:
return rc
-_GATEWAY_NAME = "castle-gateway"
+_GATEWAY_NAME = "wildpc-gateway"
-def _unit_action(config: CastleConfig, name: str, action: str, kind: str) -> int:
+def _unit_action(config: WildpcConfig, name: str, action: str, kind: str) -> int:
"""start/stop/restart one deployment (name, kind), dispatched by its manager.
systemd (a process/timer) → `systemctl`; caddy (static) → reload the gateway;
@@ -101,7 +101,7 @@ def _unit_action(config: CastleConfig, name: str, action: str, kind: str) -> int
def _managed_lifecycle(
- config: CastleConfig, name: str, action: str, manager: str, kind: str
+ config: WildpcConfig, name: str, action: str, manager: str, kind: str
) -> int:
"""Lifecycle for non-systemd managers (no unit to systemctl)."""
if manager == "caddy":
@@ -119,11 +119,11 @@ def _managed_lifecycle(
return 0
-def _path_lifecycle(config: CastleConfig, name: str, action: str, kind: str) -> int:
+def _path_lifecycle(config: WildpcConfig, name: str, action: str, kind: str) -> int:
"""A `path` (tool) deployment's lifecycle is install/uninstall on PATH."""
import asyncio
- from castle_core.lifecycle import activate, deactivate
+ from wildpc_core.lifecycle import activate, deactivate
# stop → uninstall; start/restart → ensure installed (activate skips if on PATH).
coro = deactivate if action == "stop" else activate
@@ -132,7 +132,7 @@ def _path_lifecycle(config: CastleConfig, name: str, action: str, kind: str) ->
return 0 if res.status == "ok" else 1
-def _services_restart(config: CastleConfig) -> int:
+def _services_restart(config: WildpcConfig) -> int:
"""Restart every systemd-managed deployment (service or job) unit.
caddy/path/none deployments have no unit — they ride along with the gateway
@@ -152,7 +152,7 @@ def _services_restart(config: CastleConfig) -> int:
def run_status(args: argparse.Namespace) -> int:
"""Unified status across the platform: services + jobs + programs."""
- from castle_core.lifecycle import is_active
+ from wildpc_core.lifecycle import is_active
config = load_config()
@@ -180,11 +180,11 @@ def run_status(args: argparse.Namespace) -> int:
return 0
-def _service_status(config: CastleConfig) -> int:
+def _service_status(config: WildpcConfig) -> int:
"""Show status of all services and jobs, dispatched by manager."""
- from castle_core.lifecycle import is_active
+ from wildpc_core.lifecycle import is_active
- print("\nCastle Services")
+ print("\nWild PC Services")
print("=" * 50)
for name, svc in config.services.items():
diff --git a/cli/src/castle_cli/commands/stack.py b/cli/src/wildpc_cli/commands/stack.py
similarity index 91%
rename from cli/src/castle_cli/commands/stack.py
rename to cli/src/wildpc_cli/commands/stack.py
index a1a5e88..1ad5373 100644
--- a/cli/src/castle_cli/commands/stack.py
+++ b/cli/src/wildpc_cli/commands/stack.py
@@ -1,4 +1,4 @@
-"""castle stack — the stacks lens (toolchains a program's stack needs).
+"""wildpc stack — the stacks lens (toolchains a program's stack needs).
A *stack* (python-fastapi, react-vite, hugo, …) is creation-time guidance that
also carries the **host toolchains** its programs need to build and run (`uv`,
@@ -14,10 +14,10 @@ import json
from dataclasses import asdict
from typing import TYPE_CHECKING
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
if TYPE_CHECKING:
- from castle_core.stack_status import StackStatus
+ from wildpc_core.stack_status import StackStatus
BOLD = "\033[1m"
DIM = "\033[2m"
@@ -37,7 +37,7 @@ def _record(st: StackStatus) -> dict:
def run_stack_list(args: argparse.Namespace) -> int:
"""List stacks with their toolchain health, program count, and dev verbs."""
- from castle_core.stack_status import all_stack_status
+ from wildpc_core.stack_status import all_stack_status
config = load_config()
# Skip per-tool version probes for the list (a subprocess per tool) — keep it snappy.
@@ -75,13 +75,13 @@ def run_stack_list(args: argparse.Namespace) -> int:
def run_stack_info(args: argparse.Namespace) -> int:
"""Show one stack: each tool's presence + version + fix, and who uses it."""
- from castle_core.stack_status import stack_status
+ from wildpc_core.stack_status import stack_status
config = load_config()
name = args.name
st = stack_status(config, name)
if st is None:
- from castle_core.stacks import available_stacks
+ from wildpc_core.stacks import available_stacks
print(f"Error: no stack '{name}'. Known: {', '.join(available_stacks())}")
return 1
diff --git a/cli/src/castle_cli/commands/tls.py b/cli/src/wildpc_cli/commands/tls.py
similarity index 92%
rename from cli/src/castle_cli/commands/tls.py
rename to cli/src/wildpc_cli/commands/tls.py
index 62730cd..9b662db 100644
--- a/cli/src/castle_cli/commands/tls.py
+++ b/cli/src/wildpc_cli/commands/tls.py
@@ -1,4 +1,4 @@
-"""castle tls — castle-managed TLS certs for raw-TCP services.
+"""wildpc tls — wildpc-managed TLS certs for raw-TCP services.
Each TLS-material TCP service (postgres, redis, …) gets the gateway's ACME
wildcard cert cut onto it so it presents a *trusted* cert on its raw port.
@@ -13,9 +13,9 @@ import argparse
import hashlib
from datetime import datetime, timezone
-from castle_core.tls import _tls_of, reconcile_tls, tls_dir_for, wildcard_cert
+from wildpc_core.tls import _tls_of, reconcile_tls, tls_dir_for, wildcard_cert
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
def run_tls(args: argparse.Namespace) -> int:
@@ -67,7 +67,7 @@ def _tls_status() -> int:
continue
pem = have.read_bytes()
fp = _fingerprint(pem)
- drift = "" if src_fp and fp == src_fp else " ⚠ stale (run: castle tls reconcile)"
+ drift = "" if src_fp and fp == src_fp else " ⚠ stale (run: wildpc tls reconcile)"
rows.append((name, fp, _not_after(pem), drift))
if not rows:
diff --git a/cli/src/castle_cli/commands/tool.py b/cli/src/wildpc_cli/commands/tool.py
similarity index 88%
rename from cli/src/castle_cli/commands/tool.py
rename to cli/src/wildpc_cli/commands/tool.py
index 625447d..4648a47 100644
--- a/cli/src/castle_cli/commands/tool.py
+++ b/cli/src/wildpc_cli/commands/tool.py
@@ -1,4 +1,4 @@
-"""castle tool — the tools lens (programs installed on PATH).
+"""wildpc tool — the tools lens (programs installed on PATH).
A *tool* is a program with a `path` (manager) deployment: a CLI on your PATH.
This lens is what coding assistants use to discover what's available — so the
@@ -15,11 +15,11 @@ import tomllib
from pathlib import Path
from typing import TYPE_CHECKING
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
if TYPE_CHECKING:
- from castle_core.config import CastleConfig
- from castle_core.manifest import ProgramSpec
+ from wildpc_core.config import WildpcConfig
+ from wildpc_core.manifest import ProgramSpec
BOLD = "\033[1m"
DIM = "\033[2m"
@@ -29,11 +29,11 @@ GREY = "\033[90m"
CYAN = "\033[96m"
-def _is_tool(config: CastleConfig, name: str) -> bool:
+def _is_tool(config: WildpcConfig, name: str) -> bool:
return any(kind == "tool" for _, kind in config.deployments_of(name))
-def _tool_programs(config: CastleConfig) -> dict:
+def _tool_programs(config: WildpcConfig) -> dict:
"""Programs with a tool (path) deployment, name-sorted."""
return {name: comp for name, comp in sorted(config.programs.items()) if _is_tool(config, name)}
@@ -60,12 +60,12 @@ def _executables(comp: ProgramSpec) -> list[str]:
def _tool_record(
- config: CastleConfig, name: str, comp: ProgramSpec, installed: bool, fmt: str = "openai"
+ config: WildpcConfig, name: str, comp: ProgramSpec, installed: bool, fmt: str = "openai"
) -> dict:
# The tool-call schema (for handing this CLI to an agent) is stored neutral on
# the path deployment; render it into the requested envelope for the --json
# context payload. Feed default is openai (litellm-native).
- from castle_core.tool_schema import render_tool_schema
+ from wildpc_core.tool_schema import render_tool_schema
dep = config.deployment("tool", name)
core = getattr(dep, "tool_schema", None)
@@ -83,7 +83,7 @@ def _tool_record(
def run_tool_list(args: argparse.Namespace) -> int:
"""List tools (programs on PATH) with their executable + description."""
- from castle_core.lifecycle import tool_installed
+ from wildpc_core.lifecycle import tool_installed
config = load_config()
tools = _tool_programs(config)
@@ -119,7 +119,7 @@ def run_tool_list(args: argparse.Namespace) -> int:
def run_tool_info(args: argparse.Namespace) -> int:
"""Show one tool's details — executable, description, install state, source."""
- from castle_core.lifecycle import tool_installed
+ from wildpc_core.lifecycle import tool_installed
config = load_config()
name = args.name
@@ -149,7 +149,7 @@ def run_tool_info(args: argparse.Namespace) -> int:
if comp.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(comp.system_dependencies)}")
if not installed:
- print(f"\n {DIM}enable it in its deployment, then: castle apply{RESET}")
+ print(f"\n {DIM}enable it in its deployment, then: wildpc apply{RESET}")
else:
print(f"\n {DIM}run `{exes[0]} --help` for arguments{RESET}")
print()
diff --git a/cli/src/castle_cli/config.py b/cli/src/wildpc_cli/config.py
similarity index 54%
rename from cli/src/castle_cli/config.py
rename to cli/src/wildpc_cli/config.py
index ca21fa7..92fc3a7 100644
--- a/cli/src/castle_cli/config.py
+++ b/cli/src/wildpc_cli/config.py
@@ -1,24 +1,24 @@
-"""Re-export from castle-core for backward compatibility."""
+"""Re-export from wildpc-core for backward compatibility."""
-from castle_core.config import * # noqa: F401, F403
-from castle_core.config import ( # noqa: F401 — explicit re-exports for type checkers
+from wildpc_core.config import * # noqa: F401, F403
+from wildpc_core.config import ( # noqa: F401 — explicit re-exports for type checkers
ARTIFACTS_DIR,
- CASTLE_HOME,
CODE_DIR,
CONTENT_DIR,
GENERATED_DIR,
SECRETS_DIR,
SPECS_DIR,
STATIC_DIR,
- CastleConfig,
+ WILDPC_HOME,
GatewayConfig,
+ WildpcConfig,
ensure_dirs,
- find_castle_root,
+ find_wildpc_root,
load_config,
resolve_env_vars,
save_config,
)
-from castle_core.registry import ( # noqa: F401
+from wildpc_core.registry import ( # noqa: F401
REGISTRY_PATH,
Deployment,
NodeConfig,
diff --git a/cli/src/castle_cli/main.py b/cli/src/wildpc_cli/main.py
similarity index 85%
rename from cli/src/castle_cli/main.py
rename to cli/src/wildpc_cli/main.py
index ceea3fe..1618554 100644
--- a/cli/src/castle_cli/main.py
+++ b/cli/src/wildpc_cli/main.py
@@ -1,4 +1,4 @@
-"""Castle CLI entry point — resource-first command surface.
+"""Wild PC CLI entry point — resource-first command surface.
Operations live under the resource they act on. `program` is the catalog;
`service`, `job`, and `tool` are deployment lenses (systemd services, scheduled
@@ -13,9 +13,9 @@ from __future__ import annotations
import argparse
import sys
-from castle_core.stacks import available_stacks
+from wildpc_core.stacks import available_stacks
-from castle_cli import __version__
+from wildpc_cli import __version__
DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"]
@@ -56,7 +56,7 @@ def _build_program_group(subparsers: argparse._SubParsersAction) -> None:
p = sub.add_parser("clone", help="Clone source for programs with repo:")
_add_name(p, "Program to clone (default: all with repo:)", optional=True)
- p = sub.add_parser("delete", help="Remove a program from castle.yaml")
+ p = sub.add_parser("delete", help="Remove a program from wildpc.yaml")
_add_name(p, "Program name")
p.add_argument("--source", action="store_true", help="Also delete the source directory")
p.add_argument(
@@ -109,7 +109,7 @@ def _build_stack_group(subparsers: argparse._SubParsersAction) -> None:
def _add_service_create(sub: argparse._SubParsersAction, kind: str) -> None:
- p = sub.add_parser("create", help=f"Create a {kind} in castle.yaml")
+ p = sub.add_parser("create", help=f"Create a {kind} in wildpc.yaml")
_add_name(p, f"{kind.capitalize()} name")
p.add_argument("--program", help="Program this deployment runs (convenience ref)")
p.add_argument("--description", default="", help="Description")
@@ -148,14 +148,14 @@ def _build_deployment_group(subparsers: argparse._SubParsersAction, kind: str) -
_add_service_create(sub, kind)
- p = sub.add_parser("delete", help=f"Remove a {kind} from castle.yaml")
+ p = sub.add_parser("delete", help=f"Remove a {kind} from wildpc.yaml")
_add_name(p, f"{kind.capitalize()} name")
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
p.add_argument("--source", action="store_true", help=argparse.SUPPRESS)
p.add_argument("--purge-data", action="store_true", help=argparse.SUPPRESS)
cap = f"{kind.capitalize()} name"
- # Lifecycle is convergence: `castle apply [name]`. `restart` stays as the one
+ # Lifecycle is convergence: `wildpc apply [name]`. `restart` stays as the one
# imperative bounce that doesn't change desired state.
_add_name(sub.add_parser("restart", help=f"Restart the {kind} (imperative bounce)"), cap)
@@ -167,13 +167,13 @@ def _build_deployment_group(subparsers: argparse._SubParsersAction, kind: str) -
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
- prog="castle",
+ prog="wildpc",
description=(
- "Castle platform CLI — programs, deployments "
+ "Wild PC platform CLI — programs, deployments "
"(services, jobs, tools), and infrastructure"
),
)
- parser.add_argument("--version", action="version", version=f"castle {__version__}")
+ parser.add_argument("--version", action="version", version=f"wildpc {__version__}")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
_build_program_group(subparsers)
@@ -183,7 +183,7 @@ def build_parser() -> argparse.ArgumentParser:
_build_stack_group(subparsers)
# Gateway (inspection). The gateway is a deployment — start/stop/reload it via
- # `castle apply` / `castle restart castle-gateway`; this lens just shows routes.
+ # `wildpc apply` / `wildpc restart wildpc-gateway`; this lens just shows routes.
gw = subparsers.add_parser("gateway", help="Show the gateway's status + route table")
gw_sub = gw.add_subparsers(dest="gateway_command")
gw_sub.add_parser("status", help="Show gateway status + routes (the default)")
@@ -204,7 +204,7 @@ def build_parser() -> argparse.ArgumentParser:
# TLS material for raw-TCP services (cert cut from the gateway wildcard).
tls = subparsers.add_parser(
- "tls", help="Manage castle-materialized TLS certs for raw-TCP services"
+ "tls", help="Manage wildpc-materialized TLS certs for raw-TCP services"
)
tls_sub = tls.add_subparsers(dest="tls_command")
tls_sub.add_parser(
@@ -265,38 +265,38 @@ def _dispatch_program(args: argparse.Namespace) -> int:
sub = args.program_command
if not sub:
verbs = "list|info|create|add|clone|delete|run|" + "|".join(DEV_VERBS)
- print(f"Usage: castle program {{{verbs}}}")
+ print(f"Usage: wildpc program {{{verbs}}}")
return 1
if sub == "list":
- from castle_cli.commands.list_cmd import run_list
+ from wildpc_cli.commands.list_cmd import run_list
return run_list(args)
if sub == "info":
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
return run_info(args)
if sub == "create":
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
return run_create(args)
if sub == "add":
- from castle_cli.commands.add import run_add
+ from wildpc_cli.commands.add import run_add
return run_add(args)
if sub == "clone":
- from castle_cli.commands.clone import run_clone
+ from wildpc_cli.commands.clone import run_clone
return run_clone(args)
if sub == "delete":
- from castle_cli.commands.delete import run_delete
+ from wildpc_cli.commands.delete import run_delete
return run_delete(args)
if sub == "run":
- from castle_cli.commands.run_cmd import run_run
+ from wildpc_cli.commands.run_cmd import run_run
return run_run(args)
if sub in DEV_VERBS:
- from castle_cli.commands.dev import run_verb
+ from wildpc_cli.commands.dev import run_verb
return run_verb(args, sub)
return 1
@@ -305,14 +305,14 @@ def _dispatch_program(args: argparse.Namespace) -> int:
def _dispatch_tool(args: argparse.Namespace) -> int:
sub = args.tool_command
if not sub:
- print("Usage: castle tool {list|info} (install/uninstall → edit config + castle apply)")
+ print("Usage: wildpc tool {list|info} (install/uninstall → edit config + wildpc apply)")
return 1
if sub == "list":
- from castle_cli.commands.tool import run_tool_list
+ from wildpc_cli.commands.tool import run_tool_list
return run_tool_list(args)
if sub == "info":
- from castle_cli.commands.tool import run_tool_info
+ from wildpc_cli.commands.tool import run_tool_info
return run_tool_info(args)
return 1
@@ -321,14 +321,14 @@ def _dispatch_tool(args: argparse.Namespace) -> int:
def _dispatch_stack(args: argparse.Namespace) -> int:
sub = args.stack_command
if not sub:
- print("Usage: castle stack {list|info}")
+ print("Usage: wildpc stack {list|info}")
return 1
if sub == "list":
- from castle_cli.commands.stack import run_stack_list
+ from wildpc_cli.commands.stack import run_stack_list
return run_stack_list(args)
if sub == "info":
- from castle_cli.commands.stack import run_stack_info
+ from wildpc_cli.commands.stack import run_stack_info
return run_stack_info(args)
return 1
@@ -337,31 +337,31 @@ def _dispatch_stack(args: argparse.Namespace) -> int:
def _dispatch_deployment(args: argparse.Namespace, kind: str) -> int:
sub = getattr(args, f"{kind}_command")
if not sub:
- verbs = "list|info|create|delete|restart|logs (deploy/enable/... → castle apply)"
- print(f"Usage: castle {kind} {{{verbs}}}")
+ verbs = "list|info|create|delete|restart|logs (deploy/enable/... → wildpc apply)"
+ print(f"Usage: wildpc {kind} {{{verbs}}}")
return 1
if sub == "list":
- from castle_cli.commands.list_cmd import run_list
+ from wildpc_cli.commands.list_cmd import run_list
return run_list(args)
if sub == "info":
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
return run_info(args)
if sub == "create":
- from castle_cli.commands.deploy_create import run_job_create, run_service_create
+ from wildpc_cli.commands.deploy_create import run_job_create, run_service_create
return run_service_create(args) if kind == "service" else run_job_create(args)
if sub == "delete":
- from castle_cli.commands.delete import run_delete
+ from wildpc_cli.commands.delete import run_delete
return run_delete(args)
if sub == "restart":
- from castle_cli.commands.service import run_job_cmd, run_service_cmd
+ from wildpc_cli.commands.service import run_job_cmd, run_service_cmd
return run_service_cmd(args) if kind == "service" else run_job_cmd(args)
if sub == "logs":
- from castle_cli.commands.logs import run_logs
+ from wildpc_cli.commands.logs import run_logs
return run_logs(args)
return 1
@@ -385,43 +385,43 @@ def main() -> int:
if cmd == "stack":
return _dispatch_stack(args)
if cmd == "gateway":
- from castle_cli.commands.gateway import run_gateway
+ from wildpc_cli.commands.gateway import run_gateway
return run_gateway(args)
if cmd == "mesh":
- from castle_cli.commands.mesh import run_mesh
+ from wildpc_cli.commands.mesh import run_mesh
return run_mesh(args)
if cmd == "tls":
- from castle_cli.commands.tls import run_tls
+ from wildpc_cli.commands.tls import run_tls
return run_tls(args)
if cmd == "secret":
- from castle_cli.commands.secret import run_secret
+ from wildpc_cli.commands.secret import run_secret
return run_secret(args)
if cmd == "apply":
- from castle_cli.commands.apply import run_apply
+ from wildpc_cli.commands.apply import run_apply
return run_apply(args)
if cmd == "restart":
- from castle_cli.commands.service import run_restart
+ from wildpc_cli.commands.service import run_restart
return run_restart(args)
if cmd == "status":
- from castle_cli.commands.service import run_status
+ from wildpc_cli.commands.service import run_status
return run_status(args)
if cmd == "doctor":
- from castle_cli.commands.doctor import run_doctor
+ from wildpc_cli.commands.doctor import run_doctor
return run_doctor(args)
if cmd == "graph":
- from castle_cli.commands.graph import run_graph
+ from wildpc_cli.commands.graph import run_graph
return run_graph(args)
if cmd == "list":
- from castle_cli.commands.list_cmd import run_list
+ from wildpc_cli.commands.list_cmd import run_list
return run_list(args)
diff --git a/cli/src/castle_cli/manifest.py b/cli/src/wildpc_cli/manifest.py
similarity index 74%
rename from cli/src/castle_cli/manifest.py
rename to cli/src/wildpc_cli/manifest.py
index 9b73fc1..91a1757 100644
--- a/cli/src/castle_cli/manifest.py
+++ b/cli/src/wildpc_cli/manifest.py
@@ -1,7 +1,7 @@
-"""Re-export from castle-core for backward compatibility."""
+"""Re-export from wildpc-core for backward compatibility."""
-from castle_core.manifest import * # noqa: F401, F403
-from castle_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
+from wildpc_core.manifest import * # noqa: F401, F403
+from wildpc_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
BuildSpec,
CaddyDeployment,
CommandsSpec,
diff --git a/cli/src/castle_cli/commands/__init__.py b/cli/src/wildpc_cli/templates/__init__.py
similarity index 100%
rename from cli/src/castle_cli/commands/__init__.py
rename to cli/src/wildpc_cli/templates/__init__.py
diff --git a/cli/src/castle_cli/templates/scaffold.py b/cli/src/wildpc_cli/templates/scaffold.py
similarity index 95%
rename from cli/src/castle_cli/templates/scaffold.py
rename to cli/src/wildpc_cli/templates/scaffold.py
index 319dc74..a8f201b 100644
--- a/cli/src/castle_cli/templates/scaffold.py
+++ b/cli/src/wildpc_cli/templates/scaffold.py
@@ -575,14 +575,14 @@ uv run ruff format . # Format
def _scaffold_supabase(project_dir: Path, name: str, description: str) -> None:
"""Scaffold a Patch-shaped app that targets the shared Supabase substrate.
- Produces migrations/ (applied to the substrate by `castle program build`),
+ Produces migrations/ (applied to the substrate by `wildpc program build`),
functions/ (deno edge functions), public/ (static UI served in place at
. by the gateway), and supabase.app.yaml (auth policy + wiring).
The app owns its code and stays repo-durable; only its rows/blobs live on the
shared substrate. Each app is isolated in its **own Postgres schema** (the app
- id) rather than sharing `public` — so `castle program build` creates+grants the
- schema and tracks migrations per-app, and `castle delete --purge-data` drops
+ id) rather than sharing `public` — so `wildpc program build` creates+grants the
+ schema and tracks migrations per-app, and `wildpc delete --purge-data` drops
the whole schema cleanly. Rows are further protected by RLS.
"""
ident = name.replace("-", "_") # a safe SQL/JS identifier — and the app schema
@@ -604,7 +604,7 @@ def _scaffold_supabase(project_dir: Path, name: str, description: str) -> None:
"""# Patch-shaped app targeting the shared Supabase substrate.
name: __NAME__
description: __DESC__
-substrate: supabase # the shared castle service this app deploys against
+substrate: supabase # the shared wildpc service this app deploys against
# auth policy: public | private | shared
# public — anyone with the URL (anon read/write, still RLS-gated)
@@ -614,7 +614,7 @@ auth: public
# shared: [alice, bob]
# This app is isolated in its own Postgres schema (created + exposed through
-# PostgREST by `castle program build`). The frontend selects it via
+# PostgREST by `wildpc program build`). The frontend selects it via
# supabase-js `db: { schema }`.
schema: __SCHEMA__
"""
@@ -625,7 +625,7 @@ schema: __SCHEMA__
_write(
project_dir / "migrations" / "0001_init.sql",
sub(
- """-- 0001_init: example table + RLS. Applied by `castle program build`
+ """-- 0001_init: example table + RLS. Applied by `wildpc program build`
-- via the versioned migration runner (tracked per-app in
-- .schema_migrations; only unapplied migrations run). Forward-only —
-- never edit an applied migration; add a new numbered file instead.
@@ -680,7 +680,7 @@ serve((_req: Request) => {
sub(
"""// Substrate wiring for __NAME__. The anon key is designed to be public
// (RLS enforces access) — paste yours here. Find it in the dashboard
-// Secrets page (or `castle mesh`/vault), secret name SUPABASE_ANON_KEY.
+// Secrets page (or `wildpc mesh`/vault), secret name SUPABASE_ANON_KEY.
window.APP = {
SUPABASE_URL: "https://supabase.lan",
SUPABASE_ANON_KEY: "PASTE_ANON_KEY_HERE",
@@ -748,23 +748,23 @@ window.APP = {
__DESC__
A **supabase-stack** app: it owns its code (this repo) and rents its backend from
-the shared Supabase substrate (the `supabase` castle service). Only its rows/blobs
+the shared Supabase substrate (the `supabase` wildpc service). Only its rows/blobs
live on the substrate; rebuild the rest from git anytime.
## Layout
- `migrations/` — versioned, idempotent Postgres migrations (forward-only). Applied
- to the substrate by `castle program build __NAME__`.
+ to the substrate by `wildpc program build __NAME__`.
- `functions/` — deno edge functions deployed to the substrate's edge-runtime.
- `public/` — static UI served in place at `/__NAME__/` by the gateway. Talks to
the substrate with `@supabase/supabase-js` + the public anon key (RLS-gated).
- `supabase.app.yaml` — substrate wiring + auth policy (public/private/shared).
## Develop
-- Edit `migrations/`, then `castle program build __NAME__` to apply new migrations
+- Edit `migrations/`, then `wildpc program build __NAME__` to apply new migrations
(re-running is a no-op — only unapplied migrations run).
- Set the anon key in `public/config.js` (find `SUPABASE_ANON_KEY` in the
dashboard Secrets page).
-- `castle apply` → served at `__NAME__.`.
+- `wildpc apply` → served at `__NAME__.`.
## Privacy note
RLS protects rows, not the static shell or Storage. For a `private`/`shared` app,
@@ -799,9 +799,9 @@ def _scaffold_hugo(project_dir: Path, name: str, description: str) -> None:
The canonical skeleton comes from Hugo's own `hugo new site`; on top of it we
overlay the pieces a bare skeleton lacks — the layouts needed to render a home
- page without a theme, sample content, and a castle-flavored hugo.toml/README.
+ page without a theme, sample content, and a wildpc-flavored hugo.toml/README.
- `castle program build` runs `hugo --gc --minify` → `public/`, which a caddy
+ `wildpc program build` runs `hugo --gc --minify` → `public/`, which a caddy
deployment serves in place at .. baseURL is `/` so assets
resolve at the root of the site's own subdomain. A theme (with its own asset
pipeline) can be dropped under themes/ later; such themes declare a two-step
@@ -912,7 +912,7 @@ description = "__DESC__"
title: "__NAME__"
---
-Welcome to **__NAME__** — a Hugo site on castle. Edit `content/_index.md` and
+Welcome to **__NAME__** — a Hugo site on wildpc. Edit `content/_index.md` and
add posts under `content/posts/`.
"""
),
@@ -942,7 +942,7 @@ Your first post. Edit me in `content/posts/`, or run `hugo new posts/next.md`.
__DESC__
-A [Hugo](https://gohugo.io) static site managed by castle.
+A [Hugo](https://gohugo.io) static site managed by wildpc.
## Develop
@@ -950,8 +950,8 @@ A [Hugo](https://gohugo.io) static site managed by castle.
## Build & serve
- castle program build __NAME__ # hugo --gc --minify -> public/
- castle apply __NAME__ # serve at __NAME__.
+ wildpc program build __NAME__ # hugo --gc --minify -> public/
+ wildpc apply __NAME__ # serve at __NAME__.
## Themes
diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py
index c4d4e2e..becafa0 100644
--- a/cli/tests/conftest.py
+++ b/cli/tests/conftest.py
@@ -1,4 +1,4 @@
-"""Shared fixtures for castle CLI tests."""
+"""Shared fixtures for wildpc CLI tests."""
from __future__ import annotations
@@ -51,11 +51,11 @@ def _store_for(spec: dict) -> str:
]
-def _write_castle_config(root: Path, config: dict) -> None:
- """Scatter a nested castle config dict into the on-disk layout: castle.yaml globals,
+def _write_wildpc_config(root: Path, config: dict) -> None:
+ """Scatter a nested wildpc config dict into the on-disk layout: wildpc.yaml globals,
programs/.yaml, and deployments//.yaml (fields modernized)."""
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
- (root / "castle.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
+ (root / "wildpc.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
programs = config.get("programs") or {}
if programs:
(root / "programs").mkdir(parents=True, exist_ok=True)
@@ -70,8 +70,8 @@ def _write_castle_config(root: Path, config: dict) -> None:
@pytest.fixture
-def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
- """Create a temporary castle root with directory-per-resource config."""
+def wildpc_root(tmp_path: Path) -> Generator[Path, None, None]:
+ """Create a temporary wildpc root with directory-per-resource config."""
config = {
"gateway": {"port": 18000},
"programs": {
@@ -129,7 +129,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
},
},
}
- _write_castle_config(tmp_path, config)
+ _write_wildpc_config(tmp_path, config)
# Create project directories
svc_dir = tmp_path / "test-svc"
@@ -143,9 +143,9 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
@pytest.fixture
-def castle_home(tmp_path: Path) -> Generator[Path, None, None]:
- """Create a temporary ~/.castle directory."""
- home = tmp_path / ".castle"
+def wildpc_home(tmp_path: Path) -> Generator[Path, None, None]:
+ """Create a temporary ~/.wildpc directory."""
+ home = tmp_path / ".wildpc"
home.mkdir()
(home / "generated").mkdir()
(home / "secrets").mkdir()
diff --git a/cli/tests/test_add.py b/cli/tests/test_add.py
index a8d9fa2..ea06634 100644
--- a/cli/tests/test_add.py
+++ b/cli/tests/test_add.py
@@ -1,4 +1,4 @@
-"""Tests for castle add — adopting existing repos as programs."""
+"""Tests for wildpc add — adopting existing repos as programs."""
from __future__ import annotations
@@ -6,17 +6,17 @@ from argparse import Namespace
from pathlib import Path
from unittest.mock import patch
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
-def _run_add(castle_root: Path, **kwargs: object) -> object:
+def _run_add(wildpc_root: Path, **kwargs: object) -> object:
with (
- patch("castle_cli.commands.add.load_config") as mock_load,
- patch("castle_cli.commands.add.save_config"),
+ patch("wildpc_cli.commands.add.load_config") as mock_load,
+ patch("wildpc_cli.commands.add.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
mock_load.return_value = config
- from castle_cli.commands.add import run_add
+ from wildpc_cli.commands.add import run_add
args = Namespace(name=None, description="", **kwargs)
rc = run_add(args)
@@ -24,49 +24,49 @@ def _run_add(castle_root: Path, **kwargs: object) -> object:
class TestAdd:
- def test_adopt_python_path_assigns_stack(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_adopt_python_path_assigns_stack(self, wildpc_root: Path, tmp_path: Path) -> None:
repo = tmp_path / "mytool"
repo.mkdir()
(repo / "pyproject.toml").write_text('[project]\nname = "mytool"\ndependencies = []\n')
- rc, config = _run_add(castle_root, target=str(repo))
+ rc, config = _run_add(wildpc_root, target=str(repo))
assert rc == 0
prog = config.programs["mytool"]
assert prog.stack == "python-cli" # detected
assert prog.source == str(repo.resolve())
- def test_adopt_fastapi_detects_daemon(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_adopt_fastapi_detects_daemon(self, wildpc_root: Path, tmp_path: Path) -> None:
repo = tmp_path / "svc"
repo.mkdir()
(repo / "pyproject.toml").write_text(
'[project]\nname = "svc"\ndependencies = ["fastapi>=0.1"]\n'
)
- rc, config = _run_add(castle_root, target=str(repo))
+ rc, config = _run_add(wildpc_root, target=str(repo))
assert rc == 0
# `add` adopts source only — no deployment yet (kind is a deployment
# property); a fastapi project is detected as the python-fastapi stack.
assert config.programs["svc"].stack == "python-fastapi"
assert config.deployments_of("svc") == []
- def test_adopt_rust_declares_commands(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_adopt_rust_declares_commands(self, wildpc_root: Path, tmp_path: Path) -> None:
repo = tmp_path / "rusty"
repo.mkdir()
(repo / "Cargo.toml").write_text('[package]\nname = "rusty"\n')
- rc, config = _run_add(castle_root, target=str(repo))
+ rc, config = _run_add(wildpc_root, target=str(repo))
assert rc == 0
prog = config.programs["rusty"]
- assert prog.stack is None # no castle stack for rust
+ assert prog.stack is None # no wildpc stack for rust
# build lands in BuildSpec; other verbs in CommandsSpec
assert prog.build is not None
assert prog.build.commands == [["cargo", "build", "--release"]]
assert prog.commands is not None
assert prog.commands.run == [["cargo", "run"]]
- def test_adopt_git_url_records_repo(self, castle_root: Path) -> None:
- rc, config = _run_add(castle_root, target="https://github.com/someone/widget.git")
+ def test_adopt_git_url_records_repo(self, wildpc_root: Path) -> None:
+ rc, config = _run_add(wildpc_root, target="https://github.com/someone/widget.git")
assert rc == 0
prog = config.programs["widget"]
assert prog.repo == "https://github.com/someone/widget.git"
- def test_missing_path_fails(self, castle_root: Path, tmp_path: Path) -> None:
- rc, _ = _run_add(castle_root, target=str(tmp_path / "nope"))
+ def test_missing_path_fails(self, wildpc_root: Path, tmp_path: Path) -> None:
+ rc, _ = _run_add(wildpc_root, target=str(tmp_path / "nope"))
assert rc == 1
diff --git a/cli/tests/test_create.py b/cli/tests/test_create.py
index 31cfdfd..937a329 100644
--- a/cli/tests/test_create.py
+++ b/cli/tests/test_create.py
@@ -1,4 +1,4 @@
-"""Tests for castle create command."""
+"""Tests for wildpc create command."""
from __future__ import annotations
@@ -6,24 +6,24 @@ from argparse import Namespace
from pathlib import Path
from unittest.mock import patch
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
class TestCreateCommand:
"""Tests for the create command."""
- def test_create_service(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_create_service(self, wildpc_root: Path, tmp_path: Path) -> None:
"""Create a new service project."""
repos = tmp_path / "repos"
with (
- patch("castle_cli.commands.create.load_config") as mock_load,
- patch("castle_cli.commands.create.save_config") as mock_save,
+ patch("wildpc_cli.commands.create.load_config") as mock_load,
+ patch("wildpc_cli.commands.create.save_config") as mock_save,
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.repos_dir = repos
mock_load.return_value = config
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
args = Namespace(
name="my-api",
@@ -51,18 +51,18 @@ class TestCreateCommand:
assert svc.program == "my-api"
mock_save.assert_called_once()
- def test_create_tool(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_create_tool(self, wildpc_root: Path, tmp_path: Path) -> None:
"""Create a new tool project."""
repos = tmp_path / "repos"
with (
- patch("castle_cli.commands.create.load_config") as mock_load,
- patch("castle_cli.commands.create.save_config"),
+ patch("wildpc_cli.commands.create.load_config") as mock_load,
+ patch("wildpc_cli.commands.create.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.repos_dir = repos
mock_load.return_value = config
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
args = Namespace(name="my-tool2", stack="python-cli", description="My tool", port=None)
result = run_create(args)
@@ -77,19 +77,19 @@ class TestCreateCommand:
assert config.tools["my-tool2"].manager == "path"
assert config.deployments_of("my-tool2") == [("my-tool2", "tool")]
- def test_create_supabase_app(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_create_supabase_app(self, wildpc_root: Path, tmp_path: Path) -> None:
"""A supabase app scaffolds a Patch-shaped project registered as a static
frontend (build.outputs=[public]) with no service."""
repos = tmp_path / "repos"
with (
- patch("castle_cli.commands.create.load_config") as mock_load,
- patch("castle_cli.commands.create.save_config"),
+ patch("wildpc_cli.commands.create.load_config") as mock_load,
+ patch("wildpc_cli.commands.create.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.repos_dir = repos
mock_load.return_value = config
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
args = Namespace(name="guestbook", stack="supabase", description="Guestbook", port=None)
result = run_create(args)
@@ -114,13 +114,13 @@ class TestCreateCommand:
assert [(r.kind, r.ref) for r in dep.requires] == [("deployment", "supabase")]
assert config.deployments_of("guestbook") == [("guestbook", "static")]
- def test_create_duplicate_fails(self, castle_root: Path, capsys: object) -> None:
+ def test_create_duplicate_fails(self, wildpc_root: Path, capsys: object) -> None:
"""Creating a project with existing name fails."""
- with patch("castle_cli.commands.create.load_config") as mock_load:
- config = load_config(castle_root)
+ with patch("wildpc_cli.commands.create.load_config") as mock_load:
+ config = load_config(wildpc_root)
mock_load.return_value = config
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
# test-svc exists in the services section
args = Namespace(
@@ -133,17 +133,17 @@ class TestCreateCommand:
assert result == 1
- def test_create_auto_port(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_create_auto_port(self, wildpc_root: Path, tmp_path: Path) -> None:
"""Service creation auto-assigns next available port."""
with (
- patch("castle_cli.commands.create.load_config") as mock_load,
- patch("castle_cli.commands.create.save_config"),
+ patch("wildpc_cli.commands.create.load_config") as mock_load,
+ patch("wildpc_cli.commands.create.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.repos_dir = tmp_path / "repos"
mock_load.return_value = config
- from castle_cli.commands.create import run_create
+ from wildpc_cli.commands.create import run_create
args = Namespace(
name="auto-port-svc",
diff --git a/cli/tests/test_delete.py b/cli/tests/test_delete.py
index 06bbe82..f2145b3 100644
--- a/cli/tests/test_delete.py
+++ b/cli/tests/test_delete.py
@@ -1,4 +1,4 @@
-"""Tests for castle delete."""
+"""Tests for wildpc delete."""
from __future__ import annotations
@@ -6,17 +6,17 @@ from argparse import Namespace
from pathlib import Path
from unittest.mock import patch
-from castle_cli.config import load_config
+from wildpc_cli.config import load_config
-def _run_delete(castle_root: Path, **kwargs: object) -> object:
+def _run_delete(wildpc_root: Path, **kwargs: object) -> object:
with (
- patch("castle_cli.commands.delete.load_config") as mock_load,
- patch("castle_cli.commands.delete.save_config"),
+ patch("wildpc_cli.commands.delete.load_config") as mock_load,
+ patch("wildpc_cli.commands.delete.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
mock_load.return_value = config
- from castle_cli.commands.delete import run_delete
+ from wildpc_cli.commands.delete import run_delete
args = Namespace(source=False, yes=True, **kwargs)
rc = run_delete(args)
@@ -24,43 +24,43 @@ def _run_delete(castle_root: Path, **kwargs: object) -> object:
class TestDelete:
- def test_delete_program(self, castle_root: Path) -> None:
- rc, config = _run_delete(castle_root, name="test-tool")
+ def test_delete_program(self, wildpc_root: Path) -> None:
+ rc, config = _run_delete(wildpc_root, name="test-tool")
assert rc == 0
assert "test-tool" not in config.programs
- def test_delete_unknown_fails(self, castle_root: Path) -> None:
- rc, _ = _run_delete(castle_root, name="does-not-exist")
+ def test_delete_unknown_fails(self, wildpc_root: Path) -> None:
+ rc, _ = _run_delete(wildpc_root, name="does-not-exist")
assert rc == 1
- def test_delete_source_removes_dir(self, castle_root: Path, tmp_path: Path) -> None:
+ def test_delete_source_removes_dir(self, wildpc_root: Path, tmp_path: Path) -> None:
# Point a program's source at a real temp dir, then delete with --source.
src = tmp_path / "victim"
src.mkdir()
(src / "file.txt").write_text("x")
with (
- patch("castle_cli.commands.delete.load_config") as mock_load,
- patch("castle_cli.commands.delete.save_config"),
+ patch("wildpc_cli.commands.delete.load_config") as mock_load,
+ patch("wildpc_cli.commands.delete.save_config"),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.programs["test-tool"].source = str(src)
mock_load.return_value = config
- from castle_cli.commands.delete import run_delete
+ from wildpc_cli.commands.delete import run_delete
rc = run_delete(Namespace(name="test-tool", source=True, yes=True))
assert rc == 0
assert not src.exists()
- def test_abort_without_yes_and_no_input(self, castle_root: Path) -> None:
+ def test_abort_without_yes_and_no_input(self, wildpc_root: Path) -> None:
# No --yes and no stdin → aborts safely (returns 1, leaves entry).
with (
- patch("castle_cli.commands.delete.load_config") as mock_load,
- patch("castle_cli.commands.delete.save_config"),
+ patch("wildpc_cli.commands.delete.load_config") as mock_load,
+ patch("wildpc_cli.commands.delete.save_config"),
patch("builtins.input", side_effect=EOFError),
):
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
mock_load.return_value = config
- from castle_cli.commands.delete import run_delete
+ from wildpc_cli.commands.delete import run_delete
rc = run_delete(Namespace(name="test-tool", source=False, yes=False))
assert rc == 1
diff --git a/cli/tests/test_doctor.py b/cli/tests/test_doctor.py
index 8f94742..244e69c 100644
--- a/cli/tests/test_doctor.py
+++ b/cli/tests/test_doctor.py
@@ -1,4 +1,4 @@
-"""Tests for castle doctor."""
+"""Tests for wildpc doctor."""
from __future__ import annotations
@@ -7,7 +7,7 @@ from pathlib import Path
from unittest.mock import patch
import pytest
-from castle_cli.commands.doctor import (
+from wildpc_cli.commands.doctor import (
FAIL,
OK,
WARN,
@@ -20,13 +20,13 @@ from castle_cli.commands.doctor import (
class TestDoctor:
"""The diagnosis path — a bare, unconfigured node should fail loudly."""
- def test_bare_node_reports_problems(self, castle_root: Path, capsys: object) -> None:
+ def test_bare_node_reports_problems(self, wildpc_root: Path, capsys: object) -> None:
"""No repo:, no control plane, nothing running → exit 1 with fix hints."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- # The shared fixture has no repo: and no castle-gateway/api/dashboard, so the
+ # The shared fixture has no repo: and no wildpc-gateway/api/dashboard, so the
# Configuration and Runtime sections must FAIL. Patch where doctor imports it.
- with patch("castle_core.config.load_config", return_value=load_config(castle_root)):
+ with patch("wildpc_core.config.load_config", return_value=load_config(wildpc_root)):
result = run_doctor(Namespace())
assert result == 1
@@ -34,11 +34,11 @@ class TestDoctor:
assert "repo: not set" in out
assert "control plane missing" in out
# Every failing check offers a concrete next command.
- assert "castle apply" in out
+ assert "wildpc apply" in out
def test_load_failure_is_first_fail(self, capsys: object) -> None:
- """A castle.yaml that won't load is surfaced as a FAIL, not a traceback."""
- with patch("castle_core.config.load_config", side_effect=ValueError("bad yaml")):
+ """A wildpc.yaml that won't load is surfaced as a FAIL, not a traceback."""
+ with patch("wildpc_core.config.load_config", side_effect=ValueError("bad yaml")):
result = run_doctor(Namespace())
assert result == 1
@@ -48,31 +48,31 @@ class TestDoctor:
class TestDataDirChecks:
- """The drift-prevention checks: data_dir must be writable, and a CASTLE_DATA_DIR env
+ """The drift-prevention checks: data_dir must be writable, and a WILDPC_DATA_DIR env
override (the one way the CLI and api can still diverge) must be surfaced."""
- def _config(self, castle_root: Path):
- from castle_cli.config import load_config
+ def _config(self, wildpc_root: Path):
+ from wildpc_cli.config import load_config
- return load_config(castle_root)
+ return load_config(wildpc_root)
def test_writable_dir_ok_no_warn(
- self, castle_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ self, wildpc_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.delenv("CASTLE_DATA_DIR", raising=False)
- monkeypatch.delenv("CASTLE_REPOS_DIR", raising=False)
- cfg = self._config(castle_root)
+ monkeypatch.delenv("WILDPC_DATA_DIR", raising=False)
+ monkeypatch.delenv("WILDPC_REPOS_DIR", raising=False)
+ cfg = self._config(wildpc_root)
cfg.data_dir = tmp_path # exists + writable
checks = _check_configuration(cfg)
by_label = {c.label: c for c in checks}
assert by_label["data dir writable"].status == OK
- assert not any("overrides castle.yaml" in c.label for c in checks)
+ assert not any("overrides wildpc.yaml" in c.label for c in checks)
def test_missing_dir_fails_with_hint(
- self, castle_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ self, wildpc_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.delenv("CASTLE_DATA_DIR", raising=False)
- cfg = self._config(castle_root)
+ monkeypatch.delenv("WILDPC_DATA_DIR", raising=False)
+ cfg = self._config(wildpc_root)
missing = tmp_path / "nope"
cfg.data_dir = missing
fail = next(
@@ -82,16 +82,16 @@ class TestDataDirChecks:
assert fail.hint # offers a concrete fix
def test_env_override_warns(
- self, castle_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ self, wildpc_root: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- """A CASTLE_DATA_DIR env var overrides the single-source-of-truth file — the
+ """A WILDPC_DATA_DIR env var overrides the single-source-of-truth file — the
exact CLI/api divergence we fixed. Doctor must WARN."""
- monkeypatch.setenv("CASTLE_DATA_DIR", str(tmp_path))
- cfg = self._config(castle_root)
+ monkeypatch.setenv("WILDPC_DATA_DIR", str(tmp_path))
+ cfg = self._config(wildpc_root)
cfg.data_dir = tmp_path
- warn = next(c for c in _check_configuration(cfg) if "overrides castle.yaml" in c.label)
+ warn = next(c for c in _check_configuration(cfg) if "overrides wildpc.yaml" in c.label)
assert warn.status == WARN
- assert "CASTLE_DATA_DIR" in warn.detail
+ assert "WILDPC_DATA_DIR" in warn.detail
class TestStackChecks:
@@ -99,8 +99,8 @@ class TestStackChecks:
run. Missing tooling for an enabled deployment is a FAIL with a copyable fix."""
def _fastapi_cfg(self):
- import castle_core.config as C
- from castle_core.manifest import ProgramSpec, SystemdDeployment
+ import wildpc_core.config as C
+ from wildpc_core.manifest import ProgramSpec, SystemdDeployment
prog = ProgramSpec(id="svc", stack="python-fastapi")
dep = SystemdDeployment.model_validate(
@@ -110,7 +110,7 @@ class TestStackChecks:
"run": {"launcher": "command", "argv": ["svc"]},
}
)
- return C.CastleConfig(
+ return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000),
repo=None,
@@ -119,7 +119,7 @@ class TestStackChecks:
)
def test_present_tool_is_ok(self, monkeypatch: pytest.MonkeyPatch) -> None:
- import castle_core.stack_status as SS
+ import wildpc_core.stack_status as SS
monkeypatch.setattr(SS, "_tool_available", lambda dep, tool: True)
fa = next(
@@ -130,7 +130,7 @@ class TestStackChecks:
def test_missing_tool_for_enabled_deployment_fails_with_hint(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
- import castle_core.stack_status as SS
+ import wildpc_core.stack_status as SS
monkeypatch.setattr(SS, "_tool_available", lambda dep, tool: False)
fa = next(
@@ -141,7 +141,7 @@ class TestStackChecks:
def test_unused_stacks_are_skipped(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""No react-vite program → no pnpm nag."""
- import castle_core.stack_status as SS
+ import wildpc_core.stack_status as SS
monkeypatch.setattr(SS, "_tool_available", lambda dep, tool: True)
labels = [c.label for c in _check_stacks(self._fastapi_cfg())]
diff --git a/cli/tests/test_info.py b/cli/tests/test_info.py
index 57313f7..8bc5b6f 100644
--- a/cli/tests/test_info.py
+++ b/cli/tests/test_info.py
@@ -1,4 +1,4 @@
-"""Tests for castle info command."""
+"""Tests for wildpc info command."""
from __future__ import annotations
@@ -11,14 +11,14 @@ from unittest.mock import patch
class TestInfoCommand:
"""Tests for the info command."""
- def test_info_service(self, castle_root: Path, capsys) -> None:
+ def test_info_service(self, wildpc_root: Path, capsys) -> None:
"""Show info for a service."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="test-svc", json=False))
@@ -28,14 +28,14 @@ class TestInfoCommand:
assert "service" in output
assert "19000" in output
- def test_info_tool(self, castle_root: Path, capsys) -> None:
+ def test_info_tool(self, wildpc_root: Path, capsys) -> None:
"""Show info for a tool."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="test-tool", json=False))
@@ -44,14 +44,14 @@ class TestInfoCommand:
assert "test-tool" in output
assert "tool" in output
- def test_info_not_found(self, castle_root: Path, capsys) -> None:
+ def test_info_not_found(self, wildpc_root: Path, capsys) -> None:
"""Info for nonexistent component returns error."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="nope", json=False))
@@ -59,14 +59,14 @@ class TestInfoCommand:
output = capsys.readouterr().out
assert "nope" in output
- def test_info_json_service(self, castle_root: Path, capsys) -> None:
+ def test_info_json_service(self, wildpc_root: Path, capsys) -> None:
"""--json produces valid JSON with service fields."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="test-svc", json=True))
@@ -76,14 +76,14 @@ class TestInfoCommand:
assert data["kind"] == "service"
assert data["service"]["expose"]["http"]["internal"]["port"] == 19000
- def test_info_shows_env(self, castle_root: Path, capsys) -> None:
+ def test_info_shows_env(self, wildpc_root: Path, capsys) -> None:
"""Info displays environment variables."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="test-svc", json=False))
@@ -91,14 +91,14 @@ class TestInfoCommand:
output = capsys.readouterr().out
assert "TEST_SVC_DATA_DIR" in output
- def test_info_shows_kind(self, castle_root: Path, capsys) -> None:
+ def test_info_shows_kind(self, wildpc_root: Path, capsys) -> None:
"""Info displays the derived kind."""
- from castle_cli.config import load_config
+ from wildpc_cli.config import load_config
- with patch("castle_cli.commands.info.load_config") as mock_load:
- mock_load.return_value = load_config(castle_root)
+ with patch("wildpc_cli.commands.info.load_config") as mock_load:
+ mock_load.return_value = load_config(wildpc_root)
- from castle_cli.commands.info import run_info
+ from wildpc_cli.commands.info import run_info
result = run_info(Namespace(name="test-svc", json=False))
diff --git a/cli/tests/test_list.py b/cli/tests/test_list.py
index b6695af..44dd868 100644
--- a/cli/tests/test_list.py
+++ b/cli/tests/test_list.py
@@ -1,4 +1,4 @@
-"""Tests for castle list command."""
+"""Tests for wildpc list command."""
from __future__ import annotations
@@ -7,18 +7,18 @@ from argparse import Namespace
from pathlib import Path
from unittest.mock import patch
-from castle_cli.commands.list_cmd import run_list
+from wildpc_cli.commands.list_cmd import run_list
class TestListCommand:
"""Tests for the list command."""
- def test_list_all(self, castle_root: Path, capsys: object) -> None:
+ def test_list_all(self, wildpc_root: Path, capsys: object) -> None:
"""List all components."""
- with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
- from castle_cli.config import load_config
+ with patch("wildpc_cli.commands.list_cmd.load_config") as mock_load:
+ from wildpc_cli.config import load_config
- mock_load.return_value = load_config(castle_root)
+ mock_load.return_value = load_config(wildpc_root)
args = Namespace(kind=None, stack=None, json=False)
result = run_list(args)
@@ -27,12 +27,12 @@ class TestListCommand:
assert "test-svc" in captured.out
assert "test-tool" in captured.out
- def test_list_filter_daemon(self, castle_root: Path, capsys: object) -> None:
+ def test_list_filter_daemon(self, wildpc_root: Path, capsys: object) -> None:
"""--behavior filters the program catalog by its real behavior field."""
- with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
- from castle_cli.config import load_config
+ with patch("wildpc_cli.commands.list_cmd.load_config") as mock_load:
+ from wildpc_cli.config import load_config
- mock_load.return_value = load_config(castle_root)
+ mock_load.return_value = load_config(wildpc_root)
args = Namespace(kind="service", stack=None, json=False)
result = run_list(args)
@@ -43,12 +43,12 @@ class TestListCommand:
# Services/jobs are deployment views, not behaviors — hidden under a filter.
assert "test-svc" not in captured.out
- def test_list_filter_tool(self, castle_root: Path, capsys: object) -> None:
+ def test_list_filter_tool(self, wildpc_root: Path, capsys: object) -> None:
"""List filtered to tools."""
- with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
- from castle_cli.config import load_config
+ with patch("wildpc_cli.commands.list_cmd.load_config") as mock_load:
+ from wildpc_cli.config import load_config
- mock_load.return_value = load_config(castle_root)
+ mock_load.return_value = load_config(wildpc_root)
args = Namespace(kind="tool", stack=None, json=False)
result = run_list(args)
@@ -57,12 +57,12 @@ class TestListCommand:
assert "test-tool" in captured.out
assert "test-svc" not in captured.out
- def test_list_jobs_are_deployments(self, castle_root: Path, capsys: object) -> None:
+ def test_list_jobs_are_deployments(self, wildpc_root: Path, capsys: object) -> None:
"""Jobs are a deployment view: shown unfiltered, hidden under a behavior filter."""
- with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
- from castle_cli.config import load_config
+ with patch("wildpc_cli.commands.list_cmd.load_config") as mock_load:
+ from wildpc_cli.config import load_config
- mock_load.return_value = load_config(castle_root)
+ mock_load.return_value = load_config(wildpc_root)
# Unfiltered: the job appears.
run_list(Namespace(kind=None, stack=None, json=False))
assert "test-job" in capsys.readouterr().out # type: ignore[attr-defined]
@@ -73,12 +73,12 @@ class TestListCommand:
assert "test-svc" not in out
assert "test-tool" in out
- def test_list_json(self, castle_root: Path, capsys: object) -> None:
+ def test_list_json(self, wildpc_root: Path, capsys: object) -> None:
"""JSON output tags each entry with its derived kind."""
- with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
- from castle_cli.config import load_config
+ with patch("wildpc_cli.commands.list_cmd.load_config") as mock_load:
+ from wildpc_cli.config import load_config
- mock_load.return_value = load_config(castle_root)
+ mock_load.return_value = load_config(wildpc_root)
args = Namespace(kind=None, stack=None, json=True)
result = run_list(args)
diff --git a/cli/tests/test_mesh.py b/cli/tests/test_mesh.py
index 7eaf5ec..8e6727f 100644
--- a/cli/tests/test_mesh.py
+++ b/cli/tests/test_mesh.py
@@ -1,4 +1,4 @@
-"""Tests for the castle mesh command (API calls mocked)."""
+"""Tests for the wildpc mesh command (API calls mocked)."""
from __future__ import annotations
@@ -6,7 +6,7 @@ import urllib.error
from argparse import Namespace
from unittest.mock import patch
-from castle_cli.commands.mesh import run_mesh
+from wildpc_cli.commands.mesh import run_mesh
class TestMeshCommand:
@@ -15,7 +15,7 @@ class TestMeshCommand:
"enabled": True, "connected": True, "nats_url": "tls://x:4222",
"peer_count": 1, "peers": ["primer"],
}
- with patch("castle_cli.commands.mesh._get", return_value=data):
+ with patch("wildpc_cli.commands.mesh._get", return_value=data):
rc = run_mesh(Namespace(mesh_command="status"))
assert rc == 0
out = capsys.readouterr().out # type: ignore[attr-defined]
@@ -28,7 +28,7 @@ class TestMeshCommand:
{"hostname": "primer", "online": True, "is_stale": False,
"deployed_count": 3, "is_local": False},
]
- with patch("castle_cli.commands.mesh._get", return_value=data):
+ with patch("wildpc_cli.commands.mesh._get", return_value=data):
rc = run_mesh(Namespace(mesh_command="nodes"))
assert rc == 0
out = capsys.readouterr().out # type: ignore[attr-defined]
@@ -36,7 +36,7 @@ class TestMeshCommand:
def test_config_list(self, capsys: object) -> None:
data = {"role": "authority", "keys": ["fleet/motd"]}
- with patch("castle_cli.commands.mesh._get", return_value=data):
+ with patch("wildpc_cli.commands.mesh._get", return_value=data):
rc = run_mesh(
Namespace(mesh_command="config", mesh_config_command="list")
)
@@ -45,7 +45,7 @@ class TestMeshCommand:
def test_config_set_calls_put(self) -> None:
with patch(
- "castle_cli.commands.mesh._put", return_value={"ok": True}
+ "wildpc_cli.commands.mesh._put", return_value={"ok": True}
) as put:
rc = run_mesh(
Namespace(
@@ -60,7 +60,7 @@ class TestMeshCommand:
def test_api_unreachable_returns_1(self, capsys: object) -> None:
with patch(
- "castle_cli.commands.mesh._get",
+ "wildpc_cli.commands.mesh._get",
side_effect=urllib.error.URLError("refused"),
):
rc = run_mesh(Namespace(mesh_command="status"))
diff --git a/cli/tests/test_secret.py b/cli/tests/test_secret.py
index 3db994a..bd4806e 100644
--- a/cli/tests/test_secret.py
+++ b/cli/tests/test_secret.py
@@ -1,4 +1,4 @@
-"""Tests for the `castle secret` command (reads/writes the active backend)."""
+"""Tests for the `wildpc secret` command (reads/writes the active backend)."""
from __future__ import annotations
@@ -13,13 +13,13 @@ def file_secrets(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Force the active backend to a temp file store."""
secrets = tmp_path / "secrets"
secrets.mkdir()
- monkeypatch.setenv("CASTLE_SECRET_BACKEND", "file")
- monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets)
+ monkeypatch.setenv("WILDPC_SECRET_BACKEND", "file")
+ monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets)
return secrets
def _run(**kw: object) -> int:
- from castle_cli.commands.secret import run_secret
+ from wildpc_cli.commands.secret import run_secret
return run_secret(Namespace(**kw))
diff --git a/cli/tests/test_tool.py b/cli/tests/test_tool.py
index 9ef97a5..3bd5f0d 100644
--- a/cli/tests/test_tool.py
+++ b/cli/tests/test_tool.py
@@ -1,4 +1,4 @@
-"""Tests for the `castle tool` lens."""
+"""Tests for the `wildpc tool` lens."""
from __future__ import annotations
@@ -8,16 +8,16 @@ from pathlib import Path
from unittest.mock import patch
-def _config(castle_root: Path):
- from castle_cli.config import load_config
+def _config(wildpc_root: Path):
+ from wildpc_cli.config import load_config
- return load_config(castle_root)
+ return load_config(wildpc_root)
class TestToolList:
- def test_list_includes_tool(self, castle_root: Path, capsys: object) -> None:
- with patch("castle_cli.commands.tool.load_config", return_value=_config(castle_root)):
- from castle_cli.commands.tool import run_tool_list
+ def test_list_includes_tool(self, wildpc_root: Path, capsys: object) -> None:
+ with patch("wildpc_cli.commands.tool.load_config", return_value=_config(wildpc_root)):
+ from wildpc_cli.commands.tool import run_tool_list
rc = run_tool_list(Namespace(json=False))
assert rc == 0
@@ -26,9 +26,9 @@ class TestToolList:
# test-svc is a service (systemd), not a tool — must not appear.
assert "test-svc" not in out
- def test_list_json_payload(self, castle_root: Path, capsys: object) -> None:
- with patch("castle_cli.commands.tool.load_config", return_value=_config(castle_root)):
- from castle_cli.commands.tool import run_tool_list
+ def test_list_json_payload(self, wildpc_root: Path, capsys: object) -> None:
+ with patch("wildpc_cli.commands.tool.load_config", return_value=_config(wildpc_root)):
+ from wildpc_cli.commands.tool import run_tool_list
rc = run_tool_list(Namespace(json=True))
assert rc == 0
@@ -41,9 +41,9 @@ class TestToolList:
class TestToolInfo:
- def test_info_json(self, castle_root: Path, capsys: object) -> None:
- with patch("castle_cli.commands.tool.load_config", return_value=_config(castle_root)):
- from castle_cli.commands.tool import run_tool_info
+ def test_info_json(self, wildpc_root: Path, capsys: object) -> None:
+ with patch("wildpc_cli.commands.tool.load_config", return_value=_config(wildpc_root)):
+ from wildpc_cli.commands.tool import run_tool_info
rc = run_tool_info(Namespace(name="test-tool", json=True))
assert rc == 0
@@ -51,10 +51,10 @@ class TestToolInfo:
assert data["name"] == "test-tool"
assert data["executables"]
- def test_info_rejects_non_tool(self, castle_root: Path, capsys: object) -> None:
+ def test_info_rejects_non_tool(self, wildpc_root: Path, capsys: object) -> None:
# test-svc is a service deployment, not a tool.
- with patch("castle_cli.commands.tool.load_config", return_value=_config(castle_root)):
- from castle_cli.commands.tool import run_tool_info
+ with patch("wildpc_cli.commands.tool.load_config", return_value=_config(wildpc_root)):
+ from wildpc_cli.commands.tool import run_tool_info
rc = run_tool_info(Namespace(name="test-svc", json=False))
assert rc == 1
diff --git a/cli/uv.lock b/cli/uv.lock
deleted file mode 100644
index 7938386..0000000
--- a/cli/uv.lock
+++ /dev/null
@@ -1,425 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.11"
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "castle-cli"
-version = "0.1.0"
-source = { editable = "." }
-dependencies = [
- { name = "jinja2" },
- { name = "pydantic" },
- { name = "pyyaml" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "pyright" },
- { name = "pytest" },
- { name = "ruff" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "jinja2", specifier = ">=3.1.0" },
- { name = "pydantic", specifier = ">=2.0.0" },
- { name = "pyyaml", specifier = ">=6.0.0" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "pyright", specifier = ">=1.1.0" },
- { name = "pytest", specifier = ">=7.0.0" },
- { name = "ruff", specifier = ">=0.11.0" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
- { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
- { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
- { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
- { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
- { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
- { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
- { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
- { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
- { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
- { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
- { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
- { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
- { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
- { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
- { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
- { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
- { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
- { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
- { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
- { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
- { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
- { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
- { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
- { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
- { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
- { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
- { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
- { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
- { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
- { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
- { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
- { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
- { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
- { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
- { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
- { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
- { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
- { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
- { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
- { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
- { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
- { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
- { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
- { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
- { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
- { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
- { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
- { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
- { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
- { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
- { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
- { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
- { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
- { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
- { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
- { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
- { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
- { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
-]
-
-[[package]]
-name = "nodeenv"
-version = "1.10.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.12.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.41.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
- { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
- { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
- { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
- { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
- { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
- { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
- { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
- { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
- { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
- { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
- { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
- { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
- { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
- { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
- { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
- { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
- { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
- { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
- { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
- { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
- { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
- { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
- { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
- { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
- { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
- { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
- { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
- { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
- { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
- { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
- { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
- { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
- { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
- { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
- { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pyright"
-version = "1.1.408"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "nodeenv" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
-]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
- { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
- { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
- { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
- { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
- { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
- { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
- { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
- { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
- { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
- { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
- { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
- { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
- { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
- { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
- { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
- { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
- { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
- { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
- { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
- { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
- { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
- { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
- { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
- { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
- { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
- { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
- { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
- { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
-]
-
-[[package]]
-name = "ruff"
-version = "0.15.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
- { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
- { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
- { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
- { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
- { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
- { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
- { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
- { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
- { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
- { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
- { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
- { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
- { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
- { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
- { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
-]
diff --git a/core/pyproject.toml b/core/pyproject.toml
index 462d22f..e048b95 100644
--- a/core/pyproject.toml
+++ b/core/pyproject.toml
@@ -1,7 +1,7 @@
[project]
-name = "castle-core"
+name = "wildpc-core"
version = "0.1.0"
-description = "Castle platform core library - manifest models, config, and generators"
+description = "Wild PC platform core library - manifest models, config, and generators"
requires-python = ">=3.11"
dependencies = [
"pyyaml>=6.0.0",
@@ -14,7 +14,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
-packages = ["src/castle_core"]
+packages = ["src/wildpc_core"]
[dependency-groups]
dev = [
@@ -23,4 +23,4 @@ dev = [
]
[tool.ruff.lint.isort]
-known-first-party = ["castle_core"]
+known-first-party = ["wildpc_core"]
diff --git a/core/src/castle_core/__init__.py b/core/src/castle_core/__init__.py
deleted file mode 100644
index 97d088e..0000000
--- a/core/src/castle_core/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Castle core library - manifest models, configuration, and generators."""
-
-__version__ = "0.1.0"
diff --git a/core/src/wildpc_core/__init__.py b/core/src/wildpc_core/__init__.py
new file mode 100644
index 0000000..caf967f
--- /dev/null
+++ b/core/src/wildpc_core/__init__.py
@@ -0,0 +1,3 @@
+"""Wild PC core library - manifest models, configuration, and generators."""
+
+__version__ = "0.1.0"
diff --git a/core/src/castle_core/adopt.py b/core/src/wildpc_core/adopt.py
similarity index 91%
rename from core/src/castle_core/adopt.py
rename to core/src/wildpc_core/adopt.py
index 435a737..e48578f 100644
--- a/core/src/castle_core/adopt.py
+++ b/core/src/wildpc_core/adopt.py
@@ -1,4 +1,4 @@
-"""Adopt an existing repo as a program — shared by the CLI (`castle program add`)
+"""Adopt an existing repo as a program — shared by the CLI (`wildpc program add`)
and the API (`POST /programs/adopt`).
`create` scaffolds new code from a stack; `add` adopts code that already exists —
@@ -13,8 +13,8 @@ import tomllib
from dataclasses import dataclass, field
from pathlib import Path
-from castle_core.config import CastleConfig
-from castle_core.manifest import BuildSpec, CommandsSpec, ProgramSpec
+from wildpc_core.config import WildpcConfig
+from wildpc_core.manifest import BuildSpec, CommandsSpec, ProgramSpec
class AdoptError(ValueError):
@@ -26,7 +26,7 @@ def is_git_url(s: str) -> bool:
def looks_like_program(src: Path) -> bool:
- """Whether a directory holds something castle can adopt (a project manifest or
+ """Whether a directory holds something wildpc can adopt (a project manifest or
a git repo). Used to flag candidates in the filesystem browser."""
return (
(src / ".git").exists()
@@ -95,7 +95,7 @@ class Adopted:
def build_adopted_program(
- config: CastleConfig,
+ config: WildpcConfig,
target: str,
name: str | None = None,
description: str = "",
@@ -111,7 +111,7 @@ def build_adopted_program(
if is_git_url(target):
repo_url = target
name = name or Path(target.rstrip("/")).name.removesuffix(".git")
- # Default local clone location; cloned later via `castle clone`.
+ # Default local clone location; cloned later via `wildpc clone`.
source = str(config.repos_dir / name)
src_path = Path(source)
else:
@@ -122,11 +122,11 @@ def build_adopted_program(
name = name or src_path.name
if name in config.programs or config.deployments_named(name):
- raise AdoptError(f"'{name}' already exists in castle.yaml")
+ raise AdoptError(f"'{name}' already exists in wildpc.yaml")
# Detect verbs from the working copy if we have one on disk. `kind` is derived
# from a deployment, not stored on the program — so adoption takes source only;
- # declare a deployment separately (castle service/job create).
+ # declare a deployment separately (wildpc service/job create).
stack: str | None = None
detected: dict[str, list[list[str]]] = {}
if src_path.exists():
diff --git a/core/src/castle_core/audit.py b/core/src/wildpc_core/audit.py
similarity index 92%
rename from core/src/castle_core/audit.py
rename to core/src/wildpc_core/audit.py
index 29e6dcb..5db5005 100644
--- a/core/src/castle_core/audit.py
+++ b/core/src/wildpc_core/audit.py
@@ -1,7 +1,7 @@
"""Consumption audit — *suggests* undeclared ``requires`` by matching a
deployment's env endpoint values against known provider sockets.
-This is the one place castle looks at env → dependency, and it does so **only to
+This is the one place wildpc looks at env → dependency, and it does so **only to
propose a declaration the user confirms** — never to write, and never to feed the
graph or ``functional?``. The relationship graph stays strictly declaration-derived
(see docs/relationships.md, "Env is derived *from* requires, never scraped *into*
@@ -15,8 +15,8 @@ from __future__ import annotations
import re
from dataclasses import dataclass
-from castle_core.config import CastleConfig
-from castle_core.relations import build_model
+from wildpc_core.config import WildpcConfig
+from wildpc_core.relations import build_model
# Hosts that mean "a provider on this node" — a port match against them is a strong
# signal (ports are unique per host). 172.17.0.1 is the docker bridge to the host.
@@ -62,13 +62,13 @@ def _resolve(
out.append(Suggestion(consumer, pname, env_var, f"{host}:{port}", proto))
-def suggest_consumption(config: CastleConfig) -> list[Suggestion]:
+def suggest_consumption(config: WildpcConfig) -> list[Suggestion]:
"""Undeclared consumption suggestions, derived from env endpoint values.
Two shapes are recognized in a deployment's ``defaults.env``:
(a) ``host:port`` inside a single value (a URL, a ``DATABASE_URL``); and
- (b) a split ``X_HOST`` + ``X_PORT`` pair (e.g. ``CASTLE_API_MQTT_HOST`` +
- ``CASTLE_API_MQTT_PORT``). When the port resolves to a *local* provider's socket
+ (b) a split ``X_HOST`` + ``X_PORT`` pair (e.g. ``WILDPC_API_MQTT_HOST`` +
+ ``WILDPC_API_MQTT_PORT``). When the port resolves to a *local* provider's socket
and the consumer doesn't already declare it, propose the edge — deduped per
(consumer, provider). A bare ``*_PORT`` with no ``*_HOST`` is ignored: without an
explicit host it can't be told apart from the deployment's own listen port."""
diff --git a/core/src/castle_core/config.py b/core/src/wildpc_core/config.py
similarity index 88%
rename from core/src/castle_core/config.py
rename to core/src/wildpc_core/config.py
index f6bf406..7aeb870 100644
--- a/core/src/castle_core/config.py
+++ b/core/src/wildpc_core/config.py
@@ -1,4 +1,4 @@
-"""Castle configuration and registry management."""
+"""Wild PC configuration and registry management."""
from __future__ import annotations
@@ -12,9 +12,9 @@ import yaml
from pydantic import BaseModel, TypeAdapter
if TYPE_CHECKING:
- from castle_core.secret_backends import SecretBackend
+ from wildpc_core.secret_backends import SecretBackend
-from castle_core.manifest import (
+from wildpc_core.manifest import (
AgentSpec,
DeploymentSpec,
ProgramSpec,
@@ -26,24 +26,24 @@ from castle_core.manifest import (
_DEPLOYMENT_ADAPTER: TypeAdapter[DeploymentSpec] = TypeAdapter(DeploymentSpec)
-def _resolve_castle_home() -> Path:
- """Resolve the castle home directory (config, code, artifacts, secrets).
+def _resolve_wildpc_home() -> Path:
+ """Resolve the wildpc home directory (config, code, artifacts, secrets).
- Defaults to ~/.castle. Override with the CASTLE_HOME environment variable
+ Defaults to ~/.wildpc. Override with the WILDPC_HOME environment variable
(supports ~ and relative paths, which are expanded and made absolute).
"""
- override = os.environ.get("CASTLE_HOME")
+ override = os.environ.get("WILDPC_HOME")
if override:
return Path(override).expanduser().resolve()
- return Path.home() / ".castle"
+ return Path.home() / ".wildpc"
-_DEFAULT_DATA_DIR = Path("/data/castle")
+_DEFAULT_DATA_DIR = Path("/data/wildpc")
_DEFAULT_REPOS_DIR = Path("/data/repos")
-class CastleDirError(RuntimeError):
- """A required castle directory can't be created (e.g. data_dir outside a writable
+class WildpcDirError(RuntimeError):
+ """A required wildpc directory can't be created (e.g. data_dir outside a writable
parent). Carries an actionable message; surfaced to the CLI and the api instead of
a bare PermissionError traceback."""
@@ -51,10 +51,10 @@ class CastleDirError(RuntimeError):
def _resolve_root_path(
env_var: str, yaml_value: object, anchor: Path, default: Path
) -> Path:
- """Resolve a configurable root with precedence: env var > castle.yaml > default.
+ """Resolve a configurable root with precedence: env var > wildpc.yaml > default.
`~` is expanded; a relative path is anchored to `anchor` (the dir containing
- castle.yaml) — never cwd, so the CLI (shell cwd) and the api service (unit cwd)
+ wildpc.yaml) — never cwd, so the CLI (shell cwd) and the api service (unit cwd)
resolve identically. The built-in default is returned as-is (so it compares equal
for the "persist only when non-default" check in save_config)."""
raw = os.environ.get(env_var) or yaml_value
@@ -66,29 +66,29 @@ def _resolve_root_path(
return p.resolve()
-CASTLE_HOME = _resolve_castle_home()
-CODE_DIR = CASTLE_HOME / "code"
-ARTIFACTS_DIR = CASTLE_HOME / "artifacts"
+WILDPC_HOME = _resolve_wildpc_home()
+CODE_DIR = WILDPC_HOME / "code"
+ARTIFACTS_DIR = WILDPC_HOME / "artifacts"
SPECS_DIR = ARTIFACTS_DIR / "specs"
CONTENT_DIR = ARTIFACTS_DIR / "content"
-SECRETS_DIR = CASTLE_HOME / "secrets"
-# data_dir and repos_dir are deliberately NOT module constants. Unlike the CASTLE_HOME
-# family above (env-or-default — the dir that *holds* castle.yaml can't be configured
-# inside it), these are per-instance settings read from castle.yaml. A module global
+SECRETS_DIR = WILDPC_HOME / "secrets"
+# data_dir and repos_dir are deliberately NOT module constants. Unlike the WILDPC_HOME
+# family above (env-or-default — the dir that *holds* wildpc.yaml can't be configured
+# inside it), these are per-instance settings read from wildpc.yaml. A module global
# would be a second copy of that value, resolved once at import against one process's
# environment — exactly what let the CLI and the api service drift. They live only on
-# the loaded CastleConfig; read config.data_dir / config.repos_dir (see load_config).
+# the loaded WildpcConfig; read config.data_dir / config.repos_dir (see load_config).
# User tool directories — the single source of truth for "where our CLIs live".
# Used both at build time (dev-verb subprocess PATH) and at run time (generated
-# systemd unit PATH) so a service sees the same tools castle used to build it.
+# systemd unit PATH) so a service sees the same tools wildpc used to build it.
# Order matters: pnpm's modern standalone installer puts its shim in
# $PNPM_HOME/bin, which must win over the bare dir (older installs leave a stale
# version wrapper there). nvm/node is intentionally omitted — it's versioned and
# brittle to hardcode. A program's node is resolved per-program from its declared
# pin (.node-version/.nvmrc/engines) and prepended ahead of these dirs at both the
# build subprocess (stacks._build_env) and the runtime unit PATH (Deployment
-# .path_prepend) — see castle_core.toolchains.
+# .path_prepend) — see wildpc_core.toolchains.
USER_TOOL_PATH_DIRS = [
Path.home() / ".local" / "bin",
Path.home() / ".local" / "share" / "pnpm" / "bin",
@@ -102,24 +102,24 @@ GENERATED_DIR = SPECS_DIR
STATIC_DIR = CONTENT_DIR
-def find_castle_root() -> Path:
- """Find the castle config root (directory containing castle.yaml).
+def find_wildpc_root() -> Path:
+ """Find the wildpc config root (directory containing wildpc.yaml).
Search order:
- 1. ~/.castle/castle.yaml (the canonical instance location)
+ 1. ~/.wildpc/wildpc.yaml (the canonical instance location)
2. Walk up from cwd (for development/testing)
"""
# Canonical location first
- if (CASTLE_HOME / "castle.yaml").exists():
- return CASTLE_HOME
+ if (WILDPC_HOME / "wildpc.yaml").exists():
+ return WILDPC_HOME
# Fallback: walk up from cwd
current = Path.cwd()
while current != current.parent:
- if (current / "castle.yaml").exists():
+ if (current / "wildpc.yaml").exists():
return current
current = current.parent
raise FileNotFoundError(
- f"Could not find castle.yaml.\nExpected at: {CASTLE_HOME / 'castle.yaml'}"
+ f"Could not find wildpc.yaml.\nExpected at: {WILDPC_HOME / 'wildpc.yaml'}"
)
@@ -142,7 +142,7 @@ class GatewayConfig:
# names never appear in public DNS. `tunnel_id` is the cloudflared tunnel UUID.
public_domain: str | None = None
tunnel_id: str | None = None
- # acme mode only: emit the `events { on cert_obtained exec castle tls reconcile }`
+ # acme mode only: emit the `events { on cert_obtained exec wildpc tls reconcile }`
# hook so certs materialized onto raw-TCP services refresh on renewal. Requires
# the events-exec plugin in the gateway's Caddy build — set true only once that
# Caddy is installed (see docs/tcp-exposure.md §5). Default false keeps a
@@ -150,7 +150,7 @@ class GatewayConfig:
cert_hook: bool = False
-# Deployment kinds and the CastleConfig store each lives in. Kind is STRUCTURAL —
+# Deployment kinds and the WildpcConfig store each lives in. Kind is STRUCTURAL —
# a deployment's identity is (name, kind), so names are unique within a kind but may
# collide across kinds (a `backup` tool + service + job coexist). `kind_for` (manifest)
# stays only to validate that a spec's manager/schedule matches the store it's in.
@@ -165,8 +165,8 @@ _KIND_STORE = {
@dataclass
-class CastleConfig:
- """Full castle configuration."""
+class WildpcConfig:
+ """Full wildpc configuration."""
root: Path
gateway: GatewayConfig
@@ -185,7 +185,7 @@ class CastleConfig:
# Optional; empty means the API falls back to a built-in default set.
agents: dict[str, AgentSpec] = field(default_factory=dict)
# Configurable roots — the single source of truth (no module-constant twin).
- # load_config sets them (env > castle.yaml > default); a bare constructor gets the
+ # load_config sets them (env > wildpc.yaml > default); a bare constructor gets the
# built-in defaults so tests/callers that don't care stay valid.
data_dir: Path = field(default_factory=lambda: _DEFAULT_DATA_DIR)
repos_dir: Path = field(default_factory=lambda: _DEFAULT_REPOS_DIR)
@@ -259,9 +259,9 @@ def resolve_env_split(
- ``${secret:NAME}`` resolves via the active secret backend (file or OpenBao).
- ``${port}`` / ``${data_dir}`` / ``${name}`` / ``${public_url}`` (and
- anything else in ``context``) expand to castle's computed values, so a
+ anything else in ``context``) expand to wildpc's computed values, so a
service maps them to whatever env var its program reads (e.g.
- ``MY_PORT: ${port}``) without hardcoding or castle silently injecting a
+ ``MY_PORT: ${port}``) without hardcoding or wildpc silently injecting a
guessed var name. ``${public_url}`` is the service's gateway-facing base
URL (``https://.`` under acme) — the origin an app allowlists.
"""
@@ -324,9 +324,9 @@ def resolve_env_vars(
def _secrets_settings() -> dict:
- """The ``secrets:`` block of castle.yaml — selects the backend."""
+ """The ``secrets:`` block of wildpc.yaml — selects the backend."""
try:
- data = yaml.safe_load((CASTLE_HOME / "castle.yaml").read_text()) or {}
+ data = yaml.safe_load((WILDPC_HOME / "wildpc.yaml").read_text()) or {}
return data.get("secrets") or {}
except Exception:
return {}
@@ -336,18 +336,18 @@ def active_secret_backend() -> SecretBackend:
"""The active secret backend (file or openbao), per ``secrets:`` + env.
The one place to get a handle for reading/writing/listing secrets, so callers
- (the ``castle secret`` CLI, preflight checks) act on the *active* backend
+ (the ``wildpc secret`` CLI, preflight checks) act on the *active* backend
rather than guessing the filesystem — the mistake that silently shadows an
OpenBao fleet's secret with a stray file.
"""
- from castle_core.secret_backends import build_backend
+ from wildpc_core.secret_backends import build_backend
return build_backend(SECRETS_DIR, _secrets_settings())
def active_backend_name() -> str:
"""Human name of the active backend (``openbao`` | ``file``) for messages."""
- return (os.environ.get("CASTLE_SECRET_BACKEND") or _secrets_settings().get("backend") or "file").lower()
+ return (os.environ.get("WILDPC_SECRET_BACKEND") or _secrets_settings().get("backend") or "file").lower()
def read_secret(name: str) -> str | None:
@@ -414,7 +414,7 @@ def _load_resource_dir(directory: Path) -> dict[str, dict]:
def parse_gateway(gateway_data: dict) -> GatewayConfig:
- """Build a GatewayConfig from a castle.yaml ``gateway:`` mapping.
+ """Build a GatewayConfig from a wildpc.yaml ``gateway:`` mapping.
The single parser shared by ``load_config`` and the API's whole-file editor,
so a newly added gateway field can't be honored in one place and silently
@@ -433,14 +433,14 @@ def parse_gateway(gateway_data: dict) -> GatewayConfig:
)
-def load_config(root: Path | None = None) -> CastleConfig:
- """Load castle config: global castle.yaml + programs/ and deployments/ dirs."""
+def load_config(root: Path | None = None) -> WildpcConfig:
+ """Load wildpc config: global wildpc.yaml + programs/ and deployments/ dirs."""
if root is None:
- root = find_castle_root()
+ root = find_wildpc_root()
- config_path = root / "castle.yaml"
+ config_path = root / "wildpc.yaml"
if not config_path.exists():
- raise FileNotFoundError(f"Castle config not found: {config_path}")
+ raise FileNotFoundError(f"Wild PC config not found: {config_path}")
with open(config_path) as f:
data = yaml.safe_load(f) or {}
@@ -453,13 +453,13 @@ def load_config(root: Path | None = None) -> CastleConfig:
repo_path = Path(data["repo"]).expanduser()
# Configurable roots: env > this file's data_dir/repos_dir > default, anchored to
- # `root` (the dir holding this castle.yaml) so a per-call load_config is correct
- # regardless of the import-time constants (which resolved against CASTLE_HOME).
+ # `root` (the dir holding this wildpc.yaml) so a per-call load_config is correct
+ # regardless of the import-time constants (which resolved against WILDPC_HOME).
data_dir = _resolve_root_path(
- "CASTLE_DATA_DIR", data.get("data_dir"), root, _DEFAULT_DATA_DIR
+ "WILDPC_DATA_DIR", data.get("data_dir"), root, _DEFAULT_DATA_DIR
)
repos_dir = _resolve_root_path(
- "CASTLE_REPOS_DIR", data.get("repos_dir"), root, _DEFAULT_REPOS_DIR
+ "WILDPC_REPOS_DIR", data.get("repos_dir"), root, _DEFAULT_REPOS_DIR
)
programs: dict[str, ProgramSpec] = {}
@@ -468,7 +468,7 @@ def load_config(root: Path | None = None) -> CastleConfig:
# Resolve source paths to absolute
if prog.source:
if prog.source.startswith("repo:") and repo_path:
- # repo:castle-api → /data/repos/castle/castle-api
+ # repo:wildpc-api → /data/repos/wildpc/wildpc-api
prog.source = str(repo_path / prog.source[5:])
elif not Path(prog.source).is_absolute():
prog.source = str(root / prog.source)
@@ -482,7 +482,7 @@ def load_config(root: Path | None = None) -> CastleConfig:
for name, spec in (data.get("agents") or {}).items()
}
- config = CastleConfig(
+ config = WildpcConfig(
root=root,
repo=repo_path,
gateway=gateway,
@@ -600,7 +600,7 @@ def _spec_to_yaml_dict(spec: BaseModel) -> dict:
return cleaned if isinstance(cleaned, dict) else {}
-def _program_to_yaml_dict(spec: ProgramSpec, config: CastleConfig) -> dict:
+def _program_to_yaml_dict(spec: ProgramSpec, config: WildpcConfig) -> dict:
"""Serialize a ProgramSpec, rewriting absolute source paths to relative."""
d = _spec_to_yaml_dict(spec)
if d.get("source") and Path(d["source"]).is_absolute():
@@ -632,7 +632,7 @@ def _write_resource_dir(directory: Path, specs: dict[str, dict]) -> None:
path.unlink()
-# Top-level castle.yaml keys that save_config owns and rewrites; anything else
+# Top-level wildpc.yaml keys that save_config owns and rewrites; anything else
# (e.g. `secrets:`) is preserved verbatim so a rewrite can't drop it.
_MANAGED_GLOBALS = {
"gateway", "repo", "data_dir", "repos_dir", "agents", "role",
@@ -640,10 +640,10 @@ _MANAGED_GLOBALS = {
}
-def write_deployment_file(config: CastleConfig, kind: str, name: str) -> None:
+def write_deployment_file(config: WildpcConfig, kind: str, name: str) -> None:
"""Write (or remove) a single deployment file — globals and other resources are
left untouched. This is the PATCH primitive: a deployment edit persists only
- that deployment, never rewriting castle.yaml globals."""
+ that deployment, never rewriting wildpc.yaml globals."""
directory = config.root / "deployments" / _KIND_STORE[kind]
path = directory / f"{name}.yaml"
spec = config.store_for(kind).get(name)
@@ -655,7 +655,7 @@ def write_deployment_file(config: CastleConfig, kind: str, name: str) -> None:
yaml.dump(_spec_to_yaml_dict(spec), f, default_flow_style=False, sort_keys=False)
-def write_program_file(config: CastleConfig, name: str) -> None:
+def write_program_file(config: WildpcConfig, name: str) -> None:
"""Write (or remove) a single program file — nothing else is touched."""
directory = config.root / "programs"
path = directory / f"{name}.yaml"
@@ -670,8 +670,8 @@ def write_program_file(config: CastleConfig, name: str) -> None:
)
-def save_config(config: CastleConfig) -> None:
- """Save castle config: global castle.yaml + programs/ and deployments/ dirs."""
+def save_config(config: WildpcConfig) -> None:
+ """Save wildpc config: global wildpc.yaml + programs/ and deployments/ dirs."""
gateway_data: dict = {"port": config.gateway.port}
if config.gateway.tls:
gateway_data["tls"] = config.gateway.tls
@@ -679,7 +679,7 @@ def save_config(config: CastleConfig) -> None:
gateway_data["domain"] = config.gateway.domain
if config.gateway.acme_email:
gateway_data["acme_email"] = config.gateway.acme_email
- # Only persist the provider when non-default, to keep castle.yaml minimal.
+ # Only persist the provider when non-default, to keep wildpc.yaml minimal.
if (
config.gateway.acme_dns_provider
and config.gateway.acme_dns_provider != "cloudflare"
@@ -694,7 +694,7 @@ def save_config(config: CastleConfig) -> None:
data: dict = {"gateway": gateway_data}
if config.repo:
data["repo"] = str(config.repo)
- # Persist the configurable roots only when non-default, keeping castle.yaml minimal.
+ # Persist the configurable roots only when non-default, keeping wildpc.yaml minimal.
# These MUST round-trip: save_config rewrites the file from scratch, so a root that
# isn't re-emitted here would be silently dropped on the next apply.
if config.data_dir != _DEFAULT_DATA_DIR:
@@ -713,17 +713,17 @@ def save_config(config: CastleConfig) -> None:
# from the existing file.
if config.role and config.role != "follower":
data["role"] = config.role
- # Preserve any top-level castle.yaml keys this writer doesn't model (e.g.
+ # Preserve any top-level wildpc.yaml keys this writer doesn't model (e.g.
# `secrets:`) — a full rewrite must never silently drop an unmanaged global.
try:
- existing = yaml.safe_load((config.root / "castle.yaml").read_text()) or {}
+ existing = yaml.safe_load((config.root / "wildpc.yaml").read_text()) or {}
for k, v in existing.items():
if k not in _MANAGED_GLOBALS and k not in data:
data[k] = v
except Exception:
pass
- config_path = config.root / "castle.yaml"
+ config_path = config.root / "wildpc.yaml"
with open(config_path, "w") as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
@@ -740,10 +740,10 @@ def save_config(config: CastleConfig) -> None:
)
-def ensure_dirs(config: CastleConfig) -> None:
- """Ensure castle directories exist. Takes the config so the data dir comes from the
+def ensure_dirs(config: WildpcConfig) -> None:
+ """Ensure wildpc directories exist. Takes the config so the data dir comes from the
one source of truth (config.data_dir), not a process-resolved global."""
- CASTLE_HOME.mkdir(parents=True, exist_ok=True)
+ WILDPC_HOME.mkdir(parents=True, exist_ok=True)
CODE_DIR.mkdir(parents=True, exist_ok=True)
SPECS_DIR.mkdir(parents=True, exist_ok=True)
CONTENT_DIR.mkdir(parents=True, exist_ok=True)
@@ -753,9 +753,9 @@ def ensure_dirs(config: CastleConfig) -> None:
try:
data_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
- raise CastleDirError(
+ raise WildpcDirError(
f"Cannot create data dir {data_dir}: {e.strerror or e}. "
- f"Set data_dir: in {CASTLE_HOME / 'castle.yaml'} (or export CASTLE_DATA_DIR) "
+ f"Set data_dir: in {WILDPC_HOME / 'wildpc.yaml'} (or export WILDPC_DATA_DIR) "
f"to a writable path, or create it: "
f"sudo mkdir -p {data_dir} && sudo chown $(id -un) {data_dir}"
) from e
diff --git a/core/src/castle_core/deploy.py b/core/src/wildpc_core/deploy.py
similarity index 92%
rename from core/src/castle_core/deploy.py
rename to core/src/wildpc_core/deploy.py
index 2093ff7..fa9785f 100644
--- a/core/src/castle_core/deploy.py
+++ b/core/src/wildpc_core/deploy.py
@@ -1,7 +1,7 @@
-"""Deploy logic — bridge castle.yaml spec to runtime (~/.castle/).
+"""Deploy logic — bridge wildpc.yaml spec to runtime (~/.wildpc/).
This module contains the core deploy logic shared by the CLI and API.
-It reads castle.yaml, resolves services/jobs into Deployments,
+It reads wildpc.yaml, resolves services/jobs into Deployments,
writes the registry, generates systemd units and the Caddyfile, and
copies frontend build outputs.
"""
@@ -15,24 +15,24 @@ from collections.abc import Sequence
from dataclasses import dataclass, field
from pathlib import Path
-from castle_core.config import (
+from wildpc_core.config import (
SPECS_DIR,
- CastleConfig,
+ WildpcConfig,
ensure_dirs,
load_config,
resolve_env_split,
resolve_placeholders,
)
-from castle_core.generators.caddyfile import (
+from wildpc_core.generators.caddyfile import (
_DNS_TOKEN_ENV,
generate_caddyfile_from_registry,
)
-from castle_core.generators.dns import reconcile_public_dns
-from castle_core.generators.tunnel import (
+from wildpc_core.generators.dns import reconcile_public_dns
+from wildpc_core.generators.tunnel import (
generate_tunnel_config,
public_hostnames,
)
-from castle_core.generators.systemd import (
+from wildpc_core.generators.systemd import (
SECRET_ENV_DIR,
generate_timer,
generate_unit_from_deployed,
@@ -41,7 +41,7 @@ from castle_core.generators.systemd import (
unit_env_file,
unit_name,
)
-from castle_core.manifest import (
+from wildpc_core.manifest import (
CaddyDeployment,
DeploymentBase,
DeploymentSpec,
@@ -50,7 +50,7 @@ from castle_core.manifest import (
TlsMaterial,
kind_for,
)
-from castle_core.registry import (
+from wildpc_core.registry import (
REGISTRY_PATH,
Deployment,
NodeConfig,
@@ -58,7 +58,7 @@ from castle_core.registry import (
load_registry,
save_registry,
)
-from castle_core.toolchains import ToolchainError, resolve_node_bin
+from wildpc_core.toolchains import ToolchainError, resolve_node_bin
SYSTEMD_USER_DIR = Path.home() / ".config" / "systemd" / "user"
@@ -74,7 +74,7 @@ class DeployResult:
@dataclass
class ApplyResult:
- """Result of a converge (`castle apply`): what actually changed.
+ """Result of a converge (`wildpc apply`): what actually changed.
`deploy` renders config → artifacts; `apply` renders *and then* reconciles the
running system to match, so the interesting output is the diff it enacted.
@@ -115,11 +115,11 @@ class ApplyResult:
def deploy(target_name: str | None = None, root: Path | None = None) -> DeployResult:
- """Deploy from castle.yaml to ~/.castle/.
+ """Deploy from wildpc.yaml to ~/.wildpc/.
Args:
target_name: Deploy a single service/job by name, or None for all.
- root: Config root path. If None, uses find_castle_root().
+ root: Config root path. If None, uses find_wildpc_root().
Returns:
DeployResult with deployed count, messages, and the registry.
@@ -195,10 +195,10 @@ def deploy(target_name: str | None = None, root: Path | None = None) -> DeployRe
return result
-def _node_config(config: CastleConfig) -> NodeConfig:
+def _node_config(config: WildpcConfig) -> NodeConfig:
"""The registry NodeConfig derived from a config's gateway settings."""
return NodeConfig(
- castle_root=str(config.root),
+ wildpc_root=str(config.root),
gateway_port=config.gateway.port,
gateway_tls=config.gateway.tls,
gateway_domain=config.gateway.domain,
@@ -224,7 +224,7 @@ def _unit_bytes(name: str, kind: str) -> str | None:
return path.read_text() if path.exists() else None
-def _desired_registry(config: CastleConfig, target_name: str | None) -> NodeRegistry:
+def _desired_registry(config: WildpcConfig, target_name: str | None) -> NodeRegistry:
"""The registry ``deploy()`` would write for this (optionally scoped) run.
Mirrors deploy()'s registry build: a scoped run merges the updated target over
@@ -247,7 +247,7 @@ def _desired_registry(config: CastleConfig, target_name: str | None) -> NodeRegi
return registry
-def _gateway_would_change(config: CastleConfig, target_name: str | None) -> bool:
+def _gateway_would_change(config: WildpcConfig, target_name: str | None) -> bool:
"""Whether applying would rewrite the gateway's routing artifacts — the
Caddyfile or the cloudflared ingress — vs. what's on disk.
@@ -284,8 +284,8 @@ def apply(
"""
import asyncio
- from castle_core.config import active_backend_name
- from castle_core.lifecycle import activate, deactivate, is_active
+ from wildpc_core.config import active_backend_name
+ from wildpc_core.lifecycle import activate, deactivate, is_active
config = load_config(root)
# Each item is (kind, name, spec); target_name matches every kind of that name.
@@ -315,7 +315,7 @@ def apply(
for n, secrets in blocked:
for s in secrets:
result.messages.append(
- f" {n}: ${{secret:{s}}} → not found. Fix: castle secret set {s}"
+ f" {n}: ${{secret:{s}}} → not found. Fix: wildpc secret set {s}"
)
return result
@@ -378,8 +378,8 @@ def apply(
# materialization to the deployments being applied so a scoped apply doesn't
# rewrite an unrelated service's cert without reloading it. No reload here — the
# activation loop below starts/restarts as needed; rotation-driven reloads are
- # the `castle tls reconcile` / cert_obtained path.
- from castle_core.tls import materialize_all, wait_for_wildcard
+ # the `wildpc tls reconcile` / cert_obtained path.
+ from wildpc_core.tls import materialize_all, wait_for_wildcard
wait_for_wildcard(config, names, result.messages)
materialize_all(config, result.messages, only=names)
@@ -404,7 +404,7 @@ def apply(
def _stack_preflight(
- config: CastleConfig,
+ config: WildpcConfig,
items: Sequence[tuple[str, str, object]],
messages: list[str],
) -> None:
@@ -412,8 +412,8 @@ def _stack_preflight(
*where it runs* — the moment drift actually bites: a service whose `uv`/`pnpm`
isn't on its runtime PATH won't build or start. Mirrors `_acme_preflight`: an
advisory message, no writes, no gate. The exact fix comes from the tool's hint."""
- from castle_core.relations import _tool_available
- from castle_core.stacks import tools_for
+ from wildpc_core.relations import _tool_available
+ from wildpc_core.stacks import tools_for
for _k, n, spec in items:
if not getattr(spec, "enabled", True):
@@ -441,7 +441,7 @@ def _unresolved_secrets(
the old path substituted the marker and started the service anyway. Checked
against the active backend so it's correct whichever backend is configured.
"""
- from castle_core.config import active_secret_backend, secret_refs
+ from wildpc_core.config import active_secret_backend, secret_refs
backend = active_secret_backend()
resolved: dict[str, bool] = {} # cache: one backend read per distinct secret
@@ -480,7 +480,7 @@ def _record(result: ApplyResult, name: str, action: str) -> None:
def _render_unit_preview(
- config: CastleConfig, name: str, dep: Deployment, kind: str
+ config: WildpcConfig, name: str, dep: Deployment, kind: str
) -> str | None:
"""The unit bytes `deploy` would write for the deployment we'd restart (the
.timer for a job, the .service for a service), for --plan restart detection.
@@ -491,16 +491,16 @@ def _render_unit_preview(
return files.get(timer_name(name) if kind == "job" else unit_name(name, kind))
-# Gateway service name in the registry → its systemd unit (castle-castle-gateway).
-_GATEWAY_NAME = "castle-gateway"
+# Gateway service name in the registry → its systemd unit (wildpc-wildpc-gateway).
+_GATEWAY_NAME = "wildpc-gateway"
-def _acme_preflight(config: CastleConfig, messages: list[str]) -> None:
+def _acme_preflight(config: WildpcConfig, messages: list[str]) -> None:
"""Warn (never fail, never write) if acme-mode prerequisites are missing.
acme mode needs a `gateway.domain`, the DNS-provider token mapped into the
- castle-gateway service env, and the matching secret on disk — all operator
- steps (castle never rewrites the user-authored gateway service YAML)."""
+ wildpc-gateway service env, and the matching secret on disk — all operator
+ steps (wildpc never rewrites the user-authored gateway service YAML)."""
gw = config.gateway
if not gw.domain:
messages.append(
@@ -519,7 +519,7 @@ def _acme_preflight(config: CastleConfig, messages: list[str]) -> None:
f"Add to services/{_GATEWAY_NAME}.yaml → defaults.env: "
f"{token_env}: ${{secret:{token_env}}}"
)
- from castle_core.config import read_secret
+ from wildpc_core.config import read_secret
if not read_secret(token_env):
messages.append(
@@ -528,8 +528,8 @@ def _acme_preflight(config: CastleConfig, messages: list[str]) -> None:
)
-# Tunnel service name in the registry → its systemd unit (castle-castle-tunnel).
-_TUNNEL_NAME = "castle-tunnel"
+# Tunnel service name in the registry → its systemd unit (wildpc-wildpc-tunnel).
+_TUNNEL_NAME = "wildpc-tunnel"
def _write_tunnel_config(registry: NodeRegistry, messages: list[str]) -> None:
@@ -548,7 +548,7 @@ def _write_tunnel_config(registry: NodeRegistry, messages: list[str]) -> None:
if config_path.exists():
config_path.unlink()
messages.append("No public services — removed cloudflared config.")
- # Still reconcile so any CNAMEs castle created earlier are cleaned up.
+ # Still reconcile so any CNAMEs wildpc created earlier are cleaned up.
reconcile_public_dns(node.tunnel_id, [], messages)
return
@@ -575,17 +575,17 @@ def _write_tunnel_config(registry: NodeRegistry, messages: list[str]) -> None:
messages.append("Tunnel reloaded.")
else:
messages.append(
- "Tunnel not running — enable it with 'castle service enable tunnel'."
+ "Tunnel not running — enable it with 'wildpc service enable tunnel'."
)
-def _gateway_env(config: CastleConfig) -> dict[str, str]:
+def _gateway_env(config: WildpcConfig) -> dict[str, str]:
"""The process env for validating/handling the gateway's Caddyfile — the
- current environment plus the castle-gateway service's own resolved env.
+ current environment plus the wildpc-gateway service's own resolved env.
Under acme the Caddyfile references ``{env.CLOUDFLARE_API_TOKEN}`` (or another
DNS-provider token). `caddy validate` provisions the acme module and rejects an
- empty token, so validating in castle-api's bare environment always fails and the
+ empty token, so validating in wildpc-api's bare environment always fails and the
reload is skipped. Injecting the gateway service's env (secrets resolved) gives
validate the same token the running service starts with."""
svc = config.services.get(_GATEWAY_NAME)
@@ -595,7 +595,7 @@ def _gateway_env(config: CastleConfig) -> dict[str, str]:
return {**os.environ, **resolved}
-def _reload_gateway(config: CastleConfig, messages: list[str]) -> None:
+def _reload_gateway(config: WildpcConfig, messages: list[str]) -> None:
"""Reload Caddy if the gateway is running, so new routes take effect."""
gw_unit = unit_name(_GATEWAY_NAME)
# Validate the generated Caddyfile before reloading. An invalid config (most
@@ -630,7 +630,7 @@ def _reload_gateway(config: CastleConfig, messages: list[str]) -> None:
)
if active.stdout.strip() != "active":
messages.append(
- "Gateway not running — skipped reload (start it with 'castle gateway start')."
+ "Gateway not running — skipped reload (start it with 'wildpc gateway start')."
)
return
result = subprocess.run(
@@ -648,7 +648,7 @@ def _reload_gateway(config: CastleConfig, messages: list[str]) -> None:
def _public_url(
- config: CastleConfig, name: str, exposed: bool, port: int | None
+ config: WildpcConfig, name: str, exposed: bool, port: int | None
) -> str | None:
"""The service's publicly-reachable base URL — the ``${public_url}`` placeholder.
@@ -667,7 +667,7 @@ def _public_url(
return None
-def _target_url(config: CastleConfig, target_name: str) -> str | None:
+def _target_url(config: WildpcConfig, target_name: str) -> str | None:
"""The base URL another deployment is reachable at — how a ``{kind: deployment,
bind: VAR}`` requirement projects its target into the consumer's env. A name may
span kinds; the HTTP-exposed one is what has a URL, so prefer it."""
@@ -688,7 +688,7 @@ def _target_url(config: CastleConfig, target_name: str) -> str | None:
return _public_url(config, target_name, getattr(dep, "http_exposed", False), tport)
-def _requires_env(config: CastleConfig, dep: DeploymentSpec) -> dict[str, str]:
+def _requires_env(config: WildpcConfig, dep: DeploymentSpec) -> dict[str, str]:
"""Env generated FROM a deployment's ``requires`` — a ``{ref, bind: VAR}``
requirement sets ``VAR`` to the target deployment's URL. Env is derived from the
dependency, never scraped back into one (see docs/relationships.md)."""
@@ -701,7 +701,7 @@ def _requires_env(config: CastleConfig, dep: DeploymentSpec) -> dict[str, str]:
return out
-def _supabase_app_schemas(config: CastleConfig) -> str:
+def _supabase_app_schemas(config: WildpcConfig) -> str:
"""The ``${supabase_app_schemas}`` placeholder: each registered supabase app's
own schema, comma-prefixed and joined (or '' when there are none).
@@ -710,9 +710,9 @@ def _supabase_app_schemas(config: CastleConfig) -> str:
``public,storage,graphql_public${supabase_app_schemas}``. Comma-prefixing each
entry keeps the base list valid when zero apps are registered (no trailing
comma). Adding/removing a supabase app thus changes this list — the substrate
- needs a restart (recreate) after `castle deploy` to pick it up.
+ needs a restart (recreate) after `wildpc deploy` to pick it up.
"""
- from castle_core.stacks import app_schema
+ from wildpc_core.stacks import app_schema
schemas = sorted(
app_schema(pn) for pn, ps in config.programs.items() if ps.stack == "supabase"
@@ -774,7 +774,7 @@ def _write_secret_env_file(
return path
-def _resolve_description(config: CastleConfig, spec: DeploymentBase) -> str | None:
+def _resolve_description(config: WildpcConfig, spec: DeploymentBase) -> str | None:
"""Get description, falling through to program if referenced."""
if spec.description:
return spec.description
@@ -793,7 +793,7 @@ def _registry_requires(dep: DeploymentSpec) -> list[dict]:
def _build_deployed(
- config: CastleConfig, name: str, dep: DeploymentSpec, messages: list[str]
+ config: WildpcConfig, name: str, dep: DeploymentSpec, messages: list[str]
) -> Deployment:
"""Build a runtime Deployment from a DeploymentSpec, dispatched by its manager."""
description = _resolve_description(config, dep)
@@ -851,7 +851,7 @@ def _build_deployed(
run = dep.run
# ${data_dir} is keyed by the program the deployment runs, not the deployment
# name (e.g. job `protonmail-sync` runs program `protonmail` →
- # /data/castle/protonmail). Falls back to the deployment name.
+ # /data/wildpc/protonmail). Falls back to the deployment name.
config_key = dep.program or name
managed = True
@@ -871,7 +871,7 @@ def _build_deployed(
# Env is exactly what's in defaults.env — no hidden convention injection.
# ${port}/${data_dir}/${name}/${public_url} map the program's own env var
- # names to castle's computed values. Secret-bearing vars split out to a
+ # names to wildpc's computed values. Secret-bearing vars split out to a
# mode-0600 file (never in the unit or argv).
raw_env = dict(dep.defaults.env) if (dep.defaults and dep.defaults.env) else {}
# Env generated from `requires` ({kind: deployment, bind: VAR} → target URL).
@@ -887,12 +887,12 @@ def _build_deployed(
public_url,
_supabase_app_schemas(config),
)
- # ${tls_*}: paths to castle-materialized cert files for a TLS-material TCP
+ # ${tls_*}: paths to wildpc-materialized cert files for a TLS-material TCP
# service. The deployment maps them into its own config (mount ${tls_dir} for a
# container, or reference ${tls_cert}/${tls_key} directly for a native service).
tls = dep.expose.tcp.tls if (dep.expose and dep.expose.tcp) else None
if tls and tls.material != TlsMaterial.OFF:
- from castle_core.tls import tls_dir_for
+ from wildpc_core.tls import tls_dir_for
tls_dir = tls_dir_for(config.data_dir, config_key)
ctx.update(
@@ -981,7 +981,7 @@ def _python_tool_needs_install(program: str) -> bool:
return False
-def _program_source_dir(config: CastleConfig, program: str | None) -> Path | None:
+def _program_source_dir(config: WildpcConfig, program: str | None) -> Path | None:
"""The absolute source dir of a referenced program, if any.
`load_config` has already resolved `source` to an absolute path (repo: and
@@ -994,7 +994,7 @@ def _program_source_dir(config: CastleConfig, program: str | None) -> Path | Non
def _ensure_python_tool(
- config: CastleConfig, program: str | None, messages: list[str]
+ config: WildpcConfig, program: str | None, messages: list[str]
) -> None:
"""Ensure a Python program's editable install is current.
@@ -1029,7 +1029,7 @@ def _ensure_python_tool(
def _subst(value: str, placeholders: dict[str, str] | None) -> str:
- """Expand ``${key}`` in a run-spec string field from castle's computed values
+ """Expand ``${key}`` in a run-spec string field from wildpc's computed values
(``${uid}``/``${gid}``/``${data_dir}``/``${tls_dir}``/…), via the one shared
``${...}`` resolver (:func:`resolve_placeholders`). Unknown refs pass through
unchanged (secrets never belong in argv — they go via --env-file); write
@@ -1085,12 +1085,12 @@ def _build_run_cmd(
case "container":
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
# Container name derives from the SERVICE name (matches the systemd unit),
- # not the image name — so `castle-` is stable and collision-free.
- cmd = [runtime, "run", "--rm", f"--name=castle-{name}"]
+ # not the image name — so `wildpc-` is stable and collision-free.
+ cmd = [runtime, "run", "--rm", f"--name=wildpc-{name}"]
if run.user: # type: ignore[union-attr]
# Run as the invoking user (uid uniformity → bind-mounted
# certs/data/secrets readable with no chown). ${uid}/${gid} expand
- # to the castle process's own ids.
+ # to the wildpc process's own ids.
cmd.extend(["--user", _subst(run.user, placeholders)]) # type: ignore[union-attr]
for tp in run.tmpfs: # type: ignore[union-attr]
cmd.extend(["--tmpfs", _subst(tp, placeholders)])
@@ -1147,7 +1147,7 @@ def _compose_base(name: str, run: object, source_dir: Path | None) -> list[str]:
namespaced and collision-free.
"""
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
- project = run.project_name or f"castle-{name}" # type: ignore[union-attr]
+ project = run.project_name or f"wildpc-{name}" # type: ignore[union-attr]
compose_file = Path(run.file) # type: ignore[union-attr]
if not compose_file.is_absolute() and source_dir is not None:
compose_file = source_dir / compose_file
@@ -1204,9 +1204,9 @@ def _teardown_unit(unit_file: str, messages: list[str]) -> None:
def _prune_orphans(registry: NodeRegistry, messages: list[str]) -> None:
- """Remove castle-* units no longer backed by a managed registry entry.
+ """Remove wildpc-* units no longer backed by a managed registry entry.
- The `castle-` prefix is the ownership namespace: any castle-*.service/.timer on
+ The `wildpc-` prefix is the ownership namespace: any wildpc-*.service/.timer on
disk that isn't in the desired set is an orphan (a removed/unmanaged/unscheduled
component) and is torn down. Only call on a FULL deploy — the desired set must
reflect the whole registry, not a single --target.
@@ -1214,14 +1214,14 @@ def _prune_orphans(registry: NodeRegistry, messages: list[str]) -> None:
desired = _desired_unit_files(registry)
if not SYSTEMD_USER_DIR.is_dir():
return
- for pattern in ("castle-*.service", "castle-*.timer"):
+ for pattern in ("wildpc-*.service", "wildpc-*.timer"):
for path in sorted(SYSTEMD_USER_DIR.glob(pattern)):
if path.name not in desired:
_teardown_unit(path.name, messages)
def _render_unit_files(
- config: CastleConfig, name: str, deployed: Deployment
+ config: WildpcConfig, name: str, deployed: Deployment
) -> dict[str, str]:
"""The exact unit files `deploy` would write for a deployment: {filename: content}.
@@ -1250,7 +1250,7 @@ def _render_unit_files(
return files
-def _generate_systemd_units(config: CastleConfig, registry: NodeRegistry) -> None:
+def _generate_systemd_units(config: WildpcConfig, registry: NodeRegistry) -> None:
"""Generate systemd units from the registry."""
SYSTEMD_USER_DIR.mkdir(parents=True, exist_ok=True)
diff --git a/core/src/castle_core/generators/__init__.py b/core/src/wildpc_core/generators/__init__.py
similarity index 73%
rename from core/src/castle_core/generators/__init__.py
rename to core/src/wildpc_core/generators/__init__.py
index de3e496..d4a8424 100644
--- a/core/src/castle_core/generators/__init__.py
+++ b/core/src/wildpc_core/generators/__init__.py
@@ -1,9 +1,9 @@
-"""Castle infrastructure generators."""
+"""Wild PC infrastructure generators."""
-from castle_core.generators.caddyfile import (
+from wildpc_core.generators.caddyfile import (
generate_caddyfile_from_registry,
)
-from castle_core.generators.systemd import (
+from wildpc_core.generators.systemd import (
cron_to_interval_sec,
cron_to_oncalendar,
generate_timer,
diff --git a/core/src/castle_core/generators/caddyfile.py b/core/src/wildpc_core/generators/caddyfile.py
similarity index 94%
rename from core/src/castle_core/generators/caddyfile.py
rename to core/src/wildpc_core/generators/caddyfile.py
index 756c6e6..e8a08fd 100644
--- a/core/src/castle_core/generators/caddyfile.py
+++ b/core/src/wildpc_core/generators/caddyfile.py
@@ -18,15 +18,15 @@ import os
from dataclasses import dataclass
from pathlib import Path
-from castle_core.config import SPECS_DIR, CastleConfig
-from castle_core.manifest import CaddyDeployment, SystemdDeployment
-from castle_core.registry import NodeRegistry
+from wildpc_core.config import SPECS_DIR, WildpcConfig
+from wildpc_core.manifest import CaddyDeployment, SystemdDeployment
+from wildpc_core.registry import NodeRegistry
# DNS-01 provider → the env var the Caddyfile reads its API token from. The token
# reaches Caddy via the gateway service's defaults.env (a mode-0600 EnvironmentFile).
_DNS_TOKEN_ENV = {"cloudflare": "CLOUDFLARE_API_TOKEN"}
-# Let's Encrypt staging directory — opt in via CASTLE_ACME_STAGING=1 to avoid the
+# Let's Encrypt staging directory — opt in via WILDPC_ACME_STAGING=1 to avoid the
# production rate limits while testing, then cut over to prod (unset the env var).
_ACME_STAGING_CA = "https://acme-staging-v02.api.letsencrypt.org/directory"
@@ -71,12 +71,12 @@ def service_proxy_targets(name: str, dep: SystemdDeployment) -> ProxyTargets:
def _local_routes(
- config: CastleConfig | None, registry: NodeRegistry
+ config: WildpcConfig | None, registry: NodeRegistry
) -> list[tuple[str, str, str, bool, str | None, bool]]:
"""Each local deployment's route as ``(name, kind, target, public, public_host, spa)``.
``kind`` is ``static`` (a caddy deployment — file-serve a built dir) or
- ``proxy`` (a proxied systemd process). Prefers ``castle.yaml``
+ ``proxy`` (a proxied systemd process). Prefers ``wildpc.yaml``
(``config.deployments``) so a regenerated Caddyfile reflects the current spec;
falls back to the deployed registry snapshot when config isn't available.
"""
@@ -86,7 +86,7 @@ def _local_routes(
# jobs/tools/references never do.
for kind, name, dep in config.all_deployments():
# A disabled deployment is defined but not running — no route (else it
- # would 502). `castle apply` converges it off.
+ # would 502). `wildpc apply` converges it off.
if not dep.enabled:
continue
if kind == "static" and isinstance(dep, CaddyDeployment):
@@ -111,7 +111,7 @@ def _local_routes(
return out
-def _program_source(config: CastleConfig | None, program: str | None):
+def _program_source(config: WildpcConfig | None, program: str | None):
"""Absolute source dir of a referenced program (already resolved), or None."""
if config is None or not program:
return None
@@ -123,7 +123,7 @@ def _program_source(config: CastleConfig | None, program: str | None):
def compute_routes(
registry: NodeRegistry,
- config: CastleConfig | None = None,
+ config: WildpcConfig | None = None,
remote_registries: dict[str, NodeRegistry] | None = None,
) -> list[GatewayRoute]:
"""Build the ordered list of gateway routes. Every route is a host route whose
@@ -135,7 +135,7 @@ def compute_routes(
— so a consumed cross-node service is reachable at ``.``."""
if config is None:
try:
- from castle_core.config import load_config
+ from wildpc_core.config import load_config
config = load_config()
except Exception:
@@ -158,7 +158,7 @@ def compute_routes(
def _remote_routes(
- config: CastleConfig | None,
+ config: WildpcConfig | None,
registry: NodeRegistry,
remote_registries: dict[str, NodeRegistry],
) -> list[GatewayRoute]:
@@ -278,11 +278,11 @@ def _public_site_block(host: str, kind: str, target: str, spa: bool = True) -> l
return [f"{host} {{", *body, "}", ""]
-# Castle's own control plane: the dashboard frontend and the API it calls. These
+# Wild PC's own control plane: the dashboard frontend and the API it calls. These
# names are the subdomains they're published at in acme mode, and the pair served
# on the : site in off mode (no domain → no subdomains).
-_DASHBOARD = "castle"
-_API = "castle-api"
+_DASHBOARD = "wildpc"
+_API = "wildpc-api"
def generate_caddyfile_from_registry(
@@ -299,8 +299,8 @@ def generate_caddyfile_from_registry(
The `:` site just redirects to the dashboard subdomain (the "browse to
the box by IP" entry).
- **off / no domain** — no subdomains available, so the `:` site serves
- castle's control plane only: the dashboard at `/` + `reverse_proxy /api/*` →
- castle-api (the one surviving path, for the dashboard's own backend). Other
+ wildpc's control plane only: the dashboard at `/` + `reverse_proxy /api/*` →
+ wildpc-api (the one surviving path, for the dashboard's own backend). Other
services are reachable at their `host:port` directly.
"""
routes = compute_routes(registry, None, remote_registries)
@@ -319,7 +319,7 @@ def generate_caddyfile_from_registry(
if node.acme_email:
lines.append(f" email {node.acme_email}")
lines.append(f" acme_dns {provider} {{env.{token_env}}}")
- if os.environ.get("CASTLE_ACME_STAGING") == "1":
+ if os.environ.get("WILDPC_ACME_STAGING") == "1":
lines.append(f" acme_ca {_ACME_STAGING_CA}")
# On issuance/renewal, refresh certs materialized onto raw-TCP services and
# reload them (idempotent — a no-op when nothing rotated). Requires the
@@ -330,7 +330,7 @@ def generate_caddyfile_from_registry(
if getattr(node, "cert_hook", False):
lines += [
" events {",
- " on cert_obtained exec castle tls reconcile",
+ " on cert_obtained exec wildpc tls reconcile",
" }",
]
lines += ["}", ""]
diff --git a/core/src/castle_core/generators/dns.py b/core/src/wildpc_core/generators/dns.py
similarity index 95%
rename from core/src/castle_core/generators/dns.py
rename to core/src/wildpc_core/generators/dns.py
index 64a984a..415dd0b 100644
--- a/core/src/castle_core/generators/dns.py
+++ b/core/src/wildpc_core/generators/dns.py
@@ -1,6 +1,6 @@
"""Reconcile public DNS (Cloudflare CNAMEs) for tunnel-exposed services.
-Castle owns the CNAMEs — across every zone the token can see — that point at its
+Wild PC owns the CNAMEs — across every zone the token can see — that point at its
Cloudflare tunnel: on deploy it creates one per public host (each routed to the
accessible zone whose name is its longest suffix, so apex and multi-zone hosts both
work) and deletes any that point at this tunnel but no longer correspond to a
@@ -11,7 +11,7 @@ hand-managed A/CNAME in the same zone is safe.
Needs a Cloudflare API token with **DNS:Edit** on every target zone (Cloudflare's
"Edit zone DNS" template — that single permission both lists the accessible zones
and edits records; no separate Zone:Read is needed), stored at
-`~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN`. Absent → this is a no-op and the
+`~/.wildpc/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN`. Absent → this is a no-op and the
caller falls back to surfacing the manual `cloudflared tunnel route dns` hints.
"""
@@ -31,7 +31,7 @@ PUBLIC_DNS_TOKEN = "CLOUDFLARE_PUBLIC_DNS_TOKEN"
def public_dns_token() -> str | None:
"""The public-zone DNS token from the active secret backend, or None."""
- from castle_core.config import read_secret
+ from wildpc_core.config import read_secret
return read_secret(PUBLIC_DNS_TOKEN) or None
@@ -69,8 +69,8 @@ def reconcile_public_dns(
Each desired host is routed to the accessible zone whose name is its longest
suffix (so apex hosts and hosts in different zones are handled), then per zone
- castle creates missing CNAMEs (proxied → the tunnel; Cloudflare flattens apex
- CNAMEs) and deletes castle-managed ones (content == `.cfargotunnel.com`)
+ wildpc creates missing CNAMEs (proxied → the tunnel; Cloudflare flattens apex
+ CNAMEs) and deletes wildpc-managed ones (content == `.cfargotunnel.com`)
no longer desired. Never touches records pointing elsewhere. Scanning every
visible zone also cleans up stale CNAMEs after a host moves zones or all public
services are removed.
diff --git a/core/src/castle_core/generators/systemd.py b/core/src/wildpc_core/generators/systemd.py
similarity index 92%
rename from core/src/castle_core/generators/systemd.py
rename to core/src/wildpc_core/generators/systemd.py
index 2d8f135..19282ad 100644
--- a/core/src/castle_core/generators/systemd.py
+++ b/core/src/wildpc_core/generators/systemd.py
@@ -5,12 +5,12 @@ from __future__ import annotations
import shutil
from pathlib import Path
-from castle_core.config import SECRETS_DIR, USER_TOOL_PATH_DIRS
-from castle_core.manifest import RestartPolicy, SystemdSpec
-from castle_core.registry import Deployment
+from wildpc_core.config import SECRETS_DIR, USER_TOOL_PATH_DIRS
+from wildpc_core.manifest import RestartPolicy, SystemdSpec
+from wildpc_core.registry import Deployment
SYSTEMD_USER_DIR = Path.home() / ".config" / "systemd" / "user"
-UNIT_PREFIX = "castle-"
+UNIT_PREFIX = "wildpc-"
# Generated mode-0600 env files holding a deployment's resolved secrets, kept out
# of the unit file and the process argv (loaded via EnvironmentFile= / --env-file).
@@ -18,7 +18,7 @@ SECRET_ENV_DIR = SECRETS_DIR / "env"
def runtime_path(path_prepend: list[str] | tuple[str, ...] = ()) -> str:
- """The PATH a castle service runs with: resolved toolchain dirs (e.g. a pinned
+ """The PATH a wildpc service runs with: resolved toolchain dirs (e.g. a pinned
node bin) + the user tool dirs that exist + system bins. This is the single
definition of a service's runtime PATH — the unit generator writes it into
``Environment=PATH`` and the dependency checker (``relations``) probes tools
@@ -32,8 +32,8 @@ def runtime_path(path_prepend: list[str] | tuple[str, ...] = ()) -> str:
def unit_basename(name: str, kind: str = "service") -> str:
"""The systemd unit stem for a deployment. Jobs carry a ``-job`` marker so a
- service and a job can share a name (`castle-.service` vs
- `castle--job.{service,timer}`); everything else is `castle-`."""
+ service and a job can share a name (`wildpc-.service` vs
+ `wildpc--job.{service,timer}`); everything else is `wildpc-`."""
return f"{UNIT_PREFIX}{name}-job" if kind == "job" else f"{UNIT_PREFIX}{name}"
@@ -132,7 +132,7 @@ def generate_unit_from_deployed(
env_lines = ""
for key, value in deployed.env.items():
env_lines += f"Environment={key}={value}\n"
- # Castle supplies a sensible default PATH (tool dirs + system bins). It is an
+ # Wild PC supplies a sensible default PATH (tool dirs + system bins). It is an
# escape hatch, not a mandate: if the service pins its own PATH in defaults.env
# (e.g. to add a versioned nvm node the tool dirs intentionally omit), respect
# it rather than clobbering it with a trailing Environment=PATH line that would
@@ -151,7 +151,7 @@ def generate_unit_from_deployed(
if deployed.schedule:
unit = f"""[Unit]
-Description=Castle: {description}
+Description=Wild PC: {description}
After={after}
[Service]
@@ -162,7 +162,7 @@ ExecStart={exec_start}
restart = (sd.restart if sd else RestartPolicy.ON_FAILURE).value
restart_sec = sd.restart_sec if sd else 5
unit = f"""[Unit]
-Description=Castle: {description}
+Description=Wild PC: {description}
After={after}
[Service]
@@ -224,7 +224,7 @@ def generate_timer(
timer_lines = "OnBootSec=60\nOnUnitActiveSec=300\n"
return f"""[Unit]
-Description=Castle timer: {description}
+Description=Wild PC timer: {description}
[Timer]
{timer_lines}Persistent=false
diff --git a/core/src/castle_core/generators/tunnel.py b/core/src/wildpc_core/generators/tunnel.py
similarity index 97%
rename from core/src/castle_core/generators/tunnel.py
rename to core/src/wildpc_core/generators/tunnel.py
index 2056583..a235475 100644
--- a/core/src/castle_core/generators/tunnel.py
+++ b/core/src/wildpc_core/generators/tunnel.py
@@ -20,8 +20,8 @@ from pathlib import Path
import yaml
-from castle_core.config import SECRETS_DIR
-from castle_core.registry import Deployment, NodeRegistry
+from wildpc_core.config import SECRETS_DIR
+from wildpc_core.registry import Deployment, NodeRegistry
# Cloudflared tunnel credentials (from `cloudflared tunnel create`) live here as a
# secret, one JSON per tunnel id. The generated config references this path.
diff --git a/core/src/castle_core/git.py b/core/src/wildpc_core/git.py
similarity index 96%
rename from core/src/castle_core/git.py
rename to core/src/wildpc_core/git.py
index c328234..b12d90d 100644
--- a/core/src/castle_core/git.py
+++ b/core/src/wildpc_core/git.py
@@ -1,13 +1,13 @@
"""Git working-copy status and sync for programs whose source is a git repo.
-Programs that declare a ``repo:`` URL are cloned once (``castle program clone``);
-this module lets a running castle *see how far behind* a working copy is and pull
+Programs that declare a ``repo:`` URL are cloned once (``wildpc program clone``);
+this module lets a running wildpc *see how far behind* a working copy is and pull
later updates. It is intentionally pull-only — it touches files on disk and never
builds, applies, or restarts anything. Making the running artifact reflect the new
code (rebuild a frontend, restart a service) stays an explicit, separate step via
-``castle apply`` / ``castle restart``.
+``wildpc apply`` / ``wildpc restart``.
-Plain ``git`` via subprocess (matching ``castle program clone``); no GitPython.
+Plain ``git`` via subprocess (matching ``wildpc program clone``); no GitPython.
"""
from __future__ import annotations
diff --git a/core/src/castle_core/lifecycle.py b/core/src/wildpc_core/lifecycle.py
similarity index 92%
rename from core/src/castle_core/lifecycle.py
rename to core/src/wildpc_core/lifecycle.py
index 89732c4..aee7446 100644
--- a/core/src/castle_core/lifecycle.py
+++ b/core/src/wildpc_core/lifecycle.py
@@ -18,8 +18,8 @@ import sys
import time
from pathlib import Path
-from castle_core.config import CastleConfig
-from castle_core.generators.systemd import (
+from wildpc_core.config import WildpcConfig
+from wildpc_core.generators.systemd import (
SYSTEMD_USER_DIR,
generate_timer,
generate_unit_from_deployed,
@@ -28,9 +28,9 @@ from castle_core.generators.systemd import (
unit_env_file,
unit_name,
)
-from castle_core.manifest import CaddyDeployment
-from castle_core.registry import REGISTRY_PATH, load_registry
-from castle_core.stacks import ActionResult, run_action
+from wildpc_core.manifest import CaddyDeployment
+from wildpc_core.registry import REGISTRY_PATH, load_registry
+from wildpc_core.stacks import ActionResult, run_action
def _systemctl_active(unit: str) -> bool:
@@ -73,7 +73,7 @@ def _uv_tool_packages() -> set[str]:
def _own_venv_bins() -> set[str]:
"""Bin dirs of the *running* interpreter's virtualenv.
- Excluded from install detection: when the api server (or a dev `castle`) runs
+ Excluded from install detection: when the api server (or a dev `wildpc`) runs
inside a project venv via `uv run`, a tool merely *colocated* there — e.g. an
editable a dev accidentally `uv pip install`ed while that venv was active — must
not read as "installed on PATH" for the user. We only strip *our own* venv, not
@@ -113,13 +113,13 @@ def tool_installed(name: str) -> bool:
return _on_path(name)
-def _svc_manager(name: str, kind: str, config: CastleConfig) -> str | None:
+def _svc_manager(name: str, kind: str, config: WildpcConfig) -> str | None:
"""The manager for a deployment (name, kind), or None if not in config."""
dep = config.deployment(kind, name)
return dep.manager if dep is not None else None
-def _static_built(name: str, config: CastleConfig) -> bool:
+def _static_built(name: str, config: WildpcConfig) -> bool:
"""Whether a static (caddy) deployment's served dir exists (assets are built)."""
dep = config.statics.get(name)
if not isinstance(dep, CaddyDeployment):
@@ -133,7 +133,7 @@ def _static_built(name: str, config: CastleConfig) -> bool:
# ---------------------------------------------------------------------------
-def is_active(name: str, kind: str, config: CastleConfig) -> bool:
+def is_active(name: str, kind: str, config: WildpcConfig) -> bool:
"""Whether a deployment (name, kind) is available in its mode, by manager."""
manager = _svc_manager(name, kind, config)
if manager == "systemd":
@@ -157,17 +157,17 @@ def is_active(name: str, kind: str, config: CastleConfig) -> bool:
# ---------------------------------------------------------------------------
-def enable_service(name: str, kind: str, config: CastleConfig) -> ActionResult:
+def enable_service(name: str, kind: str, config: WildpcConfig) -> ActionResult:
"""Generate+install the unit (and timer) from the registry, enable and start it."""
if not REGISTRY_PATH.exists():
return ActionResult(
- name, "activate", "error", "No registry. Run 'castle deploy' first."
+ name, "activate", "error", "No registry. Run 'wildpc deploy' first."
)
registry = load_registry()
deployed = registry.get(kind, name)
if deployed is None:
return ActionResult(
- name, "activate", "error", f"'{name}' not in registry; run 'castle deploy'."
+ name, "activate", "error", f"'{name}' not in registry; run 'wildpc deploy'."
)
if not deployed.managed:
return ActionResult(
@@ -228,14 +228,14 @@ def disable_service(name: str, kind: str) -> ActionResult:
# ---------------------------------------------------------------------------
-def _program_for(name: str, kind: str, config: CastleConfig):
+def _program_for(name: str, kind: str, config: WildpcConfig):
"""The program a deployment runs (its `program` ref, defaulting to the name)."""
dep = config.deployment(kind, name)
prog = (dep.program if dep else None) or name
return prog, config.programs.get(prog)
-async def activate(name: str, kind: str, config: CastleConfig, root: Path) -> ActionResult:
+async def activate(name: str, kind: str, config: WildpcConfig, root: Path) -> ActionResult:
"""Make a deployment (name, kind) available in its mode, dispatched by manager."""
manager = _svc_manager(name, kind, config)
@@ -251,9 +251,9 @@ async def activate(name: str, kind: str, config: CastleConfig, root: Path) -> Ac
if manager == "caddy":
# Served by the gateway — reload it so the route is live. Building the
- # assets is `castle program build` (the program's concern), not activation.
+ # assets is `wildpc program build` (the program's concern), not activation.
subprocess.run(
- ["systemctl", "--user", "reload", unit_name("castle-gateway")], check=False
+ ["systemctl", "--user", "reload", unit_name("wildpc-gateway")], check=False
)
return ActionResult(name, "activate", "ok", f"{name}: served via gateway")
@@ -275,7 +275,7 @@ async def activate(name: str, kind: str, config: CastleConfig, root: Path) -> Ac
return ActionResult(name, "activate", "error", f"'{name}' not found")
-async def deactivate(name: str, kind: str, config: CastleConfig, root: Path) -> ActionResult:
+async def deactivate(name: str, kind: str, config: WildpcConfig, root: Path) -> ActionResult:
"""Take a deployment (name, kind) offline in its mode, dispatched by manager."""
manager = _svc_manager(name, kind, config)
diff --git a/core/src/castle_core/manifest.py b/core/src/wildpc_core/manifest.py
similarity index 94%
rename from core/src/castle_core/manifest.py
rename to core/src/wildpc_core/manifest.py
index e24c57e..324245e 100644
--- a/core/src/castle_core/manifest.py
+++ b/core/src/wildpc_core/manifest.py
@@ -1,4 +1,4 @@
-"""Castle manifest models — program specs, service specs, job specs."""
+"""Wild PC manifest models — program specs, service specs, job specs."""
from __future__ import annotations
@@ -110,7 +110,7 @@ class RunCompose(LaunchBase):
"""A multi-container stack supervised as one unit via ``docker compose``.
Unlike ``container`` (a single ``docker run``), compose owns the stack's own
- networking, startup ordering, and per-service health — Castle delegates rather
+ networking, startup ordering, and per-service health — Wild PC delegates rather
than reinventing orchestration. The unit runs ``compose up`` attached
(``Type=simple``) and tears the stack down via a generated ``ExecStop`` (down).
Env/secrets flow through systemd ``Environment=``/``EnvironmentFile=`` (from
@@ -119,7 +119,7 @@ class RunCompose(LaunchBase):
launcher: Literal["compose"]
file: str = "docker-compose.yml" # resolved relative to the program source
- project_name: str | None = None # ``-p``; defaults to ``castle-``
+ project_name: str | None = None # ``-p``; defaults to ``wildpc-``
LaunchSpec = Annotated[
@@ -184,9 +184,9 @@ class HttpExposeSpec(BaseModel):
class TlsMaterial(str, Enum):
- """What cert files castle materializes onto a service from the wildcard cert.
+ """What cert files wildpc materializes onto a service from the wildcard cert.
- ``off`` → the service does its own TLS (or none); castle stays out of it.
+ ``off`` → the service does its own TLS (or none); wildpc stays out of it.
``pair`` → cert.pem + key.pem (postgres, redis, most daemons).
``combined`` → one file: key+cert concatenated (mongodb, haproxy, …).
"""
@@ -197,15 +197,15 @@ class TlsMaterial(str, Enum):
class TlsSpec(BaseModel):
- """Castle-managed TLS material for a raw-TCP service, cut from the gateway's
+ """Wild PC-managed TLS material for a raw-TCP service, cut from the gateway's
ACME wildcard cert (valid for ``.``) and refreshed on renewal.
The service consumes the materialized files via the ``${tls_*}`` placeholders.
"""
material: TlsMaterial = TlsMaterial.OFF
# Optional zero-downtime reload argv (a single command) run after the cert is
- # re-materialized on renewal — e.g. ["systemctl", "--user", "reload", "castle-postgres"].
- # Default (None): castle restarts the deployment (fine at a ~60-day cadence).
+ # re-materialized on renewal — e.g. ["systemctl", "--user", "reload", "wildpc-postgres"].
+ # Default (None): wildpc restarts the deployment (fine at a ~60-day cadence).
reload: list[str] | None = None
@@ -214,8 +214,8 @@ class TcpExposeSpec(BaseModel):
with ``reach: internal`` it's reachable at ``.:`` via the
wildcard DNS record + the bound port (no Caddy route). Publishing the port on
the LAN is the deployment's own job (a container's ``run.ports``, or a native
- service binding ``0.0.0.0``); castle doesn't rebind it, so there's no bind-host
- field here to imply otherwise. ``tls`` (optional) has castle drop the wildcard
+ service binding ``0.0.0.0``); wildpc doesn't rebind it, so there's no bind-host
+ field here to imply otherwise. ``tls`` (optional) has wildpc drop the wildcard
cert onto the service so it presents a trusted cert for ``.``.
"""
@@ -281,7 +281,7 @@ class CommandsSpec(BaseModel):
class Requirement(BaseModel):
"""A precondition — another **deployment** that must exist for this one to be
*functional* (``ref`` = the target deployment's name). ``bind`` names the env
- var castle projects the target's URL into — env is derived *from* the
+ var wildpc projects the target's URL into — env is derived *from* the
requirement, never scraped back into it.
A deployment declares these in its ``requires`` list; ``kind`` defaults to
@@ -315,7 +315,7 @@ class SessionsSpec(BaseModel):
"""Declarative session-history capability for an agent (optional).
Lets the dashboard show a unified picker of an agent's *own* past sessions
- without castle knowing anything agent-specific in code: it runs
+ without wildpc knowing anything agent-specific in code: it runs
``list_command`` (which must print a JSON array of session objects), reads
three named fields off each object, and launches ``command`` + ``resume``
(with ``{id}`` substituted) to reopen one. Field names default to opencode's
@@ -333,7 +333,7 @@ class SessionsSpec(BaseModel):
class AgentSpec(BaseModel):
"""A launchable agent CLI for the dashboard's terminal UX.
- Castle just runs ``command args`` inside a pty at ``cwd``; it never parses
+ Wild PC just runs ``command args`` inside a pty at ``cwd``; it never parses
the agent's output, so any interactive CLI works. This block only names the
launch — live sessions (list/resume/kill) are a runtime concern, not config.
"""
@@ -341,11 +341,11 @@ class AgentSpec(BaseModel):
command: str
args: list[str] = Field(default_factory=list)
description: str | None = None
- cwd: str | None = None # defaults to the castle repo root when unset
+ cwd: str | None = None # defaults to the wildpc repo root when unset
env: EnvMap = Field(default_factory=dict)
# Extra args that open the agent's own session browser / continue its last
# conversation (e.g. ["--resume"] or ["--continue"]). Optional and
- # agent-specific — castle just passes them through. Empty = no such affordance.
+ # agent-specific — wildpc just passes them through. Empty = no such affordance.
resume_args: list[str] = Field(default_factory=list)
# Optional: declares how to list + resume the agent's own sessions, so the
# dashboard can render a unified session picker (see SessionsSpec).
@@ -364,7 +364,7 @@ class ProgramSpec(BaseModel):
description: str | None = None
# A program has NO kind of its own — kind is a *deployment* property. A program
# is a catalog entry that has 0..N deployments, each with its own kind (see
- # kind_for and CastleConfig.deployments_of).
+ # kind_for and WildpcConfig.deployments_of).
source: str | None = None
stack: str | None = None
@@ -424,7 +424,7 @@ class DeploymentBase(BaseModel):
# is `- ref: ` (+ optional `bind: ENV_VAR` to project the target's
# URL into env). Drives the relationship graph's edges. See docs/relationships.md.
requires: list[Requirement] = Field(default_factory=list)
- # Declared on/off state. `castle apply` converges reality to this: enabled
+ # Declared on/off state. `wildpc apply` converges reality to this: enabled
# deployments are activated (service started, tool installed, route served),
# disabled ones are deactivated but kept in the catalog. This is *desired
# state*, not a runtime toggle — the only way to durably stop something.
@@ -548,7 +548,7 @@ class PathDeployment(DeploymentBase):
manager: Literal["path"]
# An Anthropic Messages-API tool definition ({name, description, input_schema})
# for handing this CLI to an agent. Derived from the tool's ``--help`` (see
- # ``castle_core.tool_schema``) but editable/overridable — a stored draft the
+ # ``wildpc_core.tool_schema``) but editable/overridable — a stored draft the
# completion-context builder reads. None → not yet generated.
tool_schema: dict | None = None
diff --git a/core/src/castle_core/registry.py b/core/src/wildpc_core/registry.py
similarity index 95%
rename from core/src/castle_core/registry.py
rename to core/src/wildpc_core/registry.py
index 08764ce..6b5c6a8 100644
--- a/core/src/castle_core/registry.py
+++ b/core/src/wildpc_core/registry.py
@@ -8,7 +8,7 @@ from pathlib import Path
import yaml
-from castle_core.config import CONTENT_DIR, SPECS_DIR
+from wildpc_core.config import CONTENT_DIR, SPECS_DIR
REGISTRY_PATH = SPECS_DIR / "registry.yaml"
STATIC_DIR = CONTENT_DIR # backwards-compat alias
@@ -19,7 +19,7 @@ class NodeConfig:
"""Per-node identity and settings."""
hostname: str = ""
- castle_root: str | None = None # repo path, for dev commands
+ wildpc_root: str | None = None # repo path, for dev commands
gateway_port: int = 9000
# None/"off" → HTTP-only; "internal" → Caddy local-CA HTTPS; "acme" → Let's
# Encrypt wildcard (*.gateway_domain) via DNS-01.
@@ -30,7 +30,7 @@ class NodeConfig:
# Cloudflare tunnel: public services publish at ..
public_domain: str | None = None
tunnel_id: str | None = None
- # Emit the cert_obtained → `castle tls reconcile` hook (needs events-exec plugin).
+ # Emit the cert_obtained → `wildpc tls reconcile` hook (needs events-exec plugin).
cert_hook: bool = False
# Routable host peers use to reach this node's services (LAN IP/hostname).
# Defaults to the hostname; set explicitly when the hostname isn't resolvable
@@ -38,7 +38,7 @@ class NodeConfig:
address: str | None = None
# Fleet role: "authority" may write shared config/secrets to the mesh;
# "follower" reconciles from it. Static (no election) — the authority is
- # pinned in castle.yaml. When the authority is down, shared state is
+ # pinned in wildpc.yaml. When the authority is down, shared state is
# read-only and every node keeps serving its own deployments from cache.
role: str = "follower"
@@ -98,7 +98,7 @@ class Deployment:
base_url: str | None = None
schedule: str | None = None
managed: bool = False
- # Declared desired state (from the deployment's `enabled:`). `castle apply`
+ # Declared desired state (from the deployment's `enabled:`). `wildpc apply`
# activates enabled deployments and deactivates disabled ones. Default True.
enabled: bool = True
# Deployment `requires` (list of {kind, ref, bind}) — carried so the mesh can
@@ -139,14 +139,14 @@ class NodeRegistry:
def load_registry(path: Path | None = None) -> NodeRegistry:
- """Load the node registry from ~/.castle/registry.yaml."""
+ """Load the node registry from ~/.wildpc/registry.yaml."""
if path is None:
path = REGISTRY_PATH
if not path.exists():
raise FileNotFoundError(
f"Registry not found: {path}\n"
- "Run 'castle deploy' to generate it from castle.yaml."
+ "Run 'wildpc deploy' to generate it from wildpc.yaml."
)
with open(path) as f:
@@ -158,7 +158,7 @@ def load_registry(path: Path | None = None) -> NodeRegistry:
node_data = data.get("node", {})
node = NodeConfig(
hostname=node_data.get("hostname", ""),
- castle_root=node_data.get("castle_root"),
+ wildpc_root=node_data.get("wildpc_root"),
gateway_port=node_data.get("gateway_port", 9000),
gateway_tls=node_data.get("gateway_tls"),
gateway_domain=node_data.get("gateway_domain"),
@@ -207,7 +207,7 @@ def load_registry(path: Path | None = None) -> NodeRegistry:
def save_registry(registry: NodeRegistry, path: Path | None = None) -> None:
- """Write the node registry to ~/.castle/registry.yaml."""
+ """Write the node registry to ~/.wildpc/registry.yaml."""
if path is None:
path = REGISTRY_PATH
@@ -221,8 +221,8 @@ def save_registry(registry: NodeRegistry, path: Path | None = None) -> None:
"deployed": {},
}
- if registry.node.castle_root:
- data["node"]["castle_root"] = registry.node.castle_root
+ if registry.node.wildpc_root:
+ data["node"]["wildpc_root"] = registry.node.wildpc_root
if registry.node.gateway_tls:
data["node"]["gateway_tls"] = registry.node.gateway_tls
diff --git a/core/src/castle_core/relations.py b/core/src/wildpc_core/relations.py
similarity index 96%
rename from core/src/castle_core/relations.py
rename to core/src/wildpc_core/relations.py
index ef7513f..7f0d48d 100644
--- a/core/src/castle_core/relations.py
+++ b/core/src/wildpc_core/relations.py
@@ -23,11 +23,11 @@ from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
-from castle_core import git
-from castle_core.config import USER_TOOL_PATH_DIRS, CastleConfig
-from castle_core.generators.systemd import runtime_path
-from castle_core.manifest import Requirement, SystemdDeployment
-from castle_core.stacks import ToolRequirement, tools_for
+from wildpc_core import git
+from wildpc_core.config import USER_TOOL_PATH_DIRS, WildpcConfig
+from wildpc_core.generators.systemd import runtime_path
+from wildpc_core.manifest import Requirement, SystemdDeployment
+from wildpc_core.stacks import ToolRequirement, tools_for
@dataclass
@@ -102,7 +102,7 @@ def _slug(name: str, used: set[str]) -> str:
return key
-def derive_repos(config: CastleConfig) -> dict[str, Repo]:
+def derive_repos(config: WildpcConfig) -> dict[str, Repo]:
"""Group programs by the git working copy their source lives in."""
by_top: dict[str, list[str]] = {}
for pname, prog in config.programs.items():
@@ -132,7 +132,7 @@ def derive_repos(config: CastleConfig) -> dict[str, Repo]:
return repos
-def requirements_of(config: CastleConfig, dep_name: str) -> list[Requirement]:
+def requirements_of(config: WildpcConfig, dep_name: str) -> list[Requirement]:
"""The full requirement set for a deployment: its own ``requires`` (deployment
dependencies), its program's ``system_dependencies`` synthesized as
``kind: system`` requirements, and its stack's toolchains synthesized as
@@ -159,7 +159,7 @@ def requirements_of(config: CastleConfig, dep_name: str) -> list[Requirement]:
return out
-def stack_tools_of(config: CastleConfig, dep_name: str) -> dict[str, ToolRequirement]:
+def stack_tools_of(config: WildpcConfig, dep_name: str) -> dict[str, ToolRequirement]:
"""command → its :class:`ToolRequirement`, for every stack toolchain the
deployment(s) named ``dep_name`` need. The metadata (phase, install hint) behind
the ``kind: tool`` requirements ``requirements_of`` synthesizes — used to check
@@ -210,7 +210,7 @@ def _tool_available(dep: object, tool: ToolRequirement) -> bool:
def _check(
- config: CastleConfig,
+ config: WildpcConfig,
req: Requirement,
dep: object | None = None,
tool: ToolRequirement | None = None,
@@ -265,7 +265,7 @@ def _endpoints_of(dep: object) -> list[Endpoint]:
def build_model(
- config: CastleConfig,
+ config: WildpcConfig,
check: bool = True,
active: set[str] | None = None,
freshness: bool = False,
diff --git a/core/src/castle_core/secret_backends.py b/core/src/wildpc_core/secret_backends.py
similarity index 88%
rename from core/src/castle_core/secret_backends.py
rename to core/src/wildpc_core/secret_backends.py
index 7a7a7df..c0aeadb 100644
--- a/core/src/castle_core/secret_backends.py
+++ b/core/src/wildpc_core/secret_backends.py
@@ -1,18 +1,18 @@
"""Pluggable secret backends for ``${secret:NAME}`` resolution.
-Default is the **file** backend (``~/.castle/secrets/``) — identical to the
+Default is the **file** backend (``~/.wildpc/secrets/``) — identical to the
historical behavior, so nothing changes unless a backend is explicitly selected
-via ``CASTLE_SECRET_BACKEND``.
+via ``WILDPC_SECRET_BACKEND``.
The **openbao** backend reads from an OpenBao/Vault KV-v2 mount and falls back to
the file backend, which is also how it bootstraps: the OpenBao *token* itself is a
file secret (it can't live in the vault it unlocks).
Selection (env, so it works in both the CLI and the systemd-run API):
- CASTLE_SECRET_BACKEND file | openbao (default: file)
- CASTLE_OPENBAO_ADDR http://localhost:8200
- CASTLE_OPENBAO_MOUNT castle (kv-v2 mount path)
- CASTLE_OPENBAO_TOKEN_SECRET OPENBAO_TOKEN (file secret holding the token)
+ WILDPC_SECRET_BACKEND file | openbao (default: file)
+ WILDPC_OPENBAO_ADDR http://localhost:8200
+ WILDPC_OPENBAO_MOUNT wildpc (kv-v2 mount path)
+ WILDPC_OPENBAO_TOKEN_SECRET OPENBAO_TOKEN (file secret holding the token)
"""
from __future__ import annotations
@@ -146,9 +146,9 @@ class OpenBaoBackend:
def build_backend(secrets_dir: Path, settings: dict | None = None) -> SecretBackend:
"""Construct the active secret backend.
- Selection comes from ``settings`` (the ``secrets:`` block of castle.yaml), with
+ Selection comes from ``settings`` (the ``secrets:`` block of wildpc.yaml), with
environment variables overriding — so production is configured declaratively in
- castle.yaml while tests/CI can force a backend via env. Default: file.
+ wildpc.yaml while tests/CI can force a backend via env. Default: file.
The OpenBao **token** is still read from the file backend (the bootstrap root of
trust — it can't live in the vault it unlocks); everything else comes from the
@@ -156,19 +156,19 @@ def build_backend(secrets_dir: Path, settings: dict | None = None) -> SecretBack
"""
settings = settings or {}
file_backend = FileSecretBackend(secrets_dir)
- kind = (os.environ.get("CASTLE_SECRET_BACKEND") or settings.get("backend") or "file").lower()
+ kind = (os.environ.get("WILDPC_SECRET_BACKEND") or settings.get("backend") or "file").lower()
if kind == "openbao":
- addr = os.environ.get("CASTLE_OPENBAO_ADDR") or settings.get(
+ addr = os.environ.get("WILDPC_OPENBAO_ADDR") or settings.get(
"addr"
) or "http://localhost:8200"
- mount = os.environ.get("CASTLE_OPENBAO_MOUNT") or settings.get("mount") or "castle"
- token_secret = os.environ.get("CASTLE_OPENBAO_TOKEN_SECRET") or settings.get(
+ mount = os.environ.get("WILDPC_OPENBAO_MOUNT") or settings.get("mount") or "wildpc"
+ token_secret = os.environ.get("WILDPC_OPENBAO_TOKEN_SECRET") or settings.get(
"token_secret"
) or "OPENBAO_TOKEN"
token = file_backend.read(token_secret) or os.environ.get(
- "CASTLE_OPENBAO_TOKEN", ""
+ "WILDPC_OPENBAO_TOKEN", ""
)
- node_prefix = os.environ.get("CASTLE_OPENBAO_NODE_PREFIX") or settings.get(
+ node_prefix = os.environ.get("WILDPC_OPENBAO_NODE_PREFIX") or settings.get(
"node_prefix"
)
return OpenBaoBackend(addr, token, mount, node_prefix)
diff --git a/core/src/castle_core/stack_status.py b/core/src/wildpc_core/stack_status.py
similarity index 90%
rename from core/src/castle_core/stack_status.py
rename to core/src/wildpc_core/stack_status.py
index f6dd204..054c3b2 100644
--- a/core/src/castle_core/stack_status.py
+++ b/core/src/wildpc_core/stack_status.py
@@ -1,4 +1,4 @@
-"""Stack dependency status — the derived, per-stack health the `castle stack`
+"""Stack dependency status — the derived, per-stack health the `wildpc stack`
command, the ``GET /stacks`` API, and the dashboard Stacks page all render.
A *stack* declares the host toolchains it needs (``stacks.ToolRequirement``); this
@@ -15,10 +15,10 @@ import shutil
import subprocess
from dataclasses import dataclass, field
-from castle_core.config import CastleConfig
-from castle_core.generators.systemd import runtime_path
-from castle_core.relations import _build_path, _tool_available
-from castle_core.stacks import ToolRequirement, available_stacks, get_handler, tools_for
+from wildpc_core.config import WildpcConfig
+from wildpc_core.generators.systemd import runtime_path
+from wildpc_core.relations import _build_path, _tool_available
+from wildpc_core.stacks import ToolRequirement, available_stacks, get_handler, tools_for
@dataclass
@@ -96,9 +96,9 @@ def _tool_status(
def stack_status(
- config: CastleConfig, name: str, *, with_version: bool = True
+ config: WildpcConfig, name: str, *, with_version: bool = True
) -> StackStatus | None:
- """The dependency status of one stack, or None if castle has no such handler."""
+ """The dependency status of one stack, or None if wildpc has no such handler."""
handler = get_handler(name)
if handler is None:
return None
@@ -125,9 +125,9 @@ def stack_status(
def all_stack_status(
- config: CastleConfig, *, with_version: bool = True
+ config: WildpcConfig, *, with_version: bool = True
) -> list[StackStatus]:
- """Dependency status for every stack castle knows — the Stacks catalog."""
+ """Dependency status for every stack wildpc knows — the Stacks catalog."""
out = []
for name in available_stacks():
st = stack_status(config, name, with_version=with_version)
diff --git a/core/src/castle_core/stacks.py b/core/src/wildpc_core/stacks.py
similarity index 97%
rename from core/src/castle_core/stacks.py
rename to core/src/wildpc_core/stacks.py
index dfb5584..f902802 100644
--- a/core/src/castle_core/stacks.py
+++ b/core/src/wildpc_core/stacks.py
@@ -9,9 +9,9 @@ import tomllib
from dataclasses import dataclass
from pathlib import Path
-from castle_core.config import USER_TOOL_PATH_DIRS
-from castle_core.manifest import ProgramSpec
-from castle_core.toolchains import ToolchainError, resolve_node_bin
+from wildpc_core.config import USER_TOOL_PATH_DIRS
+from wildpc_core.manifest import ProgramSpec
+from wildpc_core.toolchains import ToolchainError, resolve_node_bin
DEV_ACTIONS = ["build", "test", "lint", "format", "type-check", "check", "run"]
INSTALL_ACTIONS = ["install", "uninstall"]
@@ -45,13 +45,13 @@ class ActionResult:
@dataclass(frozen=True)
class ToolRequirement:
"""A host toolchain a stack needs — the declarative counterpart to the argv each
- handler hard-codes (``uv sync``, ``pnpm build``, …). Made explicit so castle can
+ handler hard-codes (``uv sync``, ``pnpm build``, …). Made explicit so wildpc can
*check* whether the tool is present (on the box and, for run-phase tools, on the
service's own PATH) and, when it isn't, show a copyable fix instead of failing
mid-subprocess with a raw ``command not found``.
- ``phase`` — when the tool is needed. ``build`` tools run only at
- ``castle apply``/build time (checked against the build env); ``run`` tools must
+ ``wildpc apply``/build time (checked against the build env); ``run`` tools must
also be on the *running service's* PATH (the curated systemd env, which can
drift from your shell); ``both`` is checked in both places.
- ``install_hint`` — the exact command a user can copy to install it.
@@ -70,9 +70,9 @@ def _build_env(node_source: Path | None = None) -> dict[str, str]:
"""Build a subprocess env with user tool dirs on PATH.
``node_source`` is the program's source dir: if it pins a node version (see
- :mod:`castle_core.toolchains`), that node's bin dir goes on the front of PATH so
+ :mod:`wildpc_core.toolchains`), that node's bin dir goes on the front of PATH so
the verb uses the program's node instead of whatever ambient node the caller
- happens to have (the CLI inherits your shell's; the castle-api build executor's
+ happens to have (the CLI inherits your shell's; the wildpc-api build executor's
default PATH has none). Raises :class:`ToolchainError` if the pin isn't installed.
"""
env = os.environ.copy()
@@ -111,7 +111,7 @@ async def _run(
def _vite_base(name: str) -> str:
- """The base path a castle-served static frontend builds against.
+ """The base path a wildpc-served static frontend builds against.
Every frontend now serves at the **root of its own subdomain**
(`.`), so the base is always '/'. Exposed to the build
@@ -130,7 +130,7 @@ class StackHandler:
"""Base class — subclasses implement each lifecycle action."""
# Whether this stack owns *persistent external state* (a database schema, a
- # bucket, …) that outlives a code delete. Drives whether `castle delete`
+ # bucket, …) that outlives a code delete. Drives whether `wildpc delete`
# surfaces a data remnant / honors `--purge-data`. Overridden to True by the
# stacks whose `teardown` actually destroys something.
owns_data: bool = False
@@ -143,9 +143,9 @@ class StackHandler:
# lint/type-check/test also drops `check` implicitly.
provides: set[str] = _STACK_VERBS
- # The host toolchains this stack needs, declared so castle can check them and
+ # The host toolchains this stack needs, declared so wildpc can check them and
# hint a fix. Empty by default (a stackless / declared-command program depends on
- # nothing castle can name); each real handler overrides it. See `tools_for`.
+ # nothing wildpc can name); each real handler overrides it. See `tools_for`.
tools: tuple[ToolRequirement, ...] = ()
async def build(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
@@ -194,7 +194,7 @@ class StackHandler:
delete. Distinct from `uninstall` (which only takes a program offline).
Default: nothing to tear down. Only stacks that set ``owns_data`` and own
- durable state override this; `castle delete --purge-data` invokes it.
+ durable state override this; `wildpc delete --purge-data` invokes it.
"""
return ActionResult(
program=name,
@@ -535,7 +535,7 @@ def _substrate_db_url() -> str | None:
explicit = os.environ.get("SUPABASE_DB_URL")
if explicit:
return explicit
- from castle_core.config import read_secret
+ from wildpc_core.config import read_secret
pw = read_secret("SUPABASE_POSTGRES_PASSWORD")
if pw:
@@ -558,9 +558,9 @@ def app_schema(name: str) -> str:
# The privilege grant that makes an app schema reachable through PostgREST — the
# canonical Supabase "expose a custom schema" snippet. Idempotent; run before
# migrations so `alter default privileges` also covers the tables they create.
-# Exposure ALSO requires the schema in the substrate's PGRST_DB_SCHEMAS — castle
+# Exposure ALSO requires the schema in the substrate's PGRST_DB_SCHEMAS — wildpc
# derives that from the registered supabase apps (`${supabase_app_schemas}`), so a
-# newly-added app needs a `castle deploy` + substrate restart to become routable.
+# newly-added app needs a `wildpc deploy` + substrate restart to become routable.
def _schema_setup_sql(schema: str) -> str:
roles = "anon, authenticated, service_role"
return (
@@ -714,7 +714,7 @@ class SupabaseHandler(StackHandler):
"""Drop the app's schema and everything in it (tables, its own
schema_migrations, functions) in one statement — total and knowable
because the app owns exactly one schema. The substrate's PGRST_DB_SCHEMAS
- drops the (now-absent) schema on the next `castle deploy` + restart.
+ drops the (now-absent) schema on the next `wildpc deploy` + restart.
"""
schema = app_schema(name)
psql = shutil.which("psql")
@@ -745,7 +745,7 @@ class SupabaseHandler(StackHandler):
name,
"teardown",
"ok",
- f"Dropped schema '{schema}' (all tables + rows). Run `castle deploy` "
+ f"Dropped schema '{schema}' (all tables + rows). Run `wildpc deploy` "
"and restart the substrate to prune it from PGRST_DB_SCHEMAS.",
)
@@ -806,7 +806,7 @@ def get_handler(stack: str | None) -> StackHandler | None:
def available_stacks() -> list[str]:
- """The stack names castle has handlers for — the single source of truth for the
+ """The stack names wildpc has handlers for — the single source of truth for the
CLI ``--stack`` choices, the ``GET /stacks`` endpoint, and the dashboard select.
"""
return sorted(HANDLERS)
@@ -815,7 +815,7 @@ def available_stacks() -> list[str]:
def tools_for(stack: str | None) -> tuple[ToolRequirement, ...]:
"""The host toolchains a stack declares it needs (empty for an unknown/absent
stack). The single source of truth for the dependency checks in `relations`,
- `castle stack`, `castle doctor`, and the dashboard Stacks page."""
+ `wildpc stack`, `wildpc doctor`, and the dashboard Stacks page."""
handler = get_handler(stack)
return handler.tools if handler is not None else ()
diff --git a/core/src/castle_core/tls.py b/core/src/wildpc_core/tls.py
similarity index 89%
rename from core/src/castle_core/tls.py
rename to core/src/wildpc_core/tls.py
index 9db22a4..1773266 100644
--- a/core/src/castle_core/tls.py
+++ b/core/src/wildpc_core/tls.py
@@ -1,8 +1,8 @@
-"""Castle-managed TLS material for raw-TCP services.
+"""Wild PC-managed TLS material for raw-TCP services.
Cuts the gateway's ACME wildcard cert (valid for ``.``) onto a
service so it presents a *trusted* cert on its raw port, and refreshes it on
-renewal. Protocol-agnostic: castle only copies files (in the requested format)
+renewal. Protocol-agnostic: wildpc only copies files (in the requested format)
and signals the service — each deployment declares the format (``pair`` /
``combined``) and how it consumes the files (via the ``${tls_*}`` placeholders).
@@ -10,7 +10,7 @@ Two entry points:
- ``materialize_all`` — write/refresh the cert files, no reload (used by ``apply``,
which (re)starts the service itself).
- ``reconcile_tls`` — materialize *and* reload the services whose cert changed
- (used by ``castle tls reconcile`` and the Caddy ``cert_obtained`` hook).
+ (used by ``wildpc tls reconcile`` and the Caddy ``cert_obtained`` hook).
"""
from __future__ import annotations
@@ -21,8 +21,8 @@ import subprocess
import time
from pathlib import Path
-from castle_core.config import CastleConfig
-from castle_core.manifest import SystemdDeployment, TlsMaterial
+from wildpc_core.config import WildpcConfig
+from wildpc_core.manifest import SystemdDeployment, TlsMaterial
_KEY_MODE_FILES = {"key.pem", "combined.pem"} # secret → 0600; certs → 0644
@@ -37,7 +37,7 @@ def wildcard_cert(domain: str) -> tuple[Path, Path] | None:
"""``(crt, key)`` for ``*.`` from Caddy's store, or None.
Caddy stores it at ``certificates//wildcard_./…``. Prefer a
- production cert over staging (``CASTLE_ACME_STAGING=1`` yields a staging dir).
+ production cert over staging (``WILDPC_ACME_STAGING=1`` yields a staging dir).
The ``.crt`` is the full chain (leaf + intermediates).
"""
store = _caddy_data_dir() / "certificates"
@@ -102,7 +102,7 @@ def _wanted_files(
return files
-def materialize_tls(config: CastleConfig, name: str, dep: object) -> bool:
+def materialize_tls(config: WildpcConfig, name: str, dep: object) -> bool:
"""Write ``dep``'s cert files from the wildcard, in its declared format.
Idempotent: returns ``False`` (no write) when the on-disk copy already matches
@@ -144,7 +144,7 @@ def materialize_tls(config: CastleConfig, name: str, dep: object) -> bool:
def materialize_all(
- config: CastleConfig,
+ config: WildpcConfig,
messages: list[str] | None = None,
only: list[str] | None = None,
) -> list[str]:
@@ -152,10 +152,10 @@ def materialize_all(
which starts/restarts the services itself.
``only`` scopes materialization to the given deployment names (what a scoped
- ``castle apply `` is converging). Left None → every deployment. Scoping
+ ``wildpc apply `` is converging). Left None → every deployment. Scoping
keeps a scoped apply from rewriting an *unrelated* service's cert on disk
without also reloading it (which would leave the file diverged from the running
- process until the next ``castle tls reconcile``)."""
+ process until the next ``wildpc tls reconcile``)."""
msgs = messages if messages is not None else []
scope = set(only) if only is not None else None
for _kind, name, dep in config.all_deployments():
@@ -169,14 +169,14 @@ def materialize_all(
def wait_for_wildcard(
- config: CastleConfig,
+ config: WildpcConfig,
names: list[str],
messages: list[str] | None = None,
timeout: float = 120.0,
interval: float = 3.0,
) -> list[str]:
"""Block until the ACME wildcard cert exists, when an in-scope deployment needs
- castle-materialized TLS but the cert isn't issued yet.
+ wildpc-materialized TLS but the cert isn't issued yet.
On a fresh node the gateway reload during ``apply`` only *kicks off* DNS-01
issuance of ``*.`` (seconds to a couple minutes); materializing right
@@ -184,7 +184,7 @@ def wait_for_wildcard(
and with ``gateway.cert_hook`` disabled (the default) nothing would later
reconcile it. Waiting here lets ``apply`` bring the service up with its cert in
place on first deploy. Bounded: on timeout it warns and returns so ``apply``
- still proceeds (rerun ``castle tls reconcile`` once the cert lands)."""
+ still proceeds (rerun ``wildpc tls reconcile`` once the cert lands)."""
msgs = messages if messages is not None else []
needs = [
n
@@ -205,7 +205,7 @@ def wait_for_wildcard(
return msgs
msgs.append(
f"tls: wildcard *.{domain} not ready after {int(timeout)}s — "
- f"{', '.join(needs)} may start without a cert; rerun `castle tls reconcile` "
+ f"{', '.join(needs)} may start without a cert; rerun `wildpc tls reconcile` "
"once it is issued"
)
return msgs
@@ -218,14 +218,14 @@ def _reload(name: str, tls: object, msgs: list[str]) -> None:
msgs.append(f"tls: reloaded {name} (reload command)")
else:
subprocess.run(
- ["systemctl", "--user", "restart", f"castle-{name}.service"], check=False
+ ["systemctl", "--user", "restart", f"wildpc-{name}.service"], check=False
)
msgs.append(f"tls: restarted {name} to pick up rotated cert")
-def reconcile_tls(config: CastleConfig, messages: list[str] | None = None) -> list[str]:
+def reconcile_tls(config: WildpcConfig, messages: list[str] | None = None) -> list[str]:
"""Materialize certs and reload the services whose cert changed. Idempotent —
- a no-op when nothing rotated. Invoked by ``castle tls reconcile`` and the Caddy
+ a no-op when nothing rotated. Invoked by ``wildpc tls reconcile`` and the Caddy
``cert_obtained`` hook. Logs its own outcome (the hook's ``exec`` swallows it)."""
msgs = messages if messages is not None else []
for _kind, name, dep in config.all_deployments():
diff --git a/core/src/castle_core/tool_schema.py b/core/src/wildpc_core/tool_schema.py
similarity index 96%
rename from core/src/castle_core/tool_schema.py
rename to core/src/wildpc_core/tool_schema.py
index 73018a5..6f9dc1d 100644
--- a/core/src/castle_core/tool_schema.py
+++ b/core/src/wildpc_core/tool_schema.py
@@ -1,6 +1,6 @@
"""Derive LLM tool-call schemas from a CLI tool's ``--help``.
-castle's in-process version of the standalone ``toolify`` tool: given a tool (a
+wildpc's in-process version of the standalone ``toolify`` tool: given a tool (a
program with a ``path`` deployment), resolve its executable, run ``--help``, and
build a tool-call definition.
@@ -14,10 +14,10 @@ Two parameter shapes, chosen automatically:
Schemas are built and stored in a **neutral** core (``{name, description,
parameters}``); ``render_tool_schema`` wraps that in the Anthropic or OpenAI
-envelope on read. castle's feed defaults to OpenAI (litellm-native).
+envelope on read. wildpc's feed defaults to OpenAI (litellm-native).
The extraction is intentionally duplicated from ``toolify`` rather than shared —
-``toolify`` is a standalone program that must never depend on castle. No LLM is
+``toolify`` is a standalone program that must never depend on wildpc. No LLM is
used; the output is a deterministic function of the tool's ``--help``.
"""
@@ -31,7 +31,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from castle_core.config import CastleConfig
+ from wildpc_core.config import WildpcConfig
__all__ = [
"ToolSchemaError",
@@ -252,7 +252,7 @@ def _command_core(name: str, argv: list[str], help_text: str, deep: bool) -> dic
}
-def tool_executable(config: CastleConfig, name: str) -> str:
+def tool_executable(config: WildpcConfig, name: str) -> str:
"""The console script to invoke for tool ``name`` — its first
``[project.scripts]`` key (source of truth even when uninstalled), else the
program name. Mirrors the CLI's tools lens."""
@@ -271,7 +271,7 @@ def tool_executable(config: CastleConfig, name: str) -> str:
return name
-def collect_tool_help(config: CastleConfig, name: str) -> str:
+def collect_tool_help(config: WildpcConfig, name: str) -> str:
"""The full recursive ``--help`` text for tool ``name`` — top-level plus each
subcommand's help, the raw material an LLM assist reads to structure a
subcommand tree. Deterministic; raises ``ToolSchemaError`` if unresolved.
@@ -281,7 +281,7 @@ def collect_tool_help(config: CastleConfig, name: str) -> str:
if exe is None:
raise ToolSchemaError(
f"`{exe_name}` is not on PATH — install the tool (enable it, then "
- f"`castle apply`) before generating its schema."
+ f"`wildpc apply`) before generating its schema."
)
_, help_text = _run_help([exe])
if not help_text:
@@ -345,7 +345,7 @@ def is_tool_schema_core(obj: object) -> bool:
return not validate_tool_schema_core(obj)
-def derive_tool_schema(config: CastleConfig, name: str, deep: bool = False) -> dict:
+def derive_tool_schema(config: WildpcConfig, name: str, deep: bool = False) -> dict:
"""Derive the neutral tool-call core for tool ``name`` from its ``--help``.
Structured params when the help is standard and flat; the command-string
@@ -357,7 +357,7 @@ def derive_tool_schema(config: CastleConfig, name: str, deep: bool = False) -> d
if exe is None:
raise ToolSchemaError(
f"`{exe_name}` is not on PATH — install the tool (enable it, then "
- f"`castle apply`) before generating its schema."
+ f"`wildpc apply`) before generating its schema."
)
_, help_text = _run_help([exe])
if not help_text:
diff --git a/core/src/castle_core/toolchains.py b/core/src/wildpc_core/toolchains.py
similarity index 92%
rename from core/src/castle_core/toolchains.py
rename to core/src/wildpc_core/toolchains.py
index 06ffdca..083e51c 100644
--- a/core/src/castle_core/toolchains.py
+++ b/core/src/wildpc_core/toolchains.py
@@ -3,18 +3,18 @@ binary directory on this box.
Today this covers **node**. Programs pin their node version the ecosystem-standard
way — a ``.node-version`` / ``.nvmrc`` file, or ``package.json`` ``engines.node`` /
-``volta.node`` — so a program stays castle-independent (the prime directive: regular
-programs never depend on castle). Castle *reads* that pin and resolves it to an
+``volta.node`` — so a program stays wildpc-independent (the prime directive: regular
+programs never depend on wildpc). Wild PC *reads* that pin and resolves it to an
absolute ``.../bin`` directory it can put on PATH, at both execution sites that run a
program's node:
-- **build time** — the dev-verb subprocess (``stacks._build_env``), so ``castle
+- **build time** — the dev-verb subprocess (``stacks._build_env``), so ``wildpc
program build`` uses the program's node regardless of who triggers it (your shell
- or the castle-api build executor);
+ or the wildpc-api build executor);
- **run time** — a ``launcher: node`` service's systemd unit PATH (via
``deploy._build_deployed`` → the generated unit's ``Environment=PATH``).
-Resolution scans nvm's versioned install layout (``CASTLE_NODE_VERSIONS_DIR``,
+Resolution scans nvm's versioned install layout (``WILDPC_NODE_VERSIONS_DIR``,
default ``~/.nvm/versions/node``) — the versioned dir the default tool PATH
intentionally omits. A pinned-but-not-installed version fails loud with an
actionable ``nvm install`` hint rather than surfacing later as ``node: not found``.
@@ -41,7 +41,7 @@ class ToolchainError(Exception):
def node_versions_dir() -> Path:
"""The directory nvm installs versioned node under (env-overridable)."""
- override = os.environ.get("CASTLE_NODE_VERSIONS_DIR")
+ override = os.environ.get("WILDPC_NODE_VERSIONS_DIR")
return Path(override) if override else Path.home() / ".nvm" / "versions" / "node"
@@ -86,7 +86,7 @@ def _installed() -> list[tuple[tuple[int, int, int], Path]]:
def resolve_node_bin(source: Path | None) -> Path | None:
"""Resolve a program's pinned node version to its ``.../bin`` dir on this box.
- Returns ``None`` when the program pins nothing — castle injects no node, it does
+ Returns ``None`` when the program pins nothing — wildpc injects no node, it does
not guess. Raises :class:`ToolchainError` when a version *is* pinned but no
matching version is installed.
diff --git a/core/tests/conftest.py b/core/tests/conftest.py
index 06b5933..b7abf0b 100644
--- a/core/tests/conftest.py
+++ b/core/tests/conftest.py
@@ -1,4 +1,4 @@
-"""Shared fixtures for castle core tests."""
+"""Shared fixtures for wildpc core tests."""
from __future__ import annotations
@@ -6,9 +6,9 @@ import os as _os
from collections.abc import Generator
from pathlib import Path
-# Tests must not read the host's real secret backend (castle.yaml may point
+# Tests must not read the host's real secret backend (wildpc.yaml may point
# at OpenBao); force the file backend unless CI explicitly overrides.
-_os.environ.setdefault("CASTLE_SECRET_BACKEND", "file")
+_os.environ.setdefault("WILDPC_SECRET_BACKEND", "file")
import pytest
import yaml
@@ -64,16 +64,16 @@ def _store_for(spec: dict) -> str:
}[spec["manager"]]
-def write_castle_config(root: Path, config: dict) -> None:
- """Scatter a nested castle config dict into the on-disk layout.
+def write_wildpc_config(root: Path, config: dict) -> None:
+ """Scatter a nested wildpc config dict into the on-disk layout.
`config` uses the terse nested shape (gateway/repo at top level, plus
- programs/services/jobs mappings); this writes castle.yaml with globals, one file
+ programs/services/jobs mappings); this writes wildpc.yaml with globals, one file
per program under programs/, and each deployment under deployments// after
modernizing its legacy fields (see `_modernize_deployment`).
"""
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
- (root / "castle.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
+ (root / "wildpc.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
programs = config.get("programs") or {}
if programs:
@@ -94,8 +94,8 @@ def write_castle_config(root: Path, config: dict) -> None:
@pytest.fixture
-def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
- """Create a temporary castle root with directory-per-resource config."""
+def wildpc_root(tmp_path: Path) -> Generator[Path, None, None]:
+ """Create a temporary wildpc root with directory-per-resource config."""
config = {
"gateway": {"port": 18000},
"programs": {
@@ -141,7 +141,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
},
},
}
- write_castle_config(tmp_path, config)
+ write_wildpc_config(tmp_path, config)
# Create project directories
svc_dir = tmp_path / "test-svc"
@@ -155,9 +155,9 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
@pytest.fixture
-def castle_home(tmp_path: Path) -> Generator[Path, None, None]:
- """Create a temporary ~/.castle directory."""
- home = tmp_path / ".castle"
+def wildpc_home(tmp_path: Path) -> Generator[Path, None, None]:
+ """Create a temporary ~/.wildpc directory."""
+ home = tmp_path / ".wildpc"
home.mkdir()
(home / "generated").mkdir()
(home / "secrets").mkdir()
diff --git a/core/tests/test_apply.py b/core/tests/test_apply.py
index 32dbb97..fc76264 100644
--- a/core/tests/test_apply.py
+++ b/core/tests/test_apply.py
@@ -1,4 +1,4 @@
-"""Tests for `castle apply` convergence — the diff classification (plan mode).
+"""Tests for `wildpc apply` convergence — the diff classification (plan mode).
Plan mode computes the activate/restart/deactivate/unchanged buckets without
writing or touching the runtime, so it's the deterministic way to test the diff.
@@ -11,78 +11,78 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
-import castle_core.deploy as deploy_mod
-from castle_core.config import load_config
-from castle_core.deploy import (
+import wildpc_core.deploy as deploy_mod
+from wildpc_core.config import load_config
+from wildpc_core.deploy import (
_desired_registry,
_gateway_would_change,
_render_unit_preview,
apply,
generate_caddyfile_from_registry,
)
-from castle_core.registry import Deployment
+from wildpc_core.registry import Deployment
-def _add_static(castle_root: Path, name: str = "test-static") -> None:
- """Write a caddy (static) program + deployment into an existing castle root."""
- (castle_root / "programs" / f"{name}.yaml").write_text(
- f"description: Static {name}\nsource: {castle_root / name}\n"
+def _add_static(wildpc_root: Path, name: str = "test-static") -> None:
+ """Write a caddy (static) program + deployment into an existing wildpc root."""
+ (wildpc_root / "programs" / f"{name}.yaml").write_text(
+ f"description: Static {name}\nsource: {wildpc_root / name}\n"
)
- statics = castle_root / "deployments" / "statics"
+ statics = wildpc_root / "deployments" / "statics"
statics.mkdir(parents=True, exist_ok=True)
(statics / f"{name}.yaml").write_text(
f"program: {name}\nmanager: caddy\nroot: public\nreach: internal\n"
)
- (castle_root / name / "public").mkdir(parents=True, exist_ok=True)
+ (wildpc_root / name / "public").mkdir(parents=True, exist_ok=True)
-def _plan(castle_root: Path, active: dict[str, bool]):
+def _plan(wildpc_root: Path, active: dict[str, bool]):
"""Run apply(plan=True) with is_active stubbed to `active` (default False)."""
with patch(
- "castle_core.lifecycle.is_active",
+ "wildpc_core.lifecycle.is_active",
side_effect=lambda n, k, c: active.get(n, False),
):
- return apply(root=castle_root, plan=True)
+ return apply(root=wildpc_root, plan=True)
class TestApplyPlan:
- def test_fresh_converge_activates_enabled(self, castle_root: Path) -> None:
+ def test_fresh_converge_activates_enabled(self, wildpc_root: Path) -> None:
"""Nothing running → every enabled deployment is 'activate'; no writes."""
- result = _plan(castle_root, active={})
+ result = _plan(wildpc_root, active={})
assert result.planned is True
assert set(result.activated) == {"test-svc", "test-tool", "test-job"}
assert result.deactivated == []
assert result.restarted == []
- def test_disabled_active_deployment_deactivates(self, castle_root: Path) -> None:
+ def test_disabled_active_deployment_deactivates(self, wildpc_root: Path) -> None:
"""A deployment with enabled:false that's currently up → 'deactivate'."""
# Turn the tool off in config.
- tool = castle_root / "deployments" / "tools" / "test-tool.yaml"
+ tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
tool.write_text(tool.read_text() + "enabled: false\n")
- result = _plan(castle_root, active={"test-tool": True})
+ result = _plan(wildpc_root, active={"test-tool": True})
assert "test-tool" in result.deactivated
assert "test-tool" not in result.activated
- def test_disabled_inactive_is_unchanged(self, castle_root: Path) -> None:
+ def test_disabled_inactive_is_unchanged(self, wildpc_root: Path) -> None:
"""enabled:false and already down → nothing to do."""
- tool = castle_root / "deployments" / "tools" / "test-tool.yaml"
+ tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
tool.write_text(tool.read_text() + "enabled: false\n")
- result = _plan(castle_root, active={})
+ result = _plan(wildpc_root, active={})
assert "test-tool" in result.unchanged
assert "test-tool" not in result.deactivated
- def test_active_service_with_changed_unit_restarts(self, castle_root: Path) -> None:
+ def test_active_service_with_changed_unit_restarts(self, wildpc_root: Path) -> None:
"""An up systemd service whose rendered unit differs from disk → 'restart'.
The temp home has no prior unit file (before-bytes = None), so any live
systemd deployment classifies as changed → restart, not a silent no-op.
"""
- result = _plan(castle_root, active={"test-svc": True})
+ result = _plan(wildpc_root, active={"test-svc": True})
assert "test-svc" in result.restarted
assert "test-svc" not in result.activated
@@ -95,35 +95,35 @@ class TestGatewayChange:
would-be Caddyfile/tunnel config against disk — otherwise a new/changed static
route reports a false 'already converged'.
- SPECS_DIR is the real ~/.castle path (unpatched by the fixtures), so these
+ SPECS_DIR is the real ~/.wildpc path (unpatched by the fixtures), so these
redirect it to a temp dir to stay hermetic and never touch the live Caddyfile.
"""
def test_new_route_reports_gateway_changed(
- self, castle_root: Path, tmp_path: Path, monkeypatch
+ self, wildpc_root: Path, tmp_path: Path, monkeypatch
) -> None:
"""A static whose route isn't on disk yet → gateway_changed, even when the
assets already exist so the deployment itself classifies 'unchanged'."""
monkeypatch.setattr(deploy_mod, "SPECS_DIR", tmp_path / "specs")
- _add_static(castle_root)
+ _add_static(wildpc_root)
# Static is 'active' (built) → _classify buckets it 'unchanged'; the route is
# still absent from the (missing) Caddyfile, so the gateway did change.
- result = _plan(castle_root, active={"test-static": True})
+ result = _plan(wildpc_root, active={"test-static": True})
assert "test-static" in result.unchanged
assert result.gateway_changed is True
assert result.changed is True
def test_converged_caddyfile_is_not_changed(
- self, castle_root: Path, tmp_path: Path, monkeypatch
+ self, wildpc_root: Path, tmp_path: Path, monkeypatch
) -> None:
"""When the on-disk Caddyfile already matches the desired one, no change."""
specs = tmp_path / "specs"
specs.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr(deploy_mod, "SPECS_DIR", specs)
- _add_static(castle_root)
- config = load_config(castle_root)
+ _add_static(wildpc_root)
+ config = load_config(wildpc_root)
(specs / "Caddyfile").write_text(
generate_caddyfile_from_registry(_desired_registry(config, None))
diff --git a/core/tests/test_audit.py b/core/tests/test_audit.py
index 16b638b..1d92d4f 100644
--- a/core/tests/test_audit.py
+++ b/core/tests/test_audit.py
@@ -1,10 +1,10 @@
-"""Tests for the consumption audit (core/src/castle_core/audit.py)."""
+"""Tests for the consumption audit (core/src/wildpc_core/audit.py)."""
from __future__ import annotations
-import castle_core.config as C
-from castle_core import audit
-from castle_core.manifest import SystemdDeployment
+import wildpc_core.config as C
+from wildpc_core import audit
+from wildpc_core.manifest import SystemdDeployment
def _svc(
@@ -33,8 +33,8 @@ def _svc(
return SystemdDeployment.model_validate(spec)
-def _cfg(deployments: dict) -> C.CastleConfig:
- return C.CastleConfig(
+def _cfg(deployments: dict) -> C.WildpcConfig:
+ return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000),
repo=None,
@@ -43,7 +43,7 @@ def _cfg(deployments: dict) -> C.CastleConfig:
)
-def _pairs(cfg: C.CastleConfig) -> set[tuple[str, str]]:
+def _pairs(cfg: C.WildpcConfig) -> set[tuple[str, str]]:
return {(s.consumer, s.provider) for s in audit.suggest_consumption(cfg)}
@@ -55,7 +55,7 @@ def test_split_host_port_pair_is_suggested() -> None:
"api": _svc(
"api",
http=9020,
- env={"CASTLE_API_MQTT_HOST": "localhost", "CASTLE_API_MQTT_PORT": "1883"},
+ env={"WILDPC_API_MQTT_HOST": "localhost", "WILDPC_API_MQTT_PORT": "1883"},
),
}
)
diff --git a/core/tests/test_caddyfile.py b/core/tests/test_caddyfile.py
index e23a995..46cc70c 100644
--- a/core/tests/test_caddyfile.py
+++ b/core/tests/test_caddyfile.py
@@ -4,12 +4,12 @@ from __future__ import annotations
import pytest
-from castle_core.config import CastleConfig, GatewayConfig
-from castle_core.generators.caddyfile import (
+from wildpc_core.config import WildpcConfig, GatewayConfig
+from wildpc_core.generators.caddyfile import (
compute_routes,
generate_caddyfile_from_registry,
)
-from castle_core.manifest import (
+from wildpc_core.manifest import (
CaddyDeployment,
DeploymentSpec,
ExposeSpec,
@@ -20,14 +20,14 @@ from castle_core.manifest import (
RunPython,
SystemdDeployment,
)
-from castle_core.registry import Deployment, NodeConfig, NodeRegistry
+from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
@pytest.fixture(autouse=True)
def _isolate_config(monkeypatch: pytest.MonkeyPatch) -> None:
- """Isolate the generator from the real ~/.castle config so static-frontend
+ """Isolate the generator from the real ~/.wildpc config so static-frontend
routes don't leak into these registry-focused tests."""
- import castle_core.config as config_mod
+ import wildpc_core.config as config_mod
def _no_config(*args: object, **kwargs: object) -> object:
raise FileNotFoundError("isolated in tests")
@@ -101,7 +101,7 @@ class TestAcmeMode:
def test_port_9000_redirects_to_dashboard(self) -> None:
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
assert ":9000 {" in cf
- assert "redir https://castle.example.com{uri}" in cf
+ assert "redir https://wildpc.example.com{uri}" in cf
def test_no_path_routes(self) -> None:
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
@@ -114,7 +114,7 @@ class TestAcmeMode:
assert "pg.example.com" not in cf
def test_staging_toggle(self, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setenv("CASTLE_ACME_STAGING", "1")
+ monkeypatch.setenv("WILDPC_ACME_STAGING", "1")
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
assert "acme_ca https://acme-staging-v02.api.letsencrypt.org/directory" in cf
@@ -122,20 +122,20 @@ class TestAcmeMode:
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# A static frontend is a `runner: static` service serving /.
- import castle_core.config as config_mod
+ import wildpc_core.config as config_mod
cfg = _config(
deployments={
- "castle": CaddyDeployment(
- manager="caddy", program="castle", root="dist"
+ "wildpc": CaddyDeployment(
+ manager="caddy", program="wildpc", root="dist"
)
},
- programs={"castle": ProgramSpec(source="/data/repos/castle/app")},
+ programs={"wildpc": ProgramSpec(source="/data/repos/wildpc/app")},
)
monkeypatch.setattr(config_mod, "load_config", lambda *a, **k: cfg)
cf = generate_caddyfile_from_registry(_acme({}))
- assert "@host_castle host castle.example.com" in cf
- assert "root * /data/repos/castle/app/dist" in cf
+ assert "@host_wildpc host wildpc.example.com" in cf
+ assert "root * /data/repos/wildpc/app/dist" in cf
assert "try_files {path} /index.html" in cf
assert "file_server" in cf
@@ -187,12 +187,12 @@ class TestPublicExposure:
class TestOffMode:
- """No domain → HTTP-only control plane on :: dashboard at / + /api → castle-api.
+ """No domain → HTTP-only control plane on :: dashboard at / + /api → wildpc-api.
Other services are port-only (not routed)."""
def test_control_plane(self) -> None:
cf = generate_caddyfile_from_registry(
- _make_registry(deployed={"castle-api": _dep(9020, expose=True, name="castle-api")})
+ _make_registry(deployed={"wildpc-api": _dep(9020, expose=True, name="wildpc-api")})
)
assert "auto_https off" in cf
assert "handle_path /api/* {" in cf
@@ -217,8 +217,8 @@ def _service(port: int, *, expose: bool) -> SystemdDeployment:
def _config(
deployments: dict[str, DeploymentSpec], programs: dict[str, ProgramSpec] | None = None
-) -> CastleConfig:
- return CastleConfig(
+) -> WildpcConfig:
+ return WildpcConfig(
root=None, # type: ignore[arg-type]
gateway=GatewayConfig(port=9000),
repo=None,
@@ -228,7 +228,7 @@ def _config(
class TestConfigSourceOfTruth:
- """compute_routes derives exposure/port from castle.yaml (the checkbox), so a
+ """compute_routes derives exposure/port from wildpc.yaml (the checkbox), so a
regenerated Caddyfile tracks the spec, not a stale registry."""
def test_exposed_service_becomes_a_route(self) -> None:
diff --git a/core/tests/test_caddyfile_remote.py b/core/tests/test_caddyfile_remote.py
index 2d2dbf6..bc7bc8e 100644
--- a/core/tests/test_caddyfile_remote.py
+++ b/core/tests/test_caddyfile_remote.py
@@ -5,13 +5,13 @@ from __future__ import annotations
from pathlib import Path
import yaml
-from castle_core.config import load_config
-from castle_core.generators.caddyfile import compute_routes
-from castle_core.registry import Deployment, NodeConfig, NodeRegistry
+from wildpc_core.config import load_config
+from wildpc_core.generators.caddyfile import compute_routes
+from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
def _config_requiring_widget(root: Path) -> None:
- (root / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
+ (root / "wildpc.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
svc_dir = root / "deployments" / "services"
svc_dir.mkdir(parents=True)
(svc_dir / "consumer.yaml").write_text(
@@ -79,7 +79,7 @@ def test_breaker_no_route_when_peer_absent(tmp_path: Path) -> None:
def test_no_remote_route_for_unconsumed_service(tmp_path: Path) -> None:
"""A peer service nobody requires is not routed."""
- (tmp_path / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
+ (tmp_path / "wildpc.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
config = load_config(tmp_path) # no requires anywhere
routes = compute_routes(
_local(), config, {"tower": _peer_with_widget("10.0.0.5")}
diff --git a/core/tests/test_config.py b/core/tests/test_config.py
index 1e1f540..5e93d06 100644
--- a/core/tests/test_config.py
+++ b/core/tests/test_config.py
@@ -1,4 +1,4 @@
-"""Tests for castle configuration loading."""
+"""Tests for wildpc configuration loading."""
from __future__ import annotations
@@ -6,138 +6,138 @@ from pathlib import Path
import pytest
import yaml
-from castle_core.config import (
- CastleConfig,
+from wildpc_core.config import (
+ WildpcConfig,
load_config,
resolve_env_split,
resolve_env_vars,
save_config,
secret_refs,
)
-from castle_core.manifest import ProgramSpec, SystemdDeployment
+from wildpc_core.manifest import ProgramSpec, SystemdDeployment
class TestLoadConfig:
- """Tests for loading castle.yaml."""
+ """Tests for loading wildpc.yaml."""
- def test_load_basic(self, castle_root: Path) -> None:
- """Load a castle.yaml with three sections."""
- config = load_config(castle_root)
- assert isinstance(config, CastleConfig)
+ def test_load_basic(self, wildpc_root: Path) -> None:
+ """Load a wildpc.yaml with three sections."""
+ config = load_config(wildpc_root)
+ assert isinstance(config, WildpcConfig)
assert config.gateway.port == 18000
assert "test-tool" in config.programs
assert "test-svc" in config.services
assert "test-job" in config.jobs
- def test_load_produces_typed_specs(self, castle_root: Path) -> None:
+ def test_load_produces_typed_specs(self, wildpc_root: Path) -> None:
"""Each section produces the correct spec type."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
assert isinstance(config.programs["test-tool"], ProgramSpec)
# Both a service and a job are systemd deployments; the kind (service/job)
# is derived from whether a schedule is present.
assert isinstance(config.services["test-svc"], SystemdDeployment)
assert isinstance(config.jobs["test-job"], SystemdDeployment)
- def test_service_expose(self, castle_root: Path) -> None:
+ def test_service_expose(self, wildpc_root: Path) -> None:
"""Service has correct expose spec."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.expose.http.internal.port == 19000
assert svc.expose.http.health_path == "/health"
- def test_service_proxy(self, castle_root: Path) -> None:
+ def test_service_proxy(self, wildpc_root: Path) -> None:
"""Service has correct proxy spec."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.proxy is True # exposed at .
- def test_service_run_spec(self, castle_root: Path) -> None:
+ def test_service_run_spec(self, wildpc_root: Path) -> None:
"""Service has correct launch spec (legacy runner normalized to launcher)."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.run.launcher == "python"
assert svc.run.program == "test-svc"
- def test_service_component_ref(self, castle_root: Path) -> None:
+ def test_service_component_ref(self, wildpc_root: Path) -> None:
"""Service references a component."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.program == "test-svc-comp"
- def test_job_schedule(self, castle_root: Path) -> None:
+ def test_job_schedule(self, wildpc_root: Path) -> None:
"""Job has correct schedule."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
job = config.jobs["test-job"]
assert job.schedule == "0 2 * * *"
- def test_tools_property(self, castle_root: Path) -> None:
+ def test_tools_property(self, wildpc_root: Path) -> None:
"""Tools property filters to components with install.path or tool."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
assert "test-tool" in config.tools
def test_missing_config_raises(self, tmp_path: Path) -> None:
- """Missing castle.yaml raises FileNotFoundError."""
+ """Missing wildpc.yaml raises FileNotFoundError."""
with pytest.raises(FileNotFoundError):
load_config(tmp_path)
class TestSaveConfig:
- """Tests for saving castle.yaml."""
+ """Tests for saving wildpc.yaml."""
- def test_round_trip(self, castle_root: Path) -> None:
+ def test_round_trip(self, wildpc_root: Path) -> None:
"""Load and save should produce equivalent config."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
save_config(config)
- config2 = load_config(castle_root)
+ config2 = load_config(wildpc_root)
assert config2.gateway.port == config.gateway.port
assert set(config2.programs.keys()) == set(config.programs.keys())
assert set(config2.services.keys()) == set(config.services.keys())
assert set(config2.jobs.keys()) == set(config.jobs.keys())
- def test_save_adds_component(self, castle_root: Path) -> None:
+ def test_save_adds_component(self, wildpc_root: Path) -> None:
"""Adding a component and saving persists it."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.programs["new-lib"] = ProgramSpec(
id="new-lib", description="A new library"
)
save_config(config)
- config2 = load_config(castle_root)
+ config2 = load_config(wildpc_root)
assert "new-lib" in config2.programs
assert config2.programs["new-lib"].description == "A new library"
- def test_preserves_manage_systemd(self, castle_root: Path) -> None:
+ def test_preserves_manage_systemd(self, wildpc_root: Path) -> None:
"""Roundtrip preserves manage.systemd even with all defaults."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
save_config(config)
- config2 = load_config(castle_root)
+ config2 = load_config(wildpc_root)
svc = config2.services["test-svc"]
assert svc.manage is not None
assert svc.manage.systemd is not None
- def test_writes_directory_layout(self, castle_root: Path) -> None:
+ def test_writes_directory_layout(self, wildpc_root: Path) -> None:
"""Save writes one file per resource under programs/ and one deployments/ dir."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
save_config(config)
- assert (castle_root / "programs" / "test-tool.yaml").exists()
+ assert (wildpc_root / "programs" / "test-tool.yaml").exists()
# Deployments live under per-kind subdirs (deployments//.yaml).
- assert (castle_root / "deployments" / "services" / "test-svc.yaml").exists()
- assert (castle_root / "deployments" / "jobs" / "test-job.yaml").exists()
- assert (castle_root / "deployments" / "tools" / "test-tool.yaml").exists()
+ assert (wildpc_root / "deployments" / "services" / "test-svc.yaml").exists()
+ assert (wildpc_root / "deployments" / "jobs" / "test-job.yaml").exists()
+ assert (wildpc_root / "deployments" / "tools" / "test-tool.yaml").exists()
# Global file holds gateway only, no resource sections
- global_data = yaml.safe_load((castle_root / "castle.yaml").read_text())
+ global_data = yaml.safe_load((wildpc_root / "wildpc.yaml").read_text())
assert global_data["gateway"]["port"] == 18000
assert "programs" not in global_data
assert "deployments" not in global_data
- def test_delete_prunes_file(self, castle_root: Path) -> None:
+ def test_delete_prunes_file(self, wildpc_root: Path) -> None:
"""Removing a deployment and saving deletes its on-disk file."""
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
del config.services["test-svc"]
save_config(config)
- assert not (castle_root / "deployments" / "services" / "test-svc.yaml").exists()
- config2 = load_config(castle_root)
+ assert not (wildpc_root / "deployments" / "services" / "test-svc.yaml").exists()
+ config2 = load_config(wildpc_root)
assert "test-svc" not in config2.services
assert "test-tool" in config2.programs
@@ -164,7 +164,7 @@ class TestResolveEnvVars:
secrets_dir = tmp_path / "secrets"
secrets_dir.mkdir()
(secrets_dir / "API_KEY").write_text("my-secret-key\n")
- monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
+ monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
env = {"API_KEY": "${secret:API_KEY}"}
resolved = resolve_env_vars(env)
@@ -176,7 +176,7 @@ class TestResolveEnvVars:
"""Missing secret returns placeholder."""
secrets_dir = tmp_path / "secrets"
secrets_dir.mkdir()
- monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
+ monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
env = {"API_KEY": "${secret:NONEXISTENT}"}
resolved = resolve_env_vars(env)
@@ -207,7 +207,7 @@ class TestResolveEnvSplit:
secrets_dir.mkdir()
for name, val in vals.items():
(secrets_dir / name).write_text(val + "\n")
- monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
+ monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
def test_plain_only(self) -> None:
plain, secret = resolve_env_split({"PORT": "9001", "URL": "http://x"})
@@ -273,8 +273,8 @@ class TestConfigRoundTrip:
the next write — these lock the full round-trip for the reach/TCP-TLS fields."""
def test_gateway_and_tcp_tls_survive_save_load(self, tmp_path: Path) -> None:
- from castle_core.config import GatewayConfig
- from castle_core.manifest import (
+ from wildpc_core.config import GatewayConfig
+ from wildpc_core.manifest import (
ExposeSpec,
Reach,
RunContainer,
@@ -299,7 +299,7 @@ class TestConfigRoundTrip:
tcp=TcpExposeSpec(port=5432, tls=TlsSpec(material=TlsMaterial.PAIR))
),
)
- config = CastleConfig(
+ config = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(
port=9000, tls="acme", domain="civil.payne.io", cert_hook=True
@@ -327,7 +327,7 @@ class TestConfigRoundTrip:
def test_parse_gateway_preserves_all_fields(self) -> None:
"""The shared gateway parser must honor every field — `save_yaml` used to
read only `port`, wiping tls/domain/tunnel/cert_hook on a whole-file save."""
- from castle_core.config import parse_gateway
+ from wildpc_core.config import parse_gateway
g = parse_gateway(
{
@@ -349,27 +349,27 @@ class TestConfigRoundTrip:
class TestConfigurableRoots:
- """data_dir / repos_dir: env > castle.yaml > default. The single source of truth
+ """data_dir / repos_dir: env > wildpc.yaml > default. The single source of truth
that keeps the CLI and the api service from resolving different data dirs."""
@pytest.fixture(autouse=True)
def _no_root_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
- # The test host may export CASTLE_DATA_DIR (that's the bug we're fixing);
+ # The test host may export WILDPC_DATA_DIR (that's the bug we're fixing);
# clear it so yaml/default precedence is exercised deterministically.
- monkeypatch.delenv("CASTLE_DATA_DIR", raising=False)
- monkeypatch.delenv("CASTLE_REPOS_DIR", raising=False)
+ monkeypatch.delenv("WILDPC_DATA_DIR", raising=False)
+ monkeypatch.delenv("WILDPC_REPOS_DIR", raising=False)
def test_resolve_precedence_env_over_yaml(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
+ from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
monkeypatch.setenv("X_ROOT_ENV", "/from/env")
p = _resolve_root_path("X_ROOT_ENV", "/from/yaml", tmp_path, _DEFAULT_DATA_DIR)
assert p == Path("/from/env")
def test_resolve_yaml_over_default(self, tmp_path: Path) -> None:
- from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
+ from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path(
"UNSET_ROOT_ENV", "/from/yaml", tmp_path, _DEFAULT_DATA_DIR
@@ -377,7 +377,7 @@ class TestConfigurableRoots:
assert p == Path("/from/yaml")
def test_resolve_default_when_neither(self, tmp_path: Path) -> None:
- from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
+ from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path("UNSET_ROOT_ENV", None, tmp_path, _DEFAULT_DATA_DIR)
assert p == _DEFAULT_DATA_DIR # returned as-is so save_config can compare equal
@@ -385,16 +385,16 @@ class TestConfigurableRoots:
def test_resolve_expanduser(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
+ from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
monkeypatch.setenv("X_ROOT_ENV", "~/box")
p = _resolve_root_path("X_ROOT_ENV", None, tmp_path, _DEFAULT_DATA_DIR)
assert p == (Path.home() / "box").resolve()
def test_resolve_relative_anchored_to_anchor_not_cwd(self, tmp_path: Path) -> None:
- """A relative root is anchored to the dir holding castle.yaml — never cwd, or
+ """A relative root is anchored to the dir holding wildpc.yaml — never cwd, or
the CLI (shell cwd) and api (unit cwd) would diverge again."""
- from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
+ from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path(
"UNSET_ROOT_ENV", "sub/data", tmp_path, _DEFAULT_DATA_DIR
@@ -402,23 +402,23 @@ class TestConfigurableRoots:
assert p == (tmp_path / "sub" / "data").resolve()
def test_load_config_reads_data_dir_from_yaml(self, tmp_path: Path) -> None:
- (tmp_path / "castle.yaml").write_text(
+ (tmp_path / "wildpc.yaml").write_text(
yaml.dump({"gateway": {"port": 9000}, "data_dir": "/srv/box/data"})
)
config = load_config(tmp_path)
assert config.data_dir == Path("/srv/box/data")
def test_load_config_data_dir_defaults(self, tmp_path: Path) -> None:
- from castle_core.config import _DEFAULT_DATA_DIR
+ from wildpc_core.config import _DEFAULT_DATA_DIR
- (tmp_path / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
+ (tmp_path / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
config = load_config(tmp_path)
assert config.data_dir == _DEFAULT_DATA_DIR
def test_save_round_trips_nondefault_roots(self, tmp_path: Path) -> None:
- from castle_core.config import GatewayConfig
+ from wildpc_core.config import GatewayConfig
- cfg = CastleConfig(
+ cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
@@ -427,7 +427,7 @@ class TestConfigurableRoots:
repos_dir=Path("/srv/box/repos"),
)
save_config(cfg)
- text = (tmp_path / "castle.yaml").read_text()
+ text = (tmp_path / "wildpc.yaml").read_text()
assert "data_dir: /srv/box/data" in text
assert "repos_dir: /srv/box/repos" in text
reloaded = load_config(tmp_path)
@@ -435,13 +435,13 @@ class TestConfigurableRoots:
assert reloaded.repos_dir == Path("/srv/box/repos")
def test_save_omits_default_roots(self, tmp_path: Path) -> None:
- from castle_core.config import (
+ from wildpc_core.config import (
_DEFAULT_DATA_DIR,
_DEFAULT_REPOS_DIR,
GatewayConfig,
)
- cfg = CastleConfig(
+ cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
@@ -450,20 +450,20 @@ class TestConfigurableRoots:
repos_dir=_DEFAULT_REPOS_DIR,
)
save_config(cfg)
- text = (tmp_path / "castle.yaml").read_text()
+ text = (tmp_path / "wildpc.yaml").read_text()
assert "data_dir" not in text
assert "repos_dir" not in text
def test_ensure_dirs_raises_actionable_error(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- """An uncreatable data dir yields a CastleDirError with a fix, not a bare OSError."""
- import castle_core.config as C
- from castle_core.config import GatewayConfig
+ """An uncreatable data dir yields a WildpcDirError with a fix, not a bare OSError."""
+ import wildpc_core.config as C
+ from wildpc_core.config import GatewayConfig
- # Redirect the in-$HOME dirs to tmp so the test never touches the real ~/.castle.
+ # Redirect the in-$HOME dirs to tmp so the test never touches the real ~/.wildpc.
for name in (
- "CASTLE_HOME",
+ "WILDPC_HOME",
"CODE_DIR",
"SPECS_DIR",
"CONTENT_DIR",
@@ -474,13 +474,13 @@ class TestConfigurableRoots:
# deterministically, even if the suite runs as root.
blocker = tmp_path / "afile"
blocker.write_text("x")
- cfg = CastleConfig(
+ cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
programs={},
data_dir=blocker / "sub",
)
- with pytest.raises(C.CastleDirError) as ei:
+ with pytest.raises(C.WildpcDirError) as ei:
C.ensure_dirs(cfg)
assert "data_dir" in str(ei.value)
diff --git a/core/tests/test_deploy_prune.py b/core/tests/test_deploy_prune.py
index 55f8332..b45ec33 100644
--- a/core/tests/test_deploy_prune.py
+++ b/core/tests/test_deploy_prune.py
@@ -5,9 +5,9 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
-from castle_core import deploy as deploy_mod
-from castle_core.deploy import _desired_unit_files, _prune_orphans
-from castle_core.registry import Deployment, NodeConfig, NodeRegistry
+from wildpc_core import deploy as deploy_mod
+from wildpc_core.deploy import _desired_unit_files, _prune_orphans
+from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
def _svc(managed: bool = True, schedule: str | None = None) -> Deployment:
@@ -23,7 +23,7 @@ def _svc(managed: bool = True, schedule: str | None = None) -> Deployment:
def _registry(**deployed: Deployment) -> NodeRegistry:
- reg = NodeRegistry(node=NodeConfig(castle_root="/x", gateway_port=9000))
+ reg = NodeRegistry(node=NodeConfig(wildpc_root="/x", gateway_port=9000))
for name, d in deployed.items():
d.name = name
reg.put(d)
@@ -38,11 +38,11 @@ def _touch(d: Path, *names: str) -> None:
class TestDesiredUnitFiles:
def test_managed_service_yields_service_file(self) -> None:
reg = _registry(foo=_svc())
- assert _desired_unit_files(reg) == {"castle-foo.service"}
+ assert _desired_unit_files(reg) == {"wildpc-foo.service"}
def test_scheduled_job_yields_service_and_timer(self) -> None:
reg = _registry(job=_svc(schedule="0 2 * * *"))
- assert _desired_unit_files(reg) == {"castle-job-job.service", "castle-job-job.timer"}
+ assert _desired_unit_files(reg) == {"wildpc-job-job.service", "wildpc-job-job.timer"}
def test_unmanaged_excluded(self) -> None:
reg = _registry(foo=_svc(managed=False))
@@ -54,7 +54,7 @@ class TestPruneOrphans:
units = tmp_path / "systemd"
units.mkdir()
# keep-me is in the registry; gone is not; gone.timer is also orphaned
- _touch(units, "castle-keep.service", "castle-gone.service", "castle-gone.timer")
+ _touch(units, "wildpc-keep.service", "wildpc-gone.service", "wildpc-gone.timer")
reg = _registry(keep=_svc())
msgs: list[str] = []
with (
@@ -62,17 +62,17 @@ class TestPruneOrphans:
patch.object(deploy_mod.subprocess, "run") as mock_run,
):
_prune_orphans(reg, msgs)
- assert (units / "castle-keep.service").exists()
- assert not (units / "castle-gone.service").exists()
- assert not (units / "castle-gone.timer").exists()
+ assert (units / "wildpc-keep.service").exists()
+ assert not (units / "wildpc-gone.service").exists()
+ assert not (units / "wildpc-gone.timer").exists()
# stop + disable were invoked for each removed unit
assert mock_run.call_count >= 2
- assert any("castle-gone.service" in m for m in msgs)
+ assert any("wildpc-gone.service" in m for m in msgs)
def test_unscheduled_job_drops_stale_timer_keeps_service(self, tmp_path: Path) -> None:
units = tmp_path / "systemd"
units.mkdir()
- _touch(units, "castle-job.service", "castle-job.timer")
+ _touch(units, "wildpc-job.service", "wildpc-job.timer")
# job is still managed but no longer scheduled → its timer is now an orphan
reg = _registry(job=_svc(schedule=None))
with (
@@ -80,17 +80,17 @@ class TestPruneOrphans:
patch.object(deploy_mod.subprocess, "run"),
):
_prune_orphans(reg, [])
- assert (units / "castle-job.service").exists()
- assert not (units / "castle-job.timer").exists()
+ assert (units / "wildpc-job.service").exists()
+ assert not (units / "wildpc-job.timer").exists()
def test_demoted_to_unmanaged_is_pruned(self, tmp_path: Path) -> None:
units = tmp_path / "systemd"
units.mkdir()
- _touch(units, "castle-foo.service")
+ _touch(units, "wildpc-foo.service")
reg = _registry(foo=_svc(managed=False)) # still in registry but unmanaged
with (
patch.object(deploy_mod, "SYSTEMD_USER_DIR", units),
patch.object(deploy_mod.subprocess, "run"),
):
_prune_orphans(reg, [])
- assert not (units / "castle-foo.service").exists()
+ assert not (units / "wildpc-foo.service").exists()
diff --git a/core/tests/test_deploy_run_cmd.py b/core/tests/test_deploy_run_cmd.py
index a2536c2..6bd43ae 100644
--- a/core/tests/test_deploy_run_cmd.py
+++ b/core/tests/test_deploy_run_cmd.py
@@ -5,14 +5,14 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
-from castle_core.deploy import _build_run_cmd, _build_stop_cmd
-from castle_core.manifest import RunCompose, RunContainer, RunNode, RunPython
+from wildpc_core.deploy import _build_run_cmd, _build_stop_cmd
+from wildpc_core.manifest import RunCompose, RunContainer, RunNode, RunPython
def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
"""A python service with a real source dir launches via `uv run --project`."""
run = RunPython(launcher="python", program="my-svc")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/uv"):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/uv",
@@ -26,7 +26,7 @@ def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
def test_python_runner_appends_args(tmp_path: Path) -> None:
run = RunPython(launcher="python", program="my-svc", args=["--flag", "x"])
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/uv"):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
assert cmd[-2:] == ["--flag", "x"]
assert cmd[:5] == ["/usr/bin/uv", "run", "--project", str(tmp_path), "--no-dev"]
@@ -36,7 +36,7 @@ def test_python_runner_falls_back_to_path_without_source() -> None:
"""No resolvable source → PATH lookup of the script (no uv run)."""
run = RunPython(launcher="python", program="my-svc")
with patch(
- "castle_core.deploy.shutil.which", return_value="/home/u/.local/bin/my-svc"
+ "wildpc_core.deploy.shutil.which", return_value="/home/u/.local/bin/my-svc"
):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=None)
assert cmd == ["/home/u/.local/bin/my-svc"]
@@ -46,7 +46,7 @@ def test_python_runner_warns_when_unresolvable() -> None:
"""No source and not on PATH → a warning, bare program name as last resort."""
run = RunPython(launcher="python", program="my-svc")
messages: list[str] = []
- with patch("castle_core.deploy.shutil.which", return_value=None):
+ with patch("wildpc_core.deploy.shutil.which", return_value=None):
cmd = _build_run_cmd("my-svc", run, {}, messages, source_dir=None)
assert cmd == ["my-svc"]
assert any("my-svc" in m for m in messages)
@@ -55,7 +55,7 @@ def test_python_runner_warns_when_unresolvable() -> None:
def test_node_runner_bakes_source_dir(tmp_path: Path) -> None:
"""A node service runs the script in its source dir via `--dir` (no unit cwd)."""
run = RunNode(launcher="node", script="gateway:watch:raw", package_manager="pnpm")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == ["/usr/bin/pnpm", "--dir", str(tmp_path), "run", "gateway:watch:raw"]
@@ -64,7 +64,7 @@ def test_node_runner_appends_args(tmp_path: Path) -> None:
run = RunNode(
launcher="node", script="start", package_manager="pnpm", args=["--port", "18789"]
)
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/pnpm",
@@ -80,7 +80,7 @@ def test_node_runner_appends_args(tmp_path: Path) -> None:
def test_node_runner_without_source_omits_dir() -> None:
"""No resolvable source → bare `pnpm run` (package manager still PATH-resolved)."""
run = RunNode(launcher="node", script="start", package_manager="pnpm")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=None)
assert cmd == ["/usr/bin/pnpm", "run", "start"]
@@ -88,8 +88,8 @@ def test_node_runner_without_source_omits_dir() -> None:
def test_container_secrets_use_env_file_not_argv() -> None:
"""Secrets go through --env-file; plain vars stay as -e; no secret in argv."""
run = RunContainer(launcher="container", image="img:latest", env={"PLAIN": "1"})
- env_file = Path("/home/u/.castle/secrets/env/castle-svc.service.env")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ env_file = Path("/home/u/.wildpc/secrets/env/wildpc-svc.service.env")
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {"PORT": "9001"}, [], secret_env_file=env_file)
joined = " ".join(cmd)
assert "--env-file" in cmd
@@ -102,7 +102,7 @@ def test_container_secrets_use_env_file_not_argv() -> None:
def test_container_without_secrets_has_no_env_file() -> None:
run = RunContainer(launcher="container", image="img:latest")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], secret_env_file=None)
assert "--env-file" not in cmd
@@ -120,42 +120,42 @@ def test_container_placeholders_expand_in_run_fields() -> None:
ph = {
"name": "svc",
"port": "5432",
- "data_dir": "/data/castle/svc",
- "tls_dir": "/data/castle/svc/tls",
+ "data_dir": "/data/wildpc/svc",
+ "tls_dir": "/data/wildpc/svc/tls",
}
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], placeholders=ph)
joined = " ".join(cmd)
- assert "DATA=/data/castle/svc/x" in joined # env expanded
- assert "/data/castle/svc/tls:/tls:ro" in joined # volume expanded
+ assert "DATA=/data/wildpc/svc/x" in joined # env expanded
+ assert "/data/wildpc/svc/tls:/tls:ro" in joined # volume expanded
assert "svc:5432" in cmd # arg expanded
def test_container_double_dollar_escapes_placeholder() -> None:
"""$${key} passes a literal ${key} through to the container's own shell/env
- instead of castle expanding it (docker-compose-style escape)."""
+ instead of wildpc expanding it (docker-compose-style escape)."""
run = RunContainer(
launcher="container",
image="img:latest",
args=["sh", "-c", "exec myd --advertise $${name}:${port}"],
)
ph = {"name": "svc", "port": "5432"}
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], placeholders=ph)
- # castle expands ${port} but leaves $${name} as a literal ${name} for the shell
+ # wildpc expands ${port} but leaves $${name} as a literal ${name} for the shell
assert "exec myd --advertise ${name}:5432" in cmd
def test_compose_runner_up_with_resolved_file(tmp_path: Path) -> None:
"""A compose service runs `docker compose -p -f up`."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("supabase", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/docker",
"compose",
"-p",
- "castle-supabase",
+ "wildpc-supabase",
"-f",
str(tmp_path / "docker-compose.yml"),
"up",
@@ -165,8 +165,8 @@ def test_compose_runner_up_with_resolved_file(tmp_path: Path) -> None:
def test_compose_runner_secrets_never_hit_argv(tmp_path: Path) -> None:
"""Compose reads env from the process (systemd), not --env-file/-e — argv is clean."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
- env_file = Path("/home/u/.castle/secrets/env/castle-supabase.service.env")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ env_file = Path("/home/u/.wildpc/secrets/env/wildpc-supabase.service.env")
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd(
"supabase", run, {"PORT": "8000"}, [], source_dir=tmp_path,
secret_env_file=env_file,
@@ -179,7 +179,7 @@ def test_compose_runner_secrets_never_hit_argv(tmp_path: Path) -> None:
def test_compose_project_name_override(tmp_path: Path) -> None:
run = RunCompose(launcher="compose", file="stack.yml", project_name="myproj")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("s", run, {}, [], source_dir=tmp_path)
assert cmd[:6] == [
"/usr/bin/docker", "compose", "-p", "myproj", "-f", str(tmp_path / "stack.yml"),
@@ -189,13 +189,13 @@ def test_compose_project_name_override(tmp_path: Path) -> None:
def test_compose_stop_cmd_is_down(tmp_path: Path) -> None:
"""The teardown command mirrors up but ends in `down` (same project + file)."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
- with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
+ with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
stop = _build_stop_cmd("supabase", run, tmp_path)
assert stop == [
"/usr/bin/docker",
"compose",
"-p",
- "castle-supabase",
+ "wildpc-supabase",
"-f",
str(tmp_path / "docker-compose.yml"),
"down",
diff --git a/core/tests/test_deploy_secret_env.py b/core/tests/test_deploy_secret_env.py
index 7df99ef..570db4a 100644
--- a/core/tests/test_deploy_secret_env.py
+++ b/core/tests/test_deploy_secret_env.py
@@ -7,8 +7,8 @@ from pathlib import Path
import pytest
-import castle_core.deploy as deploy
-from castle_core.registry import (
+import wildpc_core.deploy as deploy
+from wildpc_core.registry import (
Deployment,
NodeConfig,
NodeRegistry,
@@ -23,7 +23,10 @@ def secret_env_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
d = tmp_path / "secrets" / "env"
monkeypatch.setattr(deploy, "SECRET_ENV_DIR", d)
monkeypatch.setattr(
- deploy, "secret_env_path", lambda name: d / f"castle-{name}.service.env"
+ deploy,
+ "secret_env_path",
+ lambda name, kind="service": d
+ / f"wildpc-{name}{'-job' if kind == 'job' else ''}.service.env",
)
return d
diff --git a/core/tests/test_deploy_secret_gate.py b/core/tests/test_deploy_secret_gate.py
index feeb60c..0d603a7 100644
--- a/core/tests/test_deploy_secret_gate.py
+++ b/core/tests/test_deploy_secret_gate.py
@@ -1,6 +1,6 @@
"""Tests for the apply-time unresolved-secret gate.
-`castle apply` must refuse to converge a deployment whose ``${secret:NAME}`` env
+`wildpc apply` must refuse to converge a deployment whose ``${secret:NAME}`` env
references the active backend can't resolve — otherwise the value silently
degrades to a ```` placeholder and the service starts with a
bogus credential (the immich-DB-password-to-the-wrong-backend failure).
@@ -13,7 +13,7 @@ from types import SimpleNamespace
import pytest
-from castle_core.deploy import _unresolved_secrets
+from wildpc_core.deploy import _unresolved_secrets
def _spec(env: dict[str, str], enabled: bool = True) -> SimpleNamespace:
@@ -26,8 +26,8 @@ def file_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
secrets = tmp_path / "secrets"
secrets.mkdir()
(secrets / "PRESENT").write_text("value\n")
- monkeypatch.setenv("CASTLE_SECRET_BACKEND", "file")
- monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets)
+ monkeypatch.setenv("WILDPC_SECRET_BACKEND", "file")
+ monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets)
return secrets
diff --git a/core/tests/test_dns.py b/core/tests/test_dns.py
index efa8c07..2f19708 100644
--- a/core/tests/test_dns.py
+++ b/core/tests/test_dns.py
@@ -2,8 +2,8 @@
from __future__ import annotations
-import castle_core.generators.dns as dns
-from castle_core.generators.dns import _zone_for, reconcile_public_dns
+import wildpc_core.generators.dns as dns
+from wildpc_core.generators.dns import _zone_for, reconcile_public_dns
TID = "tid-abc"
TARGET = f"{TID}.cfargotunnel.com"
@@ -18,7 +18,7 @@ class _FakeCloudflare:
"""A minimal fake of the Cloudflare API used by reconcile_public_dns.
``records`` maps zone_id -> {name: (record_id, content)}. Records POST/DELETE
- calls so tests can assert exactly what castle created/removed.
+ calls so tests can assert exactly what wildpc created/removed.
"""
def __init__(self, records: dict[str, dict[str, tuple[str, str]]]):
@@ -75,11 +75,11 @@ def test_creates_apex_and_subdomain_in_correct_zones(monkeypatch) -> None:
def test_deletes_managed_cname_no_longer_desired(monkeypatch) -> None:
- # z_payne has a stale castle-managed CNAME + a hand-managed one pointing elsewhere.
+ # z_payne has a stale wildpc-managed CNAME + a hand-managed one pointing elsewhere.
fake = _FakeCloudflare(records={
"z_payne": {
- "payne.io": ("r1", TARGET), # castle-managed, still desired
- "old.payne.io": ("r2", TARGET), # castle-managed, now stale → delete
+ "payne.io": ("r1", TARGET), # wildpc-managed, still desired
+ "old.payne.io": ("r2", TARGET), # wildpc-managed, now stale → delete
"keep.payne.io": ("r3", "other.example.com"), # NOT ours → never touched
},
"z_ex": {},
diff --git a/core/tests/test_fleet_role.py b/core/tests/test_fleet_role.py
index 02e3ef6..d99d010 100644
--- a/core/tests/test_fleet_role.py
+++ b/core/tests/test_fleet_role.py
@@ -5,8 +5,8 @@ from __future__ import annotations
from pathlib import Path
import yaml
-from castle_core.config import load_config
-from castle_core.registry import (
+from wildpc_core.config import load_config
+from wildpc_core.registry import (
NodeConfig,
NodeRegistry,
load_registry,
@@ -18,7 +18,7 @@ def _write_min_config(root: Path, role: str | None) -> None:
data: dict = {"gateway": {"port": 18000}}
if role is not None:
data["role"] = role
- (root / "castle.yaml").write_text(yaml.safe_dump(data))
+ (root / "wildpc.yaml").write_text(yaml.safe_dump(data))
def test_role_defaults_to_follower(tmp_path: Path) -> None:
@@ -32,12 +32,12 @@ def test_role_loaded_from_yaml(tmp_path: Path) -> None:
def test_save_config_round_trips_role_and_secrets(tmp_path: Path) -> None:
- """save_config rewrites castle.yaml from scratch — it must re-emit `role` and
+ """save_config rewrites wildpc.yaml from scratch — it must re-emit `role` and
preserve the `secrets:` block, or a save reverts the node to a file-backend
follower (the regression this guards)."""
- from castle_core.config import load_config, save_config
+ from wildpc_core.config import load_config, save_config
- (tmp_path / "castle.yaml").write_text(
+ (tmp_path / "wildpc.yaml").write_text(
yaml.safe_dump(
{
"gateway": {"port": 18000},
@@ -47,57 +47,57 @@ def test_save_config_round_trips_role_and_secrets(tmp_path: Path) -> None:
)
)
save_config(load_config(tmp_path))
- reloaded = yaml.safe_load((tmp_path / "castle.yaml").read_text())
+ reloaded = yaml.safe_load((tmp_path / "wildpc.yaml").read_text())
assert reloaded.get("role") == "authority"
assert reloaded.get("secrets", {}).get("backend") == "openbao"
def test_save_config_preserves_arbitrary_unmanaged_global(tmp_path: Path) -> None:
"""Any top-level key save_config doesn't model must survive a rewrite."""
- from castle_core.config import load_config, save_config
+ from wildpc_core.config import load_config, save_config
- (tmp_path / "castle.yaml").write_text(
+ (tmp_path / "wildpc.yaml").write_text(
yaml.safe_dump(
{"gateway": {"port": 18000}, "role": "authority", "future_thing": {"x": 1}}
)
)
save_config(load_config(tmp_path))
- reloaded = yaml.safe_load((tmp_path / "castle.yaml").read_text())
+ reloaded = yaml.safe_load((tmp_path / "wildpc.yaml").read_text())
assert reloaded.get("future_thing") == {"x": 1}
assert reloaded.get("role") == "authority"
-def test_write_deployment_file_leaves_globals_untouched(castle_root: Path) -> None:
- """A scoped deployment write must not rewrite castle.yaml globals (the PATCH
+def test_write_deployment_file_leaves_globals_untouched(wildpc_root: Path) -> None:
+ """A scoped deployment write must not rewrite wildpc.yaml globals (the PATCH
guarantee that stops a deployment edit from dropping role/secrets)."""
- from castle_core.config import load_config, write_deployment_file
+ from wildpc_core.config import load_config, write_deployment_file
- cy = castle_root / "castle.yaml"
+ cy = wildpc_root / "wildpc.yaml"
data = yaml.safe_load(cy.read_text())
data["role"] = "authority"
data["secrets"] = {"backend": "openbao"}
cy.write_text(yaml.safe_dump(data))
before = cy.read_text()
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
kind, name, _dep = next(iter(config.all_deployments()))
write_deployment_file(config, kind, name)
assert cy.read_text() == before # globals byte-identical — nothing touched them
-def test_write_program_file_leaves_globals_untouched(castle_root: Path) -> None:
- """Scoped program write must not rewrite castle.yaml globals either."""
- from castle_core.config import load_config, write_program_file
+def test_write_program_file_leaves_globals_untouched(wildpc_root: Path) -> None:
+ """Scoped program write must not rewrite wildpc.yaml globals either."""
+ from wildpc_core.config import load_config, write_program_file
- cy = castle_root / "castle.yaml"
+ cy = wildpc_root / "wildpc.yaml"
data = yaml.safe_load(cy.read_text())
data["role"] = "authority"
data["secrets"] = {"backend": "openbao"}
cy.write_text(yaml.safe_dump(data))
before = cy.read_text()
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
name = next(iter(config.programs))
write_program_file(config, name)
diff --git a/core/tests/test_git.py b/core/tests/test_git.py
index 149c3e1..917a519 100644
--- a/core/tests/test_git.py
+++ b/core/tests/test_git.py
@@ -1,4 +1,4 @@
-"""Tests for git working-copy status/sync (core/src/castle_core/git.py)."""
+"""Tests for git working-copy status/sync (core/src/wildpc_core/git.py)."""
from __future__ import annotations
@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
-from castle_core import git as G
+from wildpc_core import git as G
# Identity so commits succeed without touching the user's global git config.
_ENV = {
diff --git a/core/tests/test_kind_identity.py b/core/tests/test_kind_identity.py
index ad0e532..25c4cde 100644
--- a/core/tests/test_kind_identity.py
+++ b/core/tests/test_kind_identity.py
@@ -9,9 +9,9 @@ from pathlib import Path
import pytest
import yaml
-from castle_core.config import load_config, save_config
-from castle_core.deploy import _build_deployed, _desired_unit_files
-from castle_core.registry import NodeConfig, NodeRegistry
+from wildpc_core.config import load_config, save_config
+from wildpc_core.deploy import _build_deployed, _desired_unit_files
+from wildpc_core.registry import NodeConfig, NodeRegistry
def _write(root: Path, store: str, name: str, spec: dict) -> None:
@@ -21,7 +21,7 @@ def _write(root: Path, store: str, name: str, spec: dict) -> None:
def _trio(root: Path) -> None:
- (root / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
+ (root / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
_write(root, "services", "backup", {
"manager": "systemd", "run": {"launcher": "python", "program": "backup"},
})
@@ -67,14 +67,14 @@ def test_service_and_job_get_distinct_units(tmp_path: Path) -> None:
dep.name = name
registry.put(dep)
units = _desired_unit_files(registry)
- # service keeps castle-.service; job carries the -job marker; tool has none.
- assert "castle-backup.service" in units
- assert "castle-backup-job.service" in units
- assert "castle-backup-job.timer" in units
+ # service keeps wildpc-.service; job carries the -job marker; tool has none.
+ assert "wildpc-backup.service" in units
+ assert "wildpc-backup-job.service" in units
+ assert "wildpc-backup-job.timer" in units
def test_service_and_static_cannot_share_a_subdomain(tmp_path: Path) -> None:
- (tmp_path / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
+ (tmp_path / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
_write(tmp_path, "services", "app", {
"manager": "systemd", "run": {"launcher": "python", "program": "app"},
"reach": "internal", "expose": {"http": {"internal": {"port": 9001}}},
diff --git a/core/tests/test_lifecycle.py b/core/tests/test_lifecycle.py
index 341035b..a87224c 100644
--- a/core/tests/test_lifecycle.py
+++ b/core/tests/test_lifecycle.py
@@ -5,63 +5,63 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
-from castle_core import lifecycle
-from castle_core.config import load_config
+from wildpc_core import lifecycle
+from wildpc_core.config import load_config
class TestIsActive:
- def test_service_uses_systemctl(self, castle_root: Path) -> None:
- config = load_config(castle_root)
+ def test_service_uses_systemctl(self, wildpc_root: Path) -> None:
+ config = load_config(wildpc_root)
with patch.object(lifecycle, "_systemctl_active", return_value=True) as mock:
assert lifecycle.is_active("test-svc", "service", config) is True
- mock.assert_called_once_with("castle-test-svc.service")
+ mock.assert_called_once_with("wildpc-test-svc.service")
- def test_job_uses_timer(self, castle_root: Path) -> None:
- config = load_config(castle_root)
+ def test_job_uses_timer(self, wildpc_root: Path) -> None:
+ config = load_config(wildpc_root)
with patch.object(lifecycle, "_systemctl_active", return_value=True) as mock:
assert lifecycle.is_active("test-job", "job", config) is True
- mock.assert_called_once_with("castle-test-job-job.timer")
+ mock.assert_called_once_with("wildpc-test-job-job.timer")
- def test_tool_checks_path(self, castle_root: Path) -> None:
- config = load_config(castle_root)
+ def test_tool_checks_path(self, wildpc_root: Path) -> None:
+ config = load_config(wildpc_root)
# give the tool a source so the tool branch is reachable
config.programs["test-tool"].source = "/tmp/test-tool"
with patch.object(lifecycle, "_on_path", return_value=True) as mock:
assert lifecycle.is_active("test-tool", "tool", config) is True
mock.assert_called_once_with("test-tool")
- def test_unknown_is_inactive(self, castle_root: Path) -> None:
- config = load_config(castle_root)
+ def test_unknown_is_inactive(self, wildpc_root: Path) -> None:
+ config = load_config(wildpc_root)
assert lifecycle.is_active("does-not-exist", "service", config) is False
- def test_path_deployment_checks_path(self, castle_root: Path) -> None:
+ def test_path_deployment_checks_path(self, wildpc_root: Path) -> None:
# A `manager: path` deployment (a tool) is active when on PATH.
- from castle_core.manifest import PathDeployment, ProgramSpec
+ from wildpc_core.manifest import PathDeployment, ProgramSpec
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.programs["mytool"] = ProgramSpec(id="mytool", source="/tmp/mytool")
config.tools["mytool"] = PathDeployment(manager="path", program="mytool")
with patch.object(lifecycle, "_on_path", return_value=True) as mock:
assert lifecycle.is_active("mytool", "tool", config) is True
mock.assert_called_once_with("mytool")
- def test_remote_deployment_is_active(self, castle_root: Path) -> None:
+ def test_remote_deployment_is_active(self, wildpc_root: Path) -> None:
# A remote deployment has no local process; the manager is `none` → available.
- from castle_core.manifest import RemoteDeployment
+ from wildpc_core.manifest import RemoteDeployment
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
config.references["ext"] = RemoteDeployment(
manager="none", program="ext", base_url="http://x"
)
assert lifecycle.is_active("ext", "reference", config) is True
def test_static_deployment_active_when_dist_built(
- self, castle_root: Path, tmp_path: Path
+ self, wildpc_root: Path, tmp_path: Path
) -> None:
# A static (caddy) deployment is active once its served dir exists.
- from castle_core.manifest import CaddyDeployment, ProgramSpec
+ from wildpc_core.manifest import CaddyDeployment, ProgramSpec
- config = load_config(castle_root)
+ config = load_config(wildpc_root)
repo = tmp_path / "fe"
config.programs["fe"] = ProgramSpec(id="fe", source=str(repo))
config.statics["fe"] = CaddyDeployment(
diff --git a/core/tests/test_manifest.py b/core/tests/test_manifest.py
index 7b63446..8623f0a 100644
--- a/core/tests/test_manifest.py
+++ b/core/tests/test_manifest.py
@@ -1,9 +1,9 @@
-"""Tests for castle manifest models."""
+"""Tests for wildpc manifest models."""
from __future__ import annotations
import pytest
-from castle_core.manifest import (
+from wildpc_core.manifest import (
Reach,
BuildSpec,
CaddyDeployment,
diff --git a/core/tests/test_relations.py b/core/tests/test_relations.py
index 7cbb82a..5afa17a 100644
--- a/core/tests/test_relations.py
+++ b/core/tests/test_relations.py
@@ -1,18 +1,18 @@
-"""Tests for the relationship model (core/src/castle_core/relations.py)."""
+"""Tests for the relationship model (core/src/wildpc_core/relations.py)."""
from __future__ import annotations
import pytest
-import castle_core.config as C
-from castle_core import relations as R
-from castle_core.manifest import (
+import wildpc_core.config as C
+from wildpc_core import relations as R
+from wildpc_core.manifest import (
CaddyDeployment,
ProgramSpec,
Requirement,
SystemdDeployment,
)
-from castle_core.stacks import tools_for
+from wildpc_core.stacks import tools_for
def _dep(program: str, requires: list | None = None) -> SystemdDeployment:
@@ -32,8 +32,8 @@ def _static(program: str) -> CaddyDeployment:
)
-def _cfg(programs: dict, deployments: dict) -> C.CastleConfig:
- return C.CastleConfig(
+def _cfg(programs: dict, deployments: dict) -> C.WildpcConfig:
+ return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000),
repo=None,
@@ -141,7 +141,7 @@ def test_stack_tool_missing_from_service_path_is_unmet(
) -> None:
"""The drift case: uv resolves in the caller's shell PATH but NOT on the
service's curated runtime PATH → unmet *for the service*, even though a bare
- `which` (what `castle tool list` uses) would report it present."""
+ `which` (what `wildpc tool list` uses) would report it present."""
prog = ProgramSpec(id="svc", stack="python-fastapi")
cfg = _cfg({"svc": prog}, {"svc": _dep("svc")})
shell_only = "/home/me/.shell-tools" # a dir the runtime PATH never includes
diff --git a/core/tests/test_resolution.py b/core/tests/test_resolution.py
index 4c37f21..ea1b28b 100644
--- a/core/tests/test_resolution.py
+++ b/core/tests/test_resolution.py
@@ -4,8 +4,8 @@ from __future__ import annotations
from pathlib import Path
-from castle_core.manifest import ProgramSpec
-from castle_core.stacks import (
+from wildpc_core.manifest import ProgramSpec
+from wildpc_core.stacks import (
_declared_commands,
_migration_version,
available_actions,
diff --git a/core/tests/test_secret_backends.py b/core/tests/test_secret_backends.py
index 8511a1c..e520d15 100644
--- a/core/tests/test_secret_backends.py
+++ b/core/tests/test_secret_backends.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from pathlib import Path
-from castle_core.secret_backends import (
+from wildpc_core.secret_backends import (
FileSecretBackend,
OpenBaoBackend,
build_backend,
@@ -34,37 +34,37 @@ def test_file_backend_write_read_list_delete(tmp_path: Path) -> None:
def test_build_backend_defaults_to_file(tmp_path: Path, monkeypatch) -> None:
- monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
+ monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
assert isinstance(build_backend(tmp_path), FileSecretBackend)
def test_build_backend_openbao_via_env(tmp_path: Path, monkeypatch) -> None:
- monkeypatch.setenv("CASTLE_SECRET_BACKEND", "openbao")
+ monkeypatch.setenv("WILDPC_SECRET_BACKEND", "openbao")
assert isinstance(build_backend(tmp_path), OpenBaoBackend)
def test_build_backend_openbao_via_settings(tmp_path: Path, monkeypatch) -> None:
- """The castle.yaml `secrets:` block selects the backend (env still overrides)."""
- monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
- settings = {"backend": "openbao", "addr": "https://vault:8200", "mount": "castle"}
+ """The wildpc.yaml `secrets:` block selects the backend (env still overrides)."""
+ monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
+ settings = {"backend": "openbao", "addr": "https://vault:8200", "mount": "wildpc"}
assert isinstance(build_backend(tmp_path, settings), OpenBaoBackend)
def test_openbao_unreachable_returns_none_no_fallback(tmp_path: Path) -> None:
"""No file fallback: an unreachable vault returns None even if a file exists."""
(tmp_path / "ONLY_IN_FILE").write_text("from-file")
- backend = OpenBaoBackend(addr="http://127.0.0.1:1", token="dummy", mount="castle")
+ backend = OpenBaoBackend(addr="http://127.0.0.1:1", token="dummy", mount="wildpc")
assert backend.read("ONLY_IN_FILE") is None
assert backend.read("NOT_ANYWHERE") is None
def test_openbao_empty_token_returns_none(tmp_path: Path) -> None:
- backend = OpenBaoBackend(addr="http://127.0.0.1:8200", token="", mount="castle")
+ backend = OpenBaoBackend(addr="http://127.0.0.1:8200", token="", mount="wildpc")
assert backend.read("K") is None
def test_openbao_node_prefix_from_settings(tmp_path: Path, monkeypatch) -> None:
- monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
+ monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
backend = build_backend(
tmp_path,
{"backend": "openbao", "addr": "http://x", "node_prefix": "nodes/primer"},
diff --git a/core/tests/test_systemd.py b/core/tests/test_systemd.py
index d1e35d7..a7da6bb 100644
--- a/core/tests/test_systemd.py
+++ b/core/tests/test_systemd.py
@@ -4,23 +4,23 @@ from __future__ import annotations
from pathlib import Path
-from castle_core.generators.systemd import (
+from wildpc_core.generators.systemd import (
generate_timer,
generate_unit_from_deployed,
secret_env_path,
unit_env_file,
unit_name,
)
-from castle_core.registry import Deployment
+from wildpc_core.registry import Deployment
class TestUnitName:
"""Tests for systemd unit naming."""
def test_unit_name_format(self) -> None:
- """Unit names follow castle-.service pattern."""
- assert unit_name("central-context") == "castle-central-context.service"
- assert unit_name("my-svc") == "castle-my-svc.service"
+ """Unit names follow wildpc-.service pattern."""
+ assert unit_name("central-context") == "wildpc-central-context.service"
+ assert unit_name("my-svc") == "wildpc-my-svc.service"
class TestUnitFromDeployed:
@@ -35,7 +35,7 @@ class TestUnitFromDeployed:
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
- assert "Description=Castle: Test service" in unit
+ assert "Description=Wild PC: Test service" in unit
def test_no_working_directory(self) -> None:
"""Unit file has no WorkingDirectory (source/runtime separation)."""
@@ -53,14 +53,14 @@ class TestUnitFromDeployed:
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
- env={"TEST_SVC_DATA_DIR": "/home/user/.castle/data/test-svc"},
+ env={"TEST_SVC_DATA_DIR": "/home/user/.wildpc/data/test-svc"},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
- assert "Environment=TEST_SVC_DATA_DIR=/home/user/.castle/data/test-svc" in unit
+ assert "Environment=TEST_SVC_DATA_DIR=/home/user/.wildpc/data/test-svc" in unit
def test_default_path_emitted_when_absent(self) -> None:
- """Castle supplies a default PATH when the service doesn't pin one."""
+ """Wild PC supplies a default PATH when the service doesn't pin one."""
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
@@ -99,7 +99,7 @@ class TestUnitFromDeployed:
assert unit.count("PATH=") == 1
def test_explicit_path_overrides_default(self) -> None:
- """A PATH pinned in defaults.env wins — Castle does not append its own,
+ """A PATH pinned in defaults.env wins — Wild PC does not append its own,
which would clobber it under systemd's last-assignment-wins rule."""
deployed = Deployment(
manager="systemd", launcher="python",
@@ -140,15 +140,15 @@ class TestUnitFromDeployed:
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
env={
"MY_SVC_PORT": "9001",
- "MY_SVC_DATA_DIR": "/home/user/.castle/data/my-svc",
+ "MY_SVC_DATA_DIR": "/home/user/.wildpc/data/my-svc",
},
description="My service",
)
unit = generate_unit_from_deployed("my-svc", deployed)
- assert "Description=Castle: My service" in unit
+ assert "Description=Wild PC: My service" in unit
assert "ExecStart=/home/user/.local/bin/uv run my-svc" in unit
assert "Environment=MY_SVC_PORT=9001" in unit
- assert "Environment=MY_SVC_DATA_DIR=/home/user/.castle/data/my-svc" in unit
+ assert "Environment=MY_SVC_DATA_DIR=/home/user/.wildpc/data/my-svc" in unit
assert "WorkingDirectory" not in unit
assert "Restart=on-failure" in unit
@@ -170,7 +170,7 @@ class TestUnitFromDeployed:
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
- env={"DATA_DIR": "/home/user/.castle/data/my-svc"},
+ env={"DATA_DIR": "/home/user/.wildpc/data/my-svc"},
description="Test",
)
unit = generate_unit_from_deployed("my-svc", deployed)
@@ -180,12 +180,12 @@ class TestUnitFromDeployed:
"""A compose deployment's stop_cmd becomes ExecStop= (clean teardown)."""
deployed = Deployment(
manager="systemd", launcher="compose",
- run_cmd=["/usr/bin/docker", "compose", "-p", "castle-x", "-f", "c.yml", "up"],
- stop_cmd=["/usr/bin/docker", "compose", "-p", "castle-x", "-f", "c.yml", "down"],
+ run_cmd=["/usr/bin/docker", "compose", "-p", "wildpc-x", "-f", "c.yml", "up"],
+ stop_cmd=["/usr/bin/docker", "compose", "-p", "wildpc-x", "-f", "c.yml", "down"],
description="Stack",
)
unit = generate_unit_from_deployed("x", deployed)
- assert "ExecStop=/usr/bin/docker compose -p castle-x -f c.yml down" in unit
+ assert "ExecStop=/usr/bin/docker compose -p wildpc-x -f c.yml down" in unit
def test_no_exec_stop_without_stop_cmd(self) -> None:
"""Runners without a stop_cmd rely on SIGTERM — no ExecStop line."""
@@ -206,7 +206,7 @@ class TestSecretEnvFile:
env={"PORT": "9001"},
secret_env_keys=["API_KEY"],
)
- path = Path("/home/u/.castle/secrets/env/castle-my-svc.service.env")
+ path = Path("/home/u/.wildpc/secrets/env/wildpc-my-svc.service.env")
unit = generate_unit_from_deployed("my-svc", deployed, env_file=path)
assert f"EnvironmentFile={path}" in unit
# fail-loud: no '-' prefix
@@ -220,7 +220,7 @@ class TestSecretEnvFile:
schedule="0 2 * * *",
secret_env_keys=["TOKEN"],
)
- path = Path("/home/u/.castle/secrets/env/castle-my-job.service.env")
+ path = Path("/home/u/.wildpc/secrets/env/wildpc-my-job.service.env")
unit = generate_unit_from_deployed("my-job", deployed, env_file=path)
assert "Type=oneshot" in unit
assert f"EnvironmentFile={path}" in unit
@@ -266,7 +266,7 @@ class TestGenerateTimer:
def test_daily_timer(self) -> None:
"""Daily cron produces OnCalendar timer."""
timer = generate_timer("my-job", schedule="0 2 * * *", description="Nightly")
- assert "Description=Castle timer: Nightly" in timer
+ assert "Description=Wild PC timer: Nightly" in timer
assert "OnCalendar=*-*-* 02:00:00" in timer
assert "WantedBy=timers.target" in timer
@@ -279,4 +279,4 @@ class TestGenerateTimer:
def test_fallback_description(self) -> None:
"""Timer uses name when no description given."""
timer = generate_timer("my-job", schedule="0 0 * * *")
- assert "Description=Castle timer: my-job" in timer
+ assert "Description=Wild PC timer: my-job" in timer
diff --git a/core/tests/test_tls.py b/core/tests/test_tls.py
index 3680ed5..395c2d2 100644
--- a/core/tests/test_tls.py
+++ b/core/tests/test_tls.py
@@ -1,4 +1,4 @@
-"""Tests for castle-managed TLS material (core/src/castle_core/tls.py)."""
+"""Tests for wildpc-managed TLS material (core/src/wildpc_core/tls.py)."""
from __future__ import annotations
@@ -6,9 +6,9 @@ from pathlib import Path
import pytest
-import castle_core.config as C
-import castle_core.tls as T
-from castle_core.manifest import SystemdDeployment
+import wildpc_core.config as C
+import wildpc_core.tls as T
+from wildpc_core.manifest import SystemdDeployment
def _write_wildcard(xdg: Path, domain: str, tag: str, acme_dir: str) -> None:
@@ -34,7 +34,7 @@ def tls_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def _cfg(C, domain, dep, data_dir):
- return C.CastleConfig(
+ return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000, domain=domain),
repo=None,
diff --git a/core/tests/test_tool_schema.py b/core/tests/test_tool_schema.py
index 9b9748a..7487587 100644
--- a/core/tests/test_tool_schema.py
+++ b/core/tests/test_tool_schema.py
@@ -1,4 +1,4 @@
-"""Tests for castle_core.tool_schema — deriving neutral tool-call cores from --help."""
+"""Tests for wildpc_core.tool_schema — deriving neutral tool-call cores from --help."""
from __future__ import annotations
@@ -6,7 +6,7 @@ from types import SimpleNamespace
import pytest
-from castle_core.tool_schema import (
+from wildpc_core.tool_schema import (
ToolSchemaError,
_command_core,
_extract_subcommands,
@@ -115,7 +115,7 @@ class TestDerive:
class TestLLMAssistHelpers:
- """Deterministic helpers that feed / validate the (castle-api) LLM assist."""
+ """Deterministic helpers that feed / validate the (wildpc-api) LLM assist."""
def test_collect_tool_help_real_tool(self) -> None:
cfg = _fake_config({"python3": SimpleNamespace(source=None)})
diff --git a/core/tests/test_toolchains.py b/core/tests/test_toolchains.py
index 2bd20e7..dd721fb 100644
--- a/core/tests/test_toolchains.py
+++ b/core/tests/test_toolchains.py
@@ -6,8 +6,8 @@ from pathlib import Path
import pytest
-from castle_core import toolchains
-from castle_core.toolchains import (
+from wildpc_core import toolchains
+from wildpc_core.toolchains import (
ToolchainError,
read_node_pin,
resolve_node_bin,
@@ -26,7 +26,7 @@ def _install(root: Path, *versions: str) -> None:
def nvm(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
root = tmp_path / "nvm"
root.mkdir()
- monkeypatch.setenv("CASTLE_NODE_VERSIONS_DIR", str(root))
+ monkeypatch.setenv("WILDPC_NODE_VERSIONS_DIR", str(root))
return root
@@ -117,5 +117,5 @@ class TestResolveNodeBin:
resolve_node_bin(tmp_path)
def test_default_dir_is_nvm(self, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.delenv("CASTLE_NODE_VERSIONS_DIR", raising=False)
+ monkeypatch.delenv("WILDPC_NODE_VERSIONS_DIR", raising=False)
assert toolchains.node_versions_dir() == Path.home() / ".nvm" / "versions" / "node"
diff --git a/core/tests/test_tunnel.py b/core/tests/test_tunnel.py
index 5f72f4c..a818a45 100644
--- a/core/tests/test_tunnel.py
+++ b/core/tests/test_tunnel.py
@@ -4,11 +4,11 @@ from __future__ import annotations
import yaml
-from castle_core.generators.tunnel import (
+from wildpc_core.generators.tunnel import (
generate_tunnel_config,
public_hostnames,
)
-from castle_core.registry import Deployment, NodeConfig, NodeRegistry
+from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
def _registry(
diff --git a/docs/design.md b/docs/design.md
index 8b5b031..240a4ad 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -1,12 +1,12 @@
-# Castle Design
+# Wild PC Design
-Castle is a personal software platform. It manages independent services,
+Wild PC is a personal software platform. It manages independent services,
tools, and frontends on a Linux machine using standard Unix primitives —
systemd for process supervision, Caddy for HTTP routing, the filesystem
-for storage, and env vars for configuration. The `castle` CLI and API
+for storage, and env vars for configuration. The `wildpc` CLI and API
provide a registry and coordination layer on top.
-The long-term goal: multiple Castle nodes (machines) that discover each
+The long-term goal: multiple Wild PC nodes (machines) that discover each
other and coordinate, forming a personal infrastructure mesh. Each node
is self-sufficient. The mesh is optional.
@@ -15,9 +15,9 @@ is self-sufficient. The mesh is optional.
1. **Unix-native.** Use the OS. systemd, journald, filesystem, signals,
env vars, DNS. Don't reimplement what Linux already provides.
-2. **Independence.** Components never depend on Castle. They accept
+2. **Independence.** Components never depend on Wild PC. They accept
standard configuration (ports, data dirs, URLs) via env vars. A
- Castle service is just a well-behaved Unix daemon that happens to
+ Wild PC service is just a well-behaved Unix daemon that happens to
be registered in a manifest.
3. **Stack and kind.** Each program has an optional *stack* (development
@@ -29,12 +29,12 @@ is self-sufficient. The mesh is optional.
4. **Language-agnostic above the build line.** Below the build line,
every language is different (uv, pnpm, cargo, go). Above it,
- everything is just processes, ports, files, and signals. Castle
+ everything is just processes, ports, files, and signals. Wild PC
operates above the line.
5. **Separate source from runtime.** The repo is for development. The
- runtime lives in standard Unix locations (`$CASTLE_HOME`, default
- `~/.castle/`, plus systemd units). Nothing running should point into the
+ runtime lives in standard Unix locations (`$WILDPC_HOME`, default
+ `~/.wildpc/`, plus systemd units). Nothing running should point into the
source tree.
6. **AI-manageable.** The CLI and API exist so that AI assistants can
@@ -70,7 +70,7 @@ responds to SIGTERM.
### Build Layer
-Transforms source code into runnable artifacts. Castle does not abstract
+Transforms source code into runnable artifacts. Wild PC does not abstract
over language toolchains — it just records the build commands and their
outputs.
@@ -81,11 +81,11 @@ outputs.
| Rust | cargo | Binary |
| Go | go build | Binary |
-Castle's `build` spec is intentionally minimal: a list of shell commands
-and a list of output paths. This works for any language without Castle
+Wild PC's `build` spec is intentionally minimal: a list of shell commands
+and a list of output paths. This works for any language without Wild PC
needing to understand the toolchain.
-For interpreted languages (Python, Node), Castle also needs to know the
+For interpreted languages (Python, Node), Wild PC also needs to know the
runtime wrapper — how to invoke the artifact. For a `manager: systemd`
deployment this is the nested `run:` block's **launcher** variants:
@@ -99,7 +99,7 @@ deployment this is the nested `run:` block's **launcher** variants:
`manager: path` installs a CLI, `manager: none` is an external reference.)
Compiled languages (Rust, Go) use the `command` launcher — once built, they're
-just binaries. No Castle-specific launcher needed.
+just binaries. No Wild PC-specific launcher needed.
### Runtime Layer
@@ -120,12 +120,12 @@ Manages running processes using standard Linux infrastructure.
- TLS termination
**Filesystem** handles storage:
-- Service data: `$CASTLE_DATA_DIR//` (default `/data/castle/`, on a
+- Service data: `$WILDPC_DATA_DIR//` (default `/data/wildpc/`, on a
dedicated volume)
-- Secrets: `$CASTLE_HOME/secrets/` (default `~/.castle/secrets/`)
-- Generated config: `$CASTLE_HOME/artifacts/specs/` (Caddyfile, registry.yaml)
+- Secrets: `$WILDPC_HOME/secrets/` (default `~/.wildpc/secrets/`)
+- Generated config: `$WILDPC_HOME/artifacts/specs/` (Caddyfile, registry.yaml)
-Castle generates systemd unit files and Caddyfile entries from the
+Wild PC generates systemd unit files and Caddyfile entries from the
registry. It doesn't run a daemon itself — it configures OS-level
infrastructure and gets out of the way.
@@ -136,7 +136,7 @@ than stage a copy, Caddy serves their built assets **in place** from the repo
### Registry Layer
-The registry is the central concept in Castle. It tracks what programs
+The registry is the central concept in Wild PC. It tracks what programs
exist, what they can do, and how they're configured. But it's not a
single thing — it's three distinct concepts:
@@ -146,7 +146,7 @@ information, version-controlled in the repo. It answers: "what programs
could exist?"
**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 Wild PC nodes
might run different subsets of programs with different parameters. It
answers: "what's running here, and how?"
@@ -156,7 +156,7 @@ answers: "is it working?"
#### Source vs. runtime split
-These map to the config root (`castle.yaml` globals plus per-resource files
+These map to the config root (`wildpc.yaml` globals plus per-resource files
under `programs/` and `deployments/`), version-controlled in the repo:
```yaml
@@ -198,19 +198,19 @@ reference) captures whether it's an always-on daemon, a scheduled task, a CLI on
PATH, a served frontend, or an external reference.
A deployment can reference a program via `program:` for description fallthrough
-and source code linking. It can also exist independently (e.g., `castle-gateway`
+and source code linking. It can also exist independently (e.g., `wildpc-gateway`
runs Caddy — not our software).
-A service's env is exactly its `defaults.env` — castle injects nothing
+A service's env is exactly its `defaults.env` — wildpc 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 apply`) — Node config:
+**`$WILDPC_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `wildpc apply`) — Node config:
```yaml
node:
hostname: tower
- castle_root: /data/repos/castle
+ wildpc_root: /data/repos/wildpc
gateway_port: 9000
deployed:
central-context:
@@ -218,7 +218,7 @@ deployed:
launcher: python
run_cmd: [/home/user/.local/bin/central-context]
env:
- CENTRAL_CONTEXT_DATA_DIR: /home/user/.castle/data/central-context
+ CENTRAL_CONTEXT_DATA_DIR: /home/user/.wildpc/data/central-context
CENTRAL_CONTEXT_PORT: "9001"
kind: service
stack: python-fastapi
@@ -229,7 +229,7 @@ deployed:
```
The node config says what's deployed *here* and with what concrete
-values. `castle apply` reads the spec from the repo, resolves the
+values. `wildpc 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.
@@ -244,31 +244,31 @@ This separation means:
Three interfaces expose the registry:
-- **CLI** (`castle`) — For AI agents and terminal users. Structured
+- **CLI** (`wildpc`) — For AI agents and terminal users. Structured
output via `--json`. Commands for listing, inspecting, creating,
and managing programs.
-- **API** (`castle-api`) — For programmatic access over HTTP. Used by
+- **API** (`wildpc-api`) — For programmatic access over HTTP. Used by
the dashboard, other nodes, and remote agents.
-- **Dashboard** (`castle`) — For human discoverability. Visual
+- **Dashboard** (`wildpc`) — For human discoverability. Visual
overview of what's running, health status, logs.
### Coordination Layer
Coordination handles discovery and communication — both between
-programs on a single node and across multiple Castle nodes.
+programs on a single node and across multiple Wild PC nodes.
**Intra-node coordination:**
- Programs find each other through the gateway or direct port access via env
vars. A gateway route maps a host address (`.`) to a target of one
kind: **proxy** (a local service port), **remote** (a service on another
node), or **static** (a built frontend's `dist/`, served as files). The same
- computed route list drives the Caddyfile, `castle gateway status`, and the
+ computed route list drives the Caddyfile, `wildpc gateway status`, and the
dashboard, so they always agree.
- The registry (CLI/API) provides discoverability.
- No service mesh or message broker required for basic operation.
**Inter-node coordination:**
-- Each Castle node runs the API, which exposes its program registry.
+- Each Wild PC node runs the API, which exposes its program registry.
- Nodes discover each other via MQTT retained messages and mDNS/DNS-SD
(python-zeroconf) for LAN environments.
- The gateway on each node can proxy to services on other nodes via
@@ -277,21 +277,21 @@ programs on a single node and across multiple Castle nodes.
they're talking to.
- MQTT provides pub/sub messaging for events, status, and coordination
across nodes.
-- All mesh features are opt-in: `CASTLE_API_MQTT_ENABLED=true` and
- `CASTLE_API_MDNS_ENABLED=true`. Single-node works without them.
+- All mesh features are opt-in: `WILDPC_API_MQTT_ENABLED=true` and
+ `WILDPC_API_MDNS_ENABLED=true`. Single-node works without them.
**MQTT topics:**
-- `castle/{hostname}/registry` — retained JSON, full NodeRegistry.
- Published on connect and after `castle apply`.
-- `castle/{hostname}/status` — `"online"` (retained) / `"offline"` (LWT).
+- `wildpc/{hostname}/registry` — retained JSON, full NodeRegistry.
+ Published on connect and after `wildpc apply`.
+- `wildpc/{hostname}/status` — `"online"` (retained) / `"offline"` (LWT).
LWT ensures nodes are marked offline if they disconnect unexpectedly.
-**MeshStateManager** (`castle_api.mesh`) holds remote NodeRegistry
+**MeshStateManager** (`wildpc_api.mesh`) holds remote NodeRegistry
instances in memory, indexed by hostname. 5-minute staleness TTL.
Updated by the MQTT client on incoming messages. Read by API endpoints
to serve cross-node data.
-**mDNS** (`castle_api.mdns`) advertises `_castle._tcp` and browses
+**mDNS** (`wildpc_api.mdns`) advertises `_wildpc._tcp` and browses
for peers and `_mqtt._tcp` broker. Uses python-zeroconf. Properties
include hostname, gateway_port, api_port.
@@ -302,27 +302,27 @@ Local paths always take precedence.
**Why MQTT over custom gossip:**
- Standard protocol, every language has a client library.
- Retained messages give new nodes an immediate view of the network.
-- Topic-based routing maps naturally to `castle/{node}/{program}`.
+- Topic-based routing maps naturally to `wildpc/{node}/{program}`.
- Works across networks (not just LAN like mDNS).
-- Mosquitto is a single binary, simple to run as a Castle program.
+- Mosquitto is a single binary, simple to run as a Wild PC program.
**Why mDNS/DNS-SD as a complement:**
- Zero-config LAN discovery via python-zeroconf.
-- Each node advertises `_castle._tcp` — standard tooling works
+- Each node advertises `_wildpc._tcp` — standard tooling works
(`avahi-browse`, `dns-sd`).
- Good for bootstrapping: find the MQTT broker without hardcoding
its address.
### Dashboard
-The web dashboard (`castle`) is a React SPA served by Caddy in place from
+The web dashboard (`wildpc`) is a React SPA served by Caddy in place from
its repo build output (`/dist/`) at the root `/`. It talks to
-`castle-api` via the gateway proxy at `/api`.
+`wildpc-api` via the gateway proxy at `/api`.
**Layout:**
```
-Castle
+Wild PC
Personal software platform
[tower] [devbox (3)] ← NodeBar (hidden in single-node)
@@ -331,7 +331,7 @@ Personal software platform
│ Gateway · tower · port 9000 · 4 routes [Reload] [Caddyfile] │
│ │
│ Path Component Port Node Health│
-│ /api castle-api 9020 tower ● up │
+│ /api wildpc-api 9020 tower ● up │
│ /central-context central-context 9001 tower ● up │
│ /notifications notification-bridge 9002 tower ● up │
│ /devbox-api devbox-api 9020 devbox ● up │
@@ -343,7 +343,7 @@ Personal software platform
Daemons · Long-running processes that expose ports
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
-│ castle-api │ │ central-ctx │ │ notif-bridge │
+│ wildpc-api │ │ central-ctx │ │ notif-bridge │
│ ● up 5ms │ │ ● up 12ms │ │ ● up 8ms │
│ :9020 │ │ :9001 │ │ :9002 │
└──────────────┘ └──────────────┘ └──────────────┘
@@ -352,7 +352,7 @@ Components · Software catalog
Name Stack Kind Schedule Status
pdf2md Python / CLI tool — installed
protonmail Python / CLI tool */5 * * * * installed
- castle React / Vite static — —
+ wildpc React / Vite static — —
backup-collect Python / CLI job 0 2 * * * —
```
@@ -381,7 +381,7 @@ Components · Software catalog
## Component Contract
-Every Castle program, regardless of language, must satisfy a minimal
+Every Wild PC program, regardless of language, must satisfy a minimal
contract. This is what makes the system uniform above the build line.
### Services (long-running daemons)
@@ -394,8 +394,8 @@ contract. This is what makes the system uniform above the build line.
| Data storage | Read `*_DATA_DIR` env var, write there |
| Logging | stdout for output, stderr for errors |
| Graceful shutdown | Handle SIGTERM, exit cleanly |
-| Secrets | Read from env vars (Castle resolves `${secret:NAME}`) |
-| No Castle dependency | Must run standalone with just env vars set |
+| Secrets | Read from env vars (Wild PC resolves `${secret:NAME}`) |
+| No Wild PC dependency | Must run standalone with just env vars set |
### Tools (CLI utilities)
@@ -419,73 +419,73 @@ Same contract as tools, plus:
## Component Lifecycle
The path from source to managed process is two moves — **develop** (against the
-program) and **converge** (`castle apply`):
+program) and **converge** (`wildpc apply`):
```
-source → [dev verbs: build/test/…] → config (programs/ + deployments/) → [castle apply] → running
+source → [dev verbs: build/test/…] → config (programs/ + deployments/) → [wildpc apply] → running
```
-- **Develop** — language-specific dev verbs (`build`, `test`, …) that Castle
+- **Develop** — language-specific dev verbs (`build`, `test`, …) that Wild PC
records but never runs implicitly. Frontends build in place under the repo
(`//`), served from there — no copy step.
-- **Converge** — `castle apply` reads the config, generates systemd units +
+- **Converge** — `wildpc apply` reads the config, generates systemd units +
Caddyfile entries, then reconciles reality to the desired state: activate what's
enabled, restart what changed, deactivate what's disabled. Activation is
*polymorphic over the manager* — systemd `enable --now`, `uv tool install` for a
tool on PATH, a gateway route for a static — so there is no separate install or
- start step, and no per-kind verb. `castle apply --plan` shows the diff first.
+ start step, and no per-kind verb. `wildpc apply --plan` shows the diff first.
Desired on/off is `enabled` on the deployment; the only way to durably stop
-something is `enabled: false` + apply. `castle restart` is the one imperative
+something is `enabled: false` + apply. `wildpc restart` is the one imperative
bounce that re-actualizes current state without changing it.
## Runtime Filesystem Layout
-Two roots, each overridable by an env var: `$CASTLE_HOME` (config, artifacts,
-secrets; default `~/.castle`) and `$CASTLE_DATA_DIR` (bulk program data; default
-`/data/castle`, on a dedicated volume). Program source lives separately under
-`/data/repos//` (`$CASTLE_REPOS_DIR`).
+Two roots, each overridable by an env var: `$WILDPC_HOME` (config, artifacts,
+secrets; default `~/.wildpc`) and `$WILDPC_DATA_DIR` (bulk program data; default
+`/data/wildpc`, on a dedicated volume). Program source lives separately under
+`/data/repos//` (`$WILDPC_REPOS_DIR`).
```
-$CASTLE_HOME/ ← Config & artifacts (default ~/.castle)
-├── castle.yaml ← Global settings (gateway, repo, agents)
+$WILDPC_HOME/ ← Config & artifacts (default ~/.wildpc)
+├── wildpc.yaml ← Global settings (gateway, repo, agents)
├── programs/ deployments/ ← One YAML file per program / deployment
├── infra.conf ← Infrastructure install choices
-├── artifacts/specs/ ← Generated by `castle apply`
+├── artifacts/specs/ ← Generated by `wildpc apply`
│ ├── Caddyfile
│ └── registry.yaml ← Node config (what's deployed here)
└── secrets/ ← Secret files (NAME → value)
/data/repos// ← Program source (your programs; absolute source:)
-$CASTLE_DATA_DIR// ← Persistent service data (default /data/castle)
-~/.config/systemd/user/ ← Systemd units + timers (castle-*.service/.timer)
+$WILDPC_DATA_DIR// ← Persistent service data (default /data/wildpc)
+~/.config/systemd/user/ ← Systemd units + timers (wildpc-*.service/.timer)
```
Compiled-language tools (Rust, Go — planned) install their binaries to the
standard `~/.local/bin/`. Source is referenced only by dev verbs and while a
deployment is materialized; everything the runtime touches lives under
-`$CASTLE_HOME`, `$CASTLE_DATA_DIR`, or standard systemd paths.
+`$WILDPC_HOME`, `$WILDPC_DATA_DIR`, or standard systemd paths.
## OTP as Design Guide
-Castle's architecture parallels Erlang/OTP, mapped onto Unix:
+Wild PC's architecture parallels Erlang/OTP, mapped onto Unix:
-| OTP Concept | Castle Equivalent |
+| OTP Concept | Wild PC Equivalent |
|-------------|------------------|
| 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 apply` (spec + node config → runtime) |
+| Application resource file | Component spec in `wildpc.yaml` |
+| Release config (sys.config) | Node config in `$WILDPC_HOME/artifacts/specs/registry.yaml` |
+| Release assembly | `wildpc apply` (spec + node config → runtime) |
| Supervisor | systemd (restart policies, ordering) |
| Process | Running service/worker/job |
| Application env | Env vars |
-| Node | A machine running Castle |
+| Node | A machine running Wild PC |
| epmd | mDNS / MQTT discovery |
| Distribution | Inter-node coordination via MQTT + gateway proxying |
| "Let it crash" | `restart: on-failure` in systemd |
| Global registry | Merged node registries via MQTT retained messages |
-The mapping is conceptual, not literal. Castle doesn't implement OTP
+The mapping is conceptual, not literal. Wild PC doesn't implement OTP
semantics — it uses OTP's *thinking* to guide which Unix primitives
to compose and how.
@@ -501,41 +501,41 @@ Key OTP ideas that apply:
can remap these across nodes.
- **Spec vs. config.** In OTP, an application defines its structure
(the `.app` file) and a release provides the deployment config
- (`sys.config`). Castle mirrors this: the program spec defines
+ (`sys.config`). Wild PC mirrors this: the program spec defines
structure, the node config provides deployment values.
## Current State
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 apply` →
- `$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
+- **CLI** — `wildpc` command, installed via `uv tool install --editable cli/`
+- **Three packages** — `wildpc-core` (models, config, generators),
+ `wildpc-cli` (commands), `wildpc-api` (HTTP API)
+- **Source/runtime split** — `wildpc.yaml` (spec) → `wildpc apply` →
+ `$WILDPC_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 apply` resolves `${port}`/`${data_dir}`/`${name}`/
+ `defaults.env`; `wildpc 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
- castle.yaml fallback for non-deployed programs)
-- **Dashboard** — `castle` React/Vite frontend, static assets
+- **API** — `wildpc-api` on port 9020, reads from registry (optional
+ wildpc.yaml fallback for non-deployed programs)
+- **Dashboard** — `wildpc` React/Vite frontend, static assets
served in place from its repo build output (`/dist/`)
- **Services** — central-context (content storage), notification-bridge
(desktop notification forwarder)
- **Jobs** — protonmail (email sync every 5 min), backup-collect (nightly),
backup-data (nightly restic backup)
- **Tools** — ~15 CLI utilities (pdf2md, docx2md, search, gpt, etc.)
-- **Manifest** — `castle.yaml` with typed Pydantic models
+- **Manifest** — `wildpc.yaml` with typed Pydantic models
- **Mesh infrastructure** — MQTT client (paho-mqtt), mDNS discovery
(python-zeroconf), MeshStateManager, all wired into API lifespan.
- Opt-in via `CASTLE_API_MQTT_ENABLED` / `CASTLE_API_MDNS_ENABLED`.
-- **MQTT broker** — Mosquitto running as `castle-mqtt` Docker container
+ Opt-in via `WILDPC_API_MQTT_ENABLED` / `WILDPC_API_MDNS_ENABLED`.
+- **MQTT broker** — Mosquitto running as `wildpc-mqtt` Docker container
on port 1883, managed by systemd. Config and data in
- `$CASTLE_DATA_DIR/castle-mqtt/`.
+ `$WILDPC_DATA_DIR/wildpc-mqtt/`.
- **Node API** — `GET /mesh/status`, `GET /nodes`, `GET /nodes/{hostname}`.
`GET /deployments?include_remote=true` for cross-node program listing.
- **Gateway panel** — Dedicated UI showing route table, health per route,
@@ -551,10 +551,10 @@ What doesn't exist yet:
- **Multi-language support** — Rust and Go programs (the abstractions
support them via the `command` launcher, but no examples exist yet)
-- **Build automation** — Castle records build specs but doesn't
+- **Build automation** — Wild PC records build specs but doesn't
orchestrate builds (each project builds independently)
- **Multi-machine testing** — Mesh infrastructure is built and running
- on one node, but not yet tested with a second Castle node
+ on one node, but not yet tested with a second Wild PC node
## Technology Map
@@ -562,18 +562,18 @@ What doesn't exist yet:
|---------|-----------|--------|
| Process supervision | systemd (user units) | Active |
| HTTP routing | Caddy (port 9000) | Active |
-| Component specs | castle.yaml + Pydantic models | Active |
-| Node config | `$CASTLE_HOME/artifacts/specs/registry.yaml` | Active |
-| CLI | castle (Python, uv) | Active |
-| API | castle-api (FastAPI) | Active |
-| Dashboard | castle (React, Vite, shadcn/ui) | Active |
+| Component specs | wildpc.yaml + Pydantic models | Active |
+| Node config | `$WILDPC_HOME/artifacts/specs/registry.yaml` | Active |
+| CLI | wildpc (Python, uv) | Active |
+| API | wildpc-api (FastAPI) | Active |
+| Dashboard | wildpc (React, Vite, shadcn/ui) | Active |
| Python packaging | uv | Active |
| Node packaging | pnpm | Active |
| Linting | ruff (Python), ESLint (TS) | Active |
| Type checking | pyright (Python), tsc (TS) | Active |
| Testing | pytest (Python), Vitest (TS) | Active |
-| Secrets | `~/.castle/secrets/` file-based | Active |
-| Data storage | Filesystem (`$CASTLE_DATA_DIR/`, default `/data/castle/`) | Active |
+| Secrets | `~/.wildpc/secrets/` file-based | Active |
+| Data storage | Filesystem (`$WILDPC_DATA_DIR/`, default `/data/wildpc/`) | Active |
| Messaging | MQTT (paho-mqtt client, Mosquitto broker) | Active (opt-in) |
| Node discovery | mDNS (python-zeroconf) + MQTT | Active (opt-in) |
| Rust packaging | cargo | Planned |
diff --git a/docs/developing-castle.md b/docs/developing-wildpc.md
similarity index 60%
rename from docs/developing-castle.md
rename to docs/developing-wildpc.md
index c30862d..6ef73fc 100644
--- a/docs/developing-castle.md
+++ b/docs/developing-wildpc.md
@@ -1,31 +1,31 @@
-# Developing Castle
+# Developing Wild PC
-How to work on **Castle's own code** — the CLI, core library, control-plane API,
-and dashboard. For *using* Castle to manage software (create/deploy/expose
+How to work on **Wild PC's own code** — the CLI, core library, control-plane API,
+and dashboard. For *using* Wild PC to manage software (create/deploy/expose
programs), see the operator guide in [`AGENTS.md`](../AGENTS.md).
-## The Castle monorepo
+## The Wild PC monorepo
-Castle's own programs live in **this git repo** (`source: repo:`) — distinct
+Wild PC's own programs live in **this git repo** (`source: repo:`) — distinct
from the programs you manage, which live under `/data/repos//`:
-- `cli/` — the `castle` CLI (installed via `uv tool install --editable cli/`)
-- `core/` — `castle_core`: manifest models, config loader, generators
-- `castle-api/` — the FastAPI control-plane service (port 9020)
-- `app/` — the dashboard frontend (React/Vite; program name `castle`, served at
- `castle.`)
+- `cli/` — the `wildpc` CLI (installed via `uv tool install --editable cli/`)
+- `core/` — `wildpc_core`: manifest models, config loader, generators
+- `wildpc-api/` — the FastAPI control-plane service (port 9020)
+- `app/` — the dashboard frontend (React/Vite; program name `wildpc`, served at
+ `wildpc.`)
-The root `pyproject.toml` is the **uv workspace** (core, cli, castle-api).
+The root `pyproject.toml` is the **uv workspace** (core, cli, wildpc-api).
## Key files
-- `core/src/castle_core/manifest.py` — Pydantic models: `ProgramSpec`,
+- `core/src/wildpc_core/manifest.py` — Pydantic models: `ProgramSpec`,
`DeploymentSpec` (discriminated union on `manager`), `LaunchSpec`, `AgentSpec`, …
-- `core/src/castle_core/config.py` — config loader (`castle.yaml` + `programs/` +
- `deployments/` → `CastleConfig`), the two roots, `source:` resolution
-- `core/src/castle_core/generators/` — systemd unit/timer + Caddyfile generation
-- `cli/src/castle_cli/` — resource-first CLI commands; `templates/scaffold.py`
-- `castle-api/src/castle_api/` — routes, health polling, SSE `stream.py`, mesh,
+- `core/src/wildpc_core/config.py` — config loader (`wildpc.yaml` + `programs/` +
+ `deployments/` → `WildpcConfig`), the two roots, `source:` resolution
+- `core/src/wildpc_core/generators/` — systemd unit/timer + Caddyfile generation
+- `cli/src/wildpc_cli/` — resource-first CLI commands; `templates/scaffold.py`
+- `wildpc-api/src/wildpc_api/` — routes, health polling, SSE `stream.py`, mesh,
and the agent terminal UX (`agents.py`, `pty_session.py`, `agent_sessions.py`,
`agent_registry.py`)
- `ruff.toml` / `pyrightconfig.json` — shared lint/type config
@@ -44,7 +44,7 @@ from the repo root to restore all members. Code style: **ruff** (100-char lines)
**pyright**, **pytest** + **pytest-asyncio**; Python **3.13** for services, **3.11+**
for tools/libraries.
-## castle-api endpoints (port 9020)
+## wildpc-api endpoints (port 9020)
- Core: `GET /health`, `GET /stream` (SSE: health, service-action, mesh)
- Converge: `POST /apply` (`{name?, plan?}`) — the one lifecycle endpoint
@@ -61,12 +61,12 @@ for tools/libraries.
## Infrastructure internals
-- Generated Caddyfile at `~/.castle/artifacts/specs/Caddyfile`; a **plugin Caddy**
+- Generated Caddyfile at `~/.wildpc/artifacts/specs/Caddyfile`; a **plugin Caddy**
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 apply` shouldn't overwrite.
+- Systemd user units at `~/.config/systemd/user/wildpc-*.service` (+ `.timer`);
+ the unit for program `X` is `wildpc-X.service`. Use drop-in `*.service.d/*.conf`
+ for extra env `wildpc 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/`
+- Service data at `$WILDPC_DATA_DIR//`; secrets at `~/.wildpc/secrets/`
(mode 700) — never in project directories.
diff --git a/docs/dns-and-tls.md b/docs/dns-and-tls.md
index 34e4cad..f5d90c7 100644
--- a/docs/dns-and-tls.md
+++ b/docs/dns-and-tls.md
@@ -1,6 +1,6 @@
-# DNS & TLS in Castle
+# DNS & TLS in Wild PC
-How services on a Castle node become reachable by name and trusted over HTTPS —
+How services on a Wild PC node become reachable by name and trusted over HTTPS —
while staying **internal-only** (no external exposure). This is the conceptual
companion to the field-level gateway reference in
[registry.md](registry.md#proxy--how-the-gateway-routes-to-it).
@@ -8,10 +8,10 @@ companion to the field-level gateway reference in
Two independent questions decide whether `https://foo.example/` works from a
browser on your LAN:
-1. **Resolve** — does the name `foo.example` point at the Castle node? *(DNS)*
+1. **Resolve** — does the name `foo.example` point at the Wild PC node? *(DNS)*
2. **Trust** — is the certificate the node serves one the browser accepts? *(TLS)*
-Castle answers #1 by leaning on your LAN's own DNS, and #2 with a per-node choice
+Wild PC answers #1 by leaning on your LAN's own DNS, and #2 with a per-node choice
of two TLS modes. They're orthogonal: you pick a resolution strategy and a trust
strategy, and any working combination is fine.
@@ -34,7 +34,7 @@ making those subdomains **resolve** (DNS) and be **trusted** (TLS).
## DNS: making a name resolve to the node
A subdomain does nothing until it resolves **to this node** on the clients that
-browse it. Castle does **not** run DNS; you add one record to your **LAN's DNS
+browse it. Wild PC does **not** run DNS; you add one record to your **LAN's DNS
server** — usually the router. A single **wildcard** covers every subdomain, so new
services need no further DNS edits:
@@ -54,7 +54,7 @@ trusted (real cert) yet reachable only from the LAN, never the internet.
## TLS: two trust modes
-`gateway.tls` (in `castle.yaml`) picks how host routes are served. It's a per-node
+`gateway.tls` (in `wildpc.yaml`) picks how host routes are served. It's a per-node
choice.
| `gateway.tls` | What the browser gets | Client setup | Use when |
@@ -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 apply` regenerates the Caddyfile and reloads Caddy.
+4. **Deploy & reload:** `wildpc apply` regenerates the Caddyfile and reloads Caddy.
5. **Update the app's origin allowlist** if it has one (§secure context).
## Operational prerequisites
@@ -137,14 +137,14 @@ a DNS token.
- **`acme` only — a DNS-plugin Caddy.** Stock Caddy has no DNS-provider modules.
Build one: `./install.sh --with-dns-plugin=` (uses `xcaddy`, installs
to `/usr/local/bin/caddy`, which precedes the apt binary on `PATH`, so the
- gateway picks it up on the next deploy). Castle now owns updates to that binary.
+ gateway picks it up on the next deploy). Wild PC now owns updates to that binary.
- **`acme` only — a provider API token.** Store it as a secret
- (`~/.castle/secrets/`, scope: the DNS provider's "edit DNS records"
+ (`~/.wildpc/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 apply` warns if the domain, env var, or secret is
+ `deployments/wildpc-gateway.yaml` (`defaults.env`), so Caddy reads it as
+ `{env.}`. `wildpc 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
+- **`acme` — stage first.** Set `WILDPC_ACME_STAGING=1` at deploy to use Let's
Encrypt's staging CA (generous rate limits) while verifying issuance, then unset
it and redeploy for a browser-trusted production cert.
diff --git a/docs/fleet-mesh-plan.md b/docs/fleet-mesh-plan.md
index bda4327..f8e855d 100644
--- a/docs/fleet-mesh-plan.md
+++ b/docs/fleet-mesh-plan.md
@@ -7,7 +7,7 @@ consumed service) — every underlying piece is verified.
## Context
-Castle's mesh today is a deliberately minimal, read-only gossip: each node
+Wild PC's mesh today is a deliberately minimal, read-only gossip: each node
publishes a secret-stripped `NodeRegistry` to Mosquitto MQTT (retained) + an
online/offline LWT, and peers aggregate it into `MeshStateManager`. It is
LAN-only, unauthenticated, carries no secrets, and does nothing with what it
@@ -26,7 +26,7 @@ Two adopted, genuinely-open components (see licensing analysis in chat history):
**TTL keys as presence** (a provider's key vanishes on death → the breaker
trip signal, no separate health plumbing).
- **OpenBao** (Linux Foundation / OpenSSF, MPL-2.0, Vault fork) — the secret
- authority behind castle's existing `${secret:...}` mechanism.
+ authority behind wildpc's existing `${secret:...}` mechanism.
Design stance: **static single-writer `role` authority, no consensus** —
availability comes from followers running cached local state, not failover.
@@ -36,11 +36,11 @@ NATS single-node matches that topology. Consensus stays explicitly out of scope.
| Plane | Component | Primary code seam (verified) |
|-------|-----------|------------------------------|
-| Transport + KV + presence | NATS (`castle-nats`) | `castle-api/.../mqtt_client.py` (whole file), `main.py` lifespan 62-77, `config.py` Settings |
+| Transport + KV + presence | NATS (`wildpc-nats`) | `wildpc-api/.../mqtt_client.py` (whole file), `main.py` lifespan 62-77, `config.py` Settings |
| Shared config + registry + presence | NATS JetStream KV | `mesh.py` `MeshStateManager`; new KV buckets |
-| Secret authority | OpenBao (`castle-openbao`) | `core/.../config.py:307` `_read_secret` (single chokepoint), `castle-api/.../secrets.py` CRUD |
+| Secret authority | OpenBao (`wildpc-openbao`) | `core/.../config.py:307` `_read_secret` (single chokepoint), `wildpc-api/.../secrets.py` CRUD |
| Binding-by-purpose + breaker | Caddy gateway | `generators/caddyfile.py` `compute_routes` (`remote` kind + ignored `remote_registries` are pre-stubbed), `deploy.py:509-540` `_target_url`/`_requires_env` (local-only today) |
-| Authority role | `NodeConfig.role` | `castle_core/registry.py` / `manifest.py` |
+| Authority role | `NodeConfig.role` | `wildpc_core/registry.py` / `manifest.py` |
## Phased plan
@@ -48,47 +48,47 @@ Each phase is independently useful; the risky secret-transport work is last.
### Phase 0 — Stand up the two services (no behavior change)
-- **`castle-nats`**: a `SystemdDeployment` + `RunContainer` (`nats:2` with
- JetStream `-js`), data volume `/data/castle/castle-nats`, `reach: internal`
+- **`wildpc-nats`**: a `SystemdDeployment` + `RunContainer` (`nats:2` with
+ JetStream `-js`), data volume `/data/wildpc/wildpc-nats`, `reach: internal`
(`nats.`). Follow the container YAML shape in `docs/tcp-exposure.md`
(postgres example). Replace the Mosquitto provisioning in `install.sh`
(`setup_mqtt`, `seed_mosquitto_config`, image/port consts) with NATS — or
better, promote it to a bootstrap deployment YAML (today the broker is only a
shell-provisioned container, not a declared deployment).
-- **`castle-openbao`**: `RunContainer` (`openbao/openbao`), data volume, a file
+- **`wildpc-openbao`**: `RunContainer` (`openbao/openbao`), data volume, a file
storage backend, `reach: internal`, **auto-unseal** (local transit/key) so the
node boots unattended (Decision 2).
### Phase 1 — NATS as the mesh transport (swap Mosquitto, behavior-preserving)
- New `nats_client.py` (async-native `nats-py`) replacing `mqtt_client.py`.
- Registry → a **JetStream KV bucket `castle-registry`** keyed by hostname
- (KV last-value replaces MQTT retained). Presence → a **`castle-presence`**
+ Registry → a **JetStream KV bucket `wildpc-registry`** keyed by hostname
+ (KV last-value replaces MQTT retained). Presence → a **`wildpc-presence`**
bucket with a per-node **TTL key** the node renews (replaces LWT + the 300s
`STALE_TTL` poll; note `prune_stale` is currently uncalled).
- Wire in `main.py` lifespan; rename `mqtt_*` → `nats_*` in `config.py` (keep
- `CASTLE_API_` prefix). `MeshStateManager` logic is reused; the KV **watch**
+ `WILDPC_API_` prefix). `MeshStateManager` logic is reused; the KV **watch**
callback drives `update_node`/`set_offline` — and because `nats-py` is
asyncio-native, the `run_coroutine_threadsafe` cross-thread hop for
`broadcast("mesh", …)` **goes away**.
- **Preserve the secret-stripping invariant** from `_registry_to_json` (env /
- run_cmd / castle_root never on the wire).
+ run_cmd / wildpc_root never on the wire).
- Rename MQTT-named fields: `models.py` `MeshStatus` (217-226) + frontend
`types/index.ts` (326-330), `MeshPanel.tsx`. Drop `paho-mqtt`; add `nats-py`.
Keep zeroconf peer-advert for LAN discovery **and** support explicit NATS seed
URLs for cross-network (Decision 1); drop the dangling unused `_mqtt._tcp`
browse. Rewrite `test_mqtt.py` → `test_nats.py`.
- Fixes a latent gap: today the registry is published **on-connect only** (no
- periodic/on-change republish). KV writes on `castle apply` fix this naturally.
+ periodic/on-change republish). KV writes on `wildpc apply` fix this naturally.
### Phase 2 — Shared config + presence via JetStream KV
-- Buckets: `castle-config` (shared LAN config, **authority-written only**),
- `castle-registry` (per-node), `castle-presence` (TTL liveness).
-- Add static **`role: authority | follower`** to `NodeConfig` (castle.yaml).
+- Buckets: `wildpc-config` (shared LAN config, **authority-written only**),
+ `wildpc-registry` (per-node), `wildpc-presence` (TTL liveness).
+- Add static **`role: authority | follower`** to `NodeConfig` (wildpc.yaml).
Pin **`civil` = authority** (Decision 3); only the authority may write
- `castle-config`. Authority-down ⇒ shared state read-only, nodes serve cached.
-- Followers **watch** `castle-config` and reconcile via `castle apply`. Presence
+ `wildpc-config`. Authority-down ⇒ shared state read-only, nodes serve cached.
+- Followers **watch** `wildpc-config` and reconcile via `wildpc apply`. Presence
key renewal is the churn signal for Phase 3.
### Phase 3 — Cross-node `requires` resolution + gateway binding + breaker (keystone)
@@ -102,7 +102,7 @@ Each phase is independently useful; the risky secret-transport work is last.
write at `deploy.py:157` (currently passed nothing). Binding = a **stable local
subdomain route** the gateway re-targets to the remote node's URL — the program
sees one unchanging URL.
-- **Circuit-breaker:** gate the remote route on the `castle-presence` key
+- **Circuit-breaker:** gate the remote route on the `wildpc-presence` key
(regenerate + `_reload_gateway` when a provider appears/vanishes), backed by
Caddy passive health (`fail_duration` / `lb_try_duration`) on the
`reverse_proxy` line in `_host_matcher_block`. Consumer degraded-mode =
@@ -112,9 +112,9 @@ Each phase is independently useful; the risky secret-transport work is last.
### Phase 4 — OpenBao as the secret backend (last; needs the security foundation)
- Introduce a **`SecretBackend` seam** at the single read chokepoint
- `_read_secret` (`config.py:307`) + the `castle-api/.../secrets.py` writer:
+ `_read_secret` (`config.py:307`) + the `wildpc-api/.../secrets.py` writer:
`FileSecretBackend` (default, unchanged) and `OpenBaoBackend`. `${secret:NAME}`
- syntax is untouched; backend selected in castle.yaml or by env.
+ syntax is untouched; backend selected in wildpc.yaml or by env.
- Followers auth to OpenBao (token / AppRole); **need-to-know scoping** via
per-deployment policies. Keep the file backend as fallback; migrate secrets in.
@@ -129,8 +129,8 @@ Each phase is independently useful; the risky secret-transport work is last.
- **P1:** bring up two nodes; confirm registry + presence propagate over NATS and
the mesh view (`/mesh/status`, System Map) is identical to the MQTT behavior.
-- **P2:** write a key to `castle-config` on the authority; observe a follower
- watch fire and `castle apply` reconcile.
+- **P2:** write a key to `wildpc-config` on the authority; observe a follower
+ watch fire and `wildpc apply` reconcile.
- **P3:** define a service on node A that `requires` a ref provided on node B;
confirm the gateway routes cross-node, then **kill node B** and confirm the
consumer fails fast (curl returns the degraded 503, not a hang) and **recovers**
@@ -145,17 +145,17 @@ Each phase is independently useful; the risky secret-transport work is last.
Both services stood up **alongside** the live MQTT mesh (no cutover yet):
-- `castle-nats` (`nats:2`, JetStream) — deployment
- `~/.castle/deployments/services/castle-nats.yaml`, config
- `/data/castle/castle-nats/config/nats-server.conf`. Verified: container active,
+- `wildpc-nats` (`nats:2`, JetStream) — deployment
+ `~/.wildpc/deployments/services/wildpc-nats.yaml`, config
+ `/data/wildpc/wildpc-nats/config/nats-server.conf`. Verified: container active,
`/healthz` ok, JetStream enabled (`/jsz` shows store dir + limits), and a real
- **KV round-trip** — created `castle-registry` (put/get) and a TTL
- `castle-presence` bucket (the presence primitive). Test buckets cleaned up.
-- `castle-openbao` (`openbao/openbao:latest`, v2.5.5) — deployment
- `~/.castle/deployments/services/castle-openbao.yaml`, config
- `/data/castle/castle-openbao/config/openbao.hcl`. Verified: initialized (1
+ **KV round-trip** — created `wildpc-registry` (put/get) and a TTL
+ `wildpc-presence` bucket (the presence primitive). Test buckets cleaned up.
+- `wildpc-openbao` (`openbao/openbao:latest`, v2.5.5) — deployment
+ `~/.wildpc/deployments/services/wildpc-openbao.yaml`, config
+ `/data/wildpc/wildpc-openbao/config/openbao.hcl`. Verified: initialized (1
share / threshold 1), unsealed, **KV-v2 secret put/get/delete** round-trip.
- Unseal key + root token stored as castle secrets `OPENBAO_UNSEAL_KEY` /
+ Unseal key + root token stored as wildpc secrets `OPENBAO_UNSEAL_KEY` /
`OPENBAO_ROOT_TOKEN`.
**Consciously deferred (tracked):**
@@ -172,10 +172,10 @@ Both services stood up **alongside** the live MQTT mesh (no cutover yet):
MQTT/Mosquitto transport replaced by NATS JetStream KV.
-- New `castle_api/mesh_wire.py` — transport-agnostic registry (de)serialization
+- New `wildpc_api/mesh_wire.py` — transport-agnostic registry (de)serialization
(moved out of `mqtt_client.py`; preserves the secret-stripping invariant).
-- New `castle_api/nats_client.py` — `CastleNATSClient`: connects, PUTs its
- registry to the `castle-registry` KV bucket, seeds from existing keys, watches
+- New `wildpc_api/nats_client.py` — `WildpcNATSClient`: connects, PUTs its
+ registry to the `wildpc-registry` KV bucket, seeds from existing keys, watches
for peer PUT/DELETE, heartbeats a re-PUT (crash liveness via the existing
stale-TTL), and DELETEs its key on graceful stop (immediate peer-offline).
Async-native → the paho cross-thread `run_coroutine_threadsafe` hop is gone.
@@ -184,54 +184,54 @@ MQTT/Mosquitto transport replaced by NATS JetStream KV.
- Frontend: `types/index.ts` `MeshStatus` + `MeshPanel.tsx` updated to the new
fields. `mqtt_client.py` + `test_mqtt.py` deleted; `test_mesh_wire.py` added.
`mdns.py` dead `_mqtt._tcp` broker-browse removed. `paho-mqtt` → `nats-py`.
-- **Live cutover:** `castle-api.yaml` env → `CASTLE_API_NATS_*`, `requires:
- castle-nats`; applied. castle-api healthy on NATS, `/mesh/status` connected,
+- **Live cutover:** `wildpc-api.yaml` env → `WILDPC_API_NATS_*`, `requires:
+ wildpc-nats`; applied. wildpc-api healthy on NATS, `/mesh/status` connected,
registry (9.7 KiB, 48 deployments) published to KV, **no secrets on the wire**.
- Verification run: ruff clean; 89 api + 196 core tests pass; frontend `tsc`
- clean; runtime KV round-trip confirmed against the live `castle-nats`.
+ clean; runtime KV round-trip confirmed against the live `wildpc-nats`.
**Pending second node:** two-node mesh-view parity (peer sees peer, offline on
-departure) — needs the second LAN node running the NATS-enabled castle-api
-pointed at civil's NATS (seed URL) or a clustered `castle-nats`. Revert path if
-needed: restore `CASTLE_API_MQTT_*` env — but note `mqtt_client.py` is deleted,
-so revert = `git checkout main -- castle-api` then re-apply. The old Mosquitto
+departure) — needs the second LAN node running the NATS-enabled wildpc-api
+pointed at civil's NATS (seed URL) or a clustered `wildpc-nats`. Revert path if
+needed: restore `WILDPC_API_MQTT_*` env — but note `mqtt_client.py` is deleted,
+so revert = `git checkout main -- wildpc-api` then re-apply. The old Mosquitto
`mqtt` service is left running (dormant) as a safety net; retire it once the
second node is proven on NATS.
### Phase 2 — DONE + single-node verified + tested (2026-07-07)
-- **`role` field** on `NodeConfig` + `CastleConfig`, wired end-to-end:
- castle.yaml top-level `role:` → config → `_node_config` → registry.yaml →
+- **`role` field** on `NodeConfig` + `WildpcConfig`, wired end-to-end:
+ wildpc.yaml top-level `role:` → config → `_node_config` → registry.yaml →
mesh wire. **`civil` pinned `role: authority`**; default `follower`.
-- **Presence** (`castle-presence`) — a TTL KV bucket each node renews on the
+- **Presence** (`wildpc-presence`) — a TTL KV bucket each node renews on the
heartbeat; expiry = the node is gone. Delete-on-stop for immediate departure.
-- **Shared config** (`castle-config`) — `get_shared_config` / `put_shared_config`
+- **Shared config** (`wildpc-config`) — `get_shared_config` / `put_shared_config`
(authority-gated: followers raise `PermissionError`), plus a watch loop that
- broadcasts a `config_changed` SSE (the follower `castle apply` reconcile hook
+ broadcasts a `config_changed` SSE (the follower `wildpc apply` reconcile hook
hangs off this — inert on one node).
- Hardened `stop()` to bound the NATS drain (can't hang systemd shutdown).
- Verified live: all three buckets present, `civil=online` presence,
`"role": "authority"` on the wire, authority write + follower-deny exercised
- against the running `castle-nats`.
+ against the running `wildpc-nats`.
- **Tests added:** `core/tests/test_fleet_role.py` (role config load + registry
- round-trip), `castle-api/tests/test_nats_client.py` (role gating, hermetic),
+ round-trip), `wildpc-api/tests/test_nats_client.py` (role gating, hermetic),
role round-trip in `test_mesh_wire.py`. Suites: **200 core + 93 api** pass.
-**Pending second node:** the follower-side reconcile (watch `castle-config` →
-`castle apply`) is wired as an SSE hook but only meaningful with a peer.
+**Pending second node:** the follower-side reconcile (watch `wildpc-config` →
+`wildpc apply`) is wired as an SSE hook but only meaningful with a peer.
### Phase 4 — secret-read backend DONE + verified + tested (2026-07-07)
-- New `core/castle_core/secret_backends.py`: `SecretBackend` protocol,
+- New `core/wildpc_core/secret_backends.py`: `SecretBackend` protocol,
`FileSecretBackend` (the historical behavior), `OpenBaoBackend` (KV-v2 read with
- file fallback), and `build_backend()` selecting via `CASTLE_SECRET_BACKEND`
+ file fallback), and `build_backend()` selecting via `WILDPC_SECRET_BACKEND`
(default **file** — production is byte-for-byte unchanged until opted in).
- `_read_secret` (the single chokepoint, `config.py`) now delegates to the active
backend. `${secret:NAME}` syntax untouched.
- OpenBao token bootstraps from the file backend (it can't live in the vault it
unlocks); a missing key / auth failure / unreachable server all fall through to
file, so a partly-migrated vault keeps working.
-- Verified live against the running `castle-openbao`: a secret stored in the vault
+- Verified live against the running `wildpc-openbao`: a secret stored in the vault
resolves through `${secret:...}` in openbao mode; file-only secrets resolve via
fallback; missing → placeholder; **default file mode unchanged**.
- **Tests:** `core/tests/test_secret_backends.py` (file hit/miss, backend
@@ -239,12 +239,12 @@ second node is proven on NATS.
**Hardening:**
1. **Write path — DONE + verified live.** `SecretBackend` gained
- `write`/`delete`/`list_names`; `castle-api/secrets.py` routes all CRUD through
+ `write`/`delete`/`list_names`; `wildpc-api/secrets.py` routes all CRUD through
the active backend, so in openbao mode the dashboard writes to the vault.
Verified: write→vault→read→list→delete round-trip against live OpenBao.
2. **Auto-unseal on boot — DONE + verified.** Added `exec_start_post` to the
systemd spec (general, `-`-prefixed so a hook failure never fails the unit);
- `castle-openbao` runs `/data/castle/castle-openbao/unseal.sh` (polls up, unseals
+ `wildpc-openbao` runs `/data/wildpc/wildpc-openbao/unseal.sh` (polls up, unseals
with `OPENBAO_UNSEAL_KEY`). Verified: manual seal → restart → auto-unsealed.
3. **TLS hardening — DONE + verified across both nodes (2026-07-07).**
Reuses the existing ACME **wildcard cert** via `expose.tcp.tls` (the postgres
@@ -253,12 +253,12 @@ second node is proven on NATS.
custom CA to distribute**.
- **NATS:** `tls{}` (wildcard cert in `/tls`) + `authorization{token}`
(`$NATS_TOKEN` from the `NATS_TOKEN` secret). Clients connect to
- `tls://castle-nats.civil.payne.io:4222` with the token; `CastleNATSClient`
+ `tls://wildpc-nats.civil.payne.io:4222` with the token; `WildpcNATSClient`
gained token support (a `tls://` URL → nats-py verifies via system CA).
**civil + primer both cut over**; plaintext now rejected (verified:
`certificate is valid for *.civil.payne.io, not localhost`).
- **OpenBao:** listener `tls_cert_file`/`tls_key_file` from `/tls`; reachable at
- `https://castle-openbao.civil.payne.io:8200`. Auto-unseal + secret backend
+ `https://wildpc-openbao.civil.payne.io:8200`. Auto-unseal + secret backend
both use the HTTPS URL. Verified: HTTPS serves, **auto-unsealed over HTTPS**,
plaintext rejected, backend reads a secret over HTTPS.
- Coordinated cutover across the whole fleet (both nodes) with a brief,
@@ -267,7 +267,7 @@ second node is proven on NATS.
### Phase 3 — cross-node routing + breaker: logic DONE + verified against a real peer (2026-07-07)
**Real second node:** `primer` (192.168.8.129) migrated onto the NATS mesh
-(branch checked out, castle-api pointed at `nats://civil:4222` via a systemd
+(branch checked out, wildpc-api pointed at `nats://civil:4222` via a systemd
drop-in). civil ↔ primer mesh confirmed over the LAN — **Phase 1 two-node parity
done for real**, not simulated. (This also *restored* the civil↔primer mesh my
Phase 1 cutover had split — primer was still on MQTT→civil.)
@@ -280,12 +280,12 @@ Phase 1 cutover had split — primer was still on MQTT→civil.)
(2s dial timeout + passive health) for the there-but-wedged case; **presence
expiry removes the peer from the route set entirely** (gone → no route) — the
primary breaker.
-- `castle_api/mesh_gateway.py` — on peer join/leave/change (+ startup) the API
+- `wildpc_api/mesh_gateway.py` — on peer join/leave/change (+ startup) the API
re-renders the Caddyfile (same generator `apply` uses + remote routes for
online peers) and reloads the gateway **iff content changed**.
- **Verified:** 4 hermetic tests (route emitted / address fallback / breaker:
peer-absent → no route / unconsumed → no route); **resolution against primer's
- real registry** pulled live from the mesh → `castle-api → primer:9020`; and the
+ real registry** pulled live from the mesh → `wildpc-api → primer:9020`; and the
live integration proven a **no-op** on civil (Caddyfile hash unchanged, gateway
healthy) since civil consumes nothing cross-node yet. Suites: **210 core + 93 api**.
@@ -309,6 +309,6 @@ primer is now a permanent mesh member on the branch (revert: remove the drop-in
as `role: authority`; all others are followers. **Confirmed:** when `civil` is
down, shared config/secrets go **read-only** fleet-wide and every node keeps
serving its own deployments from cached local state.
-4. **NATS — single server now, cluster-ready.** Run one `castle-nats` today
+4. **NATS — single server now, cluster-ready.** Run one `wildpc-nats` today
(matches the single-writer authority). Clustering is deferred but must be a
pure config addition (cluster/routes block + a few more nodes) — no re-architecture.
diff --git a/docs/registry.md b/docs/registry.md
index e94783e..4d6ebe0 100644
--- a/docs/registry.md
+++ b/docs/registry.md
@@ -1,14 +1,14 @@
# Registry
-How castle tracks, configures, and manages programs and their deployments.
-This is the central reference for `castle.yaml` structure and the registry
+How wildpc tracks, configures, and manages programs and their deployments.
+This is the central reference for `wildpc.yaml` structure and the registry
architecture.
## Vocabulary (canonical)
Use these terms consistently across code, CLI, API, and docs.
-- **program** — any project castle manages, regardless of what it does. The
+- **program** — any project wildpc manages, regardless of what it does. The
software catalog (`programs/`). Every program has an optional **stack**.
*("component" was the old name for program — don't use it.)*
- **stack** — a creation-time toolchain + scaffold template (`python-cli`,
@@ -37,11 +37,11 @@ gateway). A single `deployments/.yaml` file carries the whole thing.
## Configuration Directory Layout
-Castle splits its configuration across a root directory (`~/.castle/` or your config root) instead of a single file:
+Wild PC splits its configuration across a root directory (`~/.wildpc/` or your config root) instead of a single file:
```
-~/.castle/
-├── castle.yaml # Global settings (gateway, repo, etc.)
+~/.wildpc/
+├── wildpc.yaml # Global settings (gateway, repo, etc.)
├── programs/ # Program configuration files (one file per program)
│ └── my-tool.yaml
└── deployments/ # Deployment configuration files (one file per deployment)
@@ -51,35 +51,35 @@ Castle splits its configuration across a root directory (`~/.castle/` or your co
└── my-app.yaml # manager: caddy → kind: static
```
-### castle.yaml (Globals)
+### wildpc.yaml (Globals)
-The core `castle.yaml` contains configuration settings that apply globally to your Castle platform instance:
+The core `wildpc.yaml` contains configuration settings that apply globally to your Wild PC platform instance:
```yaml
gateway:
port: 9000
-repo: /data/repos/castle
-data_dir: /data/castle # optional — where program/service data lives
+repo: /data/repos/wildpc
+data_dir: /data/wildpc # optional — where program/service data lives
repos_dir: /data/repos # optional — default home for new program source repos
```
**`data_dir` / `repos_dir` — the configurable roots.** Both are optional and omitted
-by default (the built-ins `/data/castle` and `/data/repos` apply). Each resolves with
-precedence **env var > `castle.yaml` > built-in default**:
+by default (the built-ins `/data/wildpc` and `/data/repos` apply). Each resolves with
+precedence **env var > `wildpc.yaml` > built-in default**:
-| root | env override | castle.yaml key | default |
+| root | env override | wildpc.yaml key | default |
|------|--------------|-----------------|---------|
-| program data (`${data_dir}` base) | `CASTLE_DATA_DIR` | `data_dir:` | `/data/castle` |
-| new-repo home (`castle create`/`add`/`clone`) | `CASTLE_REPOS_DIR` | `repos_dir:` | `/data/repos` |
+| program data (`${data_dir}` base) | `WILDPC_DATA_DIR` | `data_dir:` | `/data/wildpc` |
+| new-repo home (`wildpc create`/`add`/`clone`) | `WILDPC_REPOS_DIR` | `repos_dir:` | `/data/repos` |
-Put the value in `castle.yaml`, not an env var. The `castle` CLI and the `castle-api`
+Put the value in `wildpc.yaml`, not an env var. The `wildpc` CLI and the `wildpc-api`
service each resolve config independently in their own process; a per-shell env var is
seen by only one of them, so the two silently diverge (and `apply` crashes if the
-resolved dir — e.g. a non-existent `/data/castle` — can't be created). Persisting the
-choice in `castle.yaml` is the single source of truth both read. `install.sh` writes
-these keys when you install with `CASTLE_DATA_DIR`/`CASTLE_REPOS_DIR` set; `castle
+resolved dir — e.g. a non-existent `/data/wildpc` — can't be created). Persisting the
+choice in `wildpc.yaml` is the single source of truth both read. `install.sh` writes
+these keys when you install with `WILDPC_DATA_DIR`/`WILDPC_REPOS_DIR` set; `wildpc
doctor` flags a data dir that isn't writable, or an env var that's overriding the file.
-(`CASTLE_HOME`, the dir that *contains* castle.yaml, stays env-or-default `~/.castle` —
+(`WILDPC_HOME`, the dir that *contains* wildpc.yaml, stays env-or-default `~/.wildpc` —
it can't be defined inside the file it locates.)
### Resource Configuration Files (`programs/`, `deployments/`)
@@ -139,7 +139,7 @@ root: dist
| **deployments** | `deployments/*.yaml` | How a program is realized on this node | service, job, tool, static, reference |
A deployment can reference a program via `program:` for description fallthrough
-and source code linking. It can also exist independently (e.g., `castle-gateway`
+and source code linking. It can also exist independently (e.g., `wildpc-gateway`
runs Caddy — not our software). The **kind** is derived from `manager` (+
`schedule`), never stored.
@@ -148,28 +148,28 @@ runs Caddy — not our software). The **kind** is derived from `manager` (+
Programs define **what software exists** — identity, source, builds. How a
program is *used* is not a program property: it's decided by its deployment's
`manager` and surfaces as the derived **kind** (service/job/tool/static/reference).
-A program with no deployment is just source castle knows how to develop.
+A program with no deployment is just source wildpc knows how to develop.
### `source` — Where the source lives
```yaml
-source: /data/repos/my-tool # your programs, under $CASTLE_REPOS_DIR
-source: repo:castle-api # castle's own programs, inside the git repo
+source: /data/repos/my-tool # your programs, under $WILDPC_REPOS_DIR
+source: repo:wildpc-api # wildpc's own programs, inside the git repo
```
-The `source` path is resolved one of three ways (`core/src/castle_core/config.py`):
+The `source` path is resolved one of three ways (`core/src/wildpc_core/config.py`):
| `source:` value | Resolves to | Used for |
|-----------------|-------------|----------|
| `/data/repos/my-tool` *(absolute)* | as-is | Your own programs (the default) |
-| `repo:castle-api` | `/castle-api` (via the top-level `repo:` field) | Castle's built-in programs |
-| `code/my-tool` *(relative)* | `$CASTLE_HOME/code/my-tool` | Legacy — pre-`/data/repos` layout |
+| `repo:wildpc-api` | `/wildpc-api` (via the top-level `repo:` field) | Wild PC's built-in programs |
+| `code/my-tool` *(relative)* | `$WILDPC_HOME/code/my-tool` | Legacy — pre-`/data/repos` layout |
-Programs you create or adopt live under **`$CASTLE_REPOS_DIR`** (default
-`/data/repos`, override with `CASTLE_REPOS_DIR`) and are recorded with an
-**absolute** `source:`. Castle's own programs (CLI, core, castle-api, app) live
+Programs you create or adopt live under **`$WILDPC_REPOS_DIR`** (default
+`/data/repos`, override with `WILDPC_REPOS_DIR`) and are recorded with an
+**absolute** `source:`. Wild PC's own programs (CLI, core, wildpc-api, app) live
in the git repo and use the `repo:` prefix. A relative `source:` still resolves
-against `$CASTLE_HOME` for back-compat, but new programs no longer use it.
+against `$WILDPC_HOME` for back-compat, but new programs no longer use it.
### `stack` — Development toolchain (optional)
@@ -196,7 +196,7 @@ the stack default; an absent verb falls back to the stack handler (if any), else
the verb is unavailable. `build` is declared via `build:` (it also carries
`outputs:`); every other verb via `commands:`. This is what lets a wired-in repo
with no stack be linted/tested/run. Verb resolution lives in
-`core/src/castle_core/stacks.py` (`run_action`, `available_actions`).
+`core/src/wildpc_core/stacks.py` (`run_action`, `available_actions`).
### `repo` / `ref` — Wiring in an existing repo
@@ -205,9 +205,9 @@ repo: https://github.com/me/widget.git
ref: v2.1.0 # optional branch/tag/commit
```
-`repo` records a git URL so `castle program clone` can provision the source on a fresh
+`repo` records a git URL so `wildpc program clone` can provision the source on a fresh
machine. When `source:` points at an existing working copy, that takes
-precedence. Use `castle program add ` to register an existing repo as a program.
+precedence. Use `wildpc program add ` to register an existing repo as a program.
### `system_dependencies` — Required system packages
@@ -216,7 +216,7 @@ system_dependencies: [pandoc, poppler-utils]
```
System packages that must be installed for the program to work. Displayed
-in `castle tool list` / `castle tool info` and the dashboard.
+in `wildpc tool list` / `wildpc tool info` and the dashboard.
### `version` — Program version
@@ -246,7 +246,7 @@ declares a **`manager`** — who makes it available and supervises its lifecycle
### `manager` — Who realizes it (the discriminant)
A deployment is a *managed materialization* of a program. Its **`manager`** is
-the stored discriminant — the single axis `castle apply` and status dispatch on
+the stored discriminant — the single axis `wildpc apply` and status dispatch on
(it's what makes activation polymorphic: one verb, kind-specific mechanism):
| Manager | Makes available as | Launch mechanism | how `apply` activates | Kind |
@@ -298,7 +298,7 @@ A `compose` launcher supervises a **whole multi-container stack as one systemd
unit** — `ExecStart` runs `docker compose … up` attached (`Type=simple`) and a
generated `ExecStop` runs `… down` so networks/anonymous volumes are reclaimed on
stop. Unlike the single-container `container` launcher, compose owns the stack's own
-networking, startup ordering, and per-service health — Castle delegates rather
+networking, startup ordering, and per-service health — Wild PC delegates rather
than reinventing orchestration. Secrets/env reach compose through the unit's
`Environment=`/`EnvironmentFile=` (from `defaults.env`), which compose interpolates
from the process environment. This is what runs the shared **Supabase substrate**
@@ -309,7 +309,7 @@ manager: systemd
run:
launcher: compose
file: docker-compose.yml # resolved under the program source
- # project_name: castle-my-stack # optional; defaults to castle-
+ # project_name: wildpc-my-stack # optional; defaults to wildpc-
```
### `root` — Static frontend (caddy only)
@@ -354,9 +354,9 @@ 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 apply` generates the cloudflared
+explicit — and **requires `proxy: true`**. `wildpc 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
+`gateway.tunnel_id` set and the `wildpc-tunnel` service running; see
@docs/tunnel-setup.md for the one-time setup.
```yaml
@@ -401,15 +401,15 @@ prerequisites (tokens + LAN DNS for the apex): @docs/tunnel-setup.md.
"Serving a frontend" and "proxying a service" are the same thing — a subdomain
route — differing only in whether the target is files on disk or a live process.
-The table is shown by `castle gateway status`, the dashboard Gateway panel, and
+The table is shown by `wildpc gateway status`, the dashboard Gateway panel, and
`GET /gateway`; the Caddyfile is generated from it.
-**The dashboard and its API.** `castle` (the dashboard frontend) and `castle-api`
-are just two such subdomains (`castle.`, `castle-api.`); the
-dashboard calls the API **cross-origin** (castle-api allows CORS `*`). The bare
+**The dashboard and its API.** `wildpc` (the dashboard frontend) and `wildpc-api`
+are just two such subdomains (`wildpc.`, `wildpc-api.`); the
+dashboard calls the API **cross-origin** (wildpc-api allows CORS `*`). The bare
gateway port (`:9000`) redirects to the dashboard subdomain. On a node with **no
domain** (`gateway.tls: off`), there are no subdomains, so `:9000` serves just the
-control plane — the dashboard at `/` plus a `/api` reverse-proxy to castle-api —
+control plane — the dashboard at `/` plus a `/api` reverse-proxy to wildpc-api —
and other services stay port-only.
#### Host routes need DNS, and the gateway is HTTP-only
@@ -456,7 +456,7 @@ the floor once: `net.ipv4.ip_unprivileged_port_start=80` (persist in
A private-CA approach would force every client device to trust a custom root —
which some platforms (e.g. Android browsers, and Firefox, which uses its own
-store) make painful — so Castle doesn't offer one. `acme` mode avoids it entirely:
+store) make painful — so Wild PC doesn't offer one. `acme` mode avoids it entirely:
Caddy obtains a
**real Let's Encrypt wildcard cert** (`*.`) via a **DNS-01** challenge, so
every browser trusts it with **zero CA install** — while the services stay
@@ -496,25 +496,25 @@ is published at `.`. Services stay domain-agnostic (switching
`gateway.domain` needs no service edits). One `*.` site means a single cert
covers every route — adding a service needs no new cert.
-Setup (the parts castle can't do for you):
+Setup (the parts wildpc can't do for you):
- **DNS-plugin Caddy.** Stock Caddy has no DNS modules; build one with the
provider plugin: `./install.sh --with-dns-plugin=cloudflare` (uses `xcaddy`,
installs to `/usr/local/bin/caddy`, which the gateway picks up on next deploy).
- **Provider token.** Store a scoped API token as the `CLOUDFLARE_API_TOKEN`
secret (Cloudflare scope: **Zone → DNS → Edit**), and map it into the gateway
- service env — add to `deployments/castle-gateway.yaml`:
+ service env — add to `deployments/wildpc-gateway.yaml`:
```yaml
defaults:
env:
CLOUDFLARE_API_TOKEN: ${secret:CLOUDFLARE_API_TOKEN}
```
- `castle apply` warns if the domain, this env var, or the secret is missing.
+ `wildpc 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
services aren't externally reachable.
-- **Staging first.** Set `CASTLE_ACME_STAGING=1` to use Let's Encrypt's staging CA
+- **Staging first.** Set `WILDPC_ACME_STAGING=1` to use Let's Encrypt's staging CA
(its rate limits are generous) while verifying issuance, then unset it and
redeploy to get a browser-trusted production cert. Verify with
`openssl s_client -connect :443 -servername claw. | openssl x509 -noout -issuer`.
@@ -535,8 +535,8 @@ manage:
systemd: {}
```
-Marks the deployment systemd-managed, so `castle apply` generates and reconciles
-its unit (and `castle service logs` tails it). An empty `{}` uses defaults
+Marks the deployment systemd-managed, so `wildpc apply` generates and reconciles
+its unit (and `wildpc service logs` tails it). An empty `{}` uses defaults
(enable=true, restart=on-failure, restart_sec=2).
Full options:
@@ -547,7 +547,7 @@ manage:
restart: always # on-failure | always | no
restart_sec: 2
no_new_privileges: true
- after: [network.target, castle-other.service]
+ after: [network.target, wildpc-other.service]
wanted_by: [default.target]
exec_reload: "caddy reload ..."
```
@@ -555,7 +555,7 @@ manage:
### `defaults` — Environment
`defaults.env` is the **single, explicit source** of the env a service/job runs
-with — what you write here is exactly what lands in the systemd unit. Castle
+with — what you write here is exactly what lands in the systemd unit. Wild PC
does **not** inject hidden convention vars; whatever env var your program reads
for its port, data dir, etc., you map here.
@@ -564,25 +564,25 @@ expose: { http: { internal: { port: 9001 }, health_path: /health } }
defaults:
env:
MY_SERVICE_PORT: ${port} # the program's own port var ← expose.port
- MY_SERVICE_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/
+ MY_SERVICE_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/
CENTRAL_CONTEXT_URL: http://localhost:9001
API_KEY: ${secret:MY_API_KEY}
```
-Values may contain placeholders that castle resolves at deploy:
+Values may contain placeholders that wildpc resolves at deploy:
| Placeholder | Expands to |
|-------------|------------|
| `${port}` | the service's `expose.http.internal.port` (so it can't drift) |
-| `${data_dir}` | `$CASTLE_DATA_DIR/` (the dedicated data volume) |
+| `${data_dir}` | `$WILDPC_DATA_DIR/` (the dedicated data volume) |
| `${name}` | the deployment name |
| `${public_url}` | the service's gateway-facing base URL — `https://.` when exposed under `tls: acme`, else the node-local `http://localhost:`. The origin an app allowlists (CORS/WebSocket/secure-context); tracks `gateway.domain`, so a domain change needs no app edit. |
-| `${secret:NAME}` | the contents of `~/.castle/secrets/NAME` |
+| `${secret:NAME}` | the contents of `~/.wildpc/secrets/NAME` |
Hardcode the values instead if you prefer; the placeholders just save you from
-repeating castle's computed paths/ports. `castle program create` scaffolds the
+repeating wildpc's computed paths/ports. `wildpc program create` scaffolds the
`${port}`/`${data_dir}` lines for new services. Never store secrets in
-castle.yaml — use `${secret:…}`.
+wildpc.yaml — use `${secret:…}`.
## Job fields
@@ -598,37 +598,37 @@ schedule: "*/5 * * * *"
timezone: America/Los_Angeles # default
```
-Castle generates a systemd `.timer` file alongside the `.service` unit.
+Wild PC generates a systemd `.timer` file alongside the `.service` unit.
## How programs get into `/data/repos/`
-Every program's source lives under `$CASTLE_REPOS_DIR` (default `/data/repos//`).
+Every program's source lives under `$WILDPC_REPOS_DIR` (default `/data/repos//`).
It can arrive there a few ways:
-1. **Scaffold a new one** with `castle program create` — writes the project into
- `/data/repos//` and registers it in `castle.yaml` with an absolute
+1. **Scaffold a new one** with `wildpc program create` — writes the project into
+ `/data/repos//` and registers it in `wildpc.yaml` with an absolute
`source: /data/repos/`.
-2. **Adopt an existing repo** — `castle program add ` registers it
- in place (or records its `repo:` URL for `castle program clone`).
+2. **Adopt an existing repo** — `wildpc program add ` registers it
+ in place (or records its `repo:` URL for `wildpc program clone`).
3. **Drop files in directly** — a `/data/repos//` directory is just a
working tree; it doesn't have to be under version control to be run.
`/data/repos/` holds independent repos — each program directory manages its own
version control (or none); some are standalone git clones, others loose files.
-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:`.
+Wild PC's own programs (CLI, core, wildpc-api, app) are the exception: they live
+inside the wildpc git repo and are referenced with `source: repo:`.
## Registering a new program
-### Via `castle program create` (recommended)
+### Via `wildpc program create` (recommended)
```bash
-# Service — scaffolds into /data/repos/, assigns port, registers in castle.yaml
-castle program create my-service --stack python-fastapi --description "Does something"
+# Service — scaffolds into /data/repos/, assigns port, registers in wildpc.yaml
+wildpc program create my-service --stack python-fastapi --description "Does something"
# Tool — scaffolds into /data/repos/
-castle program create my-tool --stack python-cli --description "Does something"
+wildpc program create my-tool --stack python-cli --description "Does something"
```
### Manually
@@ -672,18 +672,18 @@ manage:
## Lifecycle
-One flow for every kind — scaffold, implement, **`castle apply`**. Activation is
+One flow for every kind — scaffold, implement, **`wildpc apply`**. Activation is
polymorphic over the `manager`, so the *verb* never changes; only what apply *does*
does:
```bash
-castle program create my-thing --stack python-fastapi # scaffold source + deployment
+wildpc program create my-thing --stack python-fastapi # scaffold source + deployment
cd /data/repos/my-thing && uv sync # implement
-castle program test my-thing # dev verbs (build/test/lint/…)
-castle apply my-thing # converge: render + activate
+wildpc program test my-thing # dev verbs (build/test/lint/…)
+wildpc apply my-thing # converge: render + activate
```
-| kind (manager) | what `castle apply` does |
+| kind (manager) | what `wildpc apply` does |
|----------------|--------------------------|
| **service** (systemd) | render the `.service` unit + gateway route, `enable --now` |
| **job** (systemd + schedule) | render a `.service` (Type=oneshot) **and** a `.timer` |
@@ -693,47 +693,47 @@ castle apply my-thing # converge: render + act
Manage a running deployment:
```bash
-castle logs my-thing -f # Tail logs
-castle program run my-thing # Run in foreground (for debugging)
-castle restart my-thing # Imperative bounce — re-actualize current state
+wildpc logs my-thing -f # Tail logs
+wildpc program run my-thing # Run in foreground (for debugging)
+wildpc restart my-thing # Imperative bounce — re-actualize current state
```
To durably turn something off, set `enabled: false` in its deployment and
-`castle apply` — there is no start/stop/enable/disable/install verb.
+`wildpc apply` — there is no start/stop/enable/disable/install verb.
## Infrastructure paths
-Castle uses **two** independent roots, each overridable by an environment
+Wild PC uses **two** independent roots, each overridable by an environment
variable (both expand `~` and resolve relative paths):
-- **`CASTLE_HOME`** — config, code, artifacts, and secrets. Default `~/.castle`.
-- **`CASTLE_DATA_DIR`** — program/service data I/O (potentially large; lives on a
- dedicated volume). Default `/data/castle`. Decoupled from `CASTLE_HOME` on
+- **`WILDPC_HOME`** — config, code, artifacts, and secrets. Default `~/.wildpc`.
+- **`WILDPC_DATA_DIR`** — program/service data I/O (potentially large; lives on a
+ dedicated volume). Default `/data/wildpc`. Decoupled from `WILDPC_HOME` on
purpose so bulk data doesn't sit in the home directory.
| What | Where |
|------|-------|
-| Castle home | `$CASTLE_HOME` (default `~/.castle`) |
-| Config | `$CASTLE_HOME/castle.yaml` + `programs/` + `deployments/` |
-| Program source (yours) | `/data/repos//` (`$CASTLE_REPOS_DIR`; absolute `source:`) |
-| Program source (castle's) | `/` (via `source: repo:`) |
-| Secrets | `$CASTLE_HOME/secrets/` |
-| Generated Caddyfile | `$CASTLE_HOME/artifacts/specs/Caddyfile` |
+| Wild PC home | `$WILDPC_HOME` (default `~/.wildpc`) |
+| Config | `$WILDPC_HOME/wildpc.yaml` + `programs/` + `deployments/` |
+| Program source (yours) | `/data/repos//` (`$WILDPC_REPOS_DIR`; absolute `source:`) |
+| Program source (wildpc's) | `/` (via `source: repo:`) |
+| Secrets | `$WILDPC_HOME/secrets/` |
+| Generated Caddyfile | `$WILDPC_HOME/artifacts/specs/Caddyfile` |
| Built frontends | served in place from `//` (no copy) |
-| **Service data** | **`$CASTLE_DATA_DIR//` (default `/data/castle//`)** |
-| Systemd units | `~/.config/systemd/user/castle-*.service` |
-| Systemd timers | `~/.config/systemd/user/castle-*.timer` |
+| **Service data** | **`$WILDPC_DATA_DIR//` (default `/data/wildpc//`)** |
+| Systemd units | `~/.config/systemd/user/wildpc-*.service` |
+| Systemd timers | `~/.config/systemd/user/wildpc-*.timer` |
-Defined in `core/src/castle_core/config.py`: `CASTLE_HOME` (with derived
+Defined in `core/src/wildpc_core/config.py`: `WILDPC_HOME` (with derived
`CODE_DIR`, `SECRETS_DIR`, `SPECS_DIR`, `CONTENT_DIR`) and the independent
-`DATA_DIR` (`CASTLE_DATA_DIR`). A service reaches its data path by mapping
-`${data_dir}` (= `$CASTLE_DATA_DIR/`) to the env var its program reads, in
+`DATA_DIR` (`WILDPC_DATA_DIR`). A service reaches its data path by mapping
+`${data_dir}` (= `$WILDPC_DATA_DIR/`) to the env var its program reads, in
`defaults.env`. Systemd unit/timer paths are fixed by systemd's user-unit
convention.
## Manifest models
-The Pydantic models live in `core/src/castle_core/manifest.py`. Key classes:
+The Pydantic models live in `core/src/wildpc_core/manifest.py`. Key classes:
- `ProgramSpec` — software catalog entry (source, stack, build, system_dependencies)
- `DeploymentSpec` — a deployment, a discriminated union on `manager`:
@@ -745,8 +745,8 @@ The Pydantic models live in `core/src/castle_core/manifest.py`. Key classes:
- `ExposeSpec`, `ProxySpec`, `ManageSpec`, `BuildSpec`
- `CaddySpec`, `SystemdSpec`, `HttpExposeSpec`, `HttpInternal`
-Config loading: `core/src/castle_core/config.py` — `load_config()` parses the
-config root into `CastleConfig` with typed `programs` and `deployments` dicts.
+Config loading: `core/src/wildpc_core/config.py` — `load_config()` parses the
+config root into `WildpcConfig` with typed `programs` and `deployments` dicts.
-Infrastructure generators: `core/src/castle_core/generators/` — systemd unit/timer
+Infrastructure generators: `core/src/wildpc_core/generators/` — systemd unit/timer
generation (`systemd.py`) and Caddyfile generation (`caddyfile.py`).
diff --git a/docs/relationships.md b/docs/relationships.md
index 99aa915..8689061 100644
--- a/docs/relationships.md
+++ b/docs/relationships.md
@@ -1,6 +1,6 @@
# Relationships: requires, repos, and derived predicates
-How castle models the relationships between **programs**, **deployments**, and
+How wildpc models the relationships between **programs**, **deployments**, and
**repos** — and answers questions like *"is this functional?"*, *"is it fresh?"*,
*"is it deployed?"* — with the smallest possible amount of stored state.
@@ -10,7 +10,7 @@ How castle models the relationships between **programs**, **deployments**, and
A *predicate* is a question we ask about a program or deployment: `functional?`,
`fresh?`, `deployed?`. None of these are ever stored — each is a **function** over
-data castle already has (git, config, the registry). When a predicate can't be
+data wildpc already has (git, config, the registry). When a predicate can't be
answered from derived data, find the one missing datum and ask: is it about the
**thing** (a node property) or about a **relationship** (an edge property)? Encode
*only* that datum. Everything else stays computed.
@@ -59,17 +59,17 @@ each precondition on its natural layer — a deployment-ref is node-level wiring
the program). There is no `requires` on the program, and no `kind: system` written
into a deployment's `requires`.
-Only encode a `requires` edge that is **not derivable** and that **castle itself
+Only encode a `requires` edge that is **not derivable** and that **wildpc itself
must traverse** for an operation (status, bring-up order, group ops). Do **not**
duplicate what another layer already owns — systemd `Requires=`/`After=` for unit
-ordering, uv/pnpm for build graphs. This is *castle's* slice, uncoupled from any
+ordering, uv/pnpm for build graphs. This is *wildpc's* slice, uncoupled from any
one package ecosystem.
### Env is derived *from* `requires`, never scraped *into* it
Reading dependencies out of env strings is unstable (formats vary; a static
frontend's API URL is baked into its bundle and invisible). The stable direction is
-the reverse: from an encoded `{ref, bind}` deployment requirement castle **generates**
+the reverse: from an encoded `{ref, bind}` deployment requirement wildpc **generates**
the wiring env — it knows the target's address (`.` / its port) and
projects it into the consumer's env, optionally under the var named by `bind`. Same
move as `${public_url}`, one step further. Dependency → env, never env → dependency.
diff --git a/docs/stacks/hugo.md b/docs/stacks/hugo.md
index 8588161..93535fd 100644
--- a/docs/stacks/hugo.md
+++ b/docs/stacks/hugo.md
@@ -1,13 +1,13 @@
-# Hugo static sites in Castle
+# Hugo static sites in Wild PC
> **This is a stack — creation-time guidance for writing _new_ sites.**
-> A stack is a template + conventions, not a runtime requirement. `castle program
+> A stack is a template + conventions, not a runtime requirement. `wildpc program
> create --stack hugo` scaffolds from it (via Hugo's own `hugo new site`) and seeds
-> the program's default build verb. An existing Hugo site adopted with `castle
+> the program's default build verb. An existing Hugo site adopted with `wildpc
> program add` doesn't need this stack — it declares its own `commands:` /
> `build:`. See @docs/registry.md for `commands:`, `stack:` (optional), and `repo:`.
-How to build, serve, and manage [Hugo](https://gohugo.io) sites as castle programs.
+How to build, serve, and manage [Hugo](https://gohugo.io) sites as wildpc programs.
## Stack
@@ -18,27 +18,27 @@ How to build, serve, and manage [Hugo](https://gohugo.io) sites as castle progra
`/posts/index.html`) and 404s missing paths. (The default `spa: true` is a
single-page-app fallback that serves the root `index.html` for every unmatched
path — correct for React/Vite, but it swallows a Hugo site's in-page links back
- to the homepage. `castle program create --stack hugo` sets `spa: false` for you.)
+ to the homepage. `wildpc program create --stack hugo` sets `spa: false` for you.)
- Package manager (only if a theme needs an asset pipeline): pnpm
Hugo has one meaningful dev verb — **build**. It has no native lint/test/type-check,
-so the stack advertises only `build` / `install` / `uninstall`; `castle check` and
+so the stack advertises only `build` / `install` / `uninstall`; `wildpc check` and
friends aren't offered (a site can still declare its own, e.g. an HTML linter, under
`commands:` — a declared verb always wins over the stack).
## Create a new site
```bash
-castle program create my-site --stack hugo --description "My site"
+wildpc program create my-site --stack hugo --description "My site"
cd /data/repos/my-site
-castle program build my-site # hugo --gc --minify -> public/
-castle apply my-site # serve at my-site.
+wildpc program build my-site # hugo --gc --minify -> public/
+wildpc apply my-site # serve at my-site.
```
The scaffold delegates the canonical skeleton to `hugo new site` (archetypes/,
content/, layouts/, static/, themes/, hugo.toml) and overlays the pieces a bare
skeleton lacks: minimal `layouts/` so the site **builds and serves without a
-theme**, an example `content/posts/hello.md`, a castle-flavored `hugo.toml`
+theme**, an example `content/posts/hello.md`, a wildpc-flavored `hugo.toml`
(`baseURL = "/"`, so assets resolve at the root of the site's own subdomain), and a
`.gitignore` for the regenerated `public/` and `resources/`.
@@ -78,14 +78,14 @@ cd themes/ && pnpm install
## Deployment shape
-`castle program create --stack hugo` writes:
+`wildpc program create --stack hugo` writes:
- **`programs/.yaml`** — `source`, `stack: hugo`, `build.outputs: [public]`.
- **`deployments/statics/.yaml`** — `manager: caddy`, `root: public`,
`reach: internal` (flip to `public` to also expose over the tunnel).
The gateway serves `/public` in place — no copy, no Node/Hugo process at
-runtime. `castle program build` regenerates `public/`; `castle apply` renders the
+runtime. `wildpc program build` regenerates `public/`; `wildpc apply` renders the
route and reloads the gateway.
## Adopting an existing Hugo site
@@ -93,7 +93,7 @@ route and reloads the gateway.
No stack needed — adopt the repo and declare how it builds:
```bash
-castle program add /path/to/site --name my-site
+wildpc program add /path/to/site --name my-site
```
Then set `build.commands` (as above) and add a `manager: caddy` deployment. This is
diff --git a/docs/stacks/python-cli.md b/docs/stacks/python-cli.md
index 1670448..2e1b862 100644
--- a/docs/stacks/python-cli.md
+++ b/docs/stacks/python-cli.md
@@ -1,9 +1,9 @@
-# Python Tools in Castle
+# Python Tools in Wild PC
> **This is a stack — creation-time guidance for writing _new_ CLI tools.**
-> A stack is a template + conventions, not a runtime requirement. `castle program create
+> A stack is a template + conventions, not a runtime requirement. `wildpc program create
> --stack python-cli` scaffolds from it and seeds the program's default dev-verb
-> commands. An existing CLI adopted with `castle program add` doesn't need this stack — it
+> commands. An existing CLI adopted with `wildpc program add` doesn't need this stack — it
> declares its own `commands:`. See @docs/registry.md for `commands:`,
> `stack:` (optional), and `repo:`.
@@ -49,11 +49,11 @@ Examples: `code/pdf2md/`, `code/gpt/`, `code/protonmail/`
## Creating a new tool
```bash
-castle program create my-tool --stack python-cli --description "Does something"
+wildpc program create my-tool --stack python-cli --description "Does something"
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 `wildpc.yaml`.
## pyproject.toml
diff --git a/docs/stacks/python-fastapi.md b/docs/stacks/python-fastapi.md
index 27cc087..61aac73 100644
--- a/docs/stacks/python-fastapi.md
+++ b/docs/stacks/python-fastapi.md
@@ -1,15 +1,15 @@
-# Web APIs in Castle
+# Web APIs in Wild PC
> **This is a stack — creation-time guidance for writing _new_ FastAPI services.**
-> A stack is a template + conventions, not a runtime requirement. `castle program create
+> A stack is a template + conventions, not a runtime requirement. `wildpc program create
> --stack python-fastapi` scaffolds from it and seeds the program's default
-> dev-verb commands. An existing service adopted with `castle program add` doesn't need
+> dev-verb commands. An existing service adopted with `wildpc program add` doesn't need
> this stack — it declares its own `commands:`. See @docs/registry.md
> for `commands:`, `stack:` (optional), and `repo:`.
-How to build Python web APIs as castle service components. Based on the
+How to build Python web APIs as wildpc service components. Based on the
patterns used in [wild-cloud/api](https://github.com/civilsociety-dev/wild-cloud)
-and existing castle services (central-context, notification-bridge, event-bus).
+and existing wildpc services (central-context, notification-bridge, event-bus).
## Stack
@@ -103,7 +103,7 @@ class Settings(BaseSettings):
settings = Settings()
```
-Castle passes config via env vars in the deployment's `defaults.env`:
+Wild PC passes config via env vars in the deployment's `defaults.env`:
```yaml
# programs/my-service.yaml
@@ -127,9 +127,9 @@ manage:
systemd: {}
```
-The env a service runs with is exactly what's in `defaults.env` — castle injects
+The env a service runs with is exactly what's in `defaults.env` — wildpc injects
nothing implicitly. Map the vars your settings read (above, `env_prefix:
-"MY_SERVICE_"` → `MY_SERVICE_PORT`/`MY_SERVICE_DATA_DIR`) to castle's computed
+"MY_SERVICE_"` → `MY_SERVICE_PORT`/`MY_SERVICE_DATA_DIR`) to wildpc's computed
values with the `${port}`/`${data_dir}` placeholders — add to
`deployments/my-service.yaml`:
@@ -137,11 +137,11 @@ values with the `${port}`/`${data_dir}` placeholders — add to
defaults:
env:
MY_SERVICE_PORT: ${port} # = expose.http.internal.port
- MY_SERVICE_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/my-service
+ MY_SERVICE_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/my-service
CENTRAL_CONTEXT_URL: http://localhost:9001
```
-`castle program create` scaffolds the `${port}`/`${data_dir}` lines for you.
+`wildpc program create` scaffolds the `${port}`/`${data_dir}` lines for you.
## Application entry point
@@ -300,10 +300,10 @@ Mapping convention:
## Storage
-Castle services use filesystem storage with JSON metadata sidecars:
+Wild PC services use filesystem storage with JSON metadata sidecars:
```
-$CASTLE_DATA_DIR/my-service/ # default /data/castle/my-service/
+$WILDPC_DATA_DIR/my-service/ # default /data/wildpc/my-service/
└── bucket/
├── item-name
└── item-name.meta.json
@@ -447,11 +447,11 @@ uv run ruff format . # Format
## Scaffolding
-`castle program create` generates all of this automatically:
+`wildpc program create` generates all of this automatically:
```bash
-castle program create my-service --stack python-fastapi --description "Does something useful"
+wildpc program create my-service --stack python-fastapi --description "Does something useful"
```
-See @docs/registry.md for manifest fields, castle.yaml structure,
-and the full service lifecycle (`castle apply`, logs).
+See @docs/registry.md for manifest fields, wildpc.yaml structure,
+and the full service lifecycle (`wildpc apply`, logs).
diff --git a/docs/stacks/react-vite.md b/docs/stacks/react-vite.md
index 65228e7..8528283 100644
--- a/docs/stacks/react-vite.md
+++ b/docs/stacks/react-vite.md
@@ -1,13 +1,13 @@
-# Web Frontends in Castle
+# Web Frontends in Wild PC
> **This is a stack — creation-time guidance for writing _new_ frontends.**
-> A stack is a template + conventions, not a runtime requirement. `castle program create
+> A stack is a template + conventions, not a runtime requirement. `wildpc program create
> --stack react-vite` scaffolds from it and seeds the program's default dev-verb
-> commands. An existing frontend adopted with `castle program add` doesn't need this
+> commands. An existing frontend adopted with `wildpc program add` doesn't need this
> stack — it declares its own `commands:`. See @docs/registry.md for
> `commands:`, `stack:` (optional), and `repo:`.
-How to build, serve, and manage web frontends as castle programs.
+How to build, serve, and manage web frontends as wildpc programs.
## Stack
@@ -56,7 +56,7 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
- // Castle sets VITE_BASE=/ at build time (every frontend serves at its
+ // Wild PC sets VITE_BASE=/ at build time (every frontend serves at its
// subdomain root). Reading it here keeps the bundle.s absolute asset
// URLs resolving at the root — no hand-tuning.
base: process.env.VITE_BASE ?? "/",
@@ -70,7 +70,7 @@ export default defineConfig({
```
> **Serving behind the gateway.** A static frontend is served at its own subdomain
-> — `.` — rooted at `/`, so `VITE_BASE` is always `/`. Castle's
+> — `.` — rooted at `/`, so `VITE_BASE` is always `/`. Wild PC's
> `react-vite` build passes `VITE_BASE=/`, and a `vite.config` that reads it (above)
> works unchanged. (Frontends are no longer mounted under a `//` path.)
@@ -130,19 +130,19 @@ project root:
24.14.1
```
-Castle reads this pin and puts the matching node on PATH whenever it runs the
-program's node — at build time (`castle program build`, incl. builds triggered from
-the castle web app, which run inside the `castle-api` service) and at run time (a
+Wild PC reads this pin and puts the matching node on PATH whenever it runs the
+program's node — at build time (`wildpc program build`, incl. builds triggered from
+the wildpc web app, which run inside the `wildpc-api` service) and at run time (a
`launcher: node` service's systemd unit). This is why a build works from the web app
even though the service's default PATH deliberately omits nvm's versioned dirs.
-The pin is standard, tool-agnostic config — the program stays castle-independent; nvm
-(and editors/CI) honor the same file. Castle resolves it against
-`~/.nvm/versions/node` (override with `CASTLE_NODE_VERSIONS_DIR`), newest match wins.
+The pin is standard, tool-agnostic config — the program stays wildpc-independent; nvm
+(and editors/CI) honor the same file. Wild PC resolves it against
+`~/.nvm/versions/node` (override with `WILDPC_NODE_VERSIONS_DIR`), newest match wins.
An exact pin (`24.14.1`) matches exactly; a partial (`24`) or a range (`>=24`) matches
the newest installed major. If the pinned version isn't installed, the verb fails loud
with a `nvm install ` hint instead of a cryptic `node: not found`. Unpinned →
-Castle injects no node (it uses whatever ambient node is on PATH, and does not guess).
+Wild PC injects no node (it uses whatever ambient node is on PATH, and does not guess).
## Registering as a program
@@ -194,19 +194,19 @@ 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 apply` generates it.
+process. You do **not** write this block by hand — `wildpc apply` generates it.
The flow:
-1. You build the frontend with `castle program build ` (deploy does **not**
+1. You build the frontend with `wildpc program build ` (deploy does **not**
build — it only points Caddy at `dist/`).
2. The Caddyfile generator emits a route per `manager: caddy` deployment (kind
**static**), rooted **in place** at `/` — no copy. Each static
frontend is served at its own subdomain `.` (the
- dashboard, `castle`, is also the target of the `:9000` redirect).
+ dashboard, `wildpc`, is also the target of the `:9000` redirect).
The build is run with `VITE_BASE=/`, so a `vite.config` that reads it (see
[Vite config](#vite-config)) emits root-relative asset URLs. The generated block
-(in `~/.castle/artifacts/specs/Caddyfile`) is a host matcher inside the
+(in `~/.wildpc/artifacts/specs/Caddyfile`) is a host matcher inside the
`*.` site:
```caddyfile
@@ -225,7 +225,7 @@ not hand-configured.
## API integration
-Frontends talk to castle services via environment variables injected at build
+Frontends talk to wildpc services via environment variables injected at build
time. Vite exposes variables prefixed with `VITE_`:
```bash
@@ -256,7 +256,7 @@ class ApiClient {
export const apiClient = new ApiClient()
```
-When served behind the castle gateway, the API base URL can use the gateway's
+When served behind the wildpc gateway, the API base URL can use the gateway's
proxy paths (e.g., `/central-context/`) instead of direct ports, avoiding CORS.
## React Query setup
diff --git a/docs/stacks/supabase.md b/docs/stacks/supabase.md
index 38d3862..216b12a 100644
--- a/docs/stacks/supabase.md
+++ b/docs/stacks/supabase.md
@@ -1,13 +1,13 @@
-# Supabase Apps in Castle
+# Supabase Apps in Wild PC
> **This is a stack — creation-time guidance for writing _new_ database-backed
> web apps.** A stack is a template + conventions, not a runtime requirement.
-> `castle program create --stack supabase` scaffolds from it and seeds the
+> `wildpc program create --stack supabase` scaffolds from it and seeds the
> program's default dev-verb commands. See @docs/registry.md for `commands:`,
> `stack:` (optional), and the deployment `manager` (and derived `kind`).
-How to build tiny, database-backed web apps as castle programs that target a
-**shared Supabase substrate**. This is Castle's "a stack whose default is a
+How to build tiny, database-backed web apps as wildpc programs that target a
+**shared Supabase substrate**. This is Wild PC's "a stack whose default is a
substrate": the app owns its code (and stays repo-durable), and rents the boring
backend — Postgres + auth + storage + RLS — that an app can't reliably reinvent.
@@ -16,7 +16,7 @@ backend — Postgres + auth + storage + RLS — that an app can't reliably reinv
Unlike the other stacks (which scaffold a self-contained process), a supabase app
is **code + migrations that deploy against a shared backend**:
-- **The substrate** is one castle service (`supabase`, the `supabase-substrate`
+- **The substrate** is one wildpc service (`supabase`, the `supabase-substrate`
repo) running self-hosted Supabase via a `manager: systemd` deployment with the
`compose` launcher. It is shared by
every supabase app. Stand it up once (see that repo's README).
@@ -27,10 +27,10 @@ is **code + migrations that deploy against a shared backend**:
Apps are isolated on the shared instance by their **own Postgres schema + RLS**
(and Storage buckets), under **one identity pool** — correct for a single-operator
datalake. Each app owns a schema named after the program (`my-app` → schema
-`my_app`); `castle program build` creates and grants it, tracks migrations in a
-per-app `.schema_migrations`, and PostgREST exposes it (castle derives the
+`my_app`); `wildpc program build` creates and grants it, tracks migrations in a
+per-app `.schema_migrations`, and PostgREST exposes it (wildpc derives the
substrate's `PGRST_DB_SCHEMAS` from the registered apps). This gives a clean
-teardown — `castle delete --purge-data` runs `drop schema cascade` — and
+teardown — `wildpc delete --purge-data` runs `drop schema cascade` — and
means migration version tokens never collide across apps. Substrate-per-app is
deliberately not supported: ~14 containers per app doesn't scale to "lots of small
ideas," and a DB-backed app is a pet either way.
@@ -69,14 +69,14 @@ gateway serves `public/` in place at `.` (its own subdomai
```yaml
name: my-app
-substrate: supabase # the shared castle service this app deploys against
+substrate: supabase # the shared wildpc service this app deploys against
auth: public # public | private | shared: [handles]
schema: my_app # this app's isolated Postgres schema (frontend: db.schema)
```
## Migrations
-`migrations/*.sql` are **numbered, forward-only, and idempotent**. `castle program
+`migrations/*.sql` are **numbered, forward-only, and idempotent**. `wildpc program
build my-app` runs the versioned migration runner: it creates + grants the app's
schema, ensures a per-app `.schema_migrations` table, reads applied
versions, and applies only the **unapplied** files (in filename order) with
@@ -112,14 +112,14 @@ const db = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { db: { schema: SCHEMA
### Teardown
-An app's rows live only on the substrate, so an ordinary `castle delete my-app`
+An app's rows live only on the substrate, so an ordinary `wildpc delete my-app`
leaves the schema intact (and says so). To destroy the data too:
```bash
-castle delete my-app --purge-data # drop schema my_app cascade
+wildpc delete my-app --purge-data # drop schema my_app cascade
```
-`castle apply` then prunes the schema from `PGRST_DB_SCHEMAS`; **restart the
+`wildpc 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
@@ -158,17 +158,17 @@ service itself is likewise at `supabase.`.) See
## Commands
```bash
-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 apply # serve the static UI at .
+wildpc program create my-app --stack supabase --description "..." # scaffold + register
+wildpc program build my-app # apply unapplied migrations to the substrate
+wildpc program test my-app # deno test over functions/ (if deno present)
+wildpc apply # serve the static UI at .
```
## Scaffolding
-`castle program create --stack supabase` generates the full layout above and
+`wildpc program create --stack supabase` generates the full layout above and
registers the program as a static frontend. Set the anon key in `public/config.js`
-(`cat ~/.castle/secrets/SUPABASE_ANON_KEY`), edit your migrations, and build.
+(`cat ~/.wildpc/secrets/SUPABASE_ANON_KEY`), edit your migrations, and build.
See @docs/registry.md for the `compose` launcher, the substrate deployment definition,
and the full registry reference. The substrate itself lives in the
diff --git a/docs/tcp-exposure.md b/docs/tcp-exposure.md
index 9c4d385..edc36f8 100644
--- a/docs/tcp-exposure.md
+++ b/docs/tcp-exposure.md
@@ -1,9 +1,9 @@
-# TCP exposure & castle-managed TLS material
+# TCP exposure & wildpc-managed TLS material
> Status: **design / not yet implemented.** This specs the `reach` exposure
-> ladder, raw-TCP service exposure (postgres, redis, …), and how castle
+> ladder, raw-TCP service exposure (postgres, redis, …), and how wildpc
> materializes its ACME wildcard cert onto a service so a raw port presents a
-> *trusted* cert — generically, with **no protocol knowledge in castle's code**.
+> *trusted* cert — generically, with **no protocol knowledge in wildpc's code**.
Read `docs/dns-and-tls.md` first — this builds on the acme wildcard model.
@@ -17,14 +17,14 @@ protocols either send a cleartext preamble before any ClientHello (postgres,
mysql) so there's no SNI to route on, or they simply aren't HTTP. But they don't
*need* the gateway: the wildcard DNS record already points **every** subdomain at
the node, and each service already has its own port. So "expose a TCP service
-internally" reduces to two things castle can do without a proxy:
+internally" reduces to two things wildpc can do without a proxy:
1. **bind the port** on the LAN, and
2. **put a trusted cert on the service** — the one wildcard cert we already own,
which is valid for `.` (wildcards match).
The only per-service variation is *what file format the cert takes* and *how the
-service is told to re-read it*. Both are pushed into the deployment yaml. Castle
+service is told to re-read it*. Both are pushed into the deployment yaml. Wild PC
stays a cert-mover and a signal-sender.
Public raw-TCP is a separate, Cloudflare-edge concern — see §6.
@@ -47,8 +47,8 @@ says *what it speaks*.
### Legacy mapping (back-compat normalizer)
-`castle` accepts the old fields and normalizes them so existing yamls keep
-working; `castle apply` can rewrite them on next write:
+`wildpc` accepts the old fields and normalizes them so existing yamls keep
+working; `wildpc apply` can rewrite them on next write:
| old | new |
|-----|-----|
@@ -59,7 +59,7 @@ working; `castle apply` can rewrite them on next write:
---
-## 3. Manifest models (`core/src/castle_core/manifest.py`)
+## 3. Manifest models (`core/src/wildpc_core/manifest.py`)
```python
class Reach(str, Enum):
@@ -77,7 +77,7 @@ class TlsMaterial(str, Enum):
class TlsSpec(BaseModel):
material: TlsMaterial = TlsMaterial.OFF
# Optional zero-downtime reload argv run after re-materialize on renewal.
- # Default: castle restarts the deployment (fine for a ~60-day cadence).
+ # Default: wildpc restarts the deployment (fine for a ~60-day cadence).
reload: list[str] | None = None
@@ -85,7 +85,7 @@ class TcpExposeSpec(BaseModel):
port: int = Field(ge=1, le=65535)
tls: TlsSpec | None = None # step 3; absent = service does its own TLS
# No bind-host field: publishing the port on the LAN is the deployment's own
- # job (a container's run.ports, or a native service binding 0.0.0.0). castle
+ # job (a container's run.ports, or a native service binding 0.0.0.0). wildpc
# doesn't rebind it, so a `host:` here would be an ignored (misleading) field.
@@ -121,13 +121,13 @@ old `public: bool` normalizes to `reach`.
> **acme prerequisite:** `tls.material != off` only makes sense when
> `gateway.tls == acme` with a domain — otherwise there's no wildcard cert to
-> copy. `castle apply` / `castle doctor` warn if `material` is set without acme.
+> copy. `wildpc apply` / `wildpc doctor` warn if `material` is set without acme.
---
## 4. Placeholders (`deploy.py` `_env_context`)
-When a deployment declares `expose.tcp.tls.material != off`, castle adds these to
+When a deployment declares `expose.tcp.tls.material != off`, wildpc adds these to
the placeholder context (alongside the existing `${port}`/`${data_dir}`/… set),
pointing at the materialized copies under `‹data_dir›/tls/`:
@@ -143,7 +143,7 @@ They resolve through the one shared `${...}` resolver (`resolve_placeholders`,
which `resolve_env_split` also uses) — for a container these `${key}` refs are
expanded in `run.env`, `run.volumes`, `run.args`, `run.command`, `run.workdir`,
`run.user`, and `run.tmpfs` alike. To pass a **literal** `${key}` through to the
-container's own shell/env (rather than have castle expand it), write `$${key}`
+container's own shell/env (rather than have wildpc expand it), write `$${key}`
(docker-compose-style `$$` escape).
**Native service** (python/command launcher) references the host paths directly:
@@ -168,12 +168,12 @@ expose:
tcp: { port: 5432, tls: { material: pair } }
run:
launcher: container
- user: ${uid}:${gid} # default for castle containers — see below
+ user: ${uid}:${gid} # default for wildpc containers — see below
image: postgres:17
ports: { '5432': 5432 }
tmpfs: [/var/run/postgresql] # image runtime dir, needs to be writable by our uid
volumes:
- - /data/castle/postgres/data:/var/lib/postgresql/data
+ - /data/wildpc/postgres/data:/var/lib/postgresql/data
- ${tls_dir}:/tls:ro
args: ['-c','ssl=on','-c','ssl_cert_file=/tls/cert.pem','-c','ssl_key_file=/tls/key.pem']
```
@@ -181,10 +181,10 @@ run:
### Container key ownership — dissolved by uid uniformity (verified)
The naive problem: postgres refuses a key that isn't `0600` **and owned by the
-process uid** (999 in the stock image), while castle writes files as the host
+process uid** (999 in the stock image), while wildpc writes files as the host
user (1000), and bind mounts preserve host uid — so 999 can't read them, and
chowning across uids needs privilege. Rather than patch that with a privileged
-chown, castle **runs containers as the invoking user** (`--user ${uid}:${gid}`,
+chown, wildpc **runs containers as the invoking user** (`--user ${uid}:${gid}`,
the default on `RunContainer`, overridable per deployment). Then the process, its
data dir, its secrets *and* its certs are all one uid — nothing to chown, for any
service. Verified end-to-end against `postgres:17`:
@@ -196,8 +196,8 @@ docker run --user 1000:1000 --tmpfs /var/run/postgresql \
```
The only image-specific detail is a writable runtime dir (`--tmpfs
-/var/run/postgresql`) — declared in the deployment yaml, not castle. `RunContainer`
-gains a `user: str = "${uid}:${gid}"` field and a `tmpfs: list[str]`; castle stays
+/var/run/postgresql`) — declared in the deployment yaml, not wildpc. `RunContainer`
+gains a `user: str = "${uid}:${gid}"` field and a `tmpfs: list[str]`; wildpc stays
privilege-free. (One-time migration cost: an existing data dir owned by 999 gets
chowned to the runtime uid once when a service adopts this — not per-renewal.)
@@ -205,14 +205,14 @@ chowned to the runtime uid once when a service adopts this — not per-renewal.)
## 5. Cert materialization + the `cert_obtained` hook
-### The materializer (`castle_core`, protocol-agnostic)
+### The materializer (`wildpc_core`, protocol-agnostic)
`materialize_tls(config, name, dep) -> bool` — for one deployment with
`tls.material != off`:
1. Locate the source wildcard in Caddy's store by glob (prefer prod over staging):
`~/.local/share/caddy/certificates/acme-v02*-directory/wildcard_.‹domain›/wildcard_.‹domain›.{crt,key}`
- (falls back to `acme-staging-v02*` when `CASTLE_ACME_STAGING=1`).
+ (falls back to `acme-staging-v02*` when `WILDPC_ACME_STAGING=1`).
2. Compare a hash of the source to the materialized copy; **return False if
unchanged** (idempotent — safe to call every apply and every renewal).
3. Write `‹data_dir›/tls/` in the requested format:
@@ -226,12 +226,12 @@ chowned to the runtime uid once when a service adopts this — not per-renewal.)
5. Return True (changed).
`reconcile_tls(config)` — walk all deployments; `materialize_tls` each; for those
-that changed, run `tls.reload` argv **or** `castle restart ‹name›`. Idempotent;
+that changed, run `tls.reload` argv **or** `wildpc restart ‹name›`. Idempotent;
prints a summary.
### Wiring
-- **`castle apply`** calls `materialize_tls` for each affected deployment before
+- **`wildpc apply`** calls `materialize_tls` for each affected deployment before
(re)starting it — so first deploy has certs in place.
- **Event-driven (the hook you wanted) — VERIFIED.** Build Caddy with
`github.com/mholt/caddy-events-exec` (add the `--with` to `install.sh`'s xcaddy
@@ -241,12 +241,12 @@ prints a summary.
email …
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
events {
- on cert_obtained exec castle tls reconcile
+ on cert_obtained exec wildpc tls reconcile
}
}
```
Caddy fires `cert_obtained` on every issuance **and renewal** (renewal reuses
- the same event with `renewal: true`); the handler runs `castle tls reconcile`,
+ the same event with `renewal: true`); the handler runs `wildpc tls reconcile`,
which re-copies the rotated wildcard and reloads consumers. Immediate, no polling.
> **Proven end-to-end** (2026-07-03): built into our Caddy 2.11.4, an
> `on cert_obtained exec` handler fired on internal-CA issuance with event data
@@ -254,15 +254,15 @@ prints a summary.
> emit is issuer-agnostic (CertMagic `config.go`), acme renewal fires it identically.
> Two caveats from the test: (a) the module is **experimental** — pin the xcaddy
> build and re-verify on Caddy upgrades; (b) `exec` runs the command in the
- > **background and swallows its exit code** — so `castle tls reconcile` must be
+ > **background and swallows its exit code** — so `wildpc tls reconcile` must be
> idempotent and log its *own* outcome (don't rely on Caddy surfacing failures).
-- **Safety net:** a nightly `castle-tls-reconcile` job (`manager: systemd` +
- `schedule`) also runs `castle tls reconcile` — catches a missed event, a Caddy
+- **Safety net:** a nightly `wildpc-tls-reconcile` job (`manager: systemd` +
+ `schedule`) also runs `wildpc tls reconcile` — catches a missed event, a Caddy
restart, or a manual cert swap. Same idempotent call, so belt-and-suspenders is
free.
-New CLI: `castle tls reconcile [--plan]` (thin wrapper over `reconcile_tls`), and
-`castle tls status` to show, per deployment, source vs materialized cert fingerprint
+New CLI: `wildpc tls reconcile [--plan]` (thin wrapper over `reconcile_tls`), and
+`wildpc tls status` to show, per deployment, source vs materialized cert fingerprint
+ expiry.
---
@@ -275,8 +275,8 @@ Public raw-TCP can't reach an unmodified client below Cloudflare Enterprise
- `tunnel.py` emits an extra ingress entry per public TCP deployment:
`{ hostname: ‹name›.‹public_domain›, service: tcp://localhost:‹port› }`.
-- castle ensures a self-hosted **Access** app + policy over that hostname.
-- `castle` prints the client connect line:
+- wildpc ensures a self-hosted **Access** app + policy over that hostname.
+- `wildpc` prints the client connect line:
`cloudflared access tcp --hostname ‹name›.‹public_domain› --url localhost:‹local›`.
The client runs `cloudflared access tcp` (or WARP private-network) and points its
@@ -287,7 +287,7 @@ authenticated tunnel, never an open port. Build this only when wanted.
## 7. Generality check
-Same castle code, different yaml — the postgres-ness is two env/arg mappings + a
+Same wildpc code, different yaml — the postgres-ness is two env/arg mappings + a
format choice, nothing more:
| service | `material` | consumes | reload |
@@ -313,7 +313,7 @@ format choice, nothing more:
works, service does its own TLS.~~ **DONE (2026-07-03).** `TcpExposeSpec` +
`ExposeSpec` one-protocol validator; `http_exposed`/`tcp_port` predicates so a
TCP `reach: internal` yields **no** Caddy route (correctness); registry carries
- `tcp_port`; `castle service info` shows the `.:` endpoint;
+ `tcp_port`; `wildpc service info` shows the `.:` endpoint;
public-TCP guarded (step 5). Applied to `postgres` — verified: `psql -h
postgres.civil.payne.io` connects by name, postgres absent from HTTP routes,
`apply --plan` still converged.
@@ -321,22 +321,22 @@ format choice, nothing more:
`TlsSpec`/`TlsMaterial`; `${tls_dir|cert|key|pem|ca}` + `${uid}/${gid}`
placeholders; `RunContainer.user`/`tmpfs` (uid-uniformity, no chown);
`core/tls.py` (`wildcard_cert`/`materialize_tls`/`materialize_all`); wired into
- `castle apply`; unit-tested in isolation (pair/combined/idempotency/stale-clean).
-4. **`cert_obtained` hook + `castle tls`** — **CODE DONE (2026-07-03).** Generator
- emits the `events { on cert_obtained exec castle tls reconcile }` block, **gated
- on `CASTLE_CADDY_CERT_HOOK=1`** so the current (no-plugin) gateway is unaffected;
- `castle tls reconcile|status` CLI; `reconcile_tls` idempotent + self-logging.
+ `wildpc apply`; unit-tested in isolation (pair/combined/idempotency/stale-clean).
+4. **`cert_obtained` hook + `wildpc tls`** — **CODE DONE (2026-07-03).** Generator
+ emits the `events { on cert_obtained exec wildpc tls reconcile }` block, **gated
+ on `WILDPC_CADDY_CERT_HOOK=1`** so the current (no-plugin) gateway is unaffected;
+ `wildpc tls reconcile|status` CLI; `reconcile_tls` idempotent + self-logging.
*(Nightly safety-net job: TODO.)*
→ **LIVE ROLLOUT DONE (2026-07-03).** (a) `/usr/local/bin/caddy` rebuilt with
cloudflare-dns + `caddy-events-exec` (old binary → `caddy.bak-pre-events`;
`install.sh` bakes both plugins). (b) Gate is a durable **`gateway.cert_hook`**
config flag (not an env var) → NodeConfig → generator; set true, `apply`'d; the
- running gateway's admin API confirms the `cert_obtained → castle tls reconcile`
+ running gateway's admin API confirms the `cert_obtained → wildpc tls reconcile`
subscription is live. (c) postgres enabled: `user: ${uid}:${gid}` + `tmpfs` +
`tls.material: pair`; data dir chowned 999→1000 once (cold backup at
- `/data/castle/postgres-data-backup-pre-tls.tar.gz`); WAL-recovered clean.
+ `/data/wildpc/postgres-data-backup-pre-tls.tar.gz`); WAL-recovered clean.
**Verified:** `psql -h postgres.civil.payne.io sslmode=verify-full sslrootcert=system`
- → "verify-full OK" against the trusted LE wildcard; `castle` + `litellm` DBs intact.
+ → "verify-full OK" against the trusted LE wildcard; `wildpc` + `litellm` DBs intact.
Also landed: `${uid}/${gid}` in the placeholder context + placeholder expansion
in container run fields (`volumes`/`args`/`user`/`tmpfs`) so `${tls_dir}` mounts work.
5. **`reach: public` for TCP** (tunnel `tcp://` + Access + connect line) — when
diff --git a/docs/tunnel-setup.md b/docs/tunnel-setup.md
index 444e463..c935fde 100644
--- a/docs/tunnel-setup.md
+++ b/docs/tunnel-setup.md
@@ -1,6 +1,6 @@
# Public exposure via Cloudflare Tunnel
-Castle is LAN-only by default: services with `proxy: true` are reachable at
+Wild PC is LAN-only by default: services with `proxy: true` are reachable at
`.` (e.g. `.civil.payne.io`) through split DNS, and
nothing is on the public internet. This document sets up a **per-service public
toggle** — flip `public: true` on a service and it's also reachable from the public
@@ -17,9 +17,9 @@ 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 apply` generates
+ surface is exactly the set of `public: true` services — `wildpc apply` generates
the ingress from the registry.
-- **One kill switch.** Stop `castle-tunnel` → instantly nothing is public.
+- **One kill switch.** Stop `wildpc-tunnel` → instantly nothing is public.
```
foo.pub.payne.io ──(Cloudflare edge, public cert)──▶ cloudflared (outbound)
@@ -39,22 +39,22 @@ sudo apt-get update && sudo apt-get install -y cloudflared
# 2. Authenticate and create the tunnel (opens a browser to pick the zone)
cloudflared tunnel login
-cloudflared tunnel create castle # prints a tunnel UUID + writes creds JSON
+cloudflared tunnel create wildpc # prints a tunnel UUID + writes creds JSON
-# 3. Move the credentials into Castle's secret store (path the generator expects)
-mkdir -p ~/.castle/secrets/cloudflared
+# 3. Move the credentials into Wild PC's secret store (path the generator expects)
+mkdir -p ~/.wildpc/secrets/cloudflared
TID=
-mv ~/.cloudflared/$TID.json ~/.castle/secrets/cloudflared/$TID.json
-chmod 600 ~/.castle/secrets/cloudflared/$TID.json
+mv ~/.cloudflared/$TID.json ~/.wildpc/secrets/cloudflared/$TID.json
+chmod 600 ~/.wildpc/secrets/cloudflared/$TID.json
-# 4. Tell Castle the public zone + tunnel id (in ~/.castle/castle.yaml)
+# 4. Tell Wild PC the public zone + tunnel id (in ~/.wildpc/wildpc.yaml)
# gateway:
# ...
# public_domain: pub.payne.io
# tunnel_id:
```
-Then create the tunnel deployment at `~/.castle/deployments/castle-tunnel.yaml`
+Then create the tunnel deployment at `~/.wildpc/deployments/wildpc-tunnel.yaml`
(`manager: systemd` → kind: service):
```yaml
@@ -67,7 +67,7 @@ run:
- tunnel
- --no-autoupdate
- --config
- - /home/payne/.castle/artifacts/specs/cloudflared.yml
+ - /home/payne/.wildpc/artifacts/specs/cloudflared.yml
- run
manage:
systemd: {}
@@ -76,8 +76,8 @@ manage:
Bring it online:
```bash
-castle apply # writes cloudflared.yml from public services
-castle apply castle-tunnel # start the tunnel
+wildpc apply # writes cloudflared.yml from public services
+wildpc apply wildpc-tunnel # start the tunnel
```
## Using the toggle
@@ -92,18 +92,18 @@ public: true # also expose at .pub.payne.io via the tunnel
Then just deploy:
```bash
-castle apply
+wildpc apply
```
-`castle apply` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the
-current set of public services and restarts `castle-tunnel`. Flip `public` back to
+`wildpc apply` regenerates `~/.wildpc/artifacts/specs/cloudflared.yml` from the
+current set of public services and restarts `wildpc-tunnel`. Flip `public` back to
`false` (or remove it) and redeploy to un-expose — the hostname drops out of the
ingress immediately.
### DNS: automatic (with a token) or manual
Each public host needs a CNAME `.pub.payne.io → .cfargotunnel.com`.
-Castle can manage these for you. Create a Cloudflare API token with a single
+Wild PC can manage these for you. Create a Cloudflare API token with a single
**`DNS:Edit`** permission scoped to the **public zone** — this is exactly
Cloudflare's built-in *"Edit zone DNS"* template. That one permission is
sufficient: it resolves the zone by name *and* creates/deletes records (no
@@ -112,18 +112,18 @@ token (`cfat_…`) works (that's what this was confirmed with). Drop it into the
secret store:
```bash
-printf %s "" > ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
-chmod 600 ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
+printf %s "" > ~/.wildpc/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
+chmod 600 ~/.wildpc/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
```
-With it present, every `castle apply` **reconciles** the public CNAMEs against the
+With it present, every `wildpc 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 apply` instead prints the exact command to run per host:
+Without the token, `wildpc 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 a4ae86d..3c16104 100755
--- a/install.sh
+++ b/install.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# Castle Platform — Bootstrap Install
+# Wild PC Platform — Bootstrap Install
#
# Idempotent script that sets up the infrastructure "control layer":
# - Docker, Caddy (system binary)
@@ -18,16 +18,16 @@ set -euo pipefail
# Config
# ---------------------------------------------------------------------------
-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 apply
+WILDPC_HOME="${HOME}/.wildpc"
+WILDPC_CONF="${WILDPC_HOME}/infra.conf"
+# Program data lives on a dedicated volume, decoupled from WILDPC_HOME — must match
+# wildpc_core.config._resolve_data_dir (default /data/wildpc, override WILDPC_DATA_DIR)
+# so the data dirs + container mounts created here line up with what wildpc 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).
-REPOS_DIR="${CASTLE_REPOS_DIR:-/data/repos}"
-CASTLE_ROOT="$(cd "$(dirname "$0")" && pwd)"
+DATA_DIR="${WILDPC_DATA_DIR:-/data/wildpc}"
+# Program source repos (default /data/repos, override WILDPC_REPOS_DIR).
+REPOS_DIR="${WILDPC_REPOS_DIR:-/data/repos}"
+WILDPC_ROOT="$(cd "$(dirname "$0")" && pwd)"
# Container defaults
MQTT_IMAGE="eclipse-mosquitto:2"
@@ -83,18 +83,18 @@ ask_yes_no() {
conf_get() {
local key="$1"
- if [ -f "$CASTLE_CONF" ]; then
- grep -m1 "^${key}=" "$CASTLE_CONF" 2>/dev/null | cut -d= -f2 || true
+ if [ -f "$WILDPC_CONF" ]; then
+ grep -m1 "^${key}=" "$WILDPC_CONF" 2>/dev/null | cut -d= -f2 || true
fi
}
conf_set() {
local key="$1" val="$2"
- mkdir -p "$(dirname "$CASTLE_CONF")"
- if [ -f "$CASTLE_CONF" ] && grep -q "^${key}=" "$CASTLE_CONF" 2>/dev/null; then
- sed -i "s/^${key}=.*/${key}=${val}/" "$CASTLE_CONF"
+ mkdir -p "$(dirname "$WILDPC_CONF")"
+ if [ -f "$WILDPC_CONF" ] && grep -q "^${key}=" "$WILDPC_CONF" 2>/dev/null; then
+ sed -i "s/^${key}=.*/${key}=${val}/" "$WILDPC_CONF"
else
- echo "${key}=${val}" >> "$CASTLE_CONF"
+ echo "${key}=${val}" >> "$WILDPC_CONF"
fi
}
@@ -165,7 +165,7 @@ ensure_caddy() {
| sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
sudo apt-get update -qq
sudo apt-get install -y -qq caddy >/dev/null
- # Disable the system-level caddy service — castle manages it via user systemd
+ # Disable the system-level caddy service — wildpc manages it via user systemd
sudo systemctl disable --now caddy 2>/dev/null || true
log_ok
}
@@ -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 apply` with no spec change. Idempotent and opt-in (--with-dns-plugin).
+# `wildpc apply` with no spec change. Idempotent and opt-in (--with-dns-plugin).
ensure_caddy_dns_plugin() {
local provider="${1:-cloudflare}"
local module
@@ -202,7 +202,7 @@ ensure_caddy_dns_plugin() {
|| log_fail "xcaddy install failed"
fi
- # events-exec: lets the gateway run `castle tls reconcile` on cert issuance/
+ # events-exec: lets the gateway run `wildpc tls reconcile` on cert issuance/
# renewal (the cert_obtained hook), so certs materialized onto raw-TCP services
# refresh automatically. See docs/tcp-exposure.md §5.
local events_module="github.com/mholt/caddy-events-exec"
@@ -217,12 +217,12 @@ ensure_caddy_dns_plugin() {
|| log_fail "built caddy is missing dns.providers.$provider"
/usr/local/bin/caddy list-modules 2>/dev/null | grep -q "events.handlers.exec" \
|| log_fail "built caddy is missing events.handlers.exec"
- log_info "Built /usr/local/bin/caddy — run 'castle apply' to use it."
+ log_info "Built /usr/local/bin/caddy — run 'wildpc apply' to use it."
log_ok
}
# ---------------------------------------------------------------------------
-# Castle CLI
+# Wild PC CLI
# ---------------------------------------------------------------------------
ensure_uv() {
@@ -238,20 +238,20 @@ ensure_uv() {
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
+# Install the `wildpc` CLI from this repo as an editable uv tool, so a fresh
+# clone becomes a working `wildpc` 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_step "Installing the wildpc CLI"
+ ( cd "$WILDPC_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'"
+ cmd_exists wildpc || log_info "NOTE: ensure ~/.local/bin is on your PATH to use 'wildpc'"
log_ok
}
-# Non-interactive/non-login shells (`ssh host cmd`, systemd units, `castle` dev verbs)
+# Non-interactive/non-login shells (`ssh host cmd`, systemd units, `wildpc` dev verbs)
# load ~/.zshenv but NOT ~/.zshrc → ~/.zsh.d/*.sh — so tools installed there (uv,
-# castle, pnpm, nvm node) don't resolve over a bare `ssh host 'castle …'`. Mirror just
+# wildpc, pnpm, nvm node) don't resolve over a bare `ssh host 'wildpc …'`. Mirror just
# the PATH entries into ~/.zshenv (interactive niceties stay in ~/.zsh.d). Idempotent.
ensure_shell_path() {
log_step "Wiring PATH for non-interactive shells"
@@ -265,16 +265,16 @@ ensure_shell_path() {
;;
esac
local zshenv="${HOME}/.zshenv"
- if [ -f "$zshenv" ] && grep -q "castle non-interactive PATH" "$zshenv"; then
+ if [ -f "$zshenv" ] && grep -q "wildpc non-interactive PATH" "$zshenv"; then
log_skip "already wired"
return
fi
cat >> "$zshenv" <<'ZSHENV'
-# >>> castle non-interactive PATH >>>
-# Non-interactive/non-login shells (ssh 'cmd', systemd, castle dev verbs) load
-# ~/.zshenv but not ~/.zshrc → ~/.zsh.d/*.sh. Mirror the PATH entries castle's
-# toolchain needs (uv/castle in ~/.local/bin, pnpm, nvm node) so they resolve there.
+# >>> wildpc non-interactive PATH >>>
+# Non-interactive/non-login shells (ssh 'cmd', systemd, wildpc dev verbs) load
+# ~/.zshenv but not ~/.zshrc → ~/.zsh.d/*.sh. Mirror the PATH entries wildpc's
+# toolchain needs (uv/wildpc in ~/.local/bin, pnpm, nvm node) so they resolve there.
for _d in "$HOME/.local/bin" "$HOME/.local/share/pnpm"; do
case ":$PATH:" in *":$_d:"*) ;; *) [ -d "$_d" ] && PATH="$_d:$PATH" ;; esac
done
@@ -283,7 +283,7 @@ for _nb in "$HOME"/.nvm/versions/node/*/bin(N); do
done
export PATH
unset _d _nb
-# <<< castle non-interactive PATH <<<
+# <<< wildpc non-interactive PATH <<<
ZSHENV
log_ok "~/.zshenv"
}
@@ -295,78 +295,78 @@ ZSHENV
create_directories() {
log_step "Creating directory structure"
- # ~/.castle tree — globals (castle.yaml) plus one file per program/deployment.
- mkdir -p "${CASTLE_HOME}/programs"
- mkdir -p "${CASTLE_HOME}/deployments"
- mkdir -p "${CASTLE_HOME}/artifacts/specs"
- mkdir -p "${CASTLE_HOME}/artifacts/content"
- mkdir -p "${CASTLE_HOME}/secrets" && chmod 700 "${CASTLE_HOME}/secrets"
+ # ~/.wildpc tree — globals (wildpc.yaml) plus one file per program/deployment.
+ mkdir -p "${WILDPC_HOME}/programs"
+ mkdir -p "${WILDPC_HOME}/deployments"
+ mkdir -p "${WILDPC_HOME}/artifacts/specs"
+ mkdir -p "${WILDPC_HOME}/artifacts/content"
+ mkdir -p "${WILDPC_HOME}/secrets" && chmod 700 "${WILDPC_HOME}/secrets"
- # Program data volume (default /data/castle) lives outside $HOME, so on a fresh
+ # Program data volume (default /data/wildpc) 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 apply) can write there.
+ # later container mounts (and wildpc 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}"
sudo chown "$(id -un):$(id -gn)" "${DATA_DIR}"
fi
- # Seed a minimal global castle.yaml — never clobber an existing one.
- if [[ -f "${CASTLE_HOME}/castle.yaml" ]]; then
+ # Seed a minimal global wildpc.yaml — never clobber an existing one.
+ if [[ -f "${WILDPC_HOME}/wildpc.yaml" ]]; then
log_ok
else
- printf 'gateway:\n port: 9000\n' > "${CASTLE_HOME}/castle.yaml"
- log_ok "seeded ~/.castle/castle.yaml"
+ printf 'gateway:\n port: 9000\n' > "${WILDPC_HOME}/wildpc.yaml"
+ log_ok "seeded ~/.wildpc/wildpc.yaml"
fi
- # Persist the chosen roots into castle.yaml so every later `castle` (CLI, in the
- # shell) and `castle-api` (service) invocation resolves the SAME dirs from the file
+ # Persist the chosen roots into wildpc.yaml so every later `wildpc` (CLI, in the
+ # shell) and `wildpc-api` (service) invocation resolves the SAME dirs from the file
# — not from a per-process env var that only one of them happens to have. Only when
# non-default, to keep the file minimal; idempotent (grep-guarded), like repo: below.
- if [ "${DATA_DIR}" != "/data/castle" ]; then
- grep -q "^data_dir:" "${CASTLE_HOME}/castle.yaml" 2>/dev/null \
- || printf 'data_dir: %s\n' "${DATA_DIR}" >> "${CASTLE_HOME}/castle.yaml"
+ if [ "${DATA_DIR}" != "/data/wildpc" ]; then
+ grep -q "^data_dir:" "${WILDPC_HOME}/wildpc.yaml" 2>/dev/null \
+ || printf 'data_dir: %s\n' "${DATA_DIR}" >> "${WILDPC_HOME}/wildpc.yaml"
fi
if [ "${REPOS_DIR}" != "/data/repos" ]; then
- grep -q "^repos_dir:" "${CASTLE_HOME}/castle.yaml" 2>/dev/null \
- || printf 'repos_dir: %s\n' "${REPOS_DIR}" >> "${CASTLE_HOME}/castle.yaml"
+ grep -q "^repos_dir:" "${WILDPC_HOME}/wildpc.yaml" 2>/dev/null \
+ || printf 'repos_dir: %s\n' "${REPOS_DIR}" >> "${WILDPC_HOME}/wildpc.yaml"
fi
}
# ---------------------------------------------------------------------------
-# Control plane (Castle's own gateway + API + dashboard)
+# Control plane (Wild PC'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 apply`
+# Register Wild PC's own control-plane programs/deployments from bootstrap/ so a
+# fresh registry is not empty. Without this, `wildpc 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.
seed_control_plane() {
- log_step "Registering Castle's control plane"
- local specs="${CASTLE_HOME}/artifacts/specs"
+ log_step "Registering Wild PC's control plane"
+ local specs="${WILDPC_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"
+ # The `repo:` field lets `source: repo: