@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,14 +18,15 @@ import { assembleReplay } from "./replay-assembly.js";
18
18
  import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
19
19
  import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, resolveScript, } from "./resolver.js";
20
20
  import { resolveChannel, runSteps } from "./runner.js";
21
- import { composeNmrStep, findNmrCrossing, renderChain } from "./steps.js";
21
+ import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, renderChain } from "./steps.js";
22
22
  import { UserError } from "./UserError.js";
23
23
  import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
24
24
  import { writeVerdict } from "./verdict.js";
25
- import { readWorkspacePackageNames } from "./workspace.js";
25
+ import { readWorkspacePackageNames, resolveWorkspace } from "./workspace.js";
26
26
  const VERSION = readPackageVersion(import.meta.url);
27
27
  const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
28
- const NAME_CEILING = 10;
28
+ const RECURSIVE_REJECTION = '-R/--recursive did not match any workspace:';
29
+ const LIST_CEILING = 10;
29
30
  const NAMED_ESCAPES = new Map([
30
31
  ['\n', String.raw `\n`],
31
32
  ['\r', String.raw `\r`],
@@ -39,56 +40,56 @@ export async function runCli(options) {
39
40
  reportError(parseResult.error, stderr);
40
41
  return { exitCode: 1 };
41
42
  }
42
- const { parsed } = parseResult;
43
- const inherited = readPresentation({
43
+ const { parsedArgs } = parseResult;
44
+ const presentationRead = readPresentation({
44
45
  env,
45
- flagValue: parsed.outputStyle,
46
+ flagValue: parsedArgs.outputStyle,
46
47
  stderrIsTty: isTerminalStream(stderr),
47
48
  stdoutIsTty: isTerminalStream(stdout),
48
49
  });
49
- if (!inherited.ok) {
50
- reportError(inherited.error, stderr);
50
+ if (!presentationRead.ok) {
51
+ reportError(presentationRead.error, stderr);
51
52
  return { exitCode: 1 };
52
53
  }
53
- const { styles } = inherited;
54
- if (parsed.version) {
54
+ const { styles } = presentationRead;
55
+ if (parsedArgs.shouldShowVersion) {
55
56
  stdout.write(`${VERSION}\n`);
56
57
  return { exitCode: 0 };
57
58
  }
58
59
  const context = await resolveContext(cwd);
59
- const format = resolveReportFormat({ envFormat: inherited.format, jsonFlag: parsed.json });
60
+ const format = resolveReportFormat({ envFormat: presentationRead.format, hasJsonFlag: parsedArgs.shouldEmitJson });
60
61
  const verbosity = resolveReportingVerbosity({
61
62
  env,
62
- envVerbosity: inherited.verbosity,
63
+ envVerbosity: presentationRead.verbosity,
63
64
  format,
64
65
  output: context.config.output,
65
- quietFlag: parsed.quiet,
66
+ hasQuietFlag: parsedArgs.quiet,
66
67
  });
67
68
  const quiet = verbosity === 'quiet';
68
- const useRoot = parsed.workspaceRoot || context.isRoot;
69
- const anchorDir = useRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
70
- if (parsed.help || !parsed.command) {
71
- stdout.write(`${generateHelp(context.config, anchorDir, useRoot)}\n`);
69
+ const shouldUseRoot = parsedArgs.isWorkspaceRoot || context.isRoot;
70
+ const anchorDir = shouldUseRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
71
+ if (parsedArgs.shouldShowHelp || !parsedArgs.command) {
72
+ stdout.write(`${generateHelp(context.config, anchorDir, shouldUseRoot)}\n`);
72
73
  return { exitCode: 0 };
73
74
  }
74
- const { command } = parsed;
75
+ const { command } = parsedArgs;
75
76
  const scope = path.basename(anchorDir);
76
77
  const snapshot = openGate({
77
78
  command,
78
79
  config: context.config,
79
80
  env,
80
81
  monorepoRoot: context.monorepoRoot,
81
- passthrough: parsed.passthrough,
82
+ passthrough: parsedArgs.passthrough,
82
83
  stderr,
83
84
  style: styles.stderr,
84
85
  });
85
- const noCache = parsed.noCache || env[NO_CACHE_ENV_VAR] === '1';
86
+ const shouldBypassCache = parsedArgs.shouldBypassCache || env[NO_CACHE_ENV_VAR] === '1';
86
87
  const runId = resolveRunId(env);
87
88
  const childEnv = buildChildEnv({
88
89
  env,
89
90
  format,
90
- noCache,
91
- passthrough: parsed.passthrough,
91
+ shouldBypassCache,
92
+ passthrough: parsedArgs.passthrough,
92
93
  runId,
93
94
  snapshot,
94
95
  styles,
@@ -100,56 +101,57 @@ export async function runCli(options) {
100
101
  stderr,
101
102
  env: childEnv,
102
103
  };
103
- const delegation = composeDelegation({ childEnv, command, parsed });
104
+ const delegation = composeDelegation({ childEnv, command, parsedArgs });
104
105
  if (delegation !== undefined) {
105
- return runDelegation({ context, delegation, parsed, runOptions, stderr });
106
+ return runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
106
107
  }
107
- const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
108
+ const registry = shouldUseRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
108
109
  assertNoSelfReference({
109
110
  anchorDir,
110
111
  command,
111
- isReading: parsed.log,
112
+ isReading: parsedArgs.shouldPrintLog,
113
+ isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
112
114
  monorepoRoot: context.monorepoRoot,
113
115
  registry,
114
- workspaceRoot: parsed.workspaceRoot,
115
116
  });
116
- const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
117
- if (!resolved) {
117
+ const resolvedScript = resolveScript(command, registry, anchorDir, parsedArgs.isWorkspaceRoot);
118
+ if (!resolvedScript) {
118
119
  if (env[RUN_IF_PRESENT_ENV_VAR] === '1') {
119
120
  return { exitCode: 0 };
120
121
  }
121
122
  reportError(`Unknown command: ${command}`, stderr);
122
123
  return { exitCode: 1 };
123
124
  }
124
- const resolvedCommand = renderChain(resolved.steps);
125
- const noOpReason = findNoOpReason(resolvedCommand);
126
- if (noOpReason !== undefined && !parsed.log) {
125
+ const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
126
+ const resolvedCommand = renderChain(runnableSteps);
127
+ const noOpReason = findNoOpReason(resolvedCommand, isEmptiedByWorkspace);
128
+ if (noOpReason !== undefined && !parsedArgs.shouldPrintLog) {
127
129
  reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format, styles.stdout);
128
130
  return { exitCode: 0 };
129
131
  }
130
- const substitutedSteps = applyDevBinToSteps(resolved.steps, context.config.devBin, context.monorepoRoot);
132
+ const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
131
133
  const substitutedCommand = renderChain(substitutedSteps);
132
- const bound = bindPassthrough(substitutedSteps, parsed.passthrough, command);
133
- if (!bound.ok) {
134
- reportError(bound.error, stderr);
134
+ const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
135
+ if (!bindResult.ok) {
136
+ reportError(bindResult.error, stderr);
135
137
  return { exitCode: 1 };
136
138
  }
137
- const mainSteps = bound.steps;
139
+ const mainSteps = bindResult.steps;
138
140
  const isHookInvocation = isHookName(command);
139
141
  const fullSteps = isHookInvocation
140
142
  ? mainSteps
141
- : wrapWithHooks(command, mainSteps, registry, anchorDir, parsed.workspaceRoot);
143
+ : wrapWithHooks(command, mainSteps, registry, anchorDir, parsedArgs.isWorkspaceRoot);
142
144
  const fullCommand = renderChain(fullSteps);
143
145
  reportNmrCrossing({
144
146
  config: context.config,
145
- isReading: parsed.log,
147
+ isReading: parsedArgs.shouldPrintLog,
148
+ isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
146
149
  monorepoRoot: context.monorepoRoot,
147
150
  registry,
148
- resolved,
151
+ resolvedScript,
152
+ shouldUseRoot,
149
153
  stderr,
150
154
  style: styles.stderr,
151
- useRoot,
152
- workspaceRoot: parsed.workspaceRoot,
153
155
  });
154
156
  const key = resolveCacheKey({
155
157
  anchorDir,
@@ -161,7 +163,7 @@ export async function runCli(options) {
161
163
  stderr,
162
164
  substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
163
165
  });
164
- if (parsed.log) {
166
+ if (parsedArgs.shouldPrintLog) {
165
167
  return reportRecording({
166
168
  anchorDir,
167
169
  command,
@@ -186,11 +188,11 @@ export async function runCli(options) {
186
188
  env,
187
189
  key,
188
190
  monorepoRoot: context.monorepoRoot,
189
- noCache,
190
- overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, quiet, styles.stdout),
191
+ overrideNotice: formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, styles.stdout),
191
192
  ownSteps: mainSteps,
192
193
  runId,
193
194
  runOptions,
195
+ shouldBypassCache,
194
196
  snapshot,
195
197
  stderr,
196
198
  stdout,
@@ -199,23 +201,23 @@ export async function runCli(options) {
199
201
  return { exitCode };
200
202
  }
201
203
  const BOOLEAN_FLAGS = new Map([
202
- ['-?', 'help'],
203
- ['--help', 'help'],
204
- ['--json', 'json'],
205
- ['--log', 'log'],
206
- ['--no-cache', 'noCache'],
204
+ ['-?', 'shouldShowHelp'],
205
+ ['--help', 'shouldShowHelp'],
206
+ ['--json', 'shouldEmitJson'],
207
+ ['--log', 'shouldPrintLog'],
208
+ ['--no-cache', 'shouldBypassCache'],
207
209
  ['-q', 'quiet'],
208
210
  ['--quiet', 'quiet'],
209
211
  ['-R', 'recursive'],
210
212
  ['--recursive', 'recursive'],
211
- ['-V', 'version'],
212
- ['--version', 'version'],
213
- ['-w', 'workspaceRoot'],
214
- ['--workspace-root', 'workspaceRoot'],
213
+ ['-V', 'shouldShowVersion'],
214
+ ['--version', 'shouldShowVersion'],
215
+ ['-w', 'isWorkspaceRoot'],
216
+ ['--workspace-root', 'isWorkspaceRoot'],
215
217
  ]);
216
218
  function buildChildEnv(options) {
217
- const { env, format, noCache, passthrough, runId, snapshot, styles, verbosity } = options;
218
- const bypassesCache = noCache || passthrough.length > 0;
219
+ const { env, format, passthrough, runId, shouldBypassCache, snapshot, styles, verbosity } = options;
220
+ const bypassesCache = shouldBypassCache || passthrough.length > 0;
219
221
  return {
220
222
  ...env,
221
223
  ...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
@@ -242,41 +244,41 @@ function bindPassthrough(steps, passthrough, command) {
242
244
  }
243
245
  return step.kind === 'structural'
244
246
  ? { ...step, argv: [...step.argv, ...passthrough] }
245
- : { kind: 'opaque', command: `${step.command} ${passthrough.map(shellQuote).join(' ')}` };
247
+ : { kind: 'opaque', command: `${step.command} ${passthrough.map(quoteForShell).join(' ')}` };
246
248
  });
247
249
  return { ok: true, steps: boundSteps };
248
250
  }
249
251
  function formatUnroutableArgumentsError(command) {
250
- return (`\`${command}\` takes no trailing arguments: every step of its chain declines them. ` +
252
+ return (`\`${command}\` does not take trailing arguments: every step of its chain declines them. ` +
251
253
  'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
252
254
  }
253
255
  function composeDelegation(options) {
254
- const { childEnv, command, parsed } = options;
256
+ const { childEnv, command, parsedArgs } = options;
255
257
  let scope;
256
- let runIfPresent = parsed.log;
257
- if (parsed.filter) {
258
- scope = ['--filter', parsed.filter];
258
+ let shouldRunIfPresent = parsedArgs.shouldPrintLog;
259
+ if (parsedArgs.filter) {
260
+ scope = ['--filter', parsedArgs.filter];
259
261
  }
260
- else if (parsed.recursive) {
262
+ else if (parsedArgs.recursive) {
261
263
  scope = ['--recursive'];
262
- runIfPresent = true;
264
+ shouldRunIfPresent = true;
263
265
  }
264
266
  if (scope === undefined) {
265
267
  return undefined;
266
268
  }
267
- const flags = parsed.log ? ['--log'] : [];
269
+ const flags = parsedArgs.shouldPrintLog ? ['--log'] : [];
268
270
  return {
269
- env: runIfPresent ? { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' } : childEnv,
271
+ env: shouldRunIfPresent ? { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' } : childEnv,
270
272
  step: {
271
273
  kind: 'structural',
272
- argv: ['pnpm', ...scope, 'exec', 'nmr', ...flags, command, ...parsed.passthrough],
274
+ argv: ['pnpm', ...scope, 'exec', 'nmr', ...flags, command, ...parsedArgs.passthrough],
273
275
  },
274
276
  };
275
277
  }
276
278
  async function composeRetention(options) {
277
- const { anchorDir, command, key, retained, runId } = options;
278
- if (retained !== undefined) {
279
- const excerpt = deriveExcerpt(retained.stdout.toString('utf8')) ?? deriveExcerpt(retained.stderr.toString('utf8'));
279
+ const { anchorDir, command, key, retainedOutput, runId } = options;
280
+ if (retainedOutput !== undefined) {
281
+ const excerpt = deriveExcerpt(retainedOutput.stdout.toString('utf8')) ?? deriveExcerpt(retainedOutput.stderr.toString('utf8'));
280
282
  if (excerpt === undefined) {
281
283
  return undefined;
282
284
  }
@@ -292,7 +294,7 @@ async function composeRetention(options) {
292
294
  return replay.length === 0 ? undefined : { key, replay, runId };
293
295
  }
294
296
  function describeCrossingRemedy(options) {
295
- const { crossing, monorepoRoot, origin, registry, workspaceRoot } = options;
297
+ const { crossingStep, isWorkspaceRoot, monorepoRoot, origin, registry } = options;
296
298
  const configSite = path.relative(monorepoRoot, resolveConfigPath(monorepoRoot));
297
299
  switch (origin.tier) {
298
300
  case 'default':
@@ -307,131 +309,188 @@ function describeCrossingRemedy(options) {
307
309
  };
308
310
  case 'package':
309
311
  return {
310
- remedy: formatPackageRemedy({ configSite, crossing, key: origin.key, registry, workspaceRoot }),
312
+ remedy: formatPackageRemedy({ configSite, crossingStep, isWorkspaceRoot, key: origin.key, registry }),
311
313
  subject: `${path.relative(monorepoRoot, origin.file)}: \`scripts.${origin.key}\``,
312
314
  };
313
315
  default: {
314
- const unhandled = origin;
315
- throw new Error(`Unhandled script origin: ${JSON.stringify(unhandled)}`);
316
+ const unhandledOrigin = origin;
317
+ throw new Error(`Unhandled script origin: ${JSON.stringify(unhandledOrigin)}`);
316
318
  }
317
319
  }
318
320
  }
319
321
  function describeStepDestination(key) {
320
322
  if (isHookName(key)) {
321
- return 'a script of their own that the step list names, because a hook has no `:pre` or `:post` of its own';
323
+ return 'a script of their own that the step list names, because a hook does not have a `:pre` or `:post` of its own';
322
324
  }
323
325
  return `a \`${key}:pre\` or \`${key}:post\` script`;
324
326
  }
325
- function describeOrigin(origin, config, useRoot) {
327
+ function describeOrigin(origin, config, shouldUseRoot) {
326
328
  if (origin.tier === 'package') {
327
329
  return origin;
328
330
  }
329
- const field = useRoot ? 'rootScripts' : 'workspaceScripts';
331
+ const field = shouldUseRoot ? 'rootScripts' : 'workspaceScripts';
330
332
  const configScripts = config[field];
331
333
  return configScripts !== undefined && Object.hasOwn(configScripts, origin.key)
332
334
  ? { tier: 'config', field, key: origin.key }
333
335
  : { tier: 'default', key: origin.key };
334
336
  }
335
337
  function escapeControlCharacters(text) {
336
- let escaped = text;
338
+ let escapedText = text;
337
339
  for (const [char, escape] of NAMED_ESCAPES) {
338
- escaped = escaped.split(char).join(escape);
340
+ escapedText = escapedText.split(char).join(escape);
339
341
  }
340
- return escaped;
342
+ return escapedText;
341
343
  }
342
344
  function formatNmrCrossingWarning(options) {
343
345
  const { remedy, subject } = describeCrossingRemedy(options);
344
346
  return (`${STATUS_GLYPHS[options.style].warning.text} ${subject} reaches nmr through a shell ` +
345
- `(\`${escapeControlCharacters(options.crossing)}\`), ${CROSSING_CONSEQUENCE} ${remedy}`);
347
+ `(\`${escapeControlCharacters(options.crossingStep)}\`), ${CROSSING_CONSEQUENCE} ${remedy}`);
346
348
  }
347
- function formatOverrideNotice(resolved, registry, command, anchorDir, quiet, style) {
349
+ function formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, style) {
348
350
  const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
349
- if (quiet || resolved.origin.tier !== 'package' || registryEntry === undefined) {
351
+ if (quiet || resolvedScript.origin.tier !== 'package' || registryEntry === undefined) {
350
352
  return undefined;
351
353
  }
352
- const notice = `${path.basename(anchorDir)}: Using override script: ${renderChain(resolved.steps)}`;
354
+ const notice = `${path.basename(anchorDir)}: Using override script: ${renderChain(resolvedScript.steps)}`;
353
355
  return `${formatGlyphLine(NMR_GLYPHS, style, 'package', notice)}\n`;
354
356
  }
355
357
  function formatSelfReferenceRemedy(options) {
356
- const { command, monorepoRoot, registry, script, workspaceRoot } = options;
358
+ const { command, isWorkspaceRoot, monorepoRoot, registry, script } = options;
357
359
  if (isHookName(command)) {
358
- return `Delete the re-invocation: \`${command}\` runs the steps standing beside it.`;
360
+ return `Delete the re-invocation: \`${command}\` runs the steps beside it.`;
359
361
  }
360
362
  const configSite = path.relative(monorepoRoot, resolveConfigPath(monorepoRoot));
361
- return formatPackageRemedy({ configSite, crossing: script, key: command, registry, workspaceRoot });
363
+ return formatPackageRemedy({ configSite, crossingStep: script, isWorkspaceRoot, key: command, registry });
362
364
  }
363
365
  function formatPackageRemedy(options) {
364
- const { configSite, crossing, key, registry, workspaceRoot } = options;
366
+ const { configSite, crossingStep, isWorkspaceRoot, key, registry } = options;
365
367
  const registryEntry = Object.hasOwn(registry, key) ? registry[key] : undefined;
366
368
  if (registryEntry === undefined) {
367
- return (`A \`package.json\` script holds no step list: define \`${key}\` in \`${configSite}\` and move the ` +
369
+ return (`A \`package.json\` script cannot contain a step list: define \`${key}\` in \`${configSite}\` and move the ` +
368
370
  `package-specific steps to ${describeStepDestination(key)}.`);
369
371
  }
370
- const registryChain = renderChain(expandScript(registryEntry, workspaceRoot));
371
- if (registryChain === crossing) {
372
+ const registryChain = renderChain(expandScript(registryEntry, isWorkspaceRoot));
373
+ if (registryChain === crossingStep) {
372
374
  return `Delete the entry: nmr's own \`${key}\` already runs \`${escapeControlCharacters(registryChain)}\`.`;
373
375
  }
374
- return `Delete the entry and move the steps it adds to ${describeStepDestination(key)}.`;
376
+ return `Delete the entry and move the steps that it adds to ${describeStepDestination(key)}.`;
375
377
  }
376
- function findArgError(parsed) {
377
- if (parsed.log && parsed.command === undefined && !parsed.help && !parsed.version) {
378
+ function findArgError(parsedArgs) {
379
+ if (parsedArgs.shouldPrintLog &&
380
+ parsedArgs.command === undefined &&
381
+ !parsedArgs.shouldShowHelp &&
382
+ !parsedArgs.shouldShowVersion) {
378
383
  return '--log requires a command name: `nmr --log <command>`';
379
384
  }
380
385
  return undefined;
381
386
  }
382
387
  function findEmptySelectionRefusal(options) {
383
- const { context, parsed, selection } = options;
384
- if (parsed.filter !== undefined) {
388
+ const { context, parsedArgs, selection } = options;
389
+ const isPackageless = context.workspacePackageDirs.length === 0;
390
+ if (parsedArgs.filter !== undefined) {
385
391
  if (selection !== 'empty') {
386
392
  return undefined;
387
393
  }
388
- return formatEmptyFilterError(parsed.filter, readWorkspacePackageNames(context.workspacePackageDirs));
394
+ if (isPackageless) {
395
+ return `${formatFilterRejection(parsedArgs.filter)} ${describeEmptyWorkspace(context.monorepoRoot)}`;
396
+ }
397
+ return formatEmptyFilterError(parsedArgs.filter, readWorkspacePackageNames(context.workspacePackageDirs));
389
398
  }
390
- return context.workspacePackageDirs.length === 0 ? formatEmptyWorkspaceError() : undefined;
399
+ return isPackageless ? `${RECURSIVE_REJECTION} ${describeEmptyWorkspace(context.monorepoRoot)}` : undefined;
391
400
  }
392
401
  function formatEmptyFilterError(pattern, names) {
393
- const rejection = `-F/--filter matched no workspace: \`${pattern}\`.`;
402
+ const rejection = formatFilterRejection(pattern);
394
403
  if (pattern.startsWith('!')) {
395
- return `${rejection} A pattern beginning with \`!\` excludes what it matches, and this one leaves no package standing.`;
404
+ return `${rejection} A pattern beginning with \`!\` excludes what it matches, and this one excludes every package.`;
396
405
  }
397
406
  if (pattern.includes('[')) {
398
- return `${rejection} A pattern carrying \`[<ref>]\` selects the packages changed since a git ref, and none has changed.`;
407
+ return `${rejection} A pattern containing \`[<ref>]\` selects the packages changed since a git ref, and none has changed.`;
399
408
  }
400
409
  if (pattern.startsWith('.') || pattern.startsWith('/') || pattern.startsWith('{')) {
401
410
  return (`${rejection} ` +
402
411
  'A pattern written `./dir`, `/dir`, or `{dir}` selects the packages under a directory, ' +
403
- 'and no package sits under this one.');
412
+ 'and this directory does not contain any package.');
404
413
  }
405
414
  return (`${rejection} A pattern matches a package's manifest \`name\`, ` +
406
415
  `not its directory name.${suggestWorkspaceNames(pattern, names)}`);
407
416
  }
408
- function formatEmptyWorkspaceError() {
409
- return ('-R/--recursive matched no workspace: pnpm-workspace.yaml declares no package directory, ' +
410
- 'so there is no scope to fan out to.');
417
+ function describeEmptyWorkspace(monorepoRoot) {
418
+ const resolution = resolveWorkspace(monorepoRoot);
419
+ if (resolution.kind !== 'empty') {
420
+ return `pnpm-workspace.yaml at ${monorepoRoot} resolves differently now than it did when the run started.`;
421
+ }
422
+ const { cause, patterns } = resolution;
423
+ const declaredClause = describeDeclaredPatterns(patterns);
424
+ switch (cause) {
425
+ case 'all-excluded':
426
+ return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the positive ` +
427
+ 'patterns. Drop or narrow the exclusion.');
428
+ case 'no-package':
429
+ return (`pnpm-workspace.yaml ${declaredClause}, and the matcher did not find any directory containing a \`package.json\`. ` +
430
+ 'nmr counts a directory as a package only when it contains `package.json`; unlike pnpm, it recognizes ' +
431
+ 'neither `package.yaml` nor `package.json5`. Add a `package.json` to the directory that should be a ' +
432
+ 'package, or declare a pattern reaching a directory that contains one.');
433
+ case 'no-packages-list':
434
+ return (`pnpm-workspace.yaml at ${monorepoRoot} does not declare a \`packages\` list, so the workspace contains the root ` +
435
+ 'package alone. Declare a positive pattern such as `packages/*` to reach the packages beneath it.');
436
+ case 'no-pattern':
437
+ return (`pnpm-workspace.yaml ${declaredClause}, so the matcher does not receive any pattern. Declare a positive ` +
438
+ 'pattern such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string ' +
439
+ 'when it is unquoted.');
440
+ case 'unreadable-packages':
441
+ return (`pnpm-workspace.yaml at ${monorepoRoot} declares a \`packages\` value that is not a list of strings, so ` +
442
+ 'nothing it declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare ' +
443
+ 'value and an entry YAML read as a number or a map are the usual ones.');
444
+ case 'unreadable-manifest':
445
+ return (`pnpm-workspace.yaml at ${monorepoRoot} is not valid YAML, so the matcher does not receive anything that it declares. ` +
446
+ 'Repair the syntax error and run the command again. An unterminated quoted string and a mis-indented ' +
447
+ 'entry are the usual ones.');
448
+ default: {
449
+ const unhandledCause = cause;
450
+ throw new Error(`Unhandled empty-workspace cause: ${String(unhandledCause)}`);
451
+ }
452
+ }
453
+ }
454
+ function describeDeclaredPatterns(patterns) {
455
+ const quotablePatterns = patterns.filter((pattern) => pattern.trim() !== '');
456
+ const emptiedCount = patterns.length - quotablePatterns.length;
457
+ if (emptiedCount === 0) {
458
+ return `declares ${renderQuotedList(patterns)}`;
459
+ }
460
+ const emptiedClause = `${emptiedCount} ${emptiedCount === 1 ? 'entry' : 'entries'} that YAML left empty`;
461
+ return quotablePatterns.length === 0
462
+ ? `declares ${emptiedClause}`
463
+ : `declares ${renderQuotedList(quotablePatterns)}, beside ${emptiedClause}`;
464
+ }
465
+ function formatFilterRejection(pattern) {
466
+ return `-F/--filter did not match any workspace: \`${pattern}\`.`;
411
467
  }
412
468
  function suggestWorkspaceNames(pattern, names) {
413
469
  if (names.length === 0) {
414
470
  return '';
415
471
  }
416
- const containing = names.filter((name) => name.toLowerCase().includes(pattern.toLowerCase()));
417
- if (containing.length > 0) {
418
- return ` Did you mean ${renderNames(containing)}?`;
472
+ const containingNames = names.filter((name) => name.toLowerCase().includes(pattern.toLowerCase()));
473
+ if (containingNames.length > 0) {
474
+ return ` Did you mean ${renderQuotedList(containingNames)}?`;
419
475
  }
420
- const closest = findClosestName(pattern, names);
421
- if (closest !== undefined) {
422
- return ` Did you mean \`${closest}\`?`;
476
+ const closestName = findClosestName(pattern, names);
477
+ if (closestName !== undefined) {
478
+ return ` Did you mean \`${closestName}\`?`;
423
479
  }
424
- return ` The workspace declares ${renderNames(names)}.`;
480
+ return ` The workspace declares ${renderQuotedList(names)}.`;
425
481
  }
426
- function renderNames(names) {
427
- const shown = names
428
- .slice(0, NAME_CEILING)
429
- .map((name) => `\`${name}\``)
482
+ function renderQuotedList(items) {
483
+ const shownItems = items
484
+ .slice(0, LIST_CEILING)
485
+ .map((item) => `\`${item}\``)
430
486
  .join(', ');
431
- const remainder = names.length - NAME_CEILING;
432
- return remainder > 0 ? `${shown}, and ${remainder} more` : shown;
487
+ const remainder = items.length - LIST_CEILING;
488
+ return remainder > 0 ? `${shownItems}, and ${remainder} more` : shownItems;
433
489
  }
434
- function findNoOpReason(resolvedCommand) {
490
+ function findNoOpReason(resolvedCommand, isEmptiedByWorkspace) {
491
+ if (isEmptiedByWorkspace) {
492
+ return 'empty-workspace';
493
+ }
435
494
  if (resolvedCommand === '') {
436
495
  return 'empty-override';
437
496
  }
@@ -441,7 +500,7 @@ function findNoOpReason(resolvedCommand) {
441
500
  return undefined;
442
501
  }
443
502
  function assertNoSelfReference(options) {
444
- const { anchorDir, command, isReading, monorepoRoot, registry, workspaceRoot } = options;
503
+ const { anchorDir, command, isReading, isWorkspaceRoot, monorepoRoot, registry } = options;
445
504
  if (isReading) {
446
505
  return;
447
506
  }
@@ -449,19 +508,19 @@ function assertNoSelfReference(options) {
449
508
  if (script === undefined) {
450
509
  return;
451
510
  }
452
- const remedy = formatSelfReferenceRemedy({ command, monorepoRoot, registry, script, workspaceRoot });
511
+ const remedy = formatSelfReferenceRemedy({ command, isWorkspaceRoot, monorepoRoot, registry, script });
453
512
  const site = path.relative(monorepoRoot, resolvePackageJsonPath(anchorDir));
454
513
  throw new UserError(`${site}: \`scripts.${command}\` re-invokes \`nmr ${command}\` (\`${escapeControlCharacters(script)}\`), ` +
455
- `so nmr cannot run the steps it chains. ${remedy}`);
514
+ `so nmr cannot run the steps that it chains. ${remedy}`);
456
515
  }
457
- function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
516
+ function hasRunnableHook(hookName, registry, anchorDir, isWorkspaceRoot) {
458
517
  if (findChainedSelfReference(anchorDir, hookName) !== undefined) {
459
518
  return true;
460
519
  }
461
- const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
462
- if (!resolved)
520
+ const resolvedScript = resolveScript(hookName, registry, anchorDir, isWorkspaceRoot);
521
+ if (!resolvedScript)
463
522
  return false;
464
- const chain = renderChain(resolved.steps);
523
+ const chain = renderChain(resolvedScript.steps);
465
524
  return chain !== '' && chain !== ':';
466
525
  }
467
526
  async function lookUpRecordedPass(options) {
@@ -475,14 +534,14 @@ async function lookUpRecordedPass(options) {
475
534
  writeDebugNote(`running ${command}: the tree or its inputs changed since the last pass`, env, stderr);
476
535
  return undefined;
477
536
  }
478
- const [missing] = buildOutput.missing;
479
- if (missing !== undefined) {
480
- writeDebugNote(`running ${command}: ${missing} has no build output`, env, stderr);
537
+ const [missingPackage] = buildOutput.missingPackages;
538
+ if (missingPackage !== undefined) {
539
+ writeDebugNote(`running ${command}: ${missingPackage} does not have build output`, env, stderr);
481
540
  return undefined;
482
541
  }
483
- const stale = findStaleBuildOutput(entry.buildDigests, buildOutput.digests);
484
- if (stale !== undefined) {
485
- writeDebugNote(`running ${command}: ${stale}'s build output came from a different tree`, env, stderr);
542
+ const stalePackage = findStaleBuildOutput(entry.buildDigests, buildOutput.digests);
543
+ if (stalePackage !== undefined) {
544
+ writeDebugNote(`running ${command}: ${stalePackage}'s build output came from a different tree`, env, stderr);
486
545
  return undefined;
487
546
  }
488
547
  return {
@@ -512,60 +571,60 @@ function openGate(options) {
512
571
  return snapshot.snapshot;
513
572
  }
514
573
  function parseArgs(args) {
515
- const parsed = {
516
- json: false,
517
- log: false,
518
- noCache: false,
574
+ const parsedArgs = {
575
+ isWorkspaceRoot: false,
519
576
  quiet: false,
520
577
  recursive: false,
521
- workspaceRoot: false,
522
- help: false,
523
- version: false,
578
+ shouldBypassCache: false,
579
+ shouldEmitJson: false,
580
+ shouldPrintLog: false,
581
+ shouldShowHelp: false,
582
+ shouldShowVersion: false,
524
583
  passthrough: [],
525
584
  };
526
- let i = 0;
527
- while (i < args.length) {
528
- const arg = args[i];
585
+ let index = 0;
586
+ while (index < args.length) {
587
+ const arg = args[index];
529
588
  if (arg === undefined)
530
589
  break;
531
590
  if (arg === '-F' || arg === '--filter') {
532
- i++;
533
- const filterValue = args[i];
591
+ index++;
592
+ const filterValue = args[index];
534
593
  if (!filterValue) {
535
594
  return { ok: false, error: '-F/--filter requires a pattern argument' };
536
595
  }
537
- parsed.filter = filterValue;
538
- i++;
596
+ parsedArgs.filter = filterValue;
597
+ index++;
539
598
  continue;
540
599
  }
541
600
  const booleanFlag = BOOLEAN_FLAGS.get(arg);
542
601
  if (booleanFlag !== undefined) {
543
- parsed[booleanFlag] = true;
544
- i++;
602
+ parsedArgs[booleanFlag] = true;
603
+ index++;
545
604
  continue;
546
605
  }
547
- const styleArgument = readOutputStyleArgument(args, i);
606
+ const styleArgument = readOutputStyleArgument(args, index);
548
607
  if (styleArgument !== undefined) {
549
608
  if (!styleArgument.ok) {
550
609
  return styleArgument;
551
610
  }
552
- parsed.outputStyle = styleArgument.value;
553
- i += styleArgument.consumed;
611
+ parsedArgs.outputStyle = styleArgument.value;
612
+ index += styleArgument.consumedCount;
554
613
  continue;
555
614
  }
556
- parsed.command = arg;
557
- parsed.passthrough = args.slice(i + 1);
615
+ parsedArgs.command = arg;
616
+ parsedArgs.passthrough = args.slice(index + 1);
558
617
  break;
559
618
  }
560
- const error = findArgError(parsed);
561
- return error === undefined ? { ok: true, parsed } : { ok: false, error };
619
+ const error = findArgError(parsedArgs);
620
+ return error === undefined ? { ok: true, parsedArgs } : { ok: false, error };
562
621
  }
563
622
  async function reportRecording(options) {
564
623
  const { command, config, scope, styles } = options;
565
624
  const lookup = await resolveRecording({
566
625
  anchorDir: options.anchorDir,
567
626
  command,
568
- current: {
627
+ currentIdentity: {
569
628
  commandString: options.commandString,
570
629
  nmrVersion: VERSION,
571
630
  nodeVersion: CURRENT_RUNTIME.nodeVersion,
@@ -592,9 +651,9 @@ function readPresentation(options) {
592
651
  if (!format.ok) {
593
652
  return format;
594
653
  }
595
- const { invalid, styles } = resolveOutputStyles(options);
596
- if (invalid !== undefined) {
597
- return { ok: false, error: describeInvalidOutputStyle(invalid) };
654
+ const { invalid: invalidStyle, styles } = resolveOutputStyles(options);
655
+ if (invalidStyle !== undefined) {
656
+ return { ok: false, error: describeInvalidOutputStyle(invalidStyle) };
598
657
  }
599
658
  return { ok: true, format: format.format, styles, verbosity: verbosity.verbosity };
600
659
  }
@@ -608,20 +667,27 @@ function readOutputStyleArgument(args, index) {
608
667
  if (!value) {
609
668
  return { ok: false, error: `${OUTPUT_STYLE_FLAG} requires a value argument: auto, plain, or rich` };
610
669
  }
611
- return { ok: true, value, consumed: assignment === undefined ? 2 : 1 };
670
+ return { ok: true, value, consumedCount: assignment === undefined ? 2 : 1 };
671
+ }
672
+ function readRunnableSteps(resolvedScript, workspacePackageDirs) {
673
+ if (workspacePackageDirs.length > 0) {
674
+ return { isEmptiedByWorkspace: false, steps: resolvedScript.steps };
675
+ }
676
+ const steps = dropRecursiveSteps(resolvedScript.steps);
677
+ return { isEmptiedByWorkspace: steps.length === 0 && resolvedScript.steps.length > 0, steps };
612
678
  }
613
679
  function reportNmrCrossing(options) {
614
- const crossing = findNmrCrossing(options.resolved.steps);
615
- if (options.isReading || crossing === undefined) {
680
+ const crossingStep = findNmrCrossing(options.resolvedScript.steps);
681
+ if (options.isReading || crossingStep === undefined) {
616
682
  return;
617
683
  }
618
684
  const warning = formatNmrCrossingWarning({
619
- crossing,
685
+ crossingStep,
686
+ isWorkspaceRoot: options.isWorkspaceRoot,
620
687
  monorepoRoot: options.monorepoRoot,
621
- origin: describeOrigin(options.resolved.origin, options.config, options.useRoot),
688
+ origin: describeOrigin(options.resolvedScript.origin, options.config, options.shouldUseRoot),
622
689
  registry: options.registry,
623
690
  style: options.style,
624
- workspaceRoot: options.workspaceRoot,
625
691
  });
626
692
  options.stderr.write(`${warning}\n`);
627
693
  }
@@ -637,24 +703,24 @@ function resolveReportingVerbosity(options) {
637
703
  }
638
704
  async function recordPass(options) {
639
705
  const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
640
- const current = resolveTreeSnapshot({ monorepoRoot, env: {} });
641
- if (!current.ok) {
642
- writeDebugNote(`not recording ${command}: ${current.reason}`, env, stderr);
706
+ const currentRead = resolveTreeSnapshot({ monorepoRoot, env: {} });
707
+ if (!currentRead.ok) {
708
+ writeDebugNote(`not recording ${command}: ${currentRead.reason}`, env, stderr);
643
709
  return;
644
710
  }
645
- if (current.snapshot.hash !== snapshot.hash) {
711
+ if (currentRead.snapshot.hash !== snapshot.hash) {
646
712
  writeDebugNote(`not recording ${command}: the tree changed while it ran`, env, stderr);
647
713
  return;
648
714
  }
649
715
  const output = await readBuildOutputState(monorepoRoot, options.config);
650
- const [missing] = output.missing;
651
- if (missing !== undefined) {
652
- writeDebugNote(`not recording ${command}: ${missing} has no build output`, env, stderr);
716
+ const [missingPackage] = output.missingPackages;
717
+ if (missingPackage !== undefined) {
718
+ writeDebugNote(`not recording ${command}: ${missingPackage} does not have build output`, env, stderr);
653
719
  return;
654
720
  }
655
- const changed = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
656
- if (changed !== undefined) {
657
- writeDebugNote(`not recording ${command}: ${changed}'s build output changed while it ran`, env, stderr);
721
+ const changedPackage = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
722
+ if (changedPackage !== undefined) {
723
+ writeDebugNote(`not recording ${command}: ${changedPackage}'s build output changed while it ran`, env, stderr);
658
724
  return;
659
725
  }
660
726
  const retention = await composeRetention({
@@ -662,13 +728,13 @@ async function recordPass(options) {
662
728
  command,
663
729
  key: options.retentionKey,
664
730
  monorepoRoot,
665
- retained: options.retained,
731
+ retainedOutput: options.retainedOutput,
666
732
  runId: options.runId,
667
733
  steps: options.ownSteps,
668
734
  treeHash: snapshot.hash,
669
735
  });
670
736
  const ref = { anchorDir, command, monorepoRoot };
671
- const transcript = options.retained === undefined ? undefined : composeTranscript(options.retained);
737
+ const transcript = options.retainedOutput === undefined ? undefined : composeTranscript(options.retainedOutput);
672
738
  try {
673
739
  await recordTranscript(ref, transcript);
674
740
  await writeCheckCacheEntry({
@@ -721,14 +787,14 @@ function resolveCacheKey(options) {
721
787
  return result.key;
722
788
  }
723
789
  async function runDelegation(options) {
724
- const { context, delegation, parsed, runOptions, stderr } = options;
725
- const selection = parsed.filter === undefined ? undefined : readFilterSelection(parsed.filter, context.monorepoRoot);
726
- const refusal = findEmptySelectionRefusal({ context, parsed, selection });
790
+ const { context, delegation, parsedArgs, runOptions, stderr } = options;
791
+ const selection = parsedArgs.filter === undefined ? undefined : readFilterSelection(parsedArgs.filter, context.monorepoRoot);
792
+ const refusal = findEmptySelectionRefusal({ context, parsedArgs, selection });
727
793
  if (refusal !== undefined) {
728
794
  reportError(refusal, stderr);
729
795
  return { exitCode: 1 };
730
796
  }
731
- const shouldWithholdInput = parsed.filter === undefined || selection === 'multiple';
797
+ const shouldWithholdInput = parsedArgs.filter === undefined || selection === 'multiple';
732
798
  const step = shouldWithholdInput ? { ...delegation.step, shouldWithholdInput } : delegation.step;
733
799
  return runSteps([step], context.monorepoRoot, { ...runOptions, env: delegation.env });
734
800
  }
@@ -747,8 +813,8 @@ async function runGated(options) {
747
813
  snapshot,
748
814
  }
749
815
  : undefined;
750
- if (gate !== undefined && !options.noCache) {
751
- const recalled = await lookUpRecordedPass({
816
+ if (gate !== undefined && !options.shouldBypassCache) {
817
+ const recalledPass = await lookUpRecordedPass({
752
818
  anchorDir,
753
819
  buildOutput: gate.buildOutputBefore,
754
820
  command,
@@ -758,8 +824,8 @@ async function runGated(options) {
758
824
  retentionKey: gate.retentionKey,
759
825
  stderr,
760
826
  });
761
- if (recalled !== undefined) {
762
- const { entry, ...recall } = recalled;
827
+ if (recalledPass !== undefined) {
828
+ const { entry, ...recall } = recalledPass;
763
829
  if (recall.replay !== undefined) {
764
830
  await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: options.runId, stderr });
765
831
  }
@@ -769,9 +835,9 @@ async function runGated(options) {
769
835
  if (options.overrideNotice !== undefined) {
770
836
  stdout.write(options.overrideNotice);
771
837
  }
772
- const startedAt = Date.now();
773
- const { exitCode, retained } = await runSteps(options.steps, anchorDir, options.runOptions);
774
- const durationMs = Date.now() - startedAt;
838
+ const startedAtMs = Date.now();
839
+ const { exitCode, retainedOutput } = await runSteps(options.steps, anchorDir, options.runOptions);
840
+ const durationMs = Date.now() - startedAtMs;
775
841
  if (exitCode === 0 && gate !== undefined) {
776
842
  await recordPass({
777
843
  anchorDir,
@@ -784,7 +850,7 @@ async function runGated(options) {
784
850
  key: gate.key,
785
851
  monorepoRoot,
786
852
  ownSteps: options.ownSteps,
787
- retained,
853
+ retainedOutput,
788
854
  retentionKey: gate.retentionKey,
789
855
  runId: options.runId,
790
856
  snapshot: gate.snapshot,
@@ -796,17 +862,17 @@ async function runGated(options) {
796
862
  outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
797
863
  };
798
864
  }
799
- function shellQuote(arg) {
865
+ function quoteForShell(arg) {
800
866
  return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
801
867
  }
802
- function wrapWithHooks(command, mainSteps, registry, anchorDir, workspaceRoot) {
868
+ function wrapWithHooks(command, mainSteps, registry, anchorDir, isWorkspaceRoot) {
803
869
  const steps = [];
804
- if (hasRunnableHook(`${command}:pre`, registry, anchorDir, workspaceRoot)) {
805
- steps.push(composeNmrStep(`${command}:pre`, workspaceRoot));
870
+ if (hasRunnableHook(`${command}:pre`, registry, anchorDir, isWorkspaceRoot)) {
871
+ steps.push(composeNmrStep(`${command}:pre`, isWorkspaceRoot));
806
872
  }
807
873
  steps.push(...mainSteps);
808
- if (hasRunnableHook(`${command}:post`, registry, anchorDir, workspaceRoot)) {
809
- steps.push(composeNmrStep(`${command}:post`, workspaceRoot));
874
+ if (hasRunnableHook(`${command}:post`, registry, anchorDir, isWorkspaceRoot)) {
875
+ steps.push(composeNmrStep(`${command}:post`, isWorkspaceRoot));
810
876
  }
811
877
  return steps;
812
878
  }