transform-tauri.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. #!/usr/bin/env bun
  2. /**
  3. * Transform Tauri/Desktop config files with Kilo branding
  4. *
  5. * This script handles Tauri configuration files (JSON, TOML, Rust) by:
  6. * 1. Taking upstream's version as the base
  7. * 2. Applying predictable Kilo branding transforms
  8. *
  9. * Handles:
  10. * - tauri.conf.json / tauri.prod.conf.json
  11. * - Cargo.toml / Cargo.lock
  12. * - Rust source files (*.rs)
  13. */
  14. import { $ } from "bun"
  15. import { info, success, warn, debug } from "../utils/logger"
  16. import { defaultConfig } from "../utils/config"
  17. import { oursHasKilocodeChanges } from "../utils/git"
  18. export interface TauriTransformResult {
  19. file: string
  20. action: "transformed" | "skipped" | "failed" | "flagged"
  21. replacements: number
  22. dryRun: boolean
  23. }
  24. export interface TauriTransformOptions {
  25. dryRun?: boolean
  26. verbose?: boolean
  27. }
  28. interface TauriReplacement {
  29. pattern: RegExp
  30. replacement: string
  31. description: string
  32. fileTypes?: string[] // Only apply to these file extensions
  33. }
  34. // Tauri-specific replacements
  35. const TAURI_REPLACEMENTS: TauriReplacement[] = [
  36. // JSON config - product names
  37. {
  38. pattern: /"productName":\s*"OpenCode[^"]*"/g,
  39. replacement: '"productName": "Kilo"',
  40. description: "Product name in JSON",
  41. fileTypes: [".json"],
  42. },
  43. {
  44. pattern: /"title":\s*"OpenCode[^"]*"/g,
  45. replacement: '"title": "Kilo"',
  46. description: "Title in JSON",
  47. fileTypes: [".json"],
  48. },
  49. // JSON config - identifiers
  50. {
  51. pattern: /ai\.opencode\.desktop\.dev/g,
  52. replacement: "ai.kilo.desktop.dev",
  53. description: "Dev identifier",
  54. },
  55. {
  56. pattern: /ai\.opencode\.desktop/g,
  57. replacement: "ai.kilo.desktop",
  58. description: "Prod identifier",
  59. },
  60. // Binary names
  61. {
  62. pattern: /opencode-cli/g,
  63. replacement: "kilo-cli",
  64. description: "CLI binary name",
  65. },
  66. {
  67. pattern: /"mainBinaryName":\s*"[Oo]pen[Cc]ode"/g,
  68. replacement: '"mainBinaryName": "Kilo"',
  69. description: "Main binary name",
  70. fileTypes: [".json"],
  71. },
  72. // GitHub references
  73. {
  74. pattern: /github\.com\/anomalyco\/opencode/g,
  75. replacement: "github.com/Kilo-Org/kilocode",
  76. description: "GitHub URL",
  77. },
  78. {
  79. pattern: /anomalyco\/opencode/g,
  80. replacement: "Kilo-Org/kilocode",
  81. description: "GitHub repo",
  82. },
  83. // Cargo.toml specific
  84. {
  85. pattern: /name\s*=\s*"opencode-desktop"/g,
  86. replacement: 'name = "kilo-desktop"',
  87. description: "Cargo package name",
  88. fileTypes: [".toml"],
  89. },
  90. {
  91. pattern: /authors\s*=\s*\["OpenCode"\]/g,
  92. replacement: 'authors = ["Kilo"]',
  93. description: "Cargo authors",
  94. fileTypes: [".toml"],
  95. },
  96. {
  97. pattern: /name\s*=\s*"opencode_lib"/g,
  98. replacement: 'name = "kilo_lib"',
  99. description: "Cargo lib name",
  100. fileTypes: [".toml"],
  101. },
  102. // Rust source specific
  103. {
  104. pattern: /opencode\.db/g,
  105. replacement: "kilo.db",
  106. description: "Database filename",
  107. fileTypes: [".rs"],
  108. },
  109. {
  110. pattern: /opencode\.settings\.dat/g,
  111. replacement: "kilo.settings.dat",
  112. description: "Settings file name",
  113. fileTypes: [".rs"],
  114. },
  115. {
  116. pattern: /"\.opencode\/bin"/g,
  117. replacement: '".kilo/bin"',
  118. description: "CLI install dir",
  119. fileTypes: [".rs"],
  120. },
  121. {
  122. pattern: /CLI_BINARY_NAME\s*=\s*"opencode"/g,
  123. replacement: 'CLI_BINARY_NAME = "kilo"',
  124. description: "CLI binary constant",
  125. fileTypes: [".rs"],
  126. },
  127. {
  128. pattern: /opencode_lib::run/g,
  129. replacement: "kilo_lib::run",
  130. description: "Lib run call",
  131. fileTypes: [".rs"],
  132. },
  133. {
  134. pattern: /killall opencode-cli/g,
  135. replacement: "killall kilo-cli",
  136. description: "Killall command",
  137. fileTypes: [".rs"],
  138. },
  139. // Domain
  140. {
  141. pattern: /opencode\.ai/g,
  142. replacement: "kilo.ai",
  143. description: "Domain",
  144. },
  145. // Environment variables (exclude OPENCODE_API_KEY)
  146. {
  147. pattern: /OPENCODE_(?!API_KEY)([A-Z_]+)/g,
  148. replacement: "KILO_$1",
  149. description: "Env variable",
  150. fileTypes: [".rs"],
  151. },
  152. {
  153. pattern: /__OPENCODE__/g,
  154. replacement: "__KILO__",
  155. description: "Window global",
  156. fileTypes: [".rs", ".tsx"],
  157. },
  158. {
  159. pattern: /OPENCODE_PORT/g,
  160. replacement: "KILO_PORT",
  161. description: "Port env var",
  162. },
  163. ]
  164. /**
  165. * Check if a file is a Tauri config file
  166. */
  167. export function isTauriFile(file: string): boolean {
  168. const patterns = defaultConfig.tauriFiles
  169. return patterns.some((pattern) => {
  170. const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
  171. return regex.test(file)
  172. })
  173. }
  174. /**
  175. * Get file extension
  176. */
  177. function getExtension(file: string): string {
  178. const match = file.match(/\.[^.]+$/)
  179. return match ? match[0] : ""
  180. }
  181. /**
  182. * Apply Tauri-specific transforms to content
  183. */
  184. export function applyTauriTransforms(
  185. content: string,
  186. file: string,
  187. verbose = false,
  188. ): { result: string; replacements: number } {
  189. const ext = getExtension(file)
  190. let result = content
  191. let total = 0
  192. for (const { pattern, replacement, description, fileTypes } of TAURI_REPLACEMENTS) {
  193. // Skip if this replacement is for specific file types and doesn't match
  194. if (fileTypes && !fileTypes.includes(ext)) {
  195. continue
  196. }
  197. pattern.lastIndex = 0
  198. if (pattern.test(result)) {
  199. pattern.lastIndex = 0
  200. const before = result
  201. result = result.replace(pattern, replacement)
  202. if (before !== result) {
  203. total++
  204. if (verbose) debug(` ${description}`)
  205. }
  206. }
  207. }
  208. return { result, replacements: total }
  209. }
  210. /**
  211. * Transform a single Tauri file
  212. */
  213. export async function transformTauriFile(
  214. file: string,
  215. options: TauriTransformOptions = {},
  216. ): Promise<TauriTransformResult> {
  217. if (options.dryRun) {
  218. info(`[DRY-RUN] Would transform Tauri file: ${file}`)
  219. return { file, action: "transformed", replacements: 0, dryRun: true }
  220. }
  221. // If our version has kilocode_change markers, flag for manual resolution
  222. if (await oursHasKilocodeChanges(file)) {
  223. warn(`${file} has kilocode_change markers — skipping auto-transform, needs manual resolution`)
  224. return { file, action: "flagged", replacements: 0, dryRun: false }
  225. }
  226. try {
  227. // Take upstream's version first
  228. await $`git checkout --theirs ${file}`.quiet().nothrow()
  229. await $`git add ${file}`.quiet().nothrow()
  230. // Read content
  231. const content = await Bun.file(file).text()
  232. // Apply transforms
  233. const { result, replacements } = applyTauriTransforms(content, file, options.verbose)
  234. // Write back if changed
  235. if (replacements > 0) {
  236. await Bun.write(file, result)
  237. await $`git add ${file}`.quiet().nothrow()
  238. }
  239. success(`Transformed Tauri file ${file}: ${replacements} replacements`)
  240. return { file, action: "transformed", replacements, dryRun: false }
  241. } catch (err) {
  242. warn(`Failed to transform Tauri file ${file}: ${err}`)
  243. return { file, action: "failed", replacements: 0, dryRun: false }
  244. }
  245. }
  246. /**
  247. * Transform conflicted Tauri files
  248. */
  249. export async function transformConflictedTauri(
  250. files: string[],
  251. options: TauriTransformOptions = {},
  252. ): Promise<TauriTransformResult[]> {
  253. const results: TauriTransformResult[] = []
  254. for (const file of files) {
  255. if (!isTauriFile(file)) {
  256. debug(`Skipping ${file} - not a Tauri file`)
  257. results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
  258. continue
  259. }
  260. const result = await transformTauriFile(file, options)
  261. results.push(result)
  262. }
  263. return results
  264. }
  265. /**
  266. * Transform all Tauri files (pre-merge, on opencode branch)
  267. */
  268. export async function transformAllTauri(options: TauriTransformOptions = {}): Promise<TauriTransformResult[]> {
  269. const { Glob } = await import("bun")
  270. const results: TauriTransformResult[] = []
  271. const patterns = defaultConfig.tauriFiles
  272. for (const pattern of patterns) {
  273. const glob = new Glob(pattern)
  274. for await (const path of glob.scan({ absolute: false })) {
  275. const file = Bun.file(path)
  276. if (!(await file.exists())) continue
  277. try {
  278. const content = await file.text()
  279. const { result, replacements } = applyTauriTransforms(content, path, options.verbose)
  280. if (replacements > 0 && !options.dryRun) {
  281. await Bun.write(path, result)
  282. success(`Transformed Tauri ${path}: ${replacements} replacements`)
  283. } else if (options.dryRun && replacements > 0) {
  284. info(`[DRY-RUN] Would transform Tauri ${path}: ${replacements} replacements`)
  285. }
  286. results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
  287. } catch (err) {
  288. warn(`Failed to transform Tauri ${path}: ${err}`)
  289. results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
  290. }
  291. }
  292. }
  293. return results
  294. }
  295. // CLI entry point
  296. if (import.meta.main) {
  297. const args = process.argv.slice(2)
  298. const dryRun = args.includes("--dry-run")
  299. const verbose = args.includes("--verbose")
  300. const files = args.filter((a) => !a.startsWith("--"))
  301. if (files.length === 0) {
  302. info("Usage: transform-tauri.ts [--dry-run] [--verbose] <file1> <file2> ...")
  303. process.exit(1)
  304. }
  305. if (dryRun) {
  306. info("Running in dry-run mode")
  307. }
  308. const results = await transformConflictedTauri(files, { dryRun, verbose })
  309. const transformed = results.filter((r) => r.action === "transformed")
  310. const total = results.reduce((sum, r) => sum + r.replacements, 0)
  311. console.log()
  312. success(`Transformed ${transformed.length} Tauri files with ${total} replacements`)
  313. if (dryRun) {
  314. info("Run without --dry-run to apply changes")
  315. }
  316. }