@williamthorsen/nmr 0.43.0 → 0.45.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.
- package/.meta/changelog.json +168 -0
- package/.readyup/kits/default.js +1331 -136
- package/.readyup/manifest.json +148 -9
- package/CHANGELOG.md +51 -0
- package/README.md +17 -3
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/commands/fmt-cache.d.ts +26 -0
- package/dist/esm/commands/fmt-cache.js +71 -0
- package/dist/esm/commands/fmt.d.ts +10 -3
- package/dist/esm/commands/fmt.js +34 -6
- package/dist/esm/config.js +74 -9
- package/dist/esm/default-scripts.d.ts +10 -1
- package/dist/esm/default-scripts.js +45 -39
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/entry-chain.d.ts +24 -0
- package/dist/esm/entry-chain.js +57 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +3 -3
- package/dist/esm/helpers/createOutputRecorder.d.ts +17 -0
- package/dist/esm/helpers/createOutputRecorder.js +22 -0
- package/dist/esm/helpers/escapeControlCharacters.d.ts +10 -0
- package/dist/esm/helpers/escapeControlCharacters.js +12 -0
- package/dist/esm/resolve-scripts.d.ts +5 -3
- package/dist/esm/resolve-scripts.js +7 -3
- package/dist/esm/resolver.d.ts +5 -4
- package/dist/esm/resolver.js +16 -6
- package/dist/esm/runCli.js +146 -101
- package/dist/esm/runner.d.ts +7 -0
- package/dist/esm/runner.js +105 -15
- package/dist/esm/steps.d.ts +50 -18
- package/dist/esm/steps.js +82 -13
- package/dist/esm/taze.d.ts +5 -2
- package/dist/esm/taze.js +4 -1
- package/dist/esm/tiers.d.ts +5 -4
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/vitest-git-isolation.js +3 -1
- package/dist/esm/vitest.js +9 -5
- package/dist/esm/workspace-globs.d.ts +5 -0
- package/dist/esm/workspace-globs.js +8 -0
- package/docs/check-cache.md +2 -0
- package/docs/reporting.md +2 -0
- package/docs/scripts.md +36 -10
- package/docs/upgrades.md +1 -1
- package/docs/utilities.md +14 -2
- package/docs/vitest.md +20 -8
- package/package.json +7 -7
package/dist/esm/runCli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { describeInvalidOutputStyle, formatGlyphLine,
|
|
2
|
+
import { describeInvalidOutputStyle, formatGlyphLine, reportError, resolveSelfVersion, 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 { enterChain } from "./entry-chain.js";
|
|
6
7
|
import { NMR_GLYPHS } from "./glyphs.js";
|
|
7
8
|
import { generateHelp } from "./help.js";
|
|
8
9
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
9
10
|
import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
|
|
11
|
+
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
10
12
|
import { readFilterSelection } from "./helpers/filter-selection.js";
|
|
11
13
|
import { findClosestName } from "./helpers/findClosestName.js";
|
|
12
14
|
import { isHookName } from "./helpers/hook-name.js";
|
|
@@ -18,27 +20,38 @@ import { assembleReplay } from "./replay-assembly.js";
|
|
|
18
20
|
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
19
21
|
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, readPackageJsonScripts, resolveScript, } from "./resolver.js";
|
|
20
22
|
import { resolveChannel, runSteps } from "./runner.js";
|
|
21
|
-
import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, quoteToken, renderChain } from "./steps.js";
|
|
23
|
+
import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, quoteToken, readSoleNmrCommand, renderChain, } from "./steps.js";
|
|
22
24
|
import { UserError } from "./UserError.js";
|
|
23
25
|
import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
|
|
24
26
|
import { writeVerdict } from "./verdict.js";
|
|
25
27
|
import { readWorkspacePackageNames, resolveWorkspace } from "./workspace.js";
|
|
26
|
-
const VERSION =
|
|
28
|
+
const VERSION = resolveSelfVersion(import.meta.url);
|
|
27
29
|
const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
|
|
28
30
|
const RECURSIVE_REJECTION = '-R/--recursive did not match any workspace:';
|
|
29
31
|
const LIST_CEILING = 10;
|
|
30
|
-
const NAMED_ESCAPES = new Map([
|
|
31
|
-
['\n', String.raw `\n`],
|
|
32
|
-
['\r', String.raw `\r`],
|
|
33
|
-
['\t', String.raw `\t`],
|
|
34
|
-
]);
|
|
35
32
|
export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
|
|
36
33
|
export async function runCli(options) {
|
|
37
|
-
const
|
|
34
|
+
const established = await establishInvocation(options);
|
|
35
|
+
if (established.settled) {
|
|
36
|
+
return { exitCode: established.result.exitCode };
|
|
37
|
+
}
|
|
38
|
+
const invocation = established.value;
|
|
39
|
+
const planned = await planRun(invocation);
|
|
40
|
+
const { exitCode, isUnknownCommand = false } = planned.settled ? planned.result : await executePlan(planned.value);
|
|
41
|
+
if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(options.env)) {
|
|
42
|
+
const hint = formatMisplacedFlagHint(options.args, invocation.parsedArgs.passthrough, invocation.styles.stderr);
|
|
43
|
+
if (hint !== undefined) {
|
|
44
|
+
options.stderr.write(`${hint}\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { exitCode };
|
|
48
|
+
}
|
|
49
|
+
async function establishInvocation(options) {
|
|
50
|
+
const { args, cwd, env, stdout, stderr } = options;
|
|
38
51
|
const parseResult = parseArgs(args);
|
|
39
52
|
if (!parseResult.ok) {
|
|
40
53
|
reportError(parseResult.error, stderr);
|
|
41
|
-
return { exitCode: 1 };
|
|
54
|
+
return { settled: true, result: { exitCode: 1 } };
|
|
42
55
|
}
|
|
43
56
|
const { parsedArgs } = parseResult;
|
|
44
57
|
const presentationRead = readPresentation({
|
|
@@ -49,24 +62,12 @@ export async function runCli(options) {
|
|
|
49
62
|
});
|
|
50
63
|
if (!presentationRead.ok) {
|
|
51
64
|
reportError(presentationRead.error, stderr);
|
|
52
|
-
return { exitCode: 1 };
|
|
65
|
+
return { settled: true, result: { exitCode: 1 } };
|
|
53
66
|
}
|
|
54
67
|
const { styles } = presentationRead;
|
|
55
|
-
const { exitCode, isUnknownCommand = false } = await runInvocation({ ...options, parsedArgs, presentationRead });
|
|
56
|
-
if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(env)) {
|
|
57
|
-
const hint = formatMisplacedFlagHint(args, parsedArgs.passthrough, styles.stderr);
|
|
58
|
-
if (hint !== undefined) {
|
|
59
|
-
stderr.write(`${hint}\n`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { exitCode };
|
|
63
|
-
}
|
|
64
|
-
async function runInvocation(options) {
|
|
65
|
-
const { cwd, env, parsedArgs, presentationRead, stderr, stdout } = options;
|
|
66
|
-
const { styles } = presentationRead;
|
|
67
68
|
if (parsedArgs.shouldShowVersion) {
|
|
68
69
|
stdout.write(`${VERSION}\n`);
|
|
69
|
-
return { exitCode: 0 };
|
|
70
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
70
71
|
}
|
|
71
72
|
const context = await resolveContext(cwd);
|
|
72
73
|
const format = resolveReportFormat({ envFormat: presentationRead.format, hasJsonFlag: parsedArgs.shouldEmitJson });
|
|
@@ -77,15 +78,33 @@ async function runInvocation(options) {
|
|
|
77
78
|
output: context.config.output,
|
|
78
79
|
hasQuietFlag: parsedArgs.quiet,
|
|
79
80
|
});
|
|
80
|
-
const quiet = verbosity === 'quiet';
|
|
81
81
|
const shouldUseRoot = parsedArgs.isWorkspaceRoot || context.isRoot;
|
|
82
82
|
const anchorDir = shouldUseRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
83
83
|
if (parsedArgs.shouldShowHelp || !parsedArgs.command) {
|
|
84
|
-
stdout.write(`${generateHelp(context.config, anchorDir, shouldUseRoot)}\n`);
|
|
85
|
-
return { exitCode: 0 };
|
|
84
|
+
stdout.write(`${generateHelp(context.config, context.monorepoRoot, anchorDir, shouldUseRoot)}\n`);
|
|
85
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
return {
|
|
88
|
+
settled: false,
|
|
89
|
+
value: {
|
|
90
|
+
anchorDir,
|
|
91
|
+
command: parsedArgs.command,
|
|
92
|
+
context,
|
|
93
|
+
env,
|
|
94
|
+
format,
|
|
95
|
+
parsedArgs,
|
|
96
|
+
scope: path.basename(anchorDir),
|
|
97
|
+
shouldUseRoot,
|
|
98
|
+
stderr,
|
|
99
|
+
stdout,
|
|
100
|
+
styles,
|
|
101
|
+
verbosity,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function planRun(invocation) {
|
|
106
|
+
const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
|
|
107
|
+
const quiet = invocation.verbosity === 'quiet';
|
|
89
108
|
const snapshot = openGate({
|
|
90
109
|
command,
|
|
91
110
|
config: context.config,
|
|
@@ -99,13 +118,13 @@ async function runInvocation(options) {
|
|
|
99
118
|
const runId = resolveRunId(env);
|
|
100
119
|
const childEnv = buildChildEnv({
|
|
101
120
|
env,
|
|
102
|
-
format,
|
|
121
|
+
format: invocation.format,
|
|
103
122
|
shouldBypassCache,
|
|
104
123
|
passthrough: parsedArgs.passthrough,
|
|
105
124
|
runId,
|
|
106
125
|
snapshot,
|
|
107
126
|
styles,
|
|
108
|
-
verbosity,
|
|
127
|
+
verbosity: invocation.verbosity,
|
|
109
128
|
});
|
|
110
129
|
const runOptions = {
|
|
111
130
|
quiet,
|
|
@@ -116,9 +135,14 @@ async function runInvocation(options) {
|
|
|
116
135
|
const delegation = composeDelegation({ childEnv, command, parsedArgs });
|
|
117
136
|
if (delegation !== undefined) {
|
|
118
137
|
const result = await runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
|
|
119
|
-
return {
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
return {
|
|
139
|
+
settled: true,
|
|
140
|
+
result: { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const registry = invocation.shouldUseRoot
|
|
144
|
+
? buildRootRegistry(context.config, context.monorepoRoot)
|
|
145
|
+
: buildWorkspaceRegistry(context.config);
|
|
122
146
|
assertNoSelfReference({
|
|
123
147
|
anchorDir,
|
|
124
148
|
command,
|
|
@@ -130,24 +154,50 @@ async function runInvocation(options) {
|
|
|
130
154
|
const resolvedScript = resolveScript(command, registry, anchorDir, parsedArgs.isWorkspaceRoot);
|
|
131
155
|
if (!resolvedScript) {
|
|
132
156
|
if (env[RUN_IF_PRESENT_ENV_VAR] === '1') {
|
|
133
|
-
return { exitCode: 0 };
|
|
157
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
134
158
|
}
|
|
135
159
|
reportError(`Unknown command: ${command}`, stderr);
|
|
136
|
-
return { exitCode: 1, isUnknownCommand: true };
|
|
160
|
+
return { settled: true, result: { exitCode: 1, isUnknownCommand: true } };
|
|
161
|
+
}
|
|
162
|
+
const entryRunOptions = parsedArgs.shouldPrintLog
|
|
163
|
+
? runOptions
|
|
164
|
+
: {
|
|
165
|
+
...runOptions,
|
|
166
|
+
env: enterChain({ anchorDir, command, env: childEnv, monorepoRoot: context.monorepoRoot }),
|
|
167
|
+
};
|
|
168
|
+
const chain = assembleChain({ invocation, registry, resolvedScript, snapshot });
|
|
169
|
+
if (chain.settled) {
|
|
170
|
+
return chain;
|
|
137
171
|
}
|
|
172
|
+
return {
|
|
173
|
+
settled: false,
|
|
174
|
+
value: {
|
|
175
|
+
...chain.value,
|
|
176
|
+
invocation,
|
|
177
|
+
overrideNotice: formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, styles.stdout),
|
|
178
|
+
runId,
|
|
179
|
+
runOptions: entryRunOptions,
|
|
180
|
+
shouldBypassCache,
|
|
181
|
+
snapshot,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function assembleChain(options) {
|
|
186
|
+
const { invocation, registry, resolvedScript, snapshot } = options;
|
|
187
|
+
const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
|
|
138
188
|
const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
|
|
139
189
|
const resolvedCommand = renderChain(runnableSteps);
|
|
140
190
|
const noOpReason = findNoOpReason(resolvedCommand, isEmptiedByWorkspace);
|
|
141
191
|
if (noOpReason !== undefined && !parsedArgs.shouldPrintLog) {
|
|
142
|
-
reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format, styles.stdout);
|
|
143
|
-
return { exitCode: 0 };
|
|
192
|
+
reportVerdict({ command, scope: invocation.scope, outcome: 'no-op', reason: noOpReason }, stdout, invocation.format, styles.stdout);
|
|
193
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
144
194
|
}
|
|
145
195
|
const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
|
|
146
196
|
const substitutedCommand = renderChain(substitutedSteps);
|
|
147
197
|
const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
|
|
148
198
|
if (!bindResult.ok) {
|
|
149
199
|
reportError(bindResult.error, stderr);
|
|
150
|
-
return { exitCode: 1 };
|
|
200
|
+
return { settled: true, result: { exitCode: 1 } };
|
|
151
201
|
}
|
|
152
202
|
const mainSteps = bindResult.steps;
|
|
153
203
|
const isHookInvocation = isHookName(command);
|
|
@@ -162,7 +212,7 @@ async function runInvocation(options) {
|
|
|
162
212
|
monorepoRoot: context.monorepoRoot,
|
|
163
213
|
registry,
|
|
164
214
|
resolvedScript,
|
|
165
|
-
shouldUseRoot,
|
|
215
|
+
shouldUseRoot: invocation.shouldUseRoot,
|
|
166
216
|
stderr,
|
|
167
217
|
style: styles.stderr,
|
|
168
218
|
});
|
|
@@ -176,40 +226,14 @@ async function runInvocation(options) {
|
|
|
176
226
|
stderr,
|
|
177
227
|
substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
|
|
178
228
|
});
|
|
229
|
+
return { settled: false, value: { commandString: fullCommand, key, ownSteps: mainSteps, steps: fullSteps } };
|
|
230
|
+
}
|
|
231
|
+
async function executePlan(plan) {
|
|
232
|
+
const { command, format, parsedArgs, scope, stdout, styles } = plan.invocation;
|
|
179
233
|
if (parsedArgs.shouldPrintLog) {
|
|
180
|
-
return reportRecording(
|
|
181
|
-
anchorDir,
|
|
182
|
-
command,
|
|
183
|
-
commandString: fullCommand,
|
|
184
|
-
config: context.config,
|
|
185
|
-
env,
|
|
186
|
-
key,
|
|
187
|
-
monorepoRoot: context.monorepoRoot,
|
|
188
|
-
scope,
|
|
189
|
-
snapshot,
|
|
190
|
-
stderr,
|
|
191
|
-
stdout,
|
|
192
|
-
styles,
|
|
193
|
-
});
|
|
234
|
+
return reportRecording(plan);
|
|
194
235
|
}
|
|
195
|
-
const { exitCode, outcome } = await runGated(
|
|
196
|
-
anchorDir,
|
|
197
|
-
command,
|
|
198
|
-
commandString: fullCommand,
|
|
199
|
-
config: context.config,
|
|
200
|
-
steps: fullSteps,
|
|
201
|
-
env,
|
|
202
|
-
key,
|
|
203
|
-
monorepoRoot: context.monorepoRoot,
|
|
204
|
-
overrideNotice: formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, styles.stdout),
|
|
205
|
-
ownSteps: mainSteps,
|
|
206
|
-
runId,
|
|
207
|
-
runOptions,
|
|
208
|
-
shouldBypassCache,
|
|
209
|
-
snapshot,
|
|
210
|
-
stderr,
|
|
211
|
-
stdout,
|
|
212
|
-
});
|
|
236
|
+
const { exitCode, outcome } = await runGated(plan);
|
|
213
237
|
reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
|
|
214
238
|
return { exitCode };
|
|
215
239
|
}
|
|
@@ -323,7 +347,8 @@ function describeCrossingRemedy(options) {
|
|
|
323
347
|
};
|
|
324
348
|
case 'package':
|
|
325
349
|
return {
|
|
326
|
-
remedy:
|
|
350
|
+
remedy: formatInlineRemedy({ crossingStep, file: origin.file, isWorkspaceRoot, key: origin.key, registry }) ??
|
|
351
|
+
formatPackageRemedy({ configSite, crossingStep, isWorkspaceRoot, key: origin.key, registry }),
|
|
327
352
|
subject: `${path.relative(monorepoRoot, origin.file)}: \`scripts.${origin.key}\``,
|
|
328
353
|
};
|
|
329
354
|
default: {
|
|
@@ -348,12 +373,29 @@ function describeOrigin(origin, config, shouldUseRoot) {
|
|
|
348
373
|
? { tier: 'config', field, key: origin.key }
|
|
349
374
|
: { tier: 'default', key: origin.key };
|
|
350
375
|
}
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
376
|
+
function formatInlineRemedy(options) {
|
|
377
|
+
const { crossingStep, file, isWorkspaceRoot, key, registry } = options;
|
|
378
|
+
const name = readSoleNmrCommand(crossingStep);
|
|
379
|
+
if (name === undefined || name === key) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
const scripts = readPackageJsonScripts(path.dirname(file));
|
|
383
|
+
const value = scripts !== undefined && Object.hasOwn(scripts, name) ? scripts[name]?.trim() : undefined;
|
|
384
|
+
if (value === undefined || ['', ':'].includes(value)) {
|
|
385
|
+
return undefined;
|
|
355
386
|
}
|
|
356
|
-
|
|
387
|
+
if (findNmrCrossing([{ kind: 'opaque', command: value }]) !== undefined) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
const anchorDir = path.dirname(file);
|
|
391
|
+
if (!isHookName(name) &&
|
|
392
|
+
[`${name}:pre`, `${name}:post`].some((hook) => hasRunnableHook(hook, registry, anchorDir, isWorkspaceRoot))) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
const inlinedValue = escapeControlCharacters(value);
|
|
396
|
+
return Object.hasOwn(registry, name)
|
|
397
|
+
? `Replace the value with \`${inlinedValue}\`, and keep \`${name}\`, which overrides nmr's own.`
|
|
398
|
+
: `Replace the value with \`${inlinedValue}\` and delete \`${name}\`, the script that it names.`;
|
|
357
399
|
}
|
|
358
400
|
function formatNmrCrossingWarning(options) {
|
|
359
401
|
const { remedy, subject } = describeCrossingRemedy(options);
|
|
@@ -633,26 +675,27 @@ function parseArgs(args) {
|
|
|
633
675
|
const error = findArgError(parsedArgs);
|
|
634
676
|
return error === undefined ? { ok: true, parsedArgs } : { ok: false, error };
|
|
635
677
|
}
|
|
636
|
-
async function reportRecording(
|
|
637
|
-
const { command,
|
|
678
|
+
async function reportRecording(plan) {
|
|
679
|
+
const { anchorDir, command, context, env, scope, stderr, stdout, styles } = plan.invocation;
|
|
680
|
+
const { config, monorepoRoot } = context;
|
|
638
681
|
const lookup = await resolveRecording({
|
|
639
|
-
anchorDir
|
|
682
|
+
anchorDir,
|
|
640
683
|
command,
|
|
641
684
|
currentIdentity: {
|
|
642
|
-
commandString:
|
|
685
|
+
commandString: plan.commandString,
|
|
643
686
|
nmrVersion: VERSION,
|
|
644
687
|
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
645
|
-
treeHash:
|
|
688
|
+
treeHash: plan.snapshot?.hash,
|
|
646
689
|
},
|
|
647
690
|
isCacheable: isCacheableCommand(config.checkCache, command),
|
|
648
|
-
key:
|
|
649
|
-
monorepoRoot
|
|
691
|
+
key: plan.key,
|
|
692
|
+
monorepoRoot,
|
|
650
693
|
});
|
|
651
694
|
if (!lookup.ok) {
|
|
652
|
-
|
|
653
|
-
return { exitCode:
|
|
695
|
+
stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope, style: styles.stderr })}\n`);
|
|
696
|
+
return { exitCode: env[RUN_IF_PRESENT_ENV_VAR] === '1' ? 0 : 1 };
|
|
654
697
|
}
|
|
655
|
-
|
|
698
|
+
stdout.write(renderRecording({ command, recording: lookup.recording, scope, style: styles.stdout }));
|
|
656
699
|
return { exitCode: 0 };
|
|
657
700
|
}
|
|
658
701
|
function readPresentation(options) {
|
|
@@ -812,12 +855,14 @@ async function runDelegation(options) {
|
|
|
812
855
|
const step = shouldWithholdInput ? { ...delegation.step, shouldWithholdInput } : delegation.step;
|
|
813
856
|
return runSteps([step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
814
857
|
}
|
|
815
|
-
async function runGated(
|
|
816
|
-
const {
|
|
817
|
-
const
|
|
858
|
+
async function runGated(plan) {
|
|
859
|
+
const { commandString, key, snapshot } = plan;
|
|
860
|
+
const { anchorDir, command, context, env, stderr, stdout } = plan.invocation;
|
|
861
|
+
const { config, monorepoRoot } = context;
|
|
862
|
+
const quiet = plan.runOptions.quiet === true;
|
|
818
863
|
const gate = key !== undefined && snapshot !== undefined
|
|
819
864
|
? {
|
|
820
|
-
buildOutputBefore: await readBuildOutputState(monorepoRoot,
|
|
865
|
+
buildOutputBefore: await readBuildOutputState(monorepoRoot, config),
|
|
821
866
|
key,
|
|
822
867
|
retentionKey: computeRetentionKey({
|
|
823
868
|
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
@@ -827,7 +872,7 @@ async function runGated(options) {
|
|
|
827
872
|
snapshot,
|
|
828
873
|
}
|
|
829
874
|
: undefined;
|
|
830
|
-
if (gate !== undefined && !
|
|
875
|
+
if (gate !== undefined && !plan.shouldBypassCache) {
|
|
831
876
|
const recalledPass = await lookUpRecordedPass({
|
|
832
877
|
anchorDir,
|
|
833
878
|
buildOutput: gate.buildOutputBefore,
|
|
@@ -841,16 +886,16 @@ async function runGated(options) {
|
|
|
841
886
|
if (recalledPass !== undefined) {
|
|
842
887
|
const { entry, ...recall } = recalledPass;
|
|
843
888
|
if (recall.replay !== undefined) {
|
|
844
|
-
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId:
|
|
889
|
+
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: plan.runId, stderr });
|
|
845
890
|
}
|
|
846
891
|
return { exitCode: 0, outcome: { outcome: 'recalled', ...recall } };
|
|
847
892
|
}
|
|
848
893
|
}
|
|
849
|
-
if (
|
|
850
|
-
stdout.write(
|
|
894
|
+
if (plan.overrideNotice !== undefined) {
|
|
895
|
+
stdout.write(plan.overrideNotice);
|
|
851
896
|
}
|
|
852
897
|
const startedAtMs = Date.now();
|
|
853
|
-
const { exitCode, retainedOutput } = await runSteps(
|
|
898
|
+
const { exitCode, retainedOutput } = await runSteps(plan.steps, anchorDir, plan.runOptions);
|
|
854
899
|
const durationMs = Date.now() - startedAtMs;
|
|
855
900
|
if (exitCode === 0 && gate !== undefined) {
|
|
856
901
|
await recordPass({
|
|
@@ -858,15 +903,15 @@ async function runGated(options) {
|
|
|
858
903
|
buildOutputBefore: gate.buildOutputBefore,
|
|
859
904
|
command,
|
|
860
905
|
commandString,
|
|
861
|
-
config
|
|
906
|
+
config,
|
|
862
907
|
durationMs,
|
|
863
908
|
env,
|
|
864
909
|
key: gate.key,
|
|
865
910
|
monorepoRoot,
|
|
866
|
-
ownSteps:
|
|
911
|
+
ownSteps: plan.ownSteps,
|
|
867
912
|
retainedOutput,
|
|
868
913
|
retentionKey: gate.retentionKey,
|
|
869
|
-
runId:
|
|
914
|
+
runId: plan.runId,
|
|
870
915
|
snapshot: gate.snapshot,
|
|
871
916
|
stderr,
|
|
872
917
|
});
|
|
@@ -894,7 +939,7 @@ function formatMisplacedFlagHint(args, passthrough, style) {
|
|
|
894
939
|
`nmr reads its flags only before the command: \`${correctedTokens.map(quoteToken).join(' ')}\``);
|
|
895
940
|
}
|
|
896
941
|
function isDefinedInAnyScope(command, context) {
|
|
897
|
-
const registries = [buildRootRegistry(context.config), buildWorkspaceRegistry(context.config)];
|
|
942
|
+
const registries = [buildRootRegistry(context.config, context.monorepoRoot), buildWorkspaceRegistry(context.config)];
|
|
898
943
|
if (registries.some((registry) => Object.hasOwn(registry, command))) {
|
|
899
944
|
return true;
|
|
900
945
|
}
|
package/dist/esm/runner.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare function resolveInheritedChannel(stream: Writable): OutputChannel
|
|
|
75
75
|
* makes a signal to nmr end the run: The steps after it are never reached, whereas a shell would have gone on
|
|
76
76
|
* running them unsupervised.
|
|
77
77
|
*
|
|
78
|
+
* A background step is started and left running while the steps after it run. Its output is recorded rather
|
|
79
|
+
* than streamed, and printed whole at the first step boundary after it ends, so that it never interleaves with
|
|
80
|
+
* another step's. A failing foreground step still ends the sequence, but the list waits for every background step
|
|
81
|
+
* that it started and reports the first failure among them when the foreground did not fail. While a background
|
|
82
|
+
* step runs, a SIGINT or SIGTERM to nmr is forwarded to every running child, so that a signal sent to nmr alone
|
|
83
|
+
* still ends the children.
|
|
84
|
+
*
|
|
78
85
|
* Returns a bounded copy of what the list's own commands wrote, so that a caller can retain it. Only opaque
|
|
79
86
|
* steps contribute: A structural step is another nmr process reporting for the subtree beneath it, and its
|
|
80
87
|
* output is that subtree's to account for.
|
package/dist/esm/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
6
|
+
import { createOutputRecorder } from "./helpers/createOutputRecorder.js";
|
|
6
7
|
const SIGNAL_EXIT_BASE = 128;
|
|
7
8
|
const PIPE_DRAIN_GRACE_MS = 2_000;
|
|
8
9
|
export function resolveChannel(stream, quiet) {
|
|
@@ -22,26 +23,81 @@ export async function runSteps(steps, cwd, options) {
|
|
|
22
23
|
};
|
|
23
24
|
const parts = [];
|
|
24
25
|
let isCaptureWhole = true;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const backgroundRuns = [];
|
|
27
|
+
const runningChildren = new Set();
|
|
28
|
+
let receivedSignal;
|
|
29
|
+
let foregroundExitCode = 0;
|
|
30
|
+
function forwardSignal(signal) {
|
|
31
|
+
receivedSignal ??= signal;
|
|
32
|
+
for (const child of runningChildren)
|
|
33
|
+
child.kill(signal);
|
|
34
|
+
}
|
|
35
|
+
function launch(step, launchOptions) {
|
|
36
|
+
const started = startStep(step, cwd, launchOptions);
|
|
37
|
+
runningChildren.add(started.child);
|
|
38
|
+
void started.result.finally(() => runningChildren.delete(started.child));
|
|
39
|
+
return started;
|
|
40
|
+
}
|
|
41
|
+
function flushEndedBackgroundRuns() {
|
|
42
|
+
for (const run of backgroundRuns) {
|
|
43
|
+
if (run.result === undefined || run.isFlushed)
|
|
44
|
+
continue;
|
|
45
|
+
run.recorder.replay({ stderr: resolvedOptions.stderr, stdout: resolvedOptions.stdout });
|
|
46
|
+
run.isFlushed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
for (const step of steps) {
|
|
51
|
+
if (receivedSignal !== undefined) {
|
|
52
|
+
foregroundExitCode = describeSignalExitCode(receivedSignal);
|
|
53
|
+
break;
|
|
30
54
|
}
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
if (step.kind === 'structural' && step.shouldRunInBackground === true) {
|
|
56
|
+
if (backgroundRuns.length === 0) {
|
|
57
|
+
process.on('SIGINT', forwardSignal);
|
|
58
|
+
process.on('SIGTERM', forwardSignal);
|
|
59
|
+
}
|
|
60
|
+
backgroundRuns.push(startBackgroundRun(step, resolvedOptions, launch));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const result = await launch(step, resolvedOptions).result;
|
|
64
|
+
flushEndedBackgroundRuns();
|
|
65
|
+
if (step.kind === 'opaque') {
|
|
66
|
+
if (result.stderr === undefined || result.stdout === undefined) {
|
|
67
|
+
isCaptureWhole = false;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
parts.push({ stderr: result.stderr, stdout: result.stdout });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (result.exitCode !== 0) {
|
|
74
|
+
foregroundExitCode = result.exitCode;
|
|
75
|
+
if (result.outcome === 'signaled') {
|
|
76
|
+
for (const run of backgroundRuns)
|
|
77
|
+
run.child.kill(result.signal);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
33
80
|
}
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
for (const run of backgroundRuns) {
|
|
83
|
+
await run.settled;
|
|
84
|
+
flushEndedBackgroundRuns();
|
|
37
85
|
}
|
|
38
86
|
}
|
|
39
|
-
|
|
87
|
+
finally {
|
|
88
|
+
process.off('SIGINT', forwardSignal);
|
|
89
|
+
process.off('SIGTERM', forwardSignal);
|
|
90
|
+
}
|
|
91
|
+
const exitCode = selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns);
|
|
92
|
+
return { exitCode, ...composeRetainedOutput(parts, isCaptureWhole) };
|
|
40
93
|
}
|
|
41
94
|
export async function runCommand(command, cwd, options) {
|
|
42
95
|
return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
|
|
43
96
|
}
|
|
44
|
-
|
|
97
|
+
function runSpawned(spec, cwd, options) {
|
|
98
|
+
return startSpawned(spec, cwd, options).result;
|
|
99
|
+
}
|
|
100
|
+
function startSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
45
101
|
const quiet = options.quiet === true;
|
|
46
102
|
const stdout = options.stdout ?? process.stdout;
|
|
47
103
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -52,6 +108,9 @@ async function runSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
|
52
108
|
cwd,
|
|
53
109
|
env,
|
|
54
110
|
});
|
|
111
|
+
return { child, result: collectResult(child, { quiet, stderr, stdout }) };
|
|
112
|
+
}
|
|
113
|
+
async function collectResult(child, { quiet, stderr, stdout }) {
|
|
55
114
|
const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
|
|
56
115
|
const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
|
|
57
116
|
const commandCompletion = await awaitCompletion(child, () => {
|
|
@@ -75,17 +134,17 @@ async function runSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
|
75
134
|
}
|
|
76
135
|
return result;
|
|
77
136
|
}
|
|
78
|
-
function
|
|
137
|
+
function startStep(step, cwd, options) {
|
|
79
138
|
const { quiet, stderr, stdout } = options;
|
|
80
139
|
if (step.kind === 'opaque') {
|
|
81
|
-
return
|
|
140
|
+
return startSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: true }, cwd, {
|
|
82
141
|
...options,
|
|
83
142
|
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
84
143
|
});
|
|
85
144
|
}
|
|
86
145
|
const [file, ...args] = step.argv;
|
|
87
146
|
const input = step.shouldWithholdInput === true ? 'ignore' : 'inherit';
|
|
88
|
-
return
|
|
147
|
+
return startSpawned({ args, file, input, shouldUseShell: false }, cwd, {
|
|
89
148
|
...options,
|
|
90
149
|
quiet: false,
|
|
91
150
|
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
@@ -157,18 +216,49 @@ function awaitCompletion(child, stopReading) {
|
|
|
157
216
|
});
|
|
158
217
|
});
|
|
159
218
|
}
|
|
219
|
+
function buildBackgroundEnv(options) {
|
|
220
|
+
const env = options.env ?? process.env;
|
|
221
|
+
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
222
|
+
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
223
|
+
}
|
|
160
224
|
function describeExit(code, signal) {
|
|
161
225
|
if (signal !== null) {
|
|
162
|
-
return { outcome: 'signaled', exitCode:
|
|
226
|
+
return { outcome: 'signaled', exitCode: describeSignalExitCode(signal), signal };
|
|
163
227
|
}
|
|
164
228
|
return { outcome: 'exited', exitCode: code ?? 1 };
|
|
165
229
|
}
|
|
230
|
+
function describeSignalExitCode(signal) {
|
|
231
|
+
return SIGNAL_EXIT_BASE + os.constants.signals[signal];
|
|
232
|
+
}
|
|
166
233
|
function hasDescriptor(stream) {
|
|
167
234
|
return 'fd' in stream && typeof stream.fd === 'number';
|
|
168
235
|
}
|
|
169
236
|
function isTerminal(stream) {
|
|
170
237
|
return 'isTTY' in stream && stream.isTTY === true && hasDescriptor(stream);
|
|
171
238
|
}
|
|
239
|
+
async function recordResult(run, result) {
|
|
240
|
+
run.result = await result;
|
|
241
|
+
}
|
|
242
|
+
function selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns) {
|
|
243
|
+
if (foregroundExitCode !== 0)
|
|
244
|
+
return foregroundExitCode;
|
|
245
|
+
if (receivedSignal !== undefined)
|
|
246
|
+
return describeSignalExitCode(receivedSignal);
|
|
247
|
+
return backgroundRuns.find((run) => run.result !== undefined && run.result.exitCode !== 0)?.result?.exitCode ?? 0;
|
|
248
|
+
}
|
|
249
|
+
function startBackgroundRun(step, options, launch) {
|
|
250
|
+
const recorder = createOutputRecorder();
|
|
251
|
+
const started = launch({ ...step, shouldWithholdInput: true }, { ...options, env: buildBackgroundEnv(options), stderr: recorder.stderr, stdout: recorder.stdout });
|
|
252
|
+
const run = {
|
|
253
|
+
child: started.child,
|
|
254
|
+
isFlushed: false,
|
|
255
|
+
recorder,
|
|
256
|
+
result: undefined,
|
|
257
|
+
settled: Promise.resolve(),
|
|
258
|
+
};
|
|
259
|
+
run.settled = recordResult(run, started.result);
|
|
260
|
+
return run;
|
|
261
|
+
}
|
|
172
262
|
function writeBuffer(buffer, destination) {
|
|
173
263
|
if (buffer !== undefined && buffer.length > 0)
|
|
174
264
|
destination.write(buffer);
|