Skip to content

21. The CLI

The archlang CLI covers the whole toolchain surface: validating and formatting files, running governance and CI gates, writing model fields, exporting the resolved model, rendering diagrams headlessly, serving the HTTP API, running the language server, checking evidence bindings against the code repository, and checking a deployed system’s conformance against the model. Twelve subcommands, one binary. This chapter walks through what each one does, what flags it takes, and how to fit them into CI and pre-commit hooks.

Terminal window
npm install -g @archlang/cli

That puts archlang on your PATH. The CLI is a thin layer over @archlang/engine (parsing, resolution, validation) and @archlang/lsp (the shared package loader) — everything those packages need ships inside the binary. render’s PNG output goes through @archlang/render, which bundles a native resvg binary and the Inter font — the only subcommand with a native dependency; everything else is pure JS.

Terminal window
archlang --help
archlang --version

--version prints the toolchain version. --help lists every subcommand and its flags.

Terminal window
archlang validate <path>

Parses every .arch file under <path> (recursively, respecting nested package.archspace boundaries), runs the resolver, runs the validator. Prints diagnostics to stdout. Exits non-zero if anything failed.

Terminal window
archlang validate . # current directory
archlang validate examples/demo # a specific package
archlang validate orders.arch # a single file (its package is auto-detected)

Single-file drafts are first-class. A standalone .arch with no surrounding package is a perfectly good way to start — it can depend on locally-defined types and the standard library, which is enough to model a real idea fast. The CLI validates one on its own; promote it into a managed package later. (See the standalone-file rules in Chapter 11 and Chapter 12.)

The diagnostics use the same format the LSP produces, which means the same error you see in your editor is the one the CLI prints. Examples:

orders.arch:14:5 ERROR Required field 'team' is not fulfilled and not dropped
checkout.arch:8:9 ERROR Process step callee 'Payments' resolves to a module, not an interface

The summary line counts three distinct severities — they are not interchangeable:

2 errors, 1 warning, 4 todos, 0 info
  • Errors are contradictions — the model says something that can’t be true. Fix them.
  • Warnings are discouraged-but-valid — a smell, not a lie.
  • TODOs are draft debt, not bugs: a referenced-but-undefined module, interface, or subprocess that the compiler synthesized as a stub. A TODO means missing detail, not wrong. Process-first drafting produces them by design; they’re fine mid-draft.

A mature model shouldn’t ship with open TODOs. Pass --complete (alias --no-todos) to enforce the zero-TODO merge gate: the run then exits 3 if any todo-severity diagnostic remains, and prints a located, per-category checklist of what’s left instead of just a count.

Terminal window
archlang validate --strict <path> # treat warnings as errors too (exit 2)
archlang validate --complete <path> # fail (exit 3) if any TODO remains; prints the punch list
archlang validate --verbose <path> # print every TODO in full, not the grouped summary
Terminal window
archlang validate --watch <path>

Re-runs validation on every .arch save. Useful for keeping a terminal pane open next to your editor when you don’t want to enable the inline preview. Runs until Ctrl-C.

The non-zero exit code is the contract. In CI:

Terminal window
archlang validate .

Pull requests fail until validation passes. Combine with format --check and check below.

Terminal window
archlang format <path>

Rewrites the file (or every .arch file under a directory) into canonical form: normalized whitespace, consistent indentation. It also mints a stable ID for any ID-less declaration by default — the formatter is where the spec §3.1 “let the tooling mint it” rule actually runs. The formatter does not change semantic content otherwise — it only normalizes layout.

Terminal window
archlang format --check <path> # exit non-zero if anything would change
archlang format --diff <path> # print the diff that would be applied, but don't write
archlang format --no-mint <path> # skip ID minting; keep bare drafts bare
archlang format --indent=2 <path> # spaces per indent level (default 4)
archlang format --tabs <path> # use tabs instead of spaces

--check is the CI flag. --diff is the local “what would this do?” flag. Neither writes to disk.

If a file has parse errors, whitespace formatting still proceeds over the parseable regions, but ID minting is skipped for that file (minting writes a permanent ID — never into a source the parser can’t make sense of).

An ID is meaningless and permanent — a truly-random token that never changes even when the thing it names is renamed, re-scoped, or moved. Because hand-randomizing is hard, let the tooling mint it for you. The formatter (and editor autocomplete) generates truly-random IDs for any declaration that touches the format path without one. Prefer the tool over hand-writing every time.

Don’t hand-edit a stable ID once written — never mutate it, and don’t embed a domain or type name in it (anything meaningful eventually goes stale and tempts a rename, which defeats the point). Renames anchor on the ID, so the diff engine sees one renamed declaration instead of a delete-plus-add (Chapter 13).

Terminal window
archlang check <path>

Runs four phases in sequence and reports all of them even if an earlier one fails, so you see the whole picture instead of fix-rerun-fix: validate, then policy-check, then format --check, then evidence. The exit code is the max across phases — 0 all clean, 1 if validate found errors, a policy raised an active error finding, format would change files, or an evidence binding is broken, 2 under --strict if only warnings were found.

check is the heavier sibling of validate. Use validate in the inner editor loop where you want fast feedback; use check in CI and at major checkpoints.

Terminal window
archlang check --strict <path>
Terminal window
archlang check <path> --against=main

Adds a further phase: resolves the workspace both at the given git ref (base) and in the working tree (head), then evaluates the v0.10 when change gates over that diff and reports which gates fired and what review each would require. It also re-evaluates the two-arm waiver rule in the diff context — a waiver introduced or modified in this change is unreviewed by definition, so any finding it would otherwise suppress is reactivated in the report even though the head-state view (render/LSP/policy-check) shows it waived. This is a dry run: the CLI holds no approval state, so a fired gate just fails the check (exit 1) — recording and enforcing actual approvals is Studio’s job, not the CLI’s.

Terminal window
archlang policy-check <path>

Evaluates every policy declaration in the workspace against the derived dependency graph and reports the active (non-waived) findings. Policy severities:

  • error — fails the gate (exit 1).
  • warning — reported; fails only under --strict (exit 2).
  • advisory — report-surface only, never fails.
Terminal window
archlang policy-check --strict <path>
archlang policy-check --json <path>

A policy that uses a not-yet-supported selector or expression feature is skipped with a note on stderr rather than crashing the run — the rest of the policy set still enforces.

Terminal window
archlang evidence <path> [--repo=<path>] [--json]

Checks every sources: evidence binding in the workspace against the commit pinned in package.archspace (Chapter 37): the cited path is a blob at that revision, the line range fits inside it, and this checkout’s origin is the declared repository. --repo points at the code checkout; without it the current directory is used.

https://github.com/acme/shop @ a1b2c3d
✓ Checkout src/checkout/index.ts:12-88
✗ Checkout src/checkout/tax.ts:5-40
· Ledger src/ledger.ts
1 verified, 1 broken, 1 unverified

The exit code is 1 only if some binding is broken. A binding that could not be checked at all — no repository, no pin, the wrong repository — is reported as unverified and does not fail the run. The asymmetry is deliberate: a broken binding is a defect the author can fix, an unverifiable one usually means the machine running the check has no repository. unverified is still never a pass, and nothing displays it as one.

All seven evidence diagnostics are warning-severity, so they surface in validate and check without failing them; only the broken-binding exit code does that.

Terminal window
archlang set <path> <target> <field> <value> [--aspect]
archlang set <path> <target> <text> --description

Headless model write. <target> addresses a module by #id, a qualified path (Platform.Auth), or a unique name. Writes go through the same mutation layer the board/inspector editors use, so hand formatting and comments survive — and the write refuses (exit 1, printing the reason) instead of ever corrupting the file.

Terminal window
archlang set . Payments version 2.1 # field: version: 2.1
archlang set . Payments team Core --aspect # aspect: team: Core
archlang set . Payments "Owns payments" --description

Built for CI/CD use — bumping a version field on release, stamping a deploy environment — where a script needs to touch one field without hand-editing .arch source.

Terminal window
archlang export json <path>

Dumps the resolved workspace model as JSON — same shape as GET /api/model on archlang serve, so a CI job that wants the model can skip booting a server.

Terminal window
archlang export json . -o model.json # write to a file instead of stdout
archlang export json . --stdlib=<path> # override the stdlib lookup

The payload is { model: { modules, processes, subprocesses, views, types, documents?, backlinks? }, diagnostics: [...] }.

Terminal window
archlang export html . -o architecture.html

Writes ONE self-contained HTML file: the viewer shell, your workspace and the fonts all inlined. It opens by double-clicking it, on a machine that has never installed anything of ours, with the network unplugged — no server, no deployed viewer, nothing to configure.

That makes it the artifact for the places the embed of Chapter 23 cannot reach: attach it to a review, mail it to an architect, drop it in a release, hand it to someone outside your network. It is a full viewer inside the file — nav, spaces, process and table views, your .md documents (read-only, since the snapshot is frozen), the images they embed, PNG/SVG export — minus the editor and compare, which a frozen snapshot has no use for.

Terminal window
archlang export html . -o out.html --force # emit despite error diagnostics

-o is required (the file is megabytes of markup). The command refuses a workspace with error diagnostics unless you pass --force, and refuses to overwrite one of your own source files. --force also lets -o write outside the current directory, which is otherwise refused.

Text renders with the bundled Inter, the same font the headless PNG export uses — so the artifact matches archlang render more closely than it matches your browser, which substitutes its own system font.

It is an export, not a substitute for the embed: the file is a snapshot, so it does not update when the model does. For a living diagram on a page you control, embed the viewer.

Terminal window
archlang render [path] --out=<file.svg|file.png> [--view=board|bpmn|flow|sequence] ...

Headless SVG/PNG export — no browser, no server. It’s the same walk→layout→SVG pipeline the preview server’s /api endpoints and the MCP arch_render tool use.

Terminal window
archlang render . --view=board --out=board.svg # module board (default view)
archlang render . --view=bpmn --process=Orders.Checkout --out=flow.png --scale=3
archlang render . --view=flow --process=Orders.Checkout --out=flow.svg
archlang render . --view=bpmn --process=Orders.Checkout --out=diff.svg --diff-base=../old-workspace
archlang render . --view=board --out=compare.svg --diff-base=../old-workspace # Before/Delta/After + compare receipt
archlang render . --view-name=CheckoutOps --out=ops.svg # a declared flow view

Flags:

  • --out=<file> — required, and must end in .svg or .png (the extension picks the format; --format=svg|png overrides which bytes are written, not the required extension). The path must resolve inside the current directory and may not be one of the model’s own sources.
  • --view=<v>board (default), bpmn, flow, or sequence. bpmn/flow/sequence render a process and require --process.
  • --view-name=<name> — render a declared view by name instead (its flow body picks plain/sequence/bpmn, including a bpmn view’s lane/pool layout; view instances bind their knobs). Mutually exclusive with --process and --diff-base.
  • --process=<name> — qualified dotted process name (e.g. Orders.Checkout), required for bpmn/flow/sequence.
  • --expand=all|<ids> — expand do-subprocess nodes: all, or a comma-separated list of do-node ids.
  • --diff-base=<path> — diff against an older workspace at this path. With --view=board it renders a Before / Delta / After artifact (Delta is the union of both revisions, the only panel where an added and a removed thing appear together) and adds a compare block to the receipt: per-change classifications, an explicit identity statement, both sides’ raw and semantic hashes, and a limitations array naming what the comparison cannot tell you. With --view=bpmn it renders the union process scaffold with change accents. flow/sequence have no diff renderer and refuse.
  • --scale=<n> — PNG only: zoom factor for resolution (default 2, clamped to 8).
  • --force — render even though the model has error diagnostics, and allow an --out outside the current directory. It never allows overwriting a model source.
  • --stdlib=<path> — override the stdlib lookup path.

PNG rasterization goes through @archlang/render’s bundled resvg — deterministic output, bundled Inter fonts, no system font dependency.

Validate, then deliver. A model with error diagnostics is refused (exit 1) rather than rendered: the artifact would be missing whatever failed to resolve, and exit 0 would tell CI nothing is wrong. The artifact is written through a staging file renamed into place, so a refused or failed run leaves the previous artifact — the last good one — exactly where it was. Every run drops a <out>.receipt.json beside the target naming the input hash, the artifact hash, the counts and what the receipt does not claim (no perceptual review is ever asserted); on a refusal the receipt names the retained last-good artifact’s hash and lists each blocking diagnostic as a repair receipt — its registered code, the msgKey/params the message was rendered from (did-you-mean candidates included) and supportedFixes, which names the model or solver input a repair edits. A diagnostic whose root cause is another diagnostic in the same file is dropped, so the list names causes, not symptoms. The MCP arch_render tool (force: true, a <outFile>.receipt.json) and the server’s /api render routes (?force=1, HTTP 409 + the refusal receipt, X-ArchLang-Receipt on success) apply the same three rules.

Terminal window
archlang serve [path] [--port=<n>] [--web-root=<path>] [--quiet]

Starts the HTTP API server — the resolved model plus REST endpoints, and an optional web UI if --web-root points at a built one. Wraps @archlang/server’s CLI; every flag is forwarded through verbatim. Default port is 3100; --port=0 picks an ephemeral port. Runs forever — SIGINT/SIGTERM to exit.

Terminal window
archlang lsp

Starts the language server on stdio, for editor/IDE clients that want to launch the LSP as a subprocess rather than depend on a bundled copy. VS Code and IntelliJ bundle the LSP directly (see Chapter 22); archlang lsp is for building your own integration. Runs forever until the client sends the LSP exit notification or the process receives SIGINT/SIGTERM.

Terminal window
archlang conform <path> --source=jaeger|tempo --endpoint=<url>

Checks a deployed system against the model using OpenTelemetry traces — the model-to-reality direction, complementing validate’s syntax-to-model direction. Two dimensions: service-graph conformance (aggregate edges seen in traces vs. edges the model declares) and per-trace process conformance (does an observed trace match a modeled process’s steps, in order).

Terminal window
archlang conform . --source=jaeger --endpoint=https://jaeger.internal \
--metrics-endpoint=https://prom.internal --lookback=3600 --limit=1000 --json
  • --source=jaeger|tempo and --endpoint=<url> are required.
  • --metrics-endpoint=<url> — optional metrics backend for richer conformance signal.
  • --lookback=<sec> — trace query window (default 3600, clamped to a week).
  • --limit=<n> — max traces to pull (default 1000, clamped to 100,000).
  • Auth: set ARCHLANG_OTEL_TOKEN in the environment; it’s sent as the backend’s Authorization header and never logged or echoed.

Exit 1 on any conformance violation (telemetry-only edges the model doesn’t declare, unknown traces, extra/missing/reordered steps) or if the backend returns no data at all.

Terminal window
archlang info <path>

Prints a summary of the package: name, version, file/module/type/process/view/document counts, declared spaces, and the transitive dependency tree.

Use cases:

  • Onboarding. New team member runs archlang info against the package they’re about to work in; they get a one-screen overview without opening files.
  • CI artifacts. Save the output as a build artifact so PR reviewers can see what a branch’s architecture looks like at a glance.
  • Migration audits. Run against a legacy package being modeled, then re-run after each refactor pass.

A representative pre-commit hook:

#!/usr/bin/env bash
set -e
archlang format --check .
archlang validate .

A representative CI job:

Terminal window
archlang format --check .
archlang check . --against=origin/main
archlang info . > arch-summary.txt

format --check ensures every file is canonical. check --against runs validate + policy-check + format-check + evidence plus the change-gate dry run against the PR’s base branch. info produces the artifact for reviewers.

Treat any non-zero exit as failure for CI purposes.

  • 0 — success, no errors.
  • 1 — error-severity diagnostics, an active policy error, a format mismatch under --check, a broken evidence binding, a conformance violation, or a load failure.
  • 2--strict and at least one warning-severity diagnostic/finding (validate, check, policy-check).
  • 3--complete and at least one todo-severity diagnostic remains (validate only).
  • 64 — usage error: unknown subcommand, missing required argument.

validate --watch and serve run until interrupted; their reported exit code (0) reflects a clean shutdown via SIGINT/SIGTERM, not a validation result.

Diagram diffing isn’t its own subcommand — it’s two flags on the commands above: render --diff-base=<path> renders the visual diff (a Before/Delta/After board, or a BPMN union scaffold with --view=bpmn), and check --against=<ref> runs the change-gate diff against a git ref. The underlying diff is also a library API (@archlang/engine’s BpmnEdge.diff and friends) that the viewer’s diff mode consumes directly.

There’s no plugin system — everything the CLI does is one of the twelve subcommands above; customization happens by consuming the library packages (@archlang/engine, @archlang/viewer-core, @archlang/render) directly.

  • Twelve subcommands: validate, format, check, policy-check, evidence, set, export, render, serve, lsp, conform, info.
  • validate for the inner loop; check (optionally --against=<ref>) for CI; policy-check for governance; info/export for summaries and tooling.
  • set writes a single field/aspect/description headlessly; render produces headless SVG/PNG; serve runs the HTTP API; lsp runs the language server; conform checks a live system’s telemetry against the model.
  • Exit codes are stable; pipelines key off them.
  • No plugin system, no standalone diff subcommand — diffing lives in render --diff-base / check --against and the engine’s diff library API.

Chapter 22: Editor Integration → — what the LSP gives you in VS Code and JetBrains.