onsystem 0.5.0 → 0.6.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
@@ -6,8 +6,15 @@ Built for design-system and platform teams with a React design-system package; a
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
+ [![Downloads](https://img.shields.io/npm/dw/onsystem?style=flat-square&labelColor=0d0f12&color=22d3ee)](https://www.npmjs.com/package/onsystem)
10
+ [![MCP Registry](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.modelcontextprotocol.io%2Fv0%2Fservers%3Fsearch%3Dio.github.dgesteves%2Fonsystem%26version%3Dlatest&query=%24.servers%5B0%5D.server.version&prefix=v&label=MCP%20Registry&style=flat-square&labelColor=0d0f12&color=22d3ee&logo=modelcontextprotocol&logoColor=white)](https://registry.modelcontextprotocol.io/?q=io.github.dgesteves%2Fonsystem)
11
+ [![Cursor Directory](https://img.shields.io/badge/Cursor_Directory-Listed-0d0f12?style=flat-square&logo=cursor&logoColor=white)](https://cursor.directory/plugins/design-system-mcp)
9
12
  [![License: MIT](https://img.shields.io/github/license/dgesteves/onsystem?style=flat-square&labelColor=0d0f12&color=22d3ee)](LICENSE)
10
13
 
14
+ [![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)
15
+ [![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%2C%22--root%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D)
16
+ [![Claude Code plugin](https://img.shields.io/badge/Claude_Code-Install_plugin-d97757?style=flat-square&logo=claude&logoColor=white)](https://onsystem.vercel.app/docs/plugin)
17
+
11
18
  **Website and docs:** [onsystem.vercel.app](https://onsystem.vercel.app), with a [playground](https://onsystem.vercel.app/playground) that checks your own code.
12
19
 
13
20
  <p align="center">
@@ -22,7 +29,7 @@ Coding agents write UI from their training data, not from your design system. As
22
29
 
23
30
  ## What it checks
24
31
 
25
- onsystem reads your components, `cva` and `tv` variants, parts and tokens from source with the TypeScript compiler, and checks `.tsx` and `.jsx` against them. Every finding has a rule id, a location and the fix.
32
+ onsystem reads your components, `cva` and `tv` variants, parts and tokens from source with the TypeScript compiler, and checks `.tsx` and `.jsx` against them. Every finding has a rule id, a location and the fix. Tokens come from your stylesheets, [DTCG 2025.10 files or a DESIGN.md](https://onsystem.vercel.app/docs/tokens), and `onsystem export` writes them out as either.
26
33
 
27
34
  | Rule | Default | Catches |
28
35
  | -------------------------------- | ------- | ------------------------------------------------------------------------------- |
@@ -35,11 +42,21 @@ onsystem reads your components, `cva` and `tv` variants, parts and tokens from s
35
42
  | `no-hardcoded-spacing` | warn | Arbitrary padding, margin and gap (`p-[13px]` → `p-3`) |
36
43
  | `no-hardcoded-radius` | warn | Arbitrary radius (`rounded-[7px]` → `rounded-sm`) |
37
44
 
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).
45
+ **How often it is wrong.** The [corpus](corpus) job runs `check` on 12 public repositories pinned by commit (Documenso, Dub, Cal.com, midday, Flowise and others, the four monorepos also from their root, and Dub's app also with a config and with `@dub/ui` set up by `init`: 24 runs, 4,076 distinct findings) and compares the findings with hand labels. In a random sample of 149 findings, 88 are clearly right (59.1%), 56 are debatable and 5 are false positives (3.4%); 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: those three rules have 56 findings in the corpus, 55 of them in Flowise, an MUI app that passes props and values MUI's components do not take, and the 10 in the sample are all right. 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
46
 
40
47
  ## Quickstart
41
48
 
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.
49
+ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later:
50
+
51
+ ```sh
52
+ npx -y onsystem inspect # what it found; --explain says why
53
+ npx -y onsystem check . # the same check the agent and CI run
54
+ npx -y onsystem init # write the config, and offer a baseline
55
+ ```
56
+
57
+ At a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), `inspect` lists every app and design-system package, and `check` checks each file by its own project's design system. In a [design-system package](https://onsystem.vercel.app/docs/design-system-packages), `init` sets it up for every app that depends on it, workspace or npm, with no config in the apps.
58
+
59
+ In a new create-next-app, the template page's hardcoded colors already make the first `check .` fail; `init` offers to record them as a baseline, so only new findings fail.
43
60
 
44
61
  **Claude Code.** The plugin brings the hook, the MCP server and a skill:
45
62
 
@@ -50,7 +67,7 @@ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or la
50
67
 
51
68
  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
69
 
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.
70
+ **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%2C%22--root%22%2C%22%24%7BworkspaceFolder%7D%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
71
 
55
72
  **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
73
 
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
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";
2
+ import { A as toDtcg, B as DEFAULT_DOCS, C as updateBaseline, G as RULE_IDS, R as CONFIG_FILES, S as readBaseline, V as DEFAULT_SEVERITY, W as LEGACY_CONFIG_FILES, Z as rulesFor, _ as VERSION, _t as relativePath, at as detectProject, b as applyBaseline, c as silentLogger, et as LIBRARY_IDS, ft as plural, g as NAME, gt as matchesGlob, h as formatDiagnostics, ht as isInside, i as componentFiles, it as declaredConfig, k as toDesignMd, l as stderrLogger, m as uncheckedNotice, mt as unique, nt as PACKAGE_FIELD, o as loadDesignSystem, ot as findDesignSystemPackage, s as tokenFiles, st as isDesignSystemName, tt as libraryAdapter, v as BASELINE_FILE, vt as toPosix, w as writeBaseline, x as countBaseline, y as LEGACY_BASELINE_FILE, z as ConfigError } from "./design-system-A3pRX3Ka.js";
3
+ import { t as serveStdio } from "./stdio-CEChNFLR.js";
4
+ import { r as loadTarget } from "./workspace-CHvUwgz9.js";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { escapePath, glob } from "tinyglobby";
8
- import fs$1 from "node:fs/promises";
8
+ import fsp from "node:fs/promises";
9
9
  import { spawnSync } from "node:child_process";
10
10
  import readline from "node:readline/promises";
11
11
  import { parseArgs } from "node:util";
@@ -20,24 +20,32 @@ function explainProject(ds, cwd, indent = "") {
20
20
  const lines = [];
21
21
  const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
22
22
  out("How the design system was found");
23
+ for (const step of packageSteps(config, cwd)) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
23
24
  if (config.configured) out(` components come from ${config.configFile ? display(cwd, config.configFile) : "--components"}, so zero config did not look`);
24
25
  else {
25
26
  const steps = [];
26
- detectProject(config.root, config.tsconfig, steps);
27
+ detectProject(config.root, config.tsconfig, steps, config.designSystemPackage);
27
28
  for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
28
29
  if (!config.detected) out(` So the defaults apply: ${config.components.join(", ")}${model.stats.files.components ? "" : ", which match nothing"}.`);
29
30
  }
31
+ const named = (library) => `${library.package}${library.version ? ` ${library.version}` : ""}`;
32
+ for (const library of config.libraries ?? []) {
33
+ const count = model.components.filter((c) => c.library === library.package).length;
34
+ out(library.dir ? ` ${"found".padEnd(6)} ${named(library)}: a dependency, ${count} components from its type declarations in ${display(cwd, library.dir)}` : ` ${"no".padEnd(6)} ${named(library)}: a dependency, but not installed`);
35
+ }
36
+ for (const library of config.librariesOff ?? []) out(` ${"no".padEnd(6)} ${named(library)}: a dependency, ${librariesLeftOut(config, cwd)}`);
30
37
  out();
31
38
  out("Resolved config");
32
39
  const field = (name, value) => out(` ${name.padEnd(12)} ${value}`);
33
40
  field("root", config.root);
34
- field("config", config.configFile ? display(cwd, config.configFile) : "(none)");
41
+ field("config", config.configFile ? `${display(cwd, config.configFile)}${config.designSystemPackage ? ` (declared by ${config.designSystemPackage.name})` : ""}` : "(none)");
35
42
  field("components", listOf$1(config.components) || "(none)");
36
43
  field("exclude", listOf$1(config.exclude));
37
- field("tokens", listOf$1(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}`)));
44
+ field("tokens", listOf$1(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}${t.fallback ? " (when nothing else is found)" : ""}`)));
38
45
  field("docs", listOf$1(config.docs));
39
46
  field("tsconfig", config.tsconfig ?? (fs.existsSync(path.join(config.root, "tsconfig.json")) ? "tsconfig.json" : "(none)"));
40
47
  field("importPath", importPaths(ds));
48
+ field("libraries", config.libraryIds === void 0 ? `every library onsystem reads (${LIBRARY_IDS.join(", ")})` : config.libraryIds !== false && config.libraryIds.length ? config.libraryIds.join(", ") : `none ("libraries": ${JSON.stringify(config.libraryIds)})`);
41
49
  if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
42
50
  if (config.designSystems) field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source} (${d.imports} imports)`).join("; "));
43
51
  const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
@@ -48,7 +56,12 @@ function explainProject(ds, cwd, indent = "") {
48
56
  out("Tokens");
49
57
  const found = ds.tokens;
50
58
  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(", ")}`}`);
59
+ const themed = found.filter((t) => t.origin === "library");
60
+ out(` ${found.length - themed.length} tokens from ${files} ${files === 1 ? "file" : "files"}${files ? "" : `: no file matches ${config.tokens.map((t) => t.path).join(", ")}`}`);
61
+ if (themed.length) {
62
+ const sources = [...new Set(themed.map((t) => t.source.file))];
63
+ out(` ${themed.length} tokens from a library's theme in ${sources.join(", ")}`);
64
+ }
52
65
  const defined = new Set(found.map((t) => t.cssVar).filter((v) => Boolean(v)));
53
66
  const missing = (value) => [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1] ?? "").filter((v) => !defined.has(v));
54
67
  const unresolved = found.filter((t) => missing(t.value).length);
@@ -69,6 +82,10 @@ function explainProject(ds, cwd, indent = "") {
69
82
  }
70
83
  return lines;
71
84
  }
85
+ /** Why a library the project depends on is not read: `left out by "libraries" in onsystem.config.json`. */
86
+ function librariesLeftOut(config, cwd) {
87
+ return `left out by "libraries" in ${config.configFile ? display(cwd, config.configFile) : "the config"}`;
88
+ }
72
89
  /** The first few, then how many more: a package's exports can name hundreds of files. */
73
90
  function listOf$1(items, max = 8) {
74
91
  const shown = items.slice(0, max).join(", ");
@@ -95,15 +112,46 @@ function display(cwd, file) {
95
112
  const rel = relativePath(cwd, file);
96
113
  return rel && !rel.startsWith("..") ? rel : file;
97
114
  }
115
+ /**
116
+ * Whether a dependency's declared config was taken on, for a project without a config of its
117
+ * own: the package it came from and the others that declare one, or that none does.
118
+ */
119
+ function packageSteps(config, cwd) {
120
+ const declared = config.designSystemPackage;
121
+ if (config.configFile && !declared) return [];
122
+ const lookup = findDesignSystemPackage(config.root);
123
+ if (!lookup) return [];
124
+ const candidate = "a design-system package's config";
125
+ const skipped = lookup.skipped.length ? `; ${lookup.skipped.map((p) => p.name).join(", ")} ${lookup.skipped.length === 1 ? "declares one" : "declare one"} that cannot be read, skipped since installed` : "";
126
+ if (!declared) return [{
127
+ candidate,
128
+ accepted: false,
129
+ reason: `no dependency declares one ("onsystem" in its package.json) among ${lookup.dependencies} ${lookup.dependencies === 1 ? "dependency" : "dependencies"}${skipped}`
130
+ }];
131
+ return [{
132
+ candidate,
133
+ accepted: true,
134
+ reason: `${declared.name} (${declared.installed ? "installed" : "a workspace package"}) declares ${display(cwd, declared.config)}, which this project takes on, with its paths from ${display(cwd, declared.dir)}${lookup.others.length ? `; ${lookup.others.join(", ")} also declare one, not read: one config is taken on, from the package the code imports most` : ""}${skipped}`
135
+ }];
136
+ }
98
137
  //#endregion
99
138
  //#region src/init.ts
100
139
  const SCHEMA = "https://unpkg.com/onsystem/schema.json";
101
140
  /** More detected globs than this are files, not folders: zero config keeps them. */
102
141
  const MAX_GLOBS = 6;
142
+ /** What a design-system package's apps are told about it, next to its config. */
143
+ const GUIDE_FILE = "ONSYSTEM.md";
103
144
  /**
104
145
  * Writes `onsystem.config.json` in the root from what zero config finds there, or from
105
146
  * the answers to up to three questions when it finds nothing, then checks the project
106
147
  * once without failing and offers to record what it found as the baseline.
148
+ *
149
+ * In a design-system package (one zero config reads through its own `exports`, or named like
150
+ * one, as apps look for it), it also declares the config in package.json
151
+ * (`"onsystem": "./onsystem.config.json"`, and in `files` when the package lists them), so
152
+ * every app that depends on the package reads it, and writes ONSYSTEM.md, which tells those
153
+ * apps' teams how to add the agent tools and the CI check. In an app that takes on such a
154
+ * package's config, it writes no config of its own unless --force says to.
107
155
  */
108
156
  async function init(configOptions, options, io, runCheck) {
109
157
  const { config, workspace } = await loadTarget({
@@ -111,16 +159,23 @@ async function init(configOptions, options, io, runCheck) {
111
159
  config: void 0
112
160
  });
113
161
  const root = config.root;
114
- const existing = [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].map((name) => path.join(root, name)).find((file) => fs.existsSync(file));
162
+ const declared = declaredConfig(root);
163
+ const existing = [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].map((name) => path.join(root, name)).find((file) => fs.existsSync(file)) ?? (declared && "file" in declared ? declared.file : void 0);
164
+ const manifest = readManifest(root);
165
+ const name = typeof manifest?.json.name === "string" ? manifest.json.name : void 0;
166
+ const designSystem = !workspace && !config.designSystemPackage && name !== void 0 && (config.detected?.startsWith("package.json exports of") === true || isDesignSystemName(name));
115
167
  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.`);
168
+ const share = designSystem && !declared && existing.endsWith(".json") ? ` To share it with the apps that depend on ${name}, add "${PACKAGE_FIELD}": "./${path.basename(existing)}" to package.json, and to "files" when it lists them (https://onsystem.vercel.app/docs/design-system-packages).` : "";
169
+ 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.${share}`);
117
170
  return 2;
118
171
  }
119
172
  const ask = options.yes ? void 0 : io.prompt;
120
173
  const file = { $schema: SCHEMA };
121
174
  const notes = [];
122
175
  const relative = (abs) => relativePath(root, abs);
123
- if (configOptions.components?.length) {
176
+ const inherited = options.force ? void 0 : config.designSystemPackage;
177
+ if (inherited) notes.push(`This project takes on the config ${inherited.name} declares in its package.json (${relativePath(io.cwd, inherited.config) || inherited.config}), so it needs none of its own; nothing was written. Pass --force to write one, which is read instead.`);
178
+ else if (configOptions.components?.length) {
124
179
  file.components = configOptions.components;
125
180
  if (configOptions.tokens?.length) file.tokens = configOptions.tokens;
126
181
  if (configOptions.docs?.length) file.docs = configOptions.docs;
@@ -138,6 +193,16 @@ async function init(configOptions, options, io, runCheck) {
138
193
  if (docs.length) file.docs = [...DEFAULT_DOCS, ...docs];
139
194
  }
140
195
  if (tokens.length) file.tokens = tokens;
196
+ } else if ((await componentFiles(config)).length) {
197
+ const matching = [];
198
+ for (const glob of config.components) if ((await componentFiles({
199
+ ...config,
200
+ components: [glob]
201
+ })).length) matching.push(glob);
202
+ notes.push(`Found the design system in the default folders: ${matching.join(", ")}.`);
203
+ file.components = matching;
204
+ const tokens = (await tokenFiles(config)).map(relative);
205
+ if (tokens.length) file.tokens = tokens;
141
206
  } else {
142
207
  if (!ask) {
143
208
  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.`);
@@ -167,10 +232,19 @@ async function init(configOptions, options, io, runCheck) {
167
232
  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
233
  if (importPath) file.importPath = importPath;
169
234
  }
170
- const out = path.join(root, "onsystem.config.json");
171
- fs.writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
235
+ const written = [];
236
+ if (!inherited) {
237
+ const out = path.join(root, CONFIG_FILE);
238
+ fs.writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
239
+ written.push(CONFIG_FILE);
240
+ }
241
+ if (designSystem && manifest && name) {
242
+ written.push(...declarePackage(root, manifest, name, options.force, notes));
243
+ const shipped = await unshipped(config, manifest.json);
244
+ if (shipped) notes.push(shipped);
245
+ }
172
246
  for (const note of notes) io.stdout(note);
173
- io.stdout(`Wrote ${relativePath(io.cwd, out) || out}.`);
247
+ if (written.length) io.stdout(`Wrote ${written.map((f) => relativePath(io.cwd, path.join(root, f)) || f).join(", ")}.`);
174
248
  const checkIo = {
175
249
  ...io,
176
250
  cwd: root
@@ -202,9 +276,11 @@ async function init(configOptions, options, io, runCheck) {
202
276
  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
277
  for (const line of dry.stderr.split("\n").filter(Boolean)) io.stdout(line);
204
278
  const hasBaseline = [BASELINE_FILE, LEGACY_BASELINE_FILE].some((name) => fs.existsSync(path.join(root, name)));
279
+ let baselineWritten = false;
205
280
  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")) {
281
+ 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() : void 0;
282
+ if (answer === void 0) io.stdout(`No terminal to ask in, so the baseline question was skipped and no baseline was written; \`${NAME} check . --update-baseline\` records one.`);
283
+ else if (answer === "" || answer.startsWith("y")) {
208
284
  const recorded = await runCheck([
209
285
  "check",
210
286
  ".",
@@ -212,11 +288,128 @@ async function init(configOptions, options, io, runCheck) {
212
288
  ...options.cache ? [] : ["--no-cache"]
213
289
  ], checkIo);
214
290
  if (recorded.stdout) io.stdout(recorded.stdout);
291
+ baselineWritten = fs.existsSync(path.join(root, BASELINE_FILE));
215
292
  } else io.stdout(`No baseline written; \`${NAME} check . --update-baseline\` records one later.`);
216
293
  }
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.`);
294
+ const commit = [...written, ...baselineWritten ? [BASELINE_FILE] : []];
295
+ const files = commit.length > 1 ? `${commit.slice(0, -1).join(", ")} and ${commit.at(-1) ?? ""}` : commit[0];
296
+ io.stdout(designSystem && name ? `Next: commit ${files}. An app that depends on ${name} now finds it with no config of its own: run \`npx ${NAME} inspect\` in one to see it, and give its team ${GUIDE_FILE}.` : `Next: ${files ? `commit ${files}, and ` : ""}run \`npx ${NAME} check . --format github --require-design-system\` in CI.`);
218
297
  return 0;
219
298
  }
299
+ const CONFIG_FILE = "onsystem.config.json";
300
+ function readManifest(root) {
301
+ const file = path.join(root, "package.json");
302
+ try {
303
+ const text = fs.readFileSync(file, "utf8");
304
+ const json = JSON.parse(text);
305
+ if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
306
+ return {
307
+ file,
308
+ json,
309
+ indent: /^([ \t]+)"/m.exec(text)?.[1] ?? " ",
310
+ newline: text.endsWith("\n")
311
+ };
312
+ } catch {
313
+ return;
314
+ }
315
+ }
316
+ /**
317
+ * Declares the config in the package's package.json, `"onsystem": "./onsystem.config.json"`,
318
+ * and adds it to `files` when the package lists the files it publishes, so apps that install it
319
+ * get it too. Writes ONSYSTEM.md for the apps' teams unless one exists. Returns what it wrote.
320
+ */
321
+ function declarePackage(root, manifest, name, force, notes) {
322
+ const written = [];
323
+ const { json } = manifest;
324
+ const value = `./${CONFIG_FILE}`;
325
+ let changed = false;
326
+ if (json["onsystem"] !== value) {
327
+ json[PACKAGE_FIELD] = value;
328
+ changed = true;
329
+ }
330
+ const files = Array.isArray(json.files) ? json.files : void 0;
331
+ if (files && !ships(files, CONFIG_FILE)) {
332
+ files.push(CONFIG_FILE);
333
+ changed = true;
334
+ }
335
+ if (changed) {
336
+ fs.writeFileSync(manifest.file, `${JSON.stringify(json, null, manifest.indent)}${manifest.newline ? "\n" : ""}`);
337
+ written.push("package.json");
338
+ }
339
+ notes.push(`This is a design-system package: package.json declares its config ("${PACKAGE_FIELD}": "${value}"${files ? ", also added to \"files\"" : ""}), which every app that depends on ${name} reads when it has no config of its own.`);
340
+ const guide = path.join(root, GUIDE_FILE);
341
+ if (force || !fs.existsSync(guide)) {
342
+ fs.writeFileSync(guide, consumerGuide(name));
343
+ written.push(GUIDE_FILE);
344
+ } else notes.push(`${GUIDE_FILE} already exists; it was left as it is (--force replaces it).`);
345
+ return written;
346
+ }
347
+ /**
348
+ * What apps that install the package from a registry will not find: the component and token
349
+ * files its `files` list leaves out of the published package. Undefined for a private package,
350
+ * one without a `files` list (npm then ships everything), or when nothing is left out.
351
+ */
352
+ async function unshipped(config, json) {
353
+ if (json.private === true || !Array.isArray(json.files)) return void 0;
354
+ const files = json.files;
355
+ const missing = [...await componentFiles(config), ...await tokenFiles(config)].map((f) => relativePath(config.root, f)).filter((f) => !f.startsWith("../")).filter((f) => !ships(files, f));
356
+ if (!missing.length) return void 0;
357
+ const folders = [...new Set(missing.map((f) => f.includes("/") ? `${f.slice(0, f.indexOf("/"))}/` : f))];
358
+ return `Apps that install ${typeof json.name === "string" ? json.name : "the package"} from a registry get only what "files" in package.json publishes, which leaves out ${folders.join(", ")} (${count$1(missing.length, "component or token file")}): they will not find those. Add ${folders.length === 1 ? "it" : "them"} to "files" for them; workspace apps read the sources either way.`;
359
+ }
360
+ /** Whether npm's `files` list publishes `file`, a path relative to the package. */
361
+ function ships(files, file) {
362
+ return files.some((entry) => {
363
+ if (typeof entry !== "string") return false;
364
+ const pattern = entry.replace(/^\.?\//, "").replace(/\/+$/, "");
365
+ if (!pattern || pattern.startsWith("!")) return false;
366
+ return file === pattern || file.startsWith(`${pattern}/`) || matchesGlob(file, pattern) || matchesGlob(file, `${pattern}/**`);
367
+ });
368
+ }
369
+ /** ONSYSTEM.md: how an app that uses the package gets the design system to its agents and CI. */
370
+ function consumerGuide(name) {
371
+ return `${[
372
+ `# onsystem in apps that use ${name}`,
373
+ "",
374
+ `${name} declares its [onsystem](https://onsystem.vercel.app) config in its package.json: \`"${PACKAGE_FIELD}": "./${CONFIG_FILE}"\`. An app that depends on ${name}, as a workspace package or installed from npm, reads that config when it has none of its own, and its UI is checked against ${name}'s components, props, variants and tokens. To see what an app finds, run this in its folder:`,
375
+ "",
376
+ "```sh",
377
+ `npx -y ${NAME} inspect`,
378
+ "```",
379
+ "",
380
+ `An app with an ${CONFIG_FILE} of its own reads that instead.`,
381
+ "",
382
+ "## Coding agents",
383
+ "",
384
+ "Claude Code: the plugin checks every UI file the agent writes and hands the errors back to fix, and adds the MCP server and a skill.",
385
+ "",
386
+ "```text",
387
+ "/plugin marketplace add dgesteves/onsystem",
388
+ "/plugin install onsystem@dgesteves",
389
+ "```",
390
+ "",
391
+ `Other agents: \`npx skills add dgesteves/onsystem\` adds the skill, and \`npx -y ${NAME}\` started in the app is the MCP server. [Set up your agent](https://onsystem.vercel.app/docs/clients) has the config for each client.`,
392
+ "",
393
+ "## CI",
394
+ "",
395
+ "In the app, record the findings it already has once, and commit the baseline, so pull requests fail only on new ones:",
396
+ "",
397
+ "```sh",
398
+ `npm install --save-dev ${NAME}`,
399
+ `npx ${NAME} check . --update-baseline`,
400
+ "```",
401
+ "",
402
+ "```yaml",
403
+ `- run: npx ${NAME} check . --format github --require-design-system`,
404
+ "```",
405
+ "",
406
+ "[CI and baselines](https://onsystem.vercel.app/docs/ci) has the GitHub Action and the rest.",
407
+ "",
408
+ "## Changing the config",
409
+ "",
410
+ `${CONFIG_FILE} in ${name} sets what every app gets: where the components, tokens and docs are, and the rules' severities. Its \`tsconfig\`, \`overrides\` and \`includeDesignSystem\` apply to ${name}'s own files only. [Design-system packages](https://onsystem.vercel.app/docs/design-system-packages) has the details.`
411
+ ].join("\n")}\n`;
412
+ }
220
413
  function count$1(n, word) {
221
414
  return `${n.toLocaleString("en-US")} ${n === 1 ? word : `${word}s`}`;
222
415
  }
@@ -234,6 +427,9 @@ Usage
234
427
  onsystem check <paths...> Lint files, folders or globs with the check_ui rules
235
428
  (for CI)
236
429
  onsystem inspect Print what was extracted from the project
430
+ onsystem export --format dtcg|design-md
431
+ Print the tokens it found as a DTCG 2025.10 file or a
432
+ DESIGN.md (--out <file> writes them to a file)
237
433
  onsystem init Write onsystem.config.json from what was found, run a
238
434
  dry-run check and offer a baseline
239
435
  onsystem help Show this help
@@ -252,7 +448,9 @@ Options
252
448
  --docs <glob> Component docs (Markdown/MDX), repeatable
253
449
  --no-cache Ignore the on-disk extraction cache
254
450
  --no-watch serve: do not reload when files change
255
- --format <format> check: pretty | json | github | sarif (default: pretty)
451
+ --format <format> check: pretty | json | github | sarif (default: pretty);
452
+ export: dtcg | design-md
453
+ --out <file> export: write to this file instead of stdout
256
454
  --max-warnings <n> check: exit 1 when there are more than n warnings
257
455
  --update-baseline check: record the current findings as accepted, and exit 0
258
456
  --baseline <file> check: baseline file (default: ${BASELINE_FILE} in the root,
@@ -292,7 +490,8 @@ const defaultIo = {
292
490
  stdout: (text) => process.stdout.write(`${text}\n`),
293
491
  stderr: (text) => process.stderr.write(`${text}\n`),
294
492
  color: process.stdout.isTTY && !process.env.NO_COLOR,
295
- prompt: process.stdin.isTTY && process.stdout.isTTY ? askInTerminal : void 0
493
+ prompt: process.stdin.isTTY && process.stdout.isTTY ? askInTerminal : void 0,
494
+ stdinIsTTY: process.stdin.isTTY
296
495
  };
297
496
  /** One question on the terminal, answered with a line. */
298
497
  async function askInTerminal(question) {
@@ -337,10 +536,8 @@ async function main(argv, io = defaultIo) {
337
536
  type: "boolean",
338
537
  default: true
339
538
  },
340
- format: {
341
- type: "string",
342
- default: "pretty"
343
- },
539
+ format: { type: "string" },
540
+ out: { type: "string" },
344
541
  "max-warnings": { type: "string" },
345
542
  baseline: { type: "string" },
346
543
  "update-baseline": {
@@ -422,15 +619,18 @@ async function main(argv, io = defaultIo) {
422
619
  try {
423
620
  switch (command) {
424
621
  case "serve":
622
+ if (io.stdinIsTTY) io.stderr(`${NAME} is an MCP server: it is waiting for an agent on stdin. Add it to your agent (${DOCS}/clients), or run \`npx -y ${NAME} inspect\` or \`npx -y ${NAME} check .\` here. Ctrl+C quits.`);
425
623
  await serveStdio({
426
624
  ...configOptions,
427
625
  cache: values.cache,
428
626
  watch: values.watch,
429
- logger: stderrLogger
627
+ logger: stderrLogger,
628
+ ...io.transport ? { transport: io.transport } : {}
430
629
  });
431
630
  return 0;
432
631
  case "check": return await check(rest, configOptions, values, io);
433
632
  case "inspect": return await inspect(configOptions, values, io);
633
+ case "export": return await exportTokens(configOptions, values, io);
434
634
  case "init": return await init(configOptions, {
435
635
  yes: values.yes,
436
636
  force: values.force,
@@ -470,14 +670,14 @@ async function check(patterns, configOptions, values, io) {
470
670
  io.stderr(`check: pass files, folders or globs, e.g. ${NAME} check .`);
471
671
  return 2;
472
672
  }
473
- const format = values.format;
673
+ const format = values.format ?? "pretty";
474
674
  if (![
475
675
  "pretty",
476
676
  "json",
477
677
  "github",
478
678
  "sarif"
479
679
  ].includes(format)) {
480
- io.stderr(`check: unknown --format "${values.format}" (pretty, json, github, sarif)`);
680
+ io.stderr(`check: unknown --format "${format}" (pretty, json, github, sarif)`);
481
681
  return 2;
482
682
  }
483
683
  const maxWarnings = values["max-warnings"] === void 0 ? void 0 : Number(values["max-warnings"]);
@@ -487,6 +687,7 @@ async function check(patterns, configOptions, values, io) {
487
687
  }
488
688
  const target = await loadTarget(configOptions);
489
689
  warnDeprecated(target.config.deprecations, format, io);
690
+ warnDeprecated(configWarnings(target), format, io);
490
691
  const selection = target.workspace ? await selectInWorkspace(target.workspace, patterns, values, format, io) : await selectInProject(target.config, patterns, values, format, io);
491
692
  if (typeof selection === "number") return selection;
492
693
  const { files, notes } = selection;
@@ -518,7 +719,7 @@ async function check(patterns, configOptions, values, io) {
518
719
  let baselined = 0;
519
720
  let fixed = 0;
520
721
  for (const file of files) {
521
- const code = await fs$1.readFile(file, "utf8");
722
+ const code = await fsp.readFile(file, "utf8");
522
723
  const ds = selection.designSystemOf(file);
523
724
  let result = ds.check(code, relativePath(ds.root, file));
524
725
  const key = baselineKey(file);
@@ -660,7 +861,7 @@ async function selectInProject(config, patterns, values, format, io) {
660
861
  return 0;
661
862
  }
662
863
  const notice = ds.notice();
663
- if (notice && values["require-design-system"]) {
864
+ if (notice && values["require-design-system"] && !ds.hasDesignSystem()) {
664
865
  io.stderr(`check --require-design-system: ${notice}`);
665
866
  return 2;
666
867
  }
@@ -747,7 +948,7 @@ async function selectInWorkspace(workspace, patterns, values, format, io) {
747
948
  }
748
949
  if (values["require-design-system"]) {
749
950
  let complete = false;
750
- for (const p of workspace.projects) if (!(await load(p)).notice()) {
951
+ for (const p of workspace.projects) if ((await load(p)).hasDesignSystem()) {
751
952
  complete = true;
752
953
  break;
753
954
  }
@@ -799,6 +1000,7 @@ async function inspect(configOptions, values, io) {
799
1000
  const target = await loadTarget(configOptions);
800
1001
  const { config } = target;
801
1002
  warnDeprecated(config.deprecations, "pretty", io);
1003
+ warnDeprecated(configWarnings(target), "pretty", io);
802
1004
  if (target.workspace) return inspectWorkspace(target.workspace, values, io);
803
1005
  const ds = await loadDesignSystem(config, {
804
1006
  cache: values.cache,
@@ -811,12 +1013,13 @@ async function inspect(configOptions, values, io) {
811
1013
  }
812
1014
  const lines = [
813
1015
  `root ${model.root}`,
814
- `config ${config.configFile ? displayPath(io.cwd, config.configFile) : "(defaults)"}`,
1016
+ `config ${config.configFile ? `${displayPath(io.cwd, config.configFile)}${declaredBy(config)}` : "(defaults)"}`,
815
1017
  ...config.detected ? [`detected ${config.detected}`] : [],
1018
+ ...describeLibraries(config, model, io.cwd).map((line) => `library ${line}`),
816
1019
  `files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`,
817
1020
  `time ${model.stats.durationMs}ms${model.stats.fromCache ? " (cache)" : ""}`,
818
1021
  "",
819
- `Components (${ds.roots().length} + ${model.components.length - ds.roots().length} parts)`
1022
+ `Components (${ds.roots().length} + ${ds.components.length - ds.roots().length} parts)`
820
1023
  ];
821
1024
  for (const c of ds.roots()) {
822
1025
  const variants = c.variants.map((v) => `${v.name}[${v.values.length}]`).join(" ");
@@ -840,6 +1043,64 @@ async function inspect(configOptions, values, io) {
840
1043
  io.stdout(lines.join("\n"));
841
1044
  return 0;
842
1045
  }
1046
+ /**
1047
+ * `MUI (@mui/material 9.4.0): 149 components from its type declarations`, per library the
1048
+ * project depends on, including those the config's `libraries` leaves out.
1049
+ */
1050
+ function describeLibraries(config, model, cwd) {
1051
+ const name = (library) => `${libraryAdapter(library.package)?.title ?? library.package} (${library.package}${library.version ? ` ${library.version}` : ""})`;
1052
+ return [...(config.libraries ?? []).map((library) => {
1053
+ if (!library.dir) return `${name(library)}: a dependency, but not installed`;
1054
+ const count = model.components.filter((c) => c.library === library.package).length;
1055
+ return `${name(library)}: ${plural(count, "component")} from its type declarations`;
1056
+ }), ...(config.librariesOff ?? []).map((library) => `${name(library)}: ${librariesLeftOut(config, cwd)}`)];
1057
+ }
1058
+ /**
1059
+ * `export --format dtcg|design-md`: the project's tokens as a DTCG 2025.10 file or a
1060
+ * DESIGN.md, on stdout or in the `--out` file.
1061
+ */
1062
+ async function exportTokens(configOptions, values, io) {
1063
+ const format = values.format;
1064
+ if (format !== "dtcg" && format !== "design-md") {
1065
+ io.stderr(`export: pass --format dtcg or --format design-md${format === void 0 ? "" : ` (not "${format}")`}`);
1066
+ return 2;
1067
+ }
1068
+ const target = await loadTarget(configOptions);
1069
+ warnDeprecated(target.config.deprecations, "pretty", io);
1070
+ warnDeprecated(configWarnings(target), "pretty", io);
1071
+ if (target.workspace) {
1072
+ const dirs = target.workspace.projects.map((p) => p.dir);
1073
+ io.stderr(`export: ${target.workspace.root} is a monorepo root, and each project has its own tokens. Pass --root with one of them${dirs.length ? ` (${listOf(dirs)})` : ""}.`);
1074
+ return 2;
1075
+ }
1076
+ const ds = await loadDesignSystem(target.config, {
1077
+ cache: values.cache,
1078
+ logger: silentLogger
1079
+ });
1080
+ if (!ds.tokens.length) {
1081
+ io.stderr(`export: no design tokens found in ${ds.root}. \`${NAME} inspect --explain\` shows where it looked.`);
1082
+ return 2;
1083
+ }
1084
+ const result = format === "dtcg" ? toDtcg(ds.tokens) : toDesignMd(ds.tokens, { name: projectName(ds.root) });
1085
+ for (const warning of result.warnings) io.stderr(`export: ${warning}`);
1086
+ if (values.out === void 0) {
1087
+ io.stdout(result.text.trimEnd());
1088
+ return 0;
1089
+ }
1090
+ const file = path.resolve(io.cwd, values.out);
1091
+ await fsp.mkdir(path.dirname(file), { recursive: true });
1092
+ await fsp.writeFile(file, result.text);
1093
+ io.stdout(`Wrote ${plural(result.count, "token")} to ${displayPath(io.cwd, file)} (${format === "dtcg" ? "DTCG 2025.10" : "DESIGN.md"})`);
1094
+ return 0;
1095
+ }
1096
+ /** The package name in the root's package.json, else the folder's name. */
1097
+ function projectName(root) {
1098
+ try {
1099
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
1100
+ if (typeof pkg.name === "string" && pkg.name) return pkg.name;
1101
+ } catch {}
1102
+ return path.basename(root);
1103
+ }
843
1104
  function tokenCounts(model) {
844
1105
  const counts = /* @__PURE__ */ new Map();
845
1106
  for (const t of model.tokens) counts.set(t.category, (counts.get(t.category) ?? 0) + 1);
@@ -880,8 +1141,8 @@ async function inspectWorkspace(workspace, values, io) {
880
1141
  const { model } = ds;
881
1142
  const roots = ds.roots();
882
1143
  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] ?? ""}`] : []);
1144
+ const own = project.config.configFile && project.config.configFile !== workspace.configFile ? `${displayPath(workspace.root, project.config.configFile)}${declaredBy(project.config)}` : void 0;
1145
+ lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...own ? [` config ${own}`] : [], ...project.config.detected ? [` detected ${project.config.detected}`] : [], ...describeLibraries(project.config, model, io.cwd).map((line) => ` library ${line}`), ` files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`, ` components ${roots.length} + ${ds.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
1146
  }
886
1147
  if (!loaded.length) lines.push("", `No workspace package has a design system that zero config finds. See ${DOCS}/troubleshooting`);
887
1148
  if (workspace.others.length) lines.push("", `Without a design system (${workspace.others.length}): ${listNames(workspace.others)}`);
@@ -903,6 +1164,10 @@ function explainWorkspace(workspace, loaded, cwd) {
903
1164
  for (const { project, ds } of loaded) lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...explainProject(ds, cwd, " "));
904
1165
  return lines;
905
1166
  }
1167
+ /** ` (declared by @acme/ui)` after a config a design-system package declares; empty otherwise. */
1168
+ function declaredBy(config) {
1169
+ return config.designSystemPackage ? ` (declared by ${config.designSystemPackage.name})` : "";
1170
+ }
906
1171
  /** `A, B, C` up to a dozen names, then `and 40 more`. */
907
1172
  function listNames(names, max = 12) {
908
1173
  const shown = names.slice(0, max).join(", ");
@@ -912,6 +1177,10 @@ function listNames(names, max = 12) {
912
1177
  * Says once what a file's old name means: on stderr, or as a workflow warning with
913
1178
  * `--format github` so it shows on the run. Never on stdout otherwise, which JSON output owns.
914
1179
  */
1180
+ /** What the configs read have to say: the project's, or every package's at a monorepo root. */
1181
+ function configWarnings(target) {
1182
+ return target.workspace ? target.workspace.warnings : target.config.warnings;
1183
+ }
915
1184
  function warnDeprecated(messages, format, io) {
916
1185
  for (const text of messages ?? []) if (format === "github") io.stdout(`::warning title=${NAME}::${text}`);
917
1186
  else io.stderr(text);