Conversation
| * by transforming OpenCode references to Kilo. | ||
| */ | ||
|
|
||
| import { $ } from "bun" |
There was a problem hiding this comment.
WARNING: Unused import {$}
In script/upstream/transforms/transform-web.ts, import { $ } from "bun" is only used by transformWebFile() when options.dryRun is false, but options.dryRun currently returns early before the git checkout --theirs step (so $ is never used in dry-run). However in the non-dry-run path, $ is used; so the import is used.
(If the intent is to allow dry-run without requiring git operations, consider keeping as-is; otherwise, if this script should always do git checkout before transforming, move the dry-run early return to after the checkout stage.)
| } | ||
| } | ||
|
|
||
| export async function commit(message: string): Promise<void> { |
There was a problem hiding this comment.
CRITICAL: commit() only commits tracked changes
commit() uses git commit -am, which does not include newly added files. In this PR, the upstream-merge tool adds new files under script/upstream/, so the transformation commit in merge.ts may miss those additions unless they were already tracked.
Safer pattern for automation is git commit -m after an explicit git add -A (which merge.ts already does via stageAll()).
| */ | ||
|
|
||
| import { $ } from "bun" | ||
| import { getUpstreamTags, getCommitMessage, getTagsForCommit } from "./git" |
There was a problem hiding this comment.
SUGGESTION: Unused import getUpstreamTags
getUpstreamTags is imported but not used in this file. Consider removing it to avoid lint/tsc warnings.
Code Review SummaryStatus: 1 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (25 files)
|
|
@catrielmuller how fragile do you judge this script? It's a lot to maintain. |
5c4625d to
e305491
Compare
| * by transforming OpenCode references to Kilo. | ||
| */ | ||
|
|
||
| import { $ } from "bun" |
There was a problem hiding this comment.
WARNING: Unused import {$}
$ is imported but never referenced in this file. Keeping unused imports can hide intended-but-missing behavior (e.g., forgetting to stage via git).
| for (const line of output.trim().split("\n")) { | ||
| // Match lines like: abc123... refs/tags/v1.1.49 | ||
| // Skip annotated tag references (those ending with ^{}) | ||
| const match = line.match(/^([a-f0-9]+)\s+refs\/tags\/([^\^]+)$/) |
There was a problem hiding this comment.
WARNING: Annotated upstream tags may resolve to the tag object hash, not the commit
git ls-remote --tags returns both refs/tags/<tag> (often the tag object for annotated tags) and refs/tags/<tag>^{} (the peeled commit). The current regex explicitly ignores the ^{} line (per comment) but then uses the remaining hash as commit, which can be incorrect for annotated tags and break downstream checkout/analysis. Prefer collecting both and using the peeled ^{} hash when present (or at least skipping non-peeled hashes when a peeled one exists).
|
|
||
| try { | ||
| // Take upstream's version first | ||
| await $`git checkout --theirs ${file}`.quiet().nothrow() |
There was a problem hiding this comment.
WARNING: git checkout --theirs / git add failures can be silently ignored
These commands use .nothrow() and their exit codes aren’t checked. If git checkout --theirs fails (e.g., file not in conflict / path issues), the script will continue and report success, potentially transforming/staging the wrong content. Consider checking exitCode and throwing on non-zero (like fetchUpstream() does) so failures are surfaced reliably.
| @@ -1,5 +1,5 @@ | |||
| /// <reference path="../env.d.ts" /> | |||
| import { tool } from "@kilocode/plugin" // kilocode_change | |||
| import { tool } from "@kilocode/plugin" | |||
There was a problem hiding this comment.
SUGGESTION: Preserve kilocode_change markers for fork-maintained files
This file is not in a kilocode/ path, so removing the // kilocode_change marker makes future upstream merges harder to audit and increases conflict risk. Consider restoring the marker on Kilo-specific deltas (per AGENTS.md).
| /// <reference path="../env.d.ts" /> | ||
| // import { Octokit } from "@octokit/rest" | ||
| import { tool } from "@kilocode/plugin" // kilocode_change | ||
| import { tool } from "@kilocode/plugin" |
There was a problem hiding this comment.
SUGGESTION: Preserve kilocode_change markers for fork-maintained files
Same as above: if this is intended to be a Kilo-only deviation from upstream, keeping the marker helps keep merge diffs minimal and reviewable.
| 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 } |
There was a problem hiding this comment.
WARNING: --dry-run currently reports action: "transformed"
In dry-run mode, transformWebFile() returns action: "transformed" and will be counted as transformed by the CLI summary, even though no changes are applied. This can mislead automation/logs.
| return { file, action: "transformed", replacements: 0, dryRun: true } | |
| return { file, action: "skipped", replacements: 0, dryRun: true } |
| */ | ||
| export async function getCurrentKiloVersion(): Promise<string> { | ||
| // Resolve path relative to repo root (script is in script/upstream/) | ||
| const path = new URL("../../../packages/opencode/package.json", import.meta.url).pathname |
There was a problem hiding this comment.
WARNING: new URL(...).pathname can break on Windows paths
Using .pathname on a file: URL can produce POSIX-style paths (e.g. /C:/...) on Windows. If these scripts are expected to run cross-platform, prefer passing the URL directly to Bun.file() (if supported) or use fileURLToPath() from node:url to convert safely.
a5798ae to
294685f
Compare
| info("Analyzing changes...") | ||
|
|
||
| // Use commit hash directly since we may not have the tag fetched locally | ||
| const conflicts = await report.analyzeConflicts(target.commit, config.baseBranch, config.keepOurs) |
There was a problem hiding this comment.
WARNING: analyzeConflicts() isn’t passed skipFiles, so skip-only upstream artifacts won’t be flagged
merge.ts passes config.skipFiles, but script/upstream/analyze.ts currently doesn’t. This makes the analysis report less accurate (it may recommend manual work for files that the merge flow would auto-remove).
| const conflicts = await report.analyzeConflicts(target.commit, config.baseBranch, config.keepOurs) | |
| const conflicts = await report.analyzeConflicts(target.commit, config.baseBranch, config.keepOurs, config.skipFiles) |
| let modified = false | ||
|
|
||
| for (const { pattern, replacement } of STRING_REPLACEMENTS) { | ||
| if (pattern.test(value)) { |
There was a problem hiding this comment.
WARNING: Global RegExp + test() without resetting lastIndex can make replacements flaky
In transformStrings(), STRING_REPLACEMENTS uses /g regexes. Calling pattern.test(value) repeatedly across many literals without pattern.lastIndex = 0 can cause false negatives after the first match, leading to skipped transformations depending on iteration order. Consider resetting lastIndex before each test() (or avoid test() and rely on replace() + comparison).
| } | ||
|
|
||
| if (headModified) { | ||
| // Template head replacement is complex, skip for now |
There was a problem hiding this comment.
WARNING: TemplateExpression path increments changes without applying any edit
At script/upstream/codemods/transform-strings.ts, headModified results in changes++, but the AST/text is intentionally left unchanged. That can over-report transformations and may cause no-op saves/commits to be treated as real edits. If template-head handling is intentionally unsupported, consider not incrementing changes here (or implement the replacement).
1886e3f to
a2e1a37
Compare
| // Step 2: Fetch upstream | ||
| logger.step(2, 8, "Fetching upstream...") | ||
|
|
||
| if (!options.dryRun) { |
There was a problem hiding this comment.
CRITICAL: --dry-run skips fetching upstream, but later analysis diffs against an upstream commit
In script/upstream/merge.ts, dry-run avoids git fetch upstream, but the script still calls report.analyzeConflicts() using an upstream commit hash from git ls-remote. Without fetching that commit object locally, git diff ${base}...${commit} can fail, breaking dry-run/report generation. Consider fetching upstream even in dry-run (network-only, no working tree changes) or adjusting analysis to use available refs.
| } | ||
|
|
||
| // Check if file should always keep ours | ||
| if (keepOurs.some((pattern) => path.includes(pattern) || path === pattern)) { |
There was a problem hiding this comment.
WARNING: keepOurs matching via path.includes(...) can unintentionally match unrelated files
In getRecommendation(), keepOurs.some((pattern) => path.includes(pattern) || path === pattern) will treat any path containing e.g. README.md as keep-ours (like packages/app/README.md). That can incorrectly discard upstream changes. Prefer exact path matching or proper glob/minimatch semantics.
| * Check if a file should be skipped (not added from upstream) | ||
| */ | ||
| function shouldSkipFile(path: string, skipPatterns: string[]): boolean { | ||
| return skipPatterns.some((pattern) => path === pattern || path.includes(pattern)) |
There was a problem hiding this comment.
WARNING: skipFiles matching via substring (includes) may skip unintended paths
shouldSkipFile() returns true when path.includes(pattern). With filename-like patterns (e.g. README.de.md), this can match files in unexpected directories and remove them during merge. Prefer exact match or glob matching, consistent with how other file patterns are treated.
a2e1a37 to
a05a66b
Compare
| return regex.test(filePath) | ||
| } | ||
| // Contains match | ||
| return filePath.includes(pattern) |
There was a problem hiding this comment.
WARNING: shouldKeepOurs() uses substring matching for non-glob patterns
In shouldKeepOurs(), non-glob patterns fall back to filePath.includes(pattern), which can over-match (e.g. a pattern like README.md will also match packages/app/README.md). In an automated merge/conflict resolver, this can silently discard upstream changes for unrelated files.
Consider restricting non-glob patterns to exact path matches (or using a consistent glob matcher) to avoid accidental matches.
|
|
||
| try { | ||
| // Take upstream's version | ||
| await $`git checkout --theirs ${file}`.quiet().nothrow() |
There was a problem hiding this comment.
WARNING: git checkout --theirs / git add failures are ignored
$git checkout --theirs ....quiet().nothrow() will not throw on failure, and the code doesn’t check exitCode before continuing. If checkout/add fails (path missing, not in conflict, git error), the transform may proceed using the wrong content and still report action: "transformed", which can leave the repo in a misleading state.
Recommend checking exitCode for these commands (and returning failed / throwing) so callers can reliably detect and stop on git failures.
a05a66b to
45a4b24
Compare
| } | ||
|
|
||
| // If line has preserve patterns, skip transformation | ||
| if (hasPreserve) { |
There was a problem hiding this comment.
WARNING: PRESERVE_PATTERNS skips the entire line, which can leave other OpenCode references untransformed
In applyWebTransforms(), any line containing e.g. .opencode/ or opencode.json is pushed unchanged and the loop continues. That means other replacements on the same line (like opencode.ai → kilo.ai or OpenCode → Kilo) won't run, leaving partially-branded docs.
Consider preserving only the matched substrings (mask/restore) instead of skipping the full line.
45a4b24 to
e5f92bc
Compare
| continue | ||
| } | ||
|
|
||
| // Check if line has preserve patterns |
There was a problem hiding this comment.
WARNING: Preserve patterns detected but not enforced
In applyBrandingTransforms(), hasPreserve is computed but never used to skip transformations, so PRESERVE_PATTERNS (e.g. .opencode/, opencode.json) won’t actually be preserved and may be rewritten. Consider short-circuiting when a line matches a preserve pattern (similar to applyWebTransforms()).
Context
Kilo CLI is a fork of opencode, and we regularly merge upstream changes to stay in sync. Previously, this was a painful manual process involving many merge conflicts due to branding differences (OpenCode vs Kilo).
This PR introduces a comprehensive automation toolset in
script/upstream/that transforms upstream code before merging, drastically reducing conflicts and streamlining the sync process.Implementation
Architecture Overview
The automation follows an 8-step process:
Key Insight: Pre-Merge Transformation
The critical innovation is applying branding transforms before the merge:
The only remaining conflicts are files with actual code differences (files with
kilocode_changemarkers).Components
Main Scripts (
script/upstream/)merge.tsanalyze.tslist-versions.tsTransform Pipeline (
transforms/)package-names.tsopencode-ai->@kilocode/cliin all filespreserve-versions.tstransform-i18n.tstransform-take-theirs.tstransform-tauri.tstransform-package-json.tstransform-scripts.tstransform-extensions.tstransform-web.tskeep-ours.tsskip-files.tslock-files.tsAST-Based Codemods (
codemods/)transform-imports.tstransform-strings.tsUtilities (
utils/)config.tsgit.tslogger.tsreport.tsversion.tsConfiguration Highlights
Tradeoffs & Decisions
Pre-merge vs post-merge transforms: We chose pre-merge to minimize conflicts. The tradeoff is that the upstream branch is modified, but this is isolated to a feature branch.
String-based vs AST-based transforms: Most transforms use regex for speed. AST-based codemods (ts-morph) are reserved for complex cases where context matters.
Lock file handling: We accept "ours" and regenerate rather than attempting 3-way merge. Lock files are not meant to be manually merged.
Manual review for
kilocode_changefiles: Files with actual code differences still require manual review, which is intentional - these contain Kilo-specific logic that needs human judgment.Screenshots
How to Test
After running, you should see:
backup/dev-<timestamp><author>/kilo-opencode-<version><author>/opencode-<version>upstream-merge-report-<version>.mdRollback
If something goes wrong: