command-stream 0.1.0 → 0.3.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/$.mjs +1315 -912
- package/package.json +1 -1
package/$.mjs
CHANGED
|
@@ -6,37 +6,351 @@
|
|
|
6
6
|
// 4. Stream access: $`command`.stdout, $`command`.stderr
|
|
7
7
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
|
-
import
|
|
9
|
+
import cp from 'child_process';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
10
12
|
|
|
11
13
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
12
14
|
|
|
13
|
-
// Verbose tracing for debugging (enabled in CI or when COMMAND_STREAM_VERBOSE is set)
|
|
14
15
|
const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';
|
|
15
16
|
|
|
17
|
+
// Interactive commands that need TTY forwarding by default
|
|
18
|
+
const INTERACTIVE_COMMANDS = new Set([
|
|
19
|
+
'top', 'htop', 'btop', 'less', 'more', 'vi', 'vim', 'nano', 'emacs',
|
|
20
|
+
'man', 'pager', 'watch', 'tmux', 'screen', 'ssh', 'ftp', 'sftp',
|
|
21
|
+
'mysql', 'psql', 'redis-cli', 'mongo', 'sqlite3', 'irb', 'python',
|
|
22
|
+
'node', 'repl', 'gdb', 'lldb', 'bc', 'dc', 'ed'
|
|
23
|
+
]);
|
|
24
|
+
|
|
16
25
|
// Trace function for verbose logging
|
|
17
|
-
function trace(category,
|
|
26
|
+
function trace(category, messageOrFunc) {
|
|
18
27
|
if (!VERBOSE) return;
|
|
19
|
-
|
|
28
|
+
const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
|
|
20
29
|
const timestamp = new Date().toISOString();
|
|
21
|
-
|
|
22
|
-
console.error(`[TRACE ${timestamp}] [${category}] ${message}${dataStr}`);
|
|
30
|
+
console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
//
|
|
26
|
-
function
|
|
27
|
-
if (!
|
|
33
|
+
// Check if a command is interactive and needs TTY forwarding
|
|
34
|
+
function isInteractiveCommand(command) {
|
|
35
|
+
if (!command || typeof command !== 'string') return false;
|
|
36
|
+
|
|
37
|
+
// Extract command and arguments from shell command string
|
|
38
|
+
const parts = command.trim().split(/\s+/);
|
|
39
|
+
const commandName = parts[0];
|
|
40
|
+
const baseName = path.basename(commandName);
|
|
41
|
+
|
|
42
|
+
// Special handling for commands that are only interactive when run without arguments/scripts
|
|
43
|
+
if (baseName === 'node' || baseName === 'python' || baseName === 'python3') {
|
|
44
|
+
// These are only interactive when run without a script file
|
|
45
|
+
// If there are additional arguments (like a script file), they're not interactive
|
|
46
|
+
return parts.length === 1;
|
|
47
|
+
}
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
return INTERACTIVE_COMMANDS.has(baseName);
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
|
|
53
|
+
// Track parent stream state for graceful shutdown
|
|
54
|
+
let parentStreamsMonitored = false;
|
|
55
|
+
const activeProcessRunners = new Set();
|
|
56
|
+
|
|
57
|
+
function monitorParentStreams() {
|
|
58
|
+
if (parentStreamsMonitored) return;
|
|
59
|
+
parentStreamsMonitored = true;
|
|
60
|
+
|
|
61
|
+
const checkParentStream = (stream, name) => {
|
|
62
|
+
if (stream && typeof stream.on === 'function') {
|
|
63
|
+
stream.on('close', () => {
|
|
64
|
+
trace('ProcessRunner', () => `Parent ${name} closed - triggering graceful shutdown | ${JSON.stringify({ activeProcesses: activeProcessRunners.size }, null, 2)}`);
|
|
65
|
+
for (const runner of activeProcessRunners) {
|
|
66
|
+
runner._handleParentStreamClosure();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
checkParentStream(process.stdout, 'stdout');
|
|
73
|
+
checkParentStream(process.stderr, 'stderr');
|
|
37
74
|
}
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
function safeWrite(stream, data, processRunner = null) {
|
|
77
|
+
monitorParentStreams();
|
|
78
|
+
|
|
79
|
+
if (!StreamUtils.isStreamWritable(stream)) {
|
|
80
|
+
trace('ProcessRunner', () => `safeWrite skipped - stream not writable | ${JSON.stringify({
|
|
81
|
+
hasStream: !!stream,
|
|
82
|
+
writable: stream?.writable,
|
|
83
|
+
destroyed: stream?.destroyed,
|
|
84
|
+
closed: stream?.closed
|
|
85
|
+
}, null, 2)}`);
|
|
86
|
+
|
|
87
|
+
if (processRunner && (stream === process.stdout || stream === process.stderr)) {
|
|
88
|
+
processRunner._handleParentStreamClosure();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return stream.write(data);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
trace('ProcessRunner', () => `safeWrite error | ${JSON.stringify({
|
|
98
|
+
error: error.message,
|
|
99
|
+
code: error.code,
|
|
100
|
+
writable: stream.writable,
|
|
101
|
+
destroyed: stream.destroyed
|
|
102
|
+
}, null, 2)}`);
|
|
103
|
+
|
|
104
|
+
if (error.code === 'EPIPE' && processRunner &&
|
|
105
|
+
(stream === process.stdout || stream === process.stderr)) {
|
|
106
|
+
processRunner._handleParentStreamClosure();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Stream utility functions for safe operations and error handling
|
|
114
|
+
const StreamUtils = {
|
|
115
|
+
/**
|
|
116
|
+
* Check if a stream is safe to write to
|
|
117
|
+
*/
|
|
118
|
+
isStreamWritable(stream) {
|
|
119
|
+
return stream && stream.writable && !stream.destroyed && !stream.closed;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Add standardized error handler to stdin streams
|
|
124
|
+
*/
|
|
125
|
+
addStdinErrorHandler(stream, contextName = 'stdin', onNonEpipeError = null) {
|
|
126
|
+
if (stream && typeof stream.on === 'function') {
|
|
127
|
+
stream.on('error', (error) => {
|
|
128
|
+
const handled = this.handleStreamError(error, `${contextName} error event`, false);
|
|
129
|
+
if (!handled && onNonEpipeError) {
|
|
130
|
+
onNonEpipeError(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Safely write to a stream with comprehensive error handling
|
|
138
|
+
*/
|
|
139
|
+
safeStreamWrite(stream, data, contextName = 'stream') {
|
|
140
|
+
if (!this.isStreamWritable(stream)) {
|
|
141
|
+
trace('ProcessRunner', () => `${contextName} write skipped - not writable | ${JSON.stringify({
|
|
142
|
+
hasStream: !!stream,
|
|
143
|
+
writable: stream?.writable,
|
|
144
|
+
destroyed: stream?.destroyed,
|
|
145
|
+
closed: stream?.closed
|
|
146
|
+
}, null, 2)}`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = stream.write(data);
|
|
152
|
+
trace('ProcessRunner', () => `${contextName} write successful | ${JSON.stringify({
|
|
153
|
+
dataLength: data?.length || 0
|
|
154
|
+
}, null, 2)}`);
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.code !== 'EPIPE') {
|
|
158
|
+
trace('ProcessRunner', () => `${contextName} write error | ${JSON.stringify({
|
|
159
|
+
error: error.message,
|
|
160
|
+
code: error.code,
|
|
161
|
+
isEPIPE: false
|
|
162
|
+
}, null, 2)}`);
|
|
163
|
+
throw error; // Re-throw non-EPIPE errors
|
|
164
|
+
} else {
|
|
165
|
+
trace('ProcessRunner', () => `${contextName} EPIPE error (ignored) | ${JSON.stringify({
|
|
166
|
+
error: error.message,
|
|
167
|
+
code: error.code,
|
|
168
|
+
isEPIPE: true
|
|
169
|
+
}, null, 2)}`);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Safely end a stream with error handling
|
|
177
|
+
*/
|
|
178
|
+
safeStreamEnd(stream, contextName = 'stream') {
|
|
179
|
+
if (!this.isStreamWritable(stream) || typeof stream.end !== 'function') {
|
|
180
|
+
trace('ProcessRunner', () => `${contextName} end skipped - not available | ${JSON.stringify({
|
|
181
|
+
hasStream: !!stream,
|
|
182
|
+
hasEnd: stream && typeof stream.end === 'function',
|
|
183
|
+
writable: stream?.writable
|
|
184
|
+
}, null, 2)}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
stream.end();
|
|
190
|
+
trace('ProcessRunner', () => `${contextName} ended successfully`);
|
|
191
|
+
return true;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error.code !== 'EPIPE') {
|
|
194
|
+
trace('ProcessRunner', () => `${contextName} end error | ${JSON.stringify({
|
|
195
|
+
error: error.message,
|
|
196
|
+
code: error.code
|
|
197
|
+
}, null, 2)}`);
|
|
198
|
+
} else {
|
|
199
|
+
trace('ProcessRunner', () => `${contextName} EPIPE on end (ignored) | ${JSON.stringify({
|
|
200
|
+
error: error.message,
|
|
201
|
+
code: error.code
|
|
202
|
+
}, null, 2)}`);
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Setup comprehensive stdin handling (error handler + safe operations)
|
|
210
|
+
*/
|
|
211
|
+
setupStdinHandling(stream, contextName = 'stdin') {
|
|
212
|
+
this.addStdinErrorHandler(stream, contextName);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
write: (data) => this.safeStreamWrite(stream, data, contextName),
|
|
216
|
+
end: () => this.safeStreamEnd(stream, contextName),
|
|
217
|
+
isWritable: () => this.isStreamWritable(stream)
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle stream errors with consistent EPIPE behavior
|
|
223
|
+
*/
|
|
224
|
+
handleStreamError(error, contextName, shouldThrow = true) {
|
|
225
|
+
if (error.code !== 'EPIPE') {
|
|
226
|
+
trace('ProcessRunner', () => `${contextName} error | ${JSON.stringify({
|
|
227
|
+
error: error.message,
|
|
228
|
+
code: error.code,
|
|
229
|
+
isEPIPE: false
|
|
230
|
+
}, null, 2)}`);
|
|
231
|
+
if (shouldThrow) throw error;
|
|
232
|
+
return false;
|
|
233
|
+
} else {
|
|
234
|
+
trace('ProcessRunner', () => `${contextName} EPIPE error (ignored) | ${JSON.stringify({
|
|
235
|
+
error: error.message,
|
|
236
|
+
code: error.code,
|
|
237
|
+
isEPIPE: true
|
|
238
|
+
}, null, 2)}`);
|
|
239
|
+
return true; // EPIPE handled gracefully
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Detect if stream supports Bun-style writing
|
|
245
|
+
*/
|
|
246
|
+
isBunStream(stream) {
|
|
247
|
+
return isBun && stream && typeof stream.getWriter === 'function';
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Detect if stream supports Node.js-style writing
|
|
252
|
+
*/
|
|
253
|
+
isNodeStream(stream) {
|
|
254
|
+
return stream && typeof stream.write === 'function';
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Write to either Bun or Node.js style stream
|
|
259
|
+
*/
|
|
260
|
+
async writeToStream(stream, data, contextName = 'stream') {
|
|
261
|
+
if (this.isBunStream(stream)) {
|
|
262
|
+
try {
|
|
263
|
+
const writer = stream.getWriter();
|
|
264
|
+
await writer.write(data);
|
|
265
|
+
writer.releaseLock();
|
|
266
|
+
return true;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return this.handleStreamError(error, `${contextName} Bun writer`, false);
|
|
269
|
+
}
|
|
270
|
+
} else if (this.isNodeStream(stream)) {
|
|
271
|
+
try {
|
|
272
|
+
stream.write(data);
|
|
273
|
+
return true;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return this.handleStreamError(error, `${contextName} Node writer`, false);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Virtual command utility functions for consistent behavior and error handling
|
|
283
|
+
const VirtualUtils = {
|
|
284
|
+
/**
|
|
285
|
+
* Create standardized error response for missing operands
|
|
286
|
+
*/
|
|
287
|
+
missingOperandError(commandName, customMessage = null) {
|
|
288
|
+
const message = customMessage || `${commandName}: missing operand`;
|
|
289
|
+
return { stderr: message, code: 1 };
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create standardized error response for invalid arguments
|
|
294
|
+
*/
|
|
295
|
+
invalidArgumentError(commandName, message) {
|
|
296
|
+
return { stderr: `${commandName}: ${message}`, code: 1 };
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create standardized success response
|
|
301
|
+
*/
|
|
302
|
+
success(stdout = '', code = 0) {
|
|
303
|
+
return { stdout, stderr: '', code };
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create standardized error response
|
|
308
|
+
*/
|
|
309
|
+
error(stderr = '', code = 1) {
|
|
310
|
+
return { stdout: '', stderr, code };
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate that command has required number of arguments
|
|
315
|
+
*/
|
|
316
|
+
validateArgs(args, minCount, commandName) {
|
|
317
|
+
if (args.length < minCount) {
|
|
318
|
+
if (minCount === 1) {
|
|
319
|
+
return this.missingOperandError(commandName);
|
|
320
|
+
} else {
|
|
321
|
+
return this.invalidArgumentError(commandName, `requires at least ${minCount} arguments`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null; // No error
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Resolve file path with optional cwd parameter
|
|
329
|
+
*/
|
|
330
|
+
resolvePath(filePath, cwd = null) {
|
|
331
|
+
const basePath = cwd || process.cwd();
|
|
332
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Safe file system operation wrapper
|
|
337
|
+
*/
|
|
338
|
+
async safeFsOperation(operation, errorPrefix) {
|
|
339
|
+
try {
|
|
340
|
+
return await operation();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return this.error(`${errorPrefix}: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create async wrapper for Promise-based operations
|
|
348
|
+
*/
|
|
349
|
+
createAsyncWrapper(promiseFactory) {
|
|
350
|
+
return new Promise(promiseFactory);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
40
354
|
let globalShellSettings = {
|
|
41
355
|
errexit: false, // set -e equivalent: exit on error
|
|
42
356
|
verbose: false, // set -v equivalent: print commands
|
|
@@ -45,24 +359,20 @@ let globalShellSettings = {
|
|
|
45
359
|
nounset: false // set -u equivalent: error on undefined variables
|
|
46
360
|
};
|
|
47
361
|
|
|
48
|
-
// Helper function to create result objects with Bun.$ compatibility
|
|
49
362
|
function createResult({ code, stdout = '', stderr = '', stdin = '' }) {
|
|
50
363
|
return {
|
|
51
364
|
code,
|
|
52
365
|
stdout,
|
|
53
366
|
stderr,
|
|
54
367
|
stdin,
|
|
55
|
-
// Bun.$ compatibility method
|
|
56
368
|
async text() {
|
|
57
369
|
return stdout;
|
|
58
370
|
}
|
|
59
371
|
};
|
|
60
372
|
}
|
|
61
373
|
|
|
62
|
-
// Virtual command registry - unified system for all commands
|
|
63
374
|
const virtualCommands = new Map();
|
|
64
375
|
|
|
65
|
-
// Global flag to enable/disable virtual commands (for backward compatibility)
|
|
66
376
|
let virtualCommandsEnabled = true;
|
|
67
377
|
|
|
68
378
|
// EventEmitter-like implementation
|
|
@@ -76,9 +386,9 @@ class StreamEmitter {
|
|
|
76
386
|
this.listeners.set(event, []);
|
|
77
387
|
}
|
|
78
388
|
this.listeners.get(event).push(listener);
|
|
79
|
-
|
|
389
|
+
|
|
80
390
|
// No auto-start - explicit start() or await will start the process
|
|
81
|
-
|
|
391
|
+
|
|
82
392
|
return this;
|
|
83
393
|
}
|
|
84
394
|
|
|
@@ -113,28 +423,28 @@ function quote(value) {
|
|
|
113
423
|
}
|
|
114
424
|
|
|
115
425
|
function buildShellCommand(strings, values) {
|
|
116
|
-
|
|
426
|
+
trace('Utils', () => `buildShellCommand ENTER | ${JSON.stringify({
|
|
117
427
|
stringsLength: strings.length,
|
|
118
428
|
valuesLength: values.length
|
|
119
|
-
});
|
|
120
|
-
|
|
429
|
+
}, null, 2)}`);
|
|
430
|
+
|
|
121
431
|
let out = '';
|
|
122
432
|
for (let i = 0; i < strings.length; i++) {
|
|
123
433
|
out += strings[i];
|
|
124
434
|
if (i < values.length) {
|
|
125
435
|
const v = values[i];
|
|
126
436
|
if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
|
|
127
|
-
|
|
437
|
+
trace('Utils', () => `BRANCH: buildShellCommand => RAW_VALUE | ${JSON.stringify({ value: String(v.raw) }, null, 2)}`);
|
|
128
438
|
out += String(v.raw);
|
|
129
439
|
} else {
|
|
130
440
|
const quoted = quote(v);
|
|
131
|
-
|
|
441
|
+
trace('Utils', () => `BRANCH: buildShellCommand => QUOTED_VALUE | ${JSON.stringify({ original: v, quoted }, null, 2)}`);
|
|
132
442
|
out += quoted;
|
|
133
443
|
}
|
|
134
444
|
}
|
|
135
445
|
}
|
|
136
|
-
|
|
137
|
-
|
|
446
|
+
|
|
447
|
+
trace('Utils', () => `buildShellCommand EXIT | ${JSON.stringify({ command: out }, null, 2)}`);
|
|
138
448
|
return out;
|
|
139
449
|
}
|
|
140
450
|
|
|
@@ -155,12 +465,12 @@ async function pumpReadable(readable, onChunk) {
|
|
|
155
465
|
class ProcessRunner extends StreamEmitter {
|
|
156
466
|
constructor(spec, options = {}) {
|
|
157
467
|
super();
|
|
158
|
-
|
|
159
|
-
|
|
468
|
+
|
|
469
|
+
trace('ProcessRunner', () => `constructor ENTER | ${JSON.stringify({
|
|
160
470
|
spec: typeof spec === 'object' ? { ...spec, command: spec.command?.slice(0, 100) } : spec,
|
|
161
|
-
options
|
|
162
|
-
});
|
|
163
|
-
|
|
471
|
+
options
|
|
472
|
+
}, null, 2)}`);
|
|
473
|
+
|
|
164
474
|
this.spec = spec;
|
|
165
475
|
this.options = {
|
|
166
476
|
mirror: true,
|
|
@@ -170,168 +480,293 @@ class ProcessRunner extends StreamEmitter {
|
|
|
170
480
|
env: undefined,
|
|
171
481
|
...options
|
|
172
482
|
};
|
|
173
|
-
|
|
483
|
+
|
|
174
484
|
this.outChunks = this.options.capture ? [] : null;
|
|
175
485
|
this.errChunks = this.options.capture ? [] : null;
|
|
176
|
-
this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
486
|
+
this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
|
|
487
|
+
this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
|
|
488
|
+
[Buffer.from(this.options.stdin)] : [];
|
|
489
|
+
|
|
180
490
|
this.result = null;
|
|
181
491
|
this.child = null;
|
|
182
492
|
this.started = false;
|
|
183
493
|
this.finished = false;
|
|
184
|
-
|
|
494
|
+
|
|
185
495
|
// Promise for awaiting final result
|
|
186
496
|
this.promise = null;
|
|
187
|
-
|
|
188
|
-
// Track the execution mode
|
|
497
|
+
|
|
189
498
|
this._mode = null; // 'async' or 'sync'
|
|
190
|
-
|
|
191
|
-
// Cancellation support for virtual commands
|
|
499
|
+
|
|
192
500
|
this._cancelled = false;
|
|
193
501
|
this._virtualGenerator = null;
|
|
194
502
|
this._abortController = new AbortController();
|
|
503
|
+
|
|
504
|
+
activeProcessRunners.add(this);
|
|
505
|
+
|
|
506
|
+
// Track finished state changes to trigger cleanup
|
|
507
|
+
this._finished = false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
get finished() {
|
|
511
|
+
return this._finished;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
_emitProcessedData(type, buf) {
|
|
515
|
+
const processedBuf = processOutput(buf, this.options.ansi);
|
|
516
|
+
this.emit(type, processedBuf);
|
|
517
|
+
this.emit('data', { type, data: processedBuf });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async _forwardTTYStdin() {
|
|
521
|
+
if (!process.stdin.isTTY || !this.child.stdin) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
// Set raw mode to forward keystrokes immediately
|
|
527
|
+
if (process.stdin.setRawMode) {
|
|
528
|
+
process.stdin.setRawMode(true);
|
|
529
|
+
}
|
|
530
|
+
process.stdin.resume();
|
|
531
|
+
|
|
532
|
+
// Forward stdin data to child process
|
|
533
|
+
const onData = (chunk) => {
|
|
534
|
+
if (this.child.stdin) {
|
|
535
|
+
if (isBun && this.child.stdin.write) {
|
|
536
|
+
this.child.stdin.write(chunk);
|
|
537
|
+
} else if (this.child.stdin.write) {
|
|
538
|
+
this.child.stdin.write(chunk);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const cleanup = () => {
|
|
544
|
+
process.stdin.removeListener('data', onData);
|
|
545
|
+
if (process.stdin.setRawMode) {
|
|
546
|
+
process.stdin.setRawMode(false);
|
|
547
|
+
}
|
|
548
|
+
process.stdin.pause();
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
process.stdin.on('data', onData);
|
|
552
|
+
|
|
553
|
+
// Clean up when child process exits
|
|
554
|
+
const childExit = isBun ? this.child.exited : new Promise((resolve) => {
|
|
555
|
+
this.child.once('close', resolve);
|
|
556
|
+
this.child.once('exit', resolve);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
childExit.then(cleanup).catch(cleanup);
|
|
560
|
+
|
|
561
|
+
return childExit;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
trace('ProcessRunner', () => `TTY stdin forwarding error | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
set finished(value) {
|
|
568
|
+
if (value === true && this._finished === false) {
|
|
569
|
+
this._finished = true;
|
|
570
|
+
this._cleanup(); // Trigger cleanup when process finishes
|
|
571
|
+
} else {
|
|
572
|
+
this._finished = value;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
_handleParentStreamClosure() {
|
|
577
|
+
if (this.finished || this._cancelled) return;
|
|
578
|
+
|
|
579
|
+
trace('ProcessRunner', () => `Handling parent stream closure | ${JSON.stringify({
|
|
580
|
+
started: this.started,
|
|
581
|
+
hasChild: !!this.child,
|
|
582
|
+
command: this.spec.command?.slice(0, 50) || this.spec.file
|
|
583
|
+
}, null, 2)}`);
|
|
584
|
+
|
|
585
|
+
this._cancelled = true;
|
|
586
|
+
|
|
587
|
+
// Cancel abort controller for virtual commands
|
|
588
|
+
if (this._abortController) {
|
|
589
|
+
this._abortController.abort();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Gracefully close child process if it exists
|
|
593
|
+
if (this.child) {
|
|
594
|
+
try {
|
|
595
|
+
// Close stdin first to signal completion
|
|
596
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') {
|
|
597
|
+
this.child.stdin.end();
|
|
598
|
+
} else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
|
|
599
|
+
const writer = this.child.stdin.getWriter();
|
|
600
|
+
writer.close().catch(() => { }); // Ignore close errors
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
setTimeout(() => {
|
|
604
|
+
if (this.child && !this.finished) {
|
|
605
|
+
trace('ProcessRunner', () => 'Terminating child process after parent stream closure');
|
|
606
|
+
if (typeof this.child.kill === 'function') {
|
|
607
|
+
this.child.kill('SIGTERM');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}, 100);
|
|
611
|
+
|
|
612
|
+
} catch (error) {
|
|
613
|
+
trace('ProcessRunner', () => `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
activeProcessRunners.delete(this);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
_cleanup() {
|
|
621
|
+
activeProcessRunners.delete(this);
|
|
195
622
|
}
|
|
196
623
|
|
|
197
624
|
// Unified start method that can work in both async and sync modes
|
|
198
625
|
start(options = {}) {
|
|
199
626
|
const mode = options.mode || 'async';
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
627
|
+
|
|
628
|
+
trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({ mode, options }, null, 2)}`);
|
|
629
|
+
|
|
203
630
|
if (mode === 'sync') {
|
|
204
|
-
|
|
631
|
+
trace('ProcessRunner', () => `BRANCH: mode => sync | ${JSON.stringify({}, null, 2)}`);
|
|
205
632
|
return this._startSync();
|
|
206
633
|
} else {
|
|
207
|
-
|
|
634
|
+
trace('ProcessRunner', () => `BRANCH: mode => async | ${JSON.stringify({}, null, 2)}`);
|
|
208
635
|
return this._startAsync();
|
|
209
636
|
}
|
|
210
637
|
}
|
|
211
|
-
|
|
638
|
+
|
|
212
639
|
// Shortcut for sync mode
|
|
213
640
|
sync() {
|
|
214
641
|
return this.start({ mode: 'sync' });
|
|
215
642
|
}
|
|
216
|
-
|
|
643
|
+
|
|
217
644
|
// Shortcut for async mode
|
|
218
645
|
async() {
|
|
219
646
|
return this.start({ mode: 'async' });
|
|
220
647
|
}
|
|
221
|
-
|
|
648
|
+
|
|
222
649
|
async _startAsync() {
|
|
223
650
|
if (this.started) return this.promise;
|
|
224
651
|
if (this.promise) return this.promise;
|
|
225
|
-
|
|
652
|
+
|
|
226
653
|
this.promise = this._doStartAsync();
|
|
227
654
|
return this.promise;
|
|
228
655
|
}
|
|
229
|
-
|
|
656
|
+
|
|
230
657
|
async _doStartAsync() {
|
|
231
|
-
|
|
658
|
+
trace('ProcessRunner', () => `_doStartAsync ENTER | ${JSON.stringify({
|
|
232
659
|
mode: this.spec.mode,
|
|
233
660
|
command: this.spec.command?.slice(0, 100)
|
|
234
|
-
});
|
|
235
|
-
|
|
661
|
+
}, null, 2)}`);
|
|
662
|
+
|
|
236
663
|
this.started = true;
|
|
237
664
|
this._mode = 'async';
|
|
238
665
|
|
|
239
666
|
const { cwd, env, stdin } = this.options;
|
|
240
|
-
|
|
241
|
-
// Handle programmatic pipeline mode
|
|
667
|
+
|
|
242
668
|
if (this.spec.mode === 'pipeline') {
|
|
243
|
-
|
|
669
|
+
trace('ProcessRunner', () => `BRANCH: spec.mode => pipeline | ${JSON.stringify({
|
|
244
670
|
hasSource: !!this.spec.source,
|
|
245
671
|
hasDestination: !!this.spec.destination
|
|
246
|
-
});
|
|
672
|
+
}, null, 2)}`);
|
|
247
673
|
return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
|
|
248
674
|
}
|
|
249
|
-
|
|
250
|
-
// Check if this is a virtual command first
|
|
675
|
+
|
|
251
676
|
if (this.spec.mode === 'shell') {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Parse the command to check for virtual commands or pipelines
|
|
677
|
+
trace('ProcessRunner', () => `BRANCH: spec.mode => shell | ${JSON.stringify({}, null, 2)}`);
|
|
678
|
+
|
|
255
679
|
const parsed = this._parseCommand(this.spec.command);
|
|
256
|
-
trace('ProcessRunner',
|
|
680
|
+
trace('ProcessRunner', () => `Parsed command | ${JSON.stringify({
|
|
257
681
|
type: parsed?.type,
|
|
258
682
|
cmd: parsed?.cmd,
|
|
259
683
|
argsCount: parsed?.args?.length
|
|
260
|
-
});
|
|
261
|
-
|
|
684
|
+
}, null, 2)}`);
|
|
685
|
+
|
|
262
686
|
if (parsed) {
|
|
263
687
|
if (parsed.type === 'pipeline') {
|
|
264
|
-
|
|
265
|
-
commandCount: parsed.commands?.length
|
|
266
|
-
});
|
|
688
|
+
trace('ProcessRunner', () => `BRANCH: parsed.type => pipeline | ${JSON.stringify({
|
|
689
|
+
commandCount: parsed.commands?.length
|
|
690
|
+
}, null, 2)}`);
|
|
267
691
|
return await this._runPipeline(parsed.commands);
|
|
268
692
|
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
269
|
-
|
|
693
|
+
trace('ProcessRunner', () => `BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
|
|
270
694
|
isVirtual: true,
|
|
271
|
-
args: parsed.args
|
|
272
|
-
});
|
|
695
|
+
args: parsed.args
|
|
696
|
+
}, null, 2)}`);
|
|
273
697
|
return await this._runVirtual(parsed.cmd, parsed.args);
|
|
274
698
|
}
|
|
275
699
|
}
|
|
276
700
|
}
|
|
277
|
-
|
|
278
|
-
const spawnBun = (argv) => {
|
|
279
|
-
return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
|
|
280
|
-
};
|
|
281
|
-
const spawnNode = async (argv) => {
|
|
282
|
-
const cp = await import('child_process');
|
|
283
|
-
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
284
|
-
};
|
|
285
701
|
|
|
286
702
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
287
|
-
|
|
288
|
-
// Shell tracing (set -x equivalent)
|
|
703
|
+
|
|
289
704
|
if (globalShellSettings.xtrace) {
|
|
290
705
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
291
706
|
console.log(`+ ${traceCmd}`);
|
|
292
707
|
}
|
|
293
|
-
|
|
294
|
-
// Verbose mode (set -v equivalent)
|
|
708
|
+
|
|
295
709
|
if (globalShellSettings.verbose) {
|
|
296
710
|
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
297
711
|
console.log(verboseCmd);
|
|
298
712
|
}
|
|
299
|
-
|
|
713
|
+
|
|
714
|
+
// Detect if this is an interactive command that needs direct TTY access
|
|
715
|
+
// Only activate for interactive commands when we have a real TTY and the command is likely to need it
|
|
716
|
+
const isInteractive = stdin === 'inherit' &&
|
|
717
|
+
process.stdin.isTTY === true &&
|
|
718
|
+
process.stdout.isTTY === true &&
|
|
719
|
+
process.stderr.isTTY === true &&
|
|
720
|
+
(this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file));
|
|
721
|
+
|
|
722
|
+
const spawnBun = (argv) => {
|
|
723
|
+
if (isInteractive) {
|
|
724
|
+
// For interactive commands, use inherit to provide direct TTY access
|
|
725
|
+
return Bun.spawn(argv, { cwd, env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' });
|
|
726
|
+
}
|
|
727
|
+
return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
|
|
728
|
+
};
|
|
729
|
+
const spawnNode = async (argv) => {
|
|
730
|
+
if (isInteractive) {
|
|
731
|
+
// For interactive commands, use inherit to provide direct TTY access
|
|
732
|
+
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: 'inherit' });
|
|
733
|
+
}
|
|
734
|
+
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
735
|
+
};
|
|
736
|
+
|
|
300
737
|
const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
|
|
301
738
|
const preferNodeForInput = isBun && needsExplicitPipe;
|
|
302
739
|
this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
|
|
303
740
|
|
|
304
|
-
//
|
|
305
|
-
const outPump = pumpReadable(this.child.stdout, async (buf) => {
|
|
741
|
+
// For interactive commands with stdio: 'inherit', stdout/stderr will be null
|
|
742
|
+
const outPump = this.child.stdout ? pumpReadable(this.child.stdout, async (buf) => {
|
|
306
743
|
if (this.options.capture) this.outChunks.push(buf);
|
|
307
|
-
if (this.options.mirror) process.stdout
|
|
308
|
-
|
|
744
|
+
if (this.options.mirror) safeWrite(process.stdout, buf);
|
|
745
|
+
|
|
309
746
|
// Emit chunk events
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
});
|
|
747
|
+
this._emitProcessedData('stdout', buf);
|
|
748
|
+
}) : Promise.resolve();
|
|
313
749
|
|
|
314
|
-
|
|
315
|
-
const errPump = pumpReadable(this.child.stderr, async (buf) => {
|
|
750
|
+
const errPump = this.child.stderr ? pumpReadable(this.child.stderr, async (buf) => {
|
|
316
751
|
if (this.options.capture) this.errChunks.push(buf);
|
|
317
|
-
if (this.options.mirror) process.stderr
|
|
318
|
-
|
|
752
|
+
if (this.options.mirror) safeWrite(process.stderr, buf);
|
|
753
|
+
|
|
319
754
|
// Emit chunk events
|
|
320
|
-
this.
|
|
321
|
-
|
|
322
|
-
});
|
|
755
|
+
this._emitProcessedData('stderr', buf);
|
|
756
|
+
}) : Promise.resolve();
|
|
323
757
|
|
|
324
|
-
// Handle stdin
|
|
325
758
|
let stdinPumpPromise = Promise.resolve();
|
|
326
759
|
if (stdin === 'inherit') {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
stdinPumpPromise =
|
|
760
|
+
if (isInteractive) {
|
|
761
|
+
// For interactive commands with stdio: 'inherit', stdin is handled automatically
|
|
762
|
+
stdinPumpPromise = Promise.resolve();
|
|
330
763
|
} else {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
764
|
+
const isPipedIn = process.stdin && process.stdin.isTTY === false;
|
|
765
|
+
if (isPipedIn) {
|
|
766
|
+
stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
|
|
767
|
+
} else {
|
|
768
|
+
// For TTY (interactive terminal), forward stdin directly for non-interactive commands
|
|
769
|
+
stdinPumpPromise = this._forwardTTYStdin();
|
|
335
770
|
}
|
|
336
771
|
}
|
|
337
772
|
} else if (stdin === 'ignore') {
|
|
@@ -347,12 +782,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
347
782
|
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
348
783
|
|
|
349
784
|
// Debug: Check the raw exit code
|
|
350
|
-
trace('ProcessRunner',
|
|
785
|
+
trace('ProcessRunner', () => `Raw exit code from child | ${JSON.stringify({
|
|
351
786
|
code,
|
|
352
787
|
codeType: typeof code,
|
|
353
788
|
childExitCode: this.child?.exitCode,
|
|
354
789
|
isBun
|
|
355
|
-
});
|
|
790
|
+
}, null, 2)}`);
|
|
356
791
|
|
|
357
792
|
const resultData = {
|
|
358
793
|
code: code ?? 0, // Default to 0 if exit code is null/undefined
|
|
@@ -361,10 +796,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
361
796
|
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
362
797
|
child: this.child
|
|
363
798
|
};
|
|
364
|
-
|
|
799
|
+
|
|
365
800
|
this.result = {
|
|
366
801
|
...resultData,
|
|
367
|
-
// Bun.$ compatibility method
|
|
368
802
|
async text() {
|
|
369
803
|
return resultData.stdout || '';
|
|
370
804
|
}
|
|
@@ -373,8 +807,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
373
807
|
this.finished = true;
|
|
374
808
|
this.emit('end', this.result);
|
|
375
809
|
this.emit('exit', this.result.code);
|
|
376
|
-
|
|
377
|
-
// Handle shell settings (set -e equivalent)
|
|
810
|
+
|
|
378
811
|
if (globalShellSettings.errexit && this.result.code !== 0) {
|
|
379
812
|
const error = new Error(`Command failed with exit code ${this.result.code}`);
|
|
380
813
|
error.code = this.result.code;
|
|
@@ -383,7 +816,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
383
816
|
error.result = this.result;
|
|
384
817
|
throw error;
|
|
385
818
|
}
|
|
386
|
-
|
|
819
|
+
|
|
387
820
|
return this.result;
|
|
388
821
|
}
|
|
389
822
|
|
|
@@ -394,7 +827,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
394
827
|
const buf = asBuffer(chunk);
|
|
395
828
|
captureChunks && captureChunks.push(buf);
|
|
396
829
|
if (bunWriter) await bunWriter.write(buf);
|
|
397
|
-
else if (typeof child.stdin.write === 'function')
|
|
830
|
+
else if (typeof child.stdin.write === 'function') {
|
|
831
|
+
// Use StreamUtils for consistent stdin handling
|
|
832
|
+
StreamUtils.addStdinErrorHandler(child.stdin, 'child stdin buffer');
|
|
833
|
+
StreamUtils.safeStreamWrite(child.stdin, buf, 'child stdin buffer');
|
|
834
|
+
}
|
|
398
835
|
else if (isBun && typeof Bun.write === 'function') await Bun.write(child.stdin, buf);
|
|
399
836
|
}
|
|
400
837
|
if (bunWriter) await bunWriter.close();
|
|
@@ -402,13 +839,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
402
839
|
}
|
|
403
840
|
|
|
404
841
|
async _writeToStdin(buf) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
842
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
843
|
+
if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
|
|
844
|
+
// Successfully wrote to stream
|
|
845
|
+
if (StreamUtils.isBunStream(this.child.stdin)) {
|
|
846
|
+
// Stream was already closed by writeToStream utility
|
|
847
|
+
} else if (StreamUtils.isNodeStream(this.child.stdin)) {
|
|
848
|
+
try { this.child.stdin.end(); } catch { }
|
|
849
|
+
}
|
|
412
850
|
} else if (isBun && typeof Bun.write === 'function') {
|
|
413
851
|
await Bun.write(this.child.stdin, buf);
|
|
414
852
|
}
|
|
@@ -417,26 +855,25 @@ class ProcessRunner extends StreamEmitter {
|
|
|
417
855
|
_parseCommand(command) {
|
|
418
856
|
const trimmed = command.trim();
|
|
419
857
|
if (!trimmed) return null;
|
|
420
|
-
|
|
421
|
-
// Check for pipes
|
|
858
|
+
|
|
422
859
|
if (trimmed.includes('|')) {
|
|
423
860
|
return this._parsePipeline(trimmed);
|
|
424
861
|
}
|
|
425
|
-
|
|
862
|
+
|
|
426
863
|
// Simple command parsing
|
|
427
864
|
const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
428
865
|
if (parts.length === 0) return null;
|
|
429
|
-
|
|
866
|
+
|
|
430
867
|
const cmd = parts[0];
|
|
431
868
|
const args = parts.slice(1).map(arg => {
|
|
432
869
|
// Keep track of whether the arg was quoted
|
|
433
|
-
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
434
|
-
|
|
870
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
871
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
435
872
|
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
436
873
|
}
|
|
437
874
|
return { value: arg, quoted: false };
|
|
438
875
|
});
|
|
439
|
-
|
|
876
|
+
|
|
440
877
|
return { cmd, args, type: 'simple' };
|
|
441
878
|
}
|
|
442
879
|
|
|
@@ -446,10 +883,10 @@ class ProcessRunner extends StreamEmitter {
|
|
|
446
883
|
let current = '';
|
|
447
884
|
let inQuotes = false;
|
|
448
885
|
let quoteChar = '';
|
|
449
|
-
|
|
886
|
+
|
|
450
887
|
for (let i = 0; i < command.length; i++) {
|
|
451
888
|
const char = command[i];
|
|
452
|
-
|
|
889
|
+
|
|
453
890
|
if (!inQuotes && (char === '"' || char === "'")) {
|
|
454
891
|
inQuotes = true;
|
|
455
892
|
quoteChar = char;
|
|
@@ -465,46 +902,44 @@ class ProcessRunner extends StreamEmitter {
|
|
|
465
902
|
current += char;
|
|
466
903
|
}
|
|
467
904
|
}
|
|
468
|
-
|
|
905
|
+
|
|
469
906
|
if (current.trim()) {
|
|
470
907
|
segments.push(current.trim());
|
|
471
908
|
}
|
|
472
|
-
|
|
473
|
-
// Parse each segment as a simple command
|
|
909
|
+
|
|
474
910
|
const commands = segments.map(segment => {
|
|
475
911
|
const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
476
912
|
if (parts.length === 0) return null;
|
|
477
|
-
|
|
913
|
+
|
|
478
914
|
const cmd = parts[0];
|
|
479
915
|
const args = parts.slice(1).map(arg => {
|
|
480
916
|
// Keep track of whether the arg was quoted
|
|
481
|
-
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
482
|
-
|
|
483
|
-
// Store the original with quotes for system commands
|
|
917
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
918
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
484
919
|
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
485
920
|
}
|
|
486
921
|
return { value: arg, quoted: false };
|
|
487
922
|
});
|
|
488
|
-
|
|
923
|
+
|
|
489
924
|
return { cmd, args };
|
|
490
925
|
}).filter(Boolean);
|
|
491
|
-
|
|
926
|
+
|
|
492
927
|
return { type: 'pipeline', commands };
|
|
493
928
|
}
|
|
494
929
|
|
|
495
930
|
async _runVirtual(cmd, args) {
|
|
496
|
-
|
|
497
|
-
|
|
931
|
+
trace('ProcessRunner', () => `_runVirtual ENTER | ${JSON.stringify({ cmd, args }, null, 2)}`);
|
|
932
|
+
|
|
498
933
|
const handler = virtualCommands.get(cmd);
|
|
499
934
|
if (!handler) {
|
|
500
|
-
trace('ProcessRunner',
|
|
935
|
+
trace('ProcessRunner', () => `Virtual command not found | ${JSON.stringify({ cmd }, null, 2)}`);
|
|
501
936
|
throw new Error(`Virtual command not found: ${cmd}`);
|
|
502
937
|
}
|
|
503
938
|
|
|
504
|
-
trace('ProcessRunner',
|
|
939
|
+
trace('ProcessRunner', () => `Found virtual command handler | ${JSON.stringify({
|
|
505
940
|
cmd,
|
|
506
941
|
isGenerator: handler.constructor.name === 'AsyncGeneratorFunction'
|
|
507
|
-
});
|
|
942
|
+
}, null, 2)}`);
|
|
508
943
|
|
|
509
944
|
try {
|
|
510
945
|
// Prepare stdin
|
|
@@ -526,40 +961,34 @@ class ProcessRunner extends StreamEmitter {
|
|
|
526
961
|
console.log(`${cmd} ${argValues.join(' ')}`);
|
|
527
962
|
}
|
|
528
963
|
|
|
529
|
-
// Execute the virtual command
|
|
530
964
|
let result;
|
|
531
|
-
|
|
532
|
-
// Check if handler is async generator (streaming)
|
|
965
|
+
|
|
533
966
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
534
|
-
// Handle streaming virtual command with cancellation support
|
|
535
967
|
const chunks = [];
|
|
536
|
-
|
|
537
|
-
// Create options with cancellation check and abort signal
|
|
968
|
+
|
|
538
969
|
const commandOptions = {
|
|
539
970
|
...this.options,
|
|
540
971
|
isCancelled: () => this._cancelled,
|
|
541
972
|
signal: this._abortController.signal
|
|
542
973
|
};
|
|
543
|
-
|
|
544
|
-
const generator = handler(argValues, stdinData, commandOptions);
|
|
974
|
+
|
|
975
|
+
const generator = handler({ args: argValues, stdin: stdinData, ...commandOptions });
|
|
545
976
|
this._virtualGenerator = generator;
|
|
546
|
-
|
|
547
|
-
// Create a promise that resolves when cancelled
|
|
977
|
+
|
|
548
978
|
const cancelPromise = new Promise(resolve => {
|
|
549
979
|
this._cancelResolve = resolve;
|
|
550
980
|
});
|
|
551
|
-
|
|
981
|
+
|
|
552
982
|
try {
|
|
553
983
|
const iterator = generator[Symbol.asyncIterator]();
|
|
554
984
|
let done = false;
|
|
555
|
-
|
|
985
|
+
|
|
556
986
|
while (!done && !this._cancelled) {
|
|
557
|
-
// Race between getting next value and cancellation
|
|
558
987
|
const result = await Promise.race([
|
|
559
988
|
iterator.next(),
|
|
560
989
|
cancelPromise.then(() => ({ done: true, cancelled: true }))
|
|
561
990
|
]);
|
|
562
|
-
|
|
991
|
+
|
|
563
992
|
if (result.cancelled || this._cancelled) {
|
|
564
993
|
// Cancelled - close the generator
|
|
565
994
|
if (iterator.return) {
|
|
@@ -567,22 +996,21 @@ class ProcessRunner extends StreamEmitter {
|
|
|
567
996
|
}
|
|
568
997
|
break;
|
|
569
998
|
}
|
|
570
|
-
|
|
999
|
+
|
|
571
1000
|
done = result.done;
|
|
572
|
-
|
|
1001
|
+
|
|
573
1002
|
if (!done) {
|
|
574
1003
|
const chunk = result.value;
|
|
575
1004
|
const buf = Buffer.from(chunk);
|
|
576
1005
|
chunks.push(buf);
|
|
577
|
-
|
|
1006
|
+
|
|
578
1007
|
// Only output if not cancelled
|
|
579
1008
|
if (!this._cancelled) {
|
|
580
1009
|
if (this.options.mirror) {
|
|
581
|
-
process.stdout
|
|
1010
|
+
safeWrite(process.stdout, buf);
|
|
582
1011
|
}
|
|
583
|
-
|
|
584
|
-
this.
|
|
585
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1012
|
+
|
|
1013
|
+
this._emitProcessedData('stdout', buf);
|
|
586
1014
|
}
|
|
587
1015
|
}
|
|
588
1016
|
}
|
|
@@ -591,7 +1019,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
591
1019
|
this._virtualGenerator = null;
|
|
592
1020
|
this._cancelResolve = null;
|
|
593
1021
|
}
|
|
594
|
-
|
|
1022
|
+
|
|
595
1023
|
result = {
|
|
596
1024
|
code: 0,
|
|
597
1025
|
stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
|
|
@@ -600,9 +1028,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
600
1028
|
};
|
|
601
1029
|
} else {
|
|
602
1030
|
// Regular async function
|
|
603
|
-
result = await handler(argValues, stdinData, this.options);
|
|
604
|
-
|
|
605
|
-
// Ensure result has required fields, respecting capture option
|
|
1031
|
+
result = await handler({ args: argValues, stdin: stdinData, ...this.options });
|
|
1032
|
+
|
|
606
1033
|
result = {
|
|
607
1034
|
code: result.code ?? 0,
|
|
608
1035
|
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
@@ -610,36 +1037,33 @@ class ProcessRunner extends StreamEmitter {
|
|
|
610
1037
|
stdin: this.options.capture ? stdinData : undefined,
|
|
611
1038
|
...result
|
|
612
1039
|
};
|
|
613
|
-
|
|
1040
|
+
|
|
614
1041
|
// Mirror and emit output
|
|
615
1042
|
if (result.stdout) {
|
|
616
1043
|
const buf = Buffer.from(result.stdout);
|
|
617
1044
|
if (this.options.mirror) {
|
|
618
|
-
process.stdout
|
|
1045
|
+
safeWrite(process.stdout, buf);
|
|
619
1046
|
}
|
|
620
|
-
this.
|
|
621
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1047
|
+
this._emitProcessedData('stdout', buf);
|
|
622
1048
|
}
|
|
623
|
-
|
|
1049
|
+
|
|
624
1050
|
if (result.stderr) {
|
|
625
1051
|
const buf = Buffer.from(result.stderr);
|
|
626
1052
|
if (this.options.mirror) {
|
|
627
|
-
process.stderr
|
|
1053
|
+
safeWrite(process.stderr, buf);
|
|
628
1054
|
}
|
|
629
|
-
this.
|
|
630
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1055
|
+
this._emitProcessedData('stderr', buf);
|
|
631
1056
|
}
|
|
632
1057
|
}
|
|
633
1058
|
|
|
634
1059
|
// Store result
|
|
635
1060
|
this.result = result;
|
|
636
1061
|
this.finished = true;
|
|
637
|
-
|
|
1062
|
+
|
|
638
1063
|
// Emit completion events
|
|
639
1064
|
this.emit('end', result);
|
|
640
1065
|
this.emit('exit', result.code);
|
|
641
|
-
|
|
642
|
-
// Handle shell settings
|
|
1066
|
+
|
|
643
1067
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
644
1068
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
645
1069
|
error.code = result.code;
|
|
@@ -648,87 +1072,84 @@ class ProcessRunner extends StreamEmitter {
|
|
|
648
1072
|
error.result = result;
|
|
649
1073
|
throw error;
|
|
650
1074
|
}
|
|
651
|
-
|
|
1075
|
+
|
|
652
1076
|
return result;
|
|
653
1077
|
} catch (error) {
|
|
654
|
-
// Handle errors from virtual commands
|
|
655
1078
|
const result = {
|
|
656
1079
|
code: error.code ?? 1,
|
|
657
1080
|
stdout: error.stdout ?? '',
|
|
658
1081
|
stderr: error.stderr ?? error.message,
|
|
659
1082
|
stdin: ''
|
|
660
1083
|
};
|
|
661
|
-
|
|
1084
|
+
|
|
662
1085
|
this.result = result;
|
|
663
1086
|
this.finished = true;
|
|
664
|
-
|
|
1087
|
+
|
|
665
1088
|
if (result.stderr) {
|
|
666
1089
|
const buf = Buffer.from(result.stderr);
|
|
667
1090
|
if (this.options.mirror) {
|
|
668
|
-
process.stderr
|
|
1091
|
+
safeWrite(process.stderr, buf);
|
|
669
1092
|
}
|
|
670
|
-
this.
|
|
671
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1093
|
+
this._emitProcessedData('stderr', buf);
|
|
672
1094
|
}
|
|
673
|
-
|
|
1095
|
+
|
|
674
1096
|
this.emit('end', result);
|
|
675
1097
|
this.emit('exit', result.code);
|
|
676
|
-
|
|
1098
|
+
|
|
677
1099
|
if (globalShellSettings.errexit) {
|
|
678
1100
|
throw error;
|
|
679
1101
|
}
|
|
680
|
-
|
|
1102
|
+
|
|
681
1103
|
return result;
|
|
682
1104
|
}
|
|
683
1105
|
}
|
|
684
1106
|
|
|
685
1107
|
async _runStreamingPipelineBun(commands) {
|
|
686
|
-
|
|
687
|
-
commandsCount: commands.length
|
|
688
|
-
});
|
|
689
|
-
|
|
1108
|
+
trace('ProcessRunner', () => `_runStreamingPipelineBun ENTER | ${JSON.stringify({
|
|
1109
|
+
commandsCount: commands.length
|
|
1110
|
+
}, null, 2)}`);
|
|
1111
|
+
|
|
690
1112
|
// For true streaming, we need to handle virtual and real commands differently
|
|
691
|
-
|
|
692
|
-
|
|
1113
|
+
|
|
693
1114
|
// First, analyze the pipeline to identify virtual vs real commands
|
|
694
1115
|
const pipelineInfo = commands.map(command => {
|
|
695
1116
|
const { cmd, args } = command;
|
|
696
1117
|
const isVirtual = virtualCommandsEnabled && virtualCommands.has(cmd);
|
|
697
1118
|
return { ...command, isVirtual };
|
|
698
1119
|
});
|
|
699
|
-
|
|
700
|
-
trace('ProcessRunner',
|
|
1120
|
+
|
|
1121
|
+
trace('ProcessRunner', () => `Pipeline analysis | ${JSON.stringify({
|
|
701
1122
|
virtualCount: pipelineInfo.filter(p => p.isVirtual).length,
|
|
702
|
-
realCount: pipelineInfo.filter(p => !p.isVirtual).length
|
|
703
|
-
});
|
|
704
|
-
|
|
1123
|
+
realCount: pipelineInfo.filter(p => !p.isVirtual).length
|
|
1124
|
+
}, null, 2)}`);
|
|
1125
|
+
|
|
705
1126
|
// If pipeline contains virtual commands, use advanced streaming
|
|
706
1127
|
if (pipelineInfo.some(info => info.isVirtual)) {
|
|
707
|
-
|
|
1128
|
+
trace('ProcessRunner', () => `BRANCH: _runStreamingPipelineBun => MIXED_PIPELINE | ${JSON.stringify({}, null, 2)}`);
|
|
708
1129
|
return this._runMixedStreamingPipeline(commands);
|
|
709
1130
|
}
|
|
710
|
-
|
|
1131
|
+
|
|
711
1132
|
// For pipelines with commands that buffer (like jq), use tee streaming
|
|
712
|
-
const needsStreamingWorkaround = commands.some(c =>
|
|
1133
|
+
const needsStreamingWorkaround = commands.some(c =>
|
|
713
1134
|
c.cmd === 'jq' || c.cmd === 'grep' || c.cmd === 'sed' || c.cmd === 'cat' || c.cmd === 'awk'
|
|
714
1135
|
);
|
|
715
1136
|
if (needsStreamingWorkaround) {
|
|
716
|
-
|
|
717
|
-
bufferedCommands: commands.filter(c =>
|
|
1137
|
+
trace('ProcessRunner', () => `BRANCH: _runStreamingPipelineBun => TEE_STREAMING | ${JSON.stringify({
|
|
1138
|
+
bufferedCommands: commands.filter(c =>
|
|
718
1139
|
['jq', 'grep', 'sed', 'cat', 'awk'].includes(c.cmd)
|
|
719
1140
|
).map(c => c.cmd)
|
|
720
|
-
});
|
|
1141
|
+
}, null, 2)}`);
|
|
721
1142
|
return this._runTeeStreamingPipeline(commands);
|
|
722
1143
|
}
|
|
723
|
-
|
|
1144
|
+
|
|
724
1145
|
// All real commands - use native pipe connections
|
|
725
1146
|
const processes = [];
|
|
726
1147
|
let allStderr = '';
|
|
727
|
-
|
|
1148
|
+
|
|
728
1149
|
for (let i = 0; i < commands.length; i++) {
|
|
729
1150
|
const command = commands[i];
|
|
730
1151
|
const { cmd, args } = command;
|
|
731
|
-
|
|
1152
|
+
|
|
732
1153
|
// Build command string
|
|
733
1154
|
const commandParts = [cmd];
|
|
734
1155
|
for (const arg of args) {
|
|
@@ -749,12 +1170,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
749
1170
|
}
|
|
750
1171
|
}
|
|
751
1172
|
const commandStr = commandParts.join(' ');
|
|
752
|
-
|
|
1173
|
+
|
|
753
1174
|
// Determine stdin for this process
|
|
754
1175
|
let stdin;
|
|
755
1176
|
let needsManualStdin = false;
|
|
756
1177
|
let stdinData;
|
|
757
|
-
|
|
1178
|
+
|
|
758
1179
|
if (i === 0) {
|
|
759
1180
|
// First command - use provided stdin or pipe
|
|
760
1181
|
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
@@ -772,18 +1193,17 @@ class ProcessRunner extends StreamEmitter {
|
|
|
772
1193
|
// Connect to previous process stdout
|
|
773
1194
|
stdin = processes[i - 1].stdout;
|
|
774
1195
|
}
|
|
775
|
-
|
|
776
|
-
// Spawn the process directly (not through sh) for better streaming
|
|
1196
|
+
|
|
777
1197
|
// Only use sh -c for complex commands that need shell features
|
|
778
|
-
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const spawnArgs = needsShell
|
|
1198
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
1199
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
1200
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
1201
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
1202
|
+
|
|
1203
|
+
const spawnArgs = needsShell
|
|
784
1204
|
? ['sh', '-c', commandStr]
|
|
785
1205
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
786
|
-
|
|
1206
|
+
|
|
787
1207
|
const proc = Bun.spawn(spawnArgs, {
|
|
788
1208
|
cwd: this.options.cwd,
|
|
789
1209
|
env: this.options.env,
|
|
@@ -791,22 +1211,28 @@ class ProcessRunner extends StreamEmitter {
|
|
|
791
1211
|
stdout: 'pipe',
|
|
792
1212
|
stderr: 'pipe'
|
|
793
1213
|
});
|
|
794
|
-
|
|
1214
|
+
|
|
795
1215
|
// Write stdin data if needed for first process
|
|
796
1216
|
if (needsManualStdin && stdinData && proc.stdin) {
|
|
1217
|
+
// Use StreamUtils for consistent stdin handling
|
|
1218
|
+
const stdinHandler = StreamUtils.setupStdinHandling(proc.stdin, 'Bun process stdin');
|
|
1219
|
+
|
|
797
1220
|
(async () => {
|
|
798
1221
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1222
|
+
if (stdinHandler.isWritable()) {
|
|
1223
|
+
await proc.stdin.write(stdinData); // Bun's FileSink async write
|
|
1224
|
+
await proc.stdin.end();
|
|
1225
|
+
}
|
|
802
1226
|
} catch (e) {
|
|
803
|
-
|
|
1227
|
+
if (e.code !== 'EPIPE') {
|
|
1228
|
+
trace('ProcessRunner', () => `Error with Bun stdin async operations | ${JSON.stringify({ error: e.message, code: e.code }, null, 2)}`);
|
|
1229
|
+
}
|
|
804
1230
|
}
|
|
805
1231
|
})();
|
|
806
1232
|
}
|
|
807
|
-
|
|
1233
|
+
|
|
808
1234
|
processes.push(proc);
|
|
809
|
-
|
|
1235
|
+
|
|
810
1236
|
// Collect stderr from all processes
|
|
811
1237
|
(async () => {
|
|
812
1238
|
for await (const chunk of proc.stderr) {
|
|
@@ -815,35 +1241,32 @@ class ProcessRunner extends StreamEmitter {
|
|
|
815
1241
|
// Only emit stderr for the last command
|
|
816
1242
|
if (i === commands.length - 1) {
|
|
817
1243
|
if (this.options.mirror) {
|
|
818
|
-
process.stderr
|
|
1244
|
+
safeWrite(process.stderr, buf);
|
|
819
1245
|
}
|
|
820
|
-
this.
|
|
821
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1246
|
+
this._emitProcessedData('stderr', buf);
|
|
822
1247
|
}
|
|
823
1248
|
}
|
|
824
1249
|
})();
|
|
825
1250
|
}
|
|
826
|
-
|
|
1251
|
+
|
|
827
1252
|
// Stream output from the last process
|
|
828
1253
|
const lastProc = processes[processes.length - 1];
|
|
829
1254
|
let finalOutput = '';
|
|
830
|
-
|
|
1255
|
+
|
|
831
1256
|
// Stream stdout from last process
|
|
832
1257
|
for await (const chunk of lastProc.stdout) {
|
|
833
1258
|
const buf = Buffer.from(chunk);
|
|
834
1259
|
finalOutput += buf.toString();
|
|
835
1260
|
if (this.options.mirror) {
|
|
836
|
-
process.stdout
|
|
1261
|
+
safeWrite(process.stdout, buf);
|
|
837
1262
|
}
|
|
838
|
-
this.
|
|
839
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1263
|
+
this._emitProcessedData('stdout', buf);
|
|
840
1264
|
}
|
|
841
|
-
|
|
1265
|
+
|
|
842
1266
|
// Wait for all processes to complete
|
|
843
1267
|
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
844
1268
|
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
845
|
-
|
|
846
|
-
// Check for pipeline failures if pipefail is set
|
|
1269
|
+
|
|
847
1270
|
if (globalShellSettings.pipefail) {
|
|
848
1271
|
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
849
1272
|
if (failedIndex !== -1) {
|
|
@@ -852,21 +1275,21 @@ class ProcessRunner extends StreamEmitter {
|
|
|
852
1275
|
throw error;
|
|
853
1276
|
}
|
|
854
1277
|
}
|
|
855
|
-
|
|
1278
|
+
|
|
856
1279
|
const result = createResult({
|
|
857
1280
|
code: lastExitCode || 0,
|
|
858
1281
|
stdout: finalOutput,
|
|
859
1282
|
stderr: allStderr,
|
|
860
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
861
|
-
|
|
1283
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1284
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
862
1285
|
});
|
|
863
|
-
|
|
1286
|
+
|
|
864
1287
|
this.result = result;
|
|
865
1288
|
this.finished = true;
|
|
866
|
-
|
|
1289
|
+
|
|
867
1290
|
this.emit('end', result);
|
|
868
1291
|
this.emit('exit', result.code);
|
|
869
|
-
|
|
1292
|
+
|
|
870
1293
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
871
1294
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
872
1295
|
error.code = result.code;
|
|
@@ -875,26 +1298,26 @@ class ProcessRunner extends StreamEmitter {
|
|
|
875
1298
|
error.result = result;
|
|
876
1299
|
throw error;
|
|
877
1300
|
}
|
|
878
|
-
|
|
1301
|
+
|
|
879
1302
|
return result;
|
|
880
1303
|
}
|
|
881
1304
|
|
|
882
1305
|
async _runTeeStreamingPipeline(commands) {
|
|
883
|
-
|
|
884
|
-
commandsCount: commands.length
|
|
885
|
-
});
|
|
886
|
-
|
|
1306
|
+
trace('ProcessRunner', () => `_runTeeStreamingPipeline ENTER | ${JSON.stringify({
|
|
1307
|
+
commandsCount: commands.length
|
|
1308
|
+
}, null, 2)}`);
|
|
1309
|
+
|
|
887
1310
|
// Use tee() to split streams for real-time reading
|
|
888
1311
|
// This works around jq and similar commands that buffer when piped
|
|
889
|
-
|
|
1312
|
+
|
|
890
1313
|
const processes = [];
|
|
891
1314
|
let allStderr = '';
|
|
892
1315
|
let currentStream = null;
|
|
893
|
-
|
|
1316
|
+
|
|
894
1317
|
for (let i = 0; i < commands.length; i++) {
|
|
895
1318
|
const command = commands[i];
|
|
896
1319
|
const { cmd, args } = command;
|
|
897
|
-
|
|
1320
|
+
|
|
898
1321
|
// Build command string
|
|
899
1322
|
const commandParts = [cmd];
|
|
900
1323
|
for (const arg of args) {
|
|
@@ -915,12 +1338,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
915
1338
|
}
|
|
916
1339
|
}
|
|
917
1340
|
const commandStr = commandParts.join(' ');
|
|
918
|
-
|
|
1341
|
+
|
|
919
1342
|
// Determine stdin for this process
|
|
920
1343
|
let stdin;
|
|
921
1344
|
let needsManualStdin = false;
|
|
922
1345
|
let stdinData;
|
|
923
|
-
|
|
1346
|
+
|
|
924
1347
|
if (i === 0) {
|
|
925
1348
|
// First command - use provided stdin or ignore
|
|
926
1349
|
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
@@ -935,20 +1358,18 @@ class ProcessRunner extends StreamEmitter {
|
|
|
935
1358
|
stdin = 'ignore';
|
|
936
1359
|
}
|
|
937
1360
|
} else {
|
|
938
|
-
// Use the stream from previous process
|
|
939
1361
|
stdin = currentStream;
|
|
940
1362
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const spawnArgs = needsShell
|
|
1363
|
+
|
|
1364
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
1365
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
1366
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
1367
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
1368
|
+
|
|
1369
|
+
const spawnArgs = needsShell
|
|
949
1370
|
? ['sh', '-c', commandStr]
|
|
950
1371
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
951
|
-
|
|
1372
|
+
|
|
952
1373
|
const proc = Bun.spawn(spawnArgs, {
|
|
953
1374
|
cwd: this.options.cwd,
|
|
954
1375
|
env: this.options.env,
|
|
@@ -956,50 +1377,41 @@ class ProcessRunner extends StreamEmitter {
|
|
|
956
1377
|
stdout: 'pipe',
|
|
957
1378
|
stderr: 'pipe'
|
|
958
1379
|
});
|
|
959
|
-
|
|
1380
|
+
|
|
960
1381
|
// Write stdin data if needed for first process
|
|
961
1382
|
if (needsManualStdin && stdinData && proc.stdin) {
|
|
1383
|
+
// Use StreamUtils for consistent stdin handling
|
|
1384
|
+
const stdinHandler = StreamUtils.setupStdinHandling(proc.stdin, 'Node process stdin');
|
|
1385
|
+
|
|
962
1386
|
try {
|
|
963
|
-
|
|
964
|
-
|
|
1387
|
+
if (stdinHandler.isWritable()) {
|
|
1388
|
+
await proc.stdin.write(stdinData); // Node async write
|
|
1389
|
+
await proc.stdin.end();
|
|
1390
|
+
}
|
|
965
1391
|
} catch (e) {
|
|
966
|
-
|
|
1392
|
+
if (e.code !== 'EPIPE') {
|
|
1393
|
+
trace('ProcessRunner', () => `Error with Node stdin async operations | ${JSON.stringify({ error: e.message, code: e.code }, null, 2)}`);
|
|
1394
|
+
}
|
|
967
1395
|
}
|
|
968
1396
|
}
|
|
969
|
-
|
|
1397
|
+
|
|
970
1398
|
processes.push(proc);
|
|
971
|
-
|
|
1399
|
+
|
|
972
1400
|
// For non-last processes, tee the output so we can both pipe and read
|
|
973
1401
|
if (i < commands.length - 1) {
|
|
974
1402
|
const [readStream, pipeStream] = proc.stdout.tee();
|
|
975
1403
|
currentStream = pipeStream;
|
|
976
|
-
|
|
977
|
-
// Read from the tee'd stream
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const buf = Buffer.from(chunk);
|
|
984
|
-
if (this.options.mirror) {
|
|
985
|
-
process.stdout.write(buf);
|
|
986
|
-
}
|
|
987
|
-
this.emit('stdout', buf);
|
|
988
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
989
|
-
}
|
|
990
|
-
})();
|
|
991
|
-
} else {
|
|
992
|
-
// Consume other tee'd streams to prevent blocking
|
|
993
|
-
(async () => {
|
|
994
|
-
for await (const chunk of readStream) {
|
|
995
|
-
// Just consume to keep flowing
|
|
996
|
-
}
|
|
997
|
-
})();
|
|
998
|
-
}
|
|
1404
|
+
|
|
1405
|
+
// Read from the tee'd stream to keep it flowing
|
|
1406
|
+
(async () => {
|
|
1407
|
+
for await (const chunk of readStream) {
|
|
1408
|
+
// Just consume to keep flowing - don't emit intermediate output
|
|
1409
|
+
}
|
|
1410
|
+
})();
|
|
999
1411
|
} else {
|
|
1000
1412
|
currentStream = proc.stdout;
|
|
1001
1413
|
}
|
|
1002
|
-
|
|
1414
|
+
|
|
1003
1415
|
// Collect stderr from all processes
|
|
1004
1416
|
(async () => {
|
|
1005
1417
|
for await (const chunk of proc.stderr) {
|
|
@@ -1007,39 +1419,32 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1007
1419
|
allStderr += buf.toString();
|
|
1008
1420
|
if (i === commands.length - 1) {
|
|
1009
1421
|
if (this.options.mirror) {
|
|
1010
|
-
process.stderr
|
|
1422
|
+
safeWrite(process.stderr, buf);
|
|
1011
1423
|
}
|
|
1012
|
-
this.
|
|
1013
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1424
|
+
this._emitProcessedData('stderr', buf);
|
|
1014
1425
|
}
|
|
1015
1426
|
}
|
|
1016
1427
|
})();
|
|
1017
1428
|
}
|
|
1018
|
-
|
|
1429
|
+
|
|
1019
1430
|
// Read final output from the last process
|
|
1020
1431
|
const lastProc = processes[processes.length - 1];
|
|
1021
1432
|
let finalOutput = '';
|
|
1022
|
-
|
|
1023
|
-
//
|
|
1024
|
-
const shouldEmitFromLast = commands.length === 1;
|
|
1025
|
-
|
|
1433
|
+
|
|
1434
|
+
// Always emit from the last process for proper pipeline output
|
|
1026
1435
|
for await (const chunk of lastProc.stdout) {
|
|
1027
1436
|
const buf = Buffer.from(chunk);
|
|
1028
1437
|
finalOutput += buf.toString();
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
process.stdout.write(buf);
|
|
1032
|
-
}
|
|
1033
|
-
this.emit('stdout', buf);
|
|
1034
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1438
|
+
if (this.options.mirror) {
|
|
1439
|
+
safeWrite(process.stdout, buf);
|
|
1035
1440
|
}
|
|
1441
|
+
this._emitProcessedData('stdout', buf);
|
|
1036
1442
|
}
|
|
1037
|
-
|
|
1443
|
+
|
|
1038
1444
|
// Wait for all processes to complete
|
|
1039
1445
|
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
1040
1446
|
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
1041
|
-
|
|
1042
|
-
// Check for pipeline failures if pipefail is set
|
|
1447
|
+
|
|
1043
1448
|
if (globalShellSettings.pipefail) {
|
|
1044
1449
|
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
1045
1450
|
if (failedIndex !== -1) {
|
|
@@ -1048,21 +1453,21 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1048
1453
|
throw error;
|
|
1049
1454
|
}
|
|
1050
1455
|
}
|
|
1051
|
-
|
|
1456
|
+
|
|
1052
1457
|
const result = createResult({
|
|
1053
1458
|
code: lastExitCode || 0,
|
|
1054
1459
|
stdout: finalOutput,
|
|
1055
1460
|
stderr: allStderr,
|
|
1056
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1057
|
-
|
|
1461
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1462
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1058
1463
|
});
|
|
1059
|
-
|
|
1464
|
+
|
|
1060
1465
|
this.result = result;
|
|
1061
1466
|
this.finished = true;
|
|
1062
|
-
|
|
1467
|
+
|
|
1063
1468
|
this.emit('end', result);
|
|
1064
1469
|
this.emit('exit', result.code);
|
|
1065
|
-
|
|
1470
|
+
|
|
1066
1471
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1067
1472
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
1068
1473
|
error.code = result.code;
|
|
@@ -1071,30 +1476,27 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1071
1476
|
error.result = result;
|
|
1072
1477
|
throw error;
|
|
1073
1478
|
}
|
|
1074
|
-
|
|
1479
|
+
|
|
1075
1480
|
return result;
|
|
1076
1481
|
}
|
|
1077
1482
|
|
|
1078
1483
|
|
|
1079
1484
|
async _runMixedStreamingPipeline(commands) {
|
|
1080
|
-
|
|
1081
|
-
commandsCount: commands.length
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
// Handle pipelines with both virtual and real commands
|
|
1485
|
+
trace('ProcessRunner', () => `_runMixedStreamingPipeline ENTER | ${JSON.stringify({
|
|
1486
|
+
commandsCount: commands.length
|
|
1487
|
+
}, null, 2)}`);
|
|
1488
|
+
|
|
1085
1489
|
// Each stage reads from previous stage's output stream
|
|
1086
|
-
|
|
1490
|
+
|
|
1087
1491
|
let currentInputStream = null;
|
|
1088
1492
|
let finalOutput = '';
|
|
1089
1493
|
let allStderr = '';
|
|
1090
|
-
|
|
1091
|
-
// Set up initial input stream if provided
|
|
1494
|
+
|
|
1092
1495
|
if (this.options.stdin) {
|
|
1093
|
-
const inputData = typeof this.options.stdin === 'string'
|
|
1094
|
-
? this.options.stdin
|
|
1496
|
+
const inputData = typeof this.options.stdin === 'string'
|
|
1497
|
+
? this.options.stdin
|
|
1095
1498
|
: this.options.stdin.toString('utf8');
|
|
1096
|
-
|
|
1097
|
-
// Create a readable stream from the input
|
|
1499
|
+
|
|
1098
1500
|
currentInputStream = new ReadableStream({
|
|
1099
1501
|
start(controller) {
|
|
1100
1502
|
controller.enqueue(new TextEncoder().encode(inputData));
|
|
@@ -1102,21 +1504,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1102
1504
|
}
|
|
1103
1505
|
});
|
|
1104
1506
|
}
|
|
1105
|
-
|
|
1507
|
+
|
|
1106
1508
|
for (let i = 0; i < commands.length; i++) {
|
|
1107
1509
|
const command = commands[i];
|
|
1108
1510
|
const { cmd, args } = command;
|
|
1109
1511
|
const isLastCommand = i === commands.length - 1;
|
|
1110
|
-
|
|
1512
|
+
|
|
1111
1513
|
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1112
|
-
|
|
1113
|
-
traceBranch('ProcessRunner', '_runMixedStreamingPipeline', 'VIRTUAL_COMMAND', {
|
|
1514
|
+
trace('ProcessRunner', () => `BRANCH: _runMixedStreamingPipeline => VIRTUAL_COMMAND | ${JSON.stringify({
|
|
1114
1515
|
cmd,
|
|
1115
|
-
commandIndex: i
|
|
1116
|
-
});
|
|
1516
|
+
commandIndex: i
|
|
1517
|
+
}, null, 2)}`);
|
|
1117
1518
|
const handler = virtualCommands.get(cmd);
|
|
1118
1519
|
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
1119
|
-
|
|
1520
|
+
|
|
1120
1521
|
// Read input from stream if available
|
|
1121
1522
|
let inputData = '';
|
|
1122
1523
|
if (currentInputStream) {
|
|
@@ -1131,30 +1532,29 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1131
1532
|
reader.releaseLock();
|
|
1132
1533
|
}
|
|
1133
1534
|
}
|
|
1134
|
-
|
|
1135
|
-
// Check if handler is async generator (streaming)
|
|
1535
|
+
|
|
1136
1536
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1137
|
-
// Create output stream from generator
|
|
1138
1537
|
const chunks = [];
|
|
1139
1538
|
const self = this; // Capture this context
|
|
1140
1539
|
currentInputStream = new ReadableStream({
|
|
1141
1540
|
async start(controller) {
|
|
1142
|
-
|
|
1541
|
+
const { stdin: _, ...optionsWithoutStdin } = self.options;
|
|
1542
|
+
for await (const chunk of handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin })) {
|
|
1143
1543
|
const data = Buffer.from(chunk);
|
|
1144
1544
|
controller.enqueue(data);
|
|
1145
|
-
|
|
1545
|
+
|
|
1146
1546
|
// Emit for last command
|
|
1147
1547
|
if (isLastCommand) {
|
|
1148
1548
|
chunks.push(data);
|
|
1149
1549
|
if (self.options.mirror) {
|
|
1150
|
-
process.stdout
|
|
1550
|
+
safeWrite(process.stdout, data);
|
|
1151
1551
|
}
|
|
1152
1552
|
self.emit('stdout', data);
|
|
1153
1553
|
self.emit('data', { type: 'stdout', data });
|
|
1154
1554
|
}
|
|
1155
1555
|
}
|
|
1156
1556
|
controller.close();
|
|
1157
|
-
|
|
1557
|
+
|
|
1158
1558
|
if (isLastCommand) {
|
|
1159
1559
|
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1160
1560
|
}
|
|
@@ -1162,33 +1562,31 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1162
1562
|
});
|
|
1163
1563
|
} else {
|
|
1164
1564
|
// Regular async function
|
|
1165
|
-
const
|
|
1565
|
+
const { stdin: _, ...optionsWithoutStdin } = this.options;
|
|
1566
|
+
const result = await handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin });
|
|
1166
1567
|
const outputData = result.stdout || '';
|
|
1167
|
-
|
|
1568
|
+
|
|
1168
1569
|
if (isLastCommand) {
|
|
1169
1570
|
finalOutput = outputData;
|
|
1170
1571
|
const buf = Buffer.from(outputData);
|
|
1171
1572
|
if (this.options.mirror) {
|
|
1172
|
-
process.stdout
|
|
1573
|
+
safeWrite(process.stdout, buf);
|
|
1173
1574
|
}
|
|
1174
|
-
this.
|
|
1175
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1575
|
+
this._emitProcessedData('stdout', buf);
|
|
1176
1576
|
}
|
|
1177
|
-
|
|
1178
|
-
// Create stream from output
|
|
1577
|
+
|
|
1179
1578
|
currentInputStream = new ReadableStream({
|
|
1180
1579
|
start(controller) {
|
|
1181
1580
|
controller.enqueue(new TextEncoder().encode(outputData));
|
|
1182
1581
|
controller.close();
|
|
1183
1582
|
}
|
|
1184
1583
|
});
|
|
1185
|
-
|
|
1584
|
+
|
|
1186
1585
|
if (result.stderr) {
|
|
1187
1586
|
allStderr += result.stderr;
|
|
1188
1587
|
}
|
|
1189
1588
|
}
|
|
1190
1589
|
} else {
|
|
1191
|
-
// Handle real command - spawn with streaming
|
|
1192
1590
|
const commandParts = [cmd];
|
|
1193
1591
|
for (const arg of args) {
|
|
1194
1592
|
if (arg.value !== undefined) {
|
|
@@ -1208,8 +1606,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1208
1606
|
}
|
|
1209
1607
|
}
|
|
1210
1608
|
const commandStr = commandParts.join(' ');
|
|
1211
|
-
|
|
1212
|
-
// Spawn the process
|
|
1609
|
+
|
|
1213
1610
|
const proc = Bun.spawn(['sh', '-c', commandStr], {
|
|
1214
1611
|
cwd: this.options.cwd,
|
|
1215
1612
|
env: this.options.env,
|
|
@@ -1217,23 +1614,33 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1217
1614
|
stdout: 'pipe',
|
|
1218
1615
|
stderr: 'pipe'
|
|
1219
1616
|
});
|
|
1220
|
-
|
|
1617
|
+
|
|
1221
1618
|
// Write input stream to process stdin if needed
|
|
1222
1619
|
if (currentInputStream && proc.stdin) {
|
|
1223
1620
|
const reader = currentInputStream.getReader();
|
|
1224
1621
|
const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
|
|
1225
|
-
|
|
1622
|
+
|
|
1226
1623
|
(async () => {
|
|
1227
1624
|
try {
|
|
1228
1625
|
while (true) {
|
|
1229
1626
|
const { done, value } = await reader.read();
|
|
1230
1627
|
if (done) break;
|
|
1231
1628
|
if (writer.write) {
|
|
1232
|
-
|
|
1629
|
+
try {
|
|
1630
|
+
await writer.write(value);
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
StreamUtils.handleStreamError(error, 'stream writer', false);
|
|
1633
|
+
break; // Stop streaming if write fails
|
|
1634
|
+
}
|
|
1233
1635
|
} else if (writer.getWriter) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1636
|
+
try {
|
|
1637
|
+
const w = writer.getWriter();
|
|
1638
|
+
await w.write(value);
|
|
1639
|
+
w.releaseLock();
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
StreamUtils.handleStreamError(error, 'stream writer (getWriter)', false);
|
|
1642
|
+
break; // Stop streaming if write fails
|
|
1643
|
+
}
|
|
1237
1644
|
}
|
|
1238
1645
|
}
|
|
1239
1646
|
} finally {
|
|
@@ -1243,25 +1650,22 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1243
1650
|
}
|
|
1244
1651
|
})();
|
|
1245
1652
|
}
|
|
1246
|
-
|
|
1247
|
-
// Set up output stream
|
|
1653
|
+
|
|
1248
1654
|
currentInputStream = proc.stdout;
|
|
1249
|
-
|
|
1250
|
-
// Handle stderr
|
|
1655
|
+
|
|
1251
1656
|
(async () => {
|
|
1252
1657
|
for await (const chunk of proc.stderr) {
|
|
1253
1658
|
const buf = Buffer.from(chunk);
|
|
1254
1659
|
allStderr += buf.toString();
|
|
1255
1660
|
if (isLastCommand) {
|
|
1256
1661
|
if (this.options.mirror) {
|
|
1257
|
-
process.stderr
|
|
1662
|
+
safeWrite(process.stderr, buf);
|
|
1258
1663
|
}
|
|
1259
|
-
this.
|
|
1260
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1664
|
+
this._emitProcessedData('stderr', buf);
|
|
1261
1665
|
}
|
|
1262
1666
|
}
|
|
1263
1667
|
})();
|
|
1264
|
-
|
|
1668
|
+
|
|
1265
1669
|
// For last command, stream output
|
|
1266
1670
|
if (isLastCommand) {
|
|
1267
1671
|
const chunks = [];
|
|
@@ -1269,44 +1673,41 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1269
1673
|
const buf = Buffer.from(chunk);
|
|
1270
1674
|
chunks.push(buf);
|
|
1271
1675
|
if (this.options.mirror) {
|
|
1272
|
-
process.stdout
|
|
1676
|
+
safeWrite(process.stdout, buf);
|
|
1273
1677
|
}
|
|
1274
|
-
this.
|
|
1275
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1678
|
+
this._emitProcessedData('stdout', buf);
|
|
1276
1679
|
}
|
|
1277
1680
|
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1278
1681
|
await proc.exited;
|
|
1279
1682
|
}
|
|
1280
1683
|
}
|
|
1281
1684
|
}
|
|
1282
|
-
|
|
1685
|
+
|
|
1283
1686
|
const result = createResult({
|
|
1284
1687
|
code: 0, // TODO: Track exit codes properly
|
|
1285
1688
|
stdout: finalOutput,
|
|
1286
1689
|
stderr: allStderr,
|
|
1287
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1288
|
-
|
|
1690
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1691
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1289
1692
|
});
|
|
1290
|
-
|
|
1693
|
+
|
|
1291
1694
|
this.result = result;
|
|
1292
1695
|
this.finished = true;
|
|
1293
|
-
|
|
1696
|
+
|
|
1294
1697
|
this.emit('end', result);
|
|
1295
1698
|
this.emit('exit', result.code);
|
|
1296
|
-
|
|
1699
|
+
|
|
1297
1700
|
return result;
|
|
1298
1701
|
}
|
|
1299
1702
|
|
|
1300
1703
|
async _runPipelineNonStreaming(commands) {
|
|
1301
|
-
|
|
1302
|
-
commandsCount: commands.length
|
|
1303
|
-
});
|
|
1304
|
-
|
|
1305
|
-
// Original non-streaming implementation for fallback (e.g., virtual commands)
|
|
1704
|
+
trace('ProcessRunner', () => `_runPipelineNonStreaming ENTER | ${JSON.stringify({
|
|
1705
|
+
commandsCount: commands.length
|
|
1706
|
+
}, null, 2)}`);
|
|
1707
|
+
|
|
1306
1708
|
let currentOutput = '';
|
|
1307
1709
|
let currentInput = '';
|
|
1308
|
-
|
|
1309
|
-
// Get initial stdin from options
|
|
1710
|
+
|
|
1310
1711
|
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
1311
1712
|
currentInput = this.options.stdin;
|
|
1312
1713
|
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
@@ -1317,21 +1718,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1317
1718
|
for (let i = 0; i < commands.length; i++) {
|
|
1318
1719
|
const command = commands[i];
|
|
1319
1720
|
const { cmd, args } = command;
|
|
1320
|
-
|
|
1321
|
-
// Check if this is a virtual command (only if virtual commands are enabled)
|
|
1721
|
+
|
|
1322
1722
|
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1323
|
-
|
|
1723
|
+
trace('ProcessRunner', () => `BRANCH: _runPipelineNonStreaming => VIRTUAL_COMMAND | ${JSON.stringify({
|
|
1324
1724
|
cmd,
|
|
1325
|
-
argsCount: args.length
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1725
|
+
argsCount: args.length
|
|
1726
|
+
}, null, 2)}`);
|
|
1727
|
+
|
|
1328
1728
|
// Run virtual command with current input
|
|
1329
1729
|
const handler = virtualCommands.get(cmd);
|
|
1330
|
-
|
|
1730
|
+
|
|
1331
1731
|
try {
|
|
1332
1732
|
// Extract actual values for virtual command
|
|
1333
1733
|
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
1334
|
-
|
|
1734
|
+
|
|
1335
1735
|
// Shell tracing for virtual commands
|
|
1336
1736
|
if (globalShellSettings.xtrace) {
|
|
1337
1737
|
console.log(`+ ${cmd} ${argValues.join(' ')}`);
|
|
@@ -1341,12 +1741,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1341
1741
|
}
|
|
1342
1742
|
|
|
1343
1743
|
let result;
|
|
1344
|
-
|
|
1345
|
-
// Check if handler is async generator (streaming)
|
|
1744
|
+
|
|
1346
1745
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1347
|
-
|
|
1746
|
+
trace('ProcessRunner', () => `BRANCH: _runPipelineNonStreaming => ASYNC_GENERATOR | ${JSON.stringify({ cmd }, null, 2)}`);
|
|
1348
1747
|
const chunks = [];
|
|
1349
|
-
for await (const chunk of handler(argValues, currentInput, this.options)) {
|
|
1748
|
+
for await (const chunk of handler({ args: argValues, stdin: currentInput, ...this.options })) {
|
|
1350
1749
|
chunks.push(Buffer.from(chunk));
|
|
1351
1750
|
}
|
|
1352
1751
|
result = {
|
|
@@ -1357,7 +1756,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1357
1756
|
};
|
|
1358
1757
|
} else {
|
|
1359
1758
|
// Regular async function
|
|
1360
|
-
result = await handler(argValues, currentInput, this.options);
|
|
1759
|
+
result = await handler({ args: argValues, stdin: currentInput, ...this.options });
|
|
1361
1760
|
result = {
|
|
1362
1761
|
code: result.code ?? 0,
|
|
1363
1762
|
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
@@ -1366,50 +1765,46 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1366
1765
|
...result
|
|
1367
1766
|
};
|
|
1368
1767
|
}
|
|
1369
|
-
|
|
1768
|
+
|
|
1370
1769
|
// If this isn't the last command, pass stdout as stdin to next command
|
|
1371
1770
|
if (i < commands.length - 1) {
|
|
1372
1771
|
currentInput = result.stdout;
|
|
1373
1772
|
} else {
|
|
1374
1773
|
// This is the last command - emit output and store final result
|
|
1375
1774
|
currentOutput = result.stdout;
|
|
1376
|
-
|
|
1775
|
+
|
|
1377
1776
|
// Mirror and emit output for final command
|
|
1378
1777
|
if (result.stdout) {
|
|
1379
1778
|
const buf = Buffer.from(result.stdout);
|
|
1380
1779
|
if (this.options.mirror) {
|
|
1381
|
-
process.stdout
|
|
1780
|
+
safeWrite(process.stdout, buf);
|
|
1382
1781
|
}
|
|
1383
|
-
this.
|
|
1384
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
1782
|
+
this._emitProcessedData('stdout', buf);
|
|
1385
1783
|
}
|
|
1386
|
-
|
|
1784
|
+
|
|
1387
1785
|
if (result.stderr) {
|
|
1388
1786
|
const buf = Buffer.from(result.stderr);
|
|
1389
1787
|
if (this.options.mirror) {
|
|
1390
|
-
process.stderr
|
|
1788
|
+
safeWrite(process.stderr, buf);
|
|
1391
1789
|
}
|
|
1392
|
-
this.
|
|
1393
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1790
|
+
this._emitProcessedData('stderr', buf);
|
|
1394
1791
|
}
|
|
1395
|
-
|
|
1396
|
-
// Store final result using createResult helper for .text() method compatibility
|
|
1792
|
+
|
|
1397
1793
|
const finalResult = createResult({
|
|
1398
1794
|
code: result.code,
|
|
1399
1795
|
stdout: currentOutput,
|
|
1400
1796
|
stderr: result.stderr,
|
|
1401
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1402
|
-
|
|
1797
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1798
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1403
1799
|
});
|
|
1404
|
-
|
|
1800
|
+
|
|
1405
1801
|
this.result = finalResult;
|
|
1406
1802
|
this.finished = true;
|
|
1407
|
-
|
|
1803
|
+
|
|
1408
1804
|
// Emit completion events
|
|
1409
1805
|
this.emit('end', finalResult);
|
|
1410
1806
|
this.emit('exit', finalResult.code);
|
|
1411
|
-
|
|
1412
|
-
// Handle shell settings
|
|
1807
|
+
|
|
1413
1808
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
1414
1809
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
1415
1810
|
error.code = finalResult.code;
|
|
@@ -1418,11 +1813,10 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1418
1813
|
error.result = finalResult;
|
|
1419
1814
|
throw error;
|
|
1420
1815
|
}
|
|
1421
|
-
|
|
1816
|
+
|
|
1422
1817
|
return finalResult;
|
|
1423
1818
|
}
|
|
1424
|
-
|
|
1425
|
-
// Handle errors from intermediate commands
|
|
1819
|
+
|
|
1426
1820
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1427
1821
|
const error = new Error(`Pipeline command failed with exit code ${result.code}`);
|
|
1428
1822
|
error.code = result.code;
|
|
@@ -1432,34 +1826,32 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1432
1826
|
throw error;
|
|
1433
1827
|
}
|
|
1434
1828
|
} catch (error) {
|
|
1435
|
-
// Handle errors from virtual commands in pipeline
|
|
1436
1829
|
const result = createResult({
|
|
1437
1830
|
code: error.code ?? 1,
|
|
1438
1831
|
stdout: currentOutput,
|
|
1439
1832
|
stderr: error.stderr ?? error.message,
|
|
1440
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1441
|
-
|
|
1833
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1834
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1442
1835
|
});
|
|
1443
|
-
|
|
1836
|
+
|
|
1444
1837
|
this.result = result;
|
|
1445
1838
|
this.finished = true;
|
|
1446
|
-
|
|
1839
|
+
|
|
1447
1840
|
if (result.stderr) {
|
|
1448
1841
|
const buf = Buffer.from(result.stderr);
|
|
1449
1842
|
if (this.options.mirror) {
|
|
1450
|
-
process.stderr
|
|
1843
|
+
safeWrite(process.stderr, buf);
|
|
1451
1844
|
}
|
|
1452
|
-
this.
|
|
1453
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
1845
|
+
this._emitProcessedData('stderr', buf);
|
|
1454
1846
|
}
|
|
1455
|
-
|
|
1847
|
+
|
|
1456
1848
|
this.emit('end', result);
|
|
1457
1849
|
this.emit('exit', result.code);
|
|
1458
|
-
|
|
1850
|
+
|
|
1459
1851
|
if (globalShellSettings.errexit) {
|
|
1460
1852
|
throw error;
|
|
1461
1853
|
}
|
|
1462
|
-
|
|
1854
|
+
|
|
1463
1855
|
return result;
|
|
1464
1856
|
}
|
|
1465
1857
|
} else {
|
|
@@ -1469,7 +1861,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1469
1861
|
const commandParts = [cmd];
|
|
1470
1862
|
for (const arg of args) {
|
|
1471
1863
|
if (arg.value !== undefined) {
|
|
1472
|
-
// Handle our parsed arg structure
|
|
1473
1864
|
if (arg.quoted) {
|
|
1474
1865
|
// Preserve original quotes
|
|
1475
1866
|
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
@@ -1480,7 +1871,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1480
1871
|
commandParts.push(arg.value);
|
|
1481
1872
|
}
|
|
1482
1873
|
} else {
|
|
1483
|
-
// Handle plain string args (backward compatibility)
|
|
1484
1874
|
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
1485
1875
|
commandParts.push(`"${arg}"`);
|
|
1486
1876
|
} else {
|
|
@@ -1489,7 +1879,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1489
1879
|
}
|
|
1490
1880
|
}
|
|
1491
1881
|
const commandStr = commandParts.join(' ');
|
|
1492
|
-
|
|
1882
|
+
|
|
1493
1883
|
// Shell tracing for system commands
|
|
1494
1884
|
if (globalShellSettings.xtrace) {
|
|
1495
1885
|
console.log(`+ ${commandStr}`);
|
|
@@ -1497,46 +1887,41 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1497
1887
|
if (globalShellSettings.verbose) {
|
|
1498
1888
|
console.log(commandStr);
|
|
1499
1889
|
}
|
|
1500
|
-
|
|
1501
|
-
// Execute the system command with current input as stdin (ASYNC VERSION)
|
|
1890
|
+
|
|
1502
1891
|
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
1503
|
-
|
|
1504
|
-
const cp = require('child_process');
|
|
1505
|
-
|
|
1892
|
+
|
|
1506
1893
|
return new Promise((resolve, reject) => {
|
|
1507
1894
|
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
1508
1895
|
cwd: this.options.cwd,
|
|
1509
1896
|
env: this.options.env,
|
|
1510
1897
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1511
1898
|
});
|
|
1512
|
-
|
|
1899
|
+
|
|
1513
1900
|
let stdout = '';
|
|
1514
1901
|
let stderr = '';
|
|
1515
|
-
|
|
1902
|
+
|
|
1516
1903
|
proc.stdout.on('data', (chunk) => {
|
|
1517
1904
|
stdout += chunk.toString();
|
|
1518
1905
|
// If this is the last command, emit streaming data
|
|
1519
1906
|
if (isLastCommand) {
|
|
1520
1907
|
if (this.options.mirror) {
|
|
1521
|
-
process.stdout
|
|
1908
|
+
safeWrite(process.stdout, chunk);
|
|
1522
1909
|
}
|
|
1523
|
-
this.
|
|
1524
|
-
this.emit('data', { type: 'stdout', data: chunk });
|
|
1910
|
+
this._emitProcessedData('stdout', chunk);
|
|
1525
1911
|
}
|
|
1526
1912
|
});
|
|
1527
|
-
|
|
1913
|
+
|
|
1528
1914
|
proc.stderr.on('data', (chunk) => {
|
|
1529
1915
|
stderr += chunk.toString();
|
|
1530
1916
|
// If this is the last command, emit streaming data
|
|
1531
1917
|
if (isLastCommand) {
|
|
1532
1918
|
if (this.options.mirror) {
|
|
1533
|
-
process.stderr
|
|
1919
|
+
safeWrite(process.stderr, chunk);
|
|
1534
1920
|
}
|
|
1535
|
-
this.
|
|
1536
|
-
this.emit('data', { type: 'stderr', data: chunk });
|
|
1921
|
+
this._emitProcessedData('stderr', chunk);
|
|
1537
1922
|
}
|
|
1538
1923
|
});
|
|
1539
|
-
|
|
1924
|
+
|
|
1540
1925
|
proc.on('close', (code) => {
|
|
1541
1926
|
resolve({
|
|
1542
1927
|
status: code,
|
|
@@ -1544,29 +1929,43 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1544
1929
|
stderr
|
|
1545
1930
|
});
|
|
1546
1931
|
});
|
|
1547
|
-
|
|
1932
|
+
|
|
1548
1933
|
proc.on('error', reject);
|
|
1549
|
-
|
|
1934
|
+
|
|
1935
|
+
// Use StreamUtils for comprehensive stdin handling
|
|
1936
|
+
if (proc.stdin) {
|
|
1937
|
+
StreamUtils.addStdinErrorHandler(proc.stdin, 'spawnNodeAsync stdin', reject);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1550
1940
|
if (stdin) {
|
|
1551
|
-
|
|
1941
|
+
trace('ProcessRunner', () => `Attempting to write stdin to spawnNodeAsync | ${JSON.stringify({
|
|
1942
|
+
hasStdin: !!proc.stdin,
|
|
1943
|
+
writable: proc.stdin?.writable,
|
|
1944
|
+
destroyed: proc.stdin?.destroyed,
|
|
1945
|
+
closed: proc.stdin?.closed,
|
|
1946
|
+
stdinLength: stdin.length
|
|
1947
|
+
}, null, 2)}`);
|
|
1948
|
+
|
|
1949
|
+
StreamUtils.safeStreamWrite(proc.stdin, stdin, 'spawnNodeAsync stdin');
|
|
1552
1950
|
}
|
|
1553
|
-
|
|
1951
|
+
|
|
1952
|
+
// Safely end the stdin stream
|
|
1953
|
+
StreamUtils.safeStreamEnd(proc.stdin, 'spawnNodeAsync stdin');
|
|
1554
1954
|
});
|
|
1555
1955
|
};
|
|
1556
|
-
|
|
1956
|
+
|
|
1557
1957
|
// Execute using shell to handle complex commands
|
|
1558
1958
|
const argv = ['sh', '-c', commandStr];
|
|
1559
1959
|
const isLastCommand = (i === commands.length - 1);
|
|
1560
1960
|
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
1561
|
-
|
|
1961
|
+
|
|
1562
1962
|
let result = {
|
|
1563
1963
|
code: proc.status || 0,
|
|
1564
1964
|
stdout: proc.stdout || '',
|
|
1565
1965
|
stderr: proc.stderr || '',
|
|
1566
1966
|
stdin: currentInput
|
|
1567
1967
|
};
|
|
1568
|
-
|
|
1569
|
-
// If command failed and pipefail is set, fail the entire pipeline
|
|
1968
|
+
|
|
1570
1969
|
if (globalShellSettings.pipefail && result.code !== 0) {
|
|
1571
1970
|
const error = new Error(`Pipeline command '${commandStr}' failed with exit code ${result.code}`);
|
|
1572
1971
|
error.code = result.code;
|
|
@@ -1574,7 +1973,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1574
1973
|
error.stderr = result.stderr;
|
|
1575
1974
|
throw error;
|
|
1576
1975
|
}
|
|
1577
|
-
|
|
1976
|
+
|
|
1578
1977
|
// If this isn't the last command, pass stdout as stdin to next command
|
|
1579
1978
|
if (i < commands.length - 1) {
|
|
1580
1979
|
currentInput = result.stdout;
|
|
@@ -1586,7 +1985,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1586
1985
|
} else {
|
|
1587
1986
|
// This is the last command - store final result (streaming already handled during execution)
|
|
1588
1987
|
currentOutput = result.stdout;
|
|
1589
|
-
|
|
1988
|
+
|
|
1590
1989
|
// Collect all accumulated stderr
|
|
1591
1990
|
let allStderr = '';
|
|
1592
1991
|
if (this.errChunks && this.errChunks.length > 0) {
|
|
@@ -1595,24 +1994,22 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1595
1994
|
if (result.stderr) {
|
|
1596
1995
|
allStderr += result.stderr;
|
|
1597
1996
|
}
|
|
1598
|
-
|
|
1599
|
-
// Store final result using createResult helper for .text() method compatibility
|
|
1997
|
+
|
|
1600
1998
|
const finalResult = createResult({
|
|
1601
1999
|
code: result.code,
|
|
1602
2000
|
stdout: currentOutput,
|
|
1603
2001
|
stderr: allStderr,
|
|
1604
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1605
|
-
|
|
2002
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
2003
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1606
2004
|
});
|
|
1607
|
-
|
|
2005
|
+
|
|
1608
2006
|
this.result = finalResult;
|
|
1609
2007
|
this.finished = true;
|
|
1610
|
-
|
|
2008
|
+
|
|
1611
2009
|
// Emit completion events
|
|
1612
2010
|
this.emit('end', finalResult);
|
|
1613
2011
|
this.emit('exit', finalResult.code);
|
|
1614
|
-
|
|
1615
|
-
// Handle shell settings
|
|
2012
|
+
|
|
1616
2013
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
1617
2014
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
1618
2015
|
error.code = finalResult.code;
|
|
@@ -1621,39 +2018,37 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1621
2018
|
error.result = finalResult;
|
|
1622
2019
|
throw error;
|
|
1623
2020
|
}
|
|
1624
|
-
|
|
2021
|
+
|
|
1625
2022
|
return finalResult;
|
|
1626
2023
|
}
|
|
1627
|
-
|
|
2024
|
+
|
|
1628
2025
|
} catch (error) {
|
|
1629
|
-
// Handle errors from system commands in pipeline
|
|
1630
2026
|
const result = createResult({
|
|
1631
2027
|
code: error.code ?? 1,
|
|
1632
2028
|
stdout: currentOutput,
|
|
1633
2029
|
stderr: error.stderr ?? error.message,
|
|
1634
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1635
|
-
|
|
2030
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
2031
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1636
2032
|
});
|
|
1637
|
-
|
|
2033
|
+
|
|
1638
2034
|
this.result = result;
|
|
1639
2035
|
this.finished = true;
|
|
1640
|
-
|
|
2036
|
+
|
|
1641
2037
|
if (result.stderr) {
|
|
1642
2038
|
const buf = Buffer.from(result.stderr);
|
|
1643
2039
|
if (this.options.mirror) {
|
|
1644
|
-
process.stderr
|
|
2040
|
+
safeWrite(process.stderr, buf);
|
|
1645
2041
|
}
|
|
1646
|
-
this.
|
|
1647
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
2042
|
+
this._emitProcessedData('stderr', buf);
|
|
1648
2043
|
}
|
|
1649
|
-
|
|
2044
|
+
|
|
1650
2045
|
this.emit('end', result);
|
|
1651
2046
|
this.emit('exit', result.code);
|
|
1652
|
-
|
|
2047
|
+
|
|
1653
2048
|
if (globalShellSettings.errexit) {
|
|
1654
2049
|
throw error;
|
|
1655
2050
|
}
|
|
1656
|
-
|
|
2051
|
+
|
|
1657
2052
|
return result;
|
|
1658
2053
|
}
|
|
1659
2054
|
}
|
|
@@ -1661,110 +2056,103 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1661
2056
|
}
|
|
1662
2057
|
|
|
1663
2058
|
async _runPipeline(commands) {
|
|
1664
|
-
|
|
1665
|
-
commandsCount: commands.length
|
|
1666
|
-
});
|
|
1667
|
-
|
|
2059
|
+
trace('ProcessRunner', () => `_runPipeline ENTER | ${JSON.stringify({
|
|
2060
|
+
commandsCount: commands.length
|
|
2061
|
+
}, null, 2)}`);
|
|
2062
|
+
|
|
1668
2063
|
if (commands.length === 0) {
|
|
1669
|
-
|
|
2064
|
+
trace('ProcessRunner', () => `BRANCH: _runPipeline => NO_COMMANDS | ${JSON.stringify({}, null, 2)}`);
|
|
1670
2065
|
return createResult({ code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' });
|
|
1671
2066
|
}
|
|
1672
2067
|
|
|
1673
2068
|
|
|
1674
2069
|
// For true streaming, we need to connect processes via pipes
|
|
1675
2070
|
if (isBun) {
|
|
1676
|
-
|
|
2071
|
+
trace('ProcessRunner', () => `BRANCH: _runPipeline => BUN_STREAMING | ${JSON.stringify({}, null, 2)}`);
|
|
1677
2072
|
return this._runStreamingPipelineBun(commands);
|
|
1678
2073
|
}
|
|
1679
|
-
|
|
2074
|
+
|
|
1680
2075
|
// For Node.js, fall back to non-streaming implementation for now
|
|
1681
|
-
|
|
2076
|
+
trace('ProcessRunner', () => `BRANCH: _runPipeline => NODE_NON_STREAMING | ${JSON.stringify({}, null, 2)}`);
|
|
1682
2077
|
return this._runPipelineNonStreaming(commands);
|
|
1683
2078
|
}
|
|
1684
2079
|
|
|
1685
2080
|
// Run programmatic pipeline (.pipe() method)
|
|
1686
2081
|
async _runProgrammaticPipeline(source, destination) {
|
|
1687
|
-
|
|
1688
|
-
|
|
2082
|
+
trace('ProcessRunner', () => `_runProgrammaticPipeline ENTER | ${JSON.stringify({}, null, 2)}`);
|
|
2083
|
+
|
|
1689
2084
|
try {
|
|
1690
|
-
|
|
1691
|
-
trace('ProcessRunner', 'Executing source command', {});
|
|
2085
|
+
trace('ProcessRunner', () => 'Executing source command');
|
|
1692
2086
|
const sourceResult = await source;
|
|
1693
|
-
|
|
2087
|
+
|
|
1694
2088
|
if (sourceResult.code !== 0) {
|
|
1695
|
-
|
|
1696
|
-
traceBranch('ProcessRunner', '_runProgrammaticPipeline', 'SOURCE_FAILED', {
|
|
2089
|
+
trace('ProcessRunner', () => `BRANCH: _runProgrammaticPipeline => SOURCE_FAILED | ${JSON.stringify({
|
|
1697
2090
|
code: sourceResult.code,
|
|
1698
|
-
stderr: sourceResult.stderr
|
|
1699
|
-
});
|
|
2091
|
+
stderr: sourceResult.stderr
|
|
2092
|
+
}, null, 2)}`);
|
|
1700
2093
|
return sourceResult;
|
|
1701
2094
|
}
|
|
1702
|
-
|
|
1703
|
-
// Create a new ProcessRunner with the correct stdin for the destination
|
|
2095
|
+
|
|
1704
2096
|
const destWithStdin = new ProcessRunner(destination.spec, {
|
|
1705
2097
|
...destination.options,
|
|
1706
2098
|
stdin: sourceResult.stdout
|
|
1707
2099
|
});
|
|
1708
|
-
|
|
1709
|
-
// Execute the destination command
|
|
2100
|
+
|
|
1710
2101
|
const destResult = await destWithStdin;
|
|
1711
|
-
|
|
2102
|
+
|
|
1712
2103
|
// Debug: Log what destResult looks like
|
|
1713
|
-
trace('ProcessRunner',
|
|
1714
|
-
code: destResult.code,
|
|
2104
|
+
trace('ProcessRunner', () => `destResult debug | ${JSON.stringify({
|
|
2105
|
+
code: destResult.code,
|
|
1715
2106
|
codeType: typeof destResult.code,
|
|
1716
2107
|
hasCode: 'code' in destResult,
|
|
1717
2108
|
keys: Object.keys(destResult),
|
|
1718
2109
|
resultType: typeof destResult,
|
|
1719
2110
|
fullResult: JSON.stringify(destResult, null, 2).slice(0, 200)
|
|
1720
|
-
});
|
|
2111
|
+
}, null, 2)}`);
|
|
1721
2112
|
|
|
1722
|
-
// Return the final result with combined information
|
|
1723
2113
|
return createResult({
|
|
1724
2114
|
code: destResult.code,
|
|
1725
2115
|
stdout: destResult.stdout,
|
|
1726
2116
|
stderr: sourceResult.stderr + destResult.stderr,
|
|
1727
2117
|
stdin: sourceResult.stdin
|
|
1728
2118
|
});
|
|
1729
|
-
|
|
2119
|
+
|
|
1730
2120
|
} catch (error) {
|
|
1731
2121
|
const result = createResult({
|
|
1732
2122
|
code: error.code ?? 1,
|
|
1733
2123
|
stdout: '',
|
|
1734
2124
|
stderr: error.message || 'Pipeline execution failed',
|
|
1735
|
-
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1736
|
-
|
|
2125
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
2126
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1737
2127
|
});
|
|
1738
|
-
|
|
2128
|
+
|
|
1739
2129
|
this.result = result;
|
|
1740
2130
|
this.finished = true;
|
|
1741
|
-
|
|
2131
|
+
|
|
1742
2132
|
const buf = Buffer.from(result.stderr);
|
|
1743
2133
|
if (this.options.mirror) {
|
|
1744
|
-
process.stderr
|
|
2134
|
+
safeWrite(process.stderr, buf);
|
|
1745
2135
|
}
|
|
1746
|
-
this.
|
|
1747
|
-
|
|
1748
|
-
|
|
2136
|
+
this._emitProcessedData('stderr', buf);
|
|
2137
|
+
|
|
1749
2138
|
this.emit('end', result);
|
|
1750
2139
|
this.emit('exit', result.code);
|
|
1751
|
-
|
|
2140
|
+
|
|
1752
2141
|
return result;
|
|
1753
2142
|
}
|
|
1754
2143
|
}
|
|
1755
2144
|
|
|
1756
|
-
// Async iteration support
|
|
1757
2145
|
async* stream() {
|
|
1758
|
-
|
|
2146
|
+
trace('ProcessRunner', () => `stream ENTER | ${JSON.stringify({
|
|
1759
2147
|
started: this.started,
|
|
1760
|
-
finished: this.finished
|
|
1761
|
-
});
|
|
1762
|
-
|
|
2148
|
+
finished: this.finished
|
|
2149
|
+
}, null, 2)}`);
|
|
2150
|
+
|
|
1763
2151
|
if (!this.started) {
|
|
1764
|
-
trace('ProcessRunner', 'Auto-starting async process from stream()'
|
|
2152
|
+
trace('ProcessRunner', () => 'Auto-starting async process from stream()');
|
|
1765
2153
|
this._startAsync(); // Start but don't await
|
|
1766
2154
|
}
|
|
1767
|
-
|
|
2155
|
+
|
|
1768
2156
|
let buffer = [];
|
|
1769
2157
|
let resolve, reject;
|
|
1770
2158
|
let ended = false;
|
|
@@ -1804,115 +2192,108 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1804
2192
|
cleanedUp = true;
|
|
1805
2193
|
this.off('data', onData);
|
|
1806
2194
|
this.off('end', onEnd);
|
|
1807
|
-
|
|
1808
|
-
// Kill the process if it's still running when iteration is stopped
|
|
2195
|
+
|
|
1809
2196
|
// This happens when breaking from a for-await loop
|
|
1810
2197
|
if (!this.finished) {
|
|
1811
2198
|
this.kill();
|
|
1812
2199
|
}
|
|
1813
2200
|
}
|
|
1814
2201
|
}
|
|
1815
|
-
|
|
1816
|
-
// Kill the running process or cancel virtual command
|
|
2202
|
+
|
|
1817
2203
|
kill() {
|
|
1818
|
-
|
|
2204
|
+
trace('ProcessRunner', () => `kill ENTER | ${JSON.stringify({
|
|
1819
2205
|
cancelled: this._cancelled,
|
|
1820
2206
|
finished: this.finished,
|
|
1821
2207
|
hasChild: !!this.child,
|
|
1822
2208
|
hasVirtualGenerator: !!this._virtualGenerator
|
|
1823
|
-
});
|
|
1824
|
-
|
|
2209
|
+
}, null, 2)}`);
|
|
2210
|
+
|
|
1825
2211
|
// Mark as cancelled for virtual commands
|
|
1826
2212
|
this._cancelled = true;
|
|
1827
|
-
|
|
1828
|
-
// Resolve the cancel promise to break the race in virtual command execution
|
|
2213
|
+
|
|
1829
2214
|
if (this._cancelResolve) {
|
|
1830
|
-
trace('ProcessRunner', 'Resolving cancel promise'
|
|
2215
|
+
trace('ProcessRunner', () => 'Resolving cancel promise');
|
|
1831
2216
|
this._cancelResolve();
|
|
1832
2217
|
}
|
|
1833
|
-
|
|
2218
|
+
|
|
1834
2219
|
// Abort any async operations
|
|
1835
2220
|
if (this._abortController) {
|
|
1836
|
-
trace('ProcessRunner', 'Aborting controller'
|
|
2221
|
+
trace('ProcessRunner', () => 'Aborting controller');
|
|
1837
2222
|
this._abortController.abort();
|
|
1838
2223
|
}
|
|
1839
|
-
|
|
2224
|
+
|
|
1840
2225
|
// If it's a virtual generator, try to close it
|
|
1841
2226
|
if (this._virtualGenerator && this._virtualGenerator.return) {
|
|
1842
|
-
trace('ProcessRunner', 'Closing virtual generator'
|
|
2227
|
+
trace('ProcessRunner', () => 'Closing virtual generator');
|
|
1843
2228
|
try {
|
|
1844
2229
|
this._virtualGenerator.return();
|
|
1845
2230
|
} catch (err) {
|
|
1846
|
-
trace('ProcessRunner',
|
|
2231
|
+
trace('ProcessRunner', () => `Error closing generator | ${JSON.stringify({ error: err.message }, null, 2)}`);
|
|
1847
2232
|
}
|
|
1848
2233
|
}
|
|
1849
|
-
|
|
2234
|
+
|
|
1850
2235
|
// Kill child process if it exists
|
|
1851
2236
|
if (this.child && !this.finished) {
|
|
1852
|
-
|
|
2237
|
+
trace('ProcessRunner', () => `BRANCH: hasChild => killing | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
1853
2238
|
try {
|
|
1854
|
-
// Kill the process group to ensure all child processes are terminated
|
|
1855
2239
|
if (this.child.pid) {
|
|
1856
2240
|
if (isBun) {
|
|
1857
|
-
trace('ProcessRunner',
|
|
2241
|
+
trace('ProcessRunner', () => `Killing Bun process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
1858
2242
|
this.child.kill();
|
|
1859
2243
|
} else {
|
|
1860
2244
|
// In Node.js, kill the process group
|
|
1861
|
-
trace('ProcessRunner',
|
|
2245
|
+
trace('ProcessRunner', () => `Killing Node process group | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
1862
2246
|
process.kill(-this.child.pid, 'SIGTERM');
|
|
1863
2247
|
}
|
|
1864
2248
|
}
|
|
1865
2249
|
this.finished = true;
|
|
1866
2250
|
} catch (err) {
|
|
1867
2251
|
// Process might already be dead
|
|
1868
|
-
trace('ProcessRunner',
|
|
2252
|
+
trace('ProcessRunner', () => `Error killing process | ${JSON.stringify({ error: err.message }, null, 2)}`);
|
|
1869
2253
|
console.error('Error killing process:', err.message);
|
|
1870
2254
|
}
|
|
1871
2255
|
}
|
|
1872
|
-
|
|
2256
|
+
|
|
1873
2257
|
// Mark as finished
|
|
1874
2258
|
this.finished = true;
|
|
1875
|
-
|
|
1876
|
-
|
|
2259
|
+
|
|
2260
|
+
trace('ProcessRunner', () => `kill EXIT | ${JSON.stringify({
|
|
1877
2261
|
cancelled: this._cancelled,
|
|
1878
|
-
finished: this.finished
|
|
1879
|
-
});
|
|
2262
|
+
finished: this.finished
|
|
2263
|
+
}, null, 2)}`);
|
|
1880
2264
|
}
|
|
1881
2265
|
|
|
1882
|
-
// Programmatic piping support
|
|
1883
2266
|
pipe(destination) {
|
|
1884
|
-
|
|
2267
|
+
trace('ProcessRunner', () => `pipe ENTER | ${JSON.stringify({
|
|
1885
2268
|
hasDestination: !!destination,
|
|
1886
|
-
destinationType: destination?.constructor?.name
|
|
1887
|
-
});
|
|
1888
|
-
|
|
1889
|
-
// If destination is a ProcessRunner, create a pipeline
|
|
2269
|
+
destinationType: destination?.constructor?.name
|
|
2270
|
+
}, null, 2)}`);
|
|
2271
|
+
|
|
1890
2272
|
if (destination instanceof ProcessRunner) {
|
|
1891
|
-
|
|
1892
|
-
// Create a new ProcessRunner that represents the piped operation
|
|
2273
|
+
trace('ProcessRunner', () => `BRANCH: pipe => PROCESS_RUNNER_DEST | ${JSON.stringify({}, null, 2)}`);
|
|
1893
2274
|
const pipeSpec = {
|
|
1894
2275
|
mode: 'pipeline',
|
|
1895
2276
|
source: this,
|
|
1896
2277
|
destination: destination
|
|
1897
2278
|
};
|
|
1898
|
-
|
|
2279
|
+
|
|
1899
2280
|
const pipeRunner = new ProcessRunner(pipeSpec, {
|
|
1900
2281
|
...this.options,
|
|
1901
2282
|
capture: destination.options.capture ?? true
|
|
1902
2283
|
});
|
|
1903
|
-
|
|
1904
|
-
|
|
2284
|
+
|
|
2285
|
+
trace('ProcessRunner', () => `pipe EXIT | ${JSON.stringify({ mode: 'pipeline' }, null, 2)}`);
|
|
1905
2286
|
return pipeRunner;
|
|
1906
2287
|
}
|
|
1907
|
-
|
|
2288
|
+
|
|
1908
2289
|
// If destination is a template literal result (from $`command`), use its spec
|
|
1909
2290
|
if (destination && destination.spec) {
|
|
1910
|
-
|
|
2291
|
+
trace('ProcessRunner', () => `BRANCH: pipe => TEMPLATE_LITERAL_DEST | ${JSON.stringify({}, null, 2)}`);
|
|
1911
2292
|
const destRunner = new ProcessRunner(destination.spec, destination.options);
|
|
1912
2293
|
return this.pipe(destRunner);
|
|
1913
2294
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
2295
|
+
|
|
2296
|
+
trace('ProcessRunner', () => `BRANCH: pipe => INVALID_DEST | ${JSON.stringify({}, null, 2)}`);
|
|
1916
2297
|
throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
|
|
1917
2298
|
}
|
|
1918
2299
|
|
|
@@ -1940,110 +2321,103 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1940
2321
|
|
|
1941
2322
|
// Internal sync execution
|
|
1942
2323
|
_startSync() {
|
|
1943
|
-
|
|
2324
|
+
trace('ProcessRunner', () => `_startSync ENTER | ${JSON.stringify({
|
|
1944
2325
|
started: this.started,
|
|
1945
|
-
spec: this.spec
|
|
1946
|
-
});
|
|
1947
|
-
|
|
2326
|
+
spec: this.spec
|
|
2327
|
+
}, null, 2)}`);
|
|
2328
|
+
|
|
1948
2329
|
if (this.started) {
|
|
1949
|
-
|
|
2330
|
+
trace('ProcessRunner', () => `BRANCH: _startSync => ALREADY_STARTED | ${JSON.stringify({}, null, 2)}`);
|
|
1950
2331
|
throw new Error('Command already started - cannot run sync after async start');
|
|
1951
2332
|
}
|
|
1952
|
-
|
|
2333
|
+
|
|
1953
2334
|
this.started = true;
|
|
1954
2335
|
this._mode = 'sync';
|
|
1955
|
-
trace('ProcessRunner',
|
|
1956
|
-
|
|
2336
|
+
trace('ProcessRunner', () => `Starting sync execution | ${JSON.stringify({ mode: this._mode }, null, 2)}`);
|
|
2337
|
+
|
|
1957
2338
|
const { cwd, env, stdin } = this.options;
|
|
1958
2339
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1959
|
-
|
|
1960
|
-
// Shell tracing (set -x equivalent)
|
|
2340
|
+
|
|
1961
2341
|
if (globalShellSettings.xtrace) {
|
|
1962
2342
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
1963
2343
|
console.log(`+ ${traceCmd}`);
|
|
1964
2344
|
}
|
|
1965
|
-
|
|
1966
|
-
// Verbose mode (set -v equivalent)
|
|
2345
|
+
|
|
1967
2346
|
if (globalShellSettings.verbose) {
|
|
1968
2347
|
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
1969
2348
|
console.log(verboseCmd);
|
|
1970
2349
|
}
|
|
1971
|
-
|
|
2350
|
+
|
|
1972
2351
|
let result;
|
|
1973
|
-
|
|
2352
|
+
|
|
1974
2353
|
if (isBun) {
|
|
1975
2354
|
// Use Bun's synchronous spawn
|
|
1976
2355
|
const proc = Bun.spawnSync(argv, {
|
|
1977
2356
|
cwd,
|
|
1978
2357
|
env,
|
|
1979
|
-
stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
|
|
1980
|
-
|
|
1981
|
-
|
|
2358
|
+
stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
|
|
2359
|
+
Buffer.isBuffer(stdin) ? stdin :
|
|
2360
|
+
stdin === 'ignore' ? undefined : undefined,
|
|
1982
2361
|
stdout: 'pipe',
|
|
1983
2362
|
stderr: 'pipe'
|
|
1984
2363
|
});
|
|
1985
|
-
|
|
2364
|
+
|
|
1986
2365
|
result = createResult({
|
|
1987
2366
|
code: proc.exitCode || 0,
|
|
1988
2367
|
stdout: proc.stdout?.toString('utf8') || '',
|
|
1989
2368
|
stderr: proc.stderr?.toString('utf8') || '',
|
|
1990
|
-
stdin: typeof stdin === 'string' ? stdin :
|
|
1991
|
-
|
|
2369
|
+
stdin: typeof stdin === 'string' ? stdin :
|
|
2370
|
+
Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
|
|
1992
2371
|
});
|
|
1993
2372
|
result.child = proc;
|
|
1994
2373
|
} else {
|
|
1995
2374
|
// Use Node's synchronous spawn
|
|
1996
|
-
const require = createRequire(import.meta.url);
|
|
1997
|
-
const cp = require('child_process');
|
|
1998
2375
|
const proc = cp.spawnSync(argv[0], argv.slice(1), {
|
|
1999
2376
|
cwd,
|
|
2000
2377
|
env,
|
|
2001
|
-
input: typeof stdin === 'string' ? stdin :
|
|
2002
|
-
|
|
2378
|
+
input: typeof stdin === 'string' ? stdin :
|
|
2379
|
+
Buffer.isBuffer(stdin) ? stdin : undefined,
|
|
2003
2380
|
encoding: 'utf8',
|
|
2004
2381
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
2005
2382
|
});
|
|
2006
|
-
|
|
2383
|
+
|
|
2007
2384
|
result = createResult({
|
|
2008
2385
|
code: proc.status || 0,
|
|
2009
2386
|
stdout: proc.stdout || '',
|
|
2010
2387
|
stderr: proc.stderr || '',
|
|
2011
|
-
stdin: typeof stdin === 'string' ? stdin :
|
|
2012
|
-
|
|
2388
|
+
stdin: typeof stdin === 'string' ? stdin :
|
|
2389
|
+
Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
|
|
2013
2390
|
});
|
|
2014
2391
|
result.child = proc;
|
|
2015
2392
|
}
|
|
2016
|
-
|
|
2393
|
+
|
|
2017
2394
|
// Mirror output if requested (but always capture for result)
|
|
2018
2395
|
if (this.options.mirror) {
|
|
2019
|
-
if (result.stdout) process.stdout
|
|
2020
|
-
if (result.stderr) process.stderr
|
|
2396
|
+
if (result.stdout) safeWrite(process.stdout, result.stdout);
|
|
2397
|
+
if (result.stderr) safeWrite(process.stderr, result.stderr);
|
|
2021
2398
|
}
|
|
2022
|
-
|
|
2399
|
+
|
|
2023
2400
|
// Store chunks for events (batched after completion)
|
|
2024
2401
|
this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
|
|
2025
2402
|
this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
|
|
2026
|
-
|
|
2403
|
+
|
|
2027
2404
|
this.result = result;
|
|
2028
2405
|
this.finished = true;
|
|
2029
|
-
|
|
2406
|
+
|
|
2030
2407
|
// Emit batched events after completion
|
|
2031
2408
|
if (result.stdout) {
|
|
2032
2409
|
const stdoutBuf = Buffer.from(result.stdout);
|
|
2033
|
-
this.
|
|
2034
|
-
this.emit('data', { type: 'stdout', data: stdoutBuf });
|
|
2410
|
+
this._emitProcessedData('stdout', stdoutBuf);
|
|
2035
2411
|
}
|
|
2036
|
-
|
|
2412
|
+
|
|
2037
2413
|
if (result.stderr) {
|
|
2038
2414
|
const stderrBuf = Buffer.from(result.stderr);
|
|
2039
|
-
this.
|
|
2040
|
-
this.emit('data', { type: 'stderr', data: stderrBuf });
|
|
2415
|
+
this._emitProcessedData('stderr', stderrBuf);
|
|
2041
2416
|
}
|
|
2042
|
-
|
|
2417
|
+
|
|
2043
2418
|
this.emit('end', result);
|
|
2044
2419
|
this.emit('exit', result.code);
|
|
2045
|
-
|
|
2046
|
-
// Handle shell settings (set -e equivalent)
|
|
2420
|
+
|
|
2047
2421
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
2048
2422
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
2049
2423
|
error.code = result.code;
|
|
@@ -2052,7 +2426,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2052
2426
|
error.result = result;
|
|
2053
2427
|
throw error;
|
|
2054
2428
|
}
|
|
2055
|
-
|
|
2429
|
+
|
|
2056
2430
|
return result;
|
|
2057
2431
|
}
|
|
2058
2432
|
|
|
@@ -2072,93 +2446,91 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2072
2446
|
|
|
2073
2447
|
// Public APIs
|
|
2074
2448
|
async function sh(commandString, options = {}) {
|
|
2075
|
-
|
|
2449
|
+
trace('API', () => `sh ENTER | ${JSON.stringify({
|
|
2076
2450
|
command: commandString,
|
|
2077
|
-
options
|
|
2078
|
-
});
|
|
2079
|
-
|
|
2451
|
+
options
|
|
2452
|
+
}, null, 2)}`);
|
|
2453
|
+
|
|
2080
2454
|
const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
|
|
2081
2455
|
const result = await runner._startAsync();
|
|
2082
|
-
|
|
2083
|
-
|
|
2456
|
+
|
|
2457
|
+
trace('API', () => `sh EXIT | ${JSON.stringify({ code: result.code }, null, 2)}`);
|
|
2084
2458
|
return result;
|
|
2085
2459
|
}
|
|
2086
2460
|
|
|
2087
2461
|
async function exec(file, args = [], options = {}) {
|
|
2088
|
-
|
|
2462
|
+
trace('API', () => `exec ENTER | ${JSON.stringify({
|
|
2089
2463
|
file,
|
|
2090
2464
|
argsCount: args.length,
|
|
2091
|
-
options
|
|
2092
|
-
});
|
|
2093
|
-
|
|
2465
|
+
options
|
|
2466
|
+
}, null, 2)}`);
|
|
2467
|
+
|
|
2094
2468
|
const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
|
|
2095
2469
|
const result = await runner._startAsync();
|
|
2096
|
-
|
|
2097
|
-
|
|
2470
|
+
|
|
2471
|
+
trace('API', () => `exec EXIT | ${JSON.stringify({ code: result.code }, null, 2)}`);
|
|
2098
2472
|
return result;
|
|
2099
2473
|
}
|
|
2100
2474
|
|
|
2101
2475
|
async function run(commandOrTokens, options = {}) {
|
|
2102
|
-
|
|
2476
|
+
trace('API', () => `run ENTER | ${JSON.stringify({
|
|
2103
2477
|
type: typeof commandOrTokens,
|
|
2104
|
-
options
|
|
2105
|
-
});
|
|
2106
|
-
|
|
2478
|
+
options
|
|
2479
|
+
}, null, 2)}`);
|
|
2480
|
+
|
|
2107
2481
|
if (typeof commandOrTokens === 'string') {
|
|
2108
|
-
|
|
2482
|
+
trace('API', () => `BRANCH: run => STRING_COMMAND | ${JSON.stringify({ command: commandOrTokens }, null, 2)}`);
|
|
2109
2483
|
return sh(commandOrTokens, { ...options, mirror: false, capture: true });
|
|
2110
2484
|
}
|
|
2111
|
-
|
|
2485
|
+
|
|
2112
2486
|
const [file, ...args] = commandOrTokens;
|
|
2113
|
-
|
|
2487
|
+
trace('API', () => `BRANCH: run => TOKEN_ARRAY | ${JSON.stringify({ file, argsCount: args.length }, null, 2)}`);
|
|
2114
2488
|
return exec(file, args, { ...options, mirror: false, capture: true });
|
|
2115
2489
|
}
|
|
2116
2490
|
|
|
2117
|
-
// Enhanced tagged template that returns ProcessRunner
|
|
2118
2491
|
function $tagged(strings, ...values) {
|
|
2119
|
-
|
|
2492
|
+
trace('API', () => `$tagged ENTER | ${JSON.stringify({
|
|
2120
2493
|
stringsLength: strings.length,
|
|
2121
|
-
valuesLength: values.length
|
|
2122
|
-
});
|
|
2123
|
-
|
|
2494
|
+
valuesLength: values.length
|
|
2495
|
+
}, null, 2)}`);
|
|
2496
|
+
|
|
2124
2497
|
const cmd = buildShellCommand(strings, values);
|
|
2125
2498
|
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
|
|
2126
|
-
|
|
2127
|
-
|
|
2499
|
+
|
|
2500
|
+
trace('API', () => `$tagged EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
|
|
2128
2501
|
return runner;
|
|
2129
2502
|
}
|
|
2130
2503
|
|
|
2131
2504
|
function create(defaultOptions = {}) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2505
|
+
trace('API', () => `create ENTER | ${JSON.stringify({ defaultOptions }, null, 2)}`);
|
|
2506
|
+
|
|
2134
2507
|
const tagged = (strings, ...values) => {
|
|
2135
|
-
|
|
2508
|
+
trace('API', () => `create.tagged ENTER | ${JSON.stringify({
|
|
2136
2509
|
stringsLength: strings.length,
|
|
2137
|
-
valuesLength: values.length
|
|
2138
|
-
});
|
|
2139
|
-
|
|
2510
|
+
valuesLength: values.length
|
|
2511
|
+
}, null, 2)}`);
|
|
2512
|
+
|
|
2140
2513
|
const cmd = buildShellCommand(strings, values);
|
|
2141
2514
|
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
|
|
2142
|
-
|
|
2143
|
-
|
|
2515
|
+
|
|
2516
|
+
trace('API', () => `create.tagged EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
|
|
2144
2517
|
return runner;
|
|
2145
2518
|
};
|
|
2146
|
-
|
|
2147
|
-
|
|
2519
|
+
|
|
2520
|
+
trace('API', () => `create EXIT | ${JSON.stringify({}, null, 2)}`);
|
|
2148
2521
|
return tagged;
|
|
2149
2522
|
}
|
|
2150
2523
|
|
|
2151
|
-
function raw(value) {
|
|
2152
|
-
return { raw: String(value) };
|
|
2524
|
+
function raw(value) {
|
|
2525
|
+
return { raw: String(value) };
|
|
2153
2526
|
}
|
|
2154
2527
|
|
|
2155
|
-
// Shell setting control functions (like bash set/unset)
|
|
2156
2528
|
function set(option) {
|
|
2157
2529
|
const mapping = {
|
|
2158
2530
|
'e': 'errexit', // set -e: exit on error
|
|
2159
2531
|
'errexit': 'errexit',
|
|
2160
2532
|
'v': 'verbose', // set -v: verbose
|
|
2161
|
-
'verbose': 'verbose',
|
|
2533
|
+
'verbose': 'verbose',
|
|
2162
2534
|
'x': 'xtrace', // set -x: trace execution
|
|
2163
2535
|
'xtrace': 'xtrace',
|
|
2164
2536
|
'u': 'nounset', // set -u: error on unset vars
|
|
@@ -2166,7 +2538,7 @@ function set(option) {
|
|
|
2166
2538
|
'o pipefail': 'pipefail', // set -o pipefail
|
|
2167
2539
|
'pipefail': 'pipefail'
|
|
2168
2540
|
};
|
|
2169
|
-
|
|
2541
|
+
|
|
2170
2542
|
if (mapping[option]) {
|
|
2171
2543
|
globalShellSettings[mapping[option]] = true;
|
|
2172
2544
|
if (globalShellSettings.verbose) {
|
|
@@ -2180,7 +2552,7 @@ function unset(option) {
|
|
|
2180
2552
|
const mapping = {
|
|
2181
2553
|
'e': 'errexit',
|
|
2182
2554
|
'errexit': 'errexit',
|
|
2183
|
-
'v': 'verbose',
|
|
2555
|
+
'v': 'verbose',
|
|
2184
2556
|
'verbose': 'verbose',
|
|
2185
2557
|
'x': 'xtrace',
|
|
2186
2558
|
'xtrace': 'xtrace',
|
|
@@ -2189,7 +2561,7 @@ function unset(option) {
|
|
|
2189
2561
|
'o pipefail': 'pipefail',
|
|
2190
2562
|
'pipefail': 'pipefail'
|
|
2191
2563
|
};
|
|
2192
|
-
|
|
2564
|
+
|
|
2193
2565
|
if (mapping[option]) {
|
|
2194
2566
|
globalShellSettings[mapping[option]] = false;
|
|
2195
2567
|
if (globalShellSettings.verbose) {
|
|
@@ -2204,10 +2576,10 @@ const shell = {
|
|
|
2204
2576
|
set,
|
|
2205
2577
|
unset,
|
|
2206
2578
|
settings: () => ({ ...globalShellSettings }),
|
|
2207
|
-
|
|
2579
|
+
|
|
2208
2580
|
// Bash-like shortcuts
|
|
2209
2581
|
errexit: (enable = true) => enable ? set('e') : unset('e'),
|
|
2210
|
-
verbose: (enable = true) => enable ? set('v') : unset('v'),
|
|
2582
|
+
verbose: (enable = true) => enable ? set('v') : unset('v'),
|
|
2211
2583
|
xtrace: (enable = true) => enable ? set('x') : unset('x'),
|
|
2212
2584
|
pipefail: (enable = true) => enable ? set('o pipefail') : unset('o pipefail'),
|
|
2213
2585
|
nounset: (enable = true) => enable ? set('u') : unset('u'),
|
|
@@ -2215,20 +2587,16 @@ const shell = {
|
|
|
2215
2587
|
|
|
2216
2588
|
// Virtual command registration API
|
|
2217
2589
|
function register(name, handler) {
|
|
2218
|
-
|
|
2219
|
-
|
|
2590
|
+
trace('VirtualCommands', () => `register ENTER | ${JSON.stringify({ name }, null, 2)}`);
|
|
2220
2591
|
virtualCommands.set(name, handler);
|
|
2221
|
-
|
|
2222
|
-
traceFunc('VirtualCommands', 'register', 'EXIT', { registered: true });
|
|
2592
|
+
trace('VirtualCommands', () => `register EXIT | ${JSON.stringify({ registered: true }, null, 2)}`);
|
|
2223
2593
|
return virtualCommands;
|
|
2224
2594
|
}
|
|
2225
2595
|
|
|
2226
2596
|
function unregister(name) {
|
|
2227
|
-
|
|
2228
|
-
|
|
2597
|
+
trace('VirtualCommands', () => `unregister ENTER | ${JSON.stringify({ name }, null, 2)}`);
|
|
2229
2598
|
const deleted = virtualCommands.delete(name);
|
|
2230
|
-
|
|
2231
|
-
traceFunc('VirtualCommands', 'unregister', 'EXIT', { deleted });
|
|
2599
|
+
trace('VirtualCommands', () => `unregister EXIT | ${JSON.stringify({ deleted }, null, 2)}`);
|
|
2232
2600
|
return deleted;
|
|
2233
2601
|
}
|
|
2234
2602
|
|
|
@@ -2249,62 +2617,62 @@ function disableVirtualCommands() {
|
|
|
2249
2617
|
// Built-in commands that match Bun.$ functionality
|
|
2250
2618
|
function registerBuiltins() {
|
|
2251
2619
|
// cd - change directory
|
|
2252
|
-
register('cd', async (args) => {
|
|
2620
|
+
register('cd', async ({ args }) => {
|
|
2253
2621
|
const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
|
|
2254
|
-
trace('VirtualCommand',
|
|
2255
|
-
|
|
2622
|
+
trace('VirtualCommand', () => `cd: changing directory | ${JSON.stringify({ target }, null, 2)}`);
|
|
2623
|
+
|
|
2256
2624
|
try {
|
|
2257
2625
|
process.chdir(target);
|
|
2258
2626
|
const newDir = process.cwd();
|
|
2259
|
-
trace('VirtualCommand',
|
|
2260
|
-
return
|
|
2627
|
+
trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
|
|
2628
|
+
return VirtualUtils.success(newDir);
|
|
2261
2629
|
} catch (error) {
|
|
2262
|
-
trace('VirtualCommand',
|
|
2630
|
+
trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
2263
2631
|
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
2264
2632
|
}
|
|
2265
2633
|
});
|
|
2266
2634
|
|
|
2267
2635
|
// pwd - print working directory
|
|
2268
|
-
register('pwd', async (args, stdin,
|
|
2636
|
+
register('pwd', async ({ args, stdin, cwd }) => {
|
|
2269
2637
|
// If cwd option is provided, return that instead of process.cwd()
|
|
2270
|
-
const dir =
|
|
2271
|
-
trace('VirtualCommand',
|
|
2272
|
-
return
|
|
2638
|
+
const dir = cwd || process.cwd();
|
|
2639
|
+
trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
|
|
2640
|
+
return VirtualUtils.success(dir);
|
|
2273
2641
|
});
|
|
2274
2642
|
|
|
2275
2643
|
// echo - print arguments
|
|
2276
|
-
register('echo', async (args) => {
|
|
2277
|
-
trace('VirtualCommand',
|
|
2278
|
-
|
|
2644
|
+
register('echo', async ({ args }) => {
|
|
2645
|
+
trace('VirtualCommand', () => `echo: processing | ${JSON.stringify({ argsCount: args.length }, null, 2)}`);
|
|
2646
|
+
|
|
2279
2647
|
let output = args.join(' ');
|
|
2280
2648
|
if (args.includes('-n')) {
|
|
2281
2649
|
// Don't add newline
|
|
2282
|
-
|
|
2650
|
+
trace('VirtualCommand', () => `BRANCH: echo => NO_NEWLINE | ${JSON.stringify({}, null, 2)}`);
|
|
2283
2651
|
output = args.filter(arg => arg !== '-n').join(' ');
|
|
2284
2652
|
} else {
|
|
2285
2653
|
output += '\n';
|
|
2286
2654
|
}
|
|
2287
|
-
return
|
|
2655
|
+
return VirtualUtils.success(output);
|
|
2288
2656
|
});
|
|
2289
2657
|
|
|
2290
2658
|
// sleep - wait for specified time
|
|
2291
|
-
register('sleep', async (args) => {
|
|
2659
|
+
register('sleep', async ({ args }) => {
|
|
2292
2660
|
const seconds = parseFloat(args[0] || 0);
|
|
2293
|
-
trace('VirtualCommand',
|
|
2294
|
-
|
|
2661
|
+
trace('VirtualCommand', () => `sleep: starting | ${JSON.stringify({ seconds }, null, 2)}`);
|
|
2662
|
+
|
|
2295
2663
|
if (isNaN(seconds) || seconds < 0) {
|
|
2296
|
-
trace('VirtualCommand',
|
|
2664
|
+
trace('VirtualCommand', () => `sleep: invalid interval | ${JSON.stringify({ input: args[0] }, null, 2)}`);
|
|
2297
2665
|
return { stderr: 'sleep: invalid time interval', code: 1 };
|
|
2298
2666
|
}
|
|
2299
|
-
|
|
2667
|
+
|
|
2300
2668
|
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
2301
|
-
trace('VirtualCommand',
|
|
2669
|
+
trace('VirtualCommand', () => `sleep: completed | ${JSON.stringify({ seconds }, null, 2)}`);
|
|
2302
2670
|
return { stdout: '', code: 0 };
|
|
2303
2671
|
});
|
|
2304
2672
|
|
|
2305
2673
|
// true - always succeed
|
|
2306
2674
|
register('true', async () => {
|
|
2307
|
-
return
|
|
2675
|
+
return VirtualUtils.success();
|
|
2308
2676
|
});
|
|
2309
2677
|
|
|
2310
2678
|
// false - always fail
|
|
@@ -2313,38 +2681,35 @@ function registerBuiltins() {
|
|
|
2313
2681
|
});
|
|
2314
2682
|
|
|
2315
2683
|
// which - locate command
|
|
2316
|
-
register('which', async (args) => {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2684
|
+
register('which', async ({ args }) => {
|
|
2685
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'which');
|
|
2686
|
+
if (argError) return argError;
|
|
2687
|
+
|
|
2321
2688
|
const cmd = args[0];
|
|
2322
|
-
|
|
2323
|
-
// Check virtual commands first
|
|
2689
|
+
|
|
2324
2690
|
if (virtualCommands.has(cmd)) {
|
|
2325
|
-
return
|
|
2691
|
+
return VirtualUtils.success(`${cmd}: shell builtin\n`);
|
|
2326
2692
|
}
|
|
2327
|
-
|
|
2328
|
-
// Check PATH for system commands
|
|
2693
|
+
|
|
2329
2694
|
const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
|
|
2330
2695
|
const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
|
|
2331
|
-
|
|
2332
|
-
for (const
|
|
2696
|
+
|
|
2697
|
+
for (const pathDir of paths) {
|
|
2333
2698
|
for (const ext of extensions) {
|
|
2334
|
-
const fullPath =
|
|
2699
|
+
const fullPath = path.join(pathDir, cmd + ext);
|
|
2335
2700
|
try {
|
|
2336
|
-
if (
|
|
2337
|
-
return
|
|
2701
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
2702
|
+
return VirtualUtils.success(fullPath + '\n');
|
|
2338
2703
|
}
|
|
2339
|
-
} catch {}
|
|
2704
|
+
} catch { }
|
|
2340
2705
|
}
|
|
2341
2706
|
}
|
|
2342
|
-
|
|
2343
|
-
return
|
|
2707
|
+
|
|
2708
|
+
return VirtualUtils.error(`which: no ${cmd} in PATH`);
|
|
2344
2709
|
});
|
|
2345
2710
|
|
|
2346
2711
|
// exit - exit with code
|
|
2347
|
-
register('exit', async (args) => {
|
|
2712
|
+
register('exit', async ({ args }) => {
|
|
2348
2713
|
const code = parseInt(args[0] || 0);
|
|
2349
2714
|
if (globalShellSettings.errexit || code !== 0) {
|
|
2350
2715
|
// For virtual commands, we simulate exit by returning the code
|
|
@@ -2354,54 +2719,50 @@ function registerBuiltins() {
|
|
|
2354
2719
|
});
|
|
2355
2720
|
|
|
2356
2721
|
// env - print environment variables
|
|
2357
|
-
register('env', async (args, stdin,
|
|
2722
|
+
register('env', async ({ args, stdin, env }) => {
|
|
2358
2723
|
if (args.length === 0) {
|
|
2359
2724
|
// Use custom env if provided, otherwise use process.env
|
|
2360
|
-
const
|
|
2361
|
-
const output = Object.entries(
|
|
2725
|
+
const envVars = env || process.env;
|
|
2726
|
+
const output = Object.entries(envVars)
|
|
2362
2727
|
.map(([key, value]) => `${key}=${value}`)
|
|
2363
2728
|
.join('\n') + '\n';
|
|
2364
2729
|
return { stdout: output, code: 0 };
|
|
2365
2730
|
}
|
|
2366
|
-
|
|
2731
|
+
|
|
2367
2732
|
// TODO: Support env VAR=value command syntax
|
|
2368
2733
|
return { stderr: 'env: command execution not yet supported', code: 1 };
|
|
2369
2734
|
});
|
|
2370
2735
|
|
|
2371
2736
|
// cat - read and display file contents
|
|
2372
|
-
register('cat', async (args, stdin,
|
|
2737
|
+
register('cat', async ({ args, stdin, cwd }) => {
|
|
2373
2738
|
if (args.length === 0) {
|
|
2374
2739
|
// Read from stdin if no files specified
|
|
2375
2740
|
return { stdout: stdin || '', code: 0 };
|
|
2376
2741
|
}
|
|
2377
|
-
|
|
2742
|
+
|
|
2378
2743
|
try {
|
|
2379
|
-
const fs = await import('fs');
|
|
2380
|
-
const path = await import('path');
|
|
2381
2744
|
let output = '';
|
|
2382
|
-
|
|
2745
|
+
|
|
2383
2746
|
for (const filename of args) {
|
|
2384
|
-
// Handle special flags
|
|
2385
2747
|
if (filename === '-n') continue; // Line numbering (basic support)
|
|
2386
|
-
|
|
2748
|
+
|
|
2387
2749
|
try {
|
|
2388
2750
|
// Resolve path relative to cwd if provided
|
|
2389
|
-
const basePath =
|
|
2751
|
+
const basePath = cwd || process.cwd();
|
|
2390
2752
|
const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
|
|
2391
|
-
|
|
2753
|
+
|
|
2392
2754
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
2393
2755
|
output += content;
|
|
2394
2756
|
} catch (error) {
|
|
2395
|
-
// Format error message to match bash/sh style
|
|
2396
2757
|
const errorMsg = error.code === 'ENOENT' ? 'No such file or directory' : error.message;
|
|
2397
|
-
return {
|
|
2398
|
-
stderr: `cat: ${filename}: ${errorMsg}`,
|
|
2758
|
+
return {
|
|
2759
|
+
stderr: `cat: ${filename}: ${errorMsg}`,
|
|
2399
2760
|
stdout: output,
|
|
2400
|
-
code: 1
|
|
2761
|
+
code: 1
|
|
2401
2762
|
};
|
|
2402
2763
|
}
|
|
2403
2764
|
}
|
|
2404
|
-
|
|
2765
|
+
|
|
2405
2766
|
return { stdout: output, code: 0 };
|
|
2406
2767
|
} catch (error) {
|
|
2407
2768
|
return { stderr: `cat: ${error.message}`, code: 1 };
|
|
@@ -2409,37 +2770,34 @@ function registerBuiltins() {
|
|
|
2409
2770
|
});
|
|
2410
2771
|
|
|
2411
2772
|
// ls - list directory contents
|
|
2412
|
-
register('ls', async (args, stdin,
|
|
2773
|
+
register('ls', async ({ args, stdin, cwd }) => {
|
|
2413
2774
|
try {
|
|
2414
|
-
|
|
2415
|
-
const path = await import('path');
|
|
2416
|
-
|
|
2417
|
-
// Parse flags and paths
|
|
2775
|
+
|
|
2418
2776
|
const flags = args.filter(arg => arg.startsWith('-'));
|
|
2419
2777
|
const paths = args.filter(arg => !arg.startsWith('-'));
|
|
2420
2778
|
const isLongFormat = flags.includes('-l');
|
|
2421
2779
|
const showAll = flags.includes('-a');
|
|
2422
2780
|
const showAlmostAll = flags.includes('-A');
|
|
2423
|
-
|
|
2781
|
+
|
|
2424
2782
|
// Default to current directory if no paths specified
|
|
2425
2783
|
const targetPaths = paths.length > 0 ? paths : ['.'];
|
|
2426
|
-
|
|
2784
|
+
|
|
2427
2785
|
let output = '';
|
|
2428
|
-
|
|
2786
|
+
|
|
2429
2787
|
for (const targetPath of targetPaths) {
|
|
2430
2788
|
// Resolve path relative to cwd if provided
|
|
2431
|
-
const basePath =
|
|
2789
|
+
const basePath = cwd || process.cwd();
|
|
2432
2790
|
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
|
|
2433
|
-
|
|
2791
|
+
|
|
2434
2792
|
try {
|
|
2435
2793
|
const stat = fs.statSync(fullPath);
|
|
2436
|
-
|
|
2794
|
+
|
|
2437
2795
|
if (stat.isFile()) {
|
|
2438
2796
|
// Just show the file name if it's a file
|
|
2439
2797
|
output += path.basename(targetPath) + '\n';
|
|
2440
2798
|
} else if (stat.isDirectory()) {
|
|
2441
2799
|
const entries = fs.readdirSync(fullPath);
|
|
2442
|
-
|
|
2800
|
+
|
|
2443
2801
|
// Filter hidden files unless -a or -A is specified
|
|
2444
2802
|
let filteredEntries = entries;
|
|
2445
2803
|
if (!showAll && !showAlmostAll) {
|
|
@@ -2447,9 +2805,8 @@ function registerBuiltins() {
|
|
|
2447
2805
|
} else if (showAlmostAll) {
|
|
2448
2806
|
filteredEntries = entries.filter(entry => entry !== '.' && entry !== '..');
|
|
2449
2807
|
}
|
|
2450
|
-
|
|
2808
|
+
|
|
2451
2809
|
if (isLongFormat) {
|
|
2452
|
-
// Long format: permissions, links, owner, group, size, date, name
|
|
2453
2810
|
for (const entry of filteredEntries) {
|
|
2454
2811
|
const entryPath = path.join(fullPath, entry);
|
|
2455
2812
|
try {
|
|
@@ -2464,56 +2821,51 @@ function registerBuiltins() {
|
|
|
2464
2821
|
}
|
|
2465
2822
|
}
|
|
2466
2823
|
} else {
|
|
2467
|
-
// Simple format: just names
|
|
2468
2824
|
output += filteredEntries.join('\n') + (filteredEntries.length > 0 ? '\n' : '');
|
|
2469
2825
|
}
|
|
2470
2826
|
}
|
|
2471
2827
|
} catch (error) {
|
|
2472
|
-
return {
|
|
2473
|
-
stderr: `ls: cannot access '${targetPath}': ${error.message}`,
|
|
2474
|
-
code: 2
|
|
2828
|
+
return {
|
|
2829
|
+
stderr: `ls: cannot access '${targetPath}': ${error.message}`,
|
|
2830
|
+
code: 2
|
|
2475
2831
|
};
|
|
2476
2832
|
}
|
|
2477
2833
|
}
|
|
2478
|
-
|
|
2834
|
+
|
|
2479
2835
|
return { stdout: output, code: 0 };
|
|
2480
2836
|
} catch (error) {
|
|
2481
2837
|
return { stderr: `ls: ${error.message}`, code: 1 };
|
|
2482
2838
|
}
|
|
2483
2839
|
});
|
|
2484
2840
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
if (
|
|
2488
|
-
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2841
|
+
register('mkdir', async ({ args, stdin, cwd }) => {
|
|
2842
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'mkdir');
|
|
2843
|
+
if (argError) return argError;
|
|
2844
|
+
|
|
2491
2845
|
try {
|
|
2492
|
-
|
|
2493
|
-
const path = await import('path');
|
|
2494
|
-
|
|
2846
|
+
|
|
2495
2847
|
const flags = args.filter(arg => arg.startsWith('-'));
|
|
2496
2848
|
const dirs = args.filter(arg => !arg.startsWith('-'));
|
|
2497
2849
|
const recursive = flags.includes('-p');
|
|
2498
|
-
|
|
2850
|
+
|
|
2499
2851
|
for (const dir of dirs) {
|
|
2500
2852
|
try {
|
|
2501
|
-
const basePath =
|
|
2853
|
+
const basePath = cwd || process.cwd();
|
|
2502
2854
|
const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
|
|
2503
|
-
|
|
2855
|
+
|
|
2504
2856
|
if (recursive) {
|
|
2505
2857
|
fs.mkdirSync(fullPath, { recursive: true });
|
|
2506
2858
|
} else {
|
|
2507
2859
|
fs.mkdirSync(fullPath);
|
|
2508
2860
|
}
|
|
2509
2861
|
} catch (error) {
|
|
2510
|
-
return {
|
|
2511
|
-
stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
|
|
2512
|
-
code: 1
|
|
2862
|
+
return {
|
|
2863
|
+
stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
|
|
2864
|
+
code: 1
|
|
2513
2865
|
};
|
|
2514
2866
|
}
|
|
2515
2867
|
}
|
|
2516
|
-
|
|
2868
|
+
|
|
2517
2869
|
return { stdout: '', code: 0 };
|
|
2518
2870
|
} catch (error) {
|
|
2519
2871
|
return { stderr: `mkdir: ${error.message}`, code: 1 };
|
|
@@ -2521,32 +2873,29 @@ function registerBuiltins() {
|
|
|
2521
2873
|
});
|
|
2522
2874
|
|
|
2523
2875
|
// rm - remove files and directories
|
|
2524
|
-
register('rm', async (args, stdin,
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2876
|
+
register('rm', async ({ args, stdin, cwd }) => {
|
|
2877
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'rm');
|
|
2878
|
+
if (argError) return argError;
|
|
2879
|
+
|
|
2529
2880
|
try {
|
|
2530
|
-
|
|
2531
|
-
const path = await import('path');
|
|
2532
|
-
|
|
2881
|
+
|
|
2533
2882
|
const flags = args.filter(arg => arg.startsWith('-'));
|
|
2534
2883
|
const targets = args.filter(arg => !arg.startsWith('-'));
|
|
2535
2884
|
const recursive = flags.includes('-r') || flags.includes('-R');
|
|
2536
2885
|
const force = flags.includes('-f');
|
|
2537
|
-
|
|
2886
|
+
|
|
2538
2887
|
for (const target of targets) {
|
|
2539
2888
|
try {
|
|
2540
|
-
const basePath =
|
|
2889
|
+
const basePath = cwd || process.cwd();
|
|
2541
2890
|
const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
|
|
2542
|
-
|
|
2891
|
+
|
|
2543
2892
|
const stat = fs.statSync(fullPath);
|
|
2544
|
-
|
|
2893
|
+
|
|
2545
2894
|
if (stat.isDirectory()) {
|
|
2546
2895
|
if (!recursive) {
|
|
2547
|
-
return {
|
|
2548
|
-
stderr: `rm: cannot remove '${target}': Is a directory`,
|
|
2549
|
-
code: 1
|
|
2896
|
+
return {
|
|
2897
|
+
stderr: `rm: cannot remove '${target}': Is a directory`,
|
|
2898
|
+
code: 1
|
|
2550
2899
|
};
|
|
2551
2900
|
}
|
|
2552
2901
|
fs.rmSync(fullPath, { recursive: true, force });
|
|
@@ -2555,14 +2904,14 @@ function registerBuiltins() {
|
|
|
2555
2904
|
}
|
|
2556
2905
|
} catch (error) {
|
|
2557
2906
|
if (!force) {
|
|
2558
|
-
return {
|
|
2559
|
-
stderr: `rm: cannot remove '${target}': ${error.message}`,
|
|
2560
|
-
code: 1
|
|
2907
|
+
return {
|
|
2908
|
+
stderr: `rm: cannot remove '${target}': ${error.message}`,
|
|
2909
|
+
code: 1
|
|
2561
2910
|
};
|
|
2562
2911
|
}
|
|
2563
2912
|
}
|
|
2564
2913
|
}
|
|
2565
|
-
|
|
2914
|
+
|
|
2566
2915
|
return { stdout: '', code: 0 };
|
|
2567
2916
|
} catch (error) {
|
|
2568
2917
|
return { stderr: `rm: ${error.message}`, code: 1 };
|
|
@@ -2570,25 +2919,21 @@ function registerBuiltins() {
|
|
|
2570
2919
|
});
|
|
2571
2920
|
|
|
2572
2921
|
// mv - move/rename files and directories
|
|
2573
|
-
register('mv', async (args, stdin,
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2922
|
+
register('mv', async ({ args, stdin, cwd }) => {
|
|
2923
|
+
const argError = VirtualUtils.validateArgs(args, 2, 'mv');
|
|
2924
|
+
if (argError) return VirtualUtils.invalidArgumentError('mv', 'missing destination file operand');
|
|
2925
|
+
|
|
2578
2926
|
try {
|
|
2579
|
-
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
const basePath = options?.cwd || process.cwd();
|
|
2583
|
-
|
|
2927
|
+
|
|
2928
|
+
const basePath = cwd || process.cwd();
|
|
2929
|
+
|
|
2584
2930
|
if (args.length === 2) {
|
|
2585
2931
|
// Simple rename/move
|
|
2586
2932
|
const [source, dest] = args;
|
|
2587
2933
|
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
2588
2934
|
let destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
2589
|
-
|
|
2935
|
+
|
|
2590
2936
|
try {
|
|
2591
|
-
// Check if destination is an existing directory
|
|
2592
2937
|
try {
|
|
2593
2938
|
const destStat = fs.statSync(destPath);
|
|
2594
2939
|
if (destStat.isDirectory()) {
|
|
@@ -2599,12 +2944,12 @@ function registerBuiltins() {
|
|
|
2599
2944
|
} catch {
|
|
2600
2945
|
// Destination doesn't exist, proceed with direct rename
|
|
2601
2946
|
}
|
|
2602
|
-
|
|
2947
|
+
|
|
2603
2948
|
fs.renameSync(sourcePath, destPath);
|
|
2604
2949
|
} catch (error) {
|
|
2605
|
-
return {
|
|
2606
|
-
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
2607
|
-
code: 1
|
|
2950
|
+
return {
|
|
2951
|
+
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
2952
|
+
code: 1
|
|
2608
2953
|
};
|
|
2609
2954
|
}
|
|
2610
2955
|
} else {
|
|
@@ -2612,23 +2957,22 @@ function registerBuiltins() {
|
|
|
2612
2957
|
const sources = args.slice(0, -1);
|
|
2613
2958
|
const dest = args[args.length - 1];
|
|
2614
2959
|
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
2615
|
-
|
|
2616
|
-
// Check if destination is a directory
|
|
2960
|
+
|
|
2617
2961
|
try {
|
|
2618
2962
|
const destStat = fs.statSync(destPath);
|
|
2619
2963
|
if (!destStat.isDirectory()) {
|
|
2620
|
-
return {
|
|
2621
|
-
stderr: `mv: target '${dest}' is not a directory`,
|
|
2622
|
-
code: 1
|
|
2964
|
+
return {
|
|
2965
|
+
stderr: `mv: target '${dest}' is not a directory`,
|
|
2966
|
+
code: 1
|
|
2623
2967
|
};
|
|
2624
2968
|
}
|
|
2625
2969
|
} catch {
|
|
2626
|
-
return {
|
|
2627
|
-
stderr: `mv: cannot access '${dest}': No such file or directory`,
|
|
2628
|
-
code: 1
|
|
2970
|
+
return {
|
|
2971
|
+
stderr: `mv: cannot access '${dest}': No such file or directory`,
|
|
2972
|
+
code: 1
|
|
2629
2973
|
};
|
|
2630
2974
|
}
|
|
2631
|
-
|
|
2975
|
+
|
|
2632
2976
|
for (const source of sources) {
|
|
2633
2977
|
try {
|
|
2634
2978
|
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
@@ -2636,14 +2980,14 @@ function registerBuiltins() {
|
|
|
2636
2980
|
const newDestPath = path.join(destPath, fileName);
|
|
2637
2981
|
fs.renameSync(sourcePath, newDestPath);
|
|
2638
2982
|
} catch (error) {
|
|
2639
|
-
return {
|
|
2640
|
-
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
2641
|
-
code: 1
|
|
2983
|
+
return {
|
|
2984
|
+
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
2985
|
+
code: 1
|
|
2642
2986
|
};
|
|
2643
2987
|
}
|
|
2644
2988
|
}
|
|
2645
2989
|
}
|
|
2646
|
-
|
|
2990
|
+
|
|
2647
2991
|
return { stdout: '', code: 0 };
|
|
2648
2992
|
} catch (error) {
|
|
2649
2993
|
return { stderr: `mv: ${error.message}`, code: 1 };
|
|
@@ -2651,35 +2995,32 @@ function registerBuiltins() {
|
|
|
2651
2995
|
});
|
|
2652
2996
|
|
|
2653
2997
|
// cp - copy files and directories
|
|
2654
|
-
register('cp', async (args, stdin,
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2998
|
+
register('cp', async ({ args, stdin, cwd }) => {
|
|
2999
|
+
const argError = VirtualUtils.validateArgs(args, 2, 'cp');
|
|
3000
|
+
if (argError) return VirtualUtils.invalidArgumentError('cp', 'missing destination file operand');
|
|
3001
|
+
|
|
2659
3002
|
try {
|
|
2660
|
-
|
|
2661
|
-
const path = await import('path');
|
|
2662
|
-
|
|
3003
|
+
|
|
2663
3004
|
const flags = args.filter(arg => arg.startsWith('-'));
|
|
2664
3005
|
const paths = args.filter(arg => !arg.startsWith('-'));
|
|
2665
3006
|
const recursive = flags.includes('-r') || flags.includes('-R');
|
|
2666
|
-
|
|
2667
|
-
const basePath =
|
|
2668
|
-
|
|
3007
|
+
|
|
3008
|
+
const basePath = cwd || process.cwd();
|
|
3009
|
+
|
|
2669
3010
|
if (paths.length === 2) {
|
|
2670
3011
|
// Simple copy
|
|
2671
3012
|
const [source, dest] = paths;
|
|
2672
3013
|
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
2673
3014
|
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
2674
|
-
|
|
3015
|
+
|
|
2675
3016
|
try {
|
|
2676
3017
|
const sourceStat = fs.statSync(sourcePath);
|
|
2677
|
-
|
|
3018
|
+
|
|
2678
3019
|
if (sourceStat.isDirectory()) {
|
|
2679
3020
|
if (!recursive) {
|
|
2680
|
-
return {
|
|
2681
|
-
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
2682
|
-
code: 1
|
|
3021
|
+
return {
|
|
3022
|
+
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
3023
|
+
code: 1
|
|
2683
3024
|
};
|
|
2684
3025
|
}
|
|
2685
3026
|
fs.cpSync(sourcePath, destPath, { recursive: true });
|
|
@@ -2687,9 +3028,9 @@ function registerBuiltins() {
|
|
|
2687
3028
|
fs.copyFileSync(sourcePath, destPath);
|
|
2688
3029
|
}
|
|
2689
3030
|
} catch (error) {
|
|
2690
|
-
return {
|
|
2691
|
-
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
2692
|
-
code: 1
|
|
3031
|
+
return {
|
|
3032
|
+
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
3033
|
+
code: 1
|
|
2693
3034
|
};
|
|
2694
3035
|
}
|
|
2695
3036
|
} else {
|
|
@@ -2697,35 +3038,34 @@ function registerBuiltins() {
|
|
|
2697
3038
|
const sources = paths.slice(0, -1);
|
|
2698
3039
|
const dest = paths[paths.length - 1];
|
|
2699
3040
|
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
2700
|
-
|
|
2701
|
-
// Check if destination is a directory
|
|
3041
|
+
|
|
2702
3042
|
try {
|
|
2703
3043
|
const destStat = fs.statSync(destPath);
|
|
2704
3044
|
if (!destStat.isDirectory()) {
|
|
2705
|
-
return {
|
|
2706
|
-
stderr: `cp: target '${dest}' is not a directory`,
|
|
2707
|
-
code: 1
|
|
3045
|
+
return {
|
|
3046
|
+
stderr: `cp: target '${dest}' is not a directory`,
|
|
3047
|
+
code: 1
|
|
2708
3048
|
};
|
|
2709
3049
|
}
|
|
2710
3050
|
} catch {
|
|
2711
|
-
return {
|
|
2712
|
-
stderr: `cp: cannot access '${dest}': No such file or directory`,
|
|
2713
|
-
code: 1
|
|
3051
|
+
return {
|
|
3052
|
+
stderr: `cp: cannot access '${dest}': No such file or directory`,
|
|
3053
|
+
code: 1
|
|
2714
3054
|
};
|
|
2715
3055
|
}
|
|
2716
|
-
|
|
3056
|
+
|
|
2717
3057
|
for (const source of sources) {
|
|
2718
3058
|
try {
|
|
2719
3059
|
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
2720
3060
|
const fileName = path.basename(source);
|
|
2721
3061
|
const newDestPath = path.join(destPath, fileName);
|
|
2722
|
-
|
|
3062
|
+
|
|
2723
3063
|
const sourceStat = fs.statSync(sourcePath);
|
|
2724
3064
|
if (sourceStat.isDirectory()) {
|
|
2725
3065
|
if (!recursive) {
|
|
2726
|
-
return {
|
|
2727
|
-
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
2728
|
-
code: 1
|
|
3066
|
+
return {
|
|
3067
|
+
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
3068
|
+
code: 1
|
|
2729
3069
|
};
|
|
2730
3070
|
}
|
|
2731
3071
|
fs.cpSync(sourcePath, newDestPath, { recursive: true });
|
|
@@ -2733,14 +3073,14 @@ function registerBuiltins() {
|
|
|
2733
3073
|
fs.copyFileSync(sourcePath, newDestPath);
|
|
2734
3074
|
}
|
|
2735
3075
|
} catch (error) {
|
|
2736
|
-
return {
|
|
2737
|
-
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
2738
|
-
code: 1
|
|
3076
|
+
return {
|
|
3077
|
+
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
3078
|
+
code: 1
|
|
2739
3079
|
};
|
|
2740
3080
|
}
|
|
2741
3081
|
}
|
|
2742
3082
|
}
|
|
2743
|
-
|
|
3083
|
+
|
|
2744
3084
|
return { stdout: '', code: 0 };
|
|
2745
3085
|
} catch (error) {
|
|
2746
3086
|
return { stderr: `cp: ${error.message}`, code: 1 };
|
|
@@ -2748,37 +3088,33 @@ function registerBuiltins() {
|
|
|
2748
3088
|
});
|
|
2749
3089
|
|
|
2750
3090
|
// touch - create or update file timestamps
|
|
2751
|
-
register('touch', async (args, stdin,
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
3091
|
+
register('touch', async ({ args, stdin, cwd }) => {
|
|
3092
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'touch');
|
|
3093
|
+
if (argError) return VirtualUtils.missingOperandError('touch', 'touch: missing file operand');
|
|
3094
|
+
|
|
2756
3095
|
try {
|
|
2757
|
-
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
const basePath = options?.cwd || process.cwd();
|
|
2761
|
-
|
|
3096
|
+
|
|
3097
|
+
const basePath = cwd || process.cwd();
|
|
3098
|
+
|
|
2762
3099
|
for (const file of args) {
|
|
2763
3100
|
try {
|
|
2764
3101
|
const fullPath = path.isAbsolute(file) ? file : path.join(basePath, file);
|
|
2765
|
-
|
|
3102
|
+
|
|
2766
3103
|
// Try to update timestamps if file exists
|
|
2767
3104
|
try {
|
|
2768
3105
|
const now = new Date();
|
|
2769
3106
|
fs.utimesSync(fullPath, now, now);
|
|
2770
3107
|
} catch {
|
|
2771
|
-
// File doesn't exist, create it
|
|
2772
3108
|
fs.writeFileSync(fullPath, '', { flag: 'w' });
|
|
2773
3109
|
}
|
|
2774
3110
|
} catch (error) {
|
|
2775
|
-
return {
|
|
2776
|
-
stderr: `touch: cannot touch '${file}': ${error.message}`,
|
|
2777
|
-
code: 1
|
|
3111
|
+
return {
|
|
3112
|
+
stderr: `touch: cannot touch '${file}': ${error.message}`,
|
|
3113
|
+
code: 1
|
|
2778
3114
|
};
|
|
2779
3115
|
}
|
|
2780
3116
|
}
|
|
2781
|
-
|
|
3117
|
+
|
|
2782
3118
|
return { stdout: '', code: 0 };
|
|
2783
3119
|
} catch (error) {
|
|
2784
3120
|
return { stderr: `touch: ${error.message}`, code: 1 };
|
|
@@ -2786,24 +3122,22 @@ function registerBuiltins() {
|
|
|
2786
3122
|
});
|
|
2787
3123
|
|
|
2788
3124
|
// basename - extract filename from path
|
|
2789
|
-
register('basename', async (args) => {
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3125
|
+
register('basename', async ({ args }) => {
|
|
3126
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'basename');
|
|
3127
|
+
if (argError) return argError;
|
|
3128
|
+
|
|
2794
3129
|
try {
|
|
2795
|
-
|
|
2796
|
-
|
|
3130
|
+
|
|
2797
3131
|
const pathname = args[0];
|
|
2798
3132
|
const suffix = args[1];
|
|
2799
|
-
|
|
3133
|
+
|
|
2800
3134
|
let result = path.basename(pathname);
|
|
2801
|
-
|
|
3135
|
+
|
|
2802
3136
|
// Remove suffix if provided
|
|
2803
3137
|
if (suffix && result.endsWith(suffix)) {
|
|
2804
3138
|
result = result.slice(0, -suffix.length);
|
|
2805
3139
|
}
|
|
2806
|
-
|
|
3140
|
+
|
|
2807
3141
|
return { stdout: result + '\n', code: 0 };
|
|
2808
3142
|
} catch (error) {
|
|
2809
3143
|
return { stderr: `basename: ${error.message}`, code: 1 };
|
|
@@ -2811,17 +3145,15 @@ function registerBuiltins() {
|
|
|
2811
3145
|
});
|
|
2812
3146
|
|
|
2813
3147
|
// dirname - extract directory from path
|
|
2814
|
-
register('dirname', async (args) => {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3148
|
+
register('dirname', async ({ args }) => {
|
|
3149
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'dirname');
|
|
3150
|
+
if (argError) return argError;
|
|
3151
|
+
|
|
2819
3152
|
try {
|
|
2820
|
-
|
|
2821
|
-
|
|
3153
|
+
|
|
2822
3154
|
const pathname = args[0];
|
|
2823
3155
|
const result = path.dirname(pathname);
|
|
2824
|
-
|
|
3156
|
+
|
|
2825
3157
|
return { stdout: result + '\n', code: 0 };
|
|
2826
3158
|
} catch (error) {
|
|
2827
3159
|
return { stderr: `dirname: ${error.message}`, code: 1 };
|
|
@@ -2829,42 +3161,38 @@ function registerBuiltins() {
|
|
|
2829
3161
|
});
|
|
2830
3162
|
|
|
2831
3163
|
// yes - output a string repeatedly
|
|
2832
|
-
register('yes', async function* (args, stdin,
|
|
3164
|
+
register('yes', async function* ({ args, stdin, isCancelled, signal, ...rest }) {
|
|
2833
3165
|
const output = args.length > 0 ? args.join(' ') : 'y';
|
|
2834
|
-
trace('VirtualCommand',
|
|
2835
|
-
|
|
3166
|
+
trace('VirtualCommand', () => `yes: starting infinite generator | ${JSON.stringify({ output }, null, 2)}`);
|
|
3167
|
+
|
|
2836
3168
|
// Generate infinite stream of the output
|
|
2837
3169
|
while (true) {
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
trace('VirtualCommand', 'yes: cancelled via abort signal', {});
|
|
2846
|
-
return;
|
|
2847
|
-
}
|
|
3170
|
+
if (isCancelled && isCancelled()) {
|
|
3171
|
+
trace('VirtualCommand', () => 'yes: cancelled via function');
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (signal && signal.aborted) {
|
|
3175
|
+
trace('VirtualCommand', () => 'yes: cancelled via abort signal');
|
|
3176
|
+
return;
|
|
2848
3177
|
}
|
|
2849
|
-
|
|
3178
|
+
|
|
2850
3179
|
yield output + '\n';
|
|
2851
|
-
|
|
2852
|
-
// Small delay with abort signal support
|
|
3180
|
+
|
|
2853
3181
|
try {
|
|
2854
3182
|
await new Promise((resolve, reject) => {
|
|
2855
3183
|
const timeout = setTimeout(resolve, 0);
|
|
2856
|
-
|
|
3184
|
+
|
|
2857
3185
|
// Listen for abort signal if available
|
|
2858
|
-
if (
|
|
3186
|
+
if (signal) {
|
|
2859
3187
|
const abortHandler = () => {
|
|
2860
3188
|
clearTimeout(timeout);
|
|
2861
3189
|
reject(new Error('Aborted'));
|
|
2862
3190
|
};
|
|
2863
|
-
|
|
2864
|
-
if (
|
|
3191
|
+
|
|
3192
|
+
if (signal.aborted) {
|
|
2865
3193
|
abortHandler();
|
|
2866
3194
|
} else {
|
|
2867
|
-
|
|
3195
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
2868
3196
|
}
|
|
2869
3197
|
}
|
|
2870
3198
|
});
|
|
@@ -2876,14 +3204,13 @@ function registerBuiltins() {
|
|
|
2876
3204
|
});
|
|
2877
3205
|
|
|
2878
3206
|
// seq - generate sequence of numbers
|
|
2879
|
-
register('seq', async (args) => {
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3207
|
+
register('seq', async ({ args }) => {
|
|
3208
|
+
const argError = VirtualUtils.validateArgs(args, 1, 'seq');
|
|
3209
|
+
if (argError) return argError;
|
|
3210
|
+
|
|
2884
3211
|
try {
|
|
2885
3212
|
let start, step, end;
|
|
2886
|
-
|
|
3213
|
+
|
|
2887
3214
|
if (args.length === 1) {
|
|
2888
3215
|
start = 1;
|
|
2889
3216
|
step = 1;
|
|
@@ -2899,11 +3226,11 @@ function registerBuiltins() {
|
|
|
2899
3226
|
} else {
|
|
2900
3227
|
return { stderr: 'seq: too many operands', code: 1 };
|
|
2901
3228
|
}
|
|
2902
|
-
|
|
3229
|
+
|
|
2903
3230
|
if (isNaN(start) || isNaN(step) || isNaN(end)) {
|
|
2904
3231
|
return { stderr: 'seq: invalid number', code: 1 };
|
|
2905
3232
|
}
|
|
2906
|
-
|
|
3233
|
+
|
|
2907
3234
|
let output = '';
|
|
2908
3235
|
if (step > 0) {
|
|
2909
3236
|
for (let i = start; i <= end; i += step) {
|
|
@@ -2916,7 +3243,7 @@ function registerBuiltins() {
|
|
|
2916
3243
|
} else {
|
|
2917
3244
|
return { stderr: 'seq: invalid increment', code: 1 };
|
|
2918
3245
|
}
|
|
2919
|
-
|
|
3246
|
+
|
|
2920
3247
|
return { stdout: output, code: 0 };
|
|
2921
3248
|
} catch (error) {
|
|
2922
3249
|
return { stderr: `seq: ${error.message}`, code: 1 };
|
|
@@ -2924,38 +3251,114 @@ function registerBuiltins() {
|
|
|
2924
3251
|
});
|
|
2925
3252
|
|
|
2926
3253
|
// test - test file conditions (basic implementation)
|
|
2927
|
-
register('test', async (args) => {
|
|
3254
|
+
register('test', async ({ args }) => {
|
|
2928
3255
|
if (args.length === 0) {
|
|
2929
3256
|
return { stdout: '', code: 1 };
|
|
2930
3257
|
}
|
|
2931
|
-
|
|
3258
|
+
|
|
2932
3259
|
// Very basic test implementation
|
|
2933
3260
|
const arg = args[0];
|
|
2934
|
-
|
|
3261
|
+
|
|
2935
3262
|
try {
|
|
2936
3263
|
if (arg === '-d' && args[1]) {
|
|
2937
3264
|
// Test if directory
|
|
2938
|
-
const stat =
|
|
3265
|
+
const stat = fs.statSync(args[1]);
|
|
2939
3266
|
return { stdout: '', code: stat.isDirectory() ? 0 : 1 };
|
|
2940
3267
|
} else if (arg === '-f' && args[1]) {
|
|
2941
3268
|
// Test if file
|
|
2942
|
-
const stat =
|
|
3269
|
+
const stat = fs.statSync(args[1]);
|
|
2943
3270
|
return { stdout: '', code: stat.isFile() ? 0 : 1 };
|
|
2944
3271
|
} else if (arg === '-e' && args[1]) {
|
|
2945
3272
|
// Test if exists
|
|
2946
|
-
|
|
3273
|
+
fs.statSync(args[1]);
|
|
2947
3274
|
return { stdout: '', code: 0 };
|
|
2948
3275
|
}
|
|
2949
3276
|
} catch {
|
|
2950
3277
|
return { stdout: '', code: 1 };
|
|
2951
3278
|
}
|
|
2952
|
-
|
|
3279
|
+
|
|
2953
3280
|
return { stdout: '', code: 1 };
|
|
2954
3281
|
});
|
|
2955
3282
|
}
|
|
2956
3283
|
|
|
3284
|
+
// ANSI control character utilities
|
|
3285
|
+
const AnsiUtils = {
|
|
3286
|
+
stripAnsi(text) {
|
|
3287
|
+
if (typeof text !== 'string') return text;
|
|
3288
|
+
return text.replace(/\x1b\[[0-9;]*[mGKHFJ]/g, '');
|
|
3289
|
+
},
|
|
3290
|
+
|
|
3291
|
+
stripControlChars(text) {
|
|
3292
|
+
if (typeof text !== 'string') return text;
|
|
3293
|
+
return text.replace(/[\x00-\x1F\x7F]/g, '');
|
|
3294
|
+
},
|
|
3295
|
+
|
|
3296
|
+
stripAll(text) {
|
|
3297
|
+
if (typeof text !== 'string') return text;
|
|
3298
|
+
return text.replace(/[\x00-\x1F\x7F]|\x1b\[[0-9;]*[mGKHFJ]/g, '');
|
|
3299
|
+
},
|
|
3300
|
+
|
|
3301
|
+
cleanForProcessing(data) {
|
|
3302
|
+
if (Buffer.isBuffer(data)) {
|
|
3303
|
+
return Buffer.from(this.stripAll(data.toString('utf8')));
|
|
3304
|
+
}
|
|
3305
|
+
return this.stripAll(data);
|
|
3306
|
+
}
|
|
3307
|
+
};
|
|
3308
|
+
|
|
3309
|
+
let globalAnsiConfig = {
|
|
3310
|
+
preserveAnsi: true,
|
|
3311
|
+
preserveControlChars: true
|
|
3312
|
+
};
|
|
3313
|
+
|
|
3314
|
+
function configureAnsi(options = {}) {
|
|
3315
|
+
globalAnsiConfig = { ...globalAnsiConfig, ...options };
|
|
3316
|
+
return globalAnsiConfig;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function getAnsiConfig() {
|
|
3320
|
+
return { ...globalAnsiConfig };
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function processOutput(data, options = {}) {
|
|
3324
|
+
const config = { ...globalAnsiConfig, ...options };
|
|
3325
|
+
if (!config.preserveAnsi && !config.preserveControlChars) {
|
|
3326
|
+
return AnsiUtils.cleanForProcessing(data);
|
|
3327
|
+
} else if (!config.preserveAnsi) {
|
|
3328
|
+
return Buffer.isBuffer(data)
|
|
3329
|
+
? Buffer.from(AnsiUtils.stripAnsi(data.toString('utf8')))
|
|
3330
|
+
: AnsiUtils.stripAnsi(data);
|
|
3331
|
+
} else if (!config.preserveControlChars) {
|
|
3332
|
+
return Buffer.isBuffer(data)
|
|
3333
|
+
? Buffer.from(AnsiUtils.stripControlChars(data.toString('utf8')))
|
|
3334
|
+
: AnsiUtils.stripControlChars(data);
|
|
3335
|
+
}
|
|
3336
|
+
return data;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
2957
3339
|
// Initialize built-in commands
|
|
2958
3340
|
registerBuiltins();
|
|
2959
3341
|
|
|
2960
|
-
export {
|
|
3342
|
+
export {
|
|
3343
|
+
$tagged as $,
|
|
3344
|
+
sh,
|
|
3345
|
+
exec,
|
|
3346
|
+
run,
|
|
3347
|
+
quote,
|
|
3348
|
+
create,
|
|
3349
|
+
raw,
|
|
3350
|
+
ProcessRunner,
|
|
3351
|
+
shell,
|
|
3352
|
+
set,
|
|
3353
|
+
unset,
|
|
3354
|
+
register,
|
|
3355
|
+
unregister,
|
|
3356
|
+
listCommands,
|
|
3357
|
+
enableVirtualCommands,
|
|
3358
|
+
disableVirtualCommands,
|
|
3359
|
+
AnsiUtils,
|
|
3360
|
+
configureAnsi,
|
|
3361
|
+
getAnsiConfig,
|
|
3362
|
+
processOutput
|
|
3363
|
+
};
|
|
2961
3364
|
export default $tagged;
|