transform-strings.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #!/usr/bin/env bun
  2. /**
  3. * jscodeshift codemod: Transform string literals
  4. *
  5. * Transforms string literals containing opencode references to kilo:
  6. * - "opencode-ai" -> "@kilocode/cli"
  7. * - "npx opencode" -> "npx @kilocode/cli"
  8. * - etc.
  9. *
  10. * Usage:
  11. * bun run script/upstream/codemods/transform-strings.ts [files...]
  12. */
  13. import { Project, SyntaxKind, type SourceFile } from "ts-morph"
  14. import { Glob } from "bun"
  15. import { info, success } from "../utils/logger"
  16. import { defaultConfig } from "../utils/config"
  17. interface StringReplacement {
  18. pattern: RegExp
  19. replacement: string
  20. }
  21. const STRING_REPLACEMENTS: StringReplacement[] = [
  22. // Package names in strings (no trailing \b to allow subpath matching like @opencode-ai/sdk/v2)
  23. { pattern: /\bopencode-ai\b/g, replacement: "@kilocode/cli" },
  24. { pattern: /@opencode-ai\/cli(?=\/|"|'|`|$)/g, replacement: "@kilocode/cli" },
  25. { pattern: /@opencode-ai\/sdk(?=\/|"|'|`|$)/g, replacement: "@kilocode/sdk" },
  26. { pattern: /@opencode-ai\/plugin(?=\/|"|'|`|$)/g, replacement: "@kilocode/plugin" },
  27. // CLI commands
  28. { pattern: /\bnpx opencode\b/g, replacement: "npx @kilocode/cli" },
  29. { pattern: /\bbun add opencode\b/g, replacement: "bun add @kilocode/cli" },
  30. { pattern: /\bnpm install opencode\b/g, replacement: "npm install @kilocode/cli" },
  31. { pattern: /\bnpm i opencode\b/g, replacement: "npm i @kilocode/cli" },
  32. // Database filename
  33. { pattern: /\bopencode\.db\b/g, replacement: "kilo.db" },
  34. // Binary name references (be careful with these)
  35. { pattern: /\bopencode upgrade\b/g, replacement: "kilo upgrade" },
  36. // HTTP header prefix
  37. { pattern: /x-opencode-/g, replacement: "x-kilo-" },
  38. // Environment variables (exclude OPENCODE_API_KEY - upstream Zen SaaS key)
  39. { pattern: /\bOPENCODE_(?!API_KEY\b)([A-Z_]+)\b/g, replacement: "KILO_$1" },
  40. { pattern: /\bVITE_OPENCODE_/g, replacement: "VITE_KILO_" },
  41. { pattern: /\b_EXTENSION_OPENCODE_/g, replacement: "_EXTENSION_KILO_" },
  42. ]
  43. export interface TransformResult {
  44. file: string
  45. changes: number
  46. }
  47. /**
  48. * Transform string literals in a source file
  49. */
  50. export function transformStrings(sourceFile: SourceFile): number {
  51. let changes = 0
  52. // Get all string literals
  53. const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral)
  54. for (const literal of stringLiterals) {
  55. let value = literal.getLiteralValue()
  56. let modified = false
  57. for (const { pattern, replacement } of STRING_REPLACEMENTS) {
  58. if (pattern.test(value)) {
  59. value = value.replace(pattern, replacement)
  60. modified = true
  61. }
  62. }
  63. if (modified) {
  64. // Preserve the original quote style
  65. const text = literal.getText()
  66. const quote = text[0]
  67. literal.replaceWithText(`${quote}${value}${quote}`)
  68. changes++
  69. }
  70. }
  71. // Also handle template literals
  72. const templates = sourceFile.getDescendantsOfKind(SyntaxKind.TemplateExpression)
  73. for (const template of templates) {
  74. const head = template.getHead()
  75. let headValue = head.getLiteralValue()
  76. let headModified = false
  77. for (const { pattern, replacement } of STRING_REPLACEMENTS) {
  78. if (pattern.test(headValue)) {
  79. headValue = headValue.replace(pattern, replacement)
  80. headModified = true
  81. }
  82. }
  83. if (headModified) {
  84. // Template head replacement is complex, skip for now
  85. changes++
  86. }
  87. }
  88. // Handle no-substitution template literals
  89. const noSubTemplates = sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)
  90. for (const template of noSubTemplates) {
  91. let value = template.getLiteralValue()
  92. let modified = false
  93. for (const { pattern, replacement } of STRING_REPLACEMENTS) {
  94. if (pattern.test(value)) {
  95. value = value.replace(pattern, replacement)
  96. modified = true
  97. }
  98. }
  99. if (modified) {
  100. template.replaceWithText(`\`${value}\``)
  101. changes++
  102. }
  103. }
  104. return changes
  105. }
  106. /**
  107. * Transform all files
  108. */
  109. export async function transformAllStrings(
  110. patterns: string[] = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
  111. dryRun = false,
  112. ): Promise<TransformResult[]> {
  113. const project = new Project({
  114. skipAddingFilesFromTsConfig: true,
  115. })
  116. const results: TransformResult[] = []
  117. const excludes = defaultConfig.excludePatterns
  118. for (const pattern of patterns) {
  119. const glob = new Glob(pattern)
  120. for await (const path of glob.scan({ absolute: true })) {
  121. if (excludes.some((ex) => path.includes(ex.replace(/\*\*/g, "")))) {
  122. continue
  123. }
  124. const sourceFile = project.addSourceFileAtPath(path)
  125. const changes = transformStrings(sourceFile)
  126. if (changes > 0) {
  127. results.push({ file: path, changes })
  128. if (!dryRun) {
  129. await sourceFile.save()
  130. success(`Transformed ${path}: ${changes} string(s)`)
  131. } else {
  132. info(`[DRY-RUN] Would transform ${path}: ${changes} string(s)`)
  133. }
  134. }
  135. }
  136. }
  137. return results
  138. }
  139. // CLI entry point
  140. if (import.meta.main) {
  141. const args = process.argv.slice(2)
  142. const dryRun = args.includes("--dry-run")
  143. const files = args.filter((a) => !a.startsWith("--"))
  144. if (dryRun) {
  145. info("Running in dry-run mode")
  146. }
  147. const patterns = files.length > 0 ? files : undefined
  148. const results = await transformAllStrings(patterns, dryRun)
  149. console.log()
  150. success(`Transformed ${results.length} files`)
  151. const totalChanges = results.reduce((sum, r) => sum + r.changes, 0)
  152. info(`Total strings transformed: ${totalChanges}`)
  153. }