mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
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:
@@ -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=="],
|
||||
|
||||
@@ -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
@@ -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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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> = {}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user