command-stream 0.0.4 → 0.0.5

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.
Files changed (2) hide show
  1. package/$.mjs +209 -37
  2. package/package.json +1 -1
package/$.mjs CHANGED
@@ -311,12 +311,12 @@ class ProcessRunner extends StreamEmitter {
311
311
 
312
312
  const cmd = parts[0];
313
313
  const args = parts.slice(1).map(arg => {
314
- // Remove quotes if present
314
+ // Keep track of whether the arg was quoted
315
315
  if ((arg.startsWith('"') && arg.endsWith('"')) ||
316
316
  (arg.startsWith("'") && arg.endsWith("'"))) {
317
- return arg.slice(1, -1);
317
+ return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
318
318
  }
319
- return arg;
319
+ return { value: arg, quoted: false };
320
320
  });
321
321
 
322
322
  return { cmd, args, type: 'simple' };
@@ -359,11 +359,13 @@ class ProcessRunner extends StreamEmitter {
359
359
 
360
360
  const cmd = parts[0];
361
361
  const args = parts.slice(1).map(arg => {
362
+ // Keep track of whether the arg was quoted
362
363
  if ((arg.startsWith('"') && arg.endsWith('"')) ||
363
364
  (arg.startsWith("'") && arg.endsWith("'"))) {
364
- return arg.slice(1, -1);
365
+ // Store the original with quotes for system commands
366
+ return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
365
367
  }
366
- return arg;
368
+ return { value: arg, quoted: false };
367
369
  });
368
370
 
369
371
  return { cmd, args };
@@ -387,12 +389,15 @@ class ProcessRunner extends StreamEmitter {
387
389
  stdinData = this.options.stdin.toString('utf8');
388
390
  }
389
391
 
392
+ // Extract actual values for virtual command
393
+ const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
394
+
390
395
  // Shell tracing for virtual commands
391
396
  if (globalShellSettings.xtrace) {
392
- console.log(`+ ${cmd} ${args.join(' ')}`);
397
+ console.log(`+ ${cmd} ${argValues.join(' ')}`);
393
398
  }
394
399
  if (globalShellSettings.verbose) {
395
- console.log(`${cmd} ${args.join(' ')}`);
400
+ console.log(`${cmd} ${argValues.join(' ')}`);
396
401
  }
397
402
 
398
403
  // Execute the virtual command
@@ -402,7 +407,7 @@ class ProcessRunner extends StreamEmitter {
402
407
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
403
408
  // Handle streaming virtual command
404
409
  const chunks = [];
405
- for await (const chunk of handler(args, stdinData, this.options)) {
410
+ for await (const chunk of handler(argValues, stdinData, this.options)) {
406
411
  const buf = Buffer.from(chunk);
407
412
  chunks.push(buf);
408
413
 
@@ -422,7 +427,7 @@ class ProcessRunner extends StreamEmitter {
422
427
  };
423
428
  } else {
424
429
  // Regular async function
425
- result = await handler(args, stdinData, this.options);
430
+ result = await handler(argValues, stdinData, this.options);
426
431
 
427
432
  // Ensure result has required fields, respecting capture option
428
433
  result = {
@@ -524,18 +529,21 @@ class ProcessRunner extends StreamEmitter {
524
529
  const command = commands[i];
525
530
  const { cmd, args } = command;
526
531
 
527
- // Check if this is a virtual command
532
+ // Check if this is a virtual command (only if virtual commands are enabled)
528
533
  if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
529
534
  // Run virtual command with current input
530
535
  const handler = virtualCommands.get(cmd);
531
536
 
532
537
  try {
538
+ // Extract actual values for virtual command
539
+ const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
540
+
533
541
  // Shell tracing for virtual commands
534
542
  if (globalShellSettings.xtrace) {
535
- console.log(`+ ${cmd} ${args.join(' ')}`);
543
+ console.log(`+ ${cmd} ${argValues.join(' ')}`);
536
544
  }
537
545
  if (globalShellSettings.verbose) {
538
- console.log(`${cmd} ${args.join(' ')}`);
546
+ console.log(`${cmd} ${argValues.join(' ')}`);
539
547
  }
540
548
 
541
549
  let result;
@@ -543,7 +551,7 @@ class ProcessRunner extends StreamEmitter {
543
551
  // Check if handler is async generator (streaming)
544
552
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
545
553
  const chunks = [];
546
- for await (const chunk of handler(args, currentInput, this.options)) {
554
+ for await (const chunk of handler(argValues, currentInput, this.options)) {
547
555
  chunks.push(Buffer.from(chunk));
548
556
  }
549
557
  result = {
@@ -554,7 +562,7 @@ class ProcessRunner extends StreamEmitter {
554
562
  };
555
563
  } else {
556
564
  // Regular async function
557
- result = await handler(args, currentInput, this.options);
565
+ result = await handler(argValues, currentInput, this.options);
558
566
  result = {
559
567
  code: result.code ?? 0,
560
568
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
@@ -660,30 +668,194 @@ class ProcessRunner extends StreamEmitter {
660
668
  return result;
661
669
  }
662
670
  } else {
663
- // For system commands in pipeline, we would need to spawn processes
664
- // For now, return an error indicating this isn't supported
665
- const result = createResult({
666
- code: 1,
667
- stdout: currentOutput,
668
- stderr: `Pipeline with system command '${cmd}' not yet supported`,
669
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
670
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
671
- });
672
-
673
- this.result = result;
674
- this.finished = true;
675
-
676
- const buf = Buffer.from(result.stderr);
677
- if (this.options.mirror) {
678
- process.stderr.write(buf);
671
+ // Execute system command in pipeline
672
+ try {
673
+ // Build command string for this part of the pipeline
674
+ const commandParts = [cmd];
675
+ for (const arg of args) {
676
+ if (arg.value !== undefined) {
677
+ // Handle our parsed arg structure
678
+ if (arg.quoted) {
679
+ // Preserve original quotes
680
+ commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
681
+ } else if (arg.value.includes(' ')) {
682
+ // Quote if contains spaces
683
+ commandParts.push(`"${arg.value}"`);
684
+ } else {
685
+ commandParts.push(arg.value);
686
+ }
687
+ } else {
688
+ // Handle plain string args (backward compatibility)
689
+ if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
690
+ commandParts.push(`"${arg}"`);
691
+ } else {
692
+ commandParts.push(arg);
693
+ }
694
+ }
695
+ }
696
+ const commandStr = commandParts.join(' ');
697
+
698
+ // Shell tracing for system commands
699
+ if (globalShellSettings.xtrace) {
700
+ console.log(`+ ${commandStr}`);
701
+ }
702
+ if (globalShellSettings.verbose) {
703
+ console.log(commandStr);
704
+ }
705
+
706
+ // Execute the system command with current input as stdin
707
+ const spawnBun = (argv, stdin) => {
708
+ return Bun.spawnSync(argv, {
709
+ cwd: this.options.cwd,
710
+ env: this.options.env,
711
+ stdin: stdin ? Buffer.from(stdin) : undefined,
712
+ stdout: 'pipe',
713
+ stderr: 'pipe'
714
+ });
715
+ };
716
+
717
+ const spawnNode = (argv, stdin) => {
718
+ const require = createRequire(import.meta.url);
719
+ const cp = require('child_process');
720
+ return cp.spawnSync(argv[0], argv.slice(1), {
721
+ cwd: this.options.cwd,
722
+ env: this.options.env,
723
+ input: stdin || undefined,
724
+ encoding: 'utf8',
725
+ stdio: ['pipe', 'pipe', 'pipe']
726
+ });
727
+ };
728
+
729
+ // Execute using shell to handle complex commands
730
+ const argv = ['sh', '-c', commandStr];
731
+ const proc = isBun ? spawnBun(argv, currentInput) : spawnNode(argv, currentInput);
732
+
733
+ let result;
734
+ if (isBun) {
735
+ result = {
736
+ code: proc.exitCode || 0,
737
+ stdout: proc.stdout?.toString('utf8') || '',
738
+ stderr: proc.stderr?.toString('utf8') || '',
739
+ stdin: currentInput
740
+ };
741
+ } else {
742
+ result = {
743
+ code: proc.status || 0,
744
+ stdout: proc.stdout || '',
745
+ stderr: proc.stderr || '',
746
+ stdin: currentInput
747
+ };
748
+ }
749
+
750
+ // If command failed and pipefail is set, fail the entire pipeline
751
+ if (globalShellSettings.pipefail && result.code !== 0) {
752
+ const error = new Error(`Pipeline command '${commandStr}' failed with exit code ${result.code}`);
753
+ error.code = result.code;
754
+ error.stdout = result.stdout;
755
+ error.stderr = result.stderr;
756
+ throw error;
757
+ }
758
+
759
+ // If this isn't the last command, pass stdout as stdin to next command
760
+ if (i < commands.length - 1) {
761
+ currentInput = result.stdout;
762
+ // Accumulate stderr from all commands
763
+ if (result.stderr && this.options.capture) {
764
+ this.errChunks = this.errChunks || [];
765
+ this.errChunks.push(Buffer.from(result.stderr));
766
+ }
767
+ } else {
768
+ // This is the last command - emit output and store final result
769
+ currentOutput = result.stdout;
770
+
771
+ // Collect all accumulated stderr
772
+ let allStderr = '';
773
+ if (this.errChunks && this.errChunks.length > 0) {
774
+ allStderr = Buffer.concat(this.errChunks).toString('utf8');
775
+ }
776
+ if (result.stderr) {
777
+ allStderr += result.stderr;
778
+ }
779
+
780
+ // Mirror and emit output for final command
781
+ if (result.stdout) {
782
+ const buf = Buffer.from(result.stdout);
783
+ if (this.options.mirror) {
784
+ process.stdout.write(buf);
785
+ }
786
+ this.emit('stdout', buf);
787
+ this.emit('data', { type: 'stdout', data: buf });
788
+ }
789
+
790
+ if (allStderr) {
791
+ const buf = Buffer.from(allStderr);
792
+ if (this.options.mirror) {
793
+ process.stderr.write(buf);
794
+ }
795
+ this.emit('stderr', buf);
796
+ this.emit('data', { type: 'stderr', data: buf });
797
+ }
798
+
799
+ // Store final result using createResult helper for .text() method compatibility
800
+ const finalResult = createResult({
801
+ code: result.code,
802
+ stdout: currentOutput,
803
+ stderr: allStderr,
804
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
805
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
806
+ });
807
+
808
+ this.result = finalResult;
809
+ this.finished = true;
810
+
811
+ // Emit completion events
812
+ this.emit('end', finalResult);
813
+ this.emit('exit', finalResult.code);
814
+
815
+ // Handle shell settings
816
+ if (globalShellSettings.errexit && finalResult.code !== 0) {
817
+ const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
818
+ error.code = finalResult.code;
819
+ error.stdout = finalResult.stdout;
820
+ error.stderr = finalResult.stderr;
821
+ error.result = finalResult;
822
+ throw error;
823
+ }
824
+
825
+ return finalResult;
826
+ }
827
+
828
+ } catch (error) {
829
+ // Handle errors from system commands in pipeline
830
+ const result = createResult({
831
+ code: error.code ?? 1,
832
+ stdout: currentOutput,
833
+ stderr: error.stderr ?? error.message,
834
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
835
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
836
+ });
837
+
838
+ this.result = result;
839
+ this.finished = true;
840
+
841
+ if (result.stderr) {
842
+ const buf = Buffer.from(result.stderr);
843
+ if (this.options.mirror) {
844
+ process.stderr.write(buf);
845
+ }
846
+ this.emit('stderr', buf);
847
+ this.emit('data', { type: 'stderr', data: buf });
848
+ }
849
+
850
+ this.emit('end', result);
851
+ this.emit('exit', result.code);
852
+
853
+ if (globalShellSettings.errexit) {
854
+ throw error;
855
+ }
856
+
857
+ return result;
679
858
  }
680
- this.emit('stderr', buf);
681
- this.emit('data', { type: 'stderr', data: buf });
682
-
683
- this.emit('end', result);
684
- this.emit('exit', result.code);
685
-
686
- return result;
687
859
  }
688
860
  }
689
861
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "command-stream",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
5
5
  "type": "module",
6
6
  "main": "$.mjs",