Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion script/upstream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions script/upstream/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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!")
Expand All @@ -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) {
Expand Down
69 changes: 68 additions & 1 deletion script/upstream/transforms/transform-package-json.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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()
})
50 changes: 50 additions & 0 deletions script/upstream/transforms/transform-package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down
Loading