import { defineCommand } from "citty"; import { ctx } from "../lib/context"; import { addAppToCompose, type AppOptions } from "../lib/scaffold"; import { readCompose, resolveComposeFile } from "../lib/yaml"; import { resolveScaffoldInput, terminalPrompt, type AppInput, type ScaffoldPrompt, } from "../lib/scaffold-prompts"; export { terminalPrompt, type AppInput, type ScaffoldPrompt }; export async function resolveAppInput( root: string, input: AppInput, prompt: ScaffoldPrompt = terminalPrompt, composeFile?: string, ): Promise { const file = composeFile ?? (await resolveComposeFile(root)); const existingServices = file ? Object.keys((await readCompose(file)).services ?? {}) : []; return (await resolveScaffoldInput(root, input, prompt, { existingServices })) .app; } export const appArgs = { name: { type: "string" as const, description: "Service name" }, path: { type: "string" as const, description: "App directory relative to the project", }, source: { type: "string" as const, description: "image, auto (Buildpacks), or template", }, image: { type: "string" as const, description: "Image reference when source=image", }, cnb: { type: "string" as const, description: "Optional CNB URI when source=auto", }, template: { type: "string" as const, description: "Dockerfile template ID when source=template", }, postgres: { type: "string" as const, description: "Postgres claim name or user/database", }, s3: { type: "string" as const, description: "S3 claim name or key/bucket" }, cpu: { type: "string" as const, description: "CPU limit in cores" }, memory: { type: "string" as const, description: "Memory limit, e.g. 512m" }, replicas: { type: "string" as const, description: "Replica count" }, "non-interactive": { type: "boolean" as const, description: "Use defaults and require explicit choices when ambiguous", }, }; export function appInputFromArgs(args: Record): AppInput { return { ...Object.fromEntries( [ "name", "path", "source", "image", "template", "cnb", "postgres", "s3", "cpu", "memory", ] .filter((key) => args[key] !== undefined) .map((key) => [key, String(args[key])]), ), ...(args.replicas !== undefined ? { replicas: Number(args.replicas) } : {}), nonInteractive: Boolean(args["non-interactive"]), }; } export const add = defineCommand({ meta: { name: "add", description: "Add a resource to this project" }, subCommands: { app: defineCommand({ meta: { name: "app", description: "Add a service without changing existing services", }, args: appArgs, async run({ args }) { const { cwd, path } = ctx(); const file = await path(); const app = await resolveAppInput( cwd, appInputFromArgs(args), terminalPrompt, file, ); const composePath = await addAppToCompose(cwd, app, file); console.log(`Added ${app.name} to ${composePath}`); }, }), }, });