589 lines
19 KiB
JavaScript
589 lines
19 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* Bookworm Portable 发行版构建器
|
|||
|
|
*
|
|||
|
|
* 从管理员 .claude/ 源目录编译出脱敏、混淆的 Portable 分发版本。
|
|||
|
|
*
|
|||
|
|
* 步骤:
|
|||
|
|
* 1. 复制源文件到 dist-portable/
|
|||
|
|
* 2. 替换 CLAUDE.md → 脱敏模板
|
|||
|
|
* 3. 替换 settings.json → Portable 模板 (含 NDA hooks)
|
|||
|
|
* 4. terser 混淆 JS hooks (剥离注释 + 压缩)
|
|||
|
|
* 5. SKILL.md 脱敏 (移除触发关键词、参考资源、内部注释)
|
|||
|
|
* 6. Agent .md 脱敏 (移除内部注释)
|
|||
|
|
* 7. 移除管理员专属文件 (memory, debug, __tests__, .disabled, _deprecated, templates)
|
|||
|
|
* 8. 生成 integrity.sha256 完整性哈希
|
|||
|
|
* 9. 输出构建报告
|
|||
|
|
*
|
|||
|
|
* 用法:
|
|||
|
|
* node scripts/build-portable.js [--out <dir>] [--no-minify] [--dry-run]
|
|||
|
|
*
|
|||
|
|
* 输出:
|
|||
|
|
* 默认 ~/.claude/dist-portable/
|
|||
|
|
*/
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const { execSync } = require('child_process');
|
|||
|
|
|
|||
|
|
// --- 参数解析 ---
|
|||
|
|
const args = process.argv.slice(2);
|
|||
|
|
const DRY_RUN = args.includes('--dry-run');
|
|||
|
|
const NO_MINIFY = args.includes('--no-minify');
|
|||
|
|
const outIdx = args.indexOf('--out');
|
|||
|
|
const SRC = path.resolve(path.join(__dirname, '..'));
|
|||
|
|
const DIST = outIdx >= 0 && args[outIdx + 1]
|
|||
|
|
? path.resolve(args[outIdx + 1])
|
|||
|
|
: path.join(SRC, 'dist-portable');
|
|||
|
|
|
|||
|
|
// --- 构建统计 ---
|
|||
|
|
const stats = {
|
|||
|
|
copied: 0,
|
|||
|
|
skipped: 0,
|
|||
|
|
minified: 0,
|
|||
|
|
sanitized: 0,
|
|||
|
|
removed: 0,
|
|||
|
|
errors: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// --- 工具函数 ---
|
|||
|
|
function log(msg) { console.log(` ${msg}`); }
|
|||
|
|
function logStep(msg) { console.log(`\n--- ${msg} ---`); }
|
|||
|
|
|
|||
|
|
function sha256File(filePath) {
|
|||
|
|
return crypto.createHash('sha256')
|
|||
|
|
.update(fs.readFileSync(filePath))
|
|||
|
|
.digest('hex');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ensureDir(dir) {
|
|||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 递归复制目录,跳过排除项
|
|||
|
|
*/
|
|||
|
|
function copyDirSync(src, dest, excludes = []) {
|
|||
|
|
ensureDir(dest);
|
|||
|
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|||
|
|
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
const srcPath = path.join(src, entry.name);
|
|||
|
|
const destPath = path.join(dest, entry.name);
|
|||
|
|
const relPath = path.relative(SRC, srcPath).replace(/\\/g, '/');
|
|||
|
|
|
|||
|
|
if (excludes.some(ex => {
|
|||
|
|
if (typeof ex === 'string') return relPath === ex || relPath.startsWith(ex + '/');
|
|||
|
|
return ex.test(relPath);
|
|||
|
|
})) {
|
|||
|
|
stats.skipped++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (entry.isDirectory()) {
|
|||
|
|
copyDirSync(srcPath, destPath, excludes);
|
|||
|
|
} else {
|
|||
|
|
fs.copyFileSync(srcPath, destPath);
|
|||
|
|
stats.copied++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 1: 白名单复制 (只复制 Bookworm 配置,忽略 Claude Code 运行时数据)
|
|||
|
|
// ========================================================================
|
|||
|
|
function step1_copy() {
|
|||
|
|
logStep('1. 白名单复制');
|
|||
|
|
|
|||
|
|
if (fs.existsSync(DIST)) {
|
|||
|
|
// 保留 .git 目录 (Gitea 推送仓库),只清除内容
|
|||
|
|
const gitDir = path.join(DIST, '.git');
|
|||
|
|
const hasGit = fs.existsSync(gitDir);
|
|||
|
|
if (hasGit) {
|
|||
|
|
// 删除 .git 以外的所有内容
|
|||
|
|
for (const entry of fs.readdirSync(DIST)) {
|
|||
|
|
if (entry === '.git') continue;
|
|||
|
|
fs.rmSync(path.join(DIST, entry), { recursive: true, force: true });
|
|||
|
|
}
|
|||
|
|
log('清除旧内容 (保留 .git)');
|
|||
|
|
} else {
|
|||
|
|
fs.rmSync(DIST, { recursive: true, force: true });
|
|||
|
|
log('清除旧 dist-portable/');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- 白名单: 只复制这些顶级目录/文件 ---
|
|||
|
|
const INCLUDE_DIRS = [
|
|||
|
|
'skills',
|
|||
|
|
'agents',
|
|||
|
|
'hooks',
|
|||
|
|
'scripts',
|
|||
|
|
'constitution',
|
|||
|
|
'rules',
|
|||
|
|
'docs',
|
|||
|
|
];
|
|||
|
|
const INCLUDE_FILES = [
|
|||
|
|
'CLAUDE.md',
|
|||
|
|
'settings.json',
|
|||
|
|
'settings.local.template.json',
|
|||
|
|
'settings.template.json',
|
|||
|
|
'SKILL-REGISTRY.md',
|
|||
|
|
'feature-flags.json',
|
|||
|
|
'integrity.sha256',
|
|||
|
|
'package.json',
|
|||
|
|
'skills-index.json',
|
|||
|
|
'skills-index-lite.json',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// --- 排除: 白名单目录内部的不需要项 ---
|
|||
|
|
const EXCLUDES = [
|
|||
|
|
'.git',
|
|||
|
|
'hooks/__tests__',
|
|||
|
|
'hooks/deprecated',
|
|||
|
|
'hooks/tests',
|
|||
|
|
'skills/_deprecated',
|
|||
|
|
/\.disabled$/,
|
|||
|
|
/\.self-hash$/,
|
|||
|
|
/checksums\.sig$/,
|
|||
|
|
/node_modules/,
|
|||
|
|
/package-lock\.json$/,
|
|||
|
|
/pnpm-lock\.yaml$/,
|
|||
|
|
/yarn\.lock$/,
|
|||
|
|
// Skill 重型子目录 (任意深度)
|
|||
|
|
/^skills\/.*\/(?:src|dist|build|test|__tests__|bin|\.next|\.turbo|coverage)(?:\/|$)/,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
if (DRY_RUN) {
|
|||
|
|
log('[DRY RUN] 将复制 ' + SRC + ' → ' + DIST);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ensureDir(DIST);
|
|||
|
|
|
|||
|
|
// 复制白名单文件
|
|||
|
|
for (const file of INCLUDE_FILES) {
|
|||
|
|
const srcPath = path.join(SRC, file);
|
|||
|
|
if (fs.existsSync(srcPath)) {
|
|||
|
|
fs.copyFileSync(srcPath, path.join(DIST, file));
|
|||
|
|
stats.copied++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 复制白名单目录
|
|||
|
|
for (const dir of INCLUDE_DIRS) {
|
|||
|
|
const srcPath = path.join(SRC, dir);
|
|||
|
|
if (fs.existsSync(srcPath)) {
|
|||
|
|
copyDirSync(srcPath, path.join(DIST, dir), EXCLUDES);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log(`复制完成: ${stats.copied} 文件, 跳过 ${stats.skipped} 项`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 2: 替换 CLAUDE.md
|
|||
|
|
// ========================================================================
|
|||
|
|
function step2_claudeMd() {
|
|||
|
|
logStep('2. 替换 CLAUDE.md → 脱敏模板');
|
|||
|
|
|
|||
|
|
const templatePath = path.join(SRC, 'templates', 'CLAUDE-portable.md');
|
|||
|
|
const destPath = path.join(DIST, 'CLAUDE.md');
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(templatePath)) {
|
|||
|
|
stats.errors.push('模板文件不存在: templates/CLAUDE-portable.md');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从 stats-compiled.json 读取版本号
|
|||
|
|
let version = 'v6.5.1';
|
|||
|
|
try {
|
|||
|
|
const statsFile = path.join(SRC, 'stats-compiled.json');
|
|||
|
|
const compiled = JSON.parse(fs.readFileSync(statsFile, 'utf8'));
|
|||
|
|
version = compiled.summary?.version || version;
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
let content = fs.readFileSync(templatePath, 'utf8');
|
|||
|
|
content = content.replace(/\{version\}/g, version);
|
|||
|
|
|
|||
|
|
if (!DRY_RUN) {
|
|||
|
|
fs.writeFileSync(destPath, content, 'utf8');
|
|||
|
|
}
|
|||
|
|
log(`CLAUDE.md 替换完成 (版本: ${version})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 3: 替换 settings.json
|
|||
|
|
// ========================================================================
|
|||
|
|
function step3_settings() {
|
|||
|
|
logStep('3. 替换 settings.json → Portable 模板');
|
|||
|
|
|
|||
|
|
const templatePath = path.join(SRC, 'templates', 'settings.portable.json');
|
|||
|
|
const destPath = path.join(DIST, 'settings.json');
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(templatePath)) {
|
|||
|
|
stats.errors.push('模板文件不存在: templates/settings.portable.json');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// settings.portable.json 中的 {{CLAUDE_DIR}} 和 {{HOME}} 保留原样
|
|||
|
|
// install.ps1 在安装时会替换这些占位符
|
|||
|
|
if (!DRY_RUN) {
|
|||
|
|
fs.copyFileSync(templatePath, destPath);
|
|||
|
|
// 同时替换 settings.template.json — install.ps1 用它渲染 settings.json
|
|||
|
|
// 不替换会导致 NDA hooks 被管理员版模板覆盖 (P0 bug fix)
|
|||
|
|
const templateDest = path.join(DIST, 'settings.template.json');
|
|||
|
|
fs.copyFileSync(templatePath, templateDest);
|
|||
|
|
}
|
|||
|
|
log('settings.json + settings.template.json 替换完成 (含 NDA hooks 注册)');
|
|||
|
|
|
|||
|
|
// 3b: 用 standalone 版替换 nda-read-guard.js (内联依赖, 混淆后无 require 链开销)
|
|||
|
|
const standaloneSrc = path.join(SRC, 'hooks', 'nda-read-guard.standalone.js');
|
|||
|
|
const guardDest = path.join(DIST, 'hooks', 'nda-read-guard.js');
|
|||
|
|
if (fs.existsSync(standaloneSrc) && !DRY_RUN) {
|
|||
|
|
fs.copyFileSync(standaloneSrc, guardDest);
|
|||
|
|
log('nda-read-guard.js → standalone 版替换完成');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 4: JS Hook 混淆
|
|||
|
|
// ========================================================================
|
|||
|
|
function step4_minifyHooks() {
|
|||
|
|
logStep('4. JS Hook 混淆');
|
|||
|
|
|
|||
|
|
if (NO_MINIFY) {
|
|||
|
|
log('[SKIP] --no-minify 标志已设置');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const hooksDir = path.join(DIST, 'hooks');
|
|||
|
|
if (!fs.existsSync(hooksDir)) return;
|
|||
|
|
|
|||
|
|
const jsFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
|
|||
|
|
let terserAvailable = false;
|
|||
|
|
|
|||
|
|
// 检测 terser
|
|||
|
|
try {
|
|||
|
|
execSync('npx terser --version', { stdio: 'pipe', timeout: 10000 });
|
|||
|
|
terserAvailable = true;
|
|||
|
|
} catch {
|
|||
|
|
log('terser 不可用, 退回注释剥离模式');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const file of jsFiles) {
|
|||
|
|
const filePath = path.join(hooksDir, file);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (terserAvailable && !DRY_RUN) {
|
|||
|
|
// terser 混淆: 压缩 + 去注释 + 混淆变量名
|
|||
|
|
execSync(
|
|||
|
|
`npx terser "${filePath}" --compress --mangle --output "${filePath}" --comments false`,
|
|||
|
|
{ stdio: 'pipe', timeout: 60000 }
|
|||
|
|
);
|
|||
|
|
} else if (!DRY_RUN) {
|
|||
|
|
// 退回: 仅剥离注释和空行
|
|||
|
|
let code = fs.readFileSync(filePath, 'utf8');
|
|||
|
|
code = code
|
|||
|
|
.replace(/\/\*[\s\S]*?\*\//g, '') // 多行注释
|
|||
|
|
.replace(/\/\/.*$/gm, '') // 单行注释
|
|||
|
|
.replace(/^\s*\n/gm, ''); // 空行
|
|||
|
|
fs.writeFileSync(filePath, code, 'utf8');
|
|||
|
|
}
|
|||
|
|
stats.minified++;
|
|||
|
|
} catch (e) {
|
|||
|
|
stats.errors.push(`混淆失败: hooks/${file} — ${e.message?.slice(0, 100)}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 同时混淆 hooks/lib/ 下的模块
|
|||
|
|
const libDir = path.join(hooksDir, 'lib');
|
|||
|
|
if (fs.existsSync(libDir)) {
|
|||
|
|
const libFiles = fs.readdirSync(libDir).filter(f => f.endsWith('.js'));
|
|||
|
|
for (const file of libFiles) {
|
|||
|
|
const filePath = path.join(libDir, file);
|
|||
|
|
try {
|
|||
|
|
if (terserAvailable && !DRY_RUN) {
|
|||
|
|
execSync(
|
|||
|
|
`npx terser "${filePath}" --compress --mangle --output "${filePath}" --comments false`,
|
|||
|
|
{ stdio: 'pipe', timeout: 30000 }
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
stats.minified++;
|
|||
|
|
} catch (e) {
|
|||
|
|
stats.errors.push(`混淆失败: hooks/lib/${file} — ${e.message?.slice(0, 100)}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log(`混淆完成: ${stats.minified} 个 JS 文件`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 5: SKILL.md 脱敏
|
|||
|
|
// ========================================================================
|
|||
|
|
function step5_sanitizeSkills() {
|
|||
|
|
logStep('5. SKILL.md 脱敏');
|
|||
|
|
|
|||
|
|
const skillsDir = path.join(DIST, 'skills');
|
|||
|
|
if (!fs.existsSync(skillsDir)) return;
|
|||
|
|
|
|||
|
|
const skills = fs.readdirSync(skillsDir).filter(f => {
|
|||
|
|
const fp = path.join(skillsDir, f);
|
|||
|
|
return fs.statSync(fp).isDirectory() && f !== '_deprecated';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 需要移除的段落模式 (匹配 ##/### 开头到下一个同级标题)
|
|||
|
|
const STRIP_SECTIONS = [
|
|||
|
|
// 触发关键词表 — 暴露路由边界
|
|||
|
|
/## 触发关键词[\s\S]*?(?=\n## |\n---\s*$|$)/g,
|
|||
|
|
// 参考资源/参考文档 — 暴露内部文件路径
|
|||
|
|
/## 参考资源[\s\S]*?(?=\n## |\n---\s*$|$)/g,
|
|||
|
|
/## 参考文档[\s\S]*?(?=\n## |\n---\s*$|$)/g,
|
|||
|
|
// 内部注释 (HTML 注释)
|
|||
|
|
/<!--[\s\S]*?-->/g,
|
|||
|
|
// ARGUMENTS 行 (有时包含调试信息)
|
|||
|
|
/^ARGUMENTS:.*$/gm,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 敏感关键词替换 (模糊化内部术语)
|
|||
|
|
const REDACT_PATTERNS = [
|
|||
|
|
// BWR 路由引用
|
|||
|
|
[/BWR[: ]/g, ''],
|
|||
|
|
// Hook 名称引用
|
|||
|
|
[/(?:route-interceptor|prompt-dispatcher|bash-precheck)[a-z-]*/gi, ''],
|
|||
|
|
// 内部文件路径
|
|||
|
|
[/scripts\/[a-z-]+\.js/gi, ''],
|
|||
|
|
[/hooks\/[a-z-]+\.js/gi, ''],
|
|||
|
|
// stats-compiled 引用
|
|||
|
|
[/stats-compiled\.json/gi, ''],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let sanitizedCount = 0;
|
|||
|
|
for (const skill of skills) {
|
|||
|
|
const mdPath = path.join(skillsDir, skill, 'SKILL.md');
|
|||
|
|
if (!fs.existsSync(mdPath)) continue;
|
|||
|
|
|
|||
|
|
let content = fs.readFileSync(mdPath, 'utf8');
|
|||
|
|
const original = content;
|
|||
|
|
|
|||
|
|
// 移除敏感段落
|
|||
|
|
for (const pattern of STRIP_SECTIONS) {
|
|||
|
|
content = content.replace(pattern, '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 替换敏感关键词
|
|||
|
|
for (const [pattern, replacement] of REDACT_PATTERNS) {
|
|||
|
|
content = content.replace(pattern, replacement);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理多余空行 (脱敏后可能留下连续空行)
|
|||
|
|
content = content.replace(/\n{3,}/g, '\n\n');
|
|||
|
|
|
|||
|
|
if (content !== original) {
|
|||
|
|
if (!DRY_RUN) fs.writeFileSync(mdPath, content, 'utf8');
|
|||
|
|
sanitizedCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stats.sanitized += sanitizedCount;
|
|||
|
|
log(`脱敏完成: ${sanitizedCount}/${skills.length} 个 SKILL.md 被修改`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 6: Agent .md 脱敏
|
|||
|
|
// ========================================================================
|
|||
|
|
function step6_sanitizeAgents() {
|
|||
|
|
logStep('6. Agent .md 脱敏');
|
|||
|
|
|
|||
|
|
const agentsDir = path.join(DIST, 'agents');
|
|||
|
|
if (!fs.existsSync(agentsDir)) return;
|
|||
|
|
|
|||
|
|
const agents = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
|
|||
|
|
|
|||
|
|
let sanitizedCount = 0;
|
|||
|
|
for (const file of agents) {
|
|||
|
|
const filePath = path.join(agentsDir, file);
|
|||
|
|
let content = fs.readFileSync(filePath, 'utf8');
|
|||
|
|
const original = content;
|
|||
|
|
|
|||
|
|
// 移除 HTML 注释
|
|||
|
|
content = content.replace(/<!--[\s\S]*?-->/g, '');
|
|||
|
|
// 移除内部文件路径引用
|
|||
|
|
content = content.replace(/scripts\/[a-z-]+\.(js|json)/gi, '');
|
|||
|
|
content = content.replace(/hooks\/[a-z-]+\.js/gi, '');
|
|||
|
|
// 清理多余空行
|
|||
|
|
content = content.replace(/\n{3,}/g, '\n\n');
|
|||
|
|
|
|||
|
|
if (content !== original) {
|
|||
|
|
if (!DRY_RUN) fs.writeFileSync(filePath, content, 'utf8');
|
|||
|
|
sanitizedCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stats.sanitized += sanitizedCount;
|
|||
|
|
log(`脱敏完成: ${sanitizedCount}/${agents.length} 个 Agent .md 被修改`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 7: 清理残留管理员文件
|
|||
|
|
// ========================================================================
|
|||
|
|
function step7_cleanup() {
|
|||
|
|
logStep('7. 清理残留文件');
|
|||
|
|
|
|||
|
|
const CLEANUP_PATTERNS = [
|
|||
|
|
// MEMORY.md 索引 (指向管理员记忆)
|
|||
|
|
'MEMORY.md',
|
|||
|
|
// 项目级记忆 (projects/*/memory/) — 保留 projects/*/CLAUDE.md
|
|||
|
|
// docs 中的内部文档
|
|||
|
|
'docs/active-projects.md',
|
|||
|
|
'docs/project-config.md',
|
|||
|
|
// stats-compiled.json (含详细内部统计)
|
|||
|
|
'stats-compiled.json',
|
|||
|
|
// evolution-log (系统进化日志)
|
|||
|
|
'evolution-log.jsonl',
|
|||
|
|
// checksums.json (需重新生成)
|
|||
|
|
'hooks/checksums.json',
|
|||
|
|
'hooks/checksums.sig',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let removed = 0;
|
|||
|
|
for (const rel of CLEANUP_PATTERNS) {
|
|||
|
|
const fp = path.join(DIST, rel);
|
|||
|
|
if (fs.existsSync(fp)) {
|
|||
|
|
if (!DRY_RUN) fs.rmSync(fp, { force: true });
|
|||
|
|
removed++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stats.removed += removed;
|
|||
|
|
log(`清理完成: ${removed} 个残留文件移除`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 8: 生成 integrity.sha256
|
|||
|
|
// ========================================================================
|
|||
|
|
function step8_integrity() {
|
|||
|
|
logStep('8. 生成 integrity.sha256');
|
|||
|
|
|
|||
|
|
const checksums = {};
|
|||
|
|
|
|||
|
|
function walkDir(dir, prefix = '') {
|
|||
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
if (entry.isDirectory()) {
|
|||
|
|
walkDir(path.join(dir, entry.name), prefix + entry.name + '/');
|
|||
|
|
} else {
|
|||
|
|
const relPath = prefix + entry.name;
|
|||
|
|
const fullPath = path.join(dir, entry.name);
|
|||
|
|
checksums[relPath] = sha256File(fullPath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!DRY_RUN) {
|
|||
|
|
// 只哈希关键目录
|
|||
|
|
const KEY_DIRS = ['hooks', 'skills', 'agents', 'constitution', 'rules'];
|
|||
|
|
const KEY_FILES = ['CLAUDE.md', 'settings.json'];
|
|||
|
|
|
|||
|
|
for (const dir of KEY_DIRS) {
|
|||
|
|
const dirPath = path.join(DIST, dir);
|
|||
|
|
if (fs.existsSync(dirPath)) walkDir(dirPath, dir + '/');
|
|||
|
|
}
|
|||
|
|
for (const file of KEY_FILES) {
|
|||
|
|
const fp = path.join(DIST, file);
|
|||
|
|
if (fs.existsSync(fp)) checksums[file] = sha256File(fp);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(DIST, 'hooks', 'checksums.json'),
|
|||
|
|
JSON.stringify(checksums, null, 2),
|
|||
|
|
'utf8'
|
|||
|
|
);
|
|||
|
|
log(`完整性哈希: ${Object.keys(checksums).length} 个文件`);
|
|||
|
|
} else {
|
|||
|
|
log('[DRY RUN] 将生成完整性哈希');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 步骤 9: 构建报告
|
|||
|
|
// ========================================================================
|
|||
|
|
function step9_report() {
|
|||
|
|
logStep('9. 构建报告');
|
|||
|
|
|
|||
|
|
// 统计 dist 大小
|
|||
|
|
let totalSize = 0;
|
|||
|
|
let fileCount = 0;
|
|||
|
|
function countDir(dir) {
|
|||
|
|
if (!fs.existsSync(dir)) return;
|
|||
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
const fp = path.join(dir, entry.name);
|
|||
|
|
if (entry.isDirectory()) {
|
|||
|
|
countDir(fp);
|
|||
|
|
} else {
|
|||
|
|
totalSize += fs.statSync(fp).size;
|
|||
|
|
fileCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!DRY_RUN) countDir(DIST);
|
|||
|
|
|
|||
|
|
console.log(`
|
|||
|
|
╔════════════════════════════════════════════╗
|
|||
|
|
║ Bookworm Portable Build Report ║
|
|||
|
|
╠════════════════════════════════════════════╣
|
|||
|
|
║ 输出目录: ${DIST.slice(-35).padEnd(30)} ║
|
|||
|
|
║ 文件总数: ${String(fileCount).padEnd(30)} ║
|
|||
|
|
║ 总大小: ${((totalSize / 1024 / 1024).toFixed(1) + ' MB').padEnd(30)} ║
|
|||
|
|
║ ──────────────────────────────────────── ║
|
|||
|
|
║ 复制: ${String(stats.copied).padEnd(30)} ║
|
|||
|
|
║ 跳过: ${String(stats.skipped).padEnd(30)} ║
|
|||
|
|
║ 混淆: ${String(stats.minified).padEnd(30)} ║
|
|||
|
|
║ 脱敏: ${String(stats.sanitized).padEnd(30)} ║
|
|||
|
|
║ 移除: ${String(stats.removed).padEnd(30)} ║
|
|||
|
|
║ 错误: ${String(stats.errors.length).padEnd(30)} ║
|
|||
|
|
╚════════════════════════════════════════════╝`);
|
|||
|
|
|
|||
|
|
if (stats.errors.length > 0) {
|
|||
|
|
console.log('\n⚠ 错误详情:');
|
|||
|
|
for (const err of stats.errors) {
|
|||
|
|
console.log(` - ${err}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (DRY_RUN) {
|
|||
|
|
console.log('\n[DRY RUN] 未实际写入文件。去掉 --dry-run 执行实际构建。');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========================================================================
|
|||
|
|
// 主流程
|
|||
|
|
// ========================================================================
|
|||
|
|
console.log(`
|
|||
|
|
╔════════════════════════════════════════════╗
|
|||
|
|
║ Bookworm Portable Builder ║
|
|||
|
|
║ Source: ${SRC.slice(-35).padEnd(33)} ║
|
|||
|
|
║ Output: ${DIST.slice(-35).padEnd(33)} ║
|
|||
|
|
╚════════════════════════════════════════════╝`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
step1_copy();
|
|||
|
|
step2_claudeMd();
|
|||
|
|
step3_settings();
|
|||
|
|
step4_minifyHooks();
|
|||
|
|
step5_sanitizeSkills();
|
|||
|
|
step6_sanitizeAgents();
|
|||
|
|
step7_cleanup();
|
|||
|
|
step8_integrity();
|
|||
|
|
step9_report();
|
|||
|
|
|
|||
|
|
process.exit(stats.errors.length > 0 ? 1 : 0);
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('\n❌ 构建失败:', e.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|