#!/usr/bin/env node /** * verify-refs.mjs v2 —— 检查 dist 里文档引用的文件是否真实存在 * 区分:真实依赖(必须存在) vs 示例性提及(可忽略) */ import fs from 'node:fs'; import path from 'node:path'; const args = process.argv.slice(2); const CWD = process.cwd(); const SHOW_ALL = args.includes('--all'); const pathArg = args.find(a => !a.startsWith('--')); const DIST = path.resolve(pathArg || path.join(CWD, 'dist')); const skillsRoot = path.join(DIST, 'skills'); if (!fs.existsSync(skillsRoot)) { console.error('dist/skills 不存在,先跑 npm run build'); process.exit(1); } const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' }; function allMd(dir, out = []) { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) allMd(p, out); else if (e.name.endsWith('.md')) out.push(p); } return out; } function extractRefs(text) { const refs = new Set(); const patterns = [ /`([^`\n]+\.(?:md|mjs|js|cjs|json|sh|py|ts|html|css))`/g, /\]\(\.?\/?([^)\s]+\.(?:md|mjs|js|json|sh|py))\)/g, /(?:详见|参见|见|参考)\s*[`「]?([\w./-]+\.(?:md|mjs|js|json|sh|py))/g, ]; for (const re of patterns) { let m; while ((m = re.exec(text)) !== null) { const p = m[1].trim(); if (!p.includes(' ')) refs.add(p); } } return [...refs]; } /** 判定是否为示例性提及(无需真实存在) */ function isExample(ref) { // 通配符 / 通配占位 if (/[*?{}]/.test(ref)) return true; // 占位符形式 if (/<[^>]+>/.test(ref)) return true; // 通用命名示例 if (/^(kebab-case|PascalCase|xxx|Xxx|your-|my-|page-)/.test(ref)) return true; // 典型项目入口文件(描述用户项目结构,非技能资产) if (/^(\.?\/)?(index\.html|app\.js|index\.js|main\.js|plain\.html|theme\.css)$/.test(ref)) return true; // 用户项目目录结构示例(技能教用户怎么建,文件由用户项目生成) if (/^(src|lib|components|pages|routes|services|utils|public|assets|dist|build|workdir|user|modules)\//.test(ref)) return true; // 运行时生成物(技能运行时产出,非技能包内资产) if (/^(mint\.json|collect\.sh|url\.txt|report\.html|.*\.log)$/.test(ref)) return true; // 用户环境配置文件(由平台/用户在运行环境生成) if (/(fmode-identity\.json|settings\.local\.json|config\.json|\.fmode\/)/.test(ref)) return true; // 平台内部路径(不属于技能资产) if (/^\/npm\/|^\/index\.html$/.test(ref)) return true; // 用户上传 key 路径示例(uploader --key 的示例值) if (/^[a-z0-9-]+\/(index\.html|.*\.html)$/.test(ref) && !ref.startsWith('references/')) return true; // 代码文件名示例(文档教用户按此命名创建,非技能自带资产) if (/^(validate|machine|format|storage|browser-transport|utils|helpers)\.js$/.test(ref)) return true; if (/^(scripts\/check\.mjs|scripts\/deploy\.sh)$/.test(ref)) return true; // 示例路径(如 ./components/x.js,教用户按此建目录) if (/^\.?\/?(components|routes|services|utils|modules)\/[a-z0-9-]*x?\.js$/.test(ref)) return true; // 历史继承来源说明(如“继承自 xxx-v4.mjs”,旧版本已被替代) if (/^analyze-photos-v\d.*\.mjs$/.test(ref)) return true; // 外部规范/标准文档引用(不在本技能包内分发) if (/^[a-z-]+\/[a-z-]*standard\.md$/.test(ref)) return true; return false; } function isExternal(ref) { if (/^https?:\/\//.test(ref)) return true; if (/^(node:|npm |npx |pip |git )/.test(ref)) return true; if (ref.startsWith('/opt/') || ref.startsWith('/root/') || ref.startsWith('~')) return true; // 环境绝对路径 if (ref.startsWith('./.') || /^\.fmode\//.test(ref)) return true; // 用户环境配置 return false; } const results = []; for (const skill of fs.readdirSync(skillsRoot)) { const skillDir = path.join(skillsRoot, skill); if (!fs.statSync(skillDir).isDirectory()) continue; const mds = allMd(skillDir); const critical = []; // 真实依赖缺失(严重) const examples = []; // 示例性提及(可忽略) const checked = new Set(); for (const mdFile of mds) { const text = fs.readFileSync(mdFile, 'utf8'); const relDoc = path.relative(skillDir, mdFile); const docDir = path.dirname(mdFile); for (const ref of extractRefs(text)) { if (isExternal(ref)) continue; const key = `${relDoc}::${ref}`; if (checked.has(key)) continue; checked.add(key); const candidates = [ path.resolve(docDir, ref), path.resolve(skillDir, ref), path.resolve(skillDir, 'references', ref), path.resolve(skillDir, 'scripts', ref), path.resolve(skillDir, 'scripts/prompts', ref), path.resolve(skillDir, 'assets', ref), path.resolve(skillDir, 'assets/theme', ref), path.resolve(skillDir, ref.replace(/^references\//, '')), ]; const found = candidates.some(c => fs.existsSync(c) && fs.statSync(c).isFile()); if (found) continue; // CHANGELOG 里的历史文件名不算依赖 if (/CHANGELOG/i.test(relDoc)) continue; (isExample(ref) ? examples : critical).push({ doc: relDoc, ref }); } } results.push({ skill, docs: mds.length, checked: checked.size, critical, examples }); } let totalCritical = 0; console.log(`\n${C.b}文档引用完整性检查${C.x} (dist/skills)\n`); for (const r of results) { if (r.critical.length === 0) { const ex = r.examples.length ? ` ${C.d}(示例性提及 ${r.examples.length} 处,可忽略)${C.x}` : ''; console.log(`${C.g}✓${C.x} ${r.skill} ${C.d}(${r.docs}篇/${r.checked}处)${C.x}${ex}`); } else { totalCritical += r.critical.length; console.log(`${C.r}✗${C.x} ${r.skill} ${C.d}— ${r.critical.length} 处真实依赖缺失${C.x}`); for (const m of (SHOW_ALL ? r.critical : r.critical.slice(0, 8))) { console.log(` ${C.r}${m.ref}${C.x} ${C.d}← ${m.doc}${C.x}`); } if (!SHOW_ALL && r.critical.length > 8) console.log(` ${C.d}...还有 ${r.critical.length - 8} 处 (--all 查看全部)${C.x}`); } } console.log(`\n${C.b}汇总${C.x}: ${results.length} 技能 | ${C.r}真实依赖缺失 ${totalCritical} 处${C.x}\n`); process.exit(totalCritical > 0 ? 1 : 0);