skill-core.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. #!/usr/bin/env node
  2. /**
  3. * skill-core — Fmode Harness 平台母技能 CLI
  4. * ---------------------------------------------------------------------------
  5. * 命令:
  6. * skill-core init [dir] 从 templates/skill-starter 脚手架创建新技能
  7. * skill-core check [dir] 运行六项自动质检
  8. * skill-core publish [dir] 生成四渠道发布计划(--dry-run 默认)
  9. * skill-core verify [dir] 静态校验包结构 / package.json / SKILL.md
  10. * skill-core inventory 输出技能清单(--json 机器可读)
  11. * skill-core bootstrap 一键凭证供给(检查 ~/.fmode/)
  12. * skill-core spec 输出平台规范摘要
  13. * skill-core endpoints 输出端点真值表(含实测状态)
  14. *
  15. * 全局参数:
  16. * --json 机器可读输出
  17. * --offline 跳过所有网络检查
  18. * --help, -h 帮助
  19. * --version, -v 版本
  20. */
  21. import fs from 'node:fs';
  22. import path from 'node:path';
  23. import os from 'node:os';
  24. import { fileURLToPath } from 'node:url';
  25. import {
  26. VERSION,
  27. PLATFORM,
  28. ENDPOINTS,
  29. endpointsByStatus,
  30. CHECKS,
  31. runChecks,
  32. renderReport,
  33. publishPlan,
  34. CHANNELS,
  35. RUNTIMES,
  36. CREDENTIAL_CHAIN,
  37. TIERS,
  38. validatePackageJson,
  39. validateManifest,
  40. validateFrontmatter,
  41. validateName,
  42. INVENTORY,
  43. byTier,
  44. stats,
  45. } from '../lib/index.mjs';
  46. import {
  47. bootstrap,
  48. describeBootstrapStatus,
  49. resolveFmodeDir,
  50. resolveConfigPath,
  51. } from '../lib/bootstrap.mjs';
  52. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  53. const PKG_ROOT = path.resolve(__dirname, '..');
  54. const TEMPLATE_DIR = path.join(PKG_ROOT, 'templates', 'skill-starter');
  55. // ============================================================
  56. // 参数解析(零依赖)
  57. // ============================================================
  58. function parseArgs(argv) {
  59. const flags = {};
  60. const positional = [];
  61. for (let i = 0; i < argv.length; i++) {
  62. const a = argv[i];
  63. if (a === '--') {
  64. positional.push(...argv.slice(i + 1));
  65. break;
  66. }
  67. if (a.startsWith('--')) {
  68. const eq = a.indexOf('=');
  69. if (eq > -1) {
  70. flags[a.slice(2, eq)] = a.slice(eq + 1);
  71. } else {
  72. const key = a.slice(2);
  73. const next = argv[i + 1];
  74. if (next && !next.startsWith('-')) {
  75. flags[key] = next;
  76. i++;
  77. } else {
  78. flags[key] = true;
  79. }
  80. }
  81. } else if (a.startsWith('-') && a.length > 1) {
  82. for (const c of a.slice(1)) flags[c] = true;
  83. } else {
  84. positional.push(a);
  85. }
  86. }
  87. return { flags, positional };
  88. }
  89. const C = {
  90. reset: '\x1b[0m',
  91. bold: '\x1b[1m',
  92. dim: '\x1b[2m',
  93. red: '\x1b[31m',
  94. green: '\x1b[32m',
  95. yellow: '\x1b[33m',
  96. blue: '\x1b[34m',
  97. cyan: '\x1b[36m',
  98. };
  99. const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
  100. const c = (code, s) => (useColor ? `${code}${s}${C.reset}` : s);
  101. // ============================================================
  102. // 帮助
  103. // ============================================================
  104. function printHelp() {
  105. const out = `
  106. ${c(C.bold, 'skill-core')} · Fmode Harness 平台母技能 CLI v${VERSION}
  107. ${c(C.bold, '用法')}
  108. skill-core <command> [dir] [options]
  109. ${c(C.bold, '命令')}
  110. ${c(C.cyan, 'init')} [dir] 从脚手架创建新技能(默认交互式询问技能名)
  111. ${c(C.cyan, 'check')} [dir] 运行六项自动质检(默认当前目录)
  112. ${c(C.cyan, 'publish')} [dir] 生成四渠道发布计划并预检
  113. ${c(C.cyan, 'verify')} [dir] 静态校验包结构 / package.json / SKILL.md / manifest
  114. ${c(C.cyan, 'inventory')} 输出已发布技能清单
  115. ${c(C.cyan, 'bootstrap')} 一键凭证供给:检查并初始化 ~/.fmode/
  116. ${c(C.cyan, 'spec')} 输出平台规范摘要(ESM-first / 四渠道 / 凭据链)
  117. ${c(C.cyan, 'endpoints')} 输出端点真值表(含 2026-09-22 实测状态)
  118. ${c(C.bold, '选项')}
  119. --json 机器可读 JSON 输出
  120. --offline 跳过网络检查(对应项标记为 skip)
  121. --deep check/loop:真正执行 npx 拉取验证
  122. --only <ids> 仅运行指定检查项,逗号分隔
  123. --skip-exec check/multiRuntime:只做静态校验不执行
  124. --name <n> init:指定技能名(跳过交互)
  125. --dry-run publish:只打印计划不执行(默认行为)
  126. --apply publish:实际执行发布命令
  127. -h, --help 显示帮助
  128. -v, --version 显示版本
  129. ${c(C.bold, '示例')}
  130. skill-core init my-skill --name skill-my-skill
  131. skill-core check . --only functional,multiRuntime
  132. skill-core check . --offline
  133. skill-core publish . --apply
  134. skill-core bootstrap --json
  135. ${c(C.bold, '六项质检')}
  136. ${CHECKS.map((ch, i) => ` ${i + 1}. ${ch.title.padEnd(14)} ${c(C.dim, ch.id)}`).join('\n')}
  137. `;
  138. console.log(out);
  139. }
  140. // ============================================================
  141. // init —— 脚手架
  142. // ============================================================
  143. /** 递归复制模板目录,替换文件名与内容中的占位符 */
  144. function copyTemplate(srcDir, destDir, replacements) {
  145. const written = [];
  146. // 占位符替换:skillName → __SKILL_NAME__,cliName → __CLI_NAME__,displayName → __DISPLAY_NAME__
  147. // (camelCase → SCREAMING_SNAKE_CASE)
  148. const subst = (s) => {
  149. let out = s;
  150. for (const [k, v] of Object.entries(replacements)) {
  151. const token = k.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
  152. out = out.replace(new RegExp(`__${token}__`, 'g'), v);
  153. }
  154. return out;
  155. };
  156. const walk = (src, dest) => {
  157. if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
  158. for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
  159. const s = path.join(src, entry.name);
  160. const d = path.join(dest, subst(entry.name));
  161. if (entry.isDirectory()) {
  162. walk(s, d);
  163. } else {
  164. fs.writeFileSync(d, subst(fs.readFileSync(s, 'utf-8')));
  165. written.push(d);
  166. }
  167. }
  168. };
  169. walk(srcDir, destDir);
  170. return written;
  171. }
  172. async function cmdInit(flags, positional) {
  173. let targetDir = positional[0];
  174. let skillName = flags.name;
  175. if (!skillName) {
  176. if (!process.stdin.isTTY) {
  177. console.error(c(C.red, '✗ 非交互环境必须用 --name 指定技能名'));
  178. console.error(' 例:skill-core init my-skill --name skill-my-skill');
  179. process.exit(2);
  180. }
  181. const rl = (await import('node:readline/promises')).createInterface({
  182. input: process.stdin,
  183. output: process.stdout,
  184. });
  185. skillName = (await rl.question('技能名(如 skill-my-thing): ')).trim();
  186. if (!targetDir) {
  187. targetDir = (await rl.question(`目标目录(回车默认 ./${skillName}): `)).trim() || `./${skillName}`;
  188. }
  189. rl.close();
  190. }
  191. if (!targetDir) targetDir = `./${skillName}`;
  192. const nv = validateName(skillName);
  193. if (!nv.ok) {
  194. console.error(c(C.red, `✗ ${nv.reason}`));
  195. process.exit(2);
  196. }
  197. if (!fs.existsSync(TEMPLATE_DIR)) {
  198. console.error(c(C.red, `✗ 脚手架模板缺失:${TEMPLATE_DIR}`));
  199. console.error(' 请重新安装:npm install @fmode/skill-core-guide@latest');
  200. process.exit(2);
  201. }
  202. const abs = path.resolve(targetDir);
  203. if (fs.existsSync(abs) && fs.readdirSync(abs).length > 0) {
  204. console.error(c(C.red, `✗ 目标目录已存在且非空:${abs}`));
  205. console.error(' 为安全起见不覆盖已有内容,请换一个目录。');
  206. process.exit(2);
  207. }
  208. const cliName = skillName.replace(/^skill-/, '');
  209. const written = copyTemplate(TEMPLATE_DIR, abs, {
  210. skillName,
  211. cliName,
  212. displayName: flags.display || skillName,
  213. summary: flags.summary || `${skillName} —— 由 @fmode/skill-core-guide 脚手架生成`,
  214. });
  215. const rel = written.map((f) => path.relative(abs, f)).sort();
  216. if (flags.json) {
  217. console.log(JSON.stringify({ ok: true, dir: abs, skillName, files: rel }, null, 2));
  218. return 0;
  219. }
  220. console.log(`\n ${c(C.green, '✅')} 技能脚手架已创建:${c(C.bold, abs)}`);
  221. console.log(` 技能名:${skillName} CLI 名:${cliName}\n`);
  222. console.log(` ${c(C.bold, '生成的文件')}`);
  223. for (const f of rel) console.log(` ${f}`);
  224. console.log(`
  225. ${c(C.bold, '下一步')}
  226. cd ${targetDir}
  227. git init && git config user.email "liu@fmode.cn" && git config user.name "liuyuyang"
  228. ${c(C.dim, `# 1) 编辑 skills/${skillName}/SKILL.md —— 写清触发场景与用法`)}
  229. ${c(C.dim, '# 2) 实现 lib/index.mjs —— export 公共接口')}
  230. ${c(C.dim, `# 3) 实现 bin/${cliName}.mjs —— CLI 入口`)}
  231. npm test
  232. skill-core check . ${c(C.dim, '# 六项质检')}
  233. skill-core publish . --apply
  234. `);
  235. return 0;
  236. }
  237. // ============================================================
  238. // check —— 六项质检
  239. // ============================================================
  240. async function cmdCheck(flags, positional) {
  241. const dir = path.resolve(positional[0] || '.');
  242. const only = flags.only ? String(flags.only).split(',').map((s) => s.trim()).filter(Boolean) : undefined;
  243. const opts = {
  244. offline: !!flags.offline,
  245. deep: !!flags.deep,
  246. skipExec: !!flags['skip-exec'],
  247. only,
  248. token: process.env.FMODE_API_TOKEN,
  249. onProgress: flags.json
  250. ? undefined
  251. : (id, st) => {
  252. if (st === 'start') process.stderr.write(c(C.dim, ` … ${id}\n`));
  253. },
  254. };
  255. let report;
  256. try {
  257. report = await runChecks(dir, opts);
  258. } catch (err) {
  259. console.error(c(C.red, `✗ ${err.message}`));
  260. process.exit(2);
  261. }
  262. if (flags.json) {
  263. console.log(JSON.stringify(report, null, 2));
  264. } else {
  265. console.log(renderReport(report));
  266. }
  267. const s = report.summary;
  268. if (s.fail > 0) return 1;
  269. if (s.skip > 0 && !flags['allow-skip']) return 3; // 未验证项需显式放行
  270. return 0;
  271. }
  272. // ============================================================
  273. // verify —— 静态校验
  274. // ============================================================
  275. function cmdVerify(flags, positional) {
  276. const dir = path.resolve(positional[0] || '.');
  277. const result = { dir, ok: true, checks: [] };
  278. const add = (name, v) => {
  279. result.checks.push({ name, ...v });
  280. if (!v.ok) result.ok = false;
  281. };
  282. // package.json
  283. const pkgPath = path.join(dir, 'package.json');
  284. if (!fs.existsSync(pkgPath)) {
  285. add('package.json', { ok: false, errors: ['文件不存在'], warnings: [] });
  286. } else {
  287. let pkg = null;
  288. try {
  289. pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8').replace(/^/, ''));
  290. } catch (e) {
  291. add('package.json', { ok: false, errors: [`JSON 解析失败:${e.message}`], warnings: [] });
  292. }
  293. if (pkg) add('package.json', validatePackageJson(pkg));
  294. }
  295. // manifest
  296. const mPath = path.join(dir, 'skill-package-manifest.json');
  297. if (!fs.existsSync(mPath)) {
  298. add('skill-package-manifest.json', { ok: false, errors: ['文件不存在'], warnings: [] });
  299. } else {
  300. try {
  301. const m = JSON.parse(fs.readFileSync(mPath, 'utf-8').replace(/^/, ''));
  302. add('skill-package-manifest.json', validateManifest(m));
  303. } catch (e) {
  304. add('skill-package-manifest.json', { ok: false, errors: [`JSON 解析失败:${e.message}`], warnings: [] });
  305. }
  306. }
  307. // SKILL.md(根目录,skillhub 格式)
  308. const sPath = path.join(dir, 'SKILL.md');
  309. if (!fs.existsSync(sPath)) {
  310. add('SKILL.md', { ok: false, errors: ['文件不存在'], warnings: [] });
  311. } else {
  312. const text = fs.readFileSync(sPath, 'utf-8');
  313. const sh = validateFrontmatter(text, 'skillhub');
  314. const hm = validateFrontmatter(text, 'hermes');
  315. // 任一格式合法即算通过(两格式用途不同)
  316. add('SKILL.md', {
  317. ok: sh.ok || hm.ok,
  318. format: sh.ok ? 'skillhub' : hm.ok ? 'hermes' : null,
  319. errors: sh.ok || hm.ok ? [] : [...sh.errors],
  320. warnings: [...sh.warnings, ...hm.warnings],
  321. });
  322. }
  323. // 必需文件
  324. const required = ['lib/index.mjs', 'README.md', 'LICENSE'];
  325. const missing = required.filter((f) => !fs.existsSync(path.join(dir, f)));
  326. add('必需文件', { ok: missing.length === 0, errors: missing.map((f) => `缺少 ${f}`), warnings: [] });
  327. // bin 入口
  328. let pkgObj = null;
  329. try {
  330. pkgObj = JSON.parse(fs.readFileSync(pkgPath, 'utf-8').replace(/^/, ''));
  331. } catch {
  332. /* handled above */
  333. }
  334. if (pkgObj && pkgObj.bin) {
  335. const binRel = typeof pkgObj.bin === 'string' ? pkgObj.bin : Object.values(pkgObj.bin)[0];
  336. const bp = path.join(dir, binRel);
  337. if (!fs.existsSync(bp)) {
  338. add('bin 入口', { ok: false, errors: [`${binRel} 不存在`], warnings: [] });
  339. } else {
  340. const head = fs.readFileSync(bp, 'utf-8').slice(0, 60);
  341. add('bin 入口', {
  342. ok: head.startsWith('#!/usr/bin/env node'),
  343. errors: head.startsWith('#!/usr/bin/env node') ? [] : ['缺少 #!/usr/bin/env node shebang'],
  344. warnings: [],
  345. });
  346. }
  347. }
  348. if (flags.json) {
  349. console.log(JSON.stringify(result, null, 2));
  350. } else {
  351. console.log(`\n ${c(C.bold, '静态校验')} ${dir}\n ${'─'.repeat(60)}`);
  352. for (const ch of result.checks) {
  353. const icon = ch.ok ? c(C.green, '✅') : c(C.red, '❌');
  354. const extra = ch.format ? c(C.dim, ` (${ch.format} frontmatter)`) : '';
  355. console.log(` ${icon} ${ch.name}${extra}`);
  356. for (const e of ch.errors || []) console.log(` ${c(C.red, '✗')} ${e}`);
  357. for (const w of ch.warnings || []) console.log(` ${c(C.yellow, '!')} ${w}`);
  358. }
  359. console.log(` ${'─'.repeat(60)}`);
  360. console.log(` 结论:${result.ok ? c(C.green, '✅ 通过') : c(C.red, '❌ 未通过')}\n`);
  361. }
  362. return result.ok ? 0 : 1;
  363. }
  364. // ============================================================
  365. // publish —— 四渠道发布
  366. // ============================================================
  367. async function cmdPublish(flags, positional) {
  368. const dir = path.resolve(positional[0] || '.');
  369. const pkgPath = path.join(dir, 'package.json');
  370. let name = path.basename(dir);
  371. try {
  372. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8').replace(/^/, ''));
  373. if (pkg.name) name = pkg.name;
  374. } catch {
  375. /* 用目录名兜底 */
  376. }
  377. const changelog = flags.changelog || `release ${name} v${flags.version || ''}`.trim();
  378. const plan = publishPlan(name, { dir, changelog });
  379. const apply = !!flags.apply;
  380. if (flags.json) {
  381. console.log(JSON.stringify({ name, dir, apply, plan }, null, 2));
  382. } else {
  383. console.log(`\n ${c(C.bold, '四渠道发布计划')} ${c(C.cyan, name)}\n ${'─'.repeat(64)}`);
  384. plan.forEach((p, i) => {
  385. console.log(` ${i + 1}. ${c(C.bold, p.label)}`);
  386. for (const s of p.steps) console.log(` ${c(C.dim, '$')} ${s}`);
  387. if (p.note) console.log(` ${c(C.dim, '↳ ' + p.note)}`);
  388. console.log('');
  389. });
  390. console.log(` ${'─'.repeat(64)}`);
  391. }
  392. if (!apply) {
  393. if (!flags.json) {
  394. console.log(` ${c(C.yellow, '⚠️ dry-run 模式')} —— 未执行任何发布命令。`);
  395. console.log(` 加 ${c(C.bold, '--apply')} 实际执行(需已配置各渠道凭据)。\n`);
  396. }
  397. return 0;
  398. }
  399. // 实际执行:仅执行「安全可重入」的步骤,建仓等不可逆操作交由用户
  400. const { spawnSync } = await import('node:child_process');
  401. const results = [];
  402. for (const step of plan[0].steps) {
  403. const r = spawnSync('bash', ['-c', step], { cwd: dir, encoding: 'utf-8', timeout: 300000 });
  404. results.push({ cmd: step, status: r.status, stderr: (r.stderr || '').slice(-300) });
  405. }
  406. console.log(JSON.stringify(results, null, 2));
  407. return results.some((r) => r.status !== 0) ? 1 : 0;
  408. }
  409. // ============================================================
  410. // inventory
  411. // ============================================================
  412. function cmdInventory(flags) {
  413. if (flags.json) {
  414. console.log(JSON.stringify({ stats: stats(), skills: INVENTORY }, null, 2));
  415. return 0;
  416. }
  417. const grouped = byTier();
  418. const s = stats();
  419. console.log(`\n ${c(C.bold, 'Fmode 技能清单')} 共 ${s.total} 个\n ${'─'.repeat(76)}`);
  420. for (const [key, tier] of Object.entries(TIERS)) {
  421. const list = grouped[key] || [];
  422. console.log(`\n ${c(C.bold, tier.label)} ${c(C.dim, `(${list.length})`)}`);
  423. for (const sk of list) {
  424. const marks = sk.platforms.join('/');
  425. console.log(` ${c(C.cyan, sk.name.padEnd(24))} ${sk.displayName}`);
  426. console.log(` ${c(C.dim, marks.padEnd(26))} ${sk.summary.slice(0, 72)}${sk.summary.length > 72 ? '…' : ''}`);
  427. if (sk.npmName) console.log(` ${c(C.dim, `npm: ${sk.npmName}@${sk.npmVersion || '?'}`)}`);
  428. }
  429. }
  430. console.log(`\n ${'─'.repeat(76)}`);
  431. console.log(` 渠道分布:${Object.entries(s.byPlatform).map(([k, v]) => `${k}=${v}`).join(' ')}\n`);
  432. return 0;
  433. }
  434. // ============================================================
  435. // bootstrap
  436. // ============================================================
  437. async function cmdBootstrap(flags) {
  438. const report = await bootstrap({
  439. phone: flags.phone,
  440. code: flags.code,
  441. cwd: process.cwd(),
  442. });
  443. if (flags.json) {
  444. console.log(JSON.stringify(report, null, 2));
  445. } else {
  446. console.log(describeBootstrapStatus(report));
  447. }
  448. return report.ok ? 0 : 1;
  449. }
  450. // ============================================================
  451. // spec
  452. // ============================================================
  453. function cmdSpec(flags) {
  454. const spec = {
  455. platform: PLATFORM,
  456. runtimes: RUNTIMES,
  457. credentialChain: CREDENTIAL_CHAIN,
  458. channels: Object.keys(CHANNELS),
  459. checks: CHECKS.map((c) => ({ id: c.id, title: c.title })),
  460. tiers: TIERS,
  461. };
  462. if (flags.json) {
  463. console.log(JSON.stringify(spec, null, 2));
  464. return 0;
  465. }
  466. console.log(`\n ${c(C.bold, 'Fmode Harness 平台规范摘要')}\n ${'─'.repeat(68)}`);
  467. console.log(`\n ${c(C.bold, '① 四端可用性(ESM-first)')}`);
  468. for (const r of Object.values(RUNTIMES)) {
  469. const icon = r.supported ? c(C.green, '✅') : c(C.red, '❌');
  470. console.log(` ${icon} ${r.label.padEnd(20)} ${r.usage}`);
  471. if (r.constraint) console.log(` ${c(C.dim, r.constraint)}`);
  472. }
  473. console.log(`\n ${c(C.bold, '② 凭据解析链(5 级,命中即用)')}`);
  474. for (const l of CREDENTIAL_CHAIN) {
  475. console.log(` ${l.level}. ${l.source.padEnd(20)} ${c(C.dim, l.detail.slice(0, 60))}${l.detail.length > 60 ? '…' : ''}`);
  476. }
  477. console.log(`\n ${c(C.bold, '③ 四渠道分发')}`);
  478. for (const [k, v] of Object.entries(CHANNELS)) {
  479. console.log(` ${c(C.cyan, k.padEnd(10))} ${v.label}`);
  480. }
  481. console.log(`\n ${c(C.bold, '④ 六项自动质检')}`);
  482. CHECKS.forEach((ch, i) => console.log(` ${i + 1}. ${ch.title.padEnd(16)} ${c(C.dim, ch.id)}`));
  483. console.log(`\n ${c(C.bold, '⑤ 技能分层')}`);
  484. for (const t of Object.values(TIERS)) console.log(` ${c(C.cyan, t.key.padEnd(12))} ${t.label}`);
  485. console.log(`\n ${c(C.dim, '完整规范见 SKILL.md(可独立阅读)')}\n`);
  486. return 0;
  487. }
  488. // ============================================================
  489. // endpoints
  490. // ============================================================
  491. function cmdEndpoints(flags) {
  492. if (flags.json) {
  493. console.log(JSON.stringify(ENDPOINTS, null, 2));
  494. return 0;
  495. }
  496. const badge = {
  497. live: c(C.green, '● live'),
  498. planned: c(C.yellow, '○ planned'),
  499. deprecated: c(C.red, '✗ deprecated'),
  500. };
  501. console.log(`\n ${c(C.bold, 'Fmode 端点真值表')} ${c(C.dim, '实测于 2026-09-22')}\n ${'─'.repeat(78)}`);
  502. for (const [key, e] of Object.entries(ENDPOINTS)) {
  503. console.log(`\n ${badge[e.status]} ${c(C.bold, key)}`);
  504. console.log(` ${e.method} ${e.url}`);
  505. console.log(` ${c(C.dim, `鉴权:${e.auth}`)}`);
  506. console.log(` ${e.purpose}`);
  507. if (e.note) console.log(` ${c(C.dim, e.note)}`);
  508. }
  509. const live = endpointsByStatus('live').length;
  510. const planned = endpointsByStatus('planned').length;
  511. const dep = endpointsByStatus('deprecated').length;
  512. console.log(`\n ${'─'.repeat(78)}`);
  513. console.log(` 统计:${c(C.green, `${live} live`)} ${c(C.yellow, `${planned} planned`)} ${c(C.red, `${dep} deprecated`)}`);
  514. console.log(` ${c(C.yellow, '⚠️ ')} planned/deprecated 端点调用前必须探测并回落,禁止当作已上线。\n`);
  515. return 0;
  516. }
  517. // ============================================================
  518. // main
  519. // ============================================================
  520. async function main() {
  521. const { flags, positional } = parseArgs(process.argv.slice(2));
  522. if (flags.help || flags.h) {
  523. printHelp();
  524. return 0;
  525. }
  526. if (flags.version || flags.v) {
  527. console.log(`${VERSION}`);
  528. return 0;
  529. }
  530. const cmd = positional.shift() || (flags.help ? 'help' : null);
  531. if (!cmd) {
  532. printHelp();
  533. return 0;
  534. }
  535. switch (cmd) {
  536. case 'init':
  537. return cmdInit(flags, positional);
  538. case 'check':
  539. return cmdCheck(flags, positional);
  540. case 'verify':
  541. return cmdVerify(flags, positional);
  542. case 'publish':
  543. return cmdPublish(flags, positional);
  544. case 'inventory':
  545. return cmdInventory(flags);
  546. case 'bootstrap':
  547. return cmdBootstrap(flags);
  548. case 'spec':
  549. return cmdSpec(flags);
  550. case 'endpoints':
  551. return cmdEndpoints(flags);
  552. case 'help':
  553. printHelp();
  554. return 0;
  555. default:
  556. console.error(c(C.red, `✗ 未知命令:${cmd}`));
  557. console.error(` 运行 ${c(C.bold, 'skill-core --help')} 查看可用命令。`);
  558. return 2;
  559. }
  560. }
  561. main()
  562. .then((code) => process.exit(code))
  563. .catch((err) => {
  564. console.error(c(C.red, `✗ 未捕获异常:${err && err.stack ? err.stack : err}`));
  565. process.exit(1);
  566. });