import { dirname, join } from "node:path" import { existsSync } from "node:fs" import { writeFile } from "node:fs/promises" import { $ } from "bun" import { type RegimeConfig, findRegimeConfigs, resolveTemplateChain, getStrategy, interpolate, deinterpolate, readFileSync, templatesDir, } from "./shared" const green = Bun.color("green", "ansi") const orange = Bun.color("orange", "ansi") const purple = Bun.color("purple", "ansi") const reset = "\x1b[0m" async function gumStdin(command: string, input: string): Promise { const proc = Bun.spawn(["gum", command], { stdin: Buffer.from(input), stdout: "pipe", stderr: "inherit", }) const output = await new Response(proc.stdout).text() await proc.exited return output } interface PromotableFile { relPath: string // relative path within template/repo (e.g. "tsconfig.json") repoPath: string // absolute path in the repo configDir: string // directory containing regime.config.json vars: Record templateNames: string[] // all templates in the inheritance tree } export async function promote(targetDir: string, yes = false): Promise { // Step 1: Find regime configs const rcFiles = await findRegimeConfigs(targetDir) if (rcFiles.length === 0) { console.error("No regime.config.json files found.") process.exit(1) } // Step 2: Collect promotable files (overwrite-strategy files that differ) const promotable: PromotableFile[] = [] for (const rcFile of rcFiles) { const rcDir = dirname(rcFile) const rc: RegimeConfig = JSON.parse(readFileSync(rcFile)) const templateNames = Array.isArray(rc.templates) ? rc.templates : [rc.templates] const vars = rc.vars ?? {} const { files, patterns, templateNames: chainNames } = resolveTemplateChain(templateNames) for (const [relPath, templatePaths] of files) { const strategy = getStrategy(relPath, patterns) if (strategy !== "overwrite") continue const repoPath = join(rcDir, relPath) if (!existsSync(repoPath)) continue const repoContent = readFileSync(repoPath) const templateContent = interpolate( readFileSync(templatePaths[templatePaths.length - 1]), vars, relPath, ) if (repoContent !== templateContent) { promotable.push({ relPath, repoPath, configDir: rcDir, vars, templateNames: chainNames, }) } } } if (promotable.length === 0) { console.log("Nothing to promote.") return } // Step 3: Interactive file selection via gum filter const fileList = promotable.map(p => p.relPath).join("\n") const selectedFile = (await gumStdin("filter", fileList)).trim() if (!selectedFile) { console.log("No file selected.") return } const entry = promotable.find(p => p.relPath === selectedFile) if (!entry) { console.error(`File "${selectedFile}" not found in promotable list.`) process.exit(1) } // Step 4: Interactive template selection via gum choose let selectedTemplate: string if (entry.templateNames.length === 1) { selectedTemplate = entry.templateNames[0] } else { const templateList = entry.templateNames.join("\n") selectedTemplate = (await gumStdin("choose", templateList)).trim() } if (!selectedTemplate) { console.log("No template selected.") return } // Step 5: De-interpolate const repoContent = readFileSync(entry.repoPath) const promoted = deinterpolate(repoContent, entry.vars) // Step 6: Show diff const templateFilePath = join(templatesDir, selectedTemplate, entry.relPath) console.log(`\n${purple}Promoting${reset} ${entry.relPath} -> ${green}${selectedTemplate}${reset}`) if (existsSync(templateFilePath)) { const tmpFile = `/tmp/regime-promote-${Date.now()}` await writeFile(tmpFile, promoted) try { const diff = await $`diff -u ${templateFilePath} ${tmpFile} || true`.text() if (diff.trim()) { console.log(diff) } else { console.log("No changes detected after de-interpolation.") return } } finally { await $`rm -f ${tmpFile}`.quiet() } } else { console.log(`${orange}New file${reset} — will be created in template "${selectedTemplate}"`) console.log(promoted) } // Step 7: Confirm if (!yes) { const confirmed = await $`gum confirm "Write to template?"`.nothrow() if (confirmed.exitCode !== 0) { console.log("Cancelled.") return } } // Step 8: Write const targetFileDir = dirname(templateFilePath) if (!existsSync(targetFileDir)) { await $`mkdir -p ${targetFileDir}`.quiet() } await writeFile(templateFilePath, promoted) console.log(`${green}Written${reset} ${templateFilePath}`) }