| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- #!/usr/bin/env node
- /**
- * pack-release.mjs —— 生成可直接上传 SkillHub 的发布 zip
- *
- * 产物:release/fmode-skills-<version>-<date>.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);
- const d = path.join(dest, e.name);
- 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: 'fmode-skills',
- 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');
- // 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++;
- }
- }
- }
- // 6. 打 zip
- const zipName = `fmode-skills-${stamp}.zip`;
- const zipPath = path.join(RELEASE, zipName);
- fs.rmSync(zipPath, { force: true });
- try {
- execFileSync('zip', ['-rq', zipName, '.'], { cwd: stageDir, stdio: 'pipe' });
- } catch (e) {
- // 无 zip 命令,用 Node 的 python 兜底
- try {
- execFileSync('python3', ['-c', `
- import zipfile, os
- z = zipfile.ZipFile(${JSON.stringify(zipPath)}, 'w', zipfile.ZIP_DEFLATED)
- for root, dirs, fs in os.walk(${JSON.stringify(stageDir)}):
- for f in fs:
- p = os.path.join(root, f)
- z.write(p, os.path.relpath(p, ${JSON.stringify(stageDir)}))
- z.close()
- `], { stdio: 'pipe' });
- } catch (e2) {
- console.error('打包失败:无 zip 命令且 python 兜底也失败');
- process.exit(1);
- }
- }
- const size = fs.statSync(zipPath).size;
- fs.rmSync(stageDir, { recursive: true, force: true });
- console.log(`\n${C.b}发布包已生成${C.x}\n`);
- console.log(` 文件: ${zipPath}`);
- console.log(` 大小: ${(size / 1024).toFixed(1)} KB`);
- console.log(` 技能: ${skills.length} 个 — ${skills.join(', ')}`);
- console.log(` 文件数: ${files.length}`);
- console.log(` 敏感自检: ${leaks === 0 ? `${C.g}通过(0 处)${C.x}` : `${C.r}${leaks} 处需处理${C.x}`}`);
- console.log(`\n${C.d}上传:SkillHub 后台 → 上传技能包 → 选此 zip${C.x}\n`);
- process.exit(leaks > 0 ? 1 : 0);
|