command-stream 0.3.0 → 0.3.2

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.
@@ -625,7 +625,37 @@ class ProcessRunner extends StreamEmitter {
625
625
  start(options = {}) {
626
626
  const mode = options.mode || 'async';
627
627
 
628
- trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({ mode, options }, null, 2)}`);
628
+ trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({ mode, options, started: this.started }, null, 2)}`);
629
+
630
+ // Merge new options with existing options before starting
631
+ if (Object.keys(options).length > 0 && !this.started) {
632
+ trace('ProcessRunner', () => `BRANCH: options => MERGE | ${JSON.stringify({
633
+ oldOptions: this.options,
634
+ newOptions: options
635
+ }, null, 2)}`);
636
+
637
+ // Create a new options object merging the current ones with the new ones
638
+ this.options = { ...this.options, ...options };
639
+
640
+ // Reinitialize chunks based on updated capture option
641
+ if ('capture' in options) {
642
+ trace('ProcessRunner', () => `BRANCH: capture => REINIT_CHUNKS | ${JSON.stringify({
643
+ capture: this.options.capture
644
+ }, null, 2)}`);
645
+
646
+ this.outChunks = this.options.capture ? [] : null;
647
+ this.errChunks = this.options.capture ? [] : null;
648
+ this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
649
+ this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
650
+ [Buffer.from(this.options.stdin)] : [];
651
+ }
652
+
653
+ trace('ProcessRunner', () => `OPTIONS_MERGED | ${JSON.stringify({
654
+ finalOptions: this.options
655
+ }, null, 2)}`);
656
+ } else if (Object.keys(options).length > 0 && this.started) {
657
+ trace('ProcessRunner', () => `BRANCH: options => IGNORED_ALREADY_STARTED | ${JSON.stringify({}, null, 2)}`);
658
+ }
629
659
 
630
660
  if (mode === 'sync') {
631
661
  trace('ProcessRunner', () => `BRANCH: mode => sync | ${JSON.stringify({}, null, 2)}`);
@@ -646,6 +676,12 @@ class ProcessRunner extends StreamEmitter {
646
676
  return this.start({ mode: 'async' });
647
677
  }
648
678
 
679
+ // Alias for start() method
680
+ run(options = {}) {
681
+ trace('ProcessRunner', () => `run ENTER | ${JSON.stringify({ options }, null, 2)}`);
682
+ return this.start(options);
683
+ }
684
+
649
685
  async _startAsync() {
650
686
  if (this.started) return this.promise;
651
687
  if (this.promise) return this.promise;
@@ -694,7 +730,7 @@ class ProcessRunner extends StreamEmitter {
694
730
  isVirtual: true,
695
731
  args: parsed.args
696
732
  }, null, 2)}`);
697
- return await this._runVirtual(parsed.cmd, parsed.args);
733
+ return await this._runVirtual(parsed.cmd, parsed.args, this.spec.command);
698
734
  }
699
735
  }
700
736
  }
@@ -791,8 +827,8 @@ class ProcessRunner extends StreamEmitter {
791
827
 
792
828
  const resultData = {
793
829
  code: code ?? 0, // Default to 0 if exit code is null/undefined
794
- stdout: this.options.capture ? Buffer.concat(this.outChunks).toString('utf8') : undefined,
795
- stderr: this.options.capture ? Buffer.concat(this.errChunks).toString('utf8') : undefined,
830
+ stdout: this.options.capture ? (this.outChunks && this.outChunks.length > 0 ? Buffer.concat(this.outChunks).toString('utf8') : '') : undefined,
831
+ stderr: this.options.capture ? (this.errChunks && this.errChunks.length > 0 ? Buffer.concat(this.errChunks).toString('utf8') : '') : undefined,
796
832
  stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
797
833
  child: this.child
798
834
  };
@@ -927,8 +963,8 @@ class ProcessRunner extends StreamEmitter {
927
963
  return { type: 'pipeline', commands };
928
964
  }
929
965
 
930
- async _runVirtual(cmd, args) {
931
- trace('ProcessRunner', () => `_runVirtual ENTER | ${JSON.stringify({ cmd, args }, null, 2)}`);
966
+ async _runVirtual(cmd, args, originalCommand = null) {
967
+ trace('ProcessRunner', () => `_runVirtual ENTER | ${JSON.stringify({ cmd, args, originalCommand }, null, 2)}`);
932
968
 
933
969
  const handler = virtualCommands.get(cmd);
934
970
  if (!handler) {
@@ -955,10 +991,10 @@ class ProcessRunner extends StreamEmitter {
955
991
 
956
992
  // Shell tracing for virtual commands
957
993
  if (globalShellSettings.xtrace) {
958
- console.log(`+ ${cmd} ${argValues.join(' ')}`);
994
+ console.log(`+ ${originalCommand || `${cmd} ${argValues.join(' ')}`}`);
959
995
  }
960
996
  if (globalShellSettings.verbose) {
961
- console.log(`${cmd} ${argValues.join(' ')}`);
997
+ console.log(`${originalCommand || `${cmd} ${argValues.join(' ')}`}`);
962
998
  }
963
999
 
964
1000
  let result;
@@ -1031,11 +1067,11 @@ class ProcessRunner extends StreamEmitter {
1031
1067
  result = await handler({ args: argValues, stdin: stdinData, ...this.options });
1032
1068
 
1033
1069
  result = {
1070
+ ...result,
1034
1071
  code: result.code ?? 0,
1035
1072
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
1036
1073
  stderr: this.options.capture ? (result.stderr ?? '') : undefined,
1037
- stdin: this.options.capture ? stdinData : undefined,
1038
- ...result
1074
+ stdin: this.options.capture ? stdinData : undefined
1039
1075
  };
1040
1076
 
1041
1077
  // Mirror and emit output
@@ -1097,6 +1133,7 @@ class ProcessRunner extends StreamEmitter {
1097
1133
  this.emit('exit', result.code);
1098
1134
 
1099
1135
  if (globalShellSettings.errexit) {
1136
+ error.result = result;
1100
1137
  throw error;
1101
1138
  }
1102
1139
 
@@ -1758,11 +1795,11 @@ class ProcessRunner extends StreamEmitter {
1758
1795
  // Regular async function
1759
1796
  result = await handler({ args: argValues, stdin: currentInput, ...this.options });
1760
1797
  result = {
1798
+ ...result,
1761
1799
  code: result.code ?? 0,
1762
1800
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
1763
1801
  stderr: this.options.capture ? (result.stderr ?? '') : undefined,
1764
- stdin: this.options.capture ? currentInput : undefined,
1765
- ...result
1802
+ stdin: this.options.capture ? currentInput : undefined
1766
1803
  };
1767
1804
  }
1768
1805
 
@@ -2614,673 +2651,56 @@ function disableVirtualCommands() {
2614
2651
  return virtualCommandsEnabled;
2615
2652
  }
2616
2653
 
2654
+ // Import virtual commands
2655
+ import cdCommand from './commands/$.cd.mjs';
2656
+ import pwdCommand from './commands/$.pwd.mjs';
2657
+ import echoCommand from './commands/$.echo.mjs';
2658
+ import sleepCommand from './commands/$.sleep.mjs';
2659
+ import trueCommand from './commands/$.true.mjs';
2660
+ import falseCommand from './commands/$.false.mjs';
2661
+ import createWhichCommand from './commands/$.which.mjs';
2662
+ import createExitCommand from './commands/$.exit.mjs';
2663
+ import envCommand from './commands/$.env.mjs';
2664
+ import catCommand from './commands/$.cat.mjs';
2665
+ import lsCommand from './commands/$.ls.mjs';
2666
+ import mkdirCommand from './commands/$.mkdir.mjs';
2667
+ import rmCommand from './commands/$.rm.mjs';
2668
+ import mvCommand from './commands/$.mv.mjs';
2669
+ import cpCommand from './commands/$.cp.mjs';
2670
+ import touchCommand from './commands/$.touch.mjs';
2671
+ import basenameCommand from './commands/$.basename.mjs';
2672
+ import dirnameCommand from './commands/$.dirname.mjs';
2673
+ import yesCommand from './commands/$.yes.mjs';
2674
+ import seqCommand from './commands/$.seq.mjs';
2675
+ import testCommand from './commands/$.test.mjs';
2676
+
2617
2677
  // Built-in commands that match Bun.$ functionality
2618
2678
  function registerBuiltins() {
2619
- // cd - change directory
2620
- register('cd', async ({ args }) => {
2621
- const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
2622
- trace('VirtualCommand', () => `cd: changing directory | ${JSON.stringify({ target }, null, 2)}`);
2623
-
2624
- try {
2625
- process.chdir(target);
2626
- const newDir = process.cwd();
2627
- trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
2628
- return VirtualUtils.success(newDir);
2629
- } catch (error) {
2630
- trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
2631
- return { stderr: `cd: ${error.message}`, code: 1 };
2632
- }
2633
- });
2634
-
2635
- // pwd - print working directory
2636
- register('pwd', async ({ args, stdin, cwd }) => {
2637
- // If cwd option is provided, return that instead of process.cwd()
2638
- const dir = cwd || process.cwd();
2639
- trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
2640
- return VirtualUtils.success(dir);
2641
- });
2642
-
2643
- // echo - print arguments
2644
- register('echo', async ({ args }) => {
2645
- trace('VirtualCommand', () => `echo: processing | ${JSON.stringify({ argsCount: args.length }, null, 2)}`);
2646
-
2647
- let output = args.join(' ');
2648
- if (args.includes('-n')) {
2649
- // Don't add newline
2650
- trace('VirtualCommand', () => `BRANCH: echo => NO_NEWLINE | ${JSON.stringify({}, null, 2)}`);
2651
- output = args.filter(arg => arg !== '-n').join(' ');
2652
- } else {
2653
- output += '\n';
2654
- }
2655
- return VirtualUtils.success(output);
2656
- });
2657
-
2658
- // sleep - wait for specified time
2659
- register('sleep', async ({ args }) => {
2660
- const seconds = parseFloat(args[0] || 0);
2661
- trace('VirtualCommand', () => `sleep: starting | ${JSON.stringify({ seconds }, null, 2)}`);
2662
-
2663
- if (isNaN(seconds) || seconds < 0) {
2664
- trace('VirtualCommand', () => `sleep: invalid interval | ${JSON.stringify({ input: args[0] }, null, 2)}`);
2665
- return { stderr: 'sleep: invalid time interval', code: 1 };
2666
- }
2667
-
2668
- await new Promise(resolve => setTimeout(resolve, seconds * 1000));
2669
- trace('VirtualCommand', () => `sleep: completed | ${JSON.stringify({ seconds }, null, 2)}`);
2670
- return { stdout: '', code: 0 };
2671
- });
2672
-
2673
- // true - always succeed
2674
- register('true', async () => {
2675
- return VirtualUtils.success();
2676
- });
2677
-
2678
- // false - always fail
2679
- register('false', async () => {
2680
- return { stdout: '', code: 1 };
2681
- });
2682
-
2683
- // which - locate command
2684
- register('which', async ({ args }) => {
2685
- const argError = VirtualUtils.validateArgs(args, 1, 'which');
2686
- if (argError) return argError;
2687
-
2688
- const cmd = args[0];
2689
-
2690
- if (virtualCommands.has(cmd)) {
2691
- return VirtualUtils.success(`${cmd}: shell builtin\n`);
2692
- }
2693
-
2694
- const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
2695
- const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
2696
-
2697
- for (const pathDir of paths) {
2698
- for (const ext of extensions) {
2699
- const fullPath = path.join(pathDir, cmd + ext);
2700
- try {
2701
- if (fs.statSync(fullPath).isFile()) {
2702
- return VirtualUtils.success(fullPath + '\n');
2703
- }
2704
- } catch { }
2705
- }
2706
- }
2707
-
2708
- return VirtualUtils.error(`which: no ${cmd} in PATH`);
2709
- });
2710
-
2711
- // exit - exit with code
2712
- register('exit', async ({ args }) => {
2713
- const code = parseInt(args[0] || 0);
2714
- if (globalShellSettings.errexit || code !== 0) {
2715
- // For virtual commands, we simulate exit by returning the code
2716
- return { stdout: '', code };
2717
- }
2718
- return { stdout: '', code: 0 };
2719
- });
2720
-
2721
- // env - print environment variables
2722
- register('env', async ({ args, stdin, env }) => {
2723
- if (args.length === 0) {
2724
- // Use custom env if provided, otherwise use process.env
2725
- const envVars = env || process.env;
2726
- const output = Object.entries(envVars)
2727
- .map(([key, value]) => `${key}=${value}`)
2728
- .join('\n') + '\n';
2729
- return { stdout: output, code: 0 };
2730
- }
2731
-
2732
- // TODO: Support env VAR=value command syntax
2733
- return { stderr: 'env: command execution not yet supported', code: 1 };
2734
- });
2735
-
2736
- // cat - read and display file contents
2737
- register('cat', async ({ args, stdin, cwd }) => {
2738
- if (args.length === 0) {
2739
- // Read from stdin if no files specified
2740
- return { stdout: stdin || '', code: 0 };
2741
- }
2742
-
2743
- try {
2744
- let output = '';
2745
-
2746
- for (const filename of args) {
2747
- if (filename === '-n') continue; // Line numbering (basic support)
2748
-
2749
- try {
2750
- // Resolve path relative to cwd if provided
2751
- const basePath = cwd || process.cwd();
2752
- const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
2753
-
2754
- const content = fs.readFileSync(fullPath, 'utf8');
2755
- output += content;
2756
- } catch (error) {
2757
- const errorMsg = error.code === 'ENOENT' ? 'No such file or directory' : error.message;
2758
- return {
2759
- stderr: `cat: ${filename}: ${errorMsg}`,
2760
- stdout: output,
2761
- code: 1
2762
- };
2763
- }
2764
- }
2765
-
2766
- return { stdout: output, code: 0 };
2767
- } catch (error) {
2768
- return { stderr: `cat: ${error.message}`, code: 1 };
2769
- }
2770
- });
2771
-
2772
- // ls - list directory contents
2773
- register('ls', async ({ args, stdin, cwd }) => {
2774
- try {
2775
-
2776
- const flags = args.filter(arg => arg.startsWith('-'));
2777
- const paths = args.filter(arg => !arg.startsWith('-'));
2778
- const isLongFormat = flags.includes('-l');
2779
- const showAll = flags.includes('-a');
2780
- const showAlmostAll = flags.includes('-A');
2781
-
2782
- // Default to current directory if no paths specified
2783
- const targetPaths = paths.length > 0 ? paths : ['.'];
2784
-
2785
- let output = '';
2786
-
2787
- for (const targetPath of targetPaths) {
2788
- // Resolve path relative to cwd if provided
2789
- const basePath = cwd || process.cwd();
2790
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
2791
-
2792
- try {
2793
- const stat = fs.statSync(fullPath);
2794
-
2795
- if (stat.isFile()) {
2796
- // Just show the file name if it's a file
2797
- output += path.basename(targetPath) + '\n';
2798
- } else if (stat.isDirectory()) {
2799
- const entries = fs.readdirSync(fullPath);
2800
-
2801
- // Filter hidden files unless -a or -A is specified
2802
- let filteredEntries = entries;
2803
- if (!showAll && !showAlmostAll) {
2804
- filteredEntries = entries.filter(entry => !entry.startsWith('.'));
2805
- } else if (showAlmostAll) {
2806
- filteredEntries = entries.filter(entry => entry !== '.' && entry !== '..');
2807
- }
2808
-
2809
- if (isLongFormat) {
2810
- for (const entry of filteredEntries) {
2811
- const entryPath = path.join(fullPath, entry);
2812
- try {
2813
- const entryStat = fs.statSync(entryPath);
2814
- const isDir = entryStat.isDirectory();
2815
- const permissions = isDir ? 'drwxr-xr-x' : '-rw-r--r--';
2816
- const size = entryStat.size.toString().padStart(8);
2817
- const date = entryStat.mtime.toISOString().slice(0, 16).replace('T', ' ');
2818
- output += `${permissions} 1 user group ${size} ${date} ${entry}\n`;
2819
- } catch {
2820
- output += `?????????? 1 user group ? ??? ?? ??:?? ${entry}\n`;
2821
- }
2822
- }
2823
- } else {
2824
- output += filteredEntries.join('\n') + (filteredEntries.length > 0 ? '\n' : '');
2825
- }
2826
- }
2827
- } catch (error) {
2828
- return {
2829
- stderr: `ls: cannot access '${targetPath}': ${error.message}`,
2830
- code: 2
2831
- };
2832
- }
2833
- }
2834
-
2835
- return { stdout: output, code: 0 };
2836
- } catch (error) {
2837
- return { stderr: `ls: ${error.message}`, code: 1 };
2838
- }
2839
- });
2840
-
2841
- register('mkdir', async ({ args, stdin, cwd }) => {
2842
- const argError = VirtualUtils.validateArgs(args, 1, 'mkdir');
2843
- if (argError) return argError;
2844
-
2845
- try {
2846
-
2847
- const flags = args.filter(arg => arg.startsWith('-'));
2848
- const dirs = args.filter(arg => !arg.startsWith('-'));
2849
- const recursive = flags.includes('-p');
2850
-
2851
- for (const dir of dirs) {
2852
- try {
2853
- const basePath = cwd || process.cwd();
2854
- const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
2855
-
2856
- if (recursive) {
2857
- fs.mkdirSync(fullPath, { recursive: true });
2858
- } else {
2859
- fs.mkdirSync(fullPath);
2860
- }
2861
- } catch (error) {
2862
- return {
2863
- stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
2864
- code: 1
2865
- };
2866
- }
2867
- }
2868
-
2869
- return { stdout: '', code: 0 };
2870
- } catch (error) {
2871
- return { stderr: `mkdir: ${error.message}`, code: 1 };
2872
- }
2873
- });
2874
-
2875
- // rm - remove files and directories
2876
- register('rm', async ({ args, stdin, cwd }) => {
2877
- const argError = VirtualUtils.validateArgs(args, 1, 'rm');
2878
- if (argError) return argError;
2879
-
2880
- try {
2881
-
2882
- const flags = args.filter(arg => arg.startsWith('-'));
2883
- const targets = args.filter(arg => !arg.startsWith('-'));
2884
- const recursive = flags.includes('-r') || flags.includes('-R');
2885
- const force = flags.includes('-f');
2886
-
2887
- for (const target of targets) {
2888
- try {
2889
- const basePath = cwd || process.cwd();
2890
- const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
2891
-
2892
- const stat = fs.statSync(fullPath);
2893
-
2894
- if (stat.isDirectory()) {
2895
- if (!recursive) {
2896
- return {
2897
- stderr: `rm: cannot remove '${target}': Is a directory`,
2898
- code: 1
2899
- };
2900
- }
2901
- fs.rmSync(fullPath, { recursive: true, force });
2902
- } else {
2903
- fs.unlinkSync(fullPath);
2904
- }
2905
- } catch (error) {
2906
- if (!force) {
2907
- return {
2908
- stderr: `rm: cannot remove '${target}': ${error.message}`,
2909
- code: 1
2910
- };
2911
- }
2912
- }
2913
- }
2914
-
2915
- return { stdout: '', code: 0 };
2916
- } catch (error) {
2917
- return { stderr: `rm: ${error.message}`, code: 1 };
2918
- }
2919
- });
2920
-
2921
- // mv - move/rename files and directories
2922
- register('mv', async ({ args, stdin, cwd }) => {
2923
- const argError = VirtualUtils.validateArgs(args, 2, 'mv');
2924
- if (argError) return VirtualUtils.invalidArgumentError('mv', 'missing destination file operand');
2925
-
2926
- try {
2927
-
2928
- const basePath = cwd || process.cwd();
2929
-
2930
- if (args.length === 2) {
2931
- // Simple rename/move
2932
- const [source, dest] = args;
2933
- const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
2934
- let destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2935
-
2936
- try {
2937
- try {
2938
- const destStat = fs.statSync(destPath);
2939
- if (destStat.isDirectory()) {
2940
- // Move file into the directory
2941
- const fileName = path.basename(source);
2942
- destPath = path.join(destPath, fileName);
2943
- }
2944
- } catch {
2945
- // Destination doesn't exist, proceed with direct rename
2946
- }
2947
-
2948
- fs.renameSync(sourcePath, destPath);
2949
- } catch (error) {
2950
- return {
2951
- stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2952
- code: 1
2953
- };
2954
- }
2955
- } else {
2956
- // Multiple sources to directory
2957
- const sources = args.slice(0, -1);
2958
- const dest = args[args.length - 1];
2959
- const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2960
-
2961
- try {
2962
- const destStat = fs.statSync(destPath);
2963
- if (!destStat.isDirectory()) {
2964
- return {
2965
- stderr: `mv: target '${dest}' is not a directory`,
2966
- code: 1
2967
- };
2968
- }
2969
- } catch {
2970
- return {
2971
- stderr: `mv: cannot access '${dest}': No such file or directory`,
2972
- code: 1
2973
- };
2974
- }
2975
-
2976
- for (const source of sources) {
2977
- try {
2978
- const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
2979
- const fileName = path.basename(source);
2980
- const newDestPath = path.join(destPath, fileName);
2981
- fs.renameSync(sourcePath, newDestPath);
2982
- } catch (error) {
2983
- return {
2984
- stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2985
- code: 1
2986
- };
2987
- }
2988
- }
2989
- }
2990
-
2991
- return { stdout: '', code: 0 };
2992
- } catch (error) {
2993
- return { stderr: `mv: ${error.message}`, code: 1 };
2994
- }
2995
- });
2996
-
2997
- // cp - copy files and directories
2998
- register('cp', async ({ args, stdin, cwd }) => {
2999
- const argError = VirtualUtils.validateArgs(args, 2, 'cp');
3000
- if (argError) return VirtualUtils.invalidArgumentError('cp', 'missing destination file operand');
3001
-
3002
- try {
3003
-
3004
- const flags = args.filter(arg => arg.startsWith('-'));
3005
- const paths = args.filter(arg => !arg.startsWith('-'));
3006
- const recursive = flags.includes('-r') || flags.includes('-R');
3007
-
3008
- const basePath = cwd || process.cwd();
3009
-
3010
- if (paths.length === 2) {
3011
- // Simple copy
3012
- const [source, dest] = paths;
3013
- const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
3014
- const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
3015
-
3016
- try {
3017
- const sourceStat = fs.statSync(sourcePath);
3018
-
3019
- if (sourceStat.isDirectory()) {
3020
- if (!recursive) {
3021
- return {
3022
- stderr: `cp: -r not specified; omitting directory '${source}'`,
3023
- code: 1
3024
- };
3025
- }
3026
- fs.cpSync(sourcePath, destPath, { recursive: true });
3027
- } else {
3028
- fs.copyFileSync(sourcePath, destPath);
3029
- }
3030
- } catch (error) {
3031
- return {
3032
- stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
3033
- code: 1
3034
- };
3035
- }
3036
- } else {
3037
- // Multiple sources to directory
3038
- const sources = paths.slice(0, -1);
3039
- const dest = paths[paths.length - 1];
3040
- const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
3041
-
3042
- try {
3043
- const destStat = fs.statSync(destPath);
3044
- if (!destStat.isDirectory()) {
3045
- return {
3046
- stderr: `cp: target '${dest}' is not a directory`,
3047
- code: 1
3048
- };
3049
- }
3050
- } catch {
3051
- return {
3052
- stderr: `cp: cannot access '${dest}': No such file or directory`,
3053
- code: 1
3054
- };
3055
- }
3056
-
3057
- for (const source of sources) {
3058
- try {
3059
- const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
3060
- const fileName = path.basename(source);
3061
- const newDestPath = path.join(destPath, fileName);
3062
-
3063
- const sourceStat = fs.statSync(sourcePath);
3064
- if (sourceStat.isDirectory()) {
3065
- if (!recursive) {
3066
- return {
3067
- stderr: `cp: -r not specified; omitting directory '${source}'`,
3068
- code: 1
3069
- };
3070
- }
3071
- fs.cpSync(sourcePath, newDestPath, { recursive: true });
3072
- } else {
3073
- fs.copyFileSync(sourcePath, newDestPath);
3074
- }
3075
- } catch (error) {
3076
- return {
3077
- stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
3078
- code: 1
3079
- };
3080
- }
3081
- }
3082
- }
3083
-
3084
- return { stdout: '', code: 0 };
3085
- } catch (error) {
3086
- return { stderr: `cp: ${error.message}`, code: 1 };
3087
- }
3088
- });
3089
-
3090
- // touch - create or update file timestamps
3091
- register('touch', async ({ args, stdin, cwd }) => {
3092
- const argError = VirtualUtils.validateArgs(args, 1, 'touch');
3093
- if (argError) return VirtualUtils.missingOperandError('touch', 'touch: missing file operand');
3094
-
3095
- try {
3096
-
3097
- const basePath = cwd || process.cwd();
3098
-
3099
- for (const file of args) {
3100
- try {
3101
- const fullPath = path.isAbsolute(file) ? file : path.join(basePath, file);
3102
-
3103
- // Try to update timestamps if file exists
3104
- try {
3105
- const now = new Date();
3106
- fs.utimesSync(fullPath, now, now);
3107
- } catch {
3108
- fs.writeFileSync(fullPath, '', { flag: 'w' });
3109
- }
3110
- } catch (error) {
3111
- return {
3112
- stderr: `touch: cannot touch '${file}': ${error.message}`,
3113
- code: 1
3114
- };
3115
- }
3116
- }
3117
-
3118
- return { stdout: '', code: 0 };
3119
- } catch (error) {
3120
- return { stderr: `touch: ${error.message}`, code: 1 };
3121
- }
3122
- });
3123
-
3124
- // basename - extract filename from path
3125
- register('basename', async ({ args }) => {
3126
- const argError = VirtualUtils.validateArgs(args, 1, 'basename');
3127
- if (argError) return argError;
3128
-
3129
- try {
3130
-
3131
- const pathname = args[0];
3132
- const suffix = args[1];
3133
-
3134
- let result = path.basename(pathname);
3135
-
3136
- // Remove suffix if provided
3137
- if (suffix && result.endsWith(suffix)) {
3138
- result = result.slice(0, -suffix.length);
3139
- }
3140
-
3141
- return { stdout: result + '\n', code: 0 };
3142
- } catch (error) {
3143
- return { stderr: `basename: ${error.message}`, code: 1 };
3144
- }
3145
- });
3146
-
3147
- // dirname - extract directory from path
3148
- register('dirname', async ({ args }) => {
3149
- const argError = VirtualUtils.validateArgs(args, 1, 'dirname');
3150
- if (argError) return argError;
3151
-
3152
- try {
3153
-
3154
- const pathname = args[0];
3155
- const result = path.dirname(pathname);
3156
-
3157
- return { stdout: result + '\n', code: 0 };
3158
- } catch (error) {
3159
- return { stderr: `dirname: ${error.message}`, code: 1 };
3160
- }
3161
- });
3162
-
3163
- // yes - output a string repeatedly
3164
- register('yes', async function* ({ args, stdin, isCancelled, signal, ...rest }) {
3165
- const output = args.length > 0 ? args.join(' ') : 'y';
3166
- trace('VirtualCommand', () => `yes: starting infinite generator | ${JSON.stringify({ output }, null, 2)}`);
3167
-
3168
- // Generate infinite stream of the output
3169
- while (true) {
3170
- if (isCancelled && isCancelled()) {
3171
- trace('VirtualCommand', () => 'yes: cancelled via function');
3172
- return;
3173
- }
3174
- if (signal && signal.aborted) {
3175
- trace('VirtualCommand', () => 'yes: cancelled via abort signal');
3176
- return;
3177
- }
3178
-
3179
- yield output + '\n';
3180
-
3181
- try {
3182
- await new Promise((resolve, reject) => {
3183
- const timeout = setTimeout(resolve, 0);
3184
-
3185
- // Listen for abort signal if available
3186
- if (signal) {
3187
- const abortHandler = () => {
3188
- clearTimeout(timeout);
3189
- reject(new Error('Aborted'));
3190
- };
3191
-
3192
- if (signal.aborted) {
3193
- abortHandler();
3194
- } else {
3195
- signal.addEventListener('abort', abortHandler, { once: true });
3196
- }
3197
- }
3198
- });
3199
- } catch (err) {
3200
- // Aborted
3201
- return;
3202
- }
3203
- }
3204
- });
3205
-
3206
- // seq - generate sequence of numbers
3207
- register('seq', async ({ args }) => {
3208
- const argError = VirtualUtils.validateArgs(args, 1, 'seq');
3209
- if (argError) return argError;
3210
-
3211
- try {
3212
- let start, step, end;
3213
-
3214
- if (args.length === 1) {
3215
- start = 1;
3216
- step = 1;
3217
- end = parseInt(args[0]);
3218
- } else if (args.length === 2) {
3219
- start = parseInt(args[0]);
3220
- step = 1;
3221
- end = parseInt(args[1]);
3222
- } else if (args.length === 3) {
3223
- start = parseInt(args[0]);
3224
- step = parseInt(args[1]);
3225
- end = parseInt(args[2]);
3226
- } else {
3227
- return { stderr: 'seq: too many operands', code: 1 };
3228
- }
3229
-
3230
- if (isNaN(start) || isNaN(step) || isNaN(end)) {
3231
- return { stderr: 'seq: invalid number', code: 1 };
3232
- }
3233
-
3234
- let output = '';
3235
- if (step > 0) {
3236
- for (let i = start; i <= end; i += step) {
3237
- output += i + '\n';
3238
- }
3239
- } else if (step < 0) {
3240
- for (let i = start; i >= end; i += step) {
3241
- output += i + '\n';
3242
- }
3243
- } else {
3244
- return { stderr: 'seq: invalid increment', code: 1 };
3245
- }
3246
-
3247
- return { stdout: output, code: 0 };
3248
- } catch (error) {
3249
- return { stderr: `seq: ${error.message}`, code: 1 };
3250
- }
3251
- });
3252
-
3253
- // test - test file conditions (basic implementation)
3254
- register('test', async ({ args }) => {
3255
- if (args.length === 0) {
3256
- return { stdout: '', code: 1 };
3257
- }
3258
-
3259
- // Very basic test implementation
3260
- const arg = args[0];
3261
-
3262
- try {
3263
- if (arg === '-d' && args[1]) {
3264
- // Test if directory
3265
- const stat = fs.statSync(args[1]);
3266
- return { stdout: '', code: stat.isDirectory() ? 0 : 1 };
3267
- } else if (arg === '-f' && args[1]) {
3268
- // Test if file
3269
- const stat = fs.statSync(args[1]);
3270
- return { stdout: '', code: stat.isFile() ? 0 : 1 };
3271
- } else if (arg === '-e' && args[1]) {
3272
- // Test if exists
3273
- fs.statSync(args[1]);
3274
- return { stdout: '', code: 0 };
3275
- }
3276
- } catch {
3277
- return { stdout: '', code: 1 };
3278
- }
3279
-
3280
- return { stdout: '', code: 1 };
3281
- });
2679
+ // Register all imported commands
2680
+ register('cd', cdCommand);
2681
+ register('pwd', pwdCommand);
2682
+ register('echo', echoCommand);
2683
+ register('sleep', sleepCommand);
2684
+ register('true', trueCommand);
2685
+ register('false', falseCommand);
2686
+ register('which', createWhichCommand(virtualCommands));
2687
+ register('exit', createExitCommand(globalShellSettings));
2688
+ register('env', envCommand);
2689
+ register('cat', catCommand);
2690
+ register('ls', lsCommand);
2691
+ register('mkdir', mkdirCommand);
2692
+ register('rm', rmCommand);
2693
+ register('mv', mvCommand);
2694
+ register('cp', cpCommand);
2695
+ register('touch', touchCommand);
2696
+ register('basename', basenameCommand);
2697
+ register('dirname', dirnameCommand);
2698
+ register('yes', yesCommand);
2699
+ register('seq', seqCommand);
2700
+ register('test', testCommand);
3282
2701
  }
3283
2702
 
2703
+
3284
2704
  // ANSI control character utilities
3285
2705
  const AnsiUtils = {
3286
2706
  stripAnsi(text) {