#!/usr/bin/env node 'use strict'; const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const DEFAULT_TARGET = 'bun-linux-x64-baseline'; const DEFAULT_OUTDIR = 'dist-exe'; const DEFAULT_NAME = 'cc-web'; function parseArgs(argv) { const options = { target: DEFAULT_TARGET, outdir: DEFAULT_OUTDIR, name: DEFAULT_NAME, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; const readValue = (name) => { const inlinePrefix = `${name}=`; if (arg.startsWith(inlinePrefix)) return arg.slice(inlinePrefix.length); if (arg === name && index + 1 < argv.length) { index += 1; return argv[index]; } return null; }; const target = readValue('--target'); if (target !== null) { options.target = target; continue; } const outdir = readValue('--outdir'); if (outdir !== null) { options.outdir = outdir; continue; } const name = readValue('--name'); if (name !== null) { options.name = name; continue; } if (arg === '-h' || arg === '--help') { options.help = true; continue; } throw new Error(`未知参数: ${arg}`); } return options; } function printHelp() { console.log(`用法: npm run build:single-exe node scripts/build-single-exe.js --target bun-linux-x64-baseline --outdir dist-exe --name cc-web 说明: 默认 target 是 ${DEFAULT_TARGET},用于兼容 CentOS 7 这类老 glibc Linux x64 系统。 该命令只打包 cc-web 服务本体;Claude/Codex CLI 仍在运行时从宿主机 PATH 或 CLAUDE_PATH/CODEX_PATH 调用。`); } function isWindowsTarget(target) { return /^bun-windows(?:-|$)/.test(String(target || '')); } function resolveBinaryName(target, name) { return isWindowsTarget(target) && !name.endsWith('.exe') ? `${name}.exe` : name; } function copyFileIfExists(source, destination) { if (!fs.existsSync(source)) return false; fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.copyFileSync(source, destination); return true; } function copyDirIfExists(source, destination) { if (!fs.existsSync(source)) return false; fs.cpSync(source, destination, { recursive: true, force: true }); return true; } function writeRunningGuide(releaseDir, target, binaryName) { const runCommand = isWindowsTarget(target) ? binaryName : `./${binaryName}`; const content = `# cc-web single executable 运行说明 这个目录是 Bun single executable 发布包,面向 CentOS 7 / 老 glibc Linux 运行。 ## 直接运行 \`\`\`bash chmod +x ${binaryName} PORT=8002 CC_WEB_PASSWORD='请改成强密码' ${runCommand} \`\`\` ## 调用宿主机 Claude/Codex CLI cc-web 不内置 Claude/Codex CLI。运行时会继续从宿主机 PATH 查找: \`\`\`bash export CLAUDE_PATH=/usr/local/bin/claude export CODEX_PATH=/usr/local/bin/codex ${runCommand} \`\`\` 如果不设置上述变量,默认命令名仍是 \`claude\` 和 \`codex\`。 ## 目录说明 - \`public/\`:前端静态资源 - \`config/\`:运行时配置,首次启动会写入登录和通知配置 - \`sessions/\`:会话数据 - \`logs/\`:运行日志 不要把已有生产环境的 \`config/\`、\`sessions/\` 误删或覆盖。 `; fs.writeFileSync(path.join(releaseDir, 'RUNNING.md'), content, 'utf8'); } function copyRuntimeAssets(projectRoot, releaseDir, target, binaryName) { copyDirIfExists(path.join(projectRoot, 'public'), path.join(releaseDir, 'public')); for (const file of ['.env.example', 'README.md', 'README.en.md', 'CHANGELOG.md', 'package.json']) { copyFileIfExists(path.join(projectRoot, file), path.join(releaseDir, file)); } for (const dir of ['.codex/skills', '.codex/prompts', '.agents/skills', '.agents/prompts']) { copyDirIfExists(path.join(projectRoot, dir), path.join(releaseDir, dir)); } for (const dir of ['config', 'sessions', 'logs']) { fs.mkdirSync(path.join(releaseDir, dir), { recursive: true }); } writeRunningGuide(releaseDir, target, binaryName); } function runBunBuild(projectRoot, target, outfile) { const bunCommand = process.versions.bun ? process.execPath : (process.env.BUN_BIN || 'bun'); const args = [ 'build', '--compile', '--no-compile-autoload-dotenv', `--target=${target}`, `--outfile=${outfile}`, path.join(projectRoot, 'server.js'), ]; console.log(`[build:single-exe] ${bunCommand} ${args.join(' ')}`); const result = spawnSync(bunCommand, args, { cwd: projectRoot, env: process.env, stdio: 'inherit', }); if (result.error && result.error.code === 'ENOENT') { throw new Error('未找到 bun。请先在构建机安装 Bun,或通过 BUN_BIN 指定 bun 可执行文件。'); } if (result.error) throw result.error; if (result.status !== 0) { throw new Error(`Bun 编译失败,退出码: ${result.status}`); } } function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printHelp(); return; } const projectRoot = path.resolve(__dirname, '..'); const target = options.target || DEFAULT_TARGET; const binaryName = resolveBinaryName(target, options.name || DEFAULT_NAME); const outdir = path.resolve(projectRoot, options.outdir || DEFAULT_OUTDIR); const releaseDir = path.join(outdir, target); const outfile = path.join(releaseDir, binaryName); fs.rmSync(releaseDir, { recursive: true, force: true }); fs.mkdirSync(releaseDir, { recursive: true }); runBunBuild(projectRoot, target, outfile); if (!isWindowsTarget(target)) fs.chmodSync(outfile, 0o755); copyRuntimeAssets(projectRoot, releaseDir, target, binaryName); console.log(`[build:single-exe] 发布目录: ${releaseDir}`); console.log(`[build:single-exe] 可执行文件: ${outfile}`); } try { main(); } catch (err) { console.error(`[build:single-exe] ERROR: ${err.message || err}`); process.exit(1); }