CLI Commands
Global Flags
These flags apply to all commands:
| Flag | Description |
|---|---|
--config <file> | Additional configuration file(s) to load and merge (repeatable) |
--before-config <file> | Configuration file(s) to load before auto-discovery (repeatable; for wrappers/libraries) |
--no-auto-config | Disable auto-discovery of datamitsu.config.{js,mjs,ts} at git root |
--binary-command <name> | Override the binary command name. Also settable via DATAMITSU_BINARY_COMMAND |
--log-format <format> | Status output format: console or newline-delimited jsonl. Also settable via DATAMITSU_LOG_FORMAT |
--no-oci | Disable OCI bundle store seeding. Also settable via DATAMITSU_NO_OCI |
--no-parse | Show raw tool output instead of parsed findings; parsing still runs. Also via DATAMITSU_NO_PARSE |
-v, --verbose | Enable debug-level logging for this invocation |
With --log-format=jsonl, status and progress go to stderr as typed JSON events,
one per line, and log lines arrive the same way: each is a
log event whose level is debug, info, warn or error,
with the message and its fields in msg. Which levels appear follows
DATAMITSU_LOG_LEVEL (warn by default) and --verbose (debug), exactly as
in console mode.
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | A tool failed — it exited non-zero, or its parsed output held a finding at or above its operation's failOn (see Failing on findings) — or any error without a code of its own |
2 | Usage: an unknown flag, a flag or DATAMITSU_* value the command does not accept (--widen-to=Repo, DATAMITSU_FAIL_FAST=yes), a missing required flag, flags that cannot be combined, the wrong number of arguments, or a combination refused before anything runs (--require-coverage with --tools, a report of a narrowed run without --allow-partial) |
3 | llms: an unknown or ambiguous page |
4 | The run did not cover what it was asked to: --require-coverage (see Narrowed runs) or --fail-on-skip (see Skipped tools); when both fail, both messages are printed |
5 | A report asked for with --report or DATAMITSU_REPORT was not written (see Reports) |
130, 143 | fix, lint or check interrupted by SIGINT or SIGTERM (see Keep-going runs) |
When several apply, a tool failure (1) wins over an incomplete run (4),
which wins over a report that was not written (5); the messages of the ones
that lost are still printed. A usage error is found
before anything runs and combines with nothing. A script that tells "you called
it wrong" from "the code is bad" checks for 2 and 1 apart. An unknown command
(datamitsu bogus) and a few checks that other commands make on their own
arguments still exit 1.
exec
Execute a managed binary with all environment variables passed through.
datamitsu exec <appName> [args...]
When called without arguments, lists all available tools grouped by type (binary, uv, node, jvm, go, shell).
exec hands the app the environment it was started with, unchanged: in a GitHub Actions job a tool run through exec sees GITHUB_ACTIONS and prints its own annotations. fix, lint and check do not — they strip the variables that switch a tool's output format and set NO_COLOR=1 (see Tool Environment).
An app is installed on demand the first time you exec it. If that app declares links, its .datamitsu/ symlinks are created at the same time — this is how a lazy: true link-app that init deferred gets its managed-config links on first use.
Examples:
# List all available tools
datamitsu exec
# Run golangci-lint
datamitsu exec golangci-lint run ./...
# Run eslint via the managed Node.js runtime
datamitsu exec eslint --fix src/
install
Install one or more managed apps (and optionally runtimes) into the store and leave them ready. Where exec installs, prepares and then runs an app, install materializes its files, runs its declared prepare step (see prepare) and exits — the building block for the per-app stages of a generated Dockerfile (see devtools dockerfile).
datamitsu install [app...]
| Flag | Description |
|---|---|
--runtime <name> | Install the named runtime(s) only, without any app (repeatable) |
--no-verify | Skip the post-install version check (verification runs by default) |
The prepare step always runs before the version check; --no-verify skips only the check. By default, each installed app's version command (--version, or its configured versionCheck.args) is run after installation, so a broken install fails the command instead of producing a broken artifact. Apps whose version check is disabled, and shell apps, are skipped.
Examples:
# Install a single tool
datamitsu install shellcheck
# Install only the node runtime (no app)
datamitsu install --runtime node
# Install and verify each tool actually runs (the default)
datamitsu install eslint prettier
# Install without running the post-install version checks
datamitsu install eslint prettier --no-verify
init
Download binaries, install runtime-managed apps, and run initialization commands.
datamitsu init
Must be run from the git repository root.
| Flag | Description |
|---|---|
--all | Download all binaries (both required and optional) |
--skip-download | Skip binary downloads |
--fail-on-download-error | Stop if any binary download fails |
--dry-run | Show what would be done without making changes |
Download concurrency is controlled via the DATAMITSU_CONCURRENCY env var (default: 3).
What init does:
- Detects project types in the repository
- Downloads required binaries and runtimes
- Installs runtime-managed apps (Bun/Node/UV/JVM/Go) that are referenced by tools
- Creates
.datamitsu/symlinks for the config files of installed link-apps — an app markedlazy: trueis deferred and gets its links on firstdatamitsu execinstead - Writes
.datamitsu/configs/: every ejectable managed config the project has not ejected, removing any that no longer belong there - Runs configured init commands (e.g.,
lefthook install)
Examples:
# Standard initialization
datamitsu init
# Download all tools including optional ones
datamitsu init --all
# Preview what would happen
datamitsu init --dry-run
check
Run fix followed by lint in a single process with shared context. If fix fails,
lint is skipped — unless --fail-fast=false runs it anyway (see
Keep-going runs).
datamitsu check [files...]
| Flag | Description |
|---|---|
--explain [mode] | Show execution plan without running. Modes: summary (default), detailed, json |
--file-scoped | Only process git staged files |
--tools <list> | Comma-separated list of tools to run |
--fail-on-skip | Exit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools) |
--widen-to <level> | Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs) |
--require-coverage <level> | Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs) |
--fail-fast[=false] | Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs) |
--fail-on <level> | Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings) |
--report <format>=<path> | Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports) |
--events <what> | Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events) |
--allow-partial | Write a report that lists findings for a narrowed run instead of refusing the run (see Reports) |
--annotations <mode> | Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity) |
--output <mode> | How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output) |
--baseline <file> | Gate only on findings the baseline does not hold: a report baseline document or a run's own JSON; a tool that exits non-zero still fails (see Baselines) |
Examples:
# Check entire project
datamitsu check
# Check specific files
datamitsu check src/main.go src/handler.go
# Check only staged files (useful in git hooks)
datamitsu check --file-scoped
# Preview what would run
datamitsu check --explain
# In CI: fail if a tool you rely on has no binary for the runner's platform
datamitsu lint --fail-on-skip
# See every failure at once instead of stopping at the first
datamitsu check --fail-fast=false
check, fix and lint start every tool without GITHUB_ACTIONS, the AI agent
markers and FORCE_COLOR, and with NO_COLOR=1, so a tool prints the format its
parser reads whether the run is in CI, under an agent or in a terminal. CI
passes through. An operation gets a stripped variable back with
inheritEnv; the
Tool Environment page lists them all.
check closes with the wall clock of the whole command:
$ datamitsu check
┏━ fix ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ golang · typescript
┃
┃ ✓ gofmt 120ms
┃ ✓ prettier 1.10s
┗━ 2 tools · 2 runs · done in 1.10s · cache 80% ━━━━━━━━━━━
┏━ lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ golang · typescript
┃
┃ ✓ golangci-lint 7.90s
┃ ✓ eslint 3.20s
┗━ 2 tools · 2 runs · done in 7.90s ━━━━━━━━━━━━━━━━━━━━━━━
┗━ check · done in 9.40s · fix 1.10s · lint 7.90s · setup 400ms ━━━
done in runs from the start of the command to the end of the last
operation. fix and lint repeat their own footers' times, and setup is the
rest: loading the configuration, walking the repository, planning, installing
tools. An operation that did not run is named — lint not run after a failed
fix under fail-fast, both when a setup step such as the .datamitsuignore
check failed. The line is printed for check only, never under --explain,
and on failure too; fix and lint end with their footer.
Keep-going runs
By default a run stops at the first failing tool: nothing at a later priority
starts, a parallel sibling that is still running is stopped, a tool that runs
once per file stops at the first failing file, and check does not run lint
after a failed fix. That is the right answer for a person at a terminal, who
fixes one thing and runs again.
--fail-fast=false (or DATAMITSU_FAIL_FAST=false) runs everything to the end:
| Level | Default (fail-fast) | --fail-fast=false |
|---|---|---|
| Priority groups | Stop after the first failing group | Every group runs, in order |
| Overlapping tools | The ones after the failure do not start | All run, one after another |
| Parallel tools | Waiting ones are cancelled, running ones are stopped | All finish |
| Files of a per-file tool | Stop at the first failing file | Every file runs; when more than one fails, the frame names each with its exit code |
check's fix, then lint | Lint is not run after a failed fix | Lint runs, and its footer says lint ran after a failed fix |
| Exit code | 1 when anything failed | 1 when anything failed |
The flag wins over the variable; --fail-fast restores the default when
DATAMITSU_FAIL_FAST=false is set. datamitsu config runtime reports the
effective value as failFast, and where it came from as failFastSource
(default or env). A value of DATAMITSU_FAIL_FAST other than true,
false, 1 or 0 is refused.
In CI, run datamitsu lint --fail-fast=false: one run then reports every
failing tool. A report that lists findings turns fail-fast off by itself. When a later operation fails for another reason than an earlier
one — lint cannot install its tools after fix failed — both reasons are
reported. A tool that runs once per file and is interrupted after one of its
files failed stays a failure; the files it did not reach make the run
incomplete. CI runs lint, not fix or check, which change the working
tree.
A formatter that fails at a low priority leaves its files unformatted for the linters that run after it, so a keep-going run can report findings that disappear once the formatter passes.
Failing on findings (--fail-on)
A tool fails a run when it exits non-zero, or when its parsed output holds a
finding at or above its operation's
failOn — error unless the
configuration sets another. --fail-on <level> (or DATAMITSU_FAIL_ON) raises
that threshold for every operation of the run, to warning, info or hint; an
operation whose own failOn is already stricter keeps it, and nothing lowers a
threshold or turns a non-zero exit into a pass:
# CI: a warning fails the run too, whatever the tool's exit code
datamitsu lint --fail-on warning
The flag wins over the variable. datamitsu config runtime reports the
variable as failOn (empty when it is not set), and --explain shows each
task's threshold: failOn in the JSON, a Fail on: line when it is not
error. A value of either that is not error, warning, info or hint
exits 2 before anything runs — a mistyped gate is refused, not ignored.
The threshold gates only output a parser module with the severity contract
(descriptor schema 2) read: its levels are what the tool printed. Under an
older module the exit code alone decides, and a run that asked for a threshold
other than error says so once, after its last operation:
WARN failOn ignored for hadolint: parser module predates the severity contract
With the default threshold, a tool that exits 0 on a finding its parser reads
as an error fails the run — semgrep without --error, trivy without
--exit-code. A tool that failed because of the threshold shows exit code 0 in
its frame, with a Failed on: line naming the threshold.
Findings in the terminal
What the terminal shows is what gates: for every tool it prints the findings at or above the operation's threshold, sorted by level, file, line, column and rule, and counts the rest.
- A failed tool prints them in its red frame. A tool that failed although none
of its findings reaches the threshold — one that fails on warnings, such as
yamllint --strictoreslint --max-warnings 0— prints every finding, so a failure always explains itself; one that failed without findings prints its output. - The findings below the threshold are never dropped: the tool line counts them per level, the frame ends with one line saying what it left out, and the footer sums them.
- A passed tool prints no frame, except when a threshold other than
errorwas asked for and its parser module predates the severity contract: its findings at or above that threshold then print in a yellow frame, since nothing enforced it.
An abridged run, the frame without its directory, command and duration lines:
┃ ✗ eslint 1.20s (1 failed) · 5 warnings
┌─ eslint [per-project] (run #1) ────────────────────
│ Exit code: 1
│
│ src/a.ts:3:7 error 'x' is assigned a value but never used. [no-unused-vars]
│ src/b.ts:9:1 error Unexpected var, use let or const instead. [no-var]
│ + 5 warnings hidden (failOn=error)
└─────────────────────────────────────────────────────────
┃ ✓ yamllint 310ms · 2 warnings
┗━ 2 tools · 2 runs · done in 1.20s · 1 failed · 7 warnings hidden
--fail-on hint shows every finding and fails on any of them. --no-parse
prints each framed tool's output instead of findings and counts nothing for it,
and so does a tool run once over many files whose parser names no file for its
findings: a finding that cannot say which file it is about reads worse than the
output it came from. A tool that prints no frame keeps its counters either way.
A tool the run stopped is neither a pass nor a failure, and it is listed rather than left out:
┃ ✗ eslint 1.2s (1 failed)
…
┃ ⊘ tsc [packages/api] cancelled (fail-fast)
┃ ⊘ custom-check not started (fail-fast)
┗━ 1 tools · 1 runs · done in 1.2s · 1 failed · 2 cancelled
cancelled means the tool had started and was stopped; not started means it
never ran. (interrupted) replaces (fail-fast) when the run was stopped by
Ctrl-C or SIGTERM: datamitsu then stops every running tool, prints what it
did not finish and exits 130 (SIGINT) or 143 (SIGTERM). A second Ctrl-C ends the
process at once. A signal that arrives while the configuration is still loading,
before the run has started, ends the process at once too, with no report. The (N failed) counts never include stopped tools, and a
tool stopped in several places of one kind shares one line with a ×N count.
A tool that runs once per file stops at its first failing file under fail-fast, and the files it never reached are named under its failure:
┃ ✗ alpha 12ms (1 failed)
…
┃ ⊘ alpha 2 files not run (fail-fast): src/b.txt, src/c.txt
The line names up to three files, relative to the repository root, and counts the rest.
Agent output
--output agent (or DATAMITSU_OUTPUT=agent) prints a run for a program that
reads it, such as a coding agent: no banner, frame, colour or progress, and one
line per record on stdout. What the configuration prints while it loads
(console.log) goes to stderr, with the warnings.
$ datamitsu check --fail-fast=false --output agent
fix changed 2 files: src/a.ts, src/b.ts
fix: 3 tools · 4 runs · 0 failed · 0 errors 0 warnings · 0 hidden
src/a.ts:3:7: error eslint(no-unused-vars): 'x' is assigned a value but never used.
src/b.ts:9:1: error eslint(no-var): Unexpected var, use let or const instead.
tsc [packages/api] exited 2 without parsable findings
│ error TS5083: Cannot read file 'tsconfig.base.json'.
knip: skipped (opt-in)
lint: 4 tools · 5 runs · 2 failed · 2 errors 0 warnings · 5 hidden
check · done in 9.40s · fix 1.10s · lint 7.90s · setup 400ms
- A finding is
path:row:col: <level> <source>(<code>): <message>, the path relative to the repository root, the column as the tool counted it and left out when there is none. A line break in anything a record names — a message, a path, a skip reason — is written as the two characters\n, so a record is always one line. The findings are the ones the terminal would show, chosen by the same rule (see Findings in the terminal), and--no-parsechanges only the human frames. - A tool that failed without findings is one line —
<tool> [<dir>] exited <code> without parsable findings, after the file for a tool that runs once per file — followed by the last 20 lines of what it printed, each behind│, masked as a report is. A tool whose parser module puts it in thesecuritycategory getsfailed (exit <code>); output withheld for a security tooland no output. - What did not run — a task stopped or never started, the files a tool that runs once per file left unchecked, a tool the planner skipped — is one line each, as the human block words it.
- The files a fix changed are one line before its summary,
fix changed <N> files: <path>, <path>— an agent that holds any of them in memory reads them again. When they could not be observed (see Reports) the line isfix changes not observed: <reason>, followed by the files seen before the observation stopped. - Each operation ends with
<op>: <tools> tools · <runs> runs · <failed> failed · <E> errors <W> warnings · <H> hidden,<H>counting the findings below the threshold, and· <N> baselinedwhen a baseline held some, andcheckwith its closing wall-clock line. The exit code is the run's.
Fail-fast stays the default: add --fail-fast=false to see every failure at
once. --output agent prints on stdout, so it cannot be combined with
--log-format jsonl, which keeps stdout clean, or with a report written to
stdout (-); either exits 2, as does any other value. In a GitHub Actions job
the records print inside the
stop-commands region like any other tool text; a record
of a finding in a .go file is in the form actions/setup-go's problem matcher
reads, so a job that registers it annotates such a finding twice.
Run events
With --log-format=jsonl, fix, lint and check write their progress to
stderr as typed events, one JSON object per line:
type | When |
|---|---|
hello | Opens the stream: op_id stream, schema (datamitsu.report/1), events, the comma-separated types the stream may carry, and for fix, lint and check annotations |
phase | An operation (op: fix or lint) starts: status: "start" |
tool_run | A tool starts in a directory (status: "start") and ends: done or fail with its findings per level, or skip for a stopped tool |
chunk | A tool finished a unit of its work: index of total |
error | A tool failed: tool, dir, msg |
done | The operation ended, with its summary |
report | A report was written or not: format, path, status and, when it was not, msg (see Reports); github-annotations for the annotations |
diagnostic | One finding of a tool, once the tool finished: by default those at or above the operation's failOn |
Every task — one tool in one directory, or one file of a tool that runs once
per file — has an op_id of its own: the operation's op_id followed by
:<tool>:<dir>:<seq>, where dir is relative to the repository root (empty for
the root) and seq numbers the operation's planned tasks from 1 in plan order,
for example run-1:eslint:packages/web:3. A task's tool_run start, its chunk
events, its error and its closing tool_run share it, and no other task's
events do.
A --log-format jsonl stream opens with a hello event, whatever the
command, so a reader can tell a stream without diagnostic events from a run
that found nothing. For fix, lint and check it also says whether stdout
carries GitHub annotations beside the stream:
annotations is github or off.
{
"type": "hello",
"op_id": "stream",
"schema": "datamitsu.report/1",
"events": "hello,phase,download,install,chunk,tool_run,error,done,log,report,diagnostic",
"annotations": "off"
}
A tool_run that ends with done or fail carries the task's findings per
level — findings_error, findings_warning, findings_info, findings_hint,
zero included — and cached, true when a cache answered for the task. No other
event carries them.
A diagnostic event is one finding, emitted once its tool has finished in the
operation — every task of the tool, since a finding's fingerprint depends on
all of the tool's findings — and before the operation's done. Its op_id is
the task that reported it:
{
"type": "diagnostic",
"op_id": "run-1:eslint:packages/web:3",
"tool": "eslint",
"dir": "packages/web",
"file": "packages/web/src/a.ts",
"row": 3,
"col": 7,
"end_row": 3,
"end_col": 8,
"severity": "error",
"code": "no-unused-vars",
"source": "eslint",
"msg": "'x' is assigned a value but never used.",
"fingerprint": "8d08ad593059cec87a6c97ce43655d04b1bee8102810cd99952dd606b26bb618",
"provenance": "parser",
"reported": true,
"gates": true
}
file is relative to the repository root, rows and columns are 1-based and
end_col is exclusive; fingerprint is the finding's
report fingerprint. Every event of a JSON-L stream is masked as a
report is, op_id included, from its first line with the values the
environment names, and once a run has loaded its configuration with its apps'
and operations' values and those its tools were started with; a task's events
still share one op_id. By
default only the findings at or above the operation's failOn are emitted —
what the terminal shows. --events diagnostics=all (or
DATAMITSU_EVENTS=diagnostics=all) emits every finding, with reported and
gates saying which are at the threshold and which failed their tool, and
baselined: true on one a baseline held;
diagnostics=reported is the default. Any other value exits 2. The synthetic
finding a report lists for a tool that failed without findings is never an
event.
A stream that could not be written — a closed or full stderr — fails the
command, whichever it is and however it ended otherwise: it exits 1 and says so
on stdout, the one stream left, unless stdout carries the command's own data (a
report written to -, the language server's JSON-RPC, source's shell code).
--verbose adds datamitsu's debug log lines to the stream as log events,
with what a tool printed and the arguments it ran with withheld
("<withheld>"): a stream is what a pipeline publishes. The console keeps
them.
A tool_run with status: "skip" ends the chain of a tool the run stopped,
and is never a failure. Its msg says what happened and why:
cancelled: fail-fast or cancelled: interrupted closes the start of a tool
that was stopped while it ran; not started: fail-fast or
not started: interrupted is the only event of a tool that never started.
An operation's done carries op, status (done or fail), duration_ms,
tools (tools that ran), runs (tasks that ran), failed (tools with a failed
task), skipped (tools the planner left out, see
Skipped tools) and cancelled (tasks the run stopped, present
when there were any). A counter or duration_ms that is zero is left out. An
operation whose tools could not be installed ends with a done of status
fail and no counters.
The stream ends with one more done, for the whole command: its op_id starts
with cmd- where an operation's starts with run-, and its op is fix,
lint or check. It carries status, success (whether the command
succeeded), duration_ms (the wall clock of the whole command), the sums of the
operations' tools, runs, failed and skipped, cancelled (always
present, zero included) and complete: true when every planned operation ran,
no task was cancelled or left unstarted, and no tool that runs once per file
stopped at a failing file with files left to check. check whose fix failed under
fail-fast reports complete: false. It is emitted for every execution of fix,
lint and check — also when the run could not start, for example because the
configuration does not load — never under --explain, for a usage error, or for
a run a signal ended while its configuration was loading, and not for the fix
that config reconcile runs after writing its files.
{
"type": "done",
"op_id": "cmd-7",
"op": "check",
"status": "fail",
"success": false,
"duration_ms": 9400,
"tools": 3,
"runs": 4,
"failed": 1,
"cancelled": 1,
"complete": false
}
Reports
--report <format>=<path> writes a report of the run once its last operation
has ended, whether or not its tools failed — the run that fails is the one a
pipeline needs to read. The flag is repeatable, one per format: json, the
run's own document; markdown, the same run for a person; sarif, the run for
code scanning; junit, the run as test results; codequality, GitLab's Code
Quality report; checkstyle, Checkstyle XML; rdjsonl, reviewdog's
diagnostics; history, one line of counts appended to a trend file; and
patch, the diffs a fix's formatters applied.
The Reports guide explains what a
report holds and how far to trust it; report render writes
one again, offline, from a run's own JSON.
# CI: the whole run as one document, uploaded whatever the outcome
datamitsu lint --report json=out/run.json
- The path is required, and relative to the working directory. Missing directories are created, and the file is written to a temporary name beside it and renamed, so a reader never sees half a report. It gets the permissions a file created in its place would — the umask decides — and a report it replaces keeps its own.
-writes the report to stdout, for one format per run. stdout then carries the report alone: the human output is left out, and stderr carries the run events as it does under--log-format jsonl.- A path that ends in
/names a directory, for a format that splits a run over several files (sarif); any other format given one exits 2. Options follow the path after?:sarif=out/results.sarif?category=linux. DATAMITSU_REPORTis the variable twin, comma-separated:DATAMITSU_REPORT=json=out/run.json. A--reportnaming the same format wins over its entry.datamitsu config runtimereports the variable asreport.- A report that lists findings — every format but
historyandpatch— turns fail-fast off: a run that stopped at the first failing tool could not list every finding. Such a report together with--fail-fast=true, or withDATAMITSU_FAIL_FAST=trueand no--fail-fastflag, exits 2.historyandpatchleave fail-fast as it is. - A format named twice, an unknown format or option, a missing path, two
reports that would write one file — one path twice, a report where another's
completeness companion goes, or a file inside a split format's directory —
and a report combined with
--explain(which runs nothing) exit 2 before anything runs. - A report that cannot be written prints
error: report <format>: <path>: <cause>and makes the run exit 5 — unless a tool failed (exit 1) or the run was incomplete (exit 4), which keep their code; the line is printed either way. Under--log-format jsonlthe same fact is areportevent. - The time a report is stamped with comes from
SOURCE_DATE_EPOCHwhen it is set, the reproducible-builds convention; durations are always measured.
A report says how much of the repository its findings stand for. Every tool is
judged on three facts, and each one that fails adds a reason to the tool's
incomplete list:
| Fact | Complete when | Reasons |
|---|---|---|
| scope | the run covered the whole repository — no files named, no subdirectory, no --file-scoped — and every task its unit | narrowed-selection, partial-unit |
| execution | every planned task ran to the end | cancelled, not-started, setup-failed, platform-skip |
| extraction | every output was read into findings by a parser, or a cache replayed a pass its parser read as clean | no-extraction, parser-unavailable, parse-failed, truncated, unparsed-cache-hit, failed-without-findings |
A tool whose output no parser read — its own, a format parser, or the fallback
built into datamitsu — is never complete (no-extraction, or parse-failed when
a parser was declared): its exit code says whether it passed, not what it found.
Nor is a tool whose process exited non-zero while the parser that recognized its
output found nothing in it (failed-without-findings). The run is complete when
every tool is, every operation ran, and nothing was left out at run level:
narrowed-selection, tools-filter (with selection.excludedTools naming the
tools --tools left out — the selected tools' own runs can still be complete),
not-narrowable (a tool skipped because it could not be narrowed) and
operation-skipped. A tool disabled with skip: true is neither.
A report that lists findings is refused for a run narrowed before it starts —
files named, a subdirectory, --file-scoped or --tools — because it would
read as the findings of the repository: the run exits 2 before anything runs,
and the report is recorded as refused. --allow-partial (or
DATAMITSU_ALLOW_PARTIAL=true) writes it anyway, with every reason in the
document; it never makes a run complete. A report left on its path by an
earlier run is not deleted, so a pipeline that uploads it with if: always()
checks the exit code as well.
json writes datamitsu's own document, datamitsu.report/1: what the run was
asked to cover (selection), whether fail-fast was on (failFast), the CI job
it ran in (ci: vendor, empty outside CI, and the sha, ref, baseRef and
prNumber the vendor names — see
facts().ci), and for each
operation (fix, lint; check writes one document holding both) whether it
ran and succeeded, the tools the planner skipped and the tasks the run stopped.
Each tool lists its app as configured, its parser module and its threshold, and
one invocation per process: its task, state, exit code, extraction outcome, the
files it answered for and the findings it reported, with paths relative to the
repository root. The files a cache answered appear as one cached or
verdict-hit invocation of their task. A finding says whether it is at or above
the threshold (reported), whether it failed its tool (gates) and whether the
terminal shows it (shown, see
Findings in the terminal), which is what the
annotations, markdown and --output agent list.
Every finding carries a fingerprint that identifies it across runs: SHA-256
over the tool, the rule, the path and the text of the finding's first line, and
its position among the findings the tool reported with the same rule on the
same line. A line inserted above a finding moves its row, not its fingerprint,
and rewording a message changes neither. fingerprintBasis says what the
fingerprint rests on: line, row when the file could not be read, none for
a finding without a file. A finding's location gives its columns as the tool
counted them, in the unit its parser declares, and converted while the file
was on disk into code points (chars), UTF-8 bytes (bytes) and UTF-16 units
(utf16); precision is exact, ascii (no unit declared, but an ASCII
line), or unknown, when there was nothing to convert from. A report never holds a command line or
an environment variable, and nothing caches it: every report is written from
the run that produced it.
sarif writes SARIF 2.1.0 for GitHub code scanning and the other services
that keep alerts across uploads. It holds one run per tool of the lint
operation (check writes its lint, a fix run its fix), in the category
datamitsu — automationDetails.id datamitsu/ — or the one
?category=<name> names, and each result carries the finding's fingerprint as
partialFingerprints.primaryLocationLineHash. A tool whose completeness is not
established is left out rather than listed, and so is a tool with more than
25 000 results, which GitHub would cut: an upload without a tool leaves that
tool's alerts as they are. Each tool left out gets one WARN line on stderr
naming why and is recorded in the omitted list of the report's entry in
exports. Because it leaves out what is incomplete, sarif is written for a
narrowed run instead of being refused — named files, a subdirectory and
--file-scoped leave every tool incomplete, --tools writes the selected tools
that are complete. Code scanning refuses a SARIF file without a run, so a report
that would hold none is not written: a file an earlier run left at its path, or
the datamitsu-<n>.sarif files of its directory, are removed, the export is
omitted (a report event with that status under --log-format jsonl), and a
WARN line says so; nothing is written to stdout either. GitHub reads at most twenty runs from one
file: sarif=<dir>/ writes datamitsu-1.sarif, datamitsu-2.sarif and so on,
twenty tools each, sorted by name, and removes the datamitsu-<n>.sarif files
an earlier run left there that this one did not write; a file or - for a run
that plans more than twenty tools exits 2 before anything runs. See
Code scanning for the workflow.
junit writes JUnit XML: one suite per operation and tool (<operation>/<tool>,
its completeness in properties), one case per file the tool answered for, and
a <failure> only for a file with a finding at or above the operation's
failOn that failed its tool — the failure count follows the gate, not the
findings. Findings below the threshold are a case's <system-out>. An
invocation that failed without such a finding is one extra case named after its
directory (with the invocation's ID when several share one): a
<failure type="exit"> listing its findings, or an
<error type="exit"> with the masked tail of its output when it has none
(never for a security tool). Stopped tasks, skipped tools and an operation
that did not run are <skipped> cases. See
Test results.
codequality writes GitLab's Code Quality array: one issue per finding of the
lint operation (fix for a fix run), with check_name <source>/<rule>,
the finding's fingerprint, severity major for an error, minor for a
warning and info below, categories Security for a security tool and
Style otherwise, and a repository-relative location.path with positions
in characters, or lines where the columns could not be converted. A finding
without a file or outside the repository cannot be placed and is left out and
counted. A tool that is not complete is kept. See
GitLab Code Quality.
checkstyle writes one <file> per file and one <error> per finding of the
lint operation (fix for a fix run), with line, column in characters
where the report could convert it, severity error, warning or info,
message and source <source>/<rule>; a finding without a file is under
<file name="">. rdjsonl writes one reviewdog diagnostic per line: the
message, the path with a range in UTF-8 byte columns (exclusive end, left out
where they could not be converted), severity ERROR, WARNING or INFO, the
source with its app's official URL and the rule with its documentation; a
finding without a file has no location. See
Checkstyle and
reviewdog.
A format that lists findings but has no place to say how complete it is —
junit, codequality, checkstyle, rdjsonl — gets a completeness companion
beside its file,
<path>.completeness.json (datamitsu.completeness/1): whether the report is
complete, the run-level reasons, each tool with its own, the findings the
format could not carry (omitted), and the run's exports.
A job checks it before it publishes the report. Each incomplete tool also gets
one WARN line on stderr, and the report's entry in exports names the
companion (companion). A report on stdout has none.
markdown writes the same run for a person, as GitHub renders Markdown: what
the run covered and whether it is complete, a table of each operation's tools
(status, runs, files a cache answered, findings per level), the findings the
terminal would show — grouped by level, then by file, as
path:row:col — source(code): message, columns in characters and only where
the report could convert them — with a count of those below the threshold, the
tools skipped and the tasks stopped, and each incomplete tool with its reasons.
It carries no tool output: a tool that failed without findings is its
structured message. It lists findings, so it is refused for a narrowed run like
json.
history appends one line, datamitsu.history/1, to the file it names —
creating it and its directories the first time, never replacing it: when the
run started, the build and configuration, the CI job's vendor, commit and ref,
what the run covered (selection: mode, fileScoped, toolsFiltered),
whether it was complete and ran fail-fast, and per operation and tool the
processes that ran, the files a cache answered, the invocations that failed,
completeness with its reasons and the findings per level. It holds no finding,
no path and none of the paths the run was given, so a narrowed run writes it
without --allow-partial, and it leaves fail-fast alone. The line is appended
in one write. See History.
A fix operation records the files its tools changed. A snapshot of the working
tree — git status --porcelain=v2 over tracked and untracked files, without
optional locks, plus a hash of each file it lists — is taken before the first
group of tools that run together and after every group, a failed or cancelled
one included. Each file whose state moved between two snapshots is a change:
created, modified, deleted, or reverted for a file that differed from
the index before and matches it after — a formatter that restored what the
commit holds. A change is listed on the invocation of that group whose files
hold it, and on the operation (changes) when none does. changesObserved says
whether the changes were observed and changesScope what that covers: ignored
files and the contents of submodules and nested repositories are not observed,
and a rename is a deletion and a creation. When they were not observed,
changesReason says why — no-fix-task for lint and a fix that planned
nothing, not-run, not-executed when the tools could not be set up,
snapshot-failed (with changesDetail) when git is missing, the root is not a
repository, or a status failed — and no list stands for "nothing changed";
changes seen before a snapshot failed are kept. Snapshots are taken only when
the run records anything: a report, a JSON-L stream, annotations or agent
output.
patch writes the unified diffs the formatters that write their result on
stdout applied, captured while both versions existed, in the order they were
applied: a file two formatters changed one after the other has two hunks, the
first formatter's first, and git apply or patch -p1 takes the file — a
name that holds a control character, a quote or a backslash is quoted as git
quotes it. A tool
that writes its files itself leaves no patch; its changes carry patch: false.
Patches hold source text, so they are captured only when --report patch= is
asked for, and the run's json then holds them too, masked like everything
else, which lets report render --format patch write them again. Like
history, patch lists no finding: a narrowed run writes it, and it leaves
fail-fast alone.
A tool that exits non-zero without a finding its parser could read is listed
with one synthetic finding of level error and no location, whose message
says so — tsc exited 2 without parsable findings — rather than as a clean
tool. The message carries none of the tool's output. The document keeps the last
4 KiB of a failed invocation's output as outputTail, without its colour codes.
For a tool whose parser module puts it in the security category, the output is
never kept: its synthetic finding reads
gitleaks failed (exit 1); output withheld for a security tool, and there is no
outputTail.
Before a report is written, the value of every variable of the environment,
and of every app's and operation's env, whose name contains TOKEN,
SECRET, PASSWORD or CREDENTIAL, or ends in _KEY (in any letter case),
and that is at least 8 characters long, is replaced by *** wherever it occurs
in the document. This catches what the environment names; a secret a tool
prints that no variable holds is not caught, which is why a security tool's
output is withheld altogether.
Baselines
lint --baseline <file> and check --baseline <file> gate only on findings the
baseline does not hold. The file is a report baseline
document or a run's own JSON, whose findings are taken as one; it is read
before anything runs, and one that cannot be read, of another schema, or of
fingerprints of another version exits 2.
datamitsu lint --baseline .datamitsu-baseline.json
A finding whose fingerprint the baseline holds is marked while its process is
judged, before the threshold decides — at most as many findings of a rule on a
line as the baseline holds fingerprints of, whichever processes report them:
it is neither reported nor gates, so it
fails nothing, and under fail-fast cancels nothing. The terminal counts it —
· 2 baselined on the tool's line and in the footer — instead of printing it;
the own JSON marks it baselined, SARIF puts it in the result's
properties.baselined, JUnit lists it in <system-out> marked (baselined),
and Markdown and --output agent count it.
The exit code of a tool is untouched: a tool that exits non-zero still fails the
run, and its frame shows the baselined findings it failed on, marked
(baselined). Most linters exit 1 on the findings they print, so a baseline
makes a run fail only on new findings when the tool exits 0 and the gate
decides: give the tool its exit-zero flag — --exit-zero, --exit-code 0 —
and let failOn gate. See Baselines.
GitHub annotations
In a GitHub Actions job fix, lint and check print the run's findings as
workflow commands once the last operation has ended — ::error file=…,line=…::
— which GitHub shows as annotations in the Checks tab and on the lines a pull
request changed, with no token and no extra step:
::error file=src/a.ts,line=3,col=7,endColumn=8,title=eslint(no-unused-vars)::'x' is assigned a value but never used.
::error title=tsc::tsc exited 2 without parsable findings
--annotations (or DATAMITSU_ANNOTATIONS) chooses when:
| Mode | Prints |
|---|---|
auto | the annotations of the CI the run is in — GitHub's in a GitHub Actions job (GITHUB_ACTIONS=true, not Gitea or Forgejo), Azure Pipelines' under TF_BUILD, TeamCity's under TEAMCITY_VERSION — unless stdout carries a document or --log-format jsonl is on; the default |
github | GitHub's, wherever the run is, under --log-format jsonl too: stdout is not the event stream's channel |
azure | Azure Pipelines' logging commands, wherever the run is (see Azure Pipelines and TeamCity) |
teamcity | TeamCity's service messages, wherever the run is |
off | never |
An explicit mode together with a report written to stdout (-) or with
--explain=json exits 2: the commands would land in the document. Any other
value, from the flag or the variable, exits 2 as well. Nothing is printed when
no task ran — a plan that matched nothing, a run refused before it started.
The annotations are recorded as an export of the report,
github-annotations, azure-annotations or teamcity-annotations, written
or omitted with the reason, and under --log-format jsonl as a report
event; the stream's hello says the mode the run settled on.
The annotations are what the terminal shows (see
Findings in the terminal): each tool's findings at
or above its failOn, or all of them for a tool that failed on findings below
it, plus one error without a file for a tool that failed without a finding its
parser could read. A finding reported twice — by fix and by lint, or by two
processes of one tool — is annotated once. The path is relative to the
repository root, whatever directory the workflow checked the repository out
into; a finding outside the repository names no file. Columns are given in
characters, and only on a one-line finding whose columns could be converted
(see the report's precision). A message is escaped and cut at 4096 bytes.
GitHub keeps the first ten annotations of each type — error, warning, notice —
a step prints, so datamitsu chooses which ten: findings in the files the change
touched come first, then one finding of every other file before a second of any,
then findings without a file. When anything did not fit, a notice takes the
first notice slot and says how many were left out and where they are:
datamitsu: 15 more findings in the step summary and in out/run.json names the
step summary when it was written and every report written to a file that lists
findings. The files a change touched come from git: on a
pull_request, the merge commit the checkout holds against its first parent,
the base branch's tip (with actions/checkout's default depth of 1 that parent
is not fetched: set fetch-depth: 2); on a push, the commit the push started
from, named by the event (fetched only with fetch-depth: 0). When the change
cannot be read, one info line says why, and the order is the same without the
priority. Several datamitsu commands in one step share one budget of ten.
In a GitHub Actions job the run also appends its markdown report
to the step summary, the Markdown page of the job's run that
GITHUB_STEP_SUMMARY names, unless --annotations off — whatever stdout
carries, since the summary is a file, and under --log-format jsonl too. It
lists every finding the annotations could have shown. GitHub takes 1 MiB of
summary from a step, whoever wrote it, so the run writes what fits in what the
file has left and ends with a line saying how many findings it cut and, when the run wrote its json report to a file, that every finding is there. A summary
that cannot be written — the variable is unset, the file cannot be opened, or
it is full — is one warning (a log event under --log-format jsonl), never a
failure; nothing is written when no task ran. Gitea and Forgejo have no step
summary.
Everything the run prints between its first results block and the annotations
is one ::stop-commands:: region with a random token, so no line a tool printed
— raw output, a parsed message — can be read as a workflow command; the
annotations follow its end. Every line of tool text in a frame is indented
behind its border. The problem matchers actions/setup-go and
actions/setup-node register read every line whatever the region, and the
indentation keeps tsc's and eslint-stylish's from matching; two remain:
go's pattern accepts any text before a path, so raw output in Go's
file:line:col: message form still matches — with the border in the file name,
which GitHub drops, keeping an error annotation without a location — and so does
eslint-compact's, for output in ESLint's compact format. Such a line appears
only when a tool's output is shown raw: it has no parser, --no-parse is on, or
it failed without findings.
Azure Pipelines and TeamCity
Under TF_BUILD the run logs its findings as the issues of the Azure Pipelines
task, once the last operation has ended:
##vso[task.logissue type=error;sourcepath=src/a.ts;linenumber=3;columnnumber=7;code=eslint(no-unused-vars)]'x' is assigned a value but never used.
##vso[task.logissue type=error;code=tsc]tsc exited 2 without parsable findings
The findings are those GitHub would get — what the terminal shows, each once,
and the failures without a finding — with the path relative to the repository
root and the column in characters on a one-line finding whose columns could be
converted. The agent has two issue types, error and warning, so info and
hint findings are not logged, and keeps ten of each type per task: datamitsu
orders each type as it does for GitHub — the files the pull request touched
first, then one finding of every other file before a second of any — and when
anything did not fit, ends with one plain line, datamitsu: 4 more findings in out/run.json. The touched files are those that differ between HEAD and its
merge base with origin/<target branch> (SYSTEM_PULLREQUEST_TARGETBRANCH);
when that ref is not fetched, or the build is not a pull request, one info
line says so. A property value and the message are escaped as the agent
unescapes them: % as %AZP25, ; as %3B, a carriage return as %0D, a
line feed as %0A, ] as %5D.
The agent runs a ##vso[ command wherever it appears in a line, not only at its
start, on stdout and stderr, and has no way to suspend that. Under TF_BUILD —
whatever --annotations says — and in azure mode, every line the run prints
that holds tool output or a repository path — raw output, a parsed message, a
command line, a directory, a file name, a progress label, an agent record or
tail, a JSON-L event, a --verbose log line, an --explain plan, a GitHub
annotation printed there — has ##vso[ rewritten to ##vso [, so no tool and no
file name can set a variable, upload a file or fail the task through
datamitsu's output. That alters a line of raw output by one space. A report
written to stdout — by the run or by report render — is a document that has to
keep its meaning: a JSON format spells the bracket \u005b and an XML format
[, which read back as the same text, and Markdown and patch get the
space.
Under TEAMCITY_VERSION the run reports its findings as TeamCity inspections:
an inspectionType once per tool and rule (id <tool>/<rule>, the rule's
documentation as its description when the tool names one), an inspection per
finding with the file, the line and a SEVERITY of ERROR, WARNING, INFO
or WEAK WARNING for the four levels, and a buildProblem for a tool that
failed without a finding, whose identity is the tool's name in Java identifier
characters, at most 60. TeamCity keeps every inspection, so nothing is left out,
except a finding without a file, which an inspection cannot hold: one plain line
counts those. Values are escaped as TeamCity reads them: | as ||, ' as
|', [ and ] as |[ and |], line breaks as |n and |r, and other
control characters and the Unicode line separators as |0xNNNN.
TeamCity reads a service message anywhere in a line too. Everything the run
prints between its first results block and the inspections is wrapped in
##teamcity[disableServiceMessages] … ##teamcity[enableServiceMessages], and
the inspections follow the second line; under TEAMCITY_VERSION and in
teamcity mode every line that holds tool output or a repository path —
inside the region and out, the event stream included — has ##teamcity[
rewritten to ##teamcity [, so neither a tool nor a file name can turn the
reading back on. A run killed before it prints its inspections — a second
interrupt — leaves the reading suspended for the rest of the build step.
Tools still see TF_BUILD and TEAMCITY_VERSION (see
Tool Environment); a tool that prints logging commands
or service messages of its own when it sees them has them broken like any other
line. The recipes are in the Reports guide.
Skipped tools
A tool is reported as skipped (not run, not failed) for one of three reasons:
- Disabled in config — the tool sets
skip: true(optionally with askipReason). - No binary for this platform — the tool's binary has no build for the current OS/architecture/libc. This is a soft skip: the run still succeeds.
- Cannot narrow — a narrowed invocation selected less than the operation's declared granularity and the widening policy does not permit expanding it.
Skipped tools appear as ⊘ <tool> skipped (<reason>) lines and a · N skipped
count in the summary footer, and as a skipped array in --explain=json.
--fail-on-skip makes the run exit 4 only for platform skips (a tool you
expected to run had no binary), the code of a run that did not look at
everything. Intentional skip: true and narrowing skips never fail that flag;
use --require-coverage to enforce complete narrowed runs.
Narrowed runs
Naming files, or running from a subdirectory, asks for less than the whole
repository. Most tools follow: a formatter given one file formats that file.
Some cannot. syncpack compares versions across package.json files, so a
one-file answer is not a smaller answer, it is a wrong one.
datamitsu decides this per operation, from the tool's granularity. A narrowable tool runs on what you asked for. One whose verdict covers the whole repository is reported as skipped:
$ cd packages/api
$ datamitsu fix ./swagger.json
✓ oxfmt packages/api/swagger.json
⊘ syncpack skipped (whole-repository verdict — cannot narrow)
Before this behaviour existed those tools were dropped silently — absent from the
report, from --explain, from everything. If you were relying on that silence,
the tools have not changed; only the reporting has.
--widen-to overrides how far the core may go beyond your request, in either
direction — you typed it for this one run, so it wins over the project's policy:
# Only what I named. Anything needing more is reported, not run.
datamitsu fix ./swagger.json --widen-to=target
# Run the whole-repository tools anyway.
datamitsu fix ./swagger.json --widen-to=repo
The policy governs unit-scoped tools too, not only whole-repository ones. A tool
whose command line carries no path — go fmt ./..., golangci-lint run, and most
formatters that find their own files — reads its entire project whatever you
named, so naming one file runs it over that file's project and no other. Under
target it is reported rather than run, because a whole project is more than what
you named. Tools that take a {files} list are already narrowed by that list and
are unaffected.
Set the project default in config with
execution.widenTo.
--require-coverage turns an incomplete answer into a failure, for CI:
# Every unit the run touched was fully analysed.
datamitsu check --require-coverage=unit
# ...and the run covered the repository at all.
datamitsu check --require-coverage=repo
unit fails if a tool could not be narrowed, if a task covered only part of its
unit, or if the host lacked a binary. repo adds that the run must have targeted
the whole repository — without that clause, check --require-coverage pkg/x.ts
would pass having checked one package of nine.
A tool you disabled with skip: true never counts against coverage: a repository
that opts a tool out has given its answer, not an incomplete one.
It works under --explain too, since coverage is decided while planning — a CI
gate that costs one plan and downloads nothing. It cannot be combined with
--tools, which would make the assertion cover only the selected subset.
fix
Run fix operations on files.
datamitsu fix [files...]
| Flag | Description |
|---|---|
--explain [mode] | Show execution plan without running. Modes: summary (default), detailed, json |
--file-scoped | Only process git staged files |
--tools <list> | Comma-separated list of tools to run |
--fail-on-skip | Exit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools) |
--widen-to <level> | Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs) |
--require-coverage <level> | Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs) |
--fail-fast[=false] | Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs) |
--fail-on <level> | Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings) |
--report <format>=<path> | Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports) |
--events <what> | Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events) |
--allow-partial | Write a report that lists findings for a narrowed run instead of refusing the run (see Reports) |
--annotations <mode> | Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity) |
--output <mode> | How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output) |
Examples:
# Fix entire project
datamitsu fix
# Fix specific files
datamitsu fix src/main.go
# Fix only with specific tools
datamitsu fix --tools prettier,eslint
lint
Run lint operations on files.
datamitsu lint [files...]
| Flag | Description |
|---|---|
--explain [mode] | Show execution plan without running. Modes: summary (default), detailed, json |
--file-scoped | Only process git staged files |
--tools <list> | Comma-separated list of tools to run |
--fail-on-skip | Exit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools) |
--widen-to <level> | Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs) |
--require-coverage <level> | Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs) |
--fail-fast[=false] | Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs) |
--fail-on <level> | Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings) |
--report <format>=<path> | Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports) |
--events <what> | Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events) |
--allow-partial | Write a report that lists findings for a narrowed run instead of refusing the run (see Reports) |
--annotations <mode> | Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity) |
--output <mode> | How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output) |
--baseline <file> | Gate only on findings the baseline does not hold: a report baseline document or a run's own JSON; a tool that exits non-zero still fails (see Baselines) |
Examples:
# Lint entire project
datamitsu lint
# Lint specific files
datamitsu lint src/main.go
# Show detailed execution plan
datamitsu lint --explain detailed
inspect
Explore the resolved configuration in a browser or export a portable HTML artifact.
datamitsu inspect # Local server on loopback port 7744
datamitsu inspect --port 0 # Choose an available port
datamitsu inspect --output atlas.html # Export a self-contained HTML file
datamitsu inspect --output - > atlas.html # Write HTML to stdout
datamitsu inspect --theme theme.json # Apply color overrides
datamitsu inspect --print-theme # Print the theme that would be applied
--output (-o) and --port are mutually exclusive, and so is --print-theme with
either of them. The inspector loads the usual configuration chain and serves a fixed
snapshot until restarted. Exported HTML works offline and on static hosts, with
light/dark themes and hash-based links to views, filters, and selections.
--theme merges a JSON file of color overrides onto the built-in theme, token by
token and mode by mode; an export bakes the result in. An unknown key or a value that
is not a color is an error naming the full key path, and colors that fall below WCAG
AA contrast against their surface produce a warning, not an error. --print-theme
writes the merged theme — the complete built-in one when no --theme is given. See
Config Inspector for navigation, artifact contents,
theming, and publishing examples.
report
Work with the reports fix, lint and check write with
--report. None of these commands runs a tool.
report render
Render a run's own JSON document (datamitsu.report/1) into a report format,
offline:
datamitsu report render --input <run.json> --format <format> [--output <path>|-]
| Flag | Description |
|---|---|
--input <path> | The own JSON document a run wrote with --report json=<path> (required) |
--format <format> | The format to write: json, markdown, sarif, junit, codequality, checkstyle or rdjsonl; a format's options follow a ? (required) |
--output <path> | Where to write it; -, the default, is stdout. Written atomically, like --report |
--allow-partial | Render a format that lists findings for a document of a narrowed run |
The renderers and the completeness rule are the run's own: a document of a
narrowed run is refused (exit 2) for a format that lists findings unless
--allow-partial, and a document without its completeness fields is read as
incomplete, never as complete — as is a tool an earlier build called complete
although one of its processes failed while its parser found nothing
(failed-without-findings). The document holds everything a renderer needs,
so rendering works on another machine and after the checkout changed; json
reproduces the document byte for byte, and every other format the file the run
wrote, its completeness companion included, which is written beside
--output and never on stdout. An --output that ends in / is a directory,
for sarif; a document with more tools than one SARIF file holds is refused
(exit 2) for a file or stdout. A document of another schema, or one
that cannot be read, exits 1; an output that cannot be written exits 5.
# Print a run's report again
datamitsu report render --input out/run.json --format json
# The same run for a person
datamitsu report render --input out/run.json --format markdown --output out/run.md
# For code scanning, split into files of twenty tools
datamitsu report render --input out/run.json --format sarif --output sarif/
history renders too: it appends the document's line to --output.
report baseline
Write the fingerprints of every finding a run reported as a baseline,
datamitsu.baseline/1, which lint --baseline and
check --baseline read:
datamitsu report baseline <run.json> [--output <path>|-]
| Flag | Description |
|---|---|
--output <path> | Where to write the baseline; -, the default, is stdout. Written atomically |
The baseline holds the fingerprint version (fingerprint: dmfp1), when it
was made (createdAt, from SOURCE_DATE_EPOCH when set), the build and
configuration of the run it was made from, that run's start, commit and ref,
whether it was complete with the reasons of the run and its tools (source),
and the sorted fingerprints of every finding of every operation, whatever its
level; a tool that failed without findings adds none. A document of
fingerprints of another version exits 2. A run that was not
complete is taken with one WARN line: its baseline holds fewer fingerprints,
which only suppresses fewer findings. A document that cannot be read, or of
another schema, exits 1; an output that cannot be written exits 5.
datamitsu lint --report json=out/run.json
datamitsu report baseline out/run.json --output .datamitsu-baseline.json
report diff
Compare the findings of two runs' own JSON documents by fingerprint, tool by tool:
datamitsu report diff <before.json> <after.json> [--format json|markdown] [--output <path>|-]
| Flag | Description |
|---|---|
--format <format> | json (the default), datamitsu.diff/1, or markdown for a person |
--output <path> | Where to write it; -, the default, is stdout. Written atomically |
Each document contributes the operation a report lists — lint, or fix for a
fix-only run. A finding is:
| Class | When |
|---|---|
new | only the second run holds it |
unchanged | both hold it, on the same row |
moved | both hold it, on another row (beforeRow names the first run's) |
fixed | only the first run holds it, and the second run covered the whole repository with the tool complete |
unknown | only the first run holds it, and the second did not look everywhere: the tool's reasons say why |
unobserved | every finding of a tool only one run holds (status: before-only or after-only) |
The document counts each class per tool and in summary, and lists every
finding with its fingerprint, level, rule, message, path and row. For two
complete runs the counts are symmetric: what one direction calls new the other
calls fixed. The command exits 0 whatever it finds; a document that cannot be
read, or of another schema, exits 1, an unknown --format 2, and an output that
cannot be written 5.
datamitsu report diff main.json branch.json --format markdown
config
Configuration management commands.
config reconcile
Reconcile project-owned configuration files declared under managedConfigs
for the detected project types, then run datamitsu fix.
datamitsu config reconcile [flags]
| Flag | Description |
|---|---|
--dry-run | Preview reconciliation without writing files or running datamitsu fix |
--skip-fix | Reconcile managed files without running the post-reconciliation datamitsu fix |
--opt-in-tools | Also reconcile an all-disabled .datamitsuignore so tools are opt-in (enable them by removing names from the list) |
--tools | Comma-separated list of tools to scope reconciliation to (only their managed config files are considered) |
--no-verify-hash | Skip expectChainHash verification even when a pinned managed config drifted from its upstream chain |
With no flags, the command creates, replaces, patches, links, or removes the
managed files and then runs datamitsu fix. Use --dry-run to inspect the plan
without either action, or --skip-fix when the files should be reconciled but
formatters should not run. There are deliberately no --apply or --fix
switches: writing and the post-reconciliation fix are the default workflow.
There is also no general --force flag; the one narrow safety-check bypass is
--no-verify-hash.
:::warning init and config reconcile are different operations
After editing datamitsu.config.*, run datamitsu init to provision apps,
runtimes, bundles, .datamitsu links, and init commands. Run
datamitsu config reconcile only when you explicitly intend to rewrite
project-owned files declared under managedConfigs.
:::
With --tools, reconciliation is scoped to the named tools: it considers only
the config files associated with them (via each managed config entry's
tools field)
and leaves everything else untouched — other tools' configs and unassociated
infrastructure files (.gitignore, lefthook.yaml, pnpm-workspace.yaml, …)
are skipped. The default post-reconciliation fix is scoped to the same tools;
pass --skip-fix to omit it. This is the recommended way to iterate on a single
tool's config without rewriting your whole project. Unknown tool names fail
fast before anything is written; a selected tool that owns no config file is
reported and skipped (not an error).
With --opt-in-tools, reconciliation also writes a
.datamitsuignore at the git root that disables every configured tool via a
single **/*: <tool names> rule. You then enable tools one at a time by removing
their names from that list. The file is written before the default fix so that
fix already respects the opt-in state. Reconciliation refuses to overwrite an
existing .datamitsuignore; --dry-run previews the file without writing it.
See Generating an all-disabled file.
An ejectable
config the project has not ejected lives in .datamitsu/configs/, not in the
repository: reconciliation removes its repository copy — and writes
.datamitsu/configs/ so the post-reconciliation fix finds it — but only when the
copy holds nothing the configuration would not render again. A copy with changes
of the project's own is never deleted; reconciliation refuses before writing
anything and names the file, to be kept by adding its tool to ejectConfigs or
deleted by hand.
Any config file that pins expectChainHash
is verified before reconciliation writes anything: if its upstream chain drifted from the
pinned hash, reconciliation aborts with a report (the expected/actual hash and the full
incoming content) and no files are written. Review the change against your
overrides, update the pin, and re-run — or explicitly pass --no-verify-hash
to write regardless. The check also runs during --dry-run.
Examples:
# Preview managed config changes without writing or fixing
datamitsu config reconcile --dry-run
# Reconcile managed files, then run fix (default)
datamitsu config reconcile
# Reconcile managed files without running fix
datamitsu config reconcile --skip-fix
# Reconcile configs and start with all tools disabled (opt-in model)
datamitsu config reconcile --opt-in-tools
# Regenerate only golangci-lint's config (e.g. .golangci.yml); nothing else is touched
datamitsu config reconcile --tools golangci-lint
# Preview a tool-scoped reconciliation without writing or fixing
datamitsu config reconcile --tools golangci-lint --dry-run
# Scope to several tools at once
datamitsu config reconcile --tools golangci-lint,prettier
config show
Display the current configuration as JSON.
datamitsu config show
config types
Display the TypeScript type definitions file (config.d.ts).
datamitsu config types
config runtime
Display the full effective runtime configuration snapshot as JSON — env-resolved execution limits, the per-app install timeout, and the minimum release age. This is the introspection/debug surface: it reflects what the program runs with right now, including environment overrides. It is not the surface injected into the config JS VM (config JS receives only the minimal allowlisted datamitsuConfigInputs).
datamitsu config runtime
Examples:
# Inspect the full effective runtime config
datamitsu config runtime
# Mechanically verify a single value (and that env overrides apply)
datamitsu config runtime | jq .minimumReleaseAgeMinutes
DATAMITSU_INSTALL_TIMEOUT=1200 datamitsu config runtime | jq .installTimeoutSeconds # -> 1200
# The parse caps: bytes read from each output stream, findings kept per process
datamitsu config runtime | jq '{maxParseInputBytes, maxFindingsPerProcess}'
fix, lint and check refuse a DATAMITSU_MAX_PARSE_INPUT_BYTES or
DATAMITSU_MAX_FINDINGS_PER_PROCESS that is not a positive integer with exit 2;
config runtime reports the default in its place. See
parse caps.
config chain-hash
Print the XXH3-128 chain hash that datamitsu config reconcile verifies for managed config files — the hash of the content entering each file's root (topmost) config layer. Copy it into a managedConfigs entry's expectChainHash to pin the upstream baseline your overrides were written against, without having to trigger a drift error to read it.
datamitsu config chain-hash [file...]
The value is the input to the topmost layer, so declare your own entry for the file first (a placeholder expectChainHash is enough), then read the real hash here. With no arguments every managed config file is listed as file hash; with exactly one file only its hash is printed, ready to paste into a pin — expectChainHash accepts only the canonical xxh3:<32 lowercase hex> form.
Examples:
# List the chain hash of every managed config file
datamitsu config chain-hash
# Print just one file's hash (capture it into a variable / your config)
datamitsu config chain-hash eslint.config.mjs
pin=$(datamitsu config chain-hash eslint.config.mjs)
config lockfile
Generate lock file content for a runtime-managed app (Bun/Node/UV/Go).
datamitsu config lockfile [appName]
Without arguments, lists all apps that use lock files. With an app name, creates
the package-manager lock data from scratch and outputs it as a brotli-compressed,
base64-encoded JSON string ready to paste into configuration. The command uses a
special config-load path that permits the selected app's initially missing
lockFile; every normal config load requires lock files for Bun, Node, UV, and Go
apps.
Transitive dependencies are subject to the minimum release age: pnpm resolves
Bun and Node apps with minimumReleaseAge, uv resolves UV apps with
--exclude-newer P7D and records the window in the lock, and a Go app fails
when any resolved module is younger than the effective minimum release age
(DATAMITSU_MIN_RELEASE_AGE=0 skips that check). See
Transitive dependencies.
Examples:
# List apps that support lock files
datamitsu config lockfile
# Generate lock file for a Bun or Node app
datamitsu config lockfile eslint
# Generate the go.mod + go.sum lock payload for a Go app
datamitsu config lockfile govulncheck
devtools
Developer utility commands for maintaining datamitsu configurations.
devtools pull-releases
Update binary configurations from release providers. Requires a file argument specifying the path to the binary apps JSON file. If the file doesn't exist, an empty appstate structure is created automatically. Fetches repository descriptions from the source API and stores them in the output JSON.
datamitsu devtools pull-releases <file>
datamitsu devtools pull-releases config/src/binaryApps.json
datamitsu devtools pull-releases config/src/binaryApps.json --update
| Flag | Description |
|---|---|
--update | Fetch latest release tags before updating |
--verify-extraction | Verify that each downloaded asset extracts to an executable (ELF, Mach-O, PE or a #! script) |
--min-age <minutes> | Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age |
By default, the command scans releases for all platform combinations using OS/Arch/Libc target tuples. For Linux, both glibc and musl variants are detected separately; an asset whose name says alpine or musl counts as a Linux build even without linux in it, so tool-alpine lands on linux/amd64/musl. The output JSON uses a nested three-level storage structure:
{
"binaries": {
"linux": {
"amd64": {
"glibc": { "url": "...", "hash": "...", "contentType": "tar.gz" },
"musl": { "url": "...", "hash": "...", "contentType": "tar.gz" }
}
},
"darwin": {
"arm64": {
"unknown": { "url": "...", "hash": "...", "contentType": "tar.gz" }
}
}
}
}
Add an optional top-level platforms array to the input manifest to restrict detection:
{
"sources": { "github": { "type": "github", "url": "https://github.com" } },
"platforms": ["darwin/arm64", "linux/amd64/musl"],
"apps": {},
"binaries": {}
}
Accepted identifiers are darwin/amd64, darwin/arm64, windows/amd64,
windows/arm64, freebsd/amd64, freebsd/arm64, openbsd/amd64,
openbsd/arm64, linux/amd64/glibc, linux/amd64/musl,
linux/arm64/glibc and linux/arm64/musl. Values are case-sensitive and exact:
there are no aliases or whitespace normalization. linux/amd64 is not accepted.
An empty array, null, an invalid type or an unknown identifier fails before
filtering or GitHub requests. Repeated identifiers are processed once.
When present, the array removes every unselected platform from existing binary
entries before pulling releases, including entries without an apps record.
This deletion is saved even if a subsequent pull fails. Only selected platforms
are detected and, with --verify-extraction, downloaded and verified. A selected
platform without a compatible asset is reported and omitted. The list filters
available platforms; it does not require every app to publish every selected
platform. An app fails if none of its selected platforms has a usable asset.
A missing SHA-256 digest or failed verification still fails the app without
saving a partial new entry. Explicit selections keep separate glibc and musl
entries even when they share an asset.
The sorted, unique selection participates in configHash: changing the set
reruns detection at the same tag; reordering it does not. Removing platforms
restores default-all detection at the same tag. Without the field, existing
all-platform behavior and libc deduplication are preserved.
Non-Linux platforms use unknown as the libc key. Without an explicit selection, a missing musl variant is simply omitted.
Before scoring, the detector filters out three categories of assets: checksum files (.sha256, .md5, .sha512, etc.), attestations published beside an asset (.proof, .sig, .asc, .minisig, .pem, .crt, .sigstore.json, .intoto.jsonl, .sbom, .spdx.json, .cdx.json) and non-executable package formats and installers (.vsix, .deb, .rpm, .nupkg, .whl, .msi, .msix, .appx, .pkg, .dmg, and a name in which setup or installer stands as a word of its own, such as Harper_2.11.0_x64-setup.exe — unless the app's own name says so). This prevents IDE extensions, package-manager bundles, a desktop app's installer or a signature file from outscoring — or standing in for — actual binaries when releases mix them.
When a release holds several programs — harper publishes harper-cli-*, harper-ls-* and the Harper desktop app together — the asset whose name carries the app's name as whole tokens (case and separators aside) outranks the others of the same platform, whatever their format. An asset that names the requested libc still ranks above it: a tool-alpine-* build is the musl build even when only tool-cli-linux-* carries the app's name.
The manifest has no schema version. Every app selects a named source and a repository path:
{
"sources": {
"github": { "type": "github", "url": "https://github.com" },
"codeberg": { "type": "forgejo", "url": "https://codeberg.org" },
"gitea": { "type": "gitea", "url": "https://gitea.com" },
"gitlab": { "type": "gitlab", "url": "https://gitlab.com" }
},
"apps": {
"tool": { "source": "github", "repository": "owner/tool", "tag": "v1.0.0" }
},
"binaries": {}
}
The repository and tag above are illustrative. Use the actual tag your source
publishes. repository accepts nested GitLab namespaces. Supported type values
are exactly github, gitlab, gitea and forgejo; Codeberg is a Forgejo
instance, not a separate provider. url is the instance URL, including a
self-hosted subpath when needed. Optional apiUrl overrides the derived API
root (/api/v3 for GitHub Enterprise, /api/v4 for GitLab, /api/v1 for Gitea
and Forgejo; public GitHub uses https://api.github.com). Source URLs require
HTTPS; local loopback instances also accept HTTP.
An optional source tokenEnv names a third-party token environment variable.
No token value is saved. It authorizes discovery API requests independently of
artifact downloads. The optional downloadAuth field controls downloads:
| Value | Behavior |
|---|---|
auto (default) | Resolve access separately for each asset from provider metadata and its endpoint. Public downloads have no auth. Restricted downloads require tokenEnv. Unknown access is an error with instructions to select an explicit policy. |
required | Require tokenEnv and authorize downloads on the configured instance. External origins remain anonymous. This uses the provider's token scheme; it does not configure reverse-proxy Basic auth or SSO. |
none | Never attach download credentials. Reject endpoints known to require auth; explicitly permit unknown endpoints anonymously. Discovery still uses tokenEnv. |
For example, a source can specify "downloadAuth": "none" alongside a
tokenEnv used only for metadata. Different aliases of one instance can choose
different policies; app-level overrides are not supported.
GitHub reads explicit repository private metadata. Published public native
assets retain browser_download_url; private assets and drafts use the API
asset URL with an octet-stream Accept header. Gitea and Forgejo native release
attachments use repository visibility and draft status, retaining their browser
download URL with a Bearer token when restricted. Successful repository metadata
with known visibility is cached for the current client; absent visibility is
never silently treated as public.
GitLab reads visibility (public, internal, or private). It prefers the
original release link target over the permanent redirect URL. Generic-package
URLs for the same project use package_registry_access_level: public permits
anonymous download, private requires auth, enabled follows project visibility,
and disabled is an error. Missing package policy requires an explicit
downloadAuth. Restricted permanent release URLs use the token-capable release
API download route. Arbitrary same-origin links, including links to other
projects, require an explicit policy. External links never inherit repository
credentials, even when the project is private or the mode is required.
See the GitLab package registry access rules
and project API access levels.
Installation, checksum downloads, and --verify-extraction consume the same
resolved download auth. Missing required tokens fail before the request.
Cross-origin redirects drop credentials. There is no anonymous-to-authenticated
fallback after HTTP errors. SHA-256 verification remains mandatory in every mode.
After upgrading from the initial multi-source implementation, run
pull-releases again with the existing tags. The generation fingerprint
invalidates old entries for all providers once, replacing public download auth
and GitHub API asset URLs with anonymous download URLs. Changing downloadAuth
also invalidates generated entries.
Integrity: GitHub API digests are used directly. GitLab generic-package
links obtain SHA-256 from package-file metadata for the same project. A source
without a published digest must use explicit per-app hashes (asset filename
to SHA-256), or checksums (checksum asset filename to the checksum file's own
SHA-256). Only candidate files for selected platforms resolve digests. Checksum
files are downloaded only with their own validated SHA-256 pin; a binary is
never downloaded to manufacture its expected hash. GNU SHA-256 lists and BSD
SHA256 (filename) = hash records are supported. Conflicting records are errors.
Per-app pins apply by asset filename. When a new release needs those pins to
resolve its digests, update them from independently trusted upstream metadata
before using --update. With --verify-extraction, a stale matching pin fails verification and keeps
the previous app entry. Without that flag, a manual hashes pin cannot be
checked against the binary during pulling: the new tag and expected hash are
recorded, and installation still refuses a mismatching download. Checksum-file
pins are always verified when fetched. Pins are never refreshed by trusting
unverified downloaded bytes. Native
API digests permit automatic updates without maintaining per-app pins.
Breaking migration: rename your registry file to binaryApps.json, add
sources, and replace each app's owner/repo with source/repository.
Replace pull-github with pull-releases in scripts. The old command and old
manifest shape are not accepted. Unknown fields, sources and repository shapes
fail before pruning or network requests. configHash includes the source
profile, repository, tag, integrity pins and sorted unique platform selection.
Generated entries record their resolved provider, instance, repository and tag.
Examples:
# Detect binaries for the tags the file already pins (apps added or re-pinned by hand)
datamitsu devtools pull-releases config/src/binaryApps.json
# Update to latest releases
datamitsu devtools pull-releases config/src/binaryApps.json --update
# Update and verify that archives extract correctly
datamitsu devtools pull-releases config/src/binaryApps.json --update --verify-extraction
Failures, retries and the exit code:
Every app is attempted; one that fails does not stop the others. A request that fails for a transient reason — a network error, a timeout, a 5xx, a rate limit that names a short wait — is made up to four times with backoff, and each retry is printed with the app's request, the attempt and the reason. A 404, a release whose assets carry no digest, or a rate limit with no wait in sight is not retried. The run ends with a report naming every failed app, the stage it failed at (metadata, latest release, release <tag>, binaries for <tag>, verify <platform>) and the error, plus a hint when the cause is a GitHub rate limit, and exits with status 1 when any app failed.
With --verify-extraction, the download of each asset retries the same way. A platform whose asset turns out to be at fault — a hash mismatch, an archive that does not extract, a file that is not an executable — falls back to the next-ranked asset; a download that fails does not, since it says nothing about the asset, and a platform that still cannot be verified fails the app: its previous entry is kept, the report names the platform and the asset (verify linux/amd64/glibc), and the run exits with status 1.
The file is saved after each app that succeeds, and each save replaces the file whole. Every entry in it is therefore either the previous state of an app that failed or was not reached, or the complete new state of one that succeeded; a failed app never gets a partial new entry. The deletion of platforms excluded by platforms is applied and saved before any app pull, including pulls that fail. A save that fails stops the run, since nothing after it could be recorded either.
:::tip See also For a complete workflow including CI automation, see Maintaining Wrapper Packages — Binary Apps. :::
devtools pull-node
Pull latest npm package versions from the npm registry. Requires a file argument specifying the path to the node apps JSON file. Descriptions are always fetched from the registry. If the file doesn't exist, an empty {} JSON file is created automatically.
datamitsu devtools pull-node <file>
datamitsu devtools pull-node config/src/nodeApps.json
datamitsu devtools pull-node config/src/nodeApps.json --update
| Flag | Description |
|---|---|
--update | Update versions in the JSON file with latest from npm |
--dry-run | Show results without writing to file |
--min-age <minutes> | Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age |
Examples:
# Preview available npm updates without modifying files
datamitsu devtools pull-node config/src/nodeApps.json --dry-run
# Apply updates from npm registry
datamitsu devtools pull-node config/src/nodeApps.json --update
# After updating, regenerate lock files for affected apps
datamitsu config lockfile prettier
datamitsu config lockfile eslint
Registry requests that fail for a transient reason are made up to four times, each retry printed. With --update, a package whose entry changes is saved before the next one is looked up, so a later failure or an interrupted run keeps everything already pulled. A package that still fails keeps its previous entry; the run lists every failed package after the summary and exits with status 1. Without --update (or with --dry-run) the file is not written.
:::tip See also For the full node app update workflow including lock file regeneration, see Maintaining Wrapper Packages — Node Apps. :::
devtools pull-uv
Pull latest Python package versions from PyPI. Requires a file argument specifying the path to the UV apps JSON file. Descriptions are always fetched from the registry. If the file doesn't exist, an empty {} JSON file is created automatically.
datamitsu devtools pull-uv <file>
datamitsu devtools pull-uv config/src/uvApps.json
datamitsu devtools pull-uv config/src/uvApps.json --update
| Flag | Description |
|---|---|
--update | Update versions in the JSON file with latest from PyPI |
--dry-run | Show results without writing to file |
--min-age <minutes> | Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age |
Examples:
# Preview available PyPI updates without modifying files
datamitsu devtools pull-uv config/src/uvApps.json --dry-run
# Apply updates from PyPI
datamitsu devtools pull-uv config/src/uvApps.json --update
# After updating, regenerate lock files for affected apps
datamitsu config lockfile yamllint
Registry requests that fail for a transient reason are made up to four times, each retry printed. With --update, a package whose entry changes is saved before the next one is looked up, so a later failure or an interrupted run keeps everything already pulled. A package that still fails keeps its previous entry; the run lists every failed package after the summary and exits with status 1. Without --update (or with --dry-run) the file is not written.
:::tip See also For the full UV app update workflow including lock file regeneration, see Maintaining Wrapper Packages — UV Apps. :::
devtools pull-runtimes
Pull runtime configurations (Bun, Node, UV, JVM, Go, pnpm) with latest versions from upstream releases. Fetches latest releases from upstream, computes SHA-256 hashes, and writes the result to <file>.
datamitsu devtools pull-runtimes --update <file>
| Flag | Description |
|---|---|
--update | Required. Fetch latest versions from upstream before updating |
--dry-run | Show what would be updated without writing files |
--runtime <name> | Update only the specified runtime (bun, node, uv, jvm, go, or pnpm) |
--min-age <minutes> | Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). Applies to specific-version sources (GitHub releases, npm pnpm), not major-version-line lookups. See Minimum Release Age |
The command detects binaries for all platform combinations (OS/Arch/Libc). For Linux, both glibc and musl variants are detected when upstream provides separate binaries. If a musl binary is identical to the glibc variant (same URL and hash), the musl entry is deduplicated.
Version sources:
- bun: latest eligible Bun GitHub release; official archives and their SHA-256 digests from GitHub release metadata
- node: latest Node.js LTS resolved automatically; archives + SHA-256 from nodejs.org/dist (glibc/darwin/windows, GPG-verified via SHASUMS256.txt.asc) and unofficial-builds.nodejs.org (musl)
- pnpm: newest pnpm 12 release old enough for the minimum release age (judged by its npm publish date), with per-platform archive SHA-256 digests from the matching pnpm/pnpm GitHub release; the Node and Bun entries reference it with
pnpmRuntime: "pnpm" - UV: Python stable from endoflife.date, UV binary from GitHub
- JVM: Java version from Adoptium API, Temurin JDK from GitHub; when the newest feature release has no build old enough for the minimum release age yet, the previous feature release
- go: latest stable Go release + per-file SHA-256 from go.dev (
https://go.dev/dl/?mode=json); HTTPS with published SHA-256, no GPG (the git-pinned hash is the integrity anchor, same trust model as the musl Node path)
Examples:
# Update all runtimes
datamitsu devtools pull-runtimes --update config/src/runtimes.json
# Update only Bun runtime
datamitsu devtools pull-runtimes --update --runtime bun config/src/runtimes.json
# Update only UV runtime
datamitsu devtools pull-runtimes --update --runtime uv config/src/runtimes.json
# Update only Go runtime
datamitsu devtools pull-runtimes --update --runtime go config/src/runtimes.json
# Update only the pnpm runtime shared by Node and Bun
datamitsu devtools pull-runtimes --update --runtime pnpm config/src/runtimes.json
# Preview changes without writing
datamitsu devtools pull-runtimes --update --dry-run config/src/runtimes.json
Upstream requests that fail for a transient reason are made up to four times, each retry printed. Every runtime is attempted, pnpm first and then the rest in alphabetical order, and a runtime whose entry changes is saved before the next one starts, so a later failure or an interrupted run keeps everything already pulled. A runtime that fails keeps its previous entry — so does a Node or Bun entry that would name a pnpm runtime the file does not define — and the run lists every failed runtime with its error and exits with status 1. A save that fails stops the run.
:::tip See also For the full runtime update workflow and CI automation, see Maintaining Wrapper Packages — Runtimes. :::
devtools dockerfile
Generate an optimized, digest-pinned multi-stage Dockerfile from the loaded config. Emits a config-free base, a config-split stage, then one build stage per binary app, per managed runtime, and per runtime-managed app (each inheriting its runtime stage), then assembles the populated store with COPY --link — one cacheable layer per app, so bumping one app re-pulls only its layer. The base never carries the config and each stage loads only its own slice (see devtools split-config), so editing or regenerating the config rebuilds only the stages whose slice actually changed instead of every tool.
datamitsu devtools dockerfile -o docker/Dockerfile
| Flag | Description |
|---|---|
-o, --output <path> | Required. Output Dockerfile path including filename, fully overwritten on each run |
--alpine | Target the Alpine (musl) base image variant (<version>-alpine) |
--offline | Skip base-image digest resolution; emit an unpinned FROM with a warning |
--no-verify | Do not run apps' version checks in their build stages (verification is on by default) |
--config-js <path> | Pre-built config file COPYed into the image (default datamitsu.config.js) |
--repo <repo> | Override the base image repository (default: the repo this datamitsu build was published under) |
--label <key=value> | OCI label for the final image (repeatable) |
--arg <name[=val]> | Build ARG declared before ENV in the final stage; bare name or name=default (repeatable) |
--build-arg <n[=v]> | Build-time ARG→ENV in a dm-build stage so install stages inherit it; not in the final image (repeatable) |
--env <key=value> | ENV var baked into the final image (repeatable) |
--force-include <apps> | Keep binary apps that lack a binary for the target libc (comma-separated, repeatable) |
--emit-oci-map <path> | Also write the layer→subtree map JSON consumed by the OCI bundle annotation post-process |
libc filtering. The generated image targets one libc — musl with --alpine, glibc otherwise — and binary apps are filtered to those that ship a binary for it on every arch they declare. A glibc-only binary can't execute on a musl image (and vice versa), so incompatible apps are dropped from the Dockerfile and listed in a warning. Runtime-managed apps (bun/node/uv/jvm/go) are unaffected — their runtime carries the libc. Statically-linked tools that run on any libc but are under-declared in the registry (e.g. a static Go binary recorded as glibc-only) can be added back with --force-include name1,name2.
The base image repository and tag are baked into the datamitsu binary at release time, so the FROM points at the exact image this build came from — including across release channels, where the stable image (datamitsu/datamitsu:<version>) and the unstable image (datamitsu/datamitsu-unstable:<unstable-tag>) live in different repositories under different tags. The registry host defaults to ghcr.io (override with DATAMITSU_OCI_REGISTRY); --repo overrides the repository for mirrors or forks. The tag is resolved to a SHA-256 digest and pinned as FROM …@sha256:…. Pinning is best-effort and never fails the command: --offline, an unreachable registry, or a non-release (dev/unstable) build leave the FROM unpinned with a warning. The output file is fully overwritten (no managed regions).
Examples:
# Generate the glibc and Alpine variants
datamitsu devtools dockerfile -o docker/Dockerfile
datamitsu devtools dockerfile -o docker/Dockerfile.alpine --alpine
# Offline (no digest pinning) — useful for CI drift checks
datamitsu devtools dockerfile -o docker/Dockerfile --offline
# Resolve the base digest from a mirror registry
DATAMITSU_OCI_REGISTRY=mirror.internal datamitsu devtools dockerfile -o docker/Dockerfile
:::tip See also For the full image-publishing workflow and CI drift guard, see Maintaining Wrapper Packages — Generating a Docker image. :::
devtools split-config
Write one minimal config slice per app and per runtime into a directory. Each slice is a self-contained config defining exactly one stage's target — an app with its transitive dependencies and all referenced runtimes (including pnpm), or a runtime with its pnpm reference — that install --config can load on its own.
datamitsu devtools split-config -o ./slices
| Flag | Description |
|---|---|
-o, --output <dir> | Required. Output directory for the config slices |
This is the build-cache primitive behind devtools dockerfile: the generated Dockerfile runs it in the config-split stage so every other stage loads only its own slice. Editing an app changes its slice and those of its dependents, invalidating their build caches instead of busting the whole image. You rarely run it directly; it is documented because it appears in the generated Dockerfile. The config is read from the usual sources (--config / --before-config / auto-discovery).
devtools verify-all
Cross-platform config integrity checker. Downloads and hash-verifies binary apps and managed runtimes for all configured platforms. Every single file it extracts (all but extractDir archives) must be an executable: an ELF, Mach-O or PE file, or a script starting with #!.
datamitsu devtools verify-all
| Flag | Description |
|---|---|
--no-version-check | Skip version command execution and comparison |
--concurrency <n> | Concurrent download workers (default: DATAMITSU_CONCURRENCY or 3) |
--json | Output machine-readable JSON |
--skip-passed | Skip checks whose config is unchanged and passed last run |
--no-remote | Skip loading remote configs |
devtools pack-inline-archive
Pack a directory into a brotli-compressed tar archive for use in inline archive configs.
datamitsu devtools pack-inline-archive <directory>
Output is written to stdout in tar.br: format. Archives are deterministic.
devtools apps list
List all configured apps with their type, version, description, and install status. For binary apps, the resolved target (including libc variant) is determined by the current host's target detection. This helps verify which binary variant would be selected on the current system.
datamitsu devtools apps list
devtools apps inspect
Show install path and file tree for an installed app. For binary apps, the inspect output reflects the resolved target for the current host, including any libc fallback that may have occurred during resolution.
datamitsu devtools apps inspect <name>
devtools apps path
Print the install directory path for an app.
datamitsu devtools apps path <name>
devtools bundles list
List all configured bundles with name, version, and install status.
datamitsu devtools bundles list
devtools bundles inspect
Show install path and file tree for a bundle (collapses heavy directories).
datamitsu devtools bundles inspect <name>
devtools bundles path
Print the install directory path for a bundle.
datamitsu devtools bundles path <name>
devtools parsers
Inspect the WASM output-parser modules declared in the parsers
config and the tools they can parse. Each module self-describes (via its WASM
describe export) which tools it parses, how to invoke each, its upstream URL, and
its build-injected version — so this is the source of truth, not the config.
list aggregates every configured parser into a deduplicated catalog (a module
declared by N tools is described once); inspect shows the full detail for one
tool. A module built with descriptor schema 2 or later also says, per tool, which
level words the tool prints (levels, none when it prints none), the unit it
counts columns in where that was measured (columns), and whether it is a
security scanner (category); an older module shows none of these lines, which
is how to tell which contract a pinned module carries (see
levels). A module built with
descriptor schema 3 also lists its format parsers and the fallback sniffer,
each marked format (format parsers).
A module older than schema 3 is named once on stderr with a note: line, as
datamitsu config show names one already in the store
(older modules).
Both accept:
--json— machine-readable output, for driving configs or build pipelines.--wasm <path>— describe a local.wasmfile directly, with no config or network access (handy in CI and release tooling).--embedded— describe the fallback parser module built into this binary (format parsers and thefallbacksniffer only), with no config or network access (the embedded fallback).
# Human-readable catalog of all tools the configured parsers can parse
datamitsu devtools parsers list
# Machine-readable catalog (tools is [] when none are configured)
datamitsu devtools parsers list --json
# Full detail for one tool
datamitsu devtools parsers inspect hadolint
# Describe a locally built module without any config
datamitsu devtools parsers list --wasm ./parsers/target/wasm32-unknown-unknown/release/datamitsu_parsers.wasm
run pipes a tool's raw output (from stdin) through its parser and prints the
module's answer as JSON — abi, whether it recognized the output, the format
it read, and the structured diagnostics — the quickest way to develop or debug a
parser against real output. A module that answers with a bare array (ABI 1) is
shown in the same shape, with recognized inferred as the core infers it. It reads
stdout from stdin; pass --stderr-file / --exit-code for parsers that use them,
and --wasm <path> to use a local module instead of a configured one.
# Run eslint through datamitsu, then parse its JSON into diagnostics
datamitsu exec eslint -- --format json file.js \
| datamitsu devtools parsers run eslint --wasm ./datamitsu_parsers.wasm --exit-code 1
# Run a format parser of the fallback built into this binary
datamitsu exec shellcheck -- -f checkstyle script.sh \
| datamitsu devtools parsers run checkstyle-xml --embedded --exit-code 1
sniff [<file>|-] runs the sniffer of the built-in fallback over a captured output
(a file, or stdin with - or no argument) and prints the standard format it
recognized and the findings it read, or no standard format recognized. It is how
to check which format key a tool's flag produces before declaring it as
outputParser.parser. --stderr-file and --exit-code pass the tool's other
stream and exit code; --json prints the whole answer.
ruff check --output-format sarif src > out.sarif
datamitsu devtools parsers sniff out.sarif
# format: sarif (1 finding)
# src/a.py:1:8 error [F401] `os` imported but unused
prefetch downloads and SHA-256 verifies the configured parser modules into the
store, without compiling or running anything. Each module lands at its
content-addressed store path, so it can be COPYed into an OCI-bundle layer (the
generated Dockerfile's parser stages do exactly this) and found offline later. A
module already on disk whose bytes still match the declared SHA-256 is a no-op —
and one whose bytes do not is discarded and fetched again, whatever put it
there.
# Fetch every module declared in the parsers config
datamitsu devtools parsers prefetch
# Fetch only a subset
datamitsu devtools parsers prefetch datamitsu-parsers
devtools tools
Inspect the tools declared in the config — the fix/lint units datamitsu plans and
runs. list shows each tool's operations, project types, the app it runs, and its
output parser; inspect shows full per-operation detail. Both accept --json.
# All configured tools: name [operations] (projectTypes) → parser
datamitsu devtools tools list
# Machine-readable
datamitsu devtools tools list --json
# Full detail for one tool (per-operation app/scope/globs/args)
datamitsu devtools tools inspect eslint
Troubleshooting devtools commands
File not found errors:
If the JSON file argument doesn't exist, pull-releases, pull-node, and pull-uv create an empty file automatically. However, pull-runtimes requires the --update flag to write — running without it produces an error.
GitHub API rate limits:
Without a token the GitHub API allows 60 requests an hour, and a registry pull of a few dozen apps uses more. A rate limit that asks for a short wait (a Retry-After header, or a reset within two minutes) is waited out and retried; one that resets later is reported at once, with the reset time, and the run goes on with the next app and exits non-zero. Set the GITHUB_TOKEN environment variable to authenticate and raise the limit:
export GITHUB_TOKEN=ghp_your_token_here
datamitsu devtools pull-releases config/src/binaryApps.json --update
Hash mismatches:
If verify-all reports a hash mismatch, investigate the changed upstream
artifact before changing the expected hash. For release manifests, update manual
hashes or checksums pins only from independently trusted upstream metadata.
For a re-released tag using native API digests, clear the affected generated
entry's configHash to force discovery while preserving its archive-path history.
An unchanged tag and config hash skip discovery; --update alone does not
refresh manual pins. Then pull and verify again:
datamitsu devtools pull-releases config/src/binaryApps.json --verify-extraction
datamitsu devtools verify-all
Network errors:
Release and package devtools require network access to their configured forge, npm or PyPI. If you're behind a proxy, ensure HTTPS_PROXY is set. For intermittent failures, retry the command — downloads are idempotent.
cache
Manage the per-project cache for linting and fixing operations.
cache clear
Clear cache data.
datamitsu cache clear
| Flag | Description |
|---|---|
--all | Clear all project caches (not just the current project) |
--dry-run | Show what would be deleted without deleting |
:::warning This removes project source-mode farms
A project's source-mode farm and manifest live in the
same per-project cache directory, so cache clear deletes them along with the
lint/fix caches. Every shell already activated for that project then reports exit
127 for every declared tool — the shim deliberately does not re-bake a farm that
is not there. Run datamitsu source refresh (or re-activate) afterwards, and use
--dry-run first to see what would go.
Machine-level farms created with
datamitsu source --config <path> live in a separate configs/ namespace and are
not removed by cache clear, not even with --all. Rebuild one deliberately
with datamitsu source refresh --config <path> --force.
:::
cache clear also drops this project's cached config
evaluations
({cache}/config-eval/projects/{hash}); --all drops the whole config-eval
tree, including the machine-level --config chains whose farms survive. Clearing
them costs one re-evaluation and nothing else.
cache path
Print the absolute path to the global cache directory.
datamitsu cache path
cache path project
Print the absolute path to the current project's cache directory.
datamitsu cache path project
store
Manage the global binary and runtime store.
store path
Print the absolute path to the global store directory.
datamitsu store path
store clear
Remove the entire global store directory, including binaries, runtimes, app environments, bundles, parsers, package-manager data, managed Python installations, and remote configs.
datamitsu store clear
This removes all downloaded and installed managed content. You will need to run
datamitsu init again (and re-run lazy apps on demand) afterward.
store seed
Pull the OCI bundle declared by the config's top-level oci key into the global store — no docker/podman required. Without arguments the whole bundle is pulled (airgap seeding); with --apps only the layers of the named tools plus their runtime dependencies are pulled.
# Full pull from the config's oci declaration
datamitsu store seed
# Selective pull
datamitsu store seed --apps golangci-lint,prettier
# Explicit digest-pinned reference (overrides the config)
datamitsu store seed ghcr.io/owner/tool-store@sha256:0123…cdef
| Flag | Description |
|---|---|
--apps <list> | Seed only the layers of these apps and their runtime dependencies (repeatable) |
--resolve-tag | Allow a :<tag> reference: resolve it to a digest first and print it for pinning |
Every manifest body and blob is verified against its SHA-256 descriptor before extraction; seeded single-file binaries and JVM jars are additionally re-hashed against the published hashes from the config. A repeated full pull is a no-op (zero network requests).
Blob downloads have no overall timeout — only an attempt that delivers zero bytes for 2 minutes is aborted and retried, so large layers on slow links finish instead of being cut off mid-stream. The store commands also work without a usable git context (no git binary, dubious ownership in containers): a broken project repo only skips the project-level config with a warning.
store status
Show what the declared bundle contains for this platform and which configured apps it covers versus which still require the network.
datamitsu store status
datamitsu store status --json
store refs
List every artifact the effective config fetches from outside the machine: OCI references (the bundle and any registry-sourced parser modules) first, then the hash-pinned https downloads. It answers "what do I have to mirror?" and touches no network, so it runs inside a firewall.
# Everything this config pulls from anywhere
datamitsu store refs
# Just the registry side — the input to a mirroring loop
datamitsu store refs --oci-only | xargs -n1 -I{} crane copy {} harbor.corp/dm/{}
# Every artifact with its mandatory hash and platform
datamitsu store refs --json | jq '.https[] | select(.kind == "app-binary")'
| Flag | Description |
|---|---|
--json | Emit the references as JSON, including each artifact's mandatory hash |
--oci-only | List only OCI references, omitting the hash-pinned https downloads |
The output is sorted and byte-stable, so it is safe to pipe or diff between config revisions. Managed runtime binaries declared by the effective config, including the native pnpm archives of the pnpm runtime, are included. Package-manager dependencies fetched later by pnpm, UV, or Go are not individual entries; their exact versions and integrity data live in each app's mandatory lock file.
store import
Seed the store from a local OCI image layout directory (as produced by crane pull --format=oci, oras copy --to-oci-layout, or skopeo copy) — fully offline bundle transfer. Blobs are verified against the digest chain exactly like a registry pull.
datamitsu store import ./bundle-layout
datamitsu store import ./bundle-layout --digest sha256:0123…cdef
version
Print the version number.
datamitsu version
completion
Generate a shell-completion script for Bash, Zsh, Fish, or PowerShell.
datamitsu completion bash
datamitsu completion zsh
datamitsu completion fish
datamitsu completion powershell
The command writes the script to stdout. Run
datamitsu completion <shell> --help for the installation instructions for
that shell.
llms
Print this documentation as plain markdown, for feeding to an AI agent.
The docs are compiled into the binary, so they always match the version you are running and are served without network access. Content goes to stdout and diagnostics to stderr, so the output pipes straight into a model's context.
With no arguments it prints an index of every page, where each entry is the exact argument that fetches it — an agent can read the index and choose its next call without knowing anything about the site's URLs.
# Index of every available page
datamitsu llms
# One page, by its documentation path
datamitsu llms about
datamitsu llms contributing/brand-guidelines
# A unique page name may be abbreviated
datamitsu llms brand-guidelines
# A directory's own page is addressed by the directory
datamitsu llms getting-started/installation
# A URL copied from the website works too
datamitsu llms https://datamitsu.com/docs/guides/architecture/caching.md
Flags:
| Flag | Description |
|---|---|
--list | List every page slug, one per line |
--json | Emit JSON instead of markdown |
--web | Print the standard llms.txt index, with website URLs |
# Machine-readable page list, with titles and descriptions
datamitsu llms --list --json
# Provenance of the embedded snapshot
datamitsu llms --json | jq .pageSetHash
# One page as JSON, body included
datamitsu llms about --json
Exit codes: 0 on success, 2 for a usage error, 3 for an unknown or
ambiguous page. An unknown page suggests the nearest matches on stderr and
leaves stdout empty, so a failed fetch never pollutes a pipe.
lsp
Run a Language Server Protocol server on stdin/stdout, so an editor can format files through datamitsu's configured tools.
datamitsu lsp
This is normally launched by an editor rather than by hand — see the VS Code extension for the packaged integration.
The server answers document formatting and nothing else. It serves one
repository, which it takes from the editor at initialize (see
lsp root), and it picks up a changed configuration by itself, with
no restart (see lsp configuration reload).
A format request runs the project's fix tools on the real file, in place, so a
format also saves the file. A buffer with unsaved changes is written to disk
first, and the response holds the edits from that buffer to the fixed result.
That write goes through a temporary file, with the original's permissions,
renamed over the original, so it never leaves a truncated file behind, and it
replaces the file a symlink points to, not the symlink. A buffer that already
matches the file on disk gets an empty edit list: the fix is only on disk, and
the client has to reload the file. VS Code does so by itself. In Neovim, run
:edit after formatting; until then the buffer still shows the text from
before the format.
stdout carries only JSON-RPC. stderr carries only JSON-L events,
whatever --log-format says: status, progress, and log lines alike. --verbose
sets the log level to debug, so datamitsu's info and debug lines join the
stream as log events, never as plain text.
lsp root
One server serves one repository for its whole life. At initialize it starts
from the first of these that the editor sends:
- the first entry of
workspaceFolders; rootUri;rootPath;- none of them: the directory the server was started in.
From there it resolves the repository root the way the CLI does — the git root,
climbing out of a submodule to its topmost superproject — changes into that
root, and loads the configuration there. The initialize result names the root
in capabilities.experimental.datamitsu.root (see the
echo). Relative --config and --before-config
paths are resolved once, against the directory the server was started in,
before it changes directory.
File URIs are percent-decoded, and on Windows file:///C:/x is read as C:\x.
The root and every document path are compared after resolving symlinks, so a
workspace opened through a symlink formats like any other. A document outside
the served repository is not formatted: the server answers with an empty edit
list before planning or installing anything, and logs one info notice per
document per session.
The first workspace folder decides the repository, and every folder inside that
repository is served with it. For each other folder whose repository differs,
the server logs one info notice saying it does not format that folder. The
server does not advertise workspace-folder support, so an editor does not add
folders to a running server: one that wants several repositories formatted
starts one server for each.
Neither a missing repository nor a configuration that does not load stops the
server or fails initialize. initialize succeeds, one log event at level
error names the cause, and the session serves nothing: a format request gets
an empty edit list rather than an error, so the editor does not raise one on
every save, and the error is repeated once per distinct cause rather than on
every request. Once the configuration is fixed, the next format request loads
it.
lsp configuration reload
The server picks up a changed configuration by itself. At the start of every format request — before planning, because formatting the config file itself changes it — it compares the contents of the configuration's inputs with what it read at the last load:
- every file of the configuration chain: the discovered
datamitsu.config.*, the configs it layers on (a shared config package innode_modulesincluded), and any--configor--before-configfile; - the checkout's
HEAD(.git/HEAD, or a linked worktree's own), so a branch switch counts; pnpm-lock.yamlat the root, so an install that bumps a shared config package counts;- every auto-config file name at the root, present or not, so a second
datamitsu.config.*appearing counts.
A server started with --no-auto-config discovers no config at the root, so it
compares only the files of its --config chain.
It compares content, not modification times, so an edit within the same second counts, and a file reached through a symlink is compared by what it points to. A file that appears or disappears counts as a change too. When nothing changed, the request proceeds. When something did, the server loads the configuration again:
- It loads. The server replaces everything it built from the old
configuration — tools, planner, execution cache (the old cache is flushed
first) — and resolves the
initializationOptionsthe editor sent atinitializeagain, so aformat.toolsentry for a tool the new configuration adds now applies. It logsconfiguration reloadedatinfo, followed by the policy line, and logs the option warnings again. - It fails. Saving a half-edited config is the normal case. The session
keeps the configuration that last loaded, logs one
warnsaying the configuration changed but failed to load, with the cause, and keeps formatting with the previous configuration. The same content is not loaded or reported again on the next save; only a further change is. - No session yet, because the configuration did not load at
initialize: a successful load starts one.
A reload writes nothing: it does not run datamitsu init. When the new
configuration changes an
ejectable config
— what it renders to, or whether the project ejects it — a format that would run
a tool reading that config fails the managed-config preflight until the files
match the configuration again. The error names the command to run:
datamitsu init for a changed render. A tool the new configuration adds is
installed by the first format that needs it, like any other. The repository the
server serves, its command-line flags and its environment stay as they were
when it started.
lsp format policy
A format request plans fix for the saved file, then decides for each operation
whether it runs from the editor. The first matching rule wins; the reason is what
the server logs for a tool it leaves out:
| Rule | Result | Logged reason |
|---|---|---|
The operation has lsp: false | Left out | lsp: false in config |
format.tools[name] is false | Left out | disabled by format.tools |
Granularity repo | Left out, whatever format.tools says | repository-wide, never on save |
Granularity unit, a project without the saved file | Left out | Not logged |
Granularity unit, no globs, no opt-in | Left out | declares no globs, so it cannot tell which files it formats |
Granularity unit, level below unit | Left out | project-wide, editor policy is target or project policy is target |
Granularity unit, otherwise | Runs over the saved file's project | — |
Granularity file | Runs on the saved file | — |
A unit operation that declares no globs is planned for every file of its
project, so it would claim every save there, whatever the file's type: saving a
TypeScript file that lives in a Go module would run a glob-less
golangci-lint fmt over the whole module. It is left out unless
format.tools[name] is true. An author fixes it at the source by adding
globs (such as ["**/*.go"]), so only the files it formats bring it in. A tool
whose result for one file does not depend on its neighbours can also take
{files} and declare granularity: "file", so a save runs it on the saved file
alone.
The level of a unit operation is the project's
execution.widenTo.fix when
format.tools[name] is true, and otherwise the narrower of that and
format.widenTo. An opt-in therefore lifts the session's target, never the
project's. lsp: true on an operation is the same as leaving it unset. A tool
the policy leaves out is never downloaded.
format.timeoutMs is a watchdog, not a deadline. The plan runs one
priority group
at a time, and once the limit has elapsed no further group starts. The first
group always runs, a running tool is never interrupted, and downloads and
installs happen before the clock starts. When it fires, a warning names the file,
how many groups ran, and the tools that did not; the response still reflects
what is on disk. A cancel, unlike the watchdog, can stop a format before its
first group: see lsp cancellation.
lsp cancellation
The server keeps reading messages while a format runs, so it sees a
$/cancelRequest at once. Requests still run one at a time, in the order they
arrived. Document changes that arrive during a format are applied as they come
in, and a format request works on the text the document had when the request
arrived.
A cancelled request is always answered with RequestCancelled (-32800), never
with an error an editor would show. A request still waiting its turn never runs.
A running format stops at its next checkpoint: at the start, after planning,
after the managed-config preflight, after its tools are installed, before the
unsaved buffer is written, and before each priority group, the first one
included. A cancel never interrupts a running tool: killing a formatter that
rewrites a file in place can leave that file truncated. It does not abort a
download or install already under way either — the next save needs that tool
anyway — so the request stops once it is done.
What a cancelled format leaves on disk depends on when the cancel arrived:
| The cancel arrives | The file on disk |
|---|---|
| Before the unsaved buffer is written | Unchanged; the unsaved text stays in the editor only |
| After the buffer is written, before the first group | The unsaved text, unformatted |
| While a priority group runs | Fixed by that group and every group before it; no later group runs |
Each tool_run that started still gets its closing event, since the tool ran to
completion. One info notice names the file and, once a tool group has run, how
many ran and the tools that did not; a cancel before the first group only says
that no tool ran. done closes the request with status: "fail",
success: false and msg: "cancelled". The results of the tools that ran are
cached as usual.
Stopping the server uses the same checkpoints:
shutdown— the running format stops at its next checkpoint, every format request still waiting is answeredRequestCancelled, and any other request still waiting is handled as usual, so a queuedinitializestill initializes.shutdownis answered once nothing runs. Requests after it are answeredInvalidRequest(-32600).exitends the process: status0aftershutdown,1without it.- stdin closes, or its framing breaks (a header without a valid
Content-Length, for example) — the running format stops at its next checkpoint, waiting requests are dropped without a reply, the execution cache is flushed, and the server exits. A message whose body is not valid JSON is not a framing error: it is answered with a parse error, and the server keeps serving. SIGTERMor an interrupt — the first one does the same as stdin closing. A second one stops the running tool as well and exits at once, which can leave a file that tool was rewriting incomplete.
On Unix the server ignores SIGPIPE, so an editor that goes away in the middle
of a format cannot kill the server through a closed stdout or stderr.
lsp initialization options
The server reads initializationOptions at initialize and keeps them: a
configuration reload resolves them again against
the new configuration.
interface DatamitsuInitializationOptions {
format?: {
widenTo?: "target" | "unit";
timeoutMs?: number; // non-negative integer; 0 disables the watchdog
tools?: Record<string, boolean>; // keyed by tool name in the config's `tools`
};
}
Each key is resolved on its own, highest first: initializationOptions, then the
environment, then the default.
| Key | Values | Environment variable | Default |
|---|---|---|---|
format.widenTo | target or unit | DATAMITSU_LSP_FORMAT_WIDEN_TO | unit |
format.timeoutMs | Milliseconds, integer >= 0; 0 = off | DATAMITSU_LSP_FORMAT_TIMEOUT_MS | 15000 |
format.tools | Tool name to true or false | — | {} |
widenTo—repois refused: a repository-wide fix never runs on save. The environment variable set torepoor to an unknown value meansunit.timeoutMs— a negative or unparsable environment value means15000.tools—falsekeeps a tool out.trueruns a project-wide tool underwidenTo: "target", or one that declares noglobs, but never beyond the project'sexecution.widenTo.fix, never for a repository-wide tool, and never againstlsp: false.
A bad option never fails initialize. A value of the wrong type, a widenTo
of repo, a negative or fractional timeoutMs, a non-boolean tools entry, a
tools entry naming no configured tool, or an unknown key inside format is
ignored with a warning: a rejected widenTo or timeoutMs falls back to the
environment or the default, and a rejected tools entry has no effect. A
tools entry naming a tool the configuration does not have yet applies once a
reload adds that tool; the warnings are logged again with each reload. Unknown
top-level keys are ignored silently.
The initialize result echoes the policy the session runs with and the
repository it serves, under capabilities.experimental. tools holds the
entries the server accepted, and root is the
resolved repository root, absent when the server found none:
{
"capabilities": {
"experimental": {
"datamitsu": {
"format": { "widenTo": "unit", "timeoutMs": 15000, "tools": {} },
"root": "/work/example"
}
}
}
}
The echo is not repeated after a reload; the policy line the reload logs is the current one.
In Neovim 0.11 or later, init_options is sent as initializationOptions:
vim.lsp.config("datamitsu", {
cmd = { "datamitsu", "lsp" },
root_markers = { "datamitsu.config.ts", "datamitsu.config.mjs", "datamitsu.config.js" },
init_options = {
format = { widenTo = "target", timeoutMs = 5000, tools = { eslint = false } },
},
})
vim.lsp.enable("datamitsu")
Neovim sends the directory root_markers finds as the workspace folder, and the
server serves that directory's repository, wherever Neovim itself was started.
When no marker is found, Neovim sends no folder, and the server falls back to
the directory it was started in.
lsp events
Every stderr line is one JSON object carrying type and op_id, with
--verbose or without. The stream opens with a hello event, as a --log-format jsonl
stream does (see Run events). The language server never emits
diagnostic events: an editor gets findings through publishDiagnostics. It
emits:
type | Meaning |
|---|---|
log | A human-readable notice in msg, with level debug, info, warn or error |
phase | op: "format", status: "start": opens each format request the server starts running; no phase event closes it |
download, install | Tool provisioning progress |
tool_run | A tool task starting and ending: tool, dir (relative to the git root), success, duration_ms |
error | A tool task that failed: tool, dir, msg |
done | Closes the request's phase, under its op_id, failed and cancelled requests included: status done or fail |
done also carries op: "format", success, duration_ms, tools, runs,
failed and skipped. tools counts the distinct tools that ran and failed
the distinct tools with a failed task; runs counts the tasks that ran and
skipped the tasks the policy, the watchdog or a cancel left out. One tool failing in two
projects therefore gives runs: 2 and failed: 1. A counter or duration_ms
that is zero is left out of the line rather than written as 0: read a missing
one as zero. A tool_run op id is the format request's op id followed by
:<tool>:<dir>:<seq>, seq numbering the tasks of one priority group from 1,
so a consumer can attribute every task to its request. Every
task that starts also gets its closing tool_run, in a cancelled request too. A
cancelled request's done has status: "fail", success: false and
msg: "cancelled": there is no separate status for it.
log events carry the session's own notices:
- At
initialize: oneinfowith the effective policy (format policy: widenTo=unit timeoutMs=15000 tools={}), naming the keys that came frominitializationOptions; onewarnper rejected option; oneinfofor each other workspace folder in a different repository; and anerrorwhen there is no repository or the configuration does not load. - Per format request: one
infolisting the tools the policy left out and why, when there are any. Awarnalso marks a save the watchdog stopped, and aninfoa cancelled one. A document outside the served repository gets oneinfo, once per session. With no session, theerrorfrominitializeis repeated, once per distinct cause. - On a configuration change:
configuration reloadedatinfo, followed by the policy line, with the option warnings logged again; or onewarnwhen the changed configuration fails to load.
Once per session, and once more after each reload, a cache on disk that the
session will not write is reported too. Written by a different datamitsu
version — the editor runs another binary than the CLI — it is an info: the two
simply do not share cache entries. Written for a different configuration — for
example by a run with --tools, or while a changed configuration fails to load
in the session — it is a warn. Formatting works in both cases; it just does
not warm the cache.
Log lines from the rest of datamitsu — config loading, tool provisioning — are
log events too, each with its own op id, at its own level: warn and error
by default, debug and info as well with --verbose or a lower
DATAMITSU_LOG_LEVEL. msg carries the line's message followed by its fields.
source
Put the project's declared toolchain on PATH for the current shell, so every
tool runs as an ordinary command at the version this repository pins.
# bash / zsh
eval "$(datamitsu source bash)"
eval "$(datamitsu source zsh)"
# fish
datamitsu source fish | source
Activation downloads nothing — a tool is fetched the first time it is actually run. Switching branches needs no re-activation: the next tool invocation notices the config changed and re-resolves itself.
source requires a project. Run inside a repository whose git root holds a
datamitsu.config.{js,mjs,ts}, or name one explicitly with the global
--config flag; outside a project it exits non-zero and writes nothing to
stdout.
stdout carries shell code and nothing else, so the output is always safe to
eval. Warnings — tools not downloaded yet, system binaries this shadows — go
to stderr.
--no-auto-config without a --config is refused: activating the built-in
default config would bake a handful of built-in apps into the farm this root's
real config owns. --before-config and --no-auto-config also bypass the
manifest fast path and re-resolve the config, and a farm baked with them is not
reused by a later plain datamitsu source — a manifest baked from a different
config chain would otherwise activate a toolchain the plain invocation never
asked for.
If the farm cannot be re-baked at all — a config that does not evaluate on this
branch, a remote config unreachable offline — activation warns on stderr,
activates the farm already on disk and exits 0. Emitting nothing would exit 0
with a shell that was never activated, and every declared tool would then fall
through PATH to whatever the system has. source refresh is the repair command
and inverts this: a failed bake exits non-zero, leaving the previous farm in
place, so a CI step can trust refresh's exit code but not source's.
source --config: a machine-level toolchain
# outside any repository, e.g. from a shell rc file
datamitsu source fish --config ~/.config/datamitsu/datamitsu.config.ts | source
eval "$(datamitsu source bash --config ~/.config/datamitsu/datamitsu.config.ts)"
With the global --config flag the toolchain comes from a config you name rather
than from a project, so source works outside any git repository. status and
refresh take the same flag and act on the same farm.
Such a farm's origin is explicit-config, and it differs from a project farm in
two ways:
git-root | explicit-config | |
|---|---|---|
| identity | the git root | XXH3-128 of the resolved config chain |
| farm path | {cache}/cache/projects/{hash}/ | {cache}/cache/configs/{hash}/ |
| watch set | config chain + .git/HEAD | the config chain |
| shim | re-resolves from cwd's git root | revalidates its own recorded chain, never walks up for .git |
The chain is resolved to absolute paths with symlinks followed before hashing, so
a relative, an absolute and a symlinked spelling of the same file are one farm.
Order is significant, and --before-config files are part of the chain rather
than a modifier of it.
An explicit-config activation never merges a discovered config, even when it runs
inside a repository: the chain is exactly what was named, and every re-bake the
shim spawns carries --no-auto-config for the same reason. Unlike the other
config flags, --config does not bypass the manifest fast path — a config
farm's manifest lives at the path the chain itself hashes to, so the only
manifest such an invocation can find is one baked from those very files.
The trust boundary: an explicit-config farm never evaluates a project's
config. A shell activated against a machine-level config that cds into an
untrusted clone keeps running the machine-level tools; entering a repository's
toolchain still requires running datamitsu source there. This is a security
property, not an omission — a config is evaluated JavaScript.
Both farms can be active at once. Layering is PATH order and nothing else: the
project farm sits ahead of the config farm, so the project's pin wins, and a name
only the machine-level config declares stays reachable from inside the project.
Activation exports DATAMITSU_FARM and DATAMITSU_FARM_CONFIG, and deliberately
no DATAMITSU_ROOT — there is no git root, and an empty one would read as "the
repository could not be determined".
See Machine-Level Toolchain for the full walkthrough.
source status
datamitsu source status
datamitsu source status --json
datamitsu source status --config ~/.config/datamitsu/datamitsu.config.ts
Reports the farm for the current project: its origin, its root and directory, whether the
baked manifest still matches the working tree, every tool the farm provides with
how it is materialized (shim) and whether it has been downloaded
yet, every declared name that was refused with the reason, and every system
binary the farm shadows with the absolute path it was found at.
Names are never silently dropped, which is what makes this the first command to run when a tool is missing or resolves to the wrong version: a refused name is listed with its reason, so it can never be confused with a name that was never declared.
--json emits the same information as a single document and writes no human
text to stdout; warnings still go to stderr. origin is always present; a
git-root farm reports root, an explicit-config farm reports configPaths
and prints config: lines in place of root:.
status resolves and reports. It never downloads and never re-bakes the farm.
source refresh
datamitsu source refresh
datamitsu source refresh --force
datamitsu source refresh --config ~/.config/datamitsu/datamitsu.config.ts --force
Re-resolves the project's declared apps and rewrites the farm the current shell's
PATH points at. With --config it acts on that chain's machine-level farm
instead.
Nothing normally needs this. A tool invocation notices on its own that the config
changed and re-bakes before running, which is what makes branch switching
invisible. refresh exists for the case that detection cannot see: the staleness
check watches the config chain's files and .git/HEAD, so a config that branches
on an environment variable outside datamitsu's own namespace — reachable through
facts().env — changes what it produces without changing anything the check
compares. --force re-bakes unconditionally and is the escape hatch for exactly
that.
Without --force, a farm that already matches the tree is left alone and the
command exits 0.
Refresh resolves and materializes; it downloads nothing. A tool that has not been fetched yet stays a shim entry and installs on its first real use.
The summary goes to stderr and stdout stays empty, so refresh is safe to call
from the same shell function that runs an activation through eval.
Environment Variables
| Variable | Description | Default |
|---|---|---|
DATAMITSU_CACHE_DIR | Custom base directory; ephemeral data goes in {base}/cache, downloaded artifacts in {base}/store | $XDG_CACHE_HOME/datamitsu or ~/.cache/datamitsu |
DATAMITSU_CONCURRENCY | Number of concurrent download workers | 3 |
DATAMITSU_INSTALL_TIMEOUT | Per-app install timeout in seconds (0 = disabled) | 600 |
DATAMITSU_MIN_RELEASE_AGE | Minimum release age in minutes for pull-* and the Go lock-file check (0 = disabled) | 10080 |
DATAMITSU_MAX_CMD_LENGTH | Maximum command-line length before a list-taking operation is split into chunks | 32000 |
DATAMITSU_MAX_ERROR_CMD_DISPLAY | Maximum command length shown in an error before truncation | 120 |
DATAMITSU_MAX_PARALLEL_WORKERS | Maximum parallel tool execution workers | max(4, floor(NumCPU * 0.75)), capped at 16 |
DATAMITSU_UNIT_CACHE_TTL | Minutes a cached unit-level verdict stays trusted; 0 disables verdict caching | 1440 (24h) |
DATAMITSU_FAIL_FAST | Stop fix, lint and check at the first failing tool (true/1) or run everything (false/0) | true |
DATAMITSU_FAIL_ON | Raise every operation's failOn for fix, lint and check to error, warning, info or hint; never lowers one (twin of --fail-on) | - |
DATAMITSU_REPORT | Reports fix, lint and check write, as comma-separated format=path pairs (twin of --report; see Reports) | - |
DATAMITSU_ALLOW_PARTIAL | Write a report that lists findings for a narrowed run (true/1) instead of refusing it (twin of --allow-partial) | false |
DATAMITSU_EVENTS | Which findings the JSON-L stream of fix, lint and check emits: diagnostics=reported or diagnostics=all (twin of --events) | diagnostics=reported |
DATAMITSU_ANNOTATIONS | Whether fix, lint and check print CI annotations: auto, github, azure, teamcity or off (twin of --annotations) | auto |
DATAMITSU_OUTPUT | How fix, lint and check show their results: human or agent (twin of --output) | human |
DATAMITSU_CONFIG_CACHE | Serve evaluated config chains from disk (0/false/off/no disables it) | 1 |
DATAMITSU_LSP_FORMAT_WIDEN_TO | How far editor format-on-save may widen: target or unit | unit |
DATAMITSU_LSP_FORMAT_TIMEOUT_MS | Format-on-save watchdog in ms: no further tool group starts once it has elapsed (0 = disabled) | 15000 |
DATAMITSU_LOG_LEVEL | Log level (debug, info, warn, error) | warn |
DATAMITSU_LOG_FORMAT | Status output format (console, or newline-delimited jsonl with log lines as log events) | console |
DATAMITSU_TIMINGS | Enable detailed planner/runner timings (1 = enabled) | 0 |
DATAMITSU_STARTUP_TIMINGS | Report per-phase startup/config-load durations to stderr (1 = enabled) | 0 |
DATAMITSU_TRACE | Record a full execution trace and print its summary (1 = enabled) | 0 |
DATAMITSU_TRACE_DIR | Directory for execution trace files | {cache}/traces |
DATAMITSU_FORCE_GIT_SUBPROCESS | Resolve the git root by forking git instead of walking the filesystem (1 = enabled) | 0 |
DATAMITSU_BINARY_COMMAND | Override binary command path | - |
DATAMITSU_NO_SPONSOR | Suppress sponsor messages (any non-empty value) | - |
DATAMITSU_OFFLINE | Refuse all network access (any non-empty value; requires a pre-seeded store) | - |
DATAMITSU_NO_OCI | Disable OCI bundle store seeding (any non-empty value; twin of --no-oci) | - |
DATAMITSU_NO_PARSE | Show tools' raw output instead of parsed findings; parsing still runs (twin of --no-parse) | - |
DATAMITSU_MAX_PARSE_INPUT_BYTES | Bytes of each output stream of one tool process a parser reads; a longer stream is cut and the process is truncated | 8388608 (8 MiB) |
DATAMITSU_MAX_FINDINGS_PER_PROCESS | Findings kept from one tool process's output; the rest are dropped and the process is truncated | 10000 |
DATAMITSU_LIBC | Override host libc detection (glibc or musl); affects store paths and OCI bundle selection | auto-detected |
DATAMITSU_OCI_REGISTRY | Registry host for base-image digest resolution in devtools dockerfile | ghcr.io |
DATAMITSU_PARSERS_DIR | Override directory for downloaded WASM output-parser modules | {store}/.parsers |
DATAMITSU_ROOT | Git root of the source-mode farm activated in this shell (exported by datamitsu source) | - |
DATAMITSU_FARM | Farm directory activated in this shell (exported by datamitsu source) | - |
DATAMITSU_FARM_CONFIG | Config chain of the farm activated in this shell (exported by datamitsu source --config) | - |
NO_COLOR | Disable color output | - |
FORCE_COLOR | Force color output | - |
DATAMITSU_ROOT and DATAMITSU_FARM are written by datamitsu source. Neither
is used to resolve a tool — the shim discovers the repository root from the
working directory, so a tool run in another repository resolves against that
repository, not against the one the shell was activated in. DATAMITSU_ROOT is
purely informational; DATAMITSU_FARM is read in one place, to recognize that an
invocation arrived through a farm directory when the computed cache path and the
activated one spell the same directory differently (a DATAMITSU_CACHE_DIR or
HOME that resolves differently in this process). Without it a genuine tool
invocation could be mistaken for a renamed datamitsu binary and handed to the
CLI. DATAMITSU_FARM_CONFIG is written only by an explicit-config activation,
which exports no DATAMITSU_ROOT because it has no git root; it records the
config chain the farm was baked from, joined with the platform's list separator,
and is informational in the same way. All three are excluded from the farm's
staleness fingerprint, so exporting them cannot make a farm look stale.
DATAMITSU_FAIL_FAST, DATAMITSU_FAIL_ON, DATAMITSU_REPORT,
DATAMITSU_ALLOW_PARTIAL, DATAMITSU_EVENTS, DATAMITSU_ANNOTATIONS and DATAMITSU_OUTPUT are excluded too: they change how far one run goes and what it prints or writes, never what a
farm contains, so setting one for one command does not re-bake the farm.
DATAMITSU_FORCE_GIT_SUBPROCESS applies to the config loader's memoized git-root
lookup, which is where the pure-Go walk runs; the command handlers use a separate
resolver that always forks git. See
Startup and Config Load.
:::note --no-oci is not a network switch
--no-oci / DATAMITSU_NO_OCI turns off OCI bundle store seeding — an
accelerator that degrades gracefully to fetching each artifact individually. It
does not disable a parser that declares an oci source: that is not an
accelerator but the only route to those bytes, so disabling it would leave no
way to get the module at all.
DATAMITSU_OFFLINE is the hard gate. It refuses every network operation,
including a registry-sourced parser, and expects a pre-seeded store.
:::