@williamthorsen/nmr 0.44.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 +80 -0
- package/.readyup/kits/default.js +419 -129
- package/.readyup/manifest.json +13 -17
- package/CHANGELOG.md +24 -0
- package/README.md +10 -17
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/commands/fmt.d.ts +4 -2
- package/dist/esm/commands/fmt.js +11 -3
- package/dist/esm/config.js +46 -3
- 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/helpers/escapeControlCharacters.d.ts +10 -0
- package/dist/esm/helpers/escapeControlCharacters.js +12 -0
- package/dist/esm/resolver.js +5 -2
- package/dist/esm/runCli.js +141 -98
- package/dist/esm/steps.d.ts +38 -13
- package/dist/esm/steps.js +77 -10
- package/dist/esm/tiers.d.ts +5 -4
- package/dist/esm/vitest-git-isolation.js +3 -1
- package/docs/reporting.md +2 -0
- package/docs/scripts.md +10 -2
- package/docs/utilities.md +2 -2
- package/docs/vitest.md +12 -8
- package/package.json +2 -2
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
84
|
stdout.write(`${generateHelp(context.config, context.monorepoRoot, anchorDir, shouldUseRoot)}\n`);
|
|
85
|
-
return { exitCode: 0 };
|
|
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,12 @@ 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 {
|
|
138
|
+
return {
|
|
139
|
+
settled: true,
|
|
140
|
+
result: { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) },
|
|
141
|
+
};
|
|
120
142
|
}
|
|
121
|
-
const registry = shouldUseRoot
|
|
143
|
+
const registry = invocation.shouldUseRoot
|
|
122
144
|
? buildRootRegistry(context.config, context.monorepoRoot)
|
|
123
145
|
: buildWorkspaceRegistry(context.config);
|
|
124
146
|
assertNoSelfReference({
|
|
@@ -132,24 +154,50 @@ async function runInvocation(options) {
|
|
|
132
154
|
const resolvedScript = resolveScript(command, registry, anchorDir, parsedArgs.isWorkspaceRoot);
|
|
133
155
|
if (!resolvedScript) {
|
|
134
156
|
if (env[RUN_IF_PRESENT_ENV_VAR] === '1') {
|
|
135
|
-
return { exitCode: 0 };
|
|
157
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
136
158
|
}
|
|
137
159
|
reportError(`Unknown command: ${command}`, stderr);
|
|
138
|
-
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;
|
|
139
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;
|
|
140
188
|
const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
|
|
141
189
|
const resolvedCommand = renderChain(runnableSteps);
|
|
142
190
|
const noOpReason = findNoOpReason(resolvedCommand, isEmptiedByWorkspace);
|
|
143
191
|
if (noOpReason !== undefined && !parsedArgs.shouldPrintLog) {
|
|
144
|
-
reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format, styles.stdout);
|
|
145
|
-
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 } };
|
|
146
194
|
}
|
|
147
195
|
const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
|
|
148
196
|
const substitutedCommand = renderChain(substitutedSteps);
|
|
149
197
|
const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
|
|
150
198
|
if (!bindResult.ok) {
|
|
151
199
|
reportError(bindResult.error, stderr);
|
|
152
|
-
return { exitCode: 1 };
|
|
200
|
+
return { settled: true, result: { exitCode: 1 } };
|
|
153
201
|
}
|
|
154
202
|
const mainSteps = bindResult.steps;
|
|
155
203
|
const isHookInvocation = isHookName(command);
|
|
@@ -164,7 +212,7 @@ async function runInvocation(options) {
|
|
|
164
212
|
monorepoRoot: context.monorepoRoot,
|
|
165
213
|
registry,
|
|
166
214
|
resolvedScript,
|
|
167
|
-
shouldUseRoot,
|
|
215
|
+
shouldUseRoot: invocation.shouldUseRoot,
|
|
168
216
|
stderr,
|
|
169
217
|
style: styles.stderr,
|
|
170
218
|
});
|
|
@@ -178,40 +226,14 @@ async function runInvocation(options) {
|
|
|
178
226
|
stderr,
|
|
179
227
|
substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
|
|
180
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;
|
|
181
233
|
if (parsedArgs.shouldPrintLog) {
|
|
182
|
-
return reportRecording(
|
|
183
|
-
anchorDir,
|
|
184
|
-
command,
|
|
185
|
-
commandString: fullCommand,
|
|
186
|
-
config: context.config,
|
|
187
|
-
env,
|
|
188
|
-
key,
|
|
189
|
-
monorepoRoot: context.monorepoRoot,
|
|
190
|
-
scope,
|
|
191
|
-
snapshot,
|
|
192
|
-
stderr,
|
|
193
|
-
stdout,
|
|
194
|
-
styles,
|
|
195
|
-
});
|
|
234
|
+
return reportRecording(plan);
|
|
196
235
|
}
|
|
197
|
-
const { exitCode, outcome } = await runGated(
|
|
198
|
-
anchorDir,
|
|
199
|
-
command,
|
|
200
|
-
commandString: fullCommand,
|
|
201
|
-
config: context.config,
|
|
202
|
-
steps: fullSteps,
|
|
203
|
-
env,
|
|
204
|
-
key,
|
|
205
|
-
monorepoRoot: context.monorepoRoot,
|
|
206
|
-
overrideNotice: formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, styles.stdout),
|
|
207
|
-
ownSteps: mainSteps,
|
|
208
|
-
runId,
|
|
209
|
-
runOptions,
|
|
210
|
-
shouldBypassCache,
|
|
211
|
-
snapshot,
|
|
212
|
-
stderr,
|
|
213
|
-
stdout,
|
|
214
|
-
});
|
|
236
|
+
const { exitCode, outcome } = await runGated(plan);
|
|
215
237
|
reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
|
|
216
238
|
return { exitCode };
|
|
217
239
|
}
|
|
@@ -325,7 +347,8 @@ function describeCrossingRemedy(options) {
|
|
|
325
347
|
};
|
|
326
348
|
case 'package':
|
|
327
349
|
return {
|
|
328
|
-
remedy:
|
|
350
|
+
remedy: formatInlineRemedy({ crossingStep, file: origin.file, isWorkspaceRoot, key: origin.key, registry }) ??
|
|
351
|
+
formatPackageRemedy({ configSite, crossingStep, isWorkspaceRoot, key: origin.key, registry }),
|
|
329
352
|
subject: `${path.relative(monorepoRoot, origin.file)}: \`scripts.${origin.key}\``,
|
|
330
353
|
};
|
|
331
354
|
default: {
|
|
@@ -350,12 +373,29 @@ function describeOrigin(origin, config, shouldUseRoot) {
|
|
|
350
373
|
? { tier: 'config', field, key: origin.key }
|
|
351
374
|
: { tier: 'default', key: origin.key };
|
|
352
375
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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;
|
|
357
386
|
}
|
|
358
|
-
|
|
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.`;
|
|
359
399
|
}
|
|
360
400
|
function formatNmrCrossingWarning(options) {
|
|
361
401
|
const { remedy, subject } = describeCrossingRemedy(options);
|
|
@@ -635,26 +675,27 @@ function parseArgs(args) {
|
|
|
635
675
|
const error = findArgError(parsedArgs);
|
|
636
676
|
return error === undefined ? { ok: true, parsedArgs } : { ok: false, error };
|
|
637
677
|
}
|
|
638
|
-
async function reportRecording(
|
|
639
|
-
const { command,
|
|
678
|
+
async function reportRecording(plan) {
|
|
679
|
+
const { anchorDir, command, context, env, scope, stderr, stdout, styles } = plan.invocation;
|
|
680
|
+
const { config, monorepoRoot } = context;
|
|
640
681
|
const lookup = await resolveRecording({
|
|
641
|
-
anchorDir
|
|
682
|
+
anchorDir,
|
|
642
683
|
command,
|
|
643
684
|
currentIdentity: {
|
|
644
|
-
commandString:
|
|
685
|
+
commandString: plan.commandString,
|
|
645
686
|
nmrVersion: VERSION,
|
|
646
687
|
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
647
|
-
treeHash:
|
|
688
|
+
treeHash: plan.snapshot?.hash,
|
|
648
689
|
},
|
|
649
690
|
isCacheable: isCacheableCommand(config.checkCache, command),
|
|
650
|
-
key:
|
|
651
|
-
monorepoRoot
|
|
691
|
+
key: plan.key,
|
|
692
|
+
monorepoRoot,
|
|
652
693
|
});
|
|
653
694
|
if (!lookup.ok) {
|
|
654
|
-
|
|
655
|
-
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 };
|
|
656
697
|
}
|
|
657
|
-
|
|
698
|
+
stdout.write(renderRecording({ command, recording: lookup.recording, scope, style: styles.stdout }));
|
|
658
699
|
return { exitCode: 0 };
|
|
659
700
|
}
|
|
660
701
|
function readPresentation(options) {
|
|
@@ -814,12 +855,14 @@ async function runDelegation(options) {
|
|
|
814
855
|
const step = shouldWithholdInput ? { ...delegation.step, shouldWithholdInput } : delegation.step;
|
|
815
856
|
return runSteps([step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
816
857
|
}
|
|
817
|
-
async function runGated(
|
|
818
|
-
const {
|
|
819
|
-
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;
|
|
820
863
|
const gate = key !== undefined && snapshot !== undefined
|
|
821
864
|
? {
|
|
822
|
-
buildOutputBefore: await readBuildOutputState(monorepoRoot,
|
|
865
|
+
buildOutputBefore: await readBuildOutputState(monorepoRoot, config),
|
|
823
866
|
key,
|
|
824
867
|
retentionKey: computeRetentionKey({
|
|
825
868
|
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
@@ -829,7 +872,7 @@ async function runGated(options) {
|
|
|
829
872
|
snapshot,
|
|
830
873
|
}
|
|
831
874
|
: undefined;
|
|
832
|
-
if (gate !== undefined && !
|
|
875
|
+
if (gate !== undefined && !plan.shouldBypassCache) {
|
|
833
876
|
const recalledPass = await lookUpRecordedPass({
|
|
834
877
|
anchorDir,
|
|
835
878
|
buildOutput: gate.buildOutputBefore,
|
|
@@ -843,16 +886,16 @@ async function runGated(options) {
|
|
|
843
886
|
if (recalledPass !== undefined) {
|
|
844
887
|
const { entry, ...recall } = recalledPass;
|
|
845
888
|
if (recall.replay !== undefined) {
|
|
846
|
-
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId:
|
|
889
|
+
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: plan.runId, stderr });
|
|
847
890
|
}
|
|
848
891
|
return { exitCode: 0, outcome: { outcome: 'recalled', ...recall } };
|
|
849
892
|
}
|
|
850
893
|
}
|
|
851
|
-
if (
|
|
852
|
-
stdout.write(
|
|
894
|
+
if (plan.overrideNotice !== undefined) {
|
|
895
|
+
stdout.write(plan.overrideNotice);
|
|
853
896
|
}
|
|
854
897
|
const startedAtMs = Date.now();
|
|
855
|
-
const { exitCode, retainedOutput } = await runSteps(
|
|
898
|
+
const { exitCode, retainedOutput } = await runSteps(plan.steps, anchorDir, plan.runOptions);
|
|
856
899
|
const durationMs = Date.now() - startedAtMs;
|
|
857
900
|
if (exitCode === 0 && gate !== undefined) {
|
|
858
901
|
await recordPass({
|
|
@@ -860,15 +903,15 @@ async function runGated(options) {
|
|
|
860
903
|
buildOutputBefore: gate.buildOutputBefore,
|
|
861
904
|
command,
|
|
862
905
|
commandString,
|
|
863
|
-
config
|
|
906
|
+
config,
|
|
864
907
|
durationMs,
|
|
865
908
|
env,
|
|
866
909
|
key: gate.key,
|
|
867
910
|
monorepoRoot,
|
|
868
|
-
ownSteps:
|
|
911
|
+
ownSteps: plan.ownSteps,
|
|
869
912
|
retainedOutput,
|
|
870
913
|
retentionKey: gate.retentionKey,
|
|
871
|
-
runId:
|
|
914
|
+
runId: plan.runId,
|
|
872
915
|
snapshot: gate.snapshot,
|
|
873
916
|
stderr,
|
|
874
917
|
});
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -41,10 +41,18 @@ export interface NmrStepTarget {
|
|
|
41
41
|
isWorkspaceRoot: boolean;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* How
|
|
45
|
-
*
|
|
44
|
+
* How an entry names the command under which it is declared: as its whole value, or alongside other steps.
|
|
45
|
+
* `sole` covers an entry with trailing arguments, which do not declare a step of their own.
|
|
46
46
|
*/
|
|
47
47
|
export type SelfReference = 'chained' | 'sole';
|
|
48
|
+
/**
|
|
49
|
+
* Where an entry resolves, which decides the scopes that `-w`, `-R`, and `-F` send its command to:
|
|
50
|
+
* - `root`: the root's `package.json` or the config's `rootScripts`, resolved at the monorepo root.
|
|
51
|
+
* - `package`: a non-root package's own `package.json`, resolved in that package alone.
|
|
52
|
+
* - `workspaceRegistry`: the config's `workspaceScripts`, resolved in every package that does not override the
|
|
53
|
+
* command.
|
|
54
|
+
*/
|
|
55
|
+
export type SelfReferenceScope = 'package' | 'root' | 'workspaceRegistry';
|
|
48
56
|
/**
|
|
49
57
|
* Composes the structural step that re-invokes nmr for one composite element.
|
|
50
58
|
*
|
|
@@ -94,24 +102,41 @@ export declare function quoteToken(token: string): string;
|
|
|
94
102
|
*/
|
|
95
103
|
export declare function readNmrStep(step: Step): NmrStepTarget | undefined;
|
|
96
104
|
/**
|
|
97
|
-
* Reports whether a
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* Honouring such an entry would spawn a shell running the same command in the same directory, which reads the
|
|
101
|
-
* same entry again without bound, so resolution discards it however it reads. `chained` separates an entry that
|
|
102
|
-
* thereby loses steps from one that declares nothing to lose.
|
|
105
|
+
* Reports whether a string entry re-invokes the command under which it is declared, and whether it declares
|
|
106
|
+
* anything besides. Reports nothing for an entry that names another command, or none.
|
|
103
107
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
108
|
+
* Honouring such an entry would spawn a shell running the same command in a scope that reads the same entry
|
|
109
|
+
* again, without bound. `chained` separates an entry that thereby loses steps from one that declares nothing to
|
|
110
|
+
* lose.
|
|
106
111
|
*
|
|
107
|
-
* Partial in the same ways `findNmrCrossing` is, and for the same reason: What goes unrecognized re-enters
|
|
108
|
-
*
|
|
112
|
+
* Partial in the same ways `findNmrCrossing` is, and for the same reason: What goes unrecognized re-enters once,
|
|
113
|
+
* and the run fails when `enterChain` finds the command repeated in the same directory.
|
|
109
114
|
*/
|
|
110
115
|
export declare function readSelfReference(options: {
|
|
111
|
-
anchoredAtRoot: boolean;
|
|
112
116
|
commandName: string;
|
|
117
|
+
scope: SelfReferenceScope;
|
|
113
118
|
script: string;
|
|
114
119
|
}): SelfReference | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Returns the command that a string script names when its whole value is `nmr <command>`, or `undefined` for any
|
|
122
|
+
* other value.
|
|
123
|
+
*
|
|
124
|
+
* Exact by design: A flag, an argument, an environment assignment, or a launcher each change what the invocation
|
|
125
|
+
* runs, or where, so a value that contains one is not interchangeable with the script that it names.
|
|
126
|
+
*/
|
|
127
|
+
export declare function readSoleNmrCommand(script: string): string | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Reports whether a step list names the command under which it is declared, and whether it declares anything
|
|
130
|
+
* besides. Takes each element's instruction: the bare string, or a spec's `run`.
|
|
131
|
+
*
|
|
132
|
+
* Reads an element as the instruction that nmr composes into `nmr <element>`, and applies the declaring scope
|
|
133
|
+
* directly rather than through the `-w` that composition adds at the root.
|
|
134
|
+
*/
|
|
135
|
+
export declare function readStepListSelfReference(options: {
|
|
136
|
+
commandName: string;
|
|
137
|
+
elements: readonly string[];
|
|
138
|
+
scope: SelfReferenceScope;
|
|
139
|
+
}): SelfReference | undefined;
|
|
115
140
|
/**
|
|
116
141
|
* Renders a step list as the `&&` chain that a shell runs.
|
|
117
142
|
*
|
package/dist/esm/steps.js
CHANGED
|
@@ -49,18 +49,50 @@ export function readNmrStep(step) {
|
|
|
49
49
|
return file === 'nmr' ? readNmrTarget(rest) : undefined;
|
|
50
50
|
}
|
|
51
51
|
export function readSelfReference(options) {
|
|
52
|
-
const {
|
|
52
|
+
const { commandName, scope, script } = options;
|
|
53
53
|
const segments = splitSegments(script).filter((segment) => segment.trim() !== '');
|
|
54
54
|
const doesReenter = segments.some((segment) => {
|
|
55
|
-
const
|
|
56
|
-
const target =
|
|
57
|
-
|
|
55
|
+
const invocation = readNmrTail(segment);
|
|
56
|
+
const target = invocation === undefined ? undefined : readNmrTarget(invocation.tail);
|
|
57
|
+
if (invocation === undefined || target === undefined) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const launcherScope = readLauncherScope(invocation.launcher, invocation.launcherArgs);
|
|
61
|
+
const effectiveTarget = {
|
|
62
|
+
command: target.command,
|
|
63
|
+
isDelegate: target.isDelegate || launcherScope.isDelegate,
|
|
64
|
+
isRecursive: target.isRecursive || launcherScope.isRecursive,
|
|
65
|
+
isWorkspaceRoot: target.isWorkspaceRoot || launcherScope.isWorkspaceRoot,
|
|
66
|
+
};
|
|
67
|
+
return isReentry(effectiveTarget, commandName, scope);
|
|
58
68
|
});
|
|
59
69
|
if (!doesReenter) {
|
|
60
70
|
return undefined;
|
|
61
71
|
}
|
|
62
72
|
return segments.length === 1 ? 'sole' : 'chained';
|
|
63
73
|
}
|
|
74
|
+
export function readSoleNmrCommand(script) {
|
|
75
|
+
const segments = splitSegments(script).filter((segment) => segment.trim() !== '');
|
|
76
|
+
if (segments.length !== 1) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const [program, command, ...rest] = tokenizeSegment(segments[0] ?? '');
|
|
80
|
+
if (program !== 'nmr' || command === undefined || rest.length > 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return SHELL_SAFE_TOKEN.test(command) && !command.startsWith('-') ? command : undefined;
|
|
84
|
+
}
|
|
85
|
+
export function readStepListSelfReference(options) {
|
|
86
|
+
const { commandName, elements, scope } = options;
|
|
87
|
+
const doesReenter = elements.some((element) => {
|
|
88
|
+
const target = readNmrTarget(tokenize(element));
|
|
89
|
+
return target !== undefined && isReentry(target, commandName, scope);
|
|
90
|
+
});
|
|
91
|
+
if (!doesReenter) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return elements.length === 1 ? 'sole' : 'chained';
|
|
95
|
+
}
|
|
64
96
|
export function renderChain(steps) {
|
|
65
97
|
return steps.map(renderStep).join(' && ');
|
|
66
98
|
}
|
|
@@ -74,24 +106,59 @@ function isRedirectionOperator(char, precedingText, nextChar) {
|
|
|
74
106
|
}
|
|
75
107
|
return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
|
|
76
108
|
}
|
|
109
|
+
function isReentry(target, commandName, scope) {
|
|
110
|
+
if (target.command !== commandName) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
switch (scope) {
|
|
114
|
+
case 'root':
|
|
115
|
+
return !target.isDelegate;
|
|
116
|
+
case 'package':
|
|
117
|
+
return target.isRecursive || (!target.isDelegate && !target.isWorkspaceRoot);
|
|
118
|
+
case 'workspaceRegistry':
|
|
119
|
+
return target.isDelegate || !target.isWorkspaceRoot;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function readLauncherScope(launcher, launcherArgs) {
|
|
123
|
+
const scope = { isDelegate: false, isRecursive: false, isWorkspaceRoot: false };
|
|
124
|
+
if (launcher !== 'pnpm') {
|
|
125
|
+
return scope;
|
|
126
|
+
}
|
|
127
|
+
for (const token of launcherArgs) {
|
|
128
|
+
if (token === '-r' || token === '--recursive') {
|
|
129
|
+
scope.isDelegate = true;
|
|
130
|
+
scope.isRecursive = true;
|
|
131
|
+
}
|
|
132
|
+
else if (token === '-F' || token === '--filter' || token.startsWith('--filter=')) {
|
|
133
|
+
scope.isDelegate = true;
|
|
134
|
+
}
|
|
135
|
+
else if (token === '-w' || token === '--workspace-root') {
|
|
136
|
+
scope.isWorkspaceRoot = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return scope;
|
|
140
|
+
}
|
|
77
141
|
function readNmrTail(segment) {
|
|
78
142
|
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
79
143
|
if (head === undefined) {
|
|
80
144
|
return undefined;
|
|
81
145
|
}
|
|
82
146
|
if (head === 'nmr') {
|
|
83
|
-
return rest;
|
|
147
|
+
return { launcher: undefined, launcherArgs: [], tail: rest };
|
|
84
148
|
}
|
|
85
149
|
const subcommands = LAUNCHERS.get(head);
|
|
86
150
|
if (subcommands === undefined) {
|
|
87
151
|
return undefined;
|
|
88
152
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
153
|
+
const nmrIndex = subcommands.size === 0
|
|
154
|
+
? rest.findIndex((token) => !token.startsWith('-'))
|
|
155
|
+
: rest.findIndex((token) => subcommands.has(token)) + 1;
|
|
156
|
+
if (nmrIndex <= 0 && subcommands.size > 0) {
|
|
157
|
+
return undefined;
|
|
92
158
|
}
|
|
93
|
-
|
|
94
|
-
|
|
159
|
+
return rest[nmrIndex] === 'nmr'
|
|
160
|
+
? { launcher: head, launcherArgs: rest.slice(0, nmrIndex), tail: rest.slice(nmrIndex + 1) }
|
|
161
|
+
: undefined;
|
|
95
162
|
}
|
|
96
163
|
function readNmrTarget(tokens) {
|
|
97
164
|
let isDelegate = false;
|
package/dist/esm/tiers.d.ts
CHANGED
|
@@ -56,10 +56,11 @@ export interface TestFileScanOptions {
|
|
|
56
56
|
excludedBasenames?: readonly string[];
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* The isolation ladder, ordered by the furthest thing that a test reaches
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* The isolation ladder, ordered by the furthest thing that a test reaches, which decides what the environment must
|
|
60
|
+
* supply for the test to run. A tier names what a test reaches, never how it invokes it: A test driving a compiler
|
|
61
|
+
* through its JavaScript API is `tool`, exactly as one spawning `tsc` would be, and a spawned process is itself
|
|
62
|
+
* something reached. Each named tier's name is also its filename infix, so `parse.tool.test.ts` is collected by the
|
|
63
|
+
* `tool` project.
|
|
63
64
|
*/
|
|
64
65
|
export declare const TIER_NAMES: readonly ["unit", "tool", "localhost", "remote"];
|
|
65
66
|
/** One of the four isolation tiers. */
|
|
@@ -3,8 +3,10 @@ process.env['GIT_CONFIG_GLOBAL'] = os.devNull;
|
|
|
3
3
|
process.env['GIT_CONFIG_SYSTEM'] = os.devNull;
|
|
4
4
|
process.env['GIT_CONFIG_NOSYSTEM'] = '1';
|
|
5
5
|
process.env['GIT_ATTR_NOSYSTEM'] = '1';
|
|
6
|
-
process.env['GIT_CONFIG_COUNT'] = '
|
|
6
|
+
process.env['GIT_CONFIG_COUNT'] = '3';
|
|
7
7
|
process.env['GIT_CONFIG_KEY_0'] = 'core.excludesFile';
|
|
8
8
|
process.env['GIT_CONFIG_VALUE_0'] = os.devNull;
|
|
9
9
|
process.env['GIT_CONFIG_KEY_1'] = 'core.attributesFile';
|
|
10
10
|
process.env['GIT_CONFIG_VALUE_1'] = os.devNull;
|
|
11
|
+
process.env['GIT_CONFIG_KEY_2'] = 'maintenance.auto';
|
|
12
|
+
process.env['GIT_CONFIG_VALUE_2'] = 'false';
|
package/docs/reporting.md
CHANGED
|
@@ -65,6 +65,8 @@ Because the format is nmr's own reporting, and nmr does not tell the commands th
|
|
|
65
65
|
|
|
66
66
|
`NMR_REPORT_FORMAT` is nmr's own as well, and sets how nmr renders its own verdicts. Its values are `text` and `json`; nmr reports any other non-empty value and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so that either direction is expressible: Export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. The format has two sources, then, whereas a verbosity has four: It does not have a repo-level default or harness detection, because detection would hand every agent JSON in place of the prose that the [bundled guidance](../README.md#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects contain.
|
|
67
67
|
|
|
68
|
+
`NMR_ENTRY_CHAIN` is nmr's own as well. It lists the commands that the current run has entered and the directories in which it entered them, and nmr passes it to every process that it starts, so that a process re-entering a command already running in its directory fails instead of repeating without end; see [self-referential entries](scripts.md#three-tier-override-system). Do not set it: nmr reports a value that it cannot read and exits 1.
|
|
69
|
+
|
|
68
70
|
### Output style
|
|
69
71
|
|
|
70
72
|
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code 1, whichever source named it. The flag outranks the variable, and both accept `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
|