UNPKG

limgen

Version:

Infrastructure as Code generator

160 lines (127 loc) 5.63 kB
import z from 'zod'; import { Command } from 'commander'; import prompts from 'prompts'; import path, { basename } from 'path'; import { detectFramework, FrameworkType, renderFramework, AllFrameworkTypes, importFramework, Framework } from '@/framework'; import { AllProjectTypes, importProjectType, LimgenProject, ProjectType, renderProject } from '@/project'; import { renderWorkspace } from '@/workspace'; import { bold, colorize, parseProcessArgs, spinner } from '@/cli-helpers'; export const initOptionsSchema = z.object({ directory: z.string().optional().default(process.cwd()), name: z.string().optional(), framework: z.string().optional(), projectType: z.enum(AllProjectTypes).optional(), }); export const init = new Command() .name('init') .allowUnknownOption(true) .description('Initialize a new infrastructure project') .option('-d, --directory <directory>', 'Directory where the infrastructure folder should be added', process.cwd()) .option('-n, --name <name>', 'Name of the project') .option('-t, --projectType <type>', 'Type of project to create') .option('-f, --framework <framework>', 'Framework to use') .action(async (options) => { const cmdArgs = initOptionsSchema.parse(options); if (cmdArgs.directory) { process.chdir(cmdArgs.directory); } let frameworkType = cmdArgs.framework as FrameworkType; if (!frameworkType) { frameworkType = await detectFramework(); } await renderWorkspace(); const projectType = await getProjectType(cmdArgs); const project = await importProjectType(projectType); const projectInputs = await collectProjectInputs(project, cmdArgs, projectType, frameworkType); const projectSpinner = spinner(`${bold('Initializing project')} …`).start(); await renderProject(cmdArgs, project, projectInputs); projectSpinner.succeed(); if (AllFrameworkTypes.includes(frameworkType) && frameworkType !== 'unknown') { const frameworkSpinner = spinner(`${bold('Intializing framework')} ${colorize('yellow', frameworkType)} …`).start(); const framework = await importFramework(frameworkType); const frameworkInputs = await collectFrameworkInputs(framework, cmdArgs, projectInputs); await renderFramework(cmdArgs, framework, frameworkInputs); frameworkSpinner.succeed(); } const cmdToRun = `(cd ${path.join('infrastructure', 'projects', projectInputs.projectName)} && pulumi up)`; await animateTyping(`Project ${colorize('cyan', projectInputs.projectName)} initialized successfully!`); await delay(500); await animateTyping(`To deploy your resources, run ${bold(cmdToRun)}\n`); }); export async function getProjectType(cmdArgs: z.infer<typeof initOptionsSchema>) { if (cmdArgs.projectType) { return cmdArgs.projectType; } const answer = await prompts({ type: 'select', name: 'projectType', message: 'Select a project type', choices: AllProjectTypes.map((type) => ({ title: type, value: type })), }); return answer.projectType; } export async function collectProjectInputs(project: LimgenProject, cmdArgs: any, projectType: ProjectType, framework: FrameworkType) { const projectInput = await project.inputs(cmdArgs); const schema = projectInput.reduce((acc, opt) => { acc[opt.name] = opt.schema; return acc; }, {} as any); const processArgs = parseProcessArgs(); for (const opt of projectInput) { if (processArgs[opt.name] === undefined) { processArgs[opt.name] = 'unknown'; } } let projectName: string = cmdArgs.name as string; if (!projectName) { const result = await prompts({ type: 'text', name: 'projectName', message: 'Enter a project name', initial: `${basename(cmdArgs.directory)}-${projectType}${framework !== 'unknown' ? '-'+framework : ''}`, }); projectName = result.projectName; } const projectArgs = z.object(schema).parse(processArgs); const inputs = await project.collectInput(cmdArgs, projectArgs) as Object; return { projectName, projectType, ...inputs, }; } export async function collectFrameworkInputs(framework: Framework, cmdArgs: any, projectInput: any) { const frameworkInput = await framework.inputs(cmdArgs, projectInput); const schema = frameworkInput.reduce((acc, opt) => { acc[opt.name] = opt.schema; return acc; }, {} as any); const processArgs = parseProcessArgs(); for (const opt of frameworkInput) { if (processArgs[opt.name] === undefined) { processArgs[opt.name] = 'unknown'; } } const frameworkArgs = z.object(schema).parse(processArgs); const inputs = await framework.collectInput(cmdArgs, projectInput, frameworkArgs) as Object; return inputs; } export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const animateTyping = async (message: string) => { // only log if we are in a TTY if (!process.stdout.isTTY) { process.stdout.write(`${message}\n`); return; } const minDelay = 10; // Minimum delay between "keypresses" in milliseconds const maxDelay = 45; // Maximum delay between "keypresses" in milliseconds const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); process.stdout.write("\n"); for (let i = 0; i < message.length; i++) { const char = message[i]; process.stdout.write(char); // Print one character at a time const randomDelay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; await delay(randomDelay); } process.stdout.write("\n"); // Move to the next line after completing the animation };