comline is the command-line interface to Comline.
It drives the everyday workflow around Comline schemas: scaffold a project,
validate it, freeze immutable versions, compare versions, and generate code — all
on top of comline-core.
This is the long-form reference. For a two-minute tour see the README.
- Overview
- Installation
- Project layout
- Versioning model
- Commands
- Global flags
- Output and exit codes
- Watch mode
- Shell completions and man pages
- Editor and pre-commit integration
A Comline project is a directory with a package manifest (config.idp) and one
or more schema files (src/**/*.ids), optionally a comline.toml for
code-generation output. Running a command takes those inputs through:
parse → resolve imports → validate → (freeze into .comline/) → (generate code)
check stops after validation. build adds the freeze step and records a new
version. generate stops after validation too — it does not freeze — and
writes generated code per comline.toml. diff reads two already-frozen
versions and reports what changed between them.
comline is not published to crates.io yet. Build it from source:
git clone https://github.com/ComlineProject/cli
cd cli
cargo install --path .This installs the comline binary into ~/.cargo/bin. Man pages and shell
completions are generated during the build — see
Shell completions and man pages.
comline new my-api creates:
my-api/
├── config.idp # package manifest: name, spec version, code_generation targets
├── comline.toml # where `generate` writes code and how (all-commented by default)
├── src/
│ └── main.ids # a sample enum + struct to build on
└── .gitignore # ignores .comline/deps-cache/
config.idp starts as:
congregation my_api
specification_version = 1
code_generation = {
languages = {
rust#1.85.0 = {}
}
}
The declared languages are a capability list only — a bare
language#lang_version. Which of them to generate, where, and for which package
versions is set in comline.toml (below).
The directory keeps the name you gave; the congregation name is that name
reduced to a valid Comline identifier (letters, digits and _), so
comline new my-api produces the directory my-api/ with congregation my_api.
After the first comline build, a .comline/ directory appears next to
config.idp. It holds the content-addressable store (objects/) and the version
ref (refs/heads/main). It is the package's version history, so commit it.
Only .comline/deps-cache/, where git dependencies are fetched, is a local cache;
the scaffold's .gitignore ignores that and nothing else.
Comline stores every build as an immutable commit in an append-only chain
under .comline/, git-inspired and content-addressed. There are no branches and
history is never rewritten, so any past version stays reproducible.
Each build compares the new schemas against the previous version and bumps the
package version automatically, by the largest change it finds:
| bump | when | examples |
|---|---|---|
| major | a breaking change | removed struct/enum/field/variant/function, changed field type, added a required field |
| minor | a new feature | added struct/enum/protocol/error, added a variant, added an optional field, new schema file |
| patch | a modification | field made optional, docstring-only change |
The first build is version 0.0.1. A build with no schema changes keeps the
current version and does not add a commit. comline diff runs this same
comparison between any two stored versions on demand.
The chain in .comline/ is the only copy of this history — there is no publish
step yet. comline clean does not touch it; only
comline reset discards it, after which the next build starts
over at 0.0.1.
Every command accepts the global flags. Run comline <cmd> --help for the authoritative synopsis.
comline new <name> [--git]
Scaffold <name>/ (see Project layout). Fails if the
directory already exists.
--git— also rungit initin the new project. Ifgitis missing or fails, the project is still created and a warning is printed.
comline new my-api
comline new my-api --gitcomline add <name> <dir> [--no-hash]
comline add <name> --git <uri> --commit <sha> --version <version> [--no-hash]
Declare another Comline package as a dependency, imported under <name>:
use <name>::models::Thing. It is resolved first, the way check resolves it,
so an entry that wouldn't resolve is never written:
<dir>— a package on disk, relative to this package's directory (written toconfig.idpas given);--git <uri> --commit <sha> --version <version>— a commit of a git repository, fetched into.comline/deps-cache/with thegitbinary.--versionis the version that commit stands for.
The entry goes into config.idp's dependencies block, created at the end of
the file if there isn't one, in the file's own indentation; every other line is
left as written. Its hash is pinned to the content just compiled, so if the
dependency changes later, check and build fail until the pin is updated.
--no-hash— don't pin the hash (say, a path dependency still being worked on).
comline add shared_types ../shared-types
comline add net --git https://github.com/acme/net --commit 4f2c9e1 --version 1.2.0Exit 0 once added, 1 if the dependency doesn't resolve or can't be added
(the name is already declared, isn't an identifier, or is already one of this
package's own namespaces), 2 if the directory is not a Comline project.
comline check
Parse, resolve and validate every schema and the package config, reporting the
first error found. It writes nothing to .comline/ (beyond fetching git
dependencies into .comline/deps-cache/) and does not bump the version —
safe to run from editors, pre-commit hooks and CI lint steps.
comline check
comline --path ./services/users checkExit 0 if valid, 1 if a schema or the config is invalid, 2 if the directory
is not a Comline project.
comline inspect [--json]
What each protocol speaks on the wire, and which guarantees it keeps. Like
check, it compiles the package and writes nothing to .comline/.
For each protocol, a short header names the format, framing and handshake it resolves to, with their parameters and where each came from:
| Source | Meaning |
|---|---|
from @framing / from @handshake |
the protocol's own annotation |
from config.idp ... |
the package's wire.format, wire.framing or handshake |
core's default |
nothing named one: packed@1, datagram@2, comline@1 |
JSON-RPC's format |
the framing is JSON-RPC and the package names no format |
Then one row per guarantee (schema agreement, encoding agreement, encoding
conformance, version honesty), held or given up. A given-up row says why, and
whether it has been accepted. A dependency's protocol is accepted by this
package's accept_unverified. Who accepts this package's own protocols isn't
known here: each consumer does that in its own config.idp. A protocol's
@unverified reason is in its header.
$ comline --plain inspect
protocol api::Legacy (shop)
format json@1 JSON-RPC's format
framing jsonrpc (method_names = qualified) from @framing
handshake none from @handshake
unverified "the 2009 gateway can't run a handshake"
schema agreement given up
because handshake `none`
acceptance: up to each consumer's config.idp
encoding agreement given up
because handshake `none`
acceptance: up to each consumer's config.idp
encoding conformance held
version honesty held
The report goes to stdout. --json prints the same as JSON, as
{ "protocols": [...] }, with each choice's source given as annotation,
package_default, core_default or framing. Each guarantee's acceptance
is { "state": "not_needed" | "unknown" | "not_accepted" }, or
{ "state": "accepted", "by": "<package>" }.
A protocol's compatibility facts (its line, version and shape hash) aren't shown, because they aren't built yet (core#119).
Exit 0 on success, 1 if a schema or the config is invalid, 2 if the
directory is not a Comline project.
comline build [--release] [--watch]
Compile and validate every schema, then freeze the result into .comline/,
bumping the version per the versioning model. Prints the
version change, a grouped changelog, and the bump that was applied.
--release— reserved for future optimization work; currently a no-op (prints a note).--watch— rebuild on change; see Watch mode.
comline build
comline build --watchcomline generate [--target <lang>] [--out <dir>] [--layout <template>] [--mode <mode>] [--external-std] [--watch]
Validate the project, then write generated code for each target. Unlike build,
this does not freeze a version or write to .comline/ — it stops after
validation.
Targets come from [[generate.target]] in comline.toml; if that file lists
none, from code_generation.languages in config.idp.
Where the code goes is set in comline.toml's [generate] table:
[generate]
out = "generated" # output root, relative to comline.toml
layout = "{{language}}/{{namespace}}.{{ext}}" # path of each file under the root
mode = "code" # code (loose source) | lib (a buildable package)
external_std = false # reference comline_std instead of inlining it (rust only)
package_versions = "latest" # latest | all | ["0.3.0", "0.4.0"]
# optional, one per target you want to pin or override:
[[generate.target]]
language = "rust"
out = "src/generated"comline.toml is committed, consumer-owned, and never frozen. With no
comline.toml (or an all-commented one), the defaults above apply. Layout
variables: {{language}}, {{namespace}} (/-joined), {{ext}},
{{lang_version}}, {{spec_version}}, {{package_version}}.
Editor support. comline.toml has a JSON Schema, generated from the
same types comline generate parses it with, at
https://comlineproject.github.io/docs/schemas/comline-toml/0.x.json. A
comline new project's comline.toml starts with a line naming it:
#:schema https://comlineproject.github.io/docs/schemas/comline-toml/0.x.jsonIt's a TOML comment, so comline ignores it; editors using Taplo (Even
Better TOML in VS Code) read it, and complete keys, describe them, and flag
an unknown key or a wrong value (mode = "dylib") as you type. Add the line
to an existing comline.toml to get the same.
layout only applies to code mode. A lib-mode target ignores it
entirely (comline generate warns if you set one) and always writes the
crate's fixed internal shape (Cargo.toml, src/lib.rs, …) at
<out>/<language>/ — unless the target sets flatten = true, which writes it
straight into out instead:
[[generate.target]]
language = "rust"
mode = "lib"
out = "../my-rust-crate/"
flatten = true # crate lands at out/ directly, not out/rust/flatten only means something for lib/dylib targets with their own out
— flattening a shared out across several lib targets would land two
crates in the same directory.
External generators. A target can use a generator outside this binary:
[[generate.target]]
generator = "tools/comline-gen-go.wasm" # or a native executable
language = "go" # optional if the generator offers one language
timeout = 120 # seconds per call, default 60generator is a path, relative to comline.toml's directory. The generator
speaks the generator protocol: one CBOR request on stdin, one CBOR response on
stdout. comline asks it to describe itself first, and refuses one whose
protocol version or IR format it doesn't share, naming both sides, or one that
doesn't offer the target's language at its version. language picks among the
languages it offers and must still be one config.idp declares; lang_version
applies as for a built-in.
- A
.wasmruns under wasmtime (cargo featuregen-wasm, on by default), sandboxed: no preopened directories, no environment variables, no network, no arguments beyondargv[0], and at most 1 GiB of memory. - Anything else runs as a native process with an empty environment. It isn't sandboxed: run only executables you trust.
- Either is stopped after
timeoutseconds, and refused if it writes more than 256 MiB to stdout or generates more than 256 MiB of files. Its stderr is shown prefixed with its file name, up to 64 KiB. - Every path in its response must be relative and normalized: no
..or.components, no absolute paths, no drive or UNC prefixes (:,\), no empty components, no NUL. A response with any bad path is refused whole, and nothing from it is written.
The built-in generators answer the same protocol in process, so their output goes through the same checks.
Which versions. package_versions = "latest" (the default) generates the
working tree — no .comline/ read. "all" generates every committed version in
the CAS chain; ["0.3.0", "0.4.0"] (versions or commit hashes) generates just
those. Anything but latest requires the project to have been built at least
once, and — because more than one version would otherwise write to the same
paths — the layout must contain {{package_version}}. For latest,
{{package_version}} is the last committed version (empty if never built).
--target <lang>— only this language (case-insensitive). Errors if nothing matches.--out <dir>/--layout <template>/--mode <mode>/--external-std— override one target's[generate]values. When more than one target is configured they require--target(otherwise it is ambiguous which they apply to).COMLINE_GENERATE_OUT/COMLINE_GENERATE_LAYOUT/COMLINE_GENERATE_MODE/COMLINE_GENERATE_EXTERNAL_STD— the same overrides from the environment, sitting just below the flags but applied to every target (handy in CI). A flag still wins for its target.--watch— regenerate on change; see Watch mode.
external_std (Rust only). When on, std is referenced as its own
comline_std crate (a git dependency lib mode adds to the generated
Cargo.toml automatically) instead of being re-emitted into the output on
every run — a reference qualifies to comline_std::http::Request instead of
a locally-inlined crate::std::http::Request. code mode has no manifest to
edit on your behalf, so a file that ends up referencing it carries a comment
naming the dependency line to add by hand. Default false (inlined,
unchanged).
Currently rust and typescript are the generators; code mode is
implemented for both and lib mode for rust. dylib, and lib for other
languages, fail with a clear message.
comline generate
comline generate --target rust --out ./bindings
comline -p ./schemas generate --target rust• Generating code (in schemas/)
⚙️ language: rust, version: 1.85.0
main.ids → generated/rust/main.rs
other.ids → generated/rust/other.rs
✓ Generated 2 file(s)
The working directory is named on the header; each line under a target maps a
source schema to the file written for it (--plain uses ->).
comline diff <old> [<new>]
Compare two frozen versions from .comline/ and print the breaking changes, new
features and modifications between them — the same report build shows, on
demand. <new> defaults to HEAD.
Each argument is one of:
- a version string, e.g.
0.2.0 - a commit hash (full, or a prefix of 4+ characters)
HEAD— the latest build
comline diff 0.1.0 0.2.0
comline diff 0.1.0 # 0.1.0 against HEAD
comline diff 0.0.1 HEADFails with exit 2 if the project has never been built, and exit 1 if an
argument matches no stored version (the error lists the versions that exist).
comline doc [--out <dir>] [--version <v>] [--changelog-from <v>] [--format <f>[,<f>...]]
Write the package's documentation for every version: each release, each dev
build and the unreleased working state (when it differs from the last build).
Nothing is written to .comline/.
Each format lands in <out>/<format>/, with one folder per version:
0.2.0/, 0.3.0-dev.2/, and unreleased/ for the working tree. Each
version carries the changelog from the version before it, computed by core's
comline diff rules.
- Pages (
markdown, the default, rendered by the official renderer linked intocomline): per version,index.md(the package, its//!header, its modules), one page per module (types::useristypes/user.md) with its//!header and every unit with its///docs, fields, arguments and each protocol's wire summary,changelog.md, andnav.yml, anavfor mkdocs or zensical.<out>/markdown/index.mdlists the versions. Switching versions on a published site is left to site tooling, such as mike for mkdocs. htmlwrites a self-contained static site per version, rendered by the official HTML renderer, also linked in:index.html, one page per module (types/user.html),changelog.html,style.cssandsearch.js. It has the same content as the Markdown pages, plus a sidebar of modules and units, a search box over the units, a version picker in the header, and light and dark themes that follow the OS setting. The picker opens the same page and unit in the chosen version, or that version's index with a note when it has no such page. At<out>/html/,index.htmlpoints at the latest version (the newest release, else the newest dev build), andcompare.htmlcompares versions: two show what changed between them, grouped by unit and labelled major, minor or patch; more show a grid of every unit and member per version. The labels come from the changelogs core computed between each version and the one before it; the page never judges a change itself. Nothing loads from the network and every link is relative, so the site opens straight from disk or from any static host.jsonwrites each version's documentation model,model.json, in the shape of core'sschemas/doc-model.json, and<out>/json/index.jsonlisting the versions.
| Flag | Default | Meaning |
|---|---|---|
--out <dir> |
[doc] out, else docs/api |
where to write, relative to the project |
--version <v> |
every version | document only this built version (a version string, a commit hash, or HEAD), into <out>/<format>/<v>/ |
--changelog-from <v> |
[doc] changelog_from |
with --version: add what changed from built version <v> |
--format <f> |
[doc] formats, else markdown |
markdown, html, json, or another id the [doc] renderer offers; repeat it or separate with commas |
Caching. A built version never changes, so its folder is kept between
runs. <out>/<format>/.comline-doc.json records the key each folder was
rendered with: its commit, the commit of the version before it, the renderer
and its version, the model version and the links. A built-in renderer's
version carries a content hash of its sources and assets (0.1.0+<hash>),
so a comline with a changed renderer re-renders every folder; an external
renderer is keyed by its file's hash. A run re-renders only the
folders whose key changed, which is the unreleased state and new versions,
and removes the folders of versions no longer listed.
A built version keeps no source, so its protocols have no wire summary, and a
version built before headers were stored has no //! headers. Its pages say
so in a note.
comline.toml's [doc] table:
[doc]
out = "docs/api"
formats = ["html", "markdown", "json"]
versions = { last = 5 } # or "all" (the default), { since = "0.3.0" }, { releases_only = true }
renderer = "renderers/my-renderer.wasm" # optional: an external renderer
timeout = 60 # seconds per call to `renderer`
links = { std = "https://comlineproject.github.io/docs/std/" }
changelog_from = "0.1.0" # with --version onlyversionstrims the list.last = Nkeeps the newest N (the unreleased state counts as one),sincekeeps that version and newer, andreleases_onlykeeps releases alone (no dev builds and no unreleased state). They combine, applied in that reverse order:releases_only, thensince, thenlast.rendereris run exactly like an externalgenerator: a.wasmunder wasmtime with no filesystem, environment or network, a native executable with an empty environment, the same limits and path checks. It must list the format in itsDescribeanswer'srenderers. One that doesn't render a site index (sitesin itsDescribeanswer) still renders every version's folder.linksmaps a dependency (orstd) to the base URL of its documentation. A type from it links to<base>/<module>/#<anchor>; a dependency not listed is plain text.
comline new adds docs/api/html/ to .gitignore.
comline doc
comline doc --format html,markdown
comline doc --version 0.2.0 --changelog-from 0.1.0
comline doc --format html --out site
comline doc --format jsoncomline conformance --impl "<command line>" [--encoding <name>]... [--corpus <dir>] [--timeout <secs>]
Run an implementation of an encoding against its vectors, the way the
official runtimes are proven against the official corpus. A package that
declares an encoding under encodings in config.idp ships its vectors
beside its profile: the profile's path with .json replaced by
.vectors.json.
encodings = {
"com.acme.lines@1" = { kind = "framing" profile = "profiles/lines.json" }
}
profiles/lines.json the profile: facts, each naming the cases that prove it
profiles/lines.vectors.json the vectors: {"feature": ..., "cases": [...]}
The vectors file and the profile are in the conformance corpus's case
format, a public spec (version 0.1, early days), documented in
ComlineProject/runtime's runtime/conformance/README.md. Every case in the
file is run, and each profile fact's proven_by names cases in it.
The implementation is a process speaking the conformance ops protocol,
as comline-reference does: newline-delimited JSON requests on stdin, one
reply line each on stdout, exit 0 when stdin ends. Its version reply lists
the encodings it implements under formats and framings. It runs once,
with every question sent up front (a case's questions never depend on the
answers), under the same rules as an external generator: a cleared
environment, a time limit (--timeout, 60 seconds by default) and a capped
stdout; its stderr is shown, prefixed with its name. The command line is
split on whitespace (no shell quoting); a bare program name is looked up on
PATH.
The report, on stderr, gives per encoding the cases that pass, each profile fact as proven or not (and why), and each failed case, an encode with a byte diff:
com.acme.lines@1 (framing): 4/5 case(s) pass
proven calls: by_name
error unproven errors: by_ordinal — case `lines/error` failed
error lines/error failed: encodes wrong:
expected: 455252203720322036653666
got: 455252203720332036653666
^^ byte 6 differs
A failed case, a case the implementation reports as not implemented, or an
unproven fact makes the exit code 1.
--impl <command line>: the implementation.--encoding <name>: only this encoding (repeatable). Default: every encoding the package declares.--corpus <dir>: run a corpus laid out like ComlineProject/runtime'sruntime/conformance(profiles/<name>.json,vectors/*.json) instead of the package's declarations: the official encodings (packed@1,datagram@2, ...), whose vectors live there. Each encoding runs every case of the features its profile's facts name.--timeout <secs>: how long the implementation may take for all the cases together.
comline conformance --impl ./target/release/acme-lines-conformance
comline conformance --impl comline-reference --corpus ../runtime/runtime/conformance --encoding packed@1comline clean [--dry-run]
Remove what generate wrote — the whole output root when it is a dedicated
directory (e.g. generated/), otherwise just the individual generated files.
Resolved the same way generate resolves output (comline.toml [generate],
else config.idp's declared languages).
Does not touch .comline/. Generated code is regenerable; the version
history is not — comline reset is the command for that.
--dry-run— list what would be removed without deleting anything.
Needs the project to compile to know what generate would have written; if it
does not, there is nothing to clean.
comline clean --dry-run
comline cleancomline reset [--force] [--dry-run]
Delete .comline/ — every frozen version and the commit chain — and
generated code. This cannot be undone, and since nothing else holds the history,
the next build starts over at 0.0.1.
Guarded:
- on a terminal, you are shown how many versions will be lost and must type
resetto proceed; - without a terminal (CI, scripts) it refuses with exit
2unless--forceis given. --dry-run— list what would be removed, and the history that would be lost, without deleting anything. No confirmation needed.
comline reset --dry-run
comline reset # asks for confirmation
comline reset --force # scriptedcomline completions <shell>
Print a completion script for <shell> to stdout. Supported: bash, zsh,
fish, powershell, elvish.
comline completions bash > /etc/bash_completion.d/comline
comline completions fish > ~/.config/fish/completions/comline.fish
comline completions zsh > ~/.zfunc/_comline| flag | effect |
|---|---|
-p, --path <DIR> |
Run against <DIR> instead of the current directory. Headers echo it back (Building project (in <DIR>/)). |
-v, -vv, -vvv |
Raise log verbosity. Default shows only warnings and errors from comline-core; -v adds info, -vv debug, -vvv trace. |
-q, --quiet |
Silence all progress output; only errors are printed. Conflicts with -v. |
--plain |
Plain output: no color, no leading symbols (• / ✓), no emoji in the changelog, no spinner. For log files and CI. |
RUST_LOG overrides the verbosity flags entirely if set (standard
tracing_subscriber syntax, e.g. RUST_LOG=comline_core=debug).
- stderr carries all human-facing output: progress steps, the changelog,
warnings, errors, and
-vdiagnostics. - stdout is reserved for machine-readable payloads. Today only
comline completionswrites there, socomline completions fish | sourceand similar pipelines are clean. - Color is applied through
anstream: it is stripped automatically when stderr is not a terminal or whenNO_COLORis set.--plainalso drops the leading symbols and changelog emoji, not just color. - The progress spinner is shown only on an interactive terminal, and is
suppressed under
-v(so it doesn't fight the log output),--quietand--plain.
Exit codes are stable for scripting and CI:
| code | meaning |
|---|---|
0 |
success |
1 |
the command ran but failed — invalid schema, unresolved diff argument, a generator or filesystem error |
2 |
a precondition was not met — the directory is not a Comline project, nothing has been built yet, or a destructive command (reset) was not confirmed. clap also exits 2 for usage errors. |
if ! comline check --quiet; then
echo "schemas are invalid" >&2
exit 1
ficomline build --watch and comline generate --watch run the action once, then
re-run it whenever a file under src/ changes or config.idp / comline.toml
is modified
(300 ms debounce). A failing run is reported but does not stop the loop —
fix the error and save again. Press Ctrl-C to exit.
comline generate --watch --target rustTwo paths, for two audiences:
-
Users:
comline completions <shell>(above) prints a script on demand. -
Packagers: every build also writes, into the crate's
OUT_DIR:man/comline.1andman/comline-<subcommand>.1completions/comline.{bash,zsh,fish,elv}and_comlinefor PowerShell
After
cargo build, find them undertarget/<profile>/build/comline-*/out/{man,completions}/.
Use comline check — it validates without writing to .comline/ or bumping the
version, so it is safe to run on every save or in a hook.
# .git/hooks/pre-commit
#!/bin/sh
exec comline check --quiet