@williamthorsen/nmr 2.2.0 → 2.2.2
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 +50 -0
- package/.readyup/kits/default.js +39 -39
- package/.readyup/manifest.json +16 -16
- package/CHANGELOG.md +17 -0
- package/README.md +1 -8
- package/dist/esm/commands/build.js +25 -25
- package/dist/esm/commands/fmt.js +62 -62
- package/dist/esm/commands/node-pin.js +3 -3
- package/dist/esm/commands/planTazePasses.js +6 -6
- package/dist/esm/commands/resolveFormatTargets.js +3 -3
- package/dist/esm/config-validation.js +20 -20
- package/dist/esm/delegation/delegate-host.js +13 -13
- package/dist/esm/delegation/remote-host-session.d.ts +4 -0
- package/dist/esm/delegation/remote-host-session.js +5 -3
- package/dist/esm/delegation/remote-session.d.ts +2 -2
- package/dist/esm/delegation/remote-session.js +1 -1
- package/dist/esm/describeHelp.js +22 -22
- package/dist/esm/helpers/filter-selection.js +4 -4
- package/dist/esm/machine-lock.js +8 -8
- package/dist/esm/prettier.js +11 -11
- package/dist/esm/resolver.js +10 -10
- package/dist/esm/runCli.js +33 -33
- package/dist/esm/runner.js +23 -23
- package/dist/esm/verdict.js +37 -37
- package/dist/esm/vitest.js +48 -48
- package/docs/delegate.md +1 -1
- package/package.json +8 -8
|
@@ -98,6 +98,13 @@ function formatKeyList(keys, prefix) {
|
|
|
98
98
|
function isOptionalBoolean(value) {
|
|
99
99
|
return value === undefined || typeof value === 'boolean';
|
|
100
100
|
}
|
|
101
|
+
function isScriptElement(value) {
|
|
102
|
+
if (typeof value === 'string')
|
|
103
|
+
return true;
|
|
104
|
+
if (!isRecord(value) || typeof value['run'] !== 'string')
|
|
105
|
+
return false;
|
|
106
|
+
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
107
|
+
}
|
|
101
108
|
function isScriptRecord(value) {
|
|
102
109
|
if (!isRecord(value))
|
|
103
110
|
return false;
|
|
@@ -109,13 +116,6 @@ function isScriptRecord(value) {
|
|
|
109
116
|
}
|
|
110
117
|
return true;
|
|
111
118
|
}
|
|
112
|
-
function isScriptElement(value) {
|
|
113
|
-
if (typeof value === 'string')
|
|
114
|
-
return true;
|
|
115
|
-
if (!isRecord(value) || typeof value['run'] !== 'string')
|
|
116
|
-
return false;
|
|
117
|
-
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
118
|
-
}
|
|
119
119
|
function isStringArray(value) {
|
|
120
120
|
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
121
121
|
}
|
|
@@ -215,6 +215,19 @@ function validateOutputField(value, configPath) {
|
|
|
215
215
|
}
|
|
216
216
|
return config;
|
|
217
217
|
}
|
|
218
|
+
function validateScriptField(value, fieldName, configPath) {
|
|
219
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const scripts = value[fieldName];
|
|
223
|
+
if (!isScriptRecord(scripts)) {
|
|
224
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
225
|
+
'where an element is a command string or ' +
|
|
226
|
+
'`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
|
|
227
|
+
}
|
|
228
|
+
assertValidElements(scripts, fieldName, configPath);
|
|
229
|
+
return scripts;
|
|
230
|
+
}
|
|
218
231
|
function validateSelfHostedRunnerField(value, configPath) {
|
|
219
232
|
const selfHostedRunner = value['selfHostedRunner'];
|
|
220
233
|
if (selfHostedRunner === undefined) {
|
|
@@ -230,16 +243,3 @@ function validateSelfHostedRunnerField(value, configPath) {
|
|
|
230
243
|
}
|
|
231
244
|
return { exempt };
|
|
232
245
|
}
|
|
233
|
-
function validateScriptField(value, fieldName, configPath) {
|
|
234
|
-
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
const scripts = value[fieldName];
|
|
238
|
-
if (!isScriptRecord(scripts)) {
|
|
239
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
240
|
-
'where an element is a command string or ' +
|
|
241
|
-
'`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
|
|
242
|
-
}
|
|
243
|
-
assertValidElements(scripts, fieldName, configPath);
|
|
244
|
-
return scripts;
|
|
245
|
-
}
|
|
@@ -98,19 +98,6 @@ function createTee(stream, buffer) {
|
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
|
-
function describeDelegationSource(target, parsedArgs, env) {
|
|
102
|
-
switch (target.source) {
|
|
103
|
-
case 'config':
|
|
104
|
-
return `${formatDisplayPath(target.configPath, env)} (when: ${target.when})`;
|
|
105
|
-
case 'env':
|
|
106
|
-
return DELEGATE_ENV_VAR;
|
|
107
|
-
case 'flag':
|
|
108
|
-
return parsedArgs.delegateHosts === undefined ? DELEGATE_FLAG : DELEGATE_TO_FLAG;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function describeUnreachableHost(host, sshMessage) {
|
|
112
|
-
return sshMessage === '' ? `${host} did not answer` : `${host} did not answer: ${sshMessage}`;
|
|
113
|
-
}
|
|
114
101
|
async function delegate(options) {
|
|
115
102
|
const { invocation, remoteEnv, target } = options;
|
|
116
103
|
const { command, context, cwd, env, parsedArgs, stderr, stdout } = invocation;
|
|
@@ -178,6 +165,19 @@ async function delegate(options) {
|
|
|
178
165
|
: { outcome: 'failed', durationMs, exitCode: result.exitCode };
|
|
179
166
|
return { ...ran, exitCode: result.exitCode, outcome, passes: result.passes };
|
|
180
167
|
}
|
|
168
|
+
function describeDelegationSource(target, parsedArgs, env) {
|
|
169
|
+
switch (target.source) {
|
|
170
|
+
case 'config':
|
|
171
|
+
return `${formatDisplayPath(target.configPath, env)} (when: ${target.when})`;
|
|
172
|
+
case 'env':
|
|
173
|
+
return DELEGATE_ENV_VAR;
|
|
174
|
+
case 'flag':
|
|
175
|
+
return parsedArgs.delegateHosts === undefined ? DELEGATE_FLAG : DELEGATE_TO_FLAG;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function describeUnreachableHost(host, sshMessage) {
|
|
179
|
+
return sshMessage === '' ? `${host} did not answer` : `${host} did not answer: ${sshMessage}`;
|
|
180
|
+
}
|
|
181
181
|
function formatRunHereLine(command, listedHosts, reason) {
|
|
182
182
|
return `nmr: running ${command} here rather than on ${listedHosts}: ${reason}\n`;
|
|
183
183
|
}
|
|
@@ -55,6 +55,10 @@ export type SessionRecord = {
|
|
|
55
55
|
* out the snapshot, installs, runs nmr, and reports the passes that nmr wrote to the pass manifest. It is sent as
|
|
56
56
|
* source, so it refers to nothing outside its own body.
|
|
57
57
|
*
|
|
58
|
+
* Every process that the session starts runs on the login shell's `PATH`, which the session script passes as the
|
|
59
|
+
* argument after `params`: A version manager's `node` shim puts the `bin` directory of the Node that it chose before
|
|
60
|
+
* the checkout in front of the session's own `PATH`, and the snapshot may pin another.
|
|
61
|
+
*
|
|
58
62
|
* The lock follows the machine lock's rules: A waiter reclaims it at once when the holder's pid is gone, and once the
|
|
59
63
|
* holder has stopped refreshing it for a minute otherwise. When the session's stdin ends, which the local `ssh` causes
|
|
60
64
|
* by exiting, the session stops its current phase's process group, releases the lock, and exits.
|
|
@@ -16,6 +16,8 @@ export function runHostSession(params) {
|
|
|
16
16
|
const STOP_GRACE_MS = 5000;
|
|
17
17
|
const NODE_MODULES_DIR = path.join(params.rootDir, 'node_modules');
|
|
18
18
|
const BINS_RECORD_PATH = path.join(NODE_MODULES_DIR, '.nmr-delegate-bins');
|
|
19
|
+
const loginPath = process.argv[2] || process.env['PATH'];
|
|
20
|
+
const childEnv = { ...process.env, PATH: loginPath };
|
|
19
21
|
const token = crypto.randomUUID();
|
|
20
22
|
let isHolding = false;
|
|
21
23
|
let activeChild;
|
|
@@ -30,7 +32,7 @@ export function runHostSession(params) {
|
|
|
30
32
|
['checkout', '--quiet', '--force', '--detach', params.sha],
|
|
31
33
|
['clean', '--force', '-d', '--quiet'],
|
|
32
34
|
]) {
|
|
33
|
-
const result = await runPhase('git', args, '.',
|
|
35
|
+
const result = await runPhase('git', args, '.', childEnv);
|
|
34
36
|
if (isStopping)
|
|
35
37
|
return;
|
|
36
38
|
if (result.exitCode !== 0) {
|
|
@@ -43,7 +45,7 @@ export function runHostSession(params) {
|
|
|
43
45
|
removeWorkspaceState();
|
|
44
46
|
}
|
|
45
47
|
const install = await runPhase('pnpm', ['install', '--frozen-lockfile'], params.rootDir, {
|
|
46
|
-
...
|
|
48
|
+
...childEnv,
|
|
47
49
|
CI: 'true',
|
|
48
50
|
});
|
|
49
51
|
if (isStopping)
|
|
@@ -62,7 +64,7 @@ export function runHostSession(params) {
|
|
|
62
64
|
const child = childProcess.spawn('pnpm', ['exec', 'nmr', '--no-delegate', ...params.nmrArgs], {
|
|
63
65
|
cwd: params.runDir,
|
|
64
66
|
detached: true,
|
|
65
|
-
env: { ...
|
|
67
|
+
env: { ...childEnv, ...params.remoteEnv, ...manifestEnv },
|
|
66
68
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
67
69
|
});
|
|
68
70
|
activeChild = child;
|
|
@@ -14,8 +14,8 @@ export interface SessionDemultiplexer {
|
|
|
14
14
|
stream: Writable;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Composes the script that runs the host session from the checkout
|
|
18
|
-
* can name the host's home directory.
|
|
17
|
+
* Composes the script that runs the host session from the checkout, passing it the shell's `PATH` for the processes
|
|
18
|
+
* that it starts. `checkoutDir` is a shell expression, so that it can name the host's home directory.
|
|
19
19
|
*/
|
|
20
20
|
export declare function composeSessionScript(options: {
|
|
21
21
|
checkoutDir: string;
|
|
@@ -5,7 +5,7 @@ import { runHostSession, } from "./remote-host-session.js";
|
|
|
5
5
|
export function composeSessionScript(options) {
|
|
6
6
|
const { checkoutDir, params } = options;
|
|
7
7
|
const source = `(${runHostSession.toString()})(JSON.parse(process.argv[1]));`;
|
|
8
|
-
return `cd ${checkoutDir} && exec node -e ${quoteForShell(source)} ${quoteForShell(JSON.stringify(params))}`;
|
|
8
|
+
return `cd ${checkoutDir} && exec node -e ${quoteForShell(source)} ${quoteForShell(JSON.stringify(params))} "$PATH"`;
|
|
9
9
|
}
|
|
10
10
|
export function createSessionDemultiplexer(options) {
|
|
11
11
|
const { marker, onMalformedRecord, onRecord, stderr } = options;
|
package/dist/esm/describeHelp.js
CHANGED
|
@@ -57,6 +57,17 @@ const ENVIRONMENT_AND_FILES_LINES = [
|
|
|
57
57
|
' ($XDG_CONFIG_HOME/nmr/config.ts when that is set)',
|
|
58
58
|
' A command that it sends runs here when no host answers',
|
|
59
59
|
];
|
|
60
|
+
function applyOverrides(registry, overrides) {
|
|
61
|
+
const markedNames = new Set();
|
|
62
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
63
|
+
if (!Object.hasOwn(registry, name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
registry[name] = value;
|
|
67
|
+
markedNames.add(name);
|
|
68
|
+
}
|
|
69
|
+
return markedNames;
|
|
70
|
+
}
|
|
60
71
|
function collectOverrides(packageScripts) {
|
|
61
72
|
const candidateOverrides = {};
|
|
62
73
|
for (const [name, value] of Object.entries(packageScripts)) {
|
|
@@ -74,28 +85,6 @@ function collectUnregisteredScripts(packageScripts, registry) {
|
|
|
74
85
|
}
|
|
75
86
|
return unregisteredScripts;
|
|
76
87
|
}
|
|
77
|
-
function readRunnableScripts(packageDir) {
|
|
78
|
-
const scripts = readPackageJsonScripts(packageDir);
|
|
79
|
-
if (!scripts)
|
|
80
|
-
return {};
|
|
81
|
-
const runnableScripts = {};
|
|
82
|
-
for (const [name, value] of Object.entries(scripts)) {
|
|
83
|
-
if (!isSelfReferential(value, name, packageDir))
|
|
84
|
-
runnableScripts[name] = value;
|
|
85
|
-
}
|
|
86
|
-
return runnableScripts;
|
|
87
|
-
}
|
|
88
|
-
function applyOverrides(registry, overrides) {
|
|
89
|
-
const markedNames = new Set();
|
|
90
|
-
for (const [name, value] of Object.entries(overrides)) {
|
|
91
|
-
if (!Object.hasOwn(registry, name)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
registry[name] = value;
|
|
95
|
-
markedNames.add(name);
|
|
96
|
-
}
|
|
97
|
-
return markedNames;
|
|
98
|
-
}
|
|
99
88
|
function filterHooks(registry) {
|
|
100
89
|
const hooklessRegistry = {};
|
|
101
90
|
for (const [key, value] of Object.entries(registry)) {
|
|
@@ -115,3 +104,14 @@ function formatRegistry(registry, markedNames, lines) {
|
|
|
115
104
|
lines.push(` ${(key + marker).padEnd(padWidth)} ${describeScript(value)}`);
|
|
116
105
|
}
|
|
117
106
|
}
|
|
107
|
+
function readRunnableScripts(packageDir) {
|
|
108
|
+
const scripts = readPackageJsonScripts(packageDir);
|
|
109
|
+
if (!scripts)
|
|
110
|
+
return {};
|
|
111
|
+
const runnableScripts = {};
|
|
112
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
113
|
+
if (!isSelfReferential(value, name, packageDir))
|
|
114
|
+
runnableScripts[name] = value;
|
|
115
|
+
}
|
|
116
|
+
return runnableScripts;
|
|
117
|
+
}
|
|
@@ -47,6 +47,10 @@ function isRootEntry(entry, monorepoRoot) {
|
|
|
47
47
|
}
|
|
48
48
|
return resolveRealPath(entry['path']) === resolveRealPath(monorepoRoot);
|
|
49
49
|
}
|
|
50
|
+
function readDelegateSelection(pattern, monorepoRoot) {
|
|
51
|
+
const probeRun = spawnSync('pnpm', ['--filter', pattern, 'exec', process.execPath, '--eval', `process.stdout.write('${SELECTION_MARKER}')`], { cwd: monorepoRoot, encoding: 'utf8' });
|
|
52
|
+
return interpretDelegateProbe({ error: probeRun.error, status: probeRun.status, stdout: probeRun.stdout });
|
|
53
|
+
}
|
|
50
54
|
function resolveRealPath(target) {
|
|
51
55
|
try {
|
|
52
56
|
return realpathSync(target);
|
|
@@ -55,7 +59,3 @@ function resolveRealPath(target) {
|
|
|
55
59
|
return path.resolve(target);
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
|
-
function readDelegateSelection(pattern, monorepoRoot) {
|
|
59
|
-
const probeRun = spawnSync('pnpm', ['--filter', pattern, 'exec', process.execPath, '--eval', `process.stdout.write('${SELECTION_MARKER}')`], { cwd: monorepoRoot, encoding: 'utf8' });
|
|
60
|
-
return interpretDelegateProbe({ error: probeRun.error, status: probeRun.status, stdout: probeRun.stdout });
|
|
61
|
-
}
|
package/dist/esm/machine-lock.js
CHANGED
|
@@ -140,14 +140,6 @@ function isHolderRecord(value) {
|
|
|
140
140
|
typeof value['startedAt'] === 'string' &&
|
|
141
141
|
typeof value['token'] === 'string');
|
|
142
142
|
}
|
|
143
|
-
async function isUnderAncestorHold(lockDir, env) {
|
|
144
|
-
const inheritedToken = env[LOCK_TOKEN_ENV_VAR];
|
|
145
|
-
if (inheritedToken === undefined || inheritedToken === '') {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
const state = await readLockState(path.join(lockDir, LOCK_FILE));
|
|
149
|
-
return state?.holder?.token === inheritedToken;
|
|
150
|
-
}
|
|
151
143
|
function isProcessGone(pid) {
|
|
152
144
|
try {
|
|
153
145
|
process.kill(pid, 0);
|
|
@@ -163,6 +155,14 @@ function isStale(state, staleAfterMs) {
|
|
|
163
155
|
}
|
|
164
156
|
return state.holder !== undefined && isProcessGone(state.holder.pid);
|
|
165
157
|
}
|
|
158
|
+
async function isUnderAncestorHold(lockDir, env) {
|
|
159
|
+
const inheritedToken = env[LOCK_TOKEN_ENV_VAR];
|
|
160
|
+
if (inheritedToken === undefined || inheritedToken === '') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const state = await readLockState(path.join(lockDir, LOCK_FILE));
|
|
164
|
+
return state?.holder?.token === inheritedToken;
|
|
165
|
+
}
|
|
166
166
|
function parseHolder(content) {
|
|
167
167
|
try {
|
|
168
168
|
const value = JSON.parse(content);
|
package/dist/esm/prettier.js
CHANGED
|
@@ -54,26 +54,26 @@ export function definePrettierConfig(options = {}) {
|
|
|
54
54
|
plugins: [buildShellPlugin(), ...additionalPlugins],
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
function assertNoReplacement(options) {
|
|
58
|
+
if ('overrides' in options)
|
|
59
|
+
throw buildOwnedKeyError('overrides', 'additionalOverrides');
|
|
60
|
+
if ('plugins' in options)
|
|
61
|
+
throw buildOwnedKeyError('plugins', 'additionalPlugins');
|
|
62
|
+
}
|
|
63
|
+
function buildOwnedKeyError(key, seam) {
|
|
64
|
+
return new TypeError(`definePrettierConfig: \`${key}\` is owned by this config. Use \`${seam}\` to append to it.`);
|
|
65
|
+
}
|
|
57
66
|
function buildShellPlugin() {
|
|
58
67
|
return {
|
|
59
68
|
...shPlugin,
|
|
60
69
|
languages: shPlugin.languages.filter(isInferredLanguage).map(dropExcludedFilenames),
|
|
61
70
|
};
|
|
62
71
|
}
|
|
63
|
-
function isInferredLanguage(language) {
|
|
64
|
-
return INFERRED_LANGUAGES.has(language.name);
|
|
65
|
-
}
|
|
66
72
|
function dropExcludedFilenames(language) {
|
|
67
73
|
if (language.filenames === undefined)
|
|
68
74
|
return language;
|
|
69
75
|
return { ...language, filenames: language.filenames.filter((name) => !EXCLUDED_FILENAMES.has(name)) };
|
|
70
76
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
throw buildOwnedKeyError('overrides', 'additionalOverrides');
|
|
74
|
-
if ('plugins' in options)
|
|
75
|
-
throw buildOwnedKeyError('plugins', 'additionalPlugins');
|
|
76
|
-
}
|
|
77
|
-
function buildOwnedKeyError(key, seam) {
|
|
78
|
-
return new TypeError(`definePrettierConfig: \`${key}\` is owned by this config. Use \`${seam}\` to append to it.`);
|
|
77
|
+
function isInferredLanguage(language) {
|
|
78
|
+
return INFERRED_LANGUAGES.has(language.name);
|
|
79
79
|
}
|
package/dist/esm/resolver.js
CHANGED
|
@@ -18,16 +18,6 @@ export function expandScript(script, isWorkspaceRoot) {
|
|
|
18
18
|
export function describeScript(script) {
|
|
19
19
|
return typeof script === 'string' ? script : `[${script.map(describeElement).join(', ')}]`;
|
|
20
20
|
}
|
|
21
|
-
function describeElement(element) {
|
|
22
|
-
if (typeof element === 'string') {
|
|
23
|
-
return element;
|
|
24
|
-
}
|
|
25
|
-
const notes = [
|
|
26
|
-
...(element.shouldRunInBackground === true ? ['background'] : []),
|
|
27
|
-
...(element.shouldDeclineArguments === true ? ['no args'] : []),
|
|
28
|
-
];
|
|
29
|
-
return notes.length === 0 ? element.run : `${element.run} (${notes.join(', ')})`;
|
|
30
|
-
}
|
|
31
21
|
export function readPackageJsonScripts(packageDir) {
|
|
32
22
|
const file = resolvePackageJsonPath(packageDir);
|
|
33
23
|
let rawText;
|
|
@@ -101,6 +91,16 @@ export function resolveScript(commandName, registry, packageDir, isWorkspaceRoot
|
|
|
101
91
|
}
|
|
102
92
|
return { origin: { tier: 'registry', key: commandName }, steps: expandScript(registryEntry, isWorkspaceRoot) };
|
|
103
93
|
}
|
|
94
|
+
function describeElement(element) {
|
|
95
|
+
if (typeof element === 'string') {
|
|
96
|
+
return element;
|
|
97
|
+
}
|
|
98
|
+
const notes = [
|
|
99
|
+
...(element.shouldRunInBackground === true ? ['background'] : []),
|
|
100
|
+
...(element.shouldDeclineArguments === true ? ['no args'] : []),
|
|
101
|
+
];
|
|
102
|
+
return notes.length === 0 ? element.run : `${element.run} (${notes.join(', ')})`;
|
|
103
|
+
}
|
|
104
104
|
function readPackageScope(packageDir) {
|
|
105
105
|
return isMonorepoRoot(packageDir) ? 'root' : 'package';
|
|
106
106
|
}
|
package/dist/esm/runCli.js
CHANGED
|
@@ -47,29 +47,20 @@ export async function runCli(options) {
|
|
|
47
47
|
const routedArgs = isBare ? [...args, '--help'] : args;
|
|
48
48
|
return { exitCode: await runRouter(routedArgs, root, routerOptions) };
|
|
49
49
|
}
|
|
50
|
-
async function
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
let target = resolvedTarget.value;
|
|
56
|
-
const { filter, recursive } = invocation.parsedArgs;
|
|
57
|
-
if (target.kind === 'remote' && (filter !== undefined || recursive)) {
|
|
58
|
-
const delegated = await runInvocationOnHost(invocation, target);
|
|
59
|
-
if (delegated !== undefined) {
|
|
60
|
-
return delegated.exitCode;
|
|
61
|
-
}
|
|
62
|
-
target = { kind: 'local' };
|
|
50
|
+
async function executePlan(plan) {
|
|
51
|
+
const { command, format, parsedArgs, scope, stdout, styles } = plan.invocation;
|
|
52
|
+
if (parsedArgs.shouldPrintLog) {
|
|
53
|
+
return reportRecording(plan);
|
|
63
54
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const hint = formatMisplacedFlagHint(options.args, invocation.parsedArgs.passthrough, invocation.styles.stderr);
|
|
68
|
-
if (hint !== undefined) {
|
|
69
|
-
options.stderr.write(`${hint}\n`);
|
|
70
|
-
}
|
|
55
|
+
const { exitCode, host, outcome } = await runGated(plan);
|
|
56
|
+
if (outcome !== undefined) {
|
|
57
|
+
reportVerdict({ command, ...(host !== undefined && { host }), scope, ...outcome }, stdout, format, styles.stdout);
|
|
71
58
|
}
|
|
72
|
-
return exitCode;
|
|
59
|
+
return { exitCode };
|
|
60
|
+
}
|
|
61
|
+
function isTopLevelInvocation(env) {
|
|
62
|
+
const runId = env[RUN_ID_ENV_VAR];
|
|
63
|
+
return runId === undefined || runId === '';
|
|
73
64
|
}
|
|
74
65
|
async function planRun(invocation, target) {
|
|
75
66
|
const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
|
|
@@ -176,18 +167,27 @@ async function planRun(invocation, target) {
|
|
|
176
167
|
},
|
|
177
168
|
};
|
|
178
169
|
}
|
|
179
|
-
async function
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
170
|
+
async function runInvocation(invocation, options) {
|
|
171
|
+
const resolvedTarget = await resolveExecutionTarget(invocation);
|
|
172
|
+
if (resolvedTarget.settled) {
|
|
173
|
+
return resolvedTarget.result.exitCode;
|
|
183
174
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
175
|
+
let target = resolvedTarget.value;
|
|
176
|
+
const { filter, recursive } = invocation.parsedArgs;
|
|
177
|
+
if (target.kind === 'remote' && (filter !== undefined || recursive)) {
|
|
178
|
+
const delegated = await runInvocationOnHost(invocation, target);
|
|
179
|
+
if (delegated !== undefined) {
|
|
180
|
+
return delegated.exitCode;
|
|
181
|
+
}
|
|
182
|
+
target = { kind: 'local' };
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
const planned = await planRun(invocation, target);
|
|
185
|
+
const { exitCode, isUnknownCommand = false } = planned.settled ? planned.result : await executePlan(planned.value);
|
|
186
|
+
if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(options.env)) {
|
|
187
|
+
const hint = formatMisplacedFlagHint(options.args, invocation.parsedArgs.passthrough, invocation.styles.stderr);
|
|
188
|
+
if (hint !== undefined) {
|
|
189
|
+
options.stderr.write(`${hint}\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return exitCode;
|
|
193
193
|
}
|
package/dist/esm/runner.js
CHANGED
|
@@ -116,24 +116,10 @@ export async function runSteps(steps, cwd, options) {
|
|
|
116
116
|
export async function runCommand(command, cwd, options) {
|
|
117
117
|
return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
|
|
118
118
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const { quiet, stderr, stdout } = options;
|
|
124
|
-
if (step.kind === 'opaque') {
|
|
125
|
-
return startSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: true }, cwd, {
|
|
126
|
-
...options,
|
|
127
|
-
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
const [file, ...args] = step.argv;
|
|
131
|
-
const input = step.shouldWithholdInput === true ? 'ignore' : 'inherit';
|
|
132
|
-
return startSpawned({ args, file, input, shouldUseShell: false }, cwd, {
|
|
133
|
-
...options,
|
|
134
|
-
quiet: false,
|
|
135
|
-
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
136
|
-
});
|
|
119
|
+
function buildBackgroundEnv(options) {
|
|
120
|
+
const { env } = options;
|
|
121
|
+
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
122
|
+
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
137
123
|
}
|
|
138
124
|
function composeRetainedOutput(parts, isCaptureWhole) {
|
|
139
125
|
if (!isCaptureWhole || parts.length === 0) {
|
|
@@ -146,14 +132,12 @@ function composeRetainedOutput(parts, isCaptureWhole) {
|
|
|
146
132
|
},
|
|
147
133
|
};
|
|
148
134
|
}
|
|
149
|
-
function buildBackgroundEnv(options) {
|
|
150
|
-
const { env } = options;
|
|
151
|
-
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
152
|
-
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
153
|
-
}
|
|
154
135
|
async function recordResult(run, result) {
|
|
155
136
|
run.result = await result;
|
|
156
137
|
}
|
|
138
|
+
function runSpawned(spec, cwd, options) {
|
|
139
|
+
return startSpawned(spec, cwd, options).result;
|
|
140
|
+
}
|
|
157
141
|
function selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns) {
|
|
158
142
|
if (foregroundExitCode !== 0)
|
|
159
143
|
return foregroundExitCode;
|
|
@@ -174,3 +158,19 @@ function startBackgroundRun(step, options, launch) {
|
|
|
174
158
|
run.settled = recordResult(run, started.result);
|
|
175
159
|
return run;
|
|
176
160
|
}
|
|
161
|
+
function startStep(step, cwd, options) {
|
|
162
|
+
const { quiet, stderr, stdout } = options;
|
|
163
|
+
if (step.kind === 'opaque') {
|
|
164
|
+
return startSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: true }, cwd, {
|
|
165
|
+
...options,
|
|
166
|
+
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const [file, ...args] = step.argv;
|
|
170
|
+
const input = step.shouldWithholdInput === true ? 'ignore' : 'inherit';
|
|
171
|
+
return startSpawned({ args, file, input, shouldUseShell: false }, cwd, {
|
|
172
|
+
...options,
|
|
173
|
+
quiet: false,
|
|
174
|
+
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
175
|
+
});
|
|
176
|
+
}
|
package/dist/esm/verdict.js
CHANGED
|
@@ -21,6 +21,20 @@ const NEWLINE_BYTES = 1;
|
|
|
21
21
|
const LINE_BUDGET_BYTES = VERDICT_LINE_LIMIT_BYTES - NEWLINE_BYTES;
|
|
22
22
|
const MIN_CUT_BYTES = Buffer.byteLength(TRUNCATION_MARK);
|
|
23
23
|
const REPLAY_MARKER = 'replayed:';
|
|
24
|
+
function describeNoOpReason(reason) {
|
|
25
|
+
switch (reason) {
|
|
26
|
+
case 'empty-override':
|
|
27
|
+
return 'the override is empty';
|
|
28
|
+
case 'empty-workspace':
|
|
29
|
+
return 'the workspace does not declare any package';
|
|
30
|
+
case 'noop-override':
|
|
31
|
+
return 'the override is a no-op';
|
|
32
|
+
default: {
|
|
33
|
+
const unhandledReason = reason;
|
|
34
|
+
throw new Error(`Unhandled no-op reason: ${String(unhandledReason)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
24
38
|
function describeOutcome(verdict, style) {
|
|
25
39
|
const statuses = STATUS_GLYPHS[style];
|
|
26
40
|
const onHost = verdict.host === undefined ? '' : ` on ${formatHost(verdict.host, style)}`;
|
|
@@ -49,41 +63,6 @@ function describeOutcome(verdict, style) {
|
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
|
-
function describeNoOpReason(reason) {
|
|
53
|
-
switch (reason) {
|
|
54
|
-
case 'empty-override':
|
|
55
|
-
return 'the override is empty';
|
|
56
|
-
case 'empty-workspace':
|
|
57
|
-
return 'the workspace does not declare any package';
|
|
58
|
-
case 'noop-override':
|
|
59
|
-
return 'the override is a no-op';
|
|
60
|
-
default: {
|
|
61
|
-
const unhandledReason = reason;
|
|
62
|
-
throw new Error(`Unhandled no-op reason: ${String(unhandledReason)}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
function renderReplay(verdict) {
|
|
67
|
-
if (verdict.outcome !== 'recalled' || verdict.replay === undefined || verdict.replay.length === 0) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
const [first] = verdict.replay;
|
|
71
|
-
if (verdict.replay.length === 1 && first?.command === verdict.command && first.scope === verdict.scope) {
|
|
72
|
-
return `${REPLAY_MARKER} ${first.excerpt}`;
|
|
73
|
-
}
|
|
74
|
-
const lines = verdict.replay.map((line) => `${line.scope}: ${line.command}: ${line.excerpt}`);
|
|
75
|
-
return `${REPLAY_MARKER} ${lines.join('; ')}`;
|
|
76
|
-
}
|
|
77
|
-
function renderStated(verdict) {
|
|
78
|
-
if (verdict.outcome !== 'passed' || verdict.stated === undefined) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
const saving = formatSaving(verdict.stated.savedMs ?? 0);
|
|
82
|
-
return saving === undefined ? 'build skipped as unchanged' : `build skipped as unchanged, ${saving}`;
|
|
83
|
-
}
|
|
84
|
-
function flattenDetail(detail) {
|
|
85
|
-
return detail.replaceAll(/[\r\n]+/gu, ' ').trim();
|
|
86
|
-
}
|
|
87
66
|
function findCutTarget(sizesBytes, longestSizeBytes, overrunBytes) {
|
|
88
67
|
const smallerSizesBytes = sizesBytes.filter((size) => size < longestSizeBytes && size > MIN_CUT_BYTES);
|
|
89
68
|
const share = Math.ceil(overrunBytes / sizesBytes.filter((size) => size === longestSizeBytes).length);
|
|
@@ -92,13 +71,16 @@ function findCutTarget(sizesBytes, longestSizeBytes, overrunBytes) {
|
|
|
92
71
|
: Math.max(...smallerSizesBytes, longestSizeBytes - overrunBytes);
|
|
93
72
|
return Math.max(MIN_CUT_BYTES, target);
|
|
94
73
|
}
|
|
95
|
-
function
|
|
96
|
-
return
|
|
74
|
+
function flattenDetail(detail) {
|
|
75
|
+
return detail.replaceAll(/[\r\n]+/gu, ' ').trim();
|
|
97
76
|
}
|
|
98
77
|
function formatHost(host, style) {
|
|
99
78
|
const glyph = NMR_GLYPHS[style].host.text;
|
|
100
79
|
return glyph === '' ? host : `${glyph} ${host}`;
|
|
101
80
|
}
|
|
81
|
+
function isWithinBudget(renderedLine) {
|
|
82
|
+
return Buffer.byteLength(renderedLine) <= LINE_BUDGET_BYTES;
|
|
83
|
+
}
|
|
102
84
|
function readAttribution(verdict) {
|
|
103
85
|
if (verdict.outcome !== 'recalled' || verdict.replay === undefined) {
|
|
104
86
|
return [];
|
|
@@ -126,6 +108,24 @@ function renderClamped(record) {
|
|
|
126
108
|
}
|
|
127
109
|
return renderedLine;
|
|
128
110
|
}
|
|
111
|
+
function renderReplay(verdict) {
|
|
112
|
+
if (verdict.outcome !== 'recalled' || verdict.replay === undefined || verdict.replay.length === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const [first] = verdict.replay;
|
|
116
|
+
if (verdict.replay.length === 1 && first?.command === verdict.command && first.scope === verdict.scope) {
|
|
117
|
+
return `${REPLAY_MARKER} ${first.excerpt}`;
|
|
118
|
+
}
|
|
119
|
+
const lines = verdict.replay.map((line) => `${line.scope}: ${line.command}: ${line.excerpt}`);
|
|
120
|
+
return `${REPLAY_MARKER} ${lines.join('; ')}`;
|
|
121
|
+
}
|
|
122
|
+
function renderStated(verdict) {
|
|
123
|
+
if (verdict.outcome !== 'passed' || verdict.stated === undefined) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const saving = formatSaving(verdict.stated.savedMs ?? 0);
|
|
127
|
+
return saving === undefined ? 'build skipped as unchanged' : `build skipped as unchanged, ${saving}`;
|
|
128
|
+
}
|
|
129
129
|
function renderWithinBudget(verdict) {
|
|
130
130
|
const shortenedVerdict = shortenCuttableText(verdict);
|
|
131
131
|
const shortenedLine = JSON.stringify(shortenedVerdict);
|