Runtime half: convergent deploy + unified active lifecycle
Convergent deploy (prefix-based prune): castle deploy now removes orphaned castle-* systemd units/timers no longer in the registry. Full deploy only; partial (--name) deploys preserve siblings. Fixes the orphaned-timer / stale- path drift class. Container runner: derive --name from the service name (castle-<name>) instead of the image name; skip the <PREFIX>_DATA_DIR env injection for containers (avoids colliding with image env namespaces like NEO4J_*). Unified active lifecycle (core/lifecycle.py): install/uninstall keep their names but become activate/deactivate, dispatching by behavior — tool→PATH, daemon/self-serving-frontend/job→systemd, static frontend→served. is_active reports a uniform state (PATH-independent for tools). The CLI install/uninstall and service enable/disable route through the extracted core. API + dashboard: surface a uniform `active` state on programs alongside `installed`; ProgramDetail shows active/inactive. Tests: test_deploy_prune (6), test_lifecycle (5). 168 tests pass; lint clean.
This commit is contained in:
@@ -85,9 +85,40 @@ def run_check(args: argparse.Namespace) -> int:
|
||||
return run_verb(args, "check")
|
||||
|
||||
|
||||
def _lifecycle(config: CastleConfig, name: str, deactivate: bool) -> bool:
|
||||
"""Activate or deactivate a single program. Returns True on success."""
|
||||
from castle_core.lifecycle import activate
|
||||
from castle_core.lifecycle import deactivate as do_deactivate
|
||||
|
||||
if name not in config.programs and name not in config.services and name not in config.jobs:
|
||||
print(f"Unknown program: {name}")
|
||||
return False
|
||||
verb = "deactivate" if deactivate else "activate"
|
||||
print(f"\n{'─' * 40}\n {verb}: {name}\n{'─' * 40}")
|
||||
fn = do_deactivate if deactivate else activate
|
||||
result = asyncio.run(fn(name, config, config.root))
|
||||
if result.output:
|
||||
print(result.output)
|
||||
return result.status == "ok"
|
||||
|
||||
|
||||
def run_install(args: argparse.Namespace) -> int:
|
||||
return run_verb(args, "install")
|
||||
"""Activate (install/deploy/serve) a program — verb word kept, meaning unified."""
|
||||
config = load_config()
|
||||
if getattr(args, "project", None):
|
||||
return 0 if _lifecycle(config, args.project, deactivate=False) else 1
|
||||
ok = all(
|
||||
_lifecycle(config, name, deactivate=False)
|
||||
for name, comp in config.programs.items()
|
||||
if comp.source
|
||||
)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def run_uninstall(args: argparse.Namespace) -> int:
|
||||
return run_verb(args, "uninstall")
|
||||
"""Deactivate (uninstall/stop/unpublish) a program."""
|
||||
config = load_config()
|
||||
if getattr(args, "project", None):
|
||||
return 0 if _lifecycle(config, args.project, deactivate=True) else 1
|
||||
print("Refusing to deactivate all programs; name a target.")
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user