refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -15,12 +15,10 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
castle_yaml = tmp_path / "castle.yaml"
|
||||
config = {
|
||||
"gateway": {"port": 18000},
|
||||
"components": {
|
||||
"programs": {
|
||||
"test-tool": {
|
||||
"description": "Test tool",
|
||||
"install": {
|
||||
"path": {"alias": "test-tool"},
|
||||
},
|
||||
"behavior": "tool",
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
@@ -29,7 +27,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
"description": "Test service",
|
||||
"run": {
|
||||
"runner": "python",
|
||||
"tool": "test-svc",
|
||||
"program": "test-svc",
|
||||
},
|
||||
"defaults": {
|
||||
"env": {"TEST_SVC_DATA_DIR": str(tmp_path / "data" / "test-svc")},
|
||||
|
||||
@@ -70,8 +70,7 @@ class TestCreateCommand:
|
||||
assert (project_dir / "CLAUDE.md").exists()
|
||||
assert "my-tool2" in config.programs
|
||||
comp = config.programs["my-tool2"]
|
||||
assert comp.tool is not None
|
||||
assert comp.install is not None
|
||||
assert comp.behavior == "tool"
|
||||
|
||||
def test_create_duplicate_fails(self, castle_root: Path, capsys: object) -> None:
|
||||
"""Creating a project with existing name fails."""
|
||||
|
||||
Reference in New Issue
Block a user