@williamthorsen/nmr 0.37.0 โ†’ 0.38.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.
Files changed (45) hide show
  1. package/.meta/changelog.json +30 -0
  2. package/.readyup/kits/default.js +9 -9
  3. package/.readyup/manifest.json +5 -5
  4. package/CHANGELOG.md +19 -0
  5. package/README.md +17 -34
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache.d.ts +2 -1
  8. package/dist/esm/check-cache.js +4 -4
  9. package/dist/esm/cli-build.js +3 -0
  10. package/dist/esm/cli-clean.js +3 -1
  11. package/dist/esm/cli-ensure-prepublish-hooks.js +3 -1
  12. package/dist/esm/cli-report-catalog.js +2 -1
  13. package/dist/esm/cli-report-overrides.js +2 -1
  14. package/dist/esm/commands/build.d.ts +6 -1
  15. package/dist/esm/commands/build.js +9 -10
  16. package/dist/esm/commands/clean.d.ts +3 -2
  17. package/dist/esm/commands/clean.js +15 -14
  18. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +3 -2
  19. package/dist/esm/commands/ensure-prepublish-hooks.js +8 -6
  20. package/dist/esm/commands/report-catalog.d.ts +2 -1
  21. package/dist/esm/commands/report-catalog.js +5 -4
  22. package/dist/esm/commands/report-overrides.d.ts +2 -1
  23. package/dist/esm/commands/report-overrides.js +5 -4
  24. package/dist/esm/config.js +14 -4
  25. package/dist/esm/default-scripts.d.ts +2 -2
  26. package/dist/esm/default-scripts.js +5 -5
  27. package/dist/esm/glyphs.d.ts +9 -0
  28. package/dist/esm/glyphs.js +9 -0
  29. package/dist/esm/help.js +15 -9
  30. package/dist/esm/output-style.d.ts +41 -0
  31. package/dist/esm/output-style.js +17 -0
  32. package/dist/esm/recording.d.ts +3 -0
  33. package/dist/esm/recording.js +6 -3
  34. package/dist/esm/resolveBinStyles.d.ts +12 -0
  35. package/dist/esm/resolveBinStyles.js +16 -0
  36. package/dist/esm/resolver.js +2 -2
  37. package/dist/esm/runCli.js +82 -60
  38. package/dist/esm/steps.d.ts +5 -5
  39. package/dist/esm/steps.js +6 -2
  40. package/dist/esm/verdict.d.ts +6 -2
  41. package/dist/esm/verdict.js +16 -10
  42. package/docs/check-cache.md +5 -5
  43. package/docs/reporting.md +14 -0
  44. package/docs/scripts.md +2 -2
  45. package/package.json +8 -8
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "version": "0.38.0",
4
+ "date": "2026-09-18",
5
+ "sections": [
6
+ {
7
+ "title": "๐ŸŽ‰ Features",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Give nmr plain output in pipes and CI logs (#838)",
12
+ "body": "- Stops `nmr` from printing emoji to a pipe or a CI log: When the output stream is not a terminal or `CI` is set, a verdict reads `PASS`, `FAIL`, `SKIP`, or `NOOP` and a command icon is omitted, so that a log can be searched for a verdict.\n- Adds `--output-style <auto|plain|rich>` to `nmr` and the `NMR_OUTPUT_STYLE` variable, each forcing either style; the flag outranks the variable, a value that names no style exits 1 with a usage error, and neither enters a check-cache key: A rich run and a plain run share a recorded pass.\n- Replaces several rich glyphs, taking the status markers from the set that release-kit and v11y-check print so that one status has one glyph across the three tools: a recalled pass shows โฉ in place of โญ๏ธ, a skipped empty override shows โšช in place of โ›”, a warning and the `--log` refusal show ๐ŸŸ  in place of โš ๏ธ and ๐Ÿ“ญ, the `--log` header shows ๐Ÿ’พ in place of ๐Ÿ“ผ, and `nmr-ensure-prepublish-hooks` reports โœ… and โŒ in place of โœ“ and โœ—.",
13
+ "hash": "693b3a7f57def3d299ced240c19bda90f6253bdc"
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "title": "๐Ÿชฆ Removed",
19
+ "audience": "all",
20
+ "items": [
21
+ {
22
+ "description": "Rename the declinesArgs step declaration to shouldDeclineArguments (#822)",
23
+ "body": "- Makes a `.config/nmr.config.ts` that still declares `declinesArgs` on a composite script's `{ run, declinesArgs }` step fail to load, with an error that names `shouldDeclineArguments` as the replacement.\n- Renames the matching field of the `StepSpec` type exported by `@williamthorsen/nmr/config` and `@williamthorsen/nmr/scripts`.\n\nMigration: Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.",
24
+ "breaking": true,
25
+ "migration": "Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.",
26
+ "hash": "b12333390e731c1a9d7055d293b686028c89ad26"
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
2
32
  {
3
33
  "version": "0.37.0",
4
34
  "date": "2026-09-15",
@@ -1,18 +1,18 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.36.0";
3
+ export const __readyupVersion = "0.38.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
7
7
  import { existsSync, globSync, readdirSync as readdirSync2 } from "node:fs";
8
8
  import { basename, dirname, join, posix, sep } from "node:path";
9
9
 
10
- // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.7.0/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
10
+ // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.1/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
11
11
  function isError(error) {
12
12
  return error instanceof Error || Object.prototype.toString.call(error) === "[object Error]";
13
13
  }
14
14
 
15
- // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.7.0/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/describeError.js
15
+ // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.1/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/describeError.js
16
16
  function describeError(error) {
17
17
  try {
18
18
  if (isError(error) && typeof error.message === "string" && error.message !== "") {
@@ -41,8 +41,8 @@ import {
41
41
 
42
42
  // src/default-scripts.ts
43
43
  var GATE_PROJECTS = "--project unit --project tool";
44
- var TYPECHECK_STEP = { run: "typecheck", declinesArgs: true };
45
- var ROOT_TYPECHECK_STEP = { run: "root:typecheck", declinesArgs: true };
44
+ var TYPECHECK_STEP = { run: "typecheck", shouldDeclineArguments: true };
45
+ var ROOT_TYPECHECK_STEP = { run: "root:typecheck", shouldDeclineArguments: true };
46
46
  var workspaceScripts = {
47
47
  build: ["compile"],
48
48
  check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
@@ -77,7 +77,7 @@ var rootScripts = {
77
77
  "check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
78
78
  // Excludes the audit, which in CI has a workflow of its own. The build is what the narrowed check runs
79
79
  // against, so it declines the arguments rather than being narrowed by them.
80
- ci: [{ run: "build", declinesArgs: true }, "check:strict"],
80
+ ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
81
81
  clean: "nmr-clean",
82
82
  fix: ["lint", "fmt"],
83
83
  "fix:check": ["fmt:check", "lint:check"],
@@ -88,7 +88,7 @@ var rootScripts = {
88
88
  "lint:strict": "strict-lint",
89
89
  // The audit costs seconds and `ci` costs minutes, so the cheap gate fails first. The audit reads the
90
90
  // dependency tree, which no argument narrowing the code under test says anything about.
91
- prepush: [{ run: "audit", declinesArgs: true }, "ci"],
91
+ prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
92
92
  "report-overrides": "nmr-report-overrides",
93
93
  "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
94
94
  "root:lint": "eslint --fix --ignore-pattern 'packages/**' .",
@@ -110,7 +110,7 @@ var rootScripts = {
110
110
  "test:watch": `vitest ${GATE_PROJECTS} --watch`,
111
111
  // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
112
112
  // default options at the root and hunting for it in every package.
113
- typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", declinesArgs: true }],
113
+ typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
114
114
  // The command is a string because neither half names an nmr command: both are binaries, and a composite
115
115
  // element can name only a command.
116
116
  upgrade: "nmr-report-overrides && nmr-taze --recursive"
@@ -597,7 +597,7 @@ function formatPaths(paths) {
597
597
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
598
598
  }
599
599
  function getMinVersion() {
600
- const picked = { "version": "0.37.0" };
600
+ const picked = { "version": "0.38.0" };
601
601
  if (typeof picked["version"] !== "string") {
602
602
  throw new TypeError("nmr/package.json: 'version' must be a string");
603
603
  }
@@ -3,7 +3,7 @@
3
3
  "kits": [
4
4
  {
5
5
  "bundledDependencies": {
6
- "@williamthorsen/toolbelt.errors": "0.7.0"
6
+ "@williamthorsen/toolbelt.errors": "0.8.1"
7
7
  },
8
8
  "checklists": [
9
9
  "nmr"
@@ -16,7 +16,7 @@
16
16
  "path": "kits/default.ts"
17
17
  },
18
18
  {
19
- "hash": "d4b2e208",
19
+ "hash": "521b947a",
20
20
  "kind": "inline",
21
21
  "path": "../package.json",
22
22
  "paths": [
@@ -24,7 +24,7 @@
24
24
  ]
25
25
  },
26
26
  {
27
- "hash": "74bd7994",
27
+ "hash": "06bffeed",
28
28
  "kind": "module",
29
29
  "path": "../src/default-scripts.ts"
30
30
  },
@@ -41,10 +41,10 @@
41
41
  ],
42
42
  "name": "default",
43
43
  "path": "kits/default.js",
44
- "readyupVersion": "0.36.0",
44
+ "readyupVersion": "0.38.0",
45
45
  "source": "kits/default.ts",
46
46
  "sourceHash": "488459c0",
47
- "targetHash": "d1c8d02c"
47
+ "targetHash": "cbef780a"
48
48
  }
49
49
  ]
50
50
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.38.0 โ€” 2026-09-18
6
+
7
+ ### ๐ŸŽ‰ Features
8
+
9
+ - Give nmr plain output in pipes and CI logs (#838)
10
+
11
+ - Stops `nmr` from printing emoji to a pipe or a CI log: When the output stream is not a terminal or `CI` is set, a verdict reads `PASS`, `FAIL`, `SKIP`, or `NOOP` and a command icon is omitted, so that a log can be searched for a verdict.
12
+ - Adds `--output-style <auto|plain|rich>` to `nmr` and the `NMR_OUTPUT_STYLE` variable, each forcing either style; the flag outranks the variable, a value that names no style exits 1 with a usage error, and neither enters a check-cache key: A rich run and a plain run share a recorded pass.
13
+ - Replaces several rich glyphs, taking the status markers from the set that release-kit and v11y-check print so that one status has one glyph across the three tools: a recalled pass shows โฉ in place of โญ๏ธ, a skipped empty override shows โšช in place of โ›”, a warning and the `--log` refusal show ๐ŸŸ  in place of โš ๏ธ and ๐Ÿ“ญ, the `--log` header shows ๐Ÿ’พ in place of ๐Ÿ“ผ, and `nmr-ensure-prepublish-hooks` reports โœ… and โŒ in place of โœ“ and โœ—.
14
+
15
+ ### ๐Ÿชฆ Removed
16
+
17
+ - ๐Ÿšจ **Breaking:** Rename the declinesArgs step declaration to shouldDeclineArguments (#822)
18
+
19
+ - Makes a `.config/nmr.config.ts` that still declares `declinesArgs` on a composite script's `{ run, declinesArgs }` step fail to load, with an error that names `shouldDeclineArguments` as the replacement.
20
+ - Renames the matching field of the `StepSpec` type exported by `@williamthorsen/nmr/config` and `@williamthorsen/nmr/scripts`.
21
+
22
+ Migration: Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.
23
+
5
24
  ## 0.37.0 โ€” 2026-09-15
6
25
 
7
26
  ### ๐ŸŽ‰ Features
package/README.md CHANGED
@@ -5,43 +5,24 @@
5
5
  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.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes โ€” v0.37.0 (2026-09-15)
8
+ ## Release notes โ€” v0.38.0 (2026-09-18)
9
9
 
10
10
  ### ๐ŸŽ‰ Features
11
11
 
12
- - Guard lefthook install in root prepare and report unguarded installs (#798)
12
+ - Give nmr plain output in pipes and CI logs (#838)
13
13
 
14
- - Adds a `warn` check to the readyup kit of `@williamthorsen/nmr` that reports a root script run by `pnpm install`, such as `prepare` or `postinstall`, whose command calls `lefthook install` without `lefthook check-install`.
15
- - Includes in the check's suggested fix the guarded form `lefthook check-install || lefthook install`, which skips rewriting `.git/hooks` while `lefthook.yml` is unchanged since the last install, so that a repo using that form can run `pnpm install` without write access to `.git/hooks`, as inside the Claude Code sandbox, and under pnpm's default `verifyDepsBeforeRun` avoids a reinstall before every `pnpm exec`.
14
+ - Stops `nmr` from printing emoji to a pipe or a CI log: When the output stream is not a terminal or `CI` is set, a verdict reads `PASS`, `FAIL`, `SKIP`, or `NOOP` and a command icon is omitted, so that a log can be searched for a verdict.
15
+ - Adds `--output-style <auto|plain|rich>` to `nmr` and the `NMR_OUTPUT_STYLE` variable, each forcing either style; the flag outranks the variable, a value that names no style exits 1 with a usage error, and neither enters a check-cache key: A rich run and a plain run share a recorded pass.
16
+ - Replaces several rich glyphs, taking the status markers from the set that release-kit and v11y-check print so that one status has one glyph across the three tools: a recalled pass shows โฉ in place of โญ๏ธ, a skipped empty override shows โšช in place of โ›”, a warning and the `--log` refusal show ๐ŸŸ  in place of โš ๏ธ and ๐Ÿ“ญ, the `--log` header shows ๐Ÿ’พ in place of ๐Ÿ“ผ, and `nmr-ensure-prepublish-hooks` reports โœ… and โŒ in place of โœ“ and โœ—.
16
17
 
17
- - Report misplaced test files and a missing guard from the readyup kit (#804)
18
+ ### ๐Ÿชฆ Removed
18
19
 
19
- - Adds an `error` check to the `@williamthorsen/nmr` readyup kit that reports each `*.test.ts` or `*.test.tsx` file outside a `__tests__` directory, which no Vitest project collects and which therefore runs nowhere.
20
- - Adds a `warn` check that passes once a test file under `__tests__` imports `checkTestFileConventions` from `@williamthorsen/nmr/tests`, and whose fix shows the one-line guard test to add.
21
- - Makes the kit skip both test-file checks once it finds that guard, because the guard reports the same files and applies the repo's `exclude` list, which the kit cannot read.
20
+ - ๐Ÿšจ **Breaking:** Rename the declinesArgs step declaration to shouldDeclineArguments (#822)
22
21
 
23
- ### ๐Ÿ› Bug fixes
22
+ - Makes a `.config/nmr.config.ts` that still declares `declinesArgs` on a composite script's `{ run, declinesArgs }` step fail to load, with an error that names `shouldDeclineArguments` as the replacement.
23
+ - Renames the matching field of the `StepSpec` type exported by `@williamthorsen/nmr/config` and `@williamthorsen/nmr/scripts`.
24
24
 
25
- - Fail when a -F pattern selects no workspace (#785)
26
-
27
- - Fixes a run that reported success without doing anything. `nmr -F <pattern> <command>` printed nothing and exited 0 when the pattern matched no package. It now exits 1, names the rejected pattern, and explains the likely cause.
28
- - Covers three further selections that ran nothing and exited 0: an exclusion that leaves only the workspace root, such as `nmr -F '!./packages/*'`; `-R` in a workspace that declares no package; and an empty `-F ''`.
29
-
30
- Migration: Replace any `-F` pattern that names a directory with the package's manifest `name`. A filter that selects nothing now fails the run instead of passing in silence.
31
-
32
- - Fix test runs failing on a dependency with a TypeScript source export (#793)
33
-
34
- - Fixes the issue that, with `resolveFromSource` at its default of `true`, every test in a file failed with `Stripping types is currently unsupported for files under node_modules` when the file's imports reached a dependency whose `exports` map declares a `source` condition pointing at TypeScript.
35
- - Limits the source resolution enabled by `resolveFromSource` to packages whose real path lies outside every `node_modules`, which covers workspace packages and packages linked into the repo from elsewhere.
36
- - Makes an import of a workspace package fail when that package's `source` condition names a missing file, instead of resolving to the build output without warning.
37
- - Makes conditions that a config adds to `resolve.conditions` or `ssr.resolve.conditions` extend Vite's defaults when `resolveFromSource` is `false`, instead of replacing them.
38
-
39
- Migration: Point each workspace package's `source` condition at a file that exists, or remove the condition. Build a workspace package before running a test that reaches it through an externalized dependency (one that Vitest loads through Node instead of transforming): Node resolves that import without the `source` condition, so it loads the package's build output.
40
-
41
- - Prevent a recursive or multi-package run from hanging under pnpm 12 (#814)
42
-
43
- - Fixes a hang under pnpm 12: In a terminal, `nmr -R <command>`, a root command that runs in every package such as `nmr build`, and a `-F` pattern that selects several packages printed nothing and waited until Ctrl+C when a package command read keyboard input or changed the terminal's mode, as the spinner in `next build` does.
44
- - Gives no terminal input to each package of a `-R` run or a multi-package `-F` run, including a dependency-chain filter such as `-F 'app...'`, and keeps it for a `-F` filter that selects one package, which an interactive command such as `nmr -F <pkg> test:watch` needs.
25
+ Migration: Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.
45
26
  <!-- /section:release-notes -->
46
27
 
47
28
  ## Installation
@@ -176,13 +157,15 @@ Every command nmr runs reports one line naming the scope it ran at, the command,
176
157
  ```console
177
158
  โœ… nmr-core: test: passed in 12.4s
178
159
  โŒ nmr: build: failed in 1.2s (exit 1)
179
- โญ๏ธ nmr-core: test: passed 4m ago on this tree, saved ~12s
180
- โ›” nmr-core: lint: skipped, the override is empty
160
+ โฉ nmr-core: test: passed 4m ago on this tree, saved ~12s
161
+ โšช nmr-core: lint: skipped, the override is empty
181
162
  ```
182
163
 
164
+ **A pipe and a CI log get words rather than emoji.** The same four lines read `PASS`, `FAIL`, `SKIP`, and `NOOP` there, which `grep FAIL` finds. `--output-style` and `NMR_OUTPUT_STYLE` each force either style.
165
+
183
166
  **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
184
167
 
185
- [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, and where the verbosity of a run comes from.
168
+ [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, the output style, and where the verbosity of a run comes from.
186
169
 
187
170
  ## Check-result cache
188
171
 
@@ -193,7 +176,7 @@ $ nmr ci
193
176
  # ... four minutes of build, typecheck, lint, and tests ...
194
177
 
195
178
  $ nmr ci
196
- โญ๏ธ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
179
+ โฉ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
197
180
  ```
198
181
 
199
182
  [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.
@@ -297,7 +280,7 @@ rdy list --from npm:@williamthorsen/nmr # what nmr publishes
297
280
  ## Documentation
298
281
 
299
282
  - [Scripts and configuration](docs/scripts.md): override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
300
- - [What nmr reports](docs/reporting.md): verdicts, `--json`, and verbosity
283
+ - [What nmr reports](docs/reporting.md): verdicts, `--json`, output style, and verbosity
301
284
  - [Check-result cache](docs/check-cache.md): what is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
302
285
  - [Dependency upgrades](docs/upgrades.md): `nmr upgrade`, the upgrade policy, `report-catalog`, `report-overrides`, and `nmr-taze`
303
286
  - [Standalone utilities](docs/utilities.md): `nmr-clean`, `nmr-compile`, `nmr-fmt`, and `ensure-prepublish-hooks`
@@ -10,8 +10,8 @@ delivery: ambient
10
10
  - Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
11
11
  - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
12
12
  - `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs the audit first, then `ci`, so a vulnerability stops the run before the long gate. Neither installs or binds a git hook.
13
- - A check that already passed on the current working tree skips, printing `โญ๏ธ <scope>: <command>: passed โ€ฆ` and, where the recalled run left one, `โ€” replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
13
+ - A check that already passed on the current working tree skips, printing `SKIP <scope>: <command>: passed โ€ฆ` and, where the recalled run left one, `โ€” replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
14
14
  - `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it -- so output you need in full is a recording to read rather than a run to pay for again. It runs nothing, and it is held to the tree rather than to the terminal: a recording a piped run made prints for a reader at one. A composite prints its constituents' excerpts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. Nothing to show is said on one line and exits non-zero, naming which it is.
15
- - Every command nmr runs reports one line -- `โœ… <scope>: <command>: passed in 12.4s`, `โŒ โ€ฆ failed in 1.2s (exit 1)`, `โญ๏ธ โ€ฆ passed 4m ago on this tree`, or `โ›” โ€ฆ skipped, the override is empty` -- and a composite reports alongside every command it expands into. The lines print in every verbosity, so `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
15
+ - Every command 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 it expands into. Those are the words 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 `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
16
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 the test reaches -- `tool` (a program the environment supplies), `localhost` (a service 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 --packages` reports both and warns that the check is missing.
17
17
  - 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 (there is no repo-local build script); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; and `check-cache.md` for the check-result cache's key, the retention key a replayed excerpt is additionally held to, the retention ceiling `--log` prints within, its `checkCache` config, and every condition that disables it.
@@ -1,4 +1,5 @@
1
1
  import type { Writable } from 'node:stream';
2
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
3
  import type { OutputChannels } from './runner.js';
3
4
  import type { CheckCacheConfig, NmrConfig } from './types.js';
4
5
  /** A recorded pass: what ran, on which tree, and what it cost. */
@@ -159,7 +160,7 @@ export declare function findStaleBuildOutput(earlier: Record<string, string>, la
159
160
  * command rather than a flag to nmr. Passing it on unchanged is the honest thing to do with an argument, so
160
161
  * the warning is all that separates this from a silently un-bypassed run.
161
162
  */
162
- export declare function formatMisplacedNoCacheWarning(command: string): string;
163
+ export declare function formatMisplacedNoCacheWarning(command: string, style: OutputStyle): string;
163
164
  /**
164
165
  * Reports whether a command's passes are recorded at all, which is what separates a command with no recording
165
166
  * from one that could never have had one.
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { readFileSync, realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
- import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
5
+ import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
6
  import { describeError } from '@williamthorsen/toolbelt.errors';
7
7
  import { hasBuildOutput, readBuildDigest } from "./commands/build-output.js";
8
8
  import { loadWorkspaceConfig } from "./config.js";
@@ -107,9 +107,9 @@ export function findStaleBuildOutput(earlier, later) {
107
107
  const names = [...new Set([...Object.keys(earlier), ...Object.keys(later)])].toSorted();
108
108
  return names.find((name) => earlier[name] !== later[name]);
109
109
  }
110
- export function formatMisplacedNoCacheWarning(command) {
111
- return (`โš ๏ธ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
112
- `Did you mean \`nmr --no-cache ${command}\`?`);
110
+ export function formatMisplacedNoCacheWarning(command, style) {
111
+ return (`${STATUS_GLYPHS[style].warning.text} --no-cache after the command name is passed to \`${command}\`, ` +
112
+ `not read by nmr. Did you mean \`nmr --no-cache ${command}\`?`);
113
113
  }
114
114
  export function isCacheableCommand(checkCache, command) {
115
115
  return !isHookName(command) && checkCache?.enabled !== false && resolveCacheableCommands(checkCache).has(command);
@@ -2,10 +2,13 @@ import { reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { buildPackage } from "./commands/build.js";
4
4
  import { loadWorkspaceConfig } from "./config.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
7
+ const { stdout } = resolveBinStyles();
6
8
  const packageDir = process.cwd();
7
9
  const { build } = await loadWorkspaceConfig(packageDir);
8
10
  await buildPackage(packageDir, {
11
+ style: stdout,
9
12
  ...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
10
13
  });
11
14
  }
@@ -1,8 +1,10 @@
1
+ import process from 'node:process';
1
2
  import { reportError } from '@williamthorsen/nmr-core';
2
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { runClean } from "./commands/clean.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
6
  try {
5
- await runClean(process.cwd());
7
+ await runClean(process.cwd(), resolveBinStyles().stdout);
6
8
  }
7
9
  catch (error) {
8
10
  reportError(describeError(error));
@@ -1,6 +1,7 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { DEFAULT_HOOK, ensurePrepublishHooks, reportPrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
4
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
5
6
  const flagSchema = {
6
7
  fix: { long: '--fix', type: 'boolean' },
@@ -9,10 +10,11 @@ const flagSchema = {
9
10
  };
10
11
  const { fix, dryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
11
12
  try {
13
+ const { stdout } = resolveBinStyles();
12
14
  const monorepoRoot = findMonorepoRoot();
13
15
  const options = command ? { fix, dryRun, command } : { fix, dryRun };
14
16
  const result = ensurePrepublishHooks(monorepoRoot, options);
15
- reportPrepublishHooks(result, command ?? DEFAULT_HOOK);
17
+ reportPrepublishHooks(result, command ?? DEFAULT_HOOK, stdout);
16
18
  if (result.hasFailures) {
17
19
  process.exitCode = 1;
18
20
  }
@@ -2,8 +2,9 @@ import process from 'node:process';
2
2
  import { reportError } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { reportCatalog } from "./commands/report-catalog.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
6
- reportCatalog(process.cwd());
7
+ reportCatalog(process.cwd(), resolveBinStyles().stderr);
7
8
  }
8
9
  catch (error) {
9
10
  reportError(describeError(error));
@@ -1,10 +1,11 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { reportOverrides } from "./commands/report-overrides.js";
4
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
5
6
  try {
6
7
  const monorepoRoot = findMonorepoRoot();
7
- reportOverrides(monorepoRoot);
8
+ reportOverrides(monorepoRoot, resolveBinStyles().stderr);
8
9
  }
9
10
  catch (error) {
10
11
  reportError(describeError(error));
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import { type BuildOptions } from './build-output.js';
2
3
  /**
3
4
  * The toolchain a build's emit comes from: the resolved compiler and the nmr running it. Both join the build
@@ -7,6 +8,10 @@ export interface BuildToolchain {
7
8
  compilerVersion: string;
8
9
  fingerprint: string;
9
10
  }
11
+ /** What a build needs beyond its sources: the options shaping the emit, and the style it reports in. */
12
+ export interface BuildPackageOptions extends BuildOptions {
13
+ style: OutputStyle;
14
+ }
10
15
  /**
11
16
  * Compiles a package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and
12
17
  * `.d.ts` from two programs and rewriting relative `.ts` specifiers and tsconfig `paths` aliases to
@@ -17,7 +22,7 @@ export interface BuildToolchain {
17
22
  * current inputs rather than an accumulation of every build that ever ran. Assets belong outside it, or in a
18
23
  * `build:post` hook, which runs after the output is published.
19
24
  */
20
- export declare function buildPackage(packageDir: string, options?: BuildOptions): Promise<void>;
25
+ export declare function buildPackage(packageDir: string, options: BuildPackageOptions): Promise<void>;
21
26
  /**
22
27
  * Produces a digest of the given files (paths and contents), the emit config, and the toolchain.
23
28
  * The file list is sorted so the digest is order-invariant, and each path is folded in so renames are detected.
@@ -2,19 +2,18 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { readFile, rename, rm } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { readCacheEntry, writeCacheEntry } from '@williamthorsen/nmr-core';
5
+ import { formatGlyphLine, readCacheEntry, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
6
  import { glob } from 'glob';
7
7
  import * as ts from 'typescript';
8
+ import { NMR_GLYPHS } from "../glyphs.js";
8
9
  import { resolveConfigPath } from "../helpers/config-path.js";
9
10
  import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, resolveScratchDirs, resolveToolchainFingerprint, } from "./build-output.js";
10
- const PACKAGE_ICON = '๐Ÿ“ฆ';
11
- const SKIPPED_ICON = 'โญ๏ธ';
12
11
  const SOURCE_ROOT = 'src';
13
12
  const MINIMUM_TYPESCRIPT_MAJOR = 5;
14
13
  const MINIMUM_TYPESCRIPT_MINOR = 7;
15
14
  const TS_EXTENSION = '.ts';
16
15
  const JS_EXTENSION = '.js';
17
- export async function buildPackage(packageDir, options = {}) {
16
+ export async function buildPackage(packageDir, options) {
18
17
  assertSupportedTypeScript();
19
18
  const cachePath = resolveBuildCachePath(packageDir);
20
19
  const outdir = options.outdir ?? DEFAULT_OUTDIR;
@@ -34,14 +33,14 @@ export async function buildPackage(packageDir, options = {}) {
34
33
  compilerVersion: ts.version,
35
34
  fingerprint: await resolveToolchainFingerprint(packageDir),
36
35
  };
37
- const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
36
+ const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints), options.style);
38
37
  if (!changed) {
39
38
  await discardScratchDirs(resolveScratchDirs(emitDir));
40
39
  return;
41
40
  }
42
41
  const emittedFileCount = await emitPackage(packageDir, entryPoints, outdir);
43
42
  await writeCacheEntry(cachePath, currentHash);
44
- console.info(`${PACKAGE_ICON} ${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`);
43
+ console.info(formatGlyphLine(NMR_GLYPHS, options.style, 'package', `${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`));
45
44
  }
46
45
  export async function computeBuildHash(packageDir, files, emitConfig, toolchain) {
47
46
  const hash = createHash('sha256');
@@ -275,19 +274,19 @@ function getModuleSpecifier(node) {
275
274
  }
276
275
  return undefined;
277
276
  }
278
- async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, outputPresent) {
277
+ async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, outputPresent, style) {
279
278
  const packageName = path.basename(packageDir);
280
279
  const previousHash = await readCacheEntry(cachePath);
281
280
  const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
282
281
  if (previousHash === currentHash) {
283
282
  if (outputPresent) {
284
- console.info(`${SKIPPED_ICON} ${packageName}: No changes detected. Skipping build.`);
283
+ console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build.`);
285
284
  return { changed: false, currentHash };
286
285
  }
287
- console.info(`${PACKAGE_ICON} ${packageName}: Build output is missing. Rebuilding.`);
286
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Build output is missing. Rebuilding.`));
288
287
  return { changed: true, currentHash };
289
288
  }
290
- console.info(`${PACKAGE_ICON} ${packageName}: Changes detected.`);
289
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Changes detected.`));
291
290
  return { changed: true, currentHash };
292
291
  }
293
292
  function assertSupportedTypeScript() {
@@ -1,8 +1,9 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Removes a package's build output and the build-cache entry that describes it, so no state survives to
3
4
  * make the next build skip. Removal is idempotent: an already-clean package is a silent no-op.
4
5
  */
5
- export declare function cleanPackage(packageDir: string): Promise<void>;
6
+ export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
6
7
  /**
7
8
  * Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
8
9
  *
@@ -14,4 +15,4 @@ export declare function cleanPackage(packageDir: string): Promise<void>;
14
15
  * a `rimraf` in nmr's dependency tree would be unreachable โ€” while nmr's own bins are linked into the
15
16
  * consumer's `node_modules/.bin`, nmr being a direct dependency.
16
17
  */
17
- export declare function runClean(cwd?: string): Promise<void>;
18
+ export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
@@ -1,52 +1,53 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { formatGlyphLine } from '@williamthorsen/nmr-core';
3
4
  import { removeCheckCache } from "../check-cache.js";
4
5
  import { loadRootConfig } from "../config.js";
5
6
  import { findContainingPackageDir } from "../context.js";
7
+ import { NMR_GLYPHS } from "../glyphs.js";
6
8
  import { reportClosing } from "../helpers/reportClosing.js";
7
9
  import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
8
10
  import { resolveChannel, runCommand } from "../runner.js";
9
11
  import { renderChain } from "../steps.js";
10
12
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
11
13
  import { resolveBuildCachePath } from "./build-output.js";
12
- const CLEAN_ICON = '๐Ÿงน';
13
14
  const CLEAN_COMMAND = 'clean';
14
15
  const BUILT_IN_CLEAN = 'nmr-clean';
15
16
  const OUTPUT_ROOT = 'dist';
16
- export async function cleanPackage(packageDir) {
17
+ export async function cleanPackage(packageDir, style) {
17
18
  await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
18
19
  await rm(resolveBuildCachePath(packageDir), { force: true });
19
- console.info(`${CLEAN_ICON} ${path.basename(packageDir)}: Removed build output and cache.`);
20
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', `${path.basename(packageDir)}: Removed build output and cache.`));
20
21
  }
21
- export async function runClean(cwd = process.cwd()) {
22
+ export async function runClean(cwd, style) {
22
23
  let monorepoRoot;
23
24
  try {
24
25
  monorepoRoot = findMonorepoRoot(cwd);
25
26
  }
26
27
  catch {
27
- await clearCheckCache(cwd);
28
- await cleanPackage(cwd);
28
+ await clearCheckCache(cwd, style);
29
+ await cleanPackage(cwd, style);
29
30
  return;
30
31
  }
31
- await clearCheckCache(monorepoRoot);
32
+ await clearCheckCache(monorepoRoot, style);
32
33
  const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
33
34
  const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
34
35
  if (packageDir !== undefined) {
35
- await cleanPackage(packageDir);
36
+ await cleanPackage(packageDir, style);
36
37
  return;
37
38
  }
38
- await sweepWorkspace(monorepoRoot, workspacePackageDirs);
39
+ await sweepWorkspace(monorepoRoot, workspacePackageDirs, style);
39
40
  }
40
- async function clearCheckCache(scopeDir) {
41
+ async function clearCheckCache(scopeDir, style) {
41
42
  await removeCheckCache(scopeDir);
42
- console.info(`${CLEAN_ICON} Removed all recorded check results.`);
43
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
43
44
  }
44
45
  function describeSweep(cleaned, skipped) {
45
46
  const packages = cleaned === 1 ? '1 package' : `${cleaned} packages`;
46
47
  const skipClause = skipped === 0 ? '' : `, skipping ${skipped} with an empty clean override`;
47
48
  return `Cleaned ${packages}${skipClause}.`;
48
49
  }
49
- async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
50
+ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
50
51
  const config = await loadRootConfig(monorepoRoot);
51
52
  const registry = buildWorkspaceRegistry(config);
52
53
  let cleaned = 0;
@@ -59,7 +60,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
59
60
  continue;
60
61
  }
61
62
  if (resolvedCommand === BUILT_IN_CLEAN) {
62
- await cleanPackage(packageDir);
63
+ await cleanPackage(packageDir, style);
63
64
  cleaned++;
64
65
  continue;
65
66
  }
@@ -72,5 +73,5 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
72
73
  }
73
74
  cleaned++;
74
75
  }
75
- reportClosing(`${CLEAN_ICON} ${describeSweep(cleaned, skipped)}`);
76
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleaned, skipped)));
76
77
  }
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  export interface PackageHookStatus {
2
3
  packageName: string;
3
4
  packageDir: string;
@@ -23,7 +24,7 @@ export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
23
24
  * Reports one line per publishable package and closes with what the run came to. Private packages appear in
24
25
  * neither: they publish nothing, so a `prepublishOnly` is not theirs to carry.
25
26
  *
26
- * Every line goes to stdout, the `โœ—` included, and the exit code is what carries a failure. A report split
27
+ * Every line goes to stdout, a failure's included, and the exit code is what carries a failure. A report split
27
28
  * across two streams leaves no reader holding the whole list.
28
29
  */
29
- export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string): void;
30
+ export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string, style: OutputStyle): void;