@williamthorsen/nmr 0.28.0 → 0.30.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 +11 -10
- package/.readyup/manifest.json +2 -2
- package/README.md +45 -37
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/check-cache.d.ts +1 -1
- package/dist/esm/check-cache.js +5 -4
- package/dist/esm/commands/clean.js +9 -5
- package/dist/esm/config.js +19 -2
- package/dist/esm/default-scripts.js +8 -8
- package/dist/esm/helpers/createBoundedBuffer.d.ts +9 -0
- package/dist/esm/helpers/createBoundedBuffer.js +60 -0
- package/dist/esm/resolver.d.ts +4 -2
- package/dist/esm/resolver.js +11 -5
- package/dist/esm/runCli.d.ts +1 -0
- package/dist/esm/runCli.js +88 -44
- package/dist/esm/runner.d.ts +31 -1
- package/dist/esm/runner.js +141 -29
- package/dist/esm/steps.d.ts +11 -0
- package/dist/esm/steps.js +31 -0
- package/dist/esm/verbosity.d.ts +11 -0
- package/dist/esm/verbosity.js +17 -0
- package/package.json +3 -3
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.26.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
@@ -48,7 +48,7 @@ var rootScripts = {
|
|
|
48
48
|
audit: ["audit:prod", "audit:dev"],
|
|
49
49
|
"audit:dev": "pnpm exec v11y --dev",
|
|
50
50
|
"audit:prod": "pnpm exec v11y --prod",
|
|
51
|
-
build: "
|
|
51
|
+
build: ["-R build"],
|
|
52
52
|
check: ["typecheck", "fmt:check", "lint:check", "test"],
|
|
53
53
|
"check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
|
|
54
54
|
// Excludes the audit, which in CI has a workflow of its own.
|
|
@@ -61,7 +61,8 @@ var rootScripts = {
|
|
|
61
61
|
lint: "eslint --fix .",
|
|
62
62
|
"lint:check": "eslint .",
|
|
63
63
|
"lint:strict": "strict-lint",
|
|
64
|
-
|
|
64
|
+
// The audit costs seconds and `ci` costs minutes, so the cheap gate fails first.
|
|
65
|
+
prepush: ["audit", "ci"],
|
|
65
66
|
"report-overrides": "nmr-report-overrides",
|
|
66
67
|
"root:check": ["root:typecheck", "fmt:check", "root:lint:check", "root:test"],
|
|
67
68
|
"root:lint": "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -73,13 +74,13 @@ var rootScripts = {
|
|
|
73
74
|
"root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
|
|
74
75
|
"root:typecheck": "tsgo --noEmit",
|
|
75
76
|
"root:upgrade": "nmr-taze --include-locked",
|
|
76
|
-
test: "
|
|
77
|
-
"test:all": "
|
|
78
|
-
"test:coverage": "
|
|
79
|
-
"test:tool": "
|
|
80
|
-
"test:unit": "
|
|
77
|
+
test: ["root:test", "-R test"],
|
|
78
|
+
"test:all": ["root:test:all", "-R test:all"],
|
|
79
|
+
"test:coverage": ["root:test", "-R test:coverage"],
|
|
80
|
+
"test:tool": ["root:test:tool", "-R test:tool"],
|
|
81
|
+
"test:unit": ["root:test:unit", "-R test:unit"],
|
|
81
82
|
"test:watch": `vitest ${GATE_PROJECTS} --watch`,
|
|
82
|
-
typecheck: "
|
|
83
|
+
typecheck: ["root:typecheck", "-R typecheck"],
|
|
83
84
|
// The command must be a string rather than a composite: Passthrough args attach to the chain's last command.
|
|
84
85
|
upgrade: "nmr-report-overrides && nmr-taze --include-locked --recursive"
|
|
85
86
|
};
|
|
@@ -375,7 +376,7 @@ function formatPaths(paths) {
|
|
|
375
376
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
376
377
|
}
|
|
377
378
|
function getMinVersion() {
|
|
378
|
-
const picked = { "version": "0.
|
|
379
|
+
const picked = { "version": "0.30.0" };
|
|
379
380
|
if (typeof picked["version"] !== "string") {
|
|
380
381
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
381
382
|
}
|
package/.readyup/manifest.json
CHANGED
package/README.md
CHANGED
|
@@ -3,29 +3,25 @@
|
|
|
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.30.0 (2026-08-10)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- 🚨 **Breaking:** Resolve a command to a step list and reject a script element carrying shell syntax (#648)
|
|
11
11
|
|
|
12
|
-
A
|
|
12
|
+
A script defined in `.config/nmr.config.ts` as a list of nmr commands now holds one command name per entry, optionally preceded by nmr's own flags. An entry that carries a quoted argument or any other shell syntax previously ran as written and is now rejected when the config loads. Before upgrading, a repo relying on such an entry should define that command as a script of its own and reference it by name from the list.
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- 🚨 **Breaking:** Sequence a command's steps in nmr rather than in a shell (#649)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
When a run in quiet mode fails, it now shows only the output of the command that failed instead of everything the run had produced up to that point. Quiet mode can now be set for a whole shell session with the `NMR_COMMAND_VERBOSITY` environment variable, which accepts `full` or `quiet`; `-q` still takes precedence for a single run, and an unrecognized value is reported as an error. Interrupting a run now stops the commands that would have followed it.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Breaking: a `devBin` entry that maps `nmr` itself no longer takes effect inside a command that runs other nmr commands. Mapping any other tool (the documented use) is unaffected.
|
|
19
19
|
|
|
20
|
-
-
|
|
20
|
+
- 🚨 **Breaking:** Express the seven root defaults as steps (#650)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Running `nmr build`, `nmr test`, `nmr test:all`, `nmr test:coverage`, `nmr test:tool`, `nmr test:unit`, or `nmr typecheck` from a monorepo root now passes over a package that does not define the command, instead of failing there. The first run of `test`, `test:coverage`, `test:tool`, `test:unit`, or `typecheck` after the upgrade no longer skips as already passed; skipping resumes on the run after.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- Publish build output atomically so a failed build cannot destroy it (#631)
|
|
27
|
-
|
|
28
|
-
Fixes an issue where a failed or still-running `nmr build` could leave a package with no build output at all, breaking the tooling that a subsequent build depends on. Separately, the error reported when a package's build output is missing now names the correct recovery command.
|
|
24
|
+
`nmr` now also warns when a command it is about to run invokes `nmr` again through a shell, where a failure reports the whole inner run's output instead of the failing command's. The warning catches only a direct `nmr` call, so a command that reaches `nmr` through another program goes unreported.
|
|
29
25
|
<!-- /section:release-notes -->
|
|
30
26
|
|
|
31
27
|
## Installation
|
|
@@ -73,9 +69,9 @@ nmr's key feature is that the same command runs different scripts depending on w
|
|
|
73
69
|
|
|
74
70
|
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
75
71
|
| --------------------------------- | ----------------- | ----------------- | -------------------------------------------------------------------- |
|
|
76
|
-
| Monorepo root | Root scripts | Monorepo root |
|
|
72
|
+
| Monorepo root | Root scripts | Monorepo root | `root:test`, then `-R test` |
|
|
77
73
|
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project unit --project tool` (that package only) |
|
|
78
|
-
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root |
|
|
74
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | `root:test`, then `-R test` |
|
|
79
75
|
|
|
80
76
|
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
81
77
|
|
|
@@ -122,7 +118,13 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-
|
|
|
122
118
|
// expands to: nmr lint && nmr fmt
|
|
123
119
|
```
|
|
124
120
|
|
|
125
|
-
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
Passthrough arguments attach to the final step alone: `nmr fix --dry-run` runs `nmr lint`, then `nmr fmt --dry-run`.
|
|
124
|
+
|
|
125
|
+
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 whose first token is `nmr`.
|
|
126
|
+
|
|
127
|
+
That line reports a boundary rather than enforcing a rule, and it is partial: a step reaching nmr through another program, such as `pnpm --recursive exec nmr build`, goes unreported. Only the built-in defaults and `.config/nmr.config.ts` carry arrays, so a `package.json` script embedding nmr has nowhere to move but up a tier.
|
|
126
128
|
|
|
127
129
|
## Configuration
|
|
128
130
|
|
|
@@ -192,7 +194,7 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
192
194
|
|
|
193
195
|
## Pre and post hooks
|
|
194
196
|
|
|
195
|
-
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.
|
|
197
|
+
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.
|
|
196
198
|
|
|
197
199
|
Behaviors worth knowing:
|
|
198
200
|
|
|
@@ -277,7 +279,7 @@ A hit requires all of these to match the run that recorded the pass:
|
|
|
277
279
|
|
|
278
280
|
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. Output is git-ignored, so neither its removal nor its replacement moves the hash: restoring a tree with `git stash` or `git checkout` restores none of the output that tree was built with. nmr compares each covered package's recorded build digest against the one on disk, so a `dist` compiled from another tree is a miss the following run repairs. A package that overrides `build` or `compile` emits on terms nmr does not know and is exempt.
|
|
279
281
|
|
|
280
|
-
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started against, only when every covered package's output is present, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss).
|
|
282
|
+
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started against, 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). Output that moves mid-run leaves no answer to which output the pass was earned over, whether another process rebuilt it or the chain built it itself: `nmr ci` is the one default command whose chain does, and it records nothing on a run whose build did work. The `nmr check:strict` inside that chain still records, because its own window opens after the build.
|
|
281
283
|
|
|
282
284
|
### Out of contract
|
|
283
285
|
|
|
@@ -321,6 +323,10 @@ Reach for these in order; the first is almost always the right one.
|
|
|
321
323
|
|
|
322
324
|
`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.
|
|
323
325
|
|
|
326
|
+
`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. nmr's own messages — the skip line, the override notice, and the `""`/`":"` skip notices — keep the suppression `-q` has always given them.
|
|
327
|
+
|
|
328
|
+
`-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.
|
|
329
|
+
|
|
324
330
|
### Configuring
|
|
325
331
|
|
|
326
332
|
```ts
|
|
@@ -433,14 +439,16 @@ A run that collects no test files passes. That is what lets `nmr test:tool` fan
|
|
|
433
439
|
|
|
434
440
|
#### Build and CI
|
|
435
441
|
|
|
436
|
-
| Command | Runs
|
|
437
|
-
| --------- |
|
|
438
|
-
| `build` |
|
|
439
|
-
| `ci` | `build`, `check:strict`
|
|
440
|
-
| `clean` | `nmr-clean`
|
|
441
|
-
| `prepush` | `
|
|
442
|
+
| Command | Runs |
|
|
443
|
+
| --------- | ----------------------- |
|
|
444
|
+
| `build` | `-R build` |
|
|
445
|
+
| `ci` | `build`, `check:strict` |
|
|
446
|
+
| `clean` | `nmr-clean` |
|
|
447
|
+
| `prepush` | `audit`, `ci` |
|
|
448
|
+
|
|
449
|
+
`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.
|
|
442
450
|
|
|
443
|
-
|
|
451
|
+
The audit reaches the network. With no network, `ci` is the gate that still runs.
|
|
444
452
|
|
|
445
453
|
Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
|
|
446
454
|
|
|
@@ -461,14 +469,14 @@ Neither is bound to a git hook. `prepush` is named for when you run it, not for
|
|
|
461
469
|
|
|
462
470
|
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.
|
|
463
471
|
|
|
464
|
-
| Command | Runs
|
|
465
|
-
| --------------- |
|
|
466
|
-
| `test` | `
|
|
467
|
-
| `test:all` | `
|
|
468
|
-
| `test:coverage` | `
|
|
469
|
-
| `test:tool` | `
|
|
470
|
-
| `test:unit` | `
|
|
471
|
-
| `test:watch` | `vitest --project unit --project tool --watch`
|
|
472
|
+
| Command | Runs |
|
|
473
|
+
| --------------- | ---------------------------------------------- |
|
|
474
|
+
| `test` | `root:test`, `-R test` |
|
|
475
|
+
| `test:all` | `root:test:all`, `-R test:all` |
|
|
476
|
+
| `test:coverage` | `root:test`, `-R test:coverage` |
|
|
477
|
+
| `test:tool` | `root:test:tool`, `-R test:tool` |
|
|
478
|
+
| `test:unit` | `root:test:unit`, `-R test:unit` |
|
|
479
|
+
| `test:watch` | `vitest --project unit --project tool --watch` |
|
|
472
480
|
|
|
473
481
|
`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.
|
|
474
482
|
|
|
@@ -476,9 +484,9 @@ The same six names the workspace registry carries, so a command means the same t
|
|
|
476
484
|
|
|
477
485
|
#### Typecheck
|
|
478
486
|
|
|
479
|
-
| Command | Runs
|
|
480
|
-
| ----------- |
|
|
481
|
-
| `typecheck` | `
|
|
487
|
+
| Command | Runs |
|
|
488
|
+
| ----------- | -------------------------------- |
|
|
489
|
+
| `typecheck` | `root:typecheck`, `-R typecheck` |
|
|
482
490
|
|
|
483
491
|
#### Lint
|
|
484
492
|
|
|
@@ -576,7 +584,7 @@ nmr build # Compile to .js and .d.ts in one pass
|
|
|
576
584
|
# From the monorepo root
|
|
577
585
|
nmr test # Root tests + recursive workspace tests
|
|
578
586
|
nmr ci # build + check:strict
|
|
579
|
-
nmr prepush #
|
|
587
|
+
nmr prepush # audit + ci
|
|
580
588
|
|
|
581
589
|
# Target specific packages
|
|
582
590
|
nmr --filter core test # Test only the core package
|
|
@@ -9,6 +9,6 @@ delivery: ambient
|
|
|
9
9
|
- Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command it resolves to; a wrong guess reports only `Unknown command`, with no list.
|
|
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
|
-
- `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
|
|
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
13
|
- A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. Any edit, addition, or deletion re-runs it, so a skip is a real pass and needs no confirming: 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`.
|
|
14
14
|
- 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, its `checkCache` config, and every condition that disables it.
|
|
@@ -46,7 +46,7 @@ export declare function computeCacheKey(options: {
|
|
|
46
46
|
reason: string;
|
|
47
47
|
};
|
|
48
48
|
export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
|
|
49
|
-
export declare function findStaleBuildOutput(
|
|
49
|
+
export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
|
|
50
50
|
export declare function formatMisplacedNoCacheWarning(command: string): string;
|
|
51
51
|
export declare function formatSkipLine(command: string, entry: CheckCacheEntry, now: number): string;
|
|
52
52
|
export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -9,6 +9,7 @@ import { formatDuration, formatSaving } from "./helpers/duration.js";
|
|
|
9
9
|
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
10
10
|
import { getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
11
11
|
import { buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
12
|
+
import { renderChain } from "./steps.js";
|
|
12
13
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
13
14
|
export const CURRENT_RUNTIME = {
|
|
14
15
|
arch: process.arch,
|
|
@@ -81,9 +82,9 @@ export function computeCacheKey(options) {
|
|
|
81
82
|
export function encodeTreeSnapshot(snapshot) {
|
|
82
83
|
return `${snapshot.hash} ${snapshot.headSha}`;
|
|
83
84
|
}
|
|
84
|
-
export function findStaleBuildOutput(
|
|
85
|
-
const names = [...new Set([...Object.keys(
|
|
86
|
-
return names.find((name) =>
|
|
85
|
+
export function findStaleBuildOutput(earlier, later) {
|
|
86
|
+
const names = [...new Set([...Object.keys(earlier), ...Object.keys(later)])].toSorted();
|
|
87
|
+
return names.find((name) => earlier[name] !== later[name]);
|
|
87
88
|
}
|
|
88
89
|
export function formatMisplacedNoCacheWarning(command) {
|
|
89
90
|
return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
|
|
@@ -196,7 +197,7 @@ function isProbeSubject(packageDir, registry) {
|
|
|
196
197
|
return false;
|
|
197
198
|
}
|
|
198
199
|
const compile = resolveScript('compile', registry, packageDir, false);
|
|
199
|
-
return compile !== undefined && compile.
|
|
200
|
+
return compile !== undefined && renderChain(compile.steps) === BUILT_IN_COMPILE;
|
|
200
201
|
}
|
|
201
202
|
function resolveEntryPath(options) {
|
|
202
203
|
const anchorDir = path.resolve(options.anchorDir);
|
|
@@ -4,7 +4,8 @@ import { removeCheckCache } from "../check-cache.js";
|
|
|
4
4
|
import { loadRootConfig } from "../config.js";
|
|
5
5
|
import { findContainingPackageDir } from "../context.js";
|
|
6
6
|
import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
|
|
7
|
-
import { runCommand } from "../runner.js";
|
|
7
|
+
import { resolveChannel, runCommand } from "../runner.js";
|
|
8
|
+
import { renderChain } from "../steps.js";
|
|
8
9
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
9
10
|
import { resolveBuildCachePath } from "./build-output.js";
|
|
10
11
|
const CLEAN_ICON = '🧹';
|
|
@@ -44,15 +45,18 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
|
44
45
|
const registry = buildWorkspaceRegistry(config);
|
|
45
46
|
for (const packageDir of workspacePackageDirs) {
|
|
46
47
|
const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
47
|
-
|
|
48
|
+
const resolvedCommand = resolved === undefined ? undefined : renderChain(resolved.steps);
|
|
49
|
+
if (resolvedCommand === undefined || resolvedCommand === '') {
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
50
|
-
if (
|
|
52
|
+
if (resolvedCommand === BUILT_IN_CLEAN) {
|
|
51
53
|
await cleanPackage(packageDir);
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
54
|
-
const command = applyDevBin(
|
|
55
|
-
const exitCode = runCommand(command, packageDir
|
|
56
|
+
const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
|
|
57
|
+
const { exitCode } = await runCommand(command, packageDir, {
|
|
58
|
+
channels: { stderr: resolveChannel(process.stderr, false), stdout: resolveChannel(process.stdout, false) },
|
|
59
|
+
});
|
|
56
60
|
if (exitCode !== 0) {
|
|
57
61
|
throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
58
62
|
}
|
package/dist/esm/config.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync } from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
13
|
+
import { findUnexpressibleToken } from "./steps.js";
|
|
13
14
|
const CONFIG_FILENAME = 'nmr.config.ts';
|
|
14
15
|
const CONFIG_DIR = '.config';
|
|
15
16
|
const CONFIG_TIERS = {
|
|
@@ -45,10 +46,26 @@ function validateScriptField(value, fieldName, configPath) {
|
|
|
45
46
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
46
47
|
return undefined;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
const scripts = value[fieldName];
|
|
50
|
+
if (!isScriptRecord(scripts)) {
|
|
49
51
|
throw new Error(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | string[]>`);
|
|
50
52
|
}
|
|
51
|
-
|
|
53
|
+
assertExpressibleElements(scripts, fieldName, configPath);
|
|
54
|
+
return scripts;
|
|
55
|
+
}
|
|
56
|
+
function assertExpressibleElements(scripts, fieldName, configPath) {
|
|
57
|
+
for (const [command, script] of Object.entries(scripts)) {
|
|
58
|
+
if (typeof script === 'string')
|
|
59
|
+
continue;
|
|
60
|
+
for (const element of script) {
|
|
61
|
+
const token = findUnexpressibleToken(element);
|
|
62
|
+
if (token === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` carries ` +
|
|
65
|
+
`\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
|
|
66
|
+
`reference that name here.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
52
69
|
}
|
|
53
70
|
function isStringArray(value) {
|
|
54
71
|
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
@@ -26,7 +26,7 @@ export const rootScripts = {
|
|
|
26
26
|
audit: ['audit:prod', 'audit:dev'],
|
|
27
27
|
'audit:dev': 'pnpm exec v11y --dev',
|
|
28
28
|
'audit:prod': 'pnpm exec v11y --prod',
|
|
29
|
-
build: '
|
|
29
|
+
build: ['-R build'],
|
|
30
30
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
31
31
|
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
32
32
|
ci: ['build', 'check:strict'],
|
|
@@ -38,7 +38,7 @@ export const rootScripts = {
|
|
|
38
38
|
lint: 'eslint --fix .',
|
|
39
39
|
'lint:check': 'eslint .',
|
|
40
40
|
'lint:strict': 'strict-lint',
|
|
41
|
-
prepush: ['
|
|
41
|
+
prepush: ['audit', 'ci'],
|
|
42
42
|
'report-overrides': 'nmr-report-overrides',
|
|
43
43
|
'root:check': ['root:typecheck', 'fmt:check', 'root:lint:check', 'root:test'],
|
|
44
44
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -50,12 +50,12 @@ export const rootScripts = {
|
|
|
50
50
|
'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
|
|
51
51
|
'root:typecheck': 'tsgo --noEmit',
|
|
52
52
|
'root:upgrade': 'nmr-taze --include-locked',
|
|
53
|
-
test: '
|
|
54
|
-
'test:all': '
|
|
55
|
-
'test:coverage': '
|
|
56
|
-
'test:tool': '
|
|
57
|
-
'test:unit': '
|
|
53
|
+
test: ['root:test', '-R test'],
|
|
54
|
+
'test:all': ['root:test:all', '-R test:all'],
|
|
55
|
+
'test:coverage': ['root:test', '-R test:coverage'],
|
|
56
|
+
'test:tool': ['root:test:tool', '-R test:tool'],
|
|
57
|
+
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
58
58
|
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
59
|
-
typecheck: '
|
|
59
|
+
typecheck: ['root:typecheck', '-R typecheck'],
|
|
60
60
|
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
61
61
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface BoundedBufferOptions {
|
|
2
|
+
headLimitBytes?: number;
|
|
3
|
+
tailLimitBytes?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface BoundedBuffer {
|
|
6
|
+
append: (chunk: Buffer) => void;
|
|
7
|
+
toBuffer: () => Buffer;
|
|
8
|
+
}
|
|
9
|
+
export declare function createBoundedBuffer(options?: BoundedBufferOptions): BoundedBuffer;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULT_HEAD_LIMIT_BYTES = 1_048_576;
|
|
2
|
+
const DEFAULT_TAIL_LIMIT_BYTES = 1_048_576;
|
|
3
|
+
export function createBoundedBuffer(options = {}) {
|
|
4
|
+
const headLimitBytes = options.headLimitBytes ?? DEFAULT_HEAD_LIMIT_BYTES;
|
|
5
|
+
const tailLimitBytes = options.tailLimitBytes ?? DEFAULT_TAIL_LIMIT_BYTES;
|
|
6
|
+
const headChunks = [];
|
|
7
|
+
let headBytes = 0;
|
|
8
|
+
let tailRing;
|
|
9
|
+
let tailOffset = 0;
|
|
10
|
+
let tailBytes = 0;
|
|
11
|
+
let elidedBytes = 0;
|
|
12
|
+
function append(chunk) {
|
|
13
|
+
let remainder = chunk;
|
|
14
|
+
if (headBytes < headLimitBytes) {
|
|
15
|
+
const takenBytes = Math.min(headLimitBytes - headBytes, remainder.length);
|
|
16
|
+
headChunks.push(remainder.subarray(0, takenBytes));
|
|
17
|
+
headBytes += takenBytes;
|
|
18
|
+
remainder = remainder.subarray(takenBytes);
|
|
19
|
+
}
|
|
20
|
+
if (remainder.length > 0) {
|
|
21
|
+
appendToTail(remainder);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function appendToTail(chunk) {
|
|
25
|
+
const ring = (tailRing ??= Buffer.alloc(tailLimitBytes));
|
|
26
|
+
if (chunk.length >= tailLimitBytes) {
|
|
27
|
+
elidedBytes += tailBytes + chunk.length - tailLimitBytes;
|
|
28
|
+
chunk.copy(ring, 0, chunk.length - tailLimitBytes);
|
|
29
|
+
tailOffset = 0;
|
|
30
|
+
tailBytes = tailLimitBytes;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
elidedBytes += Math.max(0, tailBytes + chunk.length - tailLimitBytes);
|
|
34
|
+
const untilWrapBytes = Math.min(chunk.length, tailLimitBytes - tailOffset);
|
|
35
|
+
chunk.copy(ring, tailOffset, 0, untilWrapBytes);
|
|
36
|
+
if (untilWrapBytes < chunk.length) {
|
|
37
|
+
chunk.copy(ring, 0, untilWrapBytes);
|
|
38
|
+
}
|
|
39
|
+
tailOffset = (tailOffset + chunk.length) % tailLimitBytes;
|
|
40
|
+
tailBytes = Math.min(tailLimitBytes, tailBytes + chunk.length);
|
|
41
|
+
}
|
|
42
|
+
function toBuffer() {
|
|
43
|
+
const headBuffer = Buffer.concat(headChunks);
|
|
44
|
+
if (tailRing === undefined) {
|
|
45
|
+
return headBuffer;
|
|
46
|
+
}
|
|
47
|
+
const tailBuffer = tailBytes < tailLimitBytes
|
|
48
|
+
? tailRing.subarray(0, tailBytes)
|
|
49
|
+
: Buffer.concat([tailRing.subarray(tailOffset), tailRing.subarray(0, tailOffset)]);
|
|
50
|
+
if (elidedBytes === 0) {
|
|
51
|
+
return Buffer.concat([headBuffer, tailBuffer]);
|
|
52
|
+
}
|
|
53
|
+
return Buffer.concat([headBuffer, Buffer.from(formatElisionMarker(elidedBytes)), tailBuffer]);
|
|
54
|
+
}
|
|
55
|
+
return { append, toBuffer };
|
|
56
|
+
}
|
|
57
|
+
function formatElisionMarker(byteCount) {
|
|
58
|
+
const unit = byteCount === 1 ? 'byte' : 'bytes';
|
|
59
|
+
return `\n… nmr elided ${byteCount.toLocaleString('en-US')} ${unit} …\n`;
|
|
60
|
+
}
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ScriptRegistry, ScriptValue } from './resolve-scripts.js';
|
|
2
|
+
import type { Step } from './steps.js';
|
|
2
3
|
import type { NmrConfig } from './types.js';
|
|
3
4
|
export declare function applyDevBin(command: string, devBin: Record<string, string> | undefined, monorepoRoot: string): string;
|
|
5
|
+
export declare function applyDevBinToSteps(steps: readonly Step[], devBin: Record<string, string> | undefined, monorepoRoot: string): readonly Step[];
|
|
4
6
|
export interface ResolvedScript {
|
|
5
|
-
command: string;
|
|
6
7
|
source: 'default' | 'package';
|
|
8
|
+
steps: readonly Step[];
|
|
7
9
|
}
|
|
8
|
-
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean):
|
|
10
|
+
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): readonly Step[];
|
|
9
11
|
export declare function describeScript(script: ScriptValue): string;
|
|
10
12
|
export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
|
|
11
13
|
export declare function buildWorkspaceRegistry(config: NmrConfig): ScriptRegistry;
|
package/dist/esm/resolver.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isObject } from "./helpers/type-guards.js";
|
|
4
4
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
5
|
+
import { composeNmrStep } from "./steps.js";
|
|
5
6
|
export function applyDevBin(command, devBin, monorepoRoot) {
|
|
6
7
|
if (!devBin) {
|
|
7
8
|
return command;
|
|
@@ -16,6 +17,12 @@ export function applyDevBin(command, devBin, monorepoRoot) {
|
|
|
16
17
|
const resolvedReplacement = resolveReplacementPaths(replacement, monorepoRoot);
|
|
17
18
|
return resolvedReplacement + rest;
|
|
18
19
|
}
|
|
20
|
+
export function applyDevBinToSteps(steps, devBin, monorepoRoot) {
|
|
21
|
+
if (!devBin) {
|
|
22
|
+
return steps;
|
|
23
|
+
}
|
|
24
|
+
return steps.map((step) => step.kind === 'opaque' ? { kind: 'opaque', command: applyDevBin(step.command, devBin, monorepoRoot) } : step);
|
|
25
|
+
}
|
|
19
26
|
function resolveReplacementPaths(replacement, monorepoRoot) {
|
|
20
27
|
const tokens = replacement.split(' ');
|
|
21
28
|
return tokens
|
|
@@ -32,10 +39,9 @@ function resolveReplacementPaths(replacement, monorepoRoot) {
|
|
|
32
39
|
}
|
|
33
40
|
export function expandScript(script, workspaceRoot) {
|
|
34
41
|
if (typeof script === 'string') {
|
|
35
|
-
return script;
|
|
42
|
+
return [{ kind: 'opaque', command: script }];
|
|
36
43
|
}
|
|
37
|
-
|
|
38
|
-
return script.map((s) => `nmr ${flag}${s}`).join(' && ');
|
|
44
|
+
return script.map((element) => composeNmrStep(element, workspaceRoot));
|
|
39
45
|
}
|
|
40
46
|
export function describeScript(script) {
|
|
41
47
|
return typeof script === 'string' ? script : `[${script.join(', ')}]`;
|
|
@@ -85,7 +91,7 @@ export function resolveScript(commandName, registry, packageDir, workspaceRoot)
|
|
|
85
91
|
if (pkgScripts && Object.hasOwn(pkgScripts, commandName)) {
|
|
86
92
|
const override = pkgScripts[commandName];
|
|
87
93
|
if (override !== undefined && !isSelfReferential(override, commandName)) {
|
|
88
|
-
return {
|
|
94
|
+
return { source: 'package', steps: [{ kind: 'opaque', command: override }] };
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -96,5 +102,5 @@ export function resolveScript(commandName, registry, packageDir, workspaceRoot)
|
|
|
96
102
|
if (registryEntry === undefined) {
|
|
97
103
|
return undefined;
|
|
98
104
|
}
|
|
99
|
-
return {
|
|
105
|
+
return { source: 'default', steps: expandScript(registryEntry, workspaceRoot) };
|
|
100
106
|
}
|
package/dist/esm/runCli.d.ts
CHANGED
package/dist/esm/runCli.js
CHANGED
|
@@ -4,9 +4,12 @@ import { computeCacheKey, CURRENT_RUNTIME, encodeTreeSnapshot, findStaleBuildOut
|
|
|
4
4
|
import { resolveContext } from "./context.js";
|
|
5
5
|
import { generateHelp } from "./help.js";
|
|
6
6
|
import { isHookName } from "./helpers/hook-name.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
8
|
+
import { runSteps } from "./runner.js";
|
|
9
|
+
import { composeNmrStep, findShelledNmrStep, renderChain } from "./steps.js";
|
|
10
|
+
import { COMMAND_VERBOSITY_ENV_VAR, resolveVerbosity } from "./verbosity.js";
|
|
9
11
|
const VERSION = readPackageVersion(import.meta.url);
|
|
12
|
+
export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
|
|
10
13
|
export async function runCli(options) {
|
|
11
14
|
const { args, cwd, env, stdout, stderr } = options;
|
|
12
15
|
const parseResult = parseArgs(args);
|
|
@@ -15,10 +18,16 @@ export async function runCli(options) {
|
|
|
15
18
|
return { exitCode: 1 };
|
|
16
19
|
}
|
|
17
20
|
const { parsed } = parseResult;
|
|
21
|
+
const verbosity = resolveVerbosity(env, parsed.quiet);
|
|
22
|
+
if (!verbosity.ok) {
|
|
23
|
+
reportError(verbosity.error, stderr);
|
|
24
|
+
return { exitCode: 1 };
|
|
25
|
+
}
|
|
18
26
|
if (parsed.version) {
|
|
19
27
|
stdout.write(`${VERSION}\n`);
|
|
20
28
|
return { exitCode: 0 };
|
|
21
29
|
}
|
|
30
|
+
const quiet = verbosity.verbosity === 'quiet';
|
|
22
31
|
const context = await resolveContext(cwd);
|
|
23
32
|
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
24
33
|
const anchorDir = useRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
@@ -27,7 +36,6 @@ export async function runCli(options) {
|
|
|
27
36
|
return { exitCode: 0 };
|
|
28
37
|
}
|
|
29
38
|
const { command } = parsed;
|
|
30
|
-
const passthrough = parsed.passthrough.length > 0 ? ' ' + parsed.passthrough.map(shellQuote).join(' ') : '';
|
|
31
39
|
const snapshot = openGate({
|
|
32
40
|
command,
|
|
33
41
|
config: context.config,
|
|
@@ -37,38 +45,48 @@ export async function runCli(options) {
|
|
|
37
45
|
stderr,
|
|
38
46
|
});
|
|
39
47
|
const noCache = parsed.noCache || env[NO_CACHE_ENV_VAR] === '1';
|
|
40
|
-
const childEnv = buildChildEnv(env, snapshot, noCache);
|
|
41
|
-
const runOptions = {
|
|
48
|
+
const childEnv = buildChildEnv(env, snapshot, noCache, verbosity.verbosity);
|
|
49
|
+
const runOptions = {
|
|
50
|
+
quiet,
|
|
51
|
+
stdout,
|
|
52
|
+
stderr,
|
|
53
|
+
env: childEnv,
|
|
54
|
+
};
|
|
42
55
|
if (parsed.filter) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return { exitCode };
|
|
56
|
+
const delegate = composeDelegate(['--filter', parsed.filter], command, parsed.passthrough);
|
|
57
|
+
return runSteps([delegate], context.monorepoRoot, runOptions);
|
|
46
58
|
}
|
|
47
59
|
if (parsed.recursive) {
|
|
48
|
-
const delegateEnv = { ...childEnv,
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
return { exitCode };
|
|
60
|
+
const delegateEnv = { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' };
|
|
61
|
+
const delegate = composeDelegate(['--recursive'], command, parsed.passthrough);
|
|
62
|
+
return runSteps([delegate], context.monorepoRoot, { ...runOptions, env: delegateEnv });
|
|
52
63
|
}
|
|
53
64
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
54
65
|
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
55
66
|
if (!resolved) {
|
|
56
|
-
if (env[
|
|
67
|
+
if (env[RUN_IF_PRESENT_ENV_VAR] === '1') {
|
|
57
68
|
return { exitCode: 0 };
|
|
58
69
|
}
|
|
59
70
|
reportError(`Unknown command: ${command}`, stderr);
|
|
60
71
|
return { exitCode: 1 };
|
|
61
72
|
}
|
|
62
|
-
const
|
|
73
|
+
const resolvedCommand = renderChain(resolved.steps);
|
|
74
|
+
const skipExitCode = handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout);
|
|
63
75
|
if (skipExitCode !== undefined) {
|
|
64
76
|
return { exitCode: skipExitCode };
|
|
65
77
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
78
|
+
const substitutedSteps = applyDevBinToSteps(resolved.steps, context.config.devBin, context.monorepoRoot);
|
|
79
|
+
const substitutedCommand = renderChain(substitutedSteps);
|
|
80
|
+
const mainSteps = appendPassthrough(substitutedSteps, parsed.passthrough);
|
|
68
81
|
const isHookInvocation = isHookName(command);
|
|
69
|
-
const
|
|
70
|
-
?
|
|
71
|
-
: wrapWithHooks(command,
|
|
82
|
+
const fullSteps = isHookInvocation
|
|
83
|
+
? mainSteps
|
|
84
|
+
: wrapWithHooks(command, mainSteps, registry, anchorDir, parsed.workspaceRoot);
|
|
85
|
+
const fullCommand = renderChain(fullSteps);
|
|
86
|
+
const shelledStep = findShelledNmrStep(fullSteps);
|
|
87
|
+
if (shelledStep !== undefined) {
|
|
88
|
+
stderr.write(`${formatShelledNmrWarning(command, shelledStep)}\n`);
|
|
89
|
+
}
|
|
72
90
|
const key = resolveCacheKey({
|
|
73
91
|
anchorDir,
|
|
74
92
|
command,
|
|
@@ -77,19 +95,20 @@ export async function runCli(options) {
|
|
|
77
95
|
monorepoRoot: context.monorepoRoot,
|
|
78
96
|
snapshot,
|
|
79
97
|
stderr,
|
|
80
|
-
substitution: substitutedCommand ===
|
|
98
|
+
substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
|
|
81
99
|
});
|
|
82
100
|
const exitCode = await runGated({
|
|
83
101
|
anchorDir,
|
|
84
102
|
command,
|
|
85
103
|
commandString: fullCommand,
|
|
86
104
|
config: context.config,
|
|
105
|
+
steps: fullSteps,
|
|
87
106
|
env,
|
|
88
107
|
key,
|
|
89
108
|
monorepoRoot: context.monorepoRoot,
|
|
90
109
|
noCache,
|
|
91
|
-
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir,
|
|
92
|
-
quiet
|
|
110
|
+
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, quiet),
|
|
111
|
+
quiet,
|
|
93
112
|
runOptions,
|
|
94
113
|
snapshot,
|
|
95
114
|
stderr,
|
|
@@ -97,19 +116,37 @@ export async function runCli(options) {
|
|
|
97
116
|
});
|
|
98
117
|
return { exitCode };
|
|
99
118
|
}
|
|
100
|
-
function buildChildEnv(env, snapshot, noCache) {
|
|
119
|
+
function buildChildEnv(env, snapshot, noCache, verbosity) {
|
|
101
120
|
return {
|
|
102
121
|
...env,
|
|
103
122
|
...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
|
|
104
123
|
...(noCache && { [NO_CACHE_ENV_VAR]: '1' }),
|
|
124
|
+
[COMMAND_VERBOSITY_ENV_VAR]: verbosity,
|
|
105
125
|
};
|
|
106
126
|
}
|
|
127
|
+
function appendPassthrough(steps, passthrough) {
|
|
128
|
+
const last = steps.at(-1);
|
|
129
|
+
if (passthrough.length === 0 || last === undefined) {
|
|
130
|
+
return steps;
|
|
131
|
+
}
|
|
132
|
+
const bound = last.kind === 'structural'
|
|
133
|
+
? { kind: 'structural', argv: [...last.argv, ...passthrough] }
|
|
134
|
+
: { kind: 'opaque', command: `${last.command} ${passthrough.map(shellQuote).join(' ')}` };
|
|
135
|
+
return [...steps.slice(0, -1), bound];
|
|
136
|
+
}
|
|
137
|
+
function composeDelegate(scope, command, passthrough) {
|
|
138
|
+
return { kind: 'structural', argv: ['pnpm', ...scope, 'exec', 'nmr', command, ...passthrough] };
|
|
139
|
+
}
|
|
107
140
|
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
108
141
|
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
109
142
|
if (quiet || resolved.source !== 'package' || registryEntry === undefined) {
|
|
110
143
|
return undefined;
|
|
111
144
|
}
|
|
112
|
-
return `📦 ${path.basename(anchorDir)}: Using override script: ${resolved.
|
|
145
|
+
return `📦 ${path.basename(anchorDir)}: Using override script: ${renderChain(resolved.steps)}\n`;
|
|
146
|
+
}
|
|
147
|
+
function formatShelledNmrWarning(command, step) {
|
|
148
|
+
return (`⚠️ ${command}: \`${step}\` runs nmr behind a shell, so nmr handles its output as a tool's rather than a ` +
|
|
149
|
+
`nested run's. A step list in \`.config/nmr.config.ts\` avoids it.`);
|
|
113
150
|
}
|
|
114
151
|
function handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout) {
|
|
115
152
|
if (resolvedCommand === '') {
|
|
@@ -130,10 +167,11 @@ function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
|
130
167
|
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
131
168
|
if (!resolved)
|
|
132
169
|
return false;
|
|
133
|
-
|
|
170
|
+
const chain = renderChain(resolved.steps);
|
|
171
|
+
return chain !== '' && chain !== ':';
|
|
134
172
|
}
|
|
135
173
|
async function lookUpRecordedPass(options) {
|
|
136
|
-
const { anchorDir, command, env, key, monorepoRoot, stderr } = options;
|
|
174
|
+
const { anchorDir, buildOutput, command, env, key, monorepoRoot, stderr } = options;
|
|
137
175
|
const entry = await readCheckCacheEntry({ anchorDir, command, monorepoRoot });
|
|
138
176
|
if (entry === undefined) {
|
|
139
177
|
writeDebugNote(`running ${command}: no pass recorded for this scope`, env, stderr);
|
|
@@ -143,13 +181,12 @@ async function lookUpRecordedPass(options) {
|
|
|
143
181
|
writeDebugNote(`running ${command}: the tree or its inputs changed since the last pass`, env, stderr);
|
|
144
182
|
return undefined;
|
|
145
183
|
}
|
|
146
|
-
const
|
|
147
|
-
const [missing] = output.missing;
|
|
184
|
+
const [missing] = buildOutput.missing;
|
|
148
185
|
if (missing !== undefined) {
|
|
149
186
|
writeDebugNote(`running ${command}: ${missing} has no build output`, env, stderr);
|
|
150
187
|
return undefined;
|
|
151
188
|
}
|
|
152
|
-
const stale = findStaleBuildOutput(entry.buildDigests,
|
|
189
|
+
const stale = findStaleBuildOutput(entry.buildDigests, buildOutput.digests);
|
|
153
190
|
if (stale !== undefined) {
|
|
154
191
|
writeDebugNote(`running ${command}: ${stale}'s build output came from a different tree`, env, stderr);
|
|
155
192
|
return undefined;
|
|
@@ -255,6 +292,11 @@ async function recordPass(options) {
|
|
|
255
292
|
writeDebugNote(`not recording ${command}: ${missing} has no build output`, env, stderr);
|
|
256
293
|
return;
|
|
257
294
|
}
|
|
295
|
+
const changed = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
|
|
296
|
+
if (changed !== undefined) {
|
|
297
|
+
writeDebugNote(`not recording ${command}: ${changed}'s build output changed while it ran`, env, stderr);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
258
300
|
try {
|
|
259
301
|
await writeCheckCacheEntry({
|
|
260
302
|
anchorDir,
|
|
@@ -304,14 +346,16 @@ function resolveCacheKey(options) {
|
|
|
304
346
|
}
|
|
305
347
|
async function runGated(options) {
|
|
306
348
|
const { anchorDir, command, commandString, env, key, monorepoRoot, snapshot, stderr, stdout } = options;
|
|
307
|
-
const
|
|
308
|
-
|
|
349
|
+
const gate = key !== undefined && snapshot !== undefined
|
|
350
|
+
? { buildOutputBefore: await readBuildOutputState(monorepoRoot, options.config), key, snapshot }
|
|
351
|
+
: undefined;
|
|
352
|
+
if (gate !== undefined && !options.noCache) {
|
|
309
353
|
const skipLine = await lookUpRecordedPass({
|
|
310
354
|
anchorDir,
|
|
355
|
+
buildOutput: gate.buildOutputBefore,
|
|
311
356
|
command,
|
|
312
|
-
config: options.config,
|
|
313
357
|
env,
|
|
314
|
-
key,
|
|
358
|
+
key: gate.key,
|
|
315
359
|
monorepoRoot,
|
|
316
360
|
stderr,
|
|
317
361
|
});
|
|
@@ -326,18 +370,19 @@ async function runGated(options) {
|
|
|
326
370
|
stdout.write(options.overrideNotice);
|
|
327
371
|
}
|
|
328
372
|
const startedAt = Date.now();
|
|
329
|
-
const exitCode =
|
|
330
|
-
if (exitCode === 0 &&
|
|
373
|
+
const { exitCode } = await runSteps(options.steps, anchorDir, options.runOptions);
|
|
374
|
+
if (exitCode === 0 && gate !== undefined) {
|
|
331
375
|
await recordPass({
|
|
332
376
|
anchorDir,
|
|
377
|
+
buildOutputBefore: gate.buildOutputBefore,
|
|
333
378
|
command,
|
|
334
379
|
commandString,
|
|
335
380
|
config: options.config,
|
|
336
381
|
durationMs: Date.now() - startedAt,
|
|
337
382
|
env,
|
|
338
|
-
key,
|
|
383
|
+
key: gate.key,
|
|
339
384
|
monorepoRoot,
|
|
340
|
-
snapshot,
|
|
385
|
+
snapshot: gate.snapshot,
|
|
341
386
|
stderr,
|
|
342
387
|
});
|
|
343
388
|
}
|
|
@@ -346,15 +391,14 @@ async function runGated(options) {
|
|
|
346
391
|
function shellQuote(arg) {
|
|
347
392
|
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
348
393
|
}
|
|
349
|
-
function wrapWithHooks(command,
|
|
350
|
-
const
|
|
351
|
-
const flag = workspaceRoot ? '-w ' : '';
|
|
394
|
+
function wrapWithHooks(command, mainSteps, registry, anchorDir, workspaceRoot) {
|
|
395
|
+
const steps = [];
|
|
352
396
|
if (hasRunnableHook(`${command}:pre`, registry, anchorDir, workspaceRoot)) {
|
|
353
|
-
|
|
397
|
+
steps.push(composeNmrStep(`${command}:pre`, workspaceRoot));
|
|
354
398
|
}
|
|
355
|
-
|
|
399
|
+
steps.push(...mainSteps);
|
|
356
400
|
if (hasRunnableHook(`${command}:post`, registry, anchorDir, workspaceRoot)) {
|
|
357
|
-
|
|
401
|
+
steps.push(composeNmrStep(`${command}:post`, workspaceRoot));
|
|
358
402
|
}
|
|
359
|
-
return
|
|
403
|
+
return steps;
|
|
360
404
|
}
|
package/dist/esm/runner.d.ts
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { Step } from './steps.js';
|
|
3
|
+
export type OutputChannel = number | 'pipe';
|
|
4
|
+
export interface OutputChannels {
|
|
5
|
+
stderr: OutputChannel;
|
|
6
|
+
stdout: OutputChannel;
|
|
7
|
+
}
|
|
2
8
|
export interface RunCommandOptions {
|
|
9
|
+
channels: OutputChannels;
|
|
3
10
|
quiet?: boolean;
|
|
4
11
|
stdout?: Writable;
|
|
5
12
|
stderr?: Writable;
|
|
6
13
|
env?: NodeJS.ProcessEnv;
|
|
7
14
|
}
|
|
8
|
-
export
|
|
15
|
+
export type RunStepsOptions = Omit<RunCommandOptions, 'channels'>;
|
|
16
|
+
type CommandCompletion = {
|
|
17
|
+
outcome: 'exited';
|
|
18
|
+
exitCode: number;
|
|
19
|
+
} | {
|
|
20
|
+
outcome: 'signaled';
|
|
21
|
+
exitCode: number;
|
|
22
|
+
signal: NodeJS.Signals;
|
|
23
|
+
} | {
|
|
24
|
+
outcome: 'spawn-failed';
|
|
25
|
+
exitCode: number;
|
|
26
|
+
error: Error;
|
|
27
|
+
};
|
|
28
|
+
export type RunCommandResult = CommandCompletion & {
|
|
29
|
+
stdout: Buffer | undefined;
|
|
30
|
+
stderr: Buffer | undefined;
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveChannel(stream: Writable, quiet: boolean): OutputChannel;
|
|
33
|
+
export declare function resolveInheritedChannel(stream: Writable): OutputChannel;
|
|
34
|
+
export declare function runSteps(steps: readonly Step[], cwd: string | undefined, options: RunStepsOptions): Promise<{
|
|
35
|
+
exitCode: number;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function runCommand(command: string, cwd: string | undefined, options: RunCommandOptions): Promise<RunCommandResult>;
|
|
38
|
+
export {};
|
package/dist/esm/runner.js
CHANGED
|
@@ -1,41 +1,153 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import process from 'node:process';
|
|
3
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
6
|
+
const SIGNAL_EXIT_BASE = 128;
|
|
7
|
+
const PIPE_DRAIN_GRACE_MS = 2_000;
|
|
8
|
+
export function resolveChannel(stream, quiet) {
|
|
9
|
+
if (quiet)
|
|
10
|
+
return 'pipe';
|
|
11
|
+
return isTerminal(stream) ? stream.fd : 'pipe';
|
|
12
|
+
}
|
|
13
|
+
export function resolveInheritedChannel(stream) {
|
|
14
|
+
return hasDescriptor(stream) ? stream.fd : 'pipe';
|
|
15
|
+
}
|
|
16
|
+
export async function runSteps(steps, cwd, options) {
|
|
17
|
+
const resolved = {
|
|
18
|
+
...options,
|
|
19
|
+
quiet: options.quiet === true,
|
|
20
|
+
stdout: options.stdout ?? process.stdout,
|
|
21
|
+
stderr: options.stderr ?? process.stderr,
|
|
22
|
+
};
|
|
23
|
+
for (const step of steps) {
|
|
24
|
+
const { exitCode } = await runStep(step, cwd, resolved);
|
|
25
|
+
if (exitCode !== 0) {
|
|
26
|
+
return { exitCode };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { exitCode: 0 };
|
|
30
|
+
}
|
|
31
|
+
export async function runCommand(command, cwd, options) {
|
|
32
|
+
return runSpawned({ args: [], file: command, useShell: true }, cwd, options);
|
|
33
|
+
}
|
|
34
|
+
async function runSpawned({ args, file, useShell }, cwd, options) {
|
|
35
|
+
const quiet = options.quiet === true;
|
|
36
|
+
const stdout = options.stdout ?? process.stdout;
|
|
37
|
+
const stderr = options.stderr ?? process.stderr;
|
|
38
|
+
const env = options.env ?? process.env;
|
|
39
|
+
const child = spawn(file, args, {
|
|
40
|
+
shell: useShell,
|
|
41
|
+
stdio: ['inherit', options.channels.stdout, options.channels.stderr],
|
|
14
42
|
cwd,
|
|
15
43
|
env,
|
|
16
44
|
});
|
|
17
|
-
|
|
45
|
+
const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
|
|
46
|
+
const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
|
|
47
|
+
const completion = await awaitCompletion(child, () => {
|
|
48
|
+
stdoutCapture?.stopReading();
|
|
49
|
+
stderrCapture?.stopReading();
|
|
50
|
+
});
|
|
51
|
+
stdoutCapture?.detach();
|
|
52
|
+
stderrCapture?.detach();
|
|
53
|
+
const result = {
|
|
54
|
+
...completion,
|
|
55
|
+
stdout: stdoutCapture?.toBuffer(),
|
|
56
|
+
stderr: stderrCapture?.toBuffer(),
|
|
57
|
+
};
|
|
58
|
+
if (result.outcome === 'spawn-failed') {
|
|
18
59
|
reportError(result.error.message, stderr);
|
|
19
|
-
return
|
|
60
|
+
return result;
|
|
20
61
|
}
|
|
21
|
-
if (quiet) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
62
|
+
if (quiet && result.exitCode !== 0) {
|
|
63
|
+
writeBuffer(result.stdout, stderr);
|
|
64
|
+
writeBuffer(result.stderr, stderr);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function runStep(step, cwd, options) {
|
|
69
|
+
const { quiet, stderr, stdout } = options;
|
|
70
|
+
if (step.kind === 'opaque') {
|
|
71
|
+
return runSpawned({ args: [], file: step.command, useShell: true }, cwd, {
|
|
72
|
+
...options,
|
|
73
|
+
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
74
|
+
});
|
|
26
75
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
76
|
+
const [file, ...args] = step.argv;
|
|
77
|
+
return runSpawned({ args, file, useShell: false }, cwd, {
|
|
78
|
+
...options,
|
|
79
|
+
quiet: false,
|
|
80
|
+
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function captureStream(source, destination) {
|
|
84
|
+
const retained = createBoundedBuffer();
|
|
85
|
+
let isComplete = true;
|
|
86
|
+
function stopReading() {
|
|
87
|
+
if (destination !== undefined)
|
|
88
|
+
source.unpipe(destination);
|
|
89
|
+
source.destroy();
|
|
90
|
+
}
|
|
91
|
+
function abandon() {
|
|
92
|
+
isComplete = false;
|
|
93
|
+
stopReading();
|
|
94
|
+
}
|
|
95
|
+
source.on('error', abandon);
|
|
96
|
+
if (destination !== undefined) {
|
|
97
|
+
destination.on('error', abandon);
|
|
98
|
+
source.pipe(destination, { end: false });
|
|
99
|
+
}
|
|
100
|
+
source.on('data', (chunk) => retained.append(chunk));
|
|
101
|
+
return {
|
|
102
|
+
toBuffer: () => (isComplete ? retained.toBuffer() : undefined),
|
|
103
|
+
detach: () => {
|
|
104
|
+
destination?.off('error', abandon);
|
|
105
|
+
},
|
|
106
|
+
stopReading,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function awaitCompletion(child, stopReading) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
let exitCompletion;
|
|
112
|
+
let graceTimer;
|
|
113
|
+
let isSettled = false;
|
|
114
|
+
function settle(completion) {
|
|
115
|
+
if (isSettled)
|
|
116
|
+
return;
|
|
117
|
+
isSettled = true;
|
|
118
|
+
if (graceTimer !== undefined)
|
|
119
|
+
clearTimeout(graceTimer);
|
|
120
|
+
resolve(completion);
|
|
121
|
+
}
|
|
122
|
+
child.on('error', (error) => {
|
|
123
|
+
settle({ outcome: 'spawn-failed', exitCode: 1, error });
|
|
124
|
+
});
|
|
125
|
+
child.on('exit', (code, signal) => {
|
|
126
|
+
const completion = describeExit(code, signal);
|
|
127
|
+
exitCompletion = completion;
|
|
128
|
+
graceTimer = setTimeout(() => {
|
|
129
|
+
stopReading();
|
|
130
|
+
settle(completion);
|
|
131
|
+
}, PIPE_DRAIN_GRACE_MS);
|
|
132
|
+
});
|
|
133
|
+
child.on('close', () => {
|
|
134
|
+
settle(exitCompletion ?? { outcome: 'exited', exitCode: 1 });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function describeExit(code, signal) {
|
|
139
|
+
if (signal !== null) {
|
|
140
|
+
return { outcome: 'signaled', exitCode: SIGNAL_EXIT_BASE + os.constants.signals[signal], signal };
|
|
32
141
|
}
|
|
33
|
-
return
|
|
142
|
+
return { outcome: 'exited', exitCode: code ?? 1 };
|
|
143
|
+
}
|
|
144
|
+
function hasDescriptor(stream) {
|
|
145
|
+
return 'fd' in stream && typeof stream.fd === 'number';
|
|
34
146
|
}
|
|
35
|
-
function
|
|
36
|
-
return '
|
|
147
|
+
function isTerminal(stream) {
|
|
148
|
+
return 'isTTY' in stream && stream.isTTY === true && hasDescriptor(stream);
|
|
37
149
|
}
|
|
38
|
-
function writeBuffer(buffer,
|
|
39
|
-
if (buffer.length > 0)
|
|
40
|
-
|
|
150
|
+
function writeBuffer(buffer, destination) {
|
|
151
|
+
if (buffer !== undefined && buffer.length > 0)
|
|
152
|
+
destination.write(buffer);
|
|
41
153
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Step = {
|
|
2
|
+
kind: 'opaque';
|
|
3
|
+
command: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'structural';
|
|
6
|
+
argv: readonly [string, ...(readonly string[])];
|
|
7
|
+
};
|
|
8
|
+
export declare function composeNmrStep(element: string, workspaceRoot: boolean): Step;
|
|
9
|
+
export declare function findShelledNmrStep(steps: readonly Step[]): string | undefined;
|
|
10
|
+
export declare function findUnexpressibleToken(element: string): string | undefined;
|
|
11
|
+
export declare function renderChain(steps: readonly Step[]): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
2
|
+
export function composeNmrStep(element, workspaceRoot) {
|
|
3
|
+
const flags = workspaceRoot ? ['-w'] : [];
|
|
4
|
+
return { kind: 'structural', argv: ['nmr', ...flags, ...tokenize(element)] };
|
|
5
|
+
}
|
|
6
|
+
export function findShelledNmrStep(steps) {
|
|
7
|
+
for (const step of steps) {
|
|
8
|
+
if (step.kind === 'opaque' && tokenize(step.command)[0] === 'nmr') {
|
|
9
|
+
return step.command;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
export function findUnexpressibleToken(element) {
|
|
15
|
+
return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
|
|
16
|
+
}
|
|
17
|
+
export function renderChain(steps) {
|
|
18
|
+
return steps.map(renderStep).join(' && ');
|
|
19
|
+
}
|
|
20
|
+
function quoteToken(token) {
|
|
21
|
+
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
22
|
+
return token;
|
|
23
|
+
}
|
|
24
|
+
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
25
|
+
}
|
|
26
|
+
function renderStep(step) {
|
|
27
|
+
return step.kind === 'opaque' ? step.command : step.argv.map(quoteToken).join(' ');
|
|
28
|
+
}
|
|
29
|
+
function tokenize(element) {
|
|
30
|
+
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const COMMAND_VERBOSITY_ENV_VAR = "NMR_COMMAND_VERBOSITY";
|
|
2
|
+
export declare const COMMAND_VERBOSITIES: readonly ["full", "quiet"];
|
|
3
|
+
export type CommandVerbosity = (typeof COMMAND_VERBOSITIES)[number];
|
|
4
|
+
export type VerbosityResolution = {
|
|
5
|
+
ok: true;
|
|
6
|
+
verbosity: CommandVerbosity;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveVerbosity(env: NodeJS.ProcessEnv, quietFlag: boolean): VerbosityResolution;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const COMMAND_VERBOSITY_ENV_VAR = 'NMR_COMMAND_VERBOSITY';
|
|
2
|
+
export const COMMAND_VERBOSITIES = ['full', 'quiet'];
|
|
3
|
+
export function resolveVerbosity(env, quietFlag) {
|
|
4
|
+
const raw = env[COMMAND_VERBOSITY_ENV_VAR];
|
|
5
|
+
const inherited = raw === undefined || raw === '' ? 'full' : raw;
|
|
6
|
+
if (!isCommandVerbosity(inherited)) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
error: `${COMMAND_VERBOSITY_ENV_VAR} is \`${inherited}\`, which is not one of: ${COMMAND_VERBOSITIES.join(', ')}`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return { ok: true, verbosity: quietFlag ? 'quiet' : inherited };
|
|
13
|
+
}
|
|
14
|
+
function isCommandVerbosity(value) {
|
|
15
|
+
const names = COMMAND_VERBOSITIES;
|
|
16
|
+
return names.includes(value);
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"glob": "13.0.6",
|
|
68
68
|
"prettier-plugin-sh": "0.19.0",
|
|
69
|
-
"taze": "19.
|
|
69
|
+
"taze": "19.17.1",
|
|
70
70
|
"yaml": "2.9.0",
|
|
71
71
|
"@williamthorsen/nmr-core": "0.9.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"prettier": "3.9.6",
|
|
75
|
-
"readyup": "0.
|
|
75
|
+
"readyup": "0.26.0",
|
|
76
76
|
"tinyglobby": "0.2.17",
|
|
77
77
|
"vitest": "4.1.10"
|
|
78
78
|
},
|