"use client"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import type { PipelineNode } from "@/lib/pipeline-graph"; interface NodeFormProps { node: PipelineNode; onChange: (next: PipelineNode) => void; } // Typed forms per node type. Anything we don't know about renders a generic // JSON view (handled by the inspector — this component returns null in that // case so the parent shows the JSON fallback). // // Each form mutates node.parameters and calls onChange with the updated node. // We deliberately keep these dumb (no internal state); the inspector owns // debouncing and persistence. export function NodeForm({ node, onChange }: NodeFormProps) { switch (node.type) { case "flow-nodes-base.trigger": return ; case "flow-nodes-base.build": return ; case "flow-nodes-base.test": return ; case "flow-nodes-base.eval": return ; case "flow-nodes-base.policy": return ; case "flow-nodes-base.waitForApproval": return ; case "flow-nodes-base.sast": return ; case "flow-nodes-base.imageScan": return ; case "flow-nodes-base.push": return ; case "flow-nodes-base.deploy": return ; case "flow-nodes-base.promote": return ; case "flow-nodes-base.rollback": return ; default: return null; } } /** Returns true if we render a typed form for this type (so the JSON * fallback can be hidden). */ export function hasTypedForm(type: string): boolean { return [ "flow-nodes-base.trigger", "flow-nodes-base.build", "flow-nodes-base.sast", "flow-nodes-base.imageScan", "flow-nodes-base.push", "flow-nodes-base.test", "flow-nodes-base.eval", "flow-nodes-base.policy", "flow-nodes-base.waitForApproval", "flow-nodes-base.deploy", "flow-nodes-base.promote", "flow-nodes-base.rollback", ].includes(type); } // --- helpers -------------------------------------------------------------- function setParam(node: PipelineNode, key: string, value: T): PipelineNode { return { ...node, parameters: { ...(node.parameters ?? {}), [key]: value }, }; } function getString(node: PipelineNode, key: string, fallback = ""): string { const v = node.parameters?.[key]; return typeof v === "string" ? v : fallback; } function getNumber(node: PipelineNode, key: string, fallback = 0): number { const v = node.parameters?.[key]; return typeof v === "number" ? v : fallback; } function getStringArray(node: PipelineNode, key: string): string[] { const v = node.parameters?.[key]; return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []; } function getBool(node: PipelineNode, key: string, fallback: boolean): boolean { const v = node.parameters?.[key]; if (typeof v === "boolean") return v; return fallback; } // --- forms ---------------------------------------------------------------- function TriggerForm({ node, onChange }: NodeFormProps) { const mode = getString(node, "mode", "manual"); const cron = getString(node, "cron", "0 * * * *"); const fromBranch = getString(node, "fromBranch", "main"); const toBranch = getString(node, "toBranch", "production"); return (

How runs are dispatched. Webhook + manual are wired today; schedule lands once the cron worker exists.

{mode === "schedule" && (
onChange(setParam(node, "cron", e.target.value))} placeholder="0 * * * *" className="font-mono text-xs" />
)}
onChange(setParam(node, "fromBranch", e.target.value)) } placeholder="main" className="font-mono text-xs" />
onChange(setParam(node, "toBranch", e.target.value)) } placeholder="production" className="font-mono text-xs" />

From/To branch travel in the trigger payload — Build clones{" "} fromBranch, Promote opens a PR{" "} fromBranch → toBranch.

); } function BuildForm({ node, onChange }: NodeFormProps) { const mode = getString(node, "mode", "docker"); const dockerfile = getString(node, "dockerfile", "Dockerfile"); const ctx = getString(node, "context", "."); const imageName = getString(node, "imageName", ""); const registry = getString(node, "registry", "registry:5000"); const platform = getString(node, "platform", "linux/amd64"); const buildArgs = getString(node, "buildArgs", ""); const command = getString( node, "command", "docker build -t $AGENT_NAME:$COMMIT_SHA ." ); const workdir = getString(node, "workdir", "."); const timeout = getNumber(node, "timeoutSeconds", 600); return (

docker = real OCI image build & push via BuildKit.{" "} shell = run any command (escape hatch).

{mode === "docker" ? ( <>
onChange(setParam(node, "dockerfile", e.target.value)) } placeholder="Dockerfile" className="font-mono text-xs" />

Path relative to the repo root.

onChange(setParam(node, "context", e.target.value))} placeholder="." className="font-mono text-xs" />
onChange(setParam(node, "imageName", e.target.value)) } placeholder="my-org/my-agent" className="font-mono text-xs" />

Defaults to <owner>/<repo> from the agent’s connection.

onChange(setParam(node, "registry", e.target.value)) } placeholder="ghcr.io" className="font-mono text-xs" />

In docker-compose, registry:5000 is the bundled local registry (host port 5050 for docker pull). For ghcr.io, the agent’s PAT must have{" "} write:packages.

onChange(setParam(node, "platform", e.target.value)) } placeholder="linux/amd64" className="font-mono text-xs" />
onChange(setParam(node, "buildArgs", e.target.value)) } placeholder="NODE_ENV=production, FOO=bar" className="font-mono text-xs" />

key=value, comma-separated.

) : ( <>