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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ jobs:
run: git diff --check "$(git hash-object -t tree /dev/null)" HEAD
- name: Validate provider catalogs
run: node scripts/validate-provider-catalogs.mjs
- name: Install JSON Schema validation dependencies
run: npm ci --ignore-scripts --no-audit
- name: Validate machine-readable output schema
run: |
node --check examples/consume-cli-json.mjs
node --check scripts/cli-output-schema.test.mjs
node --check scripts/validate-cli-output-schema.mjs
npm run test:cli-output-schema
- name: Validate distribution contract
run: |
node scripts/validate-distribution-contract.mjs
Expand Down Expand Up @@ -98,6 +106,10 @@ jobs:
run: cargo +stable fmt --check
- name: Run tests
run: cargo +stable test --locked
- name: Run machine-readable consumer prototype
env:
STACK_BINARY: target/debug/stack
run: npm run test:consumer-prototype
- name: Validate generated completion and manual assets
shell: bash
run: |
Expand Down Expand Up @@ -196,6 +208,7 @@ jobs:
test -s CONTRIBUTING.md
test -s SECURITY.md
test -s docs/language-server.md
test -s docs/machine-readable-output.md
test -s docs/configuration.md
test -s docs/completions.md
test -s docs/self-update.md
Expand All @@ -209,15 +222,19 @@ jobs:
test -s distribution/distribution-contract.schema.json
test -s distribution/install-receipt.schema.json
test -s distribution/release-manifest.schema.json
test -s schemas/cli-output-v1.schema.json
test -s distribution/generated/share/bash-completion/completions/stack
test -s distribution/generated/share/zsh/site-functions/_stack
test -s distribution/generated/share/fish/vendor_completions.d/stack.fish
test -s distribution/generated/share/man/man1/stack.1
test -s Cargo.toml
test -s Cargo.lock
test -s package.json
test -s package-lock.json
test -s src/config.rs
test -s src/command_docs.rs
test -s src/lsp.rs
test -s src/machine_output.rs
test -s src/update.rs
test -s src/update/install.rs
test -s src/update/tests.rs
Expand All @@ -230,6 +247,8 @@ jobs:
test -s catalogs/azure.json
test -s catalogs/simple-icons.json
test -s scripts/generate-provider-catalogs.mjs
test -s scripts/cli-output-schema.test.mjs
test -s scripts/validate-cli-output-schema.mjs
test -s scripts/aqua-registry.test.mjs
test -s scripts/sync-example-templates.mjs
test -s scripts/validate-provider-catalogs.mjs
Expand Down Expand Up @@ -260,6 +279,16 @@ jobs:
test -s templates/sources/01-minimal.stack
test -s tests/specification-revision
test -s tests/fixtures/render.stack
test -s tests/fixtures/invalid.stack
test -s tests/fixtures/warning.stack
test -s tests/fixtures/cli-output/check-success.json
test -s tests/fixtures/cli-output/check-warning.json
test -s tests/fixtures/cli-output/check-stack-error.json
test -s tests/fixtures/cli-output/fmt-changes-required.json
test -s tests/fixtures/cli-output/fmt-stdin-success.json
test -s tests/fixtures/cli-output/render-file-success.json
test -s tests/fixtures/cli-output/operational-error.json
test -s examples/consume-cli-json.mjs
test -s tests/snapshots/lsp-help.txt
test -s tests/snapshots/doctor-help.txt
test -s tests/snapshots/doctor-report.txt
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/target/
/node_modules/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ stack init
stack init --template groups-and-layout
stack init --template aws-serverless-checkout -o checkout.stack
stack check arch.stack
stack check arch.stack --json
stack fmt arch.stack
stack fmt --check arch.stack
stack fmt -
stack render arch.stack
stack render arch.stack -o arch.svg
stack render arch.stack --json
stack update --check
stack lsp
stack doctor
Expand All @@ -61,6 +63,8 @@ stack manpage

`stack check` reads the file as bytes and runs the full compiler, theme, layout, and routing validation pipeline without changing the source. Diagnostics are written to standard error in source order. Standard output remains empty.

`stack check`, `stack fmt`, and `stack render` accept `--json` for CI, editor, and agent consumers. JSON mode preserves exit statuses and emits one versioned envelope containing structured diagnostics, completed artifacts, and operational errors. Inline formatted source and SVG are carried as artifact content so standard output remains valid JSON. See the [machine-readable output contract](./docs/machine-readable-output.md) and its immutable [version 1 JSON Schema](./schemas/cli-output-v1.schema.json).

`stack fmt` uses the engine formatter and preserves comments. File mode replaces changed source atomically through a temporary file in the same directory; unchanged files are not replaced. Syntax, encoding, and host I/O failures leave the original file untouched. `stack fmt -` reads bytes from standard input and writes only canonical source to standard output. `--check` never writes source and exits with status `1` when formatting is required.

`stack render` uses the same engine pipeline to produce deterministic standalone SVG. Without `-o`, standard output contains only SVG. With `-o`, the output is written atomically in the destination directory. It discovers imported `aws`, `gcp`, `azure`, and `simple-icons` packs below the shared icon store. Use `--provider-pack <DIRECTORY>` for a project-local icon-store root, and use `--notice <NOTICE>` to save the exact provider pack revisions, terms, source archives, and icon IDs embedded in that artifact. Pack files are bounded and validated before rendering. Diagnostics remain on standard error, warnings preserve SVG, and Stack errors never create or replace output.
Expand Down
5 changes: 4 additions & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Audit date: 2026-09-05
| `indexmap` / `hashbrown` / `equivalent` | `2.14.1`, `0.17.1`, `1.0.2` | Apache-2.0 OR MIT | <https://github.com/indexmap-rs/indexmap>, <https://github.com/rust-lang/hashbrown>, <https://github.com/indexmap-rs/equivalent> | ZIP archive entry index. |
| `cfg-if` / `cpufeatures` / `libc` | `1.0.4`, `0.3.1`, `0.2.189` | MIT OR Apache-2.0 | <https://github.com/rust-lang/cfg-if>, <https://github.com/RustCrypto/utils>, <https://github.com/rust-lang/libc> | Target selection and SHA-256 acceleration support. |
| `serde` / `serde_core` | `1.0.229` | MIT OR Apache-2.0 | <https://github.com/serde-rs/serde> | Runtime catalog data types through `stack-theme`. |
| `serde_json` | `1.0.151` | MIT OR Apache-2.0 | <https://github.com/serde-rs/json> | Embedded catalog decoding through `stack-theme`. |
| `serde_json` | `1.0.151` | MIT OR Apache-2.0 | <https://github.com/serde-rs/json> | Embedded catalog decoding through `stack-theme` and versioned CLI JSON output serialization. |
| `itoa` | `1.0.18` | MIT OR Apache-2.0 | <https://github.com/dtolnay/itoa> | Transitive runtime dependency of `serde_json`. |
| `memchr` | `2.8.3` | Unlicense OR MIT | <https://github.com/BurntSushi/memchr> | Transitive runtime dependency of `serde_json`. |
| `zmij` | `1.0.23` | MIT | <https://github.com/dtolnay/zmij> | Transitive runtime dependency of `serde_json`. |
Expand All @@ -43,6 +43,9 @@ Audit date: 2026-09-05
| `syn` | `3.0.4` | MIT OR Apache-2.0 | <https://github.com/dtolnay/syn> | Procedural-macro build dependency; not linked into the release binary. |
| `unicode-ident` | `1.0.24` | (MIT OR Apache-2.0) AND Unicode-3.0 | <https://github.com/dtolnay/unicode-ident> | Procedural-macro build dependency; not linked into the release binary. |
| `cc` / `find-msvc-tools` / `shlex` | `1.4.5`, `0.1.12`, `2.0.1` | MIT OR Apache-2.0 | <https://github.com/rust-lang/cc-rs> | Build dependencies of `ring`; not linked into the release binary. |
| `ajv` | `8.20.0` | MIT | <https://github.com/ajv-validator/ajv> | Lockfile-pinned JSON Schema validation for CLI output fixtures; not linked into the release binary. |
| `fast-deep-equal` / `json-schema-traverse` / `require-from-string` | `3.1.3`, `1.0.0`, `2.0.2` | MIT | <https://github.com/epoberezkin/fast-deep-equal>, <https://github.com/epoberezkin/json-schema-traverse>, <https://github.com/floatdrop/require-from-string> | Build-only dependencies of `ajv`; not linked into the release binary. |
| `fast-uri` | `3.1.7` | BSD-3-Clause | <https://github.com/fastify/fast-uri> | Build-only URI validation through `ajv`; not linked into the release binary. |

No third-party vendor icon is bundled in the repository or binary. Provider-specific assets require a separate rights record covering source revision, copyright, trademark restrictions, modification, software redistribution, commercial diagram output, and required notices.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ words="init check fmt render update lsp doctor config icons completions manpage
else
case "$context" in
"init") words="--template -o --output --force -h --help" ;;
"check") words="-h --help" ;;
"fmt") words="- --check -h --help" ;;
"render") words="--provider-pack -o --notice -h --help" ;;
"check") words="--json -h --help" ;;
"fmt") words="- --check --json -h --help" ;;
"render") words="--provider-pack -o --notice --json -h --help" ;;
"update") words="--check --version -h --help" ;;
"lsp") words="-h --help" ;;
"doctor") words="--provider-pack -h --help" ;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@ complete -c stack -n '__fish_seen_subcommand_from init' -l 'output' -r
complete -c stack -n '__fish_seen_subcommand_from init' -l 'force'
complete -c stack -n '__fish_seen_subcommand_from init' -s 'h'
complete -c stack -n '__fish_seen_subcommand_from init' -l 'help'
complete -c stack -n '__fish_seen_subcommand_from check' -l 'json'
complete -c stack -n '__fish_seen_subcommand_from check' -s 'h'
complete -c stack -n '__fish_seen_subcommand_from check' -l 'help'
complete -c stack -n '__fish_seen_subcommand_from fmt' -a '-'
complete -c stack -n '__fish_seen_subcommand_from fmt' -l 'check'
complete -c stack -n '__fish_seen_subcommand_from fmt' -l 'json'
complete -c stack -n '__fish_seen_subcommand_from fmt' -s 'h'
complete -c stack -n '__fish_seen_subcommand_from fmt' -l 'help'
complete -c stack -n '__fish_seen_subcommand_from render' -l 'provider-pack' -r
complete -c stack -n '__fish_seen_subcommand_from render' -s 'o'
complete -c stack -n '__fish_seen_subcommand_from render' -l 'notice' -r
complete -c stack -n '__fish_seen_subcommand_from render' -l 'json'
complete -c stack -n '__fish_seen_subcommand_from render' -s 'h'
complete -c stack -n '__fish_seen_subcommand_from render' -l 'help'
complete -c stack -n '__fish_seen_subcommand_from update' -l 'check'
Expand Down
16 changes: 11 additions & 5 deletions distribution/generated/share/man/man1/stack.1
Original file line number Diff line number Diff line change
Expand Up @@ -87,45 +87,49 @@ Examples:
Validate a Stack source file without modifying it

Usage:
stack check <FILE>
stack check <FILE> [\-\-json]

Arguments:
<FILE> Read Stack source bytes from this file

Options:
\-\-json Write one versioned JSON envelope to standard output
\-h, \-\-help Print help

Examples:
stack check arch.stack
stack check arch.stack \-\-json
.fi
.SS "stack fmt"
.nf
Format Stack source canonically

Usage:
stack fmt <FILE>
stack fmt \-\-check <FILE>
stack fmt \-
stack fmt <FILE> [\-\-json]
stack fmt \-\-check <FILE> [\-\-json]
stack fmt \- [\-\-json]

Arguments:
<FILE> Format the file atomically in place
\- Read from standard input and write to standard output

Options:
\-\-check Report whether formatting is required without writing output
\-\-json Write one versioned JSON envelope to standard output
\-h, \-\-help Print help

Examples:
stack fmt arch.stack
stack fmt \-\-check arch.stack
stack fmt \- < input.stack > output.stack
stack fmt \-\-json \- < input.stack
.fi
.SS "stack render"
.nf
Render Stack source as standalone SVG

Usage:
stack render <FILE> [\-\-provider\-pack <DIRECTORY>] [\-o <OUTPUT>] [\-\-notice <NOTICE>]
stack render <FILE> [\-\-provider\-pack <DIRECTORY>] [\-o <OUTPUT>] [\-\-notice <NOTICE>] [\-\-json]

Arguments:
<FILE> Read Stack source bytes from this file
Expand All @@ -134,6 +138,7 @@ Options:
\-\-provider\-pack <DIRECTORY> Read known provider packs from this icon\-store root
\-o <OUTPUT> Write SVG atomically instead of using standard output
\-\-notice <NOTICE> Write exact used\-provider notices atomically
\-\-json Write one versioned JSON envelope to standard output
\-h, \-\-help Print help

Default icon store:
Expand All @@ -143,6 +148,7 @@ Examples:
stack render arch.stack
stack render arch.stack \-o arch.svg
stack render arch.stack \-\-notice arch.NOTICE.md \-o arch.svg
stack render arch.stack \-\-json
.fi
.SS "stack update"
.nf
Expand Down
6 changes: 3 additions & 3 deletions distribution/generated/share/zsh/site-functions/_stack
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ fi
local -a candidates
case "$context" in
"init") candidates=('--template' '-o' '--output' '--force' '-h' '--help') ;;
"check") candidates=('-h' '--help') ;;
"fmt") candidates=('-' '--check' '-h' '--help') ;;
"render") candidates=('--provider-pack' '-o' '--notice' '-h' '--help') ;;
"check") candidates=('--json' '-h' '--help') ;;
"fmt") candidates=('-' '--check' '--json' '-h' '--help') ;;
"render") candidates=('--provider-pack' '-o' '--notice' '--json' '-h' '--help') ;;
"update") candidates=('--check' '--version' '-h' '--help') ;;
"lsp") candidates=('-h' '--help') ;;
"doctor") candidates=('--provider-pack' '-h' '--help') ;;
Expand Down
82 changes: 82 additions & 0 deletions docs/machine-readable-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Machine-readable CLI output

`stack check`, `stack fmt`, and `stack render` accept `--json` for CI, editor, and agent integrations. JSON mode writes exactly one newline-terminated JSON envelope to standard output and preserves the command's normal process exit status.

```sh
stack check architecture.stack --json
stack fmt --check architecture.stack --json
stack render architecture.stack -o architecture.svg --json
```

Human mode is unchanged. In JSON mode, Stack diagnostics and operational failures are represented in the envelope and standard error remains empty. If the process cannot serialize or write the envelope itself, it exits `2` and reports that last-resort failure on standard error because valid JSON can no longer be guaranteed. Help remains human-readable even when `--json` is also present.

## Version 1 envelope

Every envelope contains the same required fields:

```json
{
"$schema": "https://raw.githubusercontent.com/stack-sh/cli/main/schemas/cli-output-v1.schema.json",
"schemaVersion": 1,
"command": "check",
"outcome": "success",
"exitStatus": 0,
"diagnostics": [],
"artifacts": [],
"error": null
}
```

- `command` is `check`, `fmt`, or `render`.
- `outcome` is `success`, `changes-required`, `stack-error`, or `operational-error`.
- `exitStatus` is the process status: `0` for success including warnings, `1` for Stack errors or a `fmt --check` difference, and `2` for argument, host, configuration, provider-pack, engine, or internal failures.
- `diagnostics` contains portable Stack diagnostics in deterministic source order.
- `artifacts` identifies formatted source, rendered SVG, and provider notice results.
- `error` is non-null only for `operational-error`.

Consumers should branch on `outcome`, stable diagnostic codes, artifact kinds, and operational error codes. Human-readable `message` and `help` text can improve logs but are not parsing contracts.

## Diagnostics and ranges

Each diagnostic includes `code`, `severity`, `message`, `path`, `range`, `expected`, `help`, and `related`. Ranges are end-exclusive. `byteOffset` is a zero-based UTF-8 byte offset; `line` and Unicode scalar `column` are one-based. Related locations use the same path and range representation.

Paths are reported as the CLI received or derived them; they are not canonicalized. Standard-input diagnostics use `<stdin>`.

## Artifacts

Each artifact includes `kind`, `path`, `mediaType`, and `content`:

| Kind | File output | Standard-output result in JSON mode |
| --- | --- | --- |
| `formatted-source` | `path` identifies the formatted file and `content` is `null` | `path` is `null` and `content` contains canonical Stack source |
| `rendered-svg` | `path` identifies the written SVG and `content` is `null` | `path` is `null` and `content` contains standalone SVG |
| `provider-notice` | `path` identifies the written notice and `content` is `null` | Not applicable |

`fmt --check` produces no artifact because it never writes formatted source. A Stack semantic error can coexist with a `formatted-source` artifact, matching human mode's existing formatter behavior. Only successfully produced artifacts are listed.

## Operational errors

The operational `error.code` categories are stable within schema version 1:

| Code | Category |
| --- | --- |
| `CLI1001` | Invalid or conflicting command arguments |
| `CLI1002` | Standard-stream or filesystem I/O failure |
| `CLI1003` | Configuration or provider icon-store failure |
| `CLI1004` | Engine operational failure |
| `CLI1005` | Internal output-contract invariant failure |

Stack source problems remain in `diagnostics` with `STK` codes and use outcome `stack-error`; they are not operational errors.

## Compatibility and validation

`schemaVersion: 1` and [`schemas/cli-output-v1.schema.json`](../schemas/cli-output-v1.schema.json) define an immutable envelope contract. A structural change, field removal or addition, enum expansion, type change, or semantic reinterpretation requires a new schema version and a separately named schema file. Diagnostic messages can change without a schema version change; diagnostic codes and source-range semantics follow their owning public Stack contracts.

Consumers may vendor the schema or pin its raw GitHub URL to a reviewed commit. Repository CI validates the checked-in golden fixtures with the exact, lockfile-pinned JSON Schema validator and checks process-level output against those fixtures.

[`examples/consume-cli-json.mjs`](../examples/consume-cli-json.mjs) is a dependency-free consumer prototype. It verifies the schema version and exit-status parity, then extracts stable summary fields without parsing messages:

```sh
STACK_BINARY=target/debug/stack \
node examples/consume-cli-json.mjs check tests/fixtures/render.stack
```
42 changes: 42 additions & 0 deletions examples/consume-cli-json.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { spawnSync } from "node:child_process"

const supportedCommands = new Set(["check", "fmt", "render"])
const [command, ...arguments_] = process.argv.slice(2)
if (!supportedCommands.has(command)) {
process.stderr.write("Usage: consume-cli-json.mjs <check|fmt|render> [ARGUMENTS...]\n")
process.exitCode = 2
} else {
const binary = process.env.STACK_BINARY ?? "stack"
const completed = spawnSync(binary, [command, ...arguments_, "--json"], {
encoding: "utf8",
})
if (completed.error) throw completed.error
if (completed.signal !== null || completed.status === null) {
throw new Error("Stack CLI did not return a process exit status")
}
if (completed.stderr !== "") {
throw new Error("Stack CLI emitted unexpected standard error in JSON mode")
}

const envelope = JSON.parse(completed.stdout)
if (envelope.schemaVersion !== 1) {
throw new Error(`Unsupported Stack CLI schema version: ${envelope.schemaVersion}`)
}
if (envelope.command !== command) {
throw new Error(`Expected ${command} output, received ${envelope.command}`)
}
if (envelope.exitStatus !== completed.status) {
throw new Error("Envelope exitStatus does not match the process exit status")
}

const summary = {
command: envelope.command,
outcome: envelope.outcome,
exitStatus: envelope.exitStatus,
diagnosticCodes: envelope.diagnostics.map(({ code }) => code),
artifacts: envelope.artifacts.map(({ kind, path }) => ({ kind, path })),
errorCode: envelope.error?.code ?? null,
}
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`)
process.exitCode = completed.status
}
Loading