Merge pull request #12987 from Kilo-Org/optimize-cli-extension-compile-times

perf(build): optimize cli and extension compile and test times
This commit is contained in:
Marius
2026-08-07 14:27:28 +02:00
committed by GitHub
12 changed files with 449 additions and 197 deletions
+12 -21
View File
@@ -470,6 +470,8 @@
},
"devDependencies": {
"@axe-core/playwright": "4.11.3",
"@babel/core": "^7.28.4",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "10.2.10",
"@storybook/addon-docs": "10.2.10",
@@ -481,6 +483,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.27.2",
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
@@ -984,23 +987,23 @@
},
},
"trustedDependencies": [
"esbuild",
"protobufjs",
"web-tree-sitter",
"esbuild",
"tree-sitter-bash",
"protobufjs",
],
"patchedDependencies": {
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
},
"overrides": {
"@effect/platform-node-shared": "4.0.0-beta.74",
@@ -5443,10 +5446,6 @@
"@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
"@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
@@ -6153,14 +6152,6 @@
"@smithy/util-stream/@smithy/core/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-json/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+1 -1
View File
@@ -3,4 +3,4 @@
* Only add icons here that are actually used — esbuild/Vite will
* tree-shake unused exports but explicit re-exports keep the API small.
*/
export { WandSparkles } from "lucide-solid"
export { default as WandSparkles } from "lucide-solid/icons/wand-sparkles"
+172 -114
View File
@@ -1,10 +1,96 @@
const esbuild = require("esbuild")
const path = require("path")
const { solidPlugin } = require("esbuild-plugin-solid")
const fs = require("fs")
const crypto = require("crypto")
const core = require("@babel/core")
const solid = require("babel-preset-solid")
const ts = require("@babel/preset-typescript")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Cache transformed Solid JSX files in memory and on disk to avoid
* re-parsing and re-transforming unchanged files across builds and webviews.
*/
const solidCacheDir = path.join(__dirname, "node_modules", ".cache", "esbuild-solid")
const solidMemCache = new Map()
const buildScriptHash = crypto
.createHash("sha256")
.update(fs.readFileSync(__filename, "utf8"))
.update(require("babel-preset-solid/package.json").version || "")
.update(require("@babel/preset-typescript/package.json").version || "")
.digest("hex")
.slice(0, 8)
if (!fs.existsSync(solidCacheDir)) {
try {
fs.mkdirSync(solidCacheDir, { recursive: true })
} catch (err) {
console.warn("[esbuild] could not create solid cache directory", err)
}
}
const cachedSolidPlugin = {
name: "esbuild:solid-cached",
setup(build) {
build.onLoad({ filter: /\.(t|j)sx$/ }, async (args) => {
let mtime = 0
let size = 0
try {
const st = fs.statSync(args.path)
mtime = st.mtimeMs
size = st.size
} catch (err) {
console.warn("[esbuild] could not stat source file for cache key", args.path, err)
}
const cacheKey = `${args.path}:${mtime}:${size}:${buildScriptHash}`
const memHit = solidMemCache.get(cacheKey)
if (memHit) return { contents: memHit, loader: "js" }
const diskKey = crypto.createHash("sha256").update(cacheKey).digest("hex") + ".js"
const diskPath = path.join(solidCacheDir, diskKey)
if (fs.existsSync(diskPath)) {
try {
const diskCode = fs.readFileSync(diskPath, "utf8")
solidMemCache.set(cacheKey, diskCode)
return { contents: diskCode, loader: "js" }
} catch (err) {
console.warn("[esbuild] cache read failed, rebuilding", diskPath, err)
}
}
const source = fs.readFileSync(args.path, "utf8")
const { name, ext } = path.parse(args.path)
const filename = name + ext
const result = await core.transformAsync(source, {
presets: [
[solid, {}],
[ts, {}],
],
filename,
sourceMaps: "inline",
})
if (result?.code === void 0 || result.code === null) {
throw new Error("No result was provided from Babel")
}
if (solidMemCache.size > 2000) solidMemCache.clear()
solidMemCache.set(cacheKey, result.code)
try {
fs.writeFileSync(diskPath, result.code)
} catch (err) {
console.warn("[esbuild] cache write failed", diskPath, err)
}
return { contents: result.code, loader: "js" }
})
},
}
/**
* Force all solid-js imports (from kilo-ui and the webview) to resolve to
* the **same** copy so SolidJS contexts are shared across packages.
@@ -123,7 +209,7 @@ const svgSpritePlugin = {
name: "svg-sprite-inline",
setup(build) {
build.onLoad({ filter: /sprite\.svg$/ }, (args) => {
const content = require("fs").readFileSync(args.path, "utf8")
const content = fs.readFileSync(args.path, "utf8")
return {
contents: `
const svg = ${JSON.stringify(content)};
@@ -160,69 +246,8 @@ const cssPackageResolvePlugin = {
},
}
function createBrowserWebviewContext(entryPoint, outfile) {
return esbuild.context({
entryPoints: [entryPoint],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile,
logLevel: "silent",
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file",
},
plugins: [
solidDedupePlugin,
pierreWorkerAliasPlugin,
markdownWorkerUrlPlugin,
svgSpritePlugin,
cssPackageResolvePlugin,
solidPlugin(),
esbuildProblemMatcherPlugin,
],
})
}
// Bundle Pierre's Shiki worker into a single self-contained asset that the
// webviews load off the main thread for syntax highlighting.
function createShikiWorkerContext() {
return esbuild.context({
entryPoints: ["kilo-shiki-worker"],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/shiki-worker.js",
logLevel: "silent",
plugins: [shikiWorkerEntryPlugin, esbuildProblemMatcherPlugin],
})
}
function createMarkdownShikiWorkerContext() {
return esbuild.context({
entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")],
bundle: true,
format: "esm",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/markdown-shiki-worker.js",
logLevel: "silent",
plugins: [esbuildProblemMatcherPlugin],
})
}
async function main() {
// Build extension
const extensionCtx = await esbuild.context({
function getExtensionConfig() {
return {
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
@@ -239,68 +264,101 @@ async function main() {
outfile: "dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [esbuildProblemMatcherPlugin],
})
plugins: watch ? [esbuildProblemMatcherPlugin] : [],
}
}
// Build Agent Manager webview (SolidJS, shares components with sidebar)
const agentManagerCtx = await createBrowserWebviewContext(
"webview-ui/agent-manager/index.tsx",
"dist/agent-manager.js",
)
function getWebviewsConfig() {
return {
entryPoints: {
"agent-manager": "webview-ui/agent-manager/index.tsx",
kiloclaw: "webview-ui/kiloclaw/index.tsx",
marketplace: "webview-ui/marketplace/index.tsx",
"diff-viewer": "webview-ui/diff-viewer/index.tsx",
"diff-virtual": "webview-ui/diff-virtual/index.tsx",
webview: "webview-ui/src/index.tsx",
},
outdir: "dist",
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
logLevel: "silent",
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file",
},
plugins: [
solidDedupePlugin,
pierreWorkerAliasPlugin,
markdownWorkerUrlPlugin,
svgSpritePlugin,
cssPackageResolvePlugin,
cachedSolidPlugin,
...(watch ? [esbuildProblemMatcherPlugin] : []),
],
}
}
// Build KiloClaw webview (SolidJS, standalone chat panel)
const kiloClawCtx = await createBrowserWebviewContext("webview-ui/kiloclaw/index.tsx", "dist/kiloclaw.js")
function getShikiWorkerConfig() {
return {
entryPoints: ["kilo-shiki-worker"],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/shiki-worker.js",
logLevel: "silent",
plugins: [shikiWorkerEntryPlugin, ...(watch ? [esbuildProblemMatcherPlugin] : [])],
}
}
// Build Marketplace webview (SolidJS, standalone catalog panel)
const marketplaceCtx = await createBrowserWebviewContext("webview-ui/marketplace/index.tsx", "dist/marketplace.js")
function getMarkdownShikiWorkerConfig() {
return {
entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")],
bundle: true,
format: "esm",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/markdown-shiki-worker.js",
logLevel: "silent",
plugins: watch ? [esbuildProblemMatcherPlugin] : [],
}
}
// Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components)
const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js")
// Build Diff Virtual webview (lightweight single-file diff for permission approval)
const diffVirtualCtx = await createBrowserWebviewContext("webview-ui/diff-virtual/index.tsx", "dist/diff-virtual.js")
// Build webview
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
// Build the shared Shiki highlighting worker asset
const shikiWorkerCtx = await createShikiWorkerContext()
const markdownShikiWorkerCtx = await createMarkdownShikiWorkerContext()
async function main() {
const extensionConfig = getExtensionConfig()
const webviewsConfig = getWebviewsConfig()
const shikiWorkerConfig = getShikiWorkerConfig()
const markdownShikiWorkerConfig = getMarkdownShikiWorkerConfig()
if (watch) {
const [extensionCtx, webviewsCtx, shikiWorkerCtx, markdownShikiWorkerCtx] = await Promise.all([
esbuild.context(extensionConfig),
esbuild.context(webviewsConfig),
esbuild.context(shikiWorkerConfig),
esbuild.context(markdownShikiWorkerConfig),
])
await Promise.all([
extensionCtx.watch(),
webviewCtx.watch(),
agentManagerCtx.watch(),
diffViewerCtx.watch(),
diffVirtualCtx.watch(),
kiloClawCtx.watch(),
marketplaceCtx.watch(),
webviewsCtx.watch(),
shikiWorkerCtx.watch(),
markdownShikiWorkerCtx.watch(),
])
} else {
await Promise.all([
extensionCtx.rebuild(),
webviewCtx.rebuild(),
agentManagerCtx.rebuild(),
kiloClawCtx.rebuild(),
marketplaceCtx.rebuild(),
diffViewerCtx.rebuild(),
diffVirtualCtx.rebuild(),
shikiWorkerCtx.rebuild(),
markdownShikiWorkerCtx.rebuild(),
])
await Promise.all([
extensionCtx.dispose(),
webviewCtx.dispose(),
agentManagerCtx.dispose(),
diffViewerCtx.dispose(),
diffVirtualCtx.dispose(),
kiloClawCtx.dispose(),
marketplaceCtx.dispose(),
shikiWorkerCtx.dispose(),
markdownShikiWorkerCtx.dispose(),
esbuild.build(extensionConfig),
esbuild.build(webviewsConfig),
esbuild.build(shikiWorkerConfig),
esbuild.build(markdownShikiWorkerConfig),
])
}
}
+6 -3
View File
@@ -1226,12 +1226,12 @@
"prepare:cli-binary": "bun script/local-bin.ts",
"prepare:sdk": "bun script/prepare-sdk.ts",
"build:launch": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production",
"compile": "bun run prepare:cli-binary -- --force && bun run rebuild-sdk && bun run build:check",
"watch": "bun run rebuild-sdk && bun run --parallel watch:esbuild watch:tsc",
"compile": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check",
"watch": "bun run prepare:sdk && bun run --parallel watch:esbuild watch:tsc",
"watch:esbuild": "bun run prepare:cli-binary && node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch:cli": "bun script/watch-cli.ts",
"package": "bun run prepare:cli-binary && bun run rebuild-sdk && bun run build:check:production",
"package": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production",
"build:check": "bun run --parallel check-types check-types:webview lint bundle",
"build:check:production": "bun run --parallel check-types check-types:webview lint bundle:production",
"bundle": "bun esbuild.js",
@@ -1263,6 +1263,8 @@
},
"devDependencies": {
"@axe-core/playwright": "4.11.3",
"@babel/core": "^7.28.4",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "10.2.10",
"@storybook/addon-docs": "10.2.10",
@@ -1274,6 +1276,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.27.2",
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
+7 -5
View File
@@ -252,13 +252,15 @@ async function ensureBuiltBinary(): Promise<string> {
)
}
// Use the repository-pinned Bun version throughout. Newer canaries can fail compilation
// and must not cause packaged snapshots to fall back to the browser-mode source wrapper.
const pkg = await Bun.file(join(repoDir, "package.json")).json()
const bun = String(pkg.packageManager)
log("Installing dependencies in opencode package...")
await $`bunx ${bun} install --frozen-lockfile`.cwd(opencodeDir)
await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir)
log("Building CLI binary...")
try {
await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir)
} catch (err) {
log(`Pinned bunx build failed (${err}), running via active bun runtime...`)
await $`bun run script/build.ts --single --skip-install`.cwd(opencodeDir)
}
const built = await findKiloBinaryInOpencodeDist()
if (!built) {
@@ -0,0 +1,62 @@
import { describe, it, expect } from "bun:test"
import fs from "node:fs"
import path from "node:path"
import { builtinModules } from "node:module"
const ROOT = path.resolve(import.meta.dir, "../..")
const PKG_FILE = path.join(ROOT, "package.json")
const ESBUILD_FILE = path.join(ROOT, "esbuild.js")
const BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)])
function extractPackageName(specifier: string): string | null {
if (specifier.startsWith(".") || specifier.startsWith("/")) return null
if (BUILTINS.has(specifier)) return null
if (specifier.startsWith("node:")) return null
if (specifier.startsWith("@")) {
const parts = specifier.split("/")
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier
}
return specifier.split("/")[0]
}
function findImportsAndRequires(content: string): string[] {
const specifiers = new Set<string>()
const requireRegex = /require\(["']([^"']+)["']\)/g
const importRegex = /(?:import|from)\s+["']([^"']+)["']/g
for (const match of content.matchAll(requireRegex)) {
specifiers.add(match[1])
}
for (const match of content.matchAll(importRegex)) {
specifiers.add(match[1])
}
return Array.from(specifiers)
}
describe("Build Script Dependency Declarations", () => {
it("esbuild.js must declare all imported/required packages in package.json", () => {
const pkg = JSON.parse(fs.readFileSync(PKG_FILE, "utf8"))
const declared = new Set([
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
"vscode",
])
const esbuildContent = fs.readFileSync(ESBUILD_FILE, "utf8")
const specifiers = findImportsAndRequires(esbuildContent)
const undeclared: string[] = []
for (const spec of specifiers) {
const pkgName = extractPackageName(spec)
if (pkgName && !declared.has(pkgName)) {
undeclared.push(`${spec} (package: ${pkgName})`)
}
}
expect(undeclared).toEqual([])
})
})
@@ -24,17 +24,24 @@ afterEach(async () => {
)
})
function gitExec(args: string[]) {
const res = Bun.spawnSync(args, { stdout: "ignore", stderr: "pipe" })
if (res.exitCode !== 0) {
const err = Buffer.from(res.stderr).toString("utf8")
throw new Error(`git command failed (${args.join(" ")}): ${err}`)
}
}
/** Create a temp git repo with an initial commit (required for worktrees). */
async function createTempRepo(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-"))
tempDirs.push(dir)
const git = simpleGit(dir)
await git.init()
await git.addConfig("user.email", "test@test.com")
await git.addConfig("user.name", "Test")
gitExec(["git", "init", "-b", "main", dir])
gitExec(["git", "-C", dir, "config", "user.email", "test@test.com"])
gitExec(["git", "-C", dir, "config", "user.name", "Test"])
await fs.writeFile(path.join(dir, "README.md"), "init")
await git.add(".")
await git.commit("initial commit")
gitExec(["git", "-C", dir, "add", "."])
gitExec(["git", "-C", dir, "commit", "-m", "initial commit"])
return dir
}
@@ -51,31 +58,18 @@ async function changedFiles(cwd: string): Promise<string[]> {
/** Create a temp repo with a bare origin remote so origin/<branch> refs exist. */
async function createTempRepoWithOrigin(): Promise<{ bare: string; clone: string }> {
// Use a non-bare seed repo to control the initial branch name, then clone bare
const seed = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-seed-"))
tempDirs.push(seed)
const seedGit = simpleGit(seed)
await seedGit.init()
await seedGit.addConfig("user.email", "test@test.com")
await seedGit.addConfig("user.name", "Test")
await fs.writeFile(path.join(seed, "README.md"), "init")
await seedGit.add(".")
await seedGit.commit("initial commit")
// Ensure branch is named "main" regardless of system default
const seedBranch = (await seedGit.revparse(["--abbrev-ref", "HEAD"])).trim()
if (seedBranch !== "main") await seedGit.raw(["branch", "-m", seedBranch, "main"])
// Clone to bare, then clone again as working copy
const bare = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-bare-"))
tempDirs.push(bare)
await simpleGit().clone(seed, bare, ["--bare"])
const clone = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-clone-"))
tempDirs.push(clone)
await simpleGit().clone(bare, clone)
const cloneGit = simpleGit(clone)
await cloneGit.addConfig("user.email", "test@test.com")
await cloneGit.addConfig("user.name", "Test")
tempDirs.push(bare, clone)
gitExec(["git", "init", "--bare", "-b", "main", bare])
gitExec(["git", "clone", bare, clone])
gitExec(["git", "-C", clone, "config", "user.email", "test@test.com"])
gitExec(["git", "-C", clone, "config", "user.name", "Test"])
await fs.writeFile(path.join(clone, "README.md"), "init")
gitExec(["git", "-C", clone, "add", "."])
gitExec(["git", "-C", clone, "commit", "-m", "initial commit"])
gitExec(["git", "-C", clone, "push", "-u", "origin", "main"])
return { bare, clone }
}
+44 -4
View File
@@ -51,9 +51,46 @@ async function copyTreeSitterWasms(outputDir: string) {
console.log(`copied ${languageWasmFiles.length + 1} tree-sitter wasm files to ${targetDir}`)
}
// kilocode_change start
async function isKiloConsoleUpToDate(app: string, out: string) {
const indexHtml = path.join(out, "index.html")
if (!fs.existsSync(indexHtml)) return false
const outStat = await fs.promises.stat(indexHtml)
const inputs = [
path.join(app, "src"),
path.join(app, "package.json"),
path.join(app, "vite.config.ts"),
path.join(app, "index.html"),
path.resolve(dir, "../kilo-web-ui/src"),
path.resolve(dir, "../kilo-indexing/src"),
path.resolve(dir, "../kilo-ui/src"),
path.resolve(dir, "../ui/src"),
path.resolve(dir, "../sdk/js/src"),
path.resolve(dir, "../../bun.lock"),
]
for (const p of inputs) {
if (!fs.existsSync(p)) continue
const st = await fs.promises.stat(p)
if (st.isDirectory()) {
const glob = new Bun.Glob("**/*")
for await (const file of glob.scan({ cwd: p })) {
const fileStat = await fs.promises.stat(path.join(p, file))
if (fileStat.mtimeMs > outStat.mtimeMs) return false
}
} else if (st.mtimeMs > outStat.mtimeMs) {
return false
}
}
return true
}
async function buildKiloConsole() {
const app = path.resolve(dir, "../kilo-console")
const out = path.join(app, "dist")
if (await isKiloConsoleUpToDate(app, out)) {
console.log(`reusing existing Kilo Console build at ${out}`)
return out
}
console.log("building Kilo Console")
const proc = Bun.spawn([process.execPath, "run", "build"], {
cwd: app,
@@ -66,6 +103,7 @@ async function buildKiloConsole() {
if (code !== 0) throw new Error(`Kilo Console build failed with exit code ${code}`)
return out
}
// kilocode_change end
async function copyKiloConsole(input: string, outputDir: string) {
const target = path.join(outputDir, "console")
@@ -217,11 +255,13 @@ const targets = singleFlag
})
: allTargets
await $`rm -rf dist`
// kilocode_change start
const kiloConsoleDist = await buildKiloConsole()
const kiloSandboxWorker = await KiloSandboxWorker.bundle()
const kiloSandboxNetwork = await KiloSandboxNetwork.bundle()
await $`rm -rf dist`
const [kiloConsoleDist, kiloSandboxWorker, kiloSandboxNetwork] = await Promise.all([
buildKiloConsole(),
KiloSandboxWorker.bundle(),
KiloSandboxNetwork.bundle(),
])
// kilocode_change end
const binaries: Record<string, string> = {}
+35 -3
View File
@@ -10,9 +10,41 @@ process.chdir(dir)
const modelsUrl = process.env.KILO_MODELS_URL || "https://models.dev"
// kilocode_change start
const raw = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
const cacheFile = path.resolve(dir, "node_modules/.cache/models-dev-api.json")
const raw = await (async () => {
if (process.env.MODELS_DEV_API_JSON) {
return await Bun.file(process.env.MODELS_DEV_API_JSON).text()
}
const cached = Bun.file(cacheFile)
try {
if (await cached.exists()) {
const st = await cached.stat()
if (st && Date.now() - st.mtimeMs < 6 * 3600 * 1000) {
return await cached.text()
}
}
} catch (err) {
console.warn("[generate] cache read check failed, fetching live", err)
}
try {
const res = await fetch(`${modelsUrl}/api.json`, { signal: AbortSignal.timeout(5000) })
if (!res.ok) throw new Error(`Failed to fetch models.dev snapshot: HTTP ${res.status}`)
const text = await res.text()
try {
await Bun.write(cacheFile, text)
} catch (err) {
console.warn("[generate] cache write failed", err)
}
return text
} catch (err) {
try {
if (await cached.exists()) return await cached.text()
} catch (fallbackErr) {
console.warn("[generate] cache fallback read failed", fallbackErr)
}
throw err
}
})()
export const modelsData = JSON.stringify(parseModelsSnapshot(raw).data)
// kilocode_change end
console.log("Loaded models.dev snapshot")
+83 -4
View File
@@ -1,17 +1,78 @@
// kilocode_change - new file
import path from "path"
import fs from "fs/promises"
import fsSync from "fs"
import crypto from "crypto"
import { createRequire } from "module"
export namespace TestCli {
export const ENV = "KILO_TEST_CLI_PATH"
export async function build(root: string, dir: string) {
async function fingerprint(root: string): Promise<string> {
const hash = crypto.createHash("sha256")
const repo = path.resolve(root, "../..")
const pkgs = path.join(repo, "packages")
const lock = path.join(repo, "bun.lock")
if (fsSync.existsSync(lock)) {
const st = fsSync.statSync(lock)
hash.update("bun.lock").update(String(st.mtimeMs)).update(String(st.size))
}
const ignored = new Set(["kilo-vscode", "kilo-jetbrains", "kilo-docs"])
const entries = fsSync.readdirSync(pkgs, { withFileTypes: true })
for (const ent of entries) {
if (!ent.isDirectory() || ignored.has(ent.name)) continue
const dir = path.join(pkgs, ent.name)
const targets =
ent.name === "sdk"
? [path.join(dir, "js", "src"), path.join(dir, "js", "package.json")]
: [path.join(dir, "src"), path.join(dir, "migration"), path.join(dir, "package.json")]
for (const target of targets) {
if (!fsSync.existsSync(target)) continue
const st = fsSync.statSync(target)
if (st.isDirectory()) {
const glob = new Bun.Glob("**/*.{ts,tsx,sql,json,txt}")
for await (const file of glob.scan({ cwd: target })) {
const p = path.join(target, file)
const fst = fsSync.statSync(p)
hash.update(`${ent.name}:${file}`).update(String(fst.mtimeMs)).update(String(fst.size))
}
} else {
hash.update(`${ent.name}:pkg`).update(String(st.mtimeMs)).update(String(st.size))
}
}
}
return hash.digest("hex")
}
export async function build(root: string, targetDir?: string) {
if (path.resolve(process.cwd()) !== path.resolve(root)) {
throw new Error(`CLI test bundle must be built from ${root}`)
}
const dir = targetDir ?? path.join(root, ".artifacts", "test-cli-cached")
const out = path.join(dir, "src/storage")
const bin = path.join(out, "cli.js")
const hashFile = path.join(dir, ".hash")
if (!targetDir) {
const currentHash = await fingerprint(root)
try {
if (fsSync.existsSync(bin) && fsSync.existsSync(hashFile)) {
const storedHash = fsSync.readFileSync(hashFile, "utf8").trim()
if (storedHash === currentHash) {
return bin
}
}
} catch {}
}
const { createSolidTransformPlugin } = await import("@opentui/solid/bun-plugin")
const entry = "./src/index.ts"
const out = path.join(dir, "src/storage")
const result = await Bun.build({
entrypoints: [entry],
outdir: out,
@@ -44,8 +105,26 @@ export namespace TestCli {
return
}
})()
if (target) await fs.symlink(target, path.join(scope, name.replace("@opentui/", "")), kind)
if (target) {
const link = path.join(scope, name.replace("@opentui/", ""))
try {
await fs.rm(link, { recursive: true, force: true })
await fs.symlink(target, link, kind)
} catch (err) {
console.warn(`[test-cli] failed to link native variant ${name}:`, err)
}
}
}
return path.join(out, "cli.js")
if (!targetDir) {
try {
const currentHash = await fingerprint(root)
await fs.writeFile(hashFile, currentHash)
} catch (err) {
console.warn("[test-cli] failed to save fingerprint hash:", err)
}
}
return bin
}
}
+3 -11
View File
@@ -187,25 +187,17 @@ type Proc = ReturnType<typeof Bun.spawn>
const xmldir = ci ? path.join(os.tmpdir(), `opencode-junit-${process.pid}`) : ""
if (ci) await fs.mkdir(xmldir, { recursive: true })
// kilocode_change start
const supplied = process.env[TestCli.ENV]
const binprefix = path.join(root, ".artifacts", "test-cli-")
const built = supplied
? { binary: supplied, dir: undefined }
: await (async () => {
await fs.mkdir(path.dirname(binprefix), { recursive: true })
const dir = await fs.mkdtemp(binprefix)
return { binary: await TestCli.build(root, dir), dir }
})()
: { binary: await TestCli.build(root), dir: undefined }
async function cleanBinary() {
if (!built.dir) return
const expected = path.dirname(binprefix)
const valid =
path.dirname(built.dir) === expected && path.basename(built.dir).startsWith(path.basename(binprefix))
if (!valid) throw new Error(`Refusing to remove unexpected test CLI directory: ${built.dir}`)
// The generated directory contains the bundle, emitted assets, and copied migrations.
await fs.rm(built.dir, { recursive: true, force: true })
}
// kilocode_change end
const counter = { done: 0 }
const pad = String(files.length).length
+1 -2
View File
@@ -143,8 +143,7 @@ if (!legacyPatched.includes(sandbox)) {
}
await Bun.write(legacyTypesPath, legacyPatched)
await $`bun prettier --write src/gen`
await $`bun prettier --write src/v2`
await $`bun prettier --write src/gen src/v2`
await $`rm -rf dist tsconfig.tsbuildinfo`
await $`bun tsc`
await $`rm openapi.json`