onsystem 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { C as TOKEN_CATEGORIES, D as VariantInfo, E as TokenCategory, O as ImportMapping, S as SourceLocation, T as Token, _ as DocSection, a as ResolvedRule, b as PropInfo, c as configJsonSchema, d as CheckResult, f as ComponentDocs, g as Diagnostic, h as DesignSystemModel, i as ResolvedConfig, l as defineConfig, m as CompoundVariantInfo, n as LoadConfigOptions, o as RuleId, p as ComponentInfo, r as RULE_IDS, s as RuleOptions, t as Config, u as loadConfig, v as ExampleInfo, w as TextEdit, x as Severity, y as InheritedProps } from "./config-BbTW1jKU.js";
1
+ import { C as TOKEN_CATEGORIES, D as VariantInfo, E as TokenCategory, O as ImportMapping, S as SourceLocation, T as Token, _ as DocSection, a as ResolvedRule, b as PropInfo, c as configJsonSchema, d as CheckResult, f as ComponentDocs, g as Diagnostic, h as DesignSystemModel, i as ResolvedConfig, l as defineConfig, m as CompoundVariantInfo, n as LoadConfigOptions, o as RuleId, p as ComponentInfo, r as RULE_IDS, s as RuleOptions, t as Config, u as loadConfig, v as ExampleInfo, w as TextEdit, x as Severity, y as InheritedProps } from "./config-JaNAiR9Z.js";
2
2
  import ts from "typescript";
3
3
  import { Oklch } from "culori";
4
4
  import { McpServer, Transport } from "@modelcontextprotocol/server";
5
- //#region src/lint/analyze.d.ts
6
5
  interface ImportBinding {
7
6
  source: string;
8
7
  /** Imported name, `default`, or `*` for namespace imports. */
@@ -37,8 +36,6 @@ interface Analysis {
37
36
  classStrings: ClassString[];
38
37
  styles: StyleObject[];
39
38
  }
40
- //#endregion
41
- //#region src/modules.d.ts
42
39
  /** Where an import points: a file relative to the root, or `{}` for a declared module not on disk. */
43
40
  interface ModuleLocation {
44
41
  file?: string;
@@ -83,8 +80,6 @@ declare class ModuleResolver {
83
80
  exportsOf(file: string): ModuleExports | undefined;
84
81
  private createResolver;
85
82
  }
86
- //#endregion
87
- //#region src/tokens/roles.d.ts
88
83
  /**
89
84
  * What a color utility paints, which decides the tokens that fit it: `text-*`
90
85
  * wants a foreground, `bg-*` a surface, `border-*` a border color.
@@ -100,8 +95,6 @@ type ColorRole = 'text' | 'surface' | 'line';
100
95
  * palette (`color-red-500`).
101
96
  */
102
97
  type ScopedFamily = string;
103
- //#endregion
104
- //#region src/tokens/css.d.ts
105
98
  /**
106
99
  * Reads design tokens from CSS custom properties, the way shadcn/ui and
107
100
  * Tailwind v4 projects define them:
@@ -114,14 +107,10 @@ export declare function parseCssTokens(css: string, file: string): {
114
107
  tokens: Token[];
115
108
  warnings: string[];
116
109
  };
117
- //#endregion
118
- //#region src/tokens/design-md.d.ts
119
110
  export declare function parseDesignMd(text: string, file: string): {
120
111
  tokens: Token[];
121
112
  warnings: string[];
122
113
  };
123
- //#endregion
124
- //#region src/tokens/dtcg.d.ts
125
114
  interface DtcgOptions {
126
115
  /** Prefix for generated custom property names: `ds` → `--ds-color-primary`. */
127
116
  prefix?: string;
@@ -130,8 +119,6 @@ export declare function parseDtcgTokens(text: string, file: string, options?: Dt
130
119
  tokens: Token[];
131
120
  warnings: string[];
132
121
  };
133
- //#endregion
134
- //#region src/tokens/export.d.ts
135
122
  /**
136
123
  * Writes the tokens onsystem found as a DTCG 2025.10 file or a DESIGN.md. Both are grouped by
137
124
  * category; reading the DTCG file back gives the same tokens, with the same names.
@@ -158,8 +145,6 @@ export declare function toDtcg(tokens: readonly Token[]): Exported;
158
145
  export declare function toDesignMd(tokens: readonly Token[], options: {
159
146
  name: string;
160
147
  }): Exported;
161
- //#endregion
162
- //#region src/tokens/index.d.ts
163
148
  interface TokenSource {
164
149
  file: string;
165
150
  /** DTCG only: prefix for generated custom property names. */
@@ -281,8 +266,6 @@ export declare class TokenIndex {
281
266
  /** px value of a token-like length string, resolving `var()` against the token set. */
282
267
  toPx(value: string): number | undefined;
283
268
  }
284
- //#endregion
285
- //#region src/lint/target.d.ts
286
269
  /** What the lint rules need to know about the design system. */
287
270
  declare class LintTarget {
288
271
  readonly model: DesignSystemModel;
@@ -378,8 +361,6 @@ declare class LintTarget {
378
361
  /** Component names for "did you mean" suggestions. */
379
362
  names(): string[];
380
363
  }
381
- //#endregion
382
- //#region src/lint/context.d.ts
383
364
  interface Report {
384
365
  start: number;
385
366
  end: number;
@@ -440,12 +421,8 @@ type Resolution =
440
421
  {
441
422
  kind: 'external';
442
423
  };
443
- //#endregion
444
- //#region src/lint/rules/index.d.ts
445
424
  /** Every rule, in the order they run. */
446
425
  export declare const RULES: readonly Rule[];
447
- //#endregion
448
- //#region src/lint/format.d.ts
449
426
  type OutputFormat = 'pretty' | 'json' | 'github' | 'sarif';
450
427
  /**
451
428
  * Renders check results:
@@ -468,8 +445,6 @@ export declare function formatDiagnostics(results: CheckResult[], format: Output
468
445
  */
469
446
  uri?: ((file: string) => string) | undefined;
470
447
  }): string;
471
- //#endregion
472
- //#region src/lint/index.d.ts
473
448
  interface CheckOptions {
474
449
  /** File name used in results and to pick TSX vs JSX parsing. Default `snippet.tsx`. */
475
450
  filename?: string | undefined;
@@ -483,8 +458,6 @@ interface CheckOptions {
483
458
  export declare function checkSource(code: string, target: LintTarget, options: CheckOptions): CheckResult;
484
459
  /** Applies non-overlapping fixes from the end of the file backwards. */
485
460
  export declare function applyFixes(code: string, diagnostics: Diagnostic[]): string;
486
- //#endregion
487
- //#region src/search/bm25.d.ts
488
461
  /**
489
462
  * Okapi BM25 over weighted fields (BM25F-style: field weights scale term
490
463
  * frequency and document length). Small, dependency-free and deterministic,
@@ -512,8 +485,6 @@ declare class Bm25Index<T> {
512
485
  get size(): number;
513
486
  search(query: Map<string, number>, limit?: number): Bm25Hit<T>[];
514
487
  }
515
- //#endregion
516
- //#region src/search/index.d.ts
517
488
  interface SearchHit {
518
489
  component: ComponentInfo;
519
490
  score: number;
@@ -530,8 +501,6 @@ interface SearchHit {
530
501
  * which is not an answer to give as the best match.
531
502
  */
532
503
  export declare function searchComponents(index: Bm25Index<ComponentInfo>, query: string, limit?: number): SearchHit[];
533
- //#endregion
534
- //#region src/util/log.d.ts
535
504
  /**
536
505
  * Diagnostics go to stderr: in stdio mode stdout carries JSON-RPC and must
537
506
  * never receive anything else.
@@ -541,8 +510,6 @@ interface Logger {
541
510
  warn(message: string): void;
542
511
  error(message: string): void;
543
512
  }
544
- //#endregion
545
- //#region src/design-system.d.ts
546
513
  /** A loaded design system: the model plus the indexes the tools query. */
547
514
  export declare class DesignSystem {
548
515
  readonly model: DesignSystemModel;
@@ -659,6 +626,11 @@ export declare class DesignSystemHost {
659
626
  constructor(config: ResolvedConfig, options?: HostOptions);
660
627
  get(): Promise<DesignSystem>;
661
628
  reload(): Promise<DesignSystem>;
629
+ /**
630
+ * `build`, dropped if it fails with no earlier model to serve instead (a first build that
631
+ * fails), so the next `get()` builds again.
632
+ */
633
+ private kept;
662
634
  onChange(listener: (ds: DesignSystem) => void): () => void;
663
635
  /** Watches the component, token and docs locations, the config and the tsconfig chain. */
664
636
  watch(): void;
@@ -677,8 +649,6 @@ export declare class DesignSystemHost {
677
649
  private reloadConfig;
678
650
  private build;
679
651
  }
680
- //#endregion
681
- //#region src/extract/components.d.ts
682
652
  interface ExtractComponentsOptions {
683
653
  root: string;
684
654
  /** Absolute paths of the component source files. */
@@ -708,8 +678,6 @@ interface ExtractComponentsResult {
708
678
  * `Tabs.List` via static members or `Object.assign`).
709
679
  */
710
680
  export declare function extractComponents(options: ExtractComponentsOptions): ExtractComponentsResult;
711
- //#endregion
712
- //#region src/extract/cva.d.ts
713
681
  /** A `cva()` (class-variance-authority) or `tv()` (tailwind-variants) definition. */
714
682
  interface VariantDefinition {
715
683
  /** The variable it is assigned to, e.g. `buttonVariants`. */
@@ -721,8 +689,6 @@ interface VariantDefinition {
721
689
  }
722
690
  /** Finds every `const x = cva(base, { variants, defaultVariants })` in a file. */
723
691
  export declare function findVariantDefinitions(sourceFile: ts.SourceFile): Map<string, VariantDefinition>;
724
- //#endregion
725
- //#region src/extract/docs.d.ts
726
692
  interface ParsedDoc {
727
693
  file: string;
728
694
  title?: string;
@@ -739,8 +705,6 @@ interface ParsedDoc {
739
705
  export declare function parseDoc(text: string, file: string): ParsedDoc;
740
706
  /** Attaches docs and their examples to components. Returns warnings for unmatched pages. */
741
707
  export declare function attachDocs(components: ComponentInfo[], docs: ParsedDoc[]): string[];
742
- //#endregion
743
- //#region src/catalog.d.ts
744
708
  /** One project's design system at a workspace root. */
745
709
  interface ServedProject {
746
710
  ds: DesignSystem;
@@ -809,8 +773,6 @@ declare class Catalog {
809
773
  query?: string | undefined;
810
774
  }): CatalogToken[];
811
775
  }
812
- //#endregion
813
- //#region src/server/index.d.ts
814
776
  export declare 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.\nWhen writing or changing UI in this project:\n1. Find components with search_components (by intent) or list_components.\n2. Read get_component before using a component: use only the props and variant values it lists.\n3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette. chart-* and sidebar-* tokens are for charts and the sidebar only: for a status color the theme has no token for (success, warning, info), add a token or keep the Tailwind color as check_ui says, and never use a chart-* or sidebar-* token for it.\n4. Run check_ui on every snippet or file you write and fix all errors before you finish.\nIn a monorepo served from its root, pass `path` (the file you are editing) to list_components, get_component, search_components and get_tokens, and `path` or `filename` to check_ui, so each answer comes from that file's own project.";
815
777
  interface CreateServerOptions {
816
778
  /**
@@ -827,8 +789,6 @@ interface CreateServerOptions {
827
789
  }
828
790
  /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
829
791
  export declare function createServer(options: CreateServerOptions): McpServer;
830
- //#endregion
831
- //#region src/server/stdio.d.ts
832
792
  interface ServeOptions extends LoadConfigOptions {
833
793
  cwd: string;
834
794
  cache: boolean;
@@ -860,12 +820,18 @@ interface ServerHandle {
860
820
  * project marker, or the monorepo root above that, served as if the server had been
861
821
  * started there, except that a config there is read only when it is JSON: a prompt can
862
822
  * choose the path, so a JavaScript or TypeScript config is not run. Each root found is
863
- * loaded once and kept; a request without an absolute path is answered from the one root
864
- * found so far.
823
+ * loaded once and kept, by its real path; a request without an absolute path is answered from
824
+ * the one root found so far. The same holds for a `--root` (or a client's root) that is no
825
+ * project but holds projects in its folders, such as a folder of repositories, except that only
826
+ * an absolute path in that folder is answered, from the project in it that holds the file (never
827
+ * from a folder outside or above it), and a file in none of them from that folder, as before.
828
+ * Links are resolved: a path, or the project it leads to, that is really outside the folder is
829
+ * refused, a workspace in it serves only its packages that are really in it, and a project in it
830
+ * that a link replaces while the server runs is not built again from where the link leads.
831
+ *
832
+ * A load that fails, such as on an invalid config, is not kept: the next request loads
833
+ * again, so fixing the config is enough, also when it was invalid at startup.
865
834
  */
866
835
  export declare function serveStdio(options: ServeOptions): Promise<ServerHandle>;
867
- //#endregion
868
- //#region src/version.d.ts
869
836
  export declare const VERSION: string;
870
- //#endregion
871
837
  export { type CheckResult, type ComponentDocs, type ComponentInfo, type CompoundVariantInfo, type Config, type DesignSystemModel, type Diagnostic, type DocSection, type ExampleInfo, type HostOptions, type InheritedProps, type LoadConfigOptions, type LoadOptions, type OutputFormat, type PropInfo, RULE_IDS, type ResolvedConfig, type RuleId, type Severity, type SourceLocation, type TOKEN_CATEGORIES, type TextEdit, type Token, type TokenCategory, type VariantInfo, configJsonSchema, defineConfig, loadConfig };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, dt as findVariantDefinitions, f as searchComponents, it as defineConfig, m as checkSource, n as DesignSystemHost, nt as RULE_IDS, o as loadDesignSystem, p as applyFixes, r as buildModel, rt as configJsonSchema, st as loadConfig, t as DesignSystem, y as VERSION } from "./design-system-CPH4C7-1.js";
2
- import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-BK2kMN1x.js";
1
+ import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, at as defineConfig, ct as loadConfig, f as searchComponents, ft as findVariantDefinitions, it as configJsonSchema, m as checkSource, n as DesignSystemHost, o as loadDesignSystem, p as applyFixes, r as buildModel, rt as RULE_IDS, t as DesignSystem, y as VERSION } from "./design-system-rl__N6tw.js";
2
+ import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-CkczPt_1.js";
3
3
  export { DesignSystem, DesignSystemHost, INSTRUCTIONS, RULES, RULE_IDS, TokenIndex, VERSION, applyFixes, attachDocs, buildModel, checkSource, configJsonSchema, createServer, defineConfig, extractComponents, findVariantDefinitions, fingerprint, formatDiagnostics, loadConfig, loadDesignSystem, loadTokens, mergeTokens, parseCssTokens, parseDesignMd, parseDoc, parseDtcgTokens, searchComponents, serveStdio, toDesignMd, toDtcg };
@@ -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-rl__N6tw.js";
2
+ import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-INef0Yqt.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
@@ -151,8 +151,7 @@ function packageSteps(config, cwd) {
151
151
  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
152
  }];
153
153
  }
154
- //#endregion
155
- //#region src/not-read.ts
154
+ // src/not-read.ts
156
155
  const LIMITS_DOCS = "https://onsystem.vercel.app/docs/how-it-works#limits";
157
156
  /**
158
157
  * What `check` and `check_ui` could not read, said after the results as the notice for missing
@@ -228,8 +227,7 @@ function listOf$2(items, max = 3) {
228
227
  if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
229
228
  return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
230
229
  }
231
- //#endregion
232
- //#region src/server/render.ts
230
+ // src/server/render.ts
233
231
  /**
234
232
  * Tool results are read by models, so they are rendered as compact Markdown:
235
233
  * fewer tokens than JSON, and the same information. The JSON is still sent
@@ -1080,8 +1078,7 @@ function renderCheck(result, capped = capDiagnostics(result, Infinity), notices
1080
1078
  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
1079
  return answer(lines);
1082
1080
  }
1083
- //#endregion
1084
- //#region src/server/index.ts
1081
+ // src/server/index.ts
1085
1082
  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
1083
  When writing or changing UI in this project:
1087
1084
  1. Find components with search_components (by intent) or list_components.
@@ -1822,8 +1819,7 @@ function register(serve, { getDesignSystem, getProjects }) {
1822
1819
  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
1820
  });
1824
1821
  }
1825
- //#endregion
1826
- //#region src/catalog.ts
1822
+ // src/catalog.ts
1827
1823
  /**
1828
1824
  * Every project's components and tokens in one list, for a workspace root when no path
1829
1825
  * picks a project. A component used by several projects (a design-system package) is
@@ -1965,8 +1961,7 @@ function packageOwner(root) {
1965
1961
  return name;
1966
1962
  };
1967
1963
  }
1968
- //#endregion
1969
- //#region src/server/projects.ts
1964
+ // src/server/projects.ts
1970
1965
  /**
1971
1966
  * A workspace root's projects for the MCP server: one `DesignSystemHost` per project,
1972
1967
  * created on first use and watched from then on, and the merged catalog, rebuilt when a
@@ -2025,6 +2020,7 @@ var WorkspaceProjects = class {
2025
2020
  host = new DesignSystemHost(project.config, {
2026
2021
  cache: this.options.cache,
2027
2022
  logger: this.options.logger,
2023
+ build: this.options.build,
2028
2024
  loadConfig: () => this.workspace.configFor(project)
2029
2025
  });
2030
2026
  host.onChange(() => this.options.onChange?.());
@@ -2033,8 +2029,7 @@ var WorkspaceProjects = class {
2033
2029
  return host;
2034
2030
  }
2035
2031
  };
2036
- //#endregion
2037
- //#region src/server/stdio.ts
2032
+ // src/server/stdio.ts
2038
2033
  /**
2039
2034
  * Starts the server (on stdio unless another transport is given), for clients on
2040
2035
  * either protocol era: a 2026-07-28 client (stateless, `server/discover`) and a
@@ -2054,8 +2049,17 @@ var WorkspaceProjects = class {
2054
2049
  * project marker, or the monorepo root above that, served as if the server had been
2055
2050
  * started there, except that a config there is read only when it is JSON: a prompt can
2056
2051
  * 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.
2052
+ * loaded once and kept, by its real path; a request without an absolute path is answered from
2053
+ * the one root found so far. The same holds for a `--root` (or a client's root) that is no
2054
+ * project but holds projects in its folders, such as a folder of repositories, except that only
2055
+ * an absolute path in that folder is answered, from the project in it that holds the file (never
2056
+ * from a folder outside or above it), and a file in none of them from that folder, as before.
2057
+ * Links are resolved: a path, or the project it leads to, that is really outside the folder is
2058
+ * refused, a workspace in it serves only its packages that are really in it, and a project in it
2059
+ * that a link replaces while the server runs is not built again from where the link leads.
2060
+ *
2061
+ * A load that fails, such as on an invalid config, is not kept: the next request loads
2062
+ * again, so fixing the config is enough, also when it was invalid at startup.
2059
2063
  */
2060
2064
  function serveStdio$1(options) {
2061
2065
  const { logger } = options;
@@ -2076,15 +2080,28 @@ function serveStdio$1(options) {
2076
2080
  /**
2077
2081
  * The project at `root` (the working directory when undefined), or a monorepo root's
2078
2082
  * 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.
2083
+ * of the projects in it, are read only when they are JSON, never run. `within`: the folder of
2084
+ * projects it was found in, for the error on a config that is not JSON, and outside which no
2085
+ * workspace package is served, and no project or package is built.
2080
2086
  */
2081
- const load = async (root, jsonConfigOnly = false) => {
2087
+ const load = async (root, jsonConfigOnly = false, within) => {
2082
2088
  const configOptions = {
2083
2089
  ...options,
2084
2090
  root,
2085
- jsonConfigOnly
2091
+ jsonConfigOnly,
2092
+ jsonConfigWithin: jsonConfigOnly ? within?.dir : void 0
2086
2093
  };
2087
- const { config, workspace } = await loadTarget(configOptions);
2094
+ const target = await loadTarget(configOptions);
2095
+ const { config } = target;
2096
+ let { workspace } = target;
2097
+ if (workspace && within) {
2098
+ const outside = workspace.projects.filter((p) => !isInside(within.real, realPath(p.root)));
2099
+ if (outside.length) {
2100
+ 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)}.`);
2101
+ workspace = workspace.without(outside.map((p) => p.root));
2102
+ }
2103
+ }
2104
+ const build = within ? buildWithin(within) : void 0;
2088
2105
  for (const text of config.deprecations ?? []) logger.warn(text);
2089
2106
  for (const text of (workspace ? workspace.warnings : config.warnings) ?? []) logger.warn(text);
2090
2107
  if (workspace) {
@@ -2092,7 +2109,8 @@ function serveStdio$1(options) {
2092
2109
  cache: options.cache,
2093
2110
  watch: options.watch,
2094
2111
  logger,
2095
- onChange: notify
2112
+ onChange: notify,
2113
+ build
2096
2114
  });
2097
2115
  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
2116
  closers.push(() => {
@@ -2104,7 +2122,8 @@ function serveStdio$1(options) {
2104
2122
  const host = new DesignSystemHost(config, {
2105
2123
  cache: options.cache,
2106
2124
  logger,
2107
- loadConfig: () => loadConfig(configOptions)
2125
+ loadConfig: () => loadConfig(configOptions),
2126
+ build
2108
2127
  });
2109
2128
  host.onChange(notify);
2110
2129
  if (options.watch) host.watch();
@@ -2116,44 +2135,85 @@ function serveStdio$1(options) {
2116
2135
  });
2117
2136
  return { host };
2118
2137
  };
2138
+ /**
2139
+ * The roots loaded or loading, by `key` (the real path of `dir`, for a root found from a path).
2140
+ * One that fails is logged and dropped, so the next request loads it again, and until then it
2141
+ * is no project found.
2142
+ */
2143
+ const found = /* @__PURE__ */ new Map();
2144
+ const loadOnce = (key, dir, start) => {
2145
+ const known = found.get(key);
2146
+ if (known) return known.served;
2147
+ const served = start();
2148
+ found.set(key, {
2149
+ dir,
2150
+ served
2151
+ });
2152
+ served.catch((error) => {
2153
+ logger.error(error.message);
2154
+ if (found.get(key)?.served === served) found.delete(key);
2155
+ });
2156
+ return served;
2157
+ };
2119
2158
  let started = false;
2120
2159
  /** One project for every request: the one at `root`, or the working directory. */
2121
2160
  const serveOne = (root) => {
2122
2161
  if (started) return;
2123
2162
  started = true;
2124
- const served = load(root);
2125
- served.catch((error) => {
2126
- logger.error(error.message);
2127
- });
2128
- decide(() => served);
2163
+ const dir = path.resolve(options.cwd, root ?? ".");
2164
+ const served = () => loadOnce(dir, dir, () => load(root));
2165
+ served();
2166
+ decide(served);
2129
2167
  };
2130
- /** Each request's project, from the absolute path it names. */
2131
- const serveByPath = () => {
2168
+ /**
2169
+ * Each request's project, from the absolute path it names. `within`: a folder that is no
2170
+ * project but holds some (the `--root` or client root the server was started for), which
2171
+ * answers for a file in none of them. Then only a path in it is answered, from a project in
2172
+ * it: never from a folder outside or above it, links resolved, nor from a relative path, which
2173
+ * the tools would read against whichever project answers.
2174
+ */
2175
+ const serveByPath = (within) => {
2132
2176
  if (started) return;
2133
2177
  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();
2178
+ const folder = within === void 0 ? void 0 : {
2179
+ dir: within,
2180
+ real: realPath(within)
2181
+ };
2182
+ 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`;
2183
+ logger.info(`${where}: finding each request's project from the absolute path it names`);
2184
+ 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
2185
  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;
2186
+ const named = target !== void 0 && target !== "";
2187
+ 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}`);
2188
+ if (named && path.isAbsolute(target)) {
2189
+ const file = path.resolve(target);
2190
+ 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>.`);
2191
+ const up = findProjectRoot(file);
2192
+ const dir = within && !(up && isInside(within, up)) ? projectDirOf(file, within) : up;
2193
+ 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.`);
2194
+ const real = realPath(dir);
2195
+ 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>.`);
2196
+ if (!found.has(real)) logger.info(`using ${dir}, the project of ${target}`);
2197
+ return loadOnce(real, dir, () => load(dir, dir !== within, folder));
2150
2198
  }
2151
2199
  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>.`);
2200
+ if (only && !others.length) return only.served;
2201
+ 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
2202
  });
2155
2203
  };
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);
2204
+ /** Flags that describe the project themselves: the folder they apply to is the one served. */
2205
+ const described = options.config !== void 0 || Boolean(options.components?.length || options.tokens?.length || options.docs?.length);
2206
+ /**
2207
+ * The project at `root` (the working directory when undefined). A folder that is no project but
2208
+ * holds projects (a folder of repositories, which `--root ${workspaceFolder}` names when the
2209
+ * editor has their parent open) would mix their tokens: each request is answered from its path.
2210
+ */
2211
+ const serveAt = (root) => {
2212
+ const dir = root === void 0 ? void 0 : path.resolve(options.cwd, root);
2213
+ if (dir !== void 0 && !described && holdsOnlyProjects(dir)) serveByPath(dir);
2214
+ else serveOne(root);
2215
+ };
2216
+ 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
2217
  /**
2158
2218
  * When nothing else says where the project is: the working directory when flags
2159
2219
  * (`--components`, `--tokens`, `--docs`) describe it, else each request's path.
@@ -2189,7 +2249,7 @@ function serveStdio$1(options) {
2189
2249
  return;
2190
2250
  }
2191
2251
  if (root !== options.cwd) logger.info(`using client root ${root}`);
2192
- serveOne(root);
2252
+ serveAt(root);
2193
2253
  });
2194
2254
  };
2195
2255
  return server;
@@ -2206,6 +2266,49 @@ function serveStdio$1(options) {
2206
2266
  await handle.close();
2207
2267
  } });
2208
2268
  }
2269
+ /**
2270
+ * `buildModel` for a project found in the folder `within`, which refuses to build it once its
2271
+ * folder leads outside `within` (a project or workspace package replaced by a link while the
2272
+ * server watches it): that rebuild fails, so the model built from inside the folder stays.
2273
+ */
2274
+ function buildWithin(within) {
2275
+ return async (config, options) => {
2276
+ 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.`);
2277
+ return buildModel(config, options);
2278
+ };
2279
+ }
2280
+ /**
2281
+ * Whether `dir` is no project (no project marker, no workspace file) but a folder in it, or in one
2282
+ * of its folders, is one. Dot folders and `node_modules` are not looked in.
2283
+ */
2284
+ function holdsOnlyProjects(dir) {
2285
+ if (hasProjectMarker(dir) || isWorkspaceRoot(dir)) return false;
2286
+ const folders = (from) => {
2287
+ try {
2288
+ return fs.readdirSync(from, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => path.join(from, e.name));
2289
+ } catch {
2290
+ return [];
2291
+ }
2292
+ };
2293
+ const below = folders(dir);
2294
+ return below.some(hasProjectMarker) || below.some((f) => folders(f).some(hasProjectMarker));
2295
+ }
2296
+ /**
2297
+ * Where `p` really is: links resolved, in the letter case the file system keeps
2298
+ * (`realpathSync.native`; plain `realpathSync` keeps the case given on macOS). A path that does
2299
+ * not exist yet, such as the `filename` of new code, from the nearest folder above it that does.
2300
+ */
2301
+ function realPath(p) {
2302
+ const rest = [];
2303
+ for (let dir = path.resolve(p);;) try {
2304
+ return path.join(fs.realpathSync.native(dir), ...rest);
2305
+ } catch {
2306
+ const parent = path.dirname(dir);
2307
+ if (parent === dir) return path.resolve(p);
2308
+ rest.unshift(path.basename(dir));
2309
+ dir = parent;
2310
+ }
2311
+ }
2209
2312
  /** First `file://` root the client reports, if it supports roots (deprecated in 2026-07-28). */
2210
2313
  async function clientRoot(server) {
2211
2314
  if (!server.server.getClientCapabilities()?.roots) return void 0;
@@ -2217,5 +2320,4 @@ async function clientRoot(server) {
2217
2320
  return;
2218
2321
  }
2219
2322
  }
2220
- //#endregion
2221
2323
  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-rl__N6tw.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.2",
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": [