@optique/testing 1.3.0-dev.2497 → 1.3.0-dev.2506
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/README.md +21 -3
- package/dist/cli.cjs +551 -0
- package/dist/cli.d.cts +131 -1
- package/dist/cli.d.ts +131 -1
- package/dist/cli.js +549 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -70,9 +70,29 @@ The helper runs real command modules, hooks, and handlers. It captures
|
|
|
70
70
|
Optique's output callbacks; direct console or process-stream writes bypass
|
|
71
71
|
capture. Unexpected errors reject the promise.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Use `createCliRunner()` to capture the complete process, including direct
|
|
74
|
+
console writes and application exit codes:
|
|
75
|
+
|
|
76
|
+
~~~~ typescript
|
|
77
|
+
import { createCliRunner } from "@optique/testing/cli";
|
|
78
|
+
|
|
79
|
+
const cli = createCliRunner({
|
|
80
|
+
entrypoint: new URL("./cli.mjs", import.meta.url),
|
|
81
|
+
});
|
|
82
|
+
const result = await cli.invoke("--help");
|
|
83
|
+
console.log(result.exitCode, result.signal, result.stdout, result.stderr);
|
|
84
|
+
~~~~
|
|
85
|
+
|
|
86
|
+
The runner uses the current runtime. Set `runtimeArgs` for explicit runtime
|
|
87
|
+
flags, including Deno permissions, or use `command: ["node", "./cli.mjs"]`
|
|
88
|
+
for a specific executable. Calls accept stdin, cwd, environment overrides,
|
|
89
|
+
and cancellation. The default timeout is five seconds; `timeout: 0` disables
|
|
90
|
+
it. Nonzero exits return results; execution failures, timeouts, and
|
|
91
|
+
cancellation reject with `CliInvocationError`, including partial output.
|
|
92
|
+
See the [testing guide] for process-tree cleanup and runtime details.
|
|
74
93
|
|
|
75
94
|
[Optique]: https://optique.dev/
|
|
95
|
+
[testing guide]: https://optique.dev/concepts/testing
|
|
76
96
|
|
|
77
97
|
|
|
78
98
|
Installation
|
|
@@ -92,5 +112,3 @@ Documentation
|
|
|
92
112
|
|
|
93
113
|
For full documentation, visit the [testing guide], which explains what each
|
|
94
114
|
layer does and does not execute, and how to choose between them.
|
|
95
|
-
|
|
96
|
-
[testing guide]: https://optique.dev/concepts/testing
|
package/dist/cli.cjs
CHANGED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
+
const node_child_process = require_chunk.__toESM(require("node:child_process"));
|
|
3
|
+
const node_path = require_chunk.__toESM(require("node:path"));
|
|
4
|
+
const node_process = require_chunk.__toESM(require("node:process"));
|
|
5
|
+
const node_url = require_chunk.__toESM(require("node:url"));
|
|
6
|
+
const node_events = require_chunk.__toESM(require("node:events"));
|
|
7
|
+
const node_stream = require_chunk.__toESM(require("node:stream"));
|
|
8
|
+
|
|
9
|
+
//#region src/cli-process.ts
|
|
10
|
+
function isDenoRuntime(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && "Command" in value && typeof value.Command === "function";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Uses Deno's native asynchronous pipes when available.
|
|
15
|
+
* Kept separate so its real I/O can also be tested on non-Windows Deno hosts.
|
|
16
|
+
* @param command Executable path.
|
|
17
|
+
* @param args Literal command arguments.
|
|
18
|
+
* @param options Working directory and complete child environment.
|
|
19
|
+
* @returns A process adapter, or undefined outside Deno.
|
|
20
|
+
* @throws If Deno cannot start the requested executable.
|
|
21
|
+
*/
|
|
22
|
+
function spawnDenoProcess(command, args, options) {
|
|
23
|
+
const runtime = "Deno" in globalThis ? globalThis.Deno : void 0;
|
|
24
|
+
if (!isDenoRuntime(runtime)) return;
|
|
25
|
+
const native = new runtime.Command(command, {
|
|
26
|
+
args,
|
|
27
|
+
cwd: options.cwd,
|
|
28
|
+
env: options.env,
|
|
29
|
+
clearEnv: true,
|
|
30
|
+
stdin: "piped",
|
|
31
|
+
stdout: "piped",
|
|
32
|
+
stderr: "piped",
|
|
33
|
+
windowsHide: true
|
|
34
|
+
}).spawn();
|
|
35
|
+
const stdin = node_stream.Writable.fromWeb(native.stdin);
|
|
36
|
+
const stdout = node_stream.Readable.fromWeb(native.stdout);
|
|
37
|
+
const stderr = node_stream.Readable.fromWeb(native.stderr);
|
|
38
|
+
const child = Object.assign(new node_events.EventEmitter(), {
|
|
39
|
+
pid: native.pid,
|
|
40
|
+
stdin,
|
|
41
|
+
stdout,
|
|
42
|
+
stderr,
|
|
43
|
+
kill(signal = "SIGTERM") {
|
|
44
|
+
native.kill(signal);
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
unref() {
|
|
48
|
+
native.unref();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
let exited = false;
|
|
52
|
+
let closed = false;
|
|
53
|
+
let closedStreams = 0;
|
|
54
|
+
const maybeClose = () => {
|
|
55
|
+
if (exited && closedStreams === 3 && !closed) {
|
|
56
|
+
closed = true;
|
|
57
|
+
child.emit("close");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
for (const stream of [
|
|
61
|
+
stdin,
|
|
62
|
+
stdout,
|
|
63
|
+
stderr
|
|
64
|
+
]) stream.once("close", () => {
|
|
65
|
+
closedStreams++;
|
|
66
|
+
maybeClose();
|
|
67
|
+
});
|
|
68
|
+
queueMicrotask(() => child.emit("spawn"));
|
|
69
|
+
native.status.then((status) => {
|
|
70
|
+
exited = true;
|
|
71
|
+
child.emit("exit", status.signal === null ? status.code : null, status.signal);
|
|
72
|
+
stdin.destroy();
|
|
73
|
+
maybeClose();
|
|
74
|
+
}, (error) => {
|
|
75
|
+
exited = true;
|
|
76
|
+
child.emit("error", error);
|
|
77
|
+
maybeClose();
|
|
78
|
+
});
|
|
79
|
+
return child;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Launches a CLI with pipes that leave the host event loop responsive.
|
|
83
|
+
* @param command Executable path.
|
|
84
|
+
* @param args Literal command arguments.
|
|
85
|
+
* @param options Working directory, environment, and process group settings.
|
|
86
|
+
* @returns The launched process and its three streams.
|
|
87
|
+
* @throws If the runtime rejects the launch synchronously.
|
|
88
|
+
*/
|
|
89
|
+
function spawnCliProcess(command, args, options) {
|
|
90
|
+
if (node_process.default.platform === "win32") {
|
|
91
|
+
const native = spawnDenoProcess(command, args, options);
|
|
92
|
+
if (native !== void 0) return native;
|
|
93
|
+
}
|
|
94
|
+
return (0, node_child_process.spawn)(command, [...args], {
|
|
95
|
+
...options,
|
|
96
|
+
shell: false,
|
|
97
|
+
stdio: "pipe",
|
|
98
|
+
windowsHide: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/cli.ts
|
|
104
|
+
/**
|
|
105
|
+
* A failure of the invocation harness, with partial output and observed status.
|
|
106
|
+
* Cleanup failures retain the original failure and cleanup errors in an
|
|
107
|
+
* `AggregateError` cause. A status can be null when no exit was observed.
|
|
108
|
+
*
|
|
109
|
+
* @since 1.3.0
|
|
110
|
+
*/
|
|
111
|
+
var CliInvocationError = class extends Error {
|
|
112
|
+
/** Whether launching, waiting, cancellation, I/O, or cleanup failed. */
|
|
113
|
+
reason;
|
|
114
|
+
/** Standard output collected before the invocation finished cleaning up. */
|
|
115
|
+
stdout;
|
|
116
|
+
/** Standard error collected before the invocation finished cleaning up. */
|
|
117
|
+
stderr;
|
|
118
|
+
/** Observed exit code, including during cleanup, or null. */
|
|
119
|
+
exitCode;
|
|
120
|
+
/** Observed terminating signal, including during cleanup, or null. */
|
|
121
|
+
signal;
|
|
122
|
+
/**
|
|
123
|
+
* Constructs a harness failure from its captured state.
|
|
124
|
+
* @param reason The stage that failed.
|
|
125
|
+
* @param message A description of the failure.
|
|
126
|
+
* @param result Captured text and observed process status.
|
|
127
|
+
* @param options The underlying cause, if any.
|
|
128
|
+
*/
|
|
129
|
+
constructor(reason, message, result, options) {
|
|
130
|
+
super(message, options);
|
|
131
|
+
this.name = "CliInvocationError";
|
|
132
|
+
this.reason = reason;
|
|
133
|
+
this.stdout = result.stdout;
|
|
134
|
+
this.stderr = result.stderr;
|
|
135
|
+
this.exitCode = result.exitCode;
|
|
136
|
+
this.signal = result.signal;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const emptyResult = {
|
|
140
|
+
stdout: "",
|
|
141
|
+
stderr: "",
|
|
142
|
+
exitCode: null,
|
|
143
|
+
signal: null
|
|
144
|
+
};
|
|
145
|
+
function filePath(value) {
|
|
146
|
+
if (value instanceof URL) return (0, node_url.fileURLToPath)(value);
|
|
147
|
+
if (typeof value !== "string") throw new TypeError("Expected a file path or URL.");
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function strings(value) {
|
|
151
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new TypeError("Expected an array of strings.");
|
|
152
|
+
return [...value];
|
|
153
|
+
}
|
|
154
|
+
function validate(options) {
|
|
155
|
+
if (options == null || typeof options !== "object" || Array.isArray(options)) throw new TypeError("Expected process options.");
|
|
156
|
+
if (options.timeout !== void 0 && (!Number.isInteger(options.timeout) || options.timeout < 0 || options.timeout > 2147483647)) throw new RangeError("Timeout must be an integer between 0 and 2147483647.");
|
|
157
|
+
if (options.cleanup !== void 0 && options.cleanup !== "child" && options.cleanup !== "tree") throw new TypeError("Cleanup must be child or tree.");
|
|
158
|
+
if (options.cwd !== void 0) filePath(options.cwd);
|
|
159
|
+
if (options.env !== void 0 && (options.env === null || typeof options.env !== "object" || Array.isArray(options.env) || Object.values(options.env).some((v) => v !== void 0 && typeof v !== "string"))) throw new TypeError("Environment values must be strings or undefined.");
|
|
160
|
+
if (options.signal !== void 0 && (options.signal == null || typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function" || typeof options.signal.removeEventListener !== "function")) throw new TypeError("Expected an AbortSignal.");
|
|
161
|
+
}
|
|
162
|
+
function environment(...layers) {
|
|
163
|
+
const result = Object.create(null);
|
|
164
|
+
const names = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const layer of layers) {
|
|
166
|
+
if (layer === void 0) continue;
|
|
167
|
+
for (const [name, value] of Object.entries(layer)) {
|
|
168
|
+
const key = node_process.default.platform === "win32" ? name.toUpperCase() : name;
|
|
169
|
+
const previous = names.get(key);
|
|
170
|
+
if (previous !== void 0) delete result[previous];
|
|
171
|
+
names.set(key, name);
|
|
172
|
+
if (value !== void 0) result[name] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Creates a runner for a real CLI without building or installing its target.
|
|
179
|
+
*
|
|
180
|
+
* Output is UTF-8 text, kept separate and unmodified. The default timeout is
|
|
181
|
+
* five seconds, including output collection; zero waits indefinitely unless
|
|
182
|
+
* aborted. Failure cleanup targets the child by default, or its ordinary
|
|
183
|
+
* process tree when requested. Tree cleanup uses POSIX groups or Windows
|
|
184
|
+
* taskkill and cannot contain escaped or already-orphaned descendants. Normal
|
|
185
|
+
* completion does not sweep descendants. Abrupt termination of the test
|
|
186
|
+
* process itself is outside this cleanup contract.
|
|
187
|
+
*
|
|
188
|
+
* Deno callers must grant the harness read, environment, and subprocess
|
|
189
|
+
* permissions. Child permissions belong in `runtimeArgs`. No color settings,
|
|
190
|
+
* runtime flags, or parent `execArgv` are injected.
|
|
191
|
+
*
|
|
192
|
+
* @param options An entry point or command and invocation defaults.
|
|
193
|
+
* @returns A reusable runner with isolated invocations.
|
|
194
|
+
* @throws {TypeError} If the command, paths, or options are invalid.
|
|
195
|
+
* @throws {RangeError} If the timeout is outside its supported range.
|
|
196
|
+
* @throws If reading the factory's working directory is not permitted.
|
|
197
|
+
* @since 1.3.0
|
|
198
|
+
*/
|
|
199
|
+
function createCliRunner(options) {
|
|
200
|
+
validate(options);
|
|
201
|
+
const hasCommand = options.command !== void 0;
|
|
202
|
+
if (hasCommand === (options.entrypoint !== void 0) || hasCommand && options.runtimeArgs !== void 0) throw new TypeError("Specify either an entrypoint or a command.");
|
|
203
|
+
const base = (0, node_path.resolve)(options.cwd === void 0 ? node_process.default.cwd() : filePath(options.cwd));
|
|
204
|
+
let fixed;
|
|
205
|
+
let entrypoint;
|
|
206
|
+
if (options.command !== void 0) {
|
|
207
|
+
fixed = strings(options.command);
|
|
208
|
+
if (fixed.length === 0 || fixed[0].length === 0) throw new TypeError("Command must contain a nonempty executable.");
|
|
209
|
+
} else entrypoint = (0, node_path.resolve)(base, filePath(options.entrypoint));
|
|
210
|
+
const runtimeArgs = strings(options.runtimeArgs ?? []);
|
|
211
|
+
const defaults = {
|
|
212
|
+
timeout: options.timeout ?? 5e3,
|
|
213
|
+
signal: options.signal,
|
|
214
|
+
cleanup: options.cleanup ?? "child",
|
|
215
|
+
env: options.env === void 0 ? void 0 : { ...options.env }
|
|
216
|
+
};
|
|
217
|
+
async function invoke(...input) {
|
|
218
|
+
let call;
|
|
219
|
+
if (input.length === 1 && typeof input[0] === "object") call = input[0];
|
|
220
|
+
else {
|
|
221
|
+
const args$1 = [];
|
|
222
|
+
for (const item of input) {
|
|
223
|
+
if (typeof item !== "string") throw new TypeError("Arguments must be strings.");
|
|
224
|
+
args$1.push(item);
|
|
225
|
+
}
|
|
226
|
+
call = { args: args$1 };
|
|
227
|
+
}
|
|
228
|
+
validate(call);
|
|
229
|
+
const args = strings(call.args ?? []);
|
|
230
|
+
if (call.stdin !== void 0 && typeof call.stdin !== "string") throw new TypeError("Stdin must be a string.");
|
|
231
|
+
const signal = call.signal ?? defaults.signal;
|
|
232
|
+
if (signal?.aborted) throw new CliInvocationError("aborted", "CLI invocation was aborted.", emptyResult, { cause: signal.reason });
|
|
233
|
+
try {
|
|
234
|
+
const command = fixed ?? [
|
|
235
|
+
node_process.default.execPath,
|
|
236
|
+
..."Deno" in globalThis ? ["run"] : [],
|
|
237
|
+
...runtimeArgs,
|
|
238
|
+
...entrypoint === void 0 ? [] : [entrypoint]
|
|
239
|
+
];
|
|
240
|
+
return await runProcess(command[0], [...command.slice(1), ...args], {
|
|
241
|
+
cwd: call.cwd === void 0 ? base : (0, node_path.resolve)(base, filePath(call.cwd)),
|
|
242
|
+
env: environment(node_process.default.env, defaults.env, call.env),
|
|
243
|
+
timeout: call.timeout ?? defaults.timeout,
|
|
244
|
+
cleanup: call.cleanup ?? defaults.cleanup,
|
|
245
|
+
signal,
|
|
246
|
+
stdin: call.stdin ?? ""
|
|
247
|
+
});
|
|
248
|
+
} catch (cause) {
|
|
249
|
+
if (cause instanceof CliInvocationError) throw cause;
|
|
250
|
+
throw new CliInvocationError("spawn", "Could not start the CLI.", emptyResult, { cause });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return Object.freeze({ invoke });
|
|
254
|
+
}
|
|
255
|
+
function errorCode(error) {
|
|
256
|
+
return error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
257
|
+
}
|
|
258
|
+
function runProcess(command, args, options) {
|
|
259
|
+
return new Promise((resolveResult, reject) => {
|
|
260
|
+
let child;
|
|
261
|
+
let spawned = false;
|
|
262
|
+
let exited = false;
|
|
263
|
+
let closed = false;
|
|
264
|
+
let outEnd = false;
|
|
265
|
+
let errEnd = false;
|
|
266
|
+
let outClose = false;
|
|
267
|
+
let errClose = false;
|
|
268
|
+
let inClose = false;
|
|
269
|
+
let exitCode = null;
|
|
270
|
+
let signal = null;
|
|
271
|
+
const stdout = [];
|
|
272
|
+
const stderr = [];
|
|
273
|
+
let settled = false;
|
|
274
|
+
let failure;
|
|
275
|
+
const cleanupErrors = [];
|
|
276
|
+
let timer;
|
|
277
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
278
|
+
const snapshot = () => ({
|
|
279
|
+
stdout: stdout.join(""),
|
|
280
|
+
stderr: stderr.join(""),
|
|
281
|
+
exitCode,
|
|
282
|
+
signal
|
|
283
|
+
});
|
|
284
|
+
const onAbort = () => fail("aborted", "CLI invocation was aborted.", options.signal?.reason);
|
|
285
|
+
const clearInvocation = () => {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
288
|
+
};
|
|
289
|
+
function release() {
|
|
290
|
+
clearInvocation();
|
|
291
|
+
child?.stdout.removeListener("data", onOut);
|
|
292
|
+
child?.stderr.removeListener("data", onErr);
|
|
293
|
+
if (closed) child?.removeListener("error", onError);
|
|
294
|
+
if (outClose) child?.stdout.removeListener("error", onIoError);
|
|
295
|
+
if (errClose) child?.stderr.removeListener("error", onIoError);
|
|
296
|
+
if (inClose) child?.stdin.removeListener("error", onInputError);
|
|
297
|
+
}
|
|
298
|
+
function update() {
|
|
299
|
+
for (const wake of [...waiters]) wake();
|
|
300
|
+
if (settled) {
|
|
301
|
+
release();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!failure && exited && closed && outEnd && errEnd && outClose && errClose && inClose) {
|
|
305
|
+
settled = true;
|
|
306
|
+
release();
|
|
307
|
+
resolveResult(snapshot());
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function waitFor(predicate, milliseconds) {
|
|
311
|
+
if (predicate()) return Promise.resolve(true);
|
|
312
|
+
return new Promise((done) => {
|
|
313
|
+
const finish = (value) => {
|
|
314
|
+
clearTimeout(timeout);
|
|
315
|
+
waiters.delete(wake);
|
|
316
|
+
done(value);
|
|
317
|
+
};
|
|
318
|
+
const wake = () => {
|
|
319
|
+
if (predicate()) finish(true);
|
|
320
|
+
};
|
|
321
|
+
const timeout = setTimeout(() => finish(false), Math.max(0, milliseconds));
|
|
322
|
+
waiters.add(wake);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function fail(reason, message, cause) {
|
|
326
|
+
if (failure || settled) return;
|
|
327
|
+
failure = {
|
|
328
|
+
reason,
|
|
329
|
+
message,
|
|
330
|
+
cause
|
|
331
|
+
};
|
|
332
|
+
clearInvocation();
|
|
333
|
+
queueMicrotask(() => {
|
|
334
|
+
cleanup();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function killChild(kind) {
|
|
338
|
+
if (!child?.pid || exited || closed) return;
|
|
339
|
+
try {
|
|
340
|
+
child.kill(kind);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
cleanupErrors.push(error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function groupSignal(kind) {
|
|
346
|
+
if (!child?.pid) return false;
|
|
347
|
+
try {
|
|
348
|
+
node_process.default.kill(-child.pid, kind);
|
|
349
|
+
return true;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (errorCode(error) !== "ESRCH") cleanupErrors.push(error);
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function taskkill(deadline) {
|
|
356
|
+
let helper;
|
|
357
|
+
const root = node_process.default.env.SystemRoot ?? node_process.default.env.windir;
|
|
358
|
+
if (root === void 0 || !(0, node_path.isAbsolute)(root)) {
|
|
359
|
+
cleanupErrors.push(/* @__PURE__ */ new Error("Windows system directory is unavailable."));
|
|
360
|
+
killChild("SIGKILL");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
helper = (0, node_child_process.spawn)((0, node_path.join)(root, "System32", "taskkill.exe"), [
|
|
365
|
+
"/PID",
|
|
366
|
+
String(child?.pid),
|
|
367
|
+
"/T",
|
|
368
|
+
"/F"
|
|
369
|
+
], {
|
|
370
|
+
shell: false,
|
|
371
|
+
windowsHide: true,
|
|
372
|
+
stdio: "pipe"
|
|
373
|
+
});
|
|
374
|
+
} catch (error) {
|
|
375
|
+
cleanupErrors.push(error);
|
|
376
|
+
killChild("SIGKILL");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
let helperClosed = false;
|
|
380
|
+
let helperError;
|
|
381
|
+
let helperCode = null;
|
|
382
|
+
let diagnostic = "";
|
|
383
|
+
helper.stdout.resume();
|
|
384
|
+
helper.stderr.setEncoding("utf8");
|
|
385
|
+
helper.stderr.on("data", (text) => {
|
|
386
|
+
diagnostic += text;
|
|
387
|
+
});
|
|
388
|
+
helper.stdin.on("error", () => {});
|
|
389
|
+
helper.stdout.on("error", (error) => {
|
|
390
|
+
helperError = error;
|
|
391
|
+
});
|
|
392
|
+
helper.stderr.on("error", (error) => {
|
|
393
|
+
helperError = error;
|
|
394
|
+
});
|
|
395
|
+
helper.on("error", (error) => {
|
|
396
|
+
helperError = error;
|
|
397
|
+
killChild("SIGKILL");
|
|
398
|
+
});
|
|
399
|
+
helper.on("close", (code) => {
|
|
400
|
+
helperClosed = true;
|
|
401
|
+
helperCode = code;
|
|
402
|
+
update();
|
|
403
|
+
});
|
|
404
|
+
helper.stdin.end();
|
|
405
|
+
if (!await waitFor(() => helperClosed, Math.min(1e3, deadline - Date.now()))) {
|
|
406
|
+
cleanupErrors.push(/* @__PURE__ */ new Error("Tree cleanup command timed out."));
|
|
407
|
+
try {
|
|
408
|
+
helper.kill("SIGKILL");
|
|
409
|
+
} catch (error) {
|
|
410
|
+
cleanupErrors.push(error);
|
|
411
|
+
}
|
|
412
|
+
killChild("SIGKILL");
|
|
413
|
+
helper.stdin.destroy();
|
|
414
|
+
helper.stdout.destroy();
|
|
415
|
+
helper.stderr.destroy();
|
|
416
|
+
if (!await waitFor(() => helperClosed, deadline - Date.now())) {
|
|
417
|
+
cleanupErrors.push(/* @__PURE__ */ new Error("Tree cleanup command did not close."));
|
|
418
|
+
helper.unref();
|
|
419
|
+
}
|
|
420
|
+
} else if (helperError !== void 0 || helperCode !== 0) {
|
|
421
|
+
cleanupErrors.push(helperError ?? /* @__PURE__ */ new Error(`Tree cleanup exited with code ${helperCode}: ${diagnostic}`));
|
|
422
|
+
killChild("SIGKILL");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function cleanup() {
|
|
426
|
+
const deadline = Date.now() + 2e3;
|
|
427
|
+
try {
|
|
428
|
+
if (child !== void 0) {
|
|
429
|
+
child.stdin.destroy();
|
|
430
|
+
if (child.pid !== void 0) if (node_process.default.platform === "win32") if (options.cleanup === "tree") if (exited || closed) cleanupErrors.push(/* @__PURE__ */ new Error("Cannot trace a Windows process tree after its root exits."));
|
|
431
|
+
else await taskkill(deadline);
|
|
432
|
+
else killChild("SIGKILL");
|
|
433
|
+
else if (options.cleanup === "tree") {
|
|
434
|
+
const found = groupSignal("SIGTERM");
|
|
435
|
+
if (!found && !exited && !closed) cleanupErrors.push(/* @__PURE__ */ new Error("The child process group is unavailable."));
|
|
436
|
+
killChild("SIGTERM");
|
|
437
|
+
if (found && !await waitFor(() => (exited || closed) && !groupSignal(0), 1e3)) groupSignal("SIGKILL");
|
|
438
|
+
killChild("SIGKILL");
|
|
439
|
+
} else {
|
|
440
|
+
killChild("SIGTERM");
|
|
441
|
+
if (!await waitFor(() => exited || closed, 1e3)) killChild("SIGKILL");
|
|
442
|
+
}
|
|
443
|
+
if (!await waitFor(() => exited || closed, deadline - Date.now())) cleanupErrors.push(/* @__PURE__ */ new Error("The CLI process did not exit during cleanup."));
|
|
444
|
+
await waitFor(() => closed, Math.min(250, deadline - Date.now()));
|
|
445
|
+
child.stdout.destroy();
|
|
446
|
+
child.stderr.destroy();
|
|
447
|
+
if (!await waitFor(() => closed && outClose && errClose && inClose, deadline - Date.now())) {
|
|
448
|
+
cleanupErrors.push(/* @__PURE__ */ new Error("The CLI process streams did not close during cleanup."));
|
|
449
|
+
child.unref();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
cleanupErrors.push(error);
|
|
454
|
+
killChild("SIGKILL");
|
|
455
|
+
child?.stdin.destroy();
|
|
456
|
+
child?.stdout.destroy();
|
|
457
|
+
child?.stderr.destroy();
|
|
458
|
+
child?.unref();
|
|
459
|
+
}
|
|
460
|
+
if (failure === void 0 || settled) return;
|
|
461
|
+
const result = snapshot();
|
|
462
|
+
const primary = new CliInvocationError(failure.reason, failure.message, result, { cause: failure.cause });
|
|
463
|
+
settled = true;
|
|
464
|
+
release();
|
|
465
|
+
reject(cleanupErrors.length === 0 ? primary : new CliInvocationError("cleanup", "CLI cleanup could not be completed.", result, { cause: new AggregateError([primary, ...cleanupErrors], "CLI invocation and cleanup failed.") }));
|
|
466
|
+
}
|
|
467
|
+
function onOut(text) {
|
|
468
|
+
stdout.push(text);
|
|
469
|
+
}
|
|
470
|
+
function onErr(text) {
|
|
471
|
+
stderr.push(text);
|
|
472
|
+
}
|
|
473
|
+
function onError(error) {
|
|
474
|
+
if (failure) cleanupErrors.push(error);
|
|
475
|
+
else fail(spawned ? "io" : "spawn", "Could not execute the CLI.", error);
|
|
476
|
+
}
|
|
477
|
+
function onIoError(error) {
|
|
478
|
+
fail("io", "Could not capture CLI output.", error);
|
|
479
|
+
}
|
|
480
|
+
function onInputError(error) {
|
|
481
|
+
const code = errorCode(error);
|
|
482
|
+
if (code !== "EPIPE" && code !== "ECONNRESET" && code !== "EOF") fail("io", "Could not write CLI input.", error);
|
|
483
|
+
}
|
|
484
|
+
function checkOutput() {
|
|
485
|
+
if (spawned && (outClose && !outEnd || errClose && !errEnd)) fail("io", "CLI output closed before it could be fully read.");
|
|
486
|
+
update();
|
|
487
|
+
}
|
|
488
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
489
|
+
if (options.signal?.aborted) {
|
|
490
|
+
onAbort();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (options.timeout > 0) timer = setTimeout(() => fail("timeout", "CLI invocation timed out."), options.timeout);
|
|
494
|
+
try {
|
|
495
|
+
child = spawnCliProcess(command, args, {
|
|
496
|
+
cwd: options.cwd,
|
|
497
|
+
env: options.env,
|
|
498
|
+
detached: node_process.default.platform !== "win32" && options.cleanup === "tree"
|
|
499
|
+
});
|
|
500
|
+
child.on("error", onError);
|
|
501
|
+
child.on("spawn", () => {
|
|
502
|
+
spawned = true;
|
|
503
|
+
checkOutput();
|
|
504
|
+
});
|
|
505
|
+
child.on("exit", (code, terminatingSignal) => {
|
|
506
|
+
exited = true;
|
|
507
|
+
exitCode = code;
|
|
508
|
+
signal = terminatingSignal;
|
|
509
|
+
update();
|
|
510
|
+
});
|
|
511
|
+
child.on("close", () => {
|
|
512
|
+
closed = true;
|
|
513
|
+
update();
|
|
514
|
+
});
|
|
515
|
+
child.stdout.setEncoding("utf8");
|
|
516
|
+
child.stderr.setEncoding("utf8");
|
|
517
|
+
child.stdout.on("data", onOut);
|
|
518
|
+
child.stderr.on("data", onErr);
|
|
519
|
+
child.stdout.on("error", onIoError);
|
|
520
|
+
child.stderr.on("error", onIoError);
|
|
521
|
+
child.stdin.on("error", onInputError);
|
|
522
|
+
child.stdout.on("end", () => {
|
|
523
|
+
outEnd = true;
|
|
524
|
+
update();
|
|
525
|
+
});
|
|
526
|
+
child.stderr.on("end", () => {
|
|
527
|
+
errEnd = true;
|
|
528
|
+
update();
|
|
529
|
+
});
|
|
530
|
+
child.stdout.on("close", () => {
|
|
531
|
+
outClose = true;
|
|
532
|
+
checkOutput();
|
|
533
|
+
});
|
|
534
|
+
child.stderr.on("close", () => {
|
|
535
|
+
errClose = true;
|
|
536
|
+
checkOutput();
|
|
537
|
+
});
|
|
538
|
+
child.stdin.on("close", () => {
|
|
539
|
+
inClose = true;
|
|
540
|
+
update();
|
|
541
|
+
});
|
|
542
|
+
child.stdin.end(options.stdin, "utf8");
|
|
543
|
+
} catch (cause) {
|
|
544
|
+
fail("spawn", "Could not start the CLI.", cause);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
//#endregion
|
|
550
|
+
exports.CliInvocationError = CliInvocationError;
|
|
551
|
+
exports.createCliRunner = createCliRunner;
|