mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #11124 from Kilo-Org/clover-tuck
fix: prevent Bun downgrades during upstream merges
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.13",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
|
||||
@@ -48,7 +48,7 @@ bun run merge.ts --version v1.1.50 --base-branch catrielmuller/kilo-opencode-v1.
|
||||
| `transforms/skip-files.ts` | Skip/remove files that shouldn't exist in Kilo |
|
||||
| `transforms/transform-i18n.ts` | Transform i18n files with Kilo branding |
|
||||
| `transforms/transform-take-theirs.ts` | Take upstream + apply Kilo branding for branding-only files |
|
||||
| `transforms/transform-package-json.ts` | Enhanced package.json with Kilo dependency injection |
|
||||
| `transforms/transform-package-json.ts` | Enhanced package.json with Kilo dependency injection and newest-Bun-wins reconciliation |
|
||||
| `transforms/transform-scripts.ts` | Transform script files with GitHub API references |
|
||||
| `transforms/transform-extensions.ts` | Transform extension files (Zed, etc.) |
|
||||
| `transforms/transform-web.ts` | Transform web/docs files (.mdx) |
|
||||
@@ -196,6 +196,10 @@ Now:
|
||||
|
||||
The only remaining conflicts are files with **actual code differences** - files with `kilocode_change` markers that contain Kilo-specific logic.
|
||||
|
||||
### Bun Version Safety
|
||||
|
||||
Root `package.json` reconciliation uses the newer valid `packageManager` Bun version from Kilo and upstream. An older upstream version cannot downgrade Kilo, while a newer upstream version is retained as an upgrade. Before the merge is finalized, `merge.ts` also validates the result against the pristine Kilo base and upstream commit and aborts if the merged Bun version is lower than either input.
|
||||
|
||||
## CLI Options
|
||||
|
||||
### merge.ts
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
transformConflictedPackageJson,
|
||||
transformAllPackageJson,
|
||||
reconcileAllPackageJson,
|
||||
assertBunPackageManager,
|
||||
} from "./transforms/transform-package-json"
|
||||
import { transformConflictedScripts, transformAllScripts } from "./transforms/transform-scripts"
|
||||
import { transformConflictedExtensions, transformAllExtensions } from "./transforms/transform-extensions"
|
||||
@@ -206,6 +207,27 @@ async function getAuthor(): Promise<string> {
|
||||
.replace(/\s+/g, "")
|
||||
}
|
||||
|
||||
function manager(content: string): string | undefined {
|
||||
const pkg: unknown = JSON.parse(content)
|
||||
if (!pkg || typeof pkg !== "object" || !("packageManager" in pkg)) return undefined
|
||||
return typeof pkg.packageManager === "string" ? pkg.packageManager : undefined
|
||||
}
|
||||
|
||||
async function managerAt(ref: string): Promise<string | undefined> {
|
||||
const result = await $`git show ${ref}:package.json`.quiet().nothrow()
|
||||
if (result.exitCode === 0) return manager(result.stdout.toString())
|
||||
logger.warn(`Could not read package.json at ${ref}; excluding it from Bun packageManager validation`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function validateBun(base: string, upstream: string): Promise<void> {
|
||||
const current = manager(await Bun.file("package.json").text())
|
||||
const ours = await managerAt(base)
|
||||
const theirs = await managerAt(upstream)
|
||||
assertBunPackageManager(current, ours, theirs)
|
||||
logger.success(`Validated Bun packageManager: ${current ?? "missing"}`)
|
||||
}
|
||||
|
||||
async function createBackupBranch(baseBranch: string): Promise<string> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
|
||||
const backupName = `backup/${baseBranch}-${timestamp}`
|
||||
@@ -807,6 +829,7 @@ async function main() {
|
||||
// Exit early - don't continue to finalization steps
|
||||
process.exit(1)
|
||||
} else {
|
||||
await validateBun(baseSha, targetVersion.commit)
|
||||
await git.stageAll()
|
||||
await git.commit(`merge: upstream ${targetVersion.tag}`)
|
||||
logger.success("Merge completed - all conflicts auto-resolved!")
|
||||
@@ -824,6 +847,7 @@ async function main() {
|
||||
if (reconcileCount > 0) {
|
||||
logger.success(`Reconciled ${reconcileCount} package.json file(s) post-merge`)
|
||||
}
|
||||
await validateBun(baseSha, targetVersion.commit)
|
||||
await git.stageAll()
|
||||
const hasChanges = await git.hasUncommittedChanges()
|
||||
if (hasChanges) {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { fixCatalog, fixMetadata, fixScripts, mergeWithNewestVersions } from "./transform-package-json"
|
||||
import {
|
||||
assertBunPackageManager,
|
||||
fixCatalog,
|
||||
fixMetadata,
|
||||
fixPackageManager,
|
||||
fixScripts,
|
||||
mergeWithNewestVersions,
|
||||
selectBunPackageManager,
|
||||
} from "./transform-package-json"
|
||||
|
||||
test("fixScripts preserves Kilo-only root scripts from base", () => {
|
||||
const ours = {
|
||||
@@ -125,3 +133,62 @@ test("mergeWithNewestVersions appends theirs-only keys at the end", () => {
|
||||
const result = mergeWithNewestVersions(ours, theirs, changes, "dependencies")
|
||||
expect(Object.keys(result)).toEqual(["a", "b", "c"])
|
||||
})
|
||||
|
||||
test("selectBunPackageManager keeps the newer Bun version and prefers Kilo on ties", () => {
|
||||
expect(selectBunPackageManager("bun@1.3.14", "bun@1.3.13")).toBe("bun@1.3.14")
|
||||
expect(selectBunPackageManager("bun@1.3.14", "bun@1.3.15")).toBe("bun@1.3.15")
|
||||
expect(selectBunPackageManager("bun@1.3.14+kilo", "bun@1.3.14+upstream")).toBe("bun@1.3.14+kilo")
|
||||
})
|
||||
|
||||
test("selectBunPackageManager preserves valid versions over malformed values", () => {
|
||||
expect(selectBunPackageManager("bun@1.3.14", "bun@latest")).toBe("bun@1.3.14")
|
||||
expect(selectBunPackageManager("bun@latest", "bun@1.3.15")).toBe("bun@1.3.15")
|
||||
expect(selectBunPackageManager("bun@latest", "npm@11.0.0")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("fixPackageManager prevents root Bun downgrades", () => {
|
||||
const pkg: Record<string, unknown> = { packageManager: "bun@1.3.13" }
|
||||
const ours = { packageManager: "bun@1.3.14" }
|
||||
const changes: string[] = []
|
||||
fixPackageManager(pkg, "package.json", ours, changes)
|
||||
expect(pkg.packageManager).toBe("bun@1.3.14")
|
||||
expect(changes).toEqual(["packageManager: bun@1.3.13 -> bun@1.3.14 (preserved Kilo pin)"])
|
||||
})
|
||||
|
||||
test("fixPackageManager restores a valid Kilo pin over malformed upstream", () => {
|
||||
const pkg: Record<string, unknown> = { packageManager: "bun@latest" }
|
||||
const changes: string[] = []
|
||||
fixPackageManager(pkg, "package.json", { packageManager: "bun@1.3.14" }, changes)
|
||||
expect(pkg.packageManager).toBe("bun@1.3.14")
|
||||
expect(changes).toEqual(["packageManager: bun@latest -> bun@1.3.14 (preserved Kilo pin)"])
|
||||
})
|
||||
|
||||
test("fixPackageManager accepts upstream Bun upgrades", () => {
|
||||
const pkg: Record<string, unknown> = { packageManager: "bun@1.3.15" }
|
||||
const changes: string[] = []
|
||||
fixPackageManager(pkg, "package.json", { packageManager: "bun@1.3.14" }, changes)
|
||||
expect(pkg.packageManager).toBe("bun@1.3.15")
|
||||
expect(changes).toEqual([])
|
||||
})
|
||||
|
||||
test("fixPackageManager ignores nested package.json files", () => {
|
||||
const pkg: Record<string, unknown> = { packageManager: "bun@1.3.13" }
|
||||
const changes: string[] = []
|
||||
fixPackageManager(pkg, "packages/opencode/package.json", { packageManager: "bun@1.3.14" }, changes)
|
||||
expect(pkg.packageManager).toBe("bun@1.3.13")
|
||||
expect(changes).toEqual([])
|
||||
})
|
||||
|
||||
test("assertBunPackageManager rejects merged downgrades and invalid values", () => {
|
||||
expect(() => assertBunPackageManager("bun@1.3.13", "bun@1.3.14", "bun@1.3.12")).toThrow(
|
||||
"Bun packageManager downgrade detected",
|
||||
)
|
||||
expect(() => assertBunPackageManager("bun@latest", "bun@1.3.14", "bun@1.3.15")).toThrow(
|
||||
"Bun packageManager validation failed",
|
||||
)
|
||||
})
|
||||
|
||||
test("assertBunPackageManager accepts the newest input or a newer result", () => {
|
||||
expect(() => assertBunPackageManager("bun@1.3.15", "bun@1.3.14", "bun@1.3.15")).not.toThrow()
|
||||
expect(() => assertBunPackageManager("bun@1.3.16", "bun@1.3.14", "bun@1.3.15")).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -105,6 +105,50 @@ function compareVersions(a: string, b: string): number | null {
|
||||
return 0
|
||||
}
|
||||
|
||||
function bun(value: unknown): { value: string; version: string } | null {
|
||||
if (typeof value !== "string") return null
|
||||
const match = value.match(/^bun@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/)
|
||||
if (!match) return null
|
||||
return { value, version: match[1] }
|
||||
}
|
||||
|
||||
export function selectBunPackageManager(ours: unknown, theirs: unknown): string | undefined {
|
||||
const left = bun(ours)
|
||||
const right = bun(theirs)
|
||||
if (left && right) return compareVersions(left.version, right.version)! >= 0 ? left.value : right.value
|
||||
if (left) return left.value
|
||||
if (right) return right.value
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function fixPackageManager(
|
||||
pkg: Record<string, unknown>,
|
||||
path: string,
|
||||
ours: Record<string, unknown> | null,
|
||||
changes: string[],
|
||||
): void {
|
||||
if (path !== "package.json") return
|
||||
const next = selectBunPackageManager(ours?.packageManager, pkg.packageManager)
|
||||
if (!next || pkg.packageManager === next) return
|
||||
const prior = typeof pkg.packageManager === "string" ? pkg.packageManager : "missing or invalid"
|
||||
changes.push(`packageManager: ${prior} -> ${next} (preserved Kilo pin)`)
|
||||
pkg.packageManager = next
|
||||
}
|
||||
|
||||
export function assertBunPackageManager(current: unknown, base: unknown, upstream: unknown): void {
|
||||
const inputs = [bun(base), bun(upstream)].filter((item): item is NonNullable<typeof item> => item !== null)
|
||||
if (inputs.length === 0) return
|
||||
const required = inputs.reduce((max, item) => (compareVersions(item.version, max.version)! > 0 ? item : max))
|
||||
const actual = bun(current)
|
||||
if (!actual) {
|
||||
throw new Error(
|
||||
`Bun packageManager validation failed: merged value is invalid; expected at least ${required.value}`,
|
||||
)
|
||||
}
|
||||
if (compareVersions(actual.version, required.version)! >= 0) return
|
||||
throw new Error(`Bun packageManager downgrade detected: merged ${actual.value}, expected at least ${required.value}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two dependency objects using "newest wins" strategy
|
||||
* For non-comparable versions (URLs, catalog:, workspace:*), upstream (theirs) wins
|
||||
@@ -392,6 +436,8 @@ export async function transformPackageJson(file: string, options: PackageJsonOpt
|
||||
pkg.name = newName
|
||||
}
|
||||
|
||||
fixPackageManager(pkg, relativePath, ourPkg, changes)
|
||||
|
||||
// 2. Preserve Kilo version if requested
|
||||
if (options.preserveVersion !== false) {
|
||||
const kiloVersion = await getCurrentVersion()
|
||||
@@ -612,6 +658,8 @@ export async function transformAllPackageJson(options: PackageJsonOptions = {}):
|
||||
pkg.name = newName
|
||||
}
|
||||
|
||||
fixPackageManager(pkg, path, kiloPkg, changes)
|
||||
|
||||
// 2. Preserve Kilo version if requested
|
||||
if (options.preserveVersion !== false) {
|
||||
const kiloVersion = await getCurrentVersion()
|
||||
@@ -824,6 +872,8 @@ export async function reconcilePackageJsonFromRefs(
|
||||
pkg.name = newName
|
||||
}
|
||||
|
||||
fixPackageManager(pkg, relativePath, ourPkg, changes)
|
||||
|
||||
if (options.preserveVersion !== false) {
|
||||
const kiloVersion = await getCurrentVersion()
|
||||
if (pkg.version !== kiloVersion) {
|
||||
|
||||
Reference in New Issue
Block a user