#!/usr/bin/env node /*** @ui-arch CLI:组件库注册表命令行 + 内置 MCP 服务(零依赖,Node >= 18) ***/ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /*** 自底向上查找仓库根(以 pnpm-workspace.yaml 为界) ***/ function findRepoRoot(start) { let dir = start; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir; const parent = path.dirname(dir); if (parent === dir) return process.cwd(); dir = parent; } return process.cwd(); } const ROOT = findRepoRoot(__dirname); const PKG_DIR = path.join(ROOT, 'packages'); const INDEX_DIR = path.join(ROOT, '.ua'); const INDEX_FILE = path.join(INDEX_DIR, 'index.json'); const LLMS_FILE = path.join(ROOT, 'llms.txt'); const LLMS_FULL = path.join(ROOT, 'llms-full.txt'); /*** 汇总各包 ua.manifest.json 与 package.json 版本(扫描 packages/ 两层目录) ***/ function collectManifests() { const out = []; if (!fs.existsSync(PKG_DIR)) return out; const readManifest = (dir) => { const manifestPath = path.join(dir, 'ua.manifest.json'); if (!fs.existsSync(manifestPath)) return; // 卫语句:无清单不入索引 const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const pkgPath = path.join(dir, 'package.json'); let version = '0.0.0'; if (fs.existsSync(pkgPath)) { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? version; } out.push({ ...manifest, version, dir: path.relative(ROOT, dir).split(path.sep).join('/'), }); }; for (const layer of fs.readdirSync(PKG_DIR)) { const layerDir = path.join(PKG_DIR, layer); if (!fs.statSync(layerDir).isDirectory()) continue; readManifest(layerDir); // 兼容单层直放 for (const name of fs.readdirSync(layerDir)) { const dir = path.join(layerDir, name); if (fs.statSync(dir).isDirectory()) readManifest(dir); } } return out; } /*** 内容无变化则跳过写入(幂等,行尾归一化防 CRLF 误判;norm 可剔除时间戳后再比对),避免弄脏工作区 ***/ function writeIfChanged(file, content, norm = (s) => s) { if (fs.existsSync(file) && norm(fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n')) === norm(content)) return false; fs.writeFileSync(file, content); return true; } /*** 聚合清单,生成 .ua/index.json 与 llms.txt ***/ function cmdIndex() { const packages = collectManifests(); fs.mkdirSync(INDEX_DIR, { recursive: true }); const index = { $schema: 'ui-arch-registry-index/0.1', generatedAt: new Date().toISOString(), repoRoot: '.', packages, }; // 幂等:剔除 generatedAt 后内容一致即不写入,连续 check 不弄脏工作区 const json = JSON.stringify(index, null, 2) + '\n'; const stripTs = (s) => s.replace(/"generatedAt": "[^"]*"/, '"generatedAt": ""'); const changed = !fs.existsSync(INDEX_FILE) || stripTs(fs.readFileSync(INDEX_FILE, 'utf8').replace(/\r\n/g, '\n')) !== stripTs(json); if (changed) fs.writeFileSync(INDEX_FILE, json); const lines = [ '# @ui-arch 私人组件库索引', '', '> 本文件由 `ua index` 自动生成,请勿手改。机器可读版:`.ua/index.json`。', '> Agent 可通过 `ua mcp`(stdio MCP server)直接查询注册表。', '', ]; for (const p of packages) { lines.push(`## ${p.name} (v${p.version})`, ''); lines.push(`- 类型: ${p.type} · 目录: ${p.dir}`); lines.push(`- 描述: ${p.description}`); if (p.api?.exports?.length) { lines.push('- API:'); for (const e of p.api.exports) lines.push(` - ${e}`); } if (p.usage) lines.push(`- 用法: \`${p.usage}\``); for (const c of p.conventions ?? []) lines.push(`- 约定: ${c}`); lines.push(''); } writeIfChanged(LLMS_FILE, lines.join('\n')); console.log(`indexed ${packages.length} package(s) -> .ua/index.json, llms.txt${changed ? '' : ' (unchanged, skip)'}`); } /*** 读取注册表索引;缺失则报错退出 ***/ function loadIndex() { if (!fs.existsSync(INDEX_FILE)) { console.error('index not found, run: ua index'); process.exit(1); } return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8')); } /*** 列出全部已注册包 ***/ function cmdList() { const { packages } = loadIndex(); if (!packages.length) return console.log('(no packages indexed)'); for (const p of packages) { console.log(`${p.name} v${p.version} [${p.type}] ${p.description}`); } } /*** 输出单包完整元数据 ***/ function cmdInfo(name) { const { packages } = loadIndex(); const p = packages.find((x) => x.name === name || x.name === `@ui-arch/${name}`); if (!p) { console.error(`not found: ${name}`); process.exit(1); } console.log(JSON.stringify(p, null, 2)); } // ---- 静态文档站生成 ---- /*** HTML 转义:& < > ***/ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } /*** 渲染人类文档站:注册表 + 逐组件 props/步骤 + 现场演示 iframe + 令牌,数据内嵌支持搜索 ***/ function renderDocs(index, tokens) { const data = JSON.stringify(index.packages).replace(/ @ui-arch 组件库文档

@ui-arch私人组件库

`; } /*** 渲染 Agent 文档(llms-full.txt):无图形纯文本,逐组件参数 / 回调 / 步骤 / 约定全量收录 ***/ function renderAgentDocs(index, tokens) { const lines = [ '# @ui-arch 组件库 Agent 文档(无图形版)', '', `> 自动生成于 ${index.generatedAt},数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。`, '> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。', '', '## 快速上手', '', ]; for (const p of index.packages) { if (!p.docs?.steps?.length) continue; lines.push(`### ${p.name}`, ''); p.docs.steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`)); lines.push(''); } const order = { 'skin-dom': 0, 'headless-core': 1, 'design-tokens': 2 }; for (const p of [...index.packages].sort((a, b) => (order[a.type] ?? 9) - (order[b.type] ?? 9))) { lines.push(`## ${p.name}(${p.type})v${p.version}`, ''); lines.push((p.docs?.intro ?? p.description) + '', ''); for (const c of p.docs?.components ?? []) { lines.push(`### ${c.id} — ${c.title ?? ''}`, ''); if (c.props?.length) { lines.push('参数:', ''); for (const prop of c.props) { const marks = [prop.r ? '必填' : '', prop.k ? '回调' : '', prop.df ? `默认 ${prop.df}` : ''].filter(Boolean).join(','); lines.push(`- ${prop.n}${marks ? `(${marks})` : ''}:${prop.t}${prop.d ? ` —— ${prop.d}` : ''}`); } lines.push(''); } if (c.example) lines.push('示例:', '', '```', c.example, '```', ''); if (c.steps?.length) { lines.push('手把手:', ''); c.steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`)); lines.push(''); } if (c.demo) lines.push(`现场演示:apps/docs/demo.html#${c.demo}`, ''); } if (p.api?.exports?.length) { lines.push('API 速览:', ''); for (const e of p.api.exports) lines.push(`- ${e}`); lines.push(''); } if (p.usage) lines.push('一行用法:', '', '```', p.usage, '```', ''); if (p.conventions?.length) { lines.push('约定:', ''); for (const cv of p.conventions) lines.push(`- ${cv}`); lines.push(''); } lines.push('---', ''); } if (tokens?.color) { lines.push('## 设计令牌(--ua-*)', ''); for (const [k, v] of Object.entries(tokens.color)) { const light = typeof v === 'string' ? v : v.light; const dark = typeof v === 'string' ? v : v.dark; lines.push(`- --ua-color-${k}: ${light}${dark !== light ? `(暗色 ${dark})` : ''}`); } for (const [g, vals] of Object.entries(tokens)) { if (g === 'color' || g === '$schema') continue; lines.push('', `### ${g}`, ''); for (const [k, v] of Object.entries(vals)) lines.push(`- --ua-${g}-${k}: ${v}`); } lines.push(''); } return lines.join('\n'); } /*** 生成双轨文档:人类版 apps/docs/index.html(含演示 iframe)+ Agent 版 llms-full.txt ***/ function cmdDocs() { if (!fs.existsSync(INDEX_FILE)) cmdIndex(); const index = loadIndex(); const tokensPath = path.join(ROOT, 'packages', 'design', 'tokens', 'tokens.json'); const tokens = fs.existsSync(tokensPath) ? JSON.parse(fs.readFileSync(tokensPath, 'utf8')) : null; const outDir = path.join(ROOT, 'apps', 'docs'); fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'index.html'), renderDocs(index, tokens)); const demoSrc = path.join(ROOT, 'apps', 'demo', 'dist', 'demo.html'); // 演示页拷入文档站供 iframe if (fs.existsSync(demoSrc)) fs.copyFileSync(demoSrc, path.join(outDir, 'demo.html')); // llms-full 头部时间戳仅生成时刻之差视为无变化(与 index 幂等化同方案) const normTs = (s) => s.replace(/自动生成于 [^,]*/, '自动生成于 X'); writeIfChanged(LLMS_FULL, renderAgentDocs(index, tokens), normTs); console.log( `docs generated -> apps/docs/index.html + demo.html, llms-full.txt (${index.packages.length} packages)`, ); } // ---- 内置 stdio MCP server(JSON-RPC 2.0,按行分帧) ---- /*** 启动 MCP server:向 Agent 暴露 list / get / search 三个查询工具 ***/ function cmdMcp() { if (!fs.existsSync(INDEX_FILE)) cmdIndex(); const { packages } = loadIndex(); const tools = [ { name: 'list_components', description: 'List all packages/components in the @ui-arch private library', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_component', description: 'Get full metadata (API, usage, conventions) of one component', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'package name, e.g. @ui-arch/table-core or table-core' } }, required: ['name'], }, }, { name: 'search_components', description: 'Search packages by keyword across name/description/tags/api', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], }, }, ]; const send = (obj) => process.stdout.write(JSON.stringify(obj) + '\n'); /*** 执行 MCP 工具调用 ***/ function callTool(name, args) { if (name === 'list_components') { return packages.map((p) => ({ name: p.name, version: p.version, type: p.type, description: p.description })); } if (name === 'get_component') { const p = packages.find((x) => x.name === args.name || x.name === `@ui-arch/${args.name}`); if (!p) throw new Error(`not found: ${args.name}`); return p; } if (name === 'search_components') { const q = String(args.query ?? '').toLowerCase(); return packages .filter((p) => [p.name, p.description, p.type, ...(p.tags ?? []), ...(p.api?.exports ?? [])] .join(' ') .toLowerCase() .includes(q), ) .map((p) => ({ name: p.name, version: p.version, type: p.type, description: p.description })); } throw new Error(`unknown tool: ${name}`); } /*** 处理单条 JSON-RPC 消息;通知类消息不回包 ***/ function handle(msg) { const { id, method, params } = msg; if (method === 'initialize') { send({ jsonrpc: '2.0', id, result: { protocolVersion: params?.protocolVersion ?? '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'ui-arch-registry', version: '0.1.0' }, }, }); return; } if (method === 'tools/list') { send({ jsonrpc: '2.0', id, result: { tools } }); return; } if (method === 'tools/call') { try { const result = callTool(params?.name, params?.arguments ?? {}); send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }, }); } catch (err) { send({ jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: String(err.message) }] }, }); } return; } if (id != null) { send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } } let buf = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { buf += chunk; let idx; while ((idx = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if (!line) continue; let msg; try { msg = JSON.parse(line); } catch { continue; } handle(msg); } }); } // ---- 风格检查(CODING.md 的机器执行版) ---- /*** 递归收集仓库源码文件(packages + apps),跳过产物与工具目录 ***/ function collectSourceFiles() { const exts = new Set(['.ts', '.mts', '.cts', '.mjs', '.js']); const skip = new Set(['node_modules', 'dist', '.ua', '.git', '.workbuddy']); const out = []; const walk = (dir) => { for (const name of fs.readdirSync(dir)) { if (skip.has(name)) continue; const full = path.join(dir, name); const st = fs.statSync(full); if (st.isDirectory()) walk(full); else if (exts.has(path.extname(name))) out.push(full); } }; walk(ROOT); return out; } /*** 把字符串 / 模板字面量整体替换为等长空格,便于按位置定位真实注释(避开 URL、正则里的 //) ***/ function blankStrings(line) { return line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, (m) => ' '.repeat(m.length)); } /*** 检查单文件:导出缺注释块、接口字段缺行尾注释、行注释非中文 ***/ function styleCheckFile(file, src) { const issues = []; const lines = src.split(/\r?\n/); let iface = null; // 当前追踪中的接口名 let depth = 0; // 接口体花括号深度 for (let i = 0; i < lines.length; i++) { const raw = lines[i]; const line = raw.trim(); if (iface) { for (const ch of raw) { // 花括号计数进出接口体 if (ch === '{') depth++; else if (ch === '}') depth--; } if (/^[A-Za-z_$][\w$]*\??:/.test(line) && !line.includes('//')) { issues.push(`${file}:${i + 1} 接口 ${iface} 字段缺少行尾注释`); } if (depth <= 0) iface = null; } else { const im = raw.match(/\bexport\s+interface\s+(\w+)/); if (im) { depth = (raw.match(/\{/g) || []).length - (raw.match(/\}/g) || []).length; iface = im[1]; } } const decl = line.match(/^export\s+(?:async\s+)?(?:function|interface|type|class)\s+([A-Za-z_$][\w$]*)/); if (decl) { let j = i - 1; while (j >= 0 && lines[j].trim() === '') j--; // 找上一个非空行 if (j < 0 || !lines[j].includes('/***')) { issues.push(`${file}:${i + 1} 导出 ${decl[1]} 缺少 /*** 作用 ***/ 注释`); } } const hash = blankStrings(raw).indexOf('//'); // 注释定位:先剥离字面量,'https://' 不算注释 if (hash >= 0) { const text = raw.slice(hash + 2); const isPragma = /^[\s*/]*(eslint|@ts-|prettier-ignore|istanbul|c8|node:)/i.test(text); if (!isPragma && /[A-Za-z]{2,}/.test(text) && !/[\u4e00-\u9fff]/.test(text)) { issues.push(`${file}:${i + 1} 行注释需使用中文`); } } } return issues; } /*** 全仓风格检查:有违规退出码 1,全绿输出 OK ***/ function cmdStyle() { const files = collectSourceFiles(); const issues = []; for (const f of files) { issues.push(...styleCheckFile(f, fs.readFileSync(f, 'utf8'))); } if (issues.length) { for (const it of issues) console.error('style: ' + it); console.error(`${issues.length} violation(s)`); process.exit(1); } console.log(`style OK (${files.length} files)`); } /*** 皮肤包硬编码颜色检查:#hex 字面量一律禁止,颜色必须走 --ua-* 令牌(rgba 仅限阴影/遮罩的透明度叠加) ***/ function checkSkinCss(pkg) { const issues = []; const dir = path.join(ROOT, pkg.dir); if (!fs.existsSync(dir)) return issues; for (const name of fs.readdirSync(dir)) { if (!name.endsWith('.css')) continue; // 只查样式表 const file = path.join(dir, name); const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); lines.forEach((line, i) => { if (/#[0-9a-fA-F]{3,8}\b/.test(line)) issues.push(`${pkg.dir}/${name}:${i + 1} 皮肤包禁止硬编码颜色,改用 --ua-* 令牌`); }); } return issues; } /*** README 组件区块校验:marker 之间的 id 集合必须与全部清单的 docs.components 完全一致(防文档漂移) ***/ function checkReadme(pkgs) { const issues = []; const file = path.join(ROOT, 'README.md'); if (!fs.existsSync(file)) return issues; // 卫语句:没有 README 就不校验 const block = fs.readFileSync(file, 'utf8').match(/([\s\S]*?)/); if (!block) { issues.push('README.md 缺少组件区块标记( … )'); return issues; } const listed = new Set(block[1].split('·').map((s) => s.trim()).filter(Boolean)); const actual = new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id))); for (const id of actual) if (!listed.has(id)) issues.push(`README.md 组件区块缺少 ${id}`); for (const id of listed) if (!actual.has(id)) issues.push(`README.md 组件区块多出 ${id}(清单里没有)`); return issues; } /*** 组件数漂移校验:AGENTS.md / ARCHITECTURE.md 中的「N 个组件」必须与 skin-dom 清单实际总数一致 ***/ function checkCountDocs(actual) { const issues = []; const re = /(\d+)\s*个(?:\s*DOM\s*)?组件/; for (const name of ['AGENTS.md', 'ARCHITECTURE.md', 'README.md']) { const file = path.join(ROOT, name); if (!fs.existsSync(file)) continue; // 卫语句:文档缺失不校验 fs.readFileSync(file, 'utf8').split(/\r?\n/).forEach((line, i) => { const m = line.match(re); if (m && Number(m[1]) !== actual) { issues.push(`${name}:${i + 1} 组件数「${m[1]}」与清单实际 ${actual} 不一致,请同步`); } }); } return issues; } /*** llms-full.txt 同步校验:内联重渲染 Agent 文档(时间戳归一化)与落盘产物比对,防 manifest 改后忘跑 docs ***/ function checkLlmsFull(pkgs, issues) { if (!fs.existsSync(LLMS_FULL)) return; // 卫语句:产物缺失不算漂移(可能是新仓库) const tokensPath = path.join(ROOT, 'packages', 'design', 'tokens', 'tokens.json'); const tokens = fs.existsSync(tokensPath) ? JSON.parse(fs.readFileSync(tokensPath, 'utf8')) : null; const norm = (s) => s.replace(/\r\n/g, '\n').replace(/自动生成于 [^,]*/, '自动生成于 X'); const expected = renderAgentDocs({ generatedAt: '', packages: pkgs }, tokens); if (norm(fs.readFileSync(LLMS_FULL, 'utf8')) !== norm(expected)) { issues.push('llms-full.txt 与清单不一致,请运行 npm run docs 重新生成'); } } /*** 清单一致性校验:组件文档块的 id / props 说明 / demo 锚点必须与代码和演示产物对齐 ***/ function cmdVerify() { const issues = []; const pkgs = collectManifests(); // 一次收集,供组件校验与 README 区块校验共用 const demoPath = path.join(ROOT, 'apps', 'demo', 'main.ts'); // 演示区块定义在源码里(产物是打包后的 JS) const demoSource = fs.existsSync(demoPath) ? fs.readFileSync(demoPath, 'utf8') : ''; const anchors = new Set(demoSource.match(/sec-[a-z0-9-]+/g) ?? []); // 已定义的区块锚点 if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验'); for (const pkg of pkgs) { const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平 const exports = pkg.api?.exports ?? []; if (pkg.type === 'skin-dom') issues.push(...checkSkinCss(pkg)); if (comps.length && !exports.length) issues.push(`${pkg.name}: docs.components 有 ${comps.length} 项但 api.exports 为空`); const seen = new Set(); // 组件 id 去重(同一清单内不允许重复) for (const c of comps) { const tag = `${pkg.name}#${c.id ?? '?'}`; if (!c.id || !c.title) issues.push(`${tag}: 缺少 id 或 title`); if (seen.has(c.id)) issues.push(`${tag}: id 重复`); seen.add(c.id); if (!c.example) issues.push(`${tag}: 缺少 example`); if ((c.steps ?? []).length < 3) issues.push(`${tag}: steps 少于 3 条`); if (!exports.some((e) => e.startsWith(`${c.id}(`) || e.startsWith(`${c.id}.`))) { issues.push(`${tag}: api.exports 中没有以「${c.id}(」或「${c.id}.」开头的条目`); } for (const prop of c.props ?? []) { if (!prop.n || !prop.t || !prop.d) issues.push(`${tag}: props 条目 ${prop.n ?? '?'} 缺 n/t/d`); } if (c.demo && demoSource && !anchors.has(c.demo)) { issues.push(`${tag}: demo 锚点 ${c.demo} 在 apps/demo/main.ts 中不存在(缺对应 section)`); } } } const skinTotal = pkgs // skin-dom 清单组件总数,供文档数字漂移校验 .filter((p) => p.type === 'skin-dom') .flatMap((p) => p.docs?.components ?? []).length; issues.push(...checkCountDocs(skinTotal)); issues.push(...checkReadme(pkgs)); checkLlmsFull(pkgs, issues); if (issues.length) { for (const it of issues) console.error('verify: ' + it); console.error(`${issues.length} violation(s)`); process.exit(1); } console.log('verify OK (manifest ↔ docs ↔ demo 一致,皮肤无硬编码颜色,README 组件清单同步)'); } /*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/ function cmdCommitLint(args) { // 输入来源:文件路径(commit-msg 钩子传参)或参数拼接 let msg = ''; if (args[0] && fs.existsSync(args[0])) msg = fs.readFileSync(args[0], 'utf8'); else msg = args.join(' '); if (!msg.trim()) { console.error('commit-lint: 提交信息为空'); process.exit(1); } const head = msg.split('\n')[0].trimEnd(); // git 自动生成的合并/回滚信息直接放行 if (/^(Merge |Revert |fixup!|squash!)/.test(head)) return console.log('commit-lint OK (git 自动信息,跳过校验)'); const TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'iter']; const m = head.match(/^(\w+)(?:\(([^)]*)\))?: (.+)$/); if (!m) { console.error('commit-lint: 首行须为 (): ,规范见 COMMITTING.md'); process.exit(1); } const [, type, scope, subject] = m; const problems = []; // type 枚举 if (!TYPES.includes(type)) problems.push(`type「${type}」不在枚举:${TYPES.join('/')}`); // iter 提交 scope 必须是 backlog 条目 id if (type === 'iter' && !/^[a-z]\d{2,}$/.test(scope ?? '')) problems.push('iter 提交的 scope 必须是 backlog 条目 id(如 b02/e03)'); // 非 iter 提交 scope 必填(杜绝 chore:/docs: 裸 type 放行),且仅允许小写字母与连字符 if (type !== 'iter' && !scope) problems.push(`type「${type}」必须带 scope,如 chore(backlog):`); if (type !== 'iter' && scope && !/^[a-z][a-z-]*$/.test(scope)) problems.push(`scope「${scope}」仅允许小写字母与连字符`); // subject 长度与标点 if ([...subject].length > 50) problems.push(`subject 超过 50 字符(当前 ${[...subject].length})`); if (/[。..!!??]$/.test(subject.trim())) problems.push('subject 结尾不加句号'); // body 行宽 for (const line of msg.split('\n').slice(1)) { if (line.length > 80) problems.push(`body 行超过 80 字符:${line.slice(0, 20)}…`); } if (problems.length) { for (const p of problems) console.error('commit-lint: ' + p); process.exit(1); } console.log(`commit-lint OK (${type}${scope ? `(${scope})` : ''})`); } /*** 迭代留痕校验:.ua/iterations.log 每行 JSON 字段与格式必须符合 ITERATION.md 规范 ***/ function cmdLintIterations() { const file = path.join(INDEX_DIR, 'iterations.log'); if (!fs.existsSync(file)) return console.log('lint-iterations OK (no log)'); const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => l.trim()); const issues = []; lines.forEach((line, i) => { const tag = `iterations.log:${i + 1}`; let o; try { o = JSON.parse(line); } catch { issues.push(`${tag} 不是合法 JSON`); return; } const keys = Object.keys(o); if (keys.length !== 5 || !['ts', 'id', 'result', 'commit', 'summary'].every((k) => keys.includes(k))) { issues.push(`${tag} 字段必须恰为 {ts,id,result,commit,summary}(当前 ${keys.join(',')})`); } if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(String(o.ts ?? '')) || Number.isNaN(Date.parse(o.ts))) { issues.push(`${tag} ts 须为可解析的本地 +08:00 ISO 时间`); } if (!/^[a-z]\d{2,}$/.test(String(o.id ?? ''))) issues.push(`${tag} id 须为 backlog 条目 id`); if (o.result !== 'done' && o.result !== 'aborted') issues.push(`${tag} result 须为 done|aborted`); const commit = String(o.commit ?? ''); if (o.result === 'done' && !/^[0-9a-f]{7,40}$/.test(commit)) issues.push(`${tag} done 条目 commit 须为 7-40 位哈希`); if (o.result === 'aborted' && commit && !/^[0-9a-f]{7,40}$/.test(commit)) issues.push(`${tag} aborted 条目 commit 须为空或哈希`); if (!String(o.summary ?? '').trim()) issues.push(`${tag} summary 不能为空`); }); if (issues.length) { for (const it of issues) console.error('lint-iterations: ' + it); console.error(`${issues.length} violation(s)`); process.exit(1); } console.log(`lint-iterations OK (${lines.length} entries)`); } // ---- 入口 ---- const [cmd, ...args] = process.argv.slice(2); switch (cmd) { case 'index': cmdIndex(); break; case 'list': cmdList(); break; case 'info': if (!args[0]) { console.error('usage: ua info '); process.exit(1); } cmdInfo(args[0]); break; case 'docs': cmdDocs(); break; case 'style': cmdStyle(); break; case 'verify': cmdVerify(); break; case 'commit-lint': cmdCommitLint(args); break; case 'lint-iterations': cmdLintIterations(); break; case 'mcp': cmdMcp(); break; default: console.log('usage: ua |docs|style|verify|commit-lint|lint-iterations|mcp>'); process.exit(cmd ? 1 : 0); }