@getvitops/cli 1.0.0 → 2.0.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/CHANGELOG.md +214 -0
- package/README.md +5 -0
- package/dist/cli.mjs +521 -73
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +41 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync,
|
|
5
|
-
import { dirname,
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, parse, relative, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { NUMERIC_STEPS, SCHEMA_URL, defaultConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, resolveSiteConfig, roleColorUtilities, roleHue, roleKind, validate } from "@getvitops/generator";
|
|
8
|
-
import { generateFavicons } from "@getvitops/utils";
|
|
7
|
+
import { NUMERIC_STEPS, SCHEMA_URL, buildIconSprite, defaultConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isSiteConfig, resolveInput, resolveSiteConfig, resolveTheme, roleColorUtilities, roleHue, roleKind, validate } from "@getvitops/generator";
|
|
8
|
+
import { collectIconRefs, generateFavicons, generateIconInclude, resolveIcon, scanFiles } from "@getvitops/utils";
|
|
9
9
|
|
|
10
10
|
//#region src/agents.ts
|
|
11
11
|
/**
|
|
@@ -40,6 +40,10 @@ const TOPICS = {
|
|
|
40
40
|
path: "concepts/scales.md",
|
|
41
41
|
summary: "fluid modular type & space scales"
|
|
42
42
|
},
|
|
43
|
+
icons: {
|
|
44
|
+
path: "concepts/icons.md",
|
|
45
|
+
summary: "semantic icon names across sets, bundle derivation, sprite delivery"
|
|
46
|
+
},
|
|
43
47
|
patterns: {
|
|
44
48
|
path: "concepts/patterns.md",
|
|
45
49
|
summary: "pattern CSS: token cascade, override hooks, states, role variants"
|
|
@@ -95,6 +99,240 @@ function linkSkill(linkPath, targetDir) {
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/css-scan.ts
|
|
104
|
+
/** Strip comments, preserving newlines so line numbers survive. */
|
|
105
|
+
function stripComments(css) {
|
|
106
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ""));
|
|
107
|
+
}
|
|
108
|
+
const lineAt = (src, index) => src.slice(0, index).split("\n").length;
|
|
109
|
+
/**
|
|
110
|
+
* Parse declarations from a rule body. Splits on top-level `;` only, so a
|
|
111
|
+
* `var(--x, a; b)` or a data URI cannot end a declaration early.
|
|
112
|
+
*/
|
|
113
|
+
function parseDeclarations(body) {
|
|
114
|
+
const out = [];
|
|
115
|
+
let depth = 0;
|
|
116
|
+
let start = 0;
|
|
117
|
+
const push = (chunk) => {
|
|
118
|
+
const colon = chunk.indexOf(":");
|
|
119
|
+
if (colon < 0) return;
|
|
120
|
+
const property = chunk.slice(0, colon).trim();
|
|
121
|
+
const value = chunk.slice(colon + 1).trim();
|
|
122
|
+
if (!property || property.includes("{") || !value) return;
|
|
123
|
+
out.push({
|
|
124
|
+
property,
|
|
125
|
+
value
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
for (let i = 0; i < body.length; i++) {
|
|
129
|
+
const ch = body[i];
|
|
130
|
+
if (ch === "(" || ch === "[") depth++;
|
|
131
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
132
|
+
else if (ch === ";" && depth === 0) {
|
|
133
|
+
push(body.slice(start, i));
|
|
134
|
+
start = i + 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
push(body.slice(start));
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Scan a stylesheet into its style rules.
|
|
142
|
+
*
|
|
143
|
+
* Never throws: this runs over consumer source that may be a fragment, and a
|
|
144
|
+
* linter that dies on one unparseable file is one nobody keeps in CI.
|
|
145
|
+
*/
|
|
146
|
+
function scanCss(css) {
|
|
147
|
+
const src = stripComments(css);
|
|
148
|
+
const rules = [];
|
|
149
|
+
const stack = [];
|
|
150
|
+
let depth = 0;
|
|
151
|
+
let preludeStart = 0;
|
|
152
|
+
for (let i = 0; i < src.length; i++) {
|
|
153
|
+
const ch = src[i];
|
|
154
|
+
if (ch === "\"" || ch === "'") {
|
|
155
|
+
const quote = ch;
|
|
156
|
+
i++;
|
|
157
|
+
while (i < src.length && src[i] !== quote) {
|
|
158
|
+
if (src[i] === "\\") i++;
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "{") {
|
|
164
|
+
const raw = src.slice(preludeStart, i);
|
|
165
|
+
const prelude = raw.trim();
|
|
166
|
+
stack.push({
|
|
167
|
+
prelude,
|
|
168
|
+
index: preludeStart + raw.indexOf(prelude.slice(0, 1))
|
|
169
|
+
});
|
|
170
|
+
depth++;
|
|
171
|
+
preludeStart = i + 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ch === "}") {
|
|
175
|
+
const frame = stack.pop();
|
|
176
|
+
depth--;
|
|
177
|
+
if (frame && !frame.prelude.startsWith("@")) {
|
|
178
|
+
const bodyStart = src.indexOf("{", frame.index) + 1;
|
|
179
|
+
rules.push({
|
|
180
|
+
selector: frame.prelude,
|
|
181
|
+
declarations: parseDeclarations(src.slice(bodyStart, i)),
|
|
182
|
+
conditions: stack.filter((f) => f.prelude.startsWith("@")).map((f) => f.prelude),
|
|
183
|
+
line: lineAt(src, frame.index)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
preludeStart = i + 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return rules.filter((r) => r.selector && depth >= 0);
|
|
191
|
+
}
|
|
192
|
+
/** `<style>` block contents from an .astro/.vue/.svelte/.html file. */
|
|
193
|
+
function extractStyleBlocks(src) {
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const m of src.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
196
|
+
const line = lineAt(src, (m.index ?? 0) + m[0].indexOf(">") + 1);
|
|
197
|
+
out.push({
|
|
198
|
+
css: m[1] ?? "",
|
|
199
|
+
line
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/lint-css.ts
|
|
207
|
+
/**
|
|
208
|
+
* The framework's own track widths. Declared in two places today
|
|
209
|
+
* (`core/css/layout.css` and the tailwind emitter), so a rule keyed to the token
|
|
210
|
+
* NAME rather than to a value survives either one moving.
|
|
211
|
+
*/
|
|
212
|
+
const TRACK_WIDTHS = [
|
|
213
|
+
"--width-measure",
|
|
214
|
+
"--width-breakout",
|
|
215
|
+
"--width-spotlight"
|
|
216
|
+
];
|
|
217
|
+
/**
|
|
218
|
+
* Column ratios that correspond to a `split-<a>-<b>` class.
|
|
219
|
+
*
|
|
220
|
+
* `.split` is FLEX, not grid — `display: flex` with `flex: 1` children, and a
|
|
221
|
+
* ratio class setting `--_split-a`/`--_split-b`. Equal columns are the base
|
|
222
|
+
* `.split` with no ratio class, which is why `1fr 1fr` is deliberately absent:
|
|
223
|
+
* suggesting `split-1-1` would name a class that does not exist, and this file
|
|
224
|
+
* would then be committing the very defect `lint.ts` reports.
|
|
225
|
+
*/
|
|
226
|
+
const SPLITS = {
|
|
227
|
+
"1fr 2fr": "1-2",
|
|
228
|
+
"2fr 1fr": "2-1",
|
|
229
|
+
"1fr 3fr": "1-3",
|
|
230
|
+
"3fr 1fr": "3-1",
|
|
231
|
+
"1fr 4fr": "1-4",
|
|
232
|
+
"4fr 1fr": "4-1",
|
|
233
|
+
"2fr 3fr": "2-3",
|
|
234
|
+
"3fr 2fr": "3-2"
|
|
235
|
+
};
|
|
236
|
+
const decl = (rule, property) => rule.declarations.find((d) => d.property === property)?.value;
|
|
237
|
+
/** Is this rule inside a `min-width` media or container query? */
|
|
238
|
+
const inMinWidthQuery = (rule) => rule.conditions.some((c) => /min-width\s*:/.test(c));
|
|
239
|
+
/** The framework breakpoint a `min-width` condition corresponds to, if any. */
|
|
240
|
+
function breakpointOf(rule) {
|
|
241
|
+
const rem = rule.conditions.map((c) => /min-width\s*:\s*([\d.]+)rem/.exec(c)?.[1]).find(Boolean);
|
|
242
|
+
if (!rem) return null;
|
|
243
|
+
const n = Number(rem);
|
|
244
|
+
return n >= 80 ? "xl" : n >= 64 ? "lg" : n >= 48 ? "md" : n >= 30 ? "sm" : null;
|
|
245
|
+
}
|
|
246
|
+
/** Spell a responsive utility the way the target format actually emits it. */
|
|
247
|
+
const responsive = (bp, base, format) => format === "tailwind" ? `@${bp}:${base}` : `${bp}-${base}`;
|
|
248
|
+
/**
|
|
249
|
+
* One rule = one check. Kept as data rather than a chain of ifs so adding a
|
|
250
|
+
* primitive is adding an entry, and so each carries its own explanation.
|
|
251
|
+
*/
|
|
252
|
+
const RULES = [
|
|
253
|
+
(rule) => {
|
|
254
|
+
const token = TRACK_WIDTHS.find((t) => decl(rule, "max-inline-size")?.includes(t) || decl(rule, "max-width")?.includes(t));
|
|
255
|
+
const centred = /\bauto\b/.test(decl(rule, "margin-inline") ?? decl(rule, "margin") ?? "");
|
|
256
|
+
if (!token || !centred) return null;
|
|
257
|
+
return {
|
|
258
|
+
cls: `${rule.selector} { max-inline-size: var(${token}); margin-inline: auto }`,
|
|
259
|
+
severity: "suggestion",
|
|
260
|
+
reason: "`.centered` is this pattern — a grid of named tracks, not a max-width plus auto margins",
|
|
261
|
+
suggestion: "use `.centered`, and widen an individual child with `breakout`, `spotlight` or `fullbleed` on that child rather than overriding the parent"
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
(rule, format) => {
|
|
265
|
+
const cols = decl(rule, "grid-template-columns");
|
|
266
|
+
if (!cols || !inMinWidthQuery(rule)) return null;
|
|
267
|
+
const ratio = SPLITS[cols.replace(/\s+/g, " ").trim()];
|
|
268
|
+
const bp = ratio && breakpointOf(rule);
|
|
269
|
+
if (!ratio || !bp) return null;
|
|
270
|
+
return {
|
|
271
|
+
cls: `${rule.selector} { grid-template-columns: ${cols} }`,
|
|
272
|
+
severity: "suggestion",
|
|
273
|
+
reason: `a ${cols} split behind a min-width query is the \`split-${ratio}\` utility`,
|
|
274
|
+
suggestion: `${responsive(bp, `split-${ratio}`, format)} on the container — the responsive form is equal below the breakpoint and takes the ratio above it, which is what the query is doing by hand`
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
(rule, format) => {
|
|
278
|
+
if (decl(rule, "display") !== "flex") return null;
|
|
279
|
+
if (decl(rule, "align-items") !== "center") return null;
|
|
280
|
+
if (/column/.test(decl(rule, "flex-direction") ?? "")) return null;
|
|
281
|
+
return {
|
|
282
|
+
cls: `${rule.selector} { display: flex; align-items: center }`,
|
|
283
|
+
severity: "suggestion",
|
|
284
|
+
reason: "the framework has this as utilities",
|
|
285
|
+
suggestion: format === "tailwind" ? "Tailwind's own `flex items-center` (the framework drops these names in this format)" : "`flex items-center`, or `.cluster-start` if this is a row of related controls"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
];
|
|
289
|
+
/**
|
|
290
|
+
* Lint stylesheets and the `<style>` blocks of component files.
|
|
291
|
+
*
|
|
292
|
+
* `files` carries raw text, same as `lintSource` — this module stays free of the
|
|
293
|
+
* filesystem so the caller owns discovery.
|
|
294
|
+
*/
|
|
295
|
+
/**
|
|
296
|
+
* The banner every generated stylesheet opens with.
|
|
297
|
+
*
|
|
298
|
+
* Linting the generator's own output is the worst false positive this tool can
|
|
299
|
+
* produce: the emitted bundle contains the framework's real implementations of
|
|
300
|
+
* these very patterns, so it reports `.split` as reinventing `.split`. The
|
|
301
|
+
* `css` format writes `styles.css` into the consumer's source tree (`src/styles`
|
|
302
|
+
* by default), which is squarely inside what `--src` scans.
|
|
303
|
+
*
|
|
304
|
+
* Matched on the banner rather than on a path, because the output directory is
|
|
305
|
+
* configurable and a path rule would silently stop working when someone changes
|
|
306
|
+
* it.
|
|
307
|
+
*/
|
|
308
|
+
const GENERATED = /^\s*\/\*!?\s*GENERATED\b|do not edit by hand\./;
|
|
309
|
+
function lintCss(files, format) {
|
|
310
|
+
const findings = [];
|
|
311
|
+
const seen = /* @__PURE__ */ new Set();
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
if (GENERATED.test(file.text.slice(0, 400))) continue;
|
|
314
|
+
const blocks = file.path.endsWith(".css") ? [{
|
|
315
|
+
css: file.text,
|
|
316
|
+
line: 1
|
|
317
|
+
}] : extractStyleBlocks(file.text);
|
|
318
|
+
for (const block of blocks) for (const rule of scanCss(block.css)) for (const check of RULES) {
|
|
319
|
+
const verdict = check(rule, format);
|
|
320
|
+
if (!verdict) continue;
|
|
321
|
+
const line = block.line + rule.line - 1;
|
|
322
|
+
const key = `${file.path}:${line}:${verdict.reason}`;
|
|
323
|
+
if (seen.has(key)) continue;
|
|
324
|
+
seen.add(key);
|
|
325
|
+
findings.push({
|
|
326
|
+
file: file.path,
|
|
327
|
+
line,
|
|
328
|
+
...verdict
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return findings;
|
|
334
|
+
}
|
|
335
|
+
|
|
98
336
|
//#endregion
|
|
99
337
|
//#region src/lint.ts
|
|
100
338
|
/**
|
|
@@ -154,10 +392,21 @@ function vocabulary(ds, roleClasses) {
|
|
|
154
392
|
roleClasses: new Set(roleClasses)
|
|
155
393
|
};
|
|
156
394
|
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Strip variant prefixes (`@md:`, `hover:`, `dark:`) from a candidate.
|
|
397
|
+
*
|
|
398
|
+
* Format-aware, because the container-query spelling only exists in tailwind.
|
|
399
|
+
* Stripping `@md:` unconditionally made the check asymmetric: `md-split-1-2` in a
|
|
400
|
+
* tailwind project was caught, but `@md:split-1-2` in a css/bricks project became
|
|
401
|
+
* a bare `split-1-2` that matched nothing and returned null — the same silent
|
|
402
|
+
* no-op, in the other direction, unreported.
|
|
403
|
+
*/
|
|
404
|
+
const stripVariants = (cls, format) => {
|
|
159
405
|
let out = cls;
|
|
160
|
-
while (/^@?[a-zA-Z0-9_-]+:/.test(out))
|
|
406
|
+
while (/^@?[a-zA-Z0-9_-]+:/.test(out)) {
|
|
407
|
+
if (format !== "tailwind" && out.startsWith("@")) break;
|
|
408
|
+
out = out.slice(out.indexOf(":") + 1);
|
|
409
|
+
}
|
|
161
410
|
return out;
|
|
162
411
|
};
|
|
163
412
|
const nearest = (want, pool) => pool.find((p) => p.startsWith(want) || want.startsWith(p));
|
|
@@ -166,10 +415,18 @@ const nearest = (want, pool) => pool.find((p) => p.startsWith(want) || want.star
|
|
|
166
415
|
* which is the common case and the whole reason this is low-noise.
|
|
167
416
|
*/
|
|
168
417
|
function judge(raw, v, format) {
|
|
169
|
-
const cls = stripVariants(raw);
|
|
418
|
+
const cls = stripVariants(raw, format);
|
|
419
|
+
const cq = /^@(sm|md|lg|xl):(.+)$/.exec(cls);
|
|
420
|
+
if (cq && format !== "tailwind") return {
|
|
421
|
+
cls: raw,
|
|
422
|
+
severity: "error",
|
|
423
|
+
reason: `\`@${cq[1]}:\` container-query variants are a tailwind-format spelling; the ${format} format emits pre-expanded classes`,
|
|
424
|
+
suggestion: `${cq[1]}-${cq[2]}`
|
|
425
|
+
};
|
|
170
426
|
const bp = /^(sm|md|lg|xl)-(.+)$/.exec(cls);
|
|
171
427
|
if (bp && format === "tailwind") return {
|
|
172
428
|
cls: raw,
|
|
429
|
+
severity: "error",
|
|
173
430
|
reason: `\`${bp[1]}-\` responsive classes are not emitted in the tailwind format`,
|
|
174
431
|
suggestion: `@${bp[1]}:${bp[2]} (framework breakpoints) or ${bp[1]}:${bp[2]} (Tailwind's)`
|
|
175
432
|
};
|
|
@@ -186,6 +443,7 @@ function judge(raw, v, format) {
|
|
|
186
443
|
const valid = emittedModifiers(v.roleClasses, fam, role);
|
|
187
444
|
return {
|
|
188
445
|
cls: raw,
|
|
446
|
+
severity: "error",
|
|
189
447
|
reason: `\`${fam}-${role}${mod ? `-${mod}` : ""}\` is not emitted for the \`${role}\` role`,
|
|
190
448
|
suggestion: valid.length ? `valid modifiers: ${valid.join(", ")}` : `the \`${role}\` role emits no \`${fam}-*\` utilities`
|
|
191
449
|
};
|
|
@@ -194,11 +452,13 @@ function judge(raw, v, format) {
|
|
|
194
452
|
const step = rest.slice(hue.length + 1);
|
|
195
453
|
if (step === "") return {
|
|
196
454
|
cls: raw,
|
|
455
|
+
severity: "error",
|
|
197
456
|
reason: `\`${fam}-${hue}\` names a palette hue with no step`,
|
|
198
457
|
suggestion: `${fam}-${hue}-500, or a role such as ${fam}-${v.roles[0] ?? "neutral"}`
|
|
199
458
|
};
|
|
200
459
|
if (!v.steps.includes(step)) return {
|
|
201
460
|
cls: raw,
|
|
461
|
+
severity: "error",
|
|
202
462
|
reason: `\`${step}\` is not a step on the \`${hue}\` scale (steps are 50…950)`,
|
|
203
463
|
suggestion: `${fam}-${hue}-500, or a role such as ${fam}-${v.roles[0] ?? "neutral"} which also flips in dark mode`
|
|
204
464
|
};
|
|
@@ -211,6 +471,7 @@ function judge(raw, v, format) {
|
|
|
211
471
|
const near = nearest(font[1], v.typeRoles);
|
|
212
472
|
if (near) return {
|
|
213
473
|
cls: raw,
|
|
474
|
+
severity: "error",
|
|
214
475
|
reason: `\`${font[1]}\` is not a typography role`,
|
|
215
476
|
suggestion: `font-${near}`
|
|
216
477
|
};
|
|
@@ -218,6 +479,7 @@ function judge(raw, v, format) {
|
|
|
218
479
|
const ds = /^drop-shadow-(.+)$/.exec(base);
|
|
219
480
|
if (ds && v.shadows.length && !v.shadows.includes(ds[1])) return {
|
|
220
481
|
cls: raw,
|
|
482
|
+
severity: "error",
|
|
221
483
|
reason: `\`${ds[1]}\` is not a defined shadow`,
|
|
222
484
|
suggestion: `drop-shadow-${v.shadows[0]}`
|
|
223
485
|
};
|
|
@@ -262,8 +524,8 @@ function lintSource(files, v, format) {
|
|
|
262
524
|
*
|
|
263
525
|
* vitops generate --input <json> --format <bricks|css|tailwind|design[,…]> --out <dir>
|
|
264
526
|
* vitops init [--out design-system.json] [--force]
|
|
265
|
-
* vitops validate <design-system.json>
|
|
266
|
-
* vitops favicon --input <svg|png> --out <dir> [--low-res <svg|png>]
|
|
527
|
+
* vitops validate <design-system.json|site.json>
|
|
528
|
+
* vitops favicon --input <svg|png> --out <dir> [--low-res <svg|png>] [--background <hex>]
|
|
267
529
|
* vitops agents [--input <json>] [--out AGENTS.md] [--docs-dir <dir>]
|
|
268
530
|
* vitops docs [topic] [--input <json>] [--all]
|
|
269
531
|
* vitops lint [--input <json>] [--format <fmt>] [--src <dir>]
|
|
@@ -295,25 +557,36 @@ const LEGAL_OUTPUTS = new Set([
|
|
|
295
557
|
]);
|
|
296
558
|
const HELP = `vitops — generate design-system outputs from a design-system.json
|
|
297
559
|
|
|
560
|
+
Anywhere a command takes --input, that file may be a design-system.json OR the
|
|
561
|
+
larger site config that embeds one (company.json / site.json). The two are told
|
|
562
|
+
apart by shape, and a site config also supplies the site-level facts generation
|
|
563
|
+
reads — its default colour scheme, its legal documents, its icon sprite.
|
|
564
|
+
|
|
298
565
|
Usage:
|
|
299
566
|
vitops generate [options] Generate platform output from a config
|
|
300
567
|
vitops init [options] Scaffold a starter design-system.json
|
|
301
|
-
vitops validate <file> Validate a config against the schema
|
|
568
|
+
vitops validate <file> Validate a config against the schema it says it is
|
|
302
569
|
vitops favicon [options] Generate a favicon set from a source image
|
|
303
570
|
vitops agents [options] Link the design-system agent skill + AGENTS.md pointer
|
|
304
571
|
vitops docs [topic] Print live design-system reference docs to stdout
|
|
305
572
|
vitops lint [options] Report framework classes in your source that resolve to nothing
|
|
306
573
|
vitops legal [options] Render legal documents from a site config
|
|
574
|
+
vitops icons [options] Report which icons your source uses, and build the sprite
|
|
307
575
|
|
|
308
576
|
Generate options:
|
|
309
|
-
-i, --input <path>
|
|
577
|
+
-i, --input <path> design-system.json or site config (default: ./design-system.json)
|
|
310
578
|
-f, --format <list> bricks | css | tailwind | design (comma-separated; default: bricks)
|
|
311
579
|
design emits DESIGN.md only (the agent-facing brief) — pair it
|
|
312
580
|
with --out . to write it beside AGENTS.md, or compose:
|
|
313
581
|
--format css,design
|
|
314
582
|
-o, --out <dir> Output directory (default: ./dist)
|
|
315
|
-
--
|
|
316
|
-
|
|
583
|
+
--theme <name> Which designSystem.themes entry to build, when --input is
|
|
584
|
+
a site config (default: its defaultTheme, else "default")
|
|
585
|
+
--site <path> Site config, when it is a different file from --input.
|
|
586
|
+
Emits legal/*.html into the output directory (see:
|
|
587
|
+
vitops legal). Omitted with a plain design-system.json:
|
|
588
|
+
no legal files.
|
|
589
|
+
--site-env <env> Environment whose A/B variant applies (default: production)
|
|
317
590
|
|
|
318
591
|
Init options:
|
|
319
592
|
-o, --out <path> Where to write (default: ./design-system.json)
|
|
@@ -323,9 +596,13 @@ Favicon options:
|
|
|
323
596
|
-i, --input <path> Source SVG or PNG (required)
|
|
324
597
|
-o, --out <dir> Output directory (default: ./public)
|
|
325
598
|
--low-res <path> Optional simplified source for the 16px icon
|
|
599
|
+
--background <hex> Background under the maskable icons, which must be
|
|
600
|
+
opaque (default: #ffffff)
|
|
326
601
|
|
|
327
602
|
Agents options:
|
|
328
|
-
-i, --input <path>
|
|
603
|
+
-i, --input <path> design-system.json or site config (default: ./design-system.json;
|
|
604
|
+
validated if present)
|
|
605
|
+
--theme <name> Theme to read, when --input is a site config
|
|
329
606
|
-o, --out <path> Doc file to update, idempotently (default: ./AGENTS.md)
|
|
330
607
|
--docs-dir <dir> Legacy layout: write the docs bundle as files to this dir
|
|
331
608
|
instead of linking the packaged skill
|
|
@@ -333,14 +610,25 @@ Agents options:
|
|
|
333
610
|
Docs options:
|
|
334
611
|
<topic> classes | authoring | formats | color | scales | patterns | elements
|
|
335
612
|
(no topic: list topics with summaries)
|
|
336
|
-
-i, --input <path>
|
|
613
|
+
-i, --input <path> design-system.json or site config (default: ./design-system.json)
|
|
614
|
+
--theme <name> Theme to render docs from, when --input is a site config
|
|
337
615
|
--all Print every topic, concatenated
|
|
338
616
|
|
|
617
|
+
Icons options:
|
|
618
|
+
--site <path> Site config carrying the "icons" block (default: ./site.json)
|
|
619
|
+
--src <dir> Source to scan for icon usage (default: ./src)
|
|
620
|
+
--sprite Also build the SVG sprite
|
|
621
|
+
-o, --out <dir> Where to write icons.svg with --sprite (default: ./dist)
|
|
622
|
+
--json Machine-readable report on stdout
|
|
623
|
+
|
|
339
624
|
Lint options:
|
|
340
|
-
-i, --input <path>
|
|
625
|
+
-i, --input <path> design-system.json or site config (default: ./design-system.json)
|
|
626
|
+
--theme <name> Theme to judge classes against, when --input is a site config
|
|
341
627
|
-f, --format <fmt> Format you build (bricks | css | tailwind; default: bricks).
|
|
342
628
|
Responsive md-* classes are real in css/bricks, inert in tailwind.
|
|
343
629
|
-s, --src <dir> Directory to scan (default: ./src)
|
|
630
|
+
--strict Also fail on suggestions (reuse hints), not just on
|
|
631
|
+
classes that resolve to nothing
|
|
344
632
|
|
|
345
633
|
Legal options:
|
|
346
634
|
-i, --input <path> Site config: .json, or a .js/.ts module with a default
|
|
@@ -382,7 +670,9 @@ async function cmdGenerate(argv) {
|
|
|
382
670
|
short: "o",
|
|
383
671
|
default: "dist"
|
|
384
672
|
},
|
|
385
|
-
site: { type: "string" }
|
|
673
|
+
site: { type: "string" },
|
|
674
|
+
theme: { type: "string" },
|
|
675
|
+
"site-env": { type: "string" }
|
|
386
676
|
},
|
|
387
677
|
allowPositionals: false
|
|
388
678
|
});
|
|
@@ -397,7 +687,9 @@ async function cmdGenerate(argv) {
|
|
|
397
687
|
input,
|
|
398
688
|
format,
|
|
399
689
|
outDir: values.out,
|
|
400
|
-
...site ? { site } : {}
|
|
690
|
+
...site ? { site } : {},
|
|
691
|
+
...values.theme ? { theme: values.theme } : {},
|
|
692
|
+
...values["site-env"] ? { siteEnv: values["site-env"] } : {}
|
|
401
693
|
});
|
|
402
694
|
console.log(`✓ ${format} → ${res.outDir} (${res.written.length} paths)`);
|
|
403
695
|
} catch (err) {
|
|
@@ -427,13 +719,25 @@ function cmdInit(argv) {
|
|
|
427
719
|
console.log(` Edit it, then: vitops generate --input ${values.out} --format tailwind --out src/styles`);
|
|
428
720
|
console.log(` Editors read the "$schema" (${SCHEMA_URL}) for autocomplete + validation.`);
|
|
429
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Validate a config against the schema its shape says it is.
|
|
724
|
+
*
|
|
725
|
+
* Routing on kind rather than asking the caller matters here: pointed at a site
|
|
726
|
+
* config, the design-system schema used to report a single `unrecognized_keys`
|
|
727
|
+
* for `designSystem` and nothing about the file's actual contents — a wrong
|
|
728
|
+
* answer that reads like a right one. A site config is validated whole, which
|
|
729
|
+
* also covers the cross-field integrity JSON Schema can't express.
|
|
730
|
+
*/
|
|
430
731
|
function cmdValidate(argv) {
|
|
431
732
|
const { values, positionals } = parseArgs({
|
|
432
733
|
args: argv,
|
|
433
|
-
options: {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
734
|
+
options: {
|
|
735
|
+
input: {
|
|
736
|
+
type: "string",
|
|
737
|
+
short: "i"
|
|
738
|
+
},
|
|
739
|
+
"site-env": { type: "string" }
|
|
740
|
+
},
|
|
437
741
|
allowPositionals: true
|
|
438
742
|
});
|
|
439
743
|
const path = resolve(positionals[0] ?? values.input ?? "design-system.json");
|
|
@@ -444,6 +748,30 @@ function cmdValidate(argv) {
|
|
|
444
748
|
} catch (err) {
|
|
445
749
|
fail(`could not parse JSON: ${err.message}`);
|
|
446
750
|
}
|
|
751
|
+
if (isSiteConfig(raw)) {
|
|
752
|
+
let site;
|
|
753
|
+
try {
|
|
754
|
+
site = resolveSiteConfig(raw, values["site-env"]);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
console.error(`✖ ${path} is invalid as a site config:`);
|
|
757
|
+
console.error(err.message.replace(/^Invalid site config:\n/, ""));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
const themes = site.designSystem?.themes ?? {};
|
|
761
|
+
let bad = false;
|
|
762
|
+
for (const name of Object.keys(themes)) {
|
|
763
|
+
const result = validate(resolveTheme(themes, name));
|
|
764
|
+
for (const w of result.warnings) console.warn(` ! designSystem.themes.${name}: ${w}`);
|
|
765
|
+
if (result.ok) continue;
|
|
766
|
+
bad = true;
|
|
767
|
+
console.error(`✖ ${path} — designSystem.themes.${name} is not a complete design system:`);
|
|
768
|
+
for (const e of result.errors) console.error(` • ${e.path.join(".") || "(root)"}: ${e.message}`);
|
|
769
|
+
}
|
|
770
|
+
if (bad) process.exit(1);
|
|
771
|
+
const n = Object.keys(themes).length;
|
|
772
|
+
console.log(`✓ ${path} is a valid site config (${n} theme${n === 1 ? "" : "s"})`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
447
775
|
const result = validate(raw);
|
|
448
776
|
for (const w of result.warnings) console.warn(` ! ${w}`);
|
|
449
777
|
if (result.ok) {
|
|
@@ -467,7 +795,8 @@ async function cmdFavicon(argv) {
|
|
|
467
795
|
short: "o",
|
|
468
796
|
default: "public"
|
|
469
797
|
},
|
|
470
|
-
"low-res": { type: "string" }
|
|
798
|
+
"low-res": { type: "string" },
|
|
799
|
+
background: { type: "string" }
|
|
471
800
|
},
|
|
472
801
|
allowPositionals: false
|
|
473
802
|
});
|
|
@@ -478,14 +807,24 @@ async function cmdFavicon(argv) {
|
|
|
478
807
|
const written = await generateFavicons({
|
|
479
808
|
source,
|
|
480
809
|
outputDir: values.out,
|
|
481
|
-
...values["low-res"] ? { lowResSource: resolve(values["low-res"]) } : {}
|
|
810
|
+
...values["low-res"] ? { lowResSource: resolve(values["low-res"]) } : {},
|
|
811
|
+
...values.background ? { backgroundColor: values.background } : {}
|
|
482
812
|
});
|
|
483
813
|
console.log(`✓ favicons → ${values.out} (${written.length} files)`);
|
|
484
814
|
} catch (err) {
|
|
485
815
|
fail(err.message);
|
|
486
816
|
}
|
|
487
817
|
}
|
|
488
|
-
|
|
818
|
+
/**
|
|
819
|
+
* Read + parse + validate the design system at `path`, or exit with a message.
|
|
820
|
+
*
|
|
821
|
+
* `path` may be a `design-system.json` **or** the larger site config that embeds
|
|
822
|
+
* one — every command that judges source against the design system (`docs`,
|
|
823
|
+
* `lint`, `agents`) accepts either, so a consumer who keeps their tokens in
|
|
824
|
+
* `company.json` doesn't have to maintain a second file for the tooling's sake.
|
|
825
|
+
* `resolveInput` tells them apart by shape and resolves the theme.
|
|
826
|
+
*/
|
|
827
|
+
function loadConfig(path, theme) {
|
|
489
828
|
if (!existsSync(path)) fail(`config not found: ${path}`);
|
|
490
829
|
let raw;
|
|
491
830
|
try {
|
|
@@ -493,9 +832,16 @@ function loadConfig(path) {
|
|
|
493
832
|
} catch (err) {
|
|
494
833
|
fail(`could not parse JSON: ${err.message}`);
|
|
495
834
|
}
|
|
496
|
-
|
|
835
|
+
let resolved;
|
|
836
|
+
try {
|
|
837
|
+
resolved = resolveInput(raw, theme != null ? { theme } : {});
|
|
838
|
+
} catch (err) {
|
|
839
|
+
fail(`${path}: ${err.message}`);
|
|
840
|
+
}
|
|
841
|
+
const result = validate(resolved.designSystem);
|
|
497
842
|
if (!result.ok) {
|
|
498
|
-
|
|
843
|
+
const where = resolved.theme != null ? ` (designSystem.themes.${resolved.theme})` : "";
|
|
844
|
+
console.error(`✖ ${path}${where} is invalid:`);
|
|
499
845
|
for (const e of result.errors) console.error(` • ${e.path.join(".") || "(root)"}: ${e.message}`);
|
|
500
846
|
process.exit(1);
|
|
501
847
|
}
|
|
@@ -540,6 +886,128 @@ async function loadSiteConfig(path, siteEnv) {
|
|
|
540
886
|
* the same documents without any integration code. Prints to stdout like
|
|
541
887
|
* `vitops docs`, or writes files when given `--out`.
|
|
542
888
|
*/
|
|
889
|
+
/**
|
|
890
|
+
* Report the icon vocabulary a project actually uses, and optionally build the
|
|
891
|
+
* sprite from it.
|
|
892
|
+
*
|
|
893
|
+
* A sibling of `legal` rather than a widening of `lint`: `lint` judges classes
|
|
894
|
+
* against a design-system.json, whereas icons are anchored to a SiteConfig.
|
|
895
|
+
*
|
|
896
|
+
* Exit codes carry the same distinction the Astro integration makes. A name the
|
|
897
|
+
* config DECLARES but that doesn't resolve is a config error and fails the
|
|
898
|
+
* command; a name only the SCAN found is reported and tolerated, because a bare
|
|
899
|
+
* unmapped name is more often a local src/icons/*.svg than a mistake. Runtime-
|
|
900
|
+
* computed names are listed with file and line so they can be declared, never
|
|
901
|
+
* guessed at.
|
|
902
|
+
*/
|
|
903
|
+
async function cmdIcons(argv) {
|
|
904
|
+
const { values } = parseArgs({
|
|
905
|
+
args: argv,
|
|
906
|
+
options: {
|
|
907
|
+
site: {
|
|
908
|
+
type: "string",
|
|
909
|
+
default: "site.json"
|
|
910
|
+
},
|
|
911
|
+
src: {
|
|
912
|
+
type: "string",
|
|
913
|
+
default: "src"
|
|
914
|
+
},
|
|
915
|
+
sprite: {
|
|
916
|
+
type: "boolean",
|
|
917
|
+
default: false
|
|
918
|
+
},
|
|
919
|
+
out: {
|
|
920
|
+
type: "string",
|
|
921
|
+
short: "o",
|
|
922
|
+
default: "dist"
|
|
923
|
+
},
|
|
924
|
+
json: {
|
|
925
|
+
type: "boolean",
|
|
926
|
+
default: false
|
|
927
|
+
},
|
|
928
|
+
"site-env": {
|
|
929
|
+
type: "string",
|
|
930
|
+
default: "production"
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
allowPositionals: false
|
|
934
|
+
});
|
|
935
|
+
const sitePath = resolve(values.site);
|
|
936
|
+
const icons = existsSync(sitePath) ? (await loadSiteConfig(sitePath, values["site-env"])).icons ?? {} : {};
|
|
937
|
+
const ui = icons.ui ?? "fa7-solid";
|
|
938
|
+
const brand = icons.brand ?? "simple-icons";
|
|
939
|
+
const weight = icons.weight;
|
|
940
|
+
const weightOpt = weight ? { weight } : {};
|
|
941
|
+
let include = {};
|
|
942
|
+
try {
|
|
943
|
+
include = generateIconInclude({ ...icons });
|
|
944
|
+
} catch (e) {
|
|
945
|
+
fail(e.message);
|
|
946
|
+
}
|
|
947
|
+
const add = (prefix, name) => {
|
|
948
|
+
const list = include[prefix] ??= [];
|
|
949
|
+
if (!list.includes(name)) list.push(name);
|
|
950
|
+
};
|
|
951
|
+
const srcDir = resolve(values.src);
|
|
952
|
+
const scanned = existsSync(srcDir) ? collectIconRefs(scanFiles(srcDir)) : null;
|
|
953
|
+
const unmapped = [];
|
|
954
|
+
for (const name of scanned?.names ?? []) {
|
|
955
|
+
const colon = name.indexOf(":");
|
|
956
|
+
if (colon > 0) {
|
|
957
|
+
add(name.slice(0, colon), name.slice(colon + 1));
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
const q = resolveIcon(name, ui, weightOpt);
|
|
962
|
+
add(q.slice(0, q.indexOf(":")), q.slice(q.indexOf(":") + 1));
|
|
963
|
+
} catch {
|
|
964
|
+
unmapped.push(name);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const dynamic = (scanned?.dynamic ?? []).map((d) => ({
|
|
968
|
+
file: relative(process.cwd(), d.file),
|
|
969
|
+
line: d.line,
|
|
970
|
+
expr: d.expr
|
|
971
|
+
}));
|
|
972
|
+
const total = Object.values(include).flat().length;
|
|
973
|
+
let sprite = null;
|
|
974
|
+
if (values.sprite) {
|
|
975
|
+
const aliases = {};
|
|
976
|
+
for (const name of icons.semantic ?? []) try {
|
|
977
|
+
aliases[`icon-${name}`] = resolveIcon(name, ui, weightOpt);
|
|
978
|
+
} catch {}
|
|
979
|
+
const built = await buildIconSprite({
|
|
980
|
+
include,
|
|
981
|
+
aliases
|
|
982
|
+
});
|
|
983
|
+
const outDir = resolve(values.out);
|
|
984
|
+
mkdirSync(outDir, { recursive: true });
|
|
985
|
+
writeFileSync(join(outDir, "icons.svg"), built.svg);
|
|
986
|
+
sprite = {
|
|
987
|
+
ids: built.ids,
|
|
988
|
+
missing: built.missing
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (values.json) process.stdout.write(`${JSON.stringify({
|
|
992
|
+
ui,
|
|
993
|
+
brand,
|
|
994
|
+
weight: weight ?? null,
|
|
995
|
+
include,
|
|
996
|
+
unmapped,
|
|
997
|
+
dynamic,
|
|
998
|
+
sprite
|
|
999
|
+
}, null, 2)}\n`);
|
|
1000
|
+
else {
|
|
1001
|
+
console.log(`icon sets: ui=${ui} brand=${brand}${weight ? ` weight=${weight}` : ""}`);
|
|
1002
|
+
for (const [prefix, names] of Object.entries(include)) console.log(` ${prefix}: ${names.length} — ${names.join(", ")}`);
|
|
1003
|
+
console.log(`✓ ${total} icon(s) across ${Object.keys(include).length} set(s)`);
|
|
1004
|
+
if (sprite) console.log(`✓ sprite → ${resolve(values.out)}/icons.svg (${sprite.ids.length} symbols)`);
|
|
1005
|
+
for (const d of dynamic) console.log(` ! ${d.file}:${d.line} ${d.expr} — computed at runtime, declare it in icons`);
|
|
1006
|
+
if (unmapped.length) console.log(` ! not in the '${ui}' map: ${unmapped.join(", ")} (fine for a local svg or sprite id)`);
|
|
1007
|
+
if (sprite?.missing.length) console.log(` ! absent from the sprite: ${sprite.missing.join(", ")}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (sprite?.missing.length) process.exitCode = 1;
|
|
1010
|
+
}
|
|
543
1011
|
async function cmdLegal(argv) {
|
|
544
1012
|
const { values } = parseArgs({
|
|
545
1013
|
args: argv,
|
|
@@ -613,6 +1081,7 @@ function cmdDocs(argv) {
|
|
|
613
1081
|
short: "i",
|
|
614
1082
|
default: "design-system.json"
|
|
615
1083
|
},
|
|
1084
|
+
theme: { type: "string" },
|
|
616
1085
|
all: {
|
|
617
1086
|
type: "boolean",
|
|
618
1087
|
default: false
|
|
@@ -627,45 +1096,10 @@ function cmdDocs(argv) {
|
|
|
627
1096
|
return;
|
|
628
1097
|
}
|
|
629
1098
|
if (topic && !TOPICS[topic]) fail(`unknown topic "${topic}". Valid topics:\n${topicList()}`);
|
|
630
|
-
const docs = generateDocs(loadConfig(resolve(values.input)), generatorAssetsDir());
|
|
1099
|
+
const docs = generateDocs(loadConfig(resolve(values.input), values.theme), generatorAssetsDir());
|
|
631
1100
|
const paths = values.all ? Object.values(TOPICS).map((t) => t.path) : [TOPICS[topic].path];
|
|
632
1101
|
process.stdout.write(paths.map((p) => docs[p]).join("\n"));
|
|
633
1102
|
}
|
|
634
|
-
const SCAN_EXT = new Set([
|
|
635
|
-
".astro",
|
|
636
|
-
".html",
|
|
637
|
-
".htm",
|
|
638
|
-
".js",
|
|
639
|
-
".jsx",
|
|
640
|
-
".ts",
|
|
641
|
-
".tsx",
|
|
642
|
-
".vue",
|
|
643
|
-
".svelte",
|
|
644
|
-
".php",
|
|
645
|
-
".md",
|
|
646
|
-
".mdx"
|
|
647
|
-
]);
|
|
648
|
-
const SCAN_SKIP = new Set([
|
|
649
|
-
"node_modules",
|
|
650
|
-
"dist",
|
|
651
|
-
".git",
|
|
652
|
-
".astro",
|
|
653
|
-
"build",
|
|
654
|
-
"coverage"
|
|
655
|
-
]);
|
|
656
|
-
function scanFiles(dir, acc = []) {
|
|
657
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
658
|
-
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
659
|
-
const p = join(dir, e.name);
|
|
660
|
-
if (e.isDirectory()) {
|
|
661
|
-
if (!SCAN_SKIP.has(e.name)) scanFiles(p, acc);
|
|
662
|
-
} else if (SCAN_EXT.has(extname(e.name))) acc.push({
|
|
663
|
-
path: p,
|
|
664
|
-
text: readFileSync(p, "utf8")
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return acc;
|
|
668
|
-
}
|
|
669
1103
|
function cmdLint(argv) {
|
|
670
1104
|
const { values } = parseArgs({
|
|
671
1105
|
args: argv,
|
|
@@ -675,6 +1109,7 @@ function cmdLint(argv) {
|
|
|
675
1109
|
short: "i",
|
|
676
1110
|
default: "design-system.json"
|
|
677
1111
|
},
|
|
1112
|
+
theme: { type: "string" },
|
|
678
1113
|
format: {
|
|
679
1114
|
type: "string",
|
|
680
1115
|
short: "f",
|
|
@@ -684,13 +1119,17 @@ function cmdLint(argv) {
|
|
|
684
1119
|
type: "string",
|
|
685
1120
|
short: "s",
|
|
686
1121
|
default: "src"
|
|
1122
|
+
},
|
|
1123
|
+
strict: {
|
|
1124
|
+
type: "boolean",
|
|
1125
|
+
default: false
|
|
687
1126
|
}
|
|
688
1127
|
},
|
|
689
1128
|
allowPositionals: false
|
|
690
1129
|
});
|
|
691
1130
|
const format = values.format;
|
|
692
1131
|
if (!LINT_FORMATS.has(format)) fail(`unknown format "${format}" (expected: bricks | css | tailwind)`);
|
|
693
|
-
const ds = loadConfig(resolve(values.input));
|
|
1132
|
+
const ds = loadConfig(resolve(values.input), values.theme);
|
|
694
1133
|
const srcDir = resolve(values.src);
|
|
695
1134
|
if (!existsSync(srcDir)) fail(`source directory not found: ${srcDir}`);
|
|
696
1135
|
const palette = expandPalette(ds.colors.palette);
|
|
@@ -701,19 +1140,26 @@ function cmdLint(argv) {
|
|
|
701
1140
|
"border"
|
|
702
1141
|
]).map((u) => u.cls);
|
|
703
1142
|
const files = scanFiles(srcDir);
|
|
704
|
-
const
|
|
1143
|
+
const cssFiles = [...files, ...scanFiles(srcDir, { exts: [".css"] })];
|
|
1144
|
+
const findings = [...lintSource(files, vocabulary(ds, roleClasses), format), ...lintCss(cssFiles, format)];
|
|
705
1145
|
if (!findings.length) {
|
|
706
1146
|
console.log(`✓ no unresolvable framework classes in ${values.src} (${files.length} file${files.length === 1 ? "" : "s"} scanned)`);
|
|
707
1147
|
return;
|
|
708
1148
|
}
|
|
709
|
-
|
|
710
|
-
|
|
1149
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
1150
|
+
const suggestions = findings.filter((f) => f.severity === "suggestion");
|
|
1151
|
+
for (const f of [...errors, ...suggestions]) {
|
|
1152
|
+
console.error(`${f.file}:${f.line} ${f.severity === "error" ? f.cls : `(${f.cls})`}`);
|
|
711
1153
|
console.error(` ${f.reason}`);
|
|
712
1154
|
if (f.suggestion) console.error(` try: ${f.suggestion}`);
|
|
713
1155
|
}
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
1156
|
+
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
1157
|
+
const parts = [];
|
|
1158
|
+
if (errors.length) parts.push(count(errors.length, "unresolvable class", "unresolvable classes"));
|
|
1159
|
+
if (suggestions.length) parts.push(count(suggestions.length, "suggestion", "suggestions"));
|
|
1160
|
+
const failing = errors.length > 0 || values.strict === true && suggestions.length > 0;
|
|
1161
|
+
console.error(`\n${failing ? "✖" : "!"} ${parts.join(", ")} in ${files.length} file${files.length === 1 ? "" : "s"}` + (suggestions.length && !values.strict ? " (suggestions do not fail; use --strict)" : ""));
|
|
1162
|
+
if (failing) process.exit(1);
|
|
717
1163
|
}
|
|
718
1164
|
function cmdAgents(argv) {
|
|
719
1165
|
const { values } = parseArgs({
|
|
@@ -724,6 +1170,7 @@ function cmdAgents(argv) {
|
|
|
724
1170
|
short: "i",
|
|
725
1171
|
default: "design-system.json"
|
|
726
1172
|
},
|
|
1173
|
+
theme: { type: "string" },
|
|
727
1174
|
out: {
|
|
728
1175
|
type: "string",
|
|
729
1176
|
short: "o",
|
|
@@ -735,7 +1182,7 @@ function cmdAgents(argv) {
|
|
|
735
1182
|
});
|
|
736
1183
|
const inputRel = values.input;
|
|
737
1184
|
let ds = null;
|
|
738
|
-
if (existsSync(resolve(inputRel))) ds = loadConfig(resolve(inputRel));
|
|
1185
|
+
if (existsSync(resolve(inputRel))) ds = loadConfig(resolve(inputRel), values.theme);
|
|
739
1186
|
else console.warn(` ⚠ ${inputRel} not found — run \`vitops init\` to scaffold one`);
|
|
740
1187
|
const legacyDocsDir = values["docs-dir"];
|
|
741
1188
|
let summary;
|
|
@@ -836,7 +1283,8 @@ async function main() {
|
|
|
836
1283
|
case "docs": return cmdDocs(rest);
|
|
837
1284
|
case "lint": return cmdLint(rest);
|
|
838
1285
|
case "legal": return cmdLegal(rest);
|
|
839
|
-
|
|
1286
|
+
case "icons": return cmdIcons(rest);
|
|
1287
|
+
default: fail(`unknown command "${command}" (expected: generate | init | validate | favicon | agents | docs | lint | legal | icons). Try: vitops --help`);
|
|
840
1288
|
}
|
|
841
1289
|
}
|
|
842
1290
|
main().catch((err) => fail(err.message));
|