diff --git a/app/src/components/detail/ProgramFields.tsx b/app/src/components/detail/ProgramFields.tsx index 7272199..de94a32 100644 --- a/app/src/components/detail/ProgramFields.tsx +++ b/app/src/components/detail/ProgramFields.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import type { ProgramDetail } from "@/types" +import { useStacks } from "@/services/api/hooks" import { Field, TextField, FormFooter } from "./fields" const SELECT = @@ -21,6 +22,7 @@ type Obj = Record /** Edit a program's catalog config (its source identity), not how it's deployed. */ export function ProgramFields({ program, onSave, onDelete }: Props) { const m = program.manifest + const { data: stacks = [] } = useStacks() const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) @@ -114,9 +116,13 @@ export function ProgramFields({ program, onSave, onDelete }: Props) { > diff --git a/app/src/lib/labels.ts b/app/src/lib/labels.ts index a5e92ce..51603a8 100644 --- a/app/src/lib/labels.ts +++ b/app/src/lib/labels.ts @@ -22,6 +22,7 @@ export const STACK_LABELS: Record = { "python-fastapi": "Python / FastAPI", "python-cli": "Python / CLI", "react-vite": "React / Vite", + supabase: "Supabase", rust: "Rust", go: "Go", bash: "Bash", diff --git a/app/src/services/api/hooks.ts b/app/src/services/api/hooks.ts index 5b16790..814b24a 100644 --- a/app/src/services/api/hooks.ts +++ b/app/src/services/api/hooks.ts @@ -49,6 +49,16 @@ export function useJobs() { }) } +// The stacks castle has handlers for — authoritative source for the program +// stack select, so a new backend stack appears without a frontend change. +export function useStacks() { + return useQuery({ + queryKey: ["stacks"], + queryFn: () => apiClient.get("/stacks"), + staleTime: Infinity, + }) +} + export function useJob(name: string) { return useQuery({ queryKey: ["jobs", name], diff --git a/castle-api/src/castle_api/programs.py b/castle-api/src/castle_api/programs.py index 9f016ec..ccad9df 100644 --- a/castle-api/src/castle_api/programs.py +++ b/castle-api/src/castle_api/programs.py @@ -4,12 +4,19 @@ from __future__ import annotations from fastapi import APIRouter, HTTPException, status -from castle_core.stacks import available_actions, run_action +from castle_core.stacks import available_actions, available_stacks, run_action from castle_api.config import get_config programs_router = APIRouter(tags=["programs"]) + +@programs_router.get("/stacks") +def list_stacks() -> list[str]: + """Stack names castle has handlers for — populates the dashboard's stack select + and keeps it in sync with the backend (no hardcoded frontend list).""" + return available_stacks() + # --------------------------------------------------------------------------- # Unified program action endpoint # --------------------------------------------------------------------------- diff --git a/cli/src/castle_cli/main.py b/cli/src/castle_cli/main.py index 5c6d783..41a1880 100644 --- a/cli/src/castle_cli/main.py +++ b/cli/src/castle_cli/main.py @@ -12,6 +12,8 @@ from __future__ import annotations import argparse import sys +from castle_core.stacks import available_stacks + from castle_cli import __version__ DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"] @@ -37,11 +39,7 @@ def _build_program_group(subparsers: argparse._SubParsersAction) -> None: p = sub.add_parser("create", help="Scaffold a new program") _add_name(p, "Program name") - p.add_argument( - "--stack", - choices=["python-cli", "python-fastapi", "react-vite", "supabase"], - default=None, - ) + p.add_argument("--stack", choices=available_stacks(), default=None) p.add_argument("--description", default="", help="Program description") p.add_argument("--port", type=int, help="Port (daemons only)") diff --git a/core/src/castle_core/stacks.py b/core/src/castle_core/stacks.py index 882336c..0a625a0 100644 --- a/core/src/castle_core/stacks.py +++ b/core/src/castle_core/stacks.py @@ -520,6 +520,13 @@ def get_handler(stack: str | None) -> StackHandler | None: return HANDLERS.get(stack) +def available_stacks() -> list[str]: + """The stack names castle has handlers for — the single source of truth for the + CLI ``--stack`` choices, the ``GET /stacks`` endpoint, and the dashboard select. + """ + return sorted(HANDLERS) + + def _declared_commands(comp: ProgramSpec, verb: str) -> list[list[str]] | None: """Declared argv-lists for a verb, or None.