onsystem 0.4.0 → 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/README.md CHANGED
@@ -2,19 +2,19 @@
2
2
 
3
3
  **Keeps coding agents on your design system:** it knows your real components, props, variants and tokens, catches the moment an agent invents one and has it fix it, and the same check gates your PRs. Local, zero config, works alongside [@shadcn/lint](https://github.com/shadcn-ui/lint).
4
4
 
5
- Built for design-system and platform teams with a React design-system package; a shadcn/ui app is the zero-config demo. Formerly `@dgesteves/design-system-mcp` ([what changed](https://design-system-mcp-demo.vercel.app/docs/migrating)).
5
+ Built for design-system and platform teams with a React design-system package; a shadcn/ui app is the zero-config demo. Formerly `@dgesteves/design-system-mcp` ([what changed](https://onsystem.vercel.app/docs/migrating)).
6
6
 
7
7
  [![CI](https://img.shields.io/github/actions/workflow/status/dgesteves/onsystem/ci.yml?branch=main&style=flat-square&label=CI&labelColor=0d0f12)](https://github.com/dgesteves/onsystem/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/onsystem?style=flat-square&labelColor=0d0f12&color=22d3ee)](https://www.npmjs.com/package/onsystem)
9
9
  [![License: MIT](https://img.shields.io/github/license/dgesteves/onsystem?style=flat-square&labelColor=0d0f12&color=22d3ee)](LICENSE)
10
10
 
11
- **Website and docs:** [design-system-mcp-demo.vercel.app](https://design-system-mcp-demo.vercel.app), with a [playground](https://design-system-mcp-demo.vercel.app/playground) that checks your own code.
11
+ **Website and docs:** [onsystem.vercel.app](https://onsystem.vercel.app), with a [playground](https://onsystem.vercel.app/playground) that checks your own code.
12
12
 
13
13
  <p align="center">
14
- <a href="https://design-system-mcp-demo.vercel.app"><img src=".github/assets/demo.webp" width="100%" alt="On the website, an agent drafts a settings card, check_ui finds 11 problems with a fix each and the agent applies them. Then a real Claude Code session with the plugin, where Claude looks the components up before writing and check_ui passes. Last, the benchmark: clean components go from 6 to 10 out of 10 with Claude Haiku 4.5 and from 8 to 10 with Claude Opus 5."></a>
14
+ <a href="https://onsystem.vercel.app"><img src=".github/assets/demo.webp" width="100%" alt="On the website, an agent drafts a settings card, check_ui finds 11 problems with a fix each and the agent applies them. Then a real Claude Code session with the plugin, where Claude looks the components up before writing and check_ui passes. Last, the benchmark: clean components go from 6 to 10 out of 10 with Claude Haiku 4.5 and from 8 to 10 with Claude Opus 5."></a>
15
15
  </p>
16
16
 
17
- <sub>On the <a href="https://design-system-mcp-demo.vercel.app">website</a>, an agent drafts a settings card, <code>check_ui</code> finds 11 problems with a fix each, and the agent applies them. Then a real Claude Code session with the plugin: Claude looks the components up before writing, and <code>check_ui</code> passes. Last, the benchmark.</sub>
17
+ <sub>On the <a href="https://onsystem.vercel.app">website</a>, an agent drafts a settings card, <code>check_ui</code> finds 11 problems with a fix each, and the agent applies them. Then a real Claude Code session with the plugin: Claude looks the components up before writing, and <code>check_ui</code> passes. Last, the benchmark.</sub>
18
18
 
19
19
  ## The problem
20
20
 
@@ -35,11 +35,11 @@ onsystem reads your components, `cva` and `tv` variants, parts and tokens from s
35
35
  | `no-hardcoded-spacing` | warn | Arbitrary padding, margin and gap (`p-[13px]` → `p-3`) |
36
36
  | `no-hardcoded-radius` | warn | Arbitrary radius (`rounded-[7px]` → `rounded-sm`) |
37
37
 
38
- **How often it is wrong.** The [corpus](corpus) job runs `check` on 10 public repositories pinned by commit (Documenso, Dub, Cal.com, midday and others, the four monorepos also from their root: 21 runs, 4,731 findings) and compares the findings with hand labels. In a random sample of 126 findings, 5 are false positives (4.0%) and 55 are debatable; weighted by each run's and rule's share of all findings, about 1.0% are false positives. Shipped code rarely invents components, props or variants, so those three rules have 1 finding in the corpus and none in the sample; their precision comes from the test suite. The [rules reference](https://design-system-mcp-demo.vercel.app/rules) shows each rule on an example and how to [suppress a finding](https://design-system-mcp-demo.vercel.app/rules#suppressing-findings).
38
+ **How often it is wrong.** The [corpus](corpus) job runs `check` on 10 public repositories pinned by commit (Documenso, Dub, Cal.com, midday and others, the four monorepos also from their root: 21 runs, 4,731 findings) and compares the findings with hand labels. In a random sample of 126 findings, 5 are false positives (4.0%) and 55 are debatable; weighted by each run's and rule's share of all findings, about 1.0% are false positives. Shipped code rarely invents components, props or variants, so those three rules have 1 finding in the corpus and none in the sample; their precision comes from the test suite. The [rules reference](https://onsystem.vercel.app/rules) shows each rule on an example and how to [suppress a finding](https://onsystem.vercel.app/rules#suppressing-findings).
39
39
 
40
40
  ## Quickstart
41
41
 
42
- Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later. `npx onsystem inspect` shows what it found, and `--explain` why; at a [monorepo root](https://design-system-mcp-demo.vercel.app/docs/configuration#monorepo-roots), every app and design-system package, each file checked by its own project's design system. `npx onsystem init` writes the config from it and offers a baseline.
42
+ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later. `npx onsystem inspect` shows what it found, and `--explain` why; at a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), every app and design-system package, each file checked by its own project's design system. `npx onsystem init` writes the config from it and offers a baseline.
43
43
 
44
44
  **Claude Code.** The plugin brings the hook, the MCP server and a skill:
45
45
 
@@ -48,11 +48,13 @@ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or la
48
48
  /plugin install onsystem@dgesteves
49
49
  ```
50
50
 
51
- After each Write or Edit of a `.tsx` or `.jsx` file, the hook runs `onsystem check` on it. Errors go back to Claude as a blocking error with the fix, so Claude corrects the file before it moves on; warnings go as a note. It stays quiet in projects without a design system. [What it runs and touches](https://design-system-mcp-demo.vercel.app/docs/plugin).
51
+ After each Write or Edit of a `.tsx` or `.jsx` file, the hook runs `onsystem check` on it. Errors go back to Claude as a blocking error with the fix, so Claude corrects the file before it moves on; warnings go as a note. It stays quiet in projects without a design system. [What it runs and touches](https://onsystem.vercel.app/docs/plugin).
52
52
 
53
- **Cursor or VS Code.** [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_server-0d0f12?style=flat-square&logo=cursor&logoColor=white)](https://cursor.com/en/install-mcp?name=onsystem&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm9uc3lzdGVtIiwiLS1yb290IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_server-0098FF?style=flat-square)](https://insiders.vscode.dev/redirect/mcp/install?name=onsystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22onsystem%22%5D%7D), or commit the [config](https://design-system-mcp-demo.vercel.app/docs/clients#cursor-and-vs-code). The agent gets the tools and runs `check_ui` on what it writes.
53
+ **Cursor or VS Code.** [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_server-0d0f12?style=flat-square&logo=cursor&logoColor=white)](https://cursor.com/en/install-mcp?name=onsystem&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm9uc3lzdGVtIiwiLS1yb290IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_server-0098FF?style=flat-square)](https://insiders.vscode.dev/redirect/mcp/install?name=onsystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22onsystem%22%5D%7D), or commit the [config](https://onsystem.vercel.app/docs/clients#cursor-and-vs-code). The agent gets the tools and runs `check_ui` on what it writes.
54
54
 
55
- **Any MCP client.** Run `npx -y onsystem` as a stdio server. There are [configs for Claude Desktop, Codex CLI, GitHub Copilot CLI, Windsurf, JetBrains IDEs, Zed, Gemini CLI and Grok Build](https://design-system-mcp-demo.vercel.app/docs/clients#other-clients).
55
+ **Other agents.** `npx skills add dgesteves/onsystem` adds the skill to any agent with Agent Skills, and there are [plugins for Codex, GitHub Copilot CLI, VS Code and Cursor](https://onsystem.vercel.app/docs/clients#plugins-for-other-agents).
56
+
57
+ **Any MCP client.** Run `npx -y onsystem` as a stdio server. There are [configs for Claude Desktop, Codex CLI, GitHub Copilot CLI, Windsurf, JetBrains IDEs, Zed, Gemini CLI and Grok Build](https://onsystem.vercel.app/docs/clients#other-clients).
56
58
 
57
59
  **CI.** Install it as a dev dependency, accept the findings the codebase already has once, and fail pull requests only on new ones:
58
60
 
@@ -65,7 +67,9 @@ npx onsystem check . --update-baseline # writes onsystem.baseline.json: commit
65
67
  - run: npx onsystem check . --format github --require-design-system
66
68
  ```
67
69
 
68
- Findings show up as annotations on the pull request, and `--require-design-system` fails the job when the design system is no longer found. [CI and baselines](https://design-system-mcp-demo.vercel.app/docs/ci) has the whole workflow.
70
+ Findings show up as annotations on the pull request, and `--require-design-system` fails the job when the design system is no longer found. [CI and baselines](https://onsystem.vercel.app/docs/ci) has the whole workflow. The GitHub Action, `uses: dgesteves/onsystem@v0`, annotates only the lines a pull request changed: [make it a required check on agent pull requests](https://onsystem.vercel.app/docs/required-check).
71
+
72
+ **ESLint.** The same rules for ESLint 9 and 10: add `onsystem.configs.recommended` from `onsystem/eslint` to `eslint.config.mjs` ([ESLint plugin](https://onsystem.vercel.app/docs/eslint), with @shadcn/lint).
69
73
 
70
74
  ## Works with @shadcn/lint
71
75
 
@@ -84,11 +88,11 @@ It is small: one repository, Claude models only, 40 runs (one per task, model an
84
88
 
85
89
  ## Docs
86
90
 
87
- On the [website](https://design-system-mcp-demo.vercel.app/docs), and as Markdown in [docs/](docs):
91
+ On the [website](https://onsystem.vercel.app/docs), and as Markdown in [docs/](docs):
88
92
 
89
- - [Quickstart](https://design-system-mcp-demo.vercel.app/docs), [Claude Code plugin](https://design-system-mcp-demo.vercel.app/docs/plugin), [CI and baselines](https://design-system-mcp-demo.vercel.app/docs/ci) and [Set up your agent](https://design-system-mcp-demo.vercel.app/docs/clients), with the config for each client
90
- - [Configuration](https://design-system-mcp-demo.vercel.app/docs/configuration), [Rules](https://design-system-mcp-demo.vercel.app/rules), [MCP tools](https://design-system-mcp-demo.vercel.app/docs/tools) and [Troubleshooting](https://design-system-mcp-demo.vercel.app/docs/troubleshooting)
91
- - [How it works](https://design-system-mcp-demo.vercel.app/docs/how-it-works), [FAQ](https://design-system-mcp-demo.vercel.app/docs/faq) and [Migrating from design-system-mcp](https://design-system-mcp-demo.vercel.app/docs/migrating)
93
+ - [Quickstart](https://onsystem.vercel.app/docs), [Claude Code plugin](https://onsystem.vercel.app/docs/plugin), [CI and baselines](https://onsystem.vercel.app/docs/ci) and [Set up your agent](https://onsystem.vercel.app/docs/clients), with the config for each client
94
+ - [Configuration](https://onsystem.vercel.app/docs/configuration), [Rules](https://onsystem.vercel.app/rules), [MCP tools](https://onsystem.vercel.app/docs/tools) and [Troubleshooting](https://onsystem.vercel.app/docs/troubleshooting)
95
+ - [How it works](https://onsystem.vercel.app/docs/how-it-works), [FAQ](https://onsystem.vercel.app/docs/faq) and [Migrating from design-system-mcp](https://onsystem.vercel.app/docs/migrating)
92
96
 
93
97
  [Contributing](CONTRIBUTING.md), [security](SECURITY.md) and the [changelog](CHANGELOG.md) are in the repository.
94
98
 
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { A as CONFIG_FILES, G as unique, I as LEGACY_CONFIG_FILES, J as toPosix, K as matchesGlob, L as RULE_IDS, M as DEFAULT_DOCS, N as DEFAULT_SEVERITY, U as detectProject, V as rulesFor, W as plural, b as uncheckedNotice, c as componentFiles, d as tokenFiles, f as NAME, h as stderrLogger, j as ConfigError, m as silentLogger, n as loadTarget, p as VERSION, q as relativePath, t as serveStdio, u as loadDesignSystem, x as formatDiagnostics } from "./stdio-BcJth14k.js";
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";
3
5
  import fs from "node:fs";
4
6
  import path from "node:path";
5
7
  import { escapePath, glob } from "tinyglobby";
@@ -7,148 +9,6 @@ import fs$1 from "node:fs/promises";
7
9
  import { spawnSync } from "node:child_process";
8
10
  import readline from "node:readline/promises";
9
11
  import { parseArgs } from "node:util";
10
- //#region src/baseline.ts
11
- const BASELINE_FILE = "onsystem.baseline.json";
12
- /** The baseline's name from before the rename to onsystem: still read when the new one is absent. */
13
- const LEGACY_BASELINE_FILE = "design-system-mcp.baseline.json";
14
- /**
15
- * A finding's identity within its file and rule: the offending text with
16
- * whitespace collapsed, and without the quotes of a string literal, so a
17
- * formatter switching quote style does not make every finding look new.
18
- */
19
- function key(diagnostic) {
20
- const text = diagnostic.source.replace(/\s+/g, " ").trim();
21
- return (/^(['"`])(.*)\1$/.exec(text)?.[2] ?? text).slice(0, 200);
22
- }
23
- /** The baseline's entries, or undefined when the file does not exist. */
24
- function readBaseline(file) {
25
- let text;
26
- try {
27
- text = fs.readFileSync(file, "utf8");
28
- } catch (error) {
29
- if (error.code === "ENOENT") return void 0;
30
- throw new ConfigError(`Could not read baseline ${file}: ${error.message}`);
31
- }
32
- const invalid = (why) => new ConfigError(`${file} is not a valid onsystem baseline (${why}). Fix it, or delete it and run check --update-baseline.`);
33
- let parsed;
34
- try {
35
- parsed = JSON.parse(text);
36
- } catch (error) {
37
- throw invalid(error.message);
38
- }
39
- if (!isObject(parsed) || parsed.version !== 1) throw invalid("expected \"version\": 1");
40
- if (!isObject(parsed.files)) throw invalid("\"files\" must be an object");
41
- const entries = /* @__PURE__ */ new Map();
42
- for (const [name, rules] of Object.entries(parsed.files)) {
43
- if (!isObject(rules)) throw invalid(`"${name}" must map rules to findings`);
44
- const byRule = /* @__PURE__ */ new Map();
45
- for (const [rule, sources] of Object.entries(rules)) {
46
- if (!isObject(sources)) throw invalid(`"${name}" › "${rule}" must map findings to counts`);
47
- const counts = /* @__PURE__ */ new Map();
48
- for (const [source, count] of Object.entries(sources)) {
49
- if (!Number.isInteger(count) || count < 1) throw invalid(`"${name}" › "${rule}" › "${source}" must be a positive whole number`);
50
- counts.set(source, count);
51
- }
52
- byRule.set(rule, counts);
53
- }
54
- entries.set(name, byRule);
55
- }
56
- return entries;
57
- }
58
- /**
59
- * Removes the findings the baseline accepts for `file`. Within a file and
60
- * rule, each entry accepts that many findings with the same source text, in
61
- * order; any beyond the count are new. Entries of `disabled` rules are not
62
- * counted as fixed: the rule is off, not satisfied.
63
- */
64
- function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set()) {
65
- const remaining = /* @__PURE__ */ new Map();
66
- for (const [rule, sources] of baseline.get(file) ?? []) {
67
- if (disabled.has(rule)) continue;
68
- for (const [source, count] of sources) remaining.set(`${rule}\0${source}`, count);
69
- }
70
- const diagnostics = [];
71
- let baselined = 0;
72
- for (const diagnostic of result.diagnostics) {
73
- const id = `${diagnostic.ruleId}\0${key(diagnostic)}`;
74
- const left = remaining.get(id) ?? 0;
75
- if (left > 0) {
76
- remaining.set(id, left - 1);
77
- baselined++;
78
- } else diagnostics.push(diagnostic);
79
- }
80
- let fixed = 0;
81
- for (const count of remaining.values()) fixed += count;
82
- return {
83
- result: {
84
- ...result,
85
- diagnostics,
86
- errorCount: diagnostics.filter((d) => d.severity === "error").length,
87
- warningCount: diagnostics.filter((d) => d.severity === "warning").length
88
- },
89
- baselined,
90
- fixed
91
- };
92
- }
93
- /**
94
- * The baseline after a run over `checked` (file → its findings): those files
95
- * get exactly their current findings, plus their previous entries for
96
- * `disabled` rules; other files keep their entries while they still exist
97
- * under `root`.
98
- */
99
- function updateBaseline(previous, checked, root, disabled = /* @__PURE__ */ new Set()) {
100
- const disabledIn = typeof disabled === "function" ? disabled : () => disabled;
101
- const next = /* @__PURE__ */ new Map();
102
- for (const [file, rules] of previous ?? []) if (!checked.has(file) && fs.existsSync(path.resolve(root, file))) next.set(file, rules);
103
- for (const [file, diagnostics] of checked) {
104
- const rules = /* @__PURE__ */ new Map();
105
- const off = disabledIn(file);
106
- for (const [rule, sources] of previous?.get(file) ?? []) if (off.has(rule)) rules.set(rule, sources);
107
- for (const diagnostic of diagnostics) {
108
- let sources = rules.get(diagnostic.ruleId);
109
- if (!sources) rules.set(diagnostic.ruleId, sources = /* @__PURE__ */ new Map());
110
- const id = key(diagnostic);
111
- sources.set(id, (sources.get(id) ?? 0) + 1);
112
- }
113
- if (rules.size) next.set(file, rules);
114
- }
115
- return next;
116
- }
117
- function countBaseline(entries) {
118
- let findings = 0;
119
- for (const rules of entries.values()) for (const sources of rules.values()) for (const count of sources.values()) findings += count;
120
- return {
121
- findings,
122
- files: entries.size
123
- };
124
- }
125
- /** Writes the baseline sorted at every level, so it diffs cleanly in review. */
126
- function writeBaseline(file, entries) {
127
- const sorted = (map) => [...map].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
128
- const files = {};
129
- for (const [name, rules] of sorted(entries)) {
130
- const byRule = {};
131
- for (const [rule, sources] of sorted(rules)) byRule[rule] = Object.fromEntries(sorted(sources));
132
- Object.defineProperty(files, name, {
133
- value: byRule,
134
- enumerable: true,
135
- writable: true
136
- });
137
- }
138
- const baseline = {
139
- version: 1,
140
- files
141
- };
142
- try {
143
- fs.writeFileSync(file, `${JSON.stringify(baseline, null, 2)}\n`);
144
- } catch (error) {
145
- throw new ConfigError(`Could not write baseline ${file}: ${error.message}`);
146
- }
147
- }
148
- function isObject(value) {
149
- return typeof value === "object" && value !== null && !Array.isArray(value);
150
- }
151
- //#endregion
152
12
  //#region src/explain.ts
153
13
  /**
154
14
  * What `inspect --explain` says about one project: every candidate zero config looked
@@ -362,7 +222,7 @@ function count$1(n, word) {
362
222
  }
363
223
  //#endregion
364
224
  //#region src/cli.ts
365
- const DOCS = "https://design-system-mcp-demo.vercel.app/docs";
225
+ const DOCS = "https://onsystem.vercel.app/docs";
366
226
  const HELP = `${NAME} ${VERSION}
367
227
 
368
228
  Keeps coding agents on your React design system: checks UI code against your
@@ -392,7 +252,7 @@ Options
392
252
  --docs <glob> Component docs (Markdown/MDX), repeatable
393
253
  --no-cache Ignore the on-disk extraction cache
394
254
  --no-watch serve: do not reload when files change
395
- --format <format> check: pretty | json | github (default: pretty)
255
+ --format <format> check: pretty | json | github | sarif (default: pretty)
396
256
  --max-warnings <n> check: exit 1 when there are more than n warnings
397
257
  --update-baseline check: record the current findings as accepted, and exit 0
398
258
  --baseline <file> check: baseline file (default: ${BASELINE_FILE} in the root,
@@ -614,9 +474,10 @@ async function check(patterns, configOptions, values, io) {
614
474
  if (![
615
475
  "pretty",
616
476
  "json",
617
- "github"
477
+ "github",
478
+ "sarif"
618
479
  ].includes(format)) {
619
- io.stderr(`check: unknown --format "${values.format}" (pretty, json, github)`);
480
+ io.stderr(`check: unknown --format "${values.format}" (pretty, json, github, sarif)`);
620
481
  return 2;
621
482
  }
622
483
  const maxWarnings = values["max-warnings"] === void 0 ? void 0 : Number(values["max-warnings"]);
@@ -630,11 +491,10 @@ async function check(patterns, configOptions, values, io) {
630
491
  if (typeof selection === "number") return selection;
631
492
  const { files, notes } = selection;
632
493
  if (!files.length) {
633
- if (format === "json") io.stdout("[]");
634
- else if (format === "pretty") {
494
+ if (format === "pretty") {
635
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);
636
496
  io.stdout(`Nothing to check: ${notes.join("; ")}. ${hints.join(" ")}`.trimEnd());
637
- }
497
+ } else printNoResults(format, io);
638
498
  return 0;
639
499
  }
640
500
  const update = values["update-baseline"];
@@ -693,6 +553,7 @@ async function check(patterns, configOptions, values, io) {
693
553
  }
694
554
  const output = formatDiagnostics(format === "json" ? results : results.filter((r) => !r.skipped), format, {
695
555
  color: io.color,
556
+ uri: format === "sarif" || format === "github" ? repositoryPath(io.cwd, io.env ?? process.env) : void 0,
696
557
  baselined: baseline ? baselined : void 0,
697
558
  notes,
698
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
@@ -713,6 +574,35 @@ async function check(patterns, configOptions, values, io) {
713
574
  }
714
575
  return 0;
715
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
+ }
716
606
  /** Splits the patterns into files named outright and globs (a folder: every TSX/JSX file under it). */
717
607
  function expandPatterns(patterns, cwd) {
718
608
  const named = [];
@@ -766,7 +656,7 @@ async function selectInProject(config, patterns, values, format, io) {
766
656
  return 2;
767
657
  }
768
658
  if (values["quiet-without-design-system"] && !config.configFile && !ds.components.length) {
769
- if (format === "json") io.stdout("[]");
659
+ printNoResults(format, io);
770
660
  return 0;
771
661
  }
772
662
  const notice = ds.notice();
@@ -851,7 +741,7 @@ async function selectInWorkspace(workspace, patterns, values, format, io) {
851
741
  if (values["quiet-without-design-system"]) {
852
742
  files = files.filter((file) => Boolean(project(file).config.configFile) || designSystemOf(file).components.length > 0);
853
743
  if (!files.length) {
854
- if (format === "json") io.stdout("[]");
744
+ printNoResults(format, io);
855
745
  return 0;
856
746
  }
857
747
  }