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.
This commit is contained in:
2026-07-18 22:55:08 -07:00
parent 25d7a522cc
commit 05b28cb584
190 changed files with 2428 additions and 3337 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,6 @@ __pycache__
.pytest_cache
.claude
node_modules
castle.yaml
!bootstrap/**/castle.yaml
wildpc.yaml
!bootstrap/**/wildpc.yaml
.working

196
AGENTS.md
View File

@@ -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/<name>.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 <name> [--json]
castle program create <name> [--stack ...] [--description ...] # scaffold NEW code
castle program add <path|git-url> [--name ...] # adopt EXISTING repo
castle program clone [name] # provision repo: source
castle program delete <name> [--source] [-y]
castle program run <name> [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 <name> [--json]
wildpc program create <name> [--stack ...] [--description ...] # scaffold NEW code
wildpc program add <path|git-url> [--name ...] # adopt EXISTING repo
wildpc program clone [name] # provision repo: source
wildpc program delete <name> [--source] [-y]
wildpc program run <name> [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 <name> [--json]
castle service create <name> [--program P] [--port N] [--health ...] [--launcher ...]
castle service restart <name> # imperative bounce
castle service logs <name> [-f] [-n 50]
wildpc service list|info <name> [--json]
wildpc service create <name> [--program P] [--port N] [--health ...] [--launcher ...]
wildpc service restart <name> # imperative bounce
wildpc service logs <name> [-f] [-n 50]
# Jobs — scheduled tasks (manager: systemd + schedule). create takes --schedule
castle job create <name> [--program P] --schedule "0 2 * * *" [--launcher ...]
castle job <list|info|delete|restart|logs> ...
wildpc job create <name> [--program P] --schedule "0 2 * * *" [--launcher ...]
wildpc job <list|info|delete|restart|logs> ...
# Tools — CLIs on PATH (manager: path)
castle tool list [--json] # each tool's executable + description + install state
castle tool info <name> [--json]
wildpc tool list [--json] # each tool's executable + description + install state
wildpc tool info <name> [--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 <NAME> [VALUE] # VALUE omitted → hidden prompt / stdin
castle secret get <NAME> # print a value
castle secret rm <NAME> [-y] # delete
wildpc secret list # names in the active backend
wildpc secret set <NAME> [VALUE] # VALUE omitted → hidden prompt / stdin
wildpc secret get <NAME> # print a value
wildpc secret rm <NAME> [-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.<domain>
wildpc apply # served at my-frontend.<domain>
```
The gateway serves the build **in place** from `<source>/<root>` — 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/<name>.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.<domain>/` 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=/<domain>/<node-ip>` (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.<domain>/` 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/<name>
MY_SERVICE_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/<name>
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/<name>` (default `/data/castle/<name>`) |
| `${data_dir}` | `$WILDPC_DATA_DIR/<name>` (default `/data/wildpc/<name>`) |
| `${name}` | the deployment name |
| `${public_url}` | `https://<name>.<domain>` under acme, else `http://localhost:<port>` |
| `${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.<domain>:8200
mount: castle # KV-v2 mount
addr: https://wildpc-openbao.<domain>:8200
mount: wildpc # KV-v2 mount
token_secret: OPENBAO_ROOT_TOKEN # vault token, read from a file
node_prefix: nodes/<host> # 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/<node_prefix>/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 `<MISSING_SECRET:NAME>` 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
`<name>.<gateway.public_domain>` (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.<domain>: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.<domain>: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:<name>` → cli, core,
castle-api, app). Your programs live under `/data/repos/<name>/` with an absolute
Wild PC's own programs live in this repo (`source: repo:<name>` → cli, core,
wildpc-api, app). Your programs live under `/data/repos/<name>/` 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`.

View File

@@ -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.

150
README.md
View File

@@ -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/<name>.yaml`.
`~/.wildpc/programs/<name>.yaml`.
- **Deployments** — how a program is realized on this node. One file per deployment
under `~/.castle/deployments/<name>.yaml`, discriminated by its **manager**:
under `~/.wildpc/deployments/<name>.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 <this-repo> ~/castle && cd ~/castle
git clone <this-repo> ~/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:<name>` for castle's own programs
repo: /data/repos/wildpc # resolves `source: repo:<name>` 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 — `<name>.<gateway.domain>`
— 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.<domain>` and the API `castle-api.<domain>`; on a
served. The dashboard is `wildpc.<domain>` and the API `wildpc-api.<domain>`; 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/<name>/ # your program source (absolute source:)
/data/castle/<name>/ # per-deployment data volume
<repo>/ # castle itself: cli/ core/ castle-api/ app/ docs/
/data/wildpc/<name>/ # per-deployment data volume
<repo>/ # 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.<domain>` or `http://localhost:9000`)
The **dashboard** (`app/`, served at `wildpc.<domain>` 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.<domain>`) is the control plane:
**`wildpc-api`** (port 9020, proxied at `wildpc-api.<domain>`) 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)

View File

@@ -1,4 +1,4 @@
# The API base URL is derived at runtime (see src/services/api/client.ts):
# - dashboard at castle-app.<domain> -> castle-api.<domain> (cross-origin, CORS)
# - dashboard at wildpc-app.<domain> -> wildpc-api.<domain> (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).

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Castle</title>
<title>Wild PC</title>
</head>
<body>
<div id="root"></div>

View File

@@ -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 && (
<p className="text-xs text-[var(--muted)]">
Registering <span className="font-mono text-[var(--foreground)]">{target}</span>
{isGit && " (cloned later via castle clone)"}
{isGit && " (cloned later via wildpc clone)"}
</p>
)}

View File

@@ -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"
>
<TerminalIcon size={13} /> Shell
</button>

View File

@@ -240,7 +240,7 @@ function PaletteBody({ onClose }: { onClose: () => void }) {
)}
<button
onClick={(e) => (e.stopPropagation(), details(a))}
title="Castle details"
title="Wild PC details"
className="shrink-0 text-[var(--muted)] hover:text-[var(--card-foreground)]"
>
<Maximize2 size={12} />

View File

@@ -31,7 +31,7 @@ export function ConvergePanel() {
setMsg("Applied — the node is converged.")
qc.invalidateQueries()
} catch (e) {
// A self-apply restarts castle-api, dropping the connection — expected.
// A self-apply restarts wildpc-api, dropping the connection — expected.
if (e instanceof TypeError) {
setPlan(null)
setMsg("Applied — services are restarting.")

View File

@@ -4,7 +4,7 @@ import type { GatewayInfo } from "@/types"
import { useSaveGatewayConfig } from "@/services/api/hooks"
/** Editable gateway routing + public-exposure settings (domain / public_domain /
* tunnel / tls). Saves to castle.yaml; changes take effect on the next apply. */
* tunnel / tls). Saves to wildpc.yaml; changes take effect on the next apply. */
export function GatewaySettings({ gateway }: { gateway: GatewayInfo }) {
const { mutate: save, isPending, data: saved } = useSaveGatewayConfig()
const [editing, setEditing] = useState(false)
@@ -44,7 +44,7 @@ export function GatewaySettings({ gateway }: { gateway: GatewayInfo }) {
<button onClick={() => setEditing(false)} className="flex items-center gap-1 text-xs px-2.5 py-1 rounded bg-[var(--border)] text-[var(--muted)]">
<X size={12} /> Cancel
</button>
<span className="text-xs text-[var(--muted)]">Run <span className="font-mono">castle apply</span> (or the Apply button) to converge.</span>
<span className="text-xs text-[var(--muted)]">Run <span className="font-mono">wildpc apply</span> (or the Apply button) to converge.</span>
</div>
</div>
)

View File

@@ -4,12 +4,12 @@ import { useNodes } from "@/services/api/hooks"
import type { NodeSummary } from "@/types"
import { cn } from "@/lib/utils"
// Each machine's castle is served at its own origin (castle.<domain>), so hopping
// Each machine's wildpc is served at its own origin (wildpc.<domain>), so hopping
// between hosts is a cross-origin navigation. This shows which host you're driving
// (the is_local node) and lets you jump to a peer's dashboard, preserving the view.
function dashboardUrl(n: NodeSummary): string {
const base = n.gateway_domain
? `https://castle.${n.gateway_domain}`
? `https://wildpc.${n.gateway_domain}`
: `http://${n.hostname}:${n.gateway_port}`
return `${base}${window.location.pathname}`
}
@@ -58,7 +58,7 @@ export function HostSwitcher({ collapsed }: { collapsed: boolean }) {
{open && (
<div className="absolute left-2 z-50 mt-1 min-w-[190px] overflow-hidden rounded-md border border-[var(--border)] bg-[var(--card)] shadow-xl">
<div className="border-b border-[var(--border)] px-2.5 py-1 text-[10px] uppercase tracking-wide text-[var(--muted)]">
Castle hosts
Wild PC hosts
</div>
{list.map((n) => {
const isCur = n.is_local

View File

@@ -50,7 +50,7 @@ const NAV: (NavLeaf | NavGroup)[] = [
{ to: "/secrets", label: "Secrets", icon: KeyRound },
]
const COLLAPSE_KEY = "castle-nav-collapsed"
const COLLAPSE_KEY = "wildpc-nav-collapsed"
function NavLeafLink({
leaf,
@@ -162,7 +162,7 @@ function NavItems({ collapsed, onNavigate }: { collapsed: boolean; onNavigate?:
function Brand({ collapsed, onClick }: { collapsed?: boolean; onClick?: () => void }) {
return (
<Link to="/" onClick={onClick} className="font-bold text-lg truncate">
{collapsed ? "C" : "Castle"}
{collapsed ? "W" : "Wild PC"}
</Link>
)
}

View File

@@ -26,7 +26,7 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane
const navigate = useNavigate()
const qc = useQueryClient()
const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null)
// A deployment edit only persists to castle.yaml; the unit/process still need
// A deployment edit only persists to wildpc.yaml; the unit/process still need
// a deploy (regenerate) + restart to actually take effect.
const [pendingApply, setPendingApply] = useState(false)
const [applying, setApplying] = useState(false)
@@ -43,8 +43,8 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane
setMessage({
type: "ok",
text: isDeployment
? "Saved to castle.yaml — not live yet; apply to converge."
: "Saved to castle.yaml",
? "Saved to wildpc.yaml — not live yet; apply to converge."
: "Saved to wildpc.yaml",
})
if (isDeployment) setPendingApply(true)
onRefetch()
@@ -67,7 +67,7 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane
qc.invalidateQueries()
onRefetch()
} catch (e: unknown) {
// A self-apply of castle-api restarts it, killing the connection — expected.
// A self-apply of wildpc-api restarts it, killing the connection — expected.
if (e instanceof TypeError) {
setPendingApply(false)
setMessage({ type: "ok", text: "Applied — the service is restarting." })

View File

@@ -26,7 +26,7 @@ const KIND_INFO: Record<DeploymentKind, { label: string; hint: string }> = {
static: { label: "Static", hint: "Static site served by the gateway" },
}
/** Create a deployment in castle.yaml, then deploy (and start, for a service).
/** Create a deployment in wildpc.yaml, then deploy (and start, for a service).
* A pick-a-kind wizard: the chosen kind sets the manager and shows only its
* relevant fields. Reachable standalone or prefilled from a program page. */
export function CreateDeploymentForm({

View File

@@ -93,7 +93,7 @@ export function JobFields({ job, onSave, onDelete }: Props) {
width="w-48"
mono
placeholder="0 2 * * *"
hint="Cron expression — castle generates a systemd timer that runs the job on this schedule."
hint="Cron expression — wildpc generates a systemd timer that runs the job on this schedule."
/>
<Field label="Launch" hint="How the job runs on each tick, then exits: the launcher and its target (a console script, command/argv, image, node script, or compose file).">
<div className="flex items-center gap-2">
@@ -124,7 +124,7 @@ export function JobFields({ job, onSave, onDelete }: Props) {
onSave={handleSave}
onDelete={onDelete ? () => onDelete(job.id) : undefined}
deleteLabel="Remove job"
confirmMessage={`Remove job "${job.id}" from castle.yaml? Run a deploy afterward to tear down its timer.`}
confirmMessage={`Remove job "${job.id}" from wildpc.yaml? Run a deploy afterward to tear down its timer.`}
/>
</div>
)

View File

@@ -95,7 +95,7 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
onChange={setSource}
mono
placeholder="/data/repos/my-prog"
hint="The working copy on disk. Castle runs dev verbs and builds here. Absolute path, or repo:<name> for castle's own programs."
hint="The working copy on disk. Wild PC runs dev verbs and builds here. Absolute path, or repo:<name> for wildpc's own programs."
/>
<Field
label="Stack"
@@ -119,7 +119,7 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
onChange={setRepo}
mono
placeholder="https://github.com/me/x.git"
hint="Git URL so 'castle program clone' can fetch the source on a fresh machine. An existing working copy at Source takes precedence."
hint="Git URL so 'wildpc program clone' can fetch the source on a fresh machine. An existing working copy at Source takes precedence."
/>
<TextField
label="Ref"
@@ -134,7 +134,7 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
value={deps}
onChange={setDeps}
placeholder="pandoc, poppler-utils"
hint="OS packages the program needs. Listed for reference — castle does not install them."
hint="OS packages the program needs. Listed for reference — wildpc does not install them."
/>
<Field

View File

@@ -201,7 +201,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
width="w-32"
mono
placeholder="9001"
hint="The port the service listens on. Castle health-checks and proxies this port; map it to the program's own var with ${port} in Environment."
hint="The port the service listens on. Wild PC health-checks and proxies this port; map it to the program's own var with ${port} in Environment."
/>
<TextField
label="Health path"
@@ -210,7 +210,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
width="w-48"
mono
placeholder="/health"
hint="HTTP path castle polls to report up/down."
hint="HTTP path wildpc polls to report up/down."
/>
<Field
label="Reach"
@@ -263,7 +263,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
onSave={handleSave}
onDelete={onDelete ? () => onDelete(service.id) : undefined}
deleteLabel="Remove service"
confirmMessage={`Remove service "${service.id}" from castle.yaml? Run a deploy afterward to tear down its unit.`}
confirmMessage={`Remove service "${service.id}" from wildpc.yaml? Run a deploy afterward to tear down its unit.`}
/>
</div>
)

View File

@@ -15,7 +15,7 @@ interface Props {
/** Edit a tool's (path) deployment config. A path deployment has no launcher,
* port, or schedule — only a description and its `tool_schema` (the neutral
* tool-call definition handed to agents), plus its manager. It has no run env:
* a tool is a CLI on PATH, invoked from a shell with castle out of the loop, so
* a tool is a CLI on PATH, invoked from a shell with wildpc out of the loop, so
* `defaults.env` is never applied (deploy.py only wires env for systemd units). */
export function ToolFields({ tool, onSave, onDelete }: Props) {
const m = tool.manifest

View File

@@ -170,7 +170,7 @@ export function useEnvSecrets(initial: Record<string, string>) {
Use <code className="font-mono">${"{port}"}</code>,{" "}
<code className="font-mono">${"{data_dir}"}</code>,{" "}
<code className="font-mono">${"{name}"}</code>,{" "}
<code className="font-mono">${"{public_url}"}</code> for castle's computed values,
<code className="font-mono">${"{public_url}"}</code> for wildpc's computed values,
and <code className="font-mono">${"{secret:NAME}"}</code> for secrets.
</p>
{Object.entries(env).map(([key, val]) => (

View File

@@ -82,7 +82,7 @@ export function detailPath(name: string, kind: string): string {
/**
* Full URL for a service exposed at <subdomain>.<gateway.domain>. The domain is
* derived from the dashboard's own host (it is served at castle.<domain>), so
* derived from the dashboard's own host (it is served at wildpc.<domain>), so
* this returns null when the dashboard is on a bare host (off mode, no subdomains).
*/
export function subdomainUrl(subdomain: string): string | null {

View File

@@ -78,7 +78,7 @@ export function Overview() {
return (
<div className="max-w-6xl mx-auto px-6 py-8">
<PageHeader title="Castle" subtitle="Personal software platform" />
<PageHeader title="Wild PC" subtitle="Personal software platform" />
{nodes && <NodeBar nodes={nodes} />}

View File

@@ -93,7 +93,7 @@ export function ProgramDetailPage() {
Program Info
</h2>
<p className="text-xs text-[var(--muted)] mb-4">
Where the source lives and how castle works with it.
Where the source lives and how wildpc works with it.
</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
{deployment.source && (

View File

@@ -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) {

View File

@@ -84,7 +84,7 @@ const KIND_COLOR: Record<string, string> = {
// 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<string, string> = {
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<string, { x: number; y: number }>
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({
<ExternalLink size={13} />
</a>
)}
<button onClick={() => onOpen(info.name, info.kind, info.node)} title="Castle details" className="text-[var(--muted)] hover:text-[var(--card-foreground)]">
<button onClick={() => onOpen(info.name, info.kind, info.node)} title="Wild PC details" className="text-[var(--muted)] hover:text-[var(--card-foreground)]">
<Maximize2 size={12} />
</button>
<button onClick={onClose} title="Close" className="text-[var(--muted)] hover:text-[var(--card-foreground)]">
@@ -1593,7 +1593,7 @@ function AddExternalModal({
>
<h3 className="mb-1 font-semibold">Add external resource</h3>
<p className="mb-3 text-xs text-[var(--muted)]">
An endpoint castle doesn&apos;t run (a SaaS API, a remote service). Draw a consumes edge to
An endpoint wildpc doesn&apos;t run (a SaaS API, a remote service). Draw a consumes edge to
it from any deployment.
</p>
<label className="mb-1 block text-xs text-[var(--muted)]">Name</label>

View File

@@ -1,6 +1,6 @@
// Resolve the castle-api base URL. The gateway serves each service at its own
// subdomain (<name>.<domain>), so when the dashboard runs at castle.<domain>
// the API lives at castle-api.<domain> — a cross-origin call (castle-api allows
// Resolve the wildpc-api base URL. The gateway serves each service at its own
// subdomain (<name>.<domain>), so when the dashboard runs at wildpc.<domain>
// the API lives at wildpc-api.<domain> — 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.<domain>) and the same-origin "/api" base.
// base (https://wildpc-api.<domain>) and the same-origin "/api" base.
wsUrl(path: string): string {
const absolute = this.baseUrl.startsWith("http")
? `${this.baseUrl}${path}`

View File

@@ -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<string, string> = {
}
// 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"],

View File

@@ -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.<domain>
gateway_domain: string | null // acme domain → dashboard at wildpc.<domain>
deployed_count: number
service_count: number
is_local: boolean

View File

@@ -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: {

View File

@@ -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}

View File

@@ -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

View File

@@ -1,3 +0,0 @@
program: castle
description: Castle web app
manager: caddy

View File

@@ -0,0 +1,3 @@
program: wildpc
description: Wild PC web app
manager: caddy

View File

@@ -1,3 +0,0 @@
description: Castle API
source: repo:castle-api
stack: python-fastapi

View File

@@ -0,0 +1,3 @@
description: Wild PC API
source: repo:wildpc-api
stack: python-fastapi

View File

@@ -1,4 +1,4 @@
description: Castle web app
description: Wild PC web app
source: repo:app
stack: react-vite
build:

View File

@@ -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)

487
castle-api/uv.lock generated
View File

@@ -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" },
]

View File

@@ -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 = [

View File

@@ -1,3 +0,0 @@
"""Castle CLI - manage projects, services, and infrastructure."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,3 @@
"""Wild PC CLI - manage projects, services, and infrastructure."""
__version__ = "0.1.0"

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 <name>.<gateway.domain>
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}.<gateway.domain>")
print(f" wildpc program build {name} # apply migrations to the substrate")
print(f" wildpc apply # serve at {name}.<gateway.domain>")
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}.<gateway.domain>")
print(f" wildpc program build {name} # hugo --gc --minify -> public/")
print(f" wildpc apply {name} # serve at {name}.<gateway.domain>")
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

View File

@@ -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:

View File

@@ -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}.<gateway.domain>")
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

View File

@@ -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}")

View File

@@ -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:<name> cannot resolve castle's own programs",
hint="add 'repo: <path-to-castle-checkout>' to ~/.castle/castle.yaml",
"repo: not set in wildpc.yaml",
detail="source: repo:<name> cannot resolve wildpc's own programs",
hint="add 'repo: <path-to-wildpc-checkout>' 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: <your-zone>' under gateway: in castle.yaml",
hint="add 'domain: <your-zone>' 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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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)

View File

@@ -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

View File

@@ -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).

View File

@@ -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
``<MISSING_SECRET:NAME>`` 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()

View File

@@ -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 <name>` — the imperative bounce (only verb left).
"""`wildpc service restart <name>` — 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 <name>` — bounce the job's timer."""
"""`wildpc job restart <name>` — 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():

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
<name>.<gateway.domain> 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>.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__.<domain>`.
- `wildpc apply` served at `__NAME__.<domain>`.
## 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 <name>.<gateway.domain>. 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__.<gateway.domain>
wildpc program build __NAME__ # hugo --gc --minify -> public/
wildpc apply __NAME__ # serve at __NAME__.<gateway.domain>
## Themes

View File

@@ -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/<name>.yaml, and deployments/<kind>/<name>.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()

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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())]

View File

@@ -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))

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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))

View File

@@ -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

425
cli/uv.lock generated
View File

@@ -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" },
]

View File

@@ -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"]

View File

@@ -1,3 +0,0 @@
"""Castle core library - manifest models, configuration, and generators."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,3 @@
"""Wild PC core library - manifest models, configuration, and generators."""
__version__ = "0.1.0"

View File

@@ -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():

View File

@@ -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."""

View File

@@ -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://<name>.<domain>`` 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

View File

@@ -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-<service>` is stable and collision-free.
cmd = [runtime, "run", "--rm", f"--name=castle-{name}"]
# not the image name — so `wildpc-<service>` 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)

View File

@@ -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,

View File

@@ -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 ``<ref>.<domain>``."""
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 :<port> 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 `:<port>` site just redirects to the dashboard subdomain (the "browse to
the box by IP" entry).
- **off / no domain** no subdomains available, so the `:<port>` 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 += ["}", ""]

View File

@@ -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 == `<tunnel_id>.cfargotunnel.com`)
wildpc creates missing CNAMEs (proxied the tunnel; Cloudflare flattens apex
CNAMEs) and deletes wildpc-managed ones (content == `<tunnel_id>.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.

View File

@@ -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-<name>.service` vs
`castle-<name>-job.{service,timer}`); everything else is `castle-<name>`."""
service and a job can share a name (`wildpc-<name>.service` vs
`wildpc-<name>-job.{service,timer}`); everything else is `wildpc-<name>`."""
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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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-<name>``
project_name: str | None = None # ``-p``; defaults to ``wildpc-<name>``
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 ``<name>.<domain>``) 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 ``<name>.<domain>:<port>`` 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 ``<name>.<domain>``.
"""
@@ -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: <deployment>` (+ 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

View File

@@ -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 <subdomain>.<public_domain>.
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

View File

@@ -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,

View File

@@ -1,18 +1,18 @@
"""Pluggable secret backends for ``${secret:NAME}`` resolution.
Default is the **file** backend (``~/.castle/secrets/<name>``) identical to the
Default is the **file** backend (``~/.wildpc/secrets/<name>``) 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)

View File

@@ -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)

View File

@@ -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**
(`<name>.<gateway.domain>`), 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 ()

View File

@@ -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 ``<name>.<domain>``) 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 ``*.<domain>`` from Caddy's store, or None.
Caddy stores it at ``certificates/<acme-dir>/wildcard_.<domain>/``. 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 <name>`` is converging). Left None every deployment. Scoping
``wildpc apply <name>`` 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 ``*.<domain>`` (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():

View File

@@ -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:

Some files were not shown because too many files have changed in this diff Show More