agentcss 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/01-tokens/README.md +1 -1
- package/01-tokens/responsive-demo.html +1 -1
- package/01-tokens/tokens.core.css +1 -1
- package/01-tokens/tokens.css +2 -2
- package/02-primitives/date-range/date-range.js +1 -1
- package/02-primitives/timeline/demo.html +1 -1
- package/03-patterns/drawer/demo.html +1 -1
- package/03-patterns/overlay/overlay.css +1 -1
- package/03-patterns/states/states.css +1 -1
- package/04-recipes/app-shell/README.md +1 -1
- package/04-recipes/app-shell/demo.html +1 -1
- package/04-recipes/longform/content.html +6 -6
- package/04-recipes/report/demo.html +2 -2
- package/04-recipes/table/README.md +1 -1
- package/04-recipes/table/demo.html +1 -1
- package/09-assets/bar/bar.css +1 -1
- package/09-assets/model-viewer/model-viewer.js +1 -1
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +103 -5
- package/CONTRIBUTING.md +44 -3
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/START-HERE.md +12 -5
- package/ai/START-HERE.md +40 -6
- package/ai/check-html.js +658 -0
- package/ai/cli.js +557 -67
- package/ai/components.json +1 -1
- package/ai/pdf.js +2 -2
- package/ai/recipes.json +86 -0
- package/ai/scaffold.js +195 -0
- package/ai/shot.js +345 -0
- package/ai/validate.js +65 -6
- package/dist/COSTS.md +10 -10
- package/docs/ACCESSIBILITY.md +2 -2
- package/docs/ARCHITECTURE.md +116 -0
- package/docs/LAYER-BASELINE.md +2 -2
- package/docs/PRINT.md +3 -3
- package/docs/RELEASE-PROCESS.md +52 -2
- package/docs/STABILITY.md +1 -1
- package/docs/VERIFY-RELEASE.md +17 -0
- package/index.html +3 -3
- package/llms-full.txt +17 -3
- package/llms.txt +26 -5
- package/package.json +2 -1
package/ai/cli.js
CHANGED
|
@@ -16,13 +16,16 @@
|
|
|
16
16
|
* 用法
|
|
17
17
|
* ----------------------------------------------------------------------------
|
|
18
18
|
* agentcss ai check <doc.json> [--profile=strict]
|
|
19
|
-
* agentcss ai
|
|
19
|
+
* agentcss ai check <page.html> [--recipe=report/finance] [--strict]
|
|
20
|
+
* agentcss ai validate <file.json> --schema=<名字>(见 SCHEMAS,当前 8 份)
|
|
20
21
|
* agentcss ai diff <a.json> <b.json>
|
|
21
22
|
* agentcss ai patch <doc.json> <patch.json> [-o out.json]
|
|
22
23
|
* agentcss ai components [--maturity=stable] [--json]
|
|
23
24
|
* agentcss ai tokens [--set=light|dark] [--json]
|
|
24
25
|
* agentcss ai profile 列出三档及其差异
|
|
25
26
|
* agentcss ai capabilities
|
|
27
|
+
* agentcss ai scaffold <版式> --out <page.html>
|
|
28
|
+
* agentcss ai shot <page.html> --out <png> [--width=390,1440] [--full]
|
|
26
29
|
* ============================================================================
|
|
27
30
|
*/
|
|
28
31
|
'use strict';
|
|
@@ -47,38 +50,100 @@ function pad(s, n) {
|
|
|
47
50
|
return out;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
/* 读 JSON。`fromUser` 区分「调用方给我的文件」与「包内自带的文件」——
|
|
54
|
+
这两类读失败的性质完全不同,退出码也不同:
|
|
55
|
+
· 用户文件读不到/不是 JSON ⇒ usage(2):改调用参数就能解决
|
|
56
|
+
· 包内文件读不到 ⇒ internal(6):包坏了,是工具/打包的问题
|
|
57
|
+
不带这个区分时,两者都被顶层兜底成 internal(6),
|
|
58
|
+
于是"你指错了文件"被报成"工具内部错误"—— 调用方会去查工具的 bug。 */
|
|
59
|
+
function readJSON(file, fromUser) {
|
|
60
|
+
var txt;
|
|
61
|
+
try {
|
|
62
|
+
txt = fs.readFileSync(file, 'utf8');
|
|
63
|
+
} catch (e) {
|
|
64
|
+
var miss = new Error((fromUser ? '读不到 ' : '包内文件读不到 ') + file
|
|
65
|
+
+ '(' + (e.code || e.message) + ')');
|
|
66
|
+
miss.kind = fromUser ? 'usage' : 'internal';
|
|
67
|
+
throw miss;
|
|
68
|
+
}
|
|
52
69
|
try {
|
|
53
70
|
return JSON.parse(txt);
|
|
54
71
|
} catch (e) {
|
|
55
|
-
|
|
72
|
+
var malformed = new Error(file + ' 不是合法 JSON:' + e.message);
|
|
73
|
+
malformed.kind = fromUser ? 'usage' : 'internal';
|
|
74
|
+
throw malformed;
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
function out(obj) {
|
|
60
|
-
|
|
79
|
+
/* 🔴 `JSON.stringify(undefined)` 返回的是 **undefined**(不是字符串),
|
|
80
|
+
拼上 '\n' 之后 stdout 得到字面量 `undefined`,退出码还是 0。
|
|
81
|
+
0.26.0 的 `ai tokens --set=<拼错的>` 就是这么"成功"的:
|
|
82
|
+
调用方的 JSON.parse 拿到 `undefined` 三个字,报一个跟真实原因无关的解析错。
|
|
83
|
+
枚举校验已经把已知那条路堵死,这里再加一道兜底 —— 任何**未来**的
|
|
84
|
+
"忘了判断就 out()" 都会当场变成 internal(退出码 6),而不是一个假成功。 */
|
|
85
|
+
var txt = JSON.stringify(obj, null, 2);
|
|
86
|
+
if (txt === undefined) {
|
|
87
|
+
fail('internal', '要输出的对象是 undefined —— 这是工具的问题(没有结果却走到了输出)');
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(txt + '\n');
|
|
61
90
|
}
|
|
62
91
|
|
|
63
92
|
var CURRENT_CMD = '';
|
|
64
93
|
|
|
65
|
-
/* 🔴
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
/* 🔴 退出码表只有这一份 —— 调用方要能**按码分支**,不必按子命令写 switch。
|
|
95
|
+
*
|
|
96
|
+
* 0 通过
|
|
97
|
+
* 1 判定不通过(页面/文档确实有错,跑完了,结论是不合格)
|
|
98
|
+
* 2 用法错误(参数名、取值、组合不对)
|
|
99
|
+
* 3 环境不具备(没有浏览器、node 不在 PATH)
|
|
100
|
+
* 4 渲染失败(超时、读不出、视口没设成)
|
|
101
|
+
* 5 超出预算(产出了完整文件,只是超了页数)
|
|
102
|
+
* 6 工具内部错误(是工具的 bug,不是你的输入错)
|
|
103
|
+
*
|
|
104
|
+
* 为什么 2/3/4/5 是这个数:`pdf` 早就这么发(CHANGELOG 里公开过
|
|
105
|
+
* 「2 用法 / 3 没浏览器 / 4 渲染失败 / 5 超预算」)。把它们收敛成全 CLI 唯一一套,
|
|
106
|
+
* 而不是新增一套再共存 —— 两套码必然漂移。
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ **1 只表示"判定不通过"**,不表示工具坏了。以前 `die()` 用 1,
|
|
109
|
+
* 于是"参数写错"和"页面有 3 个错"拿到同一个码:调用方要么去改页面(改错方向),
|
|
110
|
+
* 要么去改参数(也改错方向)。这是本轮审计点名的第一个问题。
|
|
111
|
+
*/
|
|
112
|
+
var EXIT = {
|
|
113
|
+
findings: 1,
|
|
114
|
+
usage: 2,
|
|
115
|
+
'no-chrome': 3, env: 3,
|
|
116
|
+
render: 4, timeout: 4, unreadable: 4, viewport: 4,
|
|
117
|
+
'over-budget': 5,
|
|
118
|
+
internal: 6
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/* 🔴 失败也必须能被机器读,且**只有一种形状**:
|
|
122
|
+
{ ok:false, command, error:{ kind, message }, ...上下文 }
|
|
123
|
+
以前 `pdf` 发 `{ok:false, code, message}`、其它子命令发 `{ok:false, error:{...}}`
|
|
124
|
+
—— 同一个 CLI 的失败有两种形状,调用方要么写两个分支,要么只看 ok。
|
|
125
|
+
现在 kind 的取值就是上面那张退出码表的键,`kind` → 码是确定的。 */
|
|
126
|
+
function fail(kind, msg, extra) {
|
|
127
|
+
var k = EXIT[kind] === undefined ? 'internal' : kind;
|
|
70
128
|
if (isJson()) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
129
|
+
var doc = { ok: false, command: CURRENT_CMD || null,
|
|
130
|
+
error: { kind: k, message: String(msg) } };
|
|
131
|
+
if (extra) for (var ek in extra) {
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(extra, ek)) doc[ek] = extra[ek];
|
|
133
|
+
}
|
|
134
|
+
out(doc);
|
|
135
|
+
} else {
|
|
136
|
+
process.stderr.write('[FAIL] ' + msg + '\n');
|
|
137
|
+
if (k === 'internal') {
|
|
138
|
+
process.stderr.write(' 这是工具内部错误,不是你参数写错了'
|
|
139
|
+
+ '(退出码 ' + EXIT.internal + ')\n');
|
|
140
|
+
}
|
|
77
141
|
}
|
|
78
|
-
process.
|
|
79
|
-
process.exit(1);
|
|
142
|
+
process.exit(EXIT[k]);
|
|
80
143
|
}
|
|
81
144
|
|
|
145
|
+
function die(msg) { fail('usage', msg); }
|
|
146
|
+
|
|
82
147
|
/** 是否要求 JSON 输出。`--json` / `--json=true` 都认,`--json=false` 不认。 */
|
|
83
148
|
function isJson() {
|
|
84
149
|
var v = flag('json', false);
|
|
@@ -107,16 +172,114 @@ function flag(name, def) {
|
|
|
107
172
|
return v;
|
|
108
173
|
}
|
|
109
174
|
|
|
175
|
+
/* ---------------------------------------------------------------- 枚举参数
|
|
176
|
+
🔴 非法取值**必须报错**,不许退化成"空结果"。
|
|
177
|
+
0.26.0 的实测(本轮审计 F1):
|
|
178
|
+
ai tokens --set=not-a-theme --json → stdout 是字面量 `undefined`,退出码 0
|
|
179
|
+
ai tree --set=not-a-theme --json → 同上
|
|
180
|
+
ai components --maturity=not-a-level → `[]`,退出码 0
|
|
181
|
+
对人是"没输出",对 Agent 是**错误的控制流**:它会把拼错的参数读成
|
|
182
|
+
"这个库没有 beta 组件",再据此写出错误的结论,而且全程没有报错。
|
|
183
|
+
|
|
184
|
+
取值**不在本文件里重抄**:
|
|
185
|
+
· maturity 的词表来自 profiles.json 的 allowedMaturity(三档取并集)
|
|
186
|
+
· set / cat 来自 tokens.json / tokens.tree.json 的键与 meta.categories
|
|
187
|
+
抄一份就会漂 —— 组件升到 stable 后,这里还留着 beta,判据就恒真。 */
|
|
188
|
+
var ENUMS = {
|
|
189
|
+
set: function () {
|
|
190
|
+
return Object.keys(readJSON(path.join(AI, 'tokens.json')).sets);
|
|
191
|
+
},
|
|
192
|
+
cat: function () {
|
|
193
|
+
return readJSON(path.join(AI, 'tokens.tree.json')).meta.categories;
|
|
194
|
+
},
|
|
195
|
+
maturity: function () {
|
|
196
|
+
var p = readJSON(path.join(AI, 'profiles.json')).profiles;
|
|
197
|
+
var seen = [];
|
|
198
|
+
Object.keys(p).forEach(function (k) {
|
|
199
|
+
(p[k].allowedMaturity || []).forEach(function (m) {
|
|
200
|
+
if (seen.indexOf(m) < 0) seen.push(m);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
return seen;
|
|
204
|
+
},
|
|
205
|
+
profile: ['creative', 'standard', 'strict'],
|
|
206
|
+
medium: ['screen', 'paper'],
|
|
207
|
+
/* 版式清单来自 `ai/recipes.json`(由 `check-html.js` 读取并校验形状),
|
|
208
|
+
不在这里重抄 —— 加一个版式只改一个地方。 */
|
|
209
|
+
recipe: function () {
|
|
210
|
+
var r = require('./check-html.js').readRecipes();
|
|
211
|
+
return r ? Object.keys(r.recipes) : [];
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
function enumValues(name) {
|
|
216
|
+
var src = ENUMS[name];
|
|
217
|
+
if (!src) return null;
|
|
218
|
+
return typeof src === 'function' ? src() : src;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* 同一个枚举开关出现两次 ⇒ 报错。`--set=light --set=dark` 以前**静默取第一个**:
|
|
222
|
+
调用方以为切到了 dark,拿到的却是 light,而且没有任何迹象。 */
|
|
223
|
+
function flagCount(name) {
|
|
224
|
+
var eq = '--' + name + '=', bare = '--' + name, n = 0;
|
|
225
|
+
for (var i = 0; i < process.argv.length; i++) {
|
|
226
|
+
var a = process.argv[i];
|
|
227
|
+
if (a === bare || a.indexOf(eq) === 0) n++;
|
|
228
|
+
}
|
|
229
|
+
return n;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* 读一个枚举开关;没给返回 null。给了就一定是合法值,否则当场退出(非 0)。 */
|
|
233
|
+
function enumFlag(name) {
|
|
234
|
+
var v = flag(name, null);
|
|
235
|
+
if (v === null) return null;
|
|
236
|
+
var allowed = enumValues(name);
|
|
237
|
+
if (!allowed) fail('internal', '枚举 ' + name + ' 没有登记在 ENUMS 里');
|
|
238
|
+
var hint = '(可用:' + allowed.join(' | ') + ')';
|
|
239
|
+
var n = flagCount(name);
|
|
240
|
+
if (n > 1) die('--' + name + ' 给了 ' + n + ' 次。只认一个值 —— 想对比就分两次调用' + hint);
|
|
241
|
+
if (v === true) die('--' + name + ' 后面要跟取值' + hint);
|
|
242
|
+
if (String(v) === '') die('--' + name + '= 是空值' + hint);
|
|
243
|
+
if (allowed.indexOf(String(v)) < 0) {
|
|
244
|
+
die('--' + name + '=' + v + ' 不是合法值' + hint);
|
|
245
|
+
}
|
|
246
|
+
return String(v);
|
|
247
|
+
}
|
|
248
|
+
|
|
110
249
|
// ---------------------------------------------------------------- check
|
|
111
250
|
function cmdCheck(args) {
|
|
112
251
|
var file = args[0];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
var
|
|
252
|
+
/* 这两个开关**自己就能构成一次调用**(`ai check --list-recipes`),
|
|
253
|
+
所以必须在"必须有文件"之前处理 —— 否则它会被当成文件名去读。 */
|
|
254
|
+
var lr = flag('list-recipes', false);
|
|
255
|
+
if (lr === true || lr === 'true') return cmdCheckHtml(null);
|
|
256
|
+
if (!file) {
|
|
257
|
+
die('用法:ai check <doc.json> [--profile=creative|standard|strict] [--medium=screen|paper]\n'
|
|
258
|
+
+ ' ai check <page.html> [--recipe=<版式>] [--strict] [--list-recipes]');
|
|
259
|
+
}
|
|
260
|
+
/* 🔴 产物与计划是两种东西,判据也不重叠:
|
|
261
|
+
· `.json` → 文档模型(组件在不在档位里、节点类型对不对)
|
|
262
|
+
· `.html` → 写出来的那一页(类名有没有定义、结构缺不缺件)
|
|
263
|
+
用扩展名分派,是因为调用方最容易做对的一件事就是"把文件名给对"。
|
|
264
|
+
显式 `--html` 留给没有扩展名/扩展名不是 html 的临时文件;
|
|
265
|
+
`--recipe` 同理(版式判据只对页面有意义)。 */
|
|
266
|
+
var wantHtml = flag('html', false);
|
|
267
|
+
var hasRecipe = flag('recipe', null) !== null;
|
|
268
|
+
if (wantHtml === true || wantHtml === 'true' || hasRecipe || /\.html?$/i.test(file)) {
|
|
269
|
+
return cmdCheckHtml(file);
|
|
270
|
+
}
|
|
271
|
+
var doc = readJSON(file, true);
|
|
272
|
+
/* 档位优先取命令行,其次取文档自己声明的,最后 standard。
|
|
273
|
+
⚠️ 枚举校验必须在**取值之后**做:「文档里写了什么」也要是合法档位。 */
|
|
274
|
+
var profile = enumFlag('profile') || doc.profile || 'standard';
|
|
275
|
+
if (enumValues('profile').indexOf(String(profile)) < 0) {
|
|
276
|
+
die('文档的 profile 字段是 ' + JSON.stringify(profile)
|
|
277
|
+
+ ',不是合法档位(可用:' + enumValues('profile').join(' | ') + ')');
|
|
278
|
+
}
|
|
116
279
|
/* 介质是**与档位正交**的第二根轴:
|
|
117
280
|
strict+paper 与 creative+paper 都必须合法,做成第四个 profile 会把两维压成一维。
|
|
118
281
|
默认 screen ⇒ 今天所有调用零成本;paper 才追加打印义务。 */
|
|
119
|
-
var medium =
|
|
282
|
+
var medium = enumFlag('medium') || 'screen';
|
|
120
283
|
var r = validate.validateDoc(doc, { profile: profile, medium: medium });
|
|
121
284
|
var json = isJson();
|
|
122
285
|
|
|
@@ -143,6 +306,91 @@ function cmdCheck(args) {
|
|
|
143
306
|
process.exit(r.ok ? 0 : 1);
|
|
144
307
|
}
|
|
145
308
|
|
|
309
|
+
// ------------------------------------------------------- check(产物 / HTML)
|
|
310
|
+
/* ⚠️ 判据全在 `ai/check-html.js` 里,这里只负责"把结果说人话"和退出码。
|
|
311
|
+
分开的理由:门禁(05-audit/html-check-gate.js)要能直接 require 判据,
|
|
312
|
+
不必为了拿一份结论去 fork 一个进程、再去解析中文输出。 */
|
|
313
|
+
function cmdCheckHtml(file) {
|
|
314
|
+
var checkHtml = require('./check-html.js');
|
|
315
|
+
/* 🔴 `--profile` / `--medium` 是**文档模型**的轴,对一页 HTML 没有任何意义。
|
|
316
|
+
以前它们被静默忽略 ⇒ `check page.html --profile=strict` 退出码 0,
|
|
317
|
+
调用方以为"按 strict 档查过了"。**善意的忽略就是假绿。** */
|
|
318
|
+
if (flag('profile', null) !== null) {
|
|
319
|
+
die('--profile 只对 doc.json 有效(HTML 用 --recipe 指定版式)');
|
|
320
|
+
}
|
|
321
|
+
if (flag('medium', null) !== null) {
|
|
322
|
+
die('--medium 只对 doc.json 有效(HTML 用 --recipe 指定版式)');
|
|
323
|
+
}
|
|
324
|
+
/* `enumFlag` 已经把"没给值 / 空值 / 非法值 / 给两次"都挡在外面了,
|
|
325
|
+
这里拿到的要么是 null,要么是合法版式名。 */
|
|
326
|
+
var recipe = enumFlag('recipe');
|
|
327
|
+
var listRecipes = flag('list-recipes', false);
|
|
328
|
+
if (listRecipes === true || listRecipes === 'true') {
|
|
329
|
+
var all = checkHtml.readRecipes();
|
|
330
|
+
if (!all) die('ai/recipes.json 读不到 —— 这份包不完整');
|
|
331
|
+
if (isJson()) { out(all); process.exit(0); }
|
|
332
|
+
process.stdout.write(' === 可校验的版式 ===\n\n');
|
|
333
|
+
Object.keys(all.recipes).forEach(function (k) {
|
|
334
|
+
var r = all.recipes[k];
|
|
335
|
+
process.stdout.write(' ' + pad(k, 20) + r.label + '\n');
|
|
336
|
+
process.stdout.write(' ' + r.root + ' 必有件 ' + r.requires.length + ' 条'
|
|
337
|
+
+ (r.unchecked && r.unchecked.length
|
|
338
|
+
? ' · 另有 ' + r.unchecked.length + ' 条只能人工判' : '') + '\n');
|
|
339
|
+
});
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
/* `--strict` 把警告当失败。默认不这么做:警告里有一类是"页面自己的类名",
|
|
343
|
+
那在单文件页面上完全合法,不该把一页正常的报告判死。 */
|
|
344
|
+
var strictFlag = flag('strict', false);
|
|
345
|
+
var r = checkHtml.checkPage(file, {
|
|
346
|
+
recipe: recipe || null,
|
|
347
|
+
strict: strictFlag === true || strictFlag === 'true'
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (r.code === 'unreadable') die(r.message);
|
|
351
|
+
if (r.code === 'unknown-recipe') die(r.message);
|
|
352
|
+
|
|
353
|
+
if (isJson()) {
|
|
354
|
+
out(r);
|
|
355
|
+
process.exit(r.ok ? 0 : 1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
process.stdout.write(' === 页面检查(' + r.file + ')===\n');
|
|
359
|
+
process.stdout.write(' ' + r.rules.length + ' 条判据 · 本地样式表 ' + r.sources.localSheets
|
|
360
|
+
+ ' 个 · 库内类名 ' + r.sources.libraryClasses
|
|
361
|
+
+ '(' + r.sources.libraryCssFiles + ' 个 CSS)'
|
|
362
|
+
+ (r.recipe ? ' · 版式 ' + r.recipe : '') + '\n\n');
|
|
363
|
+
|
|
364
|
+
r.findings.forEach(function (f) {
|
|
365
|
+
process.stdout.write(' [' + (f.level === 'error' ? 'FAIL' : 'WARN') + '] '
|
|
366
|
+
+ pad(f.line + ' 行', 8) + pad(f.rule, 18) + f.msg + '\n');
|
|
367
|
+
process.stdout.write(' ' + checkHtml.RULES[f.rule].hint + '\n');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (r.skipped.length) {
|
|
371
|
+
process.stdout.write('\n ⚠️ 未判定(**不计入通过**):\n');
|
|
372
|
+
r.skipped.forEach(function (s) {
|
|
373
|
+
process.stdout.write(' · ' + s.rule + ' —— ' + s.why + '\n');
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (r.notes.length) {
|
|
377
|
+
process.stdout.write('\n ⚠️ 这一版还有判不了的部分(只提示,不判过):\n');
|
|
378
|
+
r.notes.forEach(function (n) {
|
|
379
|
+
process.stdout.write(' · ' + n.label + ' —— ' + n.why + '\n');
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
process.stdout.write('\n');
|
|
384
|
+
if (r.ok) {
|
|
385
|
+
process.stdout.write(' OK ' + r.rules.length + ' 条判据没有发现问题('
|
|
386
|
+
+ r.counts.error + ' 错误 / ' + r.counts.warn + ' 警告)\n');
|
|
387
|
+
} else {
|
|
388
|
+
process.stdout.write(' FAIL ' + r.counts.error + ' 个错误 / '
|
|
389
|
+
+ r.counts.warn + ' 个警告\n');
|
|
390
|
+
}
|
|
391
|
+
process.exit(r.ok ? 0 : 1);
|
|
392
|
+
}
|
|
393
|
+
|
|
146
394
|
// ---------------------------------------------------------------- validate
|
|
147
395
|
var SCHEMAS = {
|
|
148
396
|
contract: 'contract.schema.json',
|
|
@@ -154,7 +402,7 @@ var SCHEMAS = {
|
|
|
154
402
|
patch: 'patch.schema.json',
|
|
155
403
|
/* behaviors 是稳定性承诺里的核心契约(微行为矩阵),
|
|
156
404
|
它的 schema 文件一直都在,但这里漏登记 ⇒ `--schema=behaviors` 报
|
|
157
|
-
"未知 schema"
|
|
405
|
+
"未知 schema",等于这份契约**没有任何工具能校验**。 */
|
|
158
406
|
behaviors: 'behaviors.schema.json'
|
|
159
407
|
};
|
|
160
408
|
|
|
@@ -165,7 +413,7 @@ function cmdValidate(args) {
|
|
|
165
413
|
if (!SCHEMAS[which]) die('未知 schema:' + which);
|
|
166
414
|
validate.assertSchemasUseOnlySupported();
|
|
167
415
|
var schema = readJSON(path.join(AI, SCHEMAS[which]));
|
|
168
|
-
var data = readJSON(file);
|
|
416
|
+
var data = readJSON(file, true);
|
|
169
417
|
var r = validate.validate(schema, data);
|
|
170
418
|
if (isJson()) {
|
|
171
419
|
out({ ok: r.ok, schema: which, file: file, errors: r.errors || [] });
|
|
@@ -191,7 +439,7 @@ function cmdValidate(args) {
|
|
|
191
439
|
function cmdDiff(args) {
|
|
192
440
|
var fa = args[0], fb = args[1];
|
|
193
441
|
if (!fa || !fb) die('用法:ai diff <改动前.json> <改动后.json>');
|
|
194
|
-
var a = readJSON(fa), b = readJSON(fb);
|
|
442
|
+
var a = readJSON(fa, true), b = readJSON(fb, true);
|
|
195
443
|
var na = a.nodes || {}, nb = b.nodes || {};
|
|
196
444
|
var added = [], removed = [], changed = [], moved = [];
|
|
197
445
|
|
|
@@ -242,8 +490,8 @@ function cmdDiff(args) {
|
|
|
242
490
|
function cmdPatch(args) {
|
|
243
491
|
var fdoc = args[0], fpatch = args[1];
|
|
244
492
|
if (!fdoc || !fpatch) die('用法:ai patch <doc.json> <patch.json> [-o out.json]');
|
|
245
|
-
var doc = readJSON(fdoc);
|
|
246
|
-
var p = readJSON(fpatch);
|
|
493
|
+
var doc = readJSON(fdoc, true);
|
|
494
|
+
var p = readJSON(fpatch, true);
|
|
247
495
|
var r = patch.apply(doc, p);
|
|
248
496
|
|
|
249
497
|
if (!r.ok) {
|
|
@@ -384,7 +632,7 @@ function cmdPitfalls(args) {
|
|
|
384
632
|
// ---------------------------------------------------------------- 列表类
|
|
385
633
|
function cmdComponents(args) {
|
|
386
634
|
var doc = readJSON(path.join(AI, 'components.json'));
|
|
387
|
-
var m =
|
|
635
|
+
var m = enumFlag('maturity');
|
|
388
636
|
var list = doc.components.filter(function (c) {
|
|
389
637
|
return !m || c.maturity === m;
|
|
390
638
|
});
|
|
@@ -570,7 +818,7 @@ function cmdAssets(args) {
|
|
|
570
818
|
|
|
571
819
|
function cmdTokens(args) {
|
|
572
820
|
var doc = readJSON(path.join(AI, 'tokens.json'));
|
|
573
|
-
var set =
|
|
821
|
+
var set = enumFlag('set');
|
|
574
822
|
if (isJson()) {
|
|
575
823
|
out(set ? doc.sets[set] : doc);
|
|
576
824
|
process.exit(0);
|
|
@@ -592,8 +840,8 @@ function cmdTokens(args) {
|
|
|
592
840
|
|
|
593
841
|
function cmdTree(args) {
|
|
594
842
|
var doc = readJSON(path.join(AI, 'tokens.tree.json'));
|
|
595
|
-
var set =
|
|
596
|
-
var cat =
|
|
843
|
+
var set = enumFlag('set');
|
|
844
|
+
var cat = enumFlag('cat');
|
|
597
845
|
if (isJson()) {
|
|
598
846
|
out(cat && set ? doc.sets[set][cat] : (set ? doc.sets[set] : doc));
|
|
599
847
|
process.exit(0);
|
|
@@ -621,7 +869,14 @@ function cmdTree(args) {
|
|
|
621
869
|
process.exit(0);
|
|
622
870
|
}
|
|
623
871
|
|
|
624
|
-
function cmdProfile() {
|
|
872
|
+
function cmdProfile(args) {
|
|
873
|
+
/* 多余的位置参数以前被**静默忽略**:`ai profile zzz` 打印三档、退出码 0,
|
|
874
|
+
调用方会以为"zzz 这一档存在"。只拒绝位置参数,开关照收(`--json`)。 */
|
|
875
|
+
var extra = (args || []).filter(function (a) { return String(a).charAt(0) !== '-'; });
|
|
876
|
+
if (extra.length) {
|
|
877
|
+
die('ai profile 不接受位置参数(收到 ' + extra.join(' ')
|
|
878
|
+
+ ')。想查某一档的行为,用 ai check <doc.json> --profile=<档位>');
|
|
879
|
+
}
|
|
625
880
|
var doc = readJSON(path.join(AI, 'profiles.json'));
|
|
626
881
|
if (isJson()) { out(doc); process.exit(0); }
|
|
627
882
|
process.stdout.write(' === 三档 Profile ===\n\n');
|
|
@@ -642,22 +897,12 @@ function cmdProfile() {
|
|
|
642
897
|
}
|
|
643
898
|
|
|
644
899
|
// ---------------------------------------------------------------- pdf
|
|
645
|
-
/*
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
var PDF_EXIT = {
|
|
649
|
-
usage: 2, 'no-chrome': 3, render: 4, timeout: 4, unreadable: 4, 'over-budget': 5
|
|
650
|
-
};
|
|
651
|
-
|
|
900
|
+
/* 退出码用全局那一套(顶部 `EXIT`):2 用法 / 3 没浏览器 / 4 渲染失败 / 5 超预算。
|
|
901
|
+
这几条**先于其它子命令**定下来、也早在 CHANGELOG 里公开过 ——
|
|
902
|
+
所以收敛方向是「其它子命令向它看齐」,不是再定义一套。 */
|
|
652
903
|
function cmdPdf(args) {
|
|
653
|
-
/*
|
|
654
|
-
|
|
655
|
-
两种用法错误给两个退出码,调用方就没法按码分支了。 */
|
|
656
|
-
function bad(msg) {
|
|
657
|
-
if (isJson()) out({ ok: false, command: 'pdf', code: 'usage', message: msg });
|
|
658
|
-
else process.stderr.write(' [FAIL] ' + msg + '\n');
|
|
659
|
-
process.exit(PDF_EXIT.usage);
|
|
660
|
-
}
|
|
904
|
+
/* 用法错误一律 usage(退出码 2),与全 CLI 其它子命令同一个出口。 */
|
|
905
|
+
function bad(msg) { die(msg); }
|
|
661
906
|
var src = args[0];
|
|
662
907
|
if (!src) {
|
|
663
908
|
bad('用法:agentcss pdf <页面.html|URL> --out <file.pdf> '
|
|
@@ -674,7 +919,7 @@ function cmdPdf(args) {
|
|
|
674
919
|
var budget = flag('max-pages', null);
|
|
675
920
|
if (budget === true) bad('--max-pages 后面要跟数字(不设 = 无界)');
|
|
676
921
|
/* 先按字符串校验再转数字:让报错里出现**使用者打进去的那串字符**。
|
|
677
|
-
直接 Number() 再报错时,`abc` 已经变成 NaN,报「收到 NaN/null
|
|
922
|
+
直接 Number() 再报错时,`abc` 已经变成 NaN,报「收到 NaN/null」是在念实现细节。 */
|
|
678
923
|
if (budget !== null && !/^\d+$/.test(String(budget))) {
|
|
679
924
|
bad('--max-pages 必须是 ≥1 的整数,收到 ' + JSON.stringify(String(budget)));
|
|
680
925
|
}
|
|
@@ -708,17 +953,21 @@ function cmdPdf(args) {
|
|
|
708
953
|
chrome: r.chrome || null,
|
|
709
954
|
ms: r.ms || null
|
|
710
955
|
};
|
|
711
|
-
if (isJson()) {
|
|
712
|
-
out(report);
|
|
713
|
-
process.exit(r.ok ? 0 : (PDF_EXIT[r.code] || 4));
|
|
714
|
-
}
|
|
715
956
|
if (!r.ok) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
957
|
+
/* 失败时**保留上下文**(产物路径 / 页数 / 预算):
|
|
958
|
+
超预算这一类的产物是完整的,把这些字段丢掉等于让调用方再猜一次。 */
|
|
959
|
+
if (r.code === 'over-budget' && !isJson()) {
|
|
960
|
+
process.stderr.write(' (预算超了不是渲染出错,产物是完整的)\n');
|
|
720
961
|
}
|
|
721
|
-
|
|
962
|
+
fail(r.code || 'render', r.message, {
|
|
963
|
+
out: r.out || null,
|
|
964
|
+
pages: r.pages === undefined ? null : r.pages,
|
|
965
|
+
budget: r.budget === undefined ? null : r.budget
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
if (isJson()) {
|
|
969
|
+
out(report);
|
|
970
|
+
process.exit(0);
|
|
722
971
|
}
|
|
723
972
|
process.stdout.write(' OK ' + r.out + '\n');
|
|
724
973
|
process.stdout.write(' ' + r.pages + ' 页 · ' + report.paperSize + ' · '
|
|
@@ -747,7 +996,204 @@ function cmdCapabilities() {
|
|
|
747
996
|
process.exit(0);
|
|
748
997
|
}
|
|
749
998
|
|
|
999
|
+
// ---------------------------------------------------------------- scaffold
|
|
1000
|
+
function cmdScaffold(args) {
|
|
1001
|
+
var scaffold = require('./scaffold.js');
|
|
1002
|
+
var id = args[0];
|
|
1003
|
+
|
|
1004
|
+
if (!id || id === 'list') {
|
|
1005
|
+
if (isJson()) { out({ ok: true, command: 'scaffold', recipes: scaffold.list() }); process.exit(0); }
|
|
1006
|
+
process.stdout.write(' === 可生成的壳 ===\n\n');
|
|
1007
|
+
scaffold.list().forEach(function (r) {
|
|
1008
|
+
process.stdout.write(' ' + pad(r.id, 20) + r.label + '\n');
|
|
1009
|
+
});
|
|
1010
|
+
process.stdout.write('\n 用法:agentcss ai scaffold <版式> --out <page.html>\n');
|
|
1011
|
+
process.exit(0);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
var outFile = flag('out', null);
|
|
1015
|
+
if (!outFile || outFile === true) {
|
|
1016
|
+
die('用法:ai scaffold ' + id + ' --out <page.html> [--css-prefix=<样式表前缀>]');
|
|
1017
|
+
}
|
|
1018
|
+
var prefix = flag('css-prefix', null);
|
|
1019
|
+
if (prefix === true) die('--css-prefix 后面要跟路径(例:node_modules/agentcss)');
|
|
1020
|
+
|
|
1021
|
+
var r = scaffold.render(id, { cssPrefix: prefix === null ? undefined : prefix });
|
|
1022
|
+
if (!r.ok) die(r.message);
|
|
1023
|
+
|
|
1024
|
+
var abs = path.resolve(outFile);
|
|
1025
|
+
if (fs.existsSync(abs)) {
|
|
1026
|
+
/* 不覆盖:壳是用来改的,覆盖掉一份正在写的页面比报错更贵。
|
|
1027
|
+
真要覆盖就自己删 —— 这一步不该由工具代做。 */
|
|
1028
|
+
die('目标文件已存在:' + abs + '\n 壳不会覆盖已有文件(它可能正在被改)。'
|
|
1029
|
+
+ '换一个 --out,或先删掉它。');
|
|
1030
|
+
}
|
|
1031
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1032
|
+
fs.writeFileSync(abs, r.html, 'utf8');
|
|
1033
|
+
|
|
1034
|
+
if (isJson()) {
|
|
1035
|
+
out({
|
|
1036
|
+
ok: true, command: 'scaffold', recipe: r.id, out: abs,
|
|
1037
|
+
bytes: Buffer.byteLength(r.html), requires: r.requires
|
|
1038
|
+
});
|
|
1039
|
+
process.exit(0);
|
|
1040
|
+
}
|
|
1041
|
+
process.stdout.write(' OK 已写出 ' + abs + '(' + Buffer.byteLength(r.html) + ' 字节)\n');
|
|
1042
|
+
process.stdout.write(' 版式 ' + r.id + ' · 必有件 ' + r.requires.length + ' 条'
|
|
1043
|
+
+ (r.requires.length ? ':' + r.requires.join(' ') : '') + '\n');
|
|
1044
|
+
process.stdout.write(' 下一步:agentcss ai check ' + outFile + ' --recipe=' + r.id + '\n');
|
|
1045
|
+
process.stdout.write(' 再下一步:agentcss ai shot ' + outFile + ' --out shot-{width}.png --width=390,1440 --full\n');
|
|
1046
|
+
process.exit(0);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// ---------------------------------------------------------------- shot
|
|
1050
|
+
/* shot 的 code 与 pdf 同一批语义(usage / no-chrome / render / timeout),
|
|
1051
|
+
多一点 viewport(视口没设成 = 渲染失败)。都走顶部那张全局 `EXIT` ——
|
|
1052
|
+
调用方按码分支时,不该因为换了个子命令就得记第二张表。 */
|
|
1053
|
+
|
|
1054
|
+
function cmdShot(args) {
|
|
1055
|
+
var shot = require('./shot.js');
|
|
1056
|
+
var src = args[0];
|
|
1057
|
+
|
|
1058
|
+
function bad(msg) { die(msg); }
|
|
1059
|
+
|
|
1060
|
+
if (!src) {
|
|
1061
|
+
bad('用法:agentcss shot <页面.html|URL> --out <file.png> [--width=390,1440] [--height=900] '
|
|
1062
|
+
+ '[--full] [--scale=2] [--wait=800] [--chrome=<path>] [--json]');
|
|
1063
|
+
}
|
|
1064
|
+
var outFile = flag('out', null);
|
|
1065
|
+
if (!outFile || outFile === true) bad('缺 --out(多宽度时用 {width} 占位,例:shot-{width}.png)');
|
|
1066
|
+
|
|
1067
|
+
var wRaw = flag('width', null);
|
|
1068
|
+
var widths = [1440];
|
|
1069
|
+
if (wRaw === true) bad('--width 后面要跟数字(多个用逗号分隔,例:390,1440)');
|
|
1070
|
+
if (typeof wRaw === 'string') {
|
|
1071
|
+
widths = wRaw.split(',').map(function (s) { return s.trim(); })
|
|
1072
|
+
.filter(Boolean).map(function (s) { return /^\d+$/.test(s) ? Number(s) : NaN; });
|
|
1073
|
+
if (!widths.length || widths.some(function (n) { return !isFinite(n); })) {
|
|
1074
|
+
bad('--width 只接受整数,收到 ' + JSON.stringify(wRaw));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
var hRaw = flag('height', null);
|
|
1079
|
+
if (hRaw === true) bad('--height 后面要跟数字(例:900)');
|
|
1080
|
+
if (typeof hRaw === 'string' && !/^\d+$/.test(hRaw)) {
|
|
1081
|
+
bad('--height 只接受整数,收到 ' + JSON.stringify(hRaw));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
var scaleRaw = flag('scale', null);
|
|
1085
|
+
if (scaleRaw === true) bad('--scale 后面要跟数字(1 / 2)');
|
|
1086
|
+
if (typeof scaleRaw === 'string' && !/^\d+(\.\d+)?$/.test(scaleRaw)) {
|
|
1087
|
+
bad('--scale 只接受数字,收到 ' + JSON.stringify(scaleRaw));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
var waitRaw = flag('wait', null);
|
|
1091
|
+
if (waitRaw === true) bad('--wait 后面要跟毫秒数');
|
|
1092
|
+
if (typeof waitRaw === 'string' && !/^\d+$/.test(waitRaw)) {
|
|
1093
|
+
bad('--wait 只接受整数毫秒,收到 ' + JSON.stringify(waitRaw));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
var chrome = flag('chrome', null);
|
|
1097
|
+
if (chrome === true) bad('--chrome 后面要跟可执行文件路径');
|
|
1098
|
+
|
|
1099
|
+
var fullFlag = flag('full', false);
|
|
1100
|
+
var full = fullFlag === true || fullFlag === 'true';
|
|
1101
|
+
|
|
1102
|
+
shot.shoot({
|
|
1103
|
+
src: src,
|
|
1104
|
+
out: outFile,
|
|
1105
|
+
widths: widths,
|
|
1106
|
+
height: hRaw === null ? undefined : Number(hRaw),
|
|
1107
|
+
full: full,
|
|
1108
|
+
scale: scaleRaw === null ? undefined : Number(scaleRaw),
|
|
1109
|
+
wait: waitRaw === null ? undefined : Number(waitRaw),
|
|
1110
|
+
chrome: chrome === null ? undefined : chrome
|
|
1111
|
+
}).then(function (r) {
|
|
1112
|
+
/* 🔴 视口必须等于请求的宽度。不等就说明"设视口"这一步没生效
|
|
1113
|
+
(正是 `--screenshot --window-size` 那条老路会静默发生的失败)。
|
|
1114
|
+
这时候不能把图交出去:一张宽度不对的图比没有图更误导。 */
|
|
1115
|
+
if (r.ok) {
|
|
1116
|
+
var wrong = r.shots.filter(function (s) { return s.viewport !== null && s.viewport !== s.width; });
|
|
1117
|
+
if (wrong.length) {
|
|
1118
|
+
r.ok = false;
|
|
1119
|
+
r.code = 'viewport';
|
|
1120
|
+
r.message = '视口没有设成请求的宽度:' + wrong.map(function (s) {
|
|
1121
|
+
return '要 ' + s.width + ' 实际 ' + s.viewport;
|
|
1122
|
+
}).join(';') + '\n 这类图看起来是对的,实际是另一个宽度下的排版。';
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (isJson()) {
|
|
1127
|
+
out(r);
|
|
1128
|
+
process.exitCode = r.ok ? 0 : (EXIT[r.code] || EXIT.render);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (!r.ok) {
|
|
1132
|
+
process.stderr.write(' [FAIL] ' + r.message + '\n');
|
|
1133
|
+
process.exitCode = EXIT[r.code] || EXIT.render;
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
r.shots.forEach(function (s) {
|
|
1137
|
+
process.stdout.write(' OK ' + s.out + '\n');
|
|
1138
|
+
process.stdout.write(' ' + s.png.width + '×' + s.png.height + ' px'
|
|
1139
|
+
+ ' · 视口宽 ' + s.width + ' · 内容高 ' + s.scrollHeight
|
|
1140
|
+
+ ' · ' + Math.round(s.bytes / 1024) + ' KB · ' + s.ms + ' ms'
|
|
1141
|
+
+ (s.truncatedAt ? ' · 🔴 已截断到 ' + s.png.height + '(原 ' + s.truncatedAt + ')' : '') + '\n');
|
|
1142
|
+
if (s.overflowCount) {
|
|
1143
|
+
process.stdout.write(' ⚠️ 这个宽度下横向溢出 ' + s.overflowCount + ' 处'
|
|
1144
|
+
+ '(页面宽 ' + s.scrollWidth + ' > 视口 ' + s.viewport + '):' + s.overflow.join('、') + '\n');
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
if (r.consoleErrors.length) {
|
|
1148
|
+
process.stdout.write('\n ⚠️ 页面里有 ' + r.consoleErrors.length + ' 条 JS 报错 ——'
|
|
1149
|
+
+ '图可能看起来是空的:\n');
|
|
1150
|
+
r.consoleErrors.slice(0, 5).forEach(function (e) {
|
|
1151
|
+
process.stdout.write(' · ' + e + '\n');
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
process.exitCode = 0;
|
|
1155
|
+
}).catch(function (e) {
|
|
1156
|
+
fail('render', String(e && e.message || e));
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
750
1160
|
// ---------------------------------------------------------------- 分发
|
|
1161
|
+
/* 🔴 每个命令认识哪些开关,**只在这一处声明**。
|
|
1162
|
+
为什么要声明:以前无关开关被静默忽略 ——
|
|
1163
|
+
`ai components --set=dark` 退出码 0、什么都不说,
|
|
1164
|
+
调用方会以为"按 dark 过滤过了"。**善意的忽略就是假绿**,
|
|
1165
|
+
而 Agent 不会去核对"这个开关到底生效没有"。
|
|
1166
|
+
`json` / `h` 是所有命令共有的(输出格式与帮助)。 */
|
|
1167
|
+
var GLOBAL_FLAGS = ['json', 'h', 'help'];
|
|
1168
|
+
|
|
1169
|
+
var FLAGS = {
|
|
1170
|
+
check: ['profile', 'medium', 'recipe', 'strict', 'list-recipes', 'html'],
|
|
1171
|
+
validate: ['schema'],
|
|
1172
|
+
diff: [],
|
|
1173
|
+
patch: ['o'],
|
|
1174
|
+
components: ['maturity'],
|
|
1175
|
+
assets: [],
|
|
1176
|
+
tokens: ['set'],
|
|
1177
|
+
tree: ['set', 'cat'],
|
|
1178
|
+
profile: [],
|
|
1179
|
+
capabilities: [],
|
|
1180
|
+
pitfalls: ['component', 'scope', 'tag'],
|
|
1181
|
+
pdf: ['out', 'paper', 'margin', 'max-pages', 'chrome'],
|
|
1182
|
+
scaffold: ['out', 'css-prefix'],
|
|
1183
|
+
shot: ['out', 'width', 'height', 'full', 'scale', 'wait', 'chrome']
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
function rejectUnknownFlags(cmd, allowed) {
|
|
1187
|
+
for (var i = 2; i < process.argv.length; i++) {
|
|
1188
|
+
var a = process.argv[i];
|
|
1189
|
+
if (a.charAt(0) !== '-' || a === '-' || a === '--') continue;
|
|
1190
|
+
var name = a.replace(/^--?/, '').split('=')[0];
|
|
1191
|
+
if (allowed.indexOf(name) >= 0) continue;
|
|
1192
|
+
die('`' + cmd + '` 不认识开关 ' + (a.charAt(1) === '-' ? '--' : '-') + name
|
|
1193
|
+
+ '(本命令支持:' + allowed.join(' / ') + ')');
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
751
1197
|
var CMDS = {
|
|
752
1198
|
check: cmdCheck,
|
|
753
1199
|
validate: cmdValidate,
|
|
@@ -760,19 +1206,24 @@ var CMDS = {
|
|
|
760
1206
|
profile: cmdProfile,
|
|
761
1207
|
capabilities: cmdCapabilities,
|
|
762
1208
|
pitfalls: cmdPitfalls,
|
|
763
|
-
pdf: cmdPdf
|
|
1209
|
+
pdf: cmdPdf,
|
|
1210
|
+
scaffold: cmdScaffold,
|
|
1211
|
+
shot: cmdShot
|
|
764
1212
|
};
|
|
765
1213
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1214
|
+
/* 帮助正文单独成函数:`main()` 里有三处要打印它
|
|
1215
|
+
(无参数 / 显式 help / 任意位置的 --help),复制三份必然漂。 */
|
|
1216
|
+
function usage() {
|
|
1217
|
+
process.stdout.write(
|
|
770
1218
|
'agentcss ai —— 独立校验入口(零依赖、不联网、不依赖任何模型)\n\n' +
|
|
771
1219
|
' ai check <doc.json> [--profile=creative|standard|strict] [--medium=screen|paper] [--json]\n' +
|
|
772
|
-
' ai
|
|
1220
|
+
' ai check <page.html> [--recipe=<版式>] [--strict] [--list-recipes] [--json]\n' +
|
|
1221
|
+
' 产物检查:类名有没有定义、引没引到样式表、图/表/标题缺不缺件\n' +
|
|
1222
|
+
' ai validate <file.json> --schema='
|
|
1223
|
+
+ Object.keys(SCHEMAS).join('|') + ' [--json]\n' +
|
|
773
1224
|
' ai diff <改动前.json> <改动后.json> [--json] 按语义 id 比对\n' +
|
|
774
1225
|
' ai patch <doc.json> <patch.json> [-o out.json] [--json]\n' +
|
|
775
|
-
' ai components [--maturity=
|
|
1226
|
+
' ai components [--maturity=' + enumValues('maturity').join('|') + '] [--json]\n' +
|
|
776
1227
|
' ai assets [--json] 组件之外的资产:布局 / 微行为核 / 示例页 / 适配器\n' +
|
|
777
1228
|
' ai tokens [--set=light|dark] [--json]\n' +
|
|
778
1229
|
' ai tree [--set=light|dark] [--cat=color] [--json]\n' +
|
|
@@ -782,12 +1233,32 @@ function main() {
|
|
|
782
1233
|
' ai pdf <页面.html|URL> --out <file.pdf> [--paper=a4|letter|a5|a3|legal]\n' +
|
|
783
1234
|
' [--margin="18mm 15mm"] [--max-pages=N] [--chrome=<path>] [--json]\n' +
|
|
784
1235
|
' HTML → PDF。零依赖(用你机器上已装的 Chrome),@page 由导出方给。\n' +
|
|
1236
|
+
' ai scaffold <版式> --out <page.html> [--css-prefix=<前缀>] [--json]\n' +
|
|
1237
|
+
' 生成最小壳(正确的骨架 + 必有件位置),不含内容。`list` 看有哪些版式。\n' +
|
|
1238
|
+
' ai shot <页面.html|URL> --out <shot-{width}.png> [--width=390,1440] [--height=900]\n' +
|
|
1239
|
+
' [--full] [--scale=2] [--wait=800] [--chrome=<path>] [--json]\n' +
|
|
1240
|
+
' HTML → PNG(真实视口,不是"按窗口宽度裁一刀")。会报横向溢出与 JS 报错。\n' +
|
|
785
1241
|
'\n' +
|
|
786
1242
|
' 打印:`--medium=paper` 才会校验 printPolicy 与 pageBudget;默认 screen 零成本。\n' +
|
|
787
1243
|
' 每个组件的纸面行为:ai/components.json 的 printPolicy。\n' +
|
|
788
1244
|
'\n' +
|
|
789
|
-
' 每个子命令都支持 --json;失败时输出 {ok:false, command, error}
|
|
1245
|
+
' 每个子命令都支持 --json;失败时输出 {ok:false, command, error:{kind, message}}。\n' +
|
|
1246
|
+
'\n' +
|
|
1247
|
+
' 退出码(全 CLI 同一套,kind 与码一一对应):\n' +
|
|
1248
|
+
' 0 通过\n' +
|
|
1249
|
+
' 1 判定不通过(页面/文档确实有错 —— 是结论,不是工具坏了)\n' +
|
|
1250
|
+
' 2 用法错误(参数名、取值、组合不对)\n' +
|
|
1251
|
+
' 3 环境不具备(没有浏览器 / node 不在 PATH)\n' +
|
|
1252
|
+
' 4 渲染失败(超时、读不出、视口没设成)\n' +
|
|
1253
|
+
' 5 超出预算(产物是完整的,只是超了页数)\n' +
|
|
1254
|
+
' 6 工具内部错误(是工具的问题,不是你参数写错)\n' +
|
|
790
1255
|
' 前缀 `ai` 可写可不写:`agentcss ai components` 与 `agentcss components` 等价。\n');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function main() {
|
|
1259
|
+
var argv = process.argv.slice(2);
|
|
1260
|
+
if (!argv.length || argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
1261
|
+
usage();
|
|
791
1262
|
process.exit(0);
|
|
792
1263
|
}
|
|
793
1264
|
var name = argv.shift();
|
|
@@ -804,15 +1275,34 @@ function main() {
|
|
|
804
1275
|
die('`agentcss ai` 后面要跟子命令(可用:' + Object.keys(CMDS).join(' / ') + ')');
|
|
805
1276
|
}
|
|
806
1277
|
}
|
|
1278
|
+
if (argv.indexOf('--help') >= 0 || argv.indexOf('-h') >= 0) {
|
|
1279
|
+
usage();
|
|
1280
|
+
process.exit(0);
|
|
1281
|
+
}
|
|
807
1282
|
CURRENT_CMD = name;
|
|
808
1283
|
var fn = CMDS[name];
|
|
809
1284
|
if (!fn) die('未知子命令:' + name + '(可用:' + Object.keys(CMDS).join(' / ') + ')');
|
|
1285
|
+
/* 未知开关在**调用命令之前**拒掉:命令内部各自判断的话,
|
|
1286
|
+
遇到不认识的开关就只剩"忽略"和"崩"两种选择。 */
|
|
1287
|
+
rejectUnknownFlags(name, GLOBAL_FLAGS.concat(FLAGS[name] || []));
|
|
810
1288
|
try {
|
|
811
1289
|
fn(argv);
|
|
812
1290
|
} catch (e) {
|
|
813
|
-
|
|
1291
|
+
/* 🔴 这里是**兜底**:走到这儿说明是工具自己的 bug(TypeError / 读不到自带的文件)。
|
|
1292
|
+
以前一律 `die(e.message)` ⇒ kind 恒为 'usage'、退出码 1。
|
|
1293
|
+
实测(0.26.0):`ai tree --set=zzz --cat=nope` 报的是
|
|
1294
|
+
{"ok":false,"error":{"kind":"usage","message":"Cannot read properties of undefined (reading 'nope')"}}
|
|
1295
|
+
—— 调用方看到 kind=usage,会去改自己的参数,而真正该改的是工具。
|
|
1296
|
+
现在:kind=internal、退出码 6、并且带一行为"这是工具的问题"。 */
|
|
1297
|
+
fail((e && e.kind) || 'internal', (e && e.message) || String(e));
|
|
814
1298
|
}
|
|
815
1299
|
}
|
|
816
1300
|
|
|
817
1301
|
if (require.main === module) main();
|
|
818
|
-
|
|
1302
|
+
/* 导出给门禁做**反漂移**用:
|
|
1303
|
+
· `SCHEMAS` —— 判"磁盘上每份 *.schema.json 都已登记",以及"登记的都存在"
|
|
1304
|
+
· `ENUMS` —— 判"每个枚举开关的取值都来自数据,不是手抄"
|
|
1305
|
+
门禁直接 require 本文件拿真值,**不复制一份清单**(I-7)。 */
|
|
1306
|
+
module.exports = { CMDS: CMDS, SCHEMAS: SCHEMAS, ENUMS: ENUMS, EXIT: EXIT, FLAGS: FLAGS,
|
|
1307
|
+
/* 只给门禁验「输出 undefined 必须当场失败」这条兜底 */
|
|
1308
|
+
_out: out };
|