Populate the program stack select from castle-api
The dashboard's stack dropdown hardcoded its options (python-cli/fastapi/ react-vite), so a supabase program showed "(none)" — the value had no matching option. Make the backend the single source of truth instead: - core: available_stacks() returns sorted(HANDLERS) — the stacks castle has handlers for. - castle-api: GET /stacks exposes it. - cli: --stack choices derive from available_stacks() (drops the duplicated list). - app: ProgramFields fetches /stacks (useStacks) and renders options from it, always including the current value so it never silently blanks; StackBadge gets a Supabase label. A new backend stack now appears everywhere without a frontend edit.
This commit is contained in:
@@ -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<string, unknown>
|
||||
/** 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) {
|
||||
>
|
||||
<select value={stack} onChange={(e) => setStack(e.target.value)} className={`w-48 ${SELECT}`}>
|
||||
<option value="">(none)</option>
|
||||
<option value="python-cli">python-cli</option>
|
||||
<option value="python-fastapi">python-fastapi</option>
|
||||
<option value="react-vite">react-vite</option>
|
||||
{/* Options come from the backend (GET /stacks); include the current
|
||||
value even if unknown so it never silently blanks. */}
|
||||
{Array.from(new Set([...stacks, ...(stack ? [stack] : [])])).map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<TextField label="Version" value={version} onChange={setVersion} width="w-32" hint="Optional metadata." />
|
||||
|
||||
@@ -22,6 +22,7 @@ export const STACK_LABELS: Record<string, string> = {
|
||||
"python-fastapi": "Python / FastAPI",
|
||||
"python-cli": "Python / CLI",
|
||||
"react-vite": "React / Vite",
|
||||
supabase: "Supabase",
|
||||
rust: "Rust",
|
||||
go: "Go",
|
||||
bash: "Bash",
|
||||
|
||||
@@ -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<string[]>("/stacks"),
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export function useJob(name: string) {
|
||||
return useQuery({
|
||||
queryKey: ["jobs", name],
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user