mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: 修复 node 的 es 版本太高不兼容的构建问题
This commit is contained in:
18
build.ts
18
build.ts
@@ -121,23 +121,7 @@ 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
|
||||
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 6: Generate cli-bun and cli-node executable entry points
|
||||
// Step 5: Generate cli-bun and cli-node executable entry points
|
||||
const cliBun = join(outdir, 'cli-bun.js')
|
||||
const cliNode = join(outdir, 'cli-node.js')
|
||||
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
/**
|
||||
* Download ripgrep binary from GitHub releases.
|
||||
*
|
||||
* Run automatically via `bun install` (postinstall hook),
|
||||
* or manually: `bun run scripts/download-ripgrep.ts [--force]`
|
||||
*
|
||||
* Idempotent — skips download if the binary already exists.
|
||||
* Use --force to re-download.
|
||||
*
|
||||
* Environment:
|
||||
* - HTTPS_PROXY / HTTP_PROXY — when set, download uses `undici` + EnvHttpProxyAgent.
|
||||
* - RIPGREP_DOWNLOAD_BASE — override release URL prefix, e.g. mirror:
|
||||
* `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1`
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from 'fs'
|
||||
import { setDefaultResultOrder } from 'node:dns'
|
||||
import { tmpdir } from 'os'
|
||||
import { chmodSync } from 'fs'
|
||||
import { spawnSync } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths.
|
||||
try {
|
||||
setDefaultResultOrder('ipv4first')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const RG_VERSION = '15.0.1'
|
||||
const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
|
||||
const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, '')
|
||||
|
||||
// --- Platform mapping ---
|
||||
|
||||
type PlatformMapping = {
|
||||
target: string
|
||||
ext: 'tar.gz' | 'zip'
|
||||
}
|
||||
|
||||
function getPlatformMapping(): PlatformMapping {
|
||||
const arch = process.arch
|
||||
const platform = process.platform
|
||||
|
||||
if (platform === 'darwin') {
|
||||
if (arch === 'arm64') return { target: 'aarch64-apple-darwin', ext: 'tar.gz' }
|
||||
if (arch === 'x64') return { target: 'x86_64-apple-darwin', ext: 'tar.gz' }
|
||||
throw new Error(`Unsupported macOS arch: ${arch}`)
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
if (arch === 'x64') return { target: 'x86_64-pc-windows-msvc', ext: 'zip' }
|
||||
if (arch === 'arm64') return { target: 'aarch64-pc-windows-msvc', ext: 'zip' }
|
||||
throw new Error(`Unsupported Windows arch: ${arch}`)
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
const isMusl = detectMusl()
|
||||
if (arch === 'x64') {
|
||||
// x64 Linux always uses musl (statically linked, most portable)
|
||||
return { target: 'x86_64-unknown-linux-musl', ext: 'tar.gz' }
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
return isMusl
|
||||
? { target: 'aarch64-unknown-linux-musl', ext: 'tar.gz' }
|
||||
: { target: 'aarch64-unknown-linux-gnu', ext: 'tar.gz' }
|
||||
}
|
||||
throw new Error(`Unsupported Linux arch: ${arch}`)
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported platform: ${platform}`)
|
||||
}
|
||||
|
||||
function detectMusl(): boolean {
|
||||
const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64'
|
||||
try {
|
||||
statSync(`/lib/libc.musl-${muslArch}.so.1`)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Target vendor path (must match ripgrep.ts logic) ---
|
||||
|
||||
function getVendorDir(): string {
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
|
||||
// Dev mode: package root has src/ directory
|
||||
// ripgrep.ts at src/utils/ripgrep.ts: __dirname = src/utils/
|
||||
// vendor path = src/utils/vendor/ripgrep/
|
||||
if (existsSync(path.join(packageRoot, 'src'))) {
|
||||
return path.resolve(packageRoot, 'src', 'utils', 'vendor', 'ripgrep')
|
||||
}
|
||||
|
||||
// Published mode: compiled chunks are flat in dist/
|
||||
// ripgrep chunk at dist/xxxx.js: __dirname = dist/
|
||||
// vendor path = dist/vendor/ripgrep/
|
||||
return path.resolve(packageRoot, 'dist', 'vendor', 'ripgrep')
|
||||
}
|
||||
|
||||
function getBinaryPath(): string {
|
||||
const dir = getVendorDir()
|
||||
const subdir = `${process.arch}-${process.platform}`
|
||||
const binary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
return path.resolve(dir, subdir, binary)
|
||||
}
|
||||
|
||||
// --- Download & extract ---
|
||||
|
||||
function proxyEnvSet(): boolean {
|
||||
const v = (s: string | undefined) => (s ?? '').trim()
|
||||
return !!(
|
||||
v(process.env.HTTPS_PROXY) ||
|
||||
v(process.env.HTTP_PROXY) ||
|
||||
v(process.env.ALL_PROXY) ||
|
||||
v(process.env.https_proxy) ||
|
||||
v(process.env.http_proxy)
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchRelease(url: string): Promise<Response> {
|
||||
if (proxyEnvSet()) {
|
||||
const { EnvHttpProxyAgent, fetch: undiciFetch } = await import('undici')
|
||||
return (await undiciFetch(url, {
|
||||
redirect: 'follow',
|
||||
dispatcher: new EnvHttpProxyAgent(),
|
||||
})) as unknown as Response
|
||||
}
|
||||
return await fetch(url, { redirect: 'follow' })
|
||||
}
|
||||
|
||||
function tryPowerShellDownload(url: string, dest: string): boolean {
|
||||
const u = url.replace(/'/g, "''")
|
||||
const d = dest.replace(/'/g, "''")
|
||||
const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing`
|
||||
const result = spawnSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd],
|
||||
{ stdio: 'pipe', windowsHide: true },
|
||||
)
|
||||
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
|
||||
}
|
||||
|
||||
function tryCurlDownload(url: string, dest: string): boolean {
|
||||
const curl = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
||||
const result = spawnSync(curl, ['-fsSL', '-L', '--fail', '-o', dest, url], {
|
||||
stdio: 'pipe',
|
||||
windowsHide: true,
|
||||
})
|
||||
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
|
||||
}
|
||||
|
||||
/** Bun `fetch` on Windows can fail while browser / WinINET still works — use subprocess fallbacks. */
|
||||
async function downloadUrlToBuffer(url: string): Promise<Buffer> {
|
||||
const response = await fetchRelease(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer())
|
||||
}
|
||||
|
||||
async function downloadUrlToBufferWithFallback(url: string): Promise<Buffer> {
|
||||
let firstError: unknown
|
||||
try {
|
||||
return await downloadUrlToBuffer(url)
|
||||
} catch (e) {
|
||||
firstError = e
|
||||
}
|
||||
|
||||
const tmpRoot = path.join(tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`)
|
||||
const tmpFile = path.join(tmpRoot, 'archive')
|
||||
mkdirSync(tmpRoot, { recursive: true })
|
||||
try {
|
||||
if (process.platform === 'win32' && tryPowerShellDownload(url, tmpFile)) {
|
||||
return readFileSync(tmpFile)
|
||||
}
|
||||
if (tryCurlDownload(url, tmpFile)) {
|
||||
return readFileSync(tmpFile)
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmpRoot, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
throw firstError
|
||||
}
|
||||
|
||||
function findZipEntryKey(files: Record<string, Uint8Array>, want: string): string | undefined {
|
||||
return Object.keys(files).find(k => {
|
||||
const norm = k.replace(/\\/g, '/')
|
||||
return norm === want || norm.endsWith(`/${want}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadAndExtract(): Promise<void> {
|
||||
const { target, ext } = getPlatformMapping()
|
||||
const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}`
|
||||
const downloadUrl = `${RELEASE_BASE}/${assetName}`
|
||||
|
||||
const binaryPath = getBinaryPath()
|
||||
const binaryDir = path.dirname(binaryPath)
|
||||
|
||||
// Idempotent: skip if binary exists and has content
|
||||
const force = process.argv.includes('--force')
|
||||
if (!force && existsSync(binaryPath)) {
|
||||
const stat = statSync(binaryPath)
|
||||
if (stat.size > 0) {
|
||||
console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`)
|
||||
console.log(`[ripgrep] URL: ${downloadUrl}`)
|
||||
|
||||
const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
const { writeFileSync } = await import('fs')
|
||||
|
||||
try {
|
||||
const buffer = await downloadUrlToBufferWithFallback(downloadUrl)
|
||||
console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`)
|
||||
|
||||
mkdirSync(binaryDir, { recursive: true })
|
||||
|
||||
if (ext === 'tar.gz') {
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
writeFileSync(archivePath, buffer)
|
||||
const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
|
||||
}
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
renameSync(srcBinary, binaryPath)
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
} else {
|
||||
let fflateError: unknown
|
||||
try {
|
||||
const { unzipSync } = await import('fflate')
|
||||
const unzipped = unzipSync(new Uint8Array(buffer))
|
||||
const key = findZipEntryKey(unzipped, extractedBinary)
|
||||
if (!key) {
|
||||
throw new Error(`Binary ${extractedBinary} not found in zip`)
|
||||
}
|
||||
writeFileSync(binaryPath, Buffer.from(unzipped[key]))
|
||||
fflateError = undefined
|
||||
} catch (e) {
|
||||
fflateError = e
|
||||
}
|
||||
|
||||
if (fflateError) {
|
||||
// fflate failed — try PowerShell Expand-Archive on Windows, then unzip CLI
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
writeFileSync(archivePath, buffer)
|
||||
|
||||
let extracted = false
|
||||
|
||||
// On Windows, prefer PowerShell Expand-Archive
|
||||
if (process.platform === 'win32') {
|
||||
const psCmd = `Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force`
|
||||
const psResult = spawnSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', psCmd],
|
||||
{ stdio: 'pipe', windowsHide: true },
|
||||
)
|
||||
if (psResult.status === 0) {
|
||||
extracted = true
|
||||
} else {
|
||||
const psErr = psResult.stderr?.toString().trim() || 'unknown error'
|
||||
console.log(`[ripgrep] PowerShell Expand-Archive failed: ${psErr}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unzip CLI (Git for Windows, MSYS2, or Unix)
|
||||
if (!extracted) {
|
||||
const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
const unzipErr = result.stderr?.toString().trim() || 'command not found'
|
||||
const fflateMsg = fflateError instanceof Error ? fflateError.message : String(fflateError)
|
||||
throw new Error(`zip extraction failed (fflate: ${fflateMsg}; unzip: ${unzipErr})`)
|
||||
}
|
||||
}
|
||||
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
renameSync(srcBinary, binaryPath)
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
|
||||
console.log(`[ripgrep] Installed to ${binaryPath}`)
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
const hint =
|
||||
'Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header).'
|
||||
throw new Error(`${msg} ${hint}`)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
downloadAndExtract().catch(error => {
|
||||
console.error(`[ripgrep] Download failed: ${error.message}`)
|
||||
console.error(`[ripgrep] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`)
|
||||
// Don't exit with error code — postinstall should not break bun install
|
||||
process.exit(0)
|
||||
})
|
||||
@@ -1,163 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* 代码健康度检查脚本
|
||||
*
|
||||
* 汇总项目各维度指标,输出健康度报告:
|
||||
* - 代码规模(文件数、代码行数)
|
||||
* - Lint 问题数(Biome)
|
||||
* - 测试结果(Bun test)
|
||||
* - 冗余代码(Knip)
|
||||
* - 构建状态
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
|
||||
const DIVIDER = "─".repeat(60);
|
||||
|
||||
interface Metric {
|
||||
label: string;
|
||||
value: string | number;
|
||||
status: "ok" | "warn" | "error" | "info";
|
||||
}
|
||||
|
||||
const metrics: Metric[] = [];
|
||||
|
||||
function add(label: string, value: string | number, status: Metric["status"] = "info") {
|
||||
metrics.push({ label, value, status });
|
||||
}
|
||||
|
||||
function icon(status: Metric["status"]): string {
|
||||
switch (status) {
|
||||
case "ok":
|
||||
return "[OK]";
|
||||
case "warn":
|
||||
return "[!!]";
|
||||
case "error":
|
||||
return "[XX]";
|
||||
case "info":
|
||||
return "[--]";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. 代码规模
|
||||
// ---------------------------------------------------------------------------
|
||||
async function checkCodeSize() {
|
||||
const tsFiles = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules`.text();
|
||||
const fileCount = tsFiles.trim().split("\n").filter(Boolean).length;
|
||||
add("TypeScript 文件数", fileCount, "info");
|
||||
|
||||
const loc = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules | xargs wc -l | tail -1`.text();
|
||||
const totalLines = loc.trim().split(/\s+/)[0] ?? "?";
|
||||
add("总代码行数 (src/)", totalLines, "info");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Lint 检查
|
||||
// ---------------------------------------------------------------------------
|
||||
async function checkLint() {
|
||||
try {
|
||||
const result = await $`bunx biome check src/ 2>&1`.quiet().nothrow().text();
|
||||
const errorMatch = result.match(/Found (\d+) errors?/);
|
||||
const warnMatch = result.match(/Found (\d+) warnings?/);
|
||||
const errors = errorMatch ? Number.parseInt(errorMatch[1]) : 0;
|
||||
const warnings = warnMatch ? Number.parseInt(warnMatch[1]) : 0;
|
||||
add("Lint 错误", errors, errors === 0 ? "ok" : errors < 100 ? "warn" : "info");
|
||||
add("Lint 警告", warnings, warnings === 0 ? "ok" : "info");
|
||||
} catch {
|
||||
add("Lint 检查", "执行失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. 测试
|
||||
// ---------------------------------------------------------------------------
|
||||
async function checkTests() {
|
||||
try {
|
||||
const result = await $`bun test 2>&1`.quiet().nothrow().text();
|
||||
const passMatch = result.match(/(\d+) pass/);
|
||||
const failMatch = result.match(/(\d+) fail/);
|
||||
const pass = passMatch ? Number.parseInt(passMatch[1]) : 0;
|
||||
const fail = failMatch ? Number.parseInt(failMatch[1]) : 0;
|
||||
add("测试通过", pass, pass > 0 ? "ok" : "warn");
|
||||
add("测试失败", fail, fail === 0 ? "ok" : "error");
|
||||
} catch {
|
||||
add("测试", "执行失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. 冗余代码
|
||||
// ---------------------------------------------------------------------------
|
||||
async function checkUnused() {
|
||||
try {
|
||||
const result = await $`bunx knip-bun 2>&1`.quiet().nothrow().text();
|
||||
const unusedFiles = result.match(/Unused files \((\d+)\)/);
|
||||
const unusedExports = result.match(/Unused exports \((\d+)\)/);
|
||||
const unusedDeps = result.match(/Unused dependencies \((\d+)\)/);
|
||||
add("未使用文件", unusedFiles?.[1] ?? "0", "info");
|
||||
add("未使用导出", unusedExports?.[1] ?? "0", "info");
|
||||
add("未使用依赖", unusedDeps?.[1] ?? "0", unusedDeps && Number(unusedDeps[1]) > 0 ? "warn" : "ok");
|
||||
} catch {
|
||||
add("冗余代码检查", "执行失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. 构建
|
||||
// ---------------------------------------------------------------------------
|
||||
async function checkBuild() {
|
||||
try {
|
||||
const result = await $`bun run build 2>&1`.quiet().nothrow();
|
||||
if (result.exitCode === 0) {
|
||||
// 获取产物大小
|
||||
const stat = Bun.file("dist/cli.js");
|
||||
const mb = (stat.size / 1024 / 1024).toFixed(1);
|
||||
const size = `${mb} MB`;
|
||||
add("构建状态", "成功", "ok");
|
||||
add("产物大小 (dist/cli.js)", size, "info");
|
||||
} else {
|
||||
add("构建状态", "失败", "error");
|
||||
}
|
||||
} catch {
|
||||
add("构建", "执行失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Run
|
||||
// ---------------------------------------------------------------------------
|
||||
console.log("");
|
||||
console.log(DIVIDER);
|
||||
console.log(" 代码健康度检查报告");
|
||||
console.log(` ${new Date().toLocaleString("zh-CN")}`);
|
||||
console.log(DIVIDER);
|
||||
|
||||
await checkCodeSize();
|
||||
await checkLint();
|
||||
await checkTests();
|
||||
await checkUnused();
|
||||
await checkBuild();
|
||||
|
||||
console.log("");
|
||||
for (const m of metrics) {
|
||||
const tag = icon(m.status);
|
||||
console.log(` ${tag} ${m.label.padEnd(20)} ${m.value}`);
|
||||
}
|
||||
|
||||
const errorCount = metrics.filter((m) => m.status === "error").length;
|
||||
const warnCount = metrics.filter((m) => m.status === "warn").length;
|
||||
|
||||
console.log("");
|
||||
console.log(DIVIDER);
|
||||
if (errorCount > 0) {
|
||||
console.log(` 结果: ${errorCount} 个错误, ${warnCount} 个警告`);
|
||||
} else if (warnCount > 0) {
|
||||
console.log(` 结果: 无错误, ${warnCount} 个警告`);
|
||||
} else {
|
||||
console.log(" 结果: 全部通过");
|
||||
}
|
||||
console.log(DIVIDER);
|
||||
console.log("");
|
||||
|
||||
process.exit(errorCount > 0 ? 1 : 0);
|
||||
@@ -4,8 +4,7 @@
|
||||
*
|
||||
* 1. Patch globalThis.Bun destructuring in third-party deps for Node.js compat
|
||||
* 2. Copy native addon files
|
||||
* 3. Bundle standalone scripts (download-ripgrep)
|
||||
* 4. Generate dual entry points (cli-bun.js, cli-node.js)
|
||||
* 3. Generate dual entry points (cli-bun.js, cli-node.js)
|
||||
*/
|
||||
import { readdir, readFile, writeFile, cp } from "node:fs/promises";
|
||||
import { chmodSync } from "node:fs";
|
||||
@@ -41,35 +40,7 @@ async function postBuild() {
|
||||
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
|
||||
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
|
||||
|
||||
// Step 3: Bundle standalone scripts via Bun.build (kept for simplicity)
|
||||
try {
|
||||
const { default: Bun } = await import("bun");
|
||||
const rgScript = await Bun.build({
|
||||
entrypoints: ["scripts/download-ripgrep.ts"],
|
||||
outdir,
|
||||
target: "node",
|
||||
});
|
||||
if (rgScript.success) {
|
||||
console.log(`Bundled download-ripgrep script to ${outdir}/`);
|
||||
} else {
|
||||
console.warn("Failed to bundle download-ripgrep script (non-fatal)");
|
||||
}
|
||||
} catch {
|
||||
// Bun not available — try esbuild fallback
|
||||
try {
|
||||
execSync(
|
||||
`npx esbuild scripts/download-ripgrep.ts --bundle --platform=node --outfile=${outdir}/download-ripgrep.js --format=esm`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
console.log(`Bundled download-ripgrep script via esbuild to ${outdir}/`);
|
||||
} catch {
|
||||
console.warn(
|
||||
"Failed to bundle download-ripgrep script — skipping (non-fatal)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Generate dual entry points
|
||||
// Step 3: Generate dual entry points
|
||||
const cliBun = join(outdir, "cli-bun.js");
|
||||
const cliNode = join(outdir, "cli-node.js");
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import { config } from "../packages/remote-control-server/src/config";
|
||||
console.log(`[RCS] Starting Remote Control Server...`);
|
||||
console.log(`[RCS] Port: ${config.port}`);
|
||||
console.log(`[RCS] API Key configuration loaded`);
|
||||
console.log(`[RCS] JWT Secret: ${config.jwtSecret === "change-me-in-production" ? "default (set RCS_JWT_SECRET)" : "custom"}`);
|
||||
console.log(`[RCS] DB: ${config.dbPath}`);
|
||||
|
||||
const server = await import("../packages/remote-control-server/src/index.ts");
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Verify GrowthBook gate defaults and compile-time feature flags.
|
||||
*
|
||||
* Usage:
|
||||
* bun run scripts/verify-gates.ts
|
||||
*
|
||||
* This script checks that LOCAL_GATE_DEFAULTS are being returned correctly
|
||||
* when GrowthBook is not connected, and that compile-time feature flags
|
||||
* are properly enabled.
|
||||
*/
|
||||
|
||||
// We can't import feature() from bun:bundle in a standalone script,
|
||||
// so we test the GrowthBook layer directly.
|
||||
|
||||
import {
|
||||
getFeatureValue_CACHED_MAY_BE_STALE,
|
||||
checkStatsigFeatureGate_CACHED_MAY_BE_STALE,
|
||||
} from '../src/services/analytics/growthbook.js'
|
||||
|
||||
interface GateCheck {
|
||||
name: string
|
||||
gate: string
|
||||
expected: unknown
|
||||
category: string
|
||||
/** If set, this compile flag must also be enabled at build time */
|
||||
compileFlag?: string
|
||||
}
|
||||
|
||||
const gates: GateCheck[] = [
|
||||
// P0: Pure local
|
||||
{ name: 'Custom keybindings', gate: 'tengu_keybinding_customization_release', expected: true, category: 'P0' },
|
||||
{ name: 'Streaming tool exec', gate: 'tengu_streaming_tool_execution2', expected: true, category: 'P0' },
|
||||
{ name: 'Cron tasks', gate: 'tengu_kairos_cron', expected: true, category: 'P0' },
|
||||
{ name: 'JSON tools format', gate: 'tengu_amber_json_tools', expected: true, category: 'P0' },
|
||||
{ name: 'Immediate model cmd', gate: 'tengu_immediate_model_command', expected: true, category: 'P0' },
|
||||
{ name: 'MCP delta', gate: 'tengu_basalt_3kr', expected: true, category: 'P0' },
|
||||
{ name: 'Leaf pruning', gate: 'tengu_pebble_leaf_prune', expected: true, category: 'P0' },
|
||||
{ name: 'Message smooshing', gate: 'tengu_chair_sermon', expected: true, category: 'P0' },
|
||||
{ name: 'Deep link', gate: 'tengu_lodestone_enabled', expected: true, category: 'P0', compileFlag: 'LODESTONE' },
|
||||
{ name: 'Auto background', gate: 'tengu_auto_background_agents', expected: true, category: 'P0' },
|
||||
{ name: 'Fine-grained tools', gate: 'tengu_fgts', expected: true, category: 'P0' },
|
||||
|
||||
// P1: API-dependent
|
||||
{ name: 'Session memory', gate: 'tengu_session_memory', expected: true, category: 'P1' },
|
||||
{ name: 'Auto memory extract', gate: 'tengu_passport_quail', expected: true, category: 'P1', compileFlag: 'EXTRACT_MEMORIES' },
|
||||
{ name: 'Memory skip index', gate: 'tengu_moth_copse', expected: true, category: 'P1' },
|
||||
{ name: 'Memory search section', gate: 'tengu_coral_fern', expected: true, category: 'P1' },
|
||||
{ name: 'Prompt suggestions', gate: 'tengu_chomp_inflection', expected: true, category: 'P1' },
|
||||
{ name: 'Verification agent', gate: 'tengu_hive_evidence', expected: true, category: 'P1', compileFlag: 'VERIFICATION_AGENT' },
|
||||
{ name: 'Brief mode', gate: 'tengu_kairos_brief', expected: true, category: 'P1', compileFlag: 'KAIROS_BRIEF' },
|
||||
{ name: 'Away summary', gate: 'tengu_sedge_lantern', expected: true, category: 'P1', compileFlag: 'AWAY_SUMMARY' },
|
||||
{ name: 'Idle return prompt', gate: 'tengu_willow_mode', expected: 'dialog', category: 'P1' },
|
||||
|
||||
// Kill switches
|
||||
{ name: 'Ultrathink', gate: 'tengu_turtle_carbon', expected: true, category: 'KS', compileFlag: 'ULTRATHINK' },
|
||||
{ name: 'Explore/Plan agents', gate: 'tengu_amber_stoat', expected: true, category: 'KS', compileFlag: 'BUILTIN_EXPLORE_PLAN_AGENTS' },
|
||||
{ name: 'Agent teams', gate: 'tengu_amber_flint', expected: true, category: 'KS' },
|
||||
{ name: 'Slim subagent CLAUDE.md', gate: 'tengu_slim_subagent_claudemd', expected: true, category: 'KS' },
|
||||
{ name: 'Bash security', gate: 'tengu_birch_trellis', expected: true, category: 'KS' },
|
||||
{ name: 'macOS clipboard', gate: 'tengu_collage_kaleidoscope', expected: true, category: 'KS' },
|
||||
{ name: 'Compact cache prefix', gate: 'tengu_compact_cache_prefix', expected: true, category: 'KS' },
|
||||
{ name: 'Durable cron', gate: 'tengu_kairos_cron_durable', expected: true, category: 'KS' },
|
||||
{ name: 'Attribution header', gate: 'tengu_attribution_header', expected: true, category: 'KS' },
|
||||
{ name: 'Agent progress', gate: 'tengu_slate_prism', expected: true, category: 'KS' },
|
||||
]
|
||||
|
||||
console.log('=== GrowthBook Local Gate Verification ===\n')
|
||||
|
||||
let pass = 0
|
||||
let fail = 0
|
||||
|
||||
for (const category of ['P0', 'P1', 'KS']) {
|
||||
const label = category === 'KS' ? 'Kill Switches' : category
|
||||
console.log(`--- ${label} ---`)
|
||||
|
||||
for (const check of gates.filter(g => g.category === category)) {
|
||||
const actual = typeof check.expected === 'boolean'
|
||||
? checkStatsigFeatureGate_CACHED_MAY_BE_STALE(check.gate)
|
||||
: getFeatureValue_CACHED_MAY_BE_STALE(check.gate, null)
|
||||
|
||||
const matches = typeof check.expected === 'boolean'
|
||||
? actual === check.expected
|
||||
: actual === check.expected || JSON.stringify(actual) === JSON.stringify(check.expected)
|
||||
|
||||
const status = matches ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'
|
||||
const flagNote = check.compileFlag ? ` [needs feature('${check.compileFlag}')]` : ''
|
||||
|
||||
console.log(` ${status} ${check.name}: ${check.gate} = ${JSON.stringify(actual)}${flagNote}`)
|
||||
|
||||
if (matches) pass++
|
||||
else fail++
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
console.log(`\nResult: ${pass} passed, ${fail} failed out of ${pass + fail} gates`)
|
||||
|
||||
if (fail > 0) {
|
||||
console.log('\n\x1b[31mSome gates are not returning expected values!\x1b[0m')
|
||||
console.log('If CLAUDE_CODE_DISABLE_LOCAL_GATES=1 is set, all gates will return defaults.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('\n\x1b[32mAll GrowthBook gates returning expected local defaults.\x1b[0m')
|
||||
console.log('\nNote: Compile-time feature() flags cannot be verified in this script.')
|
||||
console.log('Use "bun run dev" and test manually for features with [needs feature()] markers.')
|
||||
@@ -45,7 +45,7 @@ export default defineConfig({
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
outDir: "dist",
|
||||
target: "esnext",
|
||||
target: "es2020",
|
||||
copyPublicDir: false,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
|
||||
Reference in New Issue
Block a user