@williamthorsen/nmr 0.37.0 → 0.39.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 (82) hide show
  1. package/.meta/changelog.json +114 -0
  2. package/.readyup/kits/default.js +92 -87
  3. package/.readyup/manifest.json +8 -8
  4. package/CHANGELOG.md +89 -0
  5. package/README.md +37 -28
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache.d.ts +4 -3
  8. package/dist/esm/check-cache.js +28 -28
  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 +7 -5
  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-output.d.ts +2 -2
  15. package/dist/esm/commands/build-output.js +9 -9
  16. package/dist/esm/commands/build.d.ts +6 -1
  17. package/dist/esm/commands/build.js +73 -72
  18. package/dist/esm/commands/clean.d.ts +3 -2
  19. package/dist/esm/commands/clean.js +25 -24
  20. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
  21. package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
  22. package/dist/esm/commands/fmt.js +33 -33
  23. package/dist/esm/commands/report-catalog.d.ts +2 -1
  24. package/dist/esm/commands/report-catalog.js +14 -11
  25. package/dist/esm/commands/report-overrides.d.ts +2 -1
  26. package/dist/esm/commands/report-overrides.js +11 -10
  27. package/dist/esm/config.js +39 -29
  28. package/dist/esm/context.js +8 -8
  29. package/dist/esm/default-scripts.d.ts +2 -2
  30. package/dist/esm/default-scripts.js +5 -5
  31. package/dist/esm/glyphs.d.ts +9 -0
  32. package/dist/esm/glyphs.js +9 -0
  33. package/dist/esm/help.d.ts +2 -2
  34. package/dist/esm/help.js +36 -30
  35. package/dist/esm/helpers/clampToBytes.js +3 -3
  36. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  37. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  38. package/dist/esm/helpers/duration.d.ts +2 -2
  39. package/dist/esm/helpers/duration.js +7 -7
  40. package/dist/esm/helpers/filter-selection.js +12 -12
  41. package/dist/esm/helpers/findClosestName.js +15 -15
  42. package/dist/esm/helpers/package-json.d.ts +2 -2
  43. package/dist/esm/helpers/package-json.js +26 -25
  44. package/dist/esm/helpers/transcript.d.ts +2 -2
  45. package/dist/esm/helpers/transcript.js +14 -14
  46. package/dist/esm/output-style.d.ts +41 -0
  47. package/dist/esm/output-style.js +17 -0
  48. package/dist/esm/prettier.js +5 -5
  49. package/dist/esm/recording.d.ts +8 -5
  50. package/dist/esm/recording.js +27 -17
  51. package/dist/esm/report-format.d.ts +1 -1
  52. package/dist/esm/report-format.js +7 -7
  53. package/dist/esm/resolveBinStyles.d.ts +12 -0
  54. package/dist/esm/resolveBinStyles.js +16 -0
  55. package/dist/esm/resolver.d.ts +2 -2
  56. package/dist/esm/resolver.js +16 -16
  57. package/dist/esm/runCli.js +309 -221
  58. package/dist/esm/runner.d.ts +1 -1
  59. package/dist/esm/runner.js +18 -18
  60. package/dist/esm/steps.d.ts +17 -5
  61. package/dist/esm/steps.js +37 -28
  62. package/dist/esm/tests.d.ts +7 -7
  63. package/dist/esm/tests.js +20 -8
  64. package/dist/esm/tiers.d.ts +1 -1
  65. package/dist/esm/tiers.js +8 -8
  66. package/dist/esm/verbosity.d.ts +1 -1
  67. package/dist/esm/verbosity.js +7 -7
  68. package/dist/esm/verdict.d.ts +15 -4
  69. package/dist/esm/verdict.js +78 -59
  70. package/dist/esm/vitest-source-resolution.js +48 -46
  71. package/dist/esm/vitest.d.ts +2 -2
  72. package/dist/esm/vitest.js +74 -25
  73. package/dist/esm/workspace.d.ts +2 -18
  74. package/dist/esm/workspace.js +9 -67
  75. package/docs/check-cache.md +5 -5
  76. package/docs/reporting.md +20 -2
  77. package/docs/scripts.md +2 -2
  78. package/docs/vitest.md +6 -6
  79. package/docs/workspace.md +24 -1
  80. package/package.json +9 -10
  81. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  82. 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: no formattable files matched: ${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,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Reports the catalogued dependencies a package-scoped upgrade pass cannot see.
3
4
  *
@@ -8,4 +9,4 @@
8
9
  *
9
10
  * Runs ahead of the report produced by the workspace `upgrade` script.
10
11
  */
11
- export declare function reportCatalog(cwd: string): void;
12
+ export declare function reportCatalog(cwd: string, style: OutputStyle): void;
@@ -1,10 +1,11 @@
1
+ import { formatGlyphLine } from '@williamthorsen/nmr-core';
1
2
  import { findContainingPackageDir } from "../context.js";
3
+ import { NMR_GLYPHS } from "../glyphs.js";
2
4
  import { DEPENDENCY_FIELDS, readPackageJson } from "../helpers/package-json.js";
3
5
  import { reportClosing } from "../helpers/reportClosing.js";
4
6
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
5
- const CATALOG_ICON = '📚';
6
7
  const CATALOG_PROTOCOL = 'catalog:';
7
- export function reportCatalog(cwd) {
8
+ export function reportCatalog(cwd, style) {
8
9
  const monorepoRoot = findMonorepoRoot(cwd);
9
10
  const packageDir = findContainingPackageDir(cwd, getWorkspacePackageDirs(monorepoRoot));
10
11
  if (packageDir === undefined) {
@@ -14,11 +15,11 @@ export function reportCatalog(cwd) {
14
15
  if (dependencies.length === 0) {
15
16
  return;
16
17
  }
17
- console.warn(`${CATALOG_ICON} 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 these come from:'));
18
19
  for (const { name, specifier } of dependencies) {
19
20
  console.warn(`- ${name} → ${specifier}`);
20
21
  }
21
- reportClosing(`${CATALOG_ICON} ${describeCatalog(dependencies.length, monorepoRoot)}`, console.warn);
22
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'catalog', describeCatalog(dependencies.length, monorepoRoot)), console.warn);
22
23
  }
23
24
  function describeCatalog(count, monorepoRoot) {
24
25
  const subject = count === 1 ? '1 catalogued dependency went' : `${count} catalogued dependencies went`;
@@ -26,15 +27,17 @@ function describeCatalog(count, monorepoRoot) {
26
27
  return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
27
28
  }
28
29
  function findCataloguedDependencies(packageDir) {
29
- const pkg = readPackageJson(packageDir);
30
- const found = new Map();
30
+ const packageJson = readPackageJson(packageDir);
31
+ const foundDependencies = new Map();
31
32
  for (const field of DEPENDENCY_FIELDS) {
32
- const declared = Object.entries(pkg[field] ?? {});
33
- for (const [name, specifier] of declared) {
34
- if (specifier.startsWith(CATALOG_PROTOCOL) && !found.has(name)) {
35
- 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);
36
37
  }
37
38
  }
38
39
  }
39
- 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));
40
43
  }
@@ -1,7 +1,8 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
3
4
  * `pnpm.overrides` block left behind in the root `package.json`.
4
5
  *
5
6
  * Runs ahead of the report produced by the root `upgrade` script.
6
7
  */
7
- export declare function reportOverrides(monorepoRoot: string): void;
8
+ export declare function reportOverrides(monorepoRoot: string, style: OutputStyle): void;
@@ -1,16 +1,17 @@
1
+ import { formatGlyphLine } from '@williamthorsen/nmr-core';
2
+ import { NMR_GLYPHS } from "../glyphs.js";
1
3
  import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
2
4
  import { reportClosing } from "../helpers/reportClosing.js";
3
5
  import { UserError } from "../UserError.js";
4
6
  import { readWorkspaceOverrides } from "../workspace.js";
5
- const OVERRIDES_ICON = '🔒';
6
- export function reportOverrides(monorepoRoot) {
7
- const declared = listEntries(readWorkspaceOverrides(monorepoRoot));
8
- if (declared.length > 0) {
9
- console.warn(`${OVERRIDES_ICON} WARN: pnpm overrides are active:`);
10
- for (const [name, version] of declared) {
7
+ export function reportOverrides(monorepoRoot, style) {
8
+ const declaredOverrides = listEntries(readWorkspaceOverrides(monorepoRoot));
9
+ if (declaredOverrides.length > 0) {
10
+ console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
11
+ for (const [name, version] of declaredOverrides) {
11
12
  console.warn(`- ${name} → ${version}`);
12
13
  }
13
- reportClosing(`${OVERRIDES_ICON} ${describeOverrides(declared.length)}`, console.warn);
14
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declaredOverrides.length)), console.warn);
14
15
  }
15
16
  rejectLegacyOverrides(monorepoRoot);
16
17
  }
@@ -23,13 +24,13 @@ function listEntries(overrides) {
23
24
  return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
24
25
  }
25
26
  function rejectLegacyOverrides(monorepoRoot) {
26
- const legacy = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
27
- if (legacy.length === 0) {
27
+ const legacyOverrides = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
28
+ if (legacyOverrides.length === 0) {
28
29
  return;
29
30
  }
30
31
  throw new UserError([
31
32
  'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
32
- ...legacy.map(([name, version]) => `- ${name} → ${String(version)}`),
33
+ ...legacyOverrides.map(([name, version]) => `- ${name} → ${String(version)}`),
33
34
  'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
34
35
  ].join('\n'));
35
36
  }
@@ -33,7 +33,8 @@ const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.works
33
33
  const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
34
34
  const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
35
35
  const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
36
- const RECOGNIZED_STEP_KEYS = ['declinesArgs', 'run'];
36
+ const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments'];
37
+ const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
37
38
  function isScriptRecord(value) {
38
39
  if (!isObject(value))
39
40
  return false;
@@ -50,8 +51,8 @@ function isScriptElement(value) {
50
51
  return true;
51
52
  if (!isObject(value) || typeof value['run'] !== 'string')
52
53
  return false;
53
- const declinesArgs = value['declinesArgs'];
54
- return declinesArgs === undefined || typeof declinesArgs === 'boolean';
54
+ const shouldDeclineArguments = value['shouldDeclineArguments'];
55
+ return shouldDeclineArguments === undefined || typeof shouldDeclineArguments === 'boolean';
55
56
  }
56
57
  function validateScriptField(value, fieldName, configPath) {
57
58
  if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
@@ -60,7 +61,7 @@ function validateScriptField(value, fieldName, configPath) {
60
61
  const scripts = value[fieldName];
61
62
  if (!isScriptRecord(scripts)) {
62
63
  throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
63
- 'where an element is a command string or `{ run: string, declinesArgs?: boolean }`');
64
+ 'where an element is a command string or `{ run: string, shouldDeclineArguments?: boolean }`');
64
65
  }
65
66
  assertValidElements(scripts, fieldName, configPath);
66
67
  return scripts;
@@ -71,6 +72,7 @@ function assertValidElements(scripts, fieldName, configPath) {
71
72
  continue;
72
73
  for (const spec of script) {
73
74
  if (typeof spec !== 'string') {
75
+ assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
74
76
  assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
75
77
  }
76
78
  const element = typeof spec === 'string' ? spec : spec.run;
@@ -111,12 +113,12 @@ function validateCheckCacheField(value, configPath) {
111
113
  }
112
114
  assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
113
115
  const config = {};
114
- const enabled = checkCache['enabled'];
115
- if (enabled !== undefined) {
116
- if (typeof enabled !== 'boolean') {
116
+ const enabledValue = checkCache['enabled'];
117
+ if (enabledValue !== undefined) {
118
+ if (typeof enabledValue !== 'boolean') {
117
119
  throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
118
120
  }
119
- config.enabled = enabled;
121
+ config.enabled = enabledValue;
120
122
  }
121
123
  for (const field of ['excludeCommands', 'extraCommands']) {
122
124
  const commands = checkCache[field];
@@ -142,22 +144,22 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
142
144
  throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
143
145
  'which is never gated on its own: it runs as part of the chain of the command it wraps.');
144
146
  }
145
- const registered = new Set([
147
+ const registeredNames = new Set([
146
148
  ...Object.keys(buildRootRegistry(config)),
147
149
  ...Object.keys(buildWorkspaceRegistry(config)),
148
150
  ]);
149
- const missing = entries.filter(({ command }) => !registered.has(command));
150
- if (missing.length === 0) {
151
+ const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
152
+ if (missingEntries.length === 0) {
151
153
  return;
152
154
  }
153
- const declared = readDeclaredScriptNames(baseDir);
154
- const unresolvable = missing.find(({ command }) => !declared.has(command));
155
- if (unresolvable === undefined) {
155
+ const declaredNames = readDeclaredScriptNames(baseDir);
156
+ const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
157
+ if (unresolvableEntry === undefined) {
156
158
  return;
157
159
  }
158
- const closest = findClosestName(unresolvable.command, [...registered, ...declared]);
159
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvable.field}\` names no command: ` +
160
- `\`${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}\` names no command: ` +
162
+ `\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
161
163
  }
162
164
  function readDeclaredScriptNames(monorepoRoot) {
163
165
  let packageDirs;
@@ -201,8 +203,8 @@ function validateOutputField(value, configPath) {
201
203
  const commandVerbosity = output['commandVerbosity'];
202
204
  if (commandVerbosity !== undefined) {
203
205
  if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
204
- const rendered = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
205
- 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)}`);
206
208
  }
207
209
  config.commandVerbosity = commandVerbosity;
208
210
  }
@@ -256,9 +258,9 @@ export async function loadConfig(baseDir) {
256
258
  if (!existsSync(configPath)) {
257
259
  return {};
258
260
  }
259
- const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
260
- const loaded = isObject(imported) ? imported['default'] : undefined;
261
- 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);
262
264
  }
263
265
  export async function loadRootConfig(monorepoRoot) {
264
266
  const config = await loadConfig(monorepoRoot);
@@ -270,21 +272,29 @@ export async function loadWorkspaceConfig(packageDir) {
270
272
  assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
271
273
  return config;
272
274
  }
275
+ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
276
+ for (const key of Object.keys(value)) {
277
+ const replacement = retiredKeys.get(key);
278
+ if (replacement === undefined)
279
+ continue;
280
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
281
+ }
282
+ }
273
283
  function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
274
- const unrecognized = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
275
- if (unrecognized.length === 0) {
284
+ const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
285
+ if (unrecognizedKeys.length === 0) {
276
286
  return;
277
287
  }
278
- throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognized.length === 1 ? 'key' : 'keys'} ` +
279
- `${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)}.`);
280
290
  }
281
291
  function assertTierKeys(config, tier, configPath) {
282
- const unsupported = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
283
- if (unsupported.length === 0) {
292
+ const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
293
+ if (unsupportedKeys.length === 0) {
284
294
  return;
285
295
  }
286
296
  throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
287
- `not ${unsupported.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
297
+ `not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
288
298
  }
289
299
  function formatKeyList(keys, prefix) {
290
300
  return keys
@@ -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());
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * One element of a composite, paired with what it does with the invocation's trailing arguments. The bare
3
- * string form of an element is this spec with `declinesArgs` left at its default.
3
+ * string form of an element is this spec with `shouldDeclineArguments` left at its default.
4
4
  */
5
5
  export interface StepSpec {
6
6
  /** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
@@ -9,7 +9,7 @@ export interface StepSpec {
9
9
  * Set where the trailing arguments cannot narrow this step's work: it is a prerequisite the narrowed steps
10
10
  * run against, or the tool it reaches would be misled by them. A declining step runs unnarrowed.
11
11
  */
12
- declinesArgs?: boolean;
12
+ shouldDeclineArguments?: boolean;
13
13
  }
14
14
  export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
15
15
  export type ScriptRegistry = Record<string, ScriptValue>;
@@ -1,6 +1,6 @@
1
1
  const GATE_PROJECTS = '--project unit --project tool';
2
- const TYPECHECK_STEP = { run: 'typecheck', declinesArgs: true };
3
- const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', declinesArgs: true };
2
+ const TYPECHECK_STEP = { run: 'typecheck', shouldDeclineArguments: true };
3
+ const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', shouldDeclineArguments: true };
4
4
  export const workspaceScripts = {
5
5
  build: ['compile'],
6
6
  check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
@@ -32,7 +32,7 @@ export const rootScripts = {
32
32
  build: ['-R build'],
33
33
  check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
34
34
  'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
35
- ci: [{ run: 'build', declinesArgs: true }, 'check:strict'],
35
+ ci: [{ run: 'build', shouldDeclineArguments: true }, 'check:strict'],
36
36
  clean: 'nmr-clean',
37
37
  fix: ['lint', 'fmt'],
38
38
  'fix:check': ['fmt:check', 'lint:check'],
@@ -41,7 +41,7 @@ export const rootScripts = {
41
41
  lint: 'eslint --fix .',
42
42
  'lint:check': 'eslint .',
43
43
  'lint:strict': 'strict-lint',
44
- prepush: [{ run: 'audit', declinesArgs: true }, 'ci'],
44
+ prepush: [{ run: 'audit', shouldDeclineArguments: true }, 'ci'],
45
45
  'report-overrides': 'nmr-report-overrides',
46
46
  'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
47
47
  'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
@@ -59,6 +59,6 @@ export const rootScripts = {
59
59
  'test:tool': ['root:test:tool', '-R test:tool'],
60
60
  'test:unit': ['root:test:unit', '-R test:unit'],
61
61
  'test:watch': `vitest ${GATE_PROJECTS} --watch`,
62
- typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', declinesArgs: true }],
62
+ typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', shouldDeclineArguments: true }],
63
63
  upgrade: 'nmr-report-overrides && nmr-taze --recursive',
64
64
  };
@@ -0,0 +1,9 @@
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.
4
+ *
5
+ * `noop` is a neutral circle: nothing ran because the repo asked for nothing to run, which is neither a
6
+ * failure nor a block.
7
+ */
8
+ export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "clean" | "package" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
9
+ export type NmrGlyphName = keyof (typeof NMR_GLYPHS)['rich'];
@@ -0,0 +1,9 @@
1
+ import { defineGlyphSet } from '@williamthorsen/nmr-core';
2
+ export const NMR_GLYPHS = defineGlyphSet({
3
+ catalog: { plain: '', rich: '\u{1F4DA}' },
4
+ clean: { plain: '', rich: '\u{1F9F9}' },
5
+ noop: { plain: 'NOOP', rich: '\u{26AA}' },
6
+ overrides: { plain: '', rich: '\u{1F512}' },
7
+ package: { plain: '', rich: '\u{1F4E6}' },
8
+ recording: { plain: '', rich: '\u{1F4BE}' },
9
+ });
@@ -8,8 +8,8 @@ import type { NmrConfig } from './types.js';
8
8
  * `package.json:scripts` that match a registry name in the active section
9
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;