command-stream 0.5.0 → 0.7.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/README.md +64 -9
- package/package.json +1 -1
- package/src/$.mjs +578 -44
- package/src/commands/$.cd.mjs +3 -2
- package/src/commands/$.pwd.mjs +1 -1
- package/src/shell-parser.mjs +375 -0
package/src/$.mjs
CHANGED
|
@@ -7,18 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import cp from 'child_process';
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import { parseShellCommand, needsRealShell } from './shell-parser.mjs';
|
|
10
12
|
|
|
11
13
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
12
14
|
|
|
13
15
|
const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';
|
|
14
16
|
|
|
15
|
-
// Interactive commands that need TTY forwarding by default
|
|
16
|
-
const INTERACTIVE_COMMANDS = new Set([
|
|
17
|
-
'top', 'htop', 'btop', 'less', 'more', 'vi', 'vim', 'nano', 'emacs',
|
|
18
|
-
'man', 'pager', 'watch', 'tmux', 'screen', 'ssh', 'ftp', 'sftp',
|
|
19
|
-
'mysql', 'psql', 'redis-cli', 'mongo', 'sqlite3', 'irb', 'python',
|
|
20
|
-
'node', 'repl', 'gdb', 'lldb', 'bc', 'dc', 'ed'
|
|
21
|
-
]);
|
|
22
17
|
|
|
23
18
|
// Trace function for verbose logging
|
|
24
19
|
function trace(category, messageOrFunc) {
|
|
@@ -28,26 +23,75 @@ function trace(category, messageOrFunc) {
|
|
|
28
23
|
console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
26
|
+
// Shell detection cache
|
|
27
|
+
let cachedShell = null;
|
|
28
|
+
|
|
29
|
+
// Save initial working directory for restoration
|
|
30
|
+
const initialWorkingDirectory = process.cwd();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find an available shell by checking multiple options in order
|
|
34
|
+
* Returns the shell command and arguments to use
|
|
35
|
+
*/
|
|
36
|
+
function findAvailableShell() {
|
|
37
|
+
if (cachedShell) {
|
|
38
|
+
trace('ShellDetection', () => `Using cached shell: ${cachedShell.cmd}`);
|
|
39
|
+
return cachedShell;
|
|
45
40
|
}
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
|
|
42
|
+
const shellsToTry = [
|
|
43
|
+
// Try absolute paths first (most reliable)
|
|
44
|
+
{ cmd: '/bin/sh', args: ['-l', '-c'], checkPath: true },
|
|
45
|
+
{ cmd: '/usr/bin/sh', args: ['-l', '-c'], checkPath: true },
|
|
46
|
+
{ cmd: '/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
47
|
+
{ cmd: '/usr/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
48
|
+
{ cmd: '/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
49
|
+
{ cmd: '/usr/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
50
|
+
// macOS specific paths
|
|
51
|
+
{ cmd: '/usr/local/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
52
|
+
{ cmd: '/usr/local/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
53
|
+
// Linux brew paths
|
|
54
|
+
{ cmd: '/home/linuxbrew/.linuxbrew/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
55
|
+
{ cmd: '/home/linuxbrew/.linuxbrew/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
56
|
+
// Try shells in PATH as fallback (which might not work in all environments)
|
|
57
|
+
// Using separate -l and -c flags for better compatibility
|
|
58
|
+
{ cmd: 'sh', args: ['-l', '-c'], checkPath: false },
|
|
59
|
+
{ cmd: 'bash', args: ['-l', '-c'], checkPath: false },
|
|
60
|
+
{ cmd: 'zsh', args: ['-l', '-c'], checkPath: false }
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const shell of shellsToTry) {
|
|
64
|
+
try {
|
|
65
|
+
if (shell.checkPath) {
|
|
66
|
+
// Check if the absolute path exists
|
|
67
|
+
if (fs.existsSync(shell.cmd)) {
|
|
68
|
+
trace('ShellDetection', () => `Found shell at absolute path: ${shell.cmd}`);
|
|
69
|
+
cachedShell = { cmd: shell.cmd, args: shell.args };
|
|
70
|
+
return cachedShell;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Try to execute 'which' to check if command is in PATH
|
|
74
|
+
const result = cp.spawnSync('which', [shell.cmd], { encoding: 'utf-8' });
|
|
75
|
+
if (result.status === 0 && result.stdout) {
|
|
76
|
+
const shellPath = result.stdout.trim();
|
|
77
|
+
trace('ShellDetection', () => `Found shell in PATH: ${shell.cmd} => ${shellPath}`);
|
|
78
|
+
cachedShell = { cmd: shell.cmd, args: shell.args };
|
|
79
|
+
return cachedShell;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Continue to next shell option
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Final fallback - use absolute path to sh
|
|
88
|
+
trace('ShellDetection', () => 'WARNING: No shell found, using /bin/sh as fallback');
|
|
89
|
+
cachedShell = { cmd: '/bin/sh', args: ['-l', '-c'] };
|
|
90
|
+
return cachedShell;
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
|
|
94
|
+
|
|
51
95
|
// Track parent stream state for graceful shutdown
|
|
52
96
|
let parentStreamsMonitored = false;
|
|
53
97
|
const activeProcessRunners = new Set();
|
|
@@ -236,13 +280,127 @@ function forceCleanupAll() {
|
|
|
236
280
|
// Clear activeProcessRunners
|
|
237
281
|
activeProcessRunners.clear();
|
|
238
282
|
|
|
239
|
-
// Reset flags
|
|
283
|
+
// Reset signal handler flags
|
|
240
284
|
sigintHandlerInstalled = false;
|
|
241
285
|
sigintHandler = null;
|
|
242
286
|
|
|
243
287
|
trace('SignalHandler', () => `Force cleanup completed - removed ${commandStreamListeners.length} handlers`);
|
|
244
288
|
}
|
|
245
289
|
|
|
290
|
+
// Complete global state reset for testing - clears all library state
|
|
291
|
+
function resetGlobalState() {
|
|
292
|
+
// CRITICAL: Restore working directory first before anything else
|
|
293
|
+
// This MUST succeed or tests will fail with spawn errors
|
|
294
|
+
try {
|
|
295
|
+
// Try to get current directory - this might fail if we're in a deleted directory
|
|
296
|
+
let currentDir;
|
|
297
|
+
try {
|
|
298
|
+
currentDir = process.cwd();
|
|
299
|
+
} catch (e) {
|
|
300
|
+
// Can't even get cwd, we're in a deleted directory
|
|
301
|
+
currentDir = null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Always try to restore to initial directory
|
|
305
|
+
if (!currentDir || currentDir !== initialWorkingDirectory) {
|
|
306
|
+
// Check if initial directory still exists
|
|
307
|
+
if (fs.existsSync(initialWorkingDirectory)) {
|
|
308
|
+
process.chdir(initialWorkingDirectory);
|
|
309
|
+
trace('GlobalState', () => `Restored working directory from ${currentDir} to ${initialWorkingDirectory}`);
|
|
310
|
+
} else {
|
|
311
|
+
// Initial directory is gone, use fallback
|
|
312
|
+
const fallback = process.env.HOME || '/workspace/command-stream' || '/';
|
|
313
|
+
if (fs.existsSync(fallback)) {
|
|
314
|
+
process.chdir(fallback);
|
|
315
|
+
trace('GlobalState', () => `Initial directory gone, changed to fallback: ${fallback}`);
|
|
316
|
+
} else {
|
|
317
|
+
// Last resort - try root
|
|
318
|
+
process.chdir('/');
|
|
319
|
+
trace('GlobalState', () => `Emergency fallback to root directory`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
trace('GlobalState', () => `Critical error restoring working directory: ${e.message}`);
|
|
325
|
+
// This is critical - we MUST have a valid working directory
|
|
326
|
+
try {
|
|
327
|
+
// Try home directory
|
|
328
|
+
if (process.env.HOME && fs.existsSync(process.env.HOME)) {
|
|
329
|
+
process.chdir(process.env.HOME);
|
|
330
|
+
} else {
|
|
331
|
+
// Last resort - root
|
|
332
|
+
process.chdir('/');
|
|
333
|
+
}
|
|
334
|
+
} catch (e2) {
|
|
335
|
+
console.error('FATAL: Cannot set any working directory!', e2);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// First, properly clean up all active ProcessRunners
|
|
340
|
+
for (const runner of activeProcessRunners) {
|
|
341
|
+
if (runner) {
|
|
342
|
+
try {
|
|
343
|
+
// If the runner was never started, clean it up
|
|
344
|
+
if (!runner.started) {
|
|
345
|
+
trace('resetGlobalState', () => `Cleaning up unstarted ProcessRunner: ${runner.spec?.command?.slice(0, 50)}`);
|
|
346
|
+
// Call the cleanup method to properly release resources
|
|
347
|
+
if (runner._cleanup) {
|
|
348
|
+
runner._cleanup();
|
|
349
|
+
}
|
|
350
|
+
} else if (runner.kill) {
|
|
351
|
+
// For started runners, kill them
|
|
352
|
+
runner.kill();
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
// Ignore errors
|
|
356
|
+
trace('resetGlobalState', () => `Error during cleanup: ${e.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Call existing cleanup
|
|
362
|
+
forceCleanupAll();
|
|
363
|
+
|
|
364
|
+
// Clear shell cache to force re-detection with our fixed logic
|
|
365
|
+
cachedShell = null;
|
|
366
|
+
|
|
367
|
+
// Reset parent stream monitoring
|
|
368
|
+
parentStreamsMonitored = false;
|
|
369
|
+
|
|
370
|
+
// Reset shell settings to defaults
|
|
371
|
+
globalShellSettings = {
|
|
372
|
+
xtrace: false,
|
|
373
|
+
errexit: false,
|
|
374
|
+
pipefail: false,
|
|
375
|
+
verbose: false,
|
|
376
|
+
noglob: false,
|
|
377
|
+
allexport: false
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Don't clear virtual commands - they should persist across tests
|
|
381
|
+
// Just make sure they're enabled
|
|
382
|
+
virtualCommandsEnabled = true;
|
|
383
|
+
|
|
384
|
+
// Reset ANSI config to defaults
|
|
385
|
+
globalAnsiConfig = {
|
|
386
|
+
forceColor: false,
|
|
387
|
+
noColor: false
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Make sure built-in virtual commands are registered
|
|
391
|
+
if (virtualCommands.size === 0) {
|
|
392
|
+
// Re-import to re-register commands (synchronously if possible)
|
|
393
|
+
trace('GlobalState', () => 'Re-registering virtual commands');
|
|
394
|
+
import('./commands/index.mjs').then(() => {
|
|
395
|
+
trace('GlobalState', () => `Virtual commands re-registered, count: ${virtualCommands.size}`);
|
|
396
|
+
}).catch((e) => {
|
|
397
|
+
trace('GlobalState', () => `Error re-registering virtual commands: ${e.message}`);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
trace('GlobalState', () => 'Global state reset completed');
|
|
402
|
+
}
|
|
403
|
+
|
|
246
404
|
function monitorParentStreams() {
|
|
247
405
|
if (parentStreamsMonitored) {
|
|
248
406
|
trace('StreamMonitor', () => 'Parent streams already monitored, skipping');
|
|
@@ -503,6 +661,12 @@ class StreamEmitter {
|
|
|
503
661
|
}
|
|
504
662
|
|
|
505
663
|
on(event, listener) {
|
|
664
|
+
trace('StreamEmitter', () => `on() called | ${JSON.stringify({
|
|
665
|
+
event,
|
|
666
|
+
hasExistingListeners: this.listeners.has(event),
|
|
667
|
+
listenerCount: this.listeners.get(event)?.length || 0
|
|
668
|
+
})}`);
|
|
669
|
+
|
|
506
670
|
if (!this.listeners.has(event)) {
|
|
507
671
|
this.listeners.set(event, []);
|
|
508
672
|
}
|
|
@@ -514,6 +678,7 @@ class StreamEmitter {
|
|
|
514
678
|
}
|
|
515
679
|
|
|
516
680
|
once(event, listener) {
|
|
681
|
+
trace('StreamEmitter', () => `once() called for event: ${event}`);
|
|
517
682
|
const onceWrapper = (...args) => {
|
|
518
683
|
this.off(event, onceWrapper);
|
|
519
684
|
listener(...args);
|
|
@@ -539,11 +704,18 @@ class StreamEmitter {
|
|
|
539
704
|
}
|
|
540
705
|
|
|
541
706
|
off(event, listener) {
|
|
707
|
+
trace('StreamEmitter', () => `off() called | ${JSON.stringify({
|
|
708
|
+
event,
|
|
709
|
+
hasListeners: !!this.listeners.get(event),
|
|
710
|
+
listenerCount: this.listeners.get(event)?.length || 0
|
|
711
|
+
})}`);
|
|
712
|
+
|
|
542
713
|
const eventListeners = this.listeners.get(event);
|
|
543
714
|
if (eventListeners) {
|
|
544
715
|
const index = eventListeners.indexOf(listener);
|
|
545
716
|
if (index !== -1) {
|
|
546
717
|
eventListeners.splice(index, 1);
|
|
718
|
+
trace('StreamEmitter', () => `Removed listener at index ${index}`);
|
|
547
719
|
}
|
|
548
720
|
}
|
|
549
721
|
return this;
|
|
@@ -555,6 +727,36 @@ function quote(value) {
|
|
|
555
727
|
if (Array.isArray(value)) return value.map(quote).join(' ');
|
|
556
728
|
if (typeof value !== 'string') value = String(value);
|
|
557
729
|
if (value === '') return "''";
|
|
730
|
+
|
|
731
|
+
// If the value is already properly quoted and doesn't need further escaping,
|
|
732
|
+
// check if we can use it as-is or with simpler quoting
|
|
733
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
734
|
+
// If it's already single-quoted and doesn't contain unescaped single quotes in the middle,
|
|
735
|
+
// we can potentially use it as-is
|
|
736
|
+
const inner = value.slice(1, -1);
|
|
737
|
+
if (!inner.includes("'")) {
|
|
738
|
+
// The inner content has no single quotes, so the original quoting is fine
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length > 2) {
|
|
744
|
+
// If it's already double-quoted, wrap it in single quotes to preserve it
|
|
745
|
+
return `'${value}'`;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Check if the string needs quoting at all
|
|
749
|
+
// Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus
|
|
750
|
+
// This regex matches strings that DON'T need quoting
|
|
751
|
+
const safePattern = /^[a-zA-Z0-9_\-./=,+@:]+$/;
|
|
752
|
+
|
|
753
|
+
if (safePattern.test(value)) {
|
|
754
|
+
// The string is safe and doesn't need quoting
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Default behavior: wrap in single quotes and escape any internal single quotes
|
|
759
|
+
// This handles spaces, special shell characters, etc.
|
|
558
760
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
559
761
|
}
|
|
560
762
|
|
|
@@ -585,16 +787,28 @@ function buildShellCommand(strings, values) {
|
|
|
585
787
|
}
|
|
586
788
|
|
|
587
789
|
function asBuffer(chunk) {
|
|
588
|
-
if (Buffer.isBuffer(chunk))
|
|
589
|
-
|
|
790
|
+
if (Buffer.isBuffer(chunk)) {
|
|
791
|
+
trace('Utils', () => `asBuffer: Already a buffer, length: ${chunk.length}`);
|
|
792
|
+
return chunk;
|
|
793
|
+
}
|
|
794
|
+
if (typeof chunk === 'string') {
|
|
795
|
+
trace('Utils', () => `asBuffer: Converting string to buffer, length: ${chunk.length}`);
|
|
796
|
+
return Buffer.from(chunk);
|
|
797
|
+
}
|
|
798
|
+
trace('Utils', () => 'asBuffer: Converting unknown type to buffer');
|
|
590
799
|
return Buffer.from(chunk);
|
|
591
800
|
}
|
|
592
801
|
|
|
593
802
|
async function pumpReadable(readable, onChunk) {
|
|
594
|
-
if (!readable)
|
|
803
|
+
if (!readable) {
|
|
804
|
+
trace('Utils', () => 'pumpReadable: No readable stream provided');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
trace('Utils', () => 'pumpReadable: Starting to pump readable stream');
|
|
595
808
|
for await (const chunk of readable) {
|
|
596
809
|
await onChunk(asBuffer(chunk));
|
|
597
810
|
}
|
|
811
|
+
trace('Utils', () => 'pumpReadable: Finished pumping readable stream');
|
|
598
812
|
}
|
|
599
813
|
|
|
600
814
|
// Enhanced process runner with streaming capabilities
|
|
@@ -614,6 +828,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
614
828
|
stdin: 'inherit',
|
|
615
829
|
cwd: undefined,
|
|
616
830
|
env: undefined,
|
|
831
|
+
interactive: false, // Explicitly request TTY forwarding for interactive commands
|
|
832
|
+
shellOperators: true, // Enable shell operator parsing by default
|
|
617
833
|
...options
|
|
618
834
|
};
|
|
619
835
|
|
|
@@ -654,14 +870,26 @@ class ProcessRunner extends StreamEmitter {
|
|
|
654
870
|
|
|
655
871
|
// Stream property getters for child process streams (null for virtual commands)
|
|
656
872
|
get stdout() {
|
|
873
|
+
trace('ProcessRunner', () => `stdout getter accessed | ${JSON.stringify({
|
|
874
|
+
hasChild: !!this.child,
|
|
875
|
+
hasStdout: !!(this.child && this.child.stdout)
|
|
876
|
+
}, null, 2)}`);
|
|
657
877
|
return this.child ? this.child.stdout : null;
|
|
658
878
|
}
|
|
659
879
|
|
|
660
880
|
get stderr() {
|
|
881
|
+
trace('ProcessRunner', () => `stderr getter accessed | ${JSON.stringify({
|
|
882
|
+
hasChild: !!this.child,
|
|
883
|
+
hasStderr: !!(this.child && this.child.stderr)
|
|
884
|
+
}, null, 2)}`);
|
|
661
885
|
return this.child ? this.child.stderr : null;
|
|
662
886
|
}
|
|
663
887
|
|
|
664
888
|
get stdin() {
|
|
889
|
+
trace('ProcessRunner', () => `stdin getter accessed | ${JSON.stringify({
|
|
890
|
+
hasChild: !!this.child,
|
|
891
|
+
hasStdin: !!(this.child && this.child.stdin)
|
|
892
|
+
}, null, 2)}`);
|
|
665
893
|
return this.child ? this.child.stdin : null;
|
|
666
894
|
}
|
|
667
895
|
|
|
@@ -989,7 +1217,13 @@ class ProcessRunner extends StreamEmitter {
|
|
|
989
1217
|
}
|
|
990
1218
|
|
|
991
1219
|
async _forwardTTYStdin() {
|
|
1220
|
+
trace('ProcessRunner', () => `_forwardTTYStdin ENTER | ${JSON.stringify({
|
|
1221
|
+
isTTY: process.stdin.isTTY,
|
|
1222
|
+
hasChildStdin: !!this.child?.stdin
|
|
1223
|
+
}, null, 2)}`);
|
|
1224
|
+
|
|
992
1225
|
if (!process.stdin.isTTY || !this.child.stdin) {
|
|
1226
|
+
trace('ProcessRunner', () => 'TTY forwarding skipped - no TTY or no child stdin');
|
|
993
1227
|
return;
|
|
994
1228
|
}
|
|
995
1229
|
|
|
@@ -1042,6 +1276,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1042
1276
|
};
|
|
1043
1277
|
|
|
1044
1278
|
const cleanup = () => {
|
|
1279
|
+
trace('ProcessRunner', () => 'TTY stdin cleanup - restoring terminal mode');
|
|
1045
1280
|
process.stdin.removeListener('data', onData);
|
|
1046
1281
|
if (process.stdin.setRawMode) {
|
|
1047
1282
|
process.stdin.setRawMode(false);
|
|
@@ -1395,6 +1630,49 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1395
1630
|
if (this.spec.mode === 'shell') {
|
|
1396
1631
|
trace('ProcessRunner', () => `BRANCH: spec.mode => shell | ${JSON.stringify({}, null, 2)}`);
|
|
1397
1632
|
|
|
1633
|
+
// Check if shell operator parsing is enabled and command contains operators
|
|
1634
|
+
const hasShellOperators = this.spec.command.includes('&&') ||
|
|
1635
|
+
this.spec.command.includes('||') ||
|
|
1636
|
+
this.spec.command.includes('(') ||
|
|
1637
|
+
this.spec.command.includes(';') ||
|
|
1638
|
+
(this.spec.command.includes('cd ') && this.spec.command.includes('&&'));
|
|
1639
|
+
|
|
1640
|
+
// Intelligent detection: disable shell operators for streaming patterns
|
|
1641
|
+
const isStreamingPattern = this.spec.command.includes('sleep') && this.spec.command.includes(';') &&
|
|
1642
|
+
(this.spec.command.includes('echo') || this.spec.command.includes('printf'));
|
|
1643
|
+
|
|
1644
|
+
// Also check if we're in streaming mode (via .stream() method)
|
|
1645
|
+
const shouldUseShellOperators = this.options.shellOperators && hasShellOperators && !isStreamingPattern && !this._isStreaming;
|
|
1646
|
+
|
|
1647
|
+
trace('ProcessRunner', () => `Shell operator detection | ${JSON.stringify({
|
|
1648
|
+
hasShellOperators,
|
|
1649
|
+
shellOperatorsEnabled: this.options.shellOperators,
|
|
1650
|
+
isStreamingPattern,
|
|
1651
|
+
isStreaming: this._isStreaming,
|
|
1652
|
+
shouldUseShellOperators,
|
|
1653
|
+
command: this.spec.command.slice(0, 100)
|
|
1654
|
+
}, null, 2)}`);
|
|
1655
|
+
|
|
1656
|
+
// Only use enhanced parser when appropriate
|
|
1657
|
+
if (!this.options._bypassVirtual && shouldUseShellOperators && !needsRealShell(this.spec.command)) {
|
|
1658
|
+
const enhancedParsed = parseShellCommand(this.spec.command);
|
|
1659
|
+
if (enhancedParsed && enhancedParsed.type !== 'simple') {
|
|
1660
|
+
trace('ProcessRunner', () => `Using enhanced parser for shell operators | ${JSON.stringify({
|
|
1661
|
+
type: enhancedParsed.type,
|
|
1662
|
+
command: this.spec.command.slice(0, 50)
|
|
1663
|
+
}, null, 2)}`);
|
|
1664
|
+
|
|
1665
|
+
if (enhancedParsed.type === 'sequence') {
|
|
1666
|
+
return await this._runSequence(enhancedParsed);
|
|
1667
|
+
} else if (enhancedParsed.type === 'subshell') {
|
|
1668
|
+
return await this._runSubshell(enhancedParsed);
|
|
1669
|
+
} else if (enhancedParsed.type === 'pipeline') {
|
|
1670
|
+
return await this._runPipeline(enhancedParsed.commands);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Fallback to original simple parser
|
|
1398
1676
|
const parsed = this._parseCommand(this.spec.command);
|
|
1399
1677
|
trace('ProcessRunner', () => `Parsed command | ${JSON.stringify({
|
|
1400
1678
|
type: parsed?.type,
|
|
@@ -1442,7 +1720,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1442
1720
|
}
|
|
1443
1721
|
}
|
|
1444
1722
|
|
|
1445
|
-
const
|
|
1723
|
+
const shell = findAvailableShell();
|
|
1724
|
+
const argv = this.spec.mode === 'shell' ? [shell.cmd, ...shell.args, this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1446
1725
|
trace('ProcessRunner', () => `Constructed argv | ${JSON.stringify({
|
|
1447
1726
|
mode: this.spec.mode,
|
|
1448
1727
|
argv: argv,
|
|
@@ -1462,12 +1741,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1462
1741
|
}
|
|
1463
1742
|
|
|
1464
1743
|
// Detect if this is an interactive command that needs direct TTY access
|
|
1465
|
-
// Only activate for interactive commands when we have a real TTY and
|
|
1744
|
+
// Only activate for interactive commands when we have a real TTY and interactive mode is explicitly requested
|
|
1466
1745
|
const isInteractive = stdin === 'inherit' &&
|
|
1467
1746
|
process.stdin.isTTY === true &&
|
|
1468
1747
|
process.stdout.isTTY === true &&
|
|
1469
1748
|
process.stderr.isTTY === true &&
|
|
1470
|
-
|
|
1749
|
+
this.options.interactive === true;
|
|
1471
1750
|
|
|
1472
1751
|
trace('ProcessRunner', () => `Interactive command detection | ${JSON.stringify({
|
|
1473
1752
|
isInteractive,
|
|
@@ -1475,7 +1754,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1475
1754
|
stdinTTY: process.stdin.isTTY,
|
|
1476
1755
|
stdoutTTY: process.stdout.isTTY,
|
|
1477
1756
|
stderrTTY: process.stderr.isTTY,
|
|
1478
|
-
|
|
1757
|
+
interactiveOption: this.options.interactive
|
|
1479
1758
|
}, null, 2)}`);
|
|
1480
1759
|
|
|
1481
1760
|
const spawnBun = (argv) => {
|
|
@@ -1500,6 +1779,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1500
1779
|
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1501
1780
|
// This allows us to send signals to the entire process group, killing shell and all its children
|
|
1502
1781
|
trace('ProcessRunner', () => `spawnBun: Using non-interactive mode with pipes and detached=${process.platform !== 'win32'}`);
|
|
1782
|
+
trace('ProcessRunner', () => `spawnBun: About to spawn | ${JSON.stringify({
|
|
1783
|
+
argv,
|
|
1784
|
+
cwd,
|
|
1785
|
+
shellCmd: argv[0],
|
|
1786
|
+
shellArgs: argv.slice(1, -1),
|
|
1787
|
+
command: argv[argv.length - 1]?.slice(0, 50)
|
|
1788
|
+
}, null, 2)}`);
|
|
1789
|
+
|
|
1503
1790
|
const child = Bun.spawn(argv, {
|
|
1504
1791
|
cwd,
|
|
1505
1792
|
env,
|
|
@@ -1850,7 +2137,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1850
2137
|
}
|
|
1851
2138
|
|
|
1852
2139
|
async _pumpStdinTo(child, captureChunks) {
|
|
1853
|
-
|
|
2140
|
+
trace('ProcessRunner', () => `_pumpStdinTo ENTER | ${JSON.stringify({
|
|
2141
|
+
hasChildStdin: !!child?.stdin,
|
|
2142
|
+
willCapture: !!captureChunks,
|
|
2143
|
+
isBun
|
|
2144
|
+
}, null, 2)}`);
|
|
2145
|
+
|
|
2146
|
+
if (!child.stdin) {
|
|
2147
|
+
trace('ProcessRunner', () => 'No child stdin to pump to');
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
1854
2150
|
const bunWriter = isBun && child.stdin && typeof child.stdin.getWriter === 'function' ? child.stdin.getWriter() : null;
|
|
1855
2151
|
for await (const chunk of process.stdin) {
|
|
1856
2152
|
const buf = asBuffer(chunk);
|
|
@@ -1868,6 +2164,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1868
2164
|
}
|
|
1869
2165
|
|
|
1870
2166
|
async _writeToStdin(buf) {
|
|
2167
|
+
trace('ProcessRunner', () => `_writeToStdin ENTER | ${JSON.stringify({
|
|
2168
|
+
bufferLength: buf?.length || 0,
|
|
2169
|
+
hasChildStdin: !!this.child?.stdin
|
|
2170
|
+
}, null, 2)}`);
|
|
2171
|
+
|
|
1871
2172
|
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
1872
2173
|
if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
|
|
1873
2174
|
// Successfully wrote to stream
|
|
@@ -1882,8 +2183,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1882
2183
|
}
|
|
1883
2184
|
|
|
1884
2185
|
_parseCommand(command) {
|
|
2186
|
+
trace('ProcessRunner', () => `_parseCommand ENTER | ${JSON.stringify({
|
|
2187
|
+
commandLength: command?.length || 0,
|
|
2188
|
+
preview: command?.slice(0, 50)
|
|
2189
|
+
}, null, 2)}`);
|
|
2190
|
+
|
|
1885
2191
|
const trimmed = command.trim();
|
|
1886
|
-
if (!trimmed)
|
|
2192
|
+
if (!trimmed) {
|
|
2193
|
+
trace('ProcessRunner', () => 'Empty command after trimming');
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
1887
2196
|
|
|
1888
2197
|
if (trimmed.includes('|')) {
|
|
1889
2198
|
return this._parsePipeline(trimmed);
|
|
@@ -1907,6 +2216,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1907
2216
|
}
|
|
1908
2217
|
|
|
1909
2218
|
_parsePipeline(command) {
|
|
2219
|
+
trace('ProcessRunner', () => `_parsePipeline ENTER | ${JSON.stringify({
|
|
2220
|
+
commandLength: command?.length || 0,
|
|
2221
|
+
hasPipe: command?.includes('|')
|
|
2222
|
+
}, null, 2)}`);
|
|
2223
|
+
|
|
1910
2224
|
// Split by pipe, respecting quotes
|
|
1911
2225
|
const segments = [];
|
|
1912
2226
|
let current = '';
|
|
@@ -2214,8 +2528,17 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2214
2528
|
|
|
2215
2529
|
return result;
|
|
2216
2530
|
} catch (error) {
|
|
2531
|
+
// Check if this is a cancellation error
|
|
2532
|
+
let exitCode = error.code ?? 1;
|
|
2533
|
+
if (this._cancelled && this._cancellationSignal) {
|
|
2534
|
+
// Use appropriate exit code based on the signal
|
|
2535
|
+
exitCode = this._cancellationSignal === 'SIGINT' ? 130 :
|
|
2536
|
+
this._cancellationSignal === 'SIGTERM' ? 143 : 1;
|
|
2537
|
+
trace('ProcessRunner', () => `Virtual command error during cancellation, using signal-based exit code: ${exitCode}`);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2217
2540
|
const result = {
|
|
2218
|
-
code:
|
|
2541
|
+
code: exitCode,
|
|
2219
2542
|
stdout: error.stdout ?? '',
|
|
2220
2543
|
stderr: error.stderr ?? error.message,
|
|
2221
2544
|
stdin: ''
|
|
@@ -2336,8 +2659,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2336
2659
|
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
2337
2660
|
commandStr.includes(';') || commandStr.includes('`');
|
|
2338
2661
|
|
|
2662
|
+
const shell = findAvailableShell();
|
|
2339
2663
|
const spawnArgs = needsShell
|
|
2340
|
-
? [
|
|
2664
|
+
? [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr]
|
|
2341
2665
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
2342
2666
|
|
|
2343
2667
|
const proc = Bun.spawn(spawnArgs, {
|
|
@@ -2499,8 +2823,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2499
2823
|
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
2500
2824
|
commandStr.includes(';') || commandStr.includes('`');
|
|
2501
2825
|
|
|
2826
|
+
const shell = findAvailableShell();
|
|
2502
2827
|
const spawnArgs = needsShell
|
|
2503
|
-
? [
|
|
2828
|
+
? [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr]
|
|
2504
2829
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
2505
2830
|
|
|
2506
2831
|
const proc = Bun.spawn(spawnArgs, {
|
|
@@ -2737,7 +3062,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2737
3062
|
}
|
|
2738
3063
|
const commandStr = commandParts.join(' ');
|
|
2739
3064
|
|
|
2740
|
-
const
|
|
3065
|
+
const shell = findAvailableShell();
|
|
3066
|
+
const proc = Bun.spawn([shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr], {
|
|
2741
3067
|
cwd: this.options.cwd,
|
|
2742
3068
|
env: this.options.env,
|
|
2743
3069
|
stdin: currentInputStream ? 'pipe' : 'ignore',
|
|
@@ -3125,7 +3451,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3125
3451
|
};
|
|
3126
3452
|
|
|
3127
3453
|
// Execute using shell to handle complex commands
|
|
3128
|
-
const
|
|
3454
|
+
const shell = findAvailableShell();
|
|
3455
|
+
const argv = [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr];
|
|
3129
3456
|
const isLastCommand = (i === commands.length - 1);
|
|
3130
3457
|
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
3131
3458
|
|
|
@@ -3300,14 +3627,181 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3300
3627
|
}
|
|
3301
3628
|
}
|
|
3302
3629
|
|
|
3630
|
+
async _runSequence(sequence) {
|
|
3631
|
+
trace('ProcessRunner', () => `_runSequence ENTER | ${JSON.stringify({
|
|
3632
|
+
commandCount: sequence.commands.length,
|
|
3633
|
+
operators: sequence.operators
|
|
3634
|
+
}, null, 2)}`);
|
|
3635
|
+
|
|
3636
|
+
let lastResult = { code: 0, stdout: '', stderr: '' };
|
|
3637
|
+
let combinedStdout = '';
|
|
3638
|
+
let combinedStderr = '';
|
|
3639
|
+
|
|
3640
|
+
for (let i = 0; i < sequence.commands.length; i++) {
|
|
3641
|
+
const command = sequence.commands[i];
|
|
3642
|
+
const operator = i > 0 ? sequence.operators[i - 1] : null;
|
|
3643
|
+
|
|
3644
|
+
trace('ProcessRunner', () => `Executing command ${i} | ${JSON.stringify({
|
|
3645
|
+
command: command.type,
|
|
3646
|
+
operator,
|
|
3647
|
+
lastCode: lastResult.code
|
|
3648
|
+
}, null, 2)}`);
|
|
3649
|
+
|
|
3650
|
+
// Check operator conditions
|
|
3651
|
+
if (operator === '&&' && lastResult.code !== 0) {
|
|
3652
|
+
trace('ProcessRunner', () => `Skipping due to && with exit code ${lastResult.code}`);
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
if (operator === '||' && lastResult.code === 0) {
|
|
3656
|
+
trace('ProcessRunner', () => `Skipping due to || with exit code ${lastResult.code}`);
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// Execute command based on type
|
|
3661
|
+
if (command.type === 'subshell') {
|
|
3662
|
+
lastResult = await this._runSubshell(command);
|
|
3663
|
+
} else if (command.type === 'pipeline') {
|
|
3664
|
+
lastResult = await this._runPipeline(command.commands);
|
|
3665
|
+
} else if (command.type === 'sequence') {
|
|
3666
|
+
lastResult = await this._runSequence(command);
|
|
3667
|
+
} else if (command.type === 'simple') {
|
|
3668
|
+
lastResult = await this._runSimpleCommand(command);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// Accumulate output
|
|
3672
|
+
combinedStdout += lastResult.stdout;
|
|
3673
|
+
combinedStderr += lastResult.stderr;
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
return {
|
|
3677
|
+
code: lastResult.code,
|
|
3678
|
+
stdout: combinedStdout,
|
|
3679
|
+
stderr: combinedStderr,
|
|
3680
|
+
async text() {
|
|
3681
|
+
return combinedStdout;
|
|
3682
|
+
}
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
async _runSubshell(subshell) {
|
|
3687
|
+
trace('ProcessRunner', () => `_runSubshell ENTER | ${JSON.stringify({
|
|
3688
|
+
commandType: subshell.command.type
|
|
3689
|
+
}, null, 2)}`);
|
|
3690
|
+
|
|
3691
|
+
// Save current directory
|
|
3692
|
+
const savedCwd = process.cwd();
|
|
3693
|
+
|
|
3694
|
+
try {
|
|
3695
|
+
// Execute subshell command
|
|
3696
|
+
let result;
|
|
3697
|
+
if (subshell.command.type === 'sequence') {
|
|
3698
|
+
result = await this._runSequence(subshell.command);
|
|
3699
|
+
} else if (subshell.command.type === 'pipeline') {
|
|
3700
|
+
result = await this._runPipeline(subshell.command.commands);
|
|
3701
|
+
} else if (subshell.command.type === 'simple') {
|
|
3702
|
+
result = await this._runSimpleCommand(subshell.command);
|
|
3703
|
+
} else {
|
|
3704
|
+
result = { code: 0, stdout: '', stderr: '' };
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
return result;
|
|
3708
|
+
} finally {
|
|
3709
|
+
// Restore directory - check if it still exists first
|
|
3710
|
+
trace('ProcessRunner', () => `Restoring cwd from ${process.cwd()} to ${savedCwd}`);
|
|
3711
|
+
const fs = await import('fs');
|
|
3712
|
+
if (fs.existsSync(savedCwd)) {
|
|
3713
|
+
process.chdir(savedCwd);
|
|
3714
|
+
} else {
|
|
3715
|
+
// If the saved directory was deleted, try to go to a safe location
|
|
3716
|
+
const fallbackDir = process.env.HOME || process.env.USERPROFILE || '/';
|
|
3717
|
+
trace('ProcessRunner', () => `Saved directory ${savedCwd} no longer exists, falling back to ${fallbackDir}`);
|
|
3718
|
+
try {
|
|
3719
|
+
process.chdir(fallbackDir);
|
|
3720
|
+
} catch (e) {
|
|
3721
|
+
// If even fallback fails, just stay where we are
|
|
3722
|
+
trace('ProcessRunner', () => `Failed to restore directory: ${e.message}`);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
async _runSimpleCommand(command) {
|
|
3729
|
+
trace('ProcessRunner', () => `_runSimpleCommand ENTER | ${JSON.stringify({
|
|
3730
|
+
cmd: command.cmd,
|
|
3731
|
+
argsCount: command.args?.length || 0,
|
|
3732
|
+
hasRedirects: !!command.redirects
|
|
3733
|
+
}, null, 2)}`);
|
|
3734
|
+
|
|
3735
|
+
const { cmd, args, redirects } = command;
|
|
3736
|
+
|
|
3737
|
+
// Check for virtual command
|
|
3738
|
+
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
3739
|
+
trace('ProcessRunner', () => `Using virtual command: ${cmd}`);
|
|
3740
|
+
const argValues = args.map(a => a.value || a);
|
|
3741
|
+
const result = await this._runVirtual(cmd, argValues);
|
|
3742
|
+
|
|
3743
|
+
// Handle output redirection for virtual commands
|
|
3744
|
+
if (redirects && redirects.length > 0) {
|
|
3745
|
+
for (const redirect of redirects) {
|
|
3746
|
+
if (redirect.type === '>' || redirect.type === '>>') {
|
|
3747
|
+
const fs = await import('fs');
|
|
3748
|
+
if (redirect.type === '>') {
|
|
3749
|
+
fs.writeFileSync(redirect.target, result.stdout);
|
|
3750
|
+
} else {
|
|
3751
|
+
fs.appendFileSync(redirect.target, result.stdout);
|
|
3752
|
+
}
|
|
3753
|
+
// Clear stdout since it was redirected
|
|
3754
|
+
result.stdout = '';
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
return result;
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// Build command string for real execution
|
|
3763
|
+
let commandStr = cmd;
|
|
3764
|
+
for (const arg of args) {
|
|
3765
|
+
if (arg.quoted && arg.quoteChar) {
|
|
3766
|
+
commandStr += ` ${arg.quoteChar}${arg.value}${arg.quoteChar}`;
|
|
3767
|
+
} else if (arg.value !== undefined) {
|
|
3768
|
+
commandStr += ` ${arg.value}`;
|
|
3769
|
+
} else {
|
|
3770
|
+
commandStr += ` ${arg}`;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// Add redirections
|
|
3775
|
+
if (redirects) {
|
|
3776
|
+
for (const redirect of redirects) {
|
|
3777
|
+
commandStr += ` ${redirect.type} ${redirect.target}`;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
trace('ProcessRunner', () => `Executing real command: ${commandStr}`);
|
|
3782
|
+
|
|
3783
|
+
// Create a new ProcessRunner for the real command
|
|
3784
|
+
// Use current working directory since cd virtual command may have changed it
|
|
3785
|
+
const runner = new ProcessRunner(
|
|
3786
|
+
{ mode: 'shell', command: commandStr },
|
|
3787
|
+
{ ...this.options, cwd: process.cwd(), _bypassVirtual: true }
|
|
3788
|
+
);
|
|
3789
|
+
|
|
3790
|
+
return await runner;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3303
3793
|
async* stream() {
|
|
3304
3794
|
trace('ProcessRunner', () => `stream ENTER | ${JSON.stringify({
|
|
3305
3795
|
started: this.started,
|
|
3306
|
-
finished: this.finished
|
|
3796
|
+
finished: this.finished,
|
|
3797
|
+
command: this.spec?.command?.slice(0, 100)
|
|
3307
3798
|
}, null, 2)}`);
|
|
3308
3799
|
|
|
3800
|
+
// Mark that we're in streaming mode to bypass shell operator interception
|
|
3801
|
+
this._isStreaming = true;
|
|
3802
|
+
|
|
3309
3803
|
if (!this.started) {
|
|
3310
|
-
trace('ProcessRunner', () => 'Auto-starting async process from stream()');
|
|
3804
|
+
trace('ProcessRunner', () => 'Auto-starting async process from stream() with streaming mode');
|
|
3311
3805
|
this._startAsync(); // Start but don't await
|
|
3312
3806
|
}
|
|
3313
3807
|
|
|
@@ -3631,6 +4125,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3631
4125
|
|
|
3632
4126
|
// Promise interface (for await)
|
|
3633
4127
|
then(onFulfilled, onRejected) {
|
|
4128
|
+
trace('ProcessRunner', () => `then() called | ${JSON.stringify({
|
|
4129
|
+
hasPromise: !!this.promise,
|
|
4130
|
+
started: this.started,
|
|
4131
|
+
finished: this.finished
|
|
4132
|
+
}, null, 2)}`);
|
|
4133
|
+
|
|
3634
4134
|
if (!this.promise) {
|
|
3635
4135
|
this.promise = this._startAsync();
|
|
3636
4136
|
}
|
|
@@ -3638,6 +4138,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3638
4138
|
}
|
|
3639
4139
|
|
|
3640
4140
|
catch(onRejected) {
|
|
4141
|
+
trace('ProcessRunner', () => `catch() called | ${JSON.stringify({
|
|
4142
|
+
hasPromise: !!this.promise,
|
|
4143
|
+
started: this.started,
|
|
4144
|
+
finished: this.finished
|
|
4145
|
+
}, null, 2)}`);
|
|
4146
|
+
|
|
3641
4147
|
if (!this.promise) {
|
|
3642
4148
|
this.promise = this._startAsync();
|
|
3643
4149
|
}
|
|
@@ -3645,6 +4151,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3645
4151
|
}
|
|
3646
4152
|
|
|
3647
4153
|
finally(onFinally) {
|
|
4154
|
+
trace('ProcessRunner', () => `finally() called | ${JSON.stringify({
|
|
4155
|
+
hasPromise: !!this.promise,
|
|
4156
|
+
started: this.started,
|
|
4157
|
+
finished: this.finished
|
|
4158
|
+
}, null, 2)}`);
|
|
4159
|
+
|
|
3648
4160
|
if (!this.promise) {
|
|
3649
4161
|
this.promise = this._startAsync();
|
|
3650
4162
|
}
|
|
@@ -3681,7 +4193,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3681
4193
|
trace('ProcessRunner', () => `Starting sync execution | ${JSON.stringify({ mode: this._mode }, null, 2)}`);
|
|
3682
4194
|
|
|
3683
4195
|
const { cwd, env, stdin } = this.options;
|
|
3684
|
-
const
|
|
4196
|
+
const shell = findAvailableShell();
|
|
4197
|
+
const argv = this.spec.mode === 'shell' ? [shell.cmd, ...shell.args, this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
3685
4198
|
|
|
3686
4199
|
if (globalShellSettings.xtrace) {
|
|
3687
4200
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
@@ -3873,10 +4386,12 @@ function create(defaultOptions = {}) {
|
|
|
3873
4386
|
}
|
|
3874
4387
|
|
|
3875
4388
|
function raw(value) {
|
|
4389
|
+
trace('API', () => `raw() called with value: ${String(value).slice(0, 50)}`);
|
|
3876
4390
|
return { raw: String(value) };
|
|
3877
4391
|
}
|
|
3878
4392
|
|
|
3879
4393
|
function set(option) {
|
|
4394
|
+
trace('API', () => `set() called with option: ${option}`);
|
|
3880
4395
|
const mapping = {
|
|
3881
4396
|
'e': 'errexit', // set -e: exit on error
|
|
3882
4397
|
'errexit': 'errexit',
|
|
@@ -3900,6 +4415,7 @@ function set(option) {
|
|
|
3900
4415
|
}
|
|
3901
4416
|
|
|
3902
4417
|
function unset(option) {
|
|
4418
|
+
trace('API', () => `unset() called with option: ${option}`);
|
|
3903
4419
|
const mapping = {
|
|
3904
4420
|
'e': 'errexit',
|
|
3905
4421
|
'errexit': 'errexit',
|
|
@@ -3952,15 +4468,19 @@ function unregister(name) {
|
|
|
3952
4468
|
}
|
|
3953
4469
|
|
|
3954
4470
|
function listCommands() {
|
|
3955
|
-
|
|
4471
|
+
const commands = Array.from(virtualCommands.keys());
|
|
4472
|
+
trace('VirtualCommands', () => `listCommands() returning ${commands.length} commands`);
|
|
4473
|
+
return commands;
|
|
3956
4474
|
}
|
|
3957
4475
|
|
|
3958
4476
|
function enableVirtualCommands() {
|
|
4477
|
+
trace('VirtualCommands', () => 'Enabling virtual commands');
|
|
3959
4478
|
virtualCommandsEnabled = true;
|
|
3960
4479
|
return virtualCommandsEnabled;
|
|
3961
4480
|
}
|
|
3962
4481
|
|
|
3963
4482
|
function disableVirtualCommands() {
|
|
4483
|
+
trace('VirtualCommands', () => 'Disabling virtual commands');
|
|
3964
4484
|
virtualCommandsEnabled = false;
|
|
3965
4485
|
return virtualCommandsEnabled;
|
|
3966
4486
|
}
|
|
@@ -3990,6 +4510,7 @@ import testCommand from './commands/$.test.mjs';
|
|
|
3990
4510
|
|
|
3991
4511
|
// Built-in commands that match Bun.$ functionality
|
|
3992
4512
|
function registerBuiltins() {
|
|
4513
|
+
trace('VirtualCommands', () => 'registerBuiltins() called - registering all built-in commands');
|
|
3993
4514
|
// Register all imported commands
|
|
3994
4515
|
register('cd', cdCommand);
|
|
3995
4516
|
register('pwd', pwdCommand);
|
|
@@ -4024,12 +4545,14 @@ const AnsiUtils = {
|
|
|
4024
4545
|
|
|
4025
4546
|
stripControlChars(text) {
|
|
4026
4547
|
if (typeof text !== 'string') return text;
|
|
4027
|
-
|
|
4548
|
+
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
|
|
4549
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
4028
4550
|
},
|
|
4029
4551
|
|
|
4030
4552
|
stripAll(text) {
|
|
4031
4553
|
if (typeof text !== 'string') return text;
|
|
4032
|
-
|
|
4554
|
+
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
|
|
4555
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\x1b\[[0-9;]*[mGKHFJ]/g, '');
|
|
4033
4556
|
},
|
|
4034
4557
|
|
|
4035
4558
|
cleanForProcessing(data) {
|
|
@@ -4046,15 +4569,23 @@ let globalAnsiConfig = {
|
|
|
4046
4569
|
};
|
|
4047
4570
|
|
|
4048
4571
|
function configureAnsi(options = {}) {
|
|
4572
|
+
trace('AnsiUtils', () => `configureAnsi() called | ${JSON.stringify({ options }, null, 2)}`);
|
|
4049
4573
|
globalAnsiConfig = { ...globalAnsiConfig, ...options };
|
|
4574
|
+
trace('AnsiUtils', () => `New ANSI config | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`);
|
|
4050
4575
|
return globalAnsiConfig;
|
|
4051
4576
|
}
|
|
4052
4577
|
|
|
4053
4578
|
function getAnsiConfig() {
|
|
4579
|
+
trace('AnsiUtils', () => `getAnsiConfig() returning | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`);
|
|
4054
4580
|
return { ...globalAnsiConfig };
|
|
4055
4581
|
}
|
|
4056
4582
|
|
|
4057
4583
|
function processOutput(data, options = {}) {
|
|
4584
|
+
trace('AnsiUtils', () => `processOutput() called | ${JSON.stringify({
|
|
4585
|
+
dataType: typeof data,
|
|
4586
|
+
dataLength: Buffer.isBuffer(data) ? data.length : data?.length,
|
|
4587
|
+
options
|
|
4588
|
+
}, null, 2)}`);
|
|
4058
4589
|
const config = { ...globalAnsiConfig, ...options };
|
|
4059
4590
|
if (!config.preserveAnsi && !config.preserveControlChars) {
|
|
4060
4591
|
return AnsiUtils.cleanForProcessing(data);
|
|
@@ -4071,7 +4602,9 @@ function processOutput(data, options = {}) {
|
|
|
4071
4602
|
}
|
|
4072
4603
|
|
|
4073
4604
|
// Initialize built-in commands
|
|
4605
|
+
trace('Initialization', () => 'Registering built-in virtual commands');
|
|
4074
4606
|
registerBuiltins();
|
|
4607
|
+
trace('Initialization', () => `Built-in commands registered: ${listCommands().join(', ')}`);
|
|
4075
4608
|
|
|
4076
4609
|
export {
|
|
4077
4610
|
$tagged as $,
|
|
@@ -4084,6 +4617,7 @@ export {
|
|
|
4084
4617
|
ProcessRunner,
|
|
4085
4618
|
shell,
|
|
4086
4619
|
set,
|
|
4620
|
+
resetGlobalState,
|
|
4087
4621
|
unset,
|
|
4088
4622
|
register,
|
|
4089
4623
|
unregister,
|