From 4290cbcff57282804fa90a8acd39c0057a1fa656 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:54:29 +0000 Subject: [PATCH] fix(upstream): harden kilocode marker fixer --- script/upstream/fix-kilocode-markers.ts | 135 ++++++++++++++++++------ 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/script/upstream/fix-kilocode-markers.ts b/script/upstream/fix-kilocode-markers.ts index 54a8314837..1f3a69bd8b 100644 --- a/script/upstream/fix-kilocode-markers.ts +++ b/script/upstream/fix-kilocode-markers.ts @@ -38,19 +38,26 @@ interface Range { end: number } -type Style = "slash" | "hash" | "jsx" +type Style = "slash" | "hash" | "jsx" | "block" const standalone = [ /^\s*\/\/\s*kilocode_change\b.*$/, /^\s*#\s*kilocode_change\b.*$/, /^\s*\{?\s*\/\*\s*kilocode_change\b.*\*\/\}?\s*$/, ] -const suffix = [ - /\s+\/\/\s*kilocode_change\b.*$/, - /\s+#\s*kilocode_change\b.*$/, - /\s+\{\s*\/\*\s*kilocode_change\b.*\*\/\s*\}\s*$/, -] const unsupported = new Set([".json", ".jsonc", ".lock", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico"]) +const styles = new Map([ + [".ts", "slash"], + [".tsx", "slash"], + [".js", "slash"], + [".jsx", "slash"], + [".yml", "hash"], + [".yaml", "hash"], + [".toml", "hash"], + [".sh", "hash"], + [".bash", "hash"], + [".zsh", "hash"], +]) const url = "https://github.com/anomalyco/opencode.git" const exempt = ["script/upstream/"] @@ -95,10 +102,11 @@ function ext(file: string) { return path.extname(file).toLowerCase() } -function supported(file: string) { +function supported(file: string, text: string) { const kind = ext(file) if (unsupported.has(kind)) return false - return true + if (styles.has(kind)) return true + return !kind && text.startsWith("#!") } function annotates(file: string) { @@ -116,15 +124,49 @@ function join(text: Text) { return text.lines.join(text.eol) + (text.final ? text.eol : "") } -function strip(line: string) { +function strip(file: string, line: string) { if (standalone.some((item) => item.test(line))) return null - return suffix.reduce((current, item) => current.replace(item, ""), line) + if (style(file) === "hash") return comment(line, ["#"]) + return comment(line, ["{/*", "/*", "//"]) } -function clean(text: string): Text { +function comment(line: string, tokens: string[]) { + let quote = "" + let escape = false + + for (let i = 0; i < line.length; i++) { + const char = line[i] + if (!char) continue + + if (quote) { + if (escape) { + escape = false + continue + } + if (char === "\\") { + escape = true + continue + } + if (char === quote) quote = "" + continue + } + + if (char === '"' || char === "'" || char === "`") { + quote = char + continue + } + + const token = tokens.find((item) => line.startsWith(item, i)) + if (token && line.slice(i).includes("kilocode_change")) return line.slice(0, i).trimEnd() + } + + return line +} + +function clean(file: string, text: string): Text { const parsed = split(text) const lines = parsed.lines.flatMap((line) => { - const next = strip(line) + const next = strip(file, line) if (next === null) return [] return [next] }) @@ -189,34 +231,64 @@ async function upstream(ref: string, file: string) { function style(file: string): Style { const kind = ext(file) - if ([".yml", ".yaml", ".toml", ".sh", ".bash", ".zsh"].includes(kind)) return "hash" - return "slash" + return styles.get(kind) ?? "hash" } -function jsx(lines: string[], range: Range) { - const first = lines[range.start]?.trim() ?? "" - if (!first) return false - if (first.startsWith("<")) return true - if (first.startsWith("{")) { - const prev = - lines - .slice(0, range.start) - .findLast((line) => line.trim().length > 0) - ?.trim() ?? "" - if (prev.endsWith(">") || prev.endsWith(")") || prev.endsWith("(")) return true +function context(file: string, text: Text, range: Range): Style { + const base = style(file) + if (![".tsx", ".jsx"].includes(ext(file))) return base + + if (tag(text.lines, range.start)) return "block" + if (child(text.lines, range.start)) return "jsx" + return base +} + +function nearby(lines: string[], start: number, step: number) { + for (let i = start; i >= 0 && i < lines.length; i += step) { + const line = lines[i]?.trim() + if (line) return line } + return "" +} + +function tag(lines: string[], start: number) { + const current = lines[start]?.trim() ?? "" + if (!current) return false + if (/^[A-Za-z_$][\w$.:/-]*(=|\s*=)/.test(current)) return true + + for (let i = start - 1; i >= Math.max(0, start - 20); i--) { + const line = lines[i]?.trim() ?? "" + if (!line) continue + if (line.includes(">")) return false + if (/^<\/?[A-Za-z]/.test(line)) return true + } + + return false +} + +function child(lines: string[], start: number) { + const current = lines[start]?.trim() ?? "" + const prev = nearby(lines, start - 1, -1) + const next = nearby(lines, start + 1, 1) + + if (prev.endsWith(">") && !prev.endsWith("=>")) return true + if (next.startsWith("): Range[] { @@ -245,10 +318,9 @@ function ranges(nums: Set): Range[] { function annotate(file: string, text: Text, found: Range[]) { const lines = [...text.lines] - const base = style(file) for (const range of [...found].reverse()) { - const mode = [".tsx", ".jsx"].includes(ext(file)) && jsx(text.lines, range) ? "jsx" : base + const mode = context(file, text, range) if (range.start === range.end && inline(file, text.lines, range, mode)) { lines[range.start] = `${lines[range.start]}${note(mode)}` continue @@ -340,10 +412,9 @@ async function main() { process.chdir(top) const file = normalize(top, opts.file) - if (!supported(file)) throw new Error(`Cannot safely add comment markers to ${file}`) - const abs = path.join(top, file) const current = await Bun.file(abs).text() + if (!supported(file, current)) throw new Error(`Cannot safely add comment markers to ${file}`) if (current.includes("\0")) throw new Error(`${file} appears to be binary`) header("Fix kilocode_change markers") @@ -352,8 +423,8 @@ async function main() { success(`Last merged upstream: ${version.tag} (${version.commit.slice(0, 8)})`) const base = await upstream(version.commit, file) - const head = clean(current) - const diff = base === null ? null : await changed(clean(base), head) + const head = clean(file, current) + const diff = base === null ? null : await changed(clean(file, base), head) const found = ranges(diff?.lines ?? new Set()) const next = base === null ? fresh(file, head) : annotate(file, head, found)