Files
2026-02-05 13:31:46 -03:00

185 lines
5.9 KiB
TypeScript

#!/usr/bin/env bun
/**
* Lock files transform - handles lock file conflicts by accepting ours and regenerating
*
* Lock files (bun.lock, package-lock.json, yarn.lock, Cargo.lock, etc.) should not be
* manually merged. Instead, we accept our version to resolve the conflict, then regenerate
* the lock file fresh after the merge is complete.
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
import { checkoutOurs, stageFiles, getConflictedFiles } from "../utils/git"
export interface LockFileResult {
file: string
action: "resolved" | "skipped" | "regenerated" | "failed"
dryRun: boolean
}
export interface LockFileOptions {
dryRun?: boolean
verbose?: boolean
patterns?: string[]
}
/**
* Check if a file is a lock file based on patterns
*/
export function isLockFile(path: string, patterns?: string[]): boolean {
const lockPatterns = patterns || defaultConfig.lockFiles
return lockPatterns.some((pattern) => {
// Exact match
if (path === pattern) return true
// Glob pattern with **
if (pattern.includes("**")) {
const regex = new RegExp("^" + pattern.replace(/\*\*/g, ".*").replace(/\./g, "\\.") + "$")
return regex.test(path)
}
// Simple glob pattern
if (pattern.includes("*")) {
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, "[^/]*") + "$")
return regex.test(path)
}
// Basename match (e.g., "bun.lock" matches "packages/foo/bun.lock")
const basename = path.split("/").pop()
return basename === pattern
})
}
/**
* Resolve lock file conflicts by accepting our version
*/
export async function resolveLockFileConflicts(options: LockFileOptions = {}): Promise<LockFileResult[]> {
const results: LockFileResult[] = []
const patterns = options.patterns || defaultConfig.lockFiles
const conflicted = await getConflictedFiles()
if (conflicted.length === 0) {
debug("No conflicted files found")
return results
}
const lockFiles = conflicted.filter((file) => isLockFile(file, patterns))
if (lockFiles.length === 0) {
debug("No lock file conflicts found")
return results
}
info(`Found ${lockFiles.length} conflicted lock file(s)`)
for (const file of lockFiles) {
if (options.dryRun) {
info(`[DRY-RUN] Would resolve conflict (accept ours): ${file}`)
results.push({ file, action: "resolved", dryRun: true })
continue
}
try {
await checkoutOurs([file])
await stageFiles([file])
success(`Resolved lock file conflict (accepted ours): ${file}`)
results.push({ file, action: "resolved", dryRun: false })
} catch (err) {
warn(`Failed to resolve lock file conflict: ${file} - ${err}`)
results.push({ file, action: "failed", dryRun: false })
}
}
return results
}
/**
* Regenerate lock files after merge
*/
export async function regenerateLockFiles(options: LockFileOptions = {}): Promise<LockFileResult[]> {
const results: LockFileResult[] = []
// Check if bun.lock exists or was part of the merge
const hasBunLock = await Bun.file("bun.lock").exists()
if (hasBunLock) {
if (options.dryRun) {
info("[DRY-RUN] Would regenerate bun.lock via 'bun install'")
results.push({ file: "bun.lock", action: "regenerated", dryRun: true })
} else {
info("Regenerating bun.lock...")
const result = await $`bun install`.quiet().nothrow()
if (result.exitCode === 0) {
success("Regenerated bun.lock")
results.push({ file: "bun.lock", action: "regenerated", dryRun: false })
} else {
warn(`Failed to regenerate bun.lock: ${result.stderr.toString()}`)
results.push({ file: "bun.lock", action: "failed", dryRun: false })
}
}
}
// Check for Cargo.lock in Tauri package
const cargoLockPath = "packages/desktop/src-tauri/Cargo.lock"
const hasCargoLock = await Bun.file(cargoLockPath).exists()
if (hasCargoLock) {
if (options.dryRun) {
info("[DRY-RUN] Would regenerate Cargo.lock via 'cargo generate-lockfile'")
results.push({ file: cargoLockPath, action: "regenerated", dryRun: true })
} else {
info("Regenerating Cargo.lock...")
const result = await $`cargo generate-lockfile`.cwd("packages/desktop/src-tauri").quiet().nothrow()
if (result.exitCode === 0) {
success("Regenerated Cargo.lock")
results.push({ file: cargoLockPath, action: "regenerated", dryRun: false })
} else {
// Cargo might not be installed, just warn
warn(`Could not regenerate Cargo.lock (cargo may not be installed): ${result.stderr.toString()}`)
results.push({ file: cargoLockPath, action: "skipped", dryRun: false })
}
}
}
// Note about nix/hashes.json - regenerated by CI, not locally
const nixHashesPath = "nix/hashes.json"
const hasNixHashes = await Bun.file(nixHashesPath).exists()
if (hasNixHashes) {
info("Note: nix/hashes.json will be regenerated by CI (update-nix-hashes.yml) after PR is created")
results.push({ file: nixHashesPath, action: "skipped", dryRun: options.dryRun ?? false })
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const regenerate = args.includes("--regenerate")
if (dryRun) {
info("Running in dry-run mode (no files will be modified)")
}
if (regenerate) {
const results = await regenerateLockFiles({ dryRun, verbose })
const regenerated = results.filter((r) => r.action === "regenerated")
console.log()
success(`Regenerated ${regenerated.length} lock file(s)`)
} else {
const results = await resolveLockFileConflicts({ dryRun, verbose })
const resolved = results.filter((r) => r.action === "resolved")
console.log()
success(`Resolved ${resolved.length} lock file conflict(s)`)
}
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}