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:
@@ -7,7 +7,7 @@ import argparse
|
||||
from castle_cli.config import load_config, save_config
|
||||
from castle_cli.manifest import (
|
||||
CaddySpec,
|
||||
ComponentSpec,
|
||||
ProgramSpec,
|
||||
ExposeSpec,
|
||||
HttpExposeSpec,
|
||||
HttpInternal,
|
||||
@@ -52,15 +52,15 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
stack = args.stack
|
||||
behavior = STACK_DEFAULTS.get(stack)
|
||||
|
||||
if name in config.components or name in config.services or name in config.jobs:
|
||||
if name in config.programs or name in config.services or name in config.jobs:
|
||||
print(f"Error: '{name}' already exists in castle.yaml")
|
||||
return 1
|
||||
|
||||
components_dir = config.root / "components"
|
||||
components_dir.mkdir(exist_ok=True)
|
||||
project_dir = components_dir / name
|
||||
programs_dir = config.root / "programs"
|
||||
programs_dir.mkdir(exist_ok=True)
|
||||
project_dir = programs_dir / name
|
||||
if project_dir.exists():
|
||||
print(f"Error: directory 'components/{name}' already exists")
|
||||
print(f"Error: directory 'programs/{name}' already exists")
|
||||
return 1
|
||||
|
||||
# Determine port for daemons
|
||||
@@ -77,17 +77,17 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
name=name,
|
||||
package_name=package_name,
|
||||
stack=stack,
|
||||
description=args.description or f"A castle {stack} component",
|
||||
description=args.description or f"A castle {stack} program",
|
||||
port=port,
|
||||
)
|
||||
|
||||
# Build entries
|
||||
if behavior == "daemon":
|
||||
# Component for software identity
|
||||
config.components[name] = ComponentSpec(
|
||||
# Program for software identity
|
||||
config.programs[name] = ProgramSpec(
|
||||
id=name,
|
||||
description=args.description or f"A castle {stack} component",
|
||||
source=f"components/{name}",
|
||||
description=args.description or f"A castle {stack} program",
|
||||
source=f"programs/{name}",
|
||||
stack=stack,
|
||||
)
|
||||
# Service for deployment
|
||||
@@ -105,31 +105,31 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
)
|
||||
elif behavior == "tool":
|
||||
config.components[name] = ComponentSpec(
|
||||
config.programs[name] = ProgramSpec(
|
||||
id=name,
|
||||
description=args.description or f"A castle {stack} component",
|
||||
source=f"components/{name}",
|
||||
description=args.description or f"A castle {stack} program",
|
||||
source=f"programs/{name}",
|
||||
stack=stack,
|
||||
tool=ToolSpec(),
|
||||
install=InstallSpec(path=PathInstallSpec(alias=name)),
|
||||
)
|
||||
else:
|
||||
# frontend or other
|
||||
config.components[name] = ComponentSpec(
|
||||
config.programs[name] = ProgramSpec(
|
||||
id=name,
|
||||
description=args.description or f"A castle {stack} component",
|
||||
source=f"components/{name}",
|
||||
description=args.description or f"A castle {stack} program",
|
||||
source=f"programs/{name}",
|
||||
stack=stack,
|
||||
)
|
||||
|
||||
save_config(config)
|
||||
|
||||
print(f"Created {stack} component '{name}' at {project_dir}")
|
||||
print(f"Created {stack} program '{name}' at {project_dir}")
|
||||
if port:
|
||||
print(f" Port: {port}")
|
||||
print(" Registered in castle.yaml")
|
||||
print("\nNext steps:")
|
||||
print(f" cd components/{name}")
|
||||
print(f" cd programs/{name}")
|
||||
print(" uv sync")
|
||||
if behavior == "daemon":
|
||||
print(f" uv run {name} # starts on port {port}")
|
||||
|
||||
@@ -37,16 +37,16 @@ SYSTEMD_USER_DIR = Path.home() / ".config" / "systemd" / "user"
|
||||
|
||||
|
||||
def run_deploy(args: argparse.Namespace) -> int:
|
||||
"""Deploy components from castle.yaml to ~/.castle/."""
|
||||
"""Deploy from castle.yaml to ~/.castle/."""
|
||||
config = load_config()
|
||||
target_name = getattr(args, "component", None)
|
||||
target_name = getattr(args, "name", None)
|
||||
|
||||
ensure_dirs()
|
||||
|
||||
# Build node config
|
||||
node = NodeConfig(castle_root=str(config.root), gateway_port=config.gateway.port)
|
||||
|
||||
# Load existing registry to preserve components not being redeployed,
|
||||
# Load existing registry to preserve entries not being redeployed,
|
||||
# or start fresh if deploying all
|
||||
if target_name and REGISTRY_PATH.exists():
|
||||
try:
|
||||
@@ -96,24 +96,24 @@ def run_deploy(args: argparse.Namespace) -> int:
|
||||
# Reload systemd daemon
|
||||
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
|
||||
|
||||
print(f"\nDeployed {deployed_count} component(s).")
|
||||
print(f"\nDeployed {deployed_count} item(s).")
|
||||
print("Run 'castle services start' to start all services.")
|
||||
return 0
|
||||
|
||||
|
||||
def _env_prefix(name: str) -> str:
|
||||
"""Derive env var prefix from component name: central-context → CENTRAL_CONTEXT."""
|
||||
"""Derive env var prefix from name: central-context → CENTRAL_CONTEXT."""
|
||||
return name.replace("-", "_").upper()
|
||||
|
||||
|
||||
def _resolve_description(
|
||||
config: CastleConfig, spec: ServiceSpec | JobSpec
|
||||
) -> str | None:
|
||||
"""Get description, falling through to component if referenced."""
|
||||
"""Get description, falling through to program if referenced."""
|
||||
if spec.description:
|
||||
return spec.description
|
||||
if spec.component and spec.component in config.components:
|
||||
return config.components[spec.component].description
|
||||
if spec.component and spec.component in config.programs:
|
||||
return config.programs[spec.component].description
|
||||
return None
|
||||
|
||||
|
||||
@@ -155,10 +155,10 @@ def _build_deployed_service(
|
||||
if svc.proxy and svc.proxy.caddy and svc.proxy.caddy.enable:
|
||||
proxy_path = svc.proxy.caddy.path_prefix or f"/{name}"
|
||||
|
||||
# Resolve stack from referenced component
|
||||
# Resolve stack from referenced program
|
||||
stack = None
|
||||
if svc.component and svc.component in config.components:
|
||||
stack = config.components[svc.component].stack
|
||||
if svc.component and svc.component in config.programs:
|
||||
stack = config.programs[svc.component].stack
|
||||
|
||||
return DeployedComponent(
|
||||
runner=run.runner,
|
||||
@@ -195,10 +195,10 @@ def _build_deployed_job(
|
||||
# Build run_cmd
|
||||
run_cmd = _build_run_cmd(run, env)
|
||||
|
||||
# Resolve stack from referenced component
|
||||
# Resolve stack from referenced program
|
||||
stack = None
|
||||
if job.component and job.component in config.components:
|
||||
stack = config.components[job.component].stack
|
||||
if job.component and job.component in config.programs:
|
||||
stack = config.programs[job.component].stack
|
||||
|
||||
return DeployedComponent(
|
||||
runner=run.runner,
|
||||
@@ -275,10 +275,10 @@ def _print_deployed(name: str, deployed: DeployedComponent) -> None:
|
||||
|
||||
def _copy_app_static(config: CastleConfig) -> None:
|
||||
"""Copy castle-app build output to ~/.castle/static/castle-app/."""
|
||||
if "castle-app" not in config.components:
|
||||
if "castle-app" not in config.programs:
|
||||
return
|
||||
|
||||
comp = config.components["castle-app"]
|
||||
comp = config.programs["castle-app"]
|
||||
if not (comp.build and comp.build.outputs):
|
||||
return
|
||||
|
||||
|
||||
@@ -3,37 +3,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
|
||||
from castle_core.stacks import get_handler
|
||||
|
||||
from castle_cli.config import CastleConfig, load_config
|
||||
|
||||
|
||||
def _get_project_dir(config: CastleConfig, project_name: str) -> Path:
|
||||
"""Get the directory for a component."""
|
||||
if project_name not in config.components:
|
||||
raise ValueError(f"Unknown component: {project_name}")
|
||||
manifest = config.components[project_name]
|
||||
working_dir = manifest.source_dir or project_name
|
||||
return config.root / working_dir
|
||||
def _run_action(config: CastleConfig, project_name: str, action: str) -> bool:
|
||||
"""Run a stack action for a single project. Returns True on success."""
|
||||
if project_name not in config.programs:
|
||||
print(f"Unknown component: {project_name}")
|
||||
return False
|
||||
|
||||
comp = config.programs[project_name]
|
||||
if not comp.source:
|
||||
print(f" {project_name}: no source directory, skipping")
|
||||
return True
|
||||
|
||||
def _has_pyproject(project_dir: Path) -> bool:
|
||||
"""Check if a project directory has a pyproject.toml."""
|
||||
return (project_dir / "pyproject.toml").exists()
|
||||
handler = get_handler(comp.stack)
|
||||
if handler is None:
|
||||
print(f" {project_name}: unsupported stack '{comp.stack}', skipping")
|
||||
return True
|
||||
|
||||
|
||||
def _run_in_project(project_dir: Path, cmd: list[str], label: str) -> bool:
|
||||
"""Run a command in a project directory. Returns True on success."""
|
||||
if not _has_pyproject(project_dir):
|
||||
return True # Skip projects without pyproject.toml
|
||||
method_name = action.replace("-", "_")
|
||||
method = getattr(handler, method_name, None)
|
||||
if method is None:
|
||||
print(f" {project_name}: action '{action}' not supported")
|
||||
return False
|
||||
|
||||
print(f"\n{'─' * 40}")
|
||||
print(f" {label}: {project_dir.name}")
|
||||
print(f" {action}: {project_name}")
|
||||
print(f"{'─' * 40}")
|
||||
|
||||
result = subprocess.run(cmd, cwd=project_dir)
|
||||
return result.returncode == 0
|
||||
result = asyncio.run(method(project_name, comp, config.root))
|
||||
if result.output:
|
||||
print(result.output)
|
||||
|
||||
return result.status == "ok"
|
||||
|
||||
|
||||
def run_test(args: argparse.Namespace) -> int:
|
||||
@@ -41,34 +48,23 @@ def run_test(args: argparse.Namespace) -> int:
|
||||
config = load_config()
|
||||
|
||||
if args.project:
|
||||
project_dir = _get_project_dir(config, args.project)
|
||||
tests_dir = project_dir / "tests"
|
||||
if not tests_dir.exists():
|
||||
print(f"No tests directory found for {args.project}")
|
||||
return 1
|
||||
success = _run_in_project(
|
||||
project_dir,
|
||||
["uv", "run", "pytest", "tests/", "-v"],
|
||||
"Testing",
|
||||
)
|
||||
success = _run_action(config, args.project, "test")
|
||||
return 0 if success else 1
|
||||
|
||||
# Run all
|
||||
all_passed = True
|
||||
for name, manifest in config.components.items():
|
||||
working_dir = manifest.source_dir or name
|
||||
project_dir = config.root / working_dir
|
||||
tests_dir = project_dir / "tests"
|
||||
if not tests_dir.exists():
|
||||
for name, comp in config.programs.items():
|
||||
if not comp.source:
|
||||
continue
|
||||
if not _has_pyproject(project_dir):
|
||||
handler = get_handler(comp.stack)
|
||||
if handler is None:
|
||||
continue
|
||||
success = _run_in_project(
|
||||
project_dir,
|
||||
["uv", "run", "pytest", "tests/", "-v"],
|
||||
"Testing",
|
||||
)
|
||||
if not success:
|
||||
# Skip projects without a tests directory (python) or test script (node)
|
||||
source_dir = config.root / comp.source
|
||||
if comp.stack in ("python-cli", "python-fastapi"):
|
||||
if not (source_dir / "tests").exists():
|
||||
continue
|
||||
if not _run_action(config, name, "test"):
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
@@ -83,27 +79,18 @@ def run_lint(args: argparse.Namespace) -> int:
|
||||
config = load_config()
|
||||
|
||||
if args.project:
|
||||
project_dir = _get_project_dir(config, args.project)
|
||||
success = _run_in_project(
|
||||
project_dir,
|
||||
["uv", "run", "ruff", "check", "."],
|
||||
"Linting",
|
||||
)
|
||||
success = _run_action(config, args.project, "lint")
|
||||
return 0 if success else 1
|
||||
|
||||
# Run all
|
||||
all_passed = True
|
||||
for name, manifest in config.components.items():
|
||||
working_dir = manifest.source_dir or name
|
||||
project_dir = config.root / working_dir
|
||||
if not _has_pyproject(project_dir):
|
||||
for name, comp in config.programs.items():
|
||||
if not comp.source:
|
||||
continue
|
||||
success = _run_in_project(
|
||||
project_dir,
|
||||
["uv", "run", "ruff", "check", "."],
|
||||
"Linting",
|
||||
)
|
||||
if not success:
|
||||
handler = get_handler(comp.stack)
|
||||
if handler is None:
|
||||
continue
|
||||
if not _run_action(config, name, "lint"):
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""castle info - show detailed component information."""
|
||||
"""castle info - show detailed program information."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,8 +17,8 @@ GREEN = "\033[92m"
|
||||
RED = "\033[91m"
|
||||
|
||||
|
||||
def _load_deployed_component(name: str) -> object | None:
|
||||
"""Try to load a specific deployed component from registry."""
|
||||
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
|
||||
|
||||
@@ -29,23 +29,23 @@ def _load_deployed_component(name: str) -> object | None:
|
||||
|
||||
|
||||
def run_info(args: argparse.Namespace) -> int:
|
||||
"""Show detailed info for a component, service, or job."""
|
||||
"""Show detailed info for a program, service, or job."""
|
||||
config = load_config()
|
||||
name = args.project
|
||||
|
||||
# Look up in all sections
|
||||
component = config.components.get(name)
|
||||
program = config.programs.get(name)
|
||||
service = config.services.get(name)
|
||||
job = config.jobs.get(name)
|
||||
|
||||
if not component and not service and not job:
|
||||
if not program and not service and not job:
|
||||
print(f"Error: '{name}' not found in castle.yaml")
|
||||
return 1
|
||||
|
||||
deployed = _load_deployed_component(name)
|
||||
deployed = _load_deployed_program(name)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
return _info_json(config, name, component, service, job, deployed)
|
||||
return _info_json(config, name, program, service, job, deployed)
|
||||
|
||||
# Human-readable output
|
||||
print(f"\n{BOLD}{name}{RESET}")
|
||||
@@ -56,51 +56,51 @@ def run_info(args: argparse.Namespace) -> int:
|
||||
print(f" {BOLD}behavior{RESET}: daemon")
|
||||
elif job:
|
||||
print(f" {BOLD}behavior{RESET}: tool")
|
||||
elif component:
|
||||
if component.tool or (component.install and component.install.path):
|
||||
elif program:
|
||||
if program.tool or (program.install and program.install.path):
|
||||
print(f" {BOLD}behavior{RESET}: tool")
|
||||
elif component.build:
|
||||
elif program.build:
|
||||
print(f" {BOLD}behavior{RESET}: frontend")
|
||||
else:
|
||||
print(f" {BOLD}behavior{RESET}: —")
|
||||
|
||||
# Show stack
|
||||
stack = None
|
||||
if component and component.stack:
|
||||
stack = component.stack
|
||||
elif service and service.component and service.component in config.components:
|
||||
stack = config.components[service.component].stack
|
||||
elif job and job.component and job.component in config.components:
|
||||
stack = config.components[job.component].stack
|
||||
if program and program.stack:
|
||||
stack = program.stack
|
||||
elif service and service.component and service.component in config.programs:
|
||||
stack = config.programs[service.component].stack
|
||||
elif job and job.component and job.component in config.programs:
|
||||
stack = config.programs[job.component].stack
|
||||
if stack:
|
||||
print(f" {BOLD}stack{RESET}: {stack}")
|
||||
|
||||
# Component info
|
||||
if component:
|
||||
if component.description:
|
||||
print(f" {BOLD}description{RESET}: {component.description}")
|
||||
if component.source:
|
||||
print(f" {BOLD}source{RESET}: {component.source}")
|
||||
if component.install and component.install.path:
|
||||
pi = component.install.path
|
||||
# Program info
|
||||
if program:
|
||||
if program.description:
|
||||
print(f" {BOLD}description{RESET}: {program.description}")
|
||||
if program.source:
|
||||
print(f" {BOLD}source{RESET}: {program.source}")
|
||||
if program.install and program.install.path:
|
||||
pi = program.install.path
|
||||
print(f" {BOLD}install{RESET}: path" + (f" (alias: {pi.alias})" if pi.alias else ""))
|
||||
if component.tool:
|
||||
t = component.tool
|
||||
if program.tool:
|
||||
t = program.tool
|
||||
if t.system_dependencies:
|
||||
print(f" {BOLD}requires{RESET}: {', '.join(t.system_dependencies)}")
|
||||
if component.tags:
|
||||
print(f" {BOLD}tags{RESET}: {', '.join(component.tags)}")
|
||||
if program.tags:
|
||||
print(f" {BOLD}tags{RESET}: {', '.join(program.tags)}")
|
||||
|
||||
# Service info
|
||||
spec = service or job
|
||||
if spec:
|
||||
desc = spec.description
|
||||
if not desc and spec.component and spec.component in config.components:
|
||||
desc = config.components[spec.component].description
|
||||
if desc and not (component and component.description == desc):
|
||||
if not desc and spec.component and spec.component in config.programs:
|
||||
desc = config.programs[spec.component].description
|
||||
if desc and not (program and program.description == desc):
|
||||
print(f" {BOLD}description{RESET}: {desc}")
|
||||
if spec.component:
|
||||
print(f" {BOLD}component{RESET}: {spec.component}")
|
||||
print(f" {BOLD}program{RESET}: {spec.component}")
|
||||
|
||||
# Run spec
|
||||
print(f" {BOLD}runner{RESET}: {spec.run.runner}")
|
||||
@@ -154,10 +154,10 @@ def run_info(args: argparse.Namespace) -> int:
|
||||
|
||||
# Show CLAUDE.md if it exists
|
||||
source_dir = None
|
||||
if component and component.source_dir:
|
||||
source_dir = component.source_dir
|
||||
elif spec and spec.component and spec.component in config.components:
|
||||
source_dir = config.components[spec.component].source_dir
|
||||
if program and program.source_dir:
|
||||
source_dir = program.source_dir
|
||||
elif spec and spec.component and spec.component in config.programs:
|
||||
source_dir = config.programs[spec.component].source_dir
|
||||
|
||||
if source_dir:
|
||||
claude_md = _find_claude_md(config.root, source_dir)
|
||||
@@ -173,7 +173,7 @@ def run_info(args: argparse.Namespace) -> int:
|
||||
def _info_json(
|
||||
config: object,
|
||||
name: str,
|
||||
component: object | None,
|
||||
program: object | None,
|
||||
service: object | None,
|
||||
job: object | None,
|
||||
deployed: object | None,
|
||||
@@ -181,28 +181,28 @@ def _info_json(
|
||||
"""Output JSON info."""
|
||||
data: dict = {"name": name}
|
||||
|
||||
if component:
|
||||
data["component"] = component.model_dump(exclude_none=True, exclude={"id"})
|
||||
if program:
|
||||
data["program"] = program.model_dump(exclude_none=True, exclude={"id"})
|
||||
if service:
|
||||
data["service"] = service.model_dump(exclude_none=True, exclude={"id"})
|
||||
data["behavior"] = "daemon"
|
||||
if job:
|
||||
data["job"] = job.model_dump(exclude_none=True, exclude={"id"})
|
||||
data["behavior"] = "tool"
|
||||
if not service and not job and component:
|
||||
if component.tool or (component.install and component.install.path):
|
||||
if not service and not job and program:
|
||||
if program.tool or (program.install and program.install.path):
|
||||
data["behavior"] = "tool"
|
||||
elif component.build:
|
||||
elif program.build:
|
||||
data["behavior"] = "frontend"
|
||||
|
||||
# Resolve stack
|
||||
stack = None
|
||||
if component and component.stack:
|
||||
stack = component.stack
|
||||
elif service and service.component and service.component in config.components:
|
||||
stack = config.components[service.component].stack
|
||||
elif job and job.component and job.component in config.components:
|
||||
stack = config.components[job.component].stack
|
||||
if program and program.stack:
|
||||
stack = program.stack
|
||||
elif service and service.component and service.component in config.programs:
|
||||
stack = config.programs[service.component].stack
|
||||
elif job and job.component and job.component in config.programs:
|
||||
stack = config.programs[job.component].stack
|
||||
if stack:
|
||||
data["stack"] = stack
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""castle list - show all registered components."""
|
||||
"""castle list - show all registered programs, services, and jobs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -50,27 +50,27 @@ def _load_deployed() -> dict[str, object] | None:
|
||||
|
||||
|
||||
def _resolve_stack(config: object, name: str) -> str | None:
|
||||
"""Resolve stack from component reference or direct component."""
|
||||
# Check services for component ref
|
||||
"""Resolve stack from program reference or direct program."""
|
||||
# Check services for program ref
|
||||
if name in config.services:
|
||||
svc = config.services[name]
|
||||
comp_name = svc.component
|
||||
if comp_name and comp_name in config.components:
|
||||
return config.components[comp_name].stack
|
||||
# Check jobs for component ref
|
||||
if comp_name and comp_name in config.programs:
|
||||
return config.programs[comp_name].stack
|
||||
# Check jobs for program ref
|
||||
if name in config.jobs:
|
||||
job = config.jobs[name]
|
||||
comp_name = job.component
|
||||
if comp_name and comp_name in config.components:
|
||||
return config.components[comp_name].stack
|
||||
# Direct component
|
||||
if name in config.components:
|
||||
return config.components[name].stack
|
||||
if comp_name and comp_name in config.programs:
|
||||
return config.programs[comp_name].stack
|
||||
# Direct program
|
||||
if name in config.programs:
|
||||
return config.programs[name].stack
|
||||
return None
|
||||
|
||||
|
||||
def run_list(args: argparse.Namespace) -> int:
|
||||
"""List all components, services, and jobs."""
|
||||
"""List all programs, services, and jobs."""
|
||||
config = load_config()
|
||||
deployed = _load_deployed()
|
||||
|
||||
@@ -123,12 +123,12 @@ def run_list(args: argparse.Namespace) -> int:
|
||||
sched = f" {DIM}[{job.schedule}]{RESET}"
|
||||
print(f" {status} {BOLD}{name}{RESET}{sched}{desc}")
|
||||
|
||||
# Components (tools, frontends, etc.)
|
||||
# Programs (tools, frontends, etc.)
|
||||
show_tools = not filter_behavior or filter_behavior == "tool"
|
||||
show_frontends = not filter_behavior or filter_behavior == "frontend"
|
||||
|
||||
if show_tools or show_frontends:
|
||||
# Collect non-daemon components
|
||||
# Collect non-daemon programs
|
||||
comps: dict[str, tuple[str, str | None, str | None]] = {}
|
||||
|
||||
if show_tools:
|
||||
@@ -147,7 +147,7 @@ def run_list(args: argparse.Namespace) -> int:
|
||||
if comps:
|
||||
any_output = True
|
||||
color = CYAN
|
||||
print(f"\n{BOLD}{color}Components{RESET}")
|
||||
print(f"\n{BOLD}{color}Programs{RESET}")
|
||||
print(f"{color}{'─' * 40}{RESET}")
|
||||
for name, (behavior, stack, description) in comps.items():
|
||||
stack_str = f" {DIM}{stack}{RESET}" if stack else ""
|
||||
@@ -156,7 +156,7 @@ def run_list(args: argparse.Namespace) -> int:
|
||||
print(f" {BOLD}{name}{RESET}{stack_str}{behavior_str}{desc}")
|
||||
|
||||
if not any_output:
|
||||
print("No components found.")
|
||||
print("No programs found.")
|
||||
|
||||
if deployed is None:
|
||||
print(f"\n{DIM}(no registry — run 'castle deploy' to generate){RESET}")
|
||||
|
||||
@@ -28,7 +28,7 @@ def run_sync(args: argparse.Namespace) -> int:
|
||||
all_ok = True
|
||||
synced_dirs: set[Path] = set()
|
||||
|
||||
for name, comp in config.components.items():
|
||||
for name, comp in config.programs.items():
|
||||
source_dir = comp.source_dir
|
||||
if not source_dir:
|
||||
continue
|
||||
@@ -63,7 +63,7 @@ def run_sync(args: argparse.Namespace) -> int:
|
||||
installed_dirs: set[Path] = set()
|
||||
|
||||
# Install components with install.path
|
||||
for name, comp in config.components.items():
|
||||
for name, comp in config.programs.items():
|
||||
if not (comp.install and comp.install.path):
|
||||
continue
|
||||
source = comp.source_dir
|
||||
@@ -77,8 +77,8 @@ def run_sync(args: argparse.Namespace) -> int:
|
||||
continue
|
||||
# Find source from component reference
|
||||
source = None
|
||||
if svc.component and svc.component in config.components:
|
||||
source = config.components[svc.component].source_dir
|
||||
if svc.component and svc.component in config.programs:
|
||||
source = config.programs[svc.component].source_dir
|
||||
if not source:
|
||||
continue
|
||||
source_dir = config.root / source
|
||||
|
||||
@@ -29,7 +29,7 @@ def run_tool(args: argparse.Namespace) -> int:
|
||||
def _tool_list() -> int:
|
||||
"""List all registered tools."""
|
||||
config = load_config()
|
||||
tools = {k: v for k, v in config.components.items() if v.tool}
|
||||
tools = {k: v for k, v in config.programs.items() if v.tool}
|
||||
|
||||
if not tools:
|
||||
print("No tools registered.")
|
||||
@@ -51,11 +51,11 @@ def _tool_list() -> int:
|
||||
def _tool_info(name: str) -> int:
|
||||
"""Show detailed info about a tool, including .md documentation."""
|
||||
config = load_config()
|
||||
if name not in config.components:
|
||||
if name not in config.programs:
|
||||
print(f"Error: '{name}' not found")
|
||||
return 1
|
||||
|
||||
manifest = config.components[name]
|
||||
manifest = config.programs[name]
|
||||
if not manifest.tool:
|
||||
print(f"Error: '{name}' is not a tool")
|
||||
return 1
|
||||
|
||||
@@ -19,7 +19,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# castle list
|
||||
list_parser = subparsers.add_parser("list", help="List all components")
|
||||
list_parser = subparsers.add_parser("list", help="List all programs, services, and jobs")
|
||||
list_parser.add_argument(
|
||||
"--behavior",
|
||||
choices=["daemon", "tool", "frontend"],
|
||||
@@ -43,8 +43,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
create_parser.add_argument("--description", default="", help="Project description")
|
||||
create_parser.add_argument("--port", type=int, help="Port number (daemons only)")
|
||||
# castle info
|
||||
info_parser = subparsers.add_parser("info", help="Show component details")
|
||||
info_parser.add_argument("project", help="Component name")
|
||||
info_parser = subparsers.add_parser("info", help="Show program details")
|
||||
info_parser.add_argument("project", help="Program, service, or job name")
|
||||
info_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
|
||||
# castle test
|
||||
@@ -91,25 +91,25 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
services_sub.add_parser("stop", help="Stop all services and gateway")
|
||||
|
||||
# castle logs
|
||||
logs_parser = subparsers.add_parser("logs", help="View component logs")
|
||||
logs_parser.add_argument("name", help="Component name")
|
||||
logs_parser = subparsers.add_parser("logs", help="View service/job logs")
|
||||
logs_parser.add_argument("name", help="Service or job name")
|
||||
logs_parser.add_argument("-f", "--follow", action="store_true", help="Follow log output")
|
||||
logs_parser.add_argument(
|
||||
"-n", "--lines", type=int, default=50, help="Number of lines to show (default: 50)"
|
||||
)
|
||||
|
||||
# castle run
|
||||
run_parser = subparsers.add_parser("run", help="Run a component in the foreground")
|
||||
run_parser.add_argument("name", help="Component name")
|
||||
run_parser = subparsers.add_parser("run", help="Run a service in the foreground")
|
||||
run_parser.add_argument("name", help="Service name")
|
||||
run_parser.add_argument(
|
||||
"extra", nargs=argparse.REMAINDER, help="Extra arguments passed to the component"
|
||||
"extra", nargs=argparse.REMAINDER, help="Extra arguments passed to the service"
|
||||
)
|
||||
|
||||
# castle deploy
|
||||
deploy_parser = subparsers.add_parser(
|
||||
"deploy", help="Deploy components to ~/.castle/ (spec → runtime)"
|
||||
"deploy", help="Deploy to ~/.castle/ (spec → runtime)"
|
||||
)
|
||||
deploy_parser.add_argument("component", nargs="?", help="Component to deploy (default: all)")
|
||||
deploy_parser.add_argument("name", nargs="?", help="Service or job to deploy (default: all)")
|
||||
|
||||
# castle tool
|
||||
tool_parser = subparsers.add_parser("tool", help="Manage tools")
|
||||
|
||||
@@ -5,7 +5,7 @@ from castle_core.manifest import ( # noqa: F401 — explicit re-exports for typ
|
||||
BuildSpec,
|
||||
CaddySpec,
|
||||
Capability,
|
||||
ComponentSpec,
|
||||
ProgramSpec,
|
||||
DefaultsSpec,
|
||||
EnvMap,
|
||||
ExposeSpec,
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestCreateCommand:
|
||||
result = run_create(args)
|
||||
|
||||
assert result == 0
|
||||
project_dir = castle_root / "components" / "my-api"
|
||||
project_dir = castle_root / "programs" / "my-api"
|
||||
assert project_dir.exists()
|
||||
assert (project_dir / "pyproject.toml").exists()
|
||||
assert (project_dir / "src" / "my_api" / "main.py").exists()
|
||||
@@ -41,8 +41,8 @@ class TestCreateCommand:
|
||||
assert (project_dir / "tests" / "test_health.py").exists()
|
||||
assert (project_dir / "CLAUDE.md").exists()
|
||||
|
||||
# Verify registered as ComponentSpec + ServiceSpec
|
||||
assert "my-api" in config.components
|
||||
# Verify registered as ProgramSpec + ServiceSpec
|
||||
assert "my-api" in config.programs
|
||||
assert "my-api" in config.services
|
||||
svc = config.services["my-api"]
|
||||
assert svc.expose.http.internal.port == 9050
|
||||
@@ -64,12 +64,12 @@ class TestCreateCommand:
|
||||
result = run_create(args)
|
||||
|
||||
assert result == 0
|
||||
project_dir = castle_root / "components" / "my-tool2"
|
||||
project_dir = castle_root / "programs" / "my-tool2"
|
||||
assert project_dir.exists()
|
||||
assert (project_dir / "src" / "my_tool2" / "main.py").exists()
|
||||
assert (project_dir / "CLAUDE.md").exists()
|
||||
assert "my-tool2" in config.components
|
||||
comp = config.components["my-tool2"]
|
||||
assert "my-tool2" in config.programs
|
||||
comp = config.programs["my-tool2"]
|
||||
assert comp.tool is not None
|
||||
assert comp.install is not None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user