|
| 1 | +#!/usr/bin/env node |
| 2 | +// 分发一致性门:skills 引用的命令 ⊆ 产物 requiresCommands ⊆ 已安装 CLI 的命令目录。 |
| 3 | +// 分工:cliVersion pin 只决定「装哪个 CLI」,requiresCommands 决定「这份内容能不能装进这个 CLI」, |
| 4 | +// 本门负责让两者不互相冒充。用法:npm run check:requires(CI 或本机皆可,SHADOW_DEV_CLI 可指定 cli.mjs)。 |
| 5 | +import { execFileSync } from 'node:child_process' |
| 6 | +import { readFileSync, readdirSync, existsSync } from 'node:fs' |
| 7 | +import { dirname, join } from 'node:path' |
| 8 | +import { fileURLToPath } from 'node:url' |
| 9 | + |
| 10 | +const root = dirname(dirname(fileURLToPath(import.meta.url))) |
| 11 | +const declared = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).requiresCommands || [] |
| 12 | +const set = new Set(declared) |
| 13 | +const refs = new Map() |
| 14 | +for (const d of readdirSync(join(root, 'skills'))) { |
| 15 | + const f = join(root, 'skills', d, 'SKILL.md') |
| 16 | + if (!existsSync(f)) continue |
| 17 | + for (const m of readFileSync(f, 'utf8').matchAll(/shadow-dev\s+([a-z][\w-]*(?:\s+[a-z][\w-]*){0,2})/g)) { |
| 18 | + const words = m[1].replace(/\//g, ' ').split(/\s+/).filter(Boolean) |
| 19 | + const cand = [words.slice(0, 3).join('.'), words.slice(0, 2).join('.'), words[0]] |
| 20 | + const key = cand.find(k => set.has(k)) || words.slice(0, 2).join('.') + ' (未声明)' |
| 21 | + refs.set(key, (refs.get(key) || 0) + 1) |
| 22 | + } |
| 23 | +} |
| 24 | +const undeclared = [...refs.keys()].filter(k => k.endsWith('(未声明)')) |
| 25 | +let catalog = null, missingInCli = [] |
| 26 | +const cli = process.env.SHADOW_DEV_CLI |
| 27 | +const argv = cli ? [process.execPath, [cli, 'help', '--full', '--json']] : ['shadow-dev', ['help', '--full', '--json']] |
| 28 | +try { catalog = Object.keys(JSON.parse(execFileSync(argv[0], argv[1], { encoding: 'utf8', stdio: ['ignore','pipe','ignore'] })).data.commands || {}) } catch { catalog = null } |
| 29 | +if (catalog) missingInCli = declared.filter(k => !catalog.includes(k)) |
| 30 | +console.log(`declared=${declared.length} skills-referenced=${refs.size} undeclared=[${undeclared.join(', ')}] cli-catalog=${catalog ? catalog.length : 'unresolved'} missing-in-cli=[${missingInCli.join(', ')}]`) |
| 31 | +if (undeclared.length) console.error('✗ skills 有命令未进 requiresCommands 声明:', undeclared.join(', ')) |
| 32 | +if (missingInCli.length) console.error('✗ requiresCommands 声明的命令在已装 CLI 里不存在(pin 或 CLI 版本过旧):', missingInCli.join(', ')) |
| 33 | +if (!undeclared.length && !missingInCli.length) console.log('✓ skills ⊆ requiresCommands' + (catalog ? ' ⊆ CLI 命令目录' : '(CLI 目录未参与断言)')) |
| 34 | +process.exitCode = (undeclared.length || missingInCli.length) ? 1 : 0 |
0 commit comments