Files

64 lines
3.8 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// bin/toollife.mjs — 第三方命令/API 生命周期检查
// node bin/toollife.mjs check 一致性 + 时效报告(CI 用,死条目即失败)
// node bin/toollife.mjs candidates 列出技能里出现但未登记的命令候选
// node bin/toollife.mjs stale 列出距上次核验超期的登记项
// node bin/toollife.mjs smoke 本机探测(仅报告,CI 机器未必装这些工具)
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { checkRegister, REGISTER_REL, smokeProbe, daysSince, deriveRisk } from '../lib/tool-register.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const SKILLS = join(ROOT, '.claude', 'skills');
const mode = process.argv[2] ?? 'check';
if (mode === 'check') {
const { errors, warnings, stale, pending, candidates, register } = checkRegister(ROOT, SKILLS);
for (const e of errors) console.error(`FAIL: ${e}`);
const verified = register.tools.filter((t) => t.last_verified).length;
console.log(`登记 ${register.tools.length} 项|已核验 ${verified}|超期 ${stale.length}|待核验积压 ${pending.length}`);
if (stale.length) {
console.log(`\n超期(≥180 天未复核,${stale.length} 项)——进入下一轮审查波:`);
for (const s of stale.slice(0, 20)) console.log(` ${s.name} 上次核验 ${s.last_verified}(${s.days} 天前)`);
if (stale.length > 20) console.log(` …另有 ${stale.length - 20} 项`);
}
if (pending.length) {
// 风险分层:技能里写死了层级路径/版本号的更容易随上游漂移(volatility 的教训)
const risk = deriveRisk(SKILLS, register.tools.map((t) => t.name));
const high = pending.filter((n) => risk[n]?.level === 'high')
.sort((a, b) => risk[b].spread - risk[a].spread);
console.log(`\n待核验积压 ${pending.length} 项|高风险 ${high.length}|低风险 ${pending.length - high.length}`);
console.log('高风险项(按影响面排序,= 下一轮审查波的建议顺序):');
for (const n of high.slice(0, 15)) console.log(` 面=${String(risk[n].spread).padStart(3)} ${n}`);
if (high.length > 15) console.log(` …另有 ${high.length - 15} 项(node bin/toollife.mjs stale 看全量)`);
}
if (candidates.length) {
console.log(`\n未登记候选(${candidates.length} 项,确认后加入 ${REGISTER_REL} 或加入 ignore):`);
console.log(` ${candidates.join(' ')}`);
}
for (const w of warnings) console.warn(`WARN: ${w}`);
if (errors.length) process.exit(1);
console.log('\nOK: tool register consistent');
} else if (mode === 'candidates') {
const { candidates } = checkRegister(ROOT, SKILLS);
console.log(candidates.join('\n'));
} else if (mode === 'stale') {
const { register } = checkRegister(ROOT, SKILLS);
const rows = register.tools
.map((t) => ({ name: t.name, v: t.last_verified, d: t.last_verified ? daysSince(t.last_verified) : null }))
.filter((r) => r.v === null || r.d >= 180)
.sort((a, b) => (b.d ?? Infinity) - (a.d ?? Infinity));
for (const r of rows) console.log(`${r.name}\t${r.v ?? '未核验'}\t${r.d ?? '-'}`);
console.log(`\n共 ${rows.length} 项待核验/超期`);
} else if (mode === 'smoke') {
const { register } = checkRegister(ROOT, SKILLS);
const names = register.tools.filter((t) => t.kind === 'cli').map((t) => t.name);
const res = smokeProbe(names);
const miss = res.filter((r) => !r.present).map((r) => r.name);
console.log(`本机存在 ${res.length - miss.length}/${res.length}|缺失:${miss.join(' ') || '无'}`);
console.log('(仅报告:CI 机器不必装这些工具,本模式不参与判定)');
} else {
console.error(`未知子命令:${mode}(可用 check / candidates / stale / smoke)`);
process.exit(2);
}