Bun 将源码编译为独立可执行文件后,process.argv[1] 变为内部虚拟路径 (B:/~BUN/root/cli.js),该路径含 / 被误判为脚本模式,导致 saveChildSpawnPrefix 为子进程生成 --feature / -d 参数。子进程(csc.exe 本身)不识别这些 Bun 运行时参数,以 exit code 1 退出,探针失败,/provider/capabilities 返回空模型。 修复: - getScriptArgsForChild: 识别并跳过 Bun snapshot 虚拟路径前缀 (B:/~BUN/, /snapshot/) - saveChildSpawnPrefix: 仅在 scriptArgs 非空(脚本模式)时才生成 --feature/define 参数 - build.ts: 补充 Step 7 编译独立可执行文件 csc.exe / csc Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
import { readdir, readFile, writeFile, cp } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { getMacroDefines } from './scripts/defines.ts'
|
|
|
|
const outdir = 'dist'
|
|
|
|
// Step 1: Clean output directory
|
|
const { rmSync } = await import('fs')
|
|
rmSync(outdir, { recursive: true, force: true })
|
|
|
|
// Default features that match the official CLI build.
|
|
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
|
const DEFAULT_BUILD_FEATURES = [
|
|
'AGENT_TRIGGERS_REMOTE',
|
|
'CHICAGO_MCP',
|
|
'VOICE_MODE',
|
|
'SHOT_STATS',
|
|
'PROMPT_CACHE_BREAK_DETECTION',
|
|
'TOKEN_BUDGET',
|
|
// P0: local features
|
|
'AGENT_TRIGGERS',
|
|
'ULTRATHINK',
|
|
'BUILTIN_EXPLORE_PLAN_AGENTS',
|
|
'LODESTONE',
|
|
// P1: API-dependent features
|
|
'EXTRACT_MEMORIES',
|
|
'VERIFICATION_AGENT',
|
|
'KAIROS_BRIEF',
|
|
'AWAY_SUMMARY',
|
|
'ULTRAPLAN',
|
|
// P2: daemon + remote control server
|
|
'DAEMON',
|
|
// PR-package restored features
|
|
'WORKFLOW_SCRIPTS',
|
|
'HISTORY_SNIP',
|
|
'CONTEXT_COLLAPSE',
|
|
'MONITOR_TOOL',
|
|
'FORK_SUBAGENT',
|
|
// 'UDS_INBOX',
|
|
'KAIROS',
|
|
'COORDINATOR_MODE',
|
|
'LAN_PIPES',
|
|
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
|
|
// P3: poor mode (disable extract_memories + prompt_suggestion)
|
|
'POOR',
|
|
// P3: serve mode (HTTP API server)
|
|
'DIRECT_CONNECT',
|
|
]
|
|
|
|
// Collect FEATURE_* env vars → Bun.build features
|
|
const envFeatures = Object.keys(process.env)
|
|
.filter(k => k.startsWith('FEATURE_'))
|
|
.map(k => k.replace('FEATURE_', ''))
|
|
const features = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])]
|
|
|
|
// Step 2: Bundle with splitting
|
|
const result = await Bun.build({
|
|
entrypoints: ['src/entrypoints/cli.tsx'],
|
|
outdir,
|
|
target: 'bun',
|
|
splitting: true,
|
|
define: getMacroDefines(),
|
|
features,
|
|
})
|
|
|
|
if (!result.success) {
|
|
console.error('Build failed:')
|
|
for (const log of result.logs) {
|
|
console.error(log)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
// Step 3: Post-process — replace Bun-only `import.meta.require` with Node.js compatible version
|
|
const files = await readdir(outdir)
|
|
const IMPORT_META_REQUIRE = 'var __require = import.meta.require;'
|
|
const COMPAT_REQUIRE = `var __require = typeof import.meta.require === "function" ? import.meta.require : (await import("module")).createRequire(import.meta.url);`
|
|
|
|
let patched = 0
|
|
for (const file of files) {
|
|
if (!file.endsWith('.js')) continue
|
|
const filePath = join(outdir, file)
|
|
const content = await readFile(filePath, 'utf-8')
|
|
if (content.includes(IMPORT_META_REQUIRE)) {
|
|
await writeFile(
|
|
filePath,
|
|
content.replace(IMPORT_META_REQUIRE, COMPAT_REQUIRE),
|
|
)
|
|
patched++
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
|
|
)
|
|
|
|
// Step 4: Copy native .node addon files (audio-capture)
|
|
const vendorDir = join(outdir, 'vendor', 'audio-capture')
|
|
await cp('vendor/audio-capture', vendorDir, { recursive: true })
|
|
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
|
|
|
|
// Step 5: Bundle download-ripgrep script as standalone JS for postinstall
|
|
if (await Bun.file('scripts/download-ripgrep.ts').exists()) {
|
|
const rgScript = await Bun.build({
|
|
entrypoints: ['scripts/download-ripgrep.ts'],
|
|
outdir,
|
|
target: 'node',
|
|
})
|
|
if (!rgScript.success) {
|
|
console.error('Failed to bundle download-ripgrep script:')
|
|
for (const log of rgScript.logs) {
|
|
console.error(log)
|
|
}
|
|
// Non-fatal — postinstall fallback to bun run scripts/download-ripgrep.ts
|
|
} else {
|
|
console.log(`Bundled download-ripgrep script to ${outdir}/`)
|
|
}
|
|
} else {
|
|
console.log('Skipping download-ripgrep script (not found)')
|
|
}
|
|
|
|
// Step 6: Generate cli-bun and cli-node executable entry points
|
|
const cliBun = join(outdir, 'cli-bun.js')
|
|
const cliNode = join(outdir, 'cli-node.js')
|
|
|
|
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
|
|
|
// Node.js entry needs a Bun API polyfill because Bun.build({ target: 'bun' })
|
|
// emits globalThis.Bun references (e.g. Bun.$ shell tag in computer-use-input,
|
|
// Bun.which in chunk-ys6smqg9) that crash at import time under plain Node.js.
|
|
const NODE_BUN_POLYFILL = `#!/usr/bin/env node
|
|
// Bun API polyfill for Node.js runtime
|
|
if (typeof globalThis.Bun === "undefined") {
|
|
const { execFileSync } = await import("child_process");
|
|
const { resolve, delimiter } = await import("path");
|
|
const { accessSync, constants: { X_OK } } = await import("fs");
|
|
function which(bin) {
|
|
const isWin = process.platform === "win32";
|
|
const pathExt = isWin ? (process.env.PATHEXT || ".EXE").split(";") : [""];
|
|
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
for (const ext of pathExt) {
|
|
const candidate = resolve(dir, bin + ext);
|
|
try { accessSync(candidate, X_OK); return candidate; } catch {}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
// Bun.$ is the shell template tag (e.g. $\`osascript ...\`). Only used by
|
|
// computer-use-input/darwin — stub it so the top-level destructuring
|
|
// \`var { $ } = globalThis.Bun\` doesn't crash.
|
|
function $(parts, ...args) {
|
|
throw new Error("Bun.$ shell API is not available in Node.js. Use Bun runtime for this feature.");
|
|
}
|
|
function hash(data, seed) {
|
|
let h = ((seed || 0) ^ 0x811c9dc5) >>> 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
h ^= data.charCodeAt(i);
|
|
h = Math.imul(h, 0x01000193) >>> 0;
|
|
}
|
|
return h;
|
|
}
|
|
globalThis.Bun = { which, $, hash };
|
|
}
|
|
import "./cli.js"
|
|
`
|
|
await writeFile(cliNode, NODE_BUN_POLYFILL)
|
|
// NOTE: when new Bun-specific globals appear in bundled output, add them here.
|
|
|
|
// Make both executable
|
|
const { chmodSync } = await import('fs')
|
|
chmodSync(cliBun, 0o755)
|
|
chmodSync(cliNode, 0o755)
|
|
|
|
console.log(`Generated ${cliBun} (shebang: bun) and ${cliNode} (shebang: node)`)
|
|
|
|
// Step 7: Compile standalone executable (csc.exe on Windows, csc on Unix)
|
|
// Must use Bun.build({ compile: true, features }) instead of `bun build --compile`
|
|
// because the CLI command doesn't support the --feature flag.
|
|
// NOTE: compile mode uses outdir (not outfile) — Bun names the output after the entrypoint.
|
|
const isWin = process.platform === 'win32'
|
|
const exeName = isWin ? 'csc.exe' : 'csc'
|
|
|
|
const compileResult = await Bun.build({
|
|
entrypoints: ['src/entrypoints/cli.tsx'],
|
|
target: 'bun',
|
|
compile: true,
|
|
define: getMacroDefines(),
|
|
features,
|
|
outdir: import.meta.dir,
|
|
})
|
|
|
|
if (!compileResult.success) {
|
|
console.error('Compile failed:')
|
|
for (const log of compileResult.logs) {
|
|
console.error(log)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
// Rename auto-generated cli.tsx.exe → csc.exe
|
|
const generated = compileResult.outputs[0]
|
|
const targetPath = join(import.meta.dir, exeName)
|
|
if (generated && generated.path !== targetPath) {
|
|
const { renameSync, unlinkSync, existsSync } = await import('fs')
|
|
if (existsSync(targetPath)) {
|
|
try { unlinkSync(targetPath) } catch { /* locked by running process */ }
|
|
}
|
|
renameSync(generated.path, targetPath)
|
|
}
|
|
|
|
console.log(`Compiled standalone executable: ${join(import.meta.dir, exeName)}`)
|