import z from "zod"; import { Command } from "commander"; import prompts, { PromptObject } from "prompts"; import path from "path"; import { AllFrameworkTypes, detectFramework, FrameworkType, getSupportedResourceGroupTypesForFramework, importFramework } from "@/frameworks"; import { AllResourceGroupTypes, importResourceGroupType, ResourceGroupType } from "@/resourceGroups"; import { renderWorkspace } from "@/workspace"; import { ensureMigrations, exportMigration, Migration } from "@/migrations/data"; import { cwd } from "process"; import { triggerMigration, ensureMigrationNotApplied } from "@/migrations/apply"; import { getCurrentTimestamp } from "@/utils/date"; import { getInputPrompts } from "@/templates"; import { parseOtherArgs } from "@/cli-helpers"; export const initOptionsSchema = z.object({ directory: z.string().optional().default(process.cwd()), name: z.string().optional(), framework: z.string().optional(), resourceGroupType: z.enum(AllResourceGroupTypes).optional(), noScroll: z.boolean().optional(), apply: z.boolean().optional() }); export const init = new Command() .name("init") .allowUnknownOption(true) .description("Initialize a new infrastructure resource group") .option( "-d, --directory ", "Directory where the infrastructure folder should be added", process.cwd() ) .option("-n, --name ", "Name of the resourceGroup") .option("-t, --resourceGroupType ", "Type of resource group to create") .option("-f, --framework ", "Framework to use") .option("--noScroll", "Disable scrolling output") .option("--apply", "Apply the generated platform migration") .option( "--inputs ", "Resource group / framework specific inputs: comma separated key=value format" ) .action(async (options) => { const cmdArgs = initOptionsSchema.parse(options); const other = parseOtherArgs(options.inputs?.split(",") ?? []); if (cmdArgs.directory) { process.chdir(cmdArgs.directory); } let frameworkType = cmdArgs.framework as FrameworkType; if (!frameworkType) { frameworkType = await detectFramework(cmdArgs.directory); } frameworkType = frameworkType != "unknown" ? frameworkType : await getFramework(cmdArgs); await renderWorkspace(); const resourceGroupName = await getResourceGroupName(cmdArgs); const resourceGroupType = await getResourceGroupType( cmdArgs, frameworkType ); const resourceGroup = importResourceGroupType( resourceGroupType, resourceGroupName, cmdArgs.directory ); if (!resourceGroup || !resourceGroup.inputSchema) { throw new Error("Resource group type not found"); } const promptInputs: PromptObject[] = []; if (frameworkType !== "unknown") { const framework = await importFramework( frameworkType, resourceGroupType, resourceGroupName, cmdArgs.directory ); if (!framework) { throw new Error("Framework not found"); } if (framework.inputSchema) { const frameworkInputs = await getInputPrompts(framework.inputSchema); promptInputs.push(...frameworkInputs); } } const resourceGroupInputs = await getInputPrompts( resourceGroup.inputSchema ); resourceGroupInputs.forEach((resourceGroupInput) => { if ( !promptInputs.find((input) => input.name === resourceGroupInput.name) ) { promptInputs.push(resourceGroupInput); } }); const answers: Record = {}; const newPrompts = promptInputs.filter((resourceGroup) => { const key = resourceGroup.name as string; const type = resourceGroup.type; if (other[key]) { switch (type) { case "text": answers[key] = other[key]; break; case "number": answers[key] = parseInt(other[key]); break; case "confirm": answers[key] = other[key] === "true"; break; case "select": if ( !resourceGroup.choices || !Array.isArray(resourceGroup.choices) ) { throw new Error( `Error choices not found for select type: ${resourceGroup.name}` ); } if ( resourceGroup.choices!.filter( (choice) => choice.value === other[key] ).length === 0 ) { throw new Error( `Error choice ${other[key]} not found for select type: ${resourceGroup.name}` ); } answers[key] = other[key]; default: break; } return false; } return true; }); const inputs = await prompts(newPrompts); const initID = `init-${resourceGroupName}`.replace(" ", "-"); const initMigration: Migration = { id: initID, up: [ { type: "initResourceGroup", options: { args: { name: resourceGroupName, type: resourceGroupType as string, framework: frameworkType, inputs: { ...answers, ...inputs } } } } ], down: [ { type: "removeResourceGroup", options: { args: { name: resourceGroupName } } } ] }; if (frameworkType !== "unknown") { initMigration.up.push({ type: "configureFramework", options: { args: { frameworkType: frameworkType, resourceGroupType: resourceGroupType, resourceGroupName: resourceGroupName, operation: "apply", inputs: { ...answers, ...inputs } } } }); if (!initMigration.down) { initMigration.down = []; } initMigration.down.push({ type: "configureFramework", options: { args: { frameworkType: frameworkType, resourceGroupType: resourceGroupType, resourceGroupName: resourceGroupName, operation: "revert", inputs: { ...answers, ...inputs } } } }); } await ensureMigrations(); const infrastructurePath = path.join(cwd(), "infrastructure"); const migrationPath = path.join(infrastructurePath, "migrations"); const timestamp = getCurrentTimestamp(); await ensureMigrationNotApplied(initMigration); await exportMigration( initMigration, path.join(migrationPath, `${timestamp}-${initID}.yaml`) ); if (cmdArgs.apply === true) { await triggerMigration(initMigration, true, { noScroll: cmdArgs.noScroll }); } else { console.log("Migration created but not applied."); } }); export async function getResourceGroupName( cmdArgs: z.infer ) { if (cmdArgs.name) { return cmdArgs.name; } const answer = await prompts({ type: "text", name: "name", message: "Select a resource group name" }); return answer.name; } export async function getFramework(cmdArgs: z.infer) { if (cmdArgs.framework) { return cmdArgs.framework; } const answer = await prompts({ type: "select", name: "framework", message: "Select a framework", choices: AllFrameworkTypes.map((type) => ({ title: type, value: type })) }); return answer.framework; } export async function getResourceGroupType( cmdArgs: z.infer, frameworkType: FrameworkType ) { if (cmdArgs.resourceGroupType) { return cmdArgs.resourceGroupType; } const supportedResourceGroupTypes = frameworkType === "unknown" ? AllResourceGroupTypes : await getSupportedResourceGroupTypesForFramework(frameworkType); const answer = await prompts({ type: "select", name: "resourceGroupType", message: "Select a resource group type", choices: supportedResourceGroupTypes.map((type) => ({ title: type, value: type })) }); return answer.resourceGroupType; }