feat(regime)
This commit is contained in:
commit
1f712c1c84
37 changed files with 1059 additions and 0 deletions
163
src/promote.ts
Normal file
163
src/promote.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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}`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue