CoHDL
  1. Docs
  2. CLI reference

CLI reference

The whole toolchain is one binary, cohdl, with focused verbs for compiling, canonical source, editor support, registry discovery and dependency management. The same source and locked dependency set produce the same verdict and the same output bytes.

One binary, focused verbs

Verbs that operate on a project take an optional PATH — either a project directory (containing cohdl.toml and src/) or a single .cohdl file — and default to the current directory. login, lsp and self-update take no path. search takes a query instead and works without a project.

Verb Purpose
cohdl check Parse, resolve, type-check and run residual DRC. Writes no build artifacts.
cohdl build Everything check does, then assign designators, bind parts and emit the netlist, BOM, footprints and layout artifacts.
cohdl fmt Rewrite .cohdl source into canonical form (--check to gate in CI).
cohdl lsp Start the Language Server Protocol server on stdio.
cohdl self-update Replace the installed binary with the newest released one (--check to report only).
cohdl search Search registry packages and the most-recently-published API docs for public parts.
cohdl update Re-resolve dependencies to the greatest published semantic version; rewrite the manifest and lock file.
cohdl add Add a dependency, fetch it, and pin it in cohdl.lock in one step.
cohdl remove Remove a dependency and its lock row — the inverse of add.
cohdl install Resolve every pinned dependency, fetching anything missing from the registry.
cohdl login Store a registry token for publishing.
cohdl publish Package the current project and publish it to registry.cohdl.org.
cohdl docs Emit package API documentation or upload it to an already-published version.

cohdl check

cohdl check [PATH] [--design NAME] [--std DIR | --no-std] [--json]

check runs the whole verdict ladder: the source parses, every name resolves, every unit and trait obligation type-checks, every required pin is connected, and the four residual DRC rules (voltage-exceed, polarity-mismatch, single-driver, multi-driver) pass. Before any .cohdl file is opened, the manifest's [dependencies] pins are verified against cohdl.lock — including each dependency's content hash — so a verdict is never computed against library content the lock file did not record. check writes nothing under out/ and never touches design.lock; the one file it may write is cohdl.lock, when the manifest's dependency set has changed — a row the first time a dependency resolves, a rewrite when a pin was edited, a removal when a dependency left the manifest.

  • --design NAME selects which design declaration to assemble when the project contains more than one; the default comes from [design] top in the manifest.
  • --std DIR is a development override that replaces the pinned std package (the COHDL_STD environment variable does the same). Using either emits the unsuppressable E1105 warning: the build no longer uses a locked std and must not be treated as reproducible. --no-std opts out of std entirely.

Structured diagnostics with --json

check --json (and build --json) emits exactly one JSON document to stdout instead of human-readable text. It is the same diagnostic pipeline, restructured — the JSON and plain-text outputs always report the identical diagnostic set, field for field, and that equivalence is a tested property of the compiler. The document carries:

  • schema_version — an integer, bumped only on a breaking change to the document's own shape. Consumers must check it before parsing further; new diagnostic codes are ordinary content, not schema changes.
  • verdict"pass" or "fail", computed exactly as the exit code is: any error-severity diagnostic means "fail".
  • diagnostics — a flat, ordered list. Each entry has a stable code, a severity, a message, a primary span (project-relative file, 1-based line and column range, and its own label), zero or more secondary spans, and a help list, verbatim from the text renderer.
  • On build --json, a build object naming the emitted artifact paths — present only when the verdict is "pass".

Here is a real capture. In a copy of the Pico 2 reference design, the 3.3 V rail's net annotation was changed from [3.3V] to [100nF] — a capacitance where a voltage belongs:

src/main.cohdl the deliberate mistake
net V3V3 [100nF]: reg.VOUT, reg.FB, mcu.IOVDD, mcu.QSPI_IOVDD, mcu.USB_OTP_VDD, mcu.VREG_VIN,
                 flash.VCC, hdr.V3V3, q_vsys.Gate, r_vreg_avdd.A, r_vref.A

The compiler's answer, verbatim (exit code 1):

$ cohdl check . --json
{
  "schema_version": 1,
  "verdict": "fail",
  "diagnostics": [
    {
      "code": "E110",
      "severity": "error",
      "message": "net voltage annotation has the wrong unit type: expected `Voltage`, found `Capacitance`",
      "primary": {
        "file": "src/main.cohdl",
        "start_line": 67,
        "start_col": 15,
        "end_line": 67,
        "end_col": 20,
        "message": "`100nF` is a `Capacitance`"
      },
      "secondary": [],
      "help": [
        "annotate with a voltage (e.g. `[3.3V]`), or `[gnd]` for ground"
      ]
    }
  ]
}

Note what the diagnostic contains: a stable code from the error-code registry, the exact span of the offending literal, and both the expected and the actual unit by name. That precision is a language rule, not a courtesy — it is what lets a tool (or a model) repair a design from diagnostics alone.

cohdl build

cohdl build [PATH] [--design NAME] [--std DIR | --no-std] [--out-dir DIR]
            [--emit ipc2581] [--emit kicad_pcb] [--json]

build runs the full check, then assigns designators (recorded in design.lock, so they stay stable across rebuilds), binds part numbers, and emits the fabrication artifacts. Output goes to out/ under the project directory, or to --out-dir DIR.

Artifact Path When
KiCad netlist out/<name>.net Always — importable by KiCad's pcbnew.
Bill of materials out/<name>-bom.csv Always.
Designator lock design.lock (project root) Always — commit it; it is the record of designator stability.
Footprints out/footprints/*.kicad_mod One per pad-bearing footprint the design uses.
Layout constraints out/<name>-layout.json Only when the design declares layout metadata.
Physics hints CSV set in out/ Only when the design carries physics attributes — the constraint files the Quilter autorouter reads.
IPC-2581 handoff out/<name>.xml Only with --emit ipc2581.
KiCad board out/<name>.kicad_pcb Only with --emit kicad_pcb — a native KiCad 10 board file with placements and net-bound footprints, written by the compiler itself (no KiCad installation).

The output directory is managed, not just written to. Each build records the set of files it wrote; on the next build, a file the compiler wrote before but no longer produces — say, a layout document after the design's layout metadata was removed — is deleted as stale, and a file the compiler did not write is never overwritten. A transcript from the Pico 2 reference design, with the footprint list trimmed here (the real build writes seventeen .kicad_mod files):

$ cohdl build examples/rpi-pico2
  Built design `Pico2`: 52 instances, 67 nets
  wrote examples/rpi-pico2/out/rpi-pico2.net
  wrote examples/rpi-pico2/out/rpi-pico2-bom.csv
  wrote examples/rpi-pico2/out/rpi-pico2-layout.json
  wrote examples/rpi-pico2/out/footprints/passive-CHIP_0402.kicad_mod
  wrote examples/rpi-pico2/out/footprints/qfn-QFN60N40P700X700_1EP340X340.kicad_mod
  wrote examples/rpi-pico2/out/footprints/usb-connectors-type_c-FP_USB_C_Receptacle_HRO_TYPE_C_31_M_12.kicad_mod
  wrote examples/rpi-pico2/design.lock

Every artifact is byte-deterministic: rebuilding the same source against the same locked dependencies produces identical files, so generated output can be committed and diffed like any other source.

cohdl fmt

cohdl fmt [PATH] [--check]

fmt rewrites every .cohdl file at PATH into the language's canonical form — one authoritative layout, so diffs show design changes and never formatting churn. In a project directory it also canonicalizes the manifest's [dependencies] section (entries sorted by name); it never touches cohdl.lock, which is machine-generated.

fmt only formats valid source. A file that does not parse is reported with the ordinary parse diagnostics and left untouched — fmt is not a repair tool. With --check nothing is rewritten: each drifted file is reported as would reformat <file> and the command exits non-zero, which is the shape a CI gate wants. When everything is already canonical it prints All files are in canonical form. and exits 0.

cohdl lsp

cohdl lsp

Starts the Language Server Protocol server on stdio. It takes no flags or arguments — the editor owns the transport. The server exits 0 after an orderly shutdown/exit sequence, 1 on exit without shutdown (as the protocol specifies), and 2 on a transport failure. Editor wiring — including the VS Code extension that packages this server — is covered in Editor support.

cohdl self-update

Replaces the running binary with the newest released one, consuming the same artifact contract as the one-line installer: it finds the newest vX.Y.Z release on GitHub, verifies the download against the release's published sha256sums.txt, and swaps it in place; a release that publishes no build for this platform is reported as an error. --check reports whether a newer release exists and installs nothing. It takes no other arguments.

cohdl search

cohdl search QUERY [--json]

Searches both registry packages and package-local public part declarations. It is read-only, needs neither a project nor a registry login, and never changes a manifest, lock file or package cache. The query is trimmed, must contain at least three Unicode scalar values, may occupy at most 128 UTF-8 bytes, and may not contain a control character.

cohdl search TPS59650
cohdl search TPS59650 --json
cohdl search -- -12V

Human output has separate Packages and Parts sections. Package hits include an actionable name@version; part hits include the owning package@version, importable fully-qualified name, trust tier and matching manufacturer/MPN metadata. The JSON form carries the identical bounded rows as one document: top-level query, packages and parts, with a results array and has_more boolean in each result family. There is no total count; has_more is the explicit truncation signal.

Part search is projected from the most-recently-published version's cohdl docs API sidecar. It indexes the owning package, fully-qualified and short names, device, intent, arguments, structural variant, and primary and alternate AVL fields within fixed resource-safety projection budgets. “Latest” here therefore means most recently published, and every row names that exact version; it is deliberately different from the greatest-semantic-version selection used by an unversioned cohdl add or cohdl update. Each existing package's most-recently-published version becomes searchable after its owner re-runs cohdl docs --publish once to backfill the index.

A valid query with no hits prints No packages or parts matched `QUERY`. and exits 0. Invalid input is an invocation error (exit 2, prose on stderr, nothing on stdout). A registry or response-protocol failure is E1204 and exits 1: the human form writes the diagnostic to stderr, while --json writes the existing diagnostic JSON document to stdout.

Registry commands

Dependencies in CoHDL are pinned to exact X.Y.Z versions — range syntax is rejected at parse time, permanently — and cohdl.lock records each dependency's version and sha256 content hash, re-verified on every check and build. The dependency-management commands below are the sanctioned ways those pins change; search and docs leave them untouched. This is a summary; Packages & registry covers the model.

  • cohdl search QUERY [--json] — discover packages and the most-recently-published version's public parts, without a project or login.
  • cohdl add NAME[@X.Y.Z] [PATH] — resolve the greatest published semantic version (or the exact one given), fetch it into the local cache, and write the [dependencies] entry plus its lock row in one step. The three-tier namespace is validated before any network call: a bare name is CoHDL official, @brand/name is a verified manufacturer, and @contrib/name is community.
  • cohdl remove NAME [PATH] — delete the dependency entry and its lock row; the symmetric inverse of add.
  • cohdl install [PATH] — resolve every pinned dependency per cohdl.lock, fetching anything missing from the registry. A content-hash mismatch is a hard error, never a silent re-fetch.
  • cohdl update [NAME] [PATH] [--dep NAME] — the only sanctioned pin change: re-resolve one dependency (named positionally or with --dep) or all of them to the greatest published semantic version, registry first with local packages as the fallback, rewriting [dependencies] and cohdl.lock together. It also migrates a manifest that predates dependency pinning by writing its first [dependencies] section.
  • cohdl login — opens the registry account page for you to create a token; paste it at the prompt and it is verified and stored.
  • cohdl publish [PATH] — package the project and publish it. The namespace tier, [package] version and [package] license are all checked locally before anything is uploaded. The server independently recomputes the content hash, and that server-side hash is what consumers' lock files verify against.
  • cohdl docs [PATH] [--out FILE] [--publish] — emit the package's API-document JSON; --publish uploads it to an already-published version and is the idempotent backfill path for registry part search.

Two environment variables override registry defaults: COHDL_REGISTRY replaces registry.cohdl.org as the registry endpoint, and COHDL_HOME replaces ~/.cohdl as the location of the package cache and stored credentials.

Exit codes

Exit codes separate "the design has problems" from "the invocation has problems", and the two never blur:

Code Meaning
0 Clean. Warnings are allowed on a clean exit, and a valid search with no matches is still successful.
1 The accepted command failed. Compiler errors render as source diagnostics; fmt --check uses 1 when a file is not canonical; and search uses E1204 for an unreachable registry or invalid response protocol. Human diagnostics render to stderr; with --json, stdout carries exactly one JSON document instead.
2 Invocation-level failure — the E000 class: bad flags, a flag invalid for the command, an invalid search query, a missing project, a design-selection failure, or nothing to build. Prose on stderr, never a JSON document. Source diagnostics collected before the failure still render to stderr first — they are never discarded.

The stream split is deliberate: stdout belongs to machine-readable output (a successful search --json result document or a diagnostic document) and stderr to human-readable text, so a tool never has to parse around progress messages.