refactor: 重构构建流程,新增 DIRECT_CONNECT feature 并添加 Node.js polyfill
This commit is contained in:
parent
81d9555344
commit
7b8bc9f314
151
build.ts
151
build.ts
|
|
@ -1,6 +1,6 @@
|
|||
import { readdir, readFile, writeFile, cp } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { getMacroDefines, DEFAULT_BUILD_FEATURES } from './scripts/defines.ts'
|
||||
import { getMacroDefines } from './scripts/defines.ts'
|
||||
|
||||
const outdir = 'dist'
|
||||
|
||||
|
|
@ -8,6 +8,45 @@ const outdir = 'dist'
|
|||
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_'))
|
||||
|
|
@ -51,65 +90,77 @@ for (const file of files) {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 3.5: Replace feature('FLAG_NAME') with true/false at build time
|
||||
// Bun.build does not natively replace feature flags, so we do it manually here
|
||||
// to match the behavior of vite-plugin-feature-flags.ts.
|
||||
const FEATURE_CALL_RE = /feature\s*\(\s*['"]([\w]+)['"]\s*\)/g
|
||||
let featureReplaced = 0
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.js')) continue
|
||||
const filePath = join(outdir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
let matchCount = 0
|
||||
const transformed = content.replace(FEATURE_CALL_RE, (match, flagName) => {
|
||||
matchCount++
|
||||
return features.includes(flagName) ? 'true' : 'false'
|
||||
})
|
||||
if (matchCount > 0) {
|
||||
await writeFile(filePath, transformed)
|
||||
featureReplaced += matchCount
|
||||
}
|
||||
}
|
||||
|
||||
// Also patch unguarded globalThis.Bun destructuring from third-party deps
|
||||
// (e.g. @anthropic-ai/sandbox-runtime) so Node.js doesn't crash at import time.
|
||||
let bunPatched = 0
|
||||
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g
|
||||
const BUN_DESTRUCTURE_SAFE = 'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};'
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.js')) continue
|
||||
const filePath = join(outdir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
if (BUN_DESTRUCTURE.test(content)) {
|
||||
await writeFile(
|
||||
filePath,
|
||||
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
|
||||
)
|
||||
bunPatched++
|
||||
}
|
||||
}
|
||||
BUN_DESTRUCTURE.lastIndex = 0
|
||||
|
||||
console.log(
|
||||
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure, ${featureReplaced} feature flags)`,
|
||||
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
|
||||
)
|
||||
|
||||
// Step 4: Copy native .node addon files (audio-capture) and vendored binaries (ripgrep)
|
||||
const audioCaptureDir = join(outdir, 'vendor', 'audio-capture')
|
||||
await cp('vendor/audio-capture', audioCaptureDir, { recursive: true })
|
||||
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`)
|
||||
// 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}/`)
|
||||
|
||||
const ripgrepDir = join(outdir, 'vendor', 'ripgrep')
|
||||
await cp('src/utils/vendor/ripgrep', ripgrepDir, { recursive: true })
|
||||
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`)
|
||||
// Step 5: Bundle download-ripgrep script as standalone JS for postinstall
|
||||
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}/`)
|
||||
}
|
||||
|
||||
// Step 5: Generate cli-bun and cli-node executable entry points
|
||||
// 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')
|
||||
|
||||
await writeFile(cliNode, '#!/usr/bin/env node\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')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getMacroDefines, DEFAULT_BUILD_FEATURES } from "./defines.ts";
|
||||
import { getMacroDefines } from "./defines.ts";
|
||||
|
||||
// Resolve project root from this script's location
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
|
@ -22,7 +22,37 @@ const defineArgs = Object.entries(defines).flatMap(([k, v]) => [
|
|||
]);
|
||||
|
||||
// Bun --feature flags: enable feature() gates at runtime.
|
||||
// Uses the shared DEFAULT_BUILD_FEATURES list from defines.ts.
|
||||
// Default features enabled in dev mode.
|
||||
const DEFAULT_FEATURES = [
|
||||
"BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE",
|
||||
"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",
|
||||
];
|
||||
|
||||
// Any env var matching FEATURE_<NAME>=1 will also enable that feature.
|
||||
// e.g. FEATURE_PROACTIVE=1 bun run dev
|
||||
|
|
@ -30,17 +60,9 @@ const envFeatures = Object.entries(process.env)
|
|||
.filter(([k]) => k.startsWith("FEATURE_"))
|
||||
.map(([k]) => k.replace("FEATURE_", ""));
|
||||
|
||||
const allFeatures = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])];
|
||||
const allFeatures = [...new Set([...DEFAULT_FEATURES, ...envFeatures])];
|
||||
const featureArgs = allFeatures.flatMap((name) => ["--feature", name]);
|
||||
|
||||
// Dev mode should stay interactive for real terminal launches. Nested Bun
|
||||
// launches on Windows can lose TTY metadata, but we should not force
|
||||
// interactive mode when stdin is piped because that breaks headless usage like
|
||||
// `"hello" | bun run dev`.
|
||||
if (process.stdin.isTTY) {
|
||||
process.env.CLAUDE_CODE_FORCE_INTERACTIVE ??= "1";
|
||||
}
|
||||
|
||||
// If BUN_INSPECT is set, pass --inspect-wait to the child process
|
||||
const inspectArgs = process.env.BUN_INSPECT
|
||||
? ["--inspect-wait=" + process.env.BUN_INSPECT]
|
||||
|
|
@ -53,11 +75,7 @@ const bunCmd = process.execPath;
|
|||
|
||||
const result = Bun.spawnSync(
|
||||
[bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)],
|
||||
{
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
cwd: projectRoot,
|
||||
env: process.env,
|
||||
},
|
||||
{ stdio: ["inherit", "inherit", "inherit"], cwd: projectRoot },
|
||||
);
|
||||
|
||||
process.exit(result.exitCode ?? 0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user