From 1e18ea466c10549a19cce7a10b975c41293f2184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 3 Feb 2026 18:06:12 -0300 Subject: [PATCH] refactor: improve lock files resolution --- script/upstream/merge.ts | 32 ++++- script/upstream/transforms/lock-files.ts | 175 +++++++++++++++++++++++ script/upstream/utils/config.ts | 17 +++ 3 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 script/upstream/transforms/lock-files.ts diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 02f8deb82eb..b31cbc571c0 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -47,6 +47,7 @@ import { transformAllExtensions, } from "./transforms/transform-extensions" import { transformConflictedWeb, isWebFile, transformAllWeb } from "./transforms/transform-web" +import { resolveLockFileConflicts, regenerateLockFiles } from "./transforms/lock-files" interface MergeOptions { version?: string @@ -480,6 +481,19 @@ async function main() { logger.success(`Auto-resolved ${webCount} web/docs conflicts`) } } + + // Resolve lock file conflicts (accept ours, will regenerate later) + conflictedFiles = await git.getConflictedFiles() + if (conflictedFiles.length > 0) { + const lockResults = await resolveLockFileConflicts({ + dryRun: false, + verbose: options.verbose, + }) + const lockCount = lockResults.filter((r) => r.action === "resolved").length + if (lockCount > 0) { + logger.success(`Resolved ${lockCount} lock file conflicts (will regenerate)`) + } + } } // Check remaining conflicts @@ -525,8 +539,22 @@ async function main() { } } - // Step 8: Push and cleanup (only reached if no manual conflicts) - logger.step(8, 8, "Finalizing...") + // Step 8: Regenerate lock files and finalize + logger.step(8, 8, "Regenerating lock files and finalizing...") + + // Regenerate lock files (bun.lock, Cargo.lock, etc.) + const lockRegenResults = await regenerateLockFiles({ dryRun: false, verbose: options.verbose }) + const regeneratedCount = lockRegenResults.filter((r) => r.action === "regenerated").length + if (regeneratedCount > 0) { + logger.success(`Regenerated ${regeneratedCount} lock file(s)`) + // Stage and commit the regenerated lock files + await git.stageAll() + const hasLockChanges = await git.hasUncommittedChanges() + if (hasLockChanges) { + await git.commit("chore: regenerate lock files after upstream merge") + logger.success("Committed regenerated lock files") + } + } if (options.push) { await git.push(config.originRemote, kiloBranch) diff --git a/script/upstream/transforms/lock-files.ts b/script/upstream/transforms/lock-files.ts new file mode 100644 index 00000000000..2151273389c --- /dev/null +++ b/script/upstream/transforms/lock-files.ts @@ -0,0 +1,175 @@ +#!/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 }) + } + } + } + + 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") + } +} diff --git a/script/upstream/utils/config.ts b/script/upstream/utils/config.ts index 75c09632e0f..b8cd8e0a18a 100644 --- a/script/upstream/utils/config.ts +++ b/script/upstream/utils/config.ts @@ -33,6 +33,9 @@ export interface MergeConfig { /** Web/docs files */ webFiles: string[] + /** Lock files to accept ours and regenerate after merge */ + lockFiles: string[] + /** Directories that are Kilo-specific and should be preserved */ kiloDirectories: string[] @@ -146,6 +149,20 @@ export const defaultConfig: MergeConfig = { // Web/docs files webFiles: ["packages/web/src/content/docs/**/*.mdx"], + // Lock files to accept ours and regenerate after merge + lockFiles: [ + "bun.lock", + "**/bun.lock", + "package-lock.json", + "**/package-lock.json", + "yarn.lock", + "**/yarn.lock", + "pnpm-lock.yaml", + "**/pnpm-lock.yaml", + "Cargo.lock", + "**/Cargo.lock", + ], + kiloDirectories: [ "packages/opencode/src/kilocode", "packages/opencode/test/kilocode",