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