@williamthorsen/nmr 0.38.0 → 0.40.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/.meta/changelog.json +212 -4
- package/.readyup/kits/default.js +124 -104
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +108 -0
- package/README.md +41 -47
- package/agents/README.md +8 -8
- package/agents/guidance/rulebooks/nmr.md +8 -8
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/UserError.d.ts +1 -2
- package/dist/esm/check-cache.d.ts +61 -63
- package/dist/esm/check-cache.js +24 -24
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
- package/dist/esm/commands/build-output.d.ts +37 -38
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +13 -14
- package/dist/esm/commands/build.js +67 -65
- package/dist/esm/commands/clean.d.ts +6 -6
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.d.ts +8 -8
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +5 -7
- package/dist/esm/commands/report-catalog.js +10 -8
- package/dist/esm/commands/report-overrides.d.ts +0 -2
- package/dist/esm/commands/report-overrides.js +8 -8
- package/dist/esm/commands/taze.d.ts +9 -8
- package/dist/esm/config.d.ts +5 -5
- package/dist/esm/config.js +27 -27
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +5 -6
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/git-ignored-paths.d.ts +9 -0
- package/dist/esm/git-ignored-paths.js +10 -0
- package/dist/esm/glyphs.d.ts +3 -3
- package/dist/esm/help.d.ts +4 -4
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.d.ts +4 -5
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/config-path.d.ts +0 -2
- package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
- package/dist/esm/helpers/duration.d.ts +10 -11
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.d.ts +19 -19
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.d.ts +1 -1
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/hook-name.d.ts +2 -5
- package/dist/esm/helpers/package-json.d.ts +11 -10
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/readStringValues.d.ts +2 -3
- package/dist/esm/helpers/reportClosing.d.ts +8 -9
- package/dist/esm/helpers/transcript.d.ts +15 -15
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/output-style.d.ts +12 -13
- package/dist/esm/prettier.d.ts +2 -2
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +25 -20
- package/dist/esm/recording.js +28 -21
- package/dist/esm/replay-assembly.d.ts +10 -10
- package/dist/esm/report-format.d.ts +15 -20
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/reportCliFailure.d.ts +2 -3
- package/dist/esm/resolve-scripts.d.ts +4 -5
- package/dist/esm/resolveBinStyles.d.ts +4 -5
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +16 -20
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.d.ts +6 -6
- package/dist/esm/runCli.js +261 -195
- package/dist/esm/runner.d.ts +20 -20
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +51 -38
- package/dist/esm/steps.js +32 -27
- package/dist/esm/taze.d.ts +1 -1
- package/dist/esm/tests.d.ts +23 -18
- package/dist/esm/tests.js +21 -9
- package/dist/esm/tiers.d.ts +28 -24
- package/dist/esm/tiers.js +13 -9
- package/dist/esm/types.d.ts +5 -5
- package/dist/esm/verbosity.d.ts +23 -21
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +39 -32
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.d.ts +11 -11
- package/dist/esm/vitest-source-resolution.js +49 -47
- package/dist/esm/vitest.d.ts +27 -26
- package/dist/esm/vitest.js +90 -33
- package/dist/esm/workspace.d.ts +6 -22
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +57 -57
- package/docs/prettier.md +10 -10
- package/docs/reporting.md +48 -44
- package/docs/scripts.md +29 -29
- package/docs/upgrades.md +18 -18
- package/docs/utilities.md +13 -13
- package/docs/vitest.md +68 -66
- package/docs/workspace.md +27 -4
- package/package.json +6 -6
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/workspace.js
CHANGED
|
@@ -1,76 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
|
-
import { readStringValues } from "./helpers/readStringValues.js";
|
|
5
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
6
|
-
import { resolvePackageDirs } from "./helpers/workspace-patterns.js";
|
|
1
|
+
import { findMonorepoRoot as findMonorepoRootOrNothing, resolveWorkspace } from '@williamthorsen/nmr-core/workspace';
|
|
7
2
|
import { UserError } from "./UserError.js";
|
|
8
|
-
const PACKAGE_MANIFEST = 'package.json';
|
|
9
3
|
const WORKSPACE_MANIFEST = 'pnpm-workspace.yaml';
|
|
10
|
-
export
|
|
11
|
-
return existsSync(path.join(dir, WORKSPACE_MANIFEST));
|
|
12
|
-
}
|
|
4
|
+
export { isMonorepoRoot, readWorkspaceOverrides, readWorkspacePackageNames, resolveWorkspace, } from '@williamthorsen/nmr-core/workspace';
|
|
13
5
|
export function findMonorepoRoot(startDir) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return dir;
|
|
18
|
-
}
|
|
19
|
-
const parent = path.dirname(dir);
|
|
20
|
-
if (parent === dir) {
|
|
21
|
-
throw new UserError(`Could not find monorepo root: no ${WORKSPACE_MANIFEST} found in any parent directory`);
|
|
22
|
-
}
|
|
23
|
-
dir = parent;
|
|
6
|
+
const monorepoRoot = findMonorepoRootOrNothing(startDir);
|
|
7
|
+
if (monorepoRoot === undefined) {
|
|
8
|
+
throw new UserError(`Could not find monorepo root: no ${WORKSPACE_MANIFEST} found in any parent directory`);
|
|
24
9
|
}
|
|
10
|
+
return monorepoRoot;
|
|
25
11
|
}
|
|
26
12
|
export function getWorkspacePackageDirs(monorepoRoot) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
13
|
+
const resolution = resolveWorkspace(monorepoRoot);
|
|
14
|
+
if (resolution.kind === 'not-a-workspace') {
|
|
29
15
|
throw new UserError(`Not a monorepo root: no ${WORKSPACE_MANIFEST} in ${monorepoRoot}`);
|
|
30
16
|
}
|
|
31
|
-
|
|
32
|
-
const parsed = parse(content);
|
|
33
|
-
const packages = getPackagesFromParsedYaml(parsed);
|
|
34
|
-
if (!packages) {
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
return resolvePackageDirs(monorepoRoot, packages);
|
|
38
|
-
}
|
|
39
|
-
export function readWorkspaceOverrides(monorepoRoot) {
|
|
40
|
-
const workspaceFile = path.join(monorepoRoot, WORKSPACE_MANIFEST);
|
|
41
|
-
if (!existsSync(workspaceFile)) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
const parsed = parse(readFileSync(workspaceFile, 'utf8'));
|
|
45
|
-
if (!isObject(parsed)) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
const overrides = parsed['overrides'];
|
|
49
|
-
return isObject(overrides) ? readStringValues(overrides) : undefined;
|
|
50
|
-
}
|
|
51
|
-
export function readWorkspacePackageNames(packageDirs) {
|
|
52
|
-
const names = [];
|
|
53
|
-
for (const dir of packageDirs) {
|
|
54
|
-
let parsed;
|
|
55
|
-
try {
|
|
56
|
-
parsed = JSON.parse(readFileSync(path.join(dir, PACKAGE_MANIFEST), 'utf8'));
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (isObject(parsed) && typeof parsed['name'] === 'string') {
|
|
62
|
-
names.push(parsed['name']);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return names;
|
|
66
|
-
}
|
|
67
|
-
function getPackagesFromParsedYaml(parsed) {
|
|
68
|
-
if (!isObject(parsed))
|
|
69
|
-
return undefined;
|
|
70
|
-
const packages = parsed['packages'];
|
|
71
|
-
if (!Array.isArray(packages))
|
|
72
|
-
return undefined;
|
|
73
|
-
if (!packages.every((p) => typeof p === 'string'))
|
|
74
|
-
return undefined;
|
|
75
|
-
return packages;
|
|
17
|
+
return resolution.kind === 'packages' ? resolution.packageDirs : [];
|
|
76
18
|
}
|
package/docs/check-cache.md
CHANGED
|
@@ -4,9 +4,9 @@ How nmr skips a check that already passed on the same working tree, what decides
|
|
|
4
4
|
|
|
5
5
|
The [README](../README.md#check-result-cache) shows a skip in action.
|
|
6
6
|
|
|
7
|
-
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
|
|
7
|
+
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 at which it ran. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip is [reported](reporting.md#what-nmr-reports) in every verbosity.
|
|
8
8
|
|
|
9
|
-
The cache is a working-tree cache, not a build cache:
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
11
|
## What is cached
|
|
12
12
|
|
|
@@ -14,18 +14,18 @@ Cacheable by default: `check`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `
|
|
|
14
14
|
|
|
15
15
|
Everything else runs every time:
|
|
16
16
|
|
|
17
|
-
| Command | Why it is never skipped
|
|
18
|
-
| ------------------------------- |
|
|
19
|
-
| `audit`, `prepush` | Consult a vulnerability database that
|
|
20
|
-
| `build`, `compile` |
|
|
21
|
-
| `fix`, `fmt`, `lint`, `upgrade` | Rewrite the tree they were asked about, so a recorded pass would describe a tree that no longer exists.
|
|
22
|
-
| `test:all` | Reaches the `localhost` and `remote` tiers, which the environment supplies rather than the tree.
|
|
17
|
+
| Command | Why it is never skipped |
|
|
18
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `audit`, `prepush` | Consult a vulnerability database that changes independently of the tree. `prepush`'s `ci` constituent still skips while its `audit` runs every time. |
|
|
20
|
+
| `build`, `compile` | Have a cache of their own. |
|
|
21
|
+
| `fix`, `fmt`, `lint`, `upgrade` | Rewrite the tree that they were asked about, so a recorded pass would describe a tree that no longer exists. |
|
|
22
|
+
| `test:all` | Reaches the `localhost` and `remote` tiers, which the environment supplies rather than the tree. |
|
|
23
23
|
|
|
24
24
|
## The exit-status-only contract
|
|
25
25
|
|
|
26
|
-
**A cacheable name promises that its whole contribution is an exit status**, through its entire chain, `:pre` and `:post` hooks included. A hit skips that chain wholesale, so any file a cacheable command was relied on to produce
|
|
26
|
+
**A cacheable name promises that its whole contribution is an exit status**, through its entire chain, `:pre` and `:post` hooks included. A hit skips that chain wholesale, so any file that a cacheable command was relied on to produce will not appear.
|
|
27
27
|
|
|
28
|
-
`test:coverage` is cacheable on exactly this reading:
|
|
28
|
+
`test:coverage` is cacheable on exactly this reading: It contributes the pass, not the `coverage/` directory, which a skipped run leaves as whatever the last real run wrote. After a `test:coverage` that may have skipped, `coverage/` may be stale; `--no-cache` forces a fresh one.
|
|
29
29
|
|
|
30
30
|
Overriding a cacheable name, in `package.json` or in `workspaceScripts`, inherits its cacheability. An override that writes something anyone depends on belongs in `excludeCommands`.
|
|
31
31
|
|
|
@@ -33,44 +33,44 @@ Overriding a cacheable name, in `package.json` or in `workspaceScripts`, inherit
|
|
|
33
33
|
|
|
34
34
|
A hit requires all of these to match the run that recorded the pass:
|
|
35
35
|
|
|
36
|
-
- **The working tree's content**, hashed from the commit's tree object folded with the current content of every path git reports as changed or untracked. Timestamps are not content, so a `touch` that
|
|
37
|
-
- **The command string that would run**, hooks and all, and the scope it would run
|
|
36
|
+
- **The working tree's content**, hashed from the commit's tree object folded with the current content of every path that git reports as changed or untracked. Timestamps are not content, so a `touch` that does not rewrite any bytes leaves the hash alone.
|
|
37
|
+
- **The command string that would run**, hooks and all, and the scope in which it would run.
|
|
38
38
|
- **nmr's version**, the **Node version**, and the **platform and architecture**.
|
|
39
|
-
- **What pnpm has installed
|
|
40
|
-
- **`TZ`, `LANG`, `LC_ALL`, and `NODE_OPTIONS`.** This set is fixed, so two machines that differ only in
|
|
39
|
+
- **What pnpm has installed.** An install, a prune, or a lockfile change forces a re-run.
|
|
40
|
+
- **`TZ`, `LANG`, `LC_ALL`, and `NODE_OPTIONS`.** This set is fixed, so two machines that differ only in other environment variables still agree.
|
|
41
41
|
|
|
42
|
-
A hit additionally requires the build output of every package nmr's own build covers to be present, and to have been built from this tree.
|
|
42
|
+
A hit additionally requires the build output of every package that nmr's own build covers to be present, and to have been built from this tree. Because output is git-ignored, neither its removal nor its replacement changes the hash: Restoring a tree with `git stash` or `git checkout` does not restore any of the output with which that tree was built. nmr compares each covered package's recorded build digest against the one on disk, and treats a `dist` compiled from another tree as a miss that the following run repairs. A package that overrides `build` or `compile` emits on terms that nmr does not know and is exempt.
|
|
43
43
|
|
|
44
|
-
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started
|
|
44
|
+
A pass is recorded only when the command exits 0, only when the tree still matches the one against which the run started, only when every covered package's output is present and unchanged since the run started, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss). When output changes mid-run, whether another process rebuilt it or the chain built it itself, nmr cannot tell over which output the pass was earned: `nmr ci` is the one default command whose chain builds its own output, and it does not record a pass on a run whose build did work. The `nmr check:strict` inside that chain still records a pass, because it starts after the build.
|
|
45
45
|
|
|
46
46
|
## Out of contract
|
|
47
47
|
|
|
48
|
-
These change what a check concludes without
|
|
48
|
+
These change what a check concludes without changing the hash. When one applies, exclude the affected command:
|
|
49
49
|
|
|
50
|
-
- **Content filters.** `core.autocrlf` and `.gitattributes` filters mean the bytes on disk differ from the bytes git records; the hash follows what is on disk, but a checkout on another platform may not reproduce it.
|
|
51
|
-
- **Symlinked inputs.** A symlink is hashed by the path it names, not by the content
|
|
50
|
+
- **Content filters.** `core.autocrlf` and `.gitattributes` filters mean the bytes on disk differ from the bytes that git records; the hash follows what is on disk, but a checkout on another platform may not reproduce it.
|
|
51
|
+
- **Symlinked inputs.** A symlink is hashed by the path that it names, not by the content of its target.
|
|
52
52
|
- **Gitignored inputs.** Anything git does not report is invisible to the hash. The build-output probe is the one exception, and covers only nmr's own build.
|
|
53
|
-
- **`passWithNoTests`.** A suite that
|
|
53
|
+
- **`passWithNoTests`.** A suite that does not collect any files exits 0, and that pass is recorded like any other.
|
|
54
54
|
|
|
55
|
-
Committing an already-checked tree also
|
|
55
|
+
Committing an already-checked tree also changes the hash, because the commit's tree object is the base of the fold; the next run does the work again. The reverse holds and is useful: A rebase or an amended message that preserves content leaves the hash alone, as does checking out a branch whose tree is identical.
|
|
56
56
|
|
|
57
|
-
## When the
|
|
57
|
+
## When the cache does not apply
|
|
58
58
|
|
|
59
|
-
The gate never wrongly skips.
|
|
59
|
+
The gate never wrongly skips. When it cannot be sure, it does nothing and the command runs:
|
|
60
60
|
|
|
61
|
-
- Outside a git repository, when git fails, or when
|
|
61
|
+
- Outside a git repository, when git fails, or when `HEAD` does not resolve to a commit.
|
|
62
62
|
- When the repository declares or contains submodules, whose content the hash does not cover.
|
|
63
63
|
- When the monorepo root is not the git toplevel.
|
|
64
64
|
- When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
|
|
65
|
-
- When
|
|
66
|
-
- Under a `devBin` substitution, which runs a binary built from
|
|
65
|
+
- When the repository does not have a pnpm install to fingerprint.
|
|
66
|
+
- Under a `devBin` substitution, which runs a binary built from a location that the hash does not describe.
|
|
67
67
|
- 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.
|
|
68
68
|
|
|
69
|
-
`NMR_DEBUG=1` reports why a run did not skip and why the
|
|
69
|
+
`NMR_DEBUG=1` reports why a run did not skip and why the cache did not apply.
|
|
70
70
|
|
|
71
71
|
## Replaying a skipped run's output
|
|
72
72
|
|
|
73
|
-
A skip reports an excerpt of the run it recalls, marked as a recording rather than as this run's output:
|
|
73
|
+
A skip reports an excerpt of the run that it recalls, marked as a recording rather than as this run's output:
|
|
74
74
|
|
|
75
75
|
```console
|
|
76
76
|
$ nmr test
|
|
@@ -80,11 +80,11 @@ $ nmr test
|
|
|
80
80
|
⏩ nmr: test: passed 2m ago on this tree, saved ~12s — replayed: Test Files 6 passed (6) Tests 41 passed (41)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
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.
|
|
83
|
+
The excerpt is the last blank-line-delimited block of what the command wrote, which is the closing statement that 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. nmr does not parse anything or compute any figure: It replays bytes that 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 put its whole output in the cache.
|
|
84
84
|
|
|
85
|
-
The
|
|
85
|
+
The operation that records the pass also writes the excerpt and the whole transcript from which it is drawn. A pass that nmr declines to record because the tree or the build output changed leaves neither behind, and `nmr clean` clears both along with the passes to which they belong.
|
|
86
86
|
|
|
87
|
-
**
|
|
87
|
+
**nmr captures output at the command, and only when it can see that output.** A command whose stdout or stderr is a terminal writes to a stream that 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 passes its file 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 that it wraps: An excerpt is the command's own output or none.
|
|
88
88
|
|
|
89
89
|
**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:
|
|
90
90
|
|
|
@@ -93,19 +93,19 @@ $ nmr check
|
|
|
93
93
|
⏩ 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)
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
The assembly is flat:
|
|
96
|
+
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` rather than one opaque `check:strict` line. A delegate expands into the scopes to which it fans out, and a constituent that did not record an 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. `ci` replays what `check:strict` earned and nothing for `build`.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Every excerpt in the assembly was certified during a single run, on a single tree, because nmr assembles it when the composite records its pass rather than when it skips. `NMR_RUN_ID` makes that provable; see [reserved environment variables](#reserved-environment-variables).
|
|
99
99
|
|
|
100
100
|
**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.
|
|
101
101
|
|
|
102
|
-
**A replay is held to more than the pass is.** Retention
|
|
102
|
+
**A replay is held to more than the pass is.** Retention has its own key: the [pass key](#what-the-key-is-made-of) folded with the channel on which each output stream ran and with `CI`, `COLUMNS`, `FORCE_COLOR`, `NO_COLOR`, and `TERM`. `NMR_OUTPUT_STYLE` is in neither key, so a rich run and a plain one share a recorded pass and replay each other's excerpts as recorded. 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.
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Because the whole line, excerpt included, is held to the same [512-byte ceiling](reporting.md#what-nmr-reports) as every verdict, one write still emits it whole under concurrent fan-out. An assembly wide enough to overrun it is cut and marked with `…` rather than spread over several lines.
|
|
105
105
|
|
|
106
106
|
## Reading a retained run
|
|
107
107
|
|
|
108
|
-
The excerpt is a line;
|
|
108
|
+
The excerpt is a line; nmr also keeps the run's whole transcript. `nmr --log <command>` prints that transcript after a header that dates it:
|
|
109
109
|
|
|
110
110
|
```console
|
|
111
111
|
$ nmr --log test
|
|
@@ -118,43 +118,43 @@ $ pnpm exec vitest --project unit --project tool
|
|
|
118
118
|
Tests 41 passed (41)
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
The header
|
|
121
|
+
The header 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.
|
|
122
122
|
|
|
123
|
-
**Reading a recording runs nothing.**
|
|
123
|
+
**Reading a recording runs nothing.** nmr does not run any hook or print any verdict, and it does not record or restamp anything. `--no-cache` governs running rather than reading and is ignored.
|
|
124
124
|
|
|
125
|
-
**A recording is held to the tree, not to the terminal.** `--log` prints exactly what a skip would have recalled:
|
|
125
|
+
**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. `--log` does not consult the [retention key](#replaying-a-skipped-runs-output), so a recording made under a different terminal width, or through a pipe, still prints -- the flag exists to show a reader at a terminal what a piped run wrote.
|
|
126
126
|
|
|
127
|
-
**A composite prints its assembly.** Retaining nothing of its own, a composite prints the excerpts its constituents recorded, one attributed line each.
|
|
127
|
+
**A composite prints its assembly.** Retaining nothing of its own, a composite prints the excerpts that its constituents recorded, one attributed line each. To print the transcript of any one of them, run `--log` for that constituent.
|
|
128
128
|
|
|
129
|
-
**
|
|
129
|
+
**When nmr has nothing to show, it says so rather than printing a blank.** A refusal names which case applies -- the command is outside the [cacheable set](#what-is-cached), the [cache does not apply](#when-the-cache-does-not-apply), nothing has recorded a pass, the last pass was recorded under something this run does not share, or the pass did not retain any output, having printed none or written to a terminal -- and nmr exits non-zero, so that a caller can tell an empty `stdout` from a recording. A mismatch names the ingredient that changed, whether the tree, the chain, or a version, to keep a reader whose `git status` is clean from looking there.
|
|
130
130
|
|
|
131
131
|
```console
|
|
132
132
|
$ nmr --log test
|
|
133
|
-
🟠 nmr: test: no recording; the last pass was 3m ago, on a tree this is not
|
|
133
|
+
🟠 nmr: test: no recording; the last pass was 3m ago, on a tree that this is not
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
**`--log`
|
|
136
|
+
**`--log` applies to the scopes that `-F` and `-R` select.** nmr passes the flag to the delegate. Each scope prints its own recording. There a scope with nothing to show reports its gap and exits 0: Partial coverage is normal for a survey, and failing on the first gap would hide every scope that had something to show. A selection that does not include any scope is the other case and fails, as it does for a run: An empty selection is not a partial survey.
|
|
137
137
|
|
|
138
|
-
**Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes a verdict line is held
|
|
138
|
+
**Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes to which a verdict line is held. nmr keeps 128 KiB from each end of a transcript that overruns it, because a run's opening and its closing statement each contain what the other does not, and it marks the drop with a line that names the dropped bytes, rather than cutting silently.
|
|
139
139
|
|
|
140
140
|
## Bypassing and clearing
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Try these in order; the first is almost always the right one.
|
|
143
143
|
|
|
144
|
-
| Step | Effect
|
|
145
|
-
| -------------------------------------- |
|
|
146
|
-
| `nmr --no-cache <command>` | Runs the command, then records the fresh result.
|
|
147
|
-
| `NMR_NO_CACHE=1` | The same, for every nmr invocation in the shell.
|
|
148
|
-
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone.
|
|
149
|
-
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope.
|
|
144
|
+
| Step | Effect |
|
|
145
|
+
| -------------------------------------- | ---------------------------------------------------------------------------- |
|
|
146
|
+
| `nmr --no-cache <command>` | Runs the command, then records the fresh result. Applies to the whole chain. |
|
|
147
|
+
| `NMR_NO_CACHE=1` | The same, for every nmr invocation in the shell. |
|
|
148
|
+
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone. |
|
|
149
|
+
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope. |
|
|
150
150
|
|
|
151
|
-
`--no-cache` belongs before the command name. After it, it is an argument to the command rather than a flag to nmr; nmr
|
|
151
|
+
`--no-cache` belongs before the command name. After it, it is an argument to the command rather than a flag to nmr; nmr reports this rather than silently ignoring the bypass.
|
|
152
152
|
|
|
153
153
|
## Reserved environment variables
|
|
154
154
|
|
|
155
|
-
`NMR_TREE_SNAPSHOT` is nmr's own:
|
|
155
|
+
`NMR_TREE_SNAPSHOT` is nmr's own: It passes one observation of the tree from a top-level invocation down to the processes that it spawns, so a chain hashes the tree once rather than at every link. nmr trusts an inherited value only while `HEAD` still points to the same commit as when the observation was taken, which bounds a process that outlives the run that spawned it. Because that bound does not extend to a tree edited without committing, **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.
|
|
156
156
|
|
|
157
|
-
`NMR_RUN_ID` is nmr's own too:
|
|
157
|
+
`NMR_RUN_ID` is nmr's own too: It passes one run's identity from a top-level invocation down to the processes that it spawns, so that the excerpts recorded at every scope during one run are recognizable as that run's, and a composite can assemble from them. Whereas `NMR_TREE_SNAPSHOT` is bounded by `HEAD`, this one is bounded by the tree hash that each entry records. A process that outlives its run and carries the identity into a later one contributes nothing to that run's assemblies.
|
|
158
158
|
|
|
159
159
|
## Configuring
|
|
160
160
|
|
|
@@ -169,8 +169,8 @@ export default defineConfig({
|
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
`extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest
|
|
172
|
+
`extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest. nmr applies `excludeCommands` afterwards and excludes a name that appears in both. `enabled: false` turns the gate off entirely. The key is `checkCache`, in the monorepo-root config.
|
|
173
173
|
|
|
174
|
-
**A name in either list must resolve to a command**, whether through nmr's defaults, the repo config's script records, or a package's own `package.json` scripts. Both lists are read by name alone, so a misspelt entry would otherwise be inert:
|
|
174
|
+
**A name in either list must resolve to a command**, whether through nmr's defaults, the repo config's script records, or a package's own `package.json` scripts. Both lists are read by name alone, so a misspelt entry would otherwise be inert: The command that it meant to name would go on running, indistinguishable from one that cannot be cached at all. A name that resolves nowhere fails config load, naming the closest command if one exists. A hook name is rejected too, since a hook is never gated on its own -- it runs as part of the chain of the command that it wraps, and is skipped or run with it.
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
Because the test is resolvability rather than membership of the cacheable set, an `excludeCommands` entry stays valid after a release moves its name out of the defaults.
|
package/docs/prettier.md
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
The Prettier config that `@williamthorsen/nmr/prettier` publishes: what it formats, how it scopes the shell plugin, and how to customize and adopt it.
|
|
4
4
|
|
|
5
|
-
`prettier` is a peer dependency (`>=3.9.5 <4`), declared optional
|
|
5
|
+
`prettier` is a peer dependency (`>=3.9.5 <4`), declared optional; the consuming repo provides it. The shell and Dockerfile plugin is a dependency of nmr rather than a peer, so its version is pinned centrally and two repos cannot format the same script differently.
|
|
6
6
|
|
|
7
7
|
## What it formats
|
|
8
8
|
|
|
9
|
-
Beyond everything Prettier already handles, this config formats `.sh`, `.bash`, `.zsh`, and the other extensions and dotfile names of the Shell language, plus `Dockerfile` and `Containerfile`. `nmr fmt` therefore covers shell scripts
|
|
9
|
+
Beyond everything Prettier already handles, this config formats `.sh`, `.bash`, `.zsh`, and the other extensions and dotfile names of the Shell language, plus `Dockerfile` and `Containerfile`. `nmr fmt` therefore covers shell scripts without an extra script, glob, or workflow entry.
|
|
10
10
|
|
|
11
|
-
Shell output matches `shfmt` run
|
|
11
|
+
Shell output matches `shfmt` run without flags: `binaryNextLine`, `spaceRedirects`, and `switchCaseIndent` are pinned to shfmt's CLI defaults, which the underlying plugin inverts. Adoption does not produce any diff in a repo whose scripts shfmt already formatted.
|
|
12
12
|
|
|
13
|
-
Code fences inside Markdown are left alone, in every language rather than shell alone. Registering the shell plugin also
|
|
13
|
+
Code fences inside Markdown are left alone, in every language rather than shell alone. Registering the shell plugin also sends a fence tagged `bash` to shfmt, which reads a documented command's angle-bracket placeholders as valid redirections: `cmd --type <type> --harness claude` is reprinted as `cmd --type claude <type >--harness`, which still runs but no longer does what it documents. It cannot be narrowed back to shell, because Prettier matches a fence tag against the same `extensions` that its file inference reads, so the `.bash` extension that sends a script to the shell parser also matches the tag `bash`.
|
|
14
14
|
|
|
15
15
|
## Language scoping
|
|
16
16
|
|
|
17
|
-
`prettier-plugin-sh`
|
|
17
|
+
`prettier-plugin-sh` assigns 20 further file types to the same shell parser, among them `.gitignore`, `.env`, `.csh`, `.nu`, `.properties`, `.ics`, `.vcf`, `CODEOWNERS`, and `hosts`. Since `nmr fmt` passes git's whole file list to Prettier, registering the plugin unmodified would make every one of them formattable, and the shell parser either fails on them or silently rewrites them. A `.gitignore` pattern such as `a(b)c` fails to parse, and an unquoted `&` in a `.env` or `.properties` value is split across two lines.
|
|
18
18
|
|
|
19
|
-
This config registers a narrowed plugin, so Prettier infers a parser for the Shell and Dockerfile languages alone. `.flaskenv`, `gradlew`, and `mvnw` are dropped from Shell as well:
|
|
19
|
+
This config registers a narrowed plugin, so Prettier infers a parser for the Shell and Dockerfile languages alone. `.flaskenv`, `gradlew`, and `mvnw` are dropped from Shell as well: The first contains dotenv content that the plugin misidentifies as shell, and the other two are vendored wrappers that their generators would overwrite.
|
|
20
20
|
|
|
21
|
-
The parsers themselves stay registered
|
|
21
|
+
The parsers themselves stay registered. A repo that wants a dropped language back assigns it explicitly:
|
|
22
22
|
|
|
23
23
|
```js
|
|
24
24
|
export default definePrettierConfig({
|
|
@@ -38,10 +38,10 @@ export default definePrettierConfig({
|
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
`additionalPlugins` and `additionalOverrides` append to what the config declares, so a later override wins over nmr's own and
|
|
41
|
+
`additionalPlugins` and `additionalOverrides` append to what the config declares, so a later override wins over nmr's own, and a repo cannot use either option to drop shell support. Passing `plugins` or `overrides` throws, and both are typed `never`: Replacement is inexpressible rather than merely discouraged, because a silently ignored key would not produce any diff to notice.
|
|
42
42
|
|
|
43
43
|
## Adoption caveats
|
|
44
44
|
|
|
45
|
-
- **Indentation follows `.editorconfig`**, as it did under shfmt: Prettier maps `indent_style` and `indent_size` to `useTabs` and `tabWidth`, and the plugin honours both.
|
|
46
|
-
- **Markdown fences stop being tidied.** The carve-out that protects documented shell commands
|
|
45
|
+
- **Indentation follows `.editorconfig`**, as it did under shfmt: Prettier maps `indent_style` and `indent_size` to `useTabs` and `tabWidth`, and the plugin honours both. Without an `.editorconfig`, the two disagree: shfmt indents shell with tabs, Prettier with two spaces. Add `indent_style = tab` under `[*.sh]` to keep tabs.
|
|
46
|
+
- **Markdown fences stop being tidied.** The carve-out that protects documented shell commands leaves the `ts`, `json`, and `yaml` fences unformatted too. Fences already formatted stay as they are, so adoption does not change them; new ones are simply left as written. A repo whose docs do not contain any shell can restore fence formatting for the Markdown paths that it actually uses: `additionalOverrides: [{ files: ['*.md'], options: { embeddedLanguageFormatting: 'auto' } }]` restores `.md` alone, and the carve-out still covers every other path that Prettier reads as Markdown.
|
|
47
47
|
- **Adopting the house options reformats the repo.** `singleQuote`, `trailingComma`, and the rest apply on the first run; review that diff separately from the shell one.
|
package/docs/reporting.md
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# What nmr reports
|
|
2
2
|
|
|
3
|
-
How nmr reports each command it runs, in prose or as JSON, and how loudly it relays the output of those commands.
|
|
3
|
+
How nmr reports each command that it runs, in prose or as JSON, and how loudly it relays the output of those commands.
|
|
4
4
|
|
|
5
5
|
The verdict line and its four outcomes are shown in the [README](../README.md#what-nmr-reports).
|
|
6
6
|
|
|
7
|
-
A recalled pass also
|
|
7
|
+
A recalled pass also includes an excerpt of the run that it recalls, if that run left one; see [Replaying a skipped run's output](check-cache.md#replaying-a-skipped-runs-output).
|
|
8
8
|
|
|
9
|
-
The scope is the directory the command's registry belongs
|
|
9
|
+
The scope is the directory to which the command's registry belongs: a package's own directory, or the monorepo root. Durations and ages truncate and show at most two units, so a check that passed 119.9 seconds ago reports `1m 59s ago` and never `2m ago`.
|
|
10
10
|
|
|
11
|
-
Verdicts nest. A composite reports, and so does every command it expands
|
|
11
|
+
Verdicts nest. A composite reports, and so does every command into which it expands; a `:pre` or `:post` hook reports nothing of its own, since the command that 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 to which it fans out reports instead. A cold repo-wide `nmr ci` in this package's own monorepo uses 26 verdict lines to report a run that prints roughly 250.
|
|
12
12
|
|
|
13
|
-
**A fan-out that
|
|
13
|
+
**A fan-out that does not select any scope fails.** A `-F` pattern matches a package's manifest `name` rather than its directory name, so a plausible directory name selects nothing, and pnpm exits 0 over an empty selection as it does over a passing one. nmr asks pnpm what the pattern selects and refuses the invocation when the answer is nothing, naming the pattern and, when the pattern contains a name, the names that it could have named. For a pattern of another form, the refusal states the rule to which that form answers: a directory (`./dir`, `/dir`, `{dir}`), an exclusion (`!`), or a changed-since selector (`[<ref>]`), none of which a workspace name repairs. A pattern that pnpm rejects rather than resolves, such as a bad git ref in a `[since]` selector, is left to pnpm to report. A `-R` typed on the invocation is refused on the same ground when the workspace does not declare any package at all, which is its only empty selection. That condition takes precedence over the pattern rules above: When the workspace does not contain any package, a `-F` pattern could not have matched one, so the refusal reports the workspace rather than the shape of the pattern.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**nmr reports a workspace that does not contain any package as one of four conditions.** `pnpm-workspace.yaml` may lack a positive pattern, declare patterns that do not match any directory containing a `package.json`, declare `!` exclusions that remove every directory matched by the positive patterns, or not contain valid YAML at all. Each refusal names its own condition and states a remedy, and each quotes the `packages` list that the manifest declares, apart from the last, whose manifest nmr could not read. The second is the likeliest, because nmr [counts a directory as a package](workspace.md) only if it contains `package.json` and recognizes neither `package.yaml` nor `package.json5`.
|
|
16
|
+
|
|
17
|
+
**A `-R` that nmr composed into a script is not a fan-out that the caller asked for.** In a workspace that does not contain any package, such a step -- in nmr's own default script or in a repo override alike -- is dropped from the chain, the steps beside it run, and the dropped step reports nothing of its own, as the rule above gives a `-R` invocation whose scopes report instead. When the drop leaves the command without any step at all, as it does the root `build`, the command reports a no-op naming the workspace. A repo that does not contain any workspace package therefore runs the default root commands without any entry in `.config/nmr.config.ts`.
|
|
18
|
+
|
|
19
|
+
A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` that 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.
|
|
16
20
|
|
|
17
21
|
## Reporting for a machine
|
|
18
22
|
|
|
19
|
-
`--json` renders each verdict as a JSON object instead of a line of prose
|
|
23
|
+
`--json` renders each verdict as a JSON object instead of a line of prose: one object per line, newline-delimited, without an enclosing array or separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line that it already emitted parseable, which an enclosing array would not.
|
|
20
24
|
|
|
21
25
|
```console
|
|
22
26
|
$ nmr --json check
|
|
@@ -25,80 +29,80 @@ $ nmr --json check
|
|
|
25
29
|
{"command":"check","scope":"nmr-core","outcome":"passed","durationMs":31204}
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
Every object
|
|
32
|
+
Every object contains `command`, `scope`, and `outcome`, and then the fields that only that outcome includes:
|
|
29
33
|
|
|
30
|
-
| `outcome` | Also
|
|
31
|
-
| ---------- |
|
|
32
|
-
| `passed` | `durationMs`
|
|
33
|
-
| `failed` | `durationMs`, `exitCode`
|
|
34
|
-
| `recalled` | `ageMs`, `savedMs`, and `replay`
|
|
35
|
-
| `no-op` | `reason`,
|
|
34
|
+
| `outcome` | Also contains |
|
|
35
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `passed` | `durationMs` |
|
|
37
|
+
| `failed` | `durationMs`, `exitCode` |
|
|
38
|
+
| `recalled` | `ageMs`, `savedMs`, and `replay` when the skip has excerpts to [replay](check-cache.md#replaying-a-skipped-runs-output) |
|
|
39
|
+
| `no-op` | `reason`, one of `empty-override`, `empty-workspace`, or `noop-override` |
|
|
36
40
|
|
|
37
|
-
Both renderings are produced from one record, so neither reports what the other does not.
|
|
41
|
+
Both renderings are produced from one record, so neither reports what the other does not. When the prose line makes a presentation decision that the record does not (a saving too small to be worth a clause, for example), the object contains the fact and leaves the decision to whoever reads it.
|
|
38
42
|
|
|
39
|
-
**An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) a prose line is held
|
|
43
|
+
**An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) to which a prose line is held. Packages running concurrently under `pnpm --recursive` cannot interleave within one. Because nmr cuts inside the record's text rather than across its structure, the line still parses. nmr fits a record that overruns by rungs, each dropping what a reader can spare more easily than what the rung below it drops:
|
|
40
44
|
|
|
41
|
-
1. **The excerpts are shortened toward one another**, so an assembly's constituents
|
|
42
|
-
2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry
|
|
45
|
+
1. **The excerpts are shortened toward one another**, so an assembly's constituents shrink together rather than the first ones being emptied to leave the last whole. nmr does not cut any excerpt below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
|
|
46
|
+
2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry without an `excerpt` key is one whose text did not fit.
|
|
43
47
|
3. **The trailing constituents go**, as the prose line drops its own tail, and the `replay` array itself once none fit.
|
|
44
|
-
4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself:
|
|
48
|
+
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, whereas a marked cut damages only its own.
|
|
45
49
|
|
|
46
|
-
A package-scoped `check`
|
|
50
|
+
A package-scoped `check` has 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.
|
|
47
51
|
|
|
48
|
-
**The mode implies quiet.** The commands' own output is withheld, so stdout
|
|
52
|
+
**The mode implies quiet.** The commands' own output is withheld, so stdout contains the objects alone, without a prose verdict or an override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. On a failure, nmr still prints the failing command's output on stderr, as under any quiet run. Because both modes leave a command on the same channels, a machine-readable run and a quiet one share a [retention key](check-cache.md#replaying-a-skipped-runs-output): Each replays what the other recorded.
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
Because the format is nmr's own reporting, and nmr does not tell the commands that it runs about it, the format is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor prevents a replay.
|
|
51
55
|
|
|
52
|
-
`--log`, `--help`, and `--version` are unaffected:
|
|
56
|
+
`--log`, `--help`, and `--version` are unaffected: None of them reports a verdict, and what `--log` prints is a tool's own transcript, which cannot be rendered as a record.
|
|
53
57
|
|
|
54
58
|
## Reserved environment variables
|
|
55
59
|
|
|
56
|
-
`NMR_COMMAND_VERBOSITY` is nmr's own, and
|
|
60
|
+
`NMR_COMMAND_VERBOSITY` is nmr's own, and sets how loudly a run reports the output of the commands that it runs. Its values are `full` and `quiet`; nmr reports any other non-empty value and exits 1, rather than falling back to a mode that nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command that it runs rather than of everything below it, so the failing command's output still prints on a failure. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message that a quiet run withholds.
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
`nmr -q <command>` sets it for the run and outranks an inherited value. Both values are spelled out so that either direction is expressible: Export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
|
|
59
63
|
|
|
60
|
-
`NMR_OUTPUT_STYLE` is nmr's own as well, and
|
|
64
|
+
`NMR_OUTPUT_STYLE` is nmr's own as well, and sets the style in which nmr renders every marker that it prints; see [output style](#output-style) below.
|
|
61
65
|
|
|
62
|
-
`NMR_REPORT_FORMAT` is nmr's own as well, and
|
|
66
|
+
`NMR_REPORT_FORMAT` is nmr's own as well, and sets how nmr renders its own verdicts. Its values are `text` and `json`; nmr reports any other non-empty value 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 that 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. The format has two sources, then, whereas a verbosity has four: It does not have a repo-level default or harness detection, because detection would hand every agent JSON in place of the prose that the [bundled guidance](../README.md#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects contain.
|
|
63
67
|
|
|
64
68
|
### Output style
|
|
65
69
|
|
|
66
|
-
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error
|
|
70
|
+
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code 1, whichever source named it. The flag outranks the variable, and both accept `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
|
|
67
71
|
|
|
68
|
-
Detection decides stdout and stderr separately. A stream is plain when `CI` is set to anything other than an empty string or `false`, when the stream is not a terminal, or when `TERM` is `linux`; otherwise it is rich. `--json` output
|
|
72
|
+
Detection decides stdout and stderr separately. A stream is plain when `CI` is set to anything other than an empty string or `false`, when the stream is not a terminal, or when `TERM` is `linux`; otherwise it is rich. `--json` output does not contain a marker in either style.
|
|
69
73
|
|
|
70
|
-
**The flag belongs to `nmr`.** A standalone bin -- `nmr-clean`, `nmr-compile`, `nmr-report-catalog`, `nmr-report-overrides`, `nmr-ensure-prepublish-hooks` -- takes the variable alone, and passing it the flag
|
|
74
|
+
**The flag belongs to `nmr`.** A standalone bin -- `nmr-clean`, `nmr-compile`, `nmr-report-catalog`, `nmr-report-overrides`, `nmr-ensure-prepublish-hooks` -- takes the variable alone, and passing it the flag produces that bin's unknown-option error. `nmr <command>` exports the style that it resolved, so a bin run through nmr renders as nmr does.
|
|
71
75
|
|
|
72
|
-
**A nested run follows the one that spawned it.**
|
|
76
|
+
**A nested run follows the one that spawned it.** Because nmr passes the resolved style, never `auto`, down as `NMR_OUTPUT_STYLE`, a child whose own stdout is a pipe still renders rich when the parent resolved rich. The exported style is the one to which stdout resolved, and stdout receives the verdicts and nearly every other line; a parent at a terminal whose stderr alone is redirected therefore hands its children `rich`.
|
|
73
77
|
|
|
74
|
-
The style is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the [retention key](check-cache.md#replaying-a-skipped-runs-output) alike, as the verbosity is:
|
|
78
|
+
The style is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the [retention key](check-cache.md#replaying-a-skipped-runs-output) alike, as the verbosity is: It changes how a run renders and never what a command concludes. Selecting it neither invalidates a recorded pass nor prevents a replay, and an excerpt recorded rich replays as recorded in a plain run.
|
|
75
79
|
|
|
76
80
|
### Where a verbosity comes from
|
|
77
81
|
|
|
78
|
-
Four sources, in precedence order. Each is read once, at the top of a chain: the resolved value
|
|
82
|
+
Four sources, in precedence order. Each is read once, at the top of a chain: nmr passes the resolved value down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
|
|
79
83
|
|
|
80
|
-
| Source
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
| `NMR_COMMAND_VERBOSITY`
|
|
84
|
-
| `output.commandVerbosity` in the monorepo-root config
|
|
85
|
-
| An agent harness nmr recognizes in the environment
|
|
84
|
+
| Source | Set it when |
|
|
85
|
+
| ------------------------------------------------------- | ---------------------------------------------------------- |
|
|
86
|
+
| `nmr -q <command>` | One invocation should be quiet. |
|
|
87
|
+
| `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
|
|
88
|
+
| `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
|
|
89
|
+
| An agent harness that nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
Below the last source, a run is `full`, which is what an unrecognized harness gets: Detection adds quiet when it fires and changes nothing when it does not.
|
|
88
92
|
|
|
89
|
-
Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The
|
|
93
|
+
Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The bundled set is `CLAUDECODE`, `ROVODEV_CLI`, and `ROVO_CLI`. **Expect that set to change.** The names belong to ecosystems that nmr does not control and that rename them, and a rename fails silently here, since the symptom is a return to loud output rather than an error. A repo adds a name with `output.extraAgentEnvVars` (for a harness unknown to nmr, or one whose marker was renamed after the installed version was released) without waiting for a release. A repo that does not want any detection at all configures `output.commandVerbosity: 'full'`, which outranks it.
|
|
90
94
|
|
|
91
95
|
## Configuring
|
|
92
96
|
|
|
93
97
|
```ts
|
|
94
98
|
export default defineConfig({
|
|
95
99
|
output: {
|
|
96
|
-
// The verbosity a run takes when neither `-q` nor the environment named one.
|
|
100
|
+
// The verbosity that a run takes when neither `-q` nor the environment named one.
|
|
97
101
|
commandVerbosity: 'quiet',
|
|
98
|
-
// Markers of a harness the
|
|
102
|
+
// Markers of a harness that the bundled set does not name, added to it rather than replacing it.
|
|
99
103
|
extraAgentEnvVars: ['MY_HARNESS'],
|
|
100
104
|
},
|
|
101
105
|
});
|
|
102
106
|
```
|
|
103
107
|
|
|
104
|
-
`output` is a monorepo-root key, and both of its fields
|
|
108
|
+
`output` is a monorepo-root key, and both of its fields are inputs to the [verbosity ladder](#where-a-verbosity-comes-from).
|