resolve merge conflicts

This commit is contained in:
Johnny Eric Amancio
2026-08-06 14:10:41 +02:00
299 changed files with 7144 additions and 1667 deletions
+42
View File
@@ -0,0 +1,42 @@
// kilocode_change - new file
import path from "path"
const root = path.resolve(import.meta.dir, "..")
const proc = Bun.spawnSync(["git", "ls-files", "packages", "script"], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
if (proc.exitCode !== 0) throw new Error(proc.stderr.toString() || "Unable to list tracked package tests")
const exempt = new Set(["packages/kilo-vscode"])
const dirs = new Set(
proc.stdout
.toString()
.split("\n")
.filter((file) => /^packages\/.*\.test\.tsx?$/.test(file))
.map((file) => file.split("/").slice(0, 2).join("/")),
)
const missing: string[] = []
const files = proc.stdout.toString().split("\n")
const scripts = files.filter((file) => /^script\/.*\.test\.tsx?$/.test(file))
if (scripts.length > 0) {
const pkg = (await Bun.file(path.join(root, "package.json")).json()) as { scripts?: Record<string, string> }
if (!pkg.scripts?.["test:script:ci"]?.includes("bun test ./script")) missing.push("package.json#test:script:ci")
}
for (const dir of [...dirs].sort()) {
if (exempt.has(dir)) continue
const file = path.join(root, dir, "package.json")
const source = Bun.file(file)
if (!(await source.exists())) continue
const pkg = (await source.json()) as { scripts?: Record<string, string> }
const scripts = pkg.scripts
if (!scripts?.test && !scripts?.["test:ci"]) continue
if (!scripts["test:ci"]) missing.push(`${dir}/package.json`)
}
if (missing.length > 0) throw new Error(`Test suites missing CI scheduling:\n${missing.join("\n")}`)
console.log(`check-test-ci: ok (${dirs.size - exempt.size} test-bearing package(s), ${scripts.length} root script test file(s))`)
-21
View File
@@ -1,21 +0,0 @@
Translate the product app locale `$1` from the English source dictionaries. English is the read-only source of truth. Its copy is intentional and must never be modified, rewritten, or "improved."
The translation request below contains the locale glossary, exact source and target files, plus missing, extra, and placeholder-mismatched keys.
```json
$ARGUMENTS
```
Requirements:
- Edit only the target files listed in the request. Never edit English, another locale, tests, registries, docs, or other packages.
- Treat every English key and value as intentional. Translate from it without changing the English source files in any way.
- Add every missing key with a natural, concise translation suitable for application UI.
- Remove keys listed as extra and repair values listed under `placeholders` so their `{{tokens}}` exactly match English.
- Preserve existing translations unless they have a listed placeholder mismatch.
- Preserve meaning, intent, tone, capitalization, punctuation, whitespace, and formatting.
- Preserve technical terms and artifacts exactly: OpenCode, API names, identifiers, code, commands, flags, paths, URLs, versions, error messages, config keys, and placeholder tokens.
- Apply the locale glossary included in the request.
- `ui.sessionTurn.diffs.changed.one` and `ui.sessionTurn.diffs.changed.other` are complete count phrases. Preserve `{{count}}` and translate the whole phrase naturally rather than composing translated fragments.
- Use only read, glob, grep, and edit tools. Do not run commands or delegate work.
- Finish only when every requested key is synchronized and no other file has changed.
-168
View File
@@ -1,168 +0,0 @@
import { describe, expect, test } from "bun:test"
import {
findDrift,
glossaryFile,
modelVariants,
parseTranslationArgs,
runPool,
sessionIDFromEvents,
sessionModels,
targetFiles,
textFromEvents,
translationConfig,
unexpectedChanges,
} from "./translate-app"
describe("translate app", () => {
test("parses one locale with the public model defaults", () => {
expect(parseTranslationArgs(["fr"])).toEqual({
target: "fr",
concurrency: 1,
model: "opencode/gpt-5.5",
variant: "xhigh",
dryRun: false,
check: false,
help: false,
})
})
test("parses all locales with bounded concurrency overrides", () => {
expect(
parseTranslationArgs([
"all",
"--concurrency",
"7",
"--model",
"opencode/gpt-5.4",
"--variant",
"high",
"--dry-run",
]),
).toEqual({
target: "all",
concurrency: 7,
model: "opencode/gpt-5.4",
variant: "high",
dryRun: true,
check: false,
help: false,
})
})
test("rejects unsupported targets and invalid concurrency", () => {
expect(() => parseTranslationArgs(["en"])).toThrow("Unknown locale")
expect(() => parseTranslationArgs(["fr", "de"])).toThrow("one locale")
expect(() => parseTranslationArgs(["all", "--concurrency", "0"])).toThrow("positive integer")
})
test("parses fresh-process parity checks without requesting translation", () => {
expect(parseTranslationArgs(["fr", "--check"]).check).toBe(true)
})
test("limits each locale to its app surfaces", () => {
expect(targetFiles("fr")).toEqual([
"packages/app/src/i18n/fr.ts",
"packages/ui/src/i18n/fr.ts",
"packages/desktop/src/renderer/i18n/fr.ts",
])
expect(targetFiles("tr")).toEqual(["packages/app/src/i18n/tr.ts", "packages/ui/src/i18n/tr.ts"])
})
test("maps product locale codes to their glossaries", () => {
expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
})
test("finds key and placeholder drift", () => {
expect(
findDrift(
{ keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
{ keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
),
).toEqual({ missing: ["missing"], extra: ["extra"], placeholders: ["changed"] })
})
test("runs work with the requested maximum concurrency", async () => {
const active = new Set<number>()
const peaks: number[] = []
const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
active.add(item)
peaks.push(active.size)
await Bun.sleep(5)
active.delete(item)
return item * 2
})
expect(result).toEqual([2, 4, 6, 8, 10])
expect(Math.max(...peaks)).toBe(2)
})
test("reads the actual model and variant from the completed session", () => {
expect(sessionIDFromEvents('shared: https://example.test\n{"type":"step_start","sessionID":"ses_test"}\n')).toBe(
"ses_test",
)
expect(
sessionModels({
messages: [
{ info: { role: "user" } },
{
info: {
role: "assistant",
providerID: "opencode",
modelID: "gpt-5.5",
variant: "xhigh",
},
},
],
}),
).toEqual([{ model: "opencode/gpt-5.5", variant: "xhigh" }])
expect(
textFromEvents(
'shared: https://example.test\n{"type":"text","sessionID":"ses_test","part":{"text":"finished"}}\n',
),
).toBe("finished")
})
test("resolves variants from verbose model output", () => {
const output = `opencode/other
{"variants":{}}
opencode/gpt-5.5
{"variants":{"high":{"reasoningEffort":"high"},"xhigh":{"reasoningEffort":"xhigh"}}}
opencode/next
{"variants":{}}
`
expect(modelVariants(output, "opencode/gpt-5.5")).toEqual({
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "xhigh" },
})
})
test("disables side effects and scopes edits for the translation agent", () => {
const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
expect(config.share).toBe("disabled")
expect(config.formatter).toBe(false)
expect(config.lsp).toBe(false)
expect(config.agent["translate-app-fr"].permission.edit).toEqual({
"*": "deny",
"packages/app/src/i18n/fr.ts": "allow",
})
})
test("detects edits outside the locale targets", () => {
expect(
unexpectedChanges(
{ "script/translate-app.ts": "before" },
{
"script/translate-app.ts": "before",
"packages/app/src/i18n/fr.ts": "translated",
"packages/app/src/app.tsx": "unexpected",
},
["packages/app/src/i18n/fr.ts"],
),
).toEqual(["packages/app/src/app.tsx"])
expect(unexpectedChanges({ "already-dirty.ts": "before" }, { "already-dirty.ts": "after" }, [])).toEqual([
"already-dirty.ts",
])
})
})
-523
View File
@@ -1,523 +0,0 @@
#!/usr/bin/env bun
import path from "path"
import { parseArgs } from "util"
import { pathToFileURL } from "url"
const locales = [
"ar",
"br",
"bs",
"da",
"de",
"es",
"fr",
"ja",
"ko",
"no",
"pl",
"ru",
"uk",
"th",
"tr",
"zh",
"zht",
] as const
type Locale = (typeof locales)[number]
const languages = {
ar: "Arabic",
br: "Brazilian Portuguese",
bs: "Bosnian",
da: "Danish",
de: "German",
es: "Spanish",
fr: "French",
ja: "Japanese",
ko: "Korean",
no: "Norwegian Bokmal",
pl: "Polish",
ru: "Russian",
uk: "Ukrainian",
th: "Thai",
tr: "Turkish",
zh: "Simplified Chinese",
zht: "Traditional Chinese",
} as const satisfies Record<Locale, string>
type Dictionary = Record<string, string>
type Drift = ReturnType<typeof findDrift>
type Domain = { name: string; source: string; target: string; drift: Drift }
const desktopLocales = new Set<Locale>(locales.filter((locale) => locale !== "th" && locale !== "tr"))
const root = path.resolve(import.meta.dir, "..")
export function parseTranslationArgs(args: string[]) {
const parsed = parseArgs({
args,
options: {
concurrency: { type: "string", short: "c", default: "4" },
model: { type: "string", default: "opencode/gpt-5.5" },
variant: { type: "string", default: "xhigh" },
"dry-run": { type: "boolean", default: false },
check: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
allowPositionals: true,
})
const target = parsed.positionals[0] ?? "all"
const concurrency = Number(parsed.values.concurrency)
if (!parsed.values.help && parsed.positionals.length !== 1) throw new Error("Pass one locale or 'all'.")
if (target !== "all" && !isLocale(target)) throw new Error(`Unknown locale '${target}'.`)
if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("Concurrency must be a positive integer.")
return {
target,
concurrency: target === "all" ? concurrency : 1,
model: parsed.values.model,
variant: parsed.values.variant,
dryRun: parsed.values["dry-run"],
check: parsed.values.check,
help: parsed.values.help,
}
}
export function targetFiles(locale: Locale) {
return [
`packages/app/src/i18n/${locale}.ts`,
`packages/ui/src/i18n/${locale}.ts`,
...(desktopLocales.has(locale) ? [`packages/desktop/src/renderer/i18n/${locale}.ts`] : []),
]
}
export function glossaryFile(locale: Locale) {
if (locale === "zh") return ".opencode/glossary/zh-cn.md"
if (locale === "zht") return ".opencode/glossary/zh-tw.md"
return `.opencode/glossary/${locale}.md`
}
export function findDrift(source: Dictionary, target: Dictionary) {
return {
missing: Object.keys(source).filter((key) => !Object.hasOwn(target, key)),
extra: Object.keys(target).filter((key) => !Object.hasOwn(source, key)),
placeholders: Object.keys(source).filter(
(key) => Object.hasOwn(target, key) && tokens(source[key]).join() !== tokens(target[key]).join(),
),
}
}
export function sessionIDFromEvents(output: string) {
const match = output.match(/"sessionID"\s*:\s*"([^"]+)"/)
if (!match?.[1]) throw new Error("OpenCode did not report a session ID.")
return match[1]
}
export function sessionModels(value: unknown) {
if (!isRecord(value) || !Array.isArray(value.messages))
throw new Error("OpenCode returned an invalid session export.")
return value.messages.flatMap((message) => {
if (!isRecord(message) || !isRecord(message.info) || message.info.role !== "assistant") return []
if (typeof message.info.providerID !== "string" || typeof message.info.modelID !== "string") {
throw new Error("OpenCode session export omitted the assistant model.")
}
return [
{
model: `${message.info.providerID}/${message.info.modelID}`,
variant: typeof message.info.variant === "string" ? message.info.variant : undefined,
},
]
})
}
export function modelVariants(output: string, model: string) {
const normalized = output.replaceAll("\r\n", "\n")
const marker = `${model}\n`
const start = normalized.indexOf(marker)
if (start < 0) throw new Error(`Model not found: ${model}`)
const provider = model.split("/")[0]
const rest = normalized.slice(start + marker.length)
const next = rest.search(new RegExp(`^${escapeRegExp(provider)}/`, "m"))
const metadata: unknown = JSON.parse((next < 0 ? rest : rest.slice(0, next)).trim())
if (!isRecord(metadata) || !isRecord(metadata.variants)) throw new Error(`Model variants not found: ${model}`)
return metadata.variants
}
export function translationConfig(agent: string, model: string, targets: string[]) {
return {
$schema: "https://opencode.ai/config.json",
model,
default_agent: agent,
share: "disabled" as const,
formatter: false,
lsp: false,
snapshot: false,
agent: {
[agent]: {
mode: "primary" as const,
model,
permission: {
"*": "deny" as const,
read: "allow" as const,
glob: "allow" as const,
grep: "allow" as const,
edit: Object.fromEntries([["*", "deny"], ...targets.map((target) => [target, "allow"])]),
},
},
},
}
}
export function unexpectedChanges(before: Record<string, string>, after: Record<string, string>, allowed: string[]) {
const targets = new Set(allowed)
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
.filter((file) => !targets.has(file) && before[file] !== after[file])
.sort()
}
export async function runPool<T, R>(items: readonly T[], concurrency: number, task: (item: T) => Promise<R>) {
const results = new Map<number, R>()
const entries = items.entries()
const worker = async (): Promise<void> => {
const next = entries.next()
if (next.done) return
results.set(next.value[0], await task(next.value[1]))
await worker()
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker))
return Array.from(results.entries())
.sort((a, b) => a[0] - b[0])
.map((entry) => entry[1])
}
async function main() {
const options = parseTranslationArgs(Bun.argv.slice(2))
if (options.help) {
console.log(`
Usage: bun run translate:app -- <locale|all> [options]
Synchronizes product app translations with the English app, UI, and desktop dictionaries.
Options:
-c, --concurrency <count> Maximum parallel OpenCode runs for 'all' (default: 4)
--model <provider/id> OpenCode model (default: opencode/gpt-5.5)
--variant <name> Model variant (default: xhigh)
--dry-run Report drift without running OpenCode
--check Exit nonzero when translation drift exists
-h, --help Show this help message
Examples:
bun run translate:app -- fr
bun run translate:app -- all --concurrency 4
`)
return
}
const selected = options.target === "all" ? locales : [options.target]
const plans = await Promise.all(selected.map((locale) => inspect(locale)))
plans.forEach(report)
const pending = plans.filter((plan) => plan.domains.some((domain) => changed(domain.drift)))
if (options.check) {
if (pending.length) process.exitCode = 1
return
}
if (options.dryRun || pending.length === 0) return
const targets = pending.flatMap((plan) => plan.domains.map((domain) => domain.target))
const baseline = await worktreeSnapshot()
const variant = await resolveModelVariant(options.model, options.variant)
console.log(`Resolved ${options.model} (${options.variant}): ${JSON.stringify(variant)}`)
const template = await commandTemplate()
const results = await runPool(pending, options.concurrency, (plan) =>
translate(plan, template, options.model, options.variant).catch((error) => ({
locale: plan.locale,
code: 1,
stdout: "",
stderr: error instanceof Error ? error.message : String(error),
})),
)
results.forEach((result) => {
if (result.stdout) process.stdout.write(`\n[${result.locale}]\n${result.stdout}`)
if (result.stderr) process.stderr.write(`\n[${result.locale}]\n${result.stderr}`)
})
const failed = results.filter((result) => result.code !== 0)
const checks = await runPool(pending, options.concurrency, (plan) => check(plan.locale))
const incomplete = checks.filter((result) => result.code !== 0)
const escaped = unexpectedChanges(baseline, await worktreeSnapshot(), targets)
incomplete.forEach((result) => {
if (result.stdout) process.stderr.write(`\n[${result.locale} verification]\n${result.stdout}`)
if (result.stderr) process.stderr.write(`\n[${result.locale} verification]\n${result.stderr}`)
})
if (failed.length === 0 && incomplete.length === 0 && escaped.length === 0) {
console.log(`\nTranslated ${pending.map((plan) => plan.locale).join(", ")}.`)
return
}
if (failed.length) console.error(`\nOpenCode failed for: ${failed.map((result) => result.locale).join(", ")}`)
if (incomplete.length)
console.error(`Translation remains incomplete for: ${incomplete.map((plan) => plan.locale).join(", ")}`)
if (escaped.length) console.error(`Translation changed files outside its locale targets: ${escaped.join(", ")}`)
process.exitCode = 1
}
async function worktreeSnapshot() {
const groups = await Promise.all([
gitPaths(["diff", "--name-only", "-z", "HEAD"]),
gitPaths(["ls-files", "--others", "--exclude-standard", "-z"]),
])
const files = [...new Set(groups.flat())]
return Object.fromEntries(
await Promise.all(
files.map(async (file) => {
const target = Bun.file(path.join(root, file))
if (!(await target.exists())) return [file, "<missing>"] as const
const hash = new Bun.CryptoHasher("sha256")
hash.update(await target.arrayBuffer())
return [file, hash.digest("hex")] as const
}),
),
)
}
async function gitPaths(args: string[]) {
const proc = Bun.spawn(["git", ...args], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
if (result[2] !== 0) throw new Error(result[1] || `git ${args.join(" ")} failed`)
return result[0].split("\0").filter(Boolean)
}
async function check(locale: Locale) {
const proc = Bun.spawn([process.execPath, import.meta.path, locale, "--check"], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
return { locale, stdout: result[0], stderr: result[1], code: result[2] }
}
async function inspect(locale: Locale) {
const domains = await Promise.all(
targetFiles(locale).map(async (target) => {
const source = target.replace(`/${locale}.ts`, "/en.ts")
const dictionaries = await Promise.all([dictionary(source), dictionary(target)])
return {
name: target.includes("packages/app/") ? "app" : target.includes("packages/ui/") ? "ui" : "desktop",
source,
target,
drift: findDrift(dictionaries[0], dictionaries[1]),
}
}),
)
return { locale, language: languages[locale], domains }
}
async function dictionary(file: string) {
const module: unknown = await import(pathToFileURL(path.join(root, file)).href)
if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) {
throw new Error(`Invalid translation dictionary: ${file}`)
}
return module.dict
}
async function commandTemplate() {
return (await Bun.file(path.join(root, "script/translate-app.md")).text()).trim()
}
async function translate(
plan: { locale: Locale; language: string; domains: Domain[] },
template: string,
model: string,
variant: string,
) {
const glossary = glossaryFile(plan.locale)
const glossaryContent = (await Bun.file(path.join(root, glossary)).exists())
? await Bun.file(path.join(root, glossary)).text()
: undefined
const prompt = template.replaceAll("$1", plan.locale).replaceAll(
"$ARGUMENTS",
JSON.stringify(
{
locale: plan.locale,
language: plan.language,
glossary: glossaryContent ? { file: glossary, content: glossaryContent } : undefined,
domains: plan.domains.map((domain) => ({
source: domain.source,
target: domain.target,
...domain.drift,
})),
},
null,
2,
),
)
const agent = `translate-app-${plan.locale}-${process.pid}`
const env = isolatedEnvironment()
env.KILO_DISABLE_PROJECT_CONFIG = "1"
env.KILO_CONFIG_CONTENT = JSON.stringify(
translationConfig(
agent,
model,
plan.domains.map((domain) => domain.target),
),
)
const proc = Bun.spawn(
[
"opencode",
"--pure",
"run",
"--dir",
root,
"--agent",
agent,
"--model",
model,
"--variant",
variant,
"--title",
`Translate app ${plan.locale}`,
"--format",
"json",
],
{
cwd: root,
env,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
},
)
const stdout = new Response(proc.stdout).text()
const stderr = new Response(proc.stderr).text()
await proc.stdin.write(prompt)
await proc.stdin.end()
const result = await Promise.all([stdout, stderr, proc.exited])
if (result[2] !== 0) return { locale: plan.locale, stdout: result[0], stderr: result[1], code: result[2] }
const sessionID = sessionIDFromEvents(result[0])
const exported = Bun.spawn(["opencode", "--pure", "export", sessionID, "--sanitize"], {
cwd: root,
env,
stdout: "pipe",
stderr: "pipe",
})
const exportResult = await Promise.all([
new Response(exported.stdout).text(),
new Response(exported.stderr).text(),
exported.exited,
])
if (exportResult[2] !== 0) {
return { locale: plan.locale, stdout: textFromEvents(result[0]), stderr: exportResult[1], code: exportResult[2] }
}
const session: unknown = JSON.parse(exportResult[0])
const observed = sessionModels(session)
const mismatch = observed.length === 0 || observed.some((item) => item.model !== model || item.variant !== variant)
const actual = Array.from(new Set(observed.map((item) => `${item.model} (${item.variant ?? "default"})`))).join(", ")
return {
locale: plan.locale,
stdout: `${textFromEvents(result[0])}\nVerified session model: ${actual}\n`,
stderr: mismatch
? `Requested ${model} (${variant}), but session used ${actual || "no assistant model"}.\n`
: result[1],
code: mismatch ? 1 : 0,
}
}
function report(plan: { locale: Locale; domains: Domain[] }) {
const details = plan.domains
.map(
(domain) =>
`${domain.name}: ${domain.drift.missing.length} missing, ${domain.drift.extra.length} extra, ${domain.drift.placeholders.length} placeholder mismatches`,
)
.join("; ")
console.log(`[${plan.locale}] ${details}`)
}
function changed(drift: Drift) {
return drift.missing.length > 0 || drift.extra.length > 0 || drift.placeholders.length > 0
}
function isLocale(value: string): value is Locale {
return Object.hasOwn(languages, value)
}
function isDictionary(value: unknown): value is Dictionary {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false
return Object.values(value).every((item) => typeof item === "string")
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
async function resolveModelVariant(model: string, variant: string) {
const provider = model.split("/")[0]
if (!provider || !model.includes("/")) throw new Error(`Model must use provider/model syntax: ${model}`)
const env = isolatedEnvironment()
env.KILO_DISABLE_PROJECT_CONFIG = "1"
const proc = Bun.spawn(["opencode", "--pure", "models", provider, "--verbose"], {
cwd: root,
env,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
if (result[2] !== 0) throw new Error(result[1] || `Unable to resolve model: ${model}`)
const variants = modelVariants(result[0], model)
if (!Object.hasOwn(variants, variant)) throw new Error(`Variant '${variant}' is not configured for ${model}.`)
return variants[variant]
}
function isolatedEnvironment() {
const env = { ...process.env }
delete env.KILO_CONFIG
delete env.KILO_CONFIG_DIR
delete env.KILO_CONFIG_CONTENT
delete env.KILO_PERMISSION
delete env.KILO_AUTO_SHARE
return env
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export function textFromEvents(output: string) {
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.startsWith("{") && line.endsWith("}"))
.flatMap((line) => {
const event: unknown = JSON.parse(line)
if (!isRecord(event) || event.type !== "text" || !isRecord(event.part) || typeof event.part.text !== "string") {
return []
}
return [event.part.text.trim()]
})
.filter(Boolean)
.join("\n")
}
function tokens(value: string) {
return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1] ?? "").sort()
}
if (import.meta.main) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}
@@ -1,5 +1,6 @@
import { expect, test } from "bun:test"
import { shouldSkip } from "./skip-files"
import { defaultConfig } from "../utils/config"
test("matches hosted package glob paths", () => {
expect(shouldSkip("packages/web/package.json", ["packages/web/**"])).toBe(true)
@@ -21,6 +22,16 @@ test("matches upstream stats package glob paths", () => {
expect(shouldSkip("packages/stats/core/src/index.ts", ["packages/stats/**"])).toBe(true)
})
test("matches upstream-only translation automation", () => {
expect(shouldSkip("script/translate-app.ts", defaultConfig.skipFiles)).toBe(true)
expect(shouldSkip("script/translate-app.test.ts", defaultConfig.skipFiles)).toBe(true)
expect(shouldSkip("script/translate-app.md", defaultConfig.skipFiles)).toBe(true)
})
test("transforms the Muse Spark prompt for Kilo branding", () => {
expect(defaultConfig.takeTheirsAndTransform).toContain("packages/opencode/src/session/prompt/meta.txt")
})
test("matches removed vscode sdk glob paths", () => {
expect(shouldSkip("sdks/vscode/package.json", ["sdks/vscode/**"])).toBe(true)
expect(shouldSkip("sdks/vscode/src/extension.ts", ["sdks/vscode/**"])).toBe(true)
@@ -0,0 +1,26 @@
import { expect, test } from "bun:test"
import { transformI18nContent } from "./transform-i18n"
import { translate } from "../utils/upstream"
test("marks transformed Kilo branding and preserves legacy config names", () => {
const result = transformI18nContent(
' "product": "OpenCode",\n "docs": "https://opencode.ai/docs",\n "legacy": ".opencode/opencode.json",',
false,
true,
)
expect(result.result).toContain('"product": "Kilo", // kilocode_change')
expect(result.result).toContain('"docs": "https://kilo.ai/docs", // kilocode_change')
expect(result.result).toContain('"legacy": ".opencode/opencode.json",')
expect(result.replacements).toBe(2)
})
test("does not inject source markers into non-locale content", () => {
const result = transformI18nContent("OpenCode uses opencode serve")
expect(result.result).toBe("Kilo uses kilo serve")
})
test("generic upstream translation keeps prompt text marker-free", async () => {
const result = await translate("packages/opencode/src/session/prompt/meta.txt", "OpenCode uses opencode serve")
expect(result).toBe("Kilo uses kilo serve")
expect(result).not.toContain("kilocode_change")
})
+4 -2
View File
@@ -148,6 +148,7 @@ function shouldPreserveLine(line: string): boolean {
export function transformI18nContent(
content: string,
verbose = false,
markers = false,
): { result: string; replacements: number; preserved: number } {
const lines = content.split("\n")
const transformedLines: string[] = []
@@ -200,7 +201,8 @@ export function transformI18nContent(
}
}
transformedLines.push(transformedLine)
// Kilo branding produced by this transform remains a Kilo-owned delta in shared locale files.
transformedLines.push(markers && lineReplacements > 0 ? `${transformedLine} // kilocode_change` : transformedLine)
totalReplacements += lineReplacements
}
@@ -221,7 +223,7 @@ export async function transformI18nFile(
const file = Bun.file(filePath)
const content = await file.text()
const { result, replacements, preserved } = transformI18nContent(content, options.verbose)
const { result, replacements, preserved } = transformI18nContent(content, options.verbose, true)
if (replacements > 0 && !options.dryRun) {
await Bun.write(filePath, result)
@@ -10,6 +10,7 @@ import {
mergeWithNewestVersions,
prunePatchedDependencies,
selectBunPackageManager,
transformDependencies,
} from "./transform-package-json"
test("fixScripts preserves Kilo-only root scripts from base", () => {
@@ -20,6 +21,7 @@ test("fixScripts preserves Kilo-only root scripts from base", () => {
extension: "bun --cwd packages/kilo-vscode script/launch.ts",
"extension:isolated": "bun --cwd packages/kilo-vscode script/launch.ts --isolated",
"extension:isolated:clean": "bun --cwd packages/kilo-vscode script/launch.ts --isolated --clean",
"test:script:ci": "bun test ./script",
},
}
const pkg: Record<string, unknown> = {
@@ -33,6 +35,7 @@ test("fixScripts preserves Kilo-only root scripts from base", () => {
expect(scripts.extension).toBe(ours.scripts.extension)
expect(scripts["extension:isolated"]).toBe(ours.scripts["extension:isolated"])
expect(scripts["extension:isolated:clean"]).toBe(ours.scripts["extension:isolated:clean"])
expect(scripts["test:script:ci"]).toBe(ours.scripts["test:script:ci"])
expect(changes.some((c) => c.includes("postinstall"))).toBe(true)
expect(changes.some((c) => c.includes("dev-setup"))).toBe(true)
})
@@ -69,6 +72,7 @@ test("fixScripts removes upstream-only dead scripts from root", () => {
"dev:desktop": "bun --cwd packages/desktop-electron dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"translate:app": "bun run script/translate-app.ts",
},
}
const changes: string[] = []
@@ -78,7 +82,8 @@ test("fixScripts removes upstream-only dead scripts from root", () => {
expect(scripts["dev:desktop"]).toBeUndefined()
expect(scripts["dev:web"]).toBeUndefined()
expect(scripts["dev:console"]).toBeUndefined()
expect(changes.length).toBe(3)
expect(scripts["translate:app"]).toBeUndefined()
expect(changes.length).toBe(4)
})
test("fixScripts preserves opencode test scripts", () => {
@@ -95,7 +100,12 @@ test("fixScripts preserves dev:local and shared-package test:ci scripts", () =>
const junit = "mkdir -p .artifacts/unit && bun test --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
const root: Record<string, unknown> = { scripts: { dev: "bun dev" } }
const changes: string[] = []
fixScripts(root, "package.json", { scripts: { "dev:local": "bun run packages/opencode/script/dev-local.ts" } }, changes)
fixScripts(
root,
"package.json",
{ scripts: { "dev:local": "bun run packages/opencode/script/dev-local.ts" } },
changes,
)
expect((root.scripts as Record<string, string>)["dev:local"]).toBe("bun run packages/opencode/script/dev-local.ts")
for (const path of [
@@ -154,12 +164,13 @@ test("fixScripts leaves unknown packages untouched", () => {
expect(changes.length).toBe(0)
})
test("fixCatalog removes upstream-only desktop sentry entries", () => {
test("fixCatalog removes unsupported upstream entries", () => {
const pkg: Record<string, unknown> = {
workspaces: {
catalog: {
"@sentry/solid": "10.36.0",
"@sentry/vite-plugin": "4.6.0",
"opentui-spinner": "0.0.7",
"solid-js": "1.9.12",
},
},
@@ -169,8 +180,9 @@ test("fixCatalog removes upstream-only desktop sentry entries", () => {
const cat = (pkg.workspaces as { catalog: Record<string, string> }).catalog
expect(cat["@sentry/solid"]).toBeUndefined()
expect(cat["@sentry/vite-plugin"]).toBeUndefined()
expect(cat["opentui-spinner"]).toBeUndefined()
expect(cat["solid-js"]).toBe("1.9.12")
expect(changes.length).toBe(2)
expect(changes.length).toBe(3)
})
test("fixCatalog is a no-op when catalog is absent", () => {
@@ -180,6 +192,12 @@ test("fixCatalog is a no-op when catalog is absent", () => {
expect(changes.length).toBe(0)
})
test("transformDependencies removes the incompatible spinner runtime", () => {
const result = transformDependencies({ "opentui-spinner": "catalog:", "solid-js": "catalog:" })
expect(result.result).toEqual({ "solid-js": "catalog:" })
expect(result.changes).toEqual(["opentui-spinner: removed (incompatible OpenTUI runtime)"])
})
test("fixMetadata preserves opencode publish metadata from base", () => {
const ours = { keywords: ["cli", "kilo", "opencode"], private: false }
const pkg: Record<string, unknown> = { keywords: ["opencode"], private: true }
@@ -291,6 +291,7 @@ const PRESERVE_SCRIPTS: Record<string, string[]> = {
"dev-setup",
"postinstall",
"dev:local",
"test:script:ci",
],
"packages/opencode/package.json": ["test", "test:ci"],
// Upstream-shared packages where Kilo adds a JUnit test:ci script for CI.
@@ -320,16 +321,18 @@ const DELETE_UPSTREAM_TRUSTED_DEPS: Record<string, string[]> = {
// Kilo doesn't ship (desktop-electron, console/app, app) and would otherwise
// reappear on every merge.
const DELETE_UPSTREAM_SCRIPTS: Record<string, string[]> = {
"package.json": ["dev:desktop", "dev:web", "dev:console"],
"package.json": ["dev:desktop", "dev:web", "dev:console", "translate:app"],
}
// Upstream-only catalog entries to delete per package.json. These are pulled
// in by upstream features (e.g. desktop Sentry integration) that Kilo doesn't
// ship, so they add install weight with zero consumers in our tree.
const DELETE_UPSTREAM_CATALOG: Record<string, string[]> = {
"package.json": ["@sentry/solid", "@sentry/vite-plugin"],
"package.json": ["@sentry/solid", "@sentry/vite-plugin", "opentui-spinner"],
}
const DELETE_UPSTREAM_DEPENDENCIES = new Set(["opentui-spinner"])
/**
* Re-apply Kilo-specific scripts on top of the upstream-shaped scripts block,
* and prune upstream-only scripts that target packages Kilo doesn't ship.
@@ -448,7 +451,7 @@ export function isPackageJson(file: string): boolean {
/**
* Transform dependencies in package.json
*/
function transformDependencies(deps: Record<string, string> | undefined): {
export function transformDependencies(deps: Record<string, string> | undefined): {
result: Record<string, string>
changes: string[]
} {
@@ -458,6 +461,10 @@ function transformDependencies(deps: Record<string, string> | undefined): {
const changes: string[] = []
for (const [name, version] of Object.entries(deps)) {
if (DELETE_UPSTREAM_DEPENDENCIES.has(name)) {
changes.push(`${name}: removed (incompatible OpenTUI runtime)`)
continue
}
const newName = PACKAGE_NAME_MAP[name]
if (newName) {
result[newName] = version
+6
View File
@@ -137,6 +137,10 @@ export const defaultConfig: MergeConfig = {
"packages/opencode/bin/opencode",
// Removed prompt file
"packages/opencode/src/session/prompt/build-switch.txt",
// Upstream app translation automation targets products and binaries Kilo does not ship
"script/translate-app.ts",
"script/translate-app.test.ts",
"script/translate-app.md",
// Vouch files (Kilo doesn't use Vouch).
// Upstream currently ships VOUCHED.td (typo extension). The glob covers both
// the current .td file and any future .md rename without another merge breaking.
@@ -174,6 +178,8 @@ export const defaultConfig: MergeConfig = {
// Files that should take upstream version and apply Kilo branding transforms
// These are files with only branding differences, no logic changes
takeTheirsAndTransform: [
// Model-facing prompts that need Kilo product identity and documentation links
"packages/opencode/src/session/prompt/meta.txt",
// UI components
"packages/ui/src/components/**/*.tsx",
"packages/ui/src/context/**/*.tsx",
+2 -2
View File
@@ -4,7 +4,7 @@ import { $ } from "bun"
import path from "node:path"
import { applyPackageNameTransforms } from "../transforms/package-names"
import { applyExtensionTransforms } from "../transforms/transform-extensions"
import { transformI18nContent } from "../transforms/transform-i18n"
import { isI18nFile, transformI18nContent } from "../transforms/transform-i18n"
import { applyScriptTransforms } from "../transforms/transform-scripts"
import { applyBrandingTransforms } from "../transforms/transform-take-theirs"
import { applyWebTransforms } from "../transforms/transform-web"
@@ -191,7 +191,7 @@ export async function translate(file: string, text: string) {
const names = applyPackageNameTransforms(text).result
const script = applyScriptTransforms(names).result
const branded = applyBrandingTransforms(script).result
const i18n = transformI18nContent(branded).result
const i18n = transformI18nContent(branded, false, isI18nFile(file)).result
const ext = applyExtensionTransforms(i18n, file).result
const web = applyWebTransforms(ext).result