import z, { ZodObject, ZodRawShape } from "zod"; import { parseYaml } from "./yaml"; import { PromptObject } from "prompts"; import { ResourceGroupType } from "./resourceGroups"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; export class BaseTemplate { name: string; directory: string; resourceGroupType?: ResourceGroupType; data: any; inputSchema: ZodObject | undefined; constructor( name: string, directory: string, resourceGroupType?: ResourceGroupType, dirname: string = __dirname ) { this.name = name; this.directory = directory; this.resourceGroupType = resourceGroupType; this.data = {}; const templatePath = `${dirname}/template.yaml`; if (existsSync(templatePath)) { this.inputSchema = parseInputSchema(readFileSync(templatePath, "utf-8"))!; } } public scripts: Record = {}; public static resourceGroupsSupported: ResourceGroupType[] = []; public async collectInputs(inputs: any) { if (typeof this.inputSchema === "undefined") { throw new Error("Error parsing input schema"); } this.data = this.inputSchema.parse(inputs); } async dependsOn(): Promise<{ files: string[]; packages: string[] }> { return { files: [], packages: [] }; } async generate(): Promise { throw new Error("Method not implemented."); } async destroy(): Promise { throw new Error("Method not implemented."); } async envPull(options: any): Promise { throw new Error("Method not implemented."); } } export function parseInputSchema(template: string) { const data = parseYaml(template) as Record; if (!data || typeof data !== "object") { return; } if (!data["inputs"] || !Array.isArray(data["inputs"])) { return; } const inputSchema = z.object({ name: z.string(), description: z.string(), required: z.boolean().optional().default(false), default: z.any().optional(), type: z.enum(["text", "number", "bool", "choice"]), choices: z .array( z.object({ name: z.string(), value: z.string() }) ) .optional() }); const inputs = data.inputs.map((input: Record) => { return inputSchema.parse(input); }); let resultSchema = z.object({}); inputs.forEach((input) => { let type; switch (input.type) { case "text": type = input.required ? z.string() : z.string().optional(); if (input.default) { type = type.default(input.default); } break; case "number": type = input.required ? z.number() : z.number().optional(); if (input.default) { type = type.default(input.default); } break; case "bool": type = input.required ? z.boolean() : z.boolean().optional(); if (input.default) { type = type.default(input.default); } break; case "choice": if (!input.choices) { throw new Error("Choice input must have choices"); } const choices: string[] = input.choices.map((choice) => choice.value); // @ts-ignore - allow enums from dynamic string array type = z.enum(choices); if (!input.required) { type = type.optional(); } if (input.default) { type = type.default(input.default); } break; } type = type.describe(input.description ?? "unknown"); const newSchema = { [input.name]: type }; resultSchema = resultSchema.merge(z.object(newSchema)); }); return resultSchema; } export async function getInputPrompts(schema: ZodObject) { // loop over properties in schema and prompt user for input const allPrompts: PromptObject[] = Object.keys(schema.shape).map( (key: string) => { const shape: ZodRawShape = schema.shape; const prop = shape[key]; let type = prop._def.typeName; let propChoices: string[] = []; let defaultValue = type === "ZodString" ? "" : type === "ZodNumber" ? -1 : false; if (type === "ZodEnum") { propChoices = prop._def.values; } if (type === "ZodOptional" || type === "ZodDefault") { if (type === "ZodDefault") { defaultValue = prop._def.defaultValue(); } type = prop._def.innerType._def.typeName; if (type === "ZodEnum") { propChoices = prop._def.innerType._def.values; } } if (type === "ZodEnum" && propChoices.length > 0 && defaultValue) { defaultValue = propChoices.indexOf(defaultValue as string); } let promptType = type === "ZodString" ? "text" : type === "ZodNumber" ? "number" : "confirm"; if (type === "ZodEnum") { promptType = "select"; } const choices: { choices?: { title: string; value: string }[] } = promptType === "select" ? { choices: propChoices.map((value: string) => { return { title: value, value: value }; }) } : {}; const initialValue = !defaultValue ? {} : { initial: defaultValue }; const result = { type: promptType, name: key, message: prop._def.description, ...initialValue, ...choices } as PromptObject; return result; } ); return allPrompts; }