diff --git a/script/upstream/README.md b/script/upstream/README.md index 14405df079..f5a3dd3e58 100644 --- a/script/upstream/README.md +++ b/script/upstream/README.md @@ -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 diff --git a/script/upstream/index.ts b/script/upstream/index.ts index 89a194b990..46e492d6e4 100644 --- a/script/upstream/index.ts +++ b/script/upstream/index.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" diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 1a31ab1492..0219b4560c 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -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) { diff --git a/script/upstream/transforms/transform-extensions.ts b/script/upstream/transforms/transform-extensions.ts new file mode 100644 index 0000000000..3942fc92de --- /dev/null +++ b/script/upstream/transforms/transform-extensions.ts @@ -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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/transforms/transform-package-json.ts b/script/upstream/transforms/transform-package-json.ts new file mode 100644 index 0000000000..f452dc6532 --- /dev/null +++ b/script/upstream/transforms/transform-package-json.ts @@ -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 = { + "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> = { + // 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 = { + "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 | undefined): { + result: Record + changes: string[] +} { + if (!deps) return { result: {}, changes: [] } + + const result: Record = {} + 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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/transforms/transform-scripts.ts b/script/upstream/transforms/transform-scripts.ts new file mode 100644 index 0000000000..1c2d7346ef --- /dev/null +++ b/script/upstream/transforms/transform-scripts.ts @@ -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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/transforms/transform-take-theirs.ts b/script/upstream/transforms/transform-take-theirs.ts new file mode 100644 index 0000000000..aca1986357 --- /dev/null +++ b/script/upstream/transforms/transform-take-theirs.ts @@ -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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/transforms/transform-tauri.ts b/script/upstream/transforms/transform-tauri.ts new file mode 100644 index 0000000000..b8f312565a --- /dev/null +++ b/script/upstream/transforms/transform-tauri.ts @@ -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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/transforms/transform-web.ts b/script/upstream/transforms/transform-web.ts new file mode 100644 index 0000000000..f889521813 --- /dev/null +++ b/script/upstream/transforms/transform-web.ts @@ -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 { + 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 { + 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] ...") + 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") + } +} diff --git a/script/upstream/utils/config.ts b/script/upstream/utils/config.ts index b641026d75..75c09632e0 100644 --- a/script/upstream/utils/config.ts +++ b/script/upstream/utils/config.ts @@ -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", diff --git a/script/upstream/utils/report.ts b/script/upstream/utils/report.ts index 5aaddec244..f023ac8d23 100644 --- a/script/upstream/utils/report.ts +++ b/script/upstream/utils/report.ts @@ -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 = { 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",