@williamthorsen/nmr 0.46.2 → 0.48.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 +85 -0
- package/.readyup/kits/default.js +11 -10
- package/.readyup/manifest.json +53 -8
- package/CHANGELOG.md +24 -0
- package/README.md +29 -3
- package/agents/guidance/rulebooks/nmr.md +3 -1
- package/dist/esm/bin-path.js +4 -3
- package/dist/esm/commands/fmt.js +1 -1
- package/dist/esm/help.js +5 -0
- package/dist/esm/helpers/findClosestName.js +4 -3
- package/dist/esm/helpers/quoteForShell.d.ts +5 -0
- package/dist/esm/helpers/quoteForShell.js +3 -0
- package/dist/esm/machine-lock.d.ts +64 -0
- package/dist/esm/machine-lock.js +229 -0
- package/dist/esm/remote-run.d.ts +136 -0
- package/dist/esm/remote-run.js +251 -0
- package/dist/esm/runCli.js +247 -70
- package/dist/esm/verdict.d.ts +2 -0
- package/dist/esm/verdict.js +3 -2
- package/dist/esm/vitest-max-workers.d.ts +11 -0
- package/dist/esm/vitest-max-workers.js +35 -0
- package/dist/esm/vitest-source-resolution.js +2 -3
- package/dist/esm/vitest.js +10 -8
- package/docs/check-cache.md +2 -2
- package/docs/delegate.md +61 -0
- package/docs/machine-lock.md +62 -0
- package/docs/reporting.md +1 -1
- package/docs/vitest.md +17 -3
- package/package.json +4 -4
package/dist/esm/runCli.js
CHANGED
|
@@ -14,9 +14,12 @@ import { readFilterSelection } from "./helpers/filter-selection.js";
|
|
|
14
14
|
import { findClosestName } from "./helpers/findClosestName.js";
|
|
15
15
|
import { isHookName } from "./helpers/hook-name.js";
|
|
16
16
|
import { resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
17
|
+
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
17
18
|
import { composeTranscript } from "./helpers/transcript.js";
|
|
19
|
+
import { acquireMachineLock, formatMisplacedNoLockWarning, HEAVY_COMMANDS, LOCK_TOKEN_ENV_VAR, NO_LOCK_ENV_VAR, } from "./machine-lock.js";
|
|
18
20
|
import { isTerminalStream, OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG, resolveOutputStyles } from "./output-style.js";
|
|
19
21
|
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
22
|
+
import { createSnapshot, DELEGATE_ENV_VAR, DELEGATE_FLAG, DELEGATION_FAILURE_EXIT_CODE, formatMisplacedDelegationWarning, NO_DELEGATE_FLAG, resolveDelegationTarget, runRemotely, } from "./remote-run.js";
|
|
20
23
|
import { assembleReplay } from "./replay-assembly.js";
|
|
21
24
|
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
22
25
|
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, readPackageJsonScripts, resolveScript, } from "./resolver.js";
|
|
@@ -37,6 +40,10 @@ export async function runCli(options) {
|
|
|
37
40
|
return { exitCode: established.result.exitCode };
|
|
38
41
|
}
|
|
39
42
|
const invocation = established.value;
|
|
43
|
+
const delegated = await runOnDelegateHost(invocation);
|
|
44
|
+
if (delegated !== undefined) {
|
|
45
|
+
return delegated;
|
|
46
|
+
}
|
|
40
47
|
const planned = await planRun(invocation);
|
|
41
48
|
const { exitCode, isUnknownCommand = false } = planned.settled ? planned.result : await executePlan(planned.value);
|
|
42
49
|
if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(options.env)) {
|
|
@@ -91,6 +98,7 @@ async function establishInvocation(options) {
|
|
|
91
98
|
anchorDir,
|
|
92
99
|
command: parsedArgs.command,
|
|
93
100
|
context,
|
|
101
|
+
cwd,
|
|
94
102
|
env,
|
|
95
103
|
format,
|
|
96
104
|
parsedArgs,
|
|
@@ -121,6 +129,7 @@ async function planRun(invocation) {
|
|
|
121
129
|
env,
|
|
122
130
|
format: invocation.format,
|
|
123
131
|
shouldBypassCache,
|
|
132
|
+
shouldBypassLock: parsedArgs.shouldBypassLock,
|
|
124
133
|
passthrough: parsedArgs.passthrough,
|
|
125
134
|
runId,
|
|
126
135
|
snapshot,
|
|
@@ -135,7 +144,15 @@ async function planRun(invocation) {
|
|
|
135
144
|
};
|
|
136
145
|
const delegation = composeDelegation({ childEnv, command, parsedArgs });
|
|
137
146
|
if (delegation !== undefined) {
|
|
138
|
-
const
|
|
147
|
+
const lock = await takeMachineLock(invocation);
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
const heldDelegation = { ...delegation, env: addLockEnv(delegation.env, lock) };
|
|
151
|
+
result = await runDelegation({ context, delegation: heldDelegation, parsedArgs, runOptions, stderr });
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await lock?.release();
|
|
155
|
+
}
|
|
139
156
|
return {
|
|
140
157
|
settled: true,
|
|
141
158
|
result: { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) },
|
|
@@ -243,6 +260,77 @@ async function executePlan(plan) {
|
|
|
243
260
|
reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
|
|
244
261
|
return { exitCode };
|
|
245
262
|
}
|
|
263
|
+
async function runOnDelegateHost(invocation) {
|
|
264
|
+
const { command, context, cwd, env, format, parsedArgs, scope, stderr, stdout, styles } = invocation;
|
|
265
|
+
if (parsedArgs.passthrough.some(isDelegationFlag)) {
|
|
266
|
+
stderr.write(`${formatMisplacedDelegationWarning(command, styles.stderr)}
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
const target = resolveDelegationTarget({
|
|
270
|
+
command,
|
|
271
|
+
env,
|
|
272
|
+
flagHost: parsedArgs.delegateHost,
|
|
273
|
+
isOptedOut: parsedArgs.shouldRunLocally,
|
|
274
|
+
isReading: parsedArgs.shouldPrintLog,
|
|
275
|
+
});
|
|
276
|
+
if (target.kind === 'local') {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
if (target.kind === 'refused') {
|
|
280
|
+
reportError(target.error, stderr);
|
|
281
|
+
return { exitCode: 1 };
|
|
282
|
+
}
|
|
283
|
+
const { host } = target;
|
|
284
|
+
const startedAtMs = Date.now();
|
|
285
|
+
const snapshot = createSnapshot(cwd, env);
|
|
286
|
+
if (!snapshot.ok && snapshot.isUnsupported) {
|
|
287
|
+
if (target.source === 'flag') {
|
|
288
|
+
reportError(`Cannot delegate to ${host}: ${snapshot.reason}`, stderr);
|
|
289
|
+
return { exitCode: 1 };
|
|
290
|
+
}
|
|
291
|
+
stderr.write(`nmr: running ${command} here rather than on ${host}: ${snapshot.reason}\n`);
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
const result = snapshot.ok
|
|
295
|
+
? await runRemotely({
|
|
296
|
+
cwd,
|
|
297
|
+
env,
|
|
298
|
+
host,
|
|
299
|
+
monorepoRoot: context.monorepoRoot,
|
|
300
|
+
nmrArgs: composeForwardedArgs(command, parsedArgs),
|
|
301
|
+
remoteEnv: {
|
|
302
|
+
[COMMAND_VERBOSITY_ENV_VAR]: invocation.verbosity,
|
|
303
|
+
[OUTPUT_STYLE_ENV_VAR]: styles.stdout,
|
|
304
|
+
[REPORT_FORMAT_ENV_VAR]: format,
|
|
305
|
+
},
|
|
306
|
+
snapshot,
|
|
307
|
+
stderr,
|
|
308
|
+
stdout,
|
|
309
|
+
})
|
|
310
|
+
: { ok: false, message: 'could not snapshot the working tree', output: snapshot.reason };
|
|
311
|
+
const durationMs = Date.now() - startedAtMs;
|
|
312
|
+
if (!result.ok) {
|
|
313
|
+
if (result.output.trim() !== '') {
|
|
314
|
+
stderr.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
315
|
+
}
|
|
316
|
+
const verdict = {
|
|
317
|
+
command,
|
|
318
|
+
detail: result.message,
|
|
319
|
+
durationMs,
|
|
320
|
+
exitCode: DELEGATION_FAILURE_EXIT_CODE,
|
|
321
|
+
host,
|
|
322
|
+
outcome: 'failed',
|
|
323
|
+
scope,
|
|
324
|
+
};
|
|
325
|
+
reportVerdict(verdict, stdout, format, styles.stdout);
|
|
326
|
+
return { exitCode: DELEGATION_FAILURE_EXIT_CODE };
|
|
327
|
+
}
|
|
328
|
+
const outcome = result.exitCode === 0
|
|
329
|
+
? { outcome: 'passed', durationMs }
|
|
330
|
+
: { outcome: 'failed', durationMs, exitCode: result.exitCode };
|
|
331
|
+
reportVerdict({ command, host, scope, ...outcome }, stdout, format, styles.stdout);
|
|
332
|
+
return { exitCode: result.exitCode };
|
|
333
|
+
}
|
|
246
334
|
const FILTER_FLAGS = new Set(['-F', '--filter']);
|
|
247
335
|
const BOOLEAN_FLAGS = new Map([
|
|
248
336
|
['-?', 'shouldShowHelp'],
|
|
@@ -250,6 +338,8 @@ const BOOLEAN_FLAGS = new Map([
|
|
|
250
338
|
['--json', 'shouldEmitJson'],
|
|
251
339
|
['--log', 'shouldPrintLog'],
|
|
252
340
|
['--no-cache', 'shouldBypassCache'],
|
|
341
|
+
[NO_DELEGATE_FLAG, 'shouldRunLocally'],
|
|
342
|
+
['--no-lock', 'shouldBypassLock'],
|
|
253
343
|
['-q', 'quiet'],
|
|
254
344
|
['--quiet', 'quiet'],
|
|
255
345
|
['-R', 'recursive'],
|
|
@@ -260,18 +350,29 @@ const BOOLEAN_FLAGS = new Map([
|
|
|
260
350
|
['--workspace-root', 'isWorkspaceRoot'],
|
|
261
351
|
]);
|
|
262
352
|
function buildChildEnv(options) {
|
|
263
|
-
const { env, format, passthrough, runId, shouldBypassCache, snapshot, styles, verbosity } = options;
|
|
353
|
+
const { env, format, passthrough, runId, shouldBypassCache, shouldBypassLock, snapshot, styles, verbosity } = options;
|
|
264
354
|
const bypassesCache = shouldBypassCache || passthrough.length > 0;
|
|
355
|
+
const { [DELEGATE_ENV_VAR]: _delegateHost, ...inheritedEnv } = env;
|
|
265
356
|
return {
|
|
266
|
-
...
|
|
357
|
+
...inheritedEnv,
|
|
267
358
|
...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
|
|
268
359
|
...(bypassesCache && { [NO_CACHE_ENV_VAR]: '1' }),
|
|
360
|
+
...(shouldBypassLock && { [NO_LOCK_ENV_VAR]: '1' }),
|
|
269
361
|
[COMMAND_VERBOSITY_ENV_VAR]: verbosity,
|
|
270
362
|
[OUTPUT_STYLE_ENV_VAR]: styles.stdout,
|
|
271
363
|
[REPORT_FORMAT_ENV_VAR]: format,
|
|
272
364
|
[RUN_ID_ENV_VAR]: runId,
|
|
273
365
|
};
|
|
274
366
|
}
|
|
367
|
+
function addLockEnv(env, lock) {
|
|
368
|
+
if (lock?.token !== undefined) {
|
|
369
|
+
return { ...env, [LOCK_TOKEN_ENV_VAR]: lock.token };
|
|
370
|
+
}
|
|
371
|
+
if (lock?.isChainUnlocked === true) {
|
|
372
|
+
return { ...env, [NO_LOCK_ENV_VAR]: '1' };
|
|
373
|
+
}
|
|
374
|
+
return env;
|
|
375
|
+
}
|
|
275
376
|
function acceptsArguments(step) {
|
|
276
377
|
return step.kind === 'opaque' || step.shouldDeclineArguments !== true;
|
|
277
378
|
}
|
|
@@ -319,6 +420,19 @@ function composeDelegation(options) {
|
|
|
319
420
|
},
|
|
320
421
|
};
|
|
321
422
|
}
|
|
423
|
+
function composeForwardedArgs(command, parsedArgs) {
|
|
424
|
+
return [
|
|
425
|
+
...(parsedArgs.filter === undefined ? [] : ['--filter', parsedArgs.filter]),
|
|
426
|
+
...(parsedArgs.recursive ? ['--recursive'] : []),
|
|
427
|
+
...(parsedArgs.isWorkspaceRoot ? ['--workspace-root'] : []),
|
|
428
|
+
...(parsedArgs.quiet ? ['--quiet'] : []),
|
|
429
|
+
...(parsedArgs.shouldEmitJson ? ['--json'] : []),
|
|
430
|
+
...(parsedArgs.shouldBypassCache ? ['--no-cache'] : []),
|
|
431
|
+
...(parsedArgs.shouldBypassLock ? ['--no-lock'] : []),
|
|
432
|
+
command,
|
|
433
|
+
...parsedArgs.passthrough,
|
|
434
|
+
];
|
|
435
|
+
}
|
|
322
436
|
async function composeRetention(options) {
|
|
323
437
|
const { anchorDir, command, key, retainedOutput, runId } = options;
|
|
324
438
|
if (retainedOutput !== undefined) {
|
|
@@ -638,8 +752,10 @@ function parseArgs(args) {
|
|
|
638
752
|
quiet: false,
|
|
639
753
|
recursive: false,
|
|
640
754
|
shouldBypassCache: false,
|
|
755
|
+
shouldBypassLock: false,
|
|
641
756
|
shouldEmitJson: false,
|
|
642
757
|
shouldPrintLog: false,
|
|
758
|
+
shouldRunLocally: false,
|
|
643
759
|
shouldShowHelp: false,
|
|
644
760
|
shouldShowVersion: false,
|
|
645
761
|
passthrough: [],
|
|
@@ -674,6 +790,15 @@ function parseArgs(args) {
|
|
|
674
790
|
index += styleArgument.consumedCount;
|
|
675
791
|
continue;
|
|
676
792
|
}
|
|
793
|
+
const delegateArgument = readDelegateArgument(args, index);
|
|
794
|
+
if (delegateArgument !== undefined) {
|
|
795
|
+
if (!delegateArgument.ok) {
|
|
796
|
+
return delegateArgument;
|
|
797
|
+
}
|
|
798
|
+
parsedArgs.delegateHost = delegateArgument.value;
|
|
799
|
+
index += delegateArgument.consumedCount;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
677
802
|
parsedArgs.command = arg;
|
|
678
803
|
parsedArgs.passthrough = args.slice(index + 1);
|
|
679
804
|
break;
|
|
@@ -720,18 +845,24 @@ function readPresentation(options) {
|
|
|
720
845
|
}
|
|
721
846
|
return { ok: true, format: format.format, styles, verbosity: verbosity.verbosity };
|
|
722
847
|
}
|
|
723
|
-
function
|
|
848
|
+
function readDelegateArgument(args, index) {
|
|
849
|
+
return readFlagValue(args, index, DELEGATE_FLAG, 'a host argument');
|
|
850
|
+
}
|
|
851
|
+
function readFlagValue(args, index, flag, requirement) {
|
|
724
852
|
const arg = args[index] ?? '';
|
|
725
|
-
const assignment = arg.startsWith(`${
|
|
726
|
-
if (arg !==
|
|
853
|
+
const assignment = arg.startsWith(`${flag}=`) ? arg.slice(flag.length + 1) : undefined;
|
|
854
|
+
if (arg !== flag && assignment === undefined) {
|
|
727
855
|
return undefined;
|
|
728
856
|
}
|
|
729
857
|
const value = assignment ?? args[index + 1];
|
|
730
858
|
if (!value) {
|
|
731
|
-
return { ok: false, error: `${
|
|
859
|
+
return { ok: false, error: `${flag} requires ${requirement}` };
|
|
732
860
|
}
|
|
733
861
|
return { ok: true, value, consumedCount: assignment === undefined ? 2 : 1 };
|
|
734
862
|
}
|
|
863
|
+
function readOutputStyleArgument(args, index) {
|
|
864
|
+
return readFlagValue(args, index, OUTPUT_STYLE_FLAG, 'a value argument: auto, plain, or rich');
|
|
865
|
+
}
|
|
735
866
|
function readRunnableSteps(resolvedScript, workspacePackageDirs) {
|
|
736
867
|
if (workspacePackageDirs.length > 0) {
|
|
737
868
|
return { isEmptiedByWorkspace: false, steps: resolvedScript.steps };
|
|
@@ -764,6 +895,46 @@ function resolveReportingVerbosity(options) {
|
|
|
764
895
|
const { format, ...ladder } = options;
|
|
765
896
|
return format === 'json' ? 'quiet' : resolveVerbosity(ladder);
|
|
766
897
|
}
|
|
898
|
+
async function readGate(plan) {
|
|
899
|
+
const { key, snapshot } = plan;
|
|
900
|
+
const { context, env, stderr, stdout } = plan.invocation;
|
|
901
|
+
if (key === undefined || snapshot === undefined) {
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
const quiet = plan.runOptions.quiet === true;
|
|
905
|
+
return {
|
|
906
|
+
buildOutputBefore: await readBuildOutputState(context.monorepoRoot, context.config),
|
|
907
|
+
key,
|
|
908
|
+
retentionKey: computeRetentionKey({
|
|
909
|
+
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
910
|
+
env,
|
|
911
|
+
passKey: key,
|
|
912
|
+
}),
|
|
913
|
+
snapshot,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async function recallPass(plan, gate) {
|
|
917
|
+
const { anchorDir, command, context, env, stderr } = plan.invocation;
|
|
918
|
+
const { monorepoRoot } = context;
|
|
919
|
+
const recalledPass = await lookUpRecordedPass({
|
|
920
|
+
anchorDir,
|
|
921
|
+
buildOutput: gate.buildOutputBefore,
|
|
922
|
+
command,
|
|
923
|
+
env,
|
|
924
|
+
key: gate.key,
|
|
925
|
+
monorepoRoot,
|
|
926
|
+
retentionKey: gate.retentionKey,
|
|
927
|
+
stderr,
|
|
928
|
+
});
|
|
929
|
+
if (recalledPass === undefined) {
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
const { entry, ...recall } = recalledPass;
|
|
933
|
+
if (recall.replay !== undefined) {
|
|
934
|
+
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: plan.runId, stderr });
|
|
935
|
+
}
|
|
936
|
+
return { exitCode: 0, outcome: { outcome: 'recalled', ...recall } };
|
|
937
|
+
}
|
|
767
938
|
async function recordPass(options) {
|
|
768
939
|
const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
|
|
769
940
|
const currentRead = resolveTreeSnapshot({ monorepoRoot, env: {} });
|
|
@@ -862,70 +1033,61 @@ async function runDelegation(options) {
|
|
|
862
1033
|
return runSteps([step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
863
1034
|
}
|
|
864
1035
|
async function runGated(plan) {
|
|
865
|
-
const { commandString, key
|
|
1036
|
+
const { commandString, key } = plan;
|
|
866
1037
|
const { anchorDir, command, context, env, stderr, stdout } = plan.invocation;
|
|
867
1038
|
const { config, monorepoRoot } = context;
|
|
868
|
-
|
|
869
|
-
const gate = key !== undefined && snapshot !== undefined
|
|
870
|
-
? {
|
|
871
|
-
buildOutputBefore: await readBuildOutputState(monorepoRoot, config),
|
|
872
|
-
key,
|
|
873
|
-
retentionKey: computeRetentionKey({
|
|
874
|
-
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
875
|
-
env,
|
|
876
|
-
passKey: key,
|
|
877
|
-
}),
|
|
878
|
-
snapshot,
|
|
879
|
-
}
|
|
880
|
-
: undefined;
|
|
1039
|
+
let gate = await readGate(plan);
|
|
881
1040
|
if (gate !== undefined && !plan.shouldBypassCache) {
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
command,
|
|
886
|
-
env,
|
|
887
|
-
key: gate.key,
|
|
888
|
-
monorepoRoot,
|
|
889
|
-
retentionKey: gate.retentionKey,
|
|
890
|
-
stderr,
|
|
891
|
-
});
|
|
892
|
-
if (recalledPass !== undefined) {
|
|
893
|
-
const { entry, ...recall } = recalledPass;
|
|
894
|
-
if (recall.replay !== undefined) {
|
|
895
|
-
await certifyRetention({ anchorDir, command, entry, env, monorepoRoot, runId: plan.runId, stderr });
|
|
896
|
-
}
|
|
897
|
-
return { exitCode: 0, outcome: { outcome: 'recalled', ...recall } };
|
|
1041
|
+
const recalled = await recallPass(plan, gate);
|
|
1042
|
+
if (recalled !== undefined) {
|
|
1043
|
+
return recalled;
|
|
898
1044
|
}
|
|
899
1045
|
}
|
|
900
|
-
|
|
901
|
-
|
|
1046
|
+
const lock = await takeMachineLock(plan.invocation);
|
|
1047
|
+
try {
|
|
1048
|
+
if (lock?.waited === true && key !== undefined) {
|
|
1049
|
+
gate = await readGate(plan);
|
|
1050
|
+
if (gate !== undefined && !plan.shouldBypassCache) {
|
|
1051
|
+
const recalled = await recallPass(plan, gate);
|
|
1052
|
+
if (recalled !== undefined) {
|
|
1053
|
+
return recalled;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (plan.overrideNotice !== undefined) {
|
|
1058
|
+
stdout.write(plan.overrideNotice);
|
|
1059
|
+
}
|
|
1060
|
+
const runOptions = { ...plan.runOptions, env: addLockEnv(plan.runOptions.env ?? env, lock) };
|
|
1061
|
+
const startedAtMs = Date.now();
|
|
1062
|
+
const { exitCode, retainedOutput } = await runSteps(plan.steps, anchorDir, runOptions);
|
|
1063
|
+
const durationMs = Date.now() - startedAtMs;
|
|
1064
|
+
if (exitCode === 0 && gate !== undefined) {
|
|
1065
|
+
await recordPass({
|
|
1066
|
+
anchorDir,
|
|
1067
|
+
buildOutputBefore: gate.buildOutputBefore,
|
|
1068
|
+
command,
|
|
1069
|
+
commandString,
|
|
1070
|
+
config,
|
|
1071
|
+
durationMs,
|
|
1072
|
+
env,
|
|
1073
|
+
key: gate.key,
|
|
1074
|
+
monorepoRoot,
|
|
1075
|
+
ownSteps: plan.ownSteps,
|
|
1076
|
+
retainedOutput,
|
|
1077
|
+
retentionKey: gate.retentionKey,
|
|
1078
|
+
runId: plan.runId,
|
|
1079
|
+
snapshot: gate.snapshot,
|
|
1080
|
+
stderr,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
exitCode,
|
|
1085
|
+
outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
|
|
1086
|
+
};
|
|
902
1087
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const durationMs = Date.now() - startedAtMs;
|
|
906
|
-
if (exitCode === 0 && gate !== undefined) {
|
|
907
|
-
await recordPass({
|
|
908
|
-
anchorDir,
|
|
909
|
-
buildOutputBefore: gate.buildOutputBefore,
|
|
910
|
-
command,
|
|
911
|
-
commandString,
|
|
912
|
-
config,
|
|
913
|
-
durationMs,
|
|
914
|
-
env,
|
|
915
|
-
key: gate.key,
|
|
916
|
-
monorepoRoot,
|
|
917
|
-
ownSteps: plan.ownSteps,
|
|
918
|
-
retainedOutput,
|
|
919
|
-
retentionKey: gate.retentionKey,
|
|
920
|
-
runId: plan.runId,
|
|
921
|
-
snapshot: gate.snapshot,
|
|
922
|
-
stderr,
|
|
923
|
-
});
|
|
1088
|
+
finally {
|
|
1089
|
+
await lock?.release();
|
|
924
1090
|
}
|
|
925
|
-
return {
|
|
926
|
-
exitCode,
|
|
927
|
-
outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
|
|
928
|
-
};
|
|
929
1091
|
}
|
|
930
1092
|
function formatMisplacedFlagHint(args, passthrough, style) {
|
|
931
1093
|
const { flagNames, movedTokens, remainingTokens } = splitMisplacedFlags(passthrough);
|
|
@@ -951,10 +1113,28 @@ function isDefinedInAnyScope(command, context) {
|
|
|
951
1113
|
}
|
|
952
1114
|
return [context.monorepoRoot, ...context.workspacePackageDirs].some((dir) => Object.hasOwn(readPackageJsonScripts(dir) ?? {}, command));
|
|
953
1115
|
}
|
|
1116
|
+
function isDelegationFlag(arg) {
|
|
1117
|
+
return arg === DELEGATE_FLAG || arg === NO_DELEGATE_FLAG || arg.startsWith(`${DELEGATE_FLAG}=`);
|
|
1118
|
+
}
|
|
954
1119
|
function isTopLevelInvocation(env) {
|
|
955
1120
|
const runId = env[RUN_ID_ENV_VAR];
|
|
956
1121
|
return runId === undefined || runId === '';
|
|
957
1122
|
}
|
|
1123
|
+
async function takeMachineLock(invocation) {
|
|
1124
|
+
const { anchorDir, command, env, parsedArgs, stderr, styles } = invocation;
|
|
1125
|
+
if (!HEAVY_COMMANDS.has(command) || parsedArgs.shouldPrintLog) {
|
|
1126
|
+
return undefined;
|
|
1127
|
+
}
|
|
1128
|
+
if (parsedArgs.passthrough.includes('--no-lock')) {
|
|
1129
|
+
stderr.write(`${formatMisplacedNoLockWarning(command, styles.stderr)}\n`);
|
|
1130
|
+
}
|
|
1131
|
+
return acquireMachineLock({
|
|
1132
|
+
command,
|
|
1133
|
+
dir: anchorDir,
|
|
1134
|
+
env: parsedArgs.shouldBypassLock ? { ...env, [NO_LOCK_ENV_VAR]: '1' } : env,
|
|
1135
|
+
stderr,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
958
1138
|
function readMisplacedFlagLength(args, index) {
|
|
959
1139
|
const arg = args[index] ?? '';
|
|
960
1140
|
if (FILTER_FLAGS.has(arg)) {
|
|
@@ -964,8 +1144,8 @@ function readMisplacedFlagLength(args, index) {
|
|
|
964
1144
|
if (booleanFlag !== undefined) {
|
|
965
1145
|
return booleanFlag === 'shouldShowHelp' || booleanFlag === 'shouldShowVersion' ? undefined : 1;
|
|
966
1146
|
}
|
|
967
|
-
const
|
|
968
|
-
return
|
|
1147
|
+
const valueArgument = readOutputStyleArgument(args, index) ?? readDelegateArgument(args, index);
|
|
1148
|
+
return valueArgument?.ok === true ? valueArgument.consumedCount : undefined;
|
|
969
1149
|
}
|
|
970
1150
|
function splitMisplacedFlags(passthrough) {
|
|
971
1151
|
const flagNames = new Set();
|
|
@@ -984,15 +1164,12 @@ function splitMisplacedFlags(passthrough) {
|
|
|
984
1164
|
index++;
|
|
985
1165
|
continue;
|
|
986
1166
|
}
|
|
987
|
-
flagNames.add(arg.
|
|
1167
|
+
flagNames.add(arg.split('=', 1)[0] ?? arg);
|
|
988
1168
|
movedTokens.push(...passthrough.slice(index, index + flagLength));
|
|
989
1169
|
index += flagLength;
|
|
990
1170
|
}
|
|
991
1171
|
return { flagNames: [...flagNames], movedTokens, remainingTokens };
|
|
992
1172
|
}
|
|
993
|
-
function quoteForShell(arg) {
|
|
994
|
-
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
995
|
-
}
|
|
996
1173
|
function wrapWithHooks(command, mainSteps, registry, anchorDir, isWorkspaceRoot) {
|
|
997
1174
|
const steps = [];
|
|
998
1175
|
if (hasRunnableHook(`${command}:pre`, registry, anchorDir, isWorkspaceRoot)) {
|
package/dist/esm/verdict.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type NoOpReason = 'empty-override' | 'empty-workspace' | 'noop-override';
|
|
|
29
29
|
*/
|
|
30
30
|
export type Verdict = {
|
|
31
31
|
command: string;
|
|
32
|
+
/** The host on which a delegated command ran, which a command that ran here does not have. */
|
|
33
|
+
host?: string;
|
|
32
34
|
scope: string;
|
|
33
35
|
} & VerdictOutcome;
|
|
34
36
|
/**
|
package/dist/esm/verdict.js
CHANGED
|
@@ -23,13 +23,14 @@ const MIN_CUT_BYTES = Buffer.byteLength(TRUNCATION_MARK);
|
|
|
23
23
|
const REPLAY_MARKER = 'replayed:';
|
|
24
24
|
function describeOutcome(verdict, style) {
|
|
25
25
|
const statuses = STATUS_GLYPHS[style];
|
|
26
|
+
const onHost = verdict.host === undefined ? '' : ` on ${verdict.host}`;
|
|
26
27
|
switch (verdict.outcome) {
|
|
27
28
|
case 'passed':
|
|
28
|
-
return { marker: statuses.passed.text, phrase: `passed in ${formatDuration(verdict.durationMs)}` };
|
|
29
|
+
return { marker: statuses.passed.text, phrase: `passed${onHost} in ${formatDuration(verdict.durationMs)}` };
|
|
29
30
|
case 'failed':
|
|
30
31
|
return {
|
|
31
32
|
marker: statuses.failed.text,
|
|
32
|
-
phrase: `failed in ${formatDuration(verdict.durationMs)} (exit ${verdict.exitCode})`,
|
|
33
|
+
phrase: `failed${onHost} in ${formatDuration(verdict.durationMs)} (exit ${verdict.exitCode})`,
|
|
33
34
|
};
|
|
34
35
|
case 'recalled': {
|
|
35
36
|
const saving = formatSaving(verdict.savedMs);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MachineCapacity {
|
|
2
|
+
coreCount: number;
|
|
3
|
+
totalMemoryBytes: number;
|
|
4
|
+
}
|
|
5
|
+
/** Returns the worker count for a machine: one per `GIB_PER_WORKER` of memory, bounded by one fewer than the cores. */
|
|
6
|
+
export declare function deriveDefaultMaxWorkers({ coreCount, totalMemoryBytes }: MachineCapacity): number;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the value of the last `--maxWorkers` argument: a number, or a percentage string that Vitest resolves against
|
|
9
|
+
* the core count. An argument whose value is neither counts as absent, which leaves Vitest to report it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readMaxWorkersFlag(args: readonly string[]): number | string | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const SMALL_MACHINE_MAX_CORES = 2;
|
|
2
|
+
const SMALL_MACHINE_MAX_WORKERS = 3;
|
|
3
|
+
const GIB_PER_WORKER = 4;
|
|
4
|
+
const BYTES_PER_GIB = 2 ** 30;
|
|
5
|
+
const MAX_WORKERS_FLAGS = ['--maxWorkers', '--max-workers'];
|
|
6
|
+
const PERCENTAGE_PATTERN = /^\d+%$/;
|
|
7
|
+
const POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/;
|
|
8
|
+
export function deriveDefaultMaxWorkers({ coreCount, totalMemoryBytes }) {
|
|
9
|
+
if (coreCount <= SMALL_MACHINE_MAX_CORES)
|
|
10
|
+
return SMALL_MACHINE_MAX_WORKERS;
|
|
11
|
+
const memoryBound = Math.max(1, Math.floor(totalMemoryBytes / (GIB_PER_WORKER * BYTES_PER_GIB)));
|
|
12
|
+
return Math.min(coreCount - 1, memoryBound);
|
|
13
|
+
}
|
|
14
|
+
export function readMaxWorkersFlag(args) {
|
|
15
|
+
let rawValue;
|
|
16
|
+
args.forEach((arg, index) => {
|
|
17
|
+
if (MAX_WORKERS_FLAGS.includes(arg)) {
|
|
18
|
+
rawValue = args[index + 1];
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const flag = MAX_WORKERS_FLAGS.find((name) => arg.startsWith(`${name}=`));
|
|
22
|
+
if (flag !== undefined)
|
|
23
|
+
rawValue = arg.slice(flag.length + 1);
|
|
24
|
+
});
|
|
25
|
+
return parseMaxWorkersValue(rawValue);
|
|
26
|
+
}
|
|
27
|
+
function parseMaxWorkersValue(rawValue) {
|
|
28
|
+
if (rawValue === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (POSITIVE_INTEGER_PATTERN.test(rawValue))
|
|
31
|
+
return Number(rawValue);
|
|
32
|
+
if (PERCENTAGE_PATTERN.test(rawValue))
|
|
33
|
+
return rawValue;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -135,10 +135,9 @@ function matchSubpath(exportsValue, subpath) {
|
|
|
135
135
|
continue;
|
|
136
136
|
const base = key.slice(0, star);
|
|
137
137
|
const suffix = key.slice(star + 1);
|
|
138
|
-
if (!subpath.startsWith(base) || !subpath.endsWith(suffix))
|
|
139
|
-
continue;
|
|
140
|
-
if (subpath.length < base.length + suffix.length)
|
|
138
|
+
if (!subpath.startsWith(base) || !subpath.endsWith(suffix) || subpath.length < base.length + suffix.length) {
|
|
141
139
|
continue;
|
|
140
|
+
}
|
|
142
141
|
if (bestMatch !== undefined && bestMatch.base.length >= base.length)
|
|
143
142
|
continue;
|
|
144
143
|
bestMatch = { base, value, wildcard: subpath.slice(base.length, subpath.length - suffix.length) };
|
package/dist/esm/vitest.js
CHANGED
|
@@ -5,14 +5,13 @@ import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
|
5
5
|
import { listGitIgnoredPaths } from "./git-ignored-paths.js";
|
|
6
6
|
import { isObject } from "./helpers/type-guards.js";
|
|
7
7
|
import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
|
|
8
|
+
import { deriveDefaultMaxWorkers, readMaxWorkersFlag } from "./vitest-max-workers.js";
|
|
8
9
|
import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
|
|
9
10
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
10
11
|
import { toWorkspacePackageGlobs } from "./workspace-globs.js";
|
|
11
12
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
12
13
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
13
14
|
const TIER_TIMEOUT_MS = 30_000;
|
|
14
|
-
const SMALL_MACHINE_MAX_CORES = 2;
|
|
15
|
-
const SMALL_MACHINE_MAX_WORKERS = 3;
|
|
16
15
|
const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
|
|
17
16
|
const CLIENT_CONDITIONS = ['module', 'browser', 'development|production'];
|
|
18
17
|
const SERVER_CONDITIONS = ['module', 'node', 'development|production'];
|
|
@@ -99,7 +98,11 @@ function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
|
99
98
|
})),
|
|
100
99
|
];
|
|
101
100
|
const collectionExclude = buildCollectionExclude(layers, ignoredPathsRoot);
|
|
102
|
-
const
|
|
101
|
+
const defaultMaxWorkers = deriveDefaultMaxWorkers({
|
|
102
|
+
coreCount: os.availableParallelism(),
|
|
103
|
+
totalMemoryBytes: os.totalmem(),
|
|
104
|
+
});
|
|
105
|
+
const flagMaxWorkers = readMaxWorkersFlag(process.argv.slice(2));
|
|
103
106
|
const shouldIsolateGit = resolveFlag(layers, 'shouldIsolateGit', true);
|
|
104
107
|
return projectTiers.map(({ exclude, include, name, timeoutMs }) => {
|
|
105
108
|
const project = {
|
|
@@ -108,7 +111,7 @@ function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
|
108
111
|
test: {
|
|
109
112
|
exclude: [...collectionExclude, ...exclude, ...extraExclude],
|
|
110
113
|
include,
|
|
111
|
-
|
|
114
|
+
maxWorkers: defaultMaxWorkers,
|
|
112
115
|
name,
|
|
113
116
|
...(shouldIsolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
|
|
114
117
|
...(timeoutMs !== undefined && { hookTimeout: timeoutMs, testTimeout: timeoutMs }),
|
|
@@ -118,7 +121,9 @@ function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
|
118
121
|
for (const layer of layers) {
|
|
119
122
|
mergedProject = applyLayer(mergedProject, layer, name);
|
|
120
123
|
}
|
|
121
|
-
return
|
|
124
|
+
return flagMaxWorkers === undefined
|
|
125
|
+
? mergedProject
|
|
126
|
+
: mergeConfig(mergedProject, { test: { maxWorkers: flagMaxWorkers } });
|
|
122
127
|
});
|
|
123
128
|
}
|
|
124
129
|
function applyLayer(project, layer, tier) {
|
|
@@ -175,9 +180,6 @@ function formatOptionKeys(keys) {
|
|
|
175
180
|
.map((key) => `\`${key}\``)
|
|
176
181
|
.join(', ');
|
|
177
182
|
}
|
|
178
|
-
function resolveDefaultMaxWorkers() {
|
|
179
|
-
return os.availableParallelism() <= SMALL_MACHINE_MAX_CORES ? SMALL_MACHINE_MAX_WORKERS : undefined;
|
|
180
|
-
}
|
|
181
183
|
function resolveFlag(layers, name, isOnByDefault) {
|
|
182
184
|
let resolvedFlag = isOnByDefault;
|
|
183
185
|
for (const layer of layers) {
|
package/docs/check-cache.md
CHANGED
|
@@ -93,7 +93,7 @@ $ nmr check
|
|
|
93
93
|
⏩ nmr-core: check: passed 3m ago on this tree, saved ~48s — replayed: nmr-core: fmt:check: All matched files use Prettier code style!; nmr-core: test: Test Files 6 passed (6) Tests 41 passed (41)
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
The assembly is flat: A constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test` rather than one opaque `check:strict` line. A
|
|
96
|
+
The assembly is flat: A constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test` rather than one opaque `check:strict` line. A fan-out expands into the scopes that it selects, and a constituent that did not record an excerpt is absent rather than inferred: `typecheck` and `lint:check` print nothing on success and contribute nothing above, and a command outside the [cacheable set](#what-is-cached) records nothing at all. `ci` replays what `check:strict` earned and nothing for `build`.
|
|
97
97
|
|
|
98
98
|
Every excerpt in the assembly was certified during a single run, on a single tree, because nmr assembles it when the composite records its pass rather than when it skips. `NMR_RUN_ID` makes that provable; see [reserved environment variables](#reserved-environment-variables).
|
|
99
99
|
|
|
@@ -133,7 +133,7 @@ $ nmr --log test
|
|
|
133
133
|
🟠 nmr: test: no recording; the last pass was 3m ago, on a tree that this is not
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
**`--log` applies to the scopes that `-F` and `-R` select.** nmr passes the flag to
|
|
136
|
+
**`--log` applies to the scopes that `-F` and `-R` select.** nmr passes the flag to each scope that it selects. Each scope prints its own recording. There a scope with nothing to show reports its gap and exits 0: Partial coverage is normal for a survey, and failing on the first gap would hide every scope that had something to show. A selection that does not include any scope is the other case and fails, as it does for a run: An empty selection is not a partial survey.
|
|
137
137
|
|
|
138
138
|
**Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes to which a verdict line is held. nmr keeps 128 KiB from each end of a transcript that overruns it, because a run's opening and its closing statement each contain what the other does not, and it marks the drop with a line that names the dropped bytes, rather than cutting silently.
|
|
139
139
|
|