import { useState } from "react" import type { ProgramDetail } from "@/types" import { Field, TextField, FormFooter } from "./fields" const SELECT = "bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)]" // Verbs the commands editor exposes. `build` lives in `build.commands`; the rest // in `commands`. A declared command overrides the stack default (or supplies the // verb when there's no stack). const VERBS = ["build", "test", "lint", "type-check", "run"] interface Props { program: ProgramDetail onSave: (name: string, config: Record) => Promise onDelete?: (name: string) => Promise } 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 [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [description, setDescription] = useState((m.description as string) ?? "") const [source, setSource] = useState((m.source as string) ?? "") const [behavior, setBehavior] = useState((m.behavior as string) ?? "") const [stack, setStack] = useState((m.stack as string) ?? "") const [version, setVersion] = useState((m.version as string) ?? "") const [repo, setRepo] = useState((m.repo as string) ?? "") const [ref, setRef] = useState((m.ref as string) ?? "") const [deps, setDeps] = useState(((m.system_dependencies as string[]) ?? []).join(", ")) const readVerb = (verb: string): string => { if (verb === "build") { const c = (m.build as { commands?: string[][] } | undefined)?.commands return (c?.[0] ?? []).join(" ") } const cmds = (m.commands as Record | undefined) ?? {} const key = verb === "type-check" && !cmds["type-check"] ? "type_check" : verb return (cmds[key]?.[0] ?? []).join(" ") } const [cmds, setCmds] = useState>(() => Object.fromEntries(VERBS.map((v) => [v, readVerb(v)])), ) const handleSave = async () => { setSaving(true) setSaved(false) try { const config: Obj = { ...m } delete config.id config.description = description || undefined config.source = source || undefined config.behavior = behavior || undefined config.stack = stack || undefined config.version = version || undefined config.repo = repo || undefined config.ref = ref || undefined config.system_dependencies = deps.split(",").map((d) => d.trim()).filter(Boolean) const commands: Record = {} let buildArgv: string[][] | null = null for (const v of VERBS) { const s = (cmds[v] ?? "").trim() if (!s) continue const argv: string[][] = [s.split(/\s+/)] if (v === "build") buildArgv = argv else commands[v] = argv } if (buildArgv) config.build = { ...((m.build as Obj) ?? {}), commands: buildArgv } config.commands = Object.keys(commands).length ? commands : undefined await onSave(program.id, config) setSaved(true) setTimeout(() => setSaved(false), 2000) } finally { setSaving(false) } } return (
{VERBS.map((verb) => (
{verb} setCmds((c) => ({ ...c, [verb]: e.target.value }))} placeholder={stack ? "(stack default)" : "—"} className="flex-1 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]" />
))}
onDelete(program.id) : undefined} deleteLabel="Remove program" confirmMessage={`Remove program "${program.id}" from castle.yaml? (Source on disk is untouched.)`} deleteBlocked={ program.services.length + program.jobs.length > 0 ? "Programs with active jobs or services cannot be removed — delete those first." : undefined } />
) }