Phase 2b: serve static frontends in place from repo dist

The Caddyfile generator now emits static-frontend routes that root directly at
<source>/<build.outputs[0]> (e.g. /data/repos/castle/app/dist) instead of a copy
under ~/.castle/artifacts/content. Removes _copy_app_static and the central
content-dir staging entirely; rebuilds are live without a copy step.

- caddyfile.py: manifest-driven static routes (castle-app at /, others at /<name>)
- ReactViteHandler.install just builds in place (no copy); uninstall is a no-op
- is_active(static frontend) = repo dist exists
- test isolation updated for the generator's config use

Dashboard + power-graph-app verified serving from repo dist; 168 tests pass.
This commit is contained in:
2026-06-13 18:16:09 -07:00
parent 2953aea548
commit 482524bfe5
6 changed files with 86 additions and 84 deletions

View File

@@ -4,12 +4,10 @@ from __future__ import annotations
import asyncio
import os
import shutil
import tomllib
from dataclasses import dataclass
from pathlib import Path
from castle_core.config import CONTENT_DIR
from castle_core.manifest import ProgramSpec
DEV_ACTIONS = ["build", "test", "lint", "type-check", "check", "run"]
@@ -200,47 +198,34 @@ class ReactViteHandler(StackHandler):
)
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
"""Build and copy static assets to ~/.castle/static/{name}/."""
"""Build the static assets in place. The gateway serves them directly from
<source>/<build.outputs[0]> — no copy into a central content dir."""
result = await self.build(name, comp, root)
if result.status != "ok":
return ActionResult(
component=name, action="install", status="error",
output=f"Build failed:\n{result.output}",
)
src = _source_dir(comp, root)
outputs = comp.build.outputs if comp.build else []
if not outputs:
return ActionResult(
component=name, action="install", status="error",
output="No build outputs configured.",
)
for output_dir in outputs:
src_path = src / output_dir
if src_path.exists():
dest = CONTENT_DIR / name
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(src_path, dest)
dist = _source_dir(comp, root) / outputs[0]
return ActionResult(
component=name, action="install", status="ok",
output=f"Built and deployed to {CONTENT_DIR / name}",
output=f"Built; served in place from {dist}",
)
async def uninstall(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
"""Remove static assets from ~/.castle/static/{name}/."""
dest = CONTENT_DIR / name
if dest.exists():
shutil.rmtree(dest)
return ActionResult(
component=name, action="uninstall", status="ok",
output=f"Removed {dest}",
)
"""Static frontends have no install footprint to remove (served in place).
Deactivating one means dropping its gateway route — handled by removing the
program from the registry, not by deleting build output."""
return ActionResult(
component=name, action="uninstall", status="ok",
output=f"Nothing to remove ({dest} does not exist)",
output=f"{name}: served in place; nothing to uninstall.",
)