@williamthorsen/nmr 0.44.0 → 0.46.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 +107 -0
- package/.readyup/kits/default.js +419 -129
- package/.readyup/manifest.json +13 -17
- package/CHANGELOG.md +32 -0
- package/README.md +6 -21
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/cli-taze.js +1 -1
- package/dist/esm/commands/fmt.d.ts +4 -2
- package/dist/esm/commands/fmt.js +11 -3
- package/dist/esm/commands/load-package-maturity-period.d.ts +8 -0
- package/dist/esm/commands/load-package-maturity-period.js +15 -0
- package/dist/esm/commands/taze-args.d.ts +17 -0
- package/dist/esm/commands/taze-args.js +25 -0
- package/dist/esm/commands/taze-passes.d.ts +18 -0
- package/dist/esm/commands/taze-passes.js +59 -0
- package/dist/esm/commands/taze.d.ts +15 -9
- package/dist/esm/commands/taze.js +31 -16
- 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/taze.d.ts +7 -0
- package/dist/esm/tiers.d.ts +6 -4
- package/dist/esm/vitest-git-isolation.js +3 -1
- package/docs/reporting.md +2 -0
- package/docs/scripts.md +10 -2
- package/docs/upgrades.md +29 -1
- package/docs/utilities.md +2 -2
- package/docs/vitest.md +15 -9
- package/package.json +5 -4
package/dist/esm/config.js
CHANGED
|
@@ -9,11 +9,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { pathToFileURL } from 'node:url';
|
|
11
11
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
12
|
+
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
12
13
|
import { findClosestName } from "./helpers/findClosestName.js";
|
|
13
14
|
import { isHookName } from "./helpers/hook-name.js";
|
|
14
15
|
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
17
|
+
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, readPackageJsonScripts } from "./resolver.js";
|
|
18
|
+
import { findUnexpressibleToken, readSelfReference, readStepListSelfReference, } from "./steps.js";
|
|
17
19
|
import { UserError } from "./UserError.js";
|
|
18
20
|
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
19
21
|
import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
|
|
@@ -35,6 +37,10 @@ const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommand
|
|
|
35
37
|
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
36
38
|
const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
|
|
37
39
|
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
|
|
40
|
+
const SCRIPT_FIELDS = [
|
|
41
|
+
{ field: 'rootScripts', scope: 'root' },
|
|
42
|
+
{ field: 'workspaceScripts', scope: 'workspaceRegistry' },
|
|
43
|
+
];
|
|
38
44
|
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
39
45
|
function isScriptRecord(value) {
|
|
40
46
|
if (!isObject(value))
|
|
@@ -286,7 +292,9 @@ export async function loadConfig(baseDir) {
|
|
|
286
292
|
}
|
|
287
293
|
export async function loadRootConfig(monorepoRoot) {
|
|
288
294
|
const config = await loadConfig(monorepoRoot);
|
|
289
|
-
|
|
295
|
+
const configPath = resolveConfigPath(monorepoRoot);
|
|
296
|
+
assertTierKeys(config, CONFIG_TIERS.root, configPath);
|
|
297
|
+
assertNoSelfReferences(config, configPath, monorepoRoot);
|
|
290
298
|
return config;
|
|
291
299
|
}
|
|
292
300
|
export async function loadWorkspaceConfig(packageDir) {
|
|
@@ -302,6 +310,30 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
|
302
310
|
throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
|
|
303
311
|
}
|
|
304
312
|
}
|
|
313
|
+
function assertNoSelfReferences(config, configPath, monorepoRoot) {
|
|
314
|
+
for (const { field, scope } of SCRIPT_FIELDS) {
|
|
315
|
+
const scripts = config[field] ?? {};
|
|
316
|
+
for (const [key, script] of Object.entries(scripts)) {
|
|
317
|
+
const selfReference = typeof script === 'string'
|
|
318
|
+
? readSelfReference({ commandName: key, scope, script })
|
|
319
|
+
: readStepListSelfReference({
|
|
320
|
+
commandName: key,
|
|
321
|
+
elements: script.map((element) => (typeof element === 'string' ? element : element.run)),
|
|
322
|
+
scope,
|
|
323
|
+
});
|
|
324
|
+
if (selfReference === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
const defaults = field === 'rootScripts' ? getDefaultRootScripts(monorepoRoot) : getDefaultWorkspaceScripts();
|
|
327
|
+
const remedy = formatSelfReferenceRemedy({
|
|
328
|
+
isDefault: !isHookName(key) && Object.hasOwn(defaults, key),
|
|
329
|
+
key,
|
|
330
|
+
selfReference,
|
|
331
|
+
});
|
|
332
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${field}.${key}\` re-invokes \`${key}\` ` +
|
|
333
|
+
`(\`${escapeControlCharacters(describeScript(script))}\`), which would re-enter nmr without bound. ${remedy}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
305
337
|
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
306
338
|
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
307
339
|
if (unrecognizedKeys.length === 0) {
|
|
@@ -324,3 +356,14 @@ function formatKeyList(keys, prefix) {
|
|
|
324
356
|
.map((key) => `\`${prefix}${key}\``)
|
|
325
357
|
.join(', ');
|
|
326
358
|
}
|
|
359
|
+
function formatSelfReferenceRemedy(options) {
|
|
360
|
+
const { isDefault, key, selfReference } = options;
|
|
361
|
+
if (isDefault) {
|
|
362
|
+
return selfReference === 'sole'
|
|
363
|
+
? `Delete the entry: nmr's own \`${key}\` then runs.`
|
|
364
|
+
: `Delete the entry and move the steps that it adds to a \`${key}:pre\` or \`${key}:post\` script.`;
|
|
365
|
+
}
|
|
366
|
+
return selfReference === 'sole'
|
|
367
|
+
? 'Delete the entry.'
|
|
368
|
+
: `Delete the re-invocation: \`${key}\` runs the steps beside it.`;
|
|
369
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carries the entries that a run is inside, from the outermost inward, down to every process that it spawns. An
|
|
3
|
+
* entry that finds itself already on the chain would re-enter itself without bound.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ENTRY_CHAIN_ENV_VAR = "NMR_ENTRY_CHAIN";
|
|
6
|
+
/** A command that an invocation resolved, and the directory whose registry it resolved the command from. */
|
|
7
|
+
export interface ChainEntry {
|
|
8
|
+
command: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Admits an invocation onto the chain that it inherited, returning the environment that its steps inherit in turn.
|
|
13
|
+
* Throws a `UserError` when an ancestor in the run is already running the same command from the same directory, or
|
|
14
|
+
* when the inherited chain cannot be read.
|
|
15
|
+
*
|
|
16
|
+
* Records the directory's real path, so that a symlinked checkout or a `/private` prefix seen by one process and not
|
|
17
|
+
* another cannot hide a repeat.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enterChain(options: {
|
|
20
|
+
anchorDir: string;
|
|
21
|
+
command: string;
|
|
22
|
+
env: NodeJS.ProcessEnv;
|
|
23
|
+
monorepoRoot: string;
|
|
24
|
+
}): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
4
|
+
import { UserError } from "./UserError.js";
|
|
5
|
+
export const ENTRY_CHAIN_ENV_VAR = 'NMR_ENTRY_CHAIN';
|
|
6
|
+
export function enterChain(options) {
|
|
7
|
+
const { anchorDir, command, env, monorepoRoot } = options;
|
|
8
|
+
const chain = readEntryChain(env[ENTRY_CHAIN_ENV_VAR]);
|
|
9
|
+
if (chain === undefined) {
|
|
10
|
+
throw new UserError(`${ENTRY_CHAIN_ENV_VAR} is not a chain that nmr wrote; unset it. nmr passes it to the processes that it starts.`);
|
|
11
|
+
}
|
|
12
|
+
const entry = { command, dir: realpathSync(anchorDir) };
|
|
13
|
+
const extendedChain = [...chain, entry];
|
|
14
|
+
if (chain.some((ancestor) => ancestor.command === entry.command && ancestor.dir === entry.dir)) {
|
|
15
|
+
throw new UserError(formatReentryError(extendedChain, realpathSync(monorepoRoot)));
|
|
16
|
+
}
|
|
17
|
+
return { ...env, [ENTRY_CHAIN_ENV_VAR]: JSON.stringify(extendedChain) };
|
|
18
|
+
}
|
|
19
|
+
function describeDir(dir, monorepoRoot) {
|
|
20
|
+
const relativeDir = path.relative(monorepoRoot, dir);
|
|
21
|
+
if (relativeDir === '') {
|
|
22
|
+
return 'root';
|
|
23
|
+
}
|
|
24
|
+
return relativeDir.startsWith('..') || path.isAbsolute(relativeDir) ? dir : relativeDir;
|
|
25
|
+
}
|
|
26
|
+
function formatReentryError(chain, monorepoRoot) {
|
|
27
|
+
const repeat = chain.at(-1);
|
|
28
|
+
if (repeat === undefined) {
|
|
29
|
+
throw new Error('A re-entry error needs the entry that repeats.');
|
|
30
|
+
}
|
|
31
|
+
const repeatDir = describeDir(repeat.dir, monorepoRoot);
|
|
32
|
+
const lines = chain.map((entry) => {
|
|
33
|
+
const isRepeat = entry.command === repeat.command && entry.dir === repeat.dir;
|
|
34
|
+
return ` ${entry.command} in ${describeDir(entry.dir, monorepoRoot)}${isRepeat ? ' (repeat)' : ''}`;
|
|
35
|
+
});
|
|
36
|
+
return [
|
|
37
|
+
`nmr re-entered \`${repeat.command}\` in ${repeatDir}, which this run is already running there, ` +
|
|
38
|
+
'so the run would never end. The entries that this run has entered, outermost first:',
|
|
39
|
+
...lines,
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
function isChainEntry(value) {
|
|
43
|
+
return isObject(value) && typeof value['command'] === 'string' && typeof value['dir'] === 'string';
|
|
44
|
+
}
|
|
45
|
+
function readEntryChain(value) {
|
|
46
|
+
if (value === undefined || value === '') {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return Array.isArray(parsed) && parsed.every(isChainEntry) ? parsed : undefined;
|
|
57
|
+
}
|
package/dist/esm/glyphs.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* `noop` is a neutral circle: Nothing ran because the repo asked for nothing to run, which is neither a
|
|
6
6
|
* failure nor a block.
|
|
7
7
|
*/
|
|
8
|
-
export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "
|
|
8
|
+
export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "package" | "clean" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
|
|
9
9
|
export type NmrGlyphName = keyof (typeof NMR_GLYPHS)['rich'];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a declaration's text as the file stores it, so that a value written across lines is quoted on one line.
|
|
3
|
+
*
|
|
4
|
+
* A diagnostic names a declaration that the reader has to edit, and a JSON string contains an escape sequence where
|
|
5
|
+
* a shell would read a control character. The rendered escape keeps the diagnostic on one line, and it is the text
|
|
6
|
+
* for which the reader will search the file.
|
|
7
|
+
*
|
|
8
|
+
* Apply it only where the text is quoted: A comparison against the declaration reads the unescaped text.
|
|
9
|
+
*/
|
|
10
|
+
export declare function escapeControlCharacters(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const NAMED_ESCAPES = new Map([
|
|
2
|
+
['\n', String.raw `\n`],
|
|
3
|
+
['\r', String.raw `\r`],
|
|
4
|
+
['\t', String.raw `\t`],
|
|
5
|
+
]);
|
|
6
|
+
export function escapeControlCharacters(text) {
|
|
7
|
+
let escapedText = text;
|
|
8
|
+
for (const [char, escape] of NAMED_ESCAPES) {
|
|
9
|
+
escapedText = escapedText.split(char).join(escape);
|
|
10
|
+
}
|
|
11
|
+
return escapedText;
|
|
12
|
+
}
|
package/dist/esm/resolver.js
CHANGED
|
@@ -108,12 +108,12 @@ export function findChainedSelfReference(packageDir, commandName) {
|
|
|
108
108
|
if (script === undefined) {
|
|
109
109
|
return undefined;
|
|
110
110
|
}
|
|
111
|
-
return readSelfReference({
|
|
111
|
+
return readSelfReference({ commandName, scope: readPackageScope(packageDir), script }) === 'chained'
|
|
112
112
|
? script
|
|
113
113
|
: undefined;
|
|
114
114
|
}
|
|
115
115
|
export function isSelfReferential(script, commandName, packageDir) {
|
|
116
|
-
return readSelfReference({
|
|
116
|
+
return readSelfReference({ commandName, scope: readPackageScope(packageDir), script }) !== undefined;
|
|
117
117
|
}
|
|
118
118
|
export function resolveScript(commandName, registry, packageDir, isWorkspaceRoot) {
|
|
119
119
|
if (packageDir) {
|
|
@@ -137,3 +137,6 @@ export function resolveScript(commandName, registry, packageDir, isWorkspaceRoot
|
|
|
137
137
|
}
|
|
138
138
|
return { origin: { tier: 'registry', key: commandName }, steps: expandScript(registryEntry, isWorkspaceRoot) };
|
|
139
139
|
}
|
|
140
|
+
function readPackageScope(packageDir) {
|
|
141
|
+
return isMonorepoRoot(packageDir) ? 'root' : 'package';
|
|
142
|
+
}
|
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
|
});
|