refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application

This commit is contained in:
2026-02-23 22:56:18 -08:00
parent 0d36e4f72a
commit efab2a7893
27 changed files with 258 additions and 393 deletions

View File

@@ -11,14 +11,11 @@ from castle_cli.manifest import (
ExposeSpec,
HttpExposeSpec,
HttpInternal,
InstallSpec,
ManageSpec,
PathInstallSpec,
ProxySpec,
RunPython,
ServiceSpec,
SystemdSpec,
ToolSpec,
)
from castle_cli.templates.scaffold import scaffold_project
@@ -82,19 +79,18 @@ def run_create(args: argparse.Namespace) -> int:
)
# Build entries
config.programs[name] = ProgramSpec(
id=name,
description=args.description or f"A castle {stack} program",
source=f"programs/{name}",
stack=stack,
behavior=behavior,
)
if behavior == "daemon":
# Program for software identity
config.programs[name] = ProgramSpec(
id=name,
description=args.description or f"A castle {stack} program",
source=f"programs/{name}",
stack=stack,
)
# Service for deployment
config.services[name] = ServiceSpec(
id=name,
component=name,
run=RunPython(runner="python", tool=name),
run=RunPython(runner="python", program=name),
expose=ExposeSpec(
http=HttpExposeSpec(
internal=HttpInternal(port=port),
@@ -104,23 +100,6 @@ def run_create(args: argparse.Namespace) -> int:
proxy=ProxySpec(caddy=CaddySpec(path_prefix=f"/{name}")),
manage=ManageSpec(systemd=SystemdSpec()),
)
elif behavior == "tool":
config.programs[name] = ProgramSpec(
id=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.programs[name] = ProgramSpec(
id=name,
description=args.description or f"A castle {stack} program",
source=f"programs/{name}",
stack=stack,
)
save_config(config)

View File

@@ -216,13 +216,13 @@ def _build_run_cmd(run: object, env: dict[str, str]) -> list[str]:
"""Build a run command list from a RunSpec."""
match run.runner:
case "python":
resolved = shutil.which(run.tool)
resolved = shutil.which(run.program)
if not resolved:
print(
f" Warning: '{run.tool}' not on PATH. "
f" Warning: '{run.program}' not on PATH. "
f"Install with: uv tool install --editable <source>"
)
cmd = [resolved or run.tool]
cmd = [resolved or run.program]
if run.args:
cmd.extend(run.args)
return cmd

View File

@@ -52,17 +52,15 @@ def run_info(args: argparse.Namespace) -> int:
print(f"{'' * 40}")
# Determine behavior
if service:
print(f" {BOLD}behavior{RESET}: daemon")
behavior = None
if program and program.behavior:
behavior = program.behavior
elif service:
behavior = "daemon"
elif job:
print(f" {BOLD}behavior{RESET}: tool")
elif program:
if program.tool or (program.install and program.install.path):
print(f" {BOLD}behavior{RESET}: tool")
elif program.build:
print(f" {BOLD}behavior{RESET}: frontend")
else:
print(f" {BOLD}behavior{RESET}: —")
behavior = "tool"
if behavior:
print(f" {BOLD}behavior{RESET}: {behavior}")
# Show stack
stack = None
@@ -81,13 +79,8 @@ def run_info(args: argparse.Namespace) -> int:
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 program.tool:
t = program.tool
if t.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(t.system_dependencies)}")
if program.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(program.system_dependencies)}")
if program.tags:
print(f" {BOLD}tags{RESET}: {', '.join(program.tags)}")
@@ -104,8 +97,8 @@ def run_info(args: argparse.Namespace) -> int:
# Run spec
print(f" {BOLD}runner{RESET}: {spec.run.runner}")
if hasattr(spec.run, "tool"):
print(f" {BOLD}tool{RESET}: {spec.run.tool}")
if hasattr(spec.run, "program"):
print(f" {BOLD}program{RESET}: {spec.run.program}")
elif hasattr(spec.run, "argv"):
print(f" {BOLD}argv{RESET}: {spec.run.argv}")
elif hasattr(spec.run, "image"):
@@ -185,15 +178,14 @@ def _info_json(
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"})
if program and program.behavior:
data["behavior"] = program.behavior
elif service:
data["behavior"] = "daemon"
elif job:
data["behavior"] = "tool"
if not service and not job and program:
if program.tool or (program.install and program.install.path):
data["behavior"] = "tool"
elif program.build:
data["behavior"] = "frontend"
# Resolve stack
stack = None

View File

@@ -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.programs.items() if v.tool}
tools = {k: v for k, v in config.programs.items() if v.behavior == "tool"}
if not tools:
print("No tools registered.")
@@ -40,8 +40,8 @@ def _tool_list() -> int:
for name, manifest in sorted(tools.items()):
desc = manifest.description or ""
deps = ""
if manifest.tool and manifest.tool.system_dependencies:
deps = f" {DIM}[{', '.join(manifest.tool.system_dependencies)}]{RESET}"
if manifest.system_dependencies:
deps = f" {DIM}[{', '.join(manifest.system_dependencies)}]{RESET}"
print(f" {BOLD}{name:<20}{RESET} {desc}{deps}")
print()
@@ -56,21 +56,20 @@ def _tool_info(name: str) -> int:
return 1
manifest = config.programs[name]
if not manifest.tool:
if manifest.behavior != "tool":
print(f"Error: '{name}' is not a tool")
return 1
t = manifest.tool
print(f"\n{BOLD}{name}{RESET}")
print(f"{'' * 40}")
if manifest.description:
print(f" {manifest.description}")
print(f" {BOLD}version{RESET}: {t.version}")
if manifest.version:
print(f" {BOLD}version{RESET}: {manifest.version}")
if manifest.source:
print(f" {BOLD}source{RESET}: {manifest.source}")
if t.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(t.system_dependencies)}")
if manifest.system_dependencies:
print(f" {BOLD}requires{RESET}: {', '.join(manifest.system_dependencies)}")
print()
return 0

View File

@@ -12,10 +12,8 @@ from castle_core.manifest import ( # noqa: F401 — explicit re-exports for typ
HttpExposeSpec,
HttpInternal,
HttpPublic,
InstallSpec,
JobSpec,
ManageSpec,
PathInstallSpec,
ProxySpec,
ReadinessHttpGet,
RestartPolicy,
@@ -29,5 +27,4 @@ from castle_core.manifest import ( # noqa: F401 — explicit re-exports for typ
ServiceSpec,
SystemdSpec,
TLSMode,
ToolSpec,
)