command-stream 0.9.0 → 0.9.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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
// ProcessRunner pipeline methods - all pipeline execution strategies
|
|
2
|
+
// Part of the modular ProcessRunner architecture
|
|
3
|
+
|
|
4
|
+
import cp from 'child_process';
|
|
5
|
+
import { trace } from './$.trace.mjs';
|
|
6
|
+
import { findAvailableShell } from './$.shell.mjs';
|
|
7
|
+
import { StreamUtils, safeWrite } from './$.stream-utils.mjs';
|
|
8
|
+
import { createResult } from './$.result.mjs';
|
|
9
|
+
|
|
10
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Commands that need streaming workaround
|
|
14
|
+
*/
|
|
15
|
+
const STREAMING_COMMANDS = ['jq', 'grep', 'sed', 'cat', 'awk'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if command needs streaming workaround
|
|
19
|
+
* @param {object} command - Command object
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function needsStreamingWorkaround(command) {
|
|
23
|
+
return STREAMING_COMMANDS.includes(command.cmd);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Analyze pipeline for virtual commands
|
|
28
|
+
* @param {Array} commands - Pipeline commands
|
|
29
|
+
* @param {Function} isVirtualCommandsEnabled - Check if virtual commands enabled
|
|
30
|
+
* @param {Map} virtualCommands - Virtual commands registry
|
|
31
|
+
* @returns {object} Analysis result
|
|
32
|
+
*/
|
|
33
|
+
function analyzePipeline(commands, isVirtualCommandsEnabled, virtualCommands) {
|
|
34
|
+
const pipelineInfo = commands.map((command) => ({
|
|
35
|
+
...command,
|
|
36
|
+
isVirtual: isVirtualCommandsEnabled() && virtualCommands.has(command.cmd),
|
|
37
|
+
}));
|
|
38
|
+
return {
|
|
39
|
+
pipelineInfo,
|
|
40
|
+
hasVirtual: pipelineInfo.some((info) => info.isVirtual),
|
|
41
|
+
virtualCount: pipelineInfo.filter((p) => p.isVirtual).length,
|
|
42
|
+
realCount: pipelineInfo.filter((p) => !p.isVirtual).length,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read stream to string
|
|
48
|
+
* @param {ReadableStream} stream - Stream to read
|
|
49
|
+
* @returns {Promise<string>}
|
|
50
|
+
*/
|
|
51
|
+
async function readStreamToString(stream) {
|
|
52
|
+
const reader = stream.getReader();
|
|
53
|
+
let result = '';
|
|
54
|
+
try {
|
|
55
|
+
let done = false;
|
|
56
|
+
while (!done) {
|
|
57
|
+
const readResult = await reader.read();
|
|
58
|
+
done = readResult.done;
|
|
59
|
+
if (!done && readResult.value) {
|
|
60
|
+
result += new TextDecoder().decode(readResult.value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
reader.releaseLock();
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build command parts from command object
|
|
71
|
+
* @param {object} command - Command with cmd and args
|
|
72
|
+
* @returns {string[]} Command parts array
|
|
73
|
+
*/
|
|
74
|
+
function buildCommandParts(command) {
|
|
75
|
+
const { cmd, args } = command;
|
|
76
|
+
const parts = [cmd];
|
|
77
|
+
for (const arg of args) {
|
|
78
|
+
if (arg.value !== undefined) {
|
|
79
|
+
if (arg.quoted) {
|
|
80
|
+
parts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
81
|
+
} else if (arg.value.includes(' ')) {
|
|
82
|
+
parts.push(`"${arg.value}"`);
|
|
83
|
+
} else {
|
|
84
|
+
parts.push(arg.value);
|
|
85
|
+
}
|
|
86
|
+
} else if (
|
|
87
|
+
typeof arg === 'string' &&
|
|
88
|
+
arg.includes(' ') &&
|
|
89
|
+
!arg.startsWith('"') &&
|
|
90
|
+
!arg.startsWith("'")
|
|
91
|
+
) {
|
|
92
|
+
parts.push(`"${arg}"`);
|
|
93
|
+
} else {
|
|
94
|
+
parts.push(arg);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return parts;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if command string needs shell execution
|
|
102
|
+
* @param {string} commandStr - Command string
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function needsShellExecution(commandStr) {
|
|
106
|
+
return (
|
|
107
|
+
commandStr.includes('*') ||
|
|
108
|
+
commandStr.includes('$') ||
|
|
109
|
+
commandStr.includes('>') ||
|
|
110
|
+
commandStr.includes('<') ||
|
|
111
|
+
commandStr.includes('&&') ||
|
|
112
|
+
commandStr.includes('||') ||
|
|
113
|
+
commandStr.includes(';') ||
|
|
114
|
+
commandStr.includes('`')
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get spawn args based on shell need
|
|
120
|
+
* @param {boolean} needsShell - Whether shell is needed
|
|
121
|
+
* @param {string} cmd - Command name
|
|
122
|
+
* @param {Array} args - Command args
|
|
123
|
+
* @param {string} commandStr - Full command string
|
|
124
|
+
* @returns {string[]} Spawn arguments
|
|
125
|
+
*/
|
|
126
|
+
function getSpawnArgs(needsShell, cmd, args, commandStr) {
|
|
127
|
+
if (needsShell) {
|
|
128
|
+
const shell = findAvailableShell();
|
|
129
|
+
return [shell.cmd, ...shell.args.filter((arg) => arg !== '-l'), commandStr];
|
|
130
|
+
}
|
|
131
|
+
return [cmd, ...args.map((a) => (a.value !== undefined ? a.value : a))];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Determine stdin configuration for first command
|
|
136
|
+
* @param {object} options - Runner options
|
|
137
|
+
* @returns {object} Stdin config with stdin, needsManualStdin, stdinData
|
|
138
|
+
*/
|
|
139
|
+
function getFirstCommandStdin(options) {
|
|
140
|
+
if (options.stdin && typeof options.stdin === 'string') {
|
|
141
|
+
return {
|
|
142
|
+
stdin: 'pipe',
|
|
143
|
+
needsManualStdin: true,
|
|
144
|
+
stdinData: Buffer.from(options.stdin),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (options.stdin && Buffer.isBuffer(options.stdin)) {
|
|
148
|
+
return { stdin: 'pipe', needsManualStdin: true, stdinData: options.stdin };
|
|
149
|
+
}
|
|
150
|
+
return { stdin: 'ignore', needsManualStdin: false, stdinData: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get stdin string from options
|
|
155
|
+
* @param {object} options - Runner options
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function getStdinString(options) {
|
|
159
|
+
if (options.stdin && typeof options.stdin === 'string') {
|
|
160
|
+
return options.stdin;
|
|
161
|
+
}
|
|
162
|
+
if (options.stdin && Buffer.isBuffer(options.stdin)) {
|
|
163
|
+
return options.stdin.toString('utf8');
|
|
164
|
+
}
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle pipefail check
|
|
170
|
+
* @param {number[]} exitCodes - Exit codes from pipeline
|
|
171
|
+
* @param {object} shellSettings - Shell settings
|
|
172
|
+
*/
|
|
173
|
+
function checkPipefail(exitCodes, shellSettings) {
|
|
174
|
+
if (shellSettings.pipefail) {
|
|
175
|
+
const failedIndex = exitCodes.findIndex((code) => code !== 0);
|
|
176
|
+
if (failedIndex !== -1) {
|
|
177
|
+
const error = new Error(
|
|
178
|
+
`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`
|
|
179
|
+
);
|
|
180
|
+
error.code = exitCodes[failedIndex];
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create and throw errexit error
|
|
188
|
+
* @param {object} result - Result object
|
|
189
|
+
* @param {object} shellSettings - Shell settings
|
|
190
|
+
*/
|
|
191
|
+
function throwErrexitError(result, shellSettings) {
|
|
192
|
+
if (shellSettings.errexit && result.code !== 0) {
|
|
193
|
+
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
194
|
+
error.code = result.code;
|
|
195
|
+
error.stdout = result.stdout;
|
|
196
|
+
error.stderr = result.stderr;
|
|
197
|
+
error.result = result;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Write stdin to Bun process
|
|
204
|
+
* @param {object} proc - Process with stdin
|
|
205
|
+
* @param {Buffer} stdinData - Data to write
|
|
206
|
+
*/
|
|
207
|
+
async function writeBunStdin(proc, stdinData) {
|
|
208
|
+
if (!proc.stdin) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const stdinHandler = StreamUtils.setupStdinHandling(
|
|
212
|
+
proc.stdin,
|
|
213
|
+
'Bun process stdin'
|
|
214
|
+
);
|
|
215
|
+
try {
|
|
216
|
+
if (stdinHandler.isWritable()) {
|
|
217
|
+
await proc.stdin.write(stdinData);
|
|
218
|
+
await proc.stdin.end();
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
if (e.code !== 'EPIPE') {
|
|
222
|
+
trace('ProcessRunner', () => `stdin write error | ${e.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Collect stderr from process async
|
|
229
|
+
* @param {object} runner - ProcessRunner
|
|
230
|
+
* @param {object} proc - Process
|
|
231
|
+
* @param {boolean} isLast - Is last command
|
|
232
|
+
* @param {object} collector - Object to collect stderr
|
|
233
|
+
*/
|
|
234
|
+
function collectStderrAsync(runner, proc, isLast, collector) {
|
|
235
|
+
(async () => {
|
|
236
|
+
for await (const chunk of proc.stderr) {
|
|
237
|
+
const buf = Buffer.from(chunk);
|
|
238
|
+
collector.stderr += buf.toString();
|
|
239
|
+
if (isLast) {
|
|
240
|
+
if (runner.options.mirror) {
|
|
241
|
+
safeWrite(process.stderr, buf);
|
|
242
|
+
}
|
|
243
|
+
runner._emitProcessedData('stderr', buf);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create initial input stream from stdin option
|
|
251
|
+
* @param {object} options - Runner options
|
|
252
|
+
* @returns {ReadableStream|null}
|
|
253
|
+
*/
|
|
254
|
+
function createInitialInputStream(options) {
|
|
255
|
+
if (!options.stdin) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const inputData =
|
|
259
|
+
typeof options.stdin === 'string'
|
|
260
|
+
? options.stdin
|
|
261
|
+
: options.stdin.toString('utf8');
|
|
262
|
+
return new ReadableStream({
|
|
263
|
+
start(controller) {
|
|
264
|
+
controller.enqueue(new TextEncoder().encode(inputData));
|
|
265
|
+
controller.close();
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get argument values from args array
|
|
272
|
+
* @param {Array} args - Args array
|
|
273
|
+
* @returns {Array} Argument values
|
|
274
|
+
*/
|
|
275
|
+
function getArgValues(args) {
|
|
276
|
+
return args.map((arg) => (arg.value !== undefined ? arg.value : arg));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create readable stream from string
|
|
281
|
+
* @param {string} data - String data
|
|
282
|
+
* @returns {ReadableStream}
|
|
283
|
+
*/
|
|
284
|
+
function createStringStream(data) {
|
|
285
|
+
return new ReadableStream({
|
|
286
|
+
start(controller) {
|
|
287
|
+
controller.enqueue(new TextEncoder().encode(data));
|
|
288
|
+
controller.close();
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Pipe stream to process stdin
|
|
295
|
+
* @param {ReadableStream} stream - Input stream
|
|
296
|
+
* @param {object} proc - Process
|
|
297
|
+
*/
|
|
298
|
+
function pipeStreamToProcess(stream, proc) {
|
|
299
|
+
if (!stream || !proc.stdin) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const reader = stream.getReader();
|
|
303
|
+
const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
|
|
304
|
+
|
|
305
|
+
(async () => {
|
|
306
|
+
try {
|
|
307
|
+
while (true) {
|
|
308
|
+
const { done, value } = await reader.read();
|
|
309
|
+
if (done) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (writer.write) {
|
|
313
|
+
try {
|
|
314
|
+
await writer.write(value);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
StreamUtils.handleStreamError(error, 'stream writer', false);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
} else if (writer.getWriter) {
|
|
320
|
+
try {
|
|
321
|
+
const w = writer.getWriter();
|
|
322
|
+
await w.write(value);
|
|
323
|
+
w.releaseLock();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
StreamUtils.handleStreamError(
|
|
326
|
+
error,
|
|
327
|
+
'stream writer (getWriter)',
|
|
328
|
+
false
|
|
329
|
+
);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
reader.releaseLock();
|
|
336
|
+
if (writer.close) {
|
|
337
|
+
await writer.close();
|
|
338
|
+
} else if (writer.end) {
|
|
339
|
+
writer.end();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Spawn shell command in Bun
|
|
347
|
+
* @param {string} commandStr - Command string
|
|
348
|
+
* @param {object} options - Options (cwd, env, stdin)
|
|
349
|
+
* @returns {object} Process
|
|
350
|
+
*/
|
|
351
|
+
function spawnShellCommand(commandStr, options) {
|
|
352
|
+
const shell = findAvailableShell();
|
|
353
|
+
return Bun.spawn(
|
|
354
|
+
[shell.cmd, ...shell.args.filter((arg) => arg !== '-l'), commandStr],
|
|
355
|
+
{
|
|
356
|
+
cwd: options.cwd,
|
|
357
|
+
env: options.env,
|
|
358
|
+
stdin: options.stdin,
|
|
359
|
+
stdout: 'pipe',
|
|
360
|
+
stderr: 'pipe',
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Collect last command stdout
|
|
367
|
+
* @param {object} runner - ProcessRunner
|
|
368
|
+
* @param {object} proc - Process
|
|
369
|
+
* @returns {Promise<string>} Output string
|
|
370
|
+
*/
|
|
371
|
+
async function collectFinalStdout(runner, proc) {
|
|
372
|
+
const chunks = [];
|
|
373
|
+
for await (const chunk of proc.stdout) {
|
|
374
|
+
const buf = Buffer.from(chunk);
|
|
375
|
+
chunks.push(buf);
|
|
376
|
+
if (runner.options.mirror) {
|
|
377
|
+
safeWrite(process.stdout, buf);
|
|
378
|
+
}
|
|
379
|
+
runner._emitProcessedData('stdout', buf);
|
|
380
|
+
}
|
|
381
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Spawn async node process for pipeline
|
|
386
|
+
* @param {object} runner - ProcessRunner instance
|
|
387
|
+
* @param {string[]} argv - Command arguments
|
|
388
|
+
* @param {string} stdin - Stdin input
|
|
389
|
+
* @param {boolean} isLastCommand - Is this the last command
|
|
390
|
+
* @returns {Promise<object>} Result with status, stdout, stderr
|
|
391
|
+
*/
|
|
392
|
+
function spawnNodeAsync(runner, argv, stdin, isLastCommand) {
|
|
393
|
+
return new Promise((resolve, reject) => {
|
|
394
|
+
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
395
|
+
cwd: runner.options.cwd,
|
|
396
|
+
env: runner.options.env,
|
|
397
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
let stdout = '';
|
|
401
|
+
let stderr = '';
|
|
402
|
+
|
|
403
|
+
proc.stdout.on('data', (chunk) => {
|
|
404
|
+
stdout += chunk.toString();
|
|
405
|
+
if (isLastCommand) {
|
|
406
|
+
if (runner.options.mirror) {
|
|
407
|
+
safeWrite(process.stdout, chunk);
|
|
408
|
+
}
|
|
409
|
+
runner._emitProcessedData('stdout', chunk);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
proc.stderr.on('data', (chunk) => {
|
|
414
|
+
stderr += chunk.toString();
|
|
415
|
+
if (isLastCommand) {
|
|
416
|
+
if (runner.options.mirror) {
|
|
417
|
+
safeWrite(process.stderr, chunk);
|
|
418
|
+
}
|
|
419
|
+
runner._emitProcessedData('stderr', chunk);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
proc.on('close', (code) => resolve({ status: code, stdout, stderr }));
|
|
424
|
+
proc.on('error', reject);
|
|
425
|
+
|
|
426
|
+
if (proc.stdin) {
|
|
427
|
+
StreamUtils.addStdinErrorHandler(
|
|
428
|
+
proc.stdin,
|
|
429
|
+
'spawnNodeAsync stdin',
|
|
430
|
+
reject
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (stdin) {
|
|
434
|
+
StreamUtils.safeStreamWrite(proc.stdin, stdin, 'spawnNodeAsync stdin');
|
|
435
|
+
}
|
|
436
|
+
StreamUtils.safeStreamEnd(proc.stdin, 'spawnNodeAsync stdin');
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Log shell trace/verbose
|
|
442
|
+
* @param {object} settings - Shell settings
|
|
443
|
+
* @param {string} cmd - Command
|
|
444
|
+
* @param {string[]} argValues - Argument values
|
|
445
|
+
*/
|
|
446
|
+
function logShellTrace(settings, cmd, argValues) {
|
|
447
|
+
const cmdStr = `${cmd} ${argValues.join(' ')}`;
|
|
448
|
+
if (settings.xtrace) {
|
|
449
|
+
console.log(`+ ${cmdStr}`);
|
|
450
|
+
}
|
|
451
|
+
if (settings.verbose) {
|
|
452
|
+
console.log(cmdStr);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Handle virtual command in non-streaming pipeline
|
|
458
|
+
* @param {object} runner - ProcessRunner instance
|
|
459
|
+
* @param {Function} handler - Handler function
|
|
460
|
+
* @param {string[]} argValues - Argument values
|
|
461
|
+
* @param {string} currentInput - Current input
|
|
462
|
+
* @param {object} options - Runner options
|
|
463
|
+
* @returns {Promise<object>} Result
|
|
464
|
+
*/
|
|
465
|
+
async function runVirtualHandler(
|
|
466
|
+
runner,
|
|
467
|
+
handler,
|
|
468
|
+
argValues,
|
|
469
|
+
currentInput,
|
|
470
|
+
options
|
|
471
|
+
) {
|
|
472
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
473
|
+
const chunks = [];
|
|
474
|
+
for await (const chunk of handler({
|
|
475
|
+
args: argValues,
|
|
476
|
+
stdin: currentInput,
|
|
477
|
+
...options,
|
|
478
|
+
})) {
|
|
479
|
+
chunks.push(Buffer.from(chunk));
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
code: 0,
|
|
483
|
+
stdout: options.capture
|
|
484
|
+
? Buffer.concat(chunks).toString('utf8')
|
|
485
|
+
: undefined,
|
|
486
|
+
stderr: options.capture ? '' : undefined,
|
|
487
|
+
stdin: options.capture ? currentInput : undefined,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const result = await handler({
|
|
491
|
+
args: argValues,
|
|
492
|
+
stdin: currentInput,
|
|
493
|
+
...options,
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
...result,
|
|
497
|
+
code: result.code ?? 0,
|
|
498
|
+
stdout: options.capture ? (result.stdout ?? '') : undefined,
|
|
499
|
+
stderr: options.capture ? (result.stderr ?? '') : undefined,
|
|
500
|
+
stdin: options.capture ? currentInput : undefined,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Emit final result output
|
|
506
|
+
* @param {object} runner - ProcessRunner instance
|
|
507
|
+
* @param {object} result - Result object
|
|
508
|
+
*/
|
|
509
|
+
function emitFinalOutput(runner, result) {
|
|
510
|
+
if (result.stdout) {
|
|
511
|
+
const buf = Buffer.from(result.stdout);
|
|
512
|
+
if (runner.options.mirror) {
|
|
513
|
+
safeWrite(process.stdout, buf);
|
|
514
|
+
}
|
|
515
|
+
runner._emitProcessedData('stdout', buf);
|
|
516
|
+
}
|
|
517
|
+
if (result.stderr) {
|
|
518
|
+
const buf = Buffer.from(result.stderr);
|
|
519
|
+
if (runner.options.mirror) {
|
|
520
|
+
safeWrite(process.stderr, buf);
|
|
521
|
+
}
|
|
522
|
+
runner._emitProcessedData('stderr', buf);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Create final result for pipeline
|
|
528
|
+
* @param {object} runner - ProcessRunner instance
|
|
529
|
+
* @param {object} result - Current result
|
|
530
|
+
* @param {string} currentOutput - Current output
|
|
531
|
+
* @param {object} shellSettings - Shell settings
|
|
532
|
+
* @returns {object} Final result
|
|
533
|
+
*/
|
|
534
|
+
function createFinalPipelineResult(
|
|
535
|
+
runner,
|
|
536
|
+
result,
|
|
537
|
+
currentOutput,
|
|
538
|
+
shellSettings
|
|
539
|
+
) {
|
|
540
|
+
const finalResult = createResult({
|
|
541
|
+
code: result.code,
|
|
542
|
+
stdout: currentOutput,
|
|
543
|
+
stderr: result.stderr,
|
|
544
|
+
stdin: getStdinString(runner.options),
|
|
545
|
+
});
|
|
546
|
+
runner.finish(finalResult);
|
|
547
|
+
throwErrexitError(finalResult, shellSettings);
|
|
548
|
+
return finalResult;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Handle pipeline error
|
|
553
|
+
* @param {object} runner - ProcessRunner instance
|
|
554
|
+
* @param {Error} error - Error
|
|
555
|
+
* @param {string} currentOutput - Current output
|
|
556
|
+
* @param {object} shellSettings - Shell settings
|
|
557
|
+
* @returns {object} Error result
|
|
558
|
+
*/
|
|
559
|
+
function handlePipelineError(runner, error, currentOutput, shellSettings) {
|
|
560
|
+
const result = createResult({
|
|
561
|
+
code: error.code ?? 1,
|
|
562
|
+
stdout: currentOutput,
|
|
563
|
+
stderr: error.stderr ?? error.message,
|
|
564
|
+
stdin: getStdinString(runner.options),
|
|
565
|
+
});
|
|
566
|
+
if (result.stderr) {
|
|
567
|
+
const buf = Buffer.from(result.stderr);
|
|
568
|
+
if (runner.options.mirror) {
|
|
569
|
+
safeWrite(process.stderr, buf);
|
|
570
|
+
}
|
|
571
|
+
runner._emitProcessedData('stderr', buf);
|
|
572
|
+
}
|
|
573
|
+
runner.finish(result);
|
|
574
|
+
if (shellSettings.errexit) {
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Handle virtual command in non-streaming pipeline iteration
|
|
582
|
+
* @param {object} runner - ProcessRunner instance
|
|
583
|
+
* @param {object} command - Command object
|
|
584
|
+
* @param {string} currentInput - Current input
|
|
585
|
+
* @param {boolean} isLastCommand - Is last command
|
|
586
|
+
* @param {object} deps - Dependencies
|
|
587
|
+
* @returns {Promise<object>} { output, input, finalResult }
|
|
588
|
+
*/
|
|
589
|
+
async function handleVirtualPipelineCommand(
|
|
590
|
+
runner,
|
|
591
|
+
command,
|
|
592
|
+
currentInput,
|
|
593
|
+
isLastCommand,
|
|
594
|
+
deps
|
|
595
|
+
) {
|
|
596
|
+
const { virtualCommands, globalShellSettings } = deps;
|
|
597
|
+
const handler = virtualCommands.get(command.cmd);
|
|
598
|
+
const argValues = getArgValues(command.args);
|
|
599
|
+
logShellTrace(globalShellSettings, command.cmd, argValues);
|
|
600
|
+
|
|
601
|
+
const result = await runVirtualHandler(
|
|
602
|
+
runner,
|
|
603
|
+
handler,
|
|
604
|
+
argValues,
|
|
605
|
+
currentInput,
|
|
606
|
+
runner.options
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
if (isLastCommand) {
|
|
610
|
+
emitFinalOutput(runner, result);
|
|
611
|
+
return {
|
|
612
|
+
finalResult: createFinalPipelineResult(
|
|
613
|
+
runner,
|
|
614
|
+
result,
|
|
615
|
+
result.stdout,
|
|
616
|
+
globalShellSettings
|
|
617
|
+
),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
622
|
+
const error = new Error(
|
|
623
|
+
`Pipeline command failed with exit code ${result.code}`
|
|
624
|
+
);
|
|
625
|
+
error.code = result.code;
|
|
626
|
+
error.result = result;
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return { input: result.stdout };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Handle shell command in non-streaming pipeline iteration
|
|
635
|
+
* @param {object} runner - ProcessRunner instance
|
|
636
|
+
* @param {object} command - Command object
|
|
637
|
+
* @param {string} currentInput - Current input
|
|
638
|
+
* @param {boolean} isLastCommand - Is last command
|
|
639
|
+
* @param {object} deps - Dependencies
|
|
640
|
+
* @returns {Promise<object>} { output, input, finalResult }
|
|
641
|
+
*/
|
|
642
|
+
async function handleShellPipelineCommand(
|
|
643
|
+
runner,
|
|
644
|
+
command,
|
|
645
|
+
currentInput,
|
|
646
|
+
isLastCommand,
|
|
647
|
+
deps
|
|
648
|
+
) {
|
|
649
|
+
const { globalShellSettings } = deps;
|
|
650
|
+
const commandStr = buildCommandParts(command).join(' ');
|
|
651
|
+
logShellTrace(globalShellSettings, commandStr, []);
|
|
652
|
+
|
|
653
|
+
const shell = findAvailableShell();
|
|
654
|
+
const argv = [shell.cmd, ...shell.args.filter((a) => a !== '-l'), commandStr];
|
|
655
|
+
const proc = await spawnNodeAsync(runner, argv, currentInput, isLastCommand);
|
|
656
|
+
const result = {
|
|
657
|
+
code: proc.status || 0,
|
|
658
|
+
stdout: proc.stdout || '',
|
|
659
|
+
stderr: proc.stderr || '',
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
if (globalShellSettings.pipefail && result.code !== 0) {
|
|
663
|
+
const error = new Error(
|
|
664
|
+
`Pipeline command '${commandStr}' failed with exit code ${result.code}`
|
|
665
|
+
);
|
|
666
|
+
error.code = result.code;
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (isLastCommand) {
|
|
671
|
+
let allStderr = '';
|
|
672
|
+
if (runner.errChunks?.length > 0) {
|
|
673
|
+
allStderr = Buffer.concat(runner.errChunks).toString('utf8');
|
|
674
|
+
}
|
|
675
|
+
if (result.stderr) {
|
|
676
|
+
allStderr += result.stderr;
|
|
677
|
+
}
|
|
678
|
+
const finalResult = createResult({
|
|
679
|
+
code: result.code,
|
|
680
|
+
stdout: result.stdout,
|
|
681
|
+
stderr: allStderr,
|
|
682
|
+
stdin: getStdinString(runner.options),
|
|
683
|
+
});
|
|
684
|
+
runner.finish(finalResult);
|
|
685
|
+
throwErrexitError(finalResult, globalShellSettings);
|
|
686
|
+
return { finalResult };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (result.stderr && runner.options.capture) {
|
|
690
|
+
runner.errChunks = runner.errChunks || [];
|
|
691
|
+
runner.errChunks.push(Buffer.from(result.stderr));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return { input: result.stdout };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Attach pipeline methods to ProcessRunner prototype
|
|
699
|
+
* @param {Function} ProcessRunner - The ProcessRunner class
|
|
700
|
+
* @param {Object} deps - Dependencies
|
|
701
|
+
*/
|
|
702
|
+
export function attachPipelineMethods(ProcessRunner, deps) {
|
|
703
|
+
const { virtualCommands, globalShellSettings, isVirtualCommandsEnabled } =
|
|
704
|
+
deps;
|
|
705
|
+
|
|
706
|
+
// Use module-level helper
|
|
707
|
+
ProcessRunner.prototype._readStreamToString = readStreamToString;
|
|
708
|
+
|
|
709
|
+
ProcessRunner.prototype._runStreamingPipelineBun = async function (commands) {
|
|
710
|
+
trace(
|
|
711
|
+
'ProcessRunner',
|
|
712
|
+
() => `_runStreamingPipelineBun | cmds=${commands.length}`
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const analysis = analyzePipeline(
|
|
716
|
+
commands,
|
|
717
|
+
isVirtualCommandsEnabled,
|
|
718
|
+
virtualCommands
|
|
719
|
+
);
|
|
720
|
+
if (analysis.hasVirtual) {
|
|
721
|
+
return this._runMixedStreamingPipeline(commands);
|
|
722
|
+
}
|
|
723
|
+
if (commands.some(needsStreamingWorkaround)) {
|
|
724
|
+
return this._runTeeStreamingPipeline(commands);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const processes = [];
|
|
728
|
+
const collector = { stderr: '' };
|
|
729
|
+
|
|
730
|
+
for (let i = 0; i < commands.length; i++) {
|
|
731
|
+
const command = commands[i];
|
|
732
|
+
const commandStr = buildCommandParts(command).join(' ');
|
|
733
|
+
const needsShell = needsShellExecution(commandStr);
|
|
734
|
+
const spawnArgs = getSpawnArgs(
|
|
735
|
+
needsShell,
|
|
736
|
+
command.cmd,
|
|
737
|
+
command.args,
|
|
738
|
+
commandStr
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
let stdin;
|
|
742
|
+
let stdinConfig = null;
|
|
743
|
+
if (i === 0) {
|
|
744
|
+
stdinConfig = getFirstCommandStdin(this.options);
|
|
745
|
+
stdin = stdinConfig.stdin;
|
|
746
|
+
} else {
|
|
747
|
+
stdin = processes[i - 1].stdout;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
751
|
+
cwd: this.options.cwd,
|
|
752
|
+
env: this.options.env,
|
|
753
|
+
stdin,
|
|
754
|
+
stdout: 'pipe',
|
|
755
|
+
stderr: 'pipe',
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (stdinConfig?.needsManualStdin && stdinConfig.stdinData) {
|
|
759
|
+
writeBunStdin(proc, stdinConfig.stdinData);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
processes.push(proc);
|
|
763
|
+
collectStderrAsync(this, proc, i === commands.length - 1, collector);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const lastProc = processes[processes.length - 1];
|
|
767
|
+
let finalOutput = '';
|
|
768
|
+
|
|
769
|
+
for await (const chunk of lastProc.stdout) {
|
|
770
|
+
const buf = Buffer.from(chunk);
|
|
771
|
+
finalOutput += buf.toString();
|
|
772
|
+
if (this.options.mirror) {
|
|
773
|
+
safeWrite(process.stdout, buf);
|
|
774
|
+
}
|
|
775
|
+
this._emitProcessedData('stdout', buf);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const exitCodes = await Promise.all(processes.map((p) => p.exited));
|
|
779
|
+
checkPipefail(exitCodes, globalShellSettings);
|
|
780
|
+
|
|
781
|
+
const result = createResult({
|
|
782
|
+
code: exitCodes[exitCodes.length - 1] || 0,
|
|
783
|
+
stdout: finalOutput,
|
|
784
|
+
stderr: collector.stderr,
|
|
785
|
+
stdin: getStdinString(this.options),
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
this.finish(result);
|
|
789
|
+
throwErrexitError(result, globalShellSettings);
|
|
790
|
+
return result;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
ProcessRunner.prototype._runTeeStreamingPipeline = async function (commands) {
|
|
794
|
+
trace(
|
|
795
|
+
'ProcessRunner',
|
|
796
|
+
() => `_runTeeStreamingPipeline | cmds=${commands.length}`
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const processes = [];
|
|
800
|
+
const collector = { stderr: '' };
|
|
801
|
+
let currentStream = null;
|
|
802
|
+
|
|
803
|
+
for (let i = 0; i < commands.length; i++) {
|
|
804
|
+
const command = commands[i];
|
|
805
|
+
const commandStr = buildCommandParts(command).join(' ');
|
|
806
|
+
const needsShell = needsShellExecution(commandStr);
|
|
807
|
+
const spawnArgs = getSpawnArgs(
|
|
808
|
+
needsShell,
|
|
809
|
+
command.cmd,
|
|
810
|
+
command.args,
|
|
811
|
+
commandStr
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
let stdin;
|
|
815
|
+
let stdinConfig = null;
|
|
816
|
+
if (i === 0) {
|
|
817
|
+
stdinConfig = getFirstCommandStdin(this.options);
|
|
818
|
+
stdin = stdinConfig.stdin;
|
|
819
|
+
} else {
|
|
820
|
+
stdin = currentStream;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
824
|
+
cwd: this.options.cwd,
|
|
825
|
+
env: this.options.env,
|
|
826
|
+
stdin,
|
|
827
|
+
stdout: 'pipe',
|
|
828
|
+
stderr: 'pipe',
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
if (
|
|
832
|
+
stdinConfig?.needsManualStdin &&
|
|
833
|
+
stdinConfig.stdinData &&
|
|
834
|
+
proc.stdin
|
|
835
|
+
) {
|
|
836
|
+
await writeBunStdin(proc, stdinConfig.stdinData);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
processes.push(proc);
|
|
840
|
+
|
|
841
|
+
if (i < commands.length - 1) {
|
|
842
|
+
const [readStream, pipeStream] = proc.stdout.tee();
|
|
843
|
+
currentStream = pipeStream;
|
|
844
|
+
(async () => {
|
|
845
|
+
for await (const _chunk of readStream) {
|
|
846
|
+
/* consume */
|
|
847
|
+
}
|
|
848
|
+
})();
|
|
849
|
+
} else {
|
|
850
|
+
currentStream = proc.stdout;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
collectStderrAsync(this, proc, i === commands.length - 1, collector);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const lastProc = processes[processes.length - 1];
|
|
857
|
+
let finalOutput = '';
|
|
858
|
+
|
|
859
|
+
for await (const chunk of lastProc.stdout) {
|
|
860
|
+
const buf = Buffer.from(chunk);
|
|
861
|
+
finalOutput += buf.toString();
|
|
862
|
+
if (this.options.mirror) {
|
|
863
|
+
safeWrite(process.stdout, buf);
|
|
864
|
+
}
|
|
865
|
+
this._emitProcessedData('stdout', buf);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const exitCodes = await Promise.all(processes.map((p) => p.exited));
|
|
869
|
+
checkPipefail(exitCodes, globalShellSettings);
|
|
870
|
+
|
|
871
|
+
const result = createResult({
|
|
872
|
+
code: exitCodes[exitCodes.length - 1] || 0,
|
|
873
|
+
stdout: finalOutput,
|
|
874
|
+
stderr: collector.stderr,
|
|
875
|
+
stdin: getStdinString(this.options),
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
this.finish(result);
|
|
879
|
+
throwErrexitError(result, globalShellSettings);
|
|
880
|
+
return result;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
ProcessRunner.prototype._runMixedStreamingPipeline = async function (
|
|
884
|
+
commands
|
|
885
|
+
) {
|
|
886
|
+
trace(
|
|
887
|
+
'ProcessRunner',
|
|
888
|
+
() => `_runMixedStreamingPipeline | cmds=${commands.length}`
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
let currentInputStream = createInitialInputStream(this.options);
|
|
892
|
+
let finalOutput = '';
|
|
893
|
+
const collector = { stderr: '' };
|
|
894
|
+
|
|
895
|
+
for (let i = 0; i < commands.length; i++) {
|
|
896
|
+
const command = commands[i];
|
|
897
|
+
const { cmd, args } = command;
|
|
898
|
+
const isLastCommand = i === commands.length - 1;
|
|
899
|
+
|
|
900
|
+
if (isVirtualCommandsEnabled() && virtualCommands.has(cmd)) {
|
|
901
|
+
const handler = virtualCommands.get(cmd);
|
|
902
|
+
const argValues = getArgValues(args);
|
|
903
|
+
const inputData = currentInputStream
|
|
904
|
+
? await this._readStreamToString(currentInputStream)
|
|
905
|
+
: '';
|
|
906
|
+
|
|
907
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
908
|
+
const chunks = [];
|
|
909
|
+
const self = this;
|
|
910
|
+
currentInputStream = new ReadableStream({
|
|
911
|
+
async start(controller) {
|
|
912
|
+
const { stdin: _, ...opts } = self.options;
|
|
913
|
+
for await (const chunk of handler({
|
|
914
|
+
args: argValues,
|
|
915
|
+
stdin: inputData,
|
|
916
|
+
...opts,
|
|
917
|
+
})) {
|
|
918
|
+
const data = Buffer.from(chunk);
|
|
919
|
+
controller.enqueue(data);
|
|
920
|
+
if (isLastCommand) {
|
|
921
|
+
chunks.push(data);
|
|
922
|
+
if (self.options.mirror) {
|
|
923
|
+
safeWrite(process.stdout, data);
|
|
924
|
+
}
|
|
925
|
+
self.emit('stdout', data);
|
|
926
|
+
self.emit('data', { type: 'stdout', data });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
controller.close();
|
|
930
|
+
if (isLastCommand) {
|
|
931
|
+
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
} else {
|
|
936
|
+
const { stdin: _, ...opts } = this.options;
|
|
937
|
+
const result = await handler({
|
|
938
|
+
args: argValues,
|
|
939
|
+
stdin: inputData,
|
|
940
|
+
...opts,
|
|
941
|
+
});
|
|
942
|
+
const outputData = result.stdout || '';
|
|
943
|
+
if (isLastCommand) {
|
|
944
|
+
finalOutput = outputData;
|
|
945
|
+
const buf = Buffer.from(outputData);
|
|
946
|
+
if (this.options.mirror) {
|
|
947
|
+
safeWrite(process.stdout, buf);
|
|
948
|
+
}
|
|
949
|
+
this._emitProcessedData('stdout', buf);
|
|
950
|
+
}
|
|
951
|
+
currentInputStream = createStringStream(outputData);
|
|
952
|
+
if (result.stderr) {
|
|
953
|
+
collector.stderr += result.stderr;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
const commandStr = buildCommandParts(command).join(' ');
|
|
958
|
+
const proc = spawnShellCommand(commandStr, {
|
|
959
|
+
cwd: this.options.cwd,
|
|
960
|
+
env: this.options.env,
|
|
961
|
+
stdin: currentInputStream ? 'pipe' : 'ignore',
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
pipeStreamToProcess(currentInputStream, proc);
|
|
965
|
+
currentInputStream = proc.stdout;
|
|
966
|
+
collectStderrAsync(this, proc, isLastCommand, collector);
|
|
967
|
+
|
|
968
|
+
if (isLastCommand) {
|
|
969
|
+
finalOutput = await collectFinalStdout(this, proc);
|
|
970
|
+
await proc.exited;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const result = createResult({
|
|
976
|
+
code: 0,
|
|
977
|
+
stdout: finalOutput,
|
|
978
|
+
stderr: collector.stderr,
|
|
979
|
+
stdin: getStdinString(this.options),
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
this.finish(result);
|
|
983
|
+
return result;
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
ProcessRunner.prototype._runPipelineNonStreaming = async function (commands) {
|
|
987
|
+
trace(
|
|
988
|
+
'ProcessRunner',
|
|
989
|
+
() => `_runPipelineNonStreaming | cmds=${commands.length}`
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const currentOutput = '';
|
|
993
|
+
let currentInput = getStdinString(this.options);
|
|
994
|
+
const pipelineDeps = { virtualCommands, globalShellSettings };
|
|
995
|
+
|
|
996
|
+
for (let i = 0; i < commands.length; i++) {
|
|
997
|
+
const command = commands[i];
|
|
998
|
+
const isLastCommand = i === commands.length - 1;
|
|
999
|
+
const isVirtual =
|
|
1000
|
+
isVirtualCommandsEnabled() && virtualCommands.has(command.cmd);
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
const handleResult = isVirtual
|
|
1004
|
+
? await handleVirtualPipelineCommand(
|
|
1005
|
+
this,
|
|
1006
|
+
command,
|
|
1007
|
+
currentInput,
|
|
1008
|
+
isLastCommand,
|
|
1009
|
+
pipelineDeps
|
|
1010
|
+
)
|
|
1011
|
+
: await handleShellPipelineCommand(
|
|
1012
|
+
this,
|
|
1013
|
+
command,
|
|
1014
|
+
currentInput,
|
|
1015
|
+
isLastCommand,
|
|
1016
|
+
pipelineDeps
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
if (handleResult.finalResult) {
|
|
1020
|
+
return handleResult.finalResult;
|
|
1021
|
+
}
|
|
1022
|
+
currentInput = handleResult.input;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return handlePipelineError(
|
|
1025
|
+
this,
|
|
1026
|
+
error,
|
|
1027
|
+
currentOutput,
|
|
1028
|
+
globalShellSettings
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
ProcessRunner.prototype._runPipeline = function (commands) {
|
|
1035
|
+
trace(
|
|
1036
|
+
'ProcessRunner',
|
|
1037
|
+
() =>
|
|
1038
|
+
`_runPipeline ENTER | ${JSON.stringify(
|
|
1039
|
+
{
|
|
1040
|
+
commandsCount: commands.length,
|
|
1041
|
+
},
|
|
1042
|
+
null,
|
|
1043
|
+
2
|
|
1044
|
+
)}`
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
if (commands.length === 0) {
|
|
1048
|
+
trace(
|
|
1049
|
+
'ProcessRunner',
|
|
1050
|
+
() =>
|
|
1051
|
+
`BRANCH: _runPipeline => NO_COMMANDS | ${JSON.stringify({}, null, 2)}`
|
|
1052
|
+
);
|
|
1053
|
+
return createResult({
|
|
1054
|
+
code: 1,
|
|
1055
|
+
stdout: '',
|
|
1056
|
+
stderr: 'No commands in pipeline',
|
|
1057
|
+
stdin: '',
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (isBun) {
|
|
1062
|
+
trace(
|
|
1063
|
+
'ProcessRunner',
|
|
1064
|
+
() =>
|
|
1065
|
+
`BRANCH: _runPipeline => BUN_STREAMING | ${JSON.stringify({}, null, 2)}`
|
|
1066
|
+
);
|
|
1067
|
+
return this._runStreamingPipelineBun(commands);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
trace(
|
|
1071
|
+
'ProcessRunner',
|
|
1072
|
+
() =>
|
|
1073
|
+
`BRANCH: _runPipeline => NODE_NON_STREAMING | ${JSON.stringify({}, null, 2)}`
|
|
1074
|
+
);
|
|
1075
|
+
return this._runPipelineNonStreaming(commands);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
ProcessRunner.prototype._runProgrammaticPipeline = async function (
|
|
1079
|
+
source,
|
|
1080
|
+
destination
|
|
1081
|
+
) {
|
|
1082
|
+
trace(
|
|
1083
|
+
'ProcessRunner',
|
|
1084
|
+
() => `_runProgrammaticPipeline ENTER | ${JSON.stringify({}, null, 2)}`
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
trace('ProcessRunner', () => 'Executing source command');
|
|
1089
|
+
const sourceResult = await source;
|
|
1090
|
+
|
|
1091
|
+
if (sourceResult.code !== 0) {
|
|
1092
|
+
trace(
|
|
1093
|
+
'ProcessRunner',
|
|
1094
|
+
() =>
|
|
1095
|
+
`BRANCH: _runProgrammaticPipeline => SOURCE_FAILED | ${JSON.stringify(
|
|
1096
|
+
{
|
|
1097
|
+
code: sourceResult.code,
|
|
1098
|
+
stderr: sourceResult.stderr,
|
|
1099
|
+
},
|
|
1100
|
+
null,
|
|
1101
|
+
2
|
|
1102
|
+
)}`
|
|
1103
|
+
);
|
|
1104
|
+
return sourceResult;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const ProcessRunnerRef = this.constructor;
|
|
1108
|
+
const destWithStdin = new ProcessRunnerRef(destination.spec, {
|
|
1109
|
+
...destination.options,
|
|
1110
|
+
stdin: sourceResult.stdout,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const destResult = await destWithStdin;
|
|
1114
|
+
|
|
1115
|
+
trace(
|
|
1116
|
+
'ProcessRunner',
|
|
1117
|
+
() =>
|
|
1118
|
+
`destResult debug | ${JSON.stringify(
|
|
1119
|
+
{
|
|
1120
|
+
code: destResult.code,
|
|
1121
|
+
codeType: typeof destResult.code,
|
|
1122
|
+
hasCode: 'code' in destResult,
|
|
1123
|
+
keys: Object.keys(destResult),
|
|
1124
|
+
resultType: typeof destResult,
|
|
1125
|
+
fullResult: JSON.stringify(destResult, null, 2).slice(0, 200),
|
|
1126
|
+
},
|
|
1127
|
+
null,
|
|
1128
|
+
2
|
|
1129
|
+
)}`
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
return createResult({
|
|
1133
|
+
code: destResult.code,
|
|
1134
|
+
stdout: destResult.stdout,
|
|
1135
|
+
stderr: sourceResult.stderr + destResult.stderr,
|
|
1136
|
+
stdin: sourceResult.stdin,
|
|
1137
|
+
});
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
const result = createResult({
|
|
1140
|
+
code: error.code ?? 1,
|
|
1141
|
+
stdout: '',
|
|
1142
|
+
stderr: error.message || 'Pipeline execution failed',
|
|
1143
|
+
stdin:
|
|
1144
|
+
this.options.stdin && typeof this.options.stdin === 'string'
|
|
1145
|
+
? this.options.stdin
|
|
1146
|
+
: this.options.stdin && Buffer.isBuffer(this.options.stdin)
|
|
1147
|
+
? this.options.stdin.toString('utf8')
|
|
1148
|
+
: '',
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const buf = Buffer.from(result.stderr);
|
|
1152
|
+
if (this.options.mirror) {
|
|
1153
|
+
safeWrite(process.stderr, buf);
|
|
1154
|
+
}
|
|
1155
|
+
this._emitProcessedData('stderr', buf);
|
|
1156
|
+
|
|
1157
|
+
this.finish(result);
|
|
1158
|
+
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
}
|