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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,6 @@ __pycache__
|
||||
.pytest_cache
|
||||
.claude
|
||||
node_modules
|
||||
castle.yaml
|
||||
!bootstrap/**/castle.yaml
|
||||
wildpc.yaml
|
||||
!bootstrap/**/wildpc.yaml
|
||||
.working
|
||||
|
||||
196
AGENTS.md
196
AGENTS.md
@@ -1,7 +1,7 @@
|
||||
# AGENTS.md — Castle
|
||||
# AGENTS.md — Wild PC
|
||||
|
||||
You are working in **Castle**, a personal software platform. Castle is a
|
||||
monorepo of independent programs managed by the `castle` CLI. From this directory
|
||||
You are working in **Wild PC**, a personal software platform. Wild PC is a
|
||||
monorepo of independent programs managed by the `wildpc` CLI. From this directory
|
||||
you can **manage all the software on this box from source** — create programs,
|
||||
deploy them as services, jobs, tools, or static frontends, route them through the
|
||||
gateway, expose them over TLS or a public tunnel, and coordinate across nodes.
|
||||
@@ -13,7 +13,7 @@ section links to a doc under `docs/`. Read those before non-trivial changes.
|
||||
|
||||
## 1. Mental model — two layers
|
||||
|
||||
Castle splits every piece of software into **what it is** and **how it runs here**:
|
||||
Wild PC splits every piece of software into **what it is** and **how it runs here**:
|
||||
|
||||
- **`programs/<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`.
|
||||
|
||||
@@ -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
150
README.md
@@ -1,8 +1,8 @@
|
||||
# Castle
|
||||
# Wild PC
|
||||
|
||||
> Standing to author, run, govern, and maintain your own software.
|
||||
|
||||
A personal software platform. Castle manages independent services, tools, and
|
||||
A personal software platform. Wild PC manages independent services, tools, and
|
||||
frontends — and launches your coding agents — from a single CLI, with a unified
|
||||
gateway, systemd integration, and a web dashboard.
|
||||
|
||||
@@ -18,27 +18,27 @@ and configurations of existing tools. Many of these programs do not need a publi
|
||||
release process, an app store listing, or a conventional distribution channel. They
|
||||
need a reliable place to run.
|
||||
|
||||
Castle provides that place.
|
||||
Wild PC provides that place.
|
||||
|
||||
Castle gives simple applications a consistent local environment for development,
|
||||
Wild PC gives simple applications a consistent local environment for development,
|
||||
registration, deployment, discovery, and operation. Programs remain independent, but
|
||||
Castle provides the surrounding structure: process management, routing, metadata,
|
||||
Wild PC provides the surrounding structure: process management, routing, metadata,
|
||||
service lifecycle, logs, and a common interface for running and inspecting the
|
||||
software in your domain.
|
||||
|
||||
In this sense, Castle is a **personal software estate**: a practical way to organize
|
||||
In this sense, Wild PC is a **personal software estate**: a practical way to organize
|
||||
the software you create and run yourself, without requiring every tool to become a
|
||||
fully packaged product.
|
||||
|
||||
## How it works
|
||||
|
||||
Castle separates *what software exists* from *how it runs*:
|
||||
Wild PC separates *what software exists* from *how it runs*:
|
||||
|
||||
- **Programs** — the catalog. A program is a source repo Castle knows how to work
|
||||
- **Programs** — the catalog. A program is a source repo Wild PC knows how to work
|
||||
with (dev verbs, build) and where it lives. One file per program under
|
||||
`~/.castle/programs/<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)
|
||||
|
||||
2
app/.env
2
app/.env
@@ -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).
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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." })
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) => (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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't run (a SaaS API, a remote service). Draw a consumes edge to
|
||||
An endpoint wildpc doesn't run (a SaaS API, a remote service). Draw a consumes edge to
|
||||
it from any deployment.
|
||||
</p>
|
||||
<label className="mb-1 block text-xs text-[var(--muted)]">Name</label>
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
program: castle
|
||||
description: Castle web app
|
||||
manager: caddy
|
||||
3
bootstrap/deployments/statics/wildpc.yaml
Normal file
3
bootstrap/deployments/statics/wildpc.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
program: wildpc
|
||||
description: Wild PC web app
|
||||
manager: caddy
|
||||
@@ -1,3 +0,0 @@
|
||||
description: Castle API
|
||||
source: repo:castle-api
|
||||
stack: python-fastapi
|
||||
3
bootstrap/programs/wildpc-api.yaml
Normal file
3
bootstrap/programs/wildpc-api.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: Wild PC API
|
||||
source: repo:wildpc-api
|
||||
stack: python-fastapi
|
||||
@@ -1,4 +1,4 @@
|
||||
description: Castle web app
|
||||
description: Wild PC web app
|
||||
source: repo:app
|
||||
stack: react-vite
|
||||
build:
|
||||
@@ -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
487
castle-api/uv.lock
generated
@@ -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" },
|
||||
]
|
||||
@@ -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 = [
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Castle CLI - manage projects, services, and infrastructure."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
3
cli/src/wildpc_cli/__init__.py
Normal file
3
cli/src/wildpc_cli/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Wild PC CLI - manage projects, services, and infrastructure."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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()
|
||||
@@ -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():
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
425
cli/uv.lock
generated
@@ -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" },
|
||||
]
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Castle core library - manifest models, configuration, and generators."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
3
core/src/wildpc_core/__init__.py
Normal file
3
core/src/wildpc_core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Wild PC core library - manifest models, configuration, and generators."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -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():
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
@@ -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 += ["}", ""]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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():
|
||||
@@ -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
Reference in New Issue
Block a user