refactor: Decouple roles.

This commit is contained in:
2026-02-23 01:49:24 -08:00
parent 72d35f2641
commit eeaa5045d0
55 changed files with 2144 additions and 1276 deletions

View File

@@ -7,7 +7,6 @@ import json
import logging
from castle_cli.config import load_config
from castle_cli.manifest import Role
log = logging.getLogger(__name__)
@@ -17,15 +16,15 @@ RESET = "\033[0m"
DIM = "\033[2m"
GREEN = "\033[92m"
RED = "\033[91m"
CYAN = "\033[96m"
MAGENTA = "\033[95m"
YELLOW = "\033[93m"
ROLE_COLORS: dict[str, str] = {
Role.SERVICE: "\033[92m", # green
Role.TOOL: "\033[96m", # cyan
Role.WORKER: "\033[94m", # blue
Role.JOB: "\033[95m", # magenta
Role.FRONTEND: "\033[93m", # yellow
Role.REMOTE: "\033[90m", # dim
Role.CONTAINERIZED: "\033[33m", # orange
CATEGORY_COLORS: dict[str, str] = {
"service": GREEN,
"job": MAGENTA,
"tool": CYAN,
"frontend": YELLOW,
}
@@ -41,71 +40,132 @@ def _load_deployed() -> dict[str, object] | None:
def run_list(args: argparse.Namespace) -> int:
"""List all components."""
"""List all components, services, and jobs."""
config = load_config()
deployed = _load_deployed()
components = config.components
filter_role = getattr(args, "role", None)
if filter_role:
components = {k: v for k, v in components.items() if filter_role in v.roles}
filter_type = getattr(args, "type", None)
if getattr(args, "json", False):
output = []
for name, manifest in components.items():
entry: dict = {
"name": name,
"roles": [r.value for r in manifest.roles],
"deployed": deployed is not None and name in deployed,
}
if manifest.description:
entry["description"] = manifest.description
if manifest.expose and manifest.expose.http:
entry["port"] = manifest.expose.http.internal.port
if deployed and name in deployed:
dep = deployed[name]
if dep.port is not None:
entry["port"] = dep.port
output.append(entry)
print(json.dumps(output, indent=2))
return 0
return _list_json(config, deployed, filter_type)
if not components:
any_output = False
# Services
if not filter_type or filter_type == "service":
if config.services:
any_output = True
color = CATEGORY_COLORS["service"]
print(f"\n{BOLD}{color}Services{RESET}")
print(f"{color}{'' * 40}{RESET}")
for name, svc in config.services.items():
port_str = ""
if svc.expose and svc.expose.http:
port_str = f" :{svc.expose.http.internal.port}"
if deployed is not None:
status = f"{GREEN}{RESET}" if name in deployed else f"{RED}{RESET}"
else:
status = f"{DIM}?{RESET}"
desc = f" {DIM}{svc.description}{RESET}" if svc.description else ""
print(f" {status} {BOLD}{name}{RESET}{port_str}{desc}")
# Jobs
if not filter_type or filter_type == "job":
if config.jobs:
any_output = True
color = CATEGORY_COLORS["job"]
print(f"\n{BOLD}{color}Jobs{RESET}")
print(f"{color}{'' * 40}{RESET}")
for name, job in config.jobs.items():
if deployed is not None:
status = f"{GREEN}{RESET}" if name in deployed else f"{RED}{RESET}"
else:
status = f"{DIM}?{RESET}"
desc = f" {DIM}{job.description}{RESET}" if job.description else ""
sched = f" {DIM}[{job.schedule}]{RESET}"
print(f" {status} {BOLD}{name}{RESET}{sched}{desc}")
# Tools
if not filter_type or filter_type == "tool":
tools = config.tools
if tools:
any_output = True
color = CATEGORY_COLORS["tool"]
print(f"\n{BOLD}{color}Tools{RESET}")
print(f"{color}{'' * 40}{RESET}")
for name, comp in tools.items():
desc = f" {DIM}{comp.description}{RESET}" if comp.description else ""
print(f" {BOLD}{name}{RESET}{desc}")
# Frontends
if not filter_type or filter_type == "frontend":
frontends = config.frontends
if frontends:
any_output = True
color = CATEGORY_COLORS["frontend"]
print(f"\n{BOLD}{color}Frontends{RESET}")
print(f"{color}{'' * 40}{RESET}")
for name, comp in frontends.items():
desc = f" {DIM}{comp.description}{RESET}" if comp.description else ""
print(f" {BOLD}{name}{RESET}{desc}")
if not any_output:
print("No components found.")
return 0
# Group by primary role (first in sorted list)
by_role: dict[str, list[tuple[str, object]]] = {}
for name, manifest in components.items():
primary_role = manifest.roles[0].value if manifest.roles else "other"
by_role.setdefault(primary_role, []).append((name, manifest))
# Display order
role_order = ["service", "tool", "worker", "job", "frontend", "remote", "containerized"]
for role_name in role_order:
items = by_role.get(role_name, [])
if not items:
continue
color = ROLE_COLORS.get(role_name, "")
print(f"\n{BOLD}{color}{role_name}s{RESET}")
print(f"{color}{'' * 40}{RESET}")
for name, manifest in items:
port_str = ""
if manifest.expose and manifest.expose.http:
port_str = f" :{manifest.expose.http.internal.port}"
# Show deployed status indicator
if deployed is not None:
status = f"{GREEN}{RESET}" if name in deployed else f"{RED}{RESET}"
else:
status = f"{DIM}?{RESET}"
desc = f" {DIM}{manifest.description}{RESET}" if manifest.description else ""
print(f" {status} {BOLD}{name}{RESET}{port_str}{desc}")
if deployed is None:
print(f"\n{DIM}(no registry — run 'castle deploy' to generate){RESET}")
print()
return 0
def _list_json(
config: object, deployed: dict | None, filter_type: str | None
) -> int:
"""Output JSON list of all entries."""
output = []
if not filter_type or filter_type == "service":
for name, svc in config.services.items():
entry: dict = {
"name": name,
"category": "service",
"deployed": deployed is not None and name in deployed,
}
if svc.description:
entry["description"] = svc.description
if svc.expose and svc.expose.http:
entry["port"] = svc.expose.http.internal.port
output.append(entry)
if not filter_type or filter_type == "job":
for name, job in config.jobs.items():
entry = {
"name": name,
"category": "job",
"deployed": deployed is not None and name in deployed,
"schedule": job.schedule,
}
if job.description:
entry["description"] = job.description
output.append(entry)
if not filter_type or filter_type == "tool":
for name, comp in config.tools.items():
entry = {"name": name, "category": "tool"}
if comp.description:
entry["description"] = comp.description
output.append(entry)
if not filter_type or filter_type == "frontend":
for name, comp in config.frontends.items():
entry = {"name": name, "category": "frontend"}
if comp.description:
entry["description"] = comp.description
output.append(entry)
print(json.dumps(output, indent=2))
return 0