Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ To keep what it found in a file you can edit, run `npx -y onsystem init`: it wri

## Hold your agent to it

Pick one:
Give your agent this prompt, and it does the rest of the setup itself: the config and the baseline, onsystem in the agent, and the CI check. [setup.md](https://onsystem.vercel.app/setup.md) has the steps it follows.

```text title="Prompt for your agent"
Read https://onsystem.vercel.app/setup.md and set up onsystem in this project.
```

Or add it yourself. Pick one:

- **Claude Code**: install the [plugin](plugin.md). Its hook checks every UI file Claude writes and hands the errors back before Claude moves on; it also brings the MCP server and a skill that has Claude look components up first.

Expand Down
55 changes: 43 additions & 12 deletions site/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { InlineCode, Section, SubSection, TextLink } from '@/components/section'
import { Tabs } from '@/components/tabs';
import { count } from '@/lib/benchmark';
import { benchmark, corpus, demo, readme, rules, tools } from '@/lib/data';
import { SETUP_PROMPT } from '@/lib/llms';
import { pageMetadata } from '@/lib/metadata';
import { findingCard, HOME_CARD, TSC_RULES } from '@/lib/og';
import { highlight } from '@/lib/render-code';
import {
AUTHOR,
CODEX_COMMANDS,
CURSOR_INSTALL,
docsHref,
NPM,
Expand Down Expand Up @@ -105,9 +107,7 @@ function GetStarted() {
/** What a visitor runs first: findings with fixes on their own app, nothing installed or written. */
const CHECK = 'npx -y onsystem check .';

/** The commands from docs/clients.md and docs/required-check.md (test/first-screen.test.ts checks). */
const CODEX =
'codex plugin marketplace add dgesteves/onsystem\ncodex plugin add onsystem@dgesteves';
/** The step from docs/required-check.md (test/first-screen.test.ts checks). */
const ACTION = '- uses: dgesteves/onsystem@v0';
const MARKETPLACE = 'https://github.com/marketplace/actions/onsystem-design-system-check';

Expand Down Expand Up @@ -148,13 +148,17 @@ function Hero() {
</TextLink>
.
</p>
<Link
href="/playground"
className="group mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-fg-soft transition-colors hover:text-fg"
>
Try it in the browser
<ArrowRightIcon className="size-4 text-subtle transition-transform group-hover:translate-x-0.5 group-hover:text-cyan" />
</Link>
{/* One row, so wide screens are no taller; it wraps on a phone. */}
<div className="mt-3 flex flex-wrap items-center gap-x-6 gap-y-2">
<Link
href="/playground"
className="group inline-flex items-center gap-1.5 text-sm font-medium text-fg-soft transition-colors hover:text-fg"
>
Try it in the browser
<ArrowRightIcon className="size-4 text-subtle transition-transform group-hover:translate-x-0.5 group-hover:text-cyan" />
</Link>
<AgentPrompt />
</div>
</div>
<FindingCard className="lg:col-start-2 lg:row-span-3 lg:row-start-1 lg:mt-1.5" />
<InstallTabs />
Expand Down Expand Up @@ -230,6 +234,33 @@ function CheckCommand() {
);
}

/**
* The prompt that has the visitor's agent set onsystem up from /setup.md. The label makes its
* text part of the copy button: a click on either copies the prompt, which the title shows.
* Touch screens show no title, so below sm the prompt itself is on the line under it. From sm
* up the control stays on one line, so the wide first screen is no taller.
*/
function AgentPrompt() {
return (
<label
data-agent-prompt=""
title={SETUP_PROMPT}
className="group inline-flex cursor-pointer flex-wrap items-center gap-x-1 gap-y-0.5 text-sm font-medium text-fg-soft transition-colors hover:text-fg"
>
Or ask your agent to set it up
<CopyButton
text={SETUP_PROMPT}
label={`Copy the prompt for your agent: ${SETUP_PROMPT}`}
bare
className="-my-1 group-hover:text-fg"
/>
<span data-agent-prompt-text="" className="basis-full font-normal text-muted sm:hidden">
“{SETUP_PROMPT}”
</span>
</label>
);
}

/**
* Three of the demo draft's findings as the agent gets them, with the line it wrote, and what
* `tsc` reports of the whole draft. The social card draws the same card (lib/og.tsx).
Expand Down Expand Up @@ -325,7 +356,7 @@ function InstallTabs() {
label="Install for"
// No-break spaces keep each label on one line; the row fits beside the card from 1024 px.
labels={['Claude\u00a0Code', 'Cursor', 'VS\u00a0Code', 'Codex', 'Other\u00a0MCP', 'CI']}
copies={[PLUGIN_COMMANDS, undefined, undefined, CODEX, NPX, ACTION]}
copies={[PLUGIN_COMMANDS, undefined, undefined, CODEX_COMMANDS, NPX, ACTION]}
>
{[
<Fragment key="claude-code">
Expand Down Expand Up @@ -365,7 +396,7 @@ function InstallTabs() {
<TextLink href={docsHref('setup')}>Other agents</TextLink>
</>,
)}
<CodeBody code={CODEX} lang="sh" />
<CodeBody code={CODEX_COMMANDS} lang="sh" />
</Fragment>,
<Fragment key="other">
{note(
Expand Down
8 changes: 8 additions & 0 deletions site/app/setup.md/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { setupMd } from '@/lib/llms';

// The steps an agent follows to set onsystem up, written once per build from the docs.
export const dynamic = 'force-static';

export function GET() {
return new Response(setupMd(), { headers: { 'content-type': 'text/markdown; charset=utf-8' } });
}
20 changes: 18 additions & 2 deletions site/components/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'

import { Line } from '@/components/code-line';
import { FindingMessage } from '@/components/finding-message';
import { ArrowUpRightIcon, CheckIcon, ReplayIcon } from '@/components/icons';
import { ArrowRightIcon, ArrowUpRightIcon, CheckIcon, ReplayIcon } from '@/components/icons';
import type { CheckError, CheckResponse } from '@/lib/check-service';
import { highlightLines } from '@/lib/highlight';
import type { Mark } from '@/lib/segments';
Expand Down Expand Up @@ -302,6 +302,12 @@ export function Playground({ presets, maxBytes }: { presets: Preset[]; maxBytes:
: current.result.errorCount + current.result.warningCount === 0
? 'No design-system problems'
: `${plural(current.result.errorCount, 'error')} · ${plural(current.result.warningCount, 'warning')}`;
// Below lg the findings come after the editor, a long file further down: the status there is a
// link to them.
const found =
!empty && !problem && !pending && current
? current.result.errorCount + current.result.warningCount
: 0;

return (
<div>
Expand Down Expand Up @@ -344,10 +350,19 @@ export function Playground({ presets, maxBytes }: { presets: Preset[]; maxBytes:
{preset && code === preset.code ? preset.file : 'Your code'}
</label>
<span
className={`shrink-0 text-[11px] ${empty || pending || !current || problem ? 'text-subtle' : current.result.errorCount > 0 ? 'text-error' : current.result.warningCount > 0 ? 'text-warn' : 'text-success'}`}
className={`shrink-0 text-[11px] ${found > 0 ? 'max-lg:hidden' : ''} ${empty || pending || !current || problem ? 'text-subtle' : current.result.errorCount > 0 ? 'text-error' : current.result.warningCount > 0 ? 'text-warn' : 'text-success'}`}
>
{status}
</span>
{found > 0 && (
<a
href="#findings"
className={`tap inline-flex shrink-0 items-center gap-1 text-[11px] underline decoration-line-strong underline-offset-2 transition-colors hover:decoration-current lg:hidden ${current?.result.errorCount ? 'text-error' : 'text-warn'}`}
>
{`Jump to ${plural(found, 'finding')}`}
<ArrowRightIcon className="size-3 rotate-90" />
</a>
)}
</div>
<div
ref={editor}
Expand Down Expand Up @@ -412,6 +427,7 @@ export function Playground({ presets, maxBytes }: { presets: Preset[]; maxBytes:

{/* Findings */}
<section
id="findings"
aria-labelledby={`${ids}-findings`}
className="min-w-0 rounded-xl border border-line bg-raised lg:sticky lg:top-20 lg:max-h-[calc(100dvh-6rem)] lg:overflow-y-auto"
>
Expand Down
102 changes: 102 additions & 0 deletions site/lib/llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { faqMarkdown } from '@/lib/faq';
import { withoutMarkers } from '@/lib/markers';
import { playground } from '@/lib/playground';
import {
CODEX_COMMANDS,
NPM,
NPX,
PLUGIN_COMMANDS,
Expand All @@ -32,6 +33,105 @@ const pitch =
const details =
'It reads React components, props, cva variants and tokens from source with the TypeScript compiler. In Claude Code, a hook checks each file right after it is written and hands the errors back with their fixes; other agents run check_ui over MCP; the CI check gates pull requests. Static analysis, run locally: no model calls, no API key, zero config. It works alongside @shadcn/lint.';

/**
* What a visitor gives their agent to have it set onsystem up, as @shadcn/lint's quickstart gives
* one for its SETUP.md: the home page's first screen and step 2 of the quickstart offer it.
*/
export const SETUP_PROMPT = `Read ${SITE_URL}/setup.md and set up onsystem in this project.`;

/** The code of the block titled `title` in a docs page, so /setup.md shows what the docs show. */
function titled(markdown: string, title: string): { lang: string; code: string } {
const at = markdown.indexOf(` title="${title}"\n`);
const lang = /```(\w+)$/.exec(markdown.slice(0, at))?.[1];
if (at === -1 || !lang) throw new Error(`No code block titled ${title} in the docs`);
const start = markdown.indexOf('\n', at) + 1;
return { lang, code: markdown.slice(start, markdown.indexOf('\n```', start)) };
}

/** A fenced code block, indented to sit in a list item. */
function fence({ lang, code }: { lang: string; code: string }, indent = ''): string {
return [`\`\`\`${lang}`, ...code.split('\n'), '```'].map((line) => indent + line).join('\n');
}

/**
* /setup.md: the steps an agent follows to set onsystem up in the project it is working in. The
* configs and the workflow are the ones docs/rollout.md and docs/required-check.md give.
*/
export function setupMd(): string {
const config = (file: string) => fence(titled(docs.rollout, file), ' ');
const [marketplace, install] = PLUGIN_COMMANDS.split('\n');
const [codexMarketplace, codexInstall] = CODEX_COMMANDS.split('\n');
const node = readme.engines.node.replace('>=', '');
return `# Set up onsystem

These are the steps to set onsystem up in the user's project. onsystem keeps coding agents on the project's design system: it reads the components, their props and variants, and the tokens from source, gives the agent MCP tools to look them up and \`check_ui\` to check its own UI code, and checks pull requests in CI. Make only the changes below, and ask the user before anything else, such as fixing what it finds.

The docs are at ${SITE_URL}/docs, and all of them in one file at ${SITE_URL}/llms-full.txt.

## 1. Look at the project

- onsystem checks React (\`.tsx\` and \`.jsx\`) and needs Node.js ${node} or later. If the project has no React UI, stop and tell the user.
- Find the package manager from the lockfile, and whether this is one app or a monorepo. If the dependencies are not installed, install them first: without them, the props of components that wrap a library (Radix, React Aria) are not read.
- Set it up in the folder that CI will check: the app's folder, or the root of a monorepo, where each app and design-system package is checked against its own design system.

## 2. Write the config and record the baseline

In that folder, run:

\`\`\`sh
npx -y onsystem init --yes
\`\`\`

It writes \`onsystem.config.json\` from the components and tokens it finds, checks the project once, and records the findings already in the code in \`onsystem.baseline.json\`, so that the check fails only on new ones. Keep what it prints for step 5.

- If it says a config already exists, keep that config: do not pass \`--force\`. If there is no \`onsystem.baseline.json\` next to it, record the findings already in the code with \`npx -y onsystem check . --update-baseline\`.
- If it found no design system, run \`npx -y onsystem inspect --explain\` to see where it looked, ask the user where the components and tokens are, and run it again with them: \`npx -y onsystem init --yes --components "src/components/ui/**/*.tsx" --tokens src/app/globals.css\`.

## 3. Add it to your agent

Add onsystem to the agent you are running in, in files the repository keeps, so the whole team gets it. Merge into a file that is already there; do not replace it.

- **Claude Code**: the plugin, with the hook that checks each file Claude writes, the MCP server and the skill. \`.claude/settings.json\`:

${config('.claude/settings.json')}

Claude Code loads it the next time it starts in this folder, once the user accepts the folder's trust prompt. Do not add onsystem to \`.mcp.json\` as well: both register the same tools. To install it for the user alone instead, they run \`${marketplace ?? ''}\`, then \`${install ?? ''}\`.
- **Cursor**: the MCP server. \`.cursor/mcp.json\`:

${config('.cursor/mcp.json')}

- **VS Code**: the MCP server, for Copilot agent mode. \`.vscode/mcp.json\`:

${config('.vscode/mcp.json')}

- **Codex CLI**: the plugin, with the MCP server and the skill: \`${codexMarketplace ?? ''}\`, then \`${codexInstall ?? ''}\`.
- **Any other agent**: the MCP server runs over stdio as \`${NPX} --root <the project's absolute path>\`, and [Set up your agent](${SITE_URL}/docs/clients#other-clients) has the config for each client. If the project has an \`AGENTS.md\`, add this line to it:

${config('AGENTS.md')}

## 4. Check pull requests

On GitHub, add \`.github/workflows/onsystem.yml\`. It runs the GitHub Action, which checks the lines each pull request changes, leaves out the findings in the baseline, and fails on errors:

${fence(titled(docs.requiredCheck, '.github/workflows/onsystem.yml'))}

Install the dependencies the way the project does: with pnpm, add \`pnpm/action-setup\` before \`actions/setup-node\` and run \`pnpm install --frozen-lockfile\` ([Roll out to a team](${SITE_URL}/docs/rollout#gate-pull-requests)). When the folder from step 1 is not the root, give the action that folder as \`working-directory\` ([GitHub Action](${SITE_URL}/docs/required-check)). On another CI, run \`${NPX} check . --require-design-system\` in that folder.

## 5. Check it works, and report

1. Run \`${NPX} check . --require-design-system\` in the folder from step 1, and look at its exit code:
- 0: the check passes. The findings already in the code are in the baseline.
- 1: it found errors that are not in the baseline, such as ones in code written since it was recorded. Do not fix them: list them for the user in the next step.
- 2: it found no design system or could not read the config: go back to step 2.
2. Tell the user:
- the files you added or changed, to review and commit: \`onsystem.config.json\`, \`onsystem.baseline.json\`, and the agent's and CI's files;
- what the baseline holds (what \`init\` printed by rule, or the count \`check . --update-baseline\` printed), and that \`${NPX} check . --ignore-baseline\` lists each finding with its fix;
- with code 1, the findings the check printed, each with its fix;
- what is left for them to do, such as restarting the agent so it loads the plugin or the MCP server.
3. Leave the findings as they are unless the user asks you to fix them.
`;
}

/** llms.txt (https://llmstxt.org): what this is, how to install it, and where everything is. */
export function llmsTxt(): string {
const lines = [
Expand All @@ -43,6 +143,8 @@ export function llmsTxt(): string {
'',
`Version ${tools.version}, MIT. React (.tsx/.jsx), shadcn/ui or any design system, Tailwind v3 or v4, Node.js ${readme.engines.node.replace('>=', '')}+.`,
'',
`To set onsystem up in a project, follow ${SITE_URL}/setup.md: the config and baseline (\`npx -y onsystem init --yes\`), the agent's plugin or MCP config, and the CI check. A person can give their agent this prompt: "${SETUP_PROMPT}"`,
'',
'Install:',
'',
`- Claude Code (hook, MCP server and skill): \`${PLUGIN_COMMANDS.split('\n').join('` then `')}\``,
Expand Down
3 changes: 3 additions & 0 deletions site/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export function compactNumber(value: number): string {

export const NPX = 'npx -y onsystem';
export const PLUGIN_COMMANDS = readme.install.plugin;
/** The Codex CLI plugin, as docs/clients.md installs it (site/test/setup.test.ts checks). */
export const CODEX_COMMANDS =
'codex plugin marketplace add dgesteves/onsystem\ncodex plugin add onsystem@dgesteves';
export const CURSOR_INSTALL = readme.install.cursor;
export const VSCODE_INSTALL = readme.install.vscode;

Expand Down
34 changes: 29 additions & 5 deletions site/test/first-screen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import Home, { metadata } from '@/app/page';
import { STEPS } from '@/components/loop-demo';
import { slug } from '@/components/markdown-blocks';
import { benchmark, corpus, demo, designSystem } from '@/lib/data';
import { SETUP_PROMPT } from '@/lib/llms';
import { HOME_CARD, HomeCard } from '@/lib/og';
import type { Preset } from '@/lib/playground';
import { applyFixes } from '@/lib/playground-edits';
import { DOCS_FILES } from '@/lib/site';
import { CODEX_COMMANDS, DOCS_FILES } from '@/lib/site';

// The home page's first screen (ROADMAP-v2 section 3.8), the demo it leads into, and the social
// card drawn from the same finding card.
Expand Down Expand Up @@ -124,6 +125,32 @@ describe('the first screen', () => {
expect(shown).not.toContain('Star on GitHub');
});

it('offers the setup prompt for your agent beside Try it in the browser, before the card', () => {
const at = (marker: string) => firstScreen.indexOf(marker);
expect(at('data-agent-prompt')).toBeGreaterThan(at('Try it in the browser'));
expect(at('data-agent-prompt')).toBeLessThan(at('data-finding-card'));
// One row with Try it in the browser, which wraps on a phone: no taller first screen on wide
// screens, where the install tabs and the proof row sit under it.
const row = /<div class="([^"]*)">\s*<a[^>]*href="\/playground"[\s\S]*?data-agent-prompt/.exec(
firstScreen,
);
expect(row?.[1]).toMatch(/\bflex\b/);
expect(row?.[1]).toMatch(/\bflex-wrap\b/);
const prompt = /<label[^>]*data-agent-prompt[\s\S]*?<\/label>/.exec(firstScreen)?.[0] ?? '';
expect(text(prompt)).toContain('Or ask your agent to set it up');
// The label's text and the button copy the same prompt; the button says which.
expect(decode(prompt)).toContain(`title="${SETUP_PROMPT}"`);
expect(decode(prompt)).toMatch(/<button[^>]*aria-label="Copy the prompt for your agent: Read /);
// Touch screens never show the title: below sm the label shows the prompt it copies, so a
// phone visitor sees what they copy. From sm up it is hidden, and the row stays one line.
const shown = /<span[^>]*data-agent-prompt-text[^>]*class="([^"]*)"[^>]*>([^<]*)<\/span>/.exec(
prompt,
);
expect(decode(shown?.[2] ?? '')).toBe(`“${SETUP_PROMPT}”`);
expect(shown?.[1]).toMatch(/\bsm:hidden\b/);
expect(shown?.[1]).toMatch(/\bbasis-full\b/);
});

it('shows three of the demo’s findings in a card, with the import and what tsc reports', () => {
const card = /<figure[^>]*data-finding-card[\s\S]*?<\/figure>/.exec(firstScreen)?.[0] ?? '';
const shown = flat(card);
Expand Down Expand Up @@ -151,10 +178,7 @@ describe('the first screen', () => {
);
const shown = flat(firstScreen);
// The commands are the docs' own.
for (const command of [
'codex plugin marketplace add dgesteves/onsystem',
'codex plugin add onsystem@dgesteves',
]) {
for (const command of CODEX_COMMANDS.split('\n')) {
expect(shown).toContain(command);
expect(read('docs/clients.md')).toContain(command);
}
Expand Down
Loading
Loading