@modeldesk/cli
v0.6.0
Published
Command-line interface (CLI) for the ModelDesk platform
Readme
ModelDesk CLI
The ModelDesk command-line interface pulls a model's generated documentation content bundle into your repository, so your own documentation pipeline (for example ReSpec with GitHub Actions) can render and publish it. ModelDesk generates the content and hands it off; it does not render, host, or publish documents.
Install
The CLI runs on Node.js (version 22 or newer), on Linux, macOS, and Windows. It is plain JavaScript
with no native dependencies, so the Node version is the only requirement. Run it on demand with
npx, or install it globally.
npx @modeldesk/cli@latest bundle pull --model <modelId> --stack respec --template conceptueel-toc
# or install it once
npm install -g @modeldesk/cliOn Windows, a global install is invoked through the modeldesk.ps1 shim npm generates. If your
PowerShell execution policy blocks scripts ("running scripts is disabled on this system"), call
modeldesk.cmd, use npx @modeldesk/cli@latest instead, or, if your organization permits it, allow
signed scripts for your own account with Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
Commands
modeldesk bundle pull Materialize a model's content bundle into the repository
modeldesk bundle templates List a model's document templates (its stack/template pairs)
modeldesk note add Create an empty note file for an element and add it to the repo's notes.json
modeldesk auth login Log in through the browser (OAuth authorization code + PKCE)
modeldesk auth logout Remove the stored credential for the active host
modeldesk auth status Show the active host, identity, and token validityGlobal options: --help (-h), --version (-v). Commands take named flags; note add also takes
the element's name as a positional argument.
Log in
modeldesk auth login opens your browser to the ModelDesk sign-in page. You authenticate there (the
CLI never sees your password), and after a successful login the CLI stores a refresh token in
~/.modeldesk/credentials.json (on Windows, %USERPROFILE%\.modeldesk\credentials.json). Later
commands run unattended: the CLI refreshes silently and only re-opens the browser when the stored
credential lapses.
The token file is readable only by you: on Linux and macOS the CLI sets file mode 0600, and on
Windows it inherits the default private permissions of your user profile.
modeldesk auth login --host modeldesk.io
modeldesk auth status # shows who you are logged in as, and whether the credential is valid
modeldesk auth logout # removes the stored credential for the hostAuthenticate in CI
In a pipeline the CLI authenticates with a service account (org settings → Service accounts),
in one of two ways. A machine run refuses to take the host from the committed .modeldesk/config.json,
so a PR-editable file can never decide where a credential is sent: against modeldesk.io set nothing
(it is the default), against any other instance pass --host or set MODELDESK_HOST, and leave
host out of the committed config in a repository that pulls from CI. Model, stack, template, and
version still come from the committed config.
GitHub Actions (federated — no stored secrets). Add a trust rule to the service account, grant
the workflow id-token: write, and run the CLI; it exchanges the workflow's own OIDC token itself:
permissions:
contents: read # any permissions block drops the defaults, so restate what the job needs
id-token: write # lets the workflow request an OIDC token; the CLI does the rest
steps:
- uses: actions/checkout@v7
- name: Pull the bundle
run: npx @modeldesk/cli@latest bundle pull --version 1.0.0Any other CI (issued secret). Store an issued secret in the pipeline's secret manager and set
MODELDESK_CLIENT_ID (the account's UUID) and MODELDESK_CLIENT_SECRET (the mdsk_… value). The
CLI mints short-lived tokens itself; nothing is written to disk.
Selection is deterministic (secret vars > GitHub federation > interactive) and a machine run never
falls back to browser login — a CI runner without machine credentials fails immediately with
guidance. modeldesk auth status verifies the credential with a real mint (exit non-zero on
rejection); use it sparingly, since each probe counts as credential use. Never set NODE_DEBUG=http
or DEBUG=* in CI: Node's HTTP diagnostics print the Authorization header.
Configuration
The model, stack, template, host, and version each resolve by precedence: a command-line flag, then
a MODELDESK_* environment variable, then a committed .modeldesk/config.json, then (for the host
only) a built-in default. A configured repository therefore runs modeldesk bundle pull with no
arguments, while CI can override any value per build with an environment variable.
The CLI operates on the current working directory: run it from the directory that holds
.modeldesk/config.json (typically the repository root). Config in parent directories is not
searched, and a pull writes its content and manifest under the directory you run it from.
// .modeldesk/config.json (committed to your repository)
{
"model": "3f2b…-model-uuid",
"stack": "respec",
"template": "conceptueel-toc",
"host": "modeldesk.io",
"version": "1.0.0", // optional: pin a released model version
}| Setting | Flag | Environment variable | Config key | Default |
| -------- | ------------ | -------------------- | ---------- | -------------- |
| Model | --model | MODELDESK_MODEL | model | (required) |
| Stack | --stack | MODELDESK_STACK | stack | (required) |
| Template | --template | MODELDESK_TEMPLATE | template | (required) |
| Host | --host | MODELDESK_HOST | host | modeldesk.io |
| Version | --version | MODELDESK_VERSION | version | live HEAD |
modelis the model's UUID (copy it from the model's "Command-line access" panel in the app).hostis your instance's domain —modeldesk.ioresolves tohttps://api.modeldesk.ioandhttps://auth.modeldesk.io. Uselocalhostagainst a local development stack.stackandtemplatetogether identify the document shape to pull. A template slug is unique only within its stack, so both are required — pass--stack/--template, set the env vars, or commit them. Runmodeldesk bundle templatesto list the validstack/templatepairs for a model.- Machine credentials (
MODELDESK_CLIENT_ID,MODELDESK_CLIENT_SECRET) are read from the environment only and are never placed in the committed config. In a machine run the host must also come from--hostorMODELDESK_HOST(see Authenticate in CI).
Two more keys serve notes. They are read from the config file only, and each folder is a path relative to the repository root:
notesDir(defaultmd-notes/at the repository root): the notes folder. A pull places every Markdown file in it as a note, by its path, andnote addcreates note files in it;--diroverrides it for one note. It cannot be the repository root, lie in the content root or hold it, or be a symlink.publishDirs(no default): the folders your document's publish step copies, such as["data", "media", "js", "css"]for Geonovum's NL-ReSpec-template. A ReSpec snapshot inlines the note Markdown, but an image a note links must sit in a copied folder; withpublishDirsset, a pull warns about every image outside them.
bundle pull
A pull is a full sync of the content root (for ReSpec, data/model/). It writes every
generated fragment, removes files a previous pull left orphaned, and renders and fetches the
per-diagram images, streaming the render progress to your terminal. The whole pull is pinned to a
single commit, so a moving HEAD cannot mix fragments. Text is written first: a failure in the slower
diagram phase leaves valid text on disk and exits non-zero, and the pull stays re-runnable.
modeldesk bundle pull # in a configured repository (model, stack, template from config)
modeldesk bundle pull --model <modelId> --stack respec --template conceptueel-toc --host modeldesk.ioThe content root is CLI-managed. Everything under the content root (
data/model/) is owned by the CLI: hand edits there are overwritten, and stale files are removed. Keep hand-authored prose outside the content root (your template references the generated files withdata-include). Run--dry-runfirst to preview exactly what a pull would write, overwrite, and remove.
Flags
| Flag | Effect |
| ---------------- | --------------------------------------------------------------------------------- |
| --model <id> | The model's UUID (required, unless set via env or config). |
| --stack <id> | The content stack to render (required; see bundle templates). |
| --template <s> | The document shape within the stack (required; see bundle templates). |
| --host <host> | Target a specific instance (default: modeldesk.io). |
| --version <n> | Pin a released model version for a reproducible pull (default: live HEAD). |
| --dry-run | Preview the plan (writes, overwrites, removals, diagram count) and touch nothing. |
| --no-diagrams | Write text fragments only; skip the diagram render and fetch (a fast inner loop). |
| --adopt | Allow the CLI to prune a content root it did not previously manage. |
| --yes | Skip the confirmation for an unusually large removal set. |
| --allow-empty | Permit a pull that returns no fragments (a genuinely emptied model). |
--no-diagrams is convenient while iterating on text, but any diagram data-include will 404 at
bake time until you run a full pull.
Keep the content root LF. The CLI always writes fragments with LF line endings. On Windows, Git's
core.autocrlf=truerewrites them to CRLF on checkout, so every pull would then show a diff on files whose content never changed. Pin the content root in.gitattributes:data/model/** text eol=lf
Ignore the manifest. Each pull records what it wrote in
.modeldesk/<stack>.manifest.json(for ReSpec,.modeldesk/respec.manifest.json). It is regenerated every pull and is used for precise cleanup and rename hints, so add.modeldesk/*.manifest.jsonto your.gitignore— committing it is unnecessary and a stale committed copy can only mislead a later pull.
Portable file names
Generated paths are safe to check out on any operating system. Element and diagram names that would
otherwise produce a name Windows reserves for a device (con, prn, aux, nul, com0-com9,
lpt0-lpt9) get a short id suffix automatically, exactly as any other name clash does, so no
action is needed. The one case that cannot be resolved automatically is a custom metamodel type
whose name slugs to a reserved word: a type name becomes a folder shared by every element of that
type, so there is nothing unique to suffix it with. The bundle then fails to build with an error
naming the type to rename.
Safe removal
A pull's one destructive step — removing orphaned files — is guarded so a misconfiguration cannot delete files the CLI did not create:
- A pull that returns no fragments (usually a wrong model or template) writes and deletes nothing
and exits with an error; use
--allow-emptyonly for a genuinely emptied model. - In a repository the CLI already manages, it removes only files it recorded writing.
- A non-empty content root that the CLI has not managed before is not pruned until you confirm it
(interactively, or with
--adoptin CI). - An unusually large removal set asks for confirmation (or
--yes). - All writes and removals stay within the content root; a manifest path that tries to escape it is refused.
The removal list is always printed. In CI, run a --dry-run first as a safety gate.
bundle templates
Lists the document templates available for a model — the shapes you can pass to --template. The
output is a table: the content STACK, the TEMPLATE slug (the --template value), and its display
NAME. A stack (for example respec) is the render engine and content root a template produces
for; a template slug is unique only within a stack, so the stack namespaces it — read a row as
stack / template.
modeldesk bundle templates --model <modelId> --host modeldesk.ioSTACK TEMPLATE NAME
respec conceptueel MIM conceptueel
respec conceptueel-toc MIM conceptueel (TOC hiërarchisch)
respec logisch MIM logisch
respec logisch-toc MIM logisch (TOC hiërarchisch)Take a STACK / TEMPLATE pair from the table straight into a pull. Pass --stack <id> to list
only the templates of one stack.
note add
A note is your own Markdown under a generated element, a package, or the model root, kept outside the
content root so a pull never removes it. intro sits after the element's heading and diagrams, after at
the end of the element's own content (before nested subtypes).
A note file names its element by its path: the element's generated file under data/model/, with the notes
folder (notesDir, default md-notes/) in its place, ending in .after.md for the after slot. So
md-notes/bestuurlijk-gebied/objecttype/gemeentegebied.md is the intro note of the object type whose file is
data/model/bestuurlijk-gebied/objecttype/gemeentegebied.md, and md-notes/bestuurlijk-gebied/index.after.md
the after note of that domain. Creating the file is all an editor does, in the GitHub web editor if need be.
Every pull, CI's included, matches new note files against the manifest the server sends and records them in
.modeldesk/notes.json (committed: add .modeldesk to the workflow's commit beside data/model), keyed by
element id so a rename keeps the note in place. An entry can also name a file elsewhere, written by hand:
{
"notes": {
"dafddbaf-9e2a-4eda-88f7-9fae3a47362f": {
"name": "Gemeentegebied",
"intro": "md-notes/bestuurlijk-gebied/objecttype/gemeentegebied.md"
},
"125cd721-c388-49ef-bc26-d9ee56964357": {
"name": "Domein Bestuurlijk gebied",
"intro": "data/model-overzicht.md",
"after": "md-notes/bestuurlijk-gebied/index.after.md"
}
}
}name is refreshed by every pull and read by nothing. A package's or the model's after note comes under a
heading of its own, which becomes a numbered section in the table of contents; the note names it on its
first line, <!-- heading: Afsluiting -->, an HTML comment the document does not show.
Note paths are relative to the repository root, outside data/model/, and free of .., %, #, ?,
backslashes and leading or trailing spaces. Editing a note's text needs no pull; adding or removing one, or
changing a heading line, does. Notes need a server that renders the respec stack at 1.4 or later with a
MIM template; a bundle without any note slot fails the pull once, naming its stack version.
modeldesk note add <name> [--slot intro|after] [--heading <text>] [--dir <dir>] [--element <id>] [--stack <id>]note add creates the note at its path under --dir or notesDir (holding only the heading line for a
container's after, empty otherwise) and adds its entry to notes.json. It finds the element in the last
pull's manifest, so run bundle pull first; it contacts no server. A name several elements share is asked
about on a terminal and refused elsewhere, listing the ids for --element. --heading is required for a
package's or the model's after slot and refused elsewhere. It never overwrites a file, replaces a filled
slot, or names a file another entry names.
On pull, the CLI checks notes.json and the files it names before contacting the server. Once the bundle
has arrived, it stops before writing on a note file that matches no element (naming close paths), a note
file for a slot another file already fills, a missing note file, a path inside the content root, a slot the
element does not carry, or a container's after note without a heading line. It warns on an element not in
the bundle (the entry is kept), a heading line in a note that takes none, a missing image, an image outside
publishDirs or without alt text, a heading text two notes share without an explicit {#id}, and one file
filling several slots. --dry-run prints an adopt <file>: <title> <slot> line per new note file and a
fill <title> <slot>: <path> line per placed note, and writes nothing.
Learn more at modeldesk.io.
