onsystem 0.0.1 → 0.5.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/dist/cli.js ADDED
@@ -0,0 +1,1019 @@
1
+ #!/usr/bin/env node
2
+ import { $ as plural, C as updateBaseline, F as CONFIG_FILES, H as RULE_IDS, I as ConfigError, J as detectProject, K as rulesFor, L as DEFAULT_DOCS, R as DEFAULT_SEVERITY, S as readBaseline, V as LEGACY_CONFIG_FILES, _ as VERSION, at as toPosix, b as applyBaseline, c as silentLogger, g as NAME, h as formatDiagnostics, i as componentFiles, it as relativePath, l as stderrLogger, m as uncheckedNotice, nt as isInside, o as loadDesignSystem, rt as matchesGlob, s as tokenFiles, tt as unique, v as BASELINE_FILE, w as writeBaseline, x as countBaseline, y as LEGACY_BASELINE_FILE } from "./design-system-CnEv5sIL.js";
3
+ import { t as serveStdio } from "./stdio-DjfkiDMV.js";
4
+ import { r as loadTarget } from "./workspace-DzWQ4L8R.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { escapePath, glob } from "tinyglobby";
8
+ import fs$1 from "node:fs/promises";
9
+ import { spawnSync } from "node:child_process";
10
+ import readline from "node:readline/promises";
11
+ import { parseArgs } from "node:util";
12
+ //#region src/explain.ts
13
+ /**
14
+ * What `inspect --explain` says about one project: every candidate zero config looked
15
+ * at and why each was taken or not, the config it resolved to, tokens whose value did
16
+ * not resolve, and components without props, with the likely reason.
17
+ */
18
+ function explainProject(ds, cwd, indent = "") {
19
+ const { config, model } = ds;
20
+ const lines = [];
21
+ const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
22
+ out("How the design system was found");
23
+ if (config.configured) out(` components come from ${config.configFile ? display(cwd, config.configFile) : "--components"}, so zero config did not look`);
24
+ else {
25
+ const steps = [];
26
+ detectProject(config.root, config.tsconfig, steps);
27
+ for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
28
+ if (!config.detected) out(` So the defaults apply: ${config.components.join(", ")}${model.stats.files.components ? "" : ", which match nothing"}.`);
29
+ }
30
+ out();
31
+ out("Resolved config");
32
+ const field = (name, value) => out(` ${name.padEnd(12)} ${value}`);
33
+ field("root", config.root);
34
+ field("config", config.configFile ? display(cwd, config.configFile) : "(none)");
35
+ field("components", listOf$1(config.components) || "(none)");
36
+ field("exclude", listOf$1(config.exclude));
37
+ field("tokens", listOf$1(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}`)));
38
+ field("docs", listOf$1(config.docs));
39
+ field("tsconfig", config.tsconfig ?? (fs.existsSync(path.join(config.root, "tsconfig.json")) ? "tsconfig.json" : "(none)"));
40
+ field("importPath", importPaths(ds));
41
+ if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
42
+ if (config.designSystems) field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source} (${d.imports} imports)`).join("; "));
43
+ const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
44
+ field("rules", `${RULE_IDS.map((id) => `${id} ${config.rules[id].severity}`).join(", ")}${changed.length ? ` (changed: ${changed.join(", ")})` : ""}`);
45
+ if (config.overrides?.length) for (const override of config.overrides) field("override", `${override.files.join(", ")}: ${Object.entries(override.rules).map(([id, rule]) => `${id} ${rule.severity}`).join(", ")}`);
46
+ if (config.base) field("relative to", `${config.base} (a config at the workspace root)`);
47
+ out();
48
+ out("Tokens");
49
+ const found = ds.tokens;
50
+ const files = model.stats.files.tokens;
51
+ out(` ${found.length} tokens from ${files} ${files === 1 ? "file" : "files"}${files ? "" : `: no file matches ${config.tokens.map((t) => t.path).join(", ")}`}`);
52
+ const defined = new Set(found.map((t) => t.cssVar).filter((v) => Boolean(v)));
53
+ const missing = (value) => [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1] ?? "").filter((v) => !defined.has(v));
54
+ const unresolved = found.filter((t) => missing(t.value).length);
55
+ if (unresolved.length) out(` ${unresolved.length} unresolved, reading a custom property no token file defines: ${unresolved.slice(0, 8).map((t) => `${t.cssVar ?? t.name}: ${t.value}`).join(", ")}${unresolved.length > 8 ? `, and ${unresolved.length - 8} more` : ""}`);
56
+ else if (found.length) out(" every value resolves");
57
+ for (const warning of model.warnings.filter((w) => /alias|token/i.test(w))) out(` ${warning}`);
58
+ out();
59
+ out("Props");
60
+ const empty = ds.roots().filter((c) => c.openProps && !c.props.length && !c.inherits.length);
61
+ const open = ds.components.filter((c) => c.openProps);
62
+ if (!open.length) out(` every component's props resolved (${ds.components.length} components)`);
63
+ else {
64
+ const installed = nodeModulesFor(config.root);
65
+ const names = (list) => `${list.slice(0, 6).map((c) => c.name).join(", ")}${list.length > 6 ? ` and ${list.length - 6} more` : ""}`;
66
+ if (empty.length) out(` ${empty.length} ${empty.length === 1 ? "component has" : "components have"} 0 props: ${names(empty)}.`);
67
+ out(` ${open.length} ${open.length === 1 ? "component has" : "components have"} props that did not fully resolve${empty.length === open.length ? "" : `: ${names(open)}`}, so no-unknown-prop skips ${open.length === 1 ? "it" : "them"}.`);
68
+ out(installed ? ` Dependencies are installed (${display(cwd, installed)}): a package without types, a type error, or a props type that depends on a type argument leaves them open.` : ` The dependencies are not installed: no node_modules in ${config.root} or a folder above it. Their props come from the packages they wrap (Radix, React Aria), so install them (npm install, pnpm install) and run again.`);
69
+ }
70
+ return lines;
71
+ }
72
+ /** The first few, then how many more: a package's exports can name hundreds of files. */
73
+ function listOf$1(items, max = 8) {
74
+ const shown = items.slice(0, max).join(", ");
75
+ return items.length > max ? `${shown}, and ${items.length - max} more` : shown;
76
+ }
77
+ /** How components are suggested to be imported. */
78
+ function importPaths(ds) {
79
+ if (ds.config.importPath) return `${ds.config.importPath} (from the config)`;
80
+ const counts = /* @__PURE__ */ new Map();
81
+ for (const c of ds.roots()) counts.set(c.importPath, (counts.get(c.importPath) ?? 0) + 1);
82
+ const top = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3);
83
+ const from = ds.config.imports?.length ? "package exports" : "tsconfig paths or relative paths";
84
+ return top.length ? `inferred from ${from}: ${top.map(([p, n]) => `${p} (${n})`).join(", ")}${counts.size > 3 ? `, and ${counts.size - 3} more` : ""}` : `inferred from ${from}`;
85
+ }
86
+ /** The nearest node_modules at or above `dir`, if any. */
87
+ function nodeModulesFor(dir) {
88
+ for (let current = path.resolve(dir);; current = path.dirname(current)) {
89
+ const candidate = path.join(current, "node_modules");
90
+ if (fs.existsSync(candidate)) return candidate;
91
+ if (current === path.dirname(current)) return void 0;
92
+ }
93
+ }
94
+ function display(cwd, file) {
95
+ const rel = relativePath(cwd, file);
96
+ return rel && !rel.startsWith("..") ? rel : file;
97
+ }
98
+ //#endregion
99
+ //#region src/init.ts
100
+ const SCHEMA = "https://unpkg.com/onsystem/schema.json";
101
+ /** More detected globs than this are files, not folders: zero config keeps them. */
102
+ const MAX_GLOBS = 6;
103
+ /**
104
+ * Writes `onsystem.config.json` in the root from what zero config finds there, or from
105
+ * the answers to up to three questions when it finds nothing, then checks the project
106
+ * once without failing and offers to record what it found as the baseline.
107
+ */
108
+ async function init(configOptions, options, io, runCheck) {
109
+ const { config, workspace } = await loadTarget({
110
+ ...configOptions,
111
+ config: void 0
112
+ });
113
+ const root = config.root;
114
+ const existing = [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].map((name) => path.join(root, name)).find((file) => fs.existsSync(file));
115
+ if (existing && !options.force) {
116
+ io.stderr(`init: ${relativePath(io.cwd, existing) || existing} already exists. Run \`${NAME} inspect --explain\` to see what it resolves to, or pass --force to write a new onsystem.config.json.`);
117
+ return 2;
118
+ }
119
+ const ask = options.yes ? void 0 : io.prompt;
120
+ const file = { $schema: SCHEMA };
121
+ const notes = [];
122
+ const relative = (abs) => relativePath(root, abs);
123
+ if (configOptions.components?.length) {
124
+ file.components = configOptions.components;
125
+ if (configOptions.tokens?.length) file.tokens = configOptions.tokens;
126
+ if (configOptions.docs?.length) file.docs = configOptions.docs;
127
+ } else if (workspace) notes.push(`This is a monorepo root: ${workspace.projects.length} of ${workspace.projects.length + workspace.others.length} workspace packages have a design system (${workspace.projects.map((p) => p.dir).join(", ") || "none"}). Each keeps zero config; rules and overrides you add here apply to every project without a config of its own.`);
128
+ else if (config.detected) {
129
+ notes.push(`Found the design system through ${config.detected}.`);
130
+ const specifiers = new Set((config.imports ?? []).map((m) => m.specifier));
131
+ const tokens = (await tokenFiles(config)).map(relative);
132
+ if (specifiers.size > 1) notes.push(`"components" is left to zero config: apps import them by ${specifiers.size} specifiers from the package's exports (${[...specifiers].slice(0, 2).join(", ")}, …), which a config cannot spell.`);
133
+ else if (config.components.length > MAX_GLOBS) notes.push(`"components" is left to zero config: it found ${config.components.length} files through the package's exports, which it keeps following as the package changes.`);
134
+ else {
135
+ file.components = config.components;
136
+ if ([...specifiers][0]) file.importPath = [...specifiers][0];
137
+ const docs = config.docs.filter((d) => !DEFAULT_DOCS.includes(d));
138
+ if (docs.length) file.docs = [...DEFAULT_DOCS, ...docs];
139
+ }
140
+ if (tokens.length) file.tokens = tokens;
141
+ } else {
142
+ if (!ask) {
143
+ io.stderr(`init: zero config found no design system in ${root}. Pass --components (and --tokens) to say where it is, or run init in a terminal to answer a few questions. \`${NAME} inspect --explain\` shows what it looked at.`);
144
+ return 2;
145
+ }
146
+ io.stdout(`Zero config found no design system in ${root}.`);
147
+ let components = "";
148
+ for (let attempt = 0; attempt < 3 && !components; attempt++) {
149
+ const answer = (await ask("Where are the components? A glob from the root, e.g. src/components/ui/**/*.tsx: ")).trim();
150
+ if (!answer) continue;
151
+ if ((await componentFiles({
152
+ ...config,
153
+ components: [answer]
154
+ })).length) components = answer;
155
+ else io.stdout(`No file matches ${answer}.`);
156
+ }
157
+ if (!components) {
158
+ io.stderr("init: no components given; nothing written.");
159
+ return 2;
160
+ }
161
+ file.components = [components];
162
+ const tokens = (await ask("Where are the tokens? A CSS or DTCG .json file, or leave it empty: ")).trim();
163
+ if (tokens) {
164
+ if (!fs.existsSync(path.resolve(root, tokens)) && !/[*?{]/.test(tokens)) io.stdout(`${tokens} does not exist; it is left out.`);
165
+ else file.tokens = [tokens];
166
+ }
167
+ const importPath = (await ask("How do apps import them? A package name such as @acme/ui, or leave it empty for tsconfig paths: ")).trim();
168
+ if (importPath) file.importPath = importPath;
169
+ }
170
+ const out = path.join(root, "onsystem.config.json");
171
+ fs.writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
172
+ for (const note of notes) io.stdout(note);
173
+ io.stdout(`Wrote ${relativePath(io.cwd, out) || out}.`);
174
+ const checkIo = {
175
+ ...io,
176
+ cwd: root
177
+ };
178
+ const dry = await runCheck([
179
+ "check",
180
+ ".",
181
+ "--format",
182
+ "json",
183
+ ...options.cache ? [] : ["--no-cache"]
184
+ ], checkIo);
185
+ let results = [];
186
+ try {
187
+ results = JSON.parse(dry.stdout || "[]");
188
+ } catch {
189
+ io.stderr(`init: the dry run did not finish: ${dry.stderr.split("\n")[0] ?? ""}`);
190
+ return 1;
191
+ }
192
+ const byRule = /* @__PURE__ */ new Map();
193
+ let errors = 0;
194
+ let warnings = 0;
195
+ for (const result of results) for (const d of result.diagnostics) {
196
+ byRule.set(d.ruleId, (byRule.get(d.ruleId) ?? 0) + 1);
197
+ if (d.severity === "error") errors++;
198
+ else warnings++;
199
+ }
200
+ const flagged = results.filter((r) => r.diagnostics.length).length;
201
+ const total = errors + warnings;
202
+ io.stdout(total ? `Dry run of \`${NAME} check .\`: ${count$1(errors, "error")} and ${count$1(warnings, "warning")} in ${flagged} of ${count$1(results.length, "file")}. By rule: ${[...byRule].sort((a, b) => b[1] - a[1]).map(([rule, n]) => `${rule} ${n.toLocaleString("en-US")}`).join(", ")}.` : `Dry run of \`${NAME} check .\`: no problems in ${count$1(results.length, "file")}.`);
203
+ for (const line of dry.stderr.split("\n").filter(Boolean)) io.stdout(line);
204
+ const hasBaseline = [BASELINE_FILE, LEGACY_BASELINE_FILE].some((name) => fs.existsSync(path.join(root, name)));
205
+ if (total && !hasBaseline) {
206
+ const answer = options.yes ? "y" : ask ? (await ask(`Record these ${total.toLocaleString("en-US")} findings in ${BASELINE_FILE}, so check fails only on new ones? [Y/n] `)).trim().toLowerCase() : "n";
207
+ if (answer === "" || answer.startsWith("y")) {
208
+ const recorded = await runCheck([
209
+ "check",
210
+ ".",
211
+ "--update-baseline",
212
+ ...options.cache ? [] : ["--no-cache"]
213
+ ], checkIo);
214
+ if (recorded.stdout) io.stdout(recorded.stdout);
215
+ } else io.stdout(`No baseline written; \`${NAME} check . --update-baseline\` records one later.`);
216
+ }
217
+ io.stdout(`Next: commit onsystem.config.json${total && !hasBaseline ? ` and ${BASELINE_FILE}` : ""}, and run \`npx ${NAME} check . --format github --require-design-system\` in CI.`);
218
+ return 0;
219
+ }
220
+ function count$1(n, word) {
221
+ return `${n.toLocaleString("en-US")} ${n === 1 ? word : `${word}s`}`;
222
+ }
223
+ //#endregion
224
+ //#region src/cli.ts
225
+ const DOCS = "https://onsystem.vercel.app/docs";
226
+ const HELP = `${NAME} ${VERSION}
227
+
228
+ Keeps coding agents on your React design system: checks UI code against your
229
+ real components, props, variants and tokens in a Claude Code hook, in CI with
230
+ check, and over MCP for any agent.
231
+
232
+ Usage
233
+ onsystem [serve] [options] Start the MCP server on stdio (default)
234
+ onsystem check <paths...> Lint files, folders or globs with the check_ui rules
235
+ (for CI)
236
+ onsystem inspect Print what was extracted from the project
237
+ onsystem init Write onsystem.config.json from what was found, run a
238
+ dry-run check and offer a baseline
239
+ onsystem help Show this help
240
+
241
+ At a monorepo root with no design system of its own, every command works per
242
+ project: each file is checked against the design system of the nearest folder
243
+ with a config, components.json or package.json, and inspect lists every project.
244
+
245
+ Options
246
+ --root <dir> Project root (default: the config file's directory, or cwd)
247
+ --config <file> Config file (default: onsystem.config.{json,ts,mjs,js})
248
+ --components <glob> Component sources, repeatable (default: found through
249
+ components.json or a workspace package, else
250
+ components/ui/**/*.{tsx,jsx} and src/components/ui/...)
251
+ --tokens <file> Token file (DTCG .json or CSS), repeatable
252
+ --docs <glob> Component docs (Markdown/MDX), repeatable
253
+ --no-cache Ignore the on-disk extraction cache
254
+ --no-watch serve: do not reload when files change
255
+ --format <format> check: pretty | json | github | sarif (default: pretty)
256
+ --max-warnings <n> check: exit 1 when there are more than n warnings
257
+ --update-baseline check: record the current findings as accepted, and exit 0
258
+ --baseline <file> check: baseline file (default: ${BASELINE_FILE} in the root,
259
+ used whenever it exists)
260
+ --ignore-baseline check: report every finding, baseline or not
261
+ --include-design-system
262
+ check: also lint the design system's own component
263
+ files, which are skipped by default
264
+ --include-tests check: also lint tests and stories (*.test.tsx,
265
+ *.spec.tsx, *.stories.tsx) in folders and globs
266
+ --quiet-without-design-system
267
+ check: print nothing and exit 0 when the project has no
268
+ design-system components and no config (tokens alone
269
+ are not a design system), for hooks installed across
270
+ many projects
271
+ --require-design-system
272
+ check: exit 2 when no components or no color tokens
273
+ are found, so CI cannot pass by checking nothing
274
+ --explain inspect: every candidate zero config looked at and why
275
+ it was taken or not, the resolved config, unresolved
276
+ tokens, and components whose props did not resolve
277
+ -y, --yes init: write without asking, and record the baseline
278
+ --force init: replace an existing config file
279
+ -h, --help Show this help
280
+ -v, --version Show the version
281
+
282
+ Examples
283
+ npx -y ${NAME} inspect --explain
284
+ npx -y ${NAME} init
285
+ npx -y ${NAME} check . --format github --require-design-system
286
+ npx -y ${NAME} check . --update-baseline # adopt in an existing codebase
287
+
288
+ Docs: ${DOCS}
289
+ `;
290
+ const defaultIo = {
291
+ cwd: process.cwd(),
292
+ stdout: (text) => process.stdout.write(`${text}\n`),
293
+ stderr: (text) => process.stderr.write(`${text}\n`),
294
+ color: process.stdout.isTTY && !process.env.NO_COLOR,
295
+ prompt: process.stdin.isTTY && process.stdout.isTTY ? askInTerminal : void 0
296
+ };
297
+ /** One question on the terminal, answered with a line. */
298
+ async function askInTerminal(question) {
299
+ const rl = readline.createInterface({
300
+ input: process.stdin,
301
+ output: process.stdout
302
+ });
303
+ try {
304
+ return await rl.question(question);
305
+ } finally {
306
+ rl.close();
307
+ }
308
+ }
309
+ /** Runs the CLI. Resolves to an exit code; `serve` resolves once the server is listening. */
310
+ async function main(argv, io = defaultIo) {
311
+ let parsed;
312
+ try {
313
+ parsed = parseArgs({
314
+ args: argv,
315
+ allowPositionals: true,
316
+ allowNegative: true,
317
+ options: {
318
+ root: { type: "string" },
319
+ config: { type: "string" },
320
+ components: {
321
+ type: "string",
322
+ multiple: true
323
+ },
324
+ tokens: {
325
+ type: "string",
326
+ multiple: true
327
+ },
328
+ docs: {
329
+ type: "string",
330
+ multiple: true
331
+ },
332
+ cache: {
333
+ type: "boolean",
334
+ default: true
335
+ },
336
+ watch: {
337
+ type: "boolean",
338
+ default: true
339
+ },
340
+ format: {
341
+ type: "string",
342
+ default: "pretty"
343
+ },
344
+ "max-warnings": { type: "string" },
345
+ baseline: { type: "string" },
346
+ "update-baseline": {
347
+ type: "boolean",
348
+ default: false
349
+ },
350
+ "ignore-baseline": {
351
+ type: "boolean",
352
+ default: false
353
+ },
354
+ "quiet-without-design-system": {
355
+ type: "boolean",
356
+ default: false
357
+ },
358
+ "include-design-system": { type: "boolean" },
359
+ "include-tests": {
360
+ type: "boolean",
361
+ default: false
362
+ },
363
+ "require-design-system": {
364
+ type: "boolean",
365
+ default: false
366
+ },
367
+ explain: {
368
+ type: "boolean",
369
+ default: false
370
+ },
371
+ yes: {
372
+ type: "boolean",
373
+ short: "y",
374
+ default: false
375
+ },
376
+ force: {
377
+ type: "boolean",
378
+ default: false
379
+ },
380
+ help: {
381
+ type: "boolean",
382
+ short: "h"
383
+ },
384
+ version: {
385
+ type: "boolean",
386
+ short: "v"
387
+ }
388
+ }
389
+ });
390
+ } catch (error) {
391
+ io.stderr(`${error.message}\n\nRun ${NAME} --help for usage.`);
392
+ return 2;
393
+ }
394
+ const { values, positionals } = parsed;
395
+ if (values.help) {
396
+ io.stdout(HELP);
397
+ return 0;
398
+ }
399
+ if (values.version) {
400
+ io.stdout(VERSION);
401
+ return 0;
402
+ }
403
+ const [command = "serve", ...rest] = positionals;
404
+ if (command === "help") {
405
+ io.stdout(HELP);
406
+ return 0;
407
+ }
408
+ let root = values.root;
409
+ if (root !== void 0 && /\$\{[^}]*\}/.test(root)) {
410
+ io.stderr(`--root "${root}" holds a variable the client did not expand; using the working directory instead${command === "serve" ? ", or the client's roots when it holds no project" : ""}.`);
411
+ root = void 0;
412
+ }
413
+ const configOptions = {
414
+ cwd: io.cwd,
415
+ root,
416
+ config: values.config,
417
+ components: values.components,
418
+ tokens: values.tokens,
419
+ docs: values.docs,
420
+ includeDesignSystem: values["include-design-system"]
421
+ };
422
+ try {
423
+ switch (command) {
424
+ case "serve":
425
+ await serveStdio({
426
+ ...configOptions,
427
+ cache: values.cache,
428
+ watch: values.watch,
429
+ logger: stderrLogger
430
+ });
431
+ return 0;
432
+ case "check": return await check(rest, configOptions, values, io);
433
+ case "inspect": return await inspect(configOptions, values, io);
434
+ case "init": return await init(configOptions, {
435
+ yes: values.yes,
436
+ force: values.force,
437
+ cache: values.cache
438
+ }, io, async (args, checkIo) => {
439
+ const out = [];
440
+ const err = [];
441
+ return {
442
+ code: await main(args, {
443
+ cwd: checkIo.cwd,
444
+ color: false,
445
+ stdout: (t) => out.push(t),
446
+ stderr: (t) => err.push(t)
447
+ }),
448
+ stdout: out.join("\n"),
449
+ stderr: err.join("\n")
450
+ };
451
+ });
452
+ default:
453
+ io.stderr(`Unknown command "${command}". Run ${NAME} --help for usage.`);
454
+ return 2;
455
+ }
456
+ } catch (error) {
457
+ if (error instanceof ConfigError) {
458
+ io.stderr(error.message);
459
+ return 2;
460
+ }
461
+ throw error;
462
+ }
463
+ }
464
+ async function check(patterns, configOptions, values, io) {
465
+ if (values["quiet-without-design-system"] && values["require-design-system"]) {
466
+ io.stderr("check: pass --quiet-without-design-system or --require-design-system, not both");
467
+ return 2;
468
+ }
469
+ if (!patterns.length) {
470
+ io.stderr(`check: pass files, folders or globs, e.g. ${NAME} check .`);
471
+ return 2;
472
+ }
473
+ const format = values.format;
474
+ if (![
475
+ "pretty",
476
+ "json",
477
+ "github",
478
+ "sarif"
479
+ ].includes(format)) {
480
+ io.stderr(`check: unknown --format "${values.format}" (pretty, json, github, sarif)`);
481
+ return 2;
482
+ }
483
+ const maxWarnings = values["max-warnings"] === void 0 ? void 0 : Number(values["max-warnings"]);
484
+ if (maxWarnings !== void 0 && !Number.isInteger(maxWarnings)) {
485
+ io.stderr("check: --max-warnings expects an integer");
486
+ return 2;
487
+ }
488
+ const target = await loadTarget(configOptions);
489
+ warnDeprecated(target.config.deprecations, format, io);
490
+ const selection = target.workspace ? await selectInWorkspace(target.workspace, patterns, values, format, io) : await selectInProject(target.config, patterns, values, format, io);
491
+ if (typeof selection === "number") return selection;
492
+ const { files, notes } = selection;
493
+ if (!files.length) {
494
+ if (format === "pretty") {
495
+ const hints = [selection.skipped ? `Pass --include-design-system to check ${selection.skipped === 1 ? "it" : "them"}.` : "", selection.testsLeftOut ? "Pass --include-tests to check tests and stories." : ""].filter(Boolean);
496
+ io.stdout(`Nothing to check: ${notes.join("; ")}. ${hints.join(" ")}`.trimEnd());
497
+ } else printNoResults(format, io);
498
+ return 0;
499
+ }
500
+ const update = values["update-baseline"];
501
+ const baselineFile = values.baseline ? path.resolve(io.cwd, values.baseline) : defaultBaseline(selection.root, values["ignore-baseline"] ? void 0 : (text) => {
502
+ warnDeprecated([text], format, io);
503
+ });
504
+ const baseline = values["ignore-baseline"] ? void 0 : readBaseline(baselineFile);
505
+ if (values.baseline && !update && !values["ignore-baseline"] && !baseline) {
506
+ io.stderr(`check: baseline not found: ${displayPath(io.cwd, baselineFile)}`);
507
+ return 2;
508
+ }
509
+ const disabledFor = (file) => {
510
+ const ds = selection.designSystemOf(file);
511
+ return new Set(Object.entries(rulesFor(ds.config, relativePath(ds.root, file))).filter(([, rule]) => rule.severity === "off").map(([id]) => id));
512
+ };
513
+ const disabledByKey = /* @__PURE__ */ new Map();
514
+ const realRoot = fs.realpathSync.native(selection.root);
515
+ const baselineKey = (file) => relativePath(realRoot, fs.realpathSync.native(file)).normalize("NFC");
516
+ const results = [];
517
+ const checked = /* @__PURE__ */ new Map();
518
+ let baselined = 0;
519
+ let fixed = 0;
520
+ for (const file of files) {
521
+ const code = await fs$1.readFile(file, "utf8");
522
+ const ds = selection.designSystemOf(file);
523
+ let result = ds.check(code, relativePath(ds.root, file));
524
+ const key = baselineKey(file);
525
+ const disabled = disabledFor(file);
526
+ disabledByKey.set(key, disabled);
527
+ if (update) checked.set(key, result.diagnostics);
528
+ if (baseline && !update) {
529
+ const match = applyBaseline(baseline, key, result, disabled);
530
+ result = {
531
+ ...match.result,
532
+ baselined: match.baselined
533
+ };
534
+ baselined += match.baselined;
535
+ fixed += match.fixed;
536
+ }
537
+ results.push({
538
+ ...result,
539
+ file: relativePath(io.cwd, file)
540
+ });
541
+ }
542
+ const elsewhere = results.filter((r) => r.skipped).length;
543
+ if (elsewhere) notes.push(`${plural(elsewhere, "image or email file")} not checked`);
544
+ const unchecked = uncheckedNotice(results);
545
+ if (update) {
546
+ const next = updateBaseline(baseline, checked, realRoot, (key) => disabledByKey.get(key) ?? /* @__PURE__ */ new Set());
547
+ writeBaseline(baselineFile, next);
548
+ const counts = countBaseline(next);
549
+ io.stdout(`Baseline: ${count(counts.findings, "finding")} in ${count(counts.files, "file")} → ${displayPath(io.cwd, baselineFile)}`);
550
+ for (const text of selection.notices) io.stderr(text);
551
+ if (unchecked) io.stderr(unchecked);
552
+ return 0;
553
+ }
554
+ const output = formatDiagnostics(format === "json" ? results : results.filter((r) => !r.skipped), format, {
555
+ color: io.color,
556
+ uri: format === "sarif" || format === "github" ? repositoryPath(io.cwd, io.env ?? process.env) : void 0,
557
+ baselined: baseline ? baselined : void 0,
558
+ notes,
559
+ fixedHint: fixed ? `${count(fixed, "baseline finding")} no longer ${fixed === 1 ? "occurs" : "occur"}: run \`check ${patterns.map(shellQuote).join(" ")} --update-baseline\` to drop ${fixed === 1 ? "it" : "them"}.` : void 0
560
+ });
561
+ if (output) io.stdout(output);
562
+ for (const text of [...selection.notices, unchecked]) {
563
+ if (!text) continue;
564
+ if (format === "pretty") io.stdout(text);
565
+ else if (format === "github") io.stdout(`::warning title=${NAME}::${text}`);
566
+ else io.stderr(text);
567
+ }
568
+ const errors = results.reduce((n, r) => n + r.errorCount, 0);
569
+ const warnings = results.reduce((n, r) => n + r.warningCount, 0);
570
+ if (errors > 0) return 1;
571
+ if (maxWarnings !== void 0 && warnings > maxWarnings) {
572
+ if (format === "pretty") io.stderr(`Too many warnings (${warnings}, max ${maxWarnings}).`);
573
+ return 1;
574
+ }
575
+ return 0;
576
+ }
577
+ /** What `json` and `sarif` print when there is nothing to report on: `[]`, or a log without results. */
578
+ function printNoResults(format, io) {
579
+ if (format === "json") io.stdout("[]");
580
+ else if (format === "sarif") io.stdout(formatDiagnostics([], "sarif"));
581
+ }
582
+ /**
583
+ * Maps a path relative to the working directory to one relative to the repository root, which
584
+ * GitHub resolves workflow command annotations (`--format github`) and code scanning locations
585
+ * (`--format sarif`) against, so `check` run from `apps/web` still reports
586
+ * `apps/web/app/page.tsx`. The root is GITHUB_WORKSPACE or the git top-level, whichever is the
587
+ * innermost folder holding the file: the git top-level outside Actions, GITHUB_WORKSPACE where
588
+ * git is missing or refuses the checkout, and the repository checked out in a folder of the
589
+ * workspace (`actions/checkout` with `path:`). A file under neither keeps its path relative to
590
+ * the working directory.
591
+ */
592
+ function repositoryPath(cwd, env) {
593
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
594
+ cwd,
595
+ encoding: "utf8",
596
+ windowsHide: true
597
+ });
598
+ const roots = [result.status === 0 && typeof result.stdout === "string" ? result.stdout.trim() : "", env.GITHUB_WORKSPACE ?? ""].filter(Boolean).map((dir) => realPath(path.resolve(cwd, dir))).sort((a, b) => b.length - a.length);
599
+ if (!roots.length) return toPosix;
600
+ return (file) => {
601
+ const absolute = realPath(path.resolve(cwd, file));
602
+ const root = roots.find((dir) => isInside(dir, absolute));
603
+ return root ? relativePath(root, absolute) : toPosix(file);
604
+ };
605
+ }
606
+ /** Splits the patterns into files named outright and globs (a folder: every TSX/JSX file under it). */
607
+ function expandPatterns(patterns, cwd) {
608
+ const named = [];
609
+ const globs = [];
610
+ for (const pattern of patterns) {
611
+ const stat = fs.statSync(path.resolve(cwd, pattern), { throwIfNoEntry: false });
612
+ if (stat && !stat.isDirectory()) {
613
+ named.push(path.resolve(cwd, pattern));
614
+ continue;
615
+ }
616
+ const literal = stat ? escapePath(toPosix(path.join(pattern, "."))) : toPosix(pattern);
617
+ globs.push(!stat ? literal : literal === "." ? "**/*.{tsx,jsx}" : `${literal}/**/*.{tsx,jsx}`);
618
+ }
619
+ return {
620
+ named,
621
+ globs
622
+ };
623
+ }
624
+ /** One project: the root's config and design system for every file. */
625
+ async function selectInProject(config, patterns, values, format, io) {
626
+ const ds = await loadDesignSystem(config, {
627
+ cache: values.cache,
628
+ logger: silentLogger
629
+ });
630
+ const { named, globs } = expandPatterns(patterns, io.cwd);
631
+ const options = {
632
+ cwd: io.cwd,
633
+ absolute: true,
634
+ expandDirectories: false
635
+ };
636
+ const all = globs.length ? await findFiles(globs, {
637
+ ...options,
638
+ ignore: ["**/node_modules/**"]
639
+ }) : [];
640
+ const excludes = config.exclude.filter((e) => !(values["include-tests"] && isTestPattern(e))).map((e) => excludeFromCwd(e, config.root, io.cwd)).filter((e) => e !== void 0);
641
+ const kept = globs.length ? await findFiles(globs, {
642
+ ...options,
643
+ ignore: unique(["**/node_modules/**", ...excludes])
644
+ }) : [];
645
+ const ignored = gitIgnored(io.cwd, kept);
646
+ const keptSet = new Set(kept);
647
+ const leftOut = all.filter((file) => !keptSet.has(file));
648
+ const left = {
649
+ tests: leftOut.filter((file) => TEST_FILE.test(file)).length,
650
+ excluded: leftOut.filter((file) => !TEST_FILE.test(file)).length,
651
+ gitignored: ignored.size
652
+ };
653
+ let files = unique([...named, ...kept.filter((file) => !ignored.has(file))]).sort();
654
+ if (!files.length && !all.length) {
655
+ io.stderr(`check: no files match ${patterns.join(" ")}`);
656
+ return 2;
657
+ }
658
+ if (values["quiet-without-design-system"] && !config.configFile && !ds.components.length) {
659
+ printNoResults(format, io);
660
+ return 0;
661
+ }
662
+ const notice = ds.notice();
663
+ if (notice && values["require-design-system"]) {
664
+ io.stderr(`check --require-design-system: ${notice}`);
665
+ return 2;
666
+ }
667
+ let skipped = 0;
668
+ if (!config.includeDesignSystem) {
669
+ const own = new Set((await componentFiles(config)).map(realPath));
670
+ const rest = files.filter((file) => !own.has(realPath(file)));
671
+ skipped = files.length - rest.length;
672
+ files = rest;
673
+ }
674
+ return {
675
+ files,
676
+ designSystemOf: () => ds,
677
+ root: ds.root,
678
+ notes: scopeNotes(skipped, left),
679
+ notices: notice ? [notice] : [],
680
+ skipped,
681
+ testsLeftOut: left.tests
682
+ };
683
+ }
684
+ /**
685
+ * A workspace root: each file is checked by the design system of its own project, the
686
+ * nearest folder with a config, a components.json or a package.json, with that project's
687
+ * config, exactly as `check` from that folder (and the Claude Code hook) would check it.
688
+ */
689
+ async function selectInWorkspace(workspace, patterns, values, format, io) {
690
+ const { named, globs } = expandPatterns(patterns, io.cwd);
691
+ const all = globs.length ? await findFiles(globs, {
692
+ cwd: io.cwd,
693
+ absolute: true,
694
+ expandDirectories: false,
695
+ ignore: ["**/node_modules/**"]
696
+ }) : [];
697
+ const projectOf = /* @__PURE__ */ new Map();
698
+ for (const file of unique([...named, ...all])) projectOf.set(file, await workspace.projectFor(file));
699
+ const project = (file) => {
700
+ const found = projectOf.get(file);
701
+ if (!found) throw new Error(`No project for ${file}`);
702
+ return found;
703
+ };
704
+ const designSystems = /* @__PURE__ */ new Map();
705
+ const load = (p) => {
706
+ let ds = designSystems.get(p.root);
707
+ if (!ds) {
708
+ ds = loadDesignSystem(p.config, {
709
+ cache: values.cache,
710
+ logger: silentLogger
711
+ });
712
+ designSystems.set(p.root, ds);
713
+ }
714
+ return ds;
715
+ };
716
+ const leftOut = all.filter((file) => {
717
+ const { root, config } = project(file);
718
+ const relative = relativePath(root, file);
719
+ return config.exclude.some((e) => !(values["include-tests"] && isTestPattern(e)) && matchesGlob(relative, e));
720
+ });
721
+ const leftOutSet = new Set(leftOut);
722
+ const kept = all.filter((file) => !leftOutSet.has(file));
723
+ const ignored = gitIgnored(io.cwd, kept);
724
+ const left = {
725
+ tests: leftOut.filter((file) => TEST_FILE.test(file)).length,
726
+ excluded: leftOut.filter((file) => !TEST_FILE.test(file)).length,
727
+ gitignored: ignored.size
728
+ };
729
+ let files = unique([...named, ...kept.filter((file) => !ignored.has(file))]).sort();
730
+ if (!files.length && !all.length) {
731
+ io.stderr(`check: no files match ${patterns.join(" ")}`);
732
+ return 2;
733
+ }
734
+ const loaded = /* @__PURE__ */ new Map();
735
+ for (const p of unique(files.map(project))) loaded.set(p.root, await load(p));
736
+ const designSystemOf = (file) => {
737
+ const ds = loaded.get(project(file).root);
738
+ if (!ds) throw new Error(`No design system loaded for ${file}`);
739
+ return ds;
740
+ };
741
+ if (values["quiet-without-design-system"]) {
742
+ files = files.filter((file) => Boolean(project(file).config.configFile) || designSystemOf(file).components.length > 0);
743
+ if (!files.length) {
744
+ printNoResults(format, io);
745
+ return 0;
746
+ }
747
+ }
748
+ if (values["require-design-system"]) {
749
+ let complete = false;
750
+ for (const p of workspace.projects) if (!(await load(p)).notice()) {
751
+ complete = true;
752
+ break;
753
+ }
754
+ if (!complete) {
755
+ const packages = workspace.projects.length + workspace.others.length;
756
+ io.stderr(`check --require-design-system: no workspace package has a design system with components and color tokens (${plural(packages, "package")} looked at). See ${DOCS}/troubleshooting`);
757
+ return 2;
758
+ }
759
+ }
760
+ const own = /* @__PURE__ */ new Map();
761
+ for (const p of unique(files.map(project))) {
762
+ if (p.config.includeDesignSystem) continue;
763
+ own.set(p.root, new Set((await componentFiles(p.config)).map(realPath)));
764
+ }
765
+ const rest = files.filter((file) => !own.get(project(file).root)?.has(realPath(file)));
766
+ const skipped = files.length - rest.length;
767
+ files = rest;
768
+ const gaps = /* @__PURE__ */ new Map();
769
+ for (const p of unique(files.map(project))) {
770
+ const ds = loaded.get(p.root);
771
+ const notice = ds?.notice();
772
+ if (!ds || !notice) continue;
773
+ const gap = gaps.get(notice) ?? {
774
+ ds,
775
+ dirs: []
776
+ };
777
+ gap.dirs.push(p.dir);
778
+ gaps.set(notice, gap);
779
+ }
780
+ const notices = [...gaps.values()].map(({ ds, dirs }) => ds.notice(listOf(dirs)) ?? "");
781
+ const used = unique(files.map((file) => project(file).root)).length;
782
+ return {
783
+ files,
784
+ designSystemOf,
785
+ root: workspace.root,
786
+ notes: [...used > 1 ? [plural(used, "project")] : [], ...scopeNotes(skipped, left)],
787
+ notices: notices.filter(Boolean),
788
+ skipped,
789
+ testsLeftOut: left.tests
790
+ };
791
+ }
792
+ /** `apps/web`, `apps/web and packages/ui`, `a, b, c and 4 more`. */
793
+ function listOf(items, max = 3) {
794
+ if (items.length <= 1) return items[0] ?? "";
795
+ if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
796
+ return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
797
+ }
798
+ async function inspect(configOptions, values, io) {
799
+ const target = await loadTarget(configOptions);
800
+ const { config } = target;
801
+ warnDeprecated(config.deprecations, "pretty", io);
802
+ if (target.workspace) return inspectWorkspace(target.workspace, values, io);
803
+ const ds = await loadDesignSystem(config, {
804
+ cache: values.cache,
805
+ logger: silentLogger
806
+ });
807
+ const { model } = ds;
808
+ if (values.format === "json") {
809
+ io.stdout(JSON.stringify(model, null, 2));
810
+ return 0;
811
+ }
812
+ const lines = [
813
+ `root ${model.root}`,
814
+ `config ${config.configFile ? displayPath(io.cwd, config.configFile) : "(defaults)"}`,
815
+ ...config.detected ? [`detected ${config.detected}`] : [],
816
+ `files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`,
817
+ `time ${model.stats.durationMs}ms${model.stats.fromCache ? " (cache)" : ""}`,
818
+ "",
819
+ `Components (${ds.roots().length} + ${model.components.length - ds.roots().length} parts)`
820
+ ];
821
+ for (const c of ds.roots()) {
822
+ const variants = c.variants.map((v) => `${v.name}[${v.values.length}]`).join(" ");
823
+ const meta = [
824
+ c.element ? `<${c.element}>` : "",
825
+ plural(c.props.length, "prop"),
826
+ variants,
827
+ c.subcomponents.length ? plural(c.subcomponents.length, "part") : "",
828
+ c.docs ? "docs" : "",
829
+ plural(c.examples.length, "example")
830
+ ].filter(Boolean);
831
+ lines.push(` ${c.name.padEnd(18)} ${meta.join(" · ")}`);
832
+ }
833
+ lines.push("", `Tokens (${model.tokens.length})`);
834
+ for (const [category, n] of tokenCounts(model)) lines.push(` ${category.padEnd(18)} ${n}`);
835
+ if (model.warnings.length) {
836
+ lines.push("", "Warnings");
837
+ for (const warning of model.warnings) lines.push(` ${warning}`);
838
+ }
839
+ if (values.explain) lines.push("", ...explainProject(ds, io.cwd));
840
+ io.stdout(lines.join("\n"));
841
+ return 0;
842
+ }
843
+ function tokenCounts(model) {
844
+ const counts = /* @__PURE__ */ new Map();
845
+ for (const t of model.tokens) counts.set(t.category, (counts.get(t.category) ?? 0) + 1);
846
+ return counts;
847
+ }
848
+ /** Every project of a workspace root, with what was found in each, and the packages without one. */
849
+ async function inspectWorkspace(workspace, values, io) {
850
+ const loaded = [];
851
+ for (const project of workspace.projects) loaded.push({
852
+ project,
853
+ ds: await loadDesignSystem(project.config, {
854
+ cache: values.cache,
855
+ logger: silentLogger
856
+ })
857
+ });
858
+ if (values.format === "json") {
859
+ io.stdout(JSON.stringify({
860
+ root: workspace.root,
861
+ workspace: workspace.source,
862
+ projects: loaded.map(({ project, ds }) => ({
863
+ dir: project.dir,
864
+ ...project.name ? { name: project.name } : {},
865
+ ...project.config.configFile ? { config: relativePath(workspace.root, project.config.configFile) } : {},
866
+ ...project.config.detected ? { detected: project.config.detected } : {},
867
+ model: ds.model
868
+ })),
869
+ others: workspace.others
870
+ }, null, 2));
871
+ return 0;
872
+ }
873
+ const packages = workspace.projects.length + workspace.others.length;
874
+ const lines = [
875
+ `root ${workspace.root}`,
876
+ `workspace ${plural(packages, "package")} (${workspace.source}), ${plural(workspace.projects.length, "project")} with a design system`,
877
+ `config ${workspace.configFile ? displayPath(io.cwd, workspace.configFile) : "(defaults)"}`
878
+ ];
879
+ for (const { project, ds } of loaded) {
880
+ const { model } = ds;
881
+ const roots = ds.roots();
882
+ const tokens = [...tokenCounts(model)].map(([category, n]) => `${category} ${n}`);
883
+ const own = project.config.configFile && project.config.configFile !== workspace.configFile ? displayPath(workspace.root, project.config.configFile) : void 0;
884
+ lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...own ? [` config ${own}`] : [], ...project.config.detected ? [` detected ${project.config.detected}`] : [], ` files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`, ` components ${roots.length} + ${model.components.length - roots.length} parts${roots.length ? `: ${listNames(roots.map((c) => c.name))}` : ""}`, ` tokens ${tokens.length ? tokens.join(", ") : "none"}`, ...model.warnings.length ? [` warnings ${model.warnings.length}: ${model.warnings[0] ?? ""}`] : []);
885
+ }
886
+ if (!loaded.length) lines.push("", `No workspace package has a design system that zero config finds. See ${DOCS}/troubleshooting`);
887
+ if (workspace.others.length) lines.push("", `Without a design system (${workspace.others.length}): ${listNames(workspace.others)}`);
888
+ lines.push("", `Each file is checked by its project's design system. For one project's components, tokens and warnings: ${NAME} inspect --root <folder>`);
889
+ if (values.explain) lines.push("", ...explainWorkspace(workspace, loaded, io.cwd));
890
+ io.stdout(lines.join("\n"));
891
+ return 0;
892
+ }
893
+ /** Why the root is read as a monorepo, then each project's explanation. */
894
+ function explainWorkspace(workspace, loaded, cwd) {
895
+ const { rootConfig } = workspace;
896
+ const steps = [];
897
+ detectProject(rootConfig.root, rootConfig.tsconfig, steps);
898
+ const lines = [
899
+ `Why this is read as a monorepo root: it declares workspace packages (${workspace.source}), no config there sets components, and zero config finds no design system in the root itself:`,
900
+ ...steps.map((s) => ` ${(s.accepted ? "found" : "no").padEnd(6)} ${s.candidate}: ${s.reason}`),
901
+ ` no ${rootConfig.components.join(", ")}: no file matches`
902
+ ];
903
+ for (const { project, ds } of loaded) lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...explainProject(ds, cwd, " "));
904
+ return lines;
905
+ }
906
+ /** `A, B, C` up to a dozen names, then `and 40 more`. */
907
+ function listNames(names, max = 12) {
908
+ const shown = names.slice(0, max).join(", ");
909
+ return names.length > max ? `${shown} and ${names.length - max} more` : shown;
910
+ }
911
+ /**
912
+ * Says once what a file's old name means: on stderr, or as a workflow warning with
913
+ * `--format github` so it shows on the run. Never on stdout otherwise, which JSON output owns.
914
+ */
915
+ function warnDeprecated(messages, format, io) {
916
+ for (const text of messages ?? []) if (format === "github") io.stdout(`::warning title=${NAME}::${text}`);
917
+ else io.stderr(text);
918
+ }
919
+ /**
920
+ * The baseline in the root: `onsystem.baseline.json`, or one still under its name from
921
+ * before the rename, which is read (and updated) where it is, with a notice to rename it.
922
+ */
923
+ function defaultBaseline(root, warn) {
924
+ const current = path.join(root, BASELINE_FILE);
925
+ const legacy = path.join(root, LEGACY_BASELINE_FILE);
926
+ if (!fs.existsSync(legacy)) return current;
927
+ if (fs.existsSync(current)) {
928
+ warn?.(`${LEGACY_BASELINE_FILE} is ignored: ${BASELINE_FILE} is read instead. Delete ${LEGACY_BASELINE_FILE}.`);
929
+ return current;
930
+ }
931
+ warn?.(`${LEGACY_BASELINE_FILE} is the baseline's name from before design-system-mcp became onsystem. Rename it to ${BASELINE_FILE} (git mv ${LEGACY_BASELINE_FILE} ${BASELINE_FILE}); the old name still works for now.`);
932
+ return legacy;
933
+ }
934
+ /** Tests and stories, which `check` leaves out of folders and globs unless asked. */
935
+ const TEST_FILE = /\.(?:test|spec|stories)\.[cm]?[jt]sx?$|[\\/]__(?:tests|stories|mocks)__[\\/]/;
936
+ /** The default patterns `--include-tests` lifts. */
937
+ function isTestPattern(pattern) {
938
+ return pattern === "**/*.{test,spec,stories}.{ts,tsx,js,jsx}" || pattern === "**/__{tests,stories,mocks}__/**";
939
+ }
940
+ /** What a run left out, for the summary line: `5 design-system files skipped`. */
941
+ function scopeNotes(skipped, left) {
942
+ return [
943
+ skipped ? `${plural(skipped, "design-system file")} skipped` : "",
944
+ left.tests ? `${left.tests.toLocaleString("en-US")} ${left.tests === 1 ? "test or story" : "tests and stories"} left out` : "",
945
+ left.excluded ? `${plural(left.excluded, "excluded file")} left out` : "",
946
+ left.gitignored ? `${plural(left.gitignored, "file")} git ignores left out` : ""
947
+ ].filter(Boolean);
948
+ }
949
+ /**
950
+ * A config `exclude` pattern, relative to the root, as a pattern relative to the
951
+ * working directory the globs run from; undefined when it only covers files
952
+ * outside that directory. `**\/` patterns hold anywhere.
953
+ */
954
+ function excludeFromCwd(pattern, root, cwd) {
955
+ if (pattern.startsWith("**/")) return pattern;
956
+ const fromRoot = toPosix(path.relative(root, cwd));
957
+ if (!fromRoot) return pattern;
958
+ if (fromRoot.startsWith("..")) return toPosix(path.join(path.relative(cwd, root), pattern));
959
+ return pattern.startsWith(`${fromRoot}/`) ? pattern.slice(fromRoot.length + 1) : void 0;
960
+ }
961
+ /**
962
+ * Files a glob matches, as Node spells absolute paths (with backslashes on Windows, where
963
+ * tinyglobby gives forward slashes), so they compare equal to `path.resolve` and git's.
964
+ */
965
+ async function findFiles(globs, options) {
966
+ return (await glob(globs, options)).map((file) => path.resolve(file));
967
+ }
968
+ /**
969
+ * The files git ignores, through `git check-ignore`: nested .gitignore files,
970
+ * `.git/info/exclude` and tracked-file exceptions, exactly as git reads them.
971
+ * Outside a repository, or without git, nothing is ignored.
972
+ */
973
+ function gitIgnored(cwd, files) {
974
+ if (!files.length) return /* @__PURE__ */ new Set();
975
+ const result = spawnSync("git", [
976
+ "check-ignore",
977
+ "-z",
978
+ "--stdin"
979
+ ], {
980
+ cwd,
981
+ input: files.join("\0"),
982
+ encoding: "utf8",
983
+ maxBuffer: 268435456,
984
+ windowsHide: true
985
+ });
986
+ if (result.status !== 0 || typeof result.stdout !== "string") return /* @__PURE__ */ new Set();
987
+ return new Set(result.stdout.split("\0").filter(Boolean).map((file) => path.resolve(cwd, file)));
988
+ }
989
+ /** `1,307 findings`. */
990
+ function count(n, word) {
991
+ return `${n.toLocaleString("en-US")} ${n === 1 ? word : `${word}s`}`;
992
+ }
993
+ /** Quotes a pattern for the hint when the shell would expand or split it. */
994
+ function shellQuote(pattern) {
995
+ return /^[\w./@-]+$/.test(pattern) ? pattern : `"${pattern.replace(/(["\\$`])/g, "\\$1")}"`;
996
+ }
997
+ /** The file's real path, so a linked root or a differently spelled path matches. */
998
+ function realPath(file) {
999
+ try {
1000
+ return fs.realpathSync.native(file);
1001
+ } catch {
1002
+ return file;
1003
+ }
1004
+ }
1005
+ /** Relative to cwd when the file is below it, absolute otherwise. */
1006
+ function displayPath(cwd, file) {
1007
+ const rel = path.relative(cwd, file);
1008
+ return rel && !rel.startsWith("..") && !path.isAbsolute(rel) ? rel : file;
1009
+ }
1010
+ //#endregion
1011
+ //#region src/bin.ts
1012
+ main(process.argv.slice(2)).then((code) => {
1013
+ process.exitCode = code;
1014
+ }, (error) => {
1015
+ process.stderr.write(`${error.stack ?? String(error)}\n`);
1016
+ process.exitCode = 1;
1017
+ });
1018
+ //#endregion
1019
+ export {};