diff --git a/.changeset/cli-help-and-inspect-format.md b/.changeset/cli-help-and-inspect-format.md new file mode 100644 index 00000000..2b9c4bba --- /dev/null +++ b/.changeset/cli-help-and-inspect-format.md @@ -0,0 +1,7 @@ +--- +'onsystem': patch +--- + +`onsystem --help` now says what the CLI does. `--config` names `onsystem.config.mts`, which is read along with the other four; `--tokens` names a DESIGN.md, which is a token file like a DTCG `.json` or a stylesheet; and `--format` lists `inspect: pretty | json`, which worked but was listed for `check` and `export` only. The CLI reference (`docs/cli.md`) already had all three. + +`onsystem inspect --format xml`, or a typo, now exits 2 with one line, as `check` and `export` do for a format they do not take. It printed the pretty output and exited 0, so a script that asked for JSON and got text read it as the design system having nothing in it. The check runs before the project is loaded, so it covers a monorepo root too. diff --git a/docs/cli.md b/docs/cli.md index 4029a09a..4f5880a8 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -74,7 +74,7 @@ It exits with: | Option | What it does | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--explain` | Adds every candidate zero config looked at and why it was taken or not, the config it resolved to, the tokens that did not resolve and the components whose props did not | -| `--format ` | `json` prints what it read as JSON, for scripts | +| `--format ` | `pretty` (the default) or `json`, which prints what it read as JSON, for scripts; another value exits 2 | ## `export` diff --git a/src/cli.ts b/src/cli.ts index edc7884d..a2ae85e1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -76,15 +76,16 @@ with a config, components.json or package.json, and inspect lists every project. Options --root Project root (default: the config file's directory, or cwd) - --config Config file (default: onsystem.config.{json,ts,mjs,js}) + --config Config file (default: onsystem.config.{json,ts,mts,js,mjs}) --components Component sources, repeatable (default: found through components.json or a workspace package, else components/ui/**/*.{tsx,jsx} and src/components/ui/...) - --tokens Token file (DTCG .json or CSS), repeatable + --tokens Token file (DTCG .json, CSS or DESIGN.md), repeatable --docs Component docs (Markdown/MDX), repeatable --no-cache Ignore the on-disk extraction cache --no-watch serve: do not reload when files change --format check: pretty | json | github | sarif (default: pretty); + inspect: pretty | json (default: pretty); export: dtcg | design-md --out export: write to this file instead of stdout --max-warnings check: exit 1 when there are more than n warnings @@ -1114,6 +1115,11 @@ async function inspect( values: { cache: boolean; format?: string | undefined; explain: boolean }, io: Io, ): Promise { + const format = values.format ?? 'pretty'; + if (!['pretty', 'json'].includes(format)) { + io.stderr(`inspect: unknown --format "${format}" (pretty, json)`); + return 2; + } const target = await loadTarget(configOptions); const { config } = target; warnDeprecated(config.deprecations, 'pretty', io); diff --git a/test/cli-docs.test.ts b/test/cli-docs.test.ts index 45bb3179..f69ee0c8 100644 --- a/test/cli-docs.test.ts +++ b/test/cli-docs.test.ts @@ -25,12 +25,6 @@ const read = (file: string) => const COMMANDS = ['serve', 'check', 'inspect', 'export', 'init'] as const; /** The section of docs/cli.md for options that do not name a command in `--help`. */ const SHARED = 'Options for every command'; -/** - * Options the CLI takes for a command whose `--help` text does not name it: `inspect --format - * json`. The test below runs each one, so the row stays true. - */ -const BEYOND_HELP: Record = { '`inspect`': ['--format'] }; - interface Option { /** The flags, such as `-y` and `--yes`. */ flags: string[]; @@ -166,6 +160,7 @@ describe('the CLI reference (docs/cli.md)', () => { }); expect(options.find((o) => o.flags.includes('--format'))?.commands).toEqual([ 'check', + 'inspect', 'export', ]); }); @@ -208,19 +203,20 @@ describe('the CLI reference (docs/cli.md)', () => { const expected = option?.commands.length ? option.commands.map((c) => `\`${c}\``) : [SHARED]; - if (row.flags.some((flag) => BEYOND_HELP[heading]?.includes(flag))) continue; expect(expected, `${row.flags.join(', ')} under "${heading}"`).toContain(heading); } } }); - it('runs inspect --format json, which --help does not list', async () => { + it('runs inspect --format json, the value its row names', async () => { const out: string[] = []; const io: Io = { cwd: DEMO_ROOT, color: false, stdout: (t) => out.push(t), stderr: () => {} }; expect(await main(['inspect', '--format', 'json', '--no-cache'], io)).toBe(0); const model = JSON.parse(out.join('\n')) as { components: { name: string }[] }; expect(model.components.map((c) => c.name)).toContain('Button'); - expect(section(cli, '`inspect`')).toMatch(/^\| `--format ` +\| `json` /m); + expect(section(cli, '`inspect`')).toMatch( + /^\| `--format ` +\| `pretty` \(the default\) or `json`/m, + ); }); it('names every config file name the CLI looks for, as docs/configuration.md does', () => { diff --git a/test/cli.test.ts b/test/cli.test.ts index 0fbf243c..1b20ce2f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -7,6 +7,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { describe, expect, it } from 'vitest'; import { main, type Io } from '../src/cli.js'; +import { CONFIG_FILES } from '../src/config.js'; import { VERSION } from '../src/version.js'; import { DEMO_ROOT, fixture } from './helpers.js'; @@ -169,6 +170,7 @@ describe('onsystem check', () => { expect((await run(['check'])).code).toBe(2); expect((await run(['check', 'nope/**/*.tsx'])).stderr).toContain('no files match'); expect((await run(['check', 'x.tsx', '--format', 'xml'])).code).toBe(2); + expect((await run(['inspect', '--format', 'xml'])).code).toBe(2); expect((await run(['frobnicate'])).code).toBe(2); expect((await run(['--bogus'])).code).toBe(2); }); @@ -869,6 +871,27 @@ describe('onsystem inspect, --help, --version', () => { expect((await run(['--help'])).stdout).toContain('onsystem check '); expect((await run(['-v'])).stdout).toBe(VERSION); }); + + it('names every config file extension in --help', async () => { + const { stdout } = await run(['--help']); + const listed = /onsystem\.config\.\{([^}]+)\}/.exec(stdout)?.[1]?.split(','); + const extensions = CONFIG_FILES.map((name) => name.slice('onsystem.config.'.length)); + expect([...(listed ?? [])].sort()).toEqual([...extensions].sort()); + }); + + it('rejects an unknown --format, in a project and at a monorepo root', async () => { + const monorepo = fixture({ + 'pnpm-workspace.yaml': 'packages:\n - apps/*\n', + 'package.json': '{ "name": "acme", "private": true }', + }); + appIn(monorepo); + for (const cwd of [DEMO_ROOT, monorepo]) { + const { code, stdout, stderr } = await run(['inspect', '--format', 'xml', '--no-cache'], cwd); + expect(code).toBe(2); + expect(stderr).toBe('inspect: unknown --format "xml" (pretty, json)'); + expect(stdout).toBe(''); + } + }); }); describe('onsystem with no command', () => {