command-stream 0.0.3 → 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 +213 -37
  2. package/package.json +1 -1
package/$.mjs CHANGED
@@ -5,6 +5,9 @@
5
5
  // 3. EventEmitter: $`command`.on('data', chunk => ...).on('end', result => ...)
6
6
  // 4. Stream access: $`command`.stdout, $`command`.stderr
7
7
 
8
+ import { createRequire } from 'module';
9
+ import { fileURLToPath } from 'url';
10
+
8
11
  const isBun = typeof globalThis.Bun !== 'undefined';
9
12
 
10
13
  // Global shell settings (like bash set -e / set +e)
@@ -308,12 +311,12 @@ class ProcessRunner extends StreamEmitter {
308
311
 
309
312
  const cmd = parts[0];
310
313
  const args = parts.slice(1).map(arg => {
311
- // Remove quotes if present
314
+ // Keep track of whether the arg was quoted
312
315
  if ((arg.startsWith('"') && arg.endsWith('"')) ||
313
316
  (arg.startsWith("'") && arg.endsWith("'"))) {
314
- return arg.slice(1, -1);
317
+ return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
315
318
  }
316
- return arg;
319
+ return { value: arg, quoted: false };
317
320
  });
318
321
 
319
322
  return { cmd, args, type: 'simple' };
@@ -356,11 +359,13 @@ class ProcessRunner extends StreamEmitter {
356
359
 
357
360
  const cmd = parts[0];
358
361
  const args = parts.slice(1).map(arg => {
362
+ // Keep track of whether the arg was quoted
359
363
  if ((arg.startsWith('"') && arg.endsWith('"')) ||
360
364
  (arg.startsWith("'") && arg.endsWith("'"))) {
361
- 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] };
362
367
  }
363
- return arg;
368
+ return { value: arg, quoted: false };
364
369
  });
365
370
 
366
371
  return { cmd, args };
@@ -384,12 +389,15 @@ class ProcessRunner extends StreamEmitter {
384
389
  stdinData = this.options.stdin.toString('utf8');
385
390
  }
386
391
 
392
+ // Extract actual values for virtual command
393
+ const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
394
+
387
395
  // Shell tracing for virtual commands
388
396
  if (globalShellSettings.xtrace) {
389
- console.log(`+ ${cmd} ${args.join(' ')}`);
397
+ console.log(`+ ${cmd} ${argValues.join(' ')}`);
390
398
  }
391
399
  if (globalShellSettings.verbose) {
392
- console.log(`${cmd} ${args.join(' ')}`);
400
+ console.log(`${cmd} ${argValues.join(' ')}`);
393
401
  }
394
402
 
395
403
  // Execute the virtual command
@@ -399,7 +407,7 @@ class ProcessRunner extends StreamEmitter {
399
407
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
400
408
  // Handle streaming virtual command
401
409
  const chunks = [];
402
- for await (const chunk of handler(args, stdinData, this.options)) {
410
+ for await (const chunk of handler(argValues, stdinData, this.options)) {
403
411
  const buf = Buffer.from(chunk);
404
412
  chunks.push(buf);
405
413
 
@@ -419,7 +427,7 @@ class ProcessRunner extends StreamEmitter {
419
427
  };
420
428
  } else {
421
429
  // Regular async function
422
- result = await handler(args, stdinData, this.options);
430
+ result = await handler(argValues, stdinData, this.options);
423
431
 
424
432
  // Ensure result has required fields, respecting capture option
425
433
  result = {
@@ -521,18 +529,21 @@ class ProcessRunner extends StreamEmitter {
521
529
  const command = commands[i];
522
530
  const { cmd, args } = command;
523
531
 
524
- // Check if this is a virtual command
532
+ // Check if this is a virtual command (only if virtual commands are enabled)
525
533
  if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
526
534
  // Run virtual command with current input
527
535
  const handler = virtualCommands.get(cmd);
528
536
 
529
537
  try {
538
+ // Extract actual values for virtual command
539
+ const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
540
+
530
541
  // Shell tracing for virtual commands
531
542
  if (globalShellSettings.xtrace) {
532
- console.log(`+ ${cmd} ${args.join(' ')}`);
543
+ console.log(`+ ${cmd} ${argValues.join(' ')}`);
533
544
  }
534
545
  if (globalShellSettings.verbose) {
535
- console.log(`${cmd} ${args.join(' ')}`);
546
+ console.log(`${cmd} ${argValues.join(' ')}`);
536
547
  }
537
548
 
538
549
  let result;
@@ -540,7 +551,7 @@ class ProcessRunner extends StreamEmitter {
540
551
  // Check if handler is async generator (streaming)
541
552
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
542
553
  const chunks = [];
543
- for await (const chunk of handler(args, currentInput, this.options)) {
554
+ for await (const chunk of handler(argValues, currentInput, this.options)) {
544
555
  chunks.push(Buffer.from(chunk));
545
556
  }
546
557
  result = {
@@ -551,7 +562,7 @@ class ProcessRunner extends StreamEmitter {
551
562
  };
552
563
  } else {
553
564
  // Regular async function
554
- result = await handler(args, currentInput, this.options);
565
+ result = await handler(argValues, currentInput, this.options);
555
566
  result = {
556
567
  code: result.code ?? 0,
557
568
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
@@ -657,30 +668,194 @@ class ProcessRunner extends StreamEmitter {
657
668
  return result;
658
669
  }
659
670
  } else {
660
- // For system commands in pipeline, we would need to spawn processes
661
- // For now, return an error indicating this isn't supported
662
- const result = createResult({
663
- code: 1,
664
- stdout: currentOutput,
665
- stderr: `Pipeline with system command '${cmd}' not yet supported`,
666
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
667
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
668
- });
669
-
670
- this.result = result;
671
- this.finished = true;
672
-
673
- const buf = Buffer.from(result.stderr);
674
- if (this.options.mirror) {
675
- 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;
676
858
  }
677
- this.emit('stderr', buf);
678
- this.emit('data', { type: 'stderr', data: buf });
679
-
680
- this.emit('end', result);
681
- this.emit('exit', result.code);
682
-
683
- return result;
684
859
  }
685
860
  }
686
861
  }
@@ -878,6 +1053,7 @@ class ProcessRunner extends StreamEmitter {
878
1053
  result.child = proc;
879
1054
  } else {
880
1055
  // Use Node's synchronous spawn
1056
+ const require = createRequire(import.meta.url);
881
1057
  const cp = require('child_process');
882
1058
  const proc = cp.spawnSync(argv[0], argv.slice(1), {
883
1059
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "command-stream",
3
- "version": "0.0.3",
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",