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 { readdir, readFile, writeFile, cp } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getMacroDefines, DEFAULT_BUILD_FEATURES } from './scripts/defines.ts'
|
import { getMacroDefines } from './scripts/defines.ts'
|
||||||
|
|
||||||
const outdir = 'dist'
|
const outdir = 'dist'
|
||||||
|
|
||||||
|
|
@ -8,6 +8,45 @@ const outdir = 'dist'
|
||||||
const { rmSync } = await import('fs')
|
const { rmSync } = await import('fs')
|
||||||
rmSync(outdir, { recursive: true, force: true })
|
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
|
// Collect FEATURE_* env vars → Bun.build features
|
||||||
const envFeatures = Object.keys(process.env)
|
const envFeatures = Object.keys(process.env)
|
||||||
.filter(k => k.startsWith('FEATURE_'))
|
.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(
|
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)
|
// Step 4: Copy native .node addon files (audio-capture)
|
||||||
const audioCaptureDir = join(outdir, 'vendor', 'audio-capture')
|
const vendorDir = join(outdir, 'vendor', 'audio-capture')
|
||||||
await cp('vendor/audio-capture', audioCaptureDir, { recursive: true })
|
await cp('vendor/audio-capture', vendorDir, { recursive: true })
|
||||||
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`)
|
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
|
||||||
|
|
||||||
const ripgrepDir = join(outdir, 'vendor', 'ripgrep')
|
// Step 5: Bundle download-ripgrep script as standalone JS for postinstall
|
||||||
await cp('src/utils/vendor/ripgrep', ripgrepDir, { recursive: true })
|
const rgScript = await Bun.build({
|
||||||
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`)
|
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 cliBun = join(outdir, 'cli-bun.js')
|
||||||
const cliNode = join(outdir, 'cli-node.js')
|
const cliNode = join(outdir, 'cli-node.js')
|
||||||
|
|
||||||
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
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
|
// Make both executable
|
||||||
const { chmodSync } = await import('fs')
|
const { chmodSync } = await import('fs')
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
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
|
// Resolve project root from this script's location
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
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.
|
// 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.
|
// Any env var matching FEATURE_<NAME>=1 will also enable that feature.
|
||||||
// e.g. FEATURE_PROACTIVE=1 bun run dev
|
// e.g. FEATURE_PROACTIVE=1 bun run dev
|
||||||
|
|
@ -30,17 +60,9 @@ const envFeatures = Object.entries(process.env)
|
||||||
.filter(([k]) => k.startsWith("FEATURE_"))
|
.filter(([k]) => k.startsWith("FEATURE_"))
|
||||||
.map(([k]) => k.replace("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]);
|
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
|
// If BUN_INSPECT is set, pass --inspect-wait to the child process
|
||||||
const inspectArgs = process.env.BUN_INSPECT
|
const inspectArgs = process.env.BUN_INSPECT
|
||||||
? ["--inspect-wait=" + process.env.BUN_INSPECT]
|
? ["--inspect-wait=" + process.env.BUN_INSPECT]
|
||||||
|
|
@ -53,11 +75,7 @@ const bunCmd = process.execPath;
|
||||||
|
|
||||||
const result = Bun.spawnSync(
|
const result = Bun.spawnSync(
|
||||||
[bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)],
|
[bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)],
|
||||||
{
|
{ stdio: ["inherit", "inherit", "inherit"], cwd: projectRoot },
|
||||||
stdio: ["inherit", "inherit", "inherit"],
|
|
||||||
cwd: projectRoot,
|
|
||||||
env: process.env,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(result.exitCode ?? 0);
|
process.exit(result.exitCode ?? 0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user