Skip to content

onsystem@0.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 15:10
· 417 commits to main since this release
b384c68

Minor Changes

  • #45 d43bb12 Thanks @dgesteves! - check lints what you ship, and check_ui answers in a size an agent can read.

    • In folders and globs, check leaves out tests and stories (*.test.tsx, *.spec.tsx, *.stories.tsx; --include-tests checks them), what the config's exclude lists, and what git ignores. A file named outright is always checked. The summary line counts what was left out.
    • Files that render an image or an email are not checked, since design tokens and classes don't apply there: those importing next/og, @vercel/og, satori or @react-email/*, Next.js opengraph-image, twitter-image, icon and apple-icon routes, and files in an og folder. --format json marks them skipped, and check_ui says why.
    • check_ui returns at most 50 diagnostics, errors first (limit sets another number, up to 1,000), with the total per rule and how many were left out, in both the text and structuredContent (omitted, byRule). A 3,000-line file used to come back as about 500,000 tokens.
  • #43 2cfaee2 Thanks @dgesteves! - Color fixes keep the color's meaning. A pale arbitrary value or literal is a hue, not a gray: its chroma is judged against its lightness, so bg-[#fef2f2] is no longer fixed to bg-muted. Hues must be within 15° (was 30°), and status colors (destructive, warning, success, info and their foregrounds) are only offered for nearly the same hue and never for a gray, so a brand-orange button is no longer fixed to bg-destructive with a hint to use variant="destructive". Token families that at most one design-system file uses (syntax-highlighting colors, a chart palette, one screen's background) are suggested only there, like sidebar-* and chart-*. Tokens built on Tailwind's palette (--primary: var(--color-neutral-800), as in coss/ui) now resolve, so no-hardcoded-color runs on those projects, where it used to report "No color tokens found".

  • #64 5c6b1c6 Thanks @dgesteves! - Zero config finds design systems it used to miss, and checks each import against the component it leads to.

    • Built packages. An exports target in dist/ (or build/, lib/) is read from its source: through the entry of a tsup, tsdown or Vite config, read as text and never run, else the same path under src/, and . from src/index.*. Dub's @dub/ui, built by tsup, used to give "No design system found"; it now gives 430 components.
    • Barrels. An export that leads to an index.ts stands for the .tsx files it re-exports, through export *, export { … } from and imported bindings exported again, within the package. @calcom/ui goes from 4 components to 110.
    • Several design systems. Workspace design-system packages are added to what components.json finds instead of being skipped for it: openstatus's dashboard gets @openstatus/ui next to its own components/ui, and Cal.com's app @calcom/ui next to @coss/ui. The one the app's code imports most is the primary: its components win a shared name and are the ones suggested for native elements. inspect shows each with its import count.
    • Imports. A renamed re-export (export { Table as TableNew }) is checked against the component declared under its original name, and a module namespace (export * as RadioAreaGroup) is no longer taken for a component. A component file a package does not export is left unchecked rather than called invented. A generic component whose props depend on its type argument (Cal.com's Skeleton<T>) has open props. Members written as property accesses (Object.assign(Tooltip, { Root: TooltipPrimitive.Root })) are extracted.
    • prefer-design-system-component leaves alone the asChild child of a component with classes of its own (<SidebarMenuButton asChild><button>), a bare <button />, <a /> or <label /> passed to render into, an element styled by a cva or tv definition in its own file, and an <input> whose every attribute is spread in (react-dropzone's hidden file input).
    • check leaves out files in __tests__, __stories__ and __mocks__ folders by default, as it does *.test.tsx and *.stories.tsx.
  • #46 2252ef0 Thanks @dgesteves! - The Claude Code hook speaks up when it can't check, and stays quiet where there is nothing to check against.

    • When the CLI can't run (npx can't fetch it because of a min-release-age policy, a private registry or no network, or the config is broken), the hook says so once per session and project, on stderr and as a message shown to you, with the reason and what to do. It never blocks the edit, and Claude is not asked to fix it. It used to exit silently, so edits looked checked when they weren't.
    • check --quiet-without-design-system, which the hook uses, now stays quiet in any project without design-system components or a config. A few custom properties are not a design system: in an app styled with CSS-in-JS, such as Twenty, three stray tokens made the hook block Claude on findings with nothing to compare against.
  • #65 22fe3ab Thanks @dgesteves! - inspect --explain says why, and init writes the config.

    • inspect --explain prints every candidate zero config looked at (components.json, the package's exports, each dependency named like a design system, a flat src/) and why it was taken or turned down (the ui alias ~/ui resolves through neither tsconfig paths nor a workspace package, not a workspace package: an installed package is compiled), the resolved config (globs, tokens, docs, import paths, rules and overrides), tokens whose value reads a custom property no token file defines, and components whose props did not resolve, saying when that is because the dependencies are not installed. At a monorepo root it says why the root is read as one, then explains each project.
    • onsystem init writes onsystem.config.json from what zero config found, or from the answers to up to three questions (where the components are, where the tokens are, how apps import them) when it found nothing. It then runs check . once without failing, prints the findings by rule, and offers to record them in onsystem.baseline.json for an existing codebase. --yes writes without asking and records the baseline; --components and --tokens answer for it in a script; an existing config is left alone without --force.
  • #66 e8f965d Thanks @dgesteves! - The MCP server speaks the 2026-07-28 protocol revision as well as the 2025 one, on the same stdio command.

    • Both eras. A client's first message picks the era: server/discover or a request with the 2026-07-28 _meta gets the stateless revision, and an initialize request gets the 2025 handshake, served as before. On 2026-07-28, server/discover returns the supported versions, capabilities, serverInfo and instructions; every result carries resultType; list results carry ttlMs and cacheScope (the tools, prompts and resource templates for an hour, the component resources not at all, all private); tools/list keeps one order; and an unsupported version gets -32022 naming the supported ones.
    • Finding the project. --root, a config file in the working directory, or the working directory itself when it holds a project (a package.json or components.json) come first. MCP roots, deprecated in 2026-07-28, are the fallback, for a 2025-era client that starts the server outside any project.
    • SDK. The server moves from @modelcontextprotocol/sdk 1.x to @modelcontextprotocol/server 2.x, the SDK line that implements 2026-07-28. Its install no longer brings the SDK's HTTP stack (Express, Hono). check_ui's output schema describes a fix's range as an array of two integers rather than a tuple, which clients that validate structured content as draft-07 rejected.
    • For library users: createServer returns the v2 SDK's McpServer, and serveStdio resolves to a handle with close() rather than to the McpServer.
  • #63 de818cf Thanks @dgesteves! - onsystem works from a monorepo root, where Claude Code and Cursor sessions usually start. It used to find nothing there.

    • Projects. At a workspace root (pnpm, npm, Yarn and Bun workspaces, Lerna, Nx; Turborepo through its package manager's workspaces) with no design system of its own, every workspace package goes through zero config or its own config, and each one with components is a project: the apps that use a design system and the design-system packages themselves. inspect lists each project with what it found, and the packages without one.
    • check . at the root checks each file against its own project's design system: the nearest folder with a config, a components.json, a package.json or a project.json, the folder the Claude Code hook runs the check from. It reports what check . in each of those folders reports, file for file, skips each folder's own design-system files, and keeps one baseline at the root. --require-design-system there fails when no package has components and color tokens. A root config that does not set components applies to every project without a config of its own, with its globs relative to the root.
    • MCP tools at the root. list_components, get_component, search_components and get_tokens take an optional path (the file being edited, or a project folder) and answer for that project; check_ui takes the project from path, or from filename for code. Without a path they list every project together, each component and token tagged with its package and the projects that use it, and a name two packages share (Button in @calcom/ui and @coss/ui) comes back as a list with the path that picks each.
    • Imports are followed to their declaration. A component imported through barrels (export { Button } from "./Button", export *) is checked against the one declared where the import leads, so with two design systems in one project, <Button> from @calcom/ui is no longer checked against @coss/ui's Button, and Dub's CardList.Card (an Object.assign member in a barrel) is no longer reported as missing.
    • prefer-design-system-component leaves alone elements kept from the user: hidden, aria-hidden, or out of the tab order and invisible, like the input that carries a custom select's required.
    • The Claude Code hook also treats a folder with an Nx project.json as a project.
  • #50 4b869b0 Thanks @dgesteves! - @dgesteves/design-system-mcp is now onsystem, and this is its first release under that name. Everything that had the old name moves, and what you already have keeps working until you rename it (migration guide):

    • Package and command: npm install --save-dev onsystem, npx onsystem check .. The old package gets no new releases.
    • Config file: onsystem.config.json (or .ts, .mts, .js, .mjs). A design-system-mcp.config.* is still read, with a notice that says how to rename it; the schema is at https://unpkg.com/onsystem/schema.json, and a code config imports defineConfig from onsystem.
    • Baseline: onsystem.baseline.json. A design-system-mcp.baseline.json is still read and updated where it is, with the same notice.
    • MCP: the server calls itself onsystem, and the documented client configs use onsystem as the server's key, so tools appear as mcp__onsystem__check_ui and the like. The MCP Registry entry is io.github.dgesteves/onsystem.
    • Claude Code plugin: the plugin is onsystem in the same dgesteves marketplace, and it runs an exact version of the package (onsystem@<version>) rather than a range. Replace the old one with /plugin uninstall design-system@dgesteves, /plugin marketplace update dgesteves and /plugin install onsystem@dgesteves. The skill is /onsystem:onsystem.
    • Smaller things: the cache moved to node_modules/.cache/onsystem, log lines start with [onsystem], GitHub annotations are titled onsystem, and the hook's test override is ONSYSTEM_BIN (DESIGN_SYSTEM_MCP_BIN still works, with a notice).
  • #44 2db9195 Thanks @dgesteves! - Fewer false positives from the component rules, measured on the real-world corpus:

    • prefer-design-system-component suggests a component for a native element only when the component is named for it (Button, Link, Input, TextField, Label, Select, Textarea, Image, Separator…), or when it renders the element and needs nothing the element does not (no required props of its own, and the element's attributes). <button> no longer becomes <DataTableColumnHeader>, <label> no longer becomes <FileUpload>, and <img> no longer becomes <Avatar>. Inside a component of the same name (a design system's own Table around a <table>), the element is left alone. <hr> now suggests a Separator.
    • icon-button-accessible-name counts a child as an icon only when it looks like one: imported from an icon library or an icons module, or named like an icon (XIcon, Trash2). A child with text in its props (<IconMenu text="Export">) or an unknown component makes it skip. A conditional render={cond ? undefined : <button />} is followed, a rendered button's own children count, and a story's args.render is skipped.
    • no-unknown-component no longer calls a component invented when it comes from a module the model left out (an excluded folder, a barrel that re-exports one, a package built to dist/). check and check_ui say once which components were not checked, and --format json lists them under unchecked. Members of Object.assign(Root, { Title, Close }) are linked even when they are not exported, a same-named component from another file is no longer taken for the model's, and a tag a local declaration shadows is left alone.
    • cva/tv variants come from the definitions on the component (VariantProps<typeof x>, or the className of the element it returns). A definition used further in no longer adds classes to its variants: documenso's Button size no longer includes its spinner's h-5 w-5.
  • #47 ad39127 Thanks @dgesteves! - Findings can be suppressed where they are intended. // onsystem-disable-next-line <rule>[, <rule>] covers the next line ({/* … */} in JSX), // onsystem-disable-line <rule> its own line, and /* onsystem-disable [<rule>] */ the rest of the file; text after -- explains why. A misspelled rule name is reported as a suppression warning with the closest one. In the config, overrides set rules for the files that match globs, such as [{ "files": ["app/legacy/**"], "rules": { "no-hardcoded-color": "warn" } }], validated like rules; check keeps baseline entries of rules an override turns off.

Patch Changes

  • #51 56167ba Thanks @dgesteves! - The README on npm and GitHub is one screen: what onsystem is for, what it checks with its measured false-positive rate, a quickstart for the Claude Code plugin, MCP clients and CI, and the benchmark. Client configs, the config and rules references, how it works and a new troubleshooting page (nothing found, monorepo roots, components with 0 props, min-release-age policies) are on the website and in docs/. The package description and keywords say what it does now: it keeps coding agents on your design system at the edit and in CI, with the MCP server alongside.

  • #42 68a8ff3 Thanks @dgesteves! - Every change to the linter is now checked against a real-world corpus: 10 public repositories pinned by commit (Documenso, Dub, Cal.com, openstatus, Twenty, midday, Vercel's chatbot, shadcn/ui's website, Next.js SaaS Starter and React Aria's Tailwind starter), with findings counted per rule and a hand-labelled sample whose false-positive rate is published with each release. Nothing changes in the package itself.

  • #48 0293e92 Thanks @dgesteves! - Small fixes that make the package say what is true:

    • engines asks for Node.js 20.19 or later, which the CLI and the server run on; a TypeScript config file still needs 22.18, and on an older Node.js it now says so plainly and points to .json or .mjs.
    • get_component has an output schema, like every other tool, and integer fields no longer carry ±9007199254740991 bounds in the tool definitions.
    • get_tokens in a project without tokens says that none were found and how to configure them, instead of suggesting to call it without filters; with filters that match nothing, it lists the categories there are.
    • isInvalid on a native field suggests aria-invalid, not onInvalid.
  • #67 faa0b31 Thanks @dgesteves! - Checked on Windows and macOS, and as an installed package.

    • On Windows, check linted files git ignores: the paths git check-ignore returned never matched the ones it globbed (backslashes against forward slashes). They are left out now, as on Linux and macOS.
    • The Claude Code hook names files with forward slashes on Windows too, as the CLI does (app/page.tsx, not app\page.tsx).
    • CI runs the test suite on Windows and macOS as well as Linux, and on all three packs the package, installs the tarball into a new project, and runs the installed CLI (inspect, check), the MCP server over stdio in both protocol eras, and the hook against it.