#!/usr/bin/env bun /** * jscodeshift codemod: Transform string literals * * Transforms string literals containing opencode references to kilo: * - "opencode-ai" -> "@kilocode/cli" * - "npx opencode" -> "npx @kilocode/cli" * - etc. * * Usage: * bun run script/upstream/codemods/transform-strings.ts [files...] */ import { Project, SyntaxKind, type SourceFile } from "ts-morph" import { Glob } from "bun" import { info, success } from "../utils/logger" import { defaultConfig } from "../utils/config" interface StringReplacement { pattern: RegExp replacement: string } const STRING_REPLACEMENTS: StringReplacement[] = [ // Package names in strings (no trailing \b to allow subpath matching like @opencode-ai/sdk/v2) { pattern: /\bopencode-ai\b/g, replacement: "@kilocode/cli" }, { pattern: /@opencode-ai\/cli(?=\/|"|'|`|$)/g, replacement: "@kilocode/cli" }, { pattern: /@opencode-ai\/sdk(?=\/|"|'|`|$)/g, replacement: "@kilocode/sdk" }, { pattern: /@opencode-ai\/plugin(?=\/|"|'|`|$)/g, replacement: "@kilocode/plugin" }, // CLI commands { pattern: /\bnpx opencode\b/g, replacement: "npx @kilocode/cli" }, { pattern: /\bbun add opencode\b/g, replacement: "bun add @kilocode/cli" }, { pattern: /\bnpm install opencode\b/g, replacement: "npm install @kilocode/cli" }, { pattern: /\bnpm i opencode\b/g, replacement: "npm i @kilocode/cli" }, // Database filename { pattern: /\bopencode\.db\b/g, replacement: "kilo.db" }, // Binary name references (be careful with these) { pattern: /\bopencode upgrade\b/g, replacement: "kilo upgrade" }, // HTTP header prefix { pattern: /x-opencode-/g, replacement: "x-kilo-" }, // Environment variables (exclude OPENCODE_API_KEY - upstream Zen SaaS key) { pattern: /\bOPENCODE_(?!API_KEY\b)([A-Z_]+)\b/g, replacement: "KILO_$1" }, { pattern: /\bVITE_OPENCODE_/g, replacement: "VITE_KILO_" }, { pattern: /\b_EXTENSION_OPENCODE_/g, replacement: "_EXTENSION_KILO_" }, ] export interface TransformResult { file: string changes: number } /** * Transform string literals in a source file */ export function transformStrings(sourceFile: SourceFile): number { let changes = 0 // Get all string literals const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral) for (const literal of stringLiterals) { let value = literal.getLiteralValue() let modified = false for (const { pattern, replacement } of STRING_REPLACEMENTS) { if (pattern.test(value)) { value = value.replace(pattern, replacement) modified = true } } if (modified) { // Preserve the original quote style const text = literal.getText() const quote = text[0] literal.replaceWithText(`${quote}${value}${quote}`) changes++ } } // Also handle template literals const templates = sourceFile.getDescendantsOfKind(SyntaxKind.TemplateExpression) for (const template of templates) { const head = template.getHead() let headValue = head.getLiteralValue() let headModified = false for (const { pattern, replacement } of STRING_REPLACEMENTS) { if (pattern.test(headValue)) { headValue = headValue.replace(pattern, replacement) headModified = true } } if (headModified) { // Template head replacement is complex, skip for now changes++ } } // Handle no-substitution template literals const noSubTemplates = sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral) for (const template of noSubTemplates) { let value = template.getLiteralValue() let modified = false for (const { pattern, replacement } of STRING_REPLACEMENTS) { if (pattern.test(value)) { value = value.replace(pattern, replacement) modified = true } } if (modified) { template.replaceWithText(`\`${value}\``) changes++ } } return changes } /** * Transform all files */ export async function transformAllStrings( patterns: string[] = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], dryRun = false, ): Promise { const project = new Project({ skipAddingFilesFromTsConfig: true, }) const results: TransformResult[] = [] const excludes = defaultConfig.excludePatterns for (const pattern of patterns) { const glob = new Glob(pattern) for await (const path of glob.scan({ absolute: true })) { if (excludes.some((ex) => path.includes(ex.replace(/\*\*/g, "")))) { continue } const sourceFile = project.addSourceFileAtPath(path) const changes = transformStrings(sourceFile) if (changes > 0) { results.push({ file: path, changes }) if (!dryRun) { await sourceFile.save() success(`Transformed ${path}: ${changes} string(s)`) } else { info(`[DRY-RUN] Would transform ${path}: ${changes} string(s)`) } } } } return results } // CLI entry point if (import.meta.main) { const args = process.argv.slice(2) const dryRun = args.includes("--dry-run") const files = args.filter((a) => !a.startsWith("--")) if (dryRun) { info("Running in dry-run mode") } const patterns = files.length > 0 ? files : undefined const results = await transformAllStrings(patterns, dryRun) console.log() success(`Transformed ${results.length} files`) const totalChanges = results.reduce((sum, r) => sum + r.changes, 0) info(`Total strings transformed: ${totalChanges}`) }