@williamthorsen/nmr 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.meta/changelog.json +212 -4
  2. package/.readyup/kits/default.js +124 -104
  3. package/.readyup/manifest.json +12 -7
  4. package/CHANGELOG.md +108 -0
  5. package/README.md +41 -47
  6. package/agents/README.md +8 -8
  7. package/agents/guidance/rulebooks/nmr.md +8 -8
  8. package/bin/ensure-prepublish-hooks.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-taze.js +1 -1
  15. package/bin/nmr.js +1 -1
  16. package/dist/esm/UserError.d.ts +1 -2
  17. package/dist/esm/check-cache.d.ts +61 -63
  18. package/dist/esm/check-cache.js +24 -24
  19. package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
  20. package/dist/esm/commands/build-output.d.ts +37 -38
  21. package/dist/esm/commands/build-output.js +9 -9
  22. package/dist/esm/commands/build.d.ts +13 -14
  23. package/dist/esm/commands/build.js +67 -65
  24. package/dist/esm/commands/clean.d.ts +6 -6
  25. package/dist/esm/commands/clean.js +11 -11
  26. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
  27. package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
  28. package/dist/esm/commands/fmt.d.ts +8 -8
  29. package/dist/esm/commands/fmt.js +33 -33
  30. package/dist/esm/commands/report-catalog.d.ts +5 -7
  31. package/dist/esm/commands/report-catalog.js +10 -8
  32. package/dist/esm/commands/report-overrides.d.ts +0 -2
  33. package/dist/esm/commands/report-overrides.js +8 -8
  34. package/dist/esm/commands/taze.d.ts +9 -8
  35. package/dist/esm/config.d.ts +5 -5
  36. package/dist/esm/config.js +27 -27
  37. package/dist/esm/context.d.ts +1 -2
  38. package/dist/esm/context.js +8 -8
  39. package/dist/esm/default-scripts.d.ts +5 -6
  40. package/dist/esm/defineConfig.d.ts +1 -1
  41. package/dist/esm/git-ignored-paths.d.ts +9 -0
  42. package/dist/esm/git-ignored-paths.js +10 -0
  43. package/dist/esm/glyphs.d.ts +3 -3
  44. package/dist/esm/help.d.ts +4 -4
  45. package/dist/esm/help.js +21 -21
  46. package/dist/esm/helpers/clampToBytes.d.ts +4 -5
  47. package/dist/esm/helpers/clampToBytes.js +3 -3
  48. package/dist/esm/helpers/config-path.d.ts +0 -2
  49. package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
  50. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  51. package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
  52. package/dist/esm/helpers/duration.d.ts +10 -11
  53. package/dist/esm/helpers/duration.js +7 -7
  54. package/dist/esm/helpers/filter-selection.d.ts +19 -19
  55. package/dist/esm/helpers/filter-selection.js +12 -12
  56. package/dist/esm/helpers/findClosestName.d.ts +1 -1
  57. package/dist/esm/helpers/findClosestName.js +15 -15
  58. package/dist/esm/helpers/hook-name.d.ts +2 -5
  59. package/dist/esm/helpers/package-json.d.ts +11 -10
  60. package/dist/esm/helpers/package-json.js +26 -25
  61. package/dist/esm/helpers/readStringValues.d.ts +2 -3
  62. package/dist/esm/helpers/reportClosing.d.ts +8 -9
  63. package/dist/esm/helpers/transcript.d.ts +15 -15
  64. package/dist/esm/helpers/transcript.js +14 -14
  65. package/dist/esm/helpers/type-guards.d.ts +1 -0
  66. package/dist/esm/output-style.d.ts +12 -13
  67. package/dist/esm/prettier.d.ts +2 -2
  68. package/dist/esm/prettier.js +5 -5
  69. package/dist/esm/recording.d.ts +25 -20
  70. package/dist/esm/recording.js +28 -21
  71. package/dist/esm/replay-assembly.d.ts +10 -10
  72. package/dist/esm/report-format.d.ts +15 -20
  73. package/dist/esm/report-format.js +7 -7
  74. package/dist/esm/reportCliFailure.d.ts +2 -3
  75. package/dist/esm/resolve-scripts.d.ts +4 -5
  76. package/dist/esm/resolveBinStyles.d.ts +4 -5
  77. package/dist/esm/resolveBinStyles.js +3 -3
  78. package/dist/esm/resolver.d.ts +16 -20
  79. package/dist/esm/resolver.js +15 -15
  80. package/dist/esm/runCli.d.ts +6 -6
  81. package/dist/esm/runCli.js +261 -195
  82. package/dist/esm/runner.d.ts +20 -20
  83. package/dist/esm/runner.js +18 -18
  84. package/dist/esm/steps.d.ts +51 -38
  85. package/dist/esm/steps.js +32 -27
  86. package/dist/esm/taze.d.ts +1 -1
  87. package/dist/esm/tests.d.ts +23 -18
  88. package/dist/esm/tests.js +21 -9
  89. package/dist/esm/tiers.d.ts +28 -24
  90. package/dist/esm/tiers.js +13 -9
  91. package/dist/esm/types.d.ts +5 -5
  92. package/dist/esm/verbosity.d.ts +23 -21
  93. package/dist/esm/verbosity.js +7 -7
  94. package/dist/esm/verdict.d.ts +39 -32
  95. package/dist/esm/verdict.js +63 -50
  96. package/dist/esm/vitest-source-resolution.d.ts +11 -11
  97. package/dist/esm/vitest-source-resolution.js +49 -47
  98. package/dist/esm/vitest.d.ts +27 -26
  99. package/dist/esm/vitest.js +90 -33
  100. package/dist/esm/workspace.d.ts +6 -22
  101. package/dist/esm/workspace.js +9 -67
  102. package/docs/check-cache.md +57 -57
  103. package/docs/prettier.md +10 -10
  104. package/docs/reporting.md +48 -44
  105. package/docs/scripts.md +29 -29
  106. package/docs/upgrades.md +18 -18
  107. package/docs/utilities.md +13 -13
  108. package/docs/vitest.md +68 -66
  109. package/docs/workspace.md +27 -4
  110. package/package.json +6 -6
  111. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  112. package/dist/esm/helpers/workspace-patterns.js +0 -29
@@ -18,21 +18,21 @@ const MODE_ARGS = {
18
18
  write: ['--list-different', '--write'],
19
19
  };
20
20
  export function runFmt(argv, cwd = process.cwd()) {
21
- const parsed = parseFmtArgs(argv);
22
- if (!parsed.ok) {
23
- reportError(`nmr-fmt: ${parsed.error}`);
21
+ const parsedArgs = parseFmtArgs(argv);
22
+ if (!parsedArgs.ok) {
23
+ reportError(`nmr-fmt: ${parsedArgs.error}`);
24
24
  process.stderr.write(`${USAGE}\n`);
25
25
  return 1;
26
26
  }
27
- const resolved = resolveFormatTargets(cwd, parsed.pathspecs);
28
- if (!resolved.ok) {
29
- reportError(`nmr-fmt: ${resolved.error}`);
27
+ const resolvedTargets = resolveFormatTargets(cwd, parsedArgs.pathspecs);
28
+ if (!resolvedTargets.ok) {
29
+ reportError(`nmr-fmt: ${resolvedTargets.error}`);
30
30
  return 1;
31
31
  }
32
- const { files, ignorePaths } = resolved.targets;
32
+ const { files, ignorePaths } = resolvedTargets.targets;
33
33
  if (files.length === 0) {
34
- if (parsed.pathspecs.length > 0) {
35
- reportError(`nmr-fmt: no formattable files matched: ${parsed.pathspecs.join(', ')}`);
34
+ if (parsedArgs.pathspecs.length > 0) {
35
+ reportError(`nmr-fmt: the pathspecs did not match any formattable file: ${parsedArgs.pathspecs.join(', ')}`);
36
36
  return 1;
37
37
  }
38
38
  return 0;
@@ -42,29 +42,29 @@ export function runFmt(argv, cwd = process.cwd()) {
42
42
  reportError(`nmr-fmt: ${cli.error}`);
43
43
  return 1;
44
44
  }
45
- return runPrettier({ cliPath: cli.cliPath, mode: parsed.mode, files, ignorePaths, cwd });
45
+ return runPrettier({ cliPath: cli.cliPath, mode: parsedArgs.mode, files, ignorePaths, cwd });
46
46
  }
47
47
  export function resolveFormatTargets(cwd, pathspecs = []) {
48
48
  const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
49
49
  if (!toplevel.ok)
50
50
  return toplevel;
51
51
  const repositoryRoot = toplevel.stdout.trim();
52
- const listed = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
53
- if (!listed.ok)
54
- return listed;
52
+ const listedFiles = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
53
+ if (!listedFiles.ok)
54
+ return listedFiles;
55
55
  const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
56
56
  if (!ignoreFiles.ok)
57
57
  return ignoreFiles;
58
- const discovered = splitNulSeparated(ignoreFiles.stdout)
58
+ const discoveredIgnorePaths = splitNulSeparated(ignoreFiles.stdout)
59
59
  .filter((file) => path.basename(file) === IGNORE_FILENAME)
60
60
  .map((file) => path.resolve(repositoryRoot, file));
61
61
  return {
62
62
  ok: true,
63
63
  targets: {
64
- files: dedupe(splitNulSeparated(listed.stdout))
64
+ files: dedupe(splitNulSeparated(listedFiles.stdout))
65
65
  .filter((file) => isFormattableFile(cwd, file))
66
66
  .toSorted(),
67
- ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discovered]),
67
+ ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
68
68
  },
69
69
  };
70
70
  }
@@ -76,11 +76,11 @@ function parseFmtArgs(argv) {
76
76
  const pathspecs = [];
77
77
  for (const arg of argv) {
78
78
  if (arg === '--check' || arg === '--write') {
79
- const requested = arg === '--check' ? 'check' : 'write';
80
- if (mode !== undefined && mode !== requested) {
79
+ const requestedMode = arg === '--check' ? 'check' : 'write';
80
+ if (mode !== undefined && mode !== requestedMode) {
81
81
  return { ok: false, error: 'Pass --check or --write, not both.' };
82
82
  }
83
- mode = requested;
83
+ mode = requestedMode;
84
84
  continue;
85
85
  }
86
86
  if (arg.startsWith('-')) {
@@ -94,7 +94,7 @@ function parseFmtArgs(argv) {
94
94
  return { ok: true, mode, pathspecs };
95
95
  }
96
96
  function resolvePrettierCli() {
97
- const missing = {
97
+ const missingResult = {
98
98
  ok: false,
99
99
  error: `\`${PRETTIER_PACKAGE}\` (${PRETTIER_RANGE}) could not be resolved. Install it in this repository.`,
100
100
  };
@@ -103,14 +103,14 @@ function resolvePrettierCli() {
103
103
  manifestPath = createRequire(import.meta.url).resolve(`${PRETTIER_PACKAGE}/package.json`);
104
104
  }
105
105
  catch {
106
- return missing;
106
+ return missingResult;
107
107
  }
108
108
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
109
109
  if (!isObject(manifest))
110
- return missing;
110
+ return missingResult;
111
111
  const bin = isObject(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
112
112
  if (typeof bin !== 'string')
113
- return missing;
113
+ return missingResult;
114
114
  return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
115
115
  }
116
116
  export function runPrettier(options) {
@@ -121,7 +121,7 @@ export function runPrettier(options) {
121
121
  ...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
122
122
  ...MODE_ARGS[mode],
123
123
  ];
124
- let firstFailure = 0;
124
+ let firstFailureStatus = 0;
125
125
  for (const batch of batchWithinBudget(files, budgetBytes)) {
126
126
  const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
127
127
  if (result.error) {
@@ -129,25 +129,25 @@ export function runPrettier(options) {
129
129
  return 1;
130
130
  }
131
131
  const status = result.status ?? 1;
132
- if (status !== 0 && firstFailure === 0) {
133
- firstFailure = status;
132
+ if (status !== 0 && firstFailureStatus === 0) {
133
+ firstFailureStatus = status;
134
134
  }
135
135
  }
136
- return firstFailure;
136
+ return firstFailureStatus;
137
137
  }
138
- function batchWithinBudget(files, budget) {
138
+ function batchWithinBudget(files, budgetBytes) {
139
139
  const batches = [];
140
140
  let batch = [];
141
- let size = 0;
141
+ let sizeBytes = 0;
142
142
  for (const file of files) {
143
- const cost = Buffer.byteLength(file) + 1;
144
- if (batch.length > 0 && size + cost > budget) {
143
+ const costBytes = Buffer.byteLength(file) + 1;
144
+ if (batch.length > 0 && sizeBytes + costBytes > budgetBytes) {
145
145
  batches.push(batch);
146
146
  batch = [];
147
- size = 0;
147
+ sizeBytes = 0;
148
148
  }
149
149
  batch.push(file);
150
- size += cost;
150
+ sizeBytes += costBytes;
151
151
  }
152
152
  if (batch.length > 0)
153
153
  batches.push(batch);
@@ -1,12 +1,10 @@
1
1
  import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  /**
3
- * Reports the catalogued dependencies a package-scoped upgrade pass cannot see.
3
+ * Reports the catalogued dependencies that a package-scoped upgrade pass does not read.
4
4
  *
5
- * The upgrade tool reads `pnpm-workspace.yaml` only when it sits at the working directory, so a pass run
6
- * inside a package drops every dependency a catalog declares -- and a package whose dependencies are all
7
- * catalogued reports itself up to date. Naming them, and the root a covering pass runs from, is what keeps
8
- * that from reading as a clean bill of health.
9
- *
10
- * Runs ahead of the report produced by the workspace `upgrade` script.
5
+ * The upgrade tool reads `pnpm-workspace.yaml` only when the file is in the working directory, so a pass run
6
+ * inside a package drops every dependency declared by a catalog -- and the tool reports a package whose
7
+ * dependencies are all catalogued as up to date. This report names them, and the root from which a covering
8
+ * pass runs, to keep the reader from taking that result to mean that every dependency is up to date.
11
9
  */
12
10
  export declare function reportCatalog(cwd: string, style: OutputStyle): void;
@@ -15,7 +15,7 @@ export function reportCatalog(cwd, style) {
15
15
  if (dependencies.length === 0) {
16
16
  return;
17
17
  }
18
- console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs these come from:'));
18
+ console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs from which these come:'));
19
19
  for (const { name, specifier } of dependencies) {
20
20
  console.warn(`- ${name} → ${specifier}`);
21
21
  }
@@ -27,15 +27,17 @@ function describeCatalog(count, monorepoRoot) {
27
27
  return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
28
28
  }
29
29
  function findCataloguedDependencies(packageDir) {
30
- const pkg = readPackageJson(packageDir);
31
- const found = new Map();
30
+ const packageJson = readPackageJson(packageDir);
31
+ const foundDependencies = new Map();
32
32
  for (const field of DEPENDENCY_FIELDS) {
33
- const declared = Object.entries(pkg[field] ?? {});
34
- for (const [name, specifier] of declared) {
35
- if (specifier.startsWith(CATALOG_PROTOCOL) && !found.has(name)) {
36
- found.set(name, specifier);
33
+ const declaredEntries = Object.entries(packageJson[field] ?? {});
34
+ for (const [name, specifier] of declaredEntries) {
35
+ if (specifier.startsWith(CATALOG_PROTOCOL) && !foundDependencies.has(name)) {
36
+ foundDependencies.set(name, specifier);
37
37
  }
38
38
  }
39
39
  }
40
- return [...found].map(([name, specifier]) => ({ name, specifier })).toSorted((a, b) => a.name.localeCompare(b.name));
40
+ return [...foundDependencies]
41
+ .map(([name, specifier]) => ({ name, specifier }))
42
+ .toSorted((a, b) => a.name.localeCompare(b.name));
41
43
  }
@@ -2,7 +2,5 @@ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  /**
3
3
  * Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
4
4
  * `pnpm.overrides` block left behind in the root `package.json`.
5
- *
6
- * Runs ahead of the report produced by the root `upgrade` script.
7
5
  */
8
6
  export declare function reportOverrides(monorepoRoot: string, style: OutputStyle): void;
@@ -5,13 +5,13 @@ import { reportClosing } from "../helpers/reportClosing.js";
5
5
  import { UserError } from "../UserError.js";
6
6
  import { readWorkspaceOverrides } from "../workspace.js";
7
7
  export function reportOverrides(monorepoRoot, style) {
8
- const declared = listEntries(readWorkspaceOverrides(monorepoRoot));
9
- if (declared.length > 0) {
8
+ const declaredOverrides = listEntries(readWorkspaceOverrides(monorepoRoot));
9
+ if (declaredOverrides.length > 0) {
10
10
  console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
11
- for (const [name, version] of declared) {
11
+ for (const [name, version] of declaredOverrides) {
12
12
  console.warn(`- ${name} → ${version}`);
13
13
  }
14
- reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declared.length)), console.warn);
14
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declaredOverrides.length)), console.warn);
15
15
  }
16
16
  rejectLegacyOverrides(monorepoRoot);
17
17
  }
@@ -24,13 +24,13 @@ function listEntries(overrides) {
24
24
  return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
25
25
  }
26
26
  function rejectLegacyOverrides(monorepoRoot) {
27
- const legacy = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
28
- if (legacy.length === 0) {
27
+ const legacyOverrides = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
28
+ if (legacyOverrides.length === 0) {
29
29
  return;
30
30
  }
31
31
  throw new UserError([
32
- 'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
33
- ...legacy.map(([name, version]) => `- ${name} → ${String(version)}`),
32
+ 'pnpm 11 does not read `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
33
+ ...legacyOverrides.map(([name, version]) => `- ${name} → ${String(version)}`),
34
34
  'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
35
35
  ].join('\n'));
36
36
  }
@@ -15,15 +15,16 @@ export interface RunTazeOptions {
15
15
  * Runs taze from nmr's own dependency tree, forwarding `args`, and returns its exit code.
16
16
  *
17
17
  * Consumers depend on nmr, not on taze, so taze is transitive and its bin is absent from the consumer's
18
- * root `node_modules/.bin`. This launcher is what bridges that gap: pnpm links nmr's own bins into the
19
- * consumer root, and nmr resolves taze from the tree it does control.
18
+ * root `node_modules/.bin`. This launcher runs taze without that bin: pnpm links nmr's own bins into the
19
+ * consumer root, and nmr resolves taze from the tree that it does control.
20
20
  *
21
- * A request timeout is the one argument added, and only where `args` carries none. Neither other site can hold
22
- * it: an invocation's trailing arguments are appended to the resolved command string, so the same flag in the
23
- * `upgrade` script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser collects the pair
24
- * into an array it reads as a near-zero deadline; a `taze.config.ts` setting lands in the object taze's own CLI
25
- * defaults overwrite. Every other argument is forwarded untouched, so invocation policy (`--recursive`) stays in
26
- * the script registry, visible in `nmr` help output and overridable per repo, and upgrade policy in `taze.ts`.
21
+ * A request timeout is the one argument added, and only when `args` does not set one. Neither other site can
22
+ * declare it: Because an invocation's trailing arguments are appended to the resolved command string, the same
23
+ * flag in the `upgrade` script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser
24
+ * collects the pair into an array that it reads as a near-zero deadline; a `taze.config.ts` setting goes into
25
+ * the object that taze's own CLI defaults overwrite. Because every other argument is forwarded untouched,
26
+ * invocation policy (`--recursive`) stays in the script registry, visible in `nmr` help output and overridable
27
+ * per repo, and upgrade policy stays in `taze.ts`.
27
28
  */
28
29
  export declare function runTaze(args: string[], options?: RunTazeOptions): number;
29
30
  /** Resolves the absolute path of taze's CLI entry from nmr's own dependency tree. */
@@ -9,15 +9,15 @@ export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
9
9
  * Loads the monorepo-root `.config/nmr.config.ts`, the tier that feeds script resolution and `devBin`.
10
10
  * Returns an empty config if the file doesn't exist.
11
11
  *
12
- * Throws on a recognized key this tier does not honor. `build` governs one package's compile, which reads the
12
+ * Throws on a recognized key that this tier does not honor. `build` governs one package's compile, which reads the
13
13
  * package's own config alone, so a `build` accepted here would apply its patterns nowhere.
14
14
  */
15
15
  export declare function loadRootConfig(monorepoRoot: string): Promise<NmrConfig>;
16
16
  /**
17
- * Loads a single package's `.config/nmr.config.ts`, the tier `nmr-compile` reads from its working directory.
18
- * Returns an empty config if the file doesn't exist.
17
+ * Loads a single package's `.config/nmr.config.ts`, the tier that `nmr-compile` reads from its working
18
+ * directory. Returns an empty config if the file doesn't exist.
19
19
  *
20
- * Throws on a recognized key this tier does not honor. Dropping one would leave a package building on
21
- * settings its own config file appears to change, which nothing in the build's output would reveal.
20
+ * Throws on a recognized key that this tier does not honor. Dropping one would leave a package building on
21
+ * settings that its own config file appears to change, which nothing in the build's output would reveal.
22
22
  */
23
23
  export declare function loadWorkspaceConfig(packageDir: string): Promise<NmrConfig>;
@@ -79,7 +79,7 @@ function assertValidElements(scripts, fieldName, configPath) {
79
79
  const token = findUnexpressibleToken(element);
80
80
  if (token === undefined)
81
81
  continue;
82
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` carries ` +
82
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
83
83
  `\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
84
84
  `reference that name here.`);
85
85
  }
@@ -113,12 +113,12 @@ function validateCheckCacheField(value, configPath) {
113
113
  }
114
114
  assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
115
115
  const config = {};
116
- const enabled = checkCache['enabled'];
117
- if (enabled !== undefined) {
118
- if (typeof enabled !== 'boolean') {
116
+ const enabledValue = checkCache['enabled'];
117
+ if (enabledValue !== undefined) {
118
+ if (typeof enabledValue !== 'boolean') {
119
119
  throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
120
120
  }
121
- config.enabled = enabled;
121
+ config.enabled = enabledValue;
122
122
  }
123
123
  for (const field of ['excludeCommands', 'extraCommands']) {
124
124
  const commands = checkCache[field];
@@ -142,24 +142,24 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
142
142
  const hook = entries.find(({ command }) => isHookName(command));
143
143
  if (hook !== undefined) {
144
144
  throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
145
- 'which is never gated on its own: it runs as part of the chain of the command it wraps.');
145
+ 'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
146
146
  }
147
- const registered = new Set([
147
+ const registeredNames = new Set([
148
148
  ...Object.keys(buildRootRegistry(config)),
149
149
  ...Object.keys(buildWorkspaceRegistry(config)),
150
150
  ]);
151
- const missing = entries.filter(({ command }) => !registered.has(command));
152
- if (missing.length === 0) {
151
+ const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
152
+ if (missingEntries.length === 0) {
153
153
  return;
154
154
  }
155
- const declared = readDeclaredScriptNames(baseDir);
156
- const unresolvable = missing.find(({ command }) => !declared.has(command));
157
- if (unresolvable === undefined) {
155
+ const declaredNames = readDeclaredScriptNames(baseDir);
156
+ const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
157
+ if (unresolvableEntry === undefined) {
158
158
  return;
159
159
  }
160
- const closest = findClosestName(unresolvable.command, [...registered, ...declared]);
161
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvable.field}\` names no command: ` +
162
- `\`${unresolvable.command}\`.${closest === undefined ? '' : ` Did you mean \`${closest}\`?`}`);
160
+ const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
161
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
162
+ `\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
163
163
  }
164
164
  function readDeclaredScriptNames(monorepoRoot) {
165
165
  let packageDirs;
@@ -203,8 +203,8 @@ function validateOutputField(value, configPath) {
203
203
  const commandVerbosity = output['commandVerbosity'];
204
204
  if (commandVerbosity !== undefined) {
205
205
  if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
206
- const rendered = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
207
- throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', rendered)}`);
206
+ const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
207
+ throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
208
208
  }
209
209
  config.commandVerbosity = commandVerbosity;
210
210
  }
@@ -258,9 +258,9 @@ export async function loadConfig(baseDir) {
258
258
  if (!existsSync(configPath)) {
259
259
  return {};
260
260
  }
261
- const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
262
- const loaded = isObject(imported) ? imported['default'] : undefined;
263
- return validateConfig(loaded, configPath, baseDir);
261
+ const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
262
+ const loadedConfig = isObject(importedModule) ? importedModule['default'] : undefined;
263
+ return validateConfig(loadedConfig, configPath, baseDir);
264
264
  }
265
265
  export async function loadRootConfig(monorepoRoot) {
266
266
  const config = await loadConfig(monorepoRoot);
@@ -281,20 +281,20 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
281
281
  }
282
282
  }
283
283
  function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
284
- const unrecognized = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
285
- if (unrecognized.length === 0) {
284
+ const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
285
+ if (unrecognizedKeys.length === 0) {
286
286
  return;
287
287
  }
288
- throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognized.length === 1 ? 'key' : 'keys'} ` +
289
- `${formatKeyList(unrecognized, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
288
+ throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
289
+ `${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
290
290
  }
291
291
  function assertTierKeys(config, tier, configPath) {
292
- const unsupported = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
293
- if (unsupported.length === 0) {
292
+ const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
293
+ if (unsupportedKeys.length === 0) {
294
294
  return;
295
295
  }
296
296
  throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
297
- `not ${unsupported.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
297
+ `not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
298
298
  }
299
299
  function formatKeyList(keys, prefix) {
300
300
  return keys
@@ -3,7 +3,6 @@ export interface ResolvedContext {
3
3
  monorepoRoot: string;
4
4
  isRoot: boolean;
5
5
  packageDir?: string;
6
- /** The workspace's package directories, resolved once here rather than swept for again by each reader. */
7
6
  workspacePackageDirs: string[];
8
7
  config: NmrConfig;
9
8
  }
@@ -11,7 +10,7 @@ export interface ResolvedContext {
11
10
  * Determines whether a directory is inside a workspace package.
12
11
  * Returns the package directory if so, or `undefined` if in root context.
13
12
  *
14
- * A workspace may nest one package inside another, so the deepest containing directory wins — matching
13
+ * A workspace may nest one package inside another, so the deepest containing directory wins. This matches
15
14
  * pnpm, which resolves a cwd to the package that encloses it most nearly.
16
15
  */
17
16
  export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
@@ -2,17 +2,17 @@ import path from 'node:path';
2
2
  import { loadRootConfig } from "./config.js";
3
3
  import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
4
4
  export function findContainingPackageDir(dir, workspacePackageDirs) {
5
- const resolved = path.resolve(dir);
6
- let nearest;
7
- for (const pkgDir of workspacePackageDirs) {
8
- const resolvedPkgDir = path.resolve(pkgDir);
9
- if (resolved !== resolvedPkgDir && !resolved.startsWith(resolvedPkgDir + path.sep))
5
+ const resolvedDir = path.resolve(dir);
6
+ let nearestDir;
7
+ for (const packageDir of workspacePackageDirs) {
8
+ const resolvedPackageDir = path.resolve(packageDir);
9
+ if (resolvedDir !== resolvedPackageDir && !resolvedDir.startsWith(resolvedPackageDir + path.sep))
10
10
  continue;
11
- if (nearest === undefined || resolvedPkgDir.length > nearest.length) {
12
- nearest = resolvedPkgDir;
11
+ if (nearestDir === undefined || resolvedPackageDir.length > nearestDir.length) {
12
+ nearestDir = resolvedPackageDir;
13
13
  }
14
14
  }
15
- return nearest;
15
+ return nearestDir;
16
16
  }
17
17
  export async function resolveContext(cwd) {
18
18
  const resolvedCwd = path.resolve(cwd ?? process.cwd());
@@ -6,18 +6,17 @@ export interface StepSpec {
6
6
  /** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
7
7
  run: string;
8
8
  /**
9
- * Set where the trailing arguments cannot narrow this step's work: it is a prerequisite the narrowed steps
10
- * run against, or the tool it reaches would be misled by them. A declining step runs unnarrowed.
9
+ * Set when the trailing arguments cannot narrow this step's work: It is a prerequisite against which the
10
+ * narrowed steps run, or the tool that it invokes would be misled by them. A declining step runs unnarrowed.
11
11
  */
12
12
  shouldDeclineArguments?: boolean;
13
13
  }
14
14
  export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
15
15
  export type ScriptRegistry = Record<string, ScriptValue>;
16
16
  /**
17
- * Workspace scripts, identical for every package.
18
- * Four Vitest projects are recognized: `tool`, `localhost`, `remote`, and `unit`. The latter is also a catch-all.
19
- * Name a test file with the matching infix to associate it with a project.
20
- * Example: `nmr test:tool` runs every file carrying the `tool` infix, such as `my-file.tool.test.ts`.
17
+ * Workspace scripts, identical for every package. A `test:<tier>` script runs the Vitest project of that tier, which
18
+ * collects the test files that name it: `nmr test:tool` runs `my-file.tool.test.ts`. The `unit` project also
19
+ * collects every file that does not name any other tier.
21
20
  */
22
21
  export declare const workspaceScripts: ScriptRegistry;
23
22
  export declare const rootScripts: ScriptRegistry;
@@ -1,7 +1,7 @@
1
1
  import type { NmrConfig } from './types.js';
2
2
  export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, StepSpec, } from './types.js';
3
3
  /**
4
- * Type-safe identity function for configuration files.
4
+ * Returns `config` unchanged, typing a configuration file's default export as `NmrConfig`.
5
5
  *
6
6
  * Usage in `.config/nmr.config.ts`:
7
7
  * ```ts
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lists the untracked paths that git ignores under `dir`, relative to it and POSIX-separated, sorted. An ignored
3
+ * directory is one entry ending in `/`, standing for everything beneath it; a tracked file never appears, whatever
4
+ * pattern it matches.
5
+ *
6
+ * Returns an empty list whenever git does not answer, outside a repository or with git missing, so that a caller
7
+ * excluding these paths over-reports rather than hides a path.
8
+ */
9
+ export declare function listGitIgnoredPaths(dir: string): string[];
@@ -0,0 +1,10 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export function listGitIgnoredPaths(dir) {
3
+ const result = spawnSync('git', ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--directory', '--no-empty-directory'], { cwd: dir, encoding: 'utf8' });
4
+ if (result.error !== undefined || result.status !== 0)
5
+ return [];
6
+ return result.stdout
7
+ .split('\0')
8
+ .filter((entry) => entry !== '')
9
+ .toSorted();
10
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. A plain variant is empty
3
- * where a word always follows the glyph, and a word where the glyph opens a line of its own.
2
+ * Glyphs that do not name a status; a status marker comes from nmr-core's `STATUS_GLYPHS`. A plain variant is
3
+ * empty when a word always follows the glyph, and a word when the glyph opens a line of its own.
4
4
  *
5
- * `noop` is a neutral circle: nothing ran because the repo asked for nothing to run, which is neither a
5
+ * `noop` is a neutral circle: Nothing ran because the repo asked for nothing to run, which is neither a
6
6
  * failure nor a block.
7
7
  */
8
8
  export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "clean" | "package" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
@@ -1,15 +1,15 @@
1
1
  import type { NmrConfig } from './types.js';
2
2
  /**
3
- * Generates the help text for the `nmr` CLI. Renders only nmr commands —
3
+ * Generates the help text for the `nmr` CLI. Renders only nmr commands:
4
4
  * names from the workspace and root registries, excluding hooks (`*:pre`,
5
5
  * `*:post`).
6
6
  *
7
7
  * When `packageDir` is provided, tier-3 entries from that package's
8
8
  * `package.json:scripts` that match a registry name in the active section
9
- * are inlined as overrides: the registry value is replaced with the
9
+ * are inlined as overrides: The registry value is replaced with the
10
10
  * override value and the row's command name is suffixed with `*`. The
11
- * active section is the root section when `useRoot` is true (root cwd
11
+ * active section is the root section when `shouldUseRoot` is true (root cwd
12
12
  * or `-w`), otherwise the workspace section. A footnote is appended once
13
13
  * if any override marker was rendered.
14
14
  */
15
- export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, useRoot: boolean): string;
15
+ export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, shouldUseRoot: boolean): string;