regime/src/promote.ts
2026-05-01 16:13:52 +02:00

163 lines
4.7 KiB
TypeScript

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<string> {
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<string, string>
templateNames: string[] // all templates in the inheritance tree
}
export async function promote(targetDir: string, yes = false): Promise<void> {
// 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}`)
}