diagc reference
The command-line interface. Invoked as diagc <command> from an install, from a global link, or through the repo's pnpm scripts.
Installing
curl -fsSL https://raw.githubusercontent.com/Ferroman/diagc/main/install.sh | sh
npm i -g @diagc/cli # global
npx @diagc/cli studio # without installing
npm i -D @diagc/core # optional: types for .diagram.ts authoring
Node ≥ 22. The install script (install.sh, macOS and Linux) needs only curl and tar: it uses the Node on your PATH when that is new enough, and otherwise downloads the current Node 24 into its own directory, checked against nodejs.org's published checksums. Nothing needs root.
| Variable | Default | Effect |
|---|---|---|
DIAGC_VERSION |
latest |
the @diagc/cli release to install |
DIAGC_HOME |
~/.local/share/diagc |
where the package (and a downloaded Node) go |
DIAGC_BIN_DIR |
~/.local/bin |
where the diagc launcher goes |
DIAGC_NODE |
download fetches a private Node even when the PATH has one |
|
DIAGC_NODE_MIRROR |
https://nodejs.org/dist |
where Node is downloaded from |
Re-running the script updates to the latest release. To uninstall, delete DIAGC_HOME and the launcher.
Everything the CLI needs is in the package — the prebuilt studio, the viewer shell publish stamps models into, and the icon library — so it runs against any directory with no checkout and no pnpm.
From a checkout, pnpm link --global inside the repo gives you a diagc that runs the working tree instead. Both layouts are detected automatically; the differences are called out below where they matter.
diagc <init|compile|lint|watch|publish|studio|eject|diff|guide> [files...] [--out dir] [--no-images] [--link url]
An unknown command exits 1 with that usage line.
Commands
init
Sets a repository up for diagc, from the installed CLI alone.
diagc init # .diagrams/src/example.diagram.ts from the basic starter
diagc init shop --type c4 # a C4 starter named shop
diagc init --agents # point coding agents at the guide (AGENTS.md / CLAUDE.md, Claude Code skill)
- Steps, in order: writes
.diagrams/src/<name>.diagram.tsfrom the bundled starter for--type; appends whichever of.diagrams/.artifacts/,.diagrams/html/and.diagrams/diff/are missing from.gitignore(creating it if needed); with--agents, writes a short block intoAGENTS.mdandCLAUDE.md— whichever exist, or a newAGENTS.md— between<!-- diagc:begin -->and<!-- diagc:end -->, replacing it in place on a later run, and — where the repository has a.claude/folder or aCLAUDE.md— the Claude Code skill.claude/skills/diagc/SKILL.md, whose text between the same markers it replaces on a later run; compiles the starter; prints what to do next. - Name:
exampleby default; a folder is allowed (team/app); lowercase letters, digits,-and/.shop.diagram.tsand.diagrams/src/shopmeanshop. - Type:
basicby default; the types are the diagram typesdiagc guidelists, plusbasic. An unknown type lists them. - Already set up: with neither a name nor
--type, and a diagram already under.diagrams/src, the starter step is skipped —diagc init --agentscan run in any repository. - Refuses, writing nothing: a name whose
.diagram.tsor.diagram.jsonexists, an unknown type, an unsafe name. Exit1with one line on stderr. - Never: runs a package manager (it prints
npm i -D @diagc/core@<version>when there is apackage.json; the types are for the editor, the CLI compiles without them), or asks a question — arguments and flags only, so a script or an agent can run it. - Exit code:
0when every step it attempted succeeded.
compile (default)
Compiles sources to validated artifacts. Running diagc with no command runs this.
diagc compile
diagc compile .diagrams/src/acme.diagram.ts
- Input:
files...if given, otherwise everything matching.diagrams/src/**/*.diagram.{ts,json}. - Output: one
<name>.diagram.jsonper source under--out. Subdirectories of.diagrams/src/are mirrored, sosrc/team-a/app.diagram.tsbecomes.artifacts/team-a/app.diagram.json. - Exit code:
0if every file compiled,1if any failed. Failures print✗ <file>and the error; other files still compile. A source that throws while it runs — a misspelled method, a builder call that refuses — is placed at<file>:<line>:<column>, the first stack frame inside that file.
A remote (https://…) include resolves from a vendored snapshot, not a live fetch — locked mode, the default compile (and watch/publish/studio/eject) runs in. It reads .diagrams/includes/<file> and checks its hash against .diagrams/includes.lock.json; an include missing from the lock, or whose vendored file is gone or no longer matches its recorded hash, fails the compile naming the remedy:
Include 'https://example.com/svc.diagram.json' is not snapshotted — run 'diagc compile --update-includes' and commit .diagrams/includes/ + .diagrams/includes.lock.json
diagc compile --update-includes # fetch every remote include, vendor it, rewrite the lock
Pruning entries the run no longer touched only happens on a full-tree run — pass no files... alongside --update-includes, or nothing is pruned. Local file includes are never vendored; they resolve straight off disk on every compile. See Compose diagrams § Snapshots.
lint
Reports what compiles but is probably a mistake — the slips a generated diagram makes and nothing else catches, because each one still draws.
diagc lint
diagc lint .diagrams/src/acme.diagram.ts --json
- Input: as
compile. Nothing is written. - Output: one line per finding,
<file>: <severity> <code>: <message>, or with--jsonone array of{ file, severity, code, message, ref?, line?, column? }. A source that does not compile reports its validation errors (severityerror); one that does reports its warnings (severitywarning): the lint codes plus the notation warningscompileprints. A source that throws while it runs is oneloadfinding; when the stack places it, the text form reads<file>:<line>:<column>: error load: <message>and the JSON form carrieslineandcolumn. - Exit code:
0when there are no findings,1otherwise — so a script or an agent can loop until it is clean.
diff
Compares the diagrams at two git refs, or at one ref and the working tree, and draws what changed. How to use it: Show what changed between two versions.
diagc diff v1.0..v2.0 # two refs
diagc diff main # a ref against the working tree
diagc diff v1.0..v2.0 checkout # only these diagrams, by name
- Input: the
.diagrams/tree at each ref, read withgit archive(nothing is checked out), and every*.diagram.{ts,json}in it compiled in memory. Diagrams pair up by name. - Output: the change list on stdout (
+added,-removed,~changed), or with--jsonone object{ from, to, diagrams: [{ name, status, diff?, error? }] }. When anything changed, it also writes to.diagrams/diff/<from>..<to>/(or--out): a page and a PNG per side of each changed diagram with the changes outlined,index.htmlwith the two sides next to each other, andsummary.mdfor an ADR.--no-imagesskips the PNGs; they also need Chrome, aspublishdoes. - For a comment elsewhere:
--labels main,#12names the two sides in place of the refs, and--image-url 'https://host/pr-12/{path}'makessummary.mdlink its images there ({path}is each PNG's path under the output directory). The pull-request action uses both. - What counts: nodes by id (any field, plus a move to another container), relations by id or else by endpoints and kind, layers and planes by id. Layout positions are not compared.
- Exit code:
0whether or not anything changed;1for a ref that does not exist or a failure.
watch
Recompiles on change until interrupted.
diagc watch
diagc watch .diagrams/src/team-a
Takes an optional directory (default .diagrams/src). Writes to --out. On a failed recompile it prints the error and keeps the last good artifact, so the studio never blanks out mid-edit.
publish
Compiles everything, then writes shareable output.
diagc publish
diagc publish acme shop # only these diagrams
diagc publish --no-images
diagc publish --link https://github.com/you/repo
.diagrams/html/<name>.html— self-contained interactive pages, every asset inlined as a data URI..diagrams/html/index.html— a gallery linking them, grouped by source folder; a card shows the model's name and the diagram's path..diagrams/static/<name>.png— flat images, unless--no-images.
files... here filters by diagram name, not path.
--link <url> puts one link in the index header, shown as the address it goes to — the project's repository, say. It must be an http(s) URL; anything else exits 1 before anything is compiled. Diagram pages never carry it: a published page belongs to its diagram and has no chrome of its publisher's.
Requires the viewer shell. An installed diagc ships it. From a checkout you must build it once (pnpm build:cli); until then publish exits 1 with viewer shell not built.
PNG export needs a browser. If none is found, publish prints No Chrome found — writing HTML only and continues without images. See CHROME_PATH below.
Remote includes resolve locked, same as compile; diagc publish --update-includes refreshes and re-pins them first. Publish always compiles the whole source tree regardless of files... (that only filters which pages are rendered below it), so an --update-includes run here always prunes unreferenced entries — there is no scoped/unpruned form the way a filtered compile run has.
studio
Runs the browser studio against the current directory.
diagc studio
Starts the compile watcher and an editor server (default http://127.0.0.1:5173, stepping to the next free port if that one is taken), pointed at ./.diagrams/src and ./.diagrams/.artifacts, and opens a browser. Ctrl+C stops both.
The server is local and its /api/* routes carry no authentication, so it answers only its own page: a request whose Origin is another site, or whose Host is a name other than localhost (a raw IP address is fine), gets 403, and the JSON routes take application/json only. Scripts that call the API without an Origin header are unaffected. See SECURITY.md.
Which server depends on how diagc was installed, and nothing else does:
- Installed from npm — the prebuilt studio bundle is served straight from the package over plain http. No Vite, no workspace, nothing installed into your project.
- From a checkout (
pnpm link --global, orpnpm dev) — the studio's own Vite dev server runs instead, so edits to the studio hot-reload. Itsnode_modulesmust be installed in the monorepo.
Both serve the same API from the same route table (packages/diagc/src/api), so saving, renaming, assets, and the shape library behave identically.
Remote includes resolve locked here too, same as compile — there is no --update-includes flag on studio itself; run diagc compile --update-includes first if a lock entry is missing.
eject
Promotes a JSON diagram to a generated .diagram.ts — but only after executing the generated
source and proving it rebuilds the identical model.
diagc eject shop
- Input: one diagram name, or a path under
.diagrams/src— either way only the name is kept (the.diagrams/src/prefix and.diagram.{ts,json}suffix are both stripped). - Output:
.diagrams/src/<name>.diagram.ts;<name>.diagram.jsonis deleted. Sidecars (.layout.json,.drawings.json, …) are untouched. - Exit code:
0and✓ <tsPath>on success;1and the reason on stderr otherwise.
Every refusal leaves .diagrams/src exactly as it was — nothing is written, nothing is deleted:
| Case | What happens |
|---|---|
No <name>.diagram.json |
Refuses: no such diagram. |
<name>.diagram.ts already exists |
Refuses: already TypeScript-owned. |
| The JSON does not parse, fails validation, or holds a value the generated code cannot express | Refuses, naming the parse error, listing the validation issues, or naming the diagram and the underlying error. |
| The generated source fails to execute | Refuses, surfacing the underlying execution error. |
| The generated source executes but rebuilds a model that differs from the JSON | Refuses, naming the paths where the two models first differ. |
See Eject a diagram to TypeScript for the studio side and the crash-recovery note (a crash between writing the TS and deleting the JSON leaves both — recovery is deleting one).
Like studio, eject's post-swap recompile resolves remote includes locked, with no
--update-includes flag of its own.
guide
Prints how to write a diagram, as Markdown on stdout — for a person, or for a coding agent that reads it before it writes one.
diagc guide # the DSL, the vocabularies, the rules, and the list of topics
diagc guide c4 # one diagram type: its conventions, its node types, its starter
diagc guide all # everything
- Topics:
activity,c4,causal-loop,deployment,er,fishbone,git-graph,plan,second-order,threat-model. - Matches the install. The text ships in the package, the node types and relation kinds it lists are the ones
lintchecks against, and every listing in it is compiled by the test suite. It needs no network and no diagrams in the current directory. - Also online: https://ferroman.github.io/diagc/llms.txt is
diagc guide allfor the currentmain, for a model with no shell to run the command in. - Exit code:
0;1for a topic that does not exist (the topics are listed on stderr) or for more than one, or when the install's guide or starter files are missing (the line says to reinstall diagc).
Flags
| Flag | Applies to | Default | Meaning |
|---|---|---|---|
--out <dir> |
compile, watch, publish, eject, diff |
.diagrams/.artifacts (diff: .diagrams/diff/<from>..<to>) |
Where artifacts are written — for publish, the artifacts only, its pages and images still go under .diagrams/; for eject, the dir its post-swap recompile writes into; for diff, its pages and images. |
--no-images |
publish, diff |
off | Skip PNG export; write HTML only. |
--link <url> |
publish |
none | Link the index header to this http(s) address. |
--labels <a,b> |
diff |
the refs | Names for the before and after sides. |
--image-url <template> |
diff |
relative | summary.md's image links; {path} stands for each PNG's path. |
--json |
lint, diff |
off | Print the findings (lint) or the changes (diff) as JSON. |
--type <type> |
init |
basic |
Which starter to copy. |
--agents |
init |
off | Write the coding-agent block into AGENTS.md / CLAUDE.md, and the Claude Code skill where there is a .claude/ folder or a CLAUDE.md. |
--update-includes |
compile, publish |
off | Refetch every remote include, vendor it under .diagrams/includes/, and rewrite .diagrams/includes.lock.json. On compile, pruning entries the run didn't touch only happens with no files... given (a full-tree run); publish always compiles the whole tree, so its prune is unconditional. |
Anything not recognised as a flag is collected as files....
Environment variables
| Variable | Read by | Meaning |
|---|---|---|
CHROME_PATH |
publish |
Path to a Chrome/Chromium binary for PNG export. Checked before the default locations. |
DIAGC_AUTH_TOKENS |
--update-includes |
Credentials for private include hosts: ;-separated token@host (bearer) or user:pass@host (basic). Sent over https only, to the named host only. See Compose diagrams. |
DIAGRAMS_DIR |
studio server | Source directory. Set by diagc studio. |
ARTIFACTS_DIR |
studio server | Artifact directory. Set by diagc studio. |
DIAGRAMS_CWD |
studio dev server | Extra path added to Vite's filesystem allow-list (checkout layout only). |
DIAGRAMS_OPEN |
studio server | 1 opens a browser on start. |
Chrome is otherwise looked for at /usr/bin/google-chrome, /usr/bin/google-chrome-stable, /usr/bin/chromium, /usr/bin/chromium-browser, /snap/bin/chromium, and /Applications/Google Chrome.app/Contents/MacOS/Google Chrome.
pnpm script equivalents
Inside this monorepo:
| Script | Runs |
|---|---|
pnpm compile |
diagc compile |
pnpm compile:watch |
diagc watch |
pnpm publish-diagrams |
diagc publish |
pnpm studio |
the studio dev server directly (not via diagc) |
pnpm dev |
compile:watch and the studio together |
pnpm build:cli |
builds the viewer shell publish needs |
pnpm build:studio |
builds the static studio bundle the installed CLI serves |
pnpm build:dist |
everything a release needs: both packages compiled, both bundles built and staged |
pnpm publish-site |
gh-pages -d .diagrams/html |
pnpm test / pnpm typecheck |
vitest / tsc across every package |
Files and directories
| Path | Owner | Commit it? |
|---|---|---|
.diagrams/src/*.diagram.ts |
you | yes |
.diagrams/src/*.diagram.json |
you, via the studio | yes |
.diagrams/src/*.layout.json |
the studio | yes |
.diagrams/src/*.drawings.json |
the studio (freehand drawings) | yes |
.diagrams/src/assets/ |
the studio (imported images) | yes |
.diagrams/src/library.json |
the studio (your library entries) | yes |
.diagrams/.artifacts/ |
compile |
no |
.diagrams/html/ |
publish |
no |
.diagrams/diff/ |
diff |
usually not — copy the PNGs you keep next to the ADR |
.diagrams/static/*.png |
publish |
yes — these are your doc images |
.diagrams/includes/ |
compile --update-includes |
yes — vendored snapshots of remote includes |
.diagrams/includes.lock.json |
compile --update-includes |
yes — the hashes pinning them |
AGENTS.md, CLAUDE.md |
you; init --agents owns the text between the diagc markers |
yes |
.claude/skills/diagc/SKILL.md |
init --agents; the text outside the diagc markers is yours |
yes |
See also
- Add diagc to an existing repository
- Publish and share
- Eject a diagram to TypeScript
- Compose diagrams — includes,
key, and snapshotting remote includes