@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.
- package/.meta/changelog.json +114 -0
- package/.readyup/kits/default.js +92 -87
- package/.readyup/manifest.json +8 -8
- package/CHANGELOG.md +89 -0
- package/README.md +37 -28
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache.d.ts +4 -3
- package/dist/esm/check-cache.js +28 -28
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build-output.d.ts +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +73 -72
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +25 -24
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
- package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +14 -11
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +11 -10
- package/dist/esm/config.js +39 -29
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +36 -30
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/duration.d.ts +2 -2
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/package-json.d.ts +2 -2
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/transcript.d.ts +2 -2
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +8 -5
- package/dist/esm/recording.js +27 -17
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +16 -16
- package/dist/esm/runCli.js +309 -221
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +17 -5
- package/dist/esm/steps.js +37 -28
- package/dist/esm/tests.d.ts +7 -7
- package/dist/esm/tests.js +20 -8
- package/dist/esm/tiers.d.ts +1 -1
- package/dist/esm/tiers.js +8 -8
- package/dist/esm/verbosity.d.ts +1 -1
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +15 -4
- package/dist/esm/verdict.js +78 -59
- package/dist/esm/vitest-source-resolution.js +48 -46
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +74 -25
- package/dist/esm/workspace.d.ts +2 -18
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +5 -5
- package/docs/reporting.md +20 -2
- package/docs/scripts.md +2 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +9 -10
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/runCli.js
CHANGED
|
@@ -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
|
|
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 {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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:
|
|
60
|
+
const format = resolveReportFormat({ envFormat: presentationRead.format, hasJsonFlag: parsedArgs.shouldEmitJson });
|
|
52
61
|
const verbosity = resolveReportingVerbosity({
|
|
53
62
|
env,
|
|
54
|
-
envVerbosity:
|
|
63
|
+
envVerbosity: presentationRead.verbosity,
|
|
55
64
|
format,
|
|
56
65
|
output: context.config.output,
|
|
57
|
-
|
|
66
|
+
hasQuietFlag: parsedArgs.quiet,
|
|
58
67
|
});
|
|
59
68
|
const quiet = verbosity === 'quiet';
|
|
60
|
-
const
|
|
61
|
-
const anchorDir =
|
|
62
|
-
if (
|
|
63
|
-
stdout.write(`${generateHelp(context.config, anchorDir,
|
|
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 } =
|
|
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:
|
|
82
|
+
passthrough: parsedArgs.passthrough,
|
|
74
83
|
stderr,
|
|
84
|
+
style: styles.stderr,
|
|
75
85
|
});
|
|
76
|
-
const
|
|
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
|
-
|
|
82
|
-
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,
|
|
104
|
+
const delegation = composeDelegation({ childEnv, command, parsedArgs });
|
|
94
105
|
if (delegation !== undefined) {
|
|
95
|
-
return runDelegation({ context, delegation,
|
|
106
|
+
return runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
|
|
96
107
|
}
|
|
97
|
-
const registry =
|
|
108
|
+
const registry = shouldUseRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
98
109
|
assertNoSelfReference({
|
|
99
110
|
anchorDir,
|
|
100
111
|
command,
|
|
101
|
-
isReading:
|
|
112
|
+
isReading: parsedArgs.shouldPrintLog,
|
|
113
|
+
isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
|
|
102
114
|
monorepoRoot: context.monorepoRoot,
|
|
103
115
|
registry,
|
|
104
|
-
workspaceRoot: parsed.workspaceRoot,
|
|
105
116
|
});
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
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
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
132
|
+
const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
|
|
121
133
|
const substitutedCommand = renderChain(substitutedSteps);
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
124
|
-
reportError(
|
|
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 =
|
|
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,
|
|
143
|
+
: wrapWithHooks(command, mainSteps, registry, anchorDir, parsedArgs.isWorkspaceRoot);
|
|
132
144
|
const fullCommand = renderChain(fullSteps);
|
|
133
145
|
reportNmrCrossing({
|
|
134
146
|
config: context.config,
|
|
135
|
-
isReading:
|
|
147
|
+
isReading: parsedArgs.shouldPrintLog,
|
|
148
|
+
isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
|
|
136
149
|
monorepoRoot: context.monorepoRoot,
|
|
137
150
|
registry,
|
|
138
|
-
|
|
151
|
+
resolvedScript,
|
|
152
|
+
shouldUseRoot,
|
|
139
153
|
stderr,
|
|
140
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
191
|
-
const bypassesCache =
|
|
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
|
|
202
|
-
return step.kind === 'opaque' || step.
|
|
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(
|
|
209
|
-
return { ok: false, error:
|
|
238
|
+
if (!steps.some(acceptsArguments)) {
|
|
239
|
+
return { ok: false, error: formatUnroutableArgumentsError(command) };
|
|
210
240
|
}
|
|
211
241
|
const boundSteps = steps.map((step) => {
|
|
212
|
-
if (!
|
|
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(
|
|
247
|
+
: { kind: 'opaque', command: `${step.command} ${passthrough.map(quoteForShell).join(' ')}` };
|
|
218
248
|
});
|
|
219
249
|
return { ok: true, steps: boundSteps };
|
|
220
250
|
}
|
|
221
|
-
function
|
|
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,
|
|
256
|
+
const { childEnv, command, parsedArgs } = options;
|
|
227
257
|
let scope;
|
|
228
|
-
let
|
|
229
|
-
if (
|
|
230
|
-
scope = ['--filter',
|
|
258
|
+
let shouldRunIfPresent = parsedArgs.shouldPrintLog;
|
|
259
|
+
if (parsedArgs.filter) {
|
|
260
|
+
scope = ['--filter', parsedArgs.filter];
|
|
231
261
|
}
|
|
232
|
-
else if (
|
|
262
|
+
else if (parsedArgs.recursive) {
|
|
233
263
|
scope = ['--recursive'];
|
|
234
|
-
|
|
264
|
+
shouldRunIfPresent = true;
|
|
235
265
|
}
|
|
236
266
|
if (scope === undefined) {
|
|
237
267
|
return undefined;
|
|
238
268
|
}
|
|
239
|
-
const flags =
|
|
269
|
+
const flags = parsedArgs.shouldPrintLog ? ['--log'] : [];
|
|
240
270
|
return {
|
|
241
|
-
env:
|
|
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, ...
|
|
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,
|
|
250
|
-
if (
|
|
251
|
-
const excerpt = deriveExcerpt(
|
|
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 {
|
|
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,
|
|
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
|
|
287
|
-
throw new Error(`Unhandled script origin: ${JSON.stringify(
|
|
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,
|
|
327
|
+
function describeOrigin(origin, config, shouldUseRoot) {
|
|
298
328
|
if (origin.tier === 'package') {
|
|
299
329
|
return origin;
|
|
300
330
|
}
|
|
301
|
-
const field =
|
|
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
|
|
338
|
+
let escapedText = text;
|
|
309
339
|
for (const [char, escape] of NAMED_ESCAPES) {
|
|
310
|
-
|
|
340
|
+
escapedText = escapedText.split(char).join(escape);
|
|
311
341
|
}
|
|
312
|
-
return
|
|
342
|
+
return escapedText;
|
|
313
343
|
}
|
|
314
344
|
function formatNmrCrossingWarning(options) {
|
|
315
345
|
const { remedy, subject } = describeCrossingRemedy(options);
|
|
316
|
-
return (
|
|
317
|
-
|
|
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(
|
|
349
|
+
function formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, style) {
|
|
320
350
|
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
321
|
-
if (quiet ||
|
|
351
|
+
if (quiet || resolvedScript.origin.tier !== 'package' || registryEntry === undefined) {
|
|
322
352
|
return undefined;
|
|
323
353
|
}
|
|
324
|
-
|
|
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
|
|
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,
|
|
363
|
+
return formatPackageRemedy({ configSite, crossingStep: script, isWorkspaceRoot, key: command, registry });
|
|
333
364
|
}
|
|
334
365
|
function formatPackageRemedy(options) {
|
|
335
|
-
const { configSite,
|
|
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,
|
|
342
|
-
if (registryChain ===
|
|
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(
|
|
348
|
-
if (
|
|
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,
|
|
355
|
-
|
|
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
|
-
|
|
394
|
+
if (isPackageless) {
|
|
395
|
+
return `${formatFilterRejection(parsedArgs.filter)} ${describeEmptyWorkspace(context.monorepoRoot)}`;
|
|
396
|
+
}
|
|
397
|
+
return formatEmptyFilterError(parsedArgs.filter, readWorkspacePackageNames(context.workspacePackageDirs));
|
|
360
398
|
}
|
|
361
|
-
return
|
|
399
|
+
return isPackageless ? `${RECURSIVE_REJECTION} ${describeEmptyWorkspace(context.monorepoRoot)}` : undefined;
|
|
362
400
|
}
|
|
363
401
|
function formatEmptyFilterError(pattern, names) {
|
|
364
|
-
const rejection =
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
388
|
-
if (
|
|
389
|
-
return ` Did you mean ${
|
|
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
|
|
392
|
-
if (
|
|
393
|
-
return ` Did you mean \`${
|
|
476
|
+
const closestName = findClosestName(pattern, names);
|
|
477
|
+
if (closestName !== undefined) {
|
|
478
|
+
return ` Did you mean \`${closestName}\`?`;
|
|
394
479
|
}
|
|
395
|
-
return ` The workspace declares ${
|
|
480
|
+
return ` The workspace declares ${renderQuotedList(names)}.`;
|
|
396
481
|
}
|
|
397
|
-
function
|
|
398
|
-
const
|
|
399
|
-
.slice(0,
|
|
400
|
-
.map((
|
|
482
|
+
function renderQuotedList(items) {
|
|
483
|
+
const shownItems = items
|
|
484
|
+
.slice(0, LIST_CEILING)
|
|
485
|
+
.map((item) => `\`${item}\``)
|
|
401
486
|
.join(', ');
|
|
402
|
-
const remainder =
|
|
403
|
-
return remainder > 0 ? `${
|
|
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
|
|
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
|
|
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,
|
|
516
|
+
function hasRunnableHook(hookName, registry, anchorDir, isWorkspaceRoot) {
|
|
429
517
|
if (findChainedSelfReference(anchorDir, hookName) !== undefined) {
|
|
430
518
|
return true;
|
|
431
519
|
}
|
|
432
|
-
const
|
|
433
|
-
if (!
|
|
520
|
+
const resolvedScript = resolveScript(hookName, registry, anchorDir, isWorkspaceRoot);
|
|
521
|
+
if (!resolvedScript)
|
|
434
522
|
return false;
|
|
435
|
-
const chain = renderChain(
|
|
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 [
|
|
450
|
-
if (
|
|
451
|
-
writeDebugNote(`running ${command}: ${
|
|
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
|
|
455
|
-
if (
|
|
456
|
-
writeDebugNote(`running ${command}: ${
|
|
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
|
|
487
|
-
|
|
488
|
-
log: false,
|
|
489
|
-
noCache: false,
|
|
574
|
+
const parsedArgs = {
|
|
575
|
+
isWorkspaceRoot: false,
|
|
490
576
|
quiet: false,
|
|
491
577
|
recursive: false,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
578
|
+
shouldBypassCache: false,
|
|
579
|
+
shouldEmitJson: false,
|
|
580
|
+
shouldPrintLog: false,
|
|
581
|
+
shouldShowHelp: false,
|
|
582
|
+
shouldShowVersion: false,
|
|
495
583
|
passthrough: [],
|
|
496
584
|
};
|
|
497
|
-
let
|
|
498
|
-
while (
|
|
499
|
-
const arg = args[
|
|
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
|
-
|
|
504
|
-
const filterValue = args[
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
600
|
+
const booleanFlag = BOOLEAN_FLAGS.get(arg);
|
|
601
|
+
if (booleanFlag !== undefined) {
|
|
602
|
+
parsedArgs[booleanFlag] = true;
|
|
603
|
+
index++;
|
|
525
604
|
continue;
|
|
526
605
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
553
|
-
|
|
615
|
+
parsedArgs.command = arg;
|
|
616
|
+
parsedArgs.passthrough = args.slice(index + 1);
|
|
554
617
|
break;
|
|
555
618
|
}
|
|
556
|
-
const error = findArgError(
|
|
557
|
-
return error === undefined ? { ok: true,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
594
|
-
if (options.isReading ||
|
|
680
|
+
const crossingStep = findNmrCrossing(options.resolvedScript.steps);
|
|
681
|
+
if (options.isReading || crossingStep === undefined) {
|
|
595
682
|
return;
|
|
596
683
|
}
|
|
597
684
|
const warning = formatNmrCrossingWarning({
|
|
598
|
-
|
|
685
|
+
crossingStep,
|
|
686
|
+
isWorkspaceRoot: options.isWorkspaceRoot,
|
|
599
687
|
monorepoRoot: options.monorepoRoot,
|
|
600
|
-
origin: describeOrigin(options.
|
|
688
|
+
origin: describeOrigin(options.resolvedScript.origin, options.config, options.shouldUseRoot),
|
|
601
689
|
registry: options.registry,
|
|
602
|
-
|
|
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
|
|
619
|
-
if (!
|
|
620
|
-
writeDebugNote(`not recording ${command}: ${
|
|
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 (
|
|
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 [
|
|
629
|
-
if (
|
|
630
|
-
writeDebugNote(`not recording ${command}: ${
|
|
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
|
|
634
|
-
if (
|
|
635
|
-
writeDebugNote(`not recording ${command}: ${
|
|
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
|
-
|
|
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.
|
|
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,
|
|
703
|
-
const selection =
|
|
704
|
-
const refusal = findEmptySelectionRefusal({ context,
|
|
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 =
|
|
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.
|
|
729
|
-
const
|
|
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 (
|
|
740
|
-
const { entry, ...recall } =
|
|
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
|
|
751
|
-
const { exitCode,
|
|
752
|
-
const durationMs = Date.now() -
|
|
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
|
-
|
|
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
|
|
865
|
+
function quoteForShell(arg) {
|
|
778
866
|
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
779
867
|
}
|
|
780
|
-
function wrapWithHooks(command, mainSteps, registry, anchorDir,
|
|
868
|
+
function wrapWithHooks(command, mainSteps, registry, anchorDir, isWorkspaceRoot) {
|
|
781
869
|
const steps = [];
|
|
782
|
-
if (hasRunnableHook(`${command}:pre`, registry, anchorDir,
|
|
783
|
-
steps.push(composeNmrStep(`${command}:pre`,
|
|
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,
|
|
787
|
-
steps.push(composeNmrStep(`${command}:post`,
|
|
874
|
+
if (hasRunnableHook(`${command}:post`, registry, anchorDir, isWorkspaceRoot)) {
|
|
875
|
+
steps.push(composeNmrStep(`${command}:post`, isWorkspaceRoot));
|
|
788
876
|
}
|
|
789
877
|
return steps;
|
|
790
878
|
}
|