@williamthorsen/nmr 0.30.0 → 0.32.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/.readyup/kits/default.js +56 -16
- package/.readyup/manifest.json +34 -3
- package/README.md +265 -64
- package/agents/guidance/rulebooks/nmr.md +5 -2
- package/bin/nmr-report-catalog.js +11 -0
- package/dist/esm/UserError.d.ts +3 -0
- package/dist/esm/UserError.js +6 -0
- package/dist/esm/check-cache.d.ts +36 -1
- package/dist/esm/check-cache.js +91 -23
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -24
- package/dist/esm/cli-report-catalog.d.ts +1 -0
- package/dist/esm/cli-report-catalog.js +11 -0
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/build-output.d.ts +1 -0
- package/dist/esm/commands/build-output.js +25 -2
- package/dist/esm/commands/build.d.ts +5 -1
- package/dist/esm/commands/build.js +22 -8
- package/dist/esm/commands/clean.js +12 -0
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -0
- package/dist/esm/commands/ensure-prepublish-hooks.js +43 -0
- package/dist/esm/commands/report-catalog.d.ts +1 -0
- package/dist/esm/commands/report-catalog.js +40 -0
- package/dist/esm/commands/report-overrides.js +30 -7
- package/dist/esm/commands/taze.js +2 -1
- package/dist/esm/config.d.ts +1 -0
- package/dist/esm/config.js +66 -18
- package/dist/esm/default-scripts.d.ts +5 -1
- package/dist/esm/default-scripts.js +13 -10
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.js +3 -1
- package/dist/esm/helpers/clampToBytes.d.ts +2 -0
- package/dist/esm/helpers/clampToBytes.js +20 -0
- package/dist/esm/helpers/createBoundedBuffer.js +1 -4
- package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
- package/dist/esm/helpers/deriveExcerpt.js +30 -0
- package/dist/esm/helpers/duration.js +16 -9
- package/dist/esm/helpers/package-json.d.ts +11 -4
- package/dist/esm/helpers/package-json.js +51 -22
- package/dist/esm/helpers/readStringValues.d.ts +1 -0
- package/dist/esm/helpers/readStringValues.js +8 -0
- package/dist/esm/helpers/reportClosing.d.ts +1 -0
- package/dist/esm/helpers/reportClosing.js +3 -0
- package/dist/esm/helpers/transcript.d.ts +5 -0
- package/dist/esm/helpers/transcript.js +59 -0
- package/dist/esm/recording.d.ts +65 -0
- package/dist/esm/recording.js +96 -0
- package/dist/esm/replay-assembly.d.ts +9 -0
- package/dist/esm/replay-assembly.js +26 -0
- package/dist/esm/report-format.d.ts +18 -0
- package/dist/esm/report-format.js +25 -0
- package/dist/esm/reportCliFailure.d.ts +2 -0
- package/dist/esm/reportCliFailure.js +11 -0
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolver.d.ts +9 -1
- package/dist/esm/resolver.js +28 -18
- package/dist/esm/runCli.js +339 -74
- package/dist/esm/runner.d.ts +9 -3
- package/dist/esm/runner.js +25 -4
- package/dist/esm/scripts.d.ts +1 -1
- package/dist/esm/steps.d.ts +9 -2
- package/dist/esm/steps.js +117 -4
- package/dist/esm/types.d.ts +11 -2
- package/dist/esm/verbosity.d.ts +15 -4
- package/dist/esm/verbosity.js +32 -12
- package/dist/esm/verdict.d.ts +29 -0
- package/dist/esm/verdict.js +150 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +20 -3
- package/package.json +14 -5
package/dist/esm/runCli.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
3
|
-
import {
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
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
|
+
import { resolveConfigPath } from "./config.js";
|
|
4
6
|
import { resolveContext } from "./context.js";
|
|
5
7
|
import { generateHelp } from "./help.js";
|
|
8
|
+
import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
|
|
6
9
|
import { isHookName } from "./helpers/hook-name.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { composeTranscript } from "./helpers/transcript.js";
|
|
11
|
+
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
12
|
+
import { assembleReplay } from "./replay-assembly.js";
|
|
13
|
+
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
14
|
+
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, resolveScript, } from "./resolver.js";
|
|
15
|
+
import { resolveChannel, runSteps } from "./runner.js";
|
|
16
|
+
import { composeNmrStep, findNmrCrossing, renderChain } from "./steps.js";
|
|
17
|
+
import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity } from "./verbosity.js";
|
|
18
|
+
import { writeVerdict } from "./verdict.js";
|
|
11
19
|
const VERSION = readPackageVersion(import.meta.url);
|
|
20
|
+
const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
|
|
12
21
|
export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
|
|
13
22
|
export async function runCli(options) {
|
|
14
23
|
const { args, cwd, env, stdout, stderr } = options;
|
|
@@ -18,17 +27,25 @@ export async function runCli(options) {
|
|
|
18
27
|
return { exitCode: 1 };
|
|
19
28
|
}
|
|
20
29
|
const { parsed } = parseResult;
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
reportError(
|
|
30
|
+
const inherited = readPresentationEnv(env);
|
|
31
|
+
if (!inherited.ok) {
|
|
32
|
+
reportError(inherited.error, stderr);
|
|
24
33
|
return { exitCode: 1 };
|
|
25
34
|
}
|
|
26
35
|
if (parsed.version) {
|
|
27
36
|
stdout.write(`${VERSION}\n`);
|
|
28
37
|
return { exitCode: 0 };
|
|
29
38
|
}
|
|
30
|
-
const quiet = verbosity.verbosity === 'quiet';
|
|
31
39
|
const context = await resolveContext(cwd);
|
|
40
|
+
const format = resolveReportFormat({ envFormat: inherited.format, jsonFlag: parsed.json });
|
|
41
|
+
const verbosity = resolveReportingVerbosity({
|
|
42
|
+
env,
|
|
43
|
+
envVerbosity: inherited.verbosity,
|
|
44
|
+
format,
|
|
45
|
+
output: context.config.output,
|
|
46
|
+
quietFlag: parsed.quiet,
|
|
47
|
+
});
|
|
48
|
+
const quiet = verbosity === 'quiet';
|
|
32
49
|
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
33
50
|
const anchorDir = useRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
34
51
|
if (parsed.help || !parsed.command) {
|
|
@@ -36,6 +53,7 @@ export async function runCli(options) {
|
|
|
36
53
|
return { exitCode: 0 };
|
|
37
54
|
}
|
|
38
55
|
const { command } = parsed;
|
|
56
|
+
const scope = path.basename(anchorDir);
|
|
39
57
|
const snapshot = openGate({
|
|
40
58
|
command,
|
|
41
59
|
config: context.config,
|
|
@@ -45,21 +63,25 @@ export async function runCli(options) {
|
|
|
45
63
|
stderr,
|
|
46
64
|
});
|
|
47
65
|
const noCache = parsed.noCache || env[NO_CACHE_ENV_VAR] === '1';
|
|
48
|
-
const
|
|
66
|
+
const runId = resolveRunId(env);
|
|
67
|
+
const childEnv = buildChildEnv({
|
|
68
|
+
env,
|
|
69
|
+
format,
|
|
70
|
+
noCache,
|
|
71
|
+
passthrough: parsed.passthrough,
|
|
72
|
+
runId,
|
|
73
|
+
snapshot,
|
|
74
|
+
verbosity,
|
|
75
|
+
});
|
|
49
76
|
const runOptions = {
|
|
50
77
|
quiet,
|
|
51
78
|
stdout,
|
|
52
79
|
stderr,
|
|
53
80
|
env: childEnv,
|
|
54
81
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return runSteps([
|
|
58
|
-
}
|
|
59
|
-
if (parsed.recursive) {
|
|
60
|
-
const delegateEnv = { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' };
|
|
61
|
-
const delegate = composeDelegate(['--recursive'], command, parsed.passthrough);
|
|
62
|
-
return runSteps([delegate], context.monorepoRoot, { ...runOptions, env: delegateEnv });
|
|
82
|
+
const delegation = composeDelegation({ childEnv, command, parsed });
|
|
83
|
+
if (delegation !== undefined) {
|
|
84
|
+
return runSteps([delegation.step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
63
85
|
}
|
|
64
86
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
65
87
|
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
@@ -71,22 +93,34 @@ export async function runCli(options) {
|
|
|
71
93
|
return { exitCode: 1 };
|
|
72
94
|
}
|
|
73
95
|
const resolvedCommand = renderChain(resolved.steps);
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
96
|
+
const noOpReason = findNoOpReason(resolvedCommand);
|
|
97
|
+
if (noOpReason !== undefined && !parsed.log) {
|
|
98
|
+
reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format);
|
|
99
|
+
return { exitCode: 0 };
|
|
77
100
|
}
|
|
78
101
|
const substitutedSteps = applyDevBinToSteps(resolved.steps, context.config.devBin, context.monorepoRoot);
|
|
79
102
|
const substitutedCommand = renderChain(substitutedSteps);
|
|
80
|
-
const
|
|
103
|
+
const bound = bindPassthrough(substitutedSteps, parsed.passthrough, command);
|
|
104
|
+
if (!bound.ok) {
|
|
105
|
+
reportError(bound.error, stderr);
|
|
106
|
+
return { exitCode: 1 };
|
|
107
|
+
}
|
|
108
|
+
const mainSteps = bound.steps;
|
|
81
109
|
const isHookInvocation = isHookName(command);
|
|
82
110
|
const fullSteps = isHookInvocation
|
|
83
111
|
? mainSteps
|
|
84
112
|
: wrapWithHooks(command, mainSteps, registry, anchorDir, parsed.workspaceRoot);
|
|
85
113
|
const fullCommand = renderChain(fullSteps);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
reportNmrCrossing({
|
|
115
|
+
config: context.config,
|
|
116
|
+
isReading: parsed.log,
|
|
117
|
+
monorepoRoot: context.monorepoRoot,
|
|
118
|
+
registry,
|
|
119
|
+
resolved,
|
|
120
|
+
stderr,
|
|
121
|
+
useRoot,
|
|
122
|
+
workspaceRoot: parsed.workspaceRoot,
|
|
123
|
+
});
|
|
90
124
|
const key = resolveCacheKey({
|
|
91
125
|
anchorDir,
|
|
92
126
|
command,
|
|
@@ -97,7 +131,22 @@ export async function runCli(options) {
|
|
|
97
131
|
stderr,
|
|
98
132
|
substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
|
|
99
133
|
});
|
|
100
|
-
|
|
134
|
+
if (parsed.log) {
|
|
135
|
+
return reportRecording({
|
|
136
|
+
anchorDir,
|
|
137
|
+
command,
|
|
138
|
+
commandString: fullCommand,
|
|
139
|
+
config: context.config,
|
|
140
|
+
env,
|
|
141
|
+
key,
|
|
142
|
+
monorepoRoot: context.monorepoRoot,
|
|
143
|
+
scope,
|
|
144
|
+
snapshot,
|
|
145
|
+
stderr,
|
|
146
|
+
stdout,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const { exitCode, outcome } = await runGated({
|
|
101
150
|
anchorDir,
|
|
102
151
|
command,
|
|
103
152
|
commandString: fullCommand,
|
|
@@ -108,58 +157,165 @@ export async function runCli(options) {
|
|
|
108
157
|
monorepoRoot: context.monorepoRoot,
|
|
109
158
|
noCache,
|
|
110
159
|
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, quiet),
|
|
111
|
-
|
|
160
|
+
ownSteps: mainSteps,
|
|
161
|
+
runId,
|
|
112
162
|
runOptions,
|
|
113
163
|
snapshot,
|
|
114
164
|
stderr,
|
|
115
165
|
stdout,
|
|
116
166
|
});
|
|
167
|
+
reportVerdict({ command, scope, ...outcome }, stdout, format);
|
|
117
168
|
return { exitCode };
|
|
118
169
|
}
|
|
119
|
-
function buildChildEnv(
|
|
170
|
+
function buildChildEnv(options) {
|
|
171
|
+
const { env, format, noCache, passthrough, runId, snapshot, verbosity } = options;
|
|
172
|
+
const bypassesCache = noCache || passthrough.length > 0;
|
|
120
173
|
return {
|
|
121
174
|
...env,
|
|
122
175
|
...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
|
|
123
|
-
...(
|
|
176
|
+
...(bypassesCache && { [NO_CACHE_ENV_VAR]: '1' }),
|
|
124
177
|
[COMMAND_VERBOSITY_ENV_VAR]: verbosity,
|
|
178
|
+
[REPORT_FORMAT_ENV_VAR]: format,
|
|
179
|
+
[RUN_ID_ENV_VAR]: runId,
|
|
125
180
|
};
|
|
126
181
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
182
|
+
function acceptsArgs(step) {
|
|
183
|
+
return step.kind === 'opaque' || step.declinesArgs !== true;
|
|
184
|
+
}
|
|
185
|
+
function bindPassthrough(steps, passthrough, command) {
|
|
186
|
+
if (passthrough.length === 0) {
|
|
187
|
+
return { ok: true, steps };
|
|
188
|
+
}
|
|
189
|
+
if (!steps.some(acceptsArgs)) {
|
|
190
|
+
return { ok: false, error: formatUnroutableArgsError(command) };
|
|
191
|
+
}
|
|
192
|
+
const boundSteps = steps.map((step) => {
|
|
193
|
+
if (!acceptsArgs(step)) {
|
|
194
|
+
return step;
|
|
195
|
+
}
|
|
196
|
+
return step.kind === 'structural'
|
|
197
|
+
? { ...step, argv: [...step.argv, ...passthrough] }
|
|
198
|
+
: { kind: 'opaque', command: `${step.command} ${passthrough.map(shellQuote).join(' ')}` };
|
|
199
|
+
});
|
|
200
|
+
return { ok: true, steps: boundSteps };
|
|
201
|
+
}
|
|
202
|
+
function formatUnroutableArgsError(command) {
|
|
203
|
+
return (`\`${command}\` takes no trailing arguments: every step of its chain declines them. ` +
|
|
204
|
+
'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
|
|
205
|
+
}
|
|
206
|
+
function composeDelegation(options) {
|
|
207
|
+
const { childEnv, command, parsed } = options;
|
|
208
|
+
let scope;
|
|
209
|
+
let runIfPresent = parsed.log;
|
|
210
|
+
if (parsed.filter) {
|
|
211
|
+
scope = ['--filter', parsed.filter];
|
|
212
|
+
}
|
|
213
|
+
else if (parsed.recursive) {
|
|
214
|
+
scope = ['--recursive'];
|
|
215
|
+
runIfPresent = true;
|
|
216
|
+
}
|
|
217
|
+
if (scope === undefined) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const flags = parsed.log ? ['--log'] : [];
|
|
221
|
+
return {
|
|
222
|
+
env: runIfPresent ? { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' } : childEnv,
|
|
223
|
+
step: {
|
|
224
|
+
kind: 'structural',
|
|
225
|
+
argv: ['pnpm', ...scope, 'exec', 'nmr', ...flags, command, ...parsed.passthrough],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function composeRetention(options) {
|
|
230
|
+
const { anchorDir, command, key, retained, runId } = options;
|
|
231
|
+
if (retained !== undefined) {
|
|
232
|
+
const excerpt = deriveExcerpt(retained.stdout.toString('utf8')) ?? deriveExcerpt(retained.stderr.toString('utf8'));
|
|
233
|
+
if (excerpt === undefined) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return { key, replay: [{ command, excerpt, scope: path.basename(anchorDir) }], runId };
|
|
237
|
+
}
|
|
238
|
+
const replay = await assembleReplay({
|
|
239
|
+
anchorDir,
|
|
240
|
+
monorepoRoot: options.monorepoRoot,
|
|
241
|
+
runId,
|
|
242
|
+
steps: options.steps,
|
|
243
|
+
treeHash: options.treeHash,
|
|
244
|
+
});
|
|
245
|
+
return replay.length === 0 ? undefined : { key, replay, runId };
|
|
246
|
+
}
|
|
247
|
+
function describeCrossingRemedy(options) {
|
|
248
|
+
const { crossing, monorepoRoot, origin, registry, workspaceRoot } = options;
|
|
249
|
+
const configSite = path.relative(monorepoRoot, resolveConfigPath(monorepoRoot));
|
|
250
|
+
switch (origin.tier) {
|
|
251
|
+
case 'default':
|
|
252
|
+
return {
|
|
253
|
+
remedy: 'A built-in default reaching nmr through a shell is an nmr defect: please report it.',
|
|
254
|
+
subject: `nmr built-in \`${origin.key}\``,
|
|
255
|
+
};
|
|
256
|
+
case 'config':
|
|
257
|
+
return {
|
|
258
|
+
remedy: `Write the nmr steps as a step list, and move any others to a \`${origin.key}:pre\` or ` +
|
|
259
|
+
`\`${origin.key}:post\` script.`,
|
|
260
|
+
subject: `${configSite}: \`${origin.field}.${origin.key}\``,
|
|
261
|
+
};
|
|
262
|
+
case 'package':
|
|
263
|
+
return {
|
|
264
|
+
remedy: formatPackageRemedy({ configSite, crossing, key: origin.key, registry, workspaceRoot }),
|
|
265
|
+
subject: `${path.relative(monorepoRoot, origin.file)}: \`scripts.${origin.key}\``,
|
|
266
|
+
};
|
|
267
|
+
default: {
|
|
268
|
+
const unhandled = origin;
|
|
269
|
+
throw new Error(`Unhandled script origin: ${JSON.stringify(unhandled)}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function describeOrigin(origin, config, useRoot) {
|
|
274
|
+
if (origin.tier === 'package') {
|
|
275
|
+
return origin;
|
|
276
|
+
}
|
|
277
|
+
const field = useRoot ? 'rootScripts' : 'workspaceScripts';
|
|
278
|
+
const configScripts = config[field];
|
|
279
|
+
return configScripts !== undefined && Object.hasOwn(configScripts, origin.key)
|
|
280
|
+
? { tier: 'config', field, key: origin.key }
|
|
281
|
+
: { tier: 'default', key: origin.key };
|
|
136
282
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
283
|
+
function formatNmrCrossingWarning(options) {
|
|
284
|
+
const { remedy, subject } = describeCrossingRemedy(options);
|
|
285
|
+
return `⚠️ ${subject} reaches nmr through a shell (\`${options.crossing}\`), ${CROSSING_CONSEQUENCE} ${remedy}`;
|
|
139
286
|
}
|
|
140
287
|
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
141
288
|
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
142
|
-
if (quiet || resolved.
|
|
289
|
+
if (quiet || resolved.origin.tier !== 'package' || registryEntry === undefined) {
|
|
143
290
|
return undefined;
|
|
144
291
|
}
|
|
145
292
|
return `📦 ${path.basename(anchorDir)}: Using override script: ${renderChain(resolved.steps)}\n`;
|
|
146
293
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
294
|
+
function formatPackageRemedy(options) {
|
|
295
|
+
const { configSite, crossing, key, registry, workspaceRoot } = options;
|
|
296
|
+
const registryEntry = Object.hasOwn(registry, key) ? registry[key] : undefined;
|
|
297
|
+
if (registryEntry === undefined) {
|
|
298
|
+
return (`A \`package.json\` script holds no step list: define \`${key}\` in \`${configSite}\` and move the ` +
|
|
299
|
+
`package-specific steps to a \`${key}:pre\` or \`${key}:post\` script.`);
|
|
300
|
+
}
|
|
301
|
+
const registryChain = renderChain(expandScript(registryEntry, workspaceRoot));
|
|
302
|
+
if (registryChain === crossing) {
|
|
303
|
+
return `Delete the entry: nmr's own \`${key}\` already runs \`${registryChain}\`.`;
|
|
304
|
+
}
|
|
305
|
+
return `Delete the entry and move the steps it adds to a \`${key}:pre\` or \`${key}:post\` script.`;
|
|
306
|
+
}
|
|
307
|
+
function findArgError(parsed) {
|
|
308
|
+
if (parsed.log && parsed.command === undefined && !parsed.help && !parsed.version) {
|
|
309
|
+
return '--log requires a command name: `nmr --log <command>`';
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
150
312
|
}
|
|
151
|
-
function
|
|
313
|
+
function findNoOpReason(resolvedCommand) {
|
|
152
314
|
if (resolvedCommand === '') {
|
|
153
|
-
|
|
154
|
-
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is defined but empty. Skipping.\n`);
|
|
155
|
-
}
|
|
156
|
-
return 0;
|
|
315
|
+
return 'empty-override';
|
|
157
316
|
}
|
|
158
317
|
if (resolvedCommand === ':') {
|
|
159
|
-
|
|
160
|
-
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is a no-op. Skipping.\n`);
|
|
161
|
-
}
|
|
162
|
-
return 0;
|
|
318
|
+
return 'noop-override';
|
|
163
319
|
}
|
|
164
320
|
return undefined;
|
|
165
321
|
}
|
|
@@ -191,20 +347,23 @@ async function lookUpRecordedPass(options) {
|
|
|
191
347
|
writeDebugNote(`running ${command}: ${stale}'s build output came from a different tree`, env, stderr);
|
|
192
348
|
return undefined;
|
|
193
349
|
}
|
|
194
|
-
return
|
|
350
|
+
return {
|
|
351
|
+
ageMs: Math.max(0, Date.now() - Date.parse(entry.recordedAt)),
|
|
352
|
+
entry,
|
|
353
|
+
savedMs: entry.durationMs,
|
|
354
|
+
...(entry.retention?.key === options.retentionKey && { replay: entry.retention.replay }),
|
|
355
|
+
};
|
|
195
356
|
}
|
|
196
357
|
function openGate(options) {
|
|
197
358
|
const { command, config, env, monorepoRoot, passthrough, stderr } = options;
|
|
198
|
-
|
|
199
|
-
config.checkCache?.enabled !== false &&
|
|
200
|
-
resolveCacheableCommands(config.checkCache).has(command);
|
|
201
|
-
if (!covered) {
|
|
359
|
+
if (!isCacheableCommand(config.checkCache, command)) {
|
|
202
360
|
return undefined;
|
|
203
361
|
}
|
|
204
362
|
if (passthrough.includes('--no-cache')) {
|
|
205
363
|
stderr.write(`${formatMisplacedNoCacheWarning(command)}\n`);
|
|
206
364
|
}
|
|
207
365
|
if (passthrough.length > 0) {
|
|
366
|
+
writeDebugNote(`gate disabled: ${command} was passed arguments`, env, stderr);
|
|
208
367
|
return undefined;
|
|
209
368
|
}
|
|
210
369
|
const snapshot = resolveTreeSnapshot({ monorepoRoot, env });
|
|
@@ -216,6 +375,8 @@ function openGate(options) {
|
|
|
216
375
|
}
|
|
217
376
|
function parseArgs(args) {
|
|
218
377
|
const parsed = {
|
|
378
|
+
json: false,
|
|
379
|
+
log: false,
|
|
219
380
|
noCache: false,
|
|
220
381
|
quiet: false,
|
|
221
382
|
recursive: false,
|
|
@@ -264,6 +425,16 @@ function parseArgs(args) {
|
|
|
264
425
|
i++;
|
|
265
426
|
continue;
|
|
266
427
|
}
|
|
428
|
+
if (arg === '--json') {
|
|
429
|
+
parsed.json = true;
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (arg === '--log') {
|
|
434
|
+
parsed.log = true;
|
|
435
|
+
i++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
267
438
|
if (arg === '--no-cache') {
|
|
268
439
|
parsed.noCache = true;
|
|
269
440
|
i++;
|
|
@@ -273,7 +444,65 @@ function parseArgs(args) {
|
|
|
273
444
|
parsed.passthrough = args.slice(i + 1);
|
|
274
445
|
break;
|
|
275
446
|
}
|
|
276
|
-
|
|
447
|
+
const error = findArgError(parsed);
|
|
448
|
+
return error === undefined ? { ok: true, parsed } : { ok: false, error };
|
|
449
|
+
}
|
|
450
|
+
async function reportRecording(options) {
|
|
451
|
+
const { command, config, scope } = options;
|
|
452
|
+
const lookup = await resolveRecording({
|
|
453
|
+
anchorDir: options.anchorDir,
|
|
454
|
+
command,
|
|
455
|
+
current: {
|
|
456
|
+
commandString: options.commandString,
|
|
457
|
+
nmrVersion: VERSION,
|
|
458
|
+
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
459
|
+
treeHash: options.snapshot?.hash,
|
|
460
|
+
},
|
|
461
|
+
isCacheable: isCacheableCommand(config.checkCache, command),
|
|
462
|
+
key: options.key,
|
|
463
|
+
monorepoRoot: options.monorepoRoot,
|
|
464
|
+
});
|
|
465
|
+
if (!lookup.ok) {
|
|
466
|
+
options.stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope })}\n`);
|
|
467
|
+
return { exitCode: options.env[RUN_IF_PRESENT_ENV_VAR] === '1' ? 0 : 1 };
|
|
468
|
+
}
|
|
469
|
+
options.stdout.write(renderRecording({ command, recording: lookup.recording, scope }));
|
|
470
|
+
return { exitCode: 0 };
|
|
471
|
+
}
|
|
472
|
+
function readPresentationEnv(env) {
|
|
473
|
+
const verbosity = readVerbosityEnv(env);
|
|
474
|
+
if (!verbosity.ok) {
|
|
475
|
+
return verbosity;
|
|
476
|
+
}
|
|
477
|
+
const format = readReportFormatEnv(env);
|
|
478
|
+
if (!format.ok) {
|
|
479
|
+
return format;
|
|
480
|
+
}
|
|
481
|
+
return { ok: true, format: format.format, verbosity: verbosity.verbosity };
|
|
482
|
+
}
|
|
483
|
+
function reportNmrCrossing(options) {
|
|
484
|
+
const crossing = findNmrCrossing(options.resolved.steps);
|
|
485
|
+
if (options.isReading || crossing === undefined) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const warning = formatNmrCrossingWarning({
|
|
489
|
+
crossing,
|
|
490
|
+
monorepoRoot: options.monorepoRoot,
|
|
491
|
+
origin: describeOrigin(options.resolved.origin, options.config, options.useRoot),
|
|
492
|
+
registry: options.registry,
|
|
493
|
+
workspaceRoot: options.workspaceRoot,
|
|
494
|
+
});
|
|
495
|
+
options.stderr.write(`${warning}\n`);
|
|
496
|
+
}
|
|
497
|
+
function reportVerdict(verdict, stdout, format) {
|
|
498
|
+
if (isHookName(verdict.command)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
writeVerdict(verdict, stdout, format);
|
|
502
|
+
}
|
|
503
|
+
function resolveReportingVerbosity(options) {
|
|
504
|
+
const { format, ...ladder } = options;
|
|
505
|
+
return format === 'json' ? 'quiet' : resolveVerbosity(ladder);
|
|
277
506
|
}
|
|
278
507
|
async function recordPass(options) {
|
|
279
508
|
const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
|
|
@@ -297,11 +526,22 @@ async function recordPass(options) {
|
|
|
297
526
|
writeDebugNote(`not recording ${command}: ${changed}'s build output changed while it ran`, env, stderr);
|
|
298
527
|
return;
|
|
299
528
|
}
|
|
529
|
+
const retention = await composeRetention({
|
|
530
|
+
anchorDir,
|
|
531
|
+
command,
|
|
532
|
+
key: options.retentionKey,
|
|
533
|
+
monorepoRoot,
|
|
534
|
+
retained: options.retained,
|
|
535
|
+
runId: options.runId,
|
|
536
|
+
steps: options.ownSteps,
|
|
537
|
+
treeHash: snapshot.hash,
|
|
538
|
+
});
|
|
539
|
+
const ref = { anchorDir, command, monorepoRoot };
|
|
540
|
+
const transcript = options.retained === undefined ? undefined : composeTranscript(options.retained);
|
|
300
541
|
try {
|
|
542
|
+
await recordTranscript(ref, transcript);
|
|
301
543
|
await writeCheckCacheEntry({
|
|
302
|
-
|
|
303
|
-
command,
|
|
304
|
-
monorepoRoot,
|
|
544
|
+
...ref,
|
|
305
545
|
entry: {
|
|
306
546
|
key: options.key,
|
|
307
547
|
treeHash: snapshot.hash,
|
|
@@ -312,12 +552,17 @@ async function recordPass(options) {
|
|
|
312
552
|
durationMs: options.durationMs,
|
|
313
553
|
recordedAt: new Date().toISOString(),
|
|
314
554
|
buildDigests: output.digests,
|
|
555
|
+
...(retention !== undefined && { retention }),
|
|
315
556
|
},
|
|
316
557
|
});
|
|
317
558
|
}
|
|
318
559
|
catch (error) {
|
|
319
|
-
|
|
320
|
-
|
|
560
|
+
try {
|
|
561
|
+
await recordTranscript(ref, undefined);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
}
|
|
565
|
+
writeDebugNote(`could not record ${command}: ${describeError(error)}`, env, stderr);
|
|
321
566
|
}
|
|
322
567
|
}
|
|
323
568
|
function resolveCacheKey(options) {
|
|
@@ -346,31 +591,44 @@ function resolveCacheKey(options) {
|
|
|
346
591
|
}
|
|
347
592
|
async function runGated(options) {
|
|
348
593
|
const { anchorDir, command, commandString, env, key, monorepoRoot, snapshot, stderr, stdout } = options;
|
|
594
|
+
const quiet = options.runOptions.quiet === true;
|
|
349
595
|
const gate = key !== undefined && snapshot !== undefined
|
|
350
|
-
? {
|
|
596
|
+
? {
|
|
597
|
+
buildOutputBefore: await readBuildOutputState(monorepoRoot, options.config),
|
|
598
|
+
key,
|
|
599
|
+
retentionKey: computeRetentionKey({
|
|
600
|
+
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
601
|
+
env,
|
|
602
|
+
passKey: key,
|
|
603
|
+
}),
|
|
604
|
+
snapshot,
|
|
605
|
+
}
|
|
351
606
|
: undefined;
|
|
352
607
|
if (gate !== undefined && !options.noCache) {
|
|
353
|
-
const
|
|
608
|
+
const recalled = await lookUpRecordedPass({
|
|
354
609
|
anchorDir,
|
|
355
610
|
buildOutput: gate.buildOutputBefore,
|
|
356
611
|
command,
|
|
357
612
|
env,
|
|
358
613
|
key: gate.key,
|
|
359
614
|
monorepoRoot,
|
|
615
|
+
retentionKey: gate.retentionKey,
|
|
360
616
|
stderr,
|
|
361
617
|
});
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
618
|
+
if (recalled !== undefined) {
|
|
619
|
+
const { entry, ...recall } = recalled;
|
|
620
|
+
if (recall.replay !== undefined) {
|
|
621
|
+
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: options.runId, stderr });
|
|
365
622
|
}
|
|
366
|
-
return 0;
|
|
623
|
+
return { exitCode: 0, outcome: { outcome: 'recalled', ...recall } };
|
|
367
624
|
}
|
|
368
625
|
}
|
|
369
626
|
if (options.overrideNotice !== undefined) {
|
|
370
627
|
stdout.write(options.overrideNotice);
|
|
371
628
|
}
|
|
372
629
|
const startedAt = Date.now();
|
|
373
|
-
const { exitCode } = await runSteps(options.steps, anchorDir, options.runOptions);
|
|
630
|
+
const { exitCode, retained } = await runSteps(options.steps, anchorDir, options.runOptions);
|
|
631
|
+
const durationMs = Date.now() - startedAt;
|
|
374
632
|
if (exitCode === 0 && gate !== undefined) {
|
|
375
633
|
await recordPass({
|
|
376
634
|
anchorDir,
|
|
@@ -378,15 +636,22 @@ async function runGated(options) {
|
|
|
378
636
|
command,
|
|
379
637
|
commandString,
|
|
380
638
|
config: options.config,
|
|
381
|
-
durationMs
|
|
639
|
+
durationMs,
|
|
382
640
|
env,
|
|
383
641
|
key: gate.key,
|
|
384
642
|
monorepoRoot,
|
|
643
|
+
ownSteps: options.ownSteps,
|
|
644
|
+
retained,
|
|
645
|
+
retentionKey: gate.retentionKey,
|
|
646
|
+
runId: options.runId,
|
|
385
647
|
snapshot: gate.snapshot,
|
|
386
648
|
stderr,
|
|
387
649
|
});
|
|
388
650
|
}
|
|
389
|
-
return
|
|
651
|
+
return {
|
|
652
|
+
exitCode,
|
|
653
|
+
outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
|
|
654
|
+
};
|
|
390
655
|
}
|
|
391
656
|
function shellQuote(arg) {
|
|
392
657
|
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
package/dist/esm/runner.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface RunCommandOptions {
|
|
|
13
13
|
env?: NodeJS.ProcessEnv;
|
|
14
14
|
}
|
|
15
15
|
export type RunStepsOptions = Omit<RunCommandOptions, 'channels'>;
|
|
16
|
+
export interface RetainedOutput {
|
|
17
|
+
stderr: Buffer;
|
|
18
|
+
stdout: Buffer;
|
|
19
|
+
}
|
|
20
|
+
export interface RunStepsResult {
|
|
21
|
+
exitCode: number;
|
|
22
|
+
retained?: RetainedOutput;
|
|
23
|
+
}
|
|
16
24
|
type CommandCompletion = {
|
|
17
25
|
outcome: 'exited';
|
|
18
26
|
exitCode: number;
|
|
@@ -31,8 +39,6 @@ export type RunCommandResult = CommandCompletion & {
|
|
|
31
39
|
};
|
|
32
40
|
export declare function resolveChannel(stream: Writable, quiet: boolean): OutputChannel;
|
|
33
41
|
export declare function resolveInheritedChannel(stream: Writable): OutputChannel;
|
|
34
|
-
export declare function runSteps(steps: readonly Step[], cwd: string | undefined, options: RunStepsOptions): Promise<
|
|
35
|
-
exitCode: number;
|
|
36
|
-
}>;
|
|
42
|
+
export declare function runSteps(steps: readonly Step[], cwd: string | undefined, options: RunStepsOptions): Promise<RunStepsResult>;
|
|
37
43
|
export declare function runCommand(command: string, cwd: string | undefined, options: RunCommandOptions): Promise<RunCommandResult>;
|
|
38
44
|
export {};
|
package/dist/esm/runner.js
CHANGED
|
@@ -20,13 +20,23 @@ export async function runSteps(steps, cwd, options) {
|
|
|
20
20
|
stdout: options.stdout ?? process.stdout,
|
|
21
21
|
stderr: options.stderr ?? process.stderr,
|
|
22
22
|
};
|
|
23
|
+
const parts = [];
|
|
24
|
+
let isCaptureWhole = true;
|
|
23
25
|
for (const step of steps) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
26
|
+
const result = await runStep(step, cwd, resolved);
|
|
27
|
+
if (step.kind === 'opaque') {
|
|
28
|
+
if (result.stderr === undefined || result.stdout === undefined) {
|
|
29
|
+
isCaptureWhole = false;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
parts.push({ stderr: result.stderr, stdout: result.stdout });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
return { exitCode: result.exitCode, ...composeRetainedOutput(parts, isCaptureWhole) };
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
|
-
return { exitCode: 0 };
|
|
39
|
+
return { exitCode: 0, ...composeRetainedOutput(parts, isCaptureWhole) };
|
|
30
40
|
}
|
|
31
41
|
export async function runCommand(command, cwd, options) {
|
|
32
42
|
return runSpawned({ args: [], file: command, useShell: true }, cwd, options);
|
|
@@ -80,6 +90,17 @@ function runStep(step, cwd, options) {
|
|
|
80
90
|
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
81
91
|
});
|
|
82
92
|
}
|
|
93
|
+
function composeRetainedOutput(parts, isCaptureWhole) {
|
|
94
|
+
if (!isCaptureWhole || parts.length === 0) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
retained: {
|
|
99
|
+
stderr: Buffer.concat(parts.map((part) => part.stderr)),
|
|
100
|
+
stdout: Buffer.concat(parts.map((part) => part.stdout)),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
83
104
|
function captureStream(source, destination) {
|
|
84
105
|
const retained = createBoundedBuffer();
|
|
85
106
|
let isComplete = true;
|
package/dist/esm/scripts.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { ScriptRegistry, ScriptValue } from './default-scripts.js';
|
|
1
|
+
export type { ScriptRegistry, ScriptValue, StepSpec } from './default-scripts.js';
|
|
2
2
|
export { getDefaultRootScripts, getDefaultWorkspaceScripts } from './resolve-scripts.js';
|