Refactor component terminology to programs in config and manifest

- Updated the terminology from "components" to "programs" across the codebase, including in config loading, saving, and manifest specifications.
- Introduced a new `stacks.py` file to handle lifecycle actions for development stacks, implementing handlers for Python and React Vite stacks.
- Adjusted tests to reflect the new program structure and ensure proper functionality.
- Revised documentation to align with the new terminology and structure, ensuring clarity on the purpose and configuration of programs, services, and jobs.
This commit is contained in:
2026-02-23 22:09:41 -08:00
parent f559fba143
commit 0d36e4f72a
48 changed files with 7512 additions and 632 deletions

View File

@@ -9,7 +9,7 @@ from pathlib import Path
import yaml
from castle_core.manifest import ComponentSpec, JobSpec, ServiceSpec
from castle_core.manifest import ProgramSpec, JobSpec, ServiceSpec
def find_castle_root() -> Path:
@@ -47,25 +47,25 @@ class CastleConfig:
root: Path
gateway: GatewayConfig
components: dict[str, ComponentSpec]
programs: dict[str, ProgramSpec]
services: dict[str, ServiceSpec]
jobs: dict[str, JobSpec]
@property
def tools(self) -> dict[str, ComponentSpec]:
"""Return components that are tools (have install.path or tool spec)."""
def tools(self) -> dict[str, ProgramSpec]:
"""Return programs that are tools (have install.path or tool spec)."""
return {
k: v
for k, v in self.components.items()
for k, v in self.programs.items()
if (v.install and v.install.path) or v.tool
}
@property
def frontends(self) -> dict[str, ComponentSpec]:
"""Return components that are frontends (have build outputs)."""
def frontends(self) -> dict[str, ProgramSpec]:
"""Return programs that are frontends (have build outputs)."""
return {
k: v
for k, v in self.components.items()
for k, v in self.programs.items()
if v.build and (v.build.outputs or v.build.commands)
}
@@ -94,11 +94,11 @@ def _read_secret(name: str) -> str:
return f"<MISSING_SECRET:{name}>"
def _parse_component(name: str, data: dict) -> ComponentSpec:
"""Parse a components: entry into a ComponentSpec."""
def _parse_program(name: str, data: dict) -> ProgramSpec:
"""Parse a programs: entry into a ProgramSpec."""
data_copy = dict(data)
data_copy["id"] = name
return ComponentSpec.model_validate(data_copy)
return ProgramSpec.model_validate(data_copy)
def _parse_service(name: str, data: dict) -> ServiceSpec:
@@ -130,9 +130,11 @@ def load_config(root: Path | None = None) -> CastleConfig:
gateway_data = data.get("gateway", {})
gateway = GatewayConfig(port=gateway_data.get("port", 9000))
components: dict[str, ComponentSpec] = {}
for name, comp_data in data.get("components", {}).items():
components[name] = _parse_component(name, comp_data)
programs: dict[str, ProgramSpec] = {}
# Support both "programs:" and legacy "components:" key
programs_data = data.get("programs") or data.get("components") or {}
for name, comp_data in programs_data.items():
programs[name] = _parse_program(name, comp_data)
services: dict[str, ServiceSpec] = {}
for name, svc_data in data.get("services", {}).items():
@@ -145,7 +147,7 @@ def load_config(root: Path | None = None) -> CastleConfig:
return CastleConfig(
root=root,
gateway=gateway,
components=components,
programs=programs,
services=services,
jobs=jobs,
)
@@ -187,7 +189,7 @@ _STRUCTURAL_KEYS = {
}
def _spec_to_yaml_dict(spec: ComponentSpec | ServiceSpec | JobSpec) -> dict:
def _spec_to_yaml_dict(spec: ProgramSpec | ServiceSpec | JobSpec) -> dict:
"""Serialize a spec to a YAML-friendly dict, preserving structural presence."""
exclude_fields = {"id"}
full = spec.model_dump(mode="json", exclude_none=True, exclude=exclude_fields)
@@ -228,10 +230,10 @@ def save_config(config: CastleConfig) -> None:
"""Save castle configuration to castle.yaml."""
data: dict = {"gateway": {"port": config.gateway.port}}
if config.components:
data["components"] = {}
for name, spec in config.components.items():
data["components"][name] = _spec_to_yaml_dict(spec)
if config.programs:
data["programs"] = {}
for name, spec in config.programs.items():
data["programs"][name] = _spec_to_yaml_dict(spec)
if config.services:
data["services"] = {}