163 lines
4.7 KiB
TypeScript
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}`)
|
|
}
|