refactor: Decouple roles.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user