import yaml from "js-yaml"; import z from "zod"; import path from "path"; import { existsSync, readFileSync } from "fs"; import { mkdir, writeFile, readFile, readdir } from "fs/promises"; import { jsonToYaml, parseYaml } from "@/yaml"; import { cwd } from "process"; export const operationTypes = [ "addComponents", "removeComponents", "installComponents", "addInstance", "removeInstance", "initResourceGroup", "removeResourceGroup", "applyResourceGroupChanges", "undoResourceGroupChanges", "configureFramework", "runCodeMod" ] as const; export const codeModTypes = ["configureNext"] as const; export const extraOptionTypes = [...codeModTypes] as const; export type OperationType = (typeof operationTypes)[number]; export type ExtraOptionTypes = (typeof extraOptionTypes)[number]; export type OperationOptions = { options: OptionsType; cmdArgs?: { noScroll?: boolean; }; }; export type OptionsType = { type?: ExtraOptionTypes; args: Record; }; export type Operation = { type: OperationType; options?: OptionsType; }; export type Migration = { id: string; up: Operation[]; down?: Operation[]; }; export const migrationSchema = z.object({ id: z.string(), up: z.array( z.object({ type: z.enum(operationTypes), options: z .object({ type: z.enum(extraOptionTypes).optional(), args: z.record( z.string(), z.string().or(z.record(z.string(), z.any())) ) }) .optional() }) ), down: z .array( z.object({ type: z.enum(operationTypes), options: z .object({ type: z.enum(extraOptionTypes).optional(), args: z.record( z.string(), z.string().or(z.record(z.string(), z.any())) ) }) .optional() }) ) .optional() }); export const parseMigration = (yamlString: string): Migration => { const base: unknown = yaml.load(yamlString); const parsed = migrationSchema.safeParse(base); if (!parsed.success) { throw new Error(parsed.error.errors.join(", ")); } return parsed.data; }; export const exportMigration = async (migration: Migration, path: string) => { const migrationHistory = await getMigrationHistory(); if (migrationHistory.migrations.includes(migration.id)) { throw new Error(`Migration ${migration.id} already exists`); } const newMigration = jsonToYaml(migration); await writeFile(path, newMigration); }; export async function ensureMigrations() { await mkdir(path.join("infrastructure", "migrations"), { recursive: true }); const historyPath = path.join("infrastructure", "migrations", "history.yaml"); if (!existsSync(historyPath)) { await writeFile(historyPath, jsonToYaml({ migrations: [] })); } } export async function getMigrationHistory() { const historyPath = path.join("infrastructure", "migrations", "history.yaml"); const history = await readFile(historyPath, "utf8"); const yamlHistory = parseYaml(history); return yamlHistory as { migrations: string[] }; } export async function getMigrationFiles() { const migrationsPath = path.join("infrastructure", "migrations"); const files = await readdir(migrationsPath); return files.filter( (file) => file.endsWith(".yaml") && !file.endsWith("history.yaml") ); } export async function getUnappliedMigrations(): Promise { const history = await getMigrationHistory(); const migrationFiles = (await getMigrationFiles()).sort(); const migrationPath = path.join(cwd(), "infrastructure", "migrations"); const migrations: Migration[] = migrationFiles.map((file) => { const migrationYaml = readFileSync(path.join(migrationPath, file), "utf-8"); return parseMigration(migrationYaml); }); return migrations.filter( (migration) => !history.migrations.includes(migration.id) ); } export async function updateMigrationHistory( migrationId: string, apply: boolean = true ) { const historyPath = path.join("infrastructure", "migrations", "history.yaml"); const history = await getMigrationHistory(); if (history.migrations.includes(migrationId)) { if (apply) { throw new Error(`Migration ${migrationId} already applied`); } history.migrations = history.migrations.filter((id) => id !== migrationId); } else { history.migrations.push(migrationId); } await writeFile(historyPath, jsonToYaml(history)); }