onsystem 0.10.1 → 0.10.3

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.
@@ -1,5 +1,5 @@
1
- import { Et as isInside, Ft as unique, It as defaultProjectConfig, Mt as json, Nt as plural, Ot as pathOf, Pt as truncate, Tt as SCRIPT_FILE, X as DEFAULT_SEVERITY, at as excludedBy, bt as findDesignSystemPackage, d as descriptionOf, f as searchComponents, ft as LIBRARIES, h as uncheckedNotice, ht as notReadYet, jt as closest, kt as relativePath, mt as libraryTitle, n as DesignSystemHost, nt as RULE_IDS, pt as LIBRARY_IDS, q as CONFIG_FILES, st as loadConfig, tt as LEGACY_CONFIG_FILES, u as buildSearchIndex, ut as formatPx, v as NAME, y as VERSION, yt as detectProject, z as TOKEN_CATEGORIES } from "./design-system-CPH4C7-1.js";
2
- import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-BINqxi7p.js";
1
+ import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Et as SCRIPT_FILE, Ft as truncate, It as unique, J as ConfigError, Lt as defaultProjectConfig, Mt as closest, Nt as json, Pt as plural, X as DEFAULT_SEVERITY, bt as detectProject, ct as loadConfig, d as descriptionOf, dt as formatPx, f as searchComponents, gt as notReadYet, h as uncheckedNotice, ht as libraryTitle, kt as pathOf, mt as LIBRARY_IDS, n as DesignSystemHost, ot as excludedBy, pt as LIBRARIES, q as CONFIG_FILES, r as buildModel, rt as RULE_IDS, tt as LEGACY_CONFIG_FILES, u as buildSearchIndex, v as NAME, xt as findDesignSystemPackage, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-7wZoJsMm.js";
2
+ import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-C5pMS6Xv.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -7,7 +7,7 @@ import * as z from "zod";
7
7
  import fsp from "node:fs/promises";
8
8
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
9
9
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
10
- //#region src/explain.ts
10
+ // src/explain.ts
11
11
  /**
12
12
  * What `inspect --explain` says about one project: every candidate zero config looked
13
13
  * at and why each was taken or not, the config it resolved to, tokens whose value did
@@ -19,8 +19,11 @@ function explainProject(ds, cwd, indent = "") {
19
19
  const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
20
20
  out("How the design system was found");
21
21
  for (const step of packageSteps(config, cwd)) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
22
- if (config.configured) out(` components come from ${config.inherits && config.designSystemPackage ? display(cwd, config.designSystemPackage.config) : config.configFile ? `${display(cwd, config.configFile)}${over(config, cwd)}` : "--components"}, so zero config did not look`);
23
- else {
22
+ if (config.configured) {
23
+ out(` components come from ${config.inherits && config.designSystemPackage ? display(cwd, config.designSystemPackage.config) : config.configFile ? `${display(cwd, config.configFile)}${over(config, cwd)}` : "--components"}, so zero config did not look`);
24
+ const unpublished = unpublishedComponents(ds);
25
+ if (unpublished) out(` ${unpublished}`);
26
+ } else {
24
27
  const steps = [];
25
28
  detectProject(config.root, config.tsconfig, steps, config.designSystemPackage);
26
29
  for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
@@ -85,6 +88,22 @@ function explainProject(ds, cwd, indent = "") {
85
88
  }
86
89
  return lines;
87
90
  }
91
+ /**
92
+ * Why a project that takes on the config of a design-system package installed in node_modules
93
+ * knows none of its components: the package publishes neither their sources nor their type
94
+ * declarations, or `"libraries": false` turns off the declarations it publishes. The one message
95
+ * for it, where a project without components would otherwise be told to check the "components"
96
+ * globs. Undefined otherwise.
97
+ */
98
+ function unpublishedComponents(ds) {
99
+ const declared = ds.config.designSystemPackage;
100
+ if (!declared?.installed || !ds.config.inherits || ds.model.components.length) return void 0;
101
+ const tokens = ds.model.tokens.length ? " The token rules still check against the tokens it publishes." : "";
102
+ const name = pathOf(declared.name);
103
+ const see = "See https://onsystem.vercel.app/docs/design-system-packages#what-apps-get";
104
+ if (ds.config.librariesOff?.some((library) => library.package === declared.name)) return `${name} publishes no component sources, and "libraries": false in the config turns off the type declarations it publishes, so its components are unknown here and the component rules did not run: set "libraries" to [], which reads its declarations and no library, or remove it.${tokens} ${see}`;
105
+ return `${name} publishes neither its component sources nor their type declarations, so its components are unknown here and the component rules did not run: ask its team to add its sources to "files" in its package.json, or to publish type declarations.${tokens} ${see}`;
106
+ }
88
107
  /** Why a library the project depends on is not read: `left out by "libraries" in onsystem.config.json`. */
89
108
  function librariesLeftOut(config, cwd) {
90
109
  return `left out by "libraries" in ${config.configFile ? display(cwd, config.configFile) : "the config"}`;
@@ -151,8 +170,7 @@ function packageSteps(config, cwd) {
151
170
  reason: `${declared.name} (${declared.installed ? "installed" : "a workspace package"}) declares ${display(cwd, declared.config)}, which this project ${config.configFile === declared.config ? "takes on" : `reads under ${display(cwd, config.configFile ?? "")}`}, 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}`
152
171
  }];
153
172
  }
154
- //#endregion
155
- //#region src/not-read.ts
173
+ // src/not-read.ts
156
174
  const LIMITS_DOCS = "https://onsystem.vercel.app/docs/how-it-works#limits";
157
175
  /**
158
176
  * What `check` and `check_ui` could not read, said after the results as the notice for missing
@@ -228,8 +246,7 @@ function listOf$2(items, max = 3) {
228
246
  if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
229
247
  return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
230
248
  }
231
- //#endregion
232
- //#region src/server/render.ts
249
+ // src/server/render.ts
233
250
  /**
234
251
  * Tool results are read by models, so they are rendered as compact Markdown:
235
252
  * fewer tokens than JSON, and the same information. The JSON is still sent
@@ -564,8 +581,12 @@ function listTail(listed) {
564
581
  if (listed.omitted) lines.push("", `${listed.omitted.toLocaleString("en-US")} more not shown: pass a higher \`limit\`, or a \`query\`.`);
565
582
  return lines;
566
583
  }
567
- /** The shared explanation when a project has no design-system components to show. */
568
- function noComponentsMessage(root, project, monorepo = false) {
584
+ /**
585
+ * The shared explanation when a project has no design-system components to show. `reason`, when
586
+ * known, says why instead ({@link unpublishedComponents}).
587
+ */
588
+ function noComponentsMessage(root, project, monorepo = false, reason) {
589
+ if (reason) return answer([reason]);
569
590
  if (monorepo) return answer([`No design-system components found in any project of this monorepo (root: ${root}). Check the "components" globs in the config. Run \`onsystem inspect\` there to see what was looked at.`]);
570
591
  return answer([`No design-system components found${project ? ` in ${pathOf(project)}` : ""}. Check the "components" globs in the config (root: ${root}).`]);
571
592
  }
@@ -573,7 +594,7 @@ function noComponentsMessage(root, project, monorepo = false) {
573
594
  function renderComponentList(ds, project, filter = {}) {
574
595
  const roots = ds.roots();
575
596
  const where = project ? ` in ${pathOf(project)}` : "";
576
- if (!roots.length) return noComponentsMessage(ds.root, project);
597
+ if (!roots.length) return noComponentsMessage(ds.root, project, false, unpublishedComponents(ds));
577
598
  const listed = listComponents(roots, (c) => c, filter);
578
599
  if (!listed.matched) return answer([`No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`]);
579
600
  const marks = new Marks();
@@ -688,7 +709,7 @@ function renderAmbiguous(name, entries) {
688
709
  }
689
710
  /** get_component's answer for a name no component has, with the nearest and all of them. */
690
711
  function renderNotFound(name, guess, names, where = "") {
691
- return answer([`No component named ${json(name)}${where}.` + (guess ? ` Did you mean ${json(guess)}?` : "") + ` Available: ${names.join(", ")}.`]);
712
+ return answer([`No component named ${json(name)}${where}.` + (guess ? ` Did you mean ${json(guess)}?` : "") + (names.length ? ` Available: ${names.join(", ")}.` : "")]);
692
713
  }
693
714
  /**
694
715
  * The build-with-design-system prompt: `task`, the steps, and what the design system has, its
@@ -1080,8 +1101,7 @@ function renderCheck(result, capped = capDiagnostics(result, Infinity), notices
1080
1101
  lines.push("", result.errorCount ? "Fix every error, then run check_ui again." : "Only warnings: fix them if the design system has an equivalent.", ...after);
1081
1102
  return answer(lines);
1082
1103
  }
1083
- //#endregion
1084
- //#region src/server/index.ts
1104
+ // src/server/index.ts
1085
1105
  const INSTRUCTIONS = `This server describes the project's design system: its React components (props, variants, parts), design tokens and usage docs, and lints UI code against it.
1086
1106
  When writing or changing UI in this project:
1087
1107
  1. Find components with search_components (by intent) or list_components.
@@ -1555,7 +1575,7 @@ function register(serve, { getDesignSystem, getProjects }) {
1555
1575
  const { ds } = found;
1556
1576
  const component = ds.getComponent(name);
1557
1577
  if (!component) {
1558
- if (!ds.roots().length) return refuse(noComponentsMessage(ds.root, found.project?.dir));
1578
+ if (!ds.roots().length) return refuse(noComponentsMessage(ds.root, found.project?.dir, false, unpublishedComponents(ds)));
1559
1579
  const names = ds.roots().map((c) => c.name);
1560
1580
  return refuse(renderNotFound(name, ds.suggestComponent(name), names));
1561
1581
  }
@@ -1822,8 +1842,7 @@ function register(serve, { getDesignSystem, getProjects }) {
1822
1842
  return prompted("Build UI with the design system", renderPrompt(task, "catalog" in found ? [...new Map(found.catalog.roots().map((e) => [e.component.name, e.component])).values()] : found.ds.roots(), "catalog" in found ? found.catalog.getTokens().map((t) => t.token) : found.ds.tokens, "catalog" in found ? found.set.dirs : void 0));
1823
1843
  });
1824
1844
  }
1825
- //#endregion
1826
- //#region src/catalog.ts
1845
+ // src/catalog.ts
1827
1846
  /**
1828
1847
  * Every project's components and tokens in one list, for a workspace root when no path
1829
1848
  * picks a project. A component used by several projects (a design-system package) is
@@ -1965,8 +1984,7 @@ function packageOwner(root) {
1965
1984
  return name;
1966
1985
  };
1967
1986
  }
1968
- //#endregion
1969
- //#region src/server/projects.ts
1987
+ // src/server/projects.ts
1970
1988
  /**
1971
1989
  * A workspace root's projects for the MCP server: one `DesignSystemHost` per project,
1972
1990
  * created on first use and watched from then on, and the merged catalog, rebuilt when a
@@ -2025,6 +2043,7 @@ var WorkspaceProjects = class {
2025
2043
  host = new DesignSystemHost(project.config, {
2026
2044
  cache: this.options.cache,
2027
2045
  logger: this.options.logger,
2046
+ build: this.options.build,
2028
2047
  loadConfig: () => this.workspace.configFor(project)
2029
2048
  });
2030
2049
  host.onChange(() => this.options.onChange?.());
@@ -2033,8 +2052,7 @@ var WorkspaceProjects = class {
2033
2052
  return host;
2034
2053
  }
2035
2054
  };
2036
- //#endregion
2037
- //#region src/server/stdio.ts
2055
+ // src/server/stdio.ts
2038
2056
  /**
2039
2057
  * Starts the server (on stdio unless another transport is given), for clients on
2040
2058
  * either protocol era: a 2026-07-28 client (stateless, `server/discover`) and a
@@ -2054,8 +2072,17 @@ var WorkspaceProjects = class {
2054
2072
  * project marker, or the monorepo root above that, served as if the server had been
2055
2073
  * started there, except that a config there is read only when it is JSON: a prompt can
2056
2074
  * choose the path, so a JavaScript or TypeScript config is not run. Each root found is
2057
- * loaded once and kept; a request without an absolute path is answered from the one root
2058
- * found so far.
2075
+ * loaded once and kept, by its real path; a request without an absolute path is answered from
2076
+ * the one root found so far. The same holds for a `--root` (or a client's root) that is no
2077
+ * project but holds projects in its folders, such as a folder of repositories, except that only
2078
+ * an absolute path in that folder is answered, from the project in it that holds the file (never
2079
+ * from a folder outside or above it), and a file in none of them from that folder, as before.
2080
+ * Links are resolved: a path, or the project it leads to, that is really outside the folder is
2081
+ * refused, a workspace in it serves only its packages that are really in it, and a project in it
2082
+ * that a link replaces while the server runs is not built again from where the link leads.
2083
+ *
2084
+ * A load that fails, such as on an invalid config, is not kept: the next request loads
2085
+ * again, so fixing the config is enough, also when it was invalid at startup.
2059
2086
  */
2060
2087
  function serveStdio$1(options) {
2061
2088
  const { logger } = options;
@@ -2076,15 +2103,28 @@ function serveStdio$1(options) {
2076
2103
  /**
2077
2104
  * The project at `root` (the working directory when undefined), or a monorepo root's
2078
2105
  * projects. `jsonConfigOnly` for a root found from a tool call's path: its configs, and those
2079
- * of the projects in it, are read only when they are JSON, never run.
2106
+ * of the projects in it, are read only when they are JSON, never run. `within`: the folder of
2107
+ * projects it was found in, for the error on a config that is not JSON, and outside which no
2108
+ * workspace package is served, and no project or package is built.
2080
2109
  */
2081
- const load = async (root, jsonConfigOnly = false) => {
2110
+ const load = async (root, jsonConfigOnly = false, within) => {
2082
2111
  const configOptions = {
2083
2112
  ...options,
2084
2113
  root,
2085
- jsonConfigOnly
2114
+ jsonConfigOnly,
2115
+ jsonConfigWithin: jsonConfigOnly ? within?.dir : void 0
2086
2116
  };
2087
- const { config, workspace } = await loadTarget(configOptions);
2117
+ const target = await loadTarget(configOptions);
2118
+ const { config } = target;
2119
+ let { workspace } = target;
2120
+ if (workspace && within) {
2121
+ const outside = workspace.projects.filter((p) => !isInside(within.real, realPath(p.root)));
2122
+ if (outside.length) {
2123
+ logger.warn(`${pathOf(workspace.root)} names workspace packages outside ${pathOf(within.dir)}, the folder the server answers for (links resolved), which it leaves out: ${outside.map((p) => pathOf(p.root)).join(", ")}. To include them, start the server with --root ${pathOf(workspace.root)}.`);
2124
+ workspace = workspace.without(outside.map((p) => p.root));
2125
+ }
2126
+ }
2127
+ const build = within ? buildWithin(within) : void 0;
2088
2128
  for (const text of config.deprecations ?? []) logger.warn(text);
2089
2129
  for (const text of (workspace ? workspace.warnings : config.warnings) ?? []) logger.warn(text);
2090
2130
  if (workspace) {
@@ -2092,7 +2132,8 @@ function serveStdio$1(options) {
2092
2132
  cache: options.cache,
2093
2133
  watch: options.watch,
2094
2134
  logger,
2095
- onChange: notify
2135
+ onChange: notify,
2136
+ build
2096
2137
  });
2097
2138
  logger.info(`monorepo root: ${workspace.projects.length} of ${workspace.projects.length + workspace.others.length} workspace packages have a design system${workspace.projects.length ? ` (${workspace.projects.map((p) => p.dir).join(", ")})` : ""}`);
2098
2139
  closers.push(() => {
@@ -2104,7 +2145,8 @@ function serveStdio$1(options) {
2104
2145
  const host = new DesignSystemHost(config, {
2105
2146
  cache: options.cache,
2106
2147
  logger,
2107
- loadConfig: () => loadConfig(configOptions)
2148
+ loadConfig: () => loadConfig(configOptions),
2149
+ build
2108
2150
  });
2109
2151
  host.onChange(notify);
2110
2152
  if (options.watch) host.watch();
@@ -2116,44 +2158,85 @@ function serveStdio$1(options) {
2116
2158
  });
2117
2159
  return { host };
2118
2160
  };
2161
+ /**
2162
+ * The roots loaded or loading, by `key` (the real path of `dir`, for a root found from a path).
2163
+ * One that fails is logged and dropped, so the next request loads it again, and until then it
2164
+ * is no project found.
2165
+ */
2166
+ const found = /* @__PURE__ */ new Map();
2167
+ const loadOnce = (key, dir, start) => {
2168
+ const known = found.get(key);
2169
+ if (known) return known.served;
2170
+ const served = start();
2171
+ found.set(key, {
2172
+ dir,
2173
+ served
2174
+ });
2175
+ served.catch((error) => {
2176
+ logger.error(error.message);
2177
+ if (found.get(key)?.served === served) found.delete(key);
2178
+ });
2179
+ return served;
2180
+ };
2119
2181
  let started = false;
2120
2182
  /** One project for every request: the one at `root`, or the working directory. */
2121
2183
  const serveOne = (root) => {
2122
2184
  if (started) return;
2123
2185
  started = true;
2124
- const served = load(root);
2125
- served.catch((error) => {
2126
- logger.error(error.message);
2127
- });
2128
- decide(() => served);
2186
+ const dir = path.resolve(options.cwd, root ?? ".");
2187
+ const served = () => loadOnce(dir, dir, () => load(root));
2188
+ served();
2189
+ decide(served);
2129
2190
  };
2130
- /** Each request's project, from the absolute path it names. */
2131
- const serveByPath = () => {
2191
+ /**
2192
+ * Each request's project, from the absolute path it names. `within`: a folder that is no
2193
+ * project but holds some (the `--root` or client root the server was started for), which
2194
+ * answers for a file in none of them. Then only a path in it is answered, from a project in
2195
+ * it: never from a folder outside or above it, links resolved, nor from a relative path, which
2196
+ * the tools would read against whichever project answers.
2197
+ */
2198
+ const serveByPath = (within) => {
2132
2199
  if (started) return;
2133
2200
  started = true;
2134
- logger.info(`${options.cwd} holds no project and the client reported no roots: finding each request's project from the absolute path it names`);
2135
- const found = /* @__PURE__ */ new Map();
2201
+ const folder = within === void 0 ? void 0 : {
2202
+ dir: within,
2203
+ real: realPath(within)
2204
+ };
2205
+ const where = within ? `${pathOf(within)} holds no project of its own, only projects in its folders` : `${options.cwd} holds no project and the client reported no roots`;
2206
+ logger.info(`${where}: finding each request's project from the absolute path it names`);
2207
+ const askForPath = `Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, and it answers from that file's project. Or start the server with --root <project folder>.`;
2136
2208
  decide(async (target) => {
2137
- if (target !== void 0 && path.isAbsolute(target)) {
2138
- const dir = findProjectRoot(target);
2139
- if (!dir) throw new ProjectNotFoundError(`No project at or above ${target}: no folder from there up holds a package.json, components.json, project.json or onsystem config (the home folder and the filesystem root do not count). Pass the absolute path of a file in your project.`);
2140
- let served = found.get(dir);
2141
- if (!served) {
2142
- logger.info(`using ${dir}, the project of ${target}`);
2143
- served = load(dir, true);
2144
- served.catch((error) => {
2145
- logger.error(error.message);
2146
- });
2147
- found.set(dir, served);
2148
- }
2149
- return served;
2209
+ const named = target !== void 0 && target !== "";
2210
+ if (within && named && !path.isAbsolute(target)) throw new ProjectNotFoundError(`onsystem cannot tell which project ${pathOf(target)} is in: it is a relative path, and ${pathOf(within)} holds no project of its own, only projects in its folders. ${askForPath}`);
2211
+ if (named && path.isAbsolute(target)) {
2212
+ const file = path.resolve(target);
2213
+ if (within && !isInside(within, file)) throw new ProjectNotFoundError(`${pathOf(target)} is outside ${pathOf(within)}, the folder the server answers for. Pass the absolute path of a file inside it, or start the server with --root <project folder>.`);
2214
+ const up = findProjectRoot(file);
2215
+ const dir = within && !(up && isInside(within, up)) ? projectDirOf(file, within) : up;
2216
+ if (!dir) throw new ProjectNotFoundError(`No project at or above ${pathOf(target)}: no folder from there up holds a package.json, components.json, project.json or onsystem config (the home folder and the filesystem root do not count). Pass the absolute path of a file in your project.`);
2217
+ const real = realPath(dir);
2218
+ if (folder && !(isInside(folder.real, real) && isInside(folder.real, realPath(file)))) throw new ProjectNotFoundError(`${pathOf(target)} leads outside ${pathOf(folder.dir)}, the folder the server answers for, through a link. Pass the absolute path of a file inside it, or start the server with --root <project folder>.`);
2219
+ if (!found.has(real)) logger.info(`using ${dir}, the project of ${target}`);
2220
+ return loadOnce(real, dir, () => load(dir, dir !== within, folder));
2150
2221
  }
2151
2222
  const [only, ...others] = found.values();
2152
- if (only && !others.length) return only;
2153
- throw new ProjectNotFoundError(only ? `Requests have named files in ${found.size} projects (${[...found.keys()].join(", ")}). Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, so the answer comes from its project.` : `onsystem cannot tell which project to answer for: it was started in ${options.cwd}, which holds no project, and the client reported no workspace roots. Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, and it answers from that file's project. Or start the server with --root <project folder>.`);
2223
+ if (only && !others.length) return only.served;
2224
+ throw new ProjectNotFoundError(only ? `Requests have named files in ${found.size} projects (${[...found.values()].map((f) => pathOf(f.dir)).join(", ")}). Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, so the answer comes from its project.` : within ? `onsystem cannot tell which project to answer for: ${pathOf(within)} holds no project of its own, only projects in its folders. ${askForPath}` : `onsystem cannot tell which project to answer for: it was started in ${options.cwd}, which holds no project, and the client reported no workspace roots. ${askForPath}`);
2154
2225
  });
2155
2226
  };
2156
- if (options.root !== void 0 || options.config !== void 0 || [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((file) => fs.existsSync(path.join(options.cwd, file))) || hasProjectMarker(options.cwd)) serveOne(options.root);
2227
+ /** Flags that describe the project themselves: the folder they apply to is the one served. */
2228
+ const described = options.config !== void 0 || Boolean(options.components?.length || options.tokens?.length || options.docs?.length);
2229
+ /**
2230
+ * The project at `root` (the working directory when undefined). A folder that is no project but
2231
+ * holds projects (a folder of repositories, which `--root ${workspaceFolder}` names when the
2232
+ * editor has their parent open) would mix their tokens: each request is answered from its path.
2233
+ */
2234
+ const serveAt = (root) => {
2235
+ const dir = root === void 0 ? void 0 : path.resolve(options.cwd, root);
2236
+ if (dir !== void 0 && !described && holdsOnlyProjects(dir)) serveByPath(dir);
2237
+ else serveOne(root);
2238
+ };
2239
+ if (options.root !== void 0 || options.config !== void 0 || [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((file) => fs.existsSync(path.join(options.cwd, file))) || hasProjectMarker(options.cwd)) serveAt(options.root);
2157
2240
  /**
2158
2241
  * When nothing else says where the project is: the working directory when flags
2159
2242
  * (`--components`, `--tokens`, `--docs`) describe it, else each request's path.
@@ -2189,7 +2272,7 @@ function serveStdio$1(options) {
2189
2272
  return;
2190
2273
  }
2191
2274
  if (root !== options.cwd) logger.info(`using client root ${root}`);
2192
- serveOne(root);
2275
+ serveAt(root);
2193
2276
  });
2194
2277
  };
2195
2278
  return server;
@@ -2206,6 +2289,49 @@ function serveStdio$1(options) {
2206
2289
  await handle.close();
2207
2290
  } });
2208
2291
  }
2292
+ /**
2293
+ * `buildModel` for a project found in the folder `within`, which refuses to build it once its
2294
+ * folder leads outside `within` (a project or workspace package replaced by a link while the
2295
+ * server watches it): that rebuild fails, so the model built from inside the folder stays.
2296
+ */
2297
+ function buildWithin(within) {
2298
+ return async (config, options) => {
2299
+ if (!isInside(within.real, realPath(config.root))) throw new ConfigError(`${pathOf(config.root)} now leads outside ${pathOf(within.dir)}, the folder the server answers for, through a link: it is not read from there.`);
2300
+ return buildModel(config, options);
2301
+ };
2302
+ }
2303
+ /**
2304
+ * Whether `dir` is no project (no project marker, no workspace file) but a folder in it, or in one
2305
+ * of its folders, is one. Dot folders and `node_modules` are not looked in.
2306
+ */
2307
+ function holdsOnlyProjects(dir) {
2308
+ if (hasProjectMarker(dir) || isWorkspaceRoot(dir)) return false;
2309
+ const folders = (from) => {
2310
+ try {
2311
+ return fs.readdirSync(from, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => path.join(from, e.name));
2312
+ } catch {
2313
+ return [];
2314
+ }
2315
+ };
2316
+ const below = folders(dir);
2317
+ return below.some(hasProjectMarker) || below.some((f) => folders(f).some(hasProjectMarker));
2318
+ }
2319
+ /**
2320
+ * Where `p` really is: links resolved, in the letter case the file system keeps
2321
+ * (`realpathSync.native`; plain `realpathSync` keeps the case given on macOS). A path that does
2322
+ * not exist yet, such as the `filename` of new code, from the nearest folder above it that does.
2323
+ */
2324
+ function realPath(p) {
2325
+ const rest = [];
2326
+ for (let dir = path.resolve(p);;) try {
2327
+ return path.join(fs.realpathSync.native(dir), ...rest);
2328
+ } catch {
2329
+ const parent = path.dirname(dir);
2330
+ if (parent === dir) return path.resolve(p);
2331
+ rest.unshift(path.basename(dir));
2332
+ dir = parent;
2333
+ }
2334
+ }
2209
2335
  /** First `file://` root the client reports, if it supports roots (deprecated in 2026-07-28). */
2210
2336
  async function clientRoot(server) {
2211
2337
  if (!server.server.getClientCapabilities()?.roots) return void 0;
@@ -2217,5 +2343,4 @@ async function clientRoot(server) {
2217
2343
  return;
2218
2344
  }
2219
2345
  }
2220
- //#endregion
2221
2346
  export { notReadNotices as a, missingDependencies as i, INSTRUCTIONS as n, explainProject as o, createServer as r, librariesLeftOut as s, serveStdio$1 as t };
@@ -1,21 +1,8 @@
1
- import { Ct as workspacePackageDirs, Et as isInside, St as isWorkspaceRoot, _t as PackageFinder, bt as findDesignSystemPackage, ct as loadConfigWith, i as componentFiles, kt as relativePath, ot as hasOwnConfig, q as CONFIG_FILES, st as loadConfig, tt as LEGACY_CONFIG_FILES, wt as workspacePatterns } from "./design-system-CPH4C7-1.js";
1
+ import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Tt as workspacePatterns, ct as loadConfig, i as componentFiles, lt as loadConfigWith, nt as PROJECT_MARKERS, st as hasOwnConfig, vt as PackageFinder, wt as workspacePackageDirs, xt as findDesignSystemPackage } from "./design-system-7wZoJsMm.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- //#region src/workspace.ts
6
- /**
7
- * What marks the folder a file belongs to: the nearest one at or above it with a config,
8
- * a components.json, a package.json or an Nx project.json. The Claude Code hook finds a
9
- * file's project the same way, so `check` from a workspace root and the hook agree on
10
- * every file.
11
- */
12
- const PROJECT_MARKERS = [
13
- ...CONFIG_FILES,
14
- ...LEGACY_CONFIG_FILES,
15
- "components.json",
16
- "package.json",
17
- "project.json"
18
- ];
5
+ // src/workspace.ts
19
6
  /**
20
7
  * A monorepo seen from its root: every workspace package with a design system (an app
21
8
  * that uses one, from source or a published library, or the design-system package
@@ -23,7 +10,7 @@ const PROJECT_MARKERS = [
23
10
  * any file in it. Without a design system at the root, `check`, `inspect` and the MCP
24
11
  * server work from here, each file checked by its own project's design system.
25
12
  */
26
- var Workspace = class {
13
+ var Workspace = class Workspace {
27
14
  root;
28
15
  source;
29
16
  projects;
@@ -71,6 +58,10 @@ var Workspace = class {
71
58
  }
72
59
  return project;
73
60
  }
61
+ /** The workspace without the projects at `roots`. */
62
+ without(roots) {
63
+ return new Workspace(this.root, this.source, this.projects.filter((p) => !roots.includes(p.root)), this.others, this.configFile, this.options, this.rootConfig, this.otherConfigs);
64
+ }
74
65
  rootProject() {
75
66
  return {
76
67
  root: this.root,
@@ -148,7 +139,8 @@ function loadProjectConfig(dir, workspaceRoot, rootConfigFile, options) {
148
139
  const shared = {
149
140
  cwd: options.cwd,
150
141
  includeDesignSystem: options.includeDesignSystem,
151
- jsonConfigOnly: options.jsonConfigOnly
142
+ jsonConfigOnly: options.jsonConfigOnly,
143
+ jsonConfigWithin: options.jsonConfigWithin
152
144
  };
153
145
  const packages = new PackageFinder(dir);
154
146
  if (!rootConfigFile || hasOwnConfig(dir)) return loadConfigWith({
@@ -224,5 +216,4 @@ function isDirectory(target) {
224
216
  return false;
225
217
  }
226
218
  }
227
- //#endregion
228
- export { hasProjectMarker as n, loadTarget as r, findProjectRoot as t };
219
+ export { projectDirOf as i, hasProjectMarker as n, loadTarget as r, findProjectRoot as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onsystem",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "mcpName": "io.github.dgesteves/onsystem",
5
5
  "description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.",
6
6
  "keywords": [