refactor: improve premerge transforms

This commit is contained in:
Catriel Müller
2026-02-03 17:35:42 -03:00
parent 0960e6a85b
commit 033490587f
9 changed files with 529 additions and 114 deletions
+51 -5
View File
@@ -57,7 +57,7 @@ bun run merge.ts --version v1.1.49 --dry-run
## Merge Process
The merge automation follows this process:
The merge automation follows this process, applying **all transformations BEFORE the merge** to minimize conflicts:
1. **Validate environment**
- Check for upstream remote
@@ -72,14 +72,26 @@ The merge automation follows this process:
- `<author>/kilo-opencode-<version>` - Merge target branch
- `<author>/opencode-<version>` - Transformed upstream branch
5. **Apply transformations** to upstream branch:
5. **Apply ALL transformations to upstream branch (PRE-MERGE)**:
- Transform package names (opencode-ai -> @kilocode/cli)
- Preserve Kilo's versions
- Transform i18n files with Kilo branding
- Transform branding-only files (UI components, configs)
- Transform Tauri/Desktop config files
- Transform package.json files (names, deps, Kilo injections)
- Transform script files (GitHub API references)
- Transform extension files (Zed, etc.)
- Transform web/docs files
- Reset Kilo-specific files
6. **Merge** transformed upstream into Kilo branch
- Since all branding transforms are applied pre-merge, conflicts should be minimal
- Remaining conflicts are files with actual code differences (kilocode_change markers)
7. **Auto-resolve** known conflicts (markdown, Kilo-specific files)
7. **Auto-resolve** any remaining conflicts
- Skip files that shouldn't exist in Kilo
- Keep Kilo's version of specific files
- Fallback transforms for edge cases
8. **Push** and generate final report
@@ -138,9 +150,27 @@ Configuration is defined in `utils/config.ts`:
}
```
## Auto-Resolution Strategies
## Pre-Merge Transformation Strategy
The merge tool uses different strategies based on file type:
**Key insight**: By applying all branding transforms to the upstream branch BEFORE merging, we eliminate most conflicts that would otherwise occur due to branding differences (OpenCode -> Kilo).
### Transform Order (Pre-Merge)
The following transforms are applied to the opencode branch before merging:
1. **Package names** - `opencode-ai` -> `@kilocode/cli`, etc.
2. **Versions** - Preserve Kilo's version numbers
3. **i18n files** - OpenCode -> Kilo in user-visible strings
4. **Branding files** - UI components, configs with branding only
5. **Tauri configs** - Desktop app identifiers, names
6. **package.json** - Names, dependencies, Kilo injections
7. **Scripts** - GitHub API references
8. **Extensions** - Zed, etc.
9. **Web/docs** - Documentation files
### Post-Merge Strategies
After merging, any remaining conflicts are handled based on file type:
| File Type | Strategy | Description |
| ----------------- | ----------------------- | ------------------------------------------------ |
@@ -155,6 +185,22 @@ The merge tool uses different strategies based on file type:
| GitHub workflows | `keep-ours` | Keep Kilo's version (manual review) |
| Code with markers | `manual` | Has `kilocode_change` markers, needs review |
### Why This Reduces Conflicts
Previously, conflicts occurred because:
- Upstream had `OpenCode` branding
- Kilo had `Kilo` branding
- Git saw these as conflicting changes
Now:
- We transform upstream to `Kilo` branding BEFORE merge
- Both branches have the same branding
- Git sees no conflict for branding-only files
The only remaining conflicts are files with **actual code differences** - files with `kilocode_change` markers that contain Kilo-specific logic.
## CLI Options
### merge.ts
+12 -1
View File
@@ -29,6 +29,7 @@ export {
export {
transformConflictedTakeTheirs,
transformTakeTheirs,
transformAllTakeTheirs,
shouldTakeTheirs,
applyBrandingTransforms,
matchesPattern,
@@ -37,6 +38,7 @@ export {
export {
transformConflictedTauri,
transformTauriFile,
transformAllTauri,
isTauriFile,
applyTauriTransforms,
} from "./transforms/transform-tauri"
@@ -44,12 +46,14 @@ export {
export {
transformConflictedPackageJson,
transformPackageJson,
transformAllPackageJson,
isPackageJson,
} from "./transforms/transform-package-json"
export {
transformConflictedScripts,
transformScriptFile,
transformAllScripts,
isScriptFile,
applyScriptTransforms,
} from "./transforms/transform-scripts"
@@ -57,8 +61,15 @@ export {
export {
transformConflictedExtensions,
transformExtensionFile,
transformAllExtensions,
isExtensionFile,
applyExtensionTransforms,
} from "./transforms/transform-extensions"
export { transformConflictedWeb, transformWebFile, isWebFile, applyWebTransforms } from "./transforms/transform-web"
export {
transformConflictedWeb,
transformWebFile,
transformAllWeb,
isWebFile,
applyWebTransforms,
} from "./transforms/transform-web"
+175 -108
View File
@@ -29,12 +29,24 @@ 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"
import {
transformConflictedTakeTheirs,
shouldTakeTheirs,
transformAllTakeTheirs,
} from "./transforms/transform-take-theirs"
import { transformConflictedTauri, isTauriFile, transformAllTauri } from "./transforms/transform-tauri"
import {
transformConflictedPackageJson,
isPackageJson,
transformAllPackageJson,
} from "./transforms/transform-package-json"
import { transformConflictedScripts, isScriptFile, transformAllScripts } from "./transforms/transform-scripts"
import {
transformConflictedExtensions,
isExtensionFile,
transformAllExtensions,
} from "./transforms/transform-extensions"
import { transformConflictedWeb, isWebFile, transformAllWeb } from "./transforms/transform-web"
interface MergeOptions {
version?: string
@@ -250,15 +262,16 @@ async function main() {
await git.createBranch(opencodeBranch)
logger.info(`Created opencode branch: ${opencodeBranch}`)
// Step 6: Apply transformations to opencode branch
logger.step(6, 8, "Applying transformations to opencode branch...")
// Step 6: Apply ALL transformations to opencode branch (pre-merge)
// This reduces conflicts by transforming upstream code to Kilo conventions BEFORE merging
logger.step(6, 8, "Applying transformations to opencode branch (pre-merge)...")
// Transform package names
// 6a. Transform package names (opencode-ai -> @kilocode/cli)
logger.info("Transforming package names...")
const nameResults = await transformPackageNames({ dryRun: false, verbose: options.verbose })
logger.success(`Transformed ${nameResults.length} files`)
// Preserve Kilo versions
// 6b. Preserve Kilo versions
logger.info("Preserving Kilo versions...")
const versionResults = await preserveAllVersions({
dryRun: false,
@@ -267,15 +280,71 @@ async function main() {
})
logger.success(`Preserved versions in ${versionResults.length} files`)
// Reset keep-ours files to Kilo's version
// 6c. Transform i18n files (OpenCode -> Kilo branding)
logger.info("Transforming i18n files...")
const i18nPreResults = await transformAllI18n({ dryRun: false, verbose: options.verbose })
const i18nPreCount = i18nPreResults.filter((r) => r.replacements > 0).length
if (i18nPreCount > 0) {
logger.success(`Transformed ${i18nPreCount} i18n files with Kilo branding`)
}
// 6d. Transform branding-only files (take-theirs patterns)
logger.info("Transforming branding-only files...")
const brandingResults = await transformAllTakeTheirs({ dryRun: false, verbose: options.verbose })
const brandingCount = brandingResults.filter((r) => r.action === "transformed" && r.replacements > 0).length
if (brandingCount > 0) {
logger.success(`Transformed ${brandingCount} files with Kilo branding`)
}
// 6e. Transform Tauri/Desktop config files
logger.info("Transforming Tauri/Desktop config files...")
const tauriPreResults = await transformAllTauri({ dryRun: false, verbose: options.verbose })
const tauriPreCount = tauriPreResults.filter((r) => r.action === "transformed" && r.replacements > 0).length
if (tauriPreCount > 0) {
logger.success(`Transformed ${tauriPreCount} Tauri config files`)
}
// 6f. Transform package.json files (names, deps, Kilo injections)
logger.info("Transforming package.json files...")
const pkgPreResults = await transformAllPackageJson({ dryRun: false, verbose: options.verbose })
const pkgPreCount = pkgPreResults.filter((r) => r.action === "transformed" && r.changes.length > 0).length
if (pkgPreCount > 0) {
logger.success(`Transformed ${pkgPreCount} package.json files`)
}
// 6g. Transform script files (GitHub API references)
logger.info("Transforming script files...")
const scriptPreResults = await transformAllScripts({ dryRun: false, verbose: options.verbose })
const scriptPreCount = scriptPreResults.filter((r) => r.action === "transformed" && r.replacements > 0).length
if (scriptPreCount > 0) {
logger.success(`Transformed ${scriptPreCount} script files`)
}
// 6h. Transform extension files (Zed, etc.)
logger.info("Transforming extension files...")
const extPreResults = await transformAllExtensions({ dryRun: false, verbose: options.verbose })
const extPreCount = extPreResults.filter((r) => r.action === "transformed" && r.replacements > 0).length
if (extPreCount > 0) {
logger.success(`Transformed ${extPreCount} extension files`)
}
// 6i. Transform web/docs files
logger.info("Transforming web/docs files...")
const webPreResults = await transformAllWeb({ dryRun: false, verbose: options.verbose })
const webPreCount = webPreResults.filter((r) => r.action === "transformed" && r.replacements > 0).length
if (webPreCount > 0) {
logger.success(`Transformed ${webPreCount} web/docs files`)
}
// 6j. Reset keep-ours files to Kilo's version
logger.info("Resetting Kilo-specific files...")
const keepOursResults = await resetToOurs(config.keepOurs, { dryRun: false, verbose: options.verbose })
logger.success(`Reset ${keepOursResults.length} files to Kilo's version`)
// Commit transformations
// Commit all transformations
await git.stageAll()
await git.commit(`refactor: kilo compat for ${targetVersion.tag}`)
logger.success("Committed transformations")
logger.success("Committed pre-merge transformations")
// Step 7: Merge into Kilo branch
logger.step(7, 8, "Merging into Kilo branch...")
@@ -284,10 +353,13 @@ async function main() {
const mergeResult = await git.merge(opencodeBranch)
if (!mergeResult.success) {
logger.warn("Merge has conflicts")
logger.warn("Merge has conflicts (these should only be files with actual code differences)")
logger.info("Conflicted files:")
logger.list(mergeResult.conflicts)
// Since we applied all branding transforms pre-merge, remaining conflicts should be minimal.
// These are likely files with kilocode_change markers or actual logic differences.
// Step 7a: Skip files that shouldn't exist in Kilo
logger.info("Removing files that shouldn't exist in Kilo...")
const skipResults = await skipFiles({ dryRun: false, verbose: options.verbose })
@@ -296,105 +368,104 @@ async function main() {
logger.success(`Skipped ${skippedCount} files (removed from merge)`)
}
// Step 7b: Transform i18n files (take upstream + apply Kilo branding)
logger.info("Transforming i18n files...")
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) {
logger.success(`Transformed ${i18nTransformed} i18n files with Kilo branding`)
}
// Step 7c: Auto-resolve keep-ours conflicts
// Step 7b: Auto-resolve keep-ours conflicts
logger.info("Keeping Kilo-specific files...")
const resolved = await keepOursFiles({ dryRun: false, verbose: options.verbose })
const autoResolved = resolved.filter((r) => r.action === "kept")
if (autoResolved.length > 0) {
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 7c: Try to auto-resolve remaining conflicts with post-merge transforms
// These handle edge cases where pre-merge transforms might have missed something
let conflictedFiles = await git.getConflictedFiles()
// 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`)
}
}
logger.info("Attempting to auto-resolve remaining conflicts...")
// 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`)
// Transform i18n files
const i18nResults = await transformConflictedI18n(conflictedFiles, { dryRun: false, verbose: options.verbose })
const i18nTransformed = i18nResults.filter((r) => r.replacements > 0).length
if (i18nTransformed > 0) {
logger.success(`Auto-resolved ${i18nTransformed} i18n conflicts`)
}
}
// 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`)
// Transform branding-only files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const takeTheirsResults = await transformConflictedTakeTheirs(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const takeTheirsCount = takeTheirsResults.filter((r) => r.action === "transformed").length
if (takeTheirsCount > 0) {
logger.success(`Auto-resolved ${takeTheirsCount} branding conflicts`)
}
}
}
// 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`)
// Transform Tauri files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const tauriResults = await transformConflictedTauri(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const tauriCount = tauriResults.filter((r) => r.action === "transformed").length
if (tauriCount > 0) {
logger.success(`Auto-resolved ${tauriCount} Tauri conflicts`)
}
}
}
// 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`)
// Transform package.json files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const pkgResults = await transformConflictedPackageJson(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const pkgCount = pkgResults.filter((r) => r.action === "transformed").length
if (pkgCount > 0) {
logger.success(`Auto-resolved ${pkgCount} package.json conflicts`)
}
}
// Transform script files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const scriptResults = await transformConflictedScripts(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const scriptCount = scriptResults.filter((r) => r.action === "transformed").length
if (scriptCount > 0) {
logger.success(`Auto-resolved ${scriptCount} script conflicts`)
}
}
// Transform extension files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const extResults = await transformConflictedExtensions(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const extCount = extResults.filter((r) => r.action === "transformed").length
if (extCount > 0) {
logger.success(`Auto-resolved ${extCount} extension conflicts`)
}
}
// Transform web/docs files
conflictedFiles = await git.getConflictedFiles()
if (conflictedFiles.length > 0) {
const webResults = await transformConflictedWeb(conflictedFiles, {
dryRun: false,
verbose: options.verbose,
})
const webCount = webResults.filter((r) => r.action === "transformed").length
if (webCount > 0) {
logger.success(`Auto-resolved ${webCount} web/docs conflicts`)
}
}
}
@@ -404,24 +475,20 @@ async function main() {
logger.warn(`${remaining.length} conflicts require manual resolution:`)
logger.list(remaining)
logger.info("")
logger.info("These conflicts likely contain kilocode_change markers or have actual code differences.")
logger.info("After resolving conflicts, run:")
logger.info(" git add -A && git commit -m 'resolve merge conflicts'")
} else {
await git.stageAll()
await git.commit(`merge: upstream ${targetVersion.tag}`)
logger.success("Merge completed")
logger.success("Merge completed - all conflicts auto-resolved!")
}
} else {
logger.success("Merge completed without conflicts")
// Even without conflicts, transform i18n files to ensure Kilo branding
logger.info("Transforming i18n files for Kilo branding...")
const i18nResults = await transformAllI18n({ dryRun: false, verbose: options.verbose })
const i18nTransformed = i18nResults.filter((r) => r.replacements > 0).length
if (i18nTransformed > 0) {
await git.stageAll()
await git.commit(`chore: apply Kilo branding to i18n files`)
logger.success(`Transformed ${i18nTransformed} i18n files with Kilo branding`)
logger.success("Merge completed without conflicts!")
await git.stageAll()
const hasChanges = await git.hasUncommittedChanges()
if (hasChanges) {
await git.commit(`merge: upstream ${targetVersion.tag}`)
}
}
@@ -205,6 +205,50 @@ export async function transformConflictedExtensions(
return results
}
/**
* Transform all extension files (pre-merge, on opencode branch)
*/
export async function transformAllExtensions(
options: ExtensionTransformOptions = {},
): Promise<ExtensionTransformResult[]> {
const { Glob } = await import("bun")
const results: ExtensionTransformResult[] = []
const patterns = defaultConfig.extensionFiles
for (const pattern of patterns) {
const glob = new Glob(pattern)
for await (const path of glob.scan({ absolute: false })) {
const file = Bun.file(path)
if (!(await file.exists())) continue
// Skip non-text files
if (!path.endsWith(".toml") && !path.endsWith(".json") && !path.endsWith(".ts") && !path.endsWith(".js")) {
continue
}
try {
const content = await file.text()
const { result, replacements } = applyExtensionTransforms(content, path, options.verbose)
if (replacements > 0 && !options.dryRun) {
await Bun.write(path, result)
success(`Transformed extension ${path}: ${replacements} replacements`)
} else if (options.dryRun && replacements > 0) {
info(`[DRY-RUN] Would transform extension ${path}: ${replacements} replacements`)
}
results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform extension ${path}: ${err}`)
results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
}
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
@@ -195,6 +195,103 @@ export async function transformConflictedPackageJson(
return results
}
/**
* Transform all package.json files (pre-merge, on opencode branch)
*/
export async function transformAllPackageJson(options: PackageJsonOptions = {}): Promise<PackageJsonResult[]> {
const { Glob } = await import("bun")
const results: PackageJsonResult[] = []
// Find all package.json files
const glob = new Glob("**/package.json")
for await (const path of glob.scan({ absolute: false })) {
// Skip node_modules
if (path.includes("node_modules")) continue
const file = Bun.file(path)
if (!(await file.exists())) continue
try {
const content = await file.text()
const pkg = JSON.parse(content)
const changes: string[] = []
// 1. Transform package name if needed
const newName = TRANSFORM_PACKAGE_NAMES[path]
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)
if (depChanges.length > 0) {
pkg.dependencies = result
changes.push(...depChanges.map((c) => `dependencies: ${c}`))
}
}
// 4. Transform devDependencies
if (pkg.devDependencies) {
const { result, changes: devChanges } = transformDependencies(pkg.devDependencies)
if (devChanges.length > 0) {
pkg.devDependencies = result
changes.push(...devChanges.map((c) => `devDependencies: ${c}`))
}
}
// 5. Transform peerDependencies
if (pkg.peerDependencies) {
const { result, changes: peerChanges } = transformDependencies(pkg.peerDependencies)
if (peerChanges.length > 0) {
pkg.peerDependencies = result
changes.push(...peerChanges.map((c) => `peerDependencies: ${c}`))
}
}
// 6. Inject Kilo-specific dependencies
const kiloDeps = KILO_DEPENDENCIES[path]
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}`)
}
}
}
if (changes.length > 0) {
if (!options.dryRun) {
const newContent = JSON.stringify(pkg, null, 2) + "\n"
await Bun.write(path, newContent)
success(`Transformed ${path}: ${changes.length} changes`)
} else {
info(`[DRY-RUN] Would transform ${path}: ${changes.length} changes`)
}
}
results.push({ file: path, action: "transformed", changes, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform ${path}: ${err}`)
results.push({ file: path, action: "failed", changes: [], dryRun: options.dryRun ?? false })
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
@@ -171,6 +171,43 @@ export async function transformConflictedScripts(
return results
}
/**
* Transform all script files (pre-merge, on opencode branch)
*/
export async function transformAllScripts(options: ScriptTransformOptions = {}): Promise<ScriptTransformResult[]> {
const { Glob } = await import("bun")
const results: ScriptTransformResult[] = []
const patterns = defaultConfig.scriptFiles
for (const pattern of patterns) {
const glob = new Glob(pattern)
for await (const path of glob.scan({ absolute: false })) {
const file = Bun.file(path)
if (!(await file.exists())) continue
try {
const content = await file.text()
const { result, replacements } = applyScriptTransforms(content, options.verbose)
if (replacements > 0 && !options.dryRun) {
await Bun.write(path, result)
success(`Transformed script ${path}: ${replacements} replacements`)
} else if (options.dryRun && replacements > 0) {
info(`[DRY-RUN] Would transform script ${path}: ${replacements} replacements`)
}
results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform script ${path}: ${err}`)
results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
}
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
@@ -241,6 +241,45 @@ export function shouldTakeTheirs(file: string, patterns?: string[]): boolean {
return matchesPattern(file, p)
}
/**
* Transform all files matching take-theirs patterns (pre-merge, on opencode branch)
* This applies branding transforms to files that exist on the current branch
*/
export async function transformAllTakeTheirs(options: TakeTheirsOptions = {}): Promise<TakeTheirsResult[]> {
const { Glob } = await import("bun")
const results: TakeTheirsResult[] = []
const patterns = options.patterns || defaultConfig.takeTheirsAndTransform
for (const pattern of patterns) {
const glob = new Glob(pattern)
for await (const path of glob.scan({ absolute: false })) {
// Skip if file doesn't exist
const file = Bun.file(path)
if (!(await file.exists())) continue
try {
const content = await file.text()
const { result, replacements } = applyBrandingTransforms(content, options.verbose)
if (replacements > 0 && !options.dryRun) {
await Bun.write(path, result)
success(`Transformed ${path}: ${replacements} branding replacements`)
} else if (options.dryRun && replacements > 0) {
info(`[DRY-RUN] Would transform ${path}: ${replacements} branding replacements`)
}
results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform ${path}: ${err}`)
results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
}
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
@@ -263,6 +263,43 @@ export async function transformConflictedTauri(
return results
}
/**
* Transform all Tauri files (pre-merge, on opencode branch)
*/
export async function transformAllTauri(options: TauriTransformOptions = {}): Promise<TauriTransformResult[]> {
const { Glob } = await import("bun")
const results: TauriTransformResult[] = []
const patterns = defaultConfig.tauriFiles
for (const pattern of patterns) {
const glob = new Glob(pattern)
for await (const path of glob.scan({ absolute: false })) {
const file = Bun.file(path)
if (!(await file.exists())) continue
try {
const content = await file.text()
const { result, replacements } = applyTauriTransforms(content, path, options.verbose)
if (replacements > 0 && !options.dryRun) {
await Bun.write(path, result)
success(`Transformed Tauri ${path}: ${replacements} replacements`)
} else if (options.dryRun && replacements > 0) {
info(`[DRY-RUN] Would transform Tauri ${path}: ${replacements} replacements`)
}
results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform Tauri ${path}: ${err}`)
results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
}
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)
@@ -231,6 +231,43 @@ export async function transformConflictedWeb(
return results
}
/**
* Transform all web/docs files (pre-merge, on opencode branch)
*/
export async function transformAllWeb(options: WebTransformOptions = {}): Promise<WebTransformResult[]> {
const { Glob } = await import("bun")
const results: WebTransformResult[] = []
const patterns = defaultConfig.webFiles
for (const pattern of patterns) {
const glob = new Glob(pattern)
for await (const path of glob.scan({ absolute: false })) {
const file = Bun.file(path)
if (!(await file.exists())) continue
try {
const content = await file.text()
const { result, replacements } = applyWebTransforms(content, options.verbose)
if (replacements > 0 && !options.dryRun) {
await Bun.write(path, result)
success(`Transformed web ${path}: ${replacements} replacements`)
} else if (options.dryRun && replacements > 0) {
info(`[DRY-RUN] Would transform web ${path}: ${replacements} replacements`)
}
results.push({ file: path, action: "transformed", replacements, dryRun: options.dryRun ?? false })
} catch (err) {
warn(`Failed to transform web ${path}: ${err}`)
results.push({ file: path, action: "failed", replacements: 0, dryRun: options.dryRun ?? false })
}
}
}
return results
}
// CLI entry point
if (import.meta.main) {
const args = process.argv.slice(2)