Add supabase stack + general compose runner
Adds "a stack whose default is a substrate": a shared self-hosted Supabase backend plus a `--stack supabase` that scaffolds per-app projects (migrations + edge functions + static UI) which deploy against it. Apps own their code and stay repo-durable; only their rows/blobs live on the shared substrate. - compose runner: new RunCompose supervises a multi-container stack as one systemd unit (ExecStart=`compose up`, generated ExecStop=`compose down`); secrets via EnvironmentFile. Reusable beyond Supabase. Deployment.stop_cmd carries the teardown command through the registry. - supabase stack: CLI --stack choice, STACK_DEFAULTS→frontend with build.outputs=[public], _scaffold_supabase() (migrations/RLS/functions/ static UI/app manifest), and SupabaseHandler with a forward-only idempotent migration runner (plan_migrations + psql build) and deno dev-verbs. - docs: docs/stacks/supabase.md, registered in CLAUDE.md; compose runner documented in registry.md. - tests: compose run/stop cmd, systemd ExecStop, compose host-route TLS, migration planner, supabase verb resolution, create --stack supabase.
This commit is contained in:
@@ -7,6 +7,7 @@ import subprocess
|
||||
|
||||
from castle_cli.config import REPOS_DIR, load_config, save_config
|
||||
from castle_cli.manifest import (
|
||||
BuildSpec,
|
||||
CaddySpec,
|
||||
DefaultsSpec,
|
||||
ExposeSpec,
|
||||
@@ -26,6 +27,15 @@ STACK_DEFAULTS: dict[str, str] = {
|
||||
"python-fastapi": "daemon",
|
||||
"python-cli": "tool",
|
||||
"react-vite": "frontend",
|
||||
"supabase": "frontend",
|
||||
}
|
||||
|
||||
# Static build output per stack, for `behavior: frontend` programs. The gateway
|
||||
# serves this dir in place at /<name>/ (no service, no process). A supabase app
|
||||
# ships a raw `public/`; react-vite builds to `dist/`.
|
||||
STACK_BUILD_OUTPUTS: dict[str, str] = {
|
||||
"supabase": "public",
|
||||
"react-vite": "dist",
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +95,19 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
# Initialize a git repo for the new source.
|
||||
subprocess.run(["git", "init", "-q", str(project_dir)], check=False)
|
||||
|
||||
# Frontend stacks with a static output get a build spec so the gateway emits
|
||||
# an in-place static route at /<name>/ (no service, no process).
|
||||
build = None
|
||||
if stack in STACK_BUILD_OUTPUTS:
|
||||
build = BuildSpec(outputs=[STACK_BUILD_OUTPUTS[stack]])
|
||||
|
||||
config.programs[name] = ProgramSpec(
|
||||
id=name,
|
||||
description=description,
|
||||
source=str(project_dir),
|
||||
stack=stack,
|
||||
behavior=behavior,
|
||||
build=build,
|
||||
)
|
||||
if behavior == "daemon":
|
||||
prefix = name.replace("-", "_").upper()
|
||||
@@ -122,7 +139,11 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
print(" Registered in castle.yaml")
|
||||
print("\nNext steps:")
|
||||
print(f" cd {project_dir}")
|
||||
if stack:
|
||||
if stack == "supabase":
|
||||
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
|
||||
print(f" castle program build {name} # apply migrations to the substrate")
|
||||
print(f" castle deploy && castle gateway reload # serve at /{name}/")
|
||||
elif stack:
|
||||
print(" uv sync")
|
||||
if behavior == "daemon":
|
||||
print(f" uv run {name} # starts on port {port}")
|
||||
|
||||
@@ -30,6 +30,7 @@ STACK_DISPLAY: dict[str, str] = {
|
||||
"python-fastapi": "python-fastapi",
|
||||
"python-cli": "python-cli",
|
||||
"react-vite": "react-vite",
|
||||
"supabase": "supabase",
|
||||
"rust": "rust",
|
||||
"go": "go",
|
||||
"bash": "bash",
|
||||
|
||||
@@ -19,6 +19,8 @@ def run_logs(args: argparse.Namespace) -> int:
|
||||
svc = config.services[name]
|
||||
if svc.run.runner == "container":
|
||||
return _container_logs(name, args)
|
||||
if svc.run.runner == "compose":
|
||||
return _compose_logs(name, svc, args)
|
||||
return _systemd_logs(name, args)
|
||||
|
||||
# Check jobs
|
||||
@@ -45,6 +47,25 @@ def _systemd_logs(name: str, args: argparse.Namespace) -> int:
|
||||
return result.returncode
|
||||
|
||||
|
||||
def _compose_logs(name: str, svc: object, args: argparse.Namespace) -> int:
|
||||
"""Show aggregated logs for a compose-runner stack (by project label)."""
|
||||
import shutil
|
||||
|
||||
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
|
||||
project = getattr(svc.run, "project_name", None) or f"castle-{name}" # type: ignore[attr-defined]
|
||||
cmd = [runtime, "compose", "-p", project, "logs"]
|
||||
|
||||
lines = getattr(args, "lines", 50)
|
||||
if lines:
|
||||
cmd.extend(["--tail", str(lines)])
|
||||
|
||||
if getattr(args, "follow", False):
|
||||
cmd.append("-f")
|
||||
|
||||
result = subprocess.run(cmd)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def _container_logs(name: str, args: argparse.Namespace) -> int:
|
||||
"""Show container logs."""
|
||||
import shutil
|
||||
|
||||
Reference in New Issue
Block a user