@williamthorsen/nmr 0.36.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 (62) hide show
  1. package/.meta/changelog.json +113 -0
  2. package/.readyup/kits/default.js +98 -10
  3. package/.readyup/manifest.json +7 -7
  4. package/CHANGELOG.md +82 -0
  5. package/README.md +107 -1135
  6. package/agents/README.md +1 -1
  7. package/agents/guidance/rulebooks/nmr.md +4 -4
  8. package/dist/esm/check-cache.d.ts +2 -1
  9. package/dist/esm/check-cache.js +4 -4
  10. package/dist/esm/cli-build.js +3 -0
  11. package/dist/esm/cli-clean.js +3 -1
  12. package/dist/esm/cli-ensure-prepublish-hooks.js +3 -1
  13. package/dist/esm/cli-report-catalog.js +2 -1
  14. package/dist/esm/cli-report-overrides.js +2 -1
  15. package/dist/esm/commands/build.d.ts +6 -1
  16. package/dist/esm/commands/build.js +9 -10
  17. package/dist/esm/commands/clean.d.ts +3 -2
  18. package/dist/esm/commands/clean.js +15 -14
  19. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +3 -2
  20. package/dist/esm/commands/ensure-prepublish-hooks.js +8 -6
  21. package/dist/esm/commands/report-catalog.d.ts +2 -1
  22. package/dist/esm/commands/report-catalog.js +5 -4
  23. package/dist/esm/commands/report-overrides.d.ts +2 -1
  24. package/dist/esm/commands/report-overrides.js +5 -4
  25. package/dist/esm/config.js +14 -4
  26. package/dist/esm/context.d.ts +2 -0
  27. package/dist/esm/context.js +1 -0
  28. package/dist/esm/default-scripts.d.ts +2 -2
  29. package/dist/esm/default-scripts.js +5 -5
  30. package/dist/esm/glyphs.d.ts +9 -0
  31. package/dist/esm/glyphs.js +9 -0
  32. package/dist/esm/help.js +15 -9
  33. package/dist/esm/helpers/filter-selection.d.ts +42 -0
  34. package/dist/esm/helpers/filter-selection.js +61 -0
  35. package/dist/esm/output-style.d.ts +41 -0
  36. package/dist/esm/output-style.js +17 -0
  37. package/dist/esm/recording.d.ts +3 -0
  38. package/dist/esm/recording.js +6 -3
  39. package/dist/esm/resolveBinStyles.d.ts +12 -0
  40. package/dist/esm/resolveBinStyles.js +16 -0
  41. package/dist/esm/resolver.js +2 -2
  42. package/dist/esm/runCli.js +152 -62
  43. package/dist/esm/runner.js +6 -5
  44. package/dist/esm/steps.d.ts +8 -4
  45. package/dist/esm/steps.js +6 -2
  46. package/dist/esm/verdict.d.ts +7 -3
  47. package/dist/esm/verdict.js +16 -10
  48. package/dist/esm/vitest-source-resolution.d.ts +37 -0
  49. package/dist/esm/vitest-source-resolution.js +205 -0
  50. package/dist/esm/vitest.d.ts +3 -2
  51. package/dist/esm/vitest.js +7 -6
  52. package/dist/esm/workspace.d.ts +9 -0
  53. package/dist/esm/workspace.js +17 -0
  54. package/docs/check-cache.md +176 -0
  55. package/docs/prettier.md +47 -0
  56. package/docs/reporting.md +104 -0
  57. package/docs/scripts.md +168 -0
  58. package/docs/upgrades.md +110 -0
  59. package/docs/utilities.md +64 -0
  60. package/docs/vitest.md +233 -0
  61. package/docs/workspace.md +11 -0
  62. package/package.json +11 -9
@@ -1,4 +1,117 @@
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
+ },
32
+ {
33
+ "version": "0.37.0",
34
+ "date": "2026-09-15",
35
+ "sections": [
36
+ {
37
+ "title": "๐ŸŽ‰ Features",
38
+ "audience": "all",
39
+ "items": [
40
+ {
41
+ "description": "Guard lefthook install in root prepare and report unguarded installs (#798)",
42
+ "body": "- 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`.\n- 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`.",
43
+ "hash": "bd624dbe72c843266986f725411e067ca5127275"
44
+ },
45
+ {
46
+ "description": "Report misplaced test files and a missing guard from the readyup kit (#804)",
47
+ "body": "- 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.\n- 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.\n- 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.",
48
+ "hash": "233e66c24dd77d64c041b7170fefeb608f0f53d6"
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ "title": "๐Ÿ› Bug fixes",
54
+ "audience": "all",
55
+ "items": [
56
+ {
57
+ "description": "Fail when a -F pattern selects no workspace (#785)",
58
+ "body": "- 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.\n- 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 ''`.\n\nMigration: 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.",
59
+ "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.",
60
+ "hash": "542fe79b7fe8acdcc237841b85e7bea7f466bab2"
61
+ },
62
+ {
63
+ "description": "Fix test runs failing on a dependency with a TypeScript source export (#793)",
64
+ "body": "- 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.\n- 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.\n- 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.\n- 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.\n\nMigration: 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.",
65
+ "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.",
66
+ "hash": "cd196ee47a17a891732b583ed706137ab3500507"
67
+ },
68
+ {
69
+ "description": "Prevent a recursive or multi-package run from hanging under pnpm 12 (#814)",
70
+ "body": "- 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.\n- 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.",
71
+ "hash": "c3a3305875fe2bd086b6f0cbb948186846fffb3d"
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "title": "๐Ÿงช Tests",
77
+ "audience": "dev",
78
+ "items": [
79
+ {
80
+ "description": "Run the descriptor-inheritance test on Linux and skip it in a sandbox (#803)",
81
+ "body": "- Stops `nmr ci` from failing in every Claude Code sandbox session on macOS by skipping the suite in `inherit-descriptor.tool.test.ts` when `SANDBOX_RUNTIME` is set, because the sandbox does not let `script(1)` open a pseudo-terminal.",
82
+ "hash": "fbdef48ba26d405a04241ab420c7f7eeafcdab12"
83
+ }
84
+ ]
85
+ },
86
+ {
87
+ "title": "โš™๏ธ Tooling",
88
+ "audience": "dev",
89
+ "items": [
90
+ {
91
+ "description": "Upgrade eslint-config-typescript to v17 and adopt its ignore lists (#797)",
92
+ "body": "- Declares `vitest` as `catalog:` in every workspace that imports it, with the pnpm catalog pinning 4.1.11, because v17 no longer exempts `vitest` from `n/no-extraneous-import`.\n- Builds the global ignores in `eslint.config.ts` from the published `commonIgnores` and `toolIgnores`, which also exclude `pnpm-lock.yaml` and each `.readyup/manifest.json`, so the `eslint --fix` in `nmr lint` can no longer rewrite a kit manifest and make the next build's `rdy verify` fail.",
93
+ "hash": "b201669444763ad5bef85730b114098ef352e09b"
94
+ }
95
+ ]
96
+ },
97
+ {
98
+ "title": "๐Ÿ“š Documentation",
99
+ "audience": "dev",
100
+ "items": [
101
+ {
102
+ "description": "Trim the README and move reference detail into shipped topic docs (#800)",
103
+ "body": "- Deletes the default script registry tables and the CLI flag table, which `nmr --help` prints, and the migration sections, which `CHANGELOG.md` records, rather than moving them.\n- Adds `docs` to the files published with `@williamthorsen/nmr`, and changes the closing pointer in `agents/guidance/rulebooks/nmr.md` to name each topic's document under `node_modules/@williamthorsen/nmr/docs/` in place of `README.md`.",
104
+ "hash": "4758385382a0c720bbae60eed93b7718b16fbd6b"
105
+ },
106
+ {
107
+ "description": "Trim the release-kit README and move reference detail into shipped docs (#802)",
108
+ "body": "- Adds `docs` to the published `files` of `@williamthorsen/release-kit` and a `!docs/*.v*.md` exclusion to both it and `@williamthorsen/nmr`, because `pnpm pack` would otherwise include the release-notes previews that a local checkout holds.\n- Adds `--version` to the options that `release-kit --help` lists, and points the hints for the removed `releaseNotes.shouldCreateGithubRelease` field at `release-kit init` instead of the README.",
109
+ "hash": "bce2e74071cffd44e64e8bf9e6d2e6a2adaaf363"
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ },
2
115
  {
3
116
  "version": "0.36.0",
4
117
  "date": "2026-09-08",
@@ -1,18 +1,18 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.35.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"
@@ -129,9 +129,15 @@ var TEST_EXTENSIONS = "{ts,tsx}";
129
129
  var TEST_GLOB_PREFIX = `**/${TEST_DIR}/**`;
130
130
  var TEST_FILE_PATTERN = /\.test\.tsx?$/;
131
131
  var ALL_TEST_PATTERNS = [`${TEST_GLOB_PREFIX}/*.test.${TEST_EXTENSIONS}`];
132
+ function findMisplacedTestFiles(rootDir, options = {}) {
133
+ return walkTestFiles(rootDir, options, true);
134
+ }
132
135
  function findTestFiles(rootDir, options = {}) {
133
136
  return walkTestFiles(rootDir, options, false);
134
137
  }
138
+ function findUntieredTestFiles(rootDir, options = {}) {
139
+ return findTestFiles(rootDir, options).filter((file) => !hasTierInfix(file));
140
+ }
135
141
  function hasTierInfix(filePath) {
136
142
  const tiers = TIER_NAMES;
137
143
  return tiers.includes(path.basename(filePath).split(".").at(-3) ?? "");
@@ -252,6 +258,14 @@ var default_default = defineRdyKit({
252
258
  check: noRedundantRootScripts,
253
259
  fix: "Remove scripts from root package.json that nmr provides as built-in root scripts \u2014 invoke via nmr directly"
254
260
  },
261
+ // -- Git hooks -----------------------------------------------------------
262
+ {
263
+ name: "no root install script runs lefthook install unguarded",
264
+ severity: "warn",
265
+ quiet: true,
266
+ check: () => noUnguardedLefthookInstall(),
267
+ fix: "Guard each listed script as `lefthook check-install || lefthook install`, which writes hooks only when they are missing or stale. A bare `lefthook install` rewrites `.git/hooks` on every install, so when that directory is not writable, as in an agent's sandbox, `pnpm install` fails and pnpm's `verifyDepsBeforeRun` then reinstalls before every `pnpm exec`"
268
+ },
255
269
  // -- Workspace build readiness -------------------------------------------
256
270
  {
257
271
  name: "all workspace packages can build",
@@ -298,12 +312,26 @@ var default_default = defineRdyKit({
298
312
  check: () => everyViteConfigHasVitestConfig(),
299
313
  fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory holding either name, so the Vite config otherwise wins and the projects model is never reached"
300
314
  },
315
+ {
316
+ name: "the test suite gates the test-file conventions",
317
+ severity: "warn",
318
+ check: () => testSuiteGatesTestFileConventions(),
319
+ fix: "Add a test of the repo's own under a __tests__ directory that declares the check: import { checkTestFileConventions } from '@williamthorsen/nmr/tests'; checkTestFileConventions(); -- passing `exclude` the directory names that the repo passes to `testCollectionExclude`. Without it no test run reports an untiered or misplaced test file, and this kit reports them in its place against nmr's built-in exclusions alone"
320
+ },
301
321
  {
302
322
  name: "every test file names its isolation tier",
303
323
  severity: "error",
324
+ skip: () => describeConventionsGuardSkip(),
304
325
  check: () => everyTestFileNamesItsTier(),
305
326
  fix: `Rename each to <subject>[.<aspect>].<tier>.test.ts, naming one of ${TIER_NAMES.join(", ")}. Use tool for a test that reaches a program the environment supplies, which is where a retired .int. or .integration. file belongs. Only the segment before .test. selects a project, so an untiered file runs under the residual unit project and reports success`
306
327
  },
328
+ {
329
+ name: "every test file sits under a __tests__ directory",
330
+ severity: "error",
331
+ skip: () => describeConventionsGuardSkip(),
332
+ check: () => everyTestFileSitsUnderTestsDir(),
333
+ fix: "Move each into a __tests__ directory, the only place from which the shared Vitest config collects. No project collects a file outside one, so it runs nowhere and reports nothing. For a file that is not a test, declare checkTestFileConventions with its directory in `exclude` instead, which this check then defers to"
334
+ },
307
335
  {
308
336
  name: "no package re-exports the ancestor Vitest config",
309
337
  severity: "recommend",
@@ -368,6 +396,7 @@ var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
368
396
  var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
369
397
  var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
370
398
  var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
399
+ var SHARED_TESTS_MODULE = "@williamthorsen/nmr/tests";
371
400
  var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
372
401
  var CLOBBERED_TAZE_OPTIONS = [
373
402
  { key: "concurrency", pattern: /\bconcurrency\s*:/ },
@@ -377,6 +406,17 @@ var CLOBBERED_TAZE_OPTIONS = [
377
406
  { key: "requestTimeout", pattern: /\brequestTimeout\s*:/ }
378
407
  ];
379
408
  var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
409
+ var INSTALL_LIFECYCLE_SCRIPTS = [
410
+ "pnpm:devPreinstall",
411
+ "preinstall",
412
+ "install",
413
+ "postinstall",
414
+ "preprepare",
415
+ "prepare",
416
+ "postprepare"
417
+ ];
418
+ var LEFTHOOK_CHECK_INSTALL_PATTERN = /\blefthook\s+check-install\b/;
419
+ var LEFTHOOK_INSTALL_PATTERN = /\blefthook\s+install\b/;
380
420
  var MIN_ESLINT_VERSION = "10.0.0";
381
421
  var MIN_STRICT_LINT_VERSION = "9.3.0";
382
422
  var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
@@ -526,10 +566,15 @@ function readWrapperTarget(cwd, workspace, entry) {
526
566
  return posix.normalize(posix.join(posix.dirname(entry.target), specifier));
527
567
  }
528
568
  function everyTestFileNamesItsTier(cwd = process.cwd()) {
529
- const untiered = findTestFiles(cwd).filter((path2) => !hasTierInfix(path2));
569
+ const untiered = findUntieredTestFiles(cwd);
530
570
  if (untiered.length === 0) return true;
531
571
  return { ok: false, detail: formatPaths(untiered) };
532
572
  }
573
+ function everyTestFileSitsUnderTestsDir(cwd = process.cwd()) {
574
+ const misplaced = findMisplacedTestFiles(cwd);
575
+ if (misplaced.length === 0) return true;
576
+ return { ok: false, detail: formatPaths(misplaced) };
577
+ }
533
578
  function everyViteConfigHasVitestConfig() {
534
579
  const discovery = discoverMemberWorkspaces();
535
580
  if (!discovery.ok) return discovery;
@@ -552,7 +597,7 @@ function formatPaths(paths) {
552
597
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
553
598
  }
554
599
  function getMinVersion() {
555
- const picked = { "version": "0.36.0" };
600
+ const picked = { "version": "0.38.0" };
556
601
  if (typeof picked["version"] !== "string") {
557
602
  throw new TypeError("nmr/package.json: 'version' must be a string");
558
603
  }
@@ -618,6 +663,17 @@ function noRetiredVitestConfigs(cwd = process.cwd()) {
618
663
  cwd
619
664
  );
620
665
  }
666
+ function noUnguardedLefthookInstall(cwd = process.cwd()) {
667
+ const scripts = readRootScripts(cwd);
668
+ const unguarded = INSTALL_LIFECYCLE_SCRIPTS.flatMap((name) => {
669
+ const command = scripts[name];
670
+ if (typeof command !== "string") return [];
671
+ const isUnguarded = LEFTHOOK_INSTALL_PATTERN.test(command) && !LEFTHOOK_CHECK_INSTALL_PATTERN.test(command);
672
+ return isUnguarded ? [`${name}: ${command}`] : [];
673
+ });
674
+ if (unguarded.length === 0) return true;
675
+ return { ok: false, detail: formatPaths(unguarded) };
676
+ }
621
677
  function noWorkspaceRunScriptReferences() {
622
678
  const packagesDir = join(process.cwd(), "packages");
623
679
  if (!existsSync(packagesDir)) return true;
@@ -652,6 +708,19 @@ function readPnpmFieldKeys(content) {
652
708
  const pnpm = parsed["pnpm"];
653
709
  return isRecord(pnpm) ? Object.keys(pnpm).toSorted() : void 0;
654
710
  }
711
+ function readRootScripts(cwd) {
712
+ const content = readFileIn(cwd, "package.json");
713
+ if (content === void 0) return {};
714
+ let parsed;
715
+ try {
716
+ parsed = JSON.parse(content);
717
+ } catch {
718
+ return {};
719
+ }
720
+ if (!isRecord(parsed)) return {};
721
+ const scripts = parsed["scripts"];
722
+ return isRecord(scripts) ? scripts : {};
723
+ }
655
724
  function resolvesVersionViaWorkspace(range) {
656
725
  return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
657
726
  }
@@ -683,6 +752,22 @@ function describeMissingTazeConfig(cwd) {
683
752
  if (inert.length > 0) return `holds no code to call the factory: ${inert.join(", ")}`;
684
753
  return "taze.config.ts is missing";
685
754
  }
755
+ function testSuiteGatesTestFileConventions(cwd = process.cwd()) {
756
+ if (findConventionsGuard(cwd) !== void 0) return true;
757
+ return {
758
+ ok: false,
759
+ detail: `no test file under __tests__ imports checkTestFileConventions from ${SHARED_TESTS_MODULE}`
760
+ };
761
+ }
762
+ function describeConventionsGuardSkip() {
763
+ const guard = findConventionsGuard(process.cwd());
764
+ return guard === void 0 ? false : `reported by checkTestFileConventions in ${guard}`;
765
+ }
766
+ function findConventionsGuard(cwd) {
767
+ return findTestFiles(cwd).find(
768
+ (relativePath) => importsSharedExport(readFileIn(cwd, relativePath), "checkTestFileConventions", SHARED_TESTS_MODULE)
769
+ );
770
+ }
686
771
  function toolVersionsHasNoPnpm() {
687
772
  const content = readFile(".tool-versions");
688
773
  if (content === void 0) return true;
@@ -709,15 +794,18 @@ export {
709
794
  everyBinTargetIsACommittedWrapper,
710
795
  everyBinWrapperTargetIsCoveredByFiles,
711
796
  everyTestFileNamesItsTier,
797
+ everyTestFileSitsUnderTestsDir,
712
798
  everyViteConfigHasVitestConfig,
713
799
  hasSupportedEslintVersion,
714
800
  hasSupportedStrictLintVersion,
715
801
  noPnpmFieldInPackageJson,
716
802
  noReExportOnlyVitestConfigs,
717
803
  noRetiredVitestConfigs,
804
+ noUnguardedLefthookInstall,
718
805
  prettierConfigBuildsOnSharedConfig,
719
806
  tazeConfigAvoidsClobberedOptions,
720
807
  tazeConfigBuildsOnSharedConfig,
808
+ testSuiteGatesTestFileConventions,
721
809
  vitestConfigBuildsOnSharedConfig,
722
810
  vitestRootConfigBuildsOnSharedConfig
723
811
  };
@@ -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"
@@ -11,12 +11,12 @@
11
11
  "esbuildVersion": "0.28.2",
12
12
  "inputs": [
13
13
  {
14
- "hash": "5c3cbdb9",
14
+ "hash": "488459c0",
15
15
  "kind": "module",
16
16
  "path": "kits/default.ts"
17
17
  },
18
18
  {
19
- "hash": "6f709fcd",
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.35.0",
44
+ "readyupVersion": "0.38.0",
45
45
  "source": "kits/default.ts",
46
- "sourceHash": "5c3cbdb9",
47
- "targetHash": "c24a46ec"
46
+ "sourceHash": "488459c0",
47
+ "targetHash": "cbef780a"
48
48
  }
49
49
  ]
50
50
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,88 @@
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
+
24
+ ## 0.37.0 โ€” 2026-09-15
25
+
26
+ ### ๐ŸŽ‰ Features
27
+
28
+ - Guard lefthook install in root prepare and report unguarded installs (#798)
29
+
30
+ - 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`.
31
+ - 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`.
32
+
33
+ - Report misplaced test files and a missing guard from the readyup kit (#804)
34
+
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+
39
+ ### ๐Ÿ› Bug fixes
40
+
41
+ - Fail when a -F pattern selects no workspace (#785)
42
+
43
+ - 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.
44
+ - 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 ''`.
45
+
46
+ 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.
47
+
48
+ - Fix test runs failing on a dependency with a TypeScript source export (#793)
49
+
50
+ - 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.
51
+ - 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.
52
+ - 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.
53
+ - 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.
54
+
55
+ 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.
56
+
57
+ - Prevent a recursive or multi-package run from hanging under pnpm 12 (#814)
58
+
59
+ - 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.
60
+ - 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.
61
+
62
+ ### ๐Ÿงช Tests
63
+
64
+ - Run the descriptor-inheritance test on Linux and skip it in a sandbox (#803)
65
+
66
+ - Stops `nmr ci` from failing in every Claude Code sandbox session on macOS by skipping the suite in `inherit-descriptor.tool.test.ts` when `SANDBOX_RUNTIME` is set, because the sandbox does not let `script(1)` open a pseudo-terminal.
67
+
68
+ ### โš™๏ธ Tooling
69
+
70
+ - Upgrade eslint-config-typescript to v17 and adopt its ignore lists (#797)
71
+
72
+ - Declares `vitest` as `catalog:` in every workspace that imports it, with the pnpm catalog pinning 4.1.11, because v17 no longer exempts `vitest` from `n/no-extraneous-import`.
73
+ - Builds the global ignores in `eslint.config.ts` from the published `commonIgnores` and `toolIgnores`, which also exclude `pnpm-lock.yaml` and each `.readyup/manifest.json`, so the `eslint --fix` in `nmr lint` can no longer rewrite a kit manifest and make the next build's `rdy verify` fail.
74
+
75
+ ### ๐Ÿ“š Documentation
76
+
77
+ - Trim the README and move reference detail into shipped topic docs (#800)
78
+
79
+ - Deletes the default script registry tables and the CLI flag table, which `nmr --help` prints, and the migration sections, which `CHANGELOG.md` records, rather than moving them.
80
+ - Adds `docs` to the files published with `@williamthorsen/nmr`, and changes the closing pointer in `agents/guidance/rulebooks/nmr.md` to name each topic's document under `node_modules/@williamthorsen/nmr/docs/` in place of `README.md`.
81
+
82
+ - Trim the release-kit README and move reference detail into shipped docs (#802)
83
+
84
+ - Adds `docs` to the published `files` of `@williamthorsen/release-kit` and a `!docs/*.v*.md` exclusion to both it and `@williamthorsen/nmr`, because `pnpm pack` would otherwise include the release-notes previews that a local checkout holds.
85
+ - Adds `--version` to the options that `release-kit --help` lists, and points the hints for the removed `releaseNotes.shouldCreateGithubRelease` field at `release-kit init` instead of the README.
86
+
5
87
  ## 0.36.0 โ€” 2026-09-08
6
88
 
7
89
  ### ๐ŸŽ‰ Features