transform-web.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. #!/usr/bin/env bun
  2. /**
  3. * Transform web/docs files with Kilo branding
  4. *
  5. * This script handles documentation and web content files (.mdx, etc.)
  6. * by transforming OpenCode references to Kilo.
  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 WebTransformResult {
  13. file: string
  14. action: "transformed" | "skipped" | "failed" | "flagged"
  15. replacements: number
  16. dryRun: boolean
  17. }
  18. export interface WebTransformOptions {
  19. dryRun?: boolean
  20. verbose?: boolean
  21. }
  22. interface WebReplacement {
  23. pattern: RegExp
  24. replacement: string
  25. description: string
  26. }
  27. // Web/docs replacements
  28. const WEB_REPLACEMENTS: WebReplacement[] = [
  29. // GitHub references
  30. {
  31. pattern: /github\.com\/anomalyco\/opencode/g,
  32. replacement: "github.com/Kilo-Org/kilocode",
  33. description: "GitHub URL",
  34. },
  35. {
  36. pattern: /anomalyco\/opencode/g,
  37. replacement: "Kilo-Org/kilocode",
  38. description: "GitHub repo",
  39. },
  40. // Domains
  41. {
  42. pattern: /app\.opencode\.ai/g,
  43. replacement: "app.kilo.ai",
  44. description: "App domain",
  45. },
  46. {
  47. pattern: /opencode\.ai(?!\/zen)/g,
  48. replacement: "kilo.ai",
  49. description: "Main domain (excluding zen)",
  50. },
  51. // Product names
  52. {
  53. pattern: /OpenCode Desktop/g,
  54. replacement: "Kilo Desktop",
  55. description: "Desktop name",
  56. },
  57. {
  58. pattern: /\bOpenCode\b(?!\.json|\/| Zen)/g,
  59. replacement: "Kilo",
  60. description: "Product name",
  61. },
  62. // CLI commands
  63. {
  64. pattern: /npx opencode(?!\w)/g,
  65. replacement: "npx kilo",
  66. description: "npx command",
  67. },
  68. {
  69. pattern: /bun add opencode(?!\w)/g,
  70. replacement: "bun add kilo",
  71. description: "bun add command",
  72. },
  73. {
  74. pattern: /npm install opencode(?!\w)/g,
  75. replacement: "npm install kilo",
  76. description: "npm install command",
  77. },
  78. {
  79. pattern: /opencode upgrade/g,
  80. replacement: "kilo upgrade",
  81. description: "upgrade command",
  82. },
  83. {
  84. pattern: /opencode dev/g,
  85. replacement: "kilo dev",
  86. description: "dev command",
  87. },
  88. {
  89. pattern: /opencode serve/g,
  90. replacement: "kilo serve",
  91. description: "serve command",
  92. },
  93. {
  94. pattern: /opencode auth/g,
  95. replacement: "kilo auth",
  96. description: "auth command",
  97. },
  98. ]
  99. // Patterns to preserve
  100. const PRESERVE_PATTERNS = [/opencode\.json/g, /\.opencode\//g, /`\.opencode`/g]
  101. /**
  102. * Check if file is a web/docs file
  103. */
  104. export function isWebFile(file: string): boolean {
  105. const patterns = defaultConfig.webFiles
  106. return patterns.some((pattern) => {
  107. const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
  108. return regex.test(file)
  109. })
  110. }
  111. /**
  112. * Apply web transforms to content
  113. */
  114. export function applyWebTransforms(content: string, verbose = false): { result: string; replacements: number } {
  115. const lines = content.split("\n")
  116. const transformed: string[] = []
  117. let total = 0
  118. for (const line of lines) {
  119. // Check if line has preserve patterns
  120. let hasPreserve = false
  121. for (const pattern of PRESERVE_PATTERNS) {
  122. pattern.lastIndex = 0
  123. if (pattern.test(line)) {
  124. hasPreserve = true
  125. pattern.lastIndex = 0
  126. }
  127. }
  128. // If line has preserve patterns, skip transformation
  129. if (hasPreserve) {
  130. transformed.push(line)
  131. continue
  132. }
  133. let result = line
  134. let count = 0
  135. for (const { pattern, replacement, description } of WEB_REPLACEMENTS) {
  136. pattern.lastIndex = 0
  137. if (pattern.test(result)) {
  138. pattern.lastIndex = 0
  139. const before = result
  140. result = result.replace(pattern, replacement)
  141. if (before !== result) {
  142. count++
  143. if (verbose) debug(` ${description}`)
  144. }
  145. }
  146. }
  147. transformed.push(result)
  148. total += count
  149. }
  150. return { result: transformed.join("\n"), replacements: total }
  151. }
  152. /**
  153. * Transform a web/docs file
  154. */
  155. export async function transformWebFile(file: string, options: WebTransformOptions = {}): Promise<WebTransformResult> {
  156. if (options.dryRun) {
  157. info(`[DRY-RUN] Would transform web file: ${file}`)
  158. return { file, action: "transformed", replacements: 0, dryRun: true }
  159. }
  160. // If our version has kilocode_change markers, flag for manual resolution
  161. if (await oursHasKilocodeChanges(file)) {
  162. warn(`${file} has kilocode_change markers — skipping auto-transform, needs manual resolution`)
  163. return { file, action: "flagged", replacements: 0, dryRun: false }
  164. }
  165. try {
  166. // Take upstream's version first
  167. await $`git checkout --theirs ${file}`.quiet().nothrow()
  168. await $`git add ${file}`.quiet().nothrow()
  169. // Read content
  170. const content = await Bun.file(file).text()
  171. // Apply transforms
  172. const { result, replacements } = applyWebTransforms(content, options.verbose)
  173. // Write back if changed
  174. if (replacements > 0) {
  175. await Bun.write(file, result)
  176. await $`git add ${file}`.quiet().nothrow()
  177. }
  178. success(`Transformed web file ${file}: ${replacements} replacements`)
  179. return { file, action: "transformed", replacements, dryRun: false }
  180. } catch (err) {
  181. warn(`Failed to transform web file ${file}: ${err}`)
  182. return { file, action: "failed", replacements: 0, dryRun: false }
  183. }
  184. }
  185. /**
  186. * Transform conflicted web files
  187. */
  188. export async function transformConflictedWeb(
  189. files: string[],
  190. options: WebTransformOptions = {},
  191. ): Promise<WebTransformResult[]> {
  192. const results: WebTransformResult[] = []
  193. for (const file of files) {
  194. if (!isWebFile(file)) {
  195. debug(`Skipping ${file} - not a web file`)
  196. results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
  197. continue
  198. }
  199. const result = await transformWebFile(file, options)
  200. results.push(result)
  201. }
  202. return results
  203. }
  204. /**
  205. * Transform all web/docs files (pre-merge, on opencode branch)
  206. */
  207. export async function transformAllWeb(options: WebTransformOptions = {}): Promise<WebTransformResult[]> {
  208. const { Glob } = await import("bun")
  209. const results: WebTransformResult[] = []
  210. const patterns = defaultConfig.webFiles
  211. for (const pattern of patterns) {
  212. const glob = new Glob(pattern)
  213. for await (const path of glob.scan({ absolute: false })) {
  214. const file = Bun.file(path)
  215. if (!(await file.exists())) continue
  216. try {
  217. const content = await file.text()
  218. const { result, replacements } = applyWebTransforms(content, options.verbose)
  219. if (replacements > 0 && !options.dryRun) {
  220. await Bun.write(path, result)
  221. success(`Transformed web ${path}: ${replacements} replacements`)
  222. } else if (options.dryRun && replacements > 0) {
  223. info(`[DRY-RUN] Would transform web ${path}: ${replacements} replacements`)
  224. }
  225. results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
  226. } catch (err) {
  227. warn(`Failed to transform web ${path}: ${err}`)
  228. results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
  229. }
  230. }
  231. }
  232. return results
  233. }
  234. // CLI entry point
  235. if (import.meta.main) {
  236. const args = process.argv.slice(2)
  237. const dryRun = args.includes("--dry-run")
  238. const verbose = args.includes("--verbose")
  239. const files = args.filter((a) => !a.startsWith("--"))
  240. if (files.length === 0) {
  241. info("Usage: transform-web.ts [--dry-run] [--verbose] <file1> <file2> ...")
  242. process.exit(1)
  243. }
  244. if (dryRun) {
  245. info("Running in dry-run mode")
  246. }
  247. const results = await transformConflictedWeb(files, { dryRun, verbose })
  248. const transformed = results.filter((r) => r.action === "transformed")
  249. const total = results.reduce((sum, r) => sum + r.replacements, 0)
  250. console.log()
  251. success(`Transformed ${transformed.length} web files with ${total} replacements`)
  252. if (dryRun) {
  253. info("Run without --dry-run to apply changes")
  254. }
  255. }