#!/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 { 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 { 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") } }