@williamthorsen/nmr 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.readyup/kits/default.js +33 -22
- package/.readyup/manifest.json +2 -2
- package/README.md +327 -78
- package/agents/guidance/rulebooks/nmr.md +4 -2
- package/bin/nmr-report-catalog.js +11 -0
- package/dist/esm/UserError.d.ts +3 -0
- package/dist/esm/UserError.js +6 -0
- package/dist/esm/check-cache.d.ts +36 -1
- package/dist/esm/check-cache.js +93 -24
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +2 -1
- package/dist/esm/cli-report-catalog.d.ts +1 -0
- package/dist/esm/cli-report-catalog.js +11 -0
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/clean.js +9 -5
- package/dist/esm/commands/report-catalog.d.ts +1 -0
- package/dist/esm/commands/report-catalog.js +34 -0
- package/dist/esm/commands/report-overrides.js +22 -7
- package/dist/esm/commands/taze.js +2 -1
- package/dist/esm/config.d.ts +1 -0
- package/dist/esm/config.js +81 -16
- package/dist/esm/default-scripts.d.ts +5 -1
- package/dist/esm/default-scripts.js +19 -16
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.js +3 -1
- package/dist/esm/helpers/clampToBytes.d.ts +2 -0
- package/dist/esm/helpers/clampToBytes.js +20 -0
- package/dist/esm/helpers/createBoundedBuffer.js +1 -4
- package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
- package/dist/esm/helpers/deriveExcerpt.js +30 -0
- package/dist/esm/helpers/duration.js +16 -9
- package/dist/esm/helpers/package-json.d.ts +11 -4
- package/dist/esm/helpers/package-json.js +51 -22
- package/dist/esm/helpers/readStringValues.d.ts +1 -0
- package/dist/esm/helpers/readStringValues.js +8 -0
- package/dist/esm/helpers/transcript.d.ts +5 -0
- package/dist/esm/helpers/transcript.js +59 -0
- package/dist/esm/recording.d.ts +65 -0
- package/dist/esm/recording.js +96 -0
- package/dist/esm/replay-assembly.d.ts +9 -0
- package/dist/esm/replay-assembly.js +26 -0
- package/dist/esm/report-format.d.ts +18 -0
- package/dist/esm/report-format.js +25 -0
- package/dist/esm/reportCliFailure.d.ts +2 -0
- package/dist/esm/reportCliFailure.js +11 -0
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolver.d.ts +13 -3
- package/dist/esm/resolver.js +34 -19
- package/dist/esm/runCli.d.ts +1 -0
- package/dist/esm/runCli.js +372 -70
- package/dist/esm/runner.d.ts +20 -1
- package/dist/esm/runner.js +72 -12
- package/dist/esm/scripts.d.ts +1 -1
- package/dist/esm/steps.d.ts +18 -0
- package/dist/esm/steps.js +144 -0
- package/dist/esm/types.d.ts +11 -2
- package/dist/esm/verbosity.d.ts +22 -0
- package/dist/esm/verbosity.js +37 -0
- package/dist/esm/verdict.d.ts +29 -0
- package/dist/esm/verdict.js +150 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +20 -3
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -3,23 +3,69 @@
|
|
|
3
3
|
Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.31.0 (2026-08-13)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Report a verdict for every command nmr runs (#659)
|
|
11
11
|
|
|
12
|
-
`nmr
|
|
12
|
+
`nmr` now reports one line for every command it runs, naming the scope, command, outcome, and timing. The lines print in every verbosity, which makes `-q` the low-noise way to run rather than a silent one: a passing quiet run reports its verdicts and nothing else, so a pass, a cached skip, and a no-op override stay distinguishable.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Verdicts nest. A composite reports alongside every command it expands into, while a `:pre`/`:post` hook leaf and a `--filter`/`--recursive` delegate report nothing, since each is already covered by the levels around it.
|
|
15
|
+
|
|
16
|
+
- Resolve a run's verbosity from repo config and agent-harness detection (#660)
|
|
17
|
+
|
|
18
|
+
A verbosity preference for `nmr` can now be set in the repo's root config, and `nmr` now automatically switches to quiet mode when it detects a known agent harness. This makes four ways to set verbosity: the `-q` option, the `NMR_COMMAND_VERBOSITY` env var, config, and harness detection, consulted in that order and defaulting to `full` when none applies.
|
|
19
|
+
|
|
20
|
+
- 🚨 **Breaking:** Refine nmr upgrade to support catalogs (#661)
|
|
21
|
+
|
|
22
|
+
Adds `nmr-report-catalog`, which names the dependencies a package takes from a pnpm catalog, their `catalog:` specifiers, and the monorepo root a covering pass runs from. The workspace `upgrade` default chains it ahead of the upgrade tool, and `nmr report-catalog` runs it alone. Because taze reads `pnpm-workspace.yaml` only when that file sits at its working directory, changes to the catalog are not attempted during a package-scoped upgrade.
|
|
23
|
+
|
|
24
|
+
Separately, `nmr-report-overrides` now reports the `overrides` block in `pnpm-workspace.yaml` and fails on a `pnpm.overrides` block in the root `package.json`. Both `upgrade` and `root:upgrade` run the reporter ahead of taze. This avoids a scenario where taze would meaninglessly write to the latter block.
|
|
25
|
+
|
|
26
|
+
Migration: Move the block to `overrides` in `pnpm-workspace.yaml`, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`. `engines` declares `pnpm: ">=11"`.
|
|
27
|
+
|
|
28
|
+
- Replay an excerpt of the run a skip recalls (#667)
|
|
29
|
+
|
|
30
|
+
`nmr` can now show the result of the previous check when a check is skipped. When `nmr` runs a command, it stores the result, keyed by presentation environment. The result is deemed to be the last blank-line-delimited block written by the command. If the command is run again in the same presentation environment, the stored result is shown, labeled "replayed:" for clarity.
|
|
31
|
+
|
|
32
|
+
- Assemble a skipped composite's replay from its constituents' excerpts (#668)
|
|
33
|
+
|
|
34
|
+
A skipped composite now replays what its constituents recorded, with each line attributed to the scope and command that produced it:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
⏭️ nmr: check: passed 35.7s ago on this tree, saved ~10s — replayed: nmr: fmt:check: All matched files use Prettier code style!; nmr: test: Test Files 52 passed (52) Tests 987 passed (987)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Nesting collapses. A constituent that fans out across packages contributes a line from every package that recorded one; a constituent that recorded nothing is omitted.
|
|
41
|
+
|
|
42
|
+
A new reserved variable, `NMR_RUN_ID`, carries a run's identity to every process below it. Every recorded excerpt names the run that last certified it. A composite summary includes saved results with the same status as a fresh result.
|
|
43
|
+
|
|
44
|
+
- Print a retained run with nmr --log (#669)
|
|
15
45
|
|
|
16
|
-
|
|
46
|
+
Adds `nmr --log <command>`, which prints what the last recorded pass wrote instead of running the command again. The whole transcript, up to a limit of 256 KiB, is now saved for any passing command that is not writing straight to a terminal; `--log` prints it under a header giving the instant, the duration, and the command chain that succeeded. The log of a composite is the sum of its constituents' excerpts. If a change disqualifies the recording, `--log` instead reports the change.
|
|
17
47
|
|
|
18
|
-
|
|
48
|
+
- Emit one JSON object per command for machine consumers (#672)
|
|
19
49
|
|
|
20
|
-
|
|
50
|
+
Adds `--json` to `nmr`, allowing the caller to request output in JSON format instead of prose. An env var carries the choice of `json` or `text` down a spawned chain. When the output is JSON, the consumer receives one JSON object per command; each object carries the command, scope, and outcome, and a recalled pass carries the recorded excerpts.
|
|
21
51
|
|
|
22
|
-
|
|
52
|
+
The mode implies quiet. Stdout carries the objects alone (with no prose verdict line and no override notice); a failing command still displays its output on stderr.
|
|
53
|
+
|
|
54
|
+
- 🚨 **Breaking:** Bind trailing arguments to every step that accepts them (#675)
|
|
55
|
+
|
|
56
|
+
Routes an argument passed to an `nmr` command to every step of that command that accepts it, in place of the last step alone. Thus, `nmr test <file>` therefore runs only that file's tests; previously, the root suite ran in full alongside the narrowed package runs. Steps by default accept passed arguments, but can be configured to ignore them. If no step of a command accepts the argument, `nmr` names the command and runs nothing.
|
|
57
|
+
|
|
58
|
+
A command carrying an argument also reads the check-result cache nowhere in its chain, so it no longer skips part of its work from a pass recorded for an unnarrowed run.
|
|
59
|
+
|
|
60
|
+
Migration: Existing configs need no change, since a bare string element keeps its present meaning. Two behaviors differ: A composite passes its arguments to every accepting step rather than to its final step alone; and `nmr typecheck <file>` at the repo root is refused, where it previously checked that file under default compiler options at the root and hunted for it in every package.
|
|
61
|
+
|
|
62
|
+
### 🐛 Bug fixes
|
|
63
|
+
|
|
64
|
+
- Derive the shelled-nmr remedy from the declaration site and reject a malformed package.json (#658)
|
|
65
|
+
|
|
66
|
+
Fixes a shelled-nmr warning that named `.config/nmr.config.ts` as the remedy for every step it reported, including the `package.json` entries that tier cannot resolve. The line now names the relevant declaration and remedy. Shelled nmr can now be detected not only in a step's first token but anywhere in the step.
|
|
67
|
+
|
|
68
|
+
Separately, a configuration mistake now reports as a plain message rather than a Node stack trace.
|
|
23
69
|
<!-- /section:release-notes -->
|
|
24
70
|
|
|
25
71
|
## Installation
|
|
@@ -67,9 +113,9 @@ nmr's key feature is that the same command runs different scripts depending on w
|
|
|
67
113
|
|
|
68
114
|
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
69
115
|
| --------------------------------- | ----------------- | ----------------- | -------------------------------------------------------------------- |
|
|
70
|
-
| Monorepo root | Root scripts | Monorepo root |
|
|
116
|
+
| Monorepo root | Root scripts | Monorepo root | `root:test`, then `-R test` |
|
|
71
117
|
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project unit --project tool` (that package only) |
|
|
72
|
-
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root |
|
|
118
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | `root:test`, then `-R test` |
|
|
73
119
|
|
|
74
120
|
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
75
121
|
|
|
@@ -109,14 +155,37 @@ If no per-package override exists, the highest-tier value that is set wins. Set
|
|
|
109
155
|
|
|
110
156
|
### Script values
|
|
111
157
|
|
|
112
|
-
|
|
158
|
+
A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` carry an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
|
|
113
159
|
|
|
114
160
|
```ts
|
|
115
161
|
// "fix": ["lint", "fmt"]
|
|
116
162
|
// expands to: nmr lint && nmr fmt
|
|
117
163
|
```
|
|
118
164
|
|
|
119
|
-
|
|
165
|
+
Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace — `-R test` runs the element in every package, the way `nmr -R test` does. An element carrying a quoted argument or shell syntax is rejected when the config loads: name that command as a script of its own and reference the name.
|
|
166
|
+
|
|
167
|
+
#### Where trailing arguments go
|
|
168
|
+
|
|
169
|
+
Trailing arguments reach every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// "ci": [{ run: "build", declinesArgs: true }, "check:strict"]
|
|
173
|
+
// nmr ci src/foo.ts -> nmr build && nmr check:strict src/foo.ts
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`{ run }` is the bare string element with room for the declaration; `declinesArgs` defaults to `false`, and an element that accepts is written as the string. Decline where the step is a prerequisite the narrowed steps run against, or where the tool it reaches would be misled — `tsgo --noEmit foo.ts` abandons the tsconfig, which is why every default `typecheck` step declines. A leaf tool that merely ignores what it is handed is not a reason to decline: nmr forwards, and the tool decides.
|
|
177
|
+
|
|
178
|
+
Where no step of a command accepts them, nmr rejects the invocation and runs nothing, rather than running the whole command unnarrowed. That is what `nmr typecheck src/foo.ts` gets at the root, whose two steps both decline.
|
|
179
|
+
|
|
180
|
+
Two kinds of step are not declarable. A string script is one command handed to a leaf tool, so it always receives the arguments — which is why `nmr typecheck src/foo.ts` inside a package still reaches `tsgo`, where the root rejects it. A `:pre` or `:post` hook never receives them, the arguments having been placed before the hooks wrap the chain.
|
|
181
|
+
|
|
182
|
+
An invocation carrying trailing arguments also serves no part of its work from the [check-result cache](#check-result-cache), the steps that decline them included.
|
|
183
|
+
|
|
184
|
+
A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step reaching nmr through a shell.
|
|
185
|
+
|
|
186
|
+
The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first.
|
|
187
|
+
|
|
188
|
+
nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, or `|`, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. The line reports a boundary rather than enforcing a rule, and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
|
|
120
189
|
|
|
121
190
|
## Configuration
|
|
122
191
|
|
|
@@ -141,11 +210,11 @@ export default defineConfig({
|
|
|
141
210
|
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
142
211
|
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
|
|
143
212
|
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](#check-result-cache) may skip |
|
|
144
|
-
| `workspaceScripts` | `Record<string,
|
|
145
|
-
| `rootScripts` | `Record<string,
|
|
213
|
+
| `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2) |
|
|
214
|
+
| `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2) |
|
|
146
215
|
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
|
|
147
216
|
|
|
148
|
-
All fields are optional. `
|
|
217
|
+
All fields are optional. A `ScriptValue` is a command string or an array of steps, each a command string or `{ run, declinesArgs? }`; both are described in [script values](#script-values), and both types are exported from `@williamthorsen/nmr/config`.
|
|
149
218
|
|
|
150
219
|
Each field belongs to exactly one tier, and a config loads only the fields its own tier honors: `build` in a package config, the other four in the monorepo-root config. Declaring a field at the wrong tier fails with a message naming it and where it goes, as does a key nmr does not recognize at all — a typo cannot degrade into a setting that silently applies nowhere.
|
|
151
220
|
|
|
@@ -186,7 +255,7 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
186
255
|
|
|
187
256
|
## Pre and post hooks
|
|
188
257
|
|
|
189
|
-
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: nested invocations from composite expansion get their own hook treatment.
|
|
258
|
+
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
|
|
190
259
|
|
|
191
260
|
Behaviors worth knowing:
|
|
192
261
|
|
|
@@ -222,6 +291,64 @@ Attach a step to one package's hook:
|
|
|
222
291
|
|
|
223
292
|
The second example calls the bin directly: `report-overrides` is a root-registry command, so `nmr report-overrides` from inside a package fails with `Unknown command`, while the bin runs from either scope.
|
|
224
293
|
|
|
294
|
+
## What nmr reports
|
|
295
|
+
|
|
296
|
+
Every command nmr runs reports one line naming the scope it ran at, the command, the outcome, and the timing:
|
|
297
|
+
|
|
298
|
+
```console
|
|
299
|
+
✅ nmr-core: test: passed in 12.4s
|
|
300
|
+
❌ nmr: build: failed in 1.2s (exit 1)
|
|
301
|
+
⏭️ nmr-core: test: passed 4m ago on this tree, saved ~12s
|
|
302
|
+
⛔ nmr-core: lint: skipped, the override is empty
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A recalled pass also carries an excerpt of the run it recalls, where that run left one; see [Replaying a skipped run's output](#replaying-a-skipped-runs-output).
|
|
306
|
+
|
|
307
|
+
The scope is the directory the command's registry belongs to: a package's own directory, or the monorepo root. Durations and ages truncate and carry at most two units, so a check that passed 90 seconds ago reports `1m 30s ago` and never `2m ago`.
|
|
308
|
+
|
|
309
|
+
Verdicts nest. A composite reports, and so does every command it expands into; a `:pre` or `:post` hook reports nothing of its own, since the command it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either, every scope it fans out to reporting instead. A cold repo-wide `nmr ci` in this package's own monorepo spends 26 lines on a run that prints roughly 250.
|
|
310
|
+
|
|
311
|
+
A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` POSIX permits, so packages running concurrently under `pnpm --recursive` cannot interleave within a line. A line longer than that is cut and marked with `…`; nothing nmr reports today comes near it.
|
|
312
|
+
|
|
313
|
+
**Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
|
|
314
|
+
|
|
315
|
+
### Reporting for a machine
|
|
316
|
+
|
|
317
|
+
`--json` renders each verdict as a JSON object instead of a line of prose — one object per line, newline-delimited, with no enclosing array and no separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line it already emitted parseable, which an enclosing array would not.
|
|
318
|
+
|
|
319
|
+
```console
|
|
320
|
+
$ nmr --json check
|
|
321
|
+
{"command":"typecheck","scope":"nmr-core","outcome":"passed","durationMs":255}
|
|
322
|
+
{"command":"fmt:check","scope":"nmr-core","outcome":"recalled","ageMs":240000,"savedMs":3888,"replay":[{"command":"fmt:check","excerpt":"Checking formatting... All matched files use Prettier code style!","scope":"nmr-core"}]}
|
|
323
|
+
{"command":"check","scope":"nmr-core","outcome":"passed","durationMs":31204}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Every object carries `command`, `scope`, and `outcome`, and then what that outcome carries and no other does:
|
|
327
|
+
|
|
328
|
+
| `outcome` | Also carries |
|
|
329
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------- |
|
|
330
|
+
| `passed` | `durationMs` |
|
|
331
|
+
| `failed` | `durationMs`, `exitCode` |
|
|
332
|
+
| `recalled` | `ageMs`, `savedMs`, and `replay` where the skip has excerpts to [replay](#replaying-a-skipped-runs-output) |
|
|
333
|
+
| `no-op` | `reason`, either `empty-override` or `noop-override` |
|
|
334
|
+
|
|
335
|
+
Both renderings are produced from one record, so neither reports what the other does not. Where the prose line makes a presentation decision the record does not — a saving too small to be worth a clause, say — the object carries the fact and leaves the decision to whoever reads it.
|
|
336
|
+
|
|
337
|
+
**An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) a prose line is held to, so packages running concurrently under `pnpm --recursive` cannot interleave within one. Cuts land inside the record's text rather than across its structure, so the line still parses, and a record that overruns is fitted by rungs — each shedding what a reader can better spare than the one below it:
|
|
338
|
+
|
|
339
|
+
1. **The excerpts are shortened toward one another**, so an assembly's constituents come down together rather than the first ones being emptied to leave the last whole. No excerpt is cut below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
|
|
340
|
+
2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry carrying no `excerpt` key is one whose text did not fit.
|
|
341
|
+
3. **The trailing constituents go**, as the prose line drops its own tail, and the `replay` array itself once none fit.
|
|
342
|
+
4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself: a line that overruns can be split across a pipe and corrupt the records of every scope sharing it, where a marked cut costs only its own.
|
|
343
|
+
|
|
344
|
+
A package-scoped `check` carries four constituents and fits uncut. A four-package root `check` assembles twelve — `typecheck` and `test` each fan out to every package, and a nested composite's list is spliced in flat — which is more than the ceiling can name: it reaches rung 3, keeping eight and dropping the trailing four. Rung 4 needs a scope and command running to hundreds of bytes between them.
|
|
345
|
+
|
|
346
|
+
**The mode implies quiet.** The commands' own output is withheld, so stdout carries the objects alone — no prose verdict, and no override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. A failure still surrenders the failing command's output, on stderr, as under any quiet run. Both modes leave a command on the same channels, so a machine-readable run and a quiet one share a [retention key](#replaying-a-skipped-runs-output): each replays what the other recorded.
|
|
347
|
+
|
|
348
|
+
The format is nmr's own reporting and not a mode the commands it runs are told about, so it is out of the [pass key](#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor costs a replay.
|
|
349
|
+
|
|
350
|
+
`--log`, `--help`, and `--version` are unaffected: none of them reports a verdict, and what `--log` prints is a tool's own transcript, which has no rendering as a record.
|
|
351
|
+
|
|
225
352
|
## Check-result cache
|
|
226
353
|
|
|
227
354
|
A check that passed on a working tree passes again on the same working tree. nmr records that, and the next run of the same command on an unchanged tree exits 0 without doing the work:
|
|
@@ -231,10 +358,10 @@ $ nmr ci
|
|
|
231
358
|
# ... four minutes of build, typecheck, lint, and coverage ...
|
|
232
359
|
|
|
233
360
|
$ nmr ci
|
|
234
|
-
⏭️ ci: passed 2m ago on this tree
|
|
361
|
+
⏭️ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
|
|
235
362
|
```
|
|
236
363
|
|
|
237
|
-
Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip
|
|
364
|
+
Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip is [reported](#what-nmr-reports) in every verbosity.
|
|
238
365
|
|
|
239
366
|
The cache is a working-tree cache, not a build cache: it answers "has this exact tree already passed this check?" and nothing else. It is also local to one checkout (the install fingerprint in its key is machine-specific), so it saves repeated work in a working copy rather than sharing results across machines or with CI.
|
|
240
367
|
|
|
@@ -294,10 +421,79 @@ The gate never wrongly skips. Where it cannot be sure, it does nothing and the c
|
|
|
294
421
|
- When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
|
|
295
422
|
- When there is no pnpm install to fingerprint.
|
|
296
423
|
- Under a `devBin` substitution, which runs a binary built from somewhere the hash does not describe.
|
|
297
|
-
- For any invocation carrying arguments after the command name.
|
|
424
|
+
- For any invocation carrying arguments after the command name, and for every step below it: the arguments narrow what runs, and a step served from a recorded pass is a step that did not.
|
|
298
425
|
|
|
299
426
|
`NMR_DEBUG=1` reports why a run did not skip and why the gate stood aside.
|
|
300
427
|
|
|
428
|
+
### Replaying a skipped run's output
|
|
429
|
+
|
|
430
|
+
A skip reports an excerpt of the run it recalls, marked as a recording rather than as this run's output:
|
|
431
|
+
|
|
432
|
+
```console
|
|
433
|
+
$ nmr test
|
|
434
|
+
# ... the suite runs ...
|
|
435
|
+
|
|
436
|
+
$ nmr test
|
|
437
|
+
⏭️ nmr: test: passed 2m ago on this tree, saved ~12s — replayed: Test Files 6 passed (6) Tests 41 passed (41)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The excerpt is the last blank-line-delimited block of what the command wrote, which is the closing statement a tool separates from its progress output. It is flattened onto one line, with escape sequences stripped, table rules dropped, and a redrawn progress line reduced to what a reader was left looking at. Nothing is parsed and no figure is computed: nmr replays bytes it recorded. A block wider than a few kilobytes is cut and marked with `…`, so a command whose closing statement is one long line cannot carry its whole output into the cache.
|
|
441
|
+
|
|
442
|
+
The excerpt and the whole transcript it is drawn from are written by the same operation that records the pass, so a pass declined because the tree moved or build output changed leaves neither behind, and `nmr clean` clears both along with the passes they belong to.
|
|
443
|
+
|
|
444
|
+
**Capture happens at the command, and only where nmr can see its output.** A command whose stdout or stderr is a terminal writes where nmr never reads, so an interactive run retains nothing and its progress display is untouched; a piped or redirected run, and any run under `-q`, retains both streams. A composite hands its descriptors to the child `nmr` processes below it and captures nothing of its own, and a `:pre` or `:post` hook contributes nothing to the command it wraps: an excerpt is the command's own output or none.
|
|
445
|
+
|
|
446
|
+
**A composite replays its constituents' excerpts.** Capturing nothing of its own, a composite assembles what its constituents recorded, each line attributed to the scope and command that produced it:
|
|
447
|
+
|
|
448
|
+
```console
|
|
449
|
+
$ nmr check
|
|
450
|
+
⏭️ nmr-core: check: passed 3m ago on this tree, saved ~48s — replayed: nmr-core: fmt:check: All matched files use Prettier code style!; nmr-core: test: Test Files 6 passed (6) Tests 41 passed (41)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The assembly is flat: a constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test:coverage` rather than one opaque `check:strict` line. A delegate expands into the scopes it fans out to, and a constituent that recorded no excerpt is absent rather than inferred: `typecheck` and `lint:check` print nothing on success and contribute nothing above, and a command outside the [cacheable set](#what-is-cached) records nothing at all, so `ci` replays what `check:strict` earned and nothing for `build`.
|
|
454
|
+
|
|
455
|
+
It is assembled when the composite records its pass rather than when it skips, so every excerpt in it was certified during that one run, on that one tree. `NMR_RUN_ID` is what makes that provable; see [reserved environment variables](#reserved-environment-variables).
|
|
456
|
+
|
|
457
|
+
**A skip certifies what it replays.** Recalling a pass proves the excerpt describes this tree, and a matching retention key proves it describes this presentation environment, so a skip restamps the recalled entry with the run replaying it. A constituent already warm when a composite ran therefore still contributes its line: `nmr check` after `nmr test` replays the test summary. The restamp touches nothing else -- the instant and the duration belong to the run that earned the pass.
|
|
458
|
+
|
|
459
|
+
**A replay is held to more than the pass is.** Retention carries its own key: the [pass key](#what-the-key-is-made-of) folded with the channel each output stream ran on and with `CI`, `COLUMNS`, `FORCE_COLOR`, `NO_COLOR`, and `TERM`. A recording made under a different one is recalled as a pass all the same; its excerpt is simply not replayed, and the verdict prints alone, as it does for a pass that retained nothing.
|
|
460
|
+
|
|
461
|
+
The whole line, excerpt included, is held to the same [512-byte ceiling](#what-nmr-reports) every verdict is, so one write still carries it whole under concurrent fan-out. An assembly wide enough to overrun it is cut and marked with `…` rather than spread over several lines.
|
|
462
|
+
|
|
463
|
+
### Reading a retained run
|
|
464
|
+
|
|
465
|
+
The excerpt is a line; the run behind it is kept whole. `nmr --log <command>` prints it, led by a header dating what follows:
|
|
466
|
+
|
|
467
|
+
```console
|
|
468
|
+
$ nmr --log test
|
|
469
|
+
📼 nmr: test — recorded 2026-08-12T15:04:05.412Z (12m ago), ran in 12.4s
|
|
470
|
+
$ pnpm exec vitest --project unit --project tool
|
|
471
|
+
|
|
472
|
+
✓ src/__tests__/resolver.unit.test.ts (18 tests) 12ms
|
|
473
|
+
|
|
474
|
+
Test Files 6 passed (6)
|
|
475
|
+
Tests 41 passed (41)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The header is what presents the body as a recording rather than as this invocation's output. The command string is the whole chain, hooks included, so it names what earned the pass and not merely what was typed. A flag rather than a subcommand, since `log` would collide with the script registry.
|
|
479
|
+
|
|
480
|
+
**Reading a recording runs nothing.** No hook runs, no verdict prints, and nothing is recorded or restamped. `--no-cache` governs running rather than reading and is ignored.
|
|
481
|
+
|
|
482
|
+
**A recording is held to the tree, not to the terminal.** `--log` prints exactly what a skip would have recalled: the [pass key](#what-the-key-is-made-of) has to match. The [retention key](#replaying-a-skipped-runs-output) is not consulted, so a recording made under a different terminal width, or through a pipe, still prints -- a reader at a terminal being shown what a piped run wrote is the case the flag exists for.
|
|
483
|
+
|
|
484
|
+
**A composite prints its assembly.** Retaining nothing of its own, a composite prints the excerpts its constituents recorded, one attributed line each. The transcript of any one of them is that constituent's own `--log` to print.
|
|
485
|
+
|
|
486
|
+
**Nothing to show is said rather than left blank.** A refusal names which it is -- the command is outside the [cacheable set](#what-is-cached), the gate is [standing aside](#when-the-gate-stands-aside), nothing has recorded a pass, the last pass was recorded under something this run does not share, or the pass retained no output, having printed none or written to a terminal -- and exits non-zero, so a caller can tell an empty `stdout` from a recording. A mismatch names the ingredient that moved, the tree or the chain or a version, so a reader whose `git status` is clean is not sent looking there.
|
|
487
|
+
|
|
488
|
+
```console
|
|
489
|
+
$ nmr --log test
|
|
490
|
+
📭 nmr: test: no recording; the last pass was 3m ago, on a tree this is not
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**`--log` reaches the scopes `-F` and `-R` select.** The flag rides the delegate, so each scope prints its own recording. There a scope with nothing to show reports its gap and exits 0: partial coverage is a survey's normal shape, and failing on the first gap would hide every scope that had something.
|
|
494
|
+
|
|
495
|
+
**Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes a verdict line is held to. A transcript overrunning it keeps 128 KiB from each end, because a run's opening and its closing statement each carry what the other does not, and the drop is marked with a line naming the bytes it stands for rather than being cut silently.
|
|
496
|
+
|
|
301
497
|
### Bypassing and clearing
|
|
302
498
|
|
|
303
499
|
Reach for these in order; the first is almost always the right one.
|
|
@@ -315,6 +511,29 @@ Reach for these in order; the first is almost always the right one.
|
|
|
315
511
|
|
|
316
512
|
`NMR_TREE_SNAPSHOT` is nmr's own: it carries one observation of the tree from a top-level invocation down to the processes it spawns, so a chain hashes the tree once rather than at every link. nmr trusts an inherited value only while `HEAD` still stands where it did when the observation was taken, which bounds a process that outlives the run that spawned it. That bound does not extend to a tree edited without committing, so **a process that survives its run and later invokes nmr should clear `NMR_TREE_SNAPSHOT`** — a test suite that shells out to `nmr` is the case worth checking.
|
|
317
513
|
|
|
514
|
+
`NMR_RUN_ID` is nmr's own too: it carries one run's identity from a top-level invocation down to the processes it spawns, so the excerpts recorded at every scope during one run are recognizable as that run's, and a composite can assemble from them. Where `NMR_TREE_SNAPSHOT` is bounded by `HEAD`, this is bounded by the tree hash each entry records, so a process that outlives its run and carries the identity into a later one contributes nothing to that run's assemblies.
|
|
515
|
+
|
|
516
|
+
`NMR_COMMAND_VERBOSITY` is nmr's own as well, and carries how loudly a run reports the output of the commands it runs. Its values are `full` and `quiet`; any other non-empty value is reported and exits 1, rather than falling back to a mode nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command it runs rather than of everything below it, so a failure still surrenders the failing command's output. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message a quiet run withholds.
|
|
517
|
+
|
|
518
|
+
`-q` sets it for the run and outranks an inherited value. Both values are spelled out so either direction is expressible: export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
|
|
519
|
+
|
|
520
|
+
`NMR_REPORT_FORMAT` is nmr's own as well, and carries how nmr renders its own verdicts. Its values are `text` and `json`; any other non-empty value is reported and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so either direction is expressible: export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. Two sources, then, where a verbosity has four: no repo-level default and no harness detection, since detection would hand every agent JSON in place of the prose the [shipped guidance](#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects carry.
|
|
521
|
+
|
|
522
|
+
#### Where a verbosity comes from
|
|
523
|
+
|
|
524
|
+
Four sources, in precedence order. Each is read once, at the top of a chain: the resolved value travels down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
|
|
525
|
+
|
|
526
|
+
| Source | Set it when |
|
|
527
|
+
| ----------------------------------------------------- | ---------------------------------------------------------- |
|
|
528
|
+
| `-q` | One invocation should be quiet. |
|
|
529
|
+
| `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
|
|
530
|
+
| `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
|
|
531
|
+
| An agent harness nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
|
|
532
|
+
|
|
533
|
+
Falling off the bottom leaves the run `full`, which is what an unrecognized harness gets: detection adds quiet where it fires and changes nothing where it does not.
|
|
534
|
+
|
|
535
|
+
Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The shipped set is `CLAUDECODE`, `ROVODEV_CLI`, and `ROVO_CLI`. **Treat that set as perishable.** The names belong to ecosystems nmr does not control and that rename, and a rename fails silently here, since the symptom is a return to loud output rather than an error. `output.extraAgentEnvVars` is how a repo adds a name — a harness nmr has never heard of, or one whose marker was renamed after the installed version shipped — without waiting for a release. A repo that wants no detection at all configures `output.commandVerbosity: 'full'`, which outranks it.
|
|
536
|
+
|
|
318
537
|
### Configuring
|
|
319
538
|
|
|
320
539
|
```ts
|
|
@@ -330,18 +549,35 @@ export default defineConfig({
|
|
|
330
549
|
|
|
331
550
|
`extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest; `excludeCommands` is applied afterwards, so a name in both is excluded. `enabled: false` turns the gate off entirely. The key is `checkCache`, in the monorepo-root config.
|
|
332
551
|
|
|
552
|
+
```ts
|
|
553
|
+
export default defineConfig({
|
|
554
|
+
output: {
|
|
555
|
+
// The verbosity a run takes when neither `-q` nor the environment named one.
|
|
556
|
+
commandVerbosity: 'quiet',
|
|
557
|
+
// Markers of a harness the shipped set does not name, added to it rather than replacing it.
|
|
558
|
+
extraAgentEnvVars: ['MY_HARNESS'],
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
`output` is a monorepo-root key, and both of its fields feed the [verbosity ladder](#where-a-verbosity-comes-from).
|
|
564
|
+
|
|
333
565
|
## Dependency upgrades
|
|
334
566
|
|
|
335
567
|
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
336
568
|
|
|
337
|
-
| Invocation | Covers |
|
|
338
|
-
| ---------------------------- | ------------------------------------------- |
|
|
339
|
-
| `nmr upgrade` (from root) | the root `package.json` and every workspace |
|
|
340
|
-
| `nmr upgrade` (in a package) | that package |
|
|
341
|
-
| `nmr -F <package> upgrade` | that package, from anywhere |
|
|
342
|
-
| `nmr root:upgrade` | the root `package.json`
|
|
569
|
+
| Invocation | Covers | Catalogs |
|
|
570
|
+
| ---------------------------- | ------------------------------------------- | -------- |
|
|
571
|
+
| `nmr upgrade` (from root) | the root `package.json` and every workspace | yes |
|
|
572
|
+
| `nmr upgrade` (in a package) | that package | no |
|
|
573
|
+
| `nmr -F <package> upgrade` | that package, from anywhere | no |
|
|
574
|
+
| `nmr root:upgrade` | the root `package.json` | yes |
|
|
575
|
+
|
|
576
|
+
A [pnpm catalog](https://pnpm.io/catalogs) is declared in `pnpm-workspace.yaml`, and the upgrade tool reads that file only when it sits at the working directory, irrespective of `--recursive`, which is why `root:upgrade` covers every catalog too. A package-scoped invocation therefore cannot see a dependency the catalog declares, and a package whose dependencies are all catalogued would otherwise report itself up to date. It runs [`report-catalog`](#report-catalog) first instead, which names each catalogued dependency and the root a covering pass runs from.
|
|
343
577
|
|
|
344
|
-
|
|
578
|
+
Both root-scoped invocations precede their report with any active override, which pnpm declares in `pnpm-workspace.yaml`. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
|
|
579
|
+
|
|
580
|
+
A `pnpm.overrides` block left in the root `package.json` fails the command instead. pnpm reads no setting from that field as of pnpm 11, so the block pins nothing, while the upgrade tool keeps its own list of dependency fields and goes on rewriting the versions in it under `--write` — a block that looks maintained and governs nothing. Failing ahead of the tool is what keeps that write from happening, on `root:upgrade` as much as on `upgrade`. nmr supports pnpm 11 and later.
|
|
345
581
|
|
|
346
582
|
The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
|
|
347
583
|
|
|
@@ -378,29 +614,30 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
378
614
|
|
|
379
615
|
### Workspace scripts
|
|
380
616
|
|
|
381
|
-
| Command
|
|
382
|
-
|
|
|
383
|
-
| `build`
|
|
384
|
-
| `check`
|
|
385
|
-
| `check:strict`
|
|
386
|
-
| `clean`
|
|
387
|
-
| `compile`
|
|
388
|
-
| `fix`
|
|
389
|
-
| `fix:check`
|
|
390
|
-
| `fmt`
|
|
391
|
-
| `fmt:check`
|
|
392
|
-
| `lint`
|
|
393
|
-
| `lint:check`
|
|
394
|
-
| `lint:strict`
|
|
395
|
-
| `
|
|
396
|
-
| `test
|
|
397
|
-
| `test:
|
|
398
|
-
| `test:
|
|
399
|
-
| `test:
|
|
400
|
-
| `test:
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `
|
|
617
|
+
| Command | Runs |
|
|
618
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
619
|
+
| `build` | `compile` |
|
|
620
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
621
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
622
|
+
| `clean` | `nmr-clean` |
|
|
623
|
+
| `compile` | `nmr-compile` |
|
|
624
|
+
| `fix` | `lint`, `fmt` |
|
|
625
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
626
|
+
| `fmt` | `nmr-fmt --write` |
|
|
627
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
628
|
+
| `lint` | `eslint --fix .` |
|
|
629
|
+
| `lint:check` | `eslint .` |
|
|
630
|
+
| `lint:strict` | `strict-lint` |
|
|
631
|
+
| `report-catalog` | `nmr-report-catalog` |
|
|
632
|
+
| `test` | `pnpm exec vitest --project unit --project tool` |
|
|
633
|
+
| `test:all` | `pnpm exec vitest` |
|
|
634
|
+
| `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
|
|
635
|
+
| `test:tool` | `pnpm exec vitest --project tool` |
|
|
636
|
+
| `test:unit` | `pnpm exec vitest --project unit` |
|
|
637
|
+
| `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
|
|
638
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
639
|
+
| `upgrade` | `nmr-report-catalog && nmr-taze --include-locked` |
|
|
640
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
404
641
|
|
|
405
642
|
`fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
|
|
406
643
|
|
|
@@ -427,12 +664,12 @@ A run that collects no test files passes. That is what lets `nmr test:tool` fan
|
|
|
427
664
|
|
|
428
665
|
#### Build and CI
|
|
429
666
|
|
|
430
|
-
| Command | Runs
|
|
431
|
-
| --------- |
|
|
432
|
-
| `build` |
|
|
433
|
-
| `ci` | `build`, `check:strict`
|
|
434
|
-
| `clean` | `nmr-clean`
|
|
435
|
-
| `prepush` | `audit`, `ci`
|
|
667
|
+
| Command | Runs |
|
|
668
|
+
| --------- | ----------------------- |
|
|
669
|
+
| `build` | `-R build` |
|
|
670
|
+
| `ci` | `build`, `check:strict` |
|
|
671
|
+
| `clean` | `nmr-clean` |
|
|
672
|
+
| `prepush` | `audit`, `ci` |
|
|
436
673
|
|
|
437
674
|
`ci` is what a code-quality workflow runs; it leaves out the audit, which belongs in a workflow of its own. `prepush` is what a developer runs before pushing: both gates, audit first. The audit takes seconds and `ci` takes minutes, so a vulnerability surfaces before the long gate runs. It composes `ci` rather than restating its stages, so a stage added to `ci` joins the pre-push run too.
|
|
438
675
|
|
|
@@ -457,14 +694,14 @@ Neither is bound to a git hook. `prepush` is named for when you run it, not for
|
|
|
457
694
|
|
|
458
695
|
The same six names the workspace registry carries, so a command means the same thing from the monorepo root as from inside a package. Each fans out to root-level files and every workspace package.
|
|
459
696
|
|
|
460
|
-
| Command | Runs
|
|
461
|
-
| --------------- |
|
|
462
|
-
| `test` | `
|
|
463
|
-
| `test:all` | `
|
|
464
|
-
| `test:coverage` | `
|
|
465
|
-
| `test:tool` | `
|
|
466
|
-
| `test:unit` | `
|
|
467
|
-
| `test:watch` | `vitest --project unit --project tool --watch`
|
|
697
|
+
| Command | Runs |
|
|
698
|
+
| --------------- | ---------------------------------------------- |
|
|
699
|
+
| `test` | `root:test`, `-R test` |
|
|
700
|
+
| `test:all` | `root:test:all`, `-R test:all` |
|
|
701
|
+
| `test:coverage` | `root:test`, `-R test:coverage` |
|
|
702
|
+
| `test:tool` | `root:test:tool`, `-R test:tool` |
|
|
703
|
+
| `test:unit` | `root:test:unit`, `-R test:unit` |
|
|
704
|
+
| `test:watch` | `vitest --project unit --project tool --watch` |
|
|
468
705
|
|
|
469
706
|
`test:coverage` chains `root:test` rather than a `root:test:coverage`, because the root config reports no coverage of its own; packages cover their own sources.
|
|
470
707
|
|
|
@@ -472,9 +709,9 @@ The same six names the workspace registry carries, so a command means the same t
|
|
|
472
709
|
|
|
473
710
|
#### Typecheck
|
|
474
711
|
|
|
475
|
-
| Command | Runs
|
|
476
|
-
| ----------- |
|
|
477
|
-
| `typecheck` | `
|
|
712
|
+
| Command | Runs |
|
|
713
|
+
| ----------- | -------------------------------- |
|
|
714
|
+
| `typecheck` | `root:typecheck`, `-R typecheck` |
|
|
478
715
|
|
|
479
716
|
#### Lint
|
|
480
717
|
|
|
@@ -534,7 +771,7 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
534
771
|
| `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
|
|
535
772
|
| `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
|
|
536
773
|
| `root:typecheck` | `tsgo --noEmit` |
|
|
537
|
-
| `root:upgrade` | `nmr-taze --include-locked`
|
|
774
|
+
| `root:upgrade` | `nmr-report-overrides && nmr-taze --include-locked` |
|
|
538
775
|
|
|
539
776
|
#### Utilities
|
|
540
777
|
|
|
@@ -552,15 +789,17 @@ nmr [flags] <command> [args...]
|
|
|
552
789
|
|
|
553
790
|
Position determines ownership: flags before the command name are nmr's own, and everything after the command name is forwarded untouched to the resolved command.
|
|
554
791
|
|
|
555
|
-
| Flag | Description
|
|
556
|
-
| ------------------------ |
|
|
557
|
-
| `-F, --filter <pattern>` | Run command in matching packages
|
|
558
|
-
| `-R, --recursive` | Run command in all packages
|
|
559
|
-
| `-w, --workspace-root` | Use root scripts, running at the monorepo root
|
|
560
|
-
| `-q, --quiet` | Suppress
|
|
561
|
-
| `--
|
|
562
|
-
|
|
|
563
|
-
|
|
|
792
|
+
| Flag | Description | Default |
|
|
793
|
+
| ------------------------ | -------------------------------------------------------------- | ------- |
|
|
794
|
+
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
795
|
+
| `-R, --recursive` | Run command in all packages | — |
|
|
796
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
797
|
+
| `-q, --quiet` | Suppress command output, keeping nmr's verdicts | — |
|
|
798
|
+
| `--json` | Report one JSON object per command, withholding command output | — |
|
|
799
|
+
| `--log` | Print the recorded run instead of running it | — |
|
|
800
|
+
| `--no-cache` | Run even if this tree already passed; record the result | — |
|
|
801
|
+
| `-?, --help` | Show available commands | — |
|
|
802
|
+
| `-V, --version` | Show version number | — |
|
|
564
803
|
|
|
565
804
|
### Examples
|
|
566
805
|
|
|
@@ -586,9 +825,19 @@ nmr --workspace-root check # Run root check from a package dir
|
|
|
586
825
|
|
|
587
826
|
These commands are available as `nmr` subcommands and as standalone `nmr-`-prefixed binaries (for use in lifecycle hooks).
|
|
588
827
|
|
|
828
|
+
### `report-catalog`
|
|
829
|
+
|
|
830
|
+
Report the dependencies the current package takes from a [pnpm catalog](https://pnpm.io/catalogs), which a package-scoped `upgrade` cannot reach (see [dependency upgrades](#dependency-upgrades) for why). Each line names the dependency, its `catalog:` specifier, and the monorepo root a covering pass runs from. The workspace `upgrade` script runs it automatically, so no per-repo wiring is needed. The subcommand belongs to the workspace registry, so it runs from a package directory; the `nmr-report-catalog` bin reports nothing when run from the monorepo root, because a root-scoped pass reads the catalog itself.
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
nmr report-catalog
|
|
834
|
+
```
|
|
835
|
+
|
|
589
836
|
### `report-overrides`
|
|
590
837
|
|
|
591
|
-
Report any active
|
|
838
|
+
Report any active pnpm override, declared in the root `pnpm-workspace.yaml`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so no per-repo wiring is needed. Invoking it directly is useful for a one-off report.
|
|
839
|
+
|
|
840
|
+
A `pnpm.overrides` block in the root `package.json` fails the command, naming every entry it holds and where to move it — see [dependency upgrades](#dependency-upgrades) for why the block is inert. `pnpx codemod run pnpm-v10-to-v11` performs the move.
|
|
592
841
|
|
|
593
842
|
```bash
|
|
594
843
|
nmr report-overrides
|
|
@@ -667,7 +916,7 @@ nmr-fmt --write packages/nmr
|
|
|
667
916
|
|
|
668
917
|
### `nmr-taze`
|
|
669
918
|
|
|
670
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what `
|
|
919
|
+
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow.
|
|
671
920
|
|
|
672
921
|
Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
|
|
673
922
|
|
|
@@ -10,5 +10,7 @@ delivery: ambient
|
|
|
10
10
|
- Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
|
|
11
11
|
- Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
|
|
12
12
|
- `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs the audit first, then `ci`, so a vulnerability stops the run before the long gate. Neither installs or binds a git hook.
|
|
13
|
-
- A check that already passed on the current working tree skips, printing `⏭️ <command>: passed
|
|
14
|
-
-
|
|
13
|
+
- A check that already passed on the current working tree skips, printing `⏭️ <scope>: <command>: passed …` and, where the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
|
|
14
|
+
- `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it -- so output you need in full is a recording to read rather than a run to pay for again. It runs nothing, and it is held to the tree rather than to the terminal: a recording a piped run made prints for a reader at one. A composite prints its constituents' excerpts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. Nothing to show is said on one line and exits non-zero, naming which it is.
|
|
15
|
+
- Every command nmr runs reports one line -- `✅ <scope>: <command>: passed in 12.4s`, `❌ … failed in 1.2s (exit 1)`, `⏭️ … passed 4m ago on this tree`, or `⛔ … skipped, the override is empty` -- and a composite reports alongside every command it expands into. The lines print in every verbosity, so `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
|
|
16
|
+
- The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), and the check-result cache's key, the retention key a replayed excerpt is additionally held to, the retention ceiling `--log` prints within, its `checkCache` config, and every condition that disables it.
|