@williamthorsen/nmr 0.46.2 → 0.48.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 +85 -0
- package/.readyup/kits/default.js +11 -10
- package/.readyup/manifest.json +53 -8
- package/CHANGELOG.md +24 -0
- package/README.md +29 -3
- package/agents/guidance/rulebooks/nmr.md +3 -1
- package/dist/esm/bin-path.js +4 -3
- package/dist/esm/commands/fmt.js +1 -1
- package/dist/esm/help.js +5 -0
- package/dist/esm/helpers/findClosestName.js +4 -3
- package/dist/esm/helpers/quoteForShell.d.ts +5 -0
- package/dist/esm/helpers/quoteForShell.js +3 -0
- package/dist/esm/machine-lock.d.ts +64 -0
- package/dist/esm/machine-lock.js +229 -0
- package/dist/esm/remote-run.d.ts +136 -0
- package/dist/esm/remote-run.js +251 -0
- package/dist/esm/runCli.js +247 -70
- package/dist/esm/verdict.d.ts +2 -0
- package/dist/esm/verdict.js +3 -2
- package/dist/esm/vitest-max-workers.d.ts +11 -0
- package/dist/esm/vitest-max-workers.js +35 -0
- package/dist/esm/vitest-source-resolution.js +2 -3
- package/dist/esm/vitest.js +10 -8
- package/docs/check-cache.md +2 -2
- package/docs/delegate.md +61 -0
- package/docs/machine-lock.md +62 -0
- package/docs/reporting.md +1 -1
- package/docs/vitest.md +17 -3
- package/package.json +4 -4
package/.meta/changelog.json
CHANGED
|
@@ -1,4 +1,89 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.48.0",
|
|
4
|
+
"date": "2026-10-17",
|
|
5
|
+
"sections": [
|
|
6
|
+
{
|
|
7
|
+
"title": "🎉 Features",
|
|
8
|
+
"audience": "all",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"description": "Adds the `--delegate <host>` flag to `nmr`, which runs a command whose result is its exit status, such as `ci`, `check`, or a `test` command, on that host over SSH against the local working tree with its uncommitted and untracked files, and exits 255 when the delegation fails outside the command. (#1058)",
|
|
12
|
+
"hash": "dbd1714d65a833a258ebcc859d6aed130a3d9910",
|
|
13
|
+
"entry": 1
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"description": "Adds the `NMR_DELEGATE` environment variable, which names the host on which `nmr` runs every command that can be delegated, and the `--no-delegate` flag, which runs a command on the local machine whatever `NMR_DELEGATE` names. (#1058)",
|
|
17
|
+
"hash": "dbd1714d65a833a258ebcc859d6aed130a3d9910",
|
|
18
|
+
"entry": 2
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"description": "Adds the host to the verdict for a delegated command, as `passed on <host>` in the line and as a `host` field in the `--json` object. (#1058)",
|
|
22
|
+
"hash": "dbd1714d65a833a258ebcc859d6aed130a3d9910",
|
|
23
|
+
"entry": 3
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"title": "📦 Dependencies",
|
|
29
|
+
"audience": "dev",
|
|
30
|
+
"items": [
|
|
31
|
+
{
|
|
32
|
+
"description": "Upgrades `@williamthorsen/eslint-config-typescript` from 18.0.0 to 19.0.0 and rewrites the code in `build-info`, `nmr-core`, `nmr`, and `release-kit` that its new and widened rules report, without changing behavior. (#1047)",
|
|
33
|
+
"hash": "610ee159fdc429681b1078b500a064c5df296724",
|
|
34
|
+
"entry": 1
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"description": "Upgrades `readyup` from 0.40.0 to 0.41.0 and recompiles the kits in `nmr`, `release-kit`, and `v11y-check`, whose manifests now record each checklist's check names under `checklistChecks`. (#1047)",
|
|
38
|
+
"hash": "610ee159fdc429681b1078b500a064c5df296724",
|
|
39
|
+
"entry": 2
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"description": "Upgrades `pnpm` to 12.11.2, `@types/node` to 24.19.2, `lefthook` to 2.2.1, `@williamthorsen/strict-lint` to 14.2.0, `@williamthorsen/tsconfig` to 0.11.0, `smol-toml` to 1.9.1, and `vite` to 8.3.4. (#1047)",
|
|
43
|
+
"hash": "610ee159fdc429681b1078b500a064c5df296724",
|
|
44
|
+
"entry": 3
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.47.0",
|
|
52
|
+
"date": "2026-10-15",
|
|
53
|
+
"sections": [
|
|
54
|
+
{
|
|
55
|
+
"title": "🎉 Features",
|
|
56
|
+
"audience": "all",
|
|
57
|
+
"items": [
|
|
58
|
+
{
|
|
59
|
+
"description": "Sets `maxWorkers` on every project built by `defineVitestConfig` and `defineRootVitestConfig` to one worker per 4 GiB of total memory, bounded by one fewer than the core count, in place of Vitest's default of one fewer than the core count, which does not take memory into account; machines with 2 or fewer cores keep 3 workers. (#1044)",
|
|
60
|
+
"hash": "8c761e2b054daf91ba362d3f3e878329d35d2eb4",
|
|
61
|
+
"entry": 1
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"description": "Applies the `--maxWorkers` (or `--max-workers`) flag to every project, which Vitest does not do itself, making the flag override both the derived count and a `maxWorkers` set through the `project` or `tiers` seam, while the `VITEST_MAX_WORKERS` environment variable still overrides the flag. (#1044)",
|
|
65
|
+
"hash": "8c761e2b054daf91ba362d3f3e878329d35d2eb4",
|
|
66
|
+
"entry": 2
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"description": "Adds a machine lock that makes a heavy command (`check`, `ci`, and every `test` command except `test:watch`) wait, printing `nmr: waiting for …`, until the heavy command already running on the machine finishes; a nested command and a `-R` or `-F` fan-out take the lock once, and a cached pass does not wait. (#1045)",
|
|
70
|
+
"hash": "2797c36e73b431c848a5819909aae7f3de6e0af6",
|
|
71
|
+
"entry": 1
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"description": "Adds the `--no-lock` flag and the `NMR_NO_LOCK=1` environment variable, which run a command and the commands nested in it without waiting for the machine lock, for example on a self-hosted CI machine that runs several runners in parallel. (#1045)",
|
|
75
|
+
"hash": "2797c36e73b431c848a5819909aae7f3de6e0af6",
|
|
76
|
+
"entry": 2
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"description": "Extends the `nmr` rulebook to state that a run printing `nmr: waiting for …` is waiting for the machine lock rather than stuck, and that an agent must neither kill it nor rerun it with `--no-lock`. (#1045)",
|
|
80
|
+
"hash": "2797c36e73b431c848a5819909aae7f3de6e0af6",
|
|
81
|
+
"entry": 3
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
2
87
|
{
|
|
3
88
|
"version": "0.46.2",
|
|
4
89
|
"date": "2026-10-14",
|
package/.readyup/kits/default.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @noformat -- @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.41.0";
|
|
4
4
|
import { createRequire as __rdyCreateRequire } from 'node:module';
|
|
5
5
|
const require = __rdyCreateRequire(import.meta.url);
|
|
6
6
|
|
|
@@ -7442,10 +7442,11 @@ function findClosestName(name, candidates) {
|
|
|
7442
7442
|
let shortestDistance = Infinity;
|
|
7443
7443
|
for (const candidate of candidates) {
|
|
7444
7444
|
const distance = measureDistance(name, candidate);
|
|
7445
|
-
if (distance
|
|
7446
|
-
|
|
7447
|
-
closestName = candidate;
|
|
7445
|
+
if (distance >= shortestDistance) {
|
|
7446
|
+
continue;
|
|
7448
7447
|
}
|
|
7448
|
+
shortestDistance = distance;
|
|
7449
|
+
closestName = candidate;
|
|
7449
7450
|
}
|
|
7450
7451
|
return shortestDistance <= ceiling ? closestName : void 0;
|
|
7451
7452
|
}
|
|
@@ -8510,7 +8511,7 @@ var default_default = defineRdyKit({
|
|
|
8510
8511
|
{
|
|
8511
8512
|
name: ".config/nmr.config.ts uses defineConfig",
|
|
8512
8513
|
severity: "recommend",
|
|
8513
|
-
skip: () => !fileExists(".config/nmr.config.ts")
|
|
8514
|
+
skip: () => !fileExists(".config/nmr.config.ts") && "no nmr config file",
|
|
8514
8515
|
check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
|
|
8515
8516
|
fix: "Wrap the config export with defineConfig() from @williamthorsen/nmr/config for type safety"
|
|
8516
8517
|
},
|
|
@@ -8521,7 +8522,7 @@ var default_default = defineRdyKit({
|
|
|
8521
8522
|
return `eslint >= ${MIN_ESLINT_VERSION}`;
|
|
8522
8523
|
},
|
|
8523
8524
|
severity: "error",
|
|
8524
|
-
skip: () => !hasDevDependency("eslint")
|
|
8525
|
+
skip: () => !hasDevDependency("eslint") && "eslint not installed",
|
|
8525
8526
|
check: hasSupportedEslintVersion,
|
|
8526
8527
|
get fix() {
|
|
8527
8528
|
return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION}. Earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
|
|
@@ -8532,7 +8533,7 @@ var default_default = defineRdyKit({
|
|
|
8532
8533
|
return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
|
|
8533
8534
|
},
|
|
8534
8535
|
severity: "error",
|
|
8535
|
-
skip: () => !hasDevDependency("@williamthorsen/strict-lint")
|
|
8536
|
+
skip: () => !hasDevDependency("@williamthorsen/strict-lint") && "strict-lint not installed",
|
|
8536
8537
|
check: hasSupportedStrictLintVersion,
|
|
8537
8538
|
get fix() {
|
|
8538
8539
|
return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION}. Earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
|
|
@@ -8939,7 +8940,7 @@ function formatPaths(paths) {
|
|
|
8939
8940
|
${paths.map((path7) => ` ${path7}`).join("\n")}`;
|
|
8940
8941
|
}
|
|
8941
8942
|
function getMinVersion() {
|
|
8942
|
-
const pickedFields = { "version": "0.
|
|
8943
|
+
const pickedFields = { "version": "0.48.0" };
|
|
8943
8944
|
if (typeof pickedFields["version"] !== "string") {
|
|
8944
8945
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
8945
8946
|
}
|
|
@@ -9133,7 +9134,7 @@ function testSuiteGatesTestFileConventions(cwd = process.cwd()) {
|
|
|
9133
9134
|
}
|
|
9134
9135
|
function describeConventionsGuardSkip() {
|
|
9135
9136
|
const guard = findConventionsGuard(process.cwd());
|
|
9136
|
-
return guard
|
|
9137
|
+
return guard !== void 0 && `reported by checkTestFileConventions in ${guard}`;
|
|
9137
9138
|
}
|
|
9138
9139
|
function findConventionsGuard(cwd) {
|
|
9139
9140
|
return findTestFiles(cwd).find(
|
|
@@ -9170,7 +9171,7 @@ var NEVER_INSTALLED_PROGRAMS = /* @__PURE__ */ new Set(["corepack", "gh"]);
|
|
|
9170
9171
|
var statusByCwd = /* @__PURE__ */ new Map();
|
|
9171
9172
|
async function describeSelfHostedRunnerSkip(cwd = process.cwd(), readFacts) {
|
|
9172
9173
|
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
9173
|
-
return "skip" in status
|
|
9174
|
+
return "skip" in status && status.skip;
|
|
9174
9175
|
}
|
|
9175
9176
|
function findSelfHostedRunnerFailures(facts, cwd) {
|
|
9176
9177
|
const failures = [];
|
package/.readyup/manifest.json
CHANGED
|
@@ -19,6 +19,51 @@
|
|
|
19
19
|
"wrap-ansi": "10.0.2",
|
|
20
20
|
"yaml": "2.9.1"
|
|
21
21
|
},
|
|
22
|
+
"checklistChecks": [
|
|
23
|
+
{
|
|
24
|
+
"checklist": "nmr",
|
|
25
|
+
"checks": [
|
|
26
|
+
"@williamthorsen/nmr in devDependencies",
|
|
27
|
+
"@williamthorsen/nmr >= 0.48.0",
|
|
28
|
+
"pnpm-workspace.yaml exists",
|
|
29
|
+
"catalogued dependencies use catalog:",
|
|
30
|
+
"package.json has packageManager field",
|
|
31
|
+
".tool-versions does not list pnpm",
|
|
32
|
+
"no package.json declares a pnpm field",
|
|
33
|
+
"no package.json declares a workspaces field",
|
|
34
|
+
".config/nmr.config.ts uses defineConfig",
|
|
35
|
+
"eslint >= 10.0.0",
|
|
36
|
+
"@williamthorsen/strict-lint >= 9.3.0",
|
|
37
|
+
"root package.json has no nmr-provided scripts",
|
|
38
|
+
"no root install script runs lefthook install unguarded",
|
|
39
|
+
"all workspace packages can build",
|
|
40
|
+
"every bin target is a committed wrapper",
|
|
41
|
+
"every bin wrapper's build-output target is covered by files",
|
|
42
|
+
"no retired Vitest config variants",
|
|
43
|
+
"every vitest.config builds on @williamthorsen/nmr/vitest",
|
|
44
|
+
"vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
|
|
45
|
+
"every workspace with a Vite config has a Vitest config",
|
|
46
|
+
"the test suite gates the test-file conventions",
|
|
47
|
+
"every test file names its isolation tier",
|
|
48
|
+
"every test file is under a __tests__ directory",
|
|
49
|
+
"no package re-exports the ancestor Vitest config",
|
|
50
|
+
"Prettier config builds on @williamthorsen/nmr/prettier",
|
|
51
|
+
"taze.config.ts builds on @williamthorsen/nmr/taze",
|
|
52
|
+
"taze config declares no option taze discards",
|
|
53
|
+
"v11y-check in devDependencies",
|
|
54
|
+
"scripts/run-workspace-script.ts does not exist",
|
|
55
|
+
"no workspace packages reference run-workspace-script or \"pnpm run ws\""
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"checklist": "self-hosted runner",
|
|
60
|
+
"checks": [
|
|
61
|
+
"GitHub reports the repo's runner settings",
|
|
62
|
+
"CI_RUNS_ON selects the self-hosted runner",
|
|
63
|
+
"the self-hosted runner can run CI"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
22
67
|
"checklists": [
|
|
23
68
|
"nmr",
|
|
24
69
|
"self-hosted runner"
|
|
@@ -41,7 +86,7 @@
|
|
|
41
86
|
"path": "../../nmr-core/dist/esm/hasErrnoCode.js"
|
|
42
87
|
},
|
|
43
88
|
{
|
|
44
|
-
"hash": "
|
|
89
|
+
"hash": "98509862",
|
|
45
90
|
"kind": "module",
|
|
46
91
|
"path": "../../nmr-core/dist/esm/hashWorkingTree.js"
|
|
47
92
|
},
|
|
@@ -56,7 +101,7 @@
|
|
|
56
101
|
"path": "../../nmr-core/dist/esm/index.js"
|
|
57
102
|
},
|
|
58
103
|
{
|
|
59
|
-
"hash": "
|
|
104
|
+
"hash": "89d3e3b4",
|
|
60
105
|
"kind": "module",
|
|
61
106
|
"path": "../../nmr-core/dist/esm/parseArgs.js"
|
|
62
107
|
},
|
|
@@ -81,12 +126,12 @@
|
|
|
81
126
|
"path": "../../nmr-core/dist/esm/writeFileWithCheck.js"
|
|
82
127
|
},
|
|
83
128
|
{
|
|
84
|
-
"hash": "
|
|
129
|
+
"hash": "ffa13ccc",
|
|
85
130
|
"kind": "module",
|
|
86
131
|
"path": "kits/default.ts"
|
|
87
132
|
},
|
|
88
133
|
{
|
|
89
|
-
"hash": "
|
|
134
|
+
"hash": "47f25de7",
|
|
90
135
|
"kind": "inline",
|
|
91
136
|
"path": "../package.json",
|
|
92
137
|
"paths": [
|
|
@@ -119,7 +164,7 @@
|
|
|
119
164
|
"path": "../src/helpers/escapeControlCharacters.ts"
|
|
120
165
|
},
|
|
121
166
|
{
|
|
122
|
-
"hash": "
|
|
167
|
+
"hash": "2d1cf397",
|
|
123
168
|
"kind": "module",
|
|
124
169
|
"path": "../src/helpers/findClosestName.ts"
|
|
125
170
|
},
|
|
@@ -186,10 +231,10 @@
|
|
|
186
231
|
],
|
|
187
232
|
"name": "default",
|
|
188
233
|
"path": "kits/default.js",
|
|
189
|
-
"readyupVersion": "0.
|
|
234
|
+
"readyupVersion": "0.41.0",
|
|
190
235
|
"source": "kits/default.ts",
|
|
191
|
-
"sourceHash": "
|
|
192
|
-
"targetHash": "
|
|
236
|
+
"sourceHash": "ffa13ccc",
|
|
237
|
+
"targetHash": "6baed250"
|
|
193
238
|
}
|
|
194
239
|
]
|
|
195
240
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.48.0 — 2026-10-17
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- Adds the `--delegate <host>` flag to `nmr`, which runs a command whose result is its exit status, such as `ci`, `check`, or a `test` command, on that host over SSH against the local working tree with its uncommitted and untracked files, and exits 255 when the delegation fails outside the command. (#1058)
|
|
10
|
+
- Adds the `NMR_DELEGATE` environment variable, which names the host on which `nmr` runs every command that can be delegated, and the `--no-delegate` flag, which runs a command on the local machine whatever `NMR_DELEGATE` names. (#1058)
|
|
11
|
+
- Adds the host to the verdict for a delegated command, as `passed on <host>` in the line and as a `host` field in the `--json` object. (#1058)
|
|
12
|
+
|
|
13
|
+
### 📦 Dependencies
|
|
14
|
+
|
|
15
|
+
- Upgrades `@williamthorsen/eslint-config-typescript` from 18.0.0 to 19.0.0 and rewrites the code in `build-info`, `nmr-core`, `nmr`, and `release-kit` that its new and widened rules report, without changing behavior. (#1047)
|
|
16
|
+
- Upgrades `readyup` from 0.40.0 to 0.41.0 and recompiles the kits in `nmr`, `release-kit`, and `v11y-check`, whose manifests now record each checklist's check names under `checklistChecks`. (#1047)
|
|
17
|
+
- Upgrades `pnpm` to 12.11.2, `@types/node` to 24.19.2, `lefthook` to 2.2.1, `@williamthorsen/strict-lint` to 14.2.0, `@williamthorsen/tsconfig` to 0.11.0, `smol-toml` to 1.9.1, and `vite` to 8.3.4. (#1047)
|
|
18
|
+
|
|
19
|
+
## 0.47.0 — 2026-10-15
|
|
20
|
+
|
|
21
|
+
### 🎉 Features
|
|
22
|
+
|
|
23
|
+
- Sets `maxWorkers` on every project built by `defineVitestConfig` and `defineRootVitestConfig` to one worker per 4 GiB of total memory, bounded by one fewer than the core count, in place of Vitest's default of one fewer than the core count, which does not take memory into account; machines with 2 or fewer cores keep 3 workers. (#1044)
|
|
24
|
+
- Applies the `--maxWorkers` (or `--max-workers`) flag to every project, which Vitest does not do itself, making the flag override both the derived count and a `maxWorkers` set through the `project` or `tiers` seam, while the `VITEST_MAX_WORKERS` environment variable still overrides the flag. (#1044)
|
|
25
|
+
- Adds a machine lock that makes a heavy command (`check`, `ci`, and every `test` command except `test:watch`) wait, printing `nmr: waiting for …`, until the heavy command already running on the machine finishes; a nested command and a `-R` or `-F` fan-out take the lock once, and a cached pass does not wait. (#1045)
|
|
26
|
+
- Adds the `--no-lock` flag and the `NMR_NO_LOCK=1` environment variable, which run a command and the commands nested in it without waiting for the machine lock, for example on a self-hosted CI machine that runs several runners in parallel. (#1045)
|
|
27
|
+
- Extends the `nmr` rulebook to state that a run printing `nmr: waiting for …` is waiting for the machine lock rather than stuck, and that an agent must neither kill it nor rerun it with `--no-lock`. (#1045)
|
|
28
|
+
|
|
5
29
|
## 0.46.2 — 2026-10-14
|
|
6
30
|
|
|
7
31
|
### 🐛 Bug fixes
|
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
Context-aware script runner for pnpm monorepos. Includes an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v0.
|
|
8
|
+
## Release notes — v0.48.0 (2026-10-17)
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Adds the `--delegate <host>` flag to `nmr`, which runs a command whose result is its exit status, such as `ci`, `check`, or a `test` command, on that host over SSH against the local working tree with its uncommitted and untracked files, and exits 255 when the delegation fails outside the command. (#1058)
|
|
13
|
+
- Adds the `NMR_DELEGATE` environment variable, which names the host on which `nmr` runs every command that can be delegated, and the `--no-delegate` flag, which runs a command on the local machine whatever `NMR_DELEGATE` names. (#1058)
|
|
14
|
+
- Adds the host to the verdict for a delegated command, as `passed on <host>` in the line and as a `host` field in the `--json` object. (#1058)
|
|
13
15
|
<!-- /section:release-notes -->
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
@@ -180,6 +182,28 @@ $ nmr ci
|
|
|
180
182
|
|
|
181
183
|
[Check-result cache](docs/check-cache.md) covers what is cached, what a hit requires, replaying and reading a recorded run, and bypassing or configuring the cache.
|
|
182
184
|
|
|
185
|
+
## Machine lock
|
|
186
|
+
|
|
187
|
+
Tests and checks peak at several gigabytes, so nmr runs one heavy command at a time on a machine. A second `check`, `ci`, or `test` from any repository waits for the first rather than competing with it for memory:
|
|
188
|
+
|
|
189
|
+
```console
|
|
190
|
+
$ nmr check
|
|
191
|
+
nmr: waiting for test in /Users/me/repos/other-repo (pid 41872) to finish
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The lock lives in `/tmp/nmr-<uid>/`, or in `NMR_LOCK_DIR`; a sandboxed session needs write access to it. `nmr --no-lock <command>` or `NMR_NO_LOCK=1` runs without waiting. [Machine lock](docs/machine-lock.md) covers which commands take it, nesting and fan-outs, reclaiming a lock that its holder left behind, and CI.
|
|
195
|
+
|
|
196
|
+
## Delegating to another machine
|
|
197
|
+
|
|
198
|
+
`nmr --delegate <host> <command>` runs the command on another machine over SSH, against this working tree, and exits with its status. `NMR_DELEGATE=<host>` makes that the default:
|
|
199
|
+
|
|
200
|
+
```console
|
|
201
|
+
$ nmr --delegate imac2 ci
|
|
202
|
+
✅ my-repo: ci: passed on imac2 in 1m 48s
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
[Delegating to another machine](docs/delegate.md) covers which commands can be delegated, what the host runs, how failures are reported, and setting up the host.
|
|
206
|
+
|
|
183
207
|
## Shared configs and helpers
|
|
184
208
|
|
|
185
209
|
### Shared Vitest config
|
|
@@ -283,6 +307,8 @@ rdy list --from npm:@williamthorsen/nmr # what nmr publishes
|
|
|
283
307
|
- [Scripts and configuration](docs/scripts.md): Override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
|
|
284
308
|
- [What nmr reports](docs/reporting.md): Verdicts, `--json`, output style, and verbosity
|
|
285
309
|
- [Check-result cache](docs/check-cache.md): What is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
|
|
310
|
+
- [Machine lock](docs/machine-lock.md): Which commands wait for one another, where the lock lives, and bypassing it
|
|
311
|
+
- [Delegating to another machine](docs/delegate.md): `--delegate`, `NMR_DELEGATE`, what the host runs, and setting it up
|
|
286
312
|
- [Dependency upgrades](docs/upgrades.md): `nmr upgrade`, the upgrade policy, `report-catalog`, `report-overrides`, and `nmr-taze`
|
|
287
313
|
- [Standalone utilities](docs/utilities.md): `nmr-clean`, `nmr-compile`, `nmr-fmt`, and `ensure-prepublish-hooks`
|
|
288
314
|
- [Shared Vitest config](docs/vitest.md): Test tiers, test selections, the conventions check, and customization
|
|
@@ -14,4 +14,6 @@ delivery: ambient
|
|
|
14
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. Output that you need in full is a recording to read rather than a command to run again. It runs nothing, and it is held to the tree rather than to the terminal: A recording made by a piped run prints for a reader at one. A composite prints its constituents' excerpts, not their transcripts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. When there is nothing to show, `--log` says so on one line, names which case it is, and exits non-zero.
|
|
15
15
|
- Every command that nmr runs reports one line -- `PASS <scope>: <command>: passed in 12.4s`, `FAIL … failed in 1.2s (exit 1)`, `SKIP … passed 4m ago on this tree`, or `NOOP … skipped, the override is empty` -- and a composite reports alongside every command into which it expands. Those are the words that a pipe and a CI log get; a terminal gets ✅, ❌, ⏩, and ⚪ in their place, and `--output-style rich` or `NMR_OUTPUT_STYLE=rich` forces the emoji on any stream. The lines print in every verbosity, so `nmr -q <command>` is the low-noise way to run rather than a silent one: It withholds the output of the commands that nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. nmr reads its own flags only before the command name and passes everything after the name to the commands that run: `nmr ci -q` hands `-q` to `nmr-fmt`, which rejects it. 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
16
|
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing that the test reaches -- `tool` (a process that the test starts, or a program other than the package's own code that the test makes do its work, including an embedded engine, which the package installs and the test runs in-process, such as PGlite), `localhost` (a service already running on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --sources` reports both and warns that the check is missing.
|
|
17
|
-
-
|
|
17
|
+
- A heavy command (`check`, `ci`, and the `test` commands but `test:watch`) may print `nmr: waiting for <command> in <dir> (pid <pid>) to finish` and wait: nmr runs one at a time on the machine, across every repository. The run is not stuck, and it continues once the holder ends. Do not kill it, and do not rerun it with `--no-lock` to get past the wait, which defeats the memory protection that the lock exists for. A warning that nmr is `running without the machine lock` names a directory that the sandbox cannot write.
|
|
18
|
+
- A verdict reading `passed on <host>` or `failed on <host>` reports a command that ran on another machine, because `--delegate` or `NMR_DELEGATE` sent it there; the output above it is the host's. Exit 255 with `could not prepare <host>` inside a sandbox means that the sandbox refused `ssh`: Ask for network access to the host rather than treating the host as broken, or run the command here with `nmr --no-delegate <command>`.
|
|
19
|
+
- The rest is in `node_modules/@williamthorsen/nmr/docs/`: `scripts.md` for pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`) and script overrides and their `""`/`":"` skip values; `upgrades.md` for `nmr upgrade` ceilings (report-only until `nmr upgrade --write`); `utilities.md` for the `nmr-compile` build (the repo does not define a build script of its own); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; `machine-lock.md` for which commands take the machine lock, where it lives, and its bypass; `delegate.md` for which commands `--delegate` runs on another machine, what the host runs, and its setup; and `check-cache.md` for the check-result cache's key, the retention key to which a replayed excerpt is additionally held, the retention ceiling within which `--log` prints, its `checkCache` config, and every condition that disables it.
|
package/dist/esm/bin-path.js
CHANGED
|
@@ -9,10 +9,11 @@ export function prependBinDirs(options) {
|
|
|
9
9
|
const additions = [];
|
|
10
10
|
for (const dir of [anchorDir, monorepoRoot]) {
|
|
11
11
|
const binDir = path.resolve(dir, 'node_modules', '.bin');
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
additions.push(binDir);
|
|
12
|
+
if (seen.has(binDir)) {
|
|
13
|
+
continue;
|
|
15
14
|
}
|
|
15
|
+
seen.add(binDir);
|
|
16
|
+
additions.push(binDir);
|
|
16
17
|
}
|
|
17
18
|
if (additions.length === 0) {
|
|
18
19
|
return env;
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -126,7 +126,7 @@ export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
|
126
126
|
function classifyListedFile(cwd, file) {
|
|
127
127
|
try {
|
|
128
128
|
const stats = lstatSync(path.resolve(cwd, file), { throwIfNoEntry: false });
|
|
129
|
-
return stats?.isFile() === true ?
|
|
129
|
+
return { kind: stats?.isFile() === true ? 'formattable' : 'excluded' };
|
|
130
130
|
}
|
|
131
131
|
catch (error) {
|
|
132
132
|
const code = UNREADABLE_CODES.find((candidate) => hasErrnoCode(error, candidate));
|
package/dist/esm/help.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
2
|
import { OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG } from "./output-style.js";
|
|
3
|
+
import { DELEGATE_ENV_VAR } from "./remote-run.js";
|
|
3
4
|
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
4
5
|
const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
5
6
|
'dependencies',
|
|
@@ -32,14 +33,18 @@ export function generateHelp(config, monorepoRoot, packageDir, shouldUseRoot) {
|
|
|
32
33
|
' -R, --recursive Run command in all packages',
|
|
33
34
|
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
34
35
|
" -q, --quiet Suppress command output, keeping nmr's verdicts",
|
|
36
|
+
' --delegate <host> Run on <host> over SSH, against this working tree',
|
|
35
37
|
' --json Report one JSON object per command, withholding command output',
|
|
36
38
|
' --log Print the recorded run instead of running (up to 256 KiB)',
|
|
37
39
|
' --no-cache Run even if this tree already passed; record the result',
|
|
40
|
+
' --no-delegate Run here, even when NMR_DELEGATE names a host',
|
|
41
|
+
' --no-lock Run without waiting for another heavy command on this machine',
|
|
38
42
|
` ${OUTPUT_STYLE_FLAG} <style> Output style: auto (default), plain, or rich`,
|
|
39
43
|
' -?, --help Show this help',
|
|
40
44
|
' -V, --version Show version number',
|
|
41
45
|
'',
|
|
42
46
|
'Environment:',
|
|
47
|
+
` ${DELEGATE_ENV_VAR} Host on which to run each command that can be delegated`,
|
|
43
48
|
` ${OUTPUT_STYLE_ENV_VAR} Output style: auto (default), plain, or rich. auto prints`,
|
|
44
49
|
' plain, without emoji, when CI is set or the stream is not a terminal.',
|
|
45
50
|
'',
|
|
@@ -4,10 +4,11 @@ export function findClosestName(name, candidates) {
|
|
|
4
4
|
let shortestDistance = Infinity;
|
|
5
5
|
for (const candidate of candidates) {
|
|
6
6
|
const distance = measureDistance(name, candidate);
|
|
7
|
-
if (distance
|
|
8
|
-
|
|
9
|
-
closestName = candidate;
|
|
7
|
+
if (distance >= shortestDistance) {
|
|
8
|
+
continue;
|
|
10
9
|
}
|
|
10
|
+
shortestDistance = distance;
|
|
11
|
+
closestName = candidate;
|
|
11
12
|
}
|
|
12
13
|
return shortestDistance <= ceiling ? closestName : undefined;
|
|
13
14
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
3
|
+
/** How often a holder refreshes the lock file's mtime. */
|
|
4
|
+
export declare const HEARTBEAT_INTERVAL_MS = 10000;
|
|
5
|
+
/**
|
|
6
|
+
* The commands that take the machine lock: the ones whose memory peak makes two concurrent runs slower than two in
|
|
7
|
+
* sequence. `test:watch` is absent because it runs until it is stopped, and holding the lock would block every other
|
|
8
|
+
* run for as long.
|
|
9
|
+
*/
|
|
10
|
+
export declare const HEAVY_COMMANDS: ReadonlySet<string>;
|
|
11
|
+
/** Overrides the directory that contains the lock file. */
|
|
12
|
+
export declare const LOCK_DIR_ENV_VAR = "NMR_LOCK_DIR";
|
|
13
|
+
/** Passes the holder's token to the processes below it, which run under its hold rather than waiting on it. */
|
|
14
|
+
export declare const LOCK_TOKEN_ENV_VAR = "NMR_MACHINE_LOCK_TOKEN";
|
|
15
|
+
/** Set to `1` to run without the lock, for the whole chain. */
|
|
16
|
+
export declare const NO_LOCK_ENV_VAR = "NMR_NO_LOCK";
|
|
17
|
+
/** How often a waiter checks the lock again. */
|
|
18
|
+
export declare const POLL_INTERVAL_MS = 1000;
|
|
19
|
+
/**
|
|
20
|
+
* Age at which an unrefreshed lock file is abandoned. It covers a holder whose pid was reused by another process,
|
|
21
|
+
* and a file left half-written, neither of which a liveness check detects.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STALE_AFTER_MS = 60000;
|
|
24
|
+
/** A taken lock, or the hold-free stand-in for a run that does not take one. */
|
|
25
|
+
export interface MachineLock {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the chain runs without the lock: the bypass is set, or the lock directory cannot be written. The processes
|
|
28
|
+
* below then skip the lock rather than each trying it again and repeating the warning.
|
|
29
|
+
*/
|
|
30
|
+
isChainUnlocked: boolean;
|
|
31
|
+
release: () => Promise<void>;
|
|
32
|
+
/** The token in the lock file, when this call took the lock. */
|
|
33
|
+
token: string | undefined;
|
|
34
|
+
/** Whether the call waited for another holder before returning. */
|
|
35
|
+
waited: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Takes the machine lock, waiting while another process holds it. Resolves to a hold-free lock without waiting when
|
|
39
|
+
* the bypass is set, when this process runs under an ancestor's hold, or when the lock directory cannot be written.
|
|
40
|
+
*/
|
|
41
|
+
export declare function acquireMachineLock(options: {
|
|
42
|
+
command: string;
|
|
43
|
+
dir: string;
|
|
44
|
+
env: NodeJS.ProcessEnv;
|
|
45
|
+
stderr: Writable;
|
|
46
|
+
heartbeatIntervalMs?: number;
|
|
47
|
+
lockDir?: string;
|
|
48
|
+
pollIntervalMs?: number;
|
|
49
|
+
staleAfterMs?: number;
|
|
50
|
+
}): Promise<MachineLock>;
|
|
51
|
+
/**
|
|
52
|
+
* Renders the warning for a `--no-lock` that appears after the command name, which makes it an argument to the
|
|
53
|
+
* command rather than a flag to nmr.
|
|
54
|
+
*/
|
|
55
|
+
export declare function formatMisplacedNoLockWarning(command: string, style: OutputStyle): string;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the directory that contains the lock file: one per user under `/tmp`, which a sandboxed session and a
|
|
58
|
+
* terminal resolve alike, where `TMPDIR` does not. Windows does not have `/tmp` or a uid.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveLockDir(options: {
|
|
61
|
+
env: NodeJS.ProcessEnv;
|
|
62
|
+
platform: NodeJS.Platform;
|
|
63
|
+
uid: number | undefined;
|
|
64
|
+
}): string;
|