limgen
Version:
Infrastructure as Code generator
160 lines (127 loc) • 5.63 kB
text/typescript
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
};