fix(core): unify build-time and runtime PATH via USER_TOOL_PATH_DIRS
Generated systemd units hardcoded a thin PATH (~/.local/bin + system dirs) while dev-verb builds used a separate _EXTRA_PATH_DIRS list, so a service could be built and run with different tools on PATH. Hoist a single USER_TOOL_PATH_DIRS in config.py, consumed by both stacks._build_env (build) and the systemd unit generator (runtime). Also prefer pnpm's modern $PNPM_HOME/bin shim over the bare $PNPM_HOME dir (older installs leave a stale version wrapper there) and add /usr/local/go/bin so Go-based builds/services resolve their toolchain.
This commit is contained in:
@@ -75,6 +75,20 @@ DATA_DIR = _resolve_data_dir()
|
|||||||
SECRETS_DIR = CASTLE_HOME / "secrets"
|
SECRETS_DIR = CASTLE_HOME / "secrets"
|
||||||
REPOS_DIR = _resolve_repos_dir()
|
REPOS_DIR = _resolve_repos_dir()
|
||||||
|
|
||||||
|
# User tool directories — the single source of truth for "where our CLIs live".
|
||||||
|
# Used both at build time (dev-verb subprocess PATH) and at run time (generated
|
||||||
|
# systemd unit PATH) so a service sees the same tools castle used to build it.
|
||||||
|
# Order matters: pnpm's modern standalone installer puts its shim in
|
||||||
|
# $PNPM_HOME/bin, which must win over the bare dir (older installs leave a stale
|
||||||
|
# version wrapper there). nvm/node is intentionally omitted — it's versioned and
|
||||||
|
# brittle; a service needing a specific node should pin it via defaults.env.
|
||||||
|
USER_TOOL_PATH_DIRS = [
|
||||||
|
Path.home() / ".local" / "bin",
|
||||||
|
Path.home() / ".local" / "share" / "pnpm" / "bin",
|
||||||
|
Path.home() / ".local" / "share" / "pnpm",
|
||||||
|
Path("/usr/local/go/bin"),
|
||||||
|
]
|
||||||
|
|
||||||
# Backwards-compat aliases (used by existing imports)
|
# Backwards-compat aliases (used by existing imports)
|
||||||
GENERATED_DIR = SPECS_DIR
|
GENERATED_DIR = SPECS_DIR
|
||||||
STATIC_DIR = CONTENT_DIR
|
STATIC_DIR = CONTENT_DIR
|
||||||
@@ -97,8 +111,7 @@ def find_castle_root() -> Path:
|
|||||||
return current
|
return current
|
||||||
current = current.parent
|
current = current.parent
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
"Could not find castle.yaml.\n"
|
f"Could not find castle.yaml.\nExpected at: {CASTLE_HOME / 'castle.yaml'}"
|
||||||
f"Expected at: {CASTLE_HOME / 'castle.yaml'}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -125,11 +138,7 @@ class CastleConfig:
|
|||||||
@property
|
@property
|
||||||
def tools(self) -> dict[str, ProgramSpec]:
|
def tools(self) -> dict[str, ProgramSpec]:
|
||||||
"""Return programs that are tools (behavior == 'tool')."""
|
"""Return programs that are tools (behavior == 'tool')."""
|
||||||
return {
|
return {k: v for k, v in self.programs.items() if v.behavior == "tool"}
|
||||||
k: v
|
|
||||||
for k, v in self.programs.items()
|
|
||||||
if v.behavior == "tool"
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def frontends(self) -> dict[str, ProgramSpec]:
|
def frontends(self) -> dict[str, ProgramSpec]:
|
||||||
@@ -230,8 +239,7 @@ def _expand_units(
|
|||||||
for name, unit in units.items():
|
for name, unit in units.items():
|
||||||
if name in programs or name in services or name in jobs:
|
if name in programs or name in services or name in jobs:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unit '{name}' conflicts with existing entry in "
|
f"Unit '{name}' conflicts with existing entry in programs/services/jobs"
|
||||||
f"programs/services/jobs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defaults = _STACK_DEFAULTS.get(unit.stack or "", {})
|
defaults = _STACK_DEFAULTS.get(unit.stack or "", {})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from castle_core.config import USER_TOOL_PATH_DIRS
|
||||||
from castle_core.manifest import RestartPolicy, SystemdSpec
|
from castle_core.manifest import RestartPolicy, SystemdSpec
|
||||||
from castle_core.registry import Deployment
|
from castle_core.registry import Deployment
|
||||||
|
|
||||||
@@ -86,7 +87,8 @@ def generate_unit_from_deployed(
|
|||||||
env_lines = ""
|
env_lines = ""
|
||||||
for key, value in deployed.env.items():
|
for key, value in deployed.env.items():
|
||||||
env_lines += f"Environment={key}={value}\n"
|
env_lines += f"Environment={key}={value}\n"
|
||||||
env_lines += f'Environment="PATH={Path.home() / ".local/bin"}:/usr/local/bin:/usr/bin:/bin"\n'
|
tool_path = ":".join(str(d) for d in USER_TOOL_PATH_DIRS if d.exists())
|
||||||
|
env_lines += f'Environment="PATH={tool_path}:/usr/local/bin:/usr/bin:/bin"\n'
|
||||||
|
|
||||||
sd = systemd_spec
|
sd = systemd_spec
|
||||||
description = deployed.description or name
|
description = deployed.description or name
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import tomllib
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from castle_core.config import USER_TOOL_PATH_DIRS
|
||||||
from castle_core.manifest import ProgramSpec
|
from castle_core.manifest import ProgramSpec
|
||||||
|
|
||||||
DEV_ACTIONS = ["build", "test", "lint", "format", "type-check", "check", "run"]
|
DEV_ACTIONS = ["build", "test", "lint", "format", "type-check", "check", "run"]
|
||||||
@@ -15,16 +16,19 @@ INSTALL_ACTIONS = ["install", "uninstall"]
|
|||||||
ALL_ACTIONS = DEV_ACTIONS + INSTALL_ACTIONS
|
ALL_ACTIONS = DEV_ACTIONS + INSTALL_ACTIONS
|
||||||
|
|
||||||
# Verbs a stack handler can provide (everything except `run`, which is declared-only).
|
# Verbs a stack handler can provide (everything except `run`, which is declared-only).
|
||||||
_STACK_VERBS = {"build", "test", "lint", "format", "type-check", "check", "install", "uninstall"}
|
_STACK_VERBS = {
|
||||||
|
"build",
|
||||||
|
"test",
|
||||||
|
"lint",
|
||||||
|
"format",
|
||||||
|
"type-check",
|
||||||
|
"check",
|
||||||
|
"install",
|
||||||
|
"uninstall",
|
||||||
|
}
|
||||||
# Verbs whose handler method name differs from the verb spelling.
|
# Verbs whose handler method name differs from the verb spelling.
|
||||||
_VERB_METHOD = {"type-check": "type_check"}
|
_VERB_METHOD = {"type-check": "type_check"}
|
||||||
|
|
||||||
# User-local tool directories that may not be on the systemd service PATH.
|
|
||||||
_EXTRA_PATH_DIRS = [
|
|
||||||
Path.home() / ".local" / "share" / "pnpm",
|
|
||||||
Path.home() / ".local" / "bin",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ActionResult:
|
class ActionResult:
|
||||||
@@ -39,13 +43,15 @@ class ActionResult:
|
|||||||
def _build_env() -> dict[str, str]:
|
def _build_env() -> dict[str, str]:
|
||||||
"""Build a subprocess env with user tool dirs on PATH."""
|
"""Build a subprocess env with user tool dirs on PATH."""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
extra = ":".join(str(d) for d in _EXTRA_PATH_DIRS if d.exists())
|
extra = ":".join(str(d) for d in USER_TOOL_PATH_DIRS if d.exists())
|
||||||
if extra:
|
if extra:
|
||||||
env["PATH"] = extra + ":" + env.get("PATH", "")
|
env["PATH"] = extra + ":" + env.get("PATH", "")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
async def _run(cmd: list[str], cwd: Path, env: dict[str, str] | None = None) -> tuple[int, str]:
|
async def _run(
|
||||||
|
cmd: list[str], cwd: Path, env: dict[str, str] | None = None
|
||||||
|
) -> tuple[int, str]:
|
||||||
"""Run a subprocess and return (returncode, combined output)."""
|
"""Run a subprocess and return (returncode, combined output)."""
|
||||||
run_env = _build_env()
|
run_env = _build_env()
|
||||||
if env:
|
if env:
|
||||||
@@ -93,7 +99,9 @@ class StackHandler:
|
|||||||
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def type_check(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def type_check(
|
||||||
|
self, name: str, comp: ProgramSpec, root: Path
|
||||||
|
) -> ActionResult:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def check(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def check(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
@@ -127,40 +135,59 @@ class PythonHandler(StackHandler):
|
|||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["uv", "sync"], src)
|
rc, output = await _run(["uv", "sync"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="build", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="build",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def test(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
if not (src / "tests").exists():
|
if not (src / "tests").exists():
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="test", status="ok",
|
program=name,
|
||||||
|
action="test",
|
||||||
|
status="ok",
|
||||||
output="No tests directory found, skipping.",
|
output="No tests directory found, skipping.",
|
||||||
)
|
)
|
||||||
rc, output = await _run(["uv", "run", "pytest", "tests/", "-v"], src)
|
rc, output = await _run(["uv", "run", "pytest", "tests/", "-v"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="test", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="test",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def lint(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def lint(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["uv", "run", "ruff", "check", "."], src)
|
rc, output = await _run(["uv", "run", "ruff", "check", "."], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="lint", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="lint",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["uv", "run", "ruff", "format", "."], src)
|
rc, output = await _run(["uv", "run", "ruff", "format", "."], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="format", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="format",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def type_check(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def type_check(
|
||||||
|
self, name: str, comp: ProgramSpec, root: Path
|
||||||
|
) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["uv", "run", "pyright"], src)
|
rc, output = await _run(["uv", "run", "pyright"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="type-check", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="type-check",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
@@ -172,7 +199,10 @@ class PythonHandler(StackHandler):
|
|||||||
["uv", "tool", "install", "--editable", pkg_spec, "--force"], src
|
["uv", "tool", "install", "--editable", pkg_spec, "--force"], src
|
||||||
)
|
)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="install", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def uninstall(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def uninstall(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
@@ -185,7 +215,10 @@ class PythonHandler(StackHandler):
|
|||||||
pkg_name = data.get("project", {}).get("name", pkg_name)
|
pkg_name = data.get("project", {}).get("name", pkg_name)
|
||||||
rc, output = await _run(["uv", "tool", "uninstall", pkg_name], src)
|
rc, output = await _run(["uv", "tool", "uninstall", pkg_name], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="uninstall", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="uninstall",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -196,37 +229,56 @@ class ReactViteHandler(StackHandler):
|
|||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
# Build against the gateway serve prefix so absolute asset URLs resolve at
|
# Build against the gateway serve prefix so absolute asset URLs resolve at
|
||||||
# /<name>/ (vite.config reads VITE_BASE). Removes the hand-tuned-base footgun.
|
# /<name>/ (vite.config reads VITE_BASE). Removes the hand-tuned-base footgun.
|
||||||
rc, output = await _run(["pnpm", "build"], src, env={"VITE_BASE": _vite_base(name)})
|
rc, output = await _run(
|
||||||
|
["pnpm", "build"], src, env={"VITE_BASE": _vite_base(name)}
|
||||||
|
)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="build", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="build",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def test(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["pnpm", "test"], src)
|
rc, output = await _run(["pnpm", "test"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="test", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="test",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def lint(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def lint(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["pnpm", "lint"], src)
|
rc, output = await _run(["pnpm", "lint"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="lint", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="lint",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def format(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["pnpm", "format"], src)
|
rc, output = await _run(["pnpm", "format"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="format", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="format",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def type_check(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def type_check(
|
||||||
|
self, name: str, comp: ProgramSpec, root: Path
|
||||||
|
) -> ActionResult:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
rc, output = await _run(["pnpm", "type-check"], src)
|
rc, output = await _run(["pnpm", "type-check"], src)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="type-check", status="ok" if rc == 0 else "error", output=output
|
program=name,
|
||||||
|
action="type-check",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
@@ -235,18 +287,24 @@ class ReactViteHandler(StackHandler):
|
|||||||
result = await self.build(name, comp, root)
|
result = await self.build(name, comp, root)
|
||||||
if result.status != "ok":
|
if result.status != "ok":
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="install", status="error",
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="error",
|
||||||
output=f"Build failed:\n{result.output}",
|
output=f"Build failed:\n{result.output}",
|
||||||
)
|
)
|
||||||
outputs = comp.build.outputs if comp.build else []
|
outputs = comp.build.outputs if comp.build else []
|
||||||
if not outputs:
|
if not outputs:
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="install", status="error",
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="error",
|
||||||
output="No build outputs configured.",
|
output="No build outputs configured.",
|
||||||
)
|
)
|
||||||
dist = _source_dir(comp, root) / outputs[0]
|
dist = _source_dir(comp, root) / outputs[0]
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="install", status="ok",
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="ok",
|
||||||
output=f"Built; served in place from {dist}",
|
output=f"Built; served in place from {dist}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -256,7 +314,9 @@ class ReactViteHandler(StackHandler):
|
|||||||
Deactivating one means dropping its gateway route — handled by removing the
|
Deactivating one means dropping its gateway route — handled by removing the
|
||||||
program from the registry, not by deleting build output."""
|
program from the registry, not by deleting build output."""
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="uninstall", status="ok",
|
program=name,
|
||||||
|
action="uninstall",
|
||||||
|
status="ok",
|
||||||
output=f"{name}: served in place; nothing to uninstall.",
|
output=f"{name}: served in place; nothing to uninstall.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,7 +351,11 @@ def _declared_commands(comp: ProgramSpec, verb: str) -> list[list[str]] | None:
|
|||||||
|
|
||||||
def _stack_provides(comp: ProgramSpec, verb: str) -> bool:
|
def _stack_provides(comp: ProgramSpec, verb: str) -> bool:
|
||||||
"""Whether the program's stack handler can run this verb."""
|
"""Whether the program's stack handler can run this verb."""
|
||||||
return bool(comp.source) and verb in _STACK_VERBS and get_handler(comp.stack) is not None
|
return (
|
||||||
|
bool(comp.source)
|
||||||
|
and verb in _STACK_VERBS
|
||||||
|
and get_handler(comp.stack) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_available(comp: ProgramSpec, verb: str) -> bool:
|
def is_available(comp: ProgramSpec, verb: str) -> bool:
|
||||||
@@ -319,11 +383,15 @@ async def _run_declared(
|
|||||||
rc, output = await _run(argv, src)
|
rc, output = await _run(argv, src)
|
||||||
outputs.append(output)
|
outputs.append(output)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return ActionResult(program=name, action=verb, status="error", output="".join(outputs))
|
return ActionResult(
|
||||||
|
program=name, action=verb, status="error", output="".join(outputs)
|
||||||
|
)
|
||||||
return ActionResult(program=name, action=verb, status="ok", output="".join(outputs))
|
return ActionResult(program=name, action=verb, status="ok", output="".join(outputs))
|
||||||
|
|
||||||
|
|
||||||
async def run_action(verb: str, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def run_action(
|
||||||
|
verb: str, name: str, comp: ProgramSpec, root: Path
|
||||||
|
) -> ActionResult:
|
||||||
"""Resolve and run a verb: declared command → stack default → unavailable.
|
"""Resolve and run a verb: declared command → stack default → unavailable.
|
||||||
|
|
||||||
This is the single entry point callers should use; it replaces reaching for
|
This is the single entry point callers should use; it replaces reaching for
|
||||||
@@ -335,7 +403,9 @@ async def run_action(verb: str, name: str, comp: ProgramSpec, root: Path) -> Act
|
|||||||
subs = [s for s in ("lint", "type-check", "test") if is_available(comp, s)]
|
subs = [s for s in ("lint", "type-check", "test") if is_available(comp, s)]
|
||||||
if not subs:
|
if not subs:
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="check", status="error",
|
program=name,
|
||||||
|
action="check",
|
||||||
|
status="error",
|
||||||
output="No checkable verbs available.",
|
output="No checkable verbs available.",
|
||||||
)
|
)
|
||||||
sections: list[str] = []
|
sections: list[str] = []
|
||||||
@@ -346,7 +416,9 @@ async def run_action(verb: str, name: str, comp: ProgramSpec, root: Path) -> Act
|
|||||||
sections.append(f"{mark} {sub}" + (f"\n{body}" if body else ""))
|
sections.append(f"{mark} {sub}" + (f"\n{body}" if body else ""))
|
||||||
if result.status != "ok":
|
if result.status != "ok":
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action="check", status="error",
|
program=name,
|
||||||
|
action="check",
|
||||||
|
status="error",
|
||||||
output="\n\n".join(sections),
|
output="\n\n".join(sections),
|
||||||
)
|
)
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
@@ -359,7 +431,9 @@ async def run_action(verb: str, name: str, comp: ProgramSpec, root: Path) -> Act
|
|||||||
try:
|
try:
|
||||||
src = _source_dir(comp, root)
|
src = _source_dir(comp, root)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return ActionResult(program=name, action=verb, status="error", output="No source directory")
|
return ActionResult(
|
||||||
|
program=name, action=verb, status="error", output="No source directory"
|
||||||
|
)
|
||||||
return await _run_declared(name, verb, declared, src)
|
return await _run_declared(name, verb, declared, src)
|
||||||
|
|
||||||
# 2. Stack default.
|
# 2. Stack default.
|
||||||
@@ -371,7 +445,9 @@ async def run_action(verb: str, name: str, comp: ProgramSpec, root: Path) -> Act
|
|||||||
|
|
||||||
# 3. Unavailable.
|
# 3. Unavailable.
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
program=name, action=verb, status="error",
|
program=name,
|
||||||
|
action=verb,
|
||||||
|
status="error",
|
||||||
output=f"Verb '{verb}' is not available for '{name}' "
|
output=f"Verb '{verb}' is not available for '{name}' "
|
||||||
f"(no declared command and no stack handler provides it).",
|
f"(no declared command and no stack handler provides it).",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user