Rendered from specs/F27-cli.md — edit it there, not here.
F27 — Standalone Command-Line Interface
Overview
Every non-preview capability of this project — validation, linting, diff, bundling, migration — already lives in pure, vscode-free modules (a deliberate architecture principle; see the constitution's Article III and the *-NFR "pure module" requirements throughout specs/). Those modules only have a VS Code front end today. This spec adds a second, equally thin front end: a standalone CLI that reuses the exact same core so the tools work in CI, pre-commit hooks, and any editor-less workflow — without VS Code, and without duplicating a single line of logic.
The CLI ships as its own npm package so a consumer can npx it or install it globally, independent of the extension. This directly serves the project's agnosticity principle: the value lives in tool-neutral, reusable modules, and the extension is just one of several possible front ends.
The existing schema:compat script (F26) is the proof of concept — a headless CLI over schemaDiff + schemaCompat. F27 generalises that pattern into a first-class, multi-command binary.
User Stories
- As a platform engineer, I want to validate data files and lint schemas in CI without installing VS Code, reusing the same engine my team uses in the editor.
- As a schema author, I want
npx <cli> diff old.json new.json --checkto fail a pull request on a breaking change, from any CI provider. - As a build-tooling author, I want machine-readable (
--json) output so I can wire these checks into my own pipeline. - As a maintainer, I want the CLI to reuse the extension's pure core so a fix in one place fixes both, with no divergence.
Functional Requirements
Packaging & invocation
- F27-FR-01 The CLI MUST be a standalone, publishable npm package with a
binentry, runnable without VS Code. It MUST NOT re-implement any core logic: every subcommand MUST call the same pure modules the extension uses. - F27-FR-02 Invoked with no subcommand,
--help, or-h, the CLI MUST print usage listing every subcommand and exit0.--versionMUST print the CLI package version and exit0. - F27-FR-03 An unknown subcommand MUST print an error naming it plus the usage summary, and exit with the usage-error code (
64).
Subcommands
- F27-FR-04
validate <data-file> --schema <schema-file>MUST parse the data file by its extension (JSON/JSONC/JSONL/YAML/TOML, reusing F03/F20'sparseDataText), validate it against the schema with the draft-aware Ajv pipeline (F03), print each violation with its 1-based line when locatable, and exit0when valid or1when there is at least one violation. - F27-FR-05
lint <schema-file>MUST run the F17 schema-quality rules and print each finding with its rule id and 1-based line. It MUST exit1when any finding is atwarningseverity, else0(so lint can gate CI without failing on advisory hints). - F27-FR-06
diff <old-schema> <new-schema>MUST print the F15 grouped change report. With--checkit MUST additionally compute the F26 compatibility verdict and adopt F26's exit codes (0compatible,1breaking,2strict-mode "unknown");--strictMUST apply F26 strict mode. - F27-FR-07
bundle <schema-file>MUST produce a single self-contained schema (F14) by resolving external$refs, and print it to stdout. Local (relative/absolute path) refs MUST be resolved from the filesystem; remote (http(s)) refs MUST be fetched.--dereferenceMUST inline refs (F14's dereference mode) instead of collecting them under$defs. - F27-FR-08
migrate <schema-file> --to <2020-12|2019-09|draft-07>MUST transform the schema to the target draft (F22) and print the migrated schema, reporting the list of changes to stderr (so stdout stays a clean schema).
Subcommands over the remaining non-interactive core
These extend the CLI toward parity with the extension's editor-free features. Each MUST reuse the same pure module the extension uses (F27-FR-01); none may re-implement the logic.
- F27-FR-11
infer <data-file> [--to <2020-12|2019-09|draft-07>]MUST infer a JSON Schema from a data file (F06,genson-js), declaring the target draft's meta-schema (F22'sMETA_SCHEMAmap), and print it to stdout.--todefaults to2020-12(the latest draft); an unrecognised value is a usage error (64). The data file MUST be parsed by its extension (JSON/JSONC/JSONL/YAML/TOML); a JSONL file infers over the array of its records. - F27-FR-12
sample <schema-file>MUST generate a valid sample instance from a schema (F16) and print it to stdout, resolving same-document$refs. When the schema is unsatisfiable or will not compile it MUST report the failing keyword(s) and exit with the data-error code (65), never emitting an invalid document. - F27-FR-13
types <schema-file> [--lang <id>]MUST generate typed source from a schema (F18), bundling external$refs first (F14) so the generator runs on a self-contained document.--langselects a target from F18's supported set (defaulttypescript); an unknown target is a usage error (64). The generated code MUST be printed to stdout. - F27-FR-14
coverage <data-file...> --schema <schema>MUST report which of a schema's declared properties the data exercises (F23) and print the coverage report. It MUST accept one or more data files as positional arguments (any supported format, including JSONL) and union coverage across every record of every file, matching JSONL's existing per-file record union. - F27-FR-15
validate <dir> --workspaceMUST scan a directory for supported data files, validate each file that carries an inline$schemabinding (F10) against that schema (F03/F20), and print the F20 grouped Markdown report. It MUST exit1when any file has a validation or binding error, else0. Files whose$schemais a meta-schema (i.e. schema files, F20-FR-02) MUST NOT be treated as data bindings. - F27-FR-16
graph <schema-file> [--svg]MUST print the schema's$refdependency graph (F24): an adjacency list by default, or the SVG rendering with--svg. External and unresolved refs MUST be shown without fetching.
Output & exit codes
- F27-FR-09 Every subcommand MUST accept
--json, emitting a machine-readable object (result payload +exitCode) on stdout instead of the human report, for pipeline consumption. - F27-FR-10 Exit codes MUST be consistent across subcommands:
0success / clean,1a finding or failed check,2reserved fordiff --check --strict"unknown",64a usage error (bad arguments),65a data error (a file that cannot be read or parsed). A data or usage error MUST print to stderr and MUST NOT be confused with a clean/finding exit.
Non-Functional Requirements
- F27-NFR-01 The CLI MUST NOT import the
vscodemodule, directly or transitively — it depends only on the pure core (agnosticity: the CLI must build and run with no editor present). Enforced by the bundle build (avscodeimport would fail to resolve outside the extension host). - F27-NFR-02 The CLI's argument parsing, command routing, and report formatting MUST live in a pure,
vscode-free, I/O-injected module (runCli(argv, io)returning{ stdout, stderr, code }) with ≥ 80 % unit-test coverage (Article V). Only the thin executable entry point that wires real stdio/filesystem/fetchand callsprocess.exitmay be I/O-bound, and it is the CLI's sole coverage-excluded file. - F27-NFR-03 The published CLI package MUST be self-contained: its bundle MUST inline the core modules it uses so the package has no runtime dependency on the extension's build output (
out/ordist/extension.js).
Acceptance Criteria
node cli/dist/cli.js --helplists validate, lint, diff, bundle, migrate and exits0.validateagainst a schema the data violates exits1and prints the offending path with a line number; against valid data it exits0.diff a.json b.json --checkexits1whenbdrops a required-less type or otherwise breaks compatibility, matching the extension's verdict.bundleon a schema with a relative$refprints one document with the ref resolved; running it needs no VS Code.migrate old.json --to 2020-12prints a draft-2020-12 schema on stdout and the change list on stderr.
History
- 2026-07-19 — Initial spec: standalone CLI reusing the pure core (validate/lint/diff/bundle/migrate), published as its own npm package.
- 2026-07-21 — Added F27-FR-11..16:
infer(F06),sample(F16),types(F18),coverage(F23),validate --workspace(F20), andgraph(F24), extending the CLI toward the extension's non-interactive surface. - 2026-07-25 — F27-FR-11:
infergained--to <draft>to select the declared meta-schema, defaulting to2020-12instead of always draft-07. F27-FR-14:coveragenow accepts multiple data files, unioning coverage across all of them.
Requirement traceability
Status of each requirement in specs/traceability.json. Test coverage is auto-discovered from [ID] tags in test titles and is not listed here.
| Requirement | Status | Implementation | Note |
|---|---|---|---|
F27-FR-01 | manual | src/cli/bin.ts, webpack.config.js, cli/package.json | Standalone publishable npm package (cli/) with a bin, built from the pure src/ core; packaging verified by the build/publish, core-reuse is structural (cli.ts imports the same modules). |
F27-FR-02 | implemented | src/cli/cli.ts | runCli routes no-arg/--help/-h/help to usage and --version to the injected version. |
F27-FR-03 | implemented | src/cli/cli.ts | Unknown subcommand → named error + usage, exit 64. |
F27-FR-04 | implemented | src/cli/cli.ts | validate: parseDataText + validateInstances (F03/F20); exit 0 valid / 1 finding. |
F27-FR-05 | implemented | src/cli/cli.ts | lint: lintSchema (F17); exit 1 only on a warning-severity finding. |
F27-FR-06 | implemented | src/cli/cli.ts | diff: diffSchemas (F15); --check adopts F26 verdict + exit codes; --strict. |
F27-FR-07 | implemented | src/cli/cli.ts | bundle: bundleSchema/dereferenceSchema (F14) with a fs/http resolver. |
F27-FR-08 | implemented | src/cli/cli.ts | migrate: migrateSchema (F22); schema to stdout, changes to stderr. |
F27-FR-09 | implemented | src/cli/cli.ts | Every subcommand honours --json (payload + exitCode). |
F27-FR-10 | implemented | src/cli/cli.ts | Shared exit codes 0/1/2/64/65; data & usage errors go to stderr. |
F27-FR-11 | implemented | src/cli/cli.ts | infer: genson-js createSchema + META_SCHEMA[--to] (F06/F22), default 2020-12; JSONL infers over records. |
F27-FR-12 | implemented | src/cli/cli.ts | sample: generateAndValidate (F16) with a same-document $ref resolver; exit 65 on unsatisfiable. |
F27-FR-13 | implemented | src/cli/cli.ts | types: bundleSchema (F14) then generateCode (F18); --lang selects a target, unknown → 64. |
F27-FR-14 | implemented | src/cli/cli.ts | coverage: computeCoverage + renderCoverageReport (F23); accepts multiple data-file positionals, instances concatenated. |
F27-FR-15 | implemented | src/cli/cli.ts | validate --workspace: walk dir, validate inline-$schema data files (F10/F20), renderMarkdownReport; exit 1 on any error. |
F27-FR-16 | implemented | src/cli/cli.ts | graph: buildRefGraph + renderAdjacencyList / renderGraphSvg (F24). |
F27-NFR-01 | manual | src/cli/cli.ts, src/cli/bin.ts | No vscode import, direct or transitive; enforced by the CLI bundle build (a vscode import fails webpack resolution outside the extension host). |
F27-NFR-02 | implemented | src/cli/cli.ts, src/cli/bin.ts | Pure, I/O-injected runCli(argv, io) fully unit-tested via in-memory IO; bin.ts is the sole thin, coverage/mutation-excluded entry. |
F27-NFR-03 | manual | webpack.config.js, cli/package.json | Published bundle inlines the core (self-contained); verified by the webpack build — no runtime dependency on out/ or dist/extension.js. |
Documented in
Documentation sections tagged spec:F27 (or one of its requirement ids) — 631 of ~953 expected words (66%). The expectation comes from this spec's evaluated complexity of 23.8 (19 documentable requirements — 16 FR, 3 NFR — weighted by kind and by each definition's length).