Less stack-centric and location-centric model.

This commit is contained in:
2026-06-13 17:26:49 -07:00
parent 7bf98c17b7
commit 400e0b253b
33 changed files with 1112 additions and 408 deletions

View File

@@ -1,19 +1,23 @@
"""castle build / castle test / castle lint - run dev commands across projects."""
"""castle build/test/lint/type-check/check/run/install/uninstall — dev verbs.
Verbs resolve per-program: a declared command (manifest `commands:` / `build:`)
overrides the stack default, falling back to the stack handler, else unavailable.
"""
from __future__ import annotations
import argparse
import asyncio
from castle_core.stacks import get_handler
from castle_core.stacks import is_available, run_action
from castle_cli.config import CastleConfig, load_config
def _run_action(config: CastleConfig, project_name: str, action: str) -> bool:
"""Run a stack action for a single project. Returns True on success."""
def _run_verb(config: CastleConfig, project_name: str, verb: str) -> bool:
"""Run a verb for a single program. Returns True on success."""
if project_name not in config.programs:
print(f"Unknown component: {project_name}")
print(f"Unknown program: {project_name}")
return False
comp = config.programs[project_name]
@@ -21,106 +25,69 @@ def _run_action(config: CastleConfig, project_name: str, action: str) -> bool:
print(f" {project_name}: no source directory, skipping")
return True
handler = get_handler(comp.stack)
if handler is None:
print(f" {project_name}: unsupported stack '{comp.stack}', skipping")
if not is_available(comp, verb):
print(f" {project_name}: '{verb}' not available (no declared command or stack), skipping")
return True
method_name = action.replace("-", "_")
method = getattr(handler, method_name, None)
if method is None:
print(f" {project_name}: action '{action}' not supported")
return False
print(f"\n{'' * 40}")
print(f" {action}: {project_name}")
print(f" {verb}: {project_name}")
print(f"{'' * 40}")
result = asyncio.run(method(project_name, comp, config.root))
result = asyncio.run(run_action(verb, project_name, comp, config.root))
if result.output:
print(result.output)
return result.status == "ok"
def run_test(args: argparse.Namespace) -> int:
"""Run tests for one or all projects."""
config = load_config()
if args.project:
success = _run_action(config, args.project, "test")
return 0 if success else 1
# Run all
def _run_verb_all(config: CastleConfig, verb: str) -> bool:
"""Run a verb across every program that supports it. Returns True if all pass."""
all_passed = True
ran_any = False
for name, comp in config.programs.items():
if not comp.source:
if not comp.source or not is_available(comp, verb):
continue
handler = get_handler(comp.stack)
if handler is None:
continue
# Skip projects without a tests directory (python) or test script (node)
source_dir = config.root / comp.source
if comp.stack in ("python-cli", "python-fastapi"):
if not (source_dir / "tests").exists():
continue
if not _run_action(config, name, "test"):
ran_any = True
if not _run_verb(config, name, verb):
all_passed = False
if not ran_any:
print(f"No programs support '{verb}'.")
return all_passed
if all_passed:
print("\nAll tests passed.")
else:
print("\nSome tests failed.")
return 0 if all_passed else 1
def run_verb(args: argparse.Namespace, verb: str) -> int:
"""Generic entry point for a dev verb (single project or all)."""
config = load_config()
if getattr(args, "project", None):
return 0 if _run_verb(config, args.project, verb) else 1
ok = _run_verb_all(config, verb)
print(f"\n{'All ' + verb + ' passed.' if ok else 'Some ' + verb + ' failed.'}")
return 0 if ok else 1
# Thin named wrappers wired from main.py.
def run_test(args: argparse.Namespace) -> int:
return run_verb(args, "test")
def run_lint(args: argparse.Namespace) -> int:
"""Run linter for one or all projects."""
config = load_config()
if args.project:
success = _run_action(config, args.project, "lint")
return 0 if success else 1
# Run all
all_passed = True
for name, comp in config.programs.items():
if not comp.source:
continue
handler = get_handler(comp.stack)
if handler is None:
continue
if not _run_action(config, name, "lint"):
all_passed = False
if all_passed:
print("\nAll lint checks passed.")
else:
print("\nSome lint checks failed.")
return 0 if all_passed else 1
return run_verb(args, "lint")
def run_build(args: argparse.Namespace) -> int:
"""Run build for one or all projects."""
config = load_config()
return run_verb(args, "build")
if args.project:
success = _run_action(config, args.project, "build")
return 0 if success else 1
# Run all buildable projects
all_passed = True
for name, comp in config.programs.items():
if not comp.source:
continue
handler = get_handler(comp.stack)
if handler is None:
continue
if not _run_action(config, name, "build"):
all_passed = False
def run_type_check(args: argparse.Namespace) -> int:
return run_verb(args, "type-check")
if all_passed:
print("\nAll builds succeeded.")
else:
print("\nSome builds failed.")
return 0 if all_passed else 1
def run_check(args: argparse.Namespace) -> int:
return run_verb(args, "check")
def run_install(args: argparse.Namespace) -> int:
return run_verb(args, "install")
def run_uninstall(args: argparse.Namespace) -> int:
return run_verb(args, "uninstall")