refactor: improve upstream script

This commit is contained in:
Catriel Müller
2026-02-03 16:11:24 -03:00
parent 6352beb85e
commit 0960e6a85b
11 changed files with 1852 additions and 12 deletions
+57 -7
View File
@@ -34,11 +34,19 @@ bun run merge.ts --version v1.1.49 --dry-run
### Transform Scripts
| Script | Description |
| --------------------------------- | ---------------------------------------- |
| `transforms/package-names.ts` | Transform opencode package names to kilo |
| `transforms/preserve-versions.ts` | Preserve Kilo's package versions |
| `transforms/keep-ours.ts` | Keep Kilo's version of specific files |
| Script | Description |
| -------------------------------------- | ----------------------------------------------------------- |
| `transforms/package-names.ts` | Transform opencode package names to kilo |
| `transforms/preserve-versions.ts` | Preserve Kilo's package versions |
| `transforms/keep-ours.ts` | Keep Kilo's version of specific files |
| `transforms/skip-files.ts` | Skip/remove files that shouldn't exist in Kilo |
| `transforms/transform-i18n.ts` | Transform i18n files with Kilo branding |
| `transforms/transform-take-theirs.ts` | Take upstream + apply Kilo branding for branding-only files |
| `transforms/transform-tauri.ts` | Transform Tauri/Desktop config files |
| `transforms/transform-package-json.ts` | Enhanced package.json with Kilo dependency injection |
| `transforms/transform-scripts.ts` | Transform script files with GitHub API references |
| `transforms/transform-extensions.ts` | Transform extension files (Zed, etc.) |
| `transforms/transform-web.ts` | Transform web/docs files (.mdx) |
### Codemods (AST-based)
@@ -88,23 +96,65 @@ Configuration is defined in `utils/config.ts`:
// ...
],
// Files to always keep Kilo's version
// Files to always keep Kilo's version (never take upstream)
keepOurs: [
"README.md",
"CONTRIBUTING.md",
"AGENTS.md",
".github/workflows/publish.yml", // GitHub workflows - manual review
// ...
],
// Kilo-specific directories
// Files to skip entirely (remove from merge)
skipFiles: [
"README.*.md", // Translated READMEs
"STATS.md",
".github/workflows/update-nix-hashes.yml",
// ...
],
// Files to take upstream + apply Kilo branding transforms
takeTheirsAndTransform: [
"packages/app/src/components/**/*.tsx",
"packages/app/src/context/**/*.tsx",
"packages/ui/src/**/*.tsx",
// ...
],
// Tauri/Desktop config files
tauriFiles: [
"packages/desktop/src-tauri/*.json",
"packages/desktop/src-tauri/src/*.rs",
// ...
],
// Kilo-specific directories (preserved)
kiloDirectories: [
"packages/opencode/src/kilocode",
"packages/kilo-gateway",
"packages/kilo-telemetry",
// ...
],
}
```
## Auto-Resolution Strategies
The merge tool uses different strategies based on file type:
| File Type | Strategy | Description |
| ----------------- | ----------------------- | ------------------------------------------------ |
| i18n files | `i18n-transform` | Take upstream, apply Kilo branding |
| App components | `take-theirs-transform` | Take upstream, apply branding (no logic changes) |
| Tauri configs | `tauri-transform` | Take upstream, transform identifiers/names |
| package.json | `package-transform` | Take upstream, transform names, inject Kilo deps |
| Script files | `script-transform` | Take upstream, transform GitHub references |
| Extensions | `extension-transform` | Take upstream, apply branding |
| Web/docs | `web-transform` | Take upstream, apply branding |
| README/docs | `keep-ours` | Keep Kilo's version |
| GitHub workflows | `keep-ours` | Keep Kilo's version (manual review) |
| Code with markers | `manual` | Has `kilocode_change` markers, needs review |
## CLI Options
### merge.ts
+38
View File
@@ -24,3 +24,41 @@ export {
transformI18nContent,
isI18nFile,
} from "./transforms/transform-i18n"
// New transforms for auto-resolving more conflict types
export {
transformConflictedTakeTheirs,
transformTakeTheirs,
shouldTakeTheirs,
applyBrandingTransforms,
matchesPattern,
} from "./transforms/transform-take-theirs"
export {
transformConflictedTauri,
transformTauriFile,
isTauriFile,
applyTauriTransforms,
} from "./transforms/transform-tauri"
export {
transformConflictedPackageJson,
transformPackageJson,
isPackageJson,
} from "./transforms/transform-package-json"
export {
transformConflictedScripts,
transformScriptFile,
isScriptFile,
applyScriptTransforms,
} from "./transforms/transform-scripts"
export {
transformConflictedExtensions,
transformExtensionFile,
isExtensionFile,
applyExtensionTransforms,
} from "./transforms/transform-extensions"
export { transformConflictedWeb, transformWebFile, isWebFile, applyWebTransforms } from "./transforms/transform-web"
+92 -1
View File
@@ -28,6 +28,13 @@ import { preserveAllVersions } from "./transforms/preserve-versions"
import { keepOursFiles, resetToOurs } from "./transforms/keep-ours"
import { skipFiles, skipSpecificFiles } from "./transforms/skip-files"
import { transformConflictedI18n, transformAllI18n } from "./transforms/transform-i18n"
// New transforms for auto-resolving more conflict types
import { transformConflictedTakeTheirs, shouldTakeTheirs } from "./transforms/transform-take-theirs"
import { transformConflictedTauri, isTauriFile } from "./transforms/transform-tauri"
import { transformConflictedPackageJson, isPackageJson } from "./transforms/transform-package-json"
import { transformConflictedScripts, isScriptFile } from "./transforms/transform-scripts"
import { transformConflictedExtensions, isExtensionFile } from "./transforms/transform-extensions"
import { transformConflictedWeb, isWebFile } from "./transforms/transform-web"
interface MergeOptions {
version?: string
@@ -291,7 +298,7 @@ async function main() {
// Step 7b: Transform i18n files (take upstream + apply Kilo branding)
logger.info("Transforming i18n files...")
const conflictedFiles = await git.getConflictedFiles()
let conflictedFiles = await git.getConflictedFiles()
const i18nResults = await transformConflictedI18n(conflictedFiles, { dryRun: false, verbose: options.verbose })
const i18nTransformed = i18nResults.filter((r) => r.replacements > 0).length
if (i18nTransformed > 0) {
@@ -307,6 +314,90 @@ async function main() {
logger.success(`Auto-resolved ${autoResolved.length} conflicts (kept Kilo's version)`)
}
// Step 7d: Transform branding-only files (take theirs + apply Kilo branding)
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming branding-only files...")
const takeTheirsResults = await transformConflictedTakeTheirs(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const takeTheirsCount = takeTheirsResults.filter((r) => r.action === "transformed").length
if (takeTheirsCount > 0) {
logger.success(`Transformed ${takeTheirsCount} files (take upstream + Kilo branding)`)
}
}
// Step 7e: Transform Tauri/Desktop config files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming Tauri/Desktop config files...")
const tauriResults = await transformConflictedTauri(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const tauriCount = tauriResults.filter((r) => r.action === "transformed").length
if (tauriCount > 0) {
logger.success(`Transformed ${tauriCount} Tauri config files`)
}
}
// Step 7f: Transform package.json files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming package.json files...")
const pkgResults = await transformConflictedPackageJson(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const pkgCount = pkgResults.filter((r) => r.action === "transformed").length
if (pkgCount > 0) {
logger.success(`Transformed ${pkgCount} package.json files`)
}
}
// Step 7g: Transform script files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming script files...")
const scriptResults = await transformConflictedScripts(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const scriptCount = scriptResults.filter((r) => r.action === "transformed").length
if (scriptCount > 0) {
logger.success(`Transformed ${scriptCount} script files`)
}
}
// Step 7h: Transform extension files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming extension files...")
const extResults = await transformConflictedExtensions(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const extCount = extResults.filter((r) => r.action === "transformed").length
if (extCount > 0) {
logger.success(`Transformed ${extCount} extension files`)
}
}
// Step 7i: Transform web/docs files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
logger.info("Transforming web/docs files...")
const webResults = await transformConflictedWeb(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const webCount = webResults.filter((r) => r.action === "transformed").length
if (webCount > 0) {
logger.success(`Transformed ${webCount} web/docs files`)
}
}
// Check remaining conflicts
const remaining = await git.getConflictedFiles()
if (remaining.length > 0) {
@@ -0,0 +1,236 @@
#!/usr/bin/env bun
/**
* Transform extension files (Zed, etc.) with Kilo branding
*
* This script handles extension configuration files by transforming
* OpenCode references to Kilo.
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
export interface ExtensionTransformResult {
file: string
action: "transformed" | "skipped" | "failed"
replacements: number
dryRun: boolean
}
export interface ExtensionTransformOptions {
dryRun?: boolean
verbose?: boolean
}
interface ExtensionReplacement {
pattern: RegExp
replacement: string
description: string
fileTypes?: string[]
}
// Extension-specific replacements
const EXTENSION_REPLACEMENTS: ExtensionReplacement[] = [
// TOML files (Zed extension)
{
pattern: /name\s*=\s*"opencode"/g,
replacement: 'name = "kilo"',
description: "Extension name",
fileTypes: [".toml"],
},
{
pattern: /id\s*=\s*"opencode"/g,
replacement: 'id = "kilo"',
description: "Extension ID",
fileTypes: [".toml"],
},
{
pattern: /description\s*=\s*"OpenCode[^"]*"/g,
replacement: 'description = "Kilo - AI coding assistant"',
description: "Extension description",
fileTypes: [".toml"],
},
// GitHub/Repository references
{
pattern: /repository\s*=\s*"[^"]*anomalyco\/opencode[^"]*"/g,
replacement: 'repository = "https://github.com/Kilo-Org/kilo"',
description: "Repository URL",
fileTypes: [".toml"],
},
{
pattern: /github\.com\/anomalyco\/opencode/g,
replacement: "github.com/Kilo-Org/kilo",
description: "GitHub URL",
},
{
pattern: /anomalyco\/opencode/g,
replacement: "Kilo-Org/kilo",
description: "GitHub repo",
},
// Binary/command references
{
pattern: /command\s*=\s*"opencode"/g,
replacement: 'command = "kilo"',
description: "Command name",
fileTypes: [".toml"],
},
// Generic OpenCode -> Kilo in strings
{
pattern: /"OpenCode"/g,
replacement: '"Kilo"',
description: "Product name",
},
{
pattern: /OpenCode\s+language\s+server/gi,
replacement: "Kilo language server",
description: "Language server name",
},
]
/**
* Check if file is an extension file
*/
export function isExtensionFile(file: string): boolean {
const patterns = defaultConfig.extensionFiles
return patterns.some((pattern) => {
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
return regex.test(file)
})
}
/**
* Get file extension
*/
function getExtension(file: string): string {
const match = file.match(/\.[^.]+$/)
return match ? match[0] : ""
}
/**
* Apply extension transforms to content
*/
export function applyExtensionTransforms(
content: string,
file: string,
verbose = false,
): { result: string; replacements: number } {
const ext = getExtension(file)
let result = content
let total = 0
for (const { pattern, replacement, description, fileTypes } of EXTENSION_REPLACEMENTS) {
// Skip if this replacement is for specific file types and doesn't match
if (fileTypes && !fileTypes.includes(ext)) {
continue
}
pattern.lastIndex = 0
if (pattern.test(result)) {
pattern.lastIndex = 0
const before = result
result = result.replace(pattern, replacement)
if (before !== result) {
total++
if (verbose) debug(` ${description}`)
}
}
}
return { result, replacements: total }
}
/**
* Transform an extension file
*/
export async function transformExtensionFile(
file: string,
options: ExtensionTransformOptions = {},
): Promise<ExtensionTransformResult> {
if (options.dryRun) {
info(`[DRY-RUN] Would transform extension: ${file}`)
return { file, action: "transformed", replacements: 0, dryRun: true }
}
try {
// Take upstream's version first
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read content
const content = await Bun.file(file).text()
// Apply transforms
const { result, replacements } = applyExtensionTransforms(content, file, options.verbose)
// Write back if changed
if (replacements > 0) {
await Bun.write(file, result)
await $`git add ${file}`.quiet().nothrow()
}
success(`Transformed extension ${file}: ${replacements} replacements`)
return { file, action: "transformed", replacements, dryRun: false }
} catch (err) {
warn(`Failed to transform extension ${file}: ${err}`)
return { file, action: "failed", replacements: 0, dryRun: false }
}
}
/**
* Transform conflicted extension files
*/
export async function transformConflictedExtensions(
files: string[],
options: ExtensionTransformOptions = {},
): Promise<ExtensionTransformResult[]> {
const results: ExtensionTransformResult[] = []
for (const file of files) {
if (!isExtensionFile(file)) {
debug(`Skipping ${file} - not an extension file`)
results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
continue
}
const result = await transformExtensionFile(file, options)
results.push(result)
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-extensions.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode")
}
const results = await transformConflictedExtensions(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const total = results.reduce((sum, r) => sum + r.replacements, 0)
console.log()
success(`Transformed ${transformed.length} extension files with ${total} replacements`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
@@ -0,0 +1,226 @@
#!/usr/bin/env bun
/**
* Enhanced package.json transform with Kilo dependency injection
*
* This script handles package.json conflicts by:
* 1. Taking upstream's version (to get new dependencies)
* 2. Transforming package names (opencode -> kilo)
* 3. Injecting Kilo-specific dependencies
* 4. Preserving Kilo's version number
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { getCurrentVersion } from "./preserve-versions"
export interface PackageJsonResult {
file: string
action: "transformed" | "skipped" | "failed"
changes: string[]
dryRun: boolean
}
export interface PackageJsonOptions {
dryRun?: boolean
verbose?: boolean
preserveVersion?: boolean
}
// Package name mappings
const PACKAGE_NAME_MAP: Record<string, string> = {
"opencode-ai": "@kilocode/cli",
"@opencode-ai/cli": "@kilocode/cli",
"@opencode-ai/sdk": "@kilocode/sdk",
"@opencode-ai/plugin": "@kilocode/plugin",
}
// Kilo-specific dependencies to inject into specific packages
const KILO_DEPENDENCIES: Record<string, Record<string, string>> = {
// packages/opencode/package.json needs these
"packages/opencode/package.json": {
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
},
}
// Packages that should have their name transformed
const TRANSFORM_PACKAGE_NAMES: Record<string, string> = {
"packages/opencode/package.json": "@kilocode/cli",
"packages/plugin/package.json": "@kilocode/plugin",
"packages/sdk/js/package.json": "@kilocode/sdk",
}
/**
* Check if file is a package.json
*/
export function isPackageJson(file: string): boolean {
return file.endsWith("package.json")
}
/**
* Transform dependencies in package.json
*/
function transformDependencies(deps: Record<string, string> | undefined): {
result: Record<string, string>
changes: string[]
} {
if (!deps) return { result: {}, changes: [] }
const result: Record<string, string> = {}
const changes: string[] = []
for (const [name, version] of Object.entries(deps)) {
const newName = PACKAGE_NAME_MAP[name]
if (newName) {
result[newName] = version
changes.push(`${name} -> ${newName}`)
} else {
result[name] = version
}
}
return { result, changes }
}
/**
* Transform a package.json file
*/
export async function transformPackageJson(file: string, options: PackageJsonOptions = {}): Promise<PackageJsonResult> {
const changes: string[] = []
if (options.dryRun) {
info(`[DRY-RUN] Would transform package.json: ${file}`)
return { file, action: "transformed", changes: [], dryRun: true }
}
try {
// Take upstream's version first
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read and parse
const content = await Bun.file(file).text()
const pkg = JSON.parse(content)
// 1. Transform package name if needed
const relativePath = file.replace(process.cwd() + "/", "")
const newName = TRANSFORM_PACKAGE_NAMES[relativePath]
if (newName && pkg.name !== newName) {
changes.push(`name: ${pkg.name} -> ${newName}`)
pkg.name = newName
}
// 2. Preserve Kilo version if requested
if (options.preserveVersion !== false) {
const kiloVersion = await getCurrentVersion()
if (pkg.version !== kiloVersion) {
changes.push(`version: ${pkg.version} -> ${kiloVersion}`)
pkg.version = kiloVersion
}
}
// 3. Transform dependencies
if (pkg.dependencies) {
const { result, changes: depChanges } = transformDependencies(pkg.dependencies)
pkg.dependencies = result
changes.push(...depChanges.map((c) => `dependencies: ${c}`))
}
// 4. Transform devDependencies
if (pkg.devDependencies) {
const { result, changes: devChanges } = transformDependencies(pkg.devDependencies)
pkg.devDependencies = devChanges.length > 0 ? result : pkg.devDependencies
changes.push(...devChanges.map((c) => `devDependencies: ${c}`))
}
// 5. Transform peerDependencies
if (pkg.peerDependencies) {
const { result, changes: peerChanges } = transformDependencies(pkg.peerDependencies)
pkg.peerDependencies = peerChanges.length > 0 ? result : pkg.peerDependencies
changes.push(...peerChanges.map((c) => `peerDependencies: ${c}`))
}
// 6. Inject Kilo-specific dependencies
const kiloDeps = KILO_DEPENDENCIES[relativePath]
if (kiloDeps) {
pkg.dependencies = pkg.dependencies || {}
for (const [name, version] of Object.entries(kiloDeps)) {
if (!pkg.dependencies[name]) {
pkg.dependencies[name] = version
changes.push(`injected: ${name}`)
}
}
}
// Write back with proper formatting
const newContent = JSON.stringify(pkg, null, 2) + "\n"
await Bun.write(file, newContent)
await $`git add ${file}`.quiet().nothrow()
if (changes.length > 0) {
success(`Transformed ${file}: ${changes.length} changes`)
if (options.verbose) {
for (const change of changes) {
debug(` - ${change}`)
}
}
}
return { file, action: "transformed", changes, dryRun: false }
} catch (err) {
warn(`Failed to transform ${file}: ${err}`)
return { file, action: "failed", changes: [], dryRun: false }
}
}
/**
* Transform conflicted package.json files
*/
export async function transformConflictedPackageJson(
files: string[],
options: PackageJsonOptions = {},
): Promise<PackageJsonResult[]> {
const results: PackageJsonResult[] = []
for (const file of files) {
if (!isPackageJson(file)) {
results.push({ file, action: "skipped", changes: [], dryRun: options.dryRun ?? false })
continue
}
const result = await transformPackageJson(file, options)
results.push(result)
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-package-json.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode")
}
const results = await transformConflictedPackageJson(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const totalChanges = results.reduce((sum, r) => sum + r.changes.length, 0)
console.log()
success(`Transformed ${transformed.length} package.json files with ${totalChanges} changes`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
@@ -0,0 +1,202 @@
#!/usr/bin/env bun
/**
* Transform script files with GitHub API references
*
* This script handles script files that contain GitHub API references
* by transforming them from anomalyco/opencode to Kilo-Org/kilo.
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
export interface ScriptTransformResult {
file: string
action: "transformed" | "skipped" | "failed"
replacements: number
dryRun: boolean
}
export interface ScriptTransformOptions {
dryRun?: boolean
verbose?: boolean
}
interface ScriptReplacement {
pattern: RegExp
replacement: string
description: string
}
// Script-specific replacements
const SCRIPT_REPLACEMENTS: ScriptReplacement[] = [
// GitHub API URLs
{
pattern: /api\.github\.com\/repos\/anomalyco\/opencode/g,
replacement: "api.github.com/repos/Kilo-Org/kilo",
description: "GitHub API URL",
},
{
pattern: /\/repos\/anomalyco\/opencode/g,
replacement: "/repos/Kilo-Org/kilo",
description: "GitHub repos path",
},
// gh CLI commands
{
pattern: /gh api "\/repos\/anomalyco\/opencode/g,
replacement: 'gh api "/repos/Kilo-Org/kilo',
description: "gh api command",
},
// Direct GitHub references
{
pattern: /github\.com\/anomalyco\/opencode/g,
replacement: "github.com/Kilo-Org/kilo",
description: "GitHub URL",
},
{
pattern: /anomalyco\/opencode/g,
replacement: "Kilo-Org/kilo",
description: "GitHub repo reference",
},
// OpenCode branding in strings
{
pattern: /"OpenCode"/g,
replacement: '"Kilo"',
description: "Product name in string",
},
{
pattern: /'OpenCode'/g,
replacement: "'Kilo'",
description: "Product name in single quotes",
},
]
/**
* Check if file is a script file
*/
export function isScriptFile(file: string): boolean {
const patterns = defaultConfig.scriptFiles
return patterns.some((pattern) => {
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
return regex.test(file)
})
}
/**
* Apply script transforms to content
*/
export function applyScriptTransforms(content: string, verbose = false): { result: string; replacements: number } {
let result = content
let total = 0
for (const { pattern, replacement, description } of SCRIPT_REPLACEMENTS) {
pattern.lastIndex = 0
if (pattern.test(result)) {
pattern.lastIndex = 0
const before = result
result = result.replace(pattern, replacement)
if (before !== result) {
total++
if (verbose) debug(` ${description}`)
}
}
}
return { result, replacements: total }
}
/**
* Transform a script file
*/
export async function transformScriptFile(
file: string,
options: ScriptTransformOptions = {},
): Promise<ScriptTransformResult> {
if (options.dryRun) {
info(`[DRY-RUN] Would transform script: ${file}`)
return { file, action: "transformed", replacements: 0, dryRun: true }
}
try {
// Take upstream's version first
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read content
const content = await Bun.file(file).text()
// Apply transforms
const { result, replacements } = applyScriptTransforms(content, options.verbose)
// Write back if changed
if (replacements > 0) {
await Bun.write(file, result)
await $`git add ${file}`.quiet().nothrow()
}
success(`Transformed script ${file}: ${replacements} replacements`)
return { file, action: "transformed", replacements, dryRun: false }
} catch (err) {
warn(`Failed to transform script ${file}: ${err}`)
return { file, action: "failed", replacements: 0, dryRun: false }
}
}
/**
* Transform conflicted script files
*/
export async function transformConflictedScripts(
files: string[],
options: ScriptTransformOptions = {},
): Promise<ScriptTransformResult[]> {
const results: ScriptTransformResult[] = []
for (const file of files) {
if (!isScriptFile(file)) {
debug(`Skipping ${file} - not a script file`)
results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
continue
}
const result = await transformScriptFile(file, options)
results.push(result)
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-scripts.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode")
}
const results = await transformConflictedScripts(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const total = results.reduce((sum, r) => sum + r.replacements, 0)
console.log()
success(`Transformed ${transformed.length} script files with ${total} replacements`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
@@ -0,0 +1,272 @@
#!/usr/bin/env bun
/**
* Transform files by taking upstream version and applying Kilo branding
*
* This script handles files that have only branding differences (no logic changes).
* It takes the upstream version and applies Kilo branding transforms.
*
* Use this for:
* - UI components with OpenCode -> Kilo branding
* - Config files with predictable patterns
* - Files without kilocode_change logic blocks
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
export interface TakeTheirsResult {
file: string
action: "transformed" | "skipped" | "failed"
replacements: number
dryRun: boolean
}
export interface TakeTheirsOptions {
dryRun?: boolean
verbose?: boolean
patterns?: string[]
}
interface BrandingReplacement {
pattern: RegExp
replacement: string
description: string
}
// Branding replacements - order matters (specific patterns first)
const BRANDING_REPLACEMENTS: BrandingReplacement[] = [
// GitHub repo references
{
pattern: /github\.com\/anomalyco\/opencode/g,
replacement: "github.com/Kilo-Org/kilo",
description: "GitHub URL",
},
{
pattern: /anomalyco\/opencode/g,
replacement: "Kilo-Org/kilo",
description: "GitHub repo reference",
},
// Domain replacements (specific first)
{
pattern: /app\.opencode\.ai/g,
replacement: "app.kilo.ai",
description: "App domain",
},
{
pattern: /opencode\.ai/g,
replacement: "kilo.ai",
description: "Main domain",
},
// Product name (specific phrases first)
{
pattern: /OpenCode Desktop/g,
replacement: "Kilo Desktop",
description: "Desktop app name",
},
{
pattern: /OpenCode Zen/g,
replacement: "Kilo Zen",
description: "Zen product name",
},
// CLI commands
{
pattern: /npx opencode(?!\w)/g,
replacement: "npx kilo",
description: "npx command",
},
{
pattern: /bun add opencode(?!\w)/g,
replacement: "bun add kilo",
description: "bun add command",
},
{
pattern: /npm install opencode(?!\w)/g,
replacement: "npm install kilo",
description: "npm install command",
},
{
pattern: /opencode upgrade(?!\w)/g,
replacement: "kilo upgrade",
description: "upgrade command",
},
// Generic product name replacement (must come after specific patterns)
// Only replace "OpenCode" when it's a standalone word
{
pattern: /\bOpenCode\b(?!\.json|\/)/g,
replacement: "Kilo",
description: "Product name",
},
]
// Patterns that should NOT be replaced (preserved as-is)
const PRESERVE_PATTERNS = [
/opencode\.json/g, // Config filename
/\.opencode\//g, // Directory name
/\.opencode`/g, // Directory name in template strings
/"\.opencode"/g, // Directory name in quotes
/'\.opencode'/g, // Directory name in single quotes
/\/\/\s*kilocode_change/g, // Already has marker
]
/**
* Check if a file matches any of the patterns
*/
export function matchesPattern(file: string, patterns: string[]): boolean {
return patterns.some((pattern) => {
// Convert glob pattern to regex
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
return regex.test(file)
})
}
/**
* Apply branding transforms to content
*/
export function applyBrandingTransforms(content: string, verbose = false): { result: string; replacements: number } {
const lines = content.split("\n")
const transformed: string[] = []
let total = 0
for (const line of lines) {
// Skip lines with kilocode_change marker (already customized)
if (line.includes("// kilocode_change")) {
transformed.push(line)
continue
}
// Check if line has preserve patterns
let hasPreserve = false
for (const pattern of PRESERVE_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(line)) {
hasPreserve = true
pattern.lastIndex = 0
}
}
let result = line
let count = 0
// Apply replacements
for (const { pattern, replacement, description } of BRANDING_REPLACEMENTS) {
pattern.lastIndex = 0
if (pattern.test(result)) {
pattern.lastIndex = 0
const before = result
result = result.replace(pattern, replacement)
if (before !== result) {
count++
if (verbose) debug(` ${description}: "${before.trim()}" -> "${result.trim()}"`)
}
}
}
transformed.push(result)
total += count
}
return { result: transformed.join("\n"), replacements: total }
}
/**
* Take upstream version of a file and apply branding transforms
*/
export async function transformTakeTheirs(file: string, options: TakeTheirsOptions = {}): Promise<TakeTheirsResult> {
if (options.dryRun) {
info(`[DRY-RUN] Would take theirs and transform: ${file}`)
return { file, action: "transformed", replacements: 0, dryRun: true }
}
try {
// Take upstream's version
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read the file
const content = await Bun.file(file).text()
// Apply branding transforms
const { result, replacements } = applyBrandingTransforms(content, options.verbose)
// Write back
if (replacements > 0) {
await Bun.write(file, result)
await $`git add ${file}`.quiet().nothrow()
}
success(`Transformed ${file}: took upstream + ${replacements} branding replacements`)
return { file, action: "transformed", replacements, dryRun: false }
} catch (err) {
warn(`Failed to transform ${file}: ${err}`)
return { file, action: "failed", replacements: 0, dryRun: false }
}
}
/**
* Transform multiple files that are in conflict
*/
export async function transformConflictedTakeTheirs(
files: string[],
options: TakeTheirsOptions = {},
): Promise<TakeTheirsResult[]> {
const results: TakeTheirsResult[] = []
const patterns = options.patterns || defaultConfig.takeTheirsAndTransform
for (const file of files) {
if (!matchesPattern(file, patterns)) {
debug(`Skipping ${file} - doesn't match take-theirs patterns`)
results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
continue
}
const result = await transformTakeTheirs(file, options)
results.push(result)
}
return results
}
/**
* Check if a file should use take-theirs strategy
*/
export function shouldTakeTheirs(file: string, patterns?: string[]): boolean {
const p = patterns || defaultConfig.takeTheirsAndTransform
return matchesPattern(file, p)
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-take-theirs.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode (no files will be modified)")
}
const results = await transformConflictedTakeTheirs(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const total = results.reduce((sum, r) => sum + r.replacements, 0)
console.log()
success(`Transformed ${transformed.length} files with ${total} replacements`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
@@ -0,0 +1,294 @@
#!/usr/bin/env bun
/**
* Transform Tauri/Desktop config files with Kilo branding
*
* This script handles Tauri configuration files (JSON, TOML, Rust) by:
* 1. Taking upstream's version as the base
* 2. Applying predictable Kilo branding transforms
*
* Handles:
* - tauri.conf.json / tauri.prod.conf.json
* - Cargo.toml / Cargo.lock
* - Rust source files (*.rs)
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
export interface TauriTransformResult {
file: string
action: "transformed" | "skipped" | "failed"
replacements: number
dryRun: boolean
}
export interface TauriTransformOptions {
dryRun?: boolean
verbose?: boolean
}
interface TauriReplacement {
pattern: RegExp
replacement: string
description: string
fileTypes?: string[] // Only apply to these file extensions
}
// Tauri-specific replacements
const TAURI_REPLACEMENTS: TauriReplacement[] = [
// JSON config - product names
{
pattern: /"productName":\s*"OpenCode[^"]*"/g,
replacement: '"productName": "Kilo"',
description: "Product name in JSON",
fileTypes: [".json"],
},
{
pattern: /"title":\s*"OpenCode[^"]*"/g,
replacement: '"title": "Kilo"',
description: "Title in JSON",
fileTypes: [".json"],
},
// JSON config - identifiers
{
pattern: /ai\.opencode\.desktop\.dev/g,
replacement: "ai.kilo.desktop.dev",
description: "Dev identifier",
},
{
pattern: /ai\.opencode\.desktop/g,
replacement: "ai.kilo.desktop",
description: "Prod identifier",
},
// Binary names
{
pattern: /opencode-cli/g,
replacement: "kilo-cli",
description: "CLI binary name",
},
{
pattern: /"mainBinaryName":\s*"[Oo]pen[Cc]ode"/g,
replacement: '"mainBinaryName": "Kilo"',
description: "Main binary name",
fileTypes: [".json"],
},
// GitHub references
{
pattern: /github\.com\/anomalyco\/opencode/g,
replacement: "github.com/Kilo-Org/kilo",
description: "GitHub URL",
},
{
pattern: /anomalyco\/opencode/g,
replacement: "Kilo-Org/kilo",
description: "GitHub repo",
},
// Cargo.toml specific
{
pattern: /name\s*=\s*"opencode-desktop"/g,
replacement: 'name = "kilo-desktop"',
description: "Cargo package name",
fileTypes: [".toml"],
},
{
pattern: /authors\s*=\s*\["OpenCode"\]/g,
replacement: 'authors = ["Kilo"]',
description: "Cargo authors",
fileTypes: [".toml"],
},
{
pattern: /name\s*=\s*"opencode_lib"/g,
replacement: 'name = "kilo_lib"',
description: "Cargo lib name",
fileTypes: [".toml"],
},
// Rust source specific
{
pattern: /opencode\.settings\.dat/g,
replacement: "kilo.settings.dat",
description: "Settings file name",
fileTypes: [".rs"],
},
{
pattern: /"\.opencode\/bin"/g,
replacement: '".kilo/bin"',
description: "CLI install dir",
fileTypes: [".rs"],
},
{
pattern: /CLI_BINARY_NAME\s*=\s*"opencode"/g,
replacement: 'CLI_BINARY_NAME = "kilo"',
description: "CLI binary constant",
fileTypes: [".rs"],
},
{
pattern: /opencode_lib::run/g,
replacement: "kilo_lib::run",
description: "Lib run call",
fileTypes: [".rs"],
},
{
pattern: /killall opencode-cli/g,
replacement: "killall kilo-cli",
description: "Killall command",
fileTypes: [".rs"],
},
// Domain
{
pattern: /opencode\.ai/g,
replacement: "kilo.ai",
description: "Domain",
},
]
/**
* Check if a file is a Tauri config file
*/
export function isTauriFile(file: string): boolean {
const patterns = defaultConfig.tauriFiles
return patterns.some((pattern) => {
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
return regex.test(file)
})
}
/**
* Get file extension
*/
function getExtension(file: string): string {
const match = file.match(/\.[^.]+$/)
return match ? match[0] : ""
}
/**
* Apply Tauri-specific transforms to content
*/
export function applyTauriTransforms(
content: string,
file: string,
verbose = false,
): { result: string; replacements: number } {
const ext = getExtension(file)
let result = content
let total = 0
for (const { pattern, replacement, description, fileTypes } of TAURI_REPLACEMENTS) {
// Skip if this replacement is for specific file types and doesn't match
if (fileTypes && !fileTypes.includes(ext)) {
continue
}
pattern.lastIndex = 0
if (pattern.test(result)) {
pattern.lastIndex = 0
const before = result
result = result.replace(pattern, replacement)
if (before !== result) {
total++
if (verbose) debug(` ${description}`)
}
}
}
return { result, replacements: total }
}
/**
* Transform a single Tauri file
*/
export async function transformTauriFile(
file: string,
options: TauriTransformOptions = {},
): Promise<TauriTransformResult> {
if (options.dryRun) {
info(`[DRY-RUN] Would transform Tauri file: ${file}`)
return { file, action: "transformed", replacements: 0, dryRun: true }
}
try {
// Take upstream's version first
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read content
const content = await Bun.file(file).text()
// Apply transforms
const { result, replacements } = applyTauriTransforms(content, file, options.verbose)
// Write back if changed
if (replacements > 0) {
await Bun.write(file, result)
await $`git add ${file}`.quiet().nothrow()
}
success(`Transformed Tauri file ${file}: ${replacements} replacements`)
return { file, action: "transformed", replacements, dryRun: false }
} catch (err) {
warn(`Failed to transform Tauri file ${file}: ${err}`)
return { file, action: "failed", replacements: 0, dryRun: false }
}
}
/**
* Transform conflicted Tauri files
*/
export async function transformConflictedTauri(
files: string[],
options: TauriTransformOptions = {},
): Promise<TauriTransformResult[]> {
const results: TauriTransformResult[] = []
for (const file of files) {
if (!isTauriFile(file)) {
debug(`Skipping ${file} - not a Tauri file`)
results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
continue
}
const result = await transformTauriFile(file, options)
results.push(result)
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-tauri.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode")
}
const results = await transformConflictedTauri(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const total = results.reduce((sum, r) => sum + r.replacements, 0)
console.log()
success(`Transformed ${transformed.length} Tauri files with ${total} replacements`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env bun
/**
* Transform web/docs files with Kilo branding
*
* This script handles documentation and web content files (.mdx, etc.)
* by transforming OpenCode references to Kilo.
*/
import { $ } from "bun"
import { info, success, warn, debug } from "../utils/logger"
import { defaultConfig } from "../utils/config"
export interface WebTransformResult {
file: string
action: "transformed" | "skipped" | "failed"
replacements: number
dryRun: boolean
}
export interface WebTransformOptions {
dryRun?: boolean
verbose?: boolean
}
interface WebReplacement {
pattern: RegExp
replacement: string
description: string
}
// Web/docs replacements
const WEB_REPLACEMENTS: WebReplacement[] = [
// GitHub references
{
pattern: /github\.com\/anomalyco\/opencode/g,
replacement: "github.com/Kilo-Org/kilo",
description: "GitHub URL",
},
{
pattern: /anomalyco\/opencode/g,
replacement: "Kilo-Org/kilo",
description: "GitHub repo",
},
// Domains
{
pattern: /app\.opencode\.ai/g,
replacement: "app.kilo.ai",
description: "App domain",
},
{
pattern: /opencode\.ai/g,
replacement: "kilo.ai",
description: "Main domain",
},
// Product names
{
pattern: /OpenCode Desktop/g,
replacement: "Kilo Desktop",
description: "Desktop name",
},
{
pattern: /OpenCode Zen/g,
replacement: "Kilo Zen",
description: "Zen name",
},
{
pattern: /\bOpenCode\b(?!\.json|\/)/g,
replacement: "Kilo",
description: "Product name",
},
// CLI commands
{
pattern: /npx opencode(?!\w)/g,
replacement: "npx kilo",
description: "npx command",
},
{
pattern: /bun add opencode(?!\w)/g,
replacement: "bun add kilo",
description: "bun add command",
},
{
pattern: /npm install opencode(?!\w)/g,
replacement: "npm install kilo",
description: "npm install command",
},
{
pattern: /opencode upgrade/g,
replacement: "kilo upgrade",
description: "upgrade command",
},
{
pattern: /opencode dev/g,
replacement: "kilo dev",
description: "dev command",
},
{
pattern: /opencode serve/g,
replacement: "kilo serve",
description: "serve command",
},
{
pattern: /opencode auth/g,
replacement: "kilo auth",
description: "auth command",
},
]
// Patterns to preserve
const PRESERVE_PATTERNS = [/opencode\.json/g, /\.opencode\//g, /`\.opencode`/g]
/**
* Check if file is a web/docs file
*/
export function isWebFile(file: string): boolean {
const patterns = defaultConfig.webFiles
return patterns.some((pattern) => {
const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$")
return regex.test(file)
})
}
/**
* Apply web transforms to content
*/
export function applyWebTransforms(content: string, verbose = false): { result: string; replacements: number } {
const lines = content.split("\n")
const transformed: string[] = []
let total = 0
for (const line of lines) {
// Check if line has preserve patterns
let hasPreserve = false
for (const pattern of PRESERVE_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(line)) {
hasPreserve = true
pattern.lastIndex = 0
}
}
// If line has preserve patterns, skip transformation
if (hasPreserve) {
transformed.push(line)
continue
}
let result = line
let count = 0
for (const { pattern, replacement, description } of WEB_REPLACEMENTS) {
pattern.lastIndex = 0
if (pattern.test(result)) {
pattern.lastIndex = 0
const before = result
result = result.replace(pattern, replacement)
if (before !== result) {
count++
if (verbose) debug(` ${description}`)
}
}
}
transformed.push(result)
total += count
}
return { result: transformed.join("\n"), replacements: total }
}
/**
* Transform a web/docs file
*/
export async function transformWebFile(file: string, options: WebTransformOptions = {}): Promise<WebTransformResult> {
if (options.dryRun) {
info(`[DRY-RUN] Would transform web file: ${file}`)
return { file, action: "transformed", replacements: 0, dryRun: true }
}
try {
// Take upstream's version first
await $`git checkout --theirs ${file}`.quiet().nothrow()
await $`git add ${file}`.quiet().nothrow()
// Read content
const content = await Bun.file(file).text()
// Apply transforms
const { result, replacements } = applyWebTransforms(content, options.verbose)
// Write back if changed
if (replacements > 0) {
await Bun.write(file, result)
await $`git add ${file}`.quiet().nothrow()
}
success(`Transformed web file ${file}: ${replacements} replacements`)
return { file, action: "transformed", replacements, dryRun: false }
} catch (err) {
warn(`Failed to transform web file ${file}: ${err}`)
return { file, action: "failed", replacements: 0, dryRun: false }
}
}
/**
* Transform conflicted web files
*/
export async function transformConflictedWeb(
files: string[],
options: WebTransformOptions = {},
): Promise<WebTransformResult[]> {
const results: WebTransformResult[] = []
for (const file of files) {
if (!isWebFile(file)) {
debug(`Skipping ${file} - not a web file`)
results.push({ file, action: "skipped", replacements: 0, dryRun: options.dryRun ?? false })
continue
}
const result = await transformWebFile(file, options)
results.push(result)
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const verbose = args.includes("--verbose")
const files = args.filter((a) => !a.startsWith("--"))
if (files.length === 0) {
info("Usage: transform-web.ts [--dry-run] [--verbose] <file1> <file2> ...")
process.exit(1)
}
if (dryRun) {
info("Running in dry-run mode")
}
const results = await transformConflictedWeb(files, { dryRun, verbose })
const transformed = results.filter((r) => r.action === "transformed")
const total = results.reduce((sum, r) => sum + r.replacements, 0)
console.log()
success(`Transformed ${transformed.length} web files with ${total} replacements`)
if (dryRun) {
info("Run without --dry-run to apply changes")
}
}
+64
View File
@@ -18,6 +18,21 @@ export interface MergeConfig {
/** Files to skip entirely (don't add from upstream, remove if added) */
skipFiles: string[]
/** Files that should take upstream version and apply Kilo branding transforms */
takeTheirsAndTransform: string[]
/** Tauri/Desktop config files with predictable branding patterns */
tauriFiles: string[]
/** Script files with GitHub API references */
scriptFiles: string[]
/** Extension files (Zed, etc.) */
extensionFiles: string[]
/** Web/docs files */
webFiles: string[]
/** Directories that are Kilo-specific and should be preserved */
kiloDirectories: string[]
@@ -55,7 +70,15 @@ export const defaultConfig: MergeConfig = {
"PRIVACY.md",
"SECURITY.md",
"AGENTS.md",
// GitHub workflows - MANUAL REVIEW (can break CI/CD)
".github/workflows/publish-stable.yml",
".github/workflows/publish.yml",
".github/workflows/close-stale-prs.yml",
".github/pull_request_template.md",
// Kilo-specific command files
".opencode/command/commit.md",
// Kilo-specific publish scripts
"packages/opencode/script/publish-registries.ts",
],
// Files that only exist in upstream and should NOT be added to Kilo
@@ -80,8 +103,49 @@ export const defaultConfig: MergeConfig = {
"README.zht.md",
// Stats file
"STATS.md",
// Workflows that don't exist in Kilo
".github/workflows/update-nix-hashes.yml",
],
// Files that should take upstream version and apply Kilo branding transforms
// These are files with only branding differences, no logic changes
takeTheirsAndTransform: [
// App components with branding only
"packages/app/src/components/**/*.tsx",
"packages/app/src/context/**/*.tsx",
"packages/app/src/pages/**/*.tsx",
// UI components
"packages/ui/src/components/**/*.tsx",
"packages/ui/src/context/**/*.tsx",
// Desktop TypeScript files (not Rust)
"packages/desktop/src/**/*.ts",
// E2E and test fixtures
"packages/app/e2e/**/*.ts",
"packages/app/script/**/*.ts",
// GitHub script
"github/index.ts",
// Slack integration
"packages/slack/src/**/*.ts",
],
// Tauri/Desktop config files with predictable branding patterns
tauriFiles: [
"packages/desktop/src-tauri/tauri.conf.json",
"packages/desktop/src-tauri/tauri.prod.conf.json",
"packages/desktop/src-tauri/Cargo.toml",
"packages/desktop/src-tauri/Cargo.lock",
"packages/desktop/src-tauri/src/*.rs",
],
// Script files with GitHub API references
scriptFiles: ["script/*.ts", "packages/opencode/script/*.ts"],
// Extension files
extensionFiles: ["packages/extensions/**/*"],
// Web/docs files
webFiles: ["packages/web/src/content/docs/**/*.mdx"],
kiloDirectories: [
"packages/opencode/src/kilocode",
"packages/opencode/test/kilocode",
+109 -4
View File
@@ -18,8 +18,20 @@ export interface ConflictReport {
export interface ConflictFile {
path: string
type: "markdown" | "package" | "code" | "config" | "i18n" | "other"
recommendation: "keep-ours" | "keep-theirs" | "manual" | "codemod" | "skip" | "i18n-transform"
type: "markdown" | "package" | "code" | "config" | "i18n" | "tauri" | "script" | "extension" | "web" | "other"
recommendation:
| "keep-ours"
| "keep-theirs"
| "manual"
| "codemod"
| "skip"
| "i18n-transform"
| "take-theirs-transform"
| "tauri-transform"
| "package-transform"
| "script-transform"
| "extension-transform"
| "web-transform"
reason: string
}
@@ -31,11 +43,64 @@ function isI18nFile(path: string): boolean {
return /packages\/[^/]+\/src\/i18n\/[^/]+\.ts$/.test(path) && !path.endsWith("/index.ts")
}
/**
* Check if a file is a Tauri/Desktop config file
*/
function isTauriFile(path: string): boolean {
return (
path.includes("packages/desktop/src-tauri/") &&
(path.endsWith(".json") || path.endsWith(".toml") || path.endsWith(".rs") || path.endsWith(".lock"))
)
}
/**
* Check if a file is a script file
*/
function isScriptFile(path: string): boolean {
return path.startsWith("script/") || path.includes("/script/")
}
/**
* Check if a file is an extension file
*/
function isExtensionFile(path: string): boolean {
return path.includes("packages/extensions/")
}
/**
* Check if a file is a web/docs file
*/
function isWebFile(path: string): boolean {
return path.includes("packages/web/src/content/docs/") && path.endsWith(".mdx")
}
/**
* Check if a file should use take-theirs + transform strategy
*/
function shouldTakeTheirsTransform(path: string): boolean {
const patterns = [
/^packages\/app\/src\/components\/.*\.tsx$/,
/^packages\/app\/src\/context\/.*\.tsx$/,
/^packages\/app\/src\/pages\/.*\.tsx$/,
/^packages\/ui\/src\/.*\.tsx$/,
/^packages\/desktop\/src\/.*\.ts$/,
/^packages\/app\/e2e\/.*\.ts$/,
/^packages\/app\/script\/.*\.ts$/,
/^github\/index\.ts$/,
/^packages\/slack\/src\/.*\.ts$/,
]
return patterns.some((p) => p.test(path))
}
/**
* Classify a file based on its path
*/
export function classifyFile(path: string): ConflictFile["type"] {
if (isI18nFile(path)) return "i18n"
if (isTauriFile(path)) return "tauri"
if (isScriptFile(path)) return "script"
if (isExtensionFile(path)) return "extension"
if (isWebFile(path)) return "web"
if (path.endsWith(".md")) return "markdown"
if (path.includes("package.json")) return "package"
if (path.endsWith(".ts") || path.endsWith(".tsx") || path.endsWith(".js") || path.endsWith(".jsx")) return "code"
@@ -91,12 +156,40 @@ export function getRecommendation(
const type = classifyFile(path)
// Check for specific auto-transform strategies
if (shouldTakeTheirsTransform(path)) {
return {
recommendation: "take-theirs-transform",
reason: "Branding-only file: take upstream and apply Kilo branding transforms",
}
}
switch (type) {
case "i18n":
return {
recommendation: "i18n-transform",
reason: "i18n file: take upstream translations and apply Kilo branding",
}
case "tauri":
return {
recommendation: "tauri-transform",
reason: "Tauri config: take upstream and apply Kilo branding transforms",
}
case "script":
return {
recommendation: "script-transform",
reason: "Script file: take upstream and transform GitHub references",
}
case "extension":
return {
recommendation: "extension-transform",
reason: "Extension file: take upstream and apply Kilo branding",
}
case "web":
return {
recommendation: "web-transform",
reason: "Web/docs file: take upstream and apply Kilo branding",
}
case "markdown":
return {
recommendation: "keep-ours",
@@ -104,8 +197,8 @@ export function getRecommendation(
}
case "package":
return {
recommendation: "codemod",
reason: "Package.json needs codemod to transform names and preserve version",
recommendation: "package-transform",
reason: "Package.json: take upstream, transform names, inject Kilo deps, preserve version",
}
case "code":
return {
@@ -196,6 +289,12 @@ export function generateMarkdownReport(report: ConflictReport): string {
const order: ConflictFile["recommendation"][] = [
"skip",
"i18n-transform",
"take-theirs-transform",
"tauri-transform",
"package-transform",
"script-transform",
"extension-transform",
"web-transform",
"keep-ours",
"codemod",
"keep-theirs",
@@ -209,6 +308,12 @@ export function generateMarkdownReport(report: ConflictReport): string {
const titleMap: Record<ConflictFile["recommendation"], string> = {
skip: "Skip (Auto-Remove)",
"i18n-transform": "i18n Transform (Auto-Apply Kilo Branding)",
"take-theirs-transform": "Take Upstream + Kilo Branding (Auto)",
"tauri-transform": "Tauri Config Transform (Auto)",
"package-transform": "Package.json Transform (Auto)",
"script-transform": "Script Transform (Auto)",
"extension-transform": "Extension Transform (Auto)",
"web-transform": "Web/Docs Transform (Auto)",
"keep-ours": "Keep Kilo Version (Ours)",
"keep-theirs": "Take Upstream Version (Theirs)",
codemod: "Apply Codemod",