transform-scripts.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env bun
  2. /**
  3. * Transform script files with GitHub API references
  4. *
  5. * This script handles script files that contain GitHub API references
  6. * by transforming them from anomalyco/opencode to Kilo-Org/kilocode.
  7. */
  8. import { $ } from "bun"
  9. import { info, success, warn, debug } from "../utils/logger"
  10. import { defaultConfig } from "../utils/config"
  11. import { oursHasKilocodeChanges } from "../utils/git"
  12. export interface ScriptTransformResult {
  13. file: string
  14. action: "transformed" | "skipped" | "failed" | "flagged"
  15. replacements: number
  16. dryRun: boolean
  17. }
  18. export interface ScriptTransformOptions {
  19. dryRun?: boolean
  20. verbose?: boolean
  21. }
  22. interface ScriptReplacement {
  23. pattern: RegExp
  24. replacement: string
  25. description: string
  26. }
  27. // Script-specific replacements
  28. const SCRIPT_REPLACEMENTS: ScriptReplacement[] = [
  29. // GitHub API URLs
  30. {
  31. pattern: /api\.github\.com\/repos\/anomalyco\/opencode/g,
  32. replacement: "api.github.com/repos/Kilo-Org/kilocode",
  33. description: "GitHub API URL",
  34. },
  35. {
  36. pattern: /\/repos\/anomalyco\/opencode/g,
  37. replacement: "/repos/Kilo-Org/kilocode",
  38. description: "GitHub repos path",
  39. },
  40. // gh CLI commands
  41. {
  42. pattern: /gh api "\/repos\/anomalyco\/opencode/g,
  43. replacement: 'gh api "/repos/Kilo-Org/kilocode',
  44. description: "gh api command",
  45. },
  46. // Direct GitHub references
  47. {
  48. pattern: /github\.com\/anomalyco\/opencode/g,
  49. replacement: "github.com/Kilo-Org/kilocode",
  50. description: "GitHub URL",
  51. },
  52. {
  53. pattern: /anomalyco\/opencode/g,
  54. replacement: "Kilo-Org/kilocode",
  55. description: "GitHub repo reference",
  56. },
  57. // Environment variables (exclude OPENCODE_API_KEY)
  58. {
  59. pattern: /\bOPENCODE_(?!API_KEY\b)([A-Z_]+)\b/g,
  60. replacement: "KILO_$1",
  61. description: "Environment variable",
  62. },
  63. // OpenCode branding in strings
  64. {
  65. pattern: /"OpenCode"/g,
  66. replacement: '"Kilo"',
  67. description: "Product name in string",
  68. },
  69. {
  70. pattern: /'OpenCode'/g,
  71. replacement: "'Kilo'",
  72. description: "Product name in single quotes",
  73. },
  74. ]
  75. /**
  76. * Check if file is a script file
  77. */
  78. export function isScriptFile(file: string): boolean {
  79. const patterns = defaultConfig.scriptFiles
  80. return patterns.some((pattern) => {
  81. const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
  82. return regex.test(file)
  83. })
  84. }
  85. /**
  86. * Apply script transforms to content
  87. */
  88. export function applyScriptTransforms(content: string, verbose = false): { result: string; replacements: number } {
  89. let result = content
  90. let total = 0
  91. for (const { pattern, replacement, description } of SCRIPT_REPLACEMENTS) {
  92. pattern.lastIndex = 0
  93. if (pattern.test(result)) {
  94. pattern.lastIndex = 0
  95. const before = result
  96. result = result.replace(pattern, replacement)
  97. if (before !== result) {
  98. total++
  99. if (verbose) debug(` ${description}`)
  100. }
  101. }
  102. }
  103. return { result, replacements: total }
  104. }
  105. /**
  106. * Transform a script file
  107. */
  108. export async function transformScriptFile(
  109. file: string,
  110. options: ScriptTransformOptions = {},
  111. ): Promise<ScriptTransformResult> {
  112. if (options.dryRun) {
  113. info(`[DRY-RUN] Would transform script: ${file}`)
  114. return { file, action: "transformed", replacements: 0, dryRun: true }
  115. }
  116. // If our version has kilocode_change markers, flag for manual resolution
  117. if (await oursHasKilocodeChanges(file)) {
  118. warn(`${file} has kilocode_change markers — skipping auto-transform, needs manual resolution`)
  119. return { file, action: "flagged", replacements: 0, dryRun: false }
  120. }
  121. try {
  122. // Take upstream's version first
  123. await $`git checkout --theirs ${file}`.quiet().nothrow()
  124. await $`git add ${file}`.quiet().nothrow()
  125. // Read content
  126. const content = await Bun.file(file).text()
  127. // Apply transforms
  128. const { result, replacements } = applyScriptTransforms(content, options.verbose)
  129. // Write back if changed
  130. if (replacements > 0) {
  131. await Bun.write(file, result)
  132. await $`git add ${file}`.quiet().nothrow()
  133. }
  134. success(`Transformed script ${file}: ${replacements} replacements`)
  135. return { file, action: "transformed", replacements, dryRun: false }
  136. } catch (err) {
  137. warn(`Failed to transform script ${file}: ${err}`)
  138. return { file, action: "failed", replacements: 0, dryRun: false }
  139. }
  140. }
  141. /**
  142. * Transform conflicted script files
  143. */
  144. export async function transformConflictedScripts(
  145. files: string[],
  146. options: ScriptTransformOptions = {},
  147. ): Promise<ScriptTransformResult[]> {
  148. const results: ScriptTransformResult[] = []
  149. for (const file of files) {
  150. if (!isScriptFile(file)) {
  151. debug(`Skipping ${file} - not a script file`)
  152. results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
  153. continue
  154. }
  155. const result = await transformScriptFile(file, options)
  156. results.push(result)
  157. }
  158. return results
  159. }
  160. /**
  161. * Transform all script files (pre-merge, on opencode branch)
  162. */
  163. export async function transformAllScripts(options: ScriptTransformOptions = {}): Promise<ScriptTransformResult[]> {
  164. const { Glob } = await import("bun")
  165. const results: ScriptTransformResult[] = []
  166. const patterns = defaultConfig.scriptFiles
  167. for (const pattern of patterns) {
  168. const glob = new Glob(pattern)
  169. for await (const path of glob.scan({ absolute: false })) {
  170. const file = Bun.file(path)
  171. if (!(await file.exists())) continue
  172. try {
  173. const content = await file.text()
  174. const { result, replacements } = applyScriptTransforms(content, options.verbose)
  175. if (replacements > 0 && !options.dryRun) {
  176. await Bun.write(path, result)
  177. success(`Transformed script ${path}: ${replacements} replacements`)
  178. } else if (options.dryRun && replacements > 0) {
  179. info(`[DRY-RUN] Would transform script ${path}: ${replacements} replacements`)
  180. }
  181. results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
  182. } catch (err) {
  183. warn(`Failed to transform script ${path}: ${err}`)
  184. results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
  185. }
  186. }
  187. }
  188. return results
  189. }
  190. // CLI entry point
  191. if (import.meta.main) {
  192. const args = process.argv.slice(2)
  193. const dryRun = args.includes("--dry-run")
  194. const verbose = args.includes("--verbose")
  195. const files = args.filter((a) => !a.startsWith("--"))
  196. if (files.length === 0) {
  197. info("Usage: transform-scripts.ts [--dry-run] [--verbose] <file1> <file2> ...")
  198. process.exit(1)
  199. }
  200. if (dryRun) {
  201. info("Running in dry-run mode")
  202. }
  203. const results = await transformConflictedScripts(files, { dryRun, verbose })
  204. const transformed = results.filter((r) => r.action === "transformed")
  205. const total = results.reduce((sum, r) => sum + r.replacements, 0)
  206. console.log()
  207. success(`Transformed ${transformed.length} script files with ${total} replacements`)
  208. if (dryRun) {
  209. info("Run without --dry-run to apply changes")
  210. }
  211. }