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