transform-package-json.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. #!/usr/bin/env bun
  2. /**
  3. * Enhanced package.json transform with Kilo dependency injection
  4. *
  5. * This script handles package.json conflicts by:
  6. * 1. Taking upstream's version (to get new dependencies)
  7. * 2. Transforming package names (opencode -> kilo)
  8. * 3. Injecting Kilo-specific dependencies
  9. * 4. Preserving Kilo's version number
  10. * 5. Preserving overrides and patchedDependencies
  11. * 6. Preserving Kilo's repository configuration
  12. * 7. Using "newest wins" strategy for dependency versions
  13. */
  14. import { $ } from "bun"
  15. import { info, success, warn, debug } from "../utils/logger"
  16. import { getCurrentVersion } from "./preserve-versions"
  17. import { oursHasKilocodeChanges } from "../utils/git"
  18. /**
  19. * Extract clean version string from a version specifier
  20. * Removes ^, ~, >=, etc. prefixes
  21. */
  22. function extractVersion(version: string): string | null {
  23. // Handle special formats that can't be compared
  24. if (
  25. version.startsWith("workspace:") ||
  26. version.startsWith("catalog:") ||
  27. version.startsWith("http://") ||
  28. version.startsWith("https://") ||
  29. version.startsWith("git://") ||
  30. version.startsWith("git+") ||
  31. version.startsWith("file:") ||
  32. version.startsWith("link:") ||
  33. version.startsWith("npm:")
  34. ) {
  35. return null
  36. }
  37. // Remove common prefixes: ^, ~, >=, >, <=, <, =
  38. const cleaned = version.replace(/^[\^~>=<]+/, "").trim()
  39. // Basic semver validation (x.y.z with optional pre-release/build)
  40. if (/^\d+\.\d+\.\d+/.test(cleaned)) {
  41. return cleaned
  42. }
  43. // Handle x.y format
  44. if (/^\d+\.\d+$/.test(cleaned)) {
  45. return cleaned + ".0"
  46. }
  47. // Handle single number
  48. if (/^\d+$/.test(cleaned)) {
  49. return cleaned + ".0.0"
  50. }
  51. return null
  52. }
  53. /**
  54. * Parse a semver string into components
  55. */
  56. function parseSemver(version: string): { major: number; minor: number; patch: number; prerelease: string } | null {
  57. const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/)
  58. if (!match) return null
  59. return {
  60. major: parseInt(match[1], 10),
  61. minor: parseInt(match[2], 10),
  62. patch: parseInt(match[3], 10),
  63. prerelease: match[4] || "",
  64. }
  65. }
  66. /**
  67. * Compare two version strings
  68. * Returns: 1 if a > b, -1 if a < b, 0 if equal
  69. * For special formats (URLs, catalog:, workspace:*), returns null (can't compare)
  70. */
  71. function compareVersions(a: string, b: string): number | null {
  72. const cleanA = extractVersion(a)
  73. const cleanB = extractVersion(b)
  74. // If either can't be parsed, return null (can't compare)
  75. if (!cleanA || !cleanB) return null
  76. const semverA = parseSemver(cleanA)
  77. const semverB = parseSemver(cleanB)
  78. if (!semverA || !semverB) return null
  79. // Compare major.minor.patch
  80. if (semverA.major !== semverB.major) return semverA.major > semverB.major ? 1 : -1
  81. if (semverA.minor !== semverB.minor) return semverA.minor > semverB.minor ? 1 : -1
  82. if (semverA.patch !== semverB.patch) return semverA.patch > semverB.patch ? 1 : -1
  83. // Handle prerelease (no prerelease > prerelease)
  84. if (!semverA.prerelease && semverB.prerelease) return 1
  85. if (semverA.prerelease && !semverB.prerelease) return -1
  86. if (semverA.prerelease && semverB.prerelease) {
  87. return semverA.prerelease.localeCompare(semverB.prerelease)
  88. }
  89. return 0
  90. }
  91. /**
  92. * Merge two dependency objects using "newest wins" strategy
  93. * For non-comparable versions (URLs, catalog:, workspace:*), upstream (theirs) wins
  94. */
  95. function mergeWithNewestVersions(
  96. ours: Record<string, string> | undefined,
  97. theirs: Record<string, string> | undefined,
  98. changes: string[],
  99. section: string,
  100. ): Record<string, string> {
  101. const result: Record<string, string> = {}
  102. // Start with all of theirs
  103. if (theirs) {
  104. for (const [name, version] of Object.entries(theirs)) {
  105. result[name] = version
  106. }
  107. }
  108. // Merge in ours, keeping newer versions
  109. if (ours) {
  110. for (const [name, ourVersion] of Object.entries(ours)) {
  111. const theirVersion = result[name]
  112. if (!theirVersion) {
  113. // Dependency only exists in ours - keep it
  114. result[name] = ourVersion
  115. changes.push(`${section}: preserved ${name}@${ourVersion} (kilo-only)`)
  116. } else if (ourVersion !== theirVersion) {
  117. // Both have it with different versions - compare
  118. const comparison = compareVersions(ourVersion, theirVersion)
  119. if (comparison === null) {
  120. // Can't compare (special format) - upstream wins per user preference
  121. changes.push(`${section}: ${name} kept upstream ${theirVersion} (special format)`)
  122. } else if (comparison > 0) {
  123. // Ours is newer
  124. result[name] = ourVersion
  125. changes.push(`${section}: ${name} ${theirVersion} -> ${ourVersion} (kilo newer)`)
  126. } else if (comparison < 0) {
  127. // Theirs is newer - already in result
  128. changes.push(`${section}: ${name} kept upstream ${theirVersion} (upstream newer)`)
  129. }
  130. // If equal, keep theirs (already in result)
  131. }
  132. }
  133. }
  134. return result
  135. }
  136. export interface PackageJsonResult {
  137. file: string
  138. action: "transformed" | "skipped" | "failed" | "flagged"
  139. changes: string[]
  140. dryRun: boolean
  141. }
  142. export interface PackageJsonOptions {
  143. dryRun?: boolean
  144. verbose?: boolean
  145. preserveVersion?: boolean
  146. }
  147. // Package name mappings
  148. const PACKAGE_NAME_MAP: Record<string, string> = {
  149. "opencode-ai": "@kilocode/cli",
  150. "@opencode-ai/cli": "@kilocode/cli",
  151. "@opencode-ai/sdk": "@kilocode/sdk",
  152. "@opencode-ai/plugin": "@kilocode/plugin",
  153. }
  154. // Kilo-specific dependencies to inject into specific packages
  155. // NOTE: When adding new Kilo-specific workspace dependencies (packages starting with @kilocode/kilo-*),
  156. // add them here to prevent them from being removed during upstream merges
  157. const KILO_DEPENDENCIES: Record<string, Record<string, string>> = {
  158. // packages/opencode/package.json needs these
  159. "packages/opencode/package.json": {
  160. "@kilocode/kilo-gateway": "workspace:*",
  161. "@kilocode/kilo-telemetry": "workspace:*",
  162. },
  163. // packages/app/package.json needs these
  164. "packages/app/package.json": {
  165. "@kilocode/kilo-i18n": "workspace:*",
  166. },
  167. }
  168. // Kilo-specific bin entries to set on specific packages
  169. const KILO_BIN: Record<string, Record<string, string>> = {
  170. "packages/opencode/package.json": {
  171. kilo: "./bin/kilo",
  172. kilocode: "./bin/kilo",
  173. },
  174. }
  175. // Packages that should have their name transformed
  176. const TRANSFORM_PACKAGE_NAMES: Record<string, string> = {
  177. "package.json": "@kilocode/kilo",
  178. "packages/opencode/package.json": "@kilocode/cli",
  179. "packages/plugin/package.json": "@kilocode/plugin",
  180. "packages/sdk/js/package.json": "@kilocode/sdk",
  181. }
  182. /**
  183. * Check if file is a package.json
  184. */
  185. export function isPackageJson(file: string): boolean {
  186. return file.endsWith("package.json")
  187. }
  188. /**
  189. * Transform dependencies in package.json
  190. */
  191. function transformDependencies(deps: Record<string, string> | undefined): {
  192. result: Record<string, string>
  193. changes: string[]
  194. } {
  195. if (!deps) return { result: {}, changes: [] }
  196. const result: Record<string, string> = {}
  197. const changes: string[] = []
  198. for (const [name, version] of Object.entries(deps)) {
  199. const newName = PACKAGE_NAME_MAP[name]
  200. if (newName) {
  201. result[newName] = version
  202. changes.push(`${name} -> ${newName}`)
  203. } else {
  204. result[name] = version
  205. }
  206. }
  207. return { result, changes }
  208. }
  209. /**
  210. * Transform a package.json file
  211. */
  212. export async function transformPackageJson(file: string, options: PackageJsonOptions = {}): Promise<PackageJsonResult> {
  213. const changes: string[] = []
  214. if (options.dryRun) {
  215. info(`[DRY-RUN] Would transform package.json: ${file}`)
  216. return { file, action: "transformed", changes: [], dryRun: true }
  217. }
  218. // If our version has kilocode_change markers, flag for manual resolution
  219. if (await oursHasKilocodeChanges(file)) {
  220. warn(`${file} has kilocode_change markers — skipping auto-transform, needs manual resolution`)
  221. return { file, action: "flagged", changes: [], dryRun: false }
  222. }
  223. try {
  224. // Save Kilo's version BEFORE taking theirs
  225. let ourPkg: Record<string, unknown> | null = null
  226. try {
  227. const ourContent = await $`git show :2:${file}`.text() // :2: is "ours" in merge
  228. ourPkg = JSON.parse(ourContent)
  229. } catch {
  230. // File might not exist in ours (new file from upstream)
  231. // or we're not in a merge conflict - try reading current file
  232. try {
  233. const currentContent = await Bun.file(file).text()
  234. if (!currentContent.includes("<<<<<<<")) {
  235. // Not a conflict, read as-is
  236. ourPkg = JSON.parse(currentContent)
  237. }
  238. } catch {
  239. // File doesn't exist yet
  240. }
  241. }
  242. // Take upstream's version
  243. await $`git checkout --theirs ${file}`.quiet().nothrow()
  244. await $`git add ${file}`.quiet().nothrow()
  245. // Read and parse upstream's version
  246. const content = await Bun.file(file).text()
  247. const pkg = JSON.parse(content)
  248. // 1. Transform package name if needed
  249. const relativePath = file.replace(process.cwd() + "/", "")
  250. const newName = TRANSFORM_PACKAGE_NAMES[relativePath]
  251. if (newName && pkg.name !== newName) {
  252. changes.push(`name: ${pkg.name} -> ${newName}`)
  253. pkg.name = newName
  254. }
  255. // 2. Preserve Kilo version if requested
  256. if (options.preserveVersion !== false) {
  257. const kiloVersion = await getCurrentVersion()
  258. if (pkg.version !== kiloVersion) {
  259. changes.push(`version: ${pkg.version} -> ${kiloVersion}`)
  260. pkg.version = kiloVersion
  261. }
  262. }
  263. // 3. Merge dependencies with "newest wins" strategy
  264. if (ourPkg) {
  265. pkg.dependencies = mergeWithNewestVersions(
  266. ourPkg.dependencies as Record<string, string> | undefined,
  267. pkg.dependencies,
  268. changes,
  269. "dependencies",
  270. )
  271. pkg.devDependencies = mergeWithNewestVersions(
  272. ourPkg.devDependencies as Record<string, string> | undefined,
  273. pkg.devDependencies,
  274. changes,
  275. "devDependencies",
  276. )
  277. pkg.peerDependencies = mergeWithNewestVersions(
  278. ourPkg.peerDependencies as Record<string, string> | undefined,
  279. pkg.peerDependencies,
  280. changes,
  281. "peerDependencies",
  282. )
  283. // 4. Preserve/merge overrides
  284. const ourOverrides = ourPkg.overrides as Record<string, string> | undefined
  285. if (ourOverrides || pkg.overrides) {
  286. pkg.overrides = mergeWithNewestVersions(ourOverrides, pkg.overrides, changes, "overrides")
  287. }
  288. // 5. Preserve patchedDependencies (Kilo-specific, upstream won't have these)
  289. const ourPatchedDeps = ourPkg.patchedDependencies as Record<string, string> | undefined
  290. if (ourPatchedDeps) {
  291. pkg.patchedDependencies = pkg.patchedDependencies || {}
  292. for (const [name, patch] of Object.entries(ourPatchedDeps)) {
  293. if (!pkg.patchedDependencies[name]) {
  294. pkg.patchedDependencies[name] = patch
  295. changes.push(`patchedDependencies: preserved ${name}`)
  296. }
  297. }
  298. }
  299. // 6. Preserve repository (Kilo-specific, upstream doesn't have this)
  300. const ourRepo = ourPkg.repository
  301. if (ourRepo && JSON.stringify(pkg.repository) !== JSON.stringify(ourRepo)) {
  302. pkg.repository = ourRepo
  303. changes.push(`repository: preserved Kilo's repository configuration`)
  304. }
  305. // 7. Handle workspaces for root package.json
  306. // Kilo has removed hosted platform packages (console/*, slack, etc.)
  307. // so we need to preserve Kilo's workspace configuration instead of taking upstream's
  308. const ourWorkspaces = ourPkg.workspaces as { packages?: string[]; catalog?: Record<string, string> } | undefined
  309. const theirWorkspaces = pkg.workspaces as { packages?: string[]; catalog?: Record<string, string> } | undefined
  310. if (relativePath === "package.json" && ourWorkspaces?.packages) {
  311. pkg.workspaces = pkg.workspaces || {}
  312. pkg.workspaces.packages = ourWorkspaces.packages
  313. changes.push(`workspaces.packages: preserved Kilo's workspace configuration`)
  314. }
  315. const ourScripts = ourPkg.scripts as Record<string, string> | undefined
  316. if (relativePath === "package.json" && ourScripts?.extension && pkg.scripts?.extension !== ourScripts.extension) {
  317. pkg.scripts = pkg.scripts || {}
  318. pkg.scripts.extension = ourScripts.extension
  319. changes.push(`scripts.extension: preserved Kilo's extension script`)
  320. }
  321. if (relativePath === "package.json" && ourScripts?.changeset && pkg.scripts?.changeset !== ourScripts.changeset) {
  322. pkg.scripts = pkg.scripts || {}
  323. pkg.scripts.changeset = ourScripts.changeset
  324. changes.push(`scripts.changeset: preserved Kilo's changeset script`)
  325. }
  326. if (
  327. relativePath === "package.json" &&
  328. ourScripts?.["changeset:version"] &&
  329. pkg.scripts?.["changeset:version"] !== ourScripts["changeset:version"]
  330. ) {
  331. pkg.scripts = pkg.scripts || {}
  332. pkg.scripts["changeset:version"] = ourScripts["changeset:version"]
  333. changes.push(`scripts.changeset:version: preserved Kilo's changeset:version script`)
  334. }
  335. // Preserve Kilo's test runner scripts for packages/opencode
  336. if (
  337. relativePath === "packages/opencode/package.json" &&
  338. ourScripts?.test &&
  339. pkg.scripts?.test !== ourScripts.test
  340. ) {
  341. pkg.scripts = pkg.scripts || {}
  342. pkg.scripts.test = ourScripts.test
  343. changes.push(`scripts.test: preserved Kilo's test runner script`)
  344. }
  345. if (
  346. relativePath === "packages/opencode/package.json" &&
  347. ourScripts?.["test:ci"] &&
  348. pkg.scripts?.["test:ci"] !== ourScripts["test:ci"]
  349. ) {
  350. pkg.scripts = pkg.scripts || {}
  351. pkg.scripts["test:ci"] = ourScripts["test:ci"]
  352. changes.push(`scripts.test:ci: preserved Kilo's CI test runner script`)
  353. }
  354. // Merge catalog with "newest wins" strategy
  355. if (ourWorkspaces?.catalog || theirWorkspaces?.catalog) {
  356. pkg.workspaces = pkg.workspaces || {}
  357. pkg.workspaces.catalog = mergeWithNewestVersions(
  358. ourWorkspaces?.catalog,
  359. theirWorkspaces?.catalog,
  360. changes,
  361. "workspaces.catalog",
  362. )
  363. }
  364. }
  365. // 7. Transform dependency names (opencode -> kilo)
  366. if (pkg.dependencies) {
  367. const { result, changes: depChanges } = transformDependencies(pkg.dependencies)
  368. pkg.dependencies = result
  369. changes.push(...depChanges.map((c) => `dependencies: ${c}`))
  370. }
  371. if (pkg.devDependencies) {
  372. const { result, changes: devChanges } = transformDependencies(pkg.devDependencies)
  373. if (devChanges.length > 0) {
  374. pkg.devDependencies = result
  375. changes.push(...devChanges.map((c) => `devDependencies: ${c}`))
  376. }
  377. }
  378. if (pkg.peerDependencies) {
  379. const { result, changes: peerChanges } = transformDependencies(pkg.peerDependencies)
  380. if (peerChanges.length > 0) {
  381. pkg.peerDependencies = result
  382. changes.push(...peerChanges.map((c) => `peerDependencies: ${c}`))
  383. }
  384. }
  385. // 8. Inject Kilo-specific dependencies
  386. const kiloDeps = KILO_DEPENDENCIES[relativePath]
  387. if (kiloDeps) {
  388. pkg.dependencies = pkg.dependencies || {}
  389. for (const [name, version] of Object.entries(kiloDeps)) {
  390. if (!pkg.dependencies[name]) {
  391. pkg.dependencies[name] = version
  392. changes.push(`injected: ${name}`)
  393. }
  394. }
  395. }
  396. // 9. Set Kilo-specific bin entries
  397. const kiloBin = KILO_BIN[relativePath]
  398. if (kiloBin) {
  399. pkg.bin = kiloBin
  400. changes.push(`bin: set Kilo bin entries`)
  401. }
  402. // Write back with proper formatting
  403. const newContent = JSON.stringify(pkg, null, 2) + "\n"
  404. await Bun.write(file, newContent)
  405. await $`git add ${file}`.quiet().nothrow()
  406. if (changes.length > 0) {
  407. success(`Transformed ${file}: ${changes.length} changes`)
  408. if (options.verbose) {
  409. for (const change of changes) {
  410. debug(` - ${change}`)
  411. }
  412. }
  413. }
  414. return { file, action: "transformed", changes, dryRun: false }
  415. } catch (err) {
  416. warn(`Failed to transform ${file}: ${err}`)
  417. return { file, action: "failed", changes: [], dryRun: false }
  418. }
  419. }
  420. /**
  421. * Transform conflicted package.json files
  422. */
  423. export async function transformConflictedPackageJson(
  424. files: string[],
  425. options: PackageJsonOptions = {},
  426. ): Promise<PackageJsonResult[]> {
  427. const results: PackageJsonResult[] = []
  428. for (const file of files) {
  429. if (!isPackageJson(file)) {
  430. results.push({ file, action: "skipped", changes: [], dryRun: options.dryRun ?? false })
  431. continue
  432. }
  433. const result = await transformPackageJson(file, options)
  434. results.push(result)
  435. }
  436. return results
  437. }
  438. /**
  439. * Get Kilo's package.json from the base branch (main) for comparison
  440. * Used during pre-merge to compare upstream versions against Kilo's versions
  441. */
  442. async function getKiloPackageJson(path: string, baseBranch = "main"): Promise<Record<string, unknown> | null> {
  443. try {
  444. // Try to get the file from origin/main (or whatever base branch)
  445. const content = await $`git show origin/${baseBranch}:${path}`.text()
  446. return JSON.parse(content)
  447. } catch {
  448. // File might not exist in Kilo
  449. return null
  450. }
  451. }
  452. /**
  453. * Transform all package.json files (pre-merge, on opencode branch)
  454. * This function merges Kilo's versions with upstream, using "newest wins" strategy
  455. */
  456. export async function transformAllPackageJson(options: PackageJsonOptions = {}): Promise<PackageJsonResult[]> {
  457. const { Glob } = await import("bun")
  458. const results: PackageJsonResult[] = []
  459. // Find all package.json files
  460. const glob = new Glob("**/package.json")
  461. for await (const path of glob.scan({ absolute: false })) {
  462. // Skip node_modules
  463. if (path.includes("node_modules")) continue
  464. const file = Bun.file(path)
  465. if (!(await file.exists())) continue
  466. try {
  467. const content = await file.text()
  468. const pkg = JSON.parse(content) // This is upstream's version
  469. const changes: string[] = []
  470. // Get Kilo's version from base branch for comparison
  471. const kiloPkg = await getKiloPackageJson(path)
  472. // 1. Transform package name if needed
  473. const newName = TRANSFORM_PACKAGE_NAMES[path]
  474. if (newName && pkg.name !== newName) {
  475. changes.push(`name: ${pkg.name} -> ${newName}`)
  476. pkg.name = newName
  477. }
  478. // 2. Preserve Kilo version if requested
  479. if (options.preserveVersion !== false) {
  480. const kiloVersion = await getCurrentVersion()
  481. if (pkg.version !== kiloVersion) {
  482. changes.push(`version: ${pkg.version} -> ${kiloVersion}`)
  483. pkg.version = kiloVersion
  484. }
  485. }
  486. // 3. Merge dependencies with "newest wins" strategy (if Kilo has this file)
  487. if (kiloPkg) {
  488. pkg.dependencies = mergeWithNewestVersions(
  489. kiloPkg.dependencies as Record<string, string> | undefined,
  490. pkg.dependencies,
  491. changes,
  492. "dependencies",
  493. )
  494. pkg.devDependencies = mergeWithNewestVersions(
  495. kiloPkg.devDependencies as Record<string, string> | undefined,
  496. pkg.devDependencies,
  497. changes,
  498. "devDependencies",
  499. )
  500. pkg.peerDependencies = mergeWithNewestVersions(
  501. kiloPkg.peerDependencies as Record<string, string> | undefined,
  502. pkg.peerDependencies,
  503. changes,
  504. "peerDependencies",
  505. )
  506. // 4. Preserve/merge overrides
  507. const kiloOverrides = kiloPkg.overrides as Record<string, string> | undefined
  508. if (kiloOverrides || pkg.overrides) {
  509. pkg.overrides = mergeWithNewestVersions(kiloOverrides, pkg.overrides, changes, "overrides")
  510. }
  511. // 5. Preserve patchedDependencies (Kilo-specific, upstream won't have these)
  512. const kiloPatchedDeps = kiloPkg.patchedDependencies as Record<string, string> | undefined
  513. if (kiloPatchedDeps) {
  514. pkg.patchedDependencies = pkg.patchedDependencies || {}
  515. for (const [name, patch] of Object.entries(kiloPatchedDeps)) {
  516. if (!pkg.patchedDependencies[name]) {
  517. pkg.patchedDependencies[name] = patch
  518. changes.push(`patchedDependencies: preserved ${name}`)
  519. }
  520. }
  521. }
  522. // 6. Preserve repository (Kilo-specific, upstream doesn't have this)
  523. const kiloRepo = kiloPkg.repository
  524. if (kiloRepo && JSON.stringify(pkg.repository) !== JSON.stringify(kiloRepo)) {
  525. pkg.repository = kiloRepo
  526. changes.push(`repository: preserved Kilo's repository configuration`)
  527. }
  528. // 7. Handle workspaces for root package.json
  529. // Kilo has removed hosted platform packages (console/*, slack, etc.)
  530. // so we need to preserve Kilo's workspace configuration instead of taking upstream's
  531. const kiloWorkspaces = kiloPkg.workspaces as
  532. | { packages?: string[]; catalog?: Record<string, string> }
  533. | undefined
  534. const upstreamWorkspaces = pkg.workspaces as
  535. | { packages?: string[]; catalog?: Record<string, string> }
  536. | undefined
  537. if (path === "package.json" && kiloWorkspaces?.packages) {
  538. pkg.workspaces = pkg.workspaces || {}
  539. pkg.workspaces.packages = kiloWorkspaces.packages
  540. changes.push(`workspaces.packages: preserved Kilo's workspace configuration`)
  541. }
  542. const kiloScripts = kiloPkg.scripts as Record<string, string> | undefined
  543. if (path === "package.json" && kiloScripts?.extension && pkg.scripts?.extension !== kiloScripts.extension) {
  544. pkg.scripts = pkg.scripts || {}
  545. pkg.scripts.extension = kiloScripts.extension
  546. changes.push(`scripts.extension: preserved Kilo's extension script`)
  547. }
  548. // Preserve Kilo's test runner scripts for packages/opencode
  549. if (path === "packages/opencode/package.json" && kiloScripts?.test && pkg.scripts?.test !== kiloScripts.test) {
  550. pkg.scripts = pkg.scripts || {}
  551. pkg.scripts.test = kiloScripts.test
  552. changes.push(`scripts.test: preserved Kilo's test runner script`)
  553. }
  554. if (
  555. path === "packages/opencode/package.json" &&
  556. kiloScripts?.["test:ci"] &&
  557. pkg.scripts?.["test:ci"] !== kiloScripts["test:ci"]
  558. ) {
  559. pkg.scripts = pkg.scripts || {}
  560. pkg.scripts["test:ci"] = kiloScripts["test:ci"]
  561. changes.push(`scripts.test:ci: preserved Kilo's CI test runner script`)
  562. }
  563. // Merge catalog with "newest wins" strategy
  564. if (kiloWorkspaces?.catalog || upstreamWorkspaces?.catalog) {
  565. pkg.workspaces = pkg.workspaces || {}
  566. pkg.workspaces.catalog = mergeWithNewestVersions(
  567. kiloWorkspaces?.catalog,
  568. upstreamWorkspaces?.catalog,
  569. changes,
  570. "workspaces.catalog",
  571. )
  572. }
  573. }
  574. // 7. Transform dependency names (opencode -> kilo)
  575. if (pkg.dependencies) {
  576. const { result, changes: depChanges } = transformDependencies(pkg.dependencies)
  577. if (depChanges.length > 0) {
  578. pkg.dependencies = result
  579. changes.push(...depChanges.map((c) => `dependencies: ${c}`))
  580. }
  581. }
  582. if (pkg.devDependencies) {
  583. const { result, changes: devChanges } = transformDependencies(pkg.devDependencies)
  584. if (devChanges.length > 0) {
  585. pkg.devDependencies = result
  586. changes.push(...devChanges.map((c) => `devDependencies: ${c}`))
  587. }
  588. }
  589. if (pkg.peerDependencies) {
  590. const { result, changes: peerChanges } = transformDependencies(pkg.peerDependencies)
  591. if (peerChanges.length > 0) {
  592. pkg.peerDependencies = result
  593. changes.push(...peerChanges.map((c) => `peerDependencies: ${c}`))
  594. }
  595. }
  596. // 8. Inject Kilo-specific dependencies
  597. const kiloDeps = KILO_DEPENDENCIES[path]
  598. if (kiloDeps) {
  599. pkg.dependencies = pkg.dependencies || {}
  600. for (const [name, version] of Object.entries(kiloDeps)) {
  601. if (!pkg.dependencies[name]) {
  602. pkg.dependencies[name] = version
  603. changes.push(`injected: ${name}`)
  604. }
  605. }
  606. }
  607. // 9. Set Kilo-specific bin entries
  608. const kiloBin = KILO_BIN[path]
  609. if (kiloBin) {
  610. pkg.bin = kiloBin
  611. changes.push(`bin: set Kilo bin entries`)
  612. }
  613. if (changes.length > 0) {
  614. if (!options.dryRun) {
  615. const newContent = JSON.stringify(pkg, null, 2) + "\n"
  616. await Bun.write(path, newContent)
  617. success(`Transformed ${path}: ${changes.length} changes`)
  618. } else {
  619. info(`[DRY-RUN] Would transform ${path}: ${changes.length} changes`)
  620. }
  621. }
  622. results.push({ file: path, action: "transformed", changes, dryRun: options.dryRun ?? false })
  623. } catch (err) {
  624. warn(`Failed to transform ${path}: ${err}`)
  625. results.push({ file: path, action: "failed", changes: [], dryRun: options.dryRun ?? false })
  626. }
  627. }
  628. return results
  629. }
  630. // CLI entry point
  631. if (import.meta.main) {
  632. const args = process.argv.slice(2)
  633. const dryRun = args.includes("--dry-run")
  634. const verbose = args.includes("--verbose")
  635. const files = args.filter((a) => !a.startsWith("--"))
  636. if (files.length === 0) {
  637. info("Usage: transform-package-json.ts [--dry-run] [--verbose] <file1> <file2> ...")
  638. process.exit(1)
  639. }
  640. if (dryRun) {
  641. info("Running in dry-run mode")
  642. }
  643. const results = await transformConflictedPackageJson(files, { dryRun, verbose })
  644. const transformed = results.filter((r) => r.action === "transformed")
  645. const totalChanges = results.reduce((sum, r) => sum + r.changes.length, 0)
  646. console.log()
  647. success(`Transformed ${transformed.length} package.json files with ${totalChanges} changes`)
  648. if (dryRun) {
  649. info("Run without --dry-run to apply changes")
  650. }
  651. }