mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
refactor: improve upstream script
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user