@williamthorsen/nmr 0.47.0 → 0.49.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 +206 -0
- package/.readyup/kits/default.js +1534 -1466
- package/.readyup/manifest.json +142 -12
- package/CHANGELOG.md +55 -0
- package/README.md +44 -6
- package/agents/guidance/rulebooks/nmr.md +2 -1
- package/dist/esm/bin-path.js +4 -3
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +178 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +10 -0
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- 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/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +31 -0
- package/dist/esm/remote-probe.js +78 -0
- package/dist/esm/remote-run.d.ts +91 -0
- package/dist/esm/remote-run.js +189 -0
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +41 -895
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +3 -1
- package/dist/esm/verdict.js +4 -3
- package/dist/esm/vitest-source-resolution.js +2 -3
- package/docs/check-cache.md +30 -14
- package/docs/delegate.md +141 -0
- package/docs/machine-lock.md +2 -1
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +5 -5
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
package/dist/esm/runner.d.ts
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RunCommandOptions, type RunCommandResult } from './child-process.js';
|
|
2
2
|
import type { Step } from './steps.js';
|
|
3
|
-
/** Where one of a child's output streams goes: a descriptor to which the child writes, or a pipe read by nmr. */
|
|
4
|
-
export type OutputChannel = number | 'pipe';
|
|
5
|
-
/** The channel on which each of a child's output streams runs. */
|
|
6
|
-
export interface OutputChannels {
|
|
7
|
-
stderr: OutputChannel;
|
|
8
|
-
stdout: OutputChannel;
|
|
9
|
-
}
|
|
10
|
-
export interface RunCommandOptions {
|
|
11
|
-
/** The channel on which each of the child's output streams runs. */
|
|
12
|
-
channels: OutputChannels;
|
|
13
|
-
/** When true, suppresses output on success and writes captured output to stderr on failure. */
|
|
14
|
-
quiet?: boolean;
|
|
15
|
-
/** Stream that subprocess stdout flows to in non-quiet mode. Defaults to `process.stdout`. */
|
|
16
|
-
stdout?: Writable;
|
|
17
|
-
/** Stream that subprocess stderr flows to (and that quiet-mode failure output is written to). Defaults to `process.stderr`. */
|
|
18
|
-
stderr?: Writable;
|
|
19
|
-
/** Environment for the subprocess. Defaults to `process.env`. */
|
|
20
|
-
env?: NodeJS.ProcessEnv;
|
|
21
|
-
}
|
|
22
3
|
/** What a step list needs to run. Each step's channels follow its kind, so the caller chooses none of them. */
|
|
23
4
|
export type RunStepsOptions = Omit<RunCommandOptions, 'channels'>;
|
|
24
5
|
/** A bounded copy of what a step list's own commands wrote, each stream kept apart from the other. */
|
|
@@ -34,39 +15,6 @@ export interface RunStepsResult {
|
|
|
34
15
|
*/
|
|
35
16
|
retainedOutput?: RetainedOutput;
|
|
36
17
|
}
|
|
37
|
-
/** How a command ended, and the exit code that reports that ending to nmr's own caller. */
|
|
38
|
-
type CommandCompletion = {
|
|
39
|
-
outcome: 'exited';
|
|
40
|
-
exitCode: number;
|
|
41
|
-
} | {
|
|
42
|
-
outcome: 'signaled';
|
|
43
|
-
exitCode: number;
|
|
44
|
-
signal: NodeJS.Signals;
|
|
45
|
-
} | {
|
|
46
|
-
outcome: 'spawn-failed';
|
|
47
|
-
exitCode: number;
|
|
48
|
-
error: Error;
|
|
49
|
-
};
|
|
50
|
-
export type RunCommandResult = CommandCompletion & {
|
|
51
|
-
/**
|
|
52
|
-
* Output retained while the command ran, elided in the middle when it overran the bound.
|
|
53
|
-
* `undefined` when the stream was handed to the child directly, or when capture stopped early
|
|
54
|
-
* and the retained copy would understate what the command produced.
|
|
55
|
-
*/
|
|
56
|
-
stdout: Buffer | undefined;
|
|
57
|
-
stderr: Buffer | undefined;
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Returns the channel on which a stream runs: the stream's own descriptor when it is a terminal and output is
|
|
61
|
-
* not being withheld, so that the child's terminal detection matches nmr's, and a pipe everywhere else.
|
|
62
|
-
*/
|
|
63
|
-
export declare function resolveChannel(stream: Writable, quiet: boolean): OutputChannel;
|
|
64
|
-
/**
|
|
65
|
-
* Returns the channel for a step that runs an nmr process below this one: nmr's own descriptor whenever it has
|
|
66
|
-
* one, so that the child writes where nmr writes, without an ancestor between them relaying bytes. A stream
|
|
67
|
-
* without a descriptor falls back to a pipe.
|
|
68
|
-
*/
|
|
69
|
-
export declare function resolveInheritedChannel(stream: Writable): OutputChannel;
|
|
70
18
|
/**
|
|
71
19
|
* Runs a step list in order and resolves once one fails or all have run, implementing the `&&` semantics that
|
|
72
20
|
* nmr promises: The first non-zero exit ends the sequence and is the code returned.
|
|
@@ -100,4 +48,3 @@ export declare function runSteps(steps: readonly Step[], cwd: string | undefined
|
|
|
100
48
|
* is approximate.
|
|
101
49
|
*/
|
|
102
50
|
export declare function runCommand(command: string, cwd: string | undefined, options: RunCommandOptions): Promise<RunCommandResult>;
|
|
103
|
-
export {};
|
package/dist/esm/runner.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import os from 'node:os';
|
|
3
1
|
import process from 'node:process';
|
|
4
|
-
import {
|
|
5
|
-
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
2
|
+
import { describeSignalExitCode, startSpawned, } from "./child-process.js";
|
|
6
3
|
import { createOutputRecorder } from "./helpers/createOutputRecorder.js";
|
|
7
|
-
|
|
8
|
-
const PIPE_DRAIN_GRACE_MS = 2_000;
|
|
9
|
-
export function resolveChannel(stream, quiet) {
|
|
10
|
-
if (quiet)
|
|
11
|
-
return 'pipe';
|
|
12
|
-
return isTerminal(stream) ? stream.fd : 'pipe';
|
|
13
|
-
}
|
|
14
|
-
export function resolveInheritedChannel(stream) {
|
|
15
|
-
return hasDescriptor(stream) ? stream.fd : 'pipe';
|
|
16
|
-
}
|
|
4
|
+
import { isTerminal, resolveChannel, resolveInheritedChannel } from "./output-channel.js";
|
|
17
5
|
export async function runSteps(steps, cwd, options) {
|
|
18
6
|
const resolvedOptions = {
|
|
19
7
|
...options,
|
|
@@ -97,43 +85,6 @@ export async function runCommand(command, cwd, options) {
|
|
|
97
85
|
function runSpawned(spec, cwd, options) {
|
|
98
86
|
return startSpawned(spec, cwd, options).result;
|
|
99
87
|
}
|
|
100
|
-
function startSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
101
|
-
const quiet = options.quiet === true;
|
|
102
|
-
const stdout = options.stdout ?? process.stdout;
|
|
103
|
-
const stderr = options.stderr ?? process.stderr;
|
|
104
|
-
const env = options.env ?? process.env;
|
|
105
|
-
const child = spawn(file, args, {
|
|
106
|
-
shell: shouldUseShell,
|
|
107
|
-
stdio: [input, options.channels.stdout, options.channels.stderr],
|
|
108
|
-
cwd,
|
|
109
|
-
env,
|
|
110
|
-
});
|
|
111
|
-
return { child, result: collectResult(child, { quiet, stderr, stdout }) };
|
|
112
|
-
}
|
|
113
|
-
async function collectResult(child, { quiet, stderr, stdout }) {
|
|
114
|
-
const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
|
|
115
|
-
const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
|
|
116
|
-
const commandCompletion = await awaitCompletion(child, () => {
|
|
117
|
-
stdoutCapture?.stopReading();
|
|
118
|
-
stderrCapture?.stopReading();
|
|
119
|
-
});
|
|
120
|
-
stdoutCapture?.detach();
|
|
121
|
-
stderrCapture?.detach();
|
|
122
|
-
const result = {
|
|
123
|
-
...commandCompletion,
|
|
124
|
-
stdout: stdoutCapture?.readBuffer(),
|
|
125
|
-
stderr: stderrCapture?.readBuffer(),
|
|
126
|
-
};
|
|
127
|
-
if (result.outcome === 'spawn-failed') {
|
|
128
|
-
reportError(result.error.message, stderr);
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
if (quiet && result.exitCode !== 0) {
|
|
132
|
-
writeBuffer(result.stdout, stderr);
|
|
133
|
-
writeBuffer(result.stderr, stderr);
|
|
134
|
-
}
|
|
135
|
-
return result;
|
|
136
|
-
}
|
|
137
88
|
function startStep(step, cwd, options) {
|
|
138
89
|
const { quiet, stderr, stdout } = options;
|
|
139
90
|
if (step.kind === 'opaque') {
|
|
@@ -161,81 +112,11 @@ function composeRetainedOutput(parts, isCaptureWhole) {
|
|
|
161
112
|
},
|
|
162
113
|
};
|
|
163
114
|
}
|
|
164
|
-
function captureStream(source, destination) {
|
|
165
|
-
const retainedCopy = createBoundedBuffer();
|
|
166
|
-
let isComplete = true;
|
|
167
|
-
function stopReading() {
|
|
168
|
-
if (destination !== undefined)
|
|
169
|
-
source.unpipe(destination);
|
|
170
|
-
source.destroy();
|
|
171
|
-
}
|
|
172
|
-
function abandon() {
|
|
173
|
-
isComplete = false;
|
|
174
|
-
stopReading();
|
|
175
|
-
}
|
|
176
|
-
source.on('error', abandon);
|
|
177
|
-
if (destination !== undefined) {
|
|
178
|
-
destination.on('error', abandon);
|
|
179
|
-
source.pipe(destination, { end: false });
|
|
180
|
-
}
|
|
181
|
-
source.on('data', (chunk) => retainedCopy.append(chunk));
|
|
182
|
-
return {
|
|
183
|
-
readBuffer: () => (isComplete ? retainedCopy.readBuffer() : undefined),
|
|
184
|
-
detach: () => {
|
|
185
|
-
destination?.off('error', abandon);
|
|
186
|
-
},
|
|
187
|
-
stopReading,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
function awaitCompletion(child, stopReading) {
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
let exitCompletion;
|
|
193
|
-
let graceTimer;
|
|
194
|
-
let isSettled = false;
|
|
195
|
-
function settle(completion) {
|
|
196
|
-
if (isSettled)
|
|
197
|
-
return;
|
|
198
|
-
isSettled = true;
|
|
199
|
-
if (graceTimer !== undefined)
|
|
200
|
-
clearTimeout(graceTimer);
|
|
201
|
-
resolve(completion);
|
|
202
|
-
}
|
|
203
|
-
child.on('error', (error) => {
|
|
204
|
-
settle({ outcome: 'spawn-failed', exitCode: 1, error });
|
|
205
|
-
});
|
|
206
|
-
child.on('exit', (code, signal) => {
|
|
207
|
-
const exitDescription = describeExit(code, signal);
|
|
208
|
-
exitCompletion = exitDescription;
|
|
209
|
-
graceTimer = setTimeout(() => {
|
|
210
|
-
stopReading();
|
|
211
|
-
settle(exitDescription);
|
|
212
|
-
}, PIPE_DRAIN_GRACE_MS);
|
|
213
|
-
});
|
|
214
|
-
child.on('close', () => {
|
|
215
|
-
settle(exitCompletion ?? { outcome: 'exited', exitCode: 1 });
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
115
|
function buildBackgroundEnv(options) {
|
|
220
116
|
const env = options.env ?? process.env;
|
|
221
117
|
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
222
118
|
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
223
119
|
}
|
|
224
|
-
function describeExit(code, signal) {
|
|
225
|
-
if (signal !== null) {
|
|
226
|
-
return { outcome: 'signaled', exitCode: describeSignalExitCode(signal), signal };
|
|
227
|
-
}
|
|
228
|
-
return { outcome: 'exited', exitCode: code ?? 1 };
|
|
229
|
-
}
|
|
230
|
-
function describeSignalExitCode(signal) {
|
|
231
|
-
return SIGNAL_EXIT_BASE + os.constants.signals[signal];
|
|
232
|
-
}
|
|
233
|
-
function hasDescriptor(stream) {
|
|
234
|
-
return 'fd' in stream && typeof stream.fd === 'number';
|
|
235
|
-
}
|
|
236
|
-
function isTerminal(stream) {
|
|
237
|
-
return 'isTTY' in stream && stream.isTTY === true && hasDescriptor(stream);
|
|
238
|
-
}
|
|
239
120
|
async function recordResult(run, result) {
|
|
240
121
|
run.result = await result;
|
|
241
122
|
}
|
|
@@ -259,7 +140,3 @@ function startBackgroundRun(step, options, launch) {
|
|
|
259
140
|
run.settled = recordResult(run, started.result);
|
|
260
141
|
return run;
|
|
261
142
|
}
|
|
262
|
-
function writeBuffer(buffer, destination) {
|
|
263
|
-
if (buffer !== undefined && buffer.length > 0)
|
|
264
|
-
destination.write(buffer);
|
|
265
|
-
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Drops the leading `NAME=value` assignments, leaving the program name at the head. */
|
|
2
|
+
export declare function dropLeadingAssignments(tokens: readonly string[]): readonly string[];
|
|
3
|
+
/** Reports whether a POSIX shell reads the token literally, so that it does not need any quoting. */
|
|
4
|
+
export declare function isShellSafeToken(token: string): boolean;
|
|
5
|
+
/** Quotes a token that the shell would not read literally, and leaves every other token bare. */
|
|
6
|
+
export declare function quoteToken(token: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Splits a command into the segments that a shell would run as separate commands, breaking on `&&`, `||`, `;`,
|
|
9
|
+
* `|`, and a newline, outside quotes.
|
|
10
|
+
*
|
|
11
|
+
* Quote and escape state are tracked character by character rather than by matching tokens, so a separator
|
|
12
|
+
* standing inside an argument stays part of the segment containing it. A backslash escapes outside quotes and
|
|
13
|
+
* inside a double-quoted run; inside a single-quoted run the shell reads it literally, and so does this.
|
|
14
|
+
* `tokenizeSegment` tracks the same state on the same rules.
|
|
15
|
+
*
|
|
16
|
+
* A separator standing inside a redirection operator does not end a command: `nmr build 2>&1` is one segment.
|
|
17
|
+
*/
|
|
18
|
+
export declare function splitSegments(command: string): string[];
|
|
19
|
+
/** Splits a composite element into argv tokens, dropping the empty tokens that surrounding whitespace would leave. */
|
|
20
|
+
export declare function tokenize(element: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Splits a shell segment into the tokens that a shell reads, keeping a quoted run attached to the token
|
|
23
|
+
* containing it, so that an environment assignment containing a quoted space stays one token.
|
|
24
|
+
*
|
|
25
|
+
* Tracks quote and escape state as `splitSegments` does, the two sharing one set of rules.
|
|
26
|
+
*
|
|
27
|
+
* Keeps the quotes rather than stripping them: Both questions asked of a token, whether it assigns an
|
|
28
|
+
* environment variable and whether it names the program, read the same on the raw token.
|
|
29
|
+
*/
|
|
30
|
+
export declare function tokenizeSegment(segment: string): string[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const ENV_ASSIGNMENT = /^\w+=/;
|
|
2
|
+
const QUOTES = new Set(['"', "'"]);
|
|
3
|
+
const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
|
4
|
+
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
5
|
+
const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
|
|
6
|
+
export function dropLeadingAssignments(tokens) {
|
|
7
|
+
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
8
|
+
return start === -1 ? [] : tokens.slice(start);
|
|
9
|
+
}
|
|
10
|
+
export function isShellSafeToken(token) {
|
|
11
|
+
return SHELL_SAFE_TOKEN.test(token);
|
|
12
|
+
}
|
|
13
|
+
export function quoteToken(token) {
|
|
14
|
+
if (isShellSafeToken(token)) {
|
|
15
|
+
return token;
|
|
16
|
+
}
|
|
17
|
+
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
18
|
+
}
|
|
19
|
+
export function splitSegments(command) {
|
|
20
|
+
const segments = [];
|
|
21
|
+
let currentSegment = '';
|
|
22
|
+
let quote;
|
|
23
|
+
let index = 0;
|
|
24
|
+
while (index < command.length) {
|
|
25
|
+
const char = command[index] ?? '';
|
|
26
|
+
if (quote !== undefined) {
|
|
27
|
+
if (char === '\\' && quote === '"') {
|
|
28
|
+
currentSegment += char + (command[index + 1] ?? '');
|
|
29
|
+
index += 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
currentSegment += char;
|
|
33
|
+
if (char === quote)
|
|
34
|
+
quote = undefined;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '\\') {
|
|
39
|
+
currentSegment += char + (command[index + 1] ?? '');
|
|
40
|
+
index += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (QUOTES.has(char)) {
|
|
44
|
+
quote = char;
|
|
45
|
+
currentSegment += char;
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (SEGMENT_SEPARATORS.has(char)) {
|
|
50
|
+
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
51
|
+
currentSegment += char;
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
index += command[index + 1] === char ? 2 : 1;
|
|
56
|
+
segments.push(currentSegment);
|
|
57
|
+
currentSegment = '';
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
currentSegment += char;
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
segments.push(currentSegment);
|
|
64
|
+
return segments;
|
|
65
|
+
}
|
|
66
|
+
export function tokenize(element) {
|
|
67
|
+
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
68
|
+
}
|
|
69
|
+
export function tokenizeSegment(segment) {
|
|
70
|
+
const tokens = [];
|
|
71
|
+
let currentToken = '';
|
|
72
|
+
let quote;
|
|
73
|
+
let index = 0;
|
|
74
|
+
while (index < segment.length) {
|
|
75
|
+
const char = segment[index] ?? '';
|
|
76
|
+
if (quote !== undefined) {
|
|
77
|
+
if (char === '\\' && quote === '"') {
|
|
78
|
+
currentToken += char + (segment[index + 1] ?? '');
|
|
79
|
+
index += 2;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
currentToken += char;
|
|
83
|
+
if (char === quote)
|
|
84
|
+
quote = undefined;
|
|
85
|
+
index += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (char === '\\') {
|
|
89
|
+
currentToken += char + (segment[index + 1] ?? '');
|
|
90
|
+
index += 2;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (QUOTES.has(char)) {
|
|
94
|
+
quote = char;
|
|
95
|
+
currentToken += char;
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (TOKEN_SEPARATORS.has(char)) {
|
|
100
|
+
if (currentToken !== '') {
|
|
101
|
+
tokens.push(currentToken);
|
|
102
|
+
}
|
|
103
|
+
currentToken = '';
|
|
104
|
+
index += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
currentToken += char;
|
|
108
|
+
index += 1;
|
|
109
|
+
}
|
|
110
|
+
if (currentToken !== '') {
|
|
111
|
+
tokens.push(currentToken);
|
|
112
|
+
}
|
|
113
|
+
return tokens;
|
|
114
|
+
}
|
|
115
|
+
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
116
|
+
if (char !== '&' && char !== '|') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
|
|
120
|
+
}
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -90,8 +90,6 @@ export declare function findNmrCrossing(steps: readonly Step[]): string | undefi
|
|
|
90
90
|
* literally would be quoted whole by the rendering, turning the element into a command that nobody wrote.
|
|
91
91
|
*/
|
|
92
92
|
export declare function findUnexpressibleToken(element: string): string | undefined;
|
|
93
|
-
/** Quotes a token that the shell would not read literally, and leaves every other token bare. */
|
|
94
|
-
export declare function quoteToken(token: string): string;
|
|
95
93
|
/**
|
|
96
94
|
* Returns the command that a structural step's nmr process runs, whether the step hands that command to other
|
|
97
95
|
* scopes rather than running it in its own scope, and whether it anchors the command at the monorepo root.
|
package/dist/esm/steps.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { dropLeadingAssignments, isShellSafeToken, quoteToken, splitSegments, tokenize, tokenizeSegment, } from "./shell-syntax.js";
|
|
2
2
|
const LAUNCHERS = new Map([
|
|
3
3
|
['bun', new Set(['x'])],
|
|
4
4
|
['bunx', new Set()],
|
|
@@ -7,10 +7,6 @@ const LAUNCHERS = new Map([
|
|
|
7
7
|
['pnpm', new Set(['dlx', 'exec'])],
|
|
8
8
|
['yarn', new Set(['dlx', 'exec'])],
|
|
9
9
|
]);
|
|
10
|
-
const QUOTES = new Set(['"', "'"]);
|
|
11
|
-
const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
|
12
|
-
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
13
|
-
const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
|
|
14
10
|
export function composeNmrStep(element, isWorkspaceRoot, flags = {}) {
|
|
15
11
|
const { shouldDeclineArguments = false, shouldRunInBackground = false } = flags;
|
|
16
12
|
const nmrFlags = isWorkspaceRoot ? ['-w'] : [];
|
|
@@ -33,13 +29,7 @@ export function findNmrCrossing(steps) {
|
|
|
33
29
|
return undefined;
|
|
34
30
|
}
|
|
35
31
|
export function findUnexpressibleToken(element) {
|
|
36
|
-
return tokenize(element).find((token) => !
|
|
37
|
-
}
|
|
38
|
-
export function quoteToken(token) {
|
|
39
|
-
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
40
|
-
return token;
|
|
41
|
-
}
|
|
42
|
-
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
32
|
+
return tokenize(element).find((token) => !isShellSafeToken(token));
|
|
43
33
|
}
|
|
44
34
|
export function readNmrStep(step) {
|
|
45
35
|
if (step.kind !== 'structural') {
|
|
@@ -80,7 +70,7 @@ export function readSoleNmrCommand(script) {
|
|
|
80
70
|
if (program !== 'nmr' || command === undefined || rest.length > 0) {
|
|
81
71
|
return undefined;
|
|
82
72
|
}
|
|
83
|
-
return
|
|
73
|
+
return isShellSafeToken(command) && !command.startsWith('-') ? command : undefined;
|
|
84
74
|
}
|
|
85
75
|
export function readStepListSelfReference(options) {
|
|
86
76
|
const { commandName, elements, scope } = options;
|
|
@@ -96,16 +86,6 @@ export function readStepListSelfReference(options) {
|
|
|
96
86
|
export function renderChain(steps) {
|
|
97
87
|
return steps.map(renderStep).join(' && ');
|
|
98
88
|
}
|
|
99
|
-
function dropLeadingAssignments(tokens) {
|
|
100
|
-
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
101
|
-
return start === -1 ? [] : tokens.slice(start);
|
|
102
|
-
}
|
|
103
|
-
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
104
|
-
if (char !== '&' && char !== '|') {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
|
|
108
|
-
}
|
|
109
89
|
function isReentry(target, commandName, scope) {
|
|
110
90
|
if (target.command !== commandName) {
|
|
111
91
|
return false;
|
|
@@ -196,99 +176,3 @@ function readNmrTarget(tokens) {
|
|
|
196
176
|
function renderStep(step) {
|
|
197
177
|
return step.kind === 'opaque' ? step.command : step.argv.map(quoteToken).join(' ');
|
|
198
178
|
}
|
|
199
|
-
function splitSegments(command) {
|
|
200
|
-
const segments = [];
|
|
201
|
-
let currentSegment = '';
|
|
202
|
-
let quote;
|
|
203
|
-
let index = 0;
|
|
204
|
-
while (index < command.length) {
|
|
205
|
-
const char = command[index] ?? '';
|
|
206
|
-
if (quote !== undefined) {
|
|
207
|
-
if (char === '\\' && quote === '"') {
|
|
208
|
-
currentSegment += char + (command[index + 1] ?? '');
|
|
209
|
-
index += 2;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
currentSegment += char;
|
|
213
|
-
if (char === quote)
|
|
214
|
-
quote = undefined;
|
|
215
|
-
index += 1;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (char === '\\') {
|
|
219
|
-
currentSegment += char + (command[index + 1] ?? '');
|
|
220
|
-
index += 2;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (QUOTES.has(char)) {
|
|
224
|
-
quote = char;
|
|
225
|
-
currentSegment += char;
|
|
226
|
-
index += 1;
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (SEGMENT_SEPARATORS.has(char)) {
|
|
230
|
-
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
231
|
-
currentSegment += char;
|
|
232
|
-
index += 1;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
index += command[index + 1] === char ? 2 : 1;
|
|
236
|
-
segments.push(currentSegment);
|
|
237
|
-
currentSegment = '';
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
currentSegment += char;
|
|
241
|
-
index += 1;
|
|
242
|
-
}
|
|
243
|
-
segments.push(currentSegment);
|
|
244
|
-
return segments;
|
|
245
|
-
}
|
|
246
|
-
function tokenize(element) {
|
|
247
|
-
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
248
|
-
}
|
|
249
|
-
function tokenizeSegment(segment) {
|
|
250
|
-
const tokens = [];
|
|
251
|
-
let currentToken = '';
|
|
252
|
-
let quote;
|
|
253
|
-
let index = 0;
|
|
254
|
-
while (index < segment.length) {
|
|
255
|
-
const char = segment[index] ?? '';
|
|
256
|
-
if (quote !== undefined) {
|
|
257
|
-
if (char === '\\' && quote === '"') {
|
|
258
|
-
currentToken += char + (segment[index + 1] ?? '');
|
|
259
|
-
index += 2;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
currentToken += char;
|
|
263
|
-
if (char === quote)
|
|
264
|
-
quote = undefined;
|
|
265
|
-
index += 1;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (char === '\\') {
|
|
269
|
-
currentToken += char + (segment[index + 1] ?? '');
|
|
270
|
-
index += 2;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (QUOTES.has(char)) {
|
|
274
|
-
quote = char;
|
|
275
|
-
currentToken += char;
|
|
276
|
-
index += 1;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (TOKEN_SEPARATORS.has(char)) {
|
|
280
|
-
if (currentToken !== '') {
|
|
281
|
-
tokens.push(currentToken);
|
|
282
|
-
}
|
|
283
|
-
currentToken = '';
|
|
284
|
-
index += 1;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
currentToken += char;
|
|
288
|
-
index += 1;
|
|
289
|
-
}
|
|
290
|
-
if (currentToken !== '') {
|
|
291
|
-
tokens.push(currentToken);
|
|
292
|
-
}
|
|
293
|
-
return tokens;
|
|
294
|
-
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface OutputConfig {
|
|
|
54
54
|
export interface NmrConfig {
|
|
55
55
|
build?: BuildConfig;
|
|
56
56
|
checkCache?: CheckCacheConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Set to `false` to keep every check on the machine that has the tree, whatever `NMR_DELEGATE` or the user
|
|
59
|
+
* config says. A delegation flag still sends a run elsewhere.
|
|
60
|
+
*/
|
|
61
|
+
delegate?: false;
|
|
57
62
|
devBin?: Record<string, string>;
|
|
58
63
|
output?: OutputConfig;
|
|
59
64
|
workspaceScripts?: Record<string, ScriptValue>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HostList } from './remote-run.js';
|
|
2
|
+
/** The values that `delegate.when` accepts, each naming the runs that delegate without being asked. */
|
|
3
|
+
export declare const DELEGATE_WHEN_VALUES: readonly ["agent", "always"];
|
|
4
|
+
export type DelegateWhen = (typeof DELEGATE_WHEN_VALUES)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Renders a path for a message, with `~` standing for the home directory that `env` names when the path lies under
|
|
7
|
+
* it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatDisplayPath(filePath: string, env: NodeJS.ProcessEnv): string;
|
|
10
|
+
/** A user config, with the path from which it was loaded. */
|
|
11
|
+
export interface LoadedUserConfig {
|
|
12
|
+
config: UserConfig;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Loads and validates the user config that `env` locates, or returns `undefined` when `env` locates none or the file
|
|
17
|
+
* does not exist.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadUserConfig(env: NodeJS.ProcessEnv): Promise<LoadedUserConfig | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the user config's path, resolved from `env` alone so that a caller's environment decides which file is
|
|
22
|
+
* read: under `XDG_CONFIG_HOME` when that is an absolute path, as the XDG base-directory spec requires, otherwise
|
|
23
|
+
* under `HOME`'s `.config`. Returns `undefined` when `env` names neither.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveUserConfigPath(env: NodeJS.ProcessEnv): string | undefined;
|
|
26
|
+
/** The machine-level settings that `$XDG_CONFIG_HOME/nmr/config.ts` declares. */
|
|
27
|
+
export interface UserConfig {
|
|
28
|
+
delegate?: UserDelegateConfig;
|
|
29
|
+
}
|
|
30
|
+
/** This machine's delegation hosts, and which runs delegate to them without being asked. */
|
|
31
|
+
export interface UserDelegateConfig {
|
|
32
|
+
hosts: HostList;
|
|
33
|
+
when?: DelegateWhen;
|
|
34
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { assertRecognizedKeys } from "./config-validation.js";
|
|
13
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
14
|
+
import { UserError } from "./UserError.js";
|
|
15
|
+
export const DELEGATE_WHEN_VALUES = ['agent', 'always'];
|
|
16
|
+
export function formatDisplayPath(filePath, env) {
|
|
17
|
+
const home = env['HOME'];
|
|
18
|
+
if (home === undefined || !path.isAbsolute(home)) {
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
const relativePath = path.relative(home, filePath);
|
|
22
|
+
if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
23
|
+
return filePath;
|
|
24
|
+
}
|
|
25
|
+
return path.join('~', relativePath);
|
|
26
|
+
}
|
|
27
|
+
export async function loadUserConfig(env) {
|
|
28
|
+
const configPath = resolveUserConfigPath(env);
|
|
29
|
+
if (configPath === undefined || !existsSync(configPath)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
33
|
+
const loadedConfig = isObject(importedModule) ? importedModule['default'] : undefined;
|
|
34
|
+
return { config: validateUserConfig(loadedConfig, configPath), path: configPath };
|
|
35
|
+
}
|
|
36
|
+
export function resolveUserConfigPath(env) {
|
|
37
|
+
const configHome = env['XDG_CONFIG_HOME'];
|
|
38
|
+
if (configHome !== undefined && path.isAbsolute(configHome)) {
|
|
39
|
+
return path.join(configHome, USER_CONFIG_RELATIVE_PATH);
|
|
40
|
+
}
|
|
41
|
+
const home = env['HOME'];
|
|
42
|
+
if (home !== undefined && path.isAbsolute(home)) {
|
|
43
|
+
return path.join(home, '.config', USER_CONFIG_RELATIVE_PATH);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const USER_CONFIG_RELATIVE_PATH = path.join('nmr', 'config.ts');
|
|
48
|
+
const RECOGNIZED_KEYS = ['delegate'];
|
|
49
|
+
const RECOGNIZED_DELEGATE_KEYS = ['hosts', 'when'];
|
|
50
|
+
function isDelegateWhen(value) {
|
|
51
|
+
const values = DELEGATE_WHEN_VALUES;
|
|
52
|
+
return values.includes(value);
|
|
53
|
+
}
|
|
54
|
+
function validateDelegateField(value, configPath) {
|
|
55
|
+
const delegate = value['delegate'];
|
|
56
|
+
if (delegate === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!isObject(delegate)) {
|
|
60
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` must be an object`);
|
|
61
|
+
}
|
|
62
|
+
assertRecognizedKeys(delegate, RECOGNIZED_DELEGATE_KEYS, configPath, 'delegate.');
|
|
63
|
+
const hosts = validateHosts(delegate['hosts'], configPath);
|
|
64
|
+
const when = delegate['when'];
|
|
65
|
+
if (when === undefined) {
|
|
66
|
+
return { hosts };
|
|
67
|
+
}
|
|
68
|
+
if (!isDelegateWhen(when)) {
|
|
69
|
+
const renderedValue = typeof when === 'string' ? when : JSON.stringify(when);
|
|
70
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate.when\` is \`${renderedValue}\`, which is not one of: ` +
|
|
71
|
+
DELEGATE_WHEN_VALUES.join(', '));
|
|
72
|
+
}
|
|
73
|
+
return { hosts, when };
|
|
74
|
+
}
|
|
75
|
+
function validateHosts(value, configPath) {
|
|
76
|
+
const names = Array.isArray(value) && value.every((host) => typeof host === 'string')
|
|
77
|
+
? value.map((host) => host.trim())
|
|
78
|
+
: [];
|
|
79
|
+
const [first, ...rest] = new Set(names);
|
|
80
|
+
if (first === undefined || names.includes('')) {
|
|
81
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate.hosts\` must be a non-empty array of host names, ` +
|
|
82
|
+
'in order of preference');
|
|
83
|
+
}
|
|
84
|
+
return [first, ...rest];
|
|
85
|
+
}
|
|
86
|
+
function validateUserConfig(value, configPath) {
|
|
87
|
+
if (!isObject(value)) {
|
|
88
|
+
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
89
|
+
}
|
|
90
|
+
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
91
|
+
const delegate = validateDelegateField(value, configPath);
|
|
92
|
+
return delegate === undefined ? {} : { delegate };
|
|
93
|
+
}
|