Files
UI/packages/tools/cli/bin/ua.js
T

593 lines
24 KiB
JavaScript
Raw 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
/*** @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;
}
/*** 聚合清单,生成 .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,
};
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2) + '\n');
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('');
}
fs.writeFileSync(LLMS_FILE, lines.join('\n'));
console.log(`indexed ${packages.length} package(s) -> .ua/index.json, llms.txt`);
}
/*** 读取注册表索引;缺失则报错退出 ***/
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/*** 渲染人类文档站:注册表 + 逐组件 props/步骤 + 现场演示 iframe + 令牌,数据内嵌支持搜索 ***/
function renderDocs(index, tokens) {
const data = JSON.stringify(index.packages).replace(/</g, '\\u003c');
const tokData = JSON.stringify(tokens).replace(/</g, '\\u003c');
const generatedAt = esc(index.generatedAt);
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ui-arch 组件库文档</title>
<style>
:root{--bg:#ffffff;--surface:#f6f7f9;--text:#1c1e21;--muted:#6b7280;--border:rgba(0,0,0,.14);--accent:#185fa5;--code-bg:#101216;--code-text:#e7e9ec}
@media (prefers-color-scheme:dark){:root{--bg:#101216;--surface:#1a1c22;--text:#e7e9ec;--muted:#888780;--border:rgba(255,255,255,.15);--accent:#378add}}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.65 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
header{position:sticky;top:0;z-index:2;display:flex;gap:16px;align-items:center;flex-wrap:wrap;padding:14px 24px;background:var(--bg);border-bottom:1px solid var(--border)}
header h1{margin:0;font-size:16px;font-weight:500}
header h1 span{color:var(--muted);font-weight:400;font-size:14px;margin-left:8px}
#q{flex:1;min-width:220px;max-width:420px;margin-left:auto;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);font-size:13px;outline:none}
#q:focus{border-color:var(--accent)}
nav.pkgnav{max-width:960px;margin:18px auto 0;padding:0 20px;display:flex;gap:8px;flex-wrap:wrap}
nav.pkgnav a{font-size:12px;color:var(--accent);text-decoration:none;border:1px solid var(--border);border-radius:999px;padding:3px 12px}
main{max-width:960px;margin:0 auto;padding:20px 20px 60px}
.card{border:1px solid var(--border);border-radius:12px;background:var(--surface);padding:22px 24px;margin-bottom:22px}
.card h2{margin:0;font-size:16px;font-weight:500;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ver{font-size:12px;color:var(--muted)}
.badge{font-size:11px;padding:2px 8px;border-radius:999px;background:var(--accent);color:#fff}
.desc{color:var(--muted);margin:10px 0 0}
.tag{display:inline-block;font-size:11px;padding:2px 9px;margin:10px 6px 0 0;border:1px solid var(--border);border-radius:999px;color:var(--muted)}
.card h3{font-size:13px;font-weight:500;margin:18px 0 8px}
.card ul,.card ol{margin:0;padding-left:18px}
.card li{margin:4px 0}
.comp{border:1px dashed var(--border);border-radius:10px;padding:14px 16px;margin:14px 0 0;background:var(--bg)}
.comp h4{margin:0 0 8px;font-size:14px;font-weight:500;font-family:Consolas,Menlo,monospace}
.comp h5{margin:12px 0 6px;font-size:12px;color:var(--muted);font-weight:500}
table.props{width:100%;border-collapse:collapse;font-size:12.5px;margin:8px 0}
table.props th,table.props td{border:1px solid var(--border);padding:5px 9px;text-align:left;vertical-align:top}
table.props th{background:var(--surface);font-weight:500;color:var(--muted)}
pre{background:var(--code-bg);color:var(--code-text);border-radius:8px;padding:12px 14px;overflow-x:auto;font:12.5px/1.6 Consolas,Menlo,monospace;margin:8px 0}
code{font-family:Consolas,Menlo,monospace;font-size:12.5px}
li code,td code{background:var(--code-bg);color:var(--code-text);padding:1px 6px;border-radius:4px}
iframe.demo{width:100%;height:430px;border:1px dashed var(--border);border-radius:10px;margin-top:10px;background:var(--surface)}
.sw{display:flex;align-items:center;gap:10px;margin:6px 0}
.sw i{width:26px;height:26px;border-radius:6px;border:1px solid var(--border);flex:none}
.sw code{color:var(--muted)}
.sw span{font-size:12px;color:var(--muted)}
.swatches{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:4px 20px}
.empty{color:var(--muted);text-align:center;padding:40px 0}
footer{max-width:960px;margin:0 auto;padding:0 20px 40px;color:var(--muted);font-size:12px}
</style>
</head>
<body>
<header><h1>@ui-arch<span>私人组件库</span></h1><input id="q" placeholder="搜索组件、参数、API…" autocomplete="off"></header>
<nav class="pkgnav" id="nav"></nav>
<main id="app"></main>
<footer>由 <code>ua docs</code> 生成于 ${generatedAt} · AI 版全文:<code>llms-full.txt</code> · 重新生成:npm run docs</footer>
<script>
var DATA=${data};
var TOKENS=${tokData};
var ORDER={skin-dom:0,'headless-core':1,'design-tokens':2};
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function propsTable(c){
var rows=(c.props||[]).map(function(p){
return '<tr><td><code>'+esc(p.n)+'</code></td><td><code>'+esc(p.t)+'</code></td><td>'+(p.r?'必填':'否')+'</td><td>'+(p.df?'<code>'+esc(p.df)+'</code>':'—')+'</td><td>'+esc(p.d||'')+(p.k?'(回调)':'')+'</td></tr>';
}).join('');
return '<table class="props"><tr><th>参数</th><th>类型</th><th>必填</th><th>默认值</th><th>说明</th></tr>'+rows+'</table>';
}
function compBlock(c){
var steps=(c.steps||[]).map(function(s){return '<li>'+esc(s)+'</li>'}).join('');
return '<div class="comp" id="c-'+esc(c.id)+'"><h4>'+esc(c.id)+' · '+esc(c.title||'')+'</h4>'
+(c.props?propsTable(c):'')
+(c.example?'<pre><code>'+esc(c.example)+'</code></pre>':'')
+(steps?'<h5>手把手</h5><ol>'+steps+'</ol>':'')
+(c.demo?'<iframe class="demo" src="demo.html#'+esc(c.demo)+'" loading="lazy" title="'+esc(c.id)+' 现场演示"></iframe>':'')
+'</div>';
}
function pkgCard(p){
var steps=(p.docs&&p.docs.steps||[]).map(function(s){return '<li>'+esc(s)+'</li>'}).join('');
var comps=(p.docs&&p.docs.components||[]).map(compBlock).join('');
var ex=(p.api&&p.api.exports||[]).map(function(e){return '<li><code>'+esc(e)+'</code></li>'}).join('');
var conv=(p.conventions||[]).map(function(c){return '<li>'+esc(c)+'</li>'}).join('');
var tags=(p.tags||[]).map(function(t){return '<span class="tag">'+esc(t)+'</span>'}).join('');
return '<section class="card" id="p-'+esc(p.name.replace(/[^\\w-]/g,''))+'">'
+'<h2>'+esc(p.name)+' <span class="ver">v'+esc(p.version)+'</span> <span class="badge">'+esc(p.type)+'</span></h2>'
+'<p class="desc">'+esc((p.docs&&p.docs.intro)||p.description)+'</p><div>'+tags+'</div>'
+(steps?'<h3>手把手接入</h3><ol>'+steps+'</ol>':'')
+(comps?'<h3>组件明细</h3>'+comps:'')
+(ex?'<h3>API 速览</h3><ul>'+ex+'</ul>':'')
+(p.usage?'<h3>一行用法</h3><pre><code>'+esc(p.usage)+'</code></pre>':'')
+(conv?'<h3>约定</h3><ul>'+conv+'</ul>':'')
+'</section>';
}
function quickstart(){
var ui=DATA.filter(function(p){return p.docs&&p.docs.steps}).sort(function(a,b){return (ORDER[a.type]??9)-(ORDER[b.type]??9)});
if(!ui.length)return '';
var blocks=ui.map(function(p){
var steps=p.docs.steps.map(function(s){return '<li>'+esc(s)+'</li>'}).join('');
return '<p class="desc" style="margin:10px 0 4px"><b>'+esc(p.name)+'</b></p><ol>'+steps+'</ol>';
}).join('');
return '<section class="card"><h2>快速上手 <span class="badge">guide</span></h2>'+blocks+'</section>';
}
function tokensCard(){
if(!TOKENS||!TOKENS.color)return '';
var sw=Object.keys(TOKENS.color).map(function(k){
var v=TOKENS.color[k];
var light=typeof v==='string'?v:v.light;
var dark=typeof v==='string'?v:v.dark;
var extra=dark&&dark!==light?' / '+dark:'';
return '<div class="sw"><i style="background:'+light+'"></i><code>--ua-'+esc(k)+'</code><span>'+light+extra+'</span></div>';
}).join('');
return '<section class="card"><h2>@ui-arch/tokens <span class="badge">design-tokens</span></h2><p class="desc">皮肤包只允许消费 --ua-* CSS 变量;暗色主题覆盖值不换名(light / dark)。</p><h3>颜色令牌</h3><div class="swatches">'+sw+'</div></section>';
}
function render(list){
var app=document.getElementById('app');
if(!list.length){app.innerHTML='<p class="empty">无匹配结果</p>';return}
app.innerHTML=quickstart()+list.map(pkgCard).join('')+(list.some(function(p){return p.name.indexOf('tokens')>=0})?'':tokensCard());
}
function norm(p){
var parts=[p.name,p.description,p.type,(p.tags||[]).join(' '),(p.api&&p.api.exports||[]).join(' ')];
(p.docs&&p.docs.components||[]).forEach(function(c){parts.push(c.id,c.title||'',(c.props||[]).map(function(x){return x.n}).join(' '))});
return parts.join(' ').toLowerCase();
}
var ordered=DATA.slice().sort(function(a,b){return (ORDER[a.type]??9)-(ORDER[b.type]??9)});
document.getElementById('nav').innerHTML=ordered.map(function(p){return '<a href="#p-'+esc(p.name.replace(/[^\\w-]/g,''))+'">'+esc(p.name)+'</a>'}).join('')+'<a href="#top-tokens">令牌</a>';
render(ordered);
document.getElementById('q').addEventListener('input',function(e){
var q=e.target.value.trim().toLowerCase();
render(q?ordered.filter(function(p){return norm(p).indexOf(q)>=0}):ordered);
});
</script>
</body>
</html>
`;
}
/*** 渲染 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'));
fs.writeFileSync(LLMS_FULL, renderAgentDocs(index, tokens));
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)`);
}
// ---- 入口 ----
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 <package-name>');
process.exit(1);
}
cmdInfo(args[0]);
break;
case 'docs':
cmdDocs();
break;
case 'style':
cmdStyle();
break;
case 'mcp':
cmdMcp();
break;
default:
console.log('usage: ua <index|list|info <name>|docs|style|mcp>');
process.exit(cmd ? 1 : 0);
}