185 lines
5.9 KiB
TypeScript
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")
|
|
}
|
|
}
|