Rename Castle -> Wild PC across the repo

Repo-side rename only (Phases 1-3 of the migration plan); the live box
(~/.castle, systemd units, /data/castle, domains) is a separate cutover.

- Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api),
  dist names, entry point `wildpc = wildpc_cli.main:main`.
- Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*.
- Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml ->
  wildpc.yaml, /data/castle -> /data/wildpc.
- Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc.
- Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend.
- Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated;
  redundant nested uv.lock files dropped (workspace root lock is authoritative).

Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds.
Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
This commit is contained in:
2026-07-18 22:55:08 -07:00
parent 25d7a522cc
commit 05b28cb584
190 changed files with 2428 additions and 3337 deletions

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
"""castle program add — adopt an existing repo as a program (no scaffolding).
"""wildpc program add — adopt an existing repo as a program (no scaffolding).
`castle program create` makes new code from a stack. `castle program add` adopts code that
`wildpc program create` makes new code from a stack. `wildpc program add` adopts code that
already exists a local path, or a git URL to clone. It detects sensible dev
verb commands so a non-castle project becomes usable without writing them by hand.
verb commands so a non-wildpc project becomes usable without writing them by hand.
The adopt logic itself lives in ``castle_core.adopt`` so the CLI and the API
The adopt logic itself lives in ``wildpc_core.adopt`` so the CLI and the API
(`POST /programs/adopt`, the dashboard's "Add program") behave identically.
"""
@@ -12,9 +12,9 @@ from __future__ import annotations
import argparse
from castle_core.adopt import AdoptError, build_adopted_program
from wildpc_core.adopt import AdoptError, build_adopted_program
from castle_cli.config import load_config, save_config
from wildpc_cli.config import load_config, save_config
def run_add(args: argparse.Namespace) -> int:
@@ -35,11 +35,11 @@ def run_add(args: argparse.Namespace) -> int:
print(f"Adopted '{adopted.name}' as a program.")
print(f" source: {adopted.source}")
if adopted.repo:
print(f" repo: {adopted.repo} (run 'castle clone {adopted.name}' to fetch it)")
print(f" repo: {adopted.repo} (run 'wildpc clone {adopted.name}' to fetch it)")
if adopted.stack:
print(f" stack: {adopted.stack} (verbs inherited from stack defaults)")
elif adopted.commands:
print(f" commands detected: {', '.join(adopted.commands)}")
else:
print(" no stack/commands detected — declare verbs in castle.yaml as needed")
print(" no stack/commands detected — declare verbs in wildpc.yaml as needed")
return 0

View File

@@ -1,4 +1,4 @@
"""castle apply — converge the running system to match config.
"""wildpc apply — converge the running system to match config.
The one workhorse verb: renders systemd units + the Caddyfile + tunnel config,
then reconciles the runtime (activate what's enabled and down, restart what
@@ -28,7 +28,7 @@ def _line(verb_color: str, verb: str, names: list[str]) -> None:
def run_apply(args: argparse.Namespace) -> int:
from castle_core.deploy import apply
from wildpc_core.deploy import apply
target = getattr(args, "name", None)
plan = getattr(args, "plan", False)

View File

@@ -1,4 +1,4 @@
"""castle clone — clone source for programs that declare a `repo:` URL.
"""wildpc clone — clone source for programs that declare a `repo:` URL.
Used to provision a fresh machine: every program with a `repo:` and a missing
local `source:` gets cloned. A program whose source already exists is skipped.
@@ -10,7 +10,7 @@ import argparse
import subprocess
from pathlib import Path
from castle_cli.config import load_config
from wildpc_cli.config import load_config
def _clone_one(name: str, repo: str, source: str | None, ref: str | None, repos_dir: Path) -> bool:

View File

@@ -1,12 +1,12 @@
"""castle program create — scaffold a new program from templates."""
"""wildpc program create — scaffold a new program from templates."""
from __future__ import annotations
import argparse
import subprocess
from castle_cli.config import load_config, save_config
from castle_cli.manifest import (
from wildpc_cli.config import load_config, save_config
from wildpc_cli.manifest import (
BuildSpec,
CaddyDeployment,
DefaultsSpec,
@@ -21,7 +21,7 @@ from castle_cli.manifest import (
SystemdDeployment,
SystemdSpec,
)
from castle_cli.templates.scaffold import scaffold_project
from wildpc_cli.templates.scaffold import scaffold_project
# Stack determines the default deployment kind + scaffold template.
STACK_DEFAULTS: dict[str, str] = {
@@ -79,7 +79,7 @@ def run_create(args: argparse.Namespace) -> int:
kind = STACK_DEFAULTS.get(stack) if stack else None
if name in config.programs or config.deployments_named(name):
print(f"Error: '{name}' already exists in castle.yaml")
print(f"Error: '{name}' already exists in wildpc.yaml")
return 1
config.repos_dir.mkdir(parents=True, exist_ok=True)
@@ -94,7 +94,7 @@ def run_create(args: argparse.Namespace) -> int:
port = next_available_port(config)
package_name = name.replace("-", "_")
description = args.description or (f"A castle {stack} program" if stack else f"{name}")
description = args.description or (f"A wildpc {stack} program" if stack else f"{name}")
if stack:
scaffold_project(
@@ -168,7 +168,7 @@ def run_create(args: argparse.Namespace) -> int:
requires=seeded_requires,
proxy=True, # expose at <name>.<gateway.domain>
manage=ManageSpec(systemd=SystemdSpec()),
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map wildpc's
# computed port/data dir to those vars (explicit, no hidden injection).
defaults=DefaultsSpec(
env={f"{prefix}_PORT": "${port}", f"{prefix}_DATA_DIR": "${data_dir}"}
@@ -181,24 +181,24 @@ def run_create(args: argparse.Namespace) -> int:
print(f"Created {label} '{name}' at {project_dir}")
if port:
print(f" Port: {port}")
print(" Registered in castle.yaml")
print(" Registered in wildpc.yaml")
print("\nNext steps:")
print(f" cd {project_dir}")
if stack == "supabase":
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
print(f" castle program build {name} # apply migrations to the substrate")
print(f" castle apply # serve at {name}.<gateway.domain>")
print(f" wildpc program build {name} # apply migrations to the substrate")
print(f" wildpc apply # serve at {name}.<gateway.domain>")
elif stack == "hugo":
print(" # edit content/, layouts/ (or add a theme under themes/)")
print(f" castle program build {name} # hugo --gc --minify -> public/")
print(f" castle apply {name} # serve at {name}.<gateway.domain>")
print(f" wildpc program build {name} # hugo --gc --minify -> public/")
print(f" wildpc apply {name} # serve at {name}.<gateway.domain>")
elif stack:
print(" uv sync")
if kind == "service":
print(f" uv run {name} # starts on port {port}")
print(f" castle apply {name}")
print(f" castle test {name}")
print(f" wildpc apply {name}")
print(f" wildpc test {name}")
else:
print(" # add code, then declare commands: in castle.yaml")
print(" # add code, then declare commands: in wildpc.yaml")
return 0

View File

@@ -1,4 +1,4 @@
"""castle delete — remove a program/deployment from the registry AND tear it down.
"""wildpc delete — remove a program/deployment from the registry AND tear it down.
Deleting cascades: a program's referencing deployments are taken offline (stop +
disable a service/job, uninstall a tool from PATH, drop a static route) before the
@@ -18,7 +18,7 @@ import argparse
import shutil
from pathlib import Path
from castle_cli.config import load_config, save_config
from wildpc_cli.config import load_config, save_config
def run_delete(args: argparse.Namespace) -> int:
@@ -35,7 +35,7 @@ def run_delete(args: argparse.Namespace) -> int:
in_deployment = bool(named) and resource in _DEPLOY_RESOURCES
if not (in_programs or in_deployment):
where = f" {resource}" if resource else ""
print(f"Error: no{where} '{name}' in castle.yaml")
print(f"Error: no{where} '{name}' in wildpc.yaml")
return 1
where = [
@@ -60,7 +60,7 @@ def run_delete(args: argparse.Namespace) -> int:
# entry itself — its stack may own persistent data (a DB schema) that survives
# a code delete unless --purge-data drops it via the stack's `teardown`.
public_hosts = _public_hosts(config, deployments_to_remove)
from castle_core.stacks import get_handler
from wildpc_core.stacks import get_handler
program_spec = config.programs.get(name) if in_programs else None
stack = program_spec.stack if program_spec else None
@@ -73,7 +73,7 @@ def run_delete(args: argparse.Namespace) -> int:
source_dir = Path(config.programs[name].source)
purge_data = getattr(args, "purge_data", False)
print(f"Will remove '{name}' from castle.yaml ({', '.join(where)}).")
print(f"Will remove '{name}' from wildpc.yaml ({', '.join(where)}).")
if deployments_to_remove:
print(
"Will tear down deployment(s): "
@@ -100,7 +100,7 @@ def run_delete(args: argparse.Namespace) -> int:
if deployments_to_remove:
import asyncio
from castle_core.lifecycle import deactivate
from wildpc_core.lifecycle import deactivate
for kind, d in deployments_to_remove:
try:
@@ -114,18 +114,18 @@ def run_delete(args: argparse.Namespace) -> int:
if in_programs:
del config.programs[name]
save_config(config)
print(f"Removed '{name}' from castle.yaml ({', '.join(where)}).")
print(f"Removed '{name}' from wildpc.yaml ({', '.join(where)}).")
# Converge the runtime: prune orphan units, regenerate the Caddyfile (dropping
# the static route), reload the gateway + tunnel.
if deployments_to_remove:
from castle_core.deploy import deploy
from wildpc_core.deploy import deploy
try:
deploy()
print("Reconciled runtime (castle apply).")
print("Reconciled runtime (wildpc apply).")
except Exception as e:
print(f"warning: reconcile failed — run 'castle apply': {e}")
print(f"warning: reconcile failed — run 'wildpc apply': {e}")
# Optional: delete the source directory.
if args.source and source_dir:

View File

@@ -1,6 +1,6 @@
"""castle service create / castle job create — declare a deployment.
"""wildpc service create / wildpc job create — declare a deployment.
A service or job can run anything (a castle program or not). `--program`
A service or job can run anything (a wildpc program or not). `--program`
records a convenience reference for description fallthrough; the run target is
the console script (python) or argv (command) to execute.
"""
@@ -9,8 +9,8 @@ from __future__ import annotations
import argparse
from castle_cli.config import load_config, save_config
from castle_cli.manifest import (
from wildpc_cli.config import load_config, save_config
from wildpc_cli.manifest import (
DefaultsSpec,
ExposeSpec,
HttpExposeSpec,
@@ -49,7 +49,7 @@ def _check_new(config: object, name: str, label: str) -> str | None:
def run_service_create(args: argparse.Namespace) -> int:
"""Create a service entry in castle.yaml."""
"""Create a service entry in wildpc.yaml."""
config = load_config()
name = args.name
if err := _check_new(config, name, "service"):
@@ -89,12 +89,12 @@ def run_service_create(args: argparse.Namespace) -> int:
print(f" port: {args.port}")
if reach != Reach.OFF:
print(f" subdomain: {name}.<gateway.domain>")
print(f"\nNext: castle apply {name}")
print(f"\nNext: wildpc apply {name}")
return 0
def run_job_create(args: argparse.Namespace) -> int:
"""Create a job entry in castle.yaml."""
"""Create a job entry in wildpc.yaml."""
config = load_config()
name = args.name
if err := _check_new(config, name, "job"):
@@ -119,5 +119,5 @@ def run_job_create(args: argparse.Namespace) -> int:
print(f"Created job '{name}'.")
print(f" runs: {args.launcher} ({args.run or args.program or name})")
print(f" schedule: {args.schedule}")
print(f"\nNext: castle apply {name}")
print(f"\nNext: wildpc apply {name}")
return 0

View File

@@ -1,4 +1,4 @@
"""castle build/test/lint/type-check/check/run/install/uninstall — dev verbs.
"""wildpc build/test/lint/type-check/check/run/install/uninstall — dev verbs.
Verbs resolve per-program: a declared command (manifest `commands:` / `build:`)
overrides the stack default, falling back to the stack handler, else unavailable.
@@ -9,12 +9,12 @@ from __future__ import annotations
import argparse
import asyncio
from castle_core.stacks import is_available, run_action
from wildpc_core.stacks import is_available, run_action
from castle_cli.config import CastleConfig, load_config
from wildpc_cli.config import WildpcConfig, load_config
def _run_verb(config: CastleConfig, project_name: str, verb: str) -> bool:
def _run_verb(config: WildpcConfig, project_name: str, verb: str) -> bool:
"""Run a verb for a single program. Returns True on success."""
if project_name not in config.programs:
print(f"Unknown program: {project_name}")
@@ -39,7 +39,7 @@ def _run_verb(config: CastleConfig, project_name: str, verb: str) -> bool:
return result.status == "ok"
def _run_verb_all(config: CastleConfig, verb: str) -> bool:
def _run_verb_all(config: WildpcConfig, verb: str) -> bool:
"""Run a verb across every program that supports it. Returns True if all pass."""
all_passed = True
ran_any = False
@@ -89,10 +89,10 @@ def run_check(args: argparse.Namespace) -> int:
return run_verb(args, "check")
def _lifecycle(config: CastleConfig, name: str, deactivate: bool) -> bool:
def _lifecycle(config: WildpcConfig, name: str, deactivate: bool) -> bool:
"""Activate or deactivate a single program. Returns True on success."""
from castle_core.lifecycle import activate
from castle_core.lifecycle import deactivate as do_deactivate
from wildpc_core.lifecycle import activate
from wildpc_core.lifecycle import deactivate as do_deactivate
if name not in config.programs and name not in config.services and name not in config.jobs:
print(f"Unknown program: {name}")

View File

@@ -1,4 +1,4 @@
"""castle doctor — diagnose whether this node is set up and running.
"""wildpc doctor — diagnose whether this node is set up and running.
Read-only. It answers the question the runtime status view can't: *is this node
correctly configured, and if not, what's the exact next command?* Runs a series
@@ -6,7 +6,7 @@ of checks grouped into Environment, Configuration, Runtime, and TLS & exposure;
each check reports ok / warn / fail with a one-line hint when action is needed.
Exit code: 0 when nothing FAILed (warnings are allowed), 1 otherwise so it
doubles as a scriptable smoke test after `./install.sh` or `castle apply`.
doubles as a scriptable smoke test after `./install.sh` or `wildpc apply`.
"""
from __future__ import annotations
@@ -26,9 +26,9 @@ _ICON = {
FAIL: "\033[31m✗\033[0m",
}
_GATEWAY = "castle-gateway"
_API = "castle-api"
_DASHBOARD = "castle"
_GATEWAY = "wildpc-gateway"
_API = "wildpc-api"
_DASHBOARD = "wildpc"
@dataclass
@@ -62,13 +62,13 @@ def _port_open(port: int, host: str = "127.0.0.1") -> bool:
def _check_environment() -> list[Check]:
checks: list[Check] = []
if shutil.which("castle"):
checks.append(Check(OK, "castle CLI on PATH"))
if shutil.which("wildpc"):
checks.append(Check(OK, "wildpc CLI on PATH"))
else:
checks.append(
Check(
WARN,
"castle CLI not on PATH",
"wildpc CLI not on PATH",
hint="ensure ~/.local/bin is on PATH (uv tool install target)",
)
)
@@ -119,7 +119,7 @@ def _check_configuration(config) -> list[Check]:
checks.append(
Check(
OK,
"castle.yaml loaded",
"wildpc.yaml loaded",
detail=f"gateway :{gw.port}, tls={tls}"
+ (f", domain={gw.domain}" if gw.domain else ""),
)
@@ -131,9 +131,9 @@ def _check_configuration(config) -> list[Check]:
checks.append(
Check(
FAIL,
"repo: not set in castle.yaml",
detail="source: repo:<name> cannot resolve castle's own programs",
hint="add 'repo: <path-to-castle-checkout>' to ~/.castle/castle.yaml",
"repo: not set in wildpc.yaml",
detail="source: repo:<name> cannot resolve wildpc's own programs",
hint="add 'repo: <path-to-wildpc-checkout>' to ~/.wildpc/wildpc.yaml",
)
)
@@ -148,22 +148,22 @@ def _check_configuration(config) -> list[Check]:
FAIL,
"data dir missing or not writable",
detail=str(ddir),
hint=f"set data_dir: in ~/.castle/castle.yaml, or: "
hint=f"set data_dir: in ~/.wildpc/wildpc.yaml, or: "
f"sudo mkdir -p {ddir} && sudo chown $(id -un) {ddir}",
)
)
# Drift guard: castle.yaml is the single source of truth for the roots. An env var
# Drift guard: wildpc.yaml is the single source of truth for the roots. An env var
# override is per-process, so it's the one way the CLI and the api service can still
# diverge (env set in your shell, absent in the service unit — the original bug).
for var in ("CASTLE_DATA_DIR", "CASTLE_REPOS_DIR"):
for var in ("WILDPC_DATA_DIR", "WILDPC_REPOS_DIR"):
if var in os.environ:
checks.append(
Check(
WARN,
f"{var} overrides castle.yaml",
f"{var} overrides wildpc.yaml",
detail=f"{var}={os.environ[var]}",
hint=f"set data_dir:/repos_dir: in castle.yaml and unset {var}, so "
hint=f"set data_dir:/repos_dir: in wildpc.yaml and unset {var}, so "
"every process (CLI and api) resolves the same roots",
)
)
@@ -186,7 +186,7 @@ def _check_configuration(config) -> list[Check]:
def _check_dashboard_built(config) -> Check:
from castle_core.lifecycle import _static_built
from wildpc_core.lifecycle import _static_built
if not config.deployments_named(_DASHBOARD):
return Check(WARN, "dashboard not registered", detail="skipping build check")
@@ -196,7 +196,7 @@ def _check_dashboard_built(config) -> Check:
WARN,
"dashboard not built",
detail="gateway has no UI to serve at /",
hint=f"castle program build {_DASHBOARD}",
hint=f"wildpc program build {_DASHBOARD}",
)
@@ -212,8 +212,8 @@ def _deployment_port(config, name: str) -> int | None:
def _check_runtime(config) -> list[Check]:
from castle_core.config import SPECS_DIR
from castle_core.lifecycle import is_active
from wildpc_core.config import SPECS_DIR
from wildpc_core.lifecycle import is_active
checks: list[Check] = []
@@ -228,7 +228,7 @@ def _check_runtime(config) -> list[Check]:
WARN,
"gateway active but not listening",
detail=f":{gw_port} refused",
hint="castle gateway reload; check 'castle service logs castle-gateway'",
hint="wildpc gateway reload; check 'wildpc service logs wildpc-gateway'",
)
)
else:
@@ -236,7 +236,7 @@ def _check_runtime(config) -> list[Check]:
Check(
FAIL,
"gateway not running",
hint="castle apply",
hint="wildpc apply",
)
)
@@ -247,16 +247,16 @@ def _check_runtime(config) -> list[Check]:
checks.append(
Check(
WARN,
"castle-api active but not listening",
"wildpc-api active but not listening",
detail=f":{api_port} refused",
hint="castle service logs castle-api",
hint="wildpc service logs wildpc-api",
)
)
else:
detail = f"listening on :{api_port}" if api_port else ""
checks.append(Check(OK, "castle-api running", detail=detail))
checks.append(Check(OK, "wildpc-api running", detail=detail))
else:
checks.append(Check(FAIL, "castle-api not running", hint="castle apply"))
checks.append(Check(FAIL, "wildpc-api not running", hint="wildpc apply"))
# Generated artifacts.
registry = SPECS_DIR / "registry.yaml"
@@ -270,7 +270,7 @@ def _check_runtime(config) -> list[Check]:
FAIL,
"generated specs missing",
detail=", ".join(missing),
hint="castle apply",
hint="wildpc apply",
)
)
@@ -293,7 +293,7 @@ def _check_tls_exposure(config) -> list[Check]:
Check(
FAIL,
"tls=acme but no domain set",
hint="add 'domain: <your-zone>' under gateway: in castle.yaml",
hint="add 'domain: <your-zone>' under gateway: in wildpc.yaml",
)
)
@@ -327,7 +327,7 @@ def _check_tls_exposure(config) -> list[Check]:
token_name = {"cloudflare": "CLOUDFLARE_API_TOKEN"}.get(
provider, f"{provider.upper()}_API_TOKEN"
)
from castle_core.config import read_secret
from wildpc_core.config import read_secret
if read_secret(token_name):
checks.append(Check(OK, "provider token present", detail=token_name))
@@ -348,7 +348,7 @@ def _check_tls_exposure(config) -> list[Check]:
public_specs = [(n, d) for _k, n, d in config.all_deployments() if getattr(d, "public", False)]
public = [n for n, _d in public_specs]
if public:
from castle_core.lifecycle import is_active
from wildpc_core.lifecycle import is_active
# A public deployment gets its public name from its own `public_host`
# override or the node-wide `public_domain`; the default domain is only
@@ -370,13 +370,13 @@ def _check_tls_exposure(config) -> list[Check]:
hint="see docs/tunnel-setup.md",
)
)
if not is_active("castle-tunnel", "service", config):
if not is_active("wildpc-tunnel", "service", config):
checks.append(
Check(
WARN,
"castle-tunnel not running",
"wildpc-tunnel not running",
detail="public routes are down",
hint="enable castle-tunnel in its deployment, then: castle apply",
hint="enable wildpc-tunnel in its deployment, then: wildpc apply",
)
)
checks.append(_check_public_dns(config))
@@ -387,18 +387,18 @@ def _check_tls_exposure(config) -> list[Check]:
def _check_public_dns(config) -> Check:
"""Whether Castle can manage the public CNAMEs itself.
"""Whether Wild PC can manage the public CNAMEs itself.
Read-only: confirms the token exists and can reach the public zone + its DNS
records. The required permission is a single DNS:Edit (Cloudflare's 'Edit zone
DNS' template), which also grants the zone lookup — so a correctly-scoped token
passes this probe. Write itself isn't exercised (that would mutate); a
DNS:Read-only token would false-pass here but `castle apply` then surfaces a
DNS:Read-only token would false-pass here but `wildpc apply` then surfaces a
403 with the fix. Absent token WARN (CNAMEs stay manual), not a failure.
"""
import urllib.error
from castle_core.generators.dns import PUBLIC_DNS_TOKEN, _api, public_dns_token
from wildpc_core.generators.dns import PUBLIC_DNS_TOKEN, _api, public_dns_token
gw = config.gateway
token = public_dns_token()
@@ -410,7 +410,7 @@ def _check_public_dns(config) -> Check:
hint=(
f"add a Cloudflare token with DNS:Edit on "
f"{gw.public_domain or 'the public zone(s)'} "
f"('Edit zone DNS' template) → ~/.castle/secrets/{PUBLIC_DNS_TOKEN} "
f"('Edit zone DNS' template) → ~/.wildpc/secrets/{PUBLIC_DNS_TOKEN} "
"(else route each host by hand)"
),
)
@@ -463,7 +463,7 @@ def _check_privileged_ports() -> Check:
"cannot bind :80/:443",
detail=f"unprivileged floor is {val}",
hint="echo 'net.ipv4.ip_unprivileged_port_start=80' | "
"sudo tee /etc/sysctl.d/50-castle-gateway.conf && sudo sysctl --system",
"sudo tee /etc/sysctl.d/50-wildpc-gateway.conf && sudo sysctl --system",
)
@@ -476,7 +476,7 @@ def _check_stacks(config: object) -> list[Check]:
tool for an enabled deployment is a FAIL (its service can't build/run); missing
for a not-yet-enabled program is a WARN. Unused stacks are skipped no nagging
about pnpm when there are no frontends."""
from castle_core.stack_status import all_stack_status
from wildpc_core.stack_status import all_stack_status
checks: list[Check] = []
for st in all_stack_status(config, with_version=False):
@@ -504,9 +504,9 @@ def _check_stacks(config: object) -> list[Check]:
def run_doctor(args: argparse.Namespace) -> int:
from castle_core.config import load_config
from wildpc_core.config import load_config
print("\n\033[1mCastle Doctor\033[0m")
print("\n\033[1mWild PC Doctor\033[0m")
try:
config = load_config()
@@ -515,9 +515,9 @@ def run_doctor(args: argparse.Namespace) -> int:
_print(
Check(
FAIL,
"castle.yaml failed to load",
"wildpc.yaml failed to load",
detail=str(exc),
hint="check ~/.castle/castle.yaml — re-run ./install.sh to reseed",
hint="check ~/.wildpc/wildpc.yaml — re-run ./install.sh to reseed",
)
)
print()
@@ -544,7 +544,7 @@ def run_doctor(args: argparse.Namespace) -> int:
print(f"\033[31m{fails} problem(s)\033[0m" + (f", {warns} warning(s)" if warns else ""))
return 1
if warns:
print(f"\033[33m{warns} warning(s)\033[0m — Castle is up; address when convenient")
print(f"\033[33m{warns} warning(s)\033[0m — Wild PC is up; address when convenient")
return 0
print("\033[32mAll checks passed.\033[0m")
return 0

View File

@@ -1,8 +1,8 @@
"""castle gateway — inspect the Caddy reverse proxy gateway.
"""wildpc gateway — inspect the Caddy reverse proxy gateway.
The gateway is itself a deployment (`castle-gateway`): start/stop/reload it the
same way as anything else `castle apply` (render routes + reload), `castle
restart castle-gateway` (bounce), or `enabled: false` + apply (stop). This command
The gateway is itself a deployment (`wildpc-gateway`): start/stop/reload it the
same way as anything else `wildpc apply` (render routes + reload), `wildpc
restart wildpc-gateway` (bounce), or `enabled: false` + apply (stop). This command
is the read-only inspection lens: is it up, and what's the route table.
"""
@@ -11,9 +11,9 @@ from __future__ import annotations
import argparse
import subprocess
from castle_core.registry import REGISTRY_PATH, load_registry
from wildpc_core.registry import REGISTRY_PATH, load_registry
GATEWAY_UNIT = "castle-castle-gateway.service"
GATEWAY_UNIT = "wildpc-wildpc-gateway.service"
def run_gateway(args: argparse.Namespace) -> int:
@@ -32,10 +32,10 @@ def _gateway_status() -> int:
print(f"Gateway: {'running' if status == 'active' else status}")
if not REGISTRY_PATH.exists():
print(" (no registry — run 'castle apply')")
print(" (no registry — run 'wildpc apply')")
return 0
from castle_core.generators.caddyfile import compute_routes
from wildpc_core.generators.caddyfile import compute_routes
routes = compute_routes(load_registry())
if not routes:

View File

@@ -1,4 +1,4 @@
"""castle graph — the relationship model: repos, `requires` edges, derived status.
"""wildpc graph — the relationship model: repos, `requires` edges, derived status.
A read-only diagnostic (see docs/relationships.md). Nothing here is stored repos
come from git, predicates (functional/fresh/deployed) are computed on the fly.
@@ -10,14 +10,14 @@ import argparse
import dataclasses
import json
from castle_cli.config import load_config
from wildpc_cli.config import load_config
BOLD, DIM, RESET = "\033[1m", "\033[2m", "\033[0m"
GREEN, RED, YELLOW, CYAN = "\033[32m", "\033[31m", "\033[33m", "\033[36m"
def run_graph(args: argparse.Namespace) -> int:
from castle_core.relations import build_model
from wildpc_core.relations import build_model
config = load_config()
model = build_model(config, check=True, freshness=True)

View File

@@ -1,4 +1,4 @@
"""castle info - show detailed program information."""
"""wildpc info - show detailed program information."""
from __future__ import annotations
@@ -6,7 +6,7 @@ import argparse
import json
from pathlib import Path
from castle_cli.config import load_config
from wildpc_cli.config import load_config
# Terminal colors
BOLD = "\033[1m"
@@ -20,7 +20,7 @@ RED = "\033[91m"
def _load_deployed_program(name: str) -> object | None:
"""Try to load a specific deployed program from registry."""
try:
from castle_core.registry import load_registry
from wildpc_core.registry import load_registry
registry = load_registry()
return next(iter(registry.named(name)), None)
@@ -41,7 +41,7 @@ def run_info(args: argparse.Namespace) -> int:
if not program and not service and not job:
where = f" {resource}" if resource else ""
print(f"Error: no{where} '{name}' in castle.yaml")
print(f"Error: no{where} '{name}' in wildpc.yaml")
return 1
deployed = _load_deployed_program(name)
@@ -153,7 +153,7 @@ def run_info(args: argparse.Namespace) -> int:
if deployed.secret_env_keys:
print(f" {BOLD}secrets{RESET}: {', '.join(deployed.secret_env_keys)}")
else:
print(f"\n {DIM}not applied (run 'castle apply'){RESET}")
print(f"\n {DIM}not applied (run 'wildpc apply'){RESET}")
# Show CLAUDE.md if it exists
source_dir = None

View File

@@ -1,4 +1,4 @@
"""castle list - the program catalog plus every deployment view (services, jobs,
"""wildpc list - the program catalog plus every deployment view (services, jobs,
tools, static)."""
from __future__ import annotations
@@ -7,7 +7,7 @@ import argparse
import json
import logging
from castle_cli.config import load_config
from wildpc_cli.config import load_config
log = logging.getLogger(__name__)
@@ -75,7 +75,7 @@ def run_list(args: argparse.Namespace) -> int:
and the **Services**/**Jobs** deployment views. `--kind` filters the catalog
by a program's derived kind (service/job/tool/static/reference).
"""
from castle_core.lifecycle import is_active
from wildpc_core.lifecycle import is_active
config = load_config()
@@ -199,7 +199,7 @@ def _list_json(
filter_stack: str | None,
) -> int:
"""Output JSON: the program catalog (kind-filterable) plus deployments."""
from castle_core.lifecycle import is_active
from wildpc_core.lifecycle import is_active
output = []

View File

@@ -1,12 +1,12 @@
"""castle logs - view component logs."""
"""wildpc logs - view component logs."""
from __future__ import annotations
import argparse
import subprocess
from castle_cli.commands.service import UNIT_PREFIX
from castle_cli.config import load_config
from wildpc_cli.commands.service import UNIT_PREFIX
from wildpc_cli.config import load_config
def run_logs(args: argparse.Namespace) -> int:
@@ -51,7 +51,7 @@ def _compose_logs(name: str, svc: object, args: argparse.Namespace) -> int:
import shutil
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
project = getattr(svc.run, "project_name", None) or f"castle-{name}" # type: ignore[attr-defined]
project = getattr(svc.run, "project_name", None) or f"wildpc-{name}" # type: ignore[attr-defined]
cmd = [runtime, "compose", "-p", project, "logs"]
lines = getattr(args, "lines", 50)
@@ -70,7 +70,7 @@ def _container_logs(name: str, args: argparse.Namespace) -> int:
import shutil
runtime = shutil.which("podman") or shutil.which("docker") or "podman"
container_name = f"castle-{name}"
container_name = f"wildpc-{name}"
cmd = [runtime, "logs"]
lines = getattr(args, "lines", 50)

View File

@@ -1,6 +1,6 @@
"""castle mesh — inspect the NATS mesh and manage shared config.
"""wildpc mesh — inspect the NATS mesh and manage shared config.
The mesh lives in the running castle-api (it holds the live peer state), so this
The mesh lives in the running wildpc-api (it holds the live peer state), so this
command talks to the local API over HTTP rather than reading files.
"""
@@ -15,10 +15,10 @@ import urllib.request
def _api_base() -> str:
port = None
try:
from castle_core.config import load_config
from wildpc_core.config import load_config
config = load_config()
dep = next((d for _k, d in config.deployments_named("castle-api")), None)
dep = next((d for _k, d in config.deployments_named("wildpc-api")), None)
internal = getattr(getattr(getattr(dep, "expose", None), "http", None), "internal", None)
port = getattr(internal, "port", None)
except Exception:
@@ -56,7 +56,7 @@ def run_mesh(args: argparse.Namespace) -> int:
print(f"error: HTTP {e.code}{detail}")
return 1
except urllib.error.URLError as e:
print(f"castle-api not reachable ({e.reason}). Is it running + mesh enabled?")
print(f"wildpc-api not reachable ({e.reason}). Is it running + mesh enabled?")
return 1
print(f"unknown mesh command: {sub}")
return 2

View File

@@ -1,4 +1,4 @@
"""castle run - run a program or service in the foreground.
"""wildpc run - run a program or service in the foreground.
Unified: if the target is a program with a declared `run` command, run that in
the foreground (dev-run). Otherwise fall back to the deployed-service run from
@@ -12,10 +12,10 @@ import os
import subprocess
from pathlib import Path
from castle_core.generators.systemd import secret_env_path
from castle_core.registry import REGISTRY_PATH, load_registry
from wildpc_core.generators.systemd import secret_env_path
from wildpc_core.registry import REGISTRY_PATH, load_registry
from castle_cli.config import load_config
from wildpc_cli.config import load_config
def _load_secret_env(name: str) -> dict[str, str]:
@@ -82,13 +82,13 @@ def run_run(args: argparse.Namespace) -> int:
# Service: deployed command from the registry.
if not REGISTRY_PATH.exists():
print("Error: no registry found. Run 'castle apply' first.")
print("Error: no registry found. Run 'wildpc apply' first.")
return 1
registry = load_registry()
matches = registry.named(name)
if not matches:
print(f"Error: '{name}' is not a deployed service. Run 'castle apply' first.")
print(f"Error: '{name}' is not a deployed service. Run 'wildpc apply' first.")
return 1
# A name may span kinds; run the one that has a run command (service/job).

View File

@@ -1,14 +1,14 @@
"""castle secret — read/write secrets in the *active* backend.
"""wildpc secret — read/write secrets in the *active* backend.
The active backend (file or openbao) is selected by the ``secrets:`` block of
castle.yaml (env overrides). Writing a secret by hand means knowing that choice
wildpc.yaml (env overrides). Writing a secret by hand means knowing that choice
and the backend's storage layout — get it wrong and the value lands somewhere the
resolver never reads, so ``${secret:NAME}`` silently degrades to a
``<MISSING_SECRET:NAME>`` placeholder that a service then uses as if it were the
real credential (exactly how immich's DB password went to a file on an OpenBao
fleet). This command routes every read/write through
:func:`castle_core.config.active_secret_backend`, so there's no wrong store to
pick. ``castle apply`` refuses to converge a deployment whose secrets don't
:func:`wildpc_core.config.active_secret_backend`, so there's no wrong store to
pick. ``wildpc apply`` refuses to converge a deployment whose secrets don't
resolve here.
"""
@@ -18,13 +18,13 @@ import argparse
import getpass
import sys
from castle_core.config import active_backend_name, active_secret_backend
from wildpc_core.config import active_backend_name, active_secret_backend
def run_secret(args: argparse.Namespace) -> int:
sub = getattr(args, "secret_command", None)
if not sub:
print("Usage: castle secret {list|set|get|rm}")
print("Usage: wildpc secret {list|set|get|rm}")
return 1
if sub == "list":
return _list()

View File

@@ -1,23 +1,23 @@
"""castle service / castle job — manage systemd service & timer units."""
"""wildpc service / wildpc job — manage systemd service & timer units."""
from __future__ import annotations
import argparse
import subprocess
from castle_core.generators.systemd import (
from wildpc_core.generators.systemd import (
SYSTEMD_USER_DIR,
timer_name,
unit_name,
)
from castle_cli.config import (
CastleConfig,
from wildpc_cli.config import (
WildpcConfig,
load_config,
)
# Re-export for use by other commands
UNIT_PREFIX = "castle-"
UNIT_PREFIX = "wildpc-"
def _install_unit(uname: str, content: str) -> None:
@@ -40,25 +40,25 @@ _PAST = {"start": "started", "stop": "stopped", "restart": "restarted"}
def run_service_cmd(args: argparse.Namespace) -> int:
"""`castle service restart <name>` — the imperative bounce (only verb left).
"""`wildpc service restart <name>` — the imperative bounce (only verb left).
Lifecycle (deploy/enable/disable/start/stop) is now convergence: `castle apply`.
Lifecycle (deploy/enable/disable/start/stop) is now convergence: `wildpc apply`.
"""
config = load_config()
return _unit_action(config, args.name, "restart", "service")
def run_job_cmd(args: argparse.Namespace) -> int:
"""`castle job restart <name>` — bounce the job's timer."""
"""`wildpc job restart <name>` — bounce the job's timer."""
config = load_config()
return _unit_action(config, args.name, "restart", "job")
def run_restart(args: argparse.Namespace) -> int:
"""Top-level `castle restart [name]` — bounce one deployment, or all of them.
"""Top-level `wildpc restart [name]` — bounce one deployment, or all of them.
An imperative op: it re-actualizes current desired state, it does not change it
(that's `castle apply`). A bare name bounces every kind sharing it.
(that's `wildpc apply`). A bare name bounces every kind sharing it.
"""
config = load_config()
name = getattr(args, "name", None)
@@ -74,10 +74,10 @@ def run_restart(args: argparse.Namespace) -> int:
return rc
_GATEWAY_NAME = "castle-gateway"
_GATEWAY_NAME = "wildpc-gateway"
def _unit_action(config: CastleConfig, name: str, action: str, kind: str) -> int:
def _unit_action(config: WildpcConfig, name: str, action: str, kind: str) -> int:
"""start/stop/restart one deployment (name, kind), dispatched by its manager.
systemd (a process/timer) `systemctl`; caddy (static) reload the gateway;
@@ -101,7 +101,7 @@ def _unit_action(config: CastleConfig, name: str, action: str, kind: str) -> int
def _managed_lifecycle(
config: CastleConfig, name: str, action: str, manager: str, kind: str
config: WildpcConfig, name: str, action: str, manager: str, kind: str
) -> int:
"""Lifecycle for non-systemd managers (no unit to systemctl)."""
if manager == "caddy":
@@ -119,11 +119,11 @@ def _managed_lifecycle(
return 0
def _path_lifecycle(config: CastleConfig, name: str, action: str, kind: str) -> int:
def _path_lifecycle(config: WildpcConfig, name: str, action: str, kind: str) -> int:
"""A `path` (tool) deployment's lifecycle is install/uninstall on PATH."""
import asyncio
from castle_core.lifecycle import activate, deactivate
from wildpc_core.lifecycle import activate, deactivate
# stop → uninstall; start/restart → ensure installed (activate skips if on PATH).
coro = deactivate if action == "stop" else activate
@@ -132,7 +132,7 @@ def _path_lifecycle(config: CastleConfig, name: str, action: str, kind: str) ->
return 0 if res.status == "ok" else 1
def _services_restart(config: CastleConfig) -> int:
def _services_restart(config: WildpcConfig) -> int:
"""Restart every systemd-managed deployment (service or job) unit.
caddy/path/none deployments have no unit they ride along with the gateway
@@ -152,7 +152,7 @@ def _services_restart(config: CastleConfig) -> int:
def run_status(args: argparse.Namespace) -> int:
"""Unified status across the platform: services + jobs + programs."""
from castle_core.lifecycle import is_active
from wildpc_core.lifecycle import is_active
config = load_config()
@@ -180,11 +180,11 @@ def run_status(args: argparse.Namespace) -> int:
return 0
def _service_status(config: CastleConfig) -> int:
def _service_status(config: WildpcConfig) -> int:
"""Show status of all services and jobs, dispatched by manager."""
from castle_core.lifecycle import is_active
from wildpc_core.lifecycle import is_active
print("\nCastle Services")
print("\nWild PC Services")
print("=" * 50)
for name, svc in config.services.items():

View File

@@ -1,4 +1,4 @@
"""castle stack — the stacks lens (toolchains a program's stack needs).
"""wildpc stack — the stacks lens (toolchains a program's stack needs).
A *stack* (python-fastapi, react-vite, hugo, ) is creation-time guidance that
also carries the **host toolchains** its programs need to build and run (`uv`,
@@ -14,10 +14,10 @@ import json
from dataclasses import asdict
from typing import TYPE_CHECKING
from castle_cli.config import load_config
from wildpc_cli.config import load_config
if TYPE_CHECKING:
from castle_core.stack_status import StackStatus
from wildpc_core.stack_status import StackStatus
BOLD = "\033[1m"
DIM = "\033[2m"
@@ -37,7 +37,7 @@ def _record(st: StackStatus) -> dict:
def run_stack_list(args: argparse.Namespace) -> int:
"""List stacks with their toolchain health, program count, and dev verbs."""
from castle_core.stack_status import all_stack_status
from wildpc_core.stack_status import all_stack_status
config = load_config()
# Skip per-tool version probes for the list (a subprocess per tool) — keep it snappy.
@@ -75,13 +75,13 @@ def run_stack_list(args: argparse.Namespace) -> int:
def run_stack_info(args: argparse.Namespace) -> int:
"""Show one stack: each tool's presence + version + fix, and who uses it."""
from castle_core.stack_status import stack_status
from wildpc_core.stack_status import stack_status
config = load_config()
name = args.name
st = stack_status(config, name)
if st is None:
from castle_core.stacks import available_stacks
from wildpc_core.stacks import available_stacks
print(f"Error: no stack '{name}'. Known: {', '.join(available_stacks())}")
return 1

View File

@@ -1,4 +1,4 @@
"""castle tls — castle-managed TLS certs for raw-TCP services.
"""wildpc tls — wildpc-managed TLS certs for raw-TCP services.
Each TLS-material TCP service (postgres, redis, ) gets the gateway's ACME
wildcard cert cut onto it so it presents a *trusted* cert on its raw port.
@@ -13,9 +13,9 @@ import argparse
import hashlib
from datetime import datetime, timezone
from castle_core.tls import _tls_of, reconcile_tls, tls_dir_for, wildcard_cert
from wildpc_core.tls import _tls_of, reconcile_tls, tls_dir_for, wildcard_cert
from castle_cli.config import load_config
from wildpc_cli.config import load_config
def run_tls(args: argparse.Namespace) -> int:
@@ -67,7 +67,7 @@ def _tls_status() -> int:
continue
pem = have.read_bytes()
fp = _fingerprint(pem)
drift = "" if src_fp and fp == src_fp else " ⚠ stale (run: castle tls reconcile)"
drift = "" if src_fp and fp == src_fp else " ⚠ stale (run: wildpc tls reconcile)"
rows.append((name, fp, _not_after(pem), drift))
if not rows:

View File

@@ -1,4 +1,4 @@
"""castle tool — the tools lens (programs installed on PATH).
"""wildpc tool — the tools lens (programs installed on PATH).
A *tool* is a program with a `path` (manager) deployment: a CLI on your PATH.
This lens is what coding assistants use to discover what's available — so the
@@ -15,11 +15,11 @@ import tomllib
from pathlib import Path
from typing import TYPE_CHECKING
from castle_cli.config import load_config
from wildpc_cli.config import load_config
if TYPE_CHECKING:
from castle_core.config import CastleConfig
from castle_core.manifest import ProgramSpec
from wildpc_core.config import WildpcConfig
from wildpc_core.manifest import ProgramSpec
BOLD = "\033[1m"
DIM = "\033[2m"
@@ -29,11 +29,11 @@ GREY = "\033[90m"
CYAN = "\033[96m"
def _is_tool(config: CastleConfig, name: str) -> bool:
def _is_tool(config: WildpcConfig, name: str) -> bool:
return any(kind == "tool" for _, kind in config.deployments_of(name))
def _tool_programs(config: CastleConfig) -> dict:
def _tool_programs(config: WildpcConfig) -> dict:
"""Programs with a tool (path) deployment, name-sorted."""
return {name: comp for name, comp in sorted(config.programs.items()) if _is_tool(config, name)}
@@ -60,12 +60,12 @@ def _executables(comp: ProgramSpec) -> list[str]:
def _tool_record(
config: CastleConfig, name: str, comp: ProgramSpec, installed: bool, fmt: str = "openai"
config: WildpcConfig, name: str, comp: ProgramSpec, installed: bool, fmt: str = "openai"
) -> dict:
# The tool-call schema (for handing this CLI to an agent) is stored neutral on
# the path deployment; render it into the requested envelope for the --json
# context payload. Feed default is openai (litellm-native).
from castle_core.tool_schema import render_tool_schema
from wildpc_core.tool_schema import render_tool_schema
dep = config.deployment("tool", name)
core = getattr(dep, "tool_schema", None)
@@ -83,7 +83,7 @@ def _tool_record(
def run_tool_list(args: argparse.Namespace) -> int:
"""List tools (programs on PATH) with their executable + description."""
from castle_core.lifecycle import tool_installed
from wildpc_core.lifecycle import tool_installed
config = load_config()
tools = _tool_programs(config)
@@ -119,7 +119,7 @@ def run_tool_list(args: argparse.Namespace) -> int:
def run_tool_info(args: argparse.Namespace) -> int:
"""Show one tool's details — executable, description, install state, source."""
from castle_core.lifecycle import tool_installed
from wildpc_core.lifecycle import tool_installed
config = load_config()
name = args.name
@@ -149,7 +149,7 @@ def run_tool_info(args: argparse.Namespace) -> int:
if comp.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(comp.system_dependencies)}")
if not installed:
print(f"\n {DIM}enable it in its deployment, then: castle apply{RESET}")
print(f"\n {DIM}enable it in its deployment, then: wildpc apply{RESET}")
else:
print(f"\n {DIM}run `{exes[0]} --help` for arguments{RESET}")
print()

View File

@@ -1,24 +1,24 @@
"""Re-export from castle-core for backward compatibility."""
"""Re-export from wildpc-core for backward compatibility."""
from castle_core.config import * # noqa: F401, F403
from castle_core.config import ( # noqa: F401 — explicit re-exports for type checkers
from wildpc_core.config import * # noqa: F401, F403
from wildpc_core.config import ( # noqa: F401 — explicit re-exports for type checkers
ARTIFACTS_DIR,
CASTLE_HOME,
CODE_DIR,
CONTENT_DIR,
GENERATED_DIR,
SECRETS_DIR,
SPECS_DIR,
STATIC_DIR,
CastleConfig,
WILDPC_HOME,
GatewayConfig,
WildpcConfig,
ensure_dirs,
find_castle_root,
find_wildpc_root,
load_config,
resolve_env_vars,
save_config,
)
from castle_core.registry import ( # noqa: F401
from wildpc_core.registry import ( # noqa: F401
REGISTRY_PATH,
Deployment,
NodeConfig,

View File

@@ -1,4 +1,4 @@
"""Castle CLI entry point — resource-first command surface.
"""Wild PC CLI entry point — resource-first command surface.
Operations live under the resource they act on. `program` is the catalog;
`service`, `job`, and `tool` are deployment lenses (systemd services, scheduled
@@ -13,9 +13,9 @@ from __future__ import annotations
import argparse
import sys
from castle_core.stacks import available_stacks
from wildpc_core.stacks import available_stacks
from castle_cli import __version__
from wildpc_cli import __version__
DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"]
@@ -56,7 +56,7 @@ def _build_program_group(subparsers: argparse._SubParsersAction) -> None:
p = sub.add_parser("clone", help="Clone source for programs with repo:")
_add_name(p, "Program to clone (default: all with repo:)", optional=True)
p = sub.add_parser("delete", help="Remove a program from castle.yaml")
p = sub.add_parser("delete", help="Remove a program from wildpc.yaml")
_add_name(p, "Program name")
p.add_argument("--source", action="store_true", help="Also delete the source directory")
p.add_argument(
@@ -109,7 +109,7 @@ def _build_stack_group(subparsers: argparse._SubParsersAction) -> None:
def _add_service_create(sub: argparse._SubParsersAction, kind: str) -> None:
p = sub.add_parser("create", help=f"Create a {kind} in castle.yaml")
p = sub.add_parser("create", help=f"Create a {kind} in wildpc.yaml")
_add_name(p, f"{kind.capitalize()} name")
p.add_argument("--program", help="Program this deployment runs (convenience ref)")
p.add_argument("--description", default="", help="Description")
@@ -148,14 +148,14 @@ def _build_deployment_group(subparsers: argparse._SubParsersAction, kind: str) -
_add_service_create(sub, kind)
p = sub.add_parser("delete", help=f"Remove a {kind} from castle.yaml")
p = sub.add_parser("delete", help=f"Remove a {kind} from wildpc.yaml")
_add_name(p, f"{kind.capitalize()} name")
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
p.add_argument("--source", action="store_true", help=argparse.SUPPRESS)
p.add_argument("--purge-data", action="store_true", help=argparse.SUPPRESS)
cap = f"{kind.capitalize()} name"
# Lifecycle is convergence: `castle apply [name]`. `restart` stays as the one
# Lifecycle is convergence: `wildpc apply [name]`. `restart` stays as the one
# imperative bounce that doesn't change desired state.
_add_name(sub.add_parser("restart", help=f"Restart the {kind} (imperative bounce)"), cap)
@@ -167,13 +167,13 @@ def _build_deployment_group(subparsers: argparse._SubParsersAction, kind: str) -
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="castle",
prog="wildpc",
description=(
"Castle platform CLI — programs, deployments "
"Wild PC platform CLI — programs, deployments "
"(services, jobs, tools), and infrastructure"
),
)
parser.add_argument("--version", action="version", version=f"castle {__version__}")
parser.add_argument("--version", action="version", version=f"wildpc {__version__}")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
_build_program_group(subparsers)
@@ -183,7 +183,7 @@ def build_parser() -> argparse.ArgumentParser:
_build_stack_group(subparsers)
# Gateway (inspection). The gateway is a deployment — start/stop/reload it via
# `castle apply` / `castle restart castle-gateway`; this lens just shows routes.
# `wildpc apply` / `wildpc restart wildpc-gateway`; this lens just shows routes.
gw = subparsers.add_parser("gateway", help="Show the gateway's status + route table")
gw_sub = gw.add_subparsers(dest="gateway_command")
gw_sub.add_parser("status", help="Show gateway status + routes (the default)")
@@ -204,7 +204,7 @@ def build_parser() -> argparse.ArgumentParser:
# TLS material for raw-TCP services (cert cut from the gateway wildcard).
tls = subparsers.add_parser(
"tls", help="Manage castle-materialized TLS certs for raw-TCP services"
"tls", help="Manage wildpc-materialized TLS certs for raw-TCP services"
)
tls_sub = tls.add_subparsers(dest="tls_command")
tls_sub.add_parser(
@@ -265,38 +265,38 @@ def _dispatch_program(args: argparse.Namespace) -> int:
sub = args.program_command
if not sub:
verbs = "list|info|create|add|clone|delete|run|" + "|".join(DEV_VERBS)
print(f"Usage: castle program {{{verbs}}}")
print(f"Usage: wildpc program {{{verbs}}}")
return 1
if sub == "list":
from castle_cli.commands.list_cmd import run_list
from wildpc_cli.commands.list_cmd import run_list
return run_list(args)
if sub == "info":
from castle_cli.commands.info import run_info
from wildpc_cli.commands.info import run_info
return run_info(args)
if sub == "create":
from castle_cli.commands.create import run_create
from wildpc_cli.commands.create import run_create
return run_create(args)
if sub == "add":
from castle_cli.commands.add import run_add
from wildpc_cli.commands.add import run_add
return run_add(args)
if sub == "clone":
from castle_cli.commands.clone import run_clone
from wildpc_cli.commands.clone import run_clone
return run_clone(args)
if sub == "delete":
from castle_cli.commands.delete import run_delete
from wildpc_cli.commands.delete import run_delete
return run_delete(args)
if sub == "run":
from castle_cli.commands.run_cmd import run_run
from wildpc_cli.commands.run_cmd import run_run
return run_run(args)
if sub in DEV_VERBS:
from castle_cli.commands.dev import run_verb
from wildpc_cli.commands.dev import run_verb
return run_verb(args, sub)
return 1
@@ -305,14 +305,14 @@ def _dispatch_program(args: argparse.Namespace) -> int:
def _dispatch_tool(args: argparse.Namespace) -> int:
sub = args.tool_command
if not sub:
print("Usage: castle tool {list|info} (install/uninstall → edit config + castle apply)")
print("Usage: wildpc tool {list|info} (install/uninstall → edit config + wildpc apply)")
return 1
if sub == "list":
from castle_cli.commands.tool import run_tool_list
from wildpc_cli.commands.tool import run_tool_list
return run_tool_list(args)
if sub == "info":
from castle_cli.commands.tool import run_tool_info
from wildpc_cli.commands.tool import run_tool_info
return run_tool_info(args)
return 1
@@ -321,14 +321,14 @@ def _dispatch_tool(args: argparse.Namespace) -> int:
def _dispatch_stack(args: argparse.Namespace) -> int:
sub = args.stack_command
if not sub:
print("Usage: castle stack {list|info}")
print("Usage: wildpc stack {list|info}")
return 1
if sub == "list":
from castle_cli.commands.stack import run_stack_list
from wildpc_cli.commands.stack import run_stack_list
return run_stack_list(args)
if sub == "info":
from castle_cli.commands.stack import run_stack_info
from wildpc_cli.commands.stack import run_stack_info
return run_stack_info(args)
return 1
@@ -337,31 +337,31 @@ def _dispatch_stack(args: argparse.Namespace) -> int:
def _dispatch_deployment(args: argparse.Namespace, kind: str) -> int:
sub = getattr(args, f"{kind}_command")
if not sub:
verbs = "list|info|create|delete|restart|logs (deploy/enable/... → castle apply)"
print(f"Usage: castle {kind} {{{verbs}}}")
verbs = "list|info|create|delete|restart|logs (deploy/enable/... → wildpc apply)"
print(f"Usage: wildpc {kind} {{{verbs}}}")
return 1
if sub == "list":
from castle_cli.commands.list_cmd import run_list
from wildpc_cli.commands.list_cmd import run_list
return run_list(args)
if sub == "info":
from castle_cli.commands.info import run_info
from wildpc_cli.commands.info import run_info
return run_info(args)
if sub == "create":
from castle_cli.commands.deploy_create import run_job_create, run_service_create
from wildpc_cli.commands.deploy_create import run_job_create, run_service_create
return run_service_create(args) if kind == "service" else run_job_create(args)
if sub == "delete":
from castle_cli.commands.delete import run_delete
from wildpc_cli.commands.delete import run_delete
return run_delete(args)
if sub == "restart":
from castle_cli.commands.service import run_job_cmd, run_service_cmd
from wildpc_cli.commands.service import run_job_cmd, run_service_cmd
return run_service_cmd(args) if kind == "service" else run_job_cmd(args)
if sub == "logs":
from castle_cli.commands.logs import run_logs
from wildpc_cli.commands.logs import run_logs
return run_logs(args)
return 1
@@ -385,43 +385,43 @@ def main() -> int:
if cmd == "stack":
return _dispatch_stack(args)
if cmd == "gateway":
from castle_cli.commands.gateway import run_gateway
from wildpc_cli.commands.gateway import run_gateway
return run_gateway(args)
if cmd == "mesh":
from castle_cli.commands.mesh import run_mesh
from wildpc_cli.commands.mesh import run_mesh
return run_mesh(args)
if cmd == "tls":
from castle_cli.commands.tls import run_tls
from wildpc_cli.commands.tls import run_tls
return run_tls(args)
if cmd == "secret":
from castle_cli.commands.secret import run_secret
from wildpc_cli.commands.secret import run_secret
return run_secret(args)
if cmd == "apply":
from castle_cli.commands.apply import run_apply
from wildpc_cli.commands.apply import run_apply
return run_apply(args)
if cmd == "restart":
from castle_cli.commands.service import run_restart
from wildpc_cli.commands.service import run_restart
return run_restart(args)
if cmd == "status":
from castle_cli.commands.service import run_status
from wildpc_cli.commands.service import run_status
return run_status(args)
if cmd == "doctor":
from castle_cli.commands.doctor import run_doctor
from wildpc_cli.commands.doctor import run_doctor
return run_doctor(args)
if cmd == "graph":
from castle_cli.commands.graph import run_graph
from wildpc_cli.commands.graph import run_graph
return run_graph(args)
if cmd == "list":
from castle_cli.commands.list_cmd import run_list
from wildpc_cli.commands.list_cmd import run_list
return run_list(args)

View File

@@ -1,7 +1,7 @@
"""Re-export from castle-core for backward compatibility."""
"""Re-export from wildpc-core for backward compatibility."""
from castle_core.manifest import * # noqa: F401, F403
from castle_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
from wildpc_core.manifest import * # noqa: F401, F403
from wildpc_core.manifest import ( # noqa: F401 — explicit re-exports for type checkers
BuildSpec,
CaddyDeployment,
CommandsSpec,

View File

@@ -575,14 +575,14 @@ uv run ruff format . # Format
def _scaffold_supabase(project_dir: Path, name: str, description: str) -> None:
"""Scaffold a Patch-shaped app that targets the shared Supabase substrate.
Produces migrations/ (applied to the substrate by `castle program build`),
Produces migrations/ (applied to the substrate by `wildpc program build`),
functions/ (deno edge functions), public/ (static UI served in place at
<name>.<gateway.domain> by the gateway), and supabase.app.yaml (auth policy + wiring).
The app owns its code and stays repo-durable; only its rows/blobs live on the
shared substrate. Each app is isolated in its **own Postgres schema** (the app
id) rather than sharing `public` so `castle program build` creates+grants the
schema and tracks migrations per-app, and `castle delete --purge-data` drops
id) rather than sharing `public` so `wildpc program build` creates+grants the
schema and tracks migrations per-app, and `wildpc delete --purge-data` drops
the whole schema cleanly. Rows are further protected by RLS.
"""
ident = name.replace("-", "_") # a safe SQL/JS identifier — and the app schema
@@ -604,7 +604,7 @@ def _scaffold_supabase(project_dir: Path, name: str, description: str) -> None:
"""# Patch-shaped app targeting the shared Supabase substrate.
name: __NAME__
description: __DESC__
substrate: supabase # the shared castle service this app deploys against
substrate: supabase # the shared wildpc service this app deploys against
# auth policy: public | private | shared
# public — anyone with the URL (anon read/write, still RLS-gated)
@@ -614,7 +614,7 @@ auth: public
# shared: [alice, bob]
# This app is isolated in its own Postgres schema (created + exposed through
# PostgREST by `castle program build`). The frontend selects it via
# PostgREST by `wildpc program build`). The frontend selects it via
# supabase-js `db: { schema }`.
schema: __SCHEMA__
"""
@@ -625,7 +625,7 @@ schema: __SCHEMA__
_write(
project_dir / "migrations" / "0001_init.sql",
sub(
"""-- 0001_init: example table + RLS. Applied by `castle program build`
"""-- 0001_init: example table + RLS. Applied by `wildpc program build`
-- via the versioned migration runner (tracked per-app in
-- <schema>.schema_migrations; only unapplied migrations run). Forward-only
-- never edit an applied migration; add a new numbered file instead.
@@ -680,7 +680,7 @@ serve((_req: Request) => {
sub(
"""// Substrate wiring for __NAME__. The anon key is designed to be public
// (RLS enforces access) paste yours here. Find it in the dashboard
// Secrets page (or `castle mesh`/vault), secret name SUPABASE_ANON_KEY.
// Secrets page (or `wildpc mesh`/vault), secret name SUPABASE_ANON_KEY.
window.APP = {
SUPABASE_URL: "https://supabase.lan",
SUPABASE_ANON_KEY: "PASTE_ANON_KEY_HERE",
@@ -748,23 +748,23 @@ window.APP = {
__DESC__
A **supabase-stack** app: it owns its code (this repo) and rents its backend from
the shared Supabase substrate (the `supabase` castle service). Only its rows/blobs
the shared Supabase substrate (the `supabase` wildpc service). Only its rows/blobs
live on the substrate; rebuild the rest from git anytime.
## Layout
- `migrations/` versioned, idempotent Postgres migrations (forward-only). Applied
to the substrate by `castle program build __NAME__`.
to the substrate by `wildpc program build __NAME__`.
- `functions/` deno edge functions deployed to the substrate's edge-runtime.
- `public/` static UI served in place at `/__NAME__/` by the gateway. Talks to
the substrate with `@supabase/supabase-js` + the public anon key (RLS-gated).
- `supabase.app.yaml` substrate wiring + auth policy (public/private/shared).
## Develop
- Edit `migrations/`, then `castle program build __NAME__` to apply new migrations
- Edit `migrations/`, then `wildpc program build __NAME__` to apply new migrations
(re-running is a no-op only unapplied migrations run).
- Set the anon key in `public/config.js` (find `SUPABASE_ANON_KEY` in the
dashboard Secrets page).
- `castle apply` served at `__NAME__.<domain>`.
- `wildpc apply` served at `__NAME__.<domain>`.
## Privacy note
RLS protects rows, not the static shell or Storage. For a `private`/`shared` app,
@@ -799,9 +799,9 @@ def _scaffold_hugo(project_dir: Path, name: str, description: str) -> None:
The canonical skeleton comes from Hugo's own `hugo new site`; on top of it we
overlay the pieces a bare skeleton lacks the layouts needed to render a home
page without a theme, sample content, and a castle-flavored hugo.toml/README.
page without a theme, sample content, and a wildpc-flavored hugo.toml/README.
`castle program build` runs `hugo --gc --minify` `public/`, which a caddy
`wildpc program build` runs `hugo --gc --minify` `public/`, which a caddy
deployment serves in place at <name>.<gateway.domain>. baseURL is `/` so assets
resolve at the root of the site's own subdomain. A theme (with its own asset
pipeline) can be dropped under themes/ later; such themes declare a two-step
@@ -912,7 +912,7 @@ description = "__DESC__"
title: "__NAME__"
---
Welcome to **__NAME__** a Hugo site on castle. Edit `content/_index.md` and
Welcome to **__NAME__** a Hugo site on wildpc. Edit `content/_index.md` and
add posts under `content/posts/`.
"""
),
@@ -942,7 +942,7 @@ Your first post. Edit me in `content/posts/`, or run `hugo new posts/next.md`.
__DESC__
A [Hugo](https://gohugo.io) static site managed by castle.
A [Hugo](https://gohugo.io) static site managed by wildpc.
## Develop
@@ -950,8 +950,8 @@ A [Hugo](https://gohugo.io) static site managed by castle.
## Build & serve
castle program build __NAME__ # hugo --gc --minify -> public/
castle apply __NAME__ # serve at __NAME__.<gateway.domain>
wildpc program build __NAME__ # hugo --gc --minify -> public/
wildpc apply __NAME__ # serve at __NAME__.<gateway.domain>
## Themes