command-stream 0.6.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/package.json +1 -1
- package/src/$.mjs +546 -18
- package/src/commands/$.cd.mjs +3 -2
- package/src/commands/$.pwd.mjs +1 -1
- package/src/shell-parser.mjs +375 -0
package/package.json
CHANGED
package/src/$.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
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
|
|
|
@@ -21,6 +23,73 @@ function trace(category, messageOrFunc) {
|
|
|
21
23
|
console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
|
|
22
24
|
}
|
|
23
25
|
|
|
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;
|
|
40
|
+
}
|
|
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;
|
|
91
|
+
}
|
|
92
|
+
|
|
24
93
|
|
|
25
94
|
|
|
26
95
|
// Track parent stream state for graceful shutdown
|
|
@@ -211,13 +280,127 @@ function forceCleanupAll() {
|
|
|
211
280
|
// Clear activeProcessRunners
|
|
212
281
|
activeProcessRunners.clear();
|
|
213
282
|
|
|
214
|
-
// Reset flags
|
|
283
|
+
// Reset signal handler flags
|
|
215
284
|
sigintHandlerInstalled = false;
|
|
216
285
|
sigintHandler = null;
|
|
217
286
|
|
|
218
287
|
trace('SignalHandler', () => `Force cleanup completed - removed ${commandStreamListeners.length} handlers`);
|
|
219
288
|
}
|
|
220
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
|
+
|
|
221
404
|
function monitorParentStreams() {
|
|
222
405
|
if (parentStreamsMonitored) {
|
|
223
406
|
trace('StreamMonitor', () => 'Parent streams already monitored, skipping');
|
|
@@ -478,6 +661,12 @@ class StreamEmitter {
|
|
|
478
661
|
}
|
|
479
662
|
|
|
480
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
|
+
|
|
481
670
|
if (!this.listeners.has(event)) {
|
|
482
671
|
this.listeners.set(event, []);
|
|
483
672
|
}
|
|
@@ -489,6 +678,7 @@ class StreamEmitter {
|
|
|
489
678
|
}
|
|
490
679
|
|
|
491
680
|
once(event, listener) {
|
|
681
|
+
trace('StreamEmitter', () => `once() called for event: ${event}`);
|
|
492
682
|
const onceWrapper = (...args) => {
|
|
493
683
|
this.off(event, onceWrapper);
|
|
494
684
|
listener(...args);
|
|
@@ -514,11 +704,18 @@ class StreamEmitter {
|
|
|
514
704
|
}
|
|
515
705
|
|
|
516
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
|
+
|
|
517
713
|
const eventListeners = this.listeners.get(event);
|
|
518
714
|
if (eventListeners) {
|
|
519
715
|
const index = eventListeners.indexOf(listener);
|
|
520
716
|
if (index !== -1) {
|
|
521
717
|
eventListeners.splice(index, 1);
|
|
718
|
+
trace('StreamEmitter', () => `Removed listener at index ${index}`);
|
|
522
719
|
}
|
|
523
720
|
}
|
|
524
721
|
return this;
|
|
@@ -590,16 +787,28 @@ function buildShellCommand(strings, values) {
|
|
|
590
787
|
}
|
|
591
788
|
|
|
592
789
|
function asBuffer(chunk) {
|
|
593
|
-
if (Buffer.isBuffer(chunk))
|
|
594
|
-
|
|
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');
|
|
595
799
|
return Buffer.from(chunk);
|
|
596
800
|
}
|
|
597
801
|
|
|
598
802
|
async function pumpReadable(readable, onChunk) {
|
|
599
|
-
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');
|
|
600
808
|
for await (const chunk of readable) {
|
|
601
809
|
await onChunk(asBuffer(chunk));
|
|
602
810
|
}
|
|
811
|
+
trace('Utils', () => 'pumpReadable: Finished pumping readable stream');
|
|
603
812
|
}
|
|
604
813
|
|
|
605
814
|
// Enhanced process runner with streaming capabilities
|
|
@@ -620,6 +829,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
620
829
|
cwd: undefined,
|
|
621
830
|
env: undefined,
|
|
622
831
|
interactive: false, // Explicitly request TTY forwarding for interactive commands
|
|
832
|
+
shellOperators: true, // Enable shell operator parsing by default
|
|
623
833
|
...options
|
|
624
834
|
};
|
|
625
835
|
|
|
@@ -660,14 +870,26 @@ class ProcessRunner extends StreamEmitter {
|
|
|
660
870
|
|
|
661
871
|
// Stream property getters for child process streams (null for virtual commands)
|
|
662
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)}`);
|
|
663
877
|
return this.child ? this.child.stdout : null;
|
|
664
878
|
}
|
|
665
879
|
|
|
666
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)}`);
|
|
667
885
|
return this.child ? this.child.stderr : null;
|
|
668
886
|
}
|
|
669
887
|
|
|
670
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)}`);
|
|
671
893
|
return this.child ? this.child.stdin : null;
|
|
672
894
|
}
|
|
673
895
|
|
|
@@ -995,7 +1217,13 @@ class ProcessRunner extends StreamEmitter {
|
|
|
995
1217
|
}
|
|
996
1218
|
|
|
997
1219
|
async _forwardTTYStdin() {
|
|
1220
|
+
trace('ProcessRunner', () => `_forwardTTYStdin ENTER | ${JSON.stringify({
|
|
1221
|
+
isTTY: process.stdin.isTTY,
|
|
1222
|
+
hasChildStdin: !!this.child?.stdin
|
|
1223
|
+
}, null, 2)}`);
|
|
1224
|
+
|
|
998
1225
|
if (!process.stdin.isTTY || !this.child.stdin) {
|
|
1226
|
+
trace('ProcessRunner', () => 'TTY forwarding skipped - no TTY or no child stdin');
|
|
999
1227
|
return;
|
|
1000
1228
|
}
|
|
1001
1229
|
|
|
@@ -1048,6 +1276,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1048
1276
|
};
|
|
1049
1277
|
|
|
1050
1278
|
const cleanup = () => {
|
|
1279
|
+
trace('ProcessRunner', () => 'TTY stdin cleanup - restoring terminal mode');
|
|
1051
1280
|
process.stdin.removeListener('data', onData);
|
|
1052
1281
|
if (process.stdin.setRawMode) {
|
|
1053
1282
|
process.stdin.setRawMode(false);
|
|
@@ -1401,6 +1630,49 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1401
1630
|
if (this.spec.mode === 'shell') {
|
|
1402
1631
|
trace('ProcessRunner', () => `BRANCH: spec.mode => shell | ${JSON.stringify({}, null, 2)}`);
|
|
1403
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
|
|
1404
1676
|
const parsed = this._parseCommand(this.spec.command);
|
|
1405
1677
|
trace('ProcessRunner', () => `Parsed command | ${JSON.stringify({
|
|
1406
1678
|
type: parsed?.type,
|
|
@@ -1448,7 +1720,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1448
1720
|
}
|
|
1449
1721
|
}
|
|
1450
1722
|
|
|
1451
|
-
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];
|
|
1452
1725
|
trace('ProcessRunner', () => `Constructed argv | ${JSON.stringify({
|
|
1453
1726
|
mode: this.spec.mode,
|
|
1454
1727
|
argv: argv,
|
|
@@ -1506,6 +1779,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1506
1779
|
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1507
1780
|
// This allows us to send signals to the entire process group, killing shell and all its children
|
|
1508
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
|
+
|
|
1509
1790
|
const child = Bun.spawn(argv, {
|
|
1510
1791
|
cwd,
|
|
1511
1792
|
env,
|
|
@@ -1856,7 +2137,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1856
2137
|
}
|
|
1857
2138
|
|
|
1858
2139
|
async _pumpStdinTo(child, captureChunks) {
|
|
1859
|
-
|
|
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
|
+
}
|
|
1860
2150
|
const bunWriter = isBun && child.stdin && typeof child.stdin.getWriter === 'function' ? child.stdin.getWriter() : null;
|
|
1861
2151
|
for await (const chunk of process.stdin) {
|
|
1862
2152
|
const buf = asBuffer(chunk);
|
|
@@ -1874,6 +2164,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1874
2164
|
}
|
|
1875
2165
|
|
|
1876
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
|
+
|
|
1877
2172
|
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
1878
2173
|
if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
|
|
1879
2174
|
// Successfully wrote to stream
|
|
@@ -1888,8 +2183,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1888
2183
|
}
|
|
1889
2184
|
|
|
1890
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
|
+
|
|
1891
2191
|
const trimmed = command.trim();
|
|
1892
|
-
if (!trimmed)
|
|
2192
|
+
if (!trimmed) {
|
|
2193
|
+
trace('ProcessRunner', () => 'Empty command after trimming');
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
1893
2196
|
|
|
1894
2197
|
if (trimmed.includes('|')) {
|
|
1895
2198
|
return this._parsePipeline(trimmed);
|
|
@@ -1913,6 +2216,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1913
2216
|
}
|
|
1914
2217
|
|
|
1915
2218
|
_parsePipeline(command) {
|
|
2219
|
+
trace('ProcessRunner', () => `_parsePipeline ENTER | ${JSON.stringify({
|
|
2220
|
+
commandLength: command?.length || 0,
|
|
2221
|
+
hasPipe: command?.includes('|')
|
|
2222
|
+
}, null, 2)}`);
|
|
2223
|
+
|
|
1916
2224
|
// Split by pipe, respecting quotes
|
|
1917
2225
|
const segments = [];
|
|
1918
2226
|
let current = '';
|
|
@@ -2220,8 +2528,17 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2220
2528
|
|
|
2221
2529
|
return result;
|
|
2222
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
|
+
|
|
2223
2540
|
const result = {
|
|
2224
|
-
code:
|
|
2541
|
+
code: exitCode,
|
|
2225
2542
|
stdout: error.stdout ?? '',
|
|
2226
2543
|
stderr: error.stderr ?? error.message,
|
|
2227
2544
|
stdin: ''
|
|
@@ -2342,8 +2659,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2342
2659
|
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
2343
2660
|
commandStr.includes(';') || commandStr.includes('`');
|
|
2344
2661
|
|
|
2662
|
+
const shell = findAvailableShell();
|
|
2345
2663
|
const spawnArgs = needsShell
|
|
2346
|
-
? [
|
|
2664
|
+
? [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr]
|
|
2347
2665
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
2348
2666
|
|
|
2349
2667
|
const proc = Bun.spawn(spawnArgs, {
|
|
@@ -2505,8 +2823,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2505
2823
|
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
2506
2824
|
commandStr.includes(';') || commandStr.includes('`');
|
|
2507
2825
|
|
|
2826
|
+
const shell = findAvailableShell();
|
|
2508
2827
|
const spawnArgs = needsShell
|
|
2509
|
-
? [
|
|
2828
|
+
? [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr]
|
|
2510
2829
|
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
2511
2830
|
|
|
2512
2831
|
const proc = Bun.spawn(spawnArgs, {
|
|
@@ -2743,7 +3062,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2743
3062
|
}
|
|
2744
3063
|
const commandStr = commandParts.join(' ');
|
|
2745
3064
|
|
|
2746
|
-
const
|
|
3065
|
+
const shell = findAvailableShell();
|
|
3066
|
+
const proc = Bun.spawn([shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr], {
|
|
2747
3067
|
cwd: this.options.cwd,
|
|
2748
3068
|
env: this.options.env,
|
|
2749
3069
|
stdin: currentInputStream ? 'pipe' : 'ignore',
|
|
@@ -3131,7 +3451,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3131
3451
|
};
|
|
3132
3452
|
|
|
3133
3453
|
// Execute using shell to handle complex commands
|
|
3134
|
-
const
|
|
3454
|
+
const shell = findAvailableShell();
|
|
3455
|
+
const argv = [shell.cmd, ...shell.args.filter(arg => arg !== '-l'), commandStr];
|
|
3135
3456
|
const isLastCommand = (i === commands.length - 1);
|
|
3136
3457
|
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
3137
3458
|
|
|
@@ -3306,14 +3627,181 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3306
3627
|
}
|
|
3307
3628
|
}
|
|
3308
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
|
+
|
|
3309
3793
|
async* stream() {
|
|
3310
3794
|
trace('ProcessRunner', () => `stream ENTER | ${JSON.stringify({
|
|
3311
3795
|
started: this.started,
|
|
3312
|
-
finished: this.finished
|
|
3796
|
+
finished: this.finished,
|
|
3797
|
+
command: this.spec?.command?.slice(0, 100)
|
|
3313
3798
|
}, null, 2)}`);
|
|
3314
3799
|
|
|
3800
|
+
// Mark that we're in streaming mode to bypass shell operator interception
|
|
3801
|
+
this._isStreaming = true;
|
|
3802
|
+
|
|
3315
3803
|
if (!this.started) {
|
|
3316
|
-
trace('ProcessRunner', () => 'Auto-starting async process from stream()');
|
|
3804
|
+
trace('ProcessRunner', () => 'Auto-starting async process from stream() with streaming mode');
|
|
3317
3805
|
this._startAsync(); // Start but don't await
|
|
3318
3806
|
}
|
|
3319
3807
|
|
|
@@ -3637,6 +4125,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3637
4125
|
|
|
3638
4126
|
// Promise interface (for await)
|
|
3639
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
|
+
|
|
3640
4134
|
if (!this.promise) {
|
|
3641
4135
|
this.promise = this._startAsync();
|
|
3642
4136
|
}
|
|
@@ -3644,6 +4138,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3644
4138
|
}
|
|
3645
4139
|
|
|
3646
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
|
+
|
|
3647
4147
|
if (!this.promise) {
|
|
3648
4148
|
this.promise = this._startAsync();
|
|
3649
4149
|
}
|
|
@@ -3651,6 +4151,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3651
4151
|
}
|
|
3652
4152
|
|
|
3653
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
|
+
|
|
3654
4160
|
if (!this.promise) {
|
|
3655
4161
|
this.promise = this._startAsync();
|
|
3656
4162
|
}
|
|
@@ -3687,7 +4193,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
3687
4193
|
trace('ProcessRunner', () => `Starting sync execution | ${JSON.stringify({ mode: this._mode }, null, 2)}`);
|
|
3688
4194
|
|
|
3689
4195
|
const { cwd, env, stdin } = this.options;
|
|
3690
|
-
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];
|
|
3691
4198
|
|
|
3692
4199
|
if (globalShellSettings.xtrace) {
|
|
3693
4200
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
@@ -3879,10 +4386,12 @@ function create(defaultOptions = {}) {
|
|
|
3879
4386
|
}
|
|
3880
4387
|
|
|
3881
4388
|
function raw(value) {
|
|
4389
|
+
trace('API', () => `raw() called with value: ${String(value).slice(0, 50)}`);
|
|
3882
4390
|
return { raw: String(value) };
|
|
3883
4391
|
}
|
|
3884
4392
|
|
|
3885
4393
|
function set(option) {
|
|
4394
|
+
trace('API', () => `set() called with option: ${option}`);
|
|
3886
4395
|
const mapping = {
|
|
3887
4396
|
'e': 'errexit', // set -e: exit on error
|
|
3888
4397
|
'errexit': 'errexit',
|
|
@@ -3906,6 +4415,7 @@ function set(option) {
|
|
|
3906
4415
|
}
|
|
3907
4416
|
|
|
3908
4417
|
function unset(option) {
|
|
4418
|
+
trace('API', () => `unset() called with option: ${option}`);
|
|
3909
4419
|
const mapping = {
|
|
3910
4420
|
'e': 'errexit',
|
|
3911
4421
|
'errexit': 'errexit',
|
|
@@ -3958,15 +4468,19 @@ function unregister(name) {
|
|
|
3958
4468
|
}
|
|
3959
4469
|
|
|
3960
4470
|
function listCommands() {
|
|
3961
|
-
|
|
4471
|
+
const commands = Array.from(virtualCommands.keys());
|
|
4472
|
+
trace('VirtualCommands', () => `listCommands() returning ${commands.length} commands`);
|
|
4473
|
+
return commands;
|
|
3962
4474
|
}
|
|
3963
4475
|
|
|
3964
4476
|
function enableVirtualCommands() {
|
|
4477
|
+
trace('VirtualCommands', () => 'Enabling virtual commands');
|
|
3965
4478
|
virtualCommandsEnabled = true;
|
|
3966
4479
|
return virtualCommandsEnabled;
|
|
3967
4480
|
}
|
|
3968
4481
|
|
|
3969
4482
|
function disableVirtualCommands() {
|
|
4483
|
+
trace('VirtualCommands', () => 'Disabling virtual commands');
|
|
3970
4484
|
virtualCommandsEnabled = false;
|
|
3971
4485
|
return virtualCommandsEnabled;
|
|
3972
4486
|
}
|
|
@@ -3996,6 +4510,7 @@ import testCommand from './commands/$.test.mjs';
|
|
|
3996
4510
|
|
|
3997
4511
|
// Built-in commands that match Bun.$ functionality
|
|
3998
4512
|
function registerBuiltins() {
|
|
4513
|
+
trace('VirtualCommands', () => 'registerBuiltins() called - registering all built-in commands');
|
|
3999
4514
|
// Register all imported commands
|
|
4000
4515
|
register('cd', cdCommand);
|
|
4001
4516
|
register('pwd', pwdCommand);
|
|
@@ -4030,12 +4545,14 @@ const AnsiUtils = {
|
|
|
4030
4545
|
|
|
4031
4546
|
stripControlChars(text) {
|
|
4032
4547
|
if (typeof text !== 'string') return text;
|
|
4033
|
-
|
|
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, '');
|
|
4034
4550
|
},
|
|
4035
4551
|
|
|
4036
4552
|
stripAll(text) {
|
|
4037
4553
|
if (typeof text !== 'string') return text;
|
|
4038
|
-
|
|
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, '');
|
|
4039
4556
|
},
|
|
4040
4557
|
|
|
4041
4558
|
cleanForProcessing(data) {
|
|
@@ -4052,15 +4569,23 @@ let globalAnsiConfig = {
|
|
|
4052
4569
|
};
|
|
4053
4570
|
|
|
4054
4571
|
function configureAnsi(options = {}) {
|
|
4572
|
+
trace('AnsiUtils', () => `configureAnsi() called | ${JSON.stringify({ options }, null, 2)}`);
|
|
4055
4573
|
globalAnsiConfig = { ...globalAnsiConfig, ...options };
|
|
4574
|
+
trace('AnsiUtils', () => `New ANSI config | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`);
|
|
4056
4575
|
return globalAnsiConfig;
|
|
4057
4576
|
}
|
|
4058
4577
|
|
|
4059
4578
|
function getAnsiConfig() {
|
|
4579
|
+
trace('AnsiUtils', () => `getAnsiConfig() returning | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`);
|
|
4060
4580
|
return { ...globalAnsiConfig };
|
|
4061
4581
|
}
|
|
4062
4582
|
|
|
4063
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)}`);
|
|
4064
4589
|
const config = { ...globalAnsiConfig, ...options };
|
|
4065
4590
|
if (!config.preserveAnsi && !config.preserveControlChars) {
|
|
4066
4591
|
return AnsiUtils.cleanForProcessing(data);
|
|
@@ -4077,7 +4602,9 @@ function processOutput(data, options = {}) {
|
|
|
4077
4602
|
}
|
|
4078
4603
|
|
|
4079
4604
|
// Initialize built-in commands
|
|
4605
|
+
trace('Initialization', () => 'Registering built-in virtual commands');
|
|
4080
4606
|
registerBuiltins();
|
|
4607
|
+
trace('Initialization', () => `Built-in commands registered: ${listCommands().join(', ')}`);
|
|
4081
4608
|
|
|
4082
4609
|
export {
|
|
4083
4610
|
$tagged as $,
|
|
@@ -4090,6 +4617,7 @@ export {
|
|
|
4090
4617
|
ProcessRunner,
|
|
4091
4618
|
shell,
|
|
4092
4619
|
set,
|
|
4620
|
+
resetGlobalState,
|
|
4093
4621
|
unset,
|
|
4094
4622
|
register,
|
|
4095
4623
|
unregister,
|
package/src/commands/$.cd.mjs
CHANGED
|
@@ -8,9 +8,10 @@ export default async function cd({ args }) {
|
|
|
8
8
|
process.chdir(target);
|
|
9
9
|
const newDir = process.cwd();
|
|
10
10
|
trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
|
|
11
|
-
|
|
11
|
+
// cd command should not output anything on success, just like real cd
|
|
12
|
+
return VirtualUtils.success('');
|
|
12
13
|
} catch (error) {
|
|
13
14
|
trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
14
|
-
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
15
|
+
return { stderr: `cd: ${error.message}\n`, code: 1 };
|
|
15
16
|
}
|
|
16
17
|
}
|
package/src/commands/$.pwd.mjs
CHANGED
|
@@ -4,5 +4,5 @@ export default async function pwd({ args, stdin, cwd }) {
|
|
|
4
4
|
// If cwd option is provided, return that instead of process.cwd()
|
|
5
5
|
const dir = cwd || process.cwd();
|
|
6
6
|
trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
|
|
7
|
-
return VirtualUtils.success(dir);
|
|
7
|
+
return VirtualUtils.success(dir + '\n');
|
|
8
8
|
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced shell command parser that handles &&, ||, ;, and () operators
|
|
3
|
+
* This allows virtual commands to work properly with shell operators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { trace } from './$.utils.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token types for the parser
|
|
10
|
+
*/
|
|
11
|
+
const TokenType = {
|
|
12
|
+
WORD: 'word',
|
|
13
|
+
AND: '&&',
|
|
14
|
+
OR: '||',
|
|
15
|
+
SEMICOLON: ';',
|
|
16
|
+
PIPE: '|',
|
|
17
|
+
LPAREN: '(',
|
|
18
|
+
RPAREN: ')',
|
|
19
|
+
REDIRECT_OUT: '>',
|
|
20
|
+
REDIRECT_APPEND: '>>',
|
|
21
|
+
REDIRECT_IN: '<',
|
|
22
|
+
EOF: 'eof'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tokenize a shell command string
|
|
27
|
+
*/
|
|
28
|
+
function tokenize(command) {
|
|
29
|
+
const tokens = [];
|
|
30
|
+
let i = 0;
|
|
31
|
+
|
|
32
|
+
while (i < command.length) {
|
|
33
|
+
// Skip whitespace
|
|
34
|
+
while (i < command.length && /\s/.test(command[i])) {
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (i >= command.length) break;
|
|
39
|
+
|
|
40
|
+
// Check for operators
|
|
41
|
+
if (command[i] === '&' && command[i + 1] === '&') {
|
|
42
|
+
tokens.push({ type: TokenType.AND, value: '&&' });
|
|
43
|
+
i += 2;
|
|
44
|
+
} else if (command[i] === '|' && command[i + 1] === '|') {
|
|
45
|
+
tokens.push({ type: TokenType.OR, value: '||' });
|
|
46
|
+
i += 2;
|
|
47
|
+
} else if (command[i] === '|') {
|
|
48
|
+
tokens.push({ type: TokenType.PIPE, value: '|' });
|
|
49
|
+
i++;
|
|
50
|
+
} else if (command[i] === ';') {
|
|
51
|
+
tokens.push({ type: TokenType.SEMICOLON, value: ';' });
|
|
52
|
+
i++;
|
|
53
|
+
} else if (command[i] === '(') {
|
|
54
|
+
tokens.push({ type: TokenType.LPAREN, value: '(' });
|
|
55
|
+
i++;
|
|
56
|
+
} else if (command[i] === ')') {
|
|
57
|
+
tokens.push({ type: TokenType.RPAREN, value: ')' });
|
|
58
|
+
i++;
|
|
59
|
+
} else if (command[i] === '>' && command[i + 1] === '>') {
|
|
60
|
+
tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
|
|
61
|
+
i += 2;
|
|
62
|
+
} else if (command[i] === '>') {
|
|
63
|
+
tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
|
|
64
|
+
i++;
|
|
65
|
+
} else if (command[i] === '<') {
|
|
66
|
+
tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
|
|
67
|
+
i++;
|
|
68
|
+
} else {
|
|
69
|
+
// Parse word (respecting quotes)
|
|
70
|
+
let word = '';
|
|
71
|
+
let inQuote = false;
|
|
72
|
+
let quoteChar = '';
|
|
73
|
+
|
|
74
|
+
while (i < command.length) {
|
|
75
|
+
const char = command[i];
|
|
76
|
+
|
|
77
|
+
if (!inQuote) {
|
|
78
|
+
if (char === '"' || char === "'") {
|
|
79
|
+
inQuote = true;
|
|
80
|
+
quoteChar = char;
|
|
81
|
+
word += char;
|
|
82
|
+
i++;
|
|
83
|
+
} else if (/\s/.test(char) ||
|
|
84
|
+
'&|;()<>'.includes(char)) {
|
|
85
|
+
break;
|
|
86
|
+
} else if (char === '\\' && i + 1 < command.length) {
|
|
87
|
+
// Handle escape sequences
|
|
88
|
+
word += char;
|
|
89
|
+
i++;
|
|
90
|
+
if (i < command.length) {
|
|
91
|
+
word += command[i];
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
word += char;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
100
|
+
inQuote = false;
|
|
101
|
+
quoteChar = '';
|
|
102
|
+
word += char;
|
|
103
|
+
i++;
|
|
104
|
+
} else if (char === '\\' && i + 1 < command.length &&
|
|
105
|
+
(command[i + 1] === quoteChar || command[i + 1] === '\\')) {
|
|
106
|
+
// Handle escaped quotes and backslashes inside quotes
|
|
107
|
+
word += char;
|
|
108
|
+
i++;
|
|
109
|
+
if (i < command.length) {
|
|
110
|
+
word += command[i];
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
word += char;
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (word) {
|
|
121
|
+
tokens.push({ type: TokenType.WORD, value: word });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tokens.push({ type: TokenType.EOF, value: '' });
|
|
127
|
+
return tokens;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a sequence of commands with operators
|
|
132
|
+
*/
|
|
133
|
+
class ShellParser {
|
|
134
|
+
constructor(command) {
|
|
135
|
+
this.tokens = tokenize(command);
|
|
136
|
+
this.pos = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
current() {
|
|
140
|
+
return this.tokens[this.pos] || { type: TokenType.EOF, value: '' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
peek() {
|
|
144
|
+
return this.tokens[this.pos + 1] || { type: TokenType.EOF, value: '' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
consume() {
|
|
148
|
+
const token = this.current();
|
|
149
|
+
this.pos++;
|
|
150
|
+
return token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse the main command sequence
|
|
155
|
+
*/
|
|
156
|
+
parse() {
|
|
157
|
+
return this.parseSequence();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse a sequence of commands connected by &&, ||, ;
|
|
162
|
+
*/
|
|
163
|
+
parseSequence() {
|
|
164
|
+
const commands = [];
|
|
165
|
+
const operators = [];
|
|
166
|
+
|
|
167
|
+
// Parse first command
|
|
168
|
+
let cmd = this.parsePipeline();
|
|
169
|
+
if (cmd) {
|
|
170
|
+
commands.push(cmd);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse additional commands with operators
|
|
174
|
+
while (this.current().type !== TokenType.EOF &&
|
|
175
|
+
this.current().type !== TokenType.RPAREN) {
|
|
176
|
+
const op = this.current();
|
|
177
|
+
|
|
178
|
+
if (op.type === TokenType.AND ||
|
|
179
|
+
op.type === TokenType.OR ||
|
|
180
|
+
op.type === TokenType.SEMICOLON) {
|
|
181
|
+
operators.push(op.type);
|
|
182
|
+
this.consume();
|
|
183
|
+
|
|
184
|
+
cmd = this.parsePipeline();
|
|
185
|
+
if (cmd) {
|
|
186
|
+
commands.push(cmd);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (commands.length === 1 && operators.length === 0) {
|
|
194
|
+
return commands[0];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
type: 'sequence',
|
|
199
|
+
commands,
|
|
200
|
+
operators
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse a pipeline (commands connected by |)
|
|
206
|
+
*/
|
|
207
|
+
parsePipeline() {
|
|
208
|
+
const commands = [];
|
|
209
|
+
|
|
210
|
+
let cmd = this.parseCommand();
|
|
211
|
+
if (cmd) {
|
|
212
|
+
commands.push(cmd);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
while (this.current().type === TokenType.PIPE) {
|
|
216
|
+
this.consume();
|
|
217
|
+
cmd = this.parseCommand();
|
|
218
|
+
if (cmd) {
|
|
219
|
+
commands.push(cmd);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (commands.length === 1) {
|
|
224
|
+
return commands[0];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
type: 'pipeline',
|
|
229
|
+
commands
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse a single command or subshell
|
|
235
|
+
*/
|
|
236
|
+
parseCommand() {
|
|
237
|
+
// Check for subshell
|
|
238
|
+
if (this.current().type === TokenType.LPAREN) {
|
|
239
|
+
this.consume(); // consume (
|
|
240
|
+
const subshell = this.parseSequence();
|
|
241
|
+
|
|
242
|
+
if (this.current().type === TokenType.RPAREN) {
|
|
243
|
+
this.consume(); // consume )
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
type: 'subshell',
|
|
248
|
+
command: subshell
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Parse simple command
|
|
253
|
+
return this.parseSimpleCommand();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Parse a simple command (command + args + redirections)
|
|
258
|
+
*/
|
|
259
|
+
parseSimpleCommand() {
|
|
260
|
+
const words = [];
|
|
261
|
+
const redirects = [];
|
|
262
|
+
|
|
263
|
+
while (this.current().type !== TokenType.EOF) {
|
|
264
|
+
const token = this.current();
|
|
265
|
+
|
|
266
|
+
if (token.type === TokenType.WORD) {
|
|
267
|
+
words.push(token.value);
|
|
268
|
+
this.consume();
|
|
269
|
+
} else if (token.type === TokenType.REDIRECT_OUT ||
|
|
270
|
+
token.type === TokenType.REDIRECT_APPEND ||
|
|
271
|
+
token.type === TokenType.REDIRECT_IN) {
|
|
272
|
+
this.consume();
|
|
273
|
+
const target = this.current();
|
|
274
|
+
if (target.type === TokenType.WORD) {
|
|
275
|
+
redirects.push({
|
|
276
|
+
type: token.type,
|
|
277
|
+
target: target.value
|
|
278
|
+
});
|
|
279
|
+
this.consume();
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (words.length === 0) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cmd = words[0];
|
|
291
|
+
const args = words.slice(1).map(word => {
|
|
292
|
+
// Remove quotes if present
|
|
293
|
+
if ((word.startsWith('"') && word.endsWith('"')) ||
|
|
294
|
+
(word.startsWith("'") && word.endsWith("'"))) {
|
|
295
|
+
return {
|
|
296
|
+
value: word.slice(1, -1),
|
|
297
|
+
quoted: true,
|
|
298
|
+
quoteChar: word[0]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
value: word,
|
|
303
|
+
quoted: false
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = {
|
|
308
|
+
type: 'simple',
|
|
309
|
+
cmd,
|
|
310
|
+
args
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (redirects.length > 0) {
|
|
314
|
+
result.redirects = redirects;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse a shell command with support for &&, ||, ;, and ()
|
|
323
|
+
*/
|
|
324
|
+
export function parseShellCommand(command) {
|
|
325
|
+
try {
|
|
326
|
+
const parser = new ShellParser(command);
|
|
327
|
+
const result = parser.parse();
|
|
328
|
+
|
|
329
|
+
trace('ShellParser', () => `Parsed command | ${JSON.stringify({
|
|
330
|
+
input: command.slice(0, 100),
|
|
331
|
+
result
|
|
332
|
+
}, null, 2)}`);
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
trace('ShellParser', () => `Parse error | ${JSON.stringify({
|
|
337
|
+
command: command.slice(0, 100),
|
|
338
|
+
error: error.message
|
|
339
|
+
}, null, 2)}`);
|
|
340
|
+
|
|
341
|
+
// Return null to fallback to sh -c
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if a command needs shell features we don't handle
|
|
348
|
+
*/
|
|
349
|
+
export function needsRealShell(command) {
|
|
350
|
+
// Check for features we don't handle yet
|
|
351
|
+
const unsupported = [
|
|
352
|
+
'`', // Command substitution
|
|
353
|
+
'$(', // Command substitution
|
|
354
|
+
'${', // Variable expansion
|
|
355
|
+
'~', // Home expansion (at start of word)
|
|
356
|
+
'*', // Glob patterns
|
|
357
|
+
'?', // Glob patterns
|
|
358
|
+
'[', // Glob patterns
|
|
359
|
+
'2>', // stderr redirection
|
|
360
|
+
'&>', // Combined redirection
|
|
361
|
+
'>&', // File descriptor duplication
|
|
362
|
+
'<<', // Here documents
|
|
363
|
+
'<<<', // Here strings
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
for (const feature of unsupported) {
|
|
367
|
+
if (command.includes(feature)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default { parseShellCommand, needsRealShell };
|