diff --git a/README.md b/README.md index 055c98c..3336063 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,8 @@ npx @google/design.md export --format dtcg DESIGN.md > tokens.json | `tailwind` | JSON | Alias for `json-tailwind` | | `dtcg` | JSON | W3C Design Tokens Format Module | +Exit code `0` on a successful export (regardless of any lint findings in the source — run `lint` to gate on those), `1` on an invalid `--format` or an emitter error, and `2` if the input file cannot be read. + ### `spec` Output the DESIGN.md format specification (useful for injecting spec context into agent prompts). diff --git a/packages/cli/src/commands/export-exit-code.test.ts b/packages/cli/src/commands/export-exit-code.test.ts new file mode 100644 index 0000000..7f682a3 --- /dev/null +++ b/packages/cli/src/commands/export-exit-code.test.ts @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect, afterAll } from 'bun:test'; +import { join } from 'node:path'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; + +const CLI = join(import.meta.dir, '../index.ts'); + +function run(args: string[]): { code: number | null; stdout: string } { + const proc = Bun.spawnSync(['bun', 'run', CLI, ...args], { stdout: 'pipe', stderr: 'pipe' }); + return { code: proc.exitCode, stdout: Buffer.from(proc.stdout).toString('utf-8') }; +} + +describe('export exit code', () => { + const dir = mkdtempSync(join(tmpdir(), 'designmd-export-')); + const badFile = join(dir, 'DESIGN.md'); + // An invalid color is a lint *error*, but the export itself still succeeds. + writeFileSync(badFile, '---\ncolors:\n primary: "notacolor"\n---\n## Colors\n'); + + afterAll(() => rmSync(dir, { recursive: true, force: true })); + + it('exits 0 on a successful export even when the source has lint errors', () => { + const { code, stdout } = run(['export', '--format', 'json-tailwind', badFile]); + expect(code).toBe(0); + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + it('lint still exits 1 on the same source (the validation gate)', () => { + const { code } = run(['lint', badFile]); + expect(code).toBe(1); + }); +}); diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 1bb50d5..d276055 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -87,6 +87,8 @@ export default defineCommand({ console.log(JSON.stringify(result.data, null, 2)); } - process.exitCode = report.summary.errors > 0 ? 1 : 0; + // A successful export exits 0 even if the source has lint findings; those + // are surfaced by `lint`, not by whether the export itself produced output. + // The error branches above set a non-zero code and return before this point. }, });