#!/usr/bin/env node /** * pack-release.mjs —— 生成可直接上传 SkillHub 的发布 zip * * 产物:release/fmode-skills--.zip * 内容:dist/ 全部技能 + 根 SKILL.md + README + manifest(不含内部脚本、不含缓存) * * 上传方式:SkillHub 后台「上传技能包」→ 选这个 zip */ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const DIST = path.join(ROOT, 'dist'); const RELEASE = path.join(ROOT, 'release'); const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' }; if (!fs.existsSync(DIST)) { console.error('dist 不存在,先跑 npm run build'); process.exit(1); } // 版本 const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); const version = pkg.version || '1.0.0'; const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const stamp = `${version}-${date}`; fs.mkdirSync(RELEASE, { recursive: true }); const stageDir = path.join(RELEASE, `.stage-${stamp}`); fs.rmSync(stageDir, { recursive: true, force: true }); fs.mkdirSync(stageDir, { recursive: true }); /** 递归复制,跳过 .git / 缓存 / 内部脚本 */ const SKIP = [ /(^|\/)\.git(\/|$)/, /(^|\/)__pycache__(\/|$)/, /(^|\/)node_modules(\/|$)/, /\.pyc$/, /\.log$/, /\.bak/, /\.DS_Store$/, /(^|\/)gen-.*\.(py|sh)$/, // 内部生成脚本 /(^|\/)\.legacy(\/|$)/, /(^|\/)\.cache(\/|$)/, ]; function copyTree(src, dest, rel = '') { fs.mkdirSync(dest, { recursive: true }); for (const e of fs.readdirSync(src, { withFileTypes: true })) { const relPath = rel ? `${rel}/${e.name}` : e.name; if (SKIP.some(re => re.test(relPath))) continue; const s = path.join(src, e.name); // SkillHub 拒绝无扩展名文件:LICENSE → LICENSE.md const outName = e.name === 'LICENSE' ? 'LICENSE.md' : e.name; const d = path.join(dest, outName); if (e.isDirectory()) copyTree(s, d, relPath); else if (e.isFile()) fs.copyFileSync(s, d); } } // 1. 复制 dist 里的技能 copyTree(DIST, stageDir); // 2. 复制根级 meta(SKILL.md / README / manifest) for (const f of ['SKILL.md', 'README.md', 'skill-package-manifest.json']) { const src = path.join(ROOT, f); if (fs.existsSync(src)) fs.copyFileSync(src, path.join(stageDir, f)); } // 3. 生成/更新 package.json(SkillHub 读这个) const outPkg = { name: pkg.name || 'fmode-harness', version, description: pkg.description, type: 'module', license: pkg.license || 'MPL-2.0', keywords: pkg.keywords, engines: pkg.engines, scripts: { install: 'node scripts/installer.mjs --all', check: 'node scripts/installer.mjs --check', }, }; fs.writeFileSync(path.join(stageDir, 'package.json'), JSON.stringify(outPkg, null, 2) + '\n'); // 3.5 生成 _meta.json(SkillHub 包元数据) const meta = { slug: pkg.name || 'fmode-harness', name: pkg.name || 'fmode-harness', version, displayName: 'Fmode Harness 技能库', publishedAt: Date.now(), }; fs.writeFileSync(path.join(stageDir, '_meta.json'), JSON.stringify(meta, null, 2) + '\n'); // 4. 检查 installer 是否存在(技能安装入口) const installerSrc = path.join(ROOT, 'scripts', 'installer.mjs'); if (fs.existsSync(installerSrc)) { fs.mkdirSync(path.join(stageDir, 'scripts'), { recursive: true }); fs.copyFileSync(installerSrc, path.join(stageDir, 'scripts', 'installer.mjs')); } // 5. 统计 & 自检 function walk(d, out = []) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p, out); else out.push(p); } return out; } const files = walk(stageDir); const skills = fs.existsSync(path.join(stageDir, 'skills')) ? fs.readdirSync(path.join(stageDir, 'skills')).filter(n => fs.statSync(path.join(stageDir, 'skills', n)).isDirectory()) : []; // 自检:密钥模式 const SECRET_RE = [ /\bsk-[a-zA-Z0-9]{20,}/, /\br:[a-z0-9]{20,}/, /AKIA[0-9A-Z]{16}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /(192\.168\.[0-9]{1,3}\.[0-9]{1,3})/, /(139\.159\.253\.131|172\.0\.0\.100)/, /(db\.fmode\.cn|userland\.fmode\.cn)/, ]; let leaks = 0; for (const f of files) { if (!/\.(md|json|mjs|js|sh|py|txt|html|css)$/i.test(f)) continue; const c = fs.readFileSync(f, 'utf8'); for (const re of SECRET_RE) { if (re.test(c)) { console.log(`${C.r}✗ 疑似敏感${C.x} ${path.relative(stageDir, f)} → ${re}`); leaks++; } } } // 5.5 发布前内容体检:内部地址 / 外部依赖 / 内部 IP const CONTENT_CHECKS = [ { name: '内部 Git 地址', re: /git\.fmode\.cn/, hint: '改为"已随包分发,无需安装"' }, { name: 'npx 外部包依赖', re: /npx (--yes )?(@fmode\/|skill-|fmode-)/, hint: '改用包内脚本 <技能目录>/bin|scripts/' }, { name: '内网 IP', re: /(? 0 ? 1 : 0);