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