command-stream 0.0.1

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 (4) hide show
  1. package/$.mjs +1720 -0
  2. package/LICENSE +24 -0
  3. package/README.md +776 -0
  4. package/package.json +50 -0
package/$.mjs ADDED
@@ -0,0 +1,1720 @@
1
+ // Enhanced $ shell utilities with streaming, async iteration, and EventEmitter support
2
+ // Usage patterns:
3
+ // 1. Classic await: const result = await $`command`
4
+ // 2. Async iteration: for await (const chunk of $`command`.stream()) { ... }
5
+ // 3. EventEmitter: $`command`.on('data', chunk => ...).on('end', result => ...)
6
+ // 4. Stream access: $`command`.stdout, $`command`.stderr
7
+
8
+ const isBun = typeof globalThis.Bun !== 'undefined';
9
+
10
+ // Global shell settings (like bash set -e / set +e)
11
+ let globalShellSettings = {
12
+ errexit: false, // set -e equivalent: exit on error
13
+ verbose: false, // set -v equivalent: print commands
14
+ xtrace: false, // set -x equivalent: trace execution
15
+ pipefail: false, // set -o pipefail equivalent: pipe failure detection
16
+ nounset: false // set -u equivalent: error on undefined variables
17
+ };
18
+
19
+ // Virtual command registry - unified system for all commands
20
+ const virtualCommands = new Map();
21
+
22
+ // Global flag to enable/disable virtual commands (for backward compatibility)
23
+ let virtualCommandsEnabled = true;
24
+
25
+ // EventEmitter-like implementation
26
+ class StreamEmitter {
27
+ constructor() {
28
+ this.listeners = new Map();
29
+ }
30
+
31
+ on(event, listener) {
32
+ if (!this.listeners.has(event)) {
33
+ this.listeners.set(event, []);
34
+ }
35
+ this.listeners.get(event).push(listener);
36
+ return this;
37
+ }
38
+
39
+ emit(event, ...args) {
40
+ const eventListeners = this.listeners.get(event);
41
+ if (eventListeners) {
42
+ for (const listener of eventListeners) {
43
+ listener(...args);
44
+ }
45
+ }
46
+ return this;
47
+ }
48
+
49
+ off(event, listener) {
50
+ const eventListeners = this.listeners.get(event);
51
+ if (eventListeners) {
52
+ const index = eventListeners.indexOf(listener);
53
+ if (index !== -1) {
54
+ eventListeners.splice(index, 1);
55
+ }
56
+ }
57
+ return this;
58
+ }
59
+ }
60
+
61
+ function quote(value) {
62
+ if (value == null) return "''";
63
+ if (Array.isArray(value)) return value.map(quote).join(' ');
64
+ if (typeof value !== 'string') value = String(value);
65
+ if (value === '') return "''";
66
+ return `'${value.replace(/'/g, "'\\''")}'`;
67
+ }
68
+
69
+ function buildShellCommand(strings, values) {
70
+ let out = '';
71
+ for (let i = 0; i < strings.length; i++) {
72
+ out += strings[i];
73
+ if (i < values.length) {
74
+ const v = values[i];
75
+ if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
76
+ out += String(v.raw);
77
+ } else {
78
+ out += quote(v);
79
+ }
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function asBuffer(chunk) {
86
+ if (Buffer.isBuffer(chunk)) return chunk;
87
+ if (typeof chunk === 'string') return Buffer.from(chunk);
88
+ return Buffer.from(chunk);
89
+ }
90
+
91
+ async function pumpReadable(readable, onChunk) {
92
+ if (!readable) return;
93
+ for await (const chunk of readable) {
94
+ await onChunk(asBuffer(chunk));
95
+ }
96
+ }
97
+
98
+ // Enhanced process runner with streaming capabilities
99
+ class ProcessRunner extends StreamEmitter {
100
+ constructor(spec, options = {}) {
101
+ super();
102
+ this.spec = spec;
103
+ this.options = {
104
+ mirror: true,
105
+ capture: true,
106
+ stdin: 'inherit',
107
+ cwd: undefined,
108
+ env: undefined,
109
+ ...options
110
+ };
111
+
112
+ this.outChunks = this.options.capture ? [] : null;
113
+ this.errChunks = this.options.capture ? [] : null;
114
+ this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
115
+ this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
116
+ [Buffer.from(this.options.stdin)] : [];
117
+
118
+ this.result = null;
119
+ this.child = null;
120
+ this.started = false;
121
+ this.finished = false;
122
+
123
+ // Promise for awaiting final result
124
+ this.promise = null;
125
+ }
126
+
127
+ async _start() {
128
+ if (this.started) return;
129
+ this.started = true;
130
+
131
+ const { cwd, env, stdin } = this.options;
132
+
133
+ // Handle programmatic pipeline mode
134
+ if (this.spec.mode === 'pipeline') {
135
+ return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
136
+ }
137
+
138
+ // Check if this is a virtual command first
139
+ if (this.spec.mode === 'shell') {
140
+ // Parse the command to check for virtual commands or pipelines
141
+ const parsed = this._parseCommand(this.spec.command);
142
+ if (parsed) {
143
+ if (parsed.type === 'pipeline') {
144
+ return await this._runPipeline(parsed.commands);
145
+ } else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
146
+ return await this._runVirtual(parsed.cmd, parsed.args);
147
+ }
148
+ }
149
+ }
150
+
151
+ const spawnBun = (argv) => {
152
+ return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
153
+ };
154
+ const spawnNode = async (argv) => {
155
+ const cp = await import('child_process');
156
+ return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
157
+ };
158
+
159
+ const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
160
+
161
+ // Shell tracing (set -x equivalent)
162
+ if (globalShellSettings.xtrace) {
163
+ const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
164
+ console.log(`+ ${traceCmd}`);
165
+ }
166
+
167
+ // Verbose mode (set -v equivalent)
168
+ if (globalShellSettings.verbose) {
169
+ const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
170
+ console.log(verboseCmd);
171
+ }
172
+
173
+ const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
174
+ const preferNodeForInput = isBun && needsExplicitPipe;
175
+ this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
176
+
177
+ // Setup stdout streaming
178
+ const outPump = pumpReadable(this.child.stdout, async (buf) => {
179
+ if (this.options.capture) this.outChunks.push(buf);
180
+ if (this.options.mirror) process.stdout.write(buf);
181
+
182
+ // Emit chunk events
183
+ this.emit('stdout', buf);
184
+ this.emit('data', { type: 'stdout', data: buf });
185
+ });
186
+
187
+ // Setup stderr streaming
188
+ const errPump = pumpReadable(this.child.stderr, async (buf) => {
189
+ if (this.options.capture) this.errChunks.push(buf);
190
+ if (this.options.mirror) process.stderr.write(buf);
191
+
192
+ // Emit chunk events
193
+ this.emit('stderr', buf);
194
+ this.emit('data', { type: 'stderr', data: buf });
195
+ });
196
+
197
+ // Handle stdin
198
+ let stdinPumpPromise = Promise.resolve();
199
+ if (stdin === 'inherit') {
200
+ const isPipedIn = process.stdin && process.stdin.isTTY === false;
201
+ if (isPipedIn) {
202
+ stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
203
+ } else {
204
+ if (this.child.stdin && typeof this.child.stdin.end === 'function') {
205
+ try { this.child.stdin.end(); } catch {}
206
+ } else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
207
+ try { const w = this.child.stdin.getWriter(); await w.close(); } catch {}
208
+ }
209
+ }
210
+ } else if (stdin === 'ignore') {
211
+ if (this.child.stdin && typeof this.child.stdin.end === 'function') this.child.stdin.end();
212
+ } else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
213
+ const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
214
+ if (this.options.capture && this.inChunks) this.inChunks.push(Buffer.from(buf));
215
+ stdinPumpPromise = this._writeToStdin(buf);
216
+ }
217
+
218
+ const exited = isBun ? this.child.exited : new Promise((resolve) => this.child.on('close', resolve));
219
+ const code = await exited;
220
+ await Promise.all([outPump, errPump, stdinPumpPromise]);
221
+
222
+ this.result = {
223
+ code,
224
+ stdout: this.options.capture ? Buffer.concat(this.outChunks).toString('utf8') : undefined,
225
+ stderr: this.options.capture ? Buffer.concat(this.errChunks).toString('utf8') : undefined,
226
+ stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
227
+ child: this.child
228
+ };
229
+
230
+ this.finished = true;
231
+ this.emit('end', this.result);
232
+ this.emit('exit', this.result.code);
233
+
234
+ // Handle shell settings (set -e equivalent)
235
+ if (globalShellSettings.errexit && this.result.code !== 0) {
236
+ const error = new Error(`Command failed with exit code ${this.result.code}`);
237
+ error.code = this.result.code;
238
+ error.stdout = this.result.stdout;
239
+ error.stderr = this.result.stderr;
240
+ error.result = this.result;
241
+ throw error;
242
+ }
243
+
244
+ return this.result;
245
+ }
246
+
247
+ async _pumpStdinTo(child, captureChunks) {
248
+ if (!child.stdin) return;
249
+ const bunWriter = isBun && child.stdin && typeof child.stdin.getWriter === 'function' ? child.stdin.getWriter() : null;
250
+ for await (const chunk of process.stdin) {
251
+ const buf = asBuffer(chunk);
252
+ captureChunks && captureChunks.push(buf);
253
+ if (bunWriter) await bunWriter.write(buf);
254
+ else if (typeof child.stdin.write === 'function') child.stdin.write(buf);
255
+ else if (isBun && typeof Bun.write === 'function') await Bun.write(child.stdin, buf);
256
+ }
257
+ if (bunWriter) await bunWriter.close();
258
+ else if (typeof child.stdin.end === 'function') child.stdin.end();
259
+ }
260
+
261
+ async _writeToStdin(buf) {
262
+ if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
263
+ const w = this.child.stdin.getWriter();
264
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
265
+ await w.write(bytes);
266
+ await w.close();
267
+ } else if (this.child.stdin && typeof this.child.stdin.write === 'function') {
268
+ this.child.stdin.end(buf);
269
+ } else if (isBun && typeof Bun.write === 'function') {
270
+ await Bun.write(this.child.stdin, buf);
271
+ }
272
+ }
273
+
274
+ _parseCommand(command) {
275
+ const trimmed = command.trim();
276
+ if (!trimmed) return null;
277
+
278
+ // Check for pipes
279
+ if (trimmed.includes('|')) {
280
+ return this._parsePipeline(trimmed);
281
+ }
282
+
283
+ // Simple command parsing
284
+ const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
285
+ if (parts.length === 0) return null;
286
+
287
+ const cmd = parts[0];
288
+ const args = parts.slice(1).map(arg => {
289
+ // Remove quotes if present
290
+ if ((arg.startsWith('"') && arg.endsWith('"')) ||
291
+ (arg.startsWith("'") && arg.endsWith("'"))) {
292
+ return arg.slice(1, -1);
293
+ }
294
+ return arg;
295
+ });
296
+
297
+ return { cmd, args, type: 'simple' };
298
+ }
299
+
300
+ _parsePipeline(command) {
301
+ // Split by pipe, respecting quotes
302
+ const segments = [];
303
+ let current = '';
304
+ let inQuotes = false;
305
+ let quoteChar = '';
306
+
307
+ for (let i = 0; i < command.length; i++) {
308
+ const char = command[i];
309
+
310
+ if (!inQuotes && (char === '"' || char === "'")) {
311
+ inQuotes = true;
312
+ quoteChar = char;
313
+ current += char;
314
+ } else if (inQuotes && char === quoteChar) {
315
+ inQuotes = false;
316
+ quoteChar = '';
317
+ current += char;
318
+ } else if (!inQuotes && char === '|') {
319
+ segments.push(current.trim());
320
+ current = '';
321
+ } else {
322
+ current += char;
323
+ }
324
+ }
325
+
326
+ if (current.trim()) {
327
+ segments.push(current.trim());
328
+ }
329
+
330
+ // Parse each segment as a simple command
331
+ const commands = segments.map(segment => {
332
+ const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
333
+ if (parts.length === 0) return null;
334
+
335
+ const cmd = parts[0];
336
+ const args = parts.slice(1).map(arg => {
337
+ if ((arg.startsWith('"') && arg.endsWith('"')) ||
338
+ (arg.startsWith("'") && arg.endsWith("'"))) {
339
+ return arg.slice(1, -1);
340
+ }
341
+ return arg;
342
+ });
343
+
344
+ return { cmd, args };
345
+ }).filter(Boolean);
346
+
347
+ return { type: 'pipeline', commands };
348
+ }
349
+
350
+ async _runVirtual(cmd, args) {
351
+ const handler = virtualCommands.get(cmd);
352
+ if (!handler) {
353
+ throw new Error(`Virtual command not found: ${cmd}`);
354
+ }
355
+
356
+ try {
357
+ // Prepare stdin
358
+ let stdinData = '';
359
+ if (this.options.stdin && typeof this.options.stdin === 'string') {
360
+ stdinData = this.options.stdin;
361
+ } else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
362
+ stdinData = this.options.stdin.toString('utf8');
363
+ }
364
+
365
+ // Shell tracing for virtual commands
366
+ if (globalShellSettings.xtrace) {
367
+ console.log(`+ ${cmd} ${args.join(' ')}`);
368
+ }
369
+ if (globalShellSettings.verbose) {
370
+ console.log(`${cmd} ${args.join(' ')}`);
371
+ }
372
+
373
+ // Execute the virtual command
374
+ let result;
375
+
376
+ // Check if handler is async generator (streaming)
377
+ if (handler.constructor.name === 'AsyncGeneratorFunction') {
378
+ // Handle streaming virtual command
379
+ const chunks = [];
380
+ for await (const chunk of handler(args, stdinData, this.options)) {
381
+ const buf = Buffer.from(chunk);
382
+ chunks.push(buf);
383
+
384
+ if (this.options.mirror) {
385
+ process.stdout.write(buf);
386
+ }
387
+
388
+ this.emit('stdout', buf);
389
+ this.emit('data', { type: 'stdout', data: buf });
390
+ }
391
+
392
+ result = {
393
+ code: 0,
394
+ stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
395
+ stderr: this.options.capture ? '' : undefined,
396
+ stdin: this.options.capture ? stdinData : undefined
397
+ };
398
+ } else {
399
+ // Regular async function
400
+ result = await handler(args, stdinData, this.options);
401
+
402
+ // Ensure result has required fields, respecting capture option
403
+ result = {
404
+ code: result.code ?? 0,
405
+ stdout: this.options.capture ? (result.stdout ?? '') : undefined,
406
+ stderr: this.options.capture ? (result.stderr ?? '') : undefined,
407
+ stdin: this.options.capture ? stdinData : undefined,
408
+ ...result
409
+ };
410
+
411
+ // Mirror and emit output
412
+ if (result.stdout) {
413
+ const buf = Buffer.from(result.stdout);
414
+ if (this.options.mirror) {
415
+ process.stdout.write(buf);
416
+ }
417
+ this.emit('stdout', buf);
418
+ this.emit('data', { type: 'stdout', data: buf });
419
+ }
420
+
421
+ if (result.stderr) {
422
+ const buf = Buffer.from(result.stderr);
423
+ if (this.options.mirror) {
424
+ process.stderr.write(buf);
425
+ }
426
+ this.emit('stderr', buf);
427
+ this.emit('data', { type: 'stderr', data: buf });
428
+ }
429
+ }
430
+
431
+ // Store result
432
+ this.result = result;
433
+ this.finished = true;
434
+
435
+ // Emit completion events
436
+ this.emit('end', result);
437
+ this.emit('exit', result.code);
438
+
439
+ // Handle shell settings
440
+ if (globalShellSettings.errexit && result.code !== 0) {
441
+ const error = new Error(`Command failed with exit code ${result.code}`);
442
+ error.code = result.code;
443
+ error.stdout = result.stdout;
444
+ error.stderr = result.stderr;
445
+ error.result = result;
446
+ throw error;
447
+ }
448
+
449
+ return result;
450
+ } catch (error) {
451
+ // Handle errors from virtual commands
452
+ const result = {
453
+ code: error.code ?? 1,
454
+ stdout: error.stdout ?? '',
455
+ stderr: error.stderr ?? error.message,
456
+ stdin: ''
457
+ };
458
+
459
+ this.result = result;
460
+ this.finished = true;
461
+
462
+ if (result.stderr) {
463
+ const buf = Buffer.from(result.stderr);
464
+ if (this.options.mirror) {
465
+ process.stderr.write(buf);
466
+ }
467
+ this.emit('stderr', buf);
468
+ this.emit('data', { type: 'stderr', data: buf });
469
+ }
470
+
471
+ this.emit('end', result);
472
+ this.emit('exit', result.code);
473
+
474
+ if (globalShellSettings.errexit) {
475
+ throw error;
476
+ }
477
+
478
+ return result;
479
+ }
480
+ }
481
+
482
+ async _runPipeline(commands) {
483
+ if (commands.length === 0) {
484
+ return { code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' };
485
+ }
486
+
487
+ let currentOutput = '';
488
+ let currentInput = '';
489
+
490
+ // Get initial stdin from options
491
+ if (this.options.stdin && typeof this.options.stdin === 'string') {
492
+ currentInput = this.options.stdin;
493
+ } else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
494
+ currentInput = this.options.stdin.toString('utf8');
495
+ }
496
+
497
+ // Execute each command in the pipeline
498
+ for (let i = 0; i < commands.length; i++) {
499
+ const command = commands[i];
500
+ const { cmd, args } = command;
501
+
502
+ // Check if this is a virtual command
503
+ if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
504
+ // Run virtual command with current input
505
+ const handler = virtualCommands.get(cmd);
506
+
507
+ try {
508
+ // Shell tracing for virtual commands
509
+ if (globalShellSettings.xtrace) {
510
+ console.log(`+ ${cmd} ${args.join(' ')}`);
511
+ }
512
+ if (globalShellSettings.verbose) {
513
+ console.log(`${cmd} ${args.join(' ')}`);
514
+ }
515
+
516
+ let result;
517
+
518
+ // Check if handler is async generator (streaming)
519
+ if (handler.constructor.name === 'AsyncGeneratorFunction') {
520
+ const chunks = [];
521
+ for await (const chunk of handler(args, currentInput, this.options)) {
522
+ chunks.push(Buffer.from(chunk));
523
+ }
524
+ result = {
525
+ code: 0,
526
+ stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
527
+ stderr: this.options.capture ? '' : undefined,
528
+ stdin: this.options.capture ? currentInput : undefined
529
+ };
530
+ } else {
531
+ // Regular async function
532
+ result = await handler(args, currentInput, this.options);
533
+ result = {
534
+ code: result.code ?? 0,
535
+ stdout: this.options.capture ? (result.stdout ?? '') : undefined,
536
+ stderr: this.options.capture ? (result.stderr ?? '') : undefined,
537
+ stdin: this.options.capture ? currentInput : undefined,
538
+ ...result
539
+ };
540
+ }
541
+
542
+ // If this isn't the last command, pass stdout as stdin to next command
543
+ if (i < commands.length - 1) {
544
+ currentInput = result.stdout;
545
+ } else {
546
+ // This is the last command - emit output and store final result
547
+ currentOutput = result.stdout;
548
+
549
+ // Mirror and emit output for final command
550
+ if (result.stdout) {
551
+ const buf = Buffer.from(result.stdout);
552
+ if (this.options.mirror) {
553
+ process.stdout.write(buf);
554
+ }
555
+ this.emit('stdout', buf);
556
+ this.emit('data', { type: 'stdout', data: buf });
557
+ }
558
+
559
+ if (result.stderr) {
560
+ const buf = Buffer.from(result.stderr);
561
+ if (this.options.mirror) {
562
+ process.stderr.write(buf);
563
+ }
564
+ this.emit('stderr', buf);
565
+ this.emit('data', { type: 'stderr', data: buf });
566
+ }
567
+
568
+ // Store final result
569
+ const finalResult = {
570
+ code: result.code,
571
+ stdout: currentOutput,
572
+ stderr: result.stderr,
573
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
574
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
575
+ };
576
+
577
+ this.result = finalResult;
578
+ this.finished = true;
579
+
580
+ // Emit completion events
581
+ this.emit('end', finalResult);
582
+ this.emit('exit', finalResult.code);
583
+
584
+ // Handle shell settings
585
+ if (globalShellSettings.errexit && finalResult.code !== 0) {
586
+ const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
587
+ error.code = finalResult.code;
588
+ error.stdout = finalResult.stdout;
589
+ error.stderr = finalResult.stderr;
590
+ error.result = finalResult;
591
+ throw error;
592
+ }
593
+
594
+ return finalResult;
595
+ }
596
+
597
+ // Handle errors from intermediate commands
598
+ if (globalShellSettings.errexit && result.code !== 0) {
599
+ const error = new Error(`Pipeline command failed with exit code ${result.code}`);
600
+ error.code = result.code;
601
+ error.stdout = result.stdout;
602
+ error.stderr = result.stderr;
603
+ error.result = result;
604
+ throw error;
605
+ }
606
+ } catch (error) {
607
+ // Handle errors from virtual commands in pipeline
608
+ const result = {
609
+ code: error.code ?? 1,
610
+ stdout: currentOutput,
611
+ stderr: error.stderr ?? error.message,
612
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
613
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
614
+ };
615
+
616
+ this.result = result;
617
+ this.finished = true;
618
+
619
+ if (result.stderr) {
620
+ const buf = Buffer.from(result.stderr);
621
+ if (this.options.mirror) {
622
+ process.stderr.write(buf);
623
+ }
624
+ this.emit('stderr', buf);
625
+ this.emit('data', { type: 'stderr', data: buf });
626
+ }
627
+
628
+ this.emit('end', result);
629
+ this.emit('exit', result.code);
630
+
631
+ if (globalShellSettings.errexit) {
632
+ throw error;
633
+ }
634
+
635
+ return result;
636
+ }
637
+ } else {
638
+ // For system commands in pipeline, we would need to spawn processes
639
+ // For now, return an error indicating this isn't supported
640
+ const result = {
641
+ code: 1,
642
+ stdout: currentOutput,
643
+ stderr: `Pipeline with system command '${cmd}' not yet supported`,
644
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
645
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
646
+ };
647
+
648
+ this.result = result;
649
+ this.finished = true;
650
+
651
+ const buf = Buffer.from(result.stderr);
652
+ if (this.options.mirror) {
653
+ process.stderr.write(buf);
654
+ }
655
+ this.emit('stderr', buf);
656
+ this.emit('data', { type: 'stderr', data: buf });
657
+
658
+ this.emit('end', result);
659
+ this.emit('exit', result.code);
660
+
661
+ return result;
662
+ }
663
+ }
664
+ }
665
+
666
+ // Run programmatic pipeline (.pipe() method)
667
+ async _runProgrammaticPipeline(source, destination) {
668
+ try {
669
+ // Execute the source command first
670
+ const sourceResult = await source;
671
+
672
+ if (sourceResult.code !== 0) {
673
+ // If source failed, return its result
674
+ return sourceResult;
675
+ }
676
+
677
+ // Set the destination's stdin to the source's stdout
678
+ destination.options = {
679
+ ...destination.options,
680
+ stdin: sourceResult.stdout
681
+ };
682
+
683
+ // Execute the destination command
684
+ const destResult = await destination;
685
+
686
+ // Return the final result with combined information
687
+ return {
688
+ code: destResult.code,
689
+ stdout: destResult.stdout,
690
+ stderr: sourceResult.stderr + destResult.stderr,
691
+ stdin: sourceResult.stdin
692
+ };
693
+
694
+ } catch (error) {
695
+ const result = {
696
+ code: error.code ?? 1,
697
+ stdout: '',
698
+ stderr: error.message || 'Pipeline execution failed',
699
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
700
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
701
+ };
702
+
703
+ this.result = result;
704
+ this.finished = true;
705
+
706
+ const buf = Buffer.from(result.stderr);
707
+ if (this.options.mirror) {
708
+ process.stderr.write(buf);
709
+ }
710
+ this.emit('stderr', buf);
711
+ this.emit('data', { type: 'stderr', data: buf });
712
+
713
+ this.emit('end', result);
714
+ this.emit('exit', result.code);
715
+
716
+ return result;
717
+ }
718
+ }
719
+
720
+ // Async iteration support
721
+ async* stream() {
722
+ if (!this.started) {
723
+ this._start(); // Start but don't await
724
+ }
725
+
726
+ let buffer = [];
727
+ let resolve, reject;
728
+ let ended = false;
729
+
730
+ const onData = (chunk) => {
731
+ buffer.push(chunk);
732
+ if (resolve) {
733
+ resolve();
734
+ resolve = reject = null;
735
+ }
736
+ };
737
+
738
+ const onEnd = () => {
739
+ ended = true;
740
+ if (resolve) {
741
+ resolve();
742
+ resolve = reject = null;
743
+ }
744
+ };
745
+
746
+ this.on('data', onData);
747
+ this.on('end', onEnd);
748
+
749
+ try {
750
+ while (!ended || buffer.length > 0) {
751
+ if (buffer.length > 0) {
752
+ yield buffer.shift();
753
+ } else if (!ended) {
754
+ await new Promise((res, rej) => {
755
+ resolve = res;
756
+ reject = rej;
757
+ });
758
+ }
759
+ }
760
+ } finally {
761
+ this.off('data', onData);
762
+ this.off('end', onEnd);
763
+ }
764
+ }
765
+
766
+ // Programmatic piping support
767
+ pipe(destination) {
768
+ // If destination is a ProcessRunner, create a pipeline
769
+ if (destination instanceof ProcessRunner) {
770
+ // Create a new ProcessRunner that represents the piped operation
771
+ const pipeSpec = {
772
+ mode: 'pipeline',
773
+ source: this,
774
+ destination: destination
775
+ };
776
+
777
+ return new ProcessRunner(pipeSpec, {
778
+ ...this.options,
779
+ capture: destination.options.capture ?? true
780
+ });
781
+ }
782
+
783
+ // If destination is a template literal result (from $`command`), use its spec
784
+ if (destination && destination.spec) {
785
+ const destRunner = new ProcessRunner(destination.spec, destination.options);
786
+ return this.pipe(destRunner);
787
+ }
788
+
789
+ throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
790
+ }
791
+
792
+ // Promise interface (for await)
793
+ then(onFulfilled, onRejected) {
794
+ if (!this.promise) {
795
+ this.promise = this._start();
796
+ }
797
+ return this.promise.then(onFulfilled, onRejected);
798
+ }
799
+
800
+ catch(onRejected) {
801
+ if (!this.promise) {
802
+ this.promise = this._start();
803
+ }
804
+ return this.promise.catch(onRejected);
805
+ }
806
+
807
+ finally(onFinally) {
808
+ if (!this.promise) {
809
+ this.promise = this._start();
810
+ }
811
+ return this.promise.finally(onFinally);
812
+ }
813
+
814
+ // Synchronous execution
815
+ sync() {
816
+ if (this.started) {
817
+ throw new Error('Command already started - cannot run sync after async start');
818
+ }
819
+
820
+ const { cwd, env, stdin } = this.options;
821
+ const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
822
+
823
+ // Shell tracing (set -x equivalent)
824
+ if (globalShellSettings.xtrace) {
825
+ const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
826
+ console.log(`+ ${traceCmd}`);
827
+ }
828
+
829
+ // Verbose mode (set -v equivalent)
830
+ if (globalShellSettings.verbose) {
831
+ const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
832
+ console.log(verboseCmd);
833
+ }
834
+
835
+ let result;
836
+
837
+ if (isBun) {
838
+ // Use Bun's synchronous spawn
839
+ const proc = Bun.spawnSync(argv, {
840
+ cwd,
841
+ env,
842
+ stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
843
+ Buffer.isBuffer(stdin) ? stdin :
844
+ stdin === 'ignore' ? undefined : undefined,
845
+ stdout: 'pipe',
846
+ stderr: 'pipe'
847
+ });
848
+
849
+ result = {
850
+ code: proc.exitCode || 0,
851
+ stdout: proc.stdout?.toString('utf8') || '',
852
+ stderr: proc.stderr?.toString('utf8') || '',
853
+ stdin: typeof stdin === 'string' ? stdin :
854
+ Buffer.isBuffer(stdin) ? stdin.toString('utf8') : '',
855
+ child: proc
856
+ };
857
+ } else {
858
+ // Use Node's synchronous spawn
859
+ const cp = require('child_process');
860
+ const proc = cp.spawnSync(argv[0], argv.slice(1), {
861
+ cwd,
862
+ env,
863
+ input: typeof stdin === 'string' ? stdin :
864
+ Buffer.isBuffer(stdin) ? stdin : undefined,
865
+ encoding: 'utf8',
866
+ stdio: ['pipe', 'pipe', 'pipe']
867
+ });
868
+
869
+ result = {
870
+ code: proc.status || 0,
871
+ stdout: proc.stdout || '',
872
+ stderr: proc.stderr || '',
873
+ stdin: typeof stdin === 'string' ? stdin :
874
+ Buffer.isBuffer(stdin) ? stdin.toString('utf8') : '',
875
+ child: proc
876
+ };
877
+ }
878
+
879
+ // Mirror output if requested (but always capture for result)
880
+ if (this.options.mirror) {
881
+ if (result.stdout) process.stdout.write(result.stdout);
882
+ if (result.stderr) process.stderr.write(result.stderr);
883
+ }
884
+
885
+ // Store chunks for events (batched after completion)
886
+ this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
887
+ this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
888
+
889
+ this.result = result;
890
+ this.finished = true;
891
+
892
+ // Emit batched events after completion
893
+ if (result.stdout) {
894
+ const stdoutBuf = Buffer.from(result.stdout);
895
+ this.emit('stdout', stdoutBuf);
896
+ this.emit('data', { type: 'stdout', data: stdoutBuf });
897
+ }
898
+
899
+ if (result.stderr) {
900
+ const stderrBuf = Buffer.from(result.stderr);
901
+ this.emit('stderr', stderrBuf);
902
+ this.emit('data', { type: 'stderr', data: stderrBuf });
903
+ }
904
+
905
+ this.emit('end', result);
906
+ this.emit('exit', result.code);
907
+
908
+ // Handle shell settings (set -e equivalent)
909
+ if (globalShellSettings.errexit && result.code !== 0) {
910
+ const error = new Error(`Command failed with exit code ${result.code}`);
911
+ error.code = result.code;
912
+ error.stdout = result.stdout;
913
+ error.stderr = result.stderr;
914
+ error.result = result;
915
+ throw error;
916
+ }
917
+
918
+ return result;
919
+ }
920
+
921
+ // Stream properties
922
+ get stdout() {
923
+ return this.child?.stdout;
924
+ }
925
+
926
+ get stderr() {
927
+ return this.child?.stderr;
928
+ }
929
+
930
+ get stdin() {
931
+ return this.child?.stdin;
932
+ }
933
+ }
934
+
935
+ // Public APIs
936
+ async function sh(commandString, options = {}) {
937
+ const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
938
+ return runner._start();
939
+ }
940
+
941
+ async function exec(file, args = [], options = {}) {
942
+ const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
943
+ return runner._start();
944
+ }
945
+
946
+ async function run(commandOrTokens, options = {}) {
947
+ if (typeof commandOrTokens === 'string') {
948
+ return sh(commandOrTokens, { ...options, mirror: false, capture: true });
949
+ }
950
+ const [file, ...args] = commandOrTokens;
951
+ return exec(file, args, { ...options, mirror: false, capture: true });
952
+ }
953
+
954
+ // Enhanced tagged template that returns ProcessRunner
955
+ function $tagged(strings, ...values) {
956
+ const cmd = buildShellCommand(strings, values);
957
+ return new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
958
+ }
959
+
960
+ function create(defaultOptions = {}) {
961
+ const tagged = (strings, ...values) => {
962
+ const cmd = buildShellCommand(strings, values);
963
+ return new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
964
+ };
965
+ return tagged;
966
+ }
967
+
968
+ function raw(value) {
969
+ return { raw: String(value) };
970
+ }
971
+
972
+ // Shell setting control functions (like bash set/unset)
973
+ function set(option) {
974
+ const mapping = {
975
+ 'e': 'errexit', // set -e: exit on error
976
+ 'errexit': 'errexit',
977
+ 'v': 'verbose', // set -v: verbose
978
+ 'verbose': 'verbose',
979
+ 'x': 'xtrace', // set -x: trace execution
980
+ 'xtrace': 'xtrace',
981
+ 'u': 'nounset', // set -u: error on unset vars
982
+ 'nounset': 'nounset',
983
+ 'o pipefail': 'pipefail', // set -o pipefail
984
+ 'pipefail': 'pipefail'
985
+ };
986
+
987
+ if (mapping[option]) {
988
+ globalShellSettings[mapping[option]] = true;
989
+ if (globalShellSettings.verbose) {
990
+ console.log(`+ set -${option}`);
991
+ }
992
+ }
993
+ return globalShellSettings;
994
+ }
995
+
996
+ function unset(option) {
997
+ const mapping = {
998
+ 'e': 'errexit',
999
+ 'errexit': 'errexit',
1000
+ 'v': 'verbose',
1001
+ 'verbose': 'verbose',
1002
+ 'x': 'xtrace',
1003
+ 'xtrace': 'xtrace',
1004
+ 'u': 'nounset',
1005
+ 'nounset': 'nounset',
1006
+ 'o pipefail': 'pipefail',
1007
+ 'pipefail': 'pipefail'
1008
+ };
1009
+
1010
+ if (mapping[option]) {
1011
+ globalShellSettings[mapping[option]] = false;
1012
+ if (globalShellSettings.verbose) {
1013
+ console.log(`+ set +${option}`);
1014
+ }
1015
+ }
1016
+ return globalShellSettings;
1017
+ }
1018
+
1019
+ // Convenience functions for common patterns
1020
+ const shell = {
1021
+ set,
1022
+ unset,
1023
+ settings: () => ({ ...globalShellSettings }),
1024
+
1025
+ // Bash-like shortcuts
1026
+ errexit: (enable = true) => enable ? set('e') : unset('e'),
1027
+ verbose: (enable = true) => enable ? set('v') : unset('v'),
1028
+ xtrace: (enable = true) => enable ? set('x') : unset('x'),
1029
+ pipefail: (enable = true) => enable ? set('o pipefail') : unset('o pipefail'),
1030
+ nounset: (enable = true) => enable ? set('u') : unset('u'),
1031
+ };
1032
+
1033
+ // Virtual command registration API
1034
+ function register(name, handler) {
1035
+ virtualCommands.set(name, handler);
1036
+ return virtualCommands;
1037
+ }
1038
+
1039
+ function unregister(name) {
1040
+ return virtualCommands.delete(name);
1041
+ }
1042
+
1043
+ function listCommands() {
1044
+ return Array.from(virtualCommands.keys());
1045
+ }
1046
+
1047
+ function enableVirtualCommands() {
1048
+ virtualCommandsEnabled = true;
1049
+ return virtualCommandsEnabled;
1050
+ }
1051
+
1052
+ function disableVirtualCommands() {
1053
+ virtualCommandsEnabled = false;
1054
+ return virtualCommandsEnabled;
1055
+ }
1056
+
1057
+ // Built-in commands that match Bun.$ functionality
1058
+ function registerBuiltins() {
1059
+ // cd - change directory
1060
+ register('cd', async (args) => {
1061
+ const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
1062
+ try {
1063
+ process.chdir(target);
1064
+ return { stdout: process.cwd(), code: 0 };
1065
+ } catch (error) {
1066
+ return { stderr: `cd: ${error.message}`, code: 1 };
1067
+ }
1068
+ });
1069
+
1070
+ // pwd - print working directory
1071
+ register('pwd', async (args, stdin, options) => {
1072
+ // If cwd option is provided, return that instead of process.cwd()
1073
+ const dir = options?.cwd || process.cwd();
1074
+ return { stdout: dir, code: 0 };
1075
+ });
1076
+
1077
+ // echo - print arguments
1078
+ register('echo', async (args) => {
1079
+ let output = args.join(' ');
1080
+ if (args.includes('-n')) {
1081
+ // Don't add newline
1082
+ output = args.filter(arg => arg !== '-n').join(' ');
1083
+ } else {
1084
+ output += '\n';
1085
+ }
1086
+ return { stdout: output, code: 0 };
1087
+ });
1088
+
1089
+ // sleep - wait for specified time
1090
+ register('sleep', async (args) => {
1091
+ const seconds = parseFloat(args[0] || 0);
1092
+ if (isNaN(seconds) || seconds < 0) {
1093
+ return { stderr: 'sleep: invalid time interval', code: 1 };
1094
+ }
1095
+ await new Promise(resolve => setTimeout(resolve, seconds * 1000));
1096
+ return { stdout: '', code: 0 };
1097
+ });
1098
+
1099
+ // true - always succeed
1100
+ register('true', async () => {
1101
+ return { stdout: '', code: 0 };
1102
+ });
1103
+
1104
+ // false - always fail
1105
+ register('false', async () => {
1106
+ return { stdout: '', code: 1 };
1107
+ });
1108
+
1109
+ // which - locate command
1110
+ register('which', async (args) => {
1111
+ if (args.length === 0) {
1112
+ return { stderr: 'which: missing operand', code: 1 };
1113
+ }
1114
+
1115
+ const cmd = args[0];
1116
+
1117
+ // Check virtual commands first
1118
+ if (virtualCommands.has(cmd)) {
1119
+ return { stdout: `${cmd}: shell builtin\n`, code: 0 };
1120
+ }
1121
+
1122
+ // Check PATH for system commands
1123
+ const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
1124
+ const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
1125
+
1126
+ for (const path of paths) {
1127
+ for (const ext of extensions) {
1128
+ const fullPath = require('path').join(path, cmd + ext);
1129
+ try {
1130
+ if (require('fs').statSync(fullPath).isFile()) {
1131
+ return { stdout: fullPath, code: 0 };
1132
+ }
1133
+ } catch {}
1134
+ }
1135
+ }
1136
+
1137
+ return { stderr: `which: no ${cmd} in PATH`, code: 1 };
1138
+ });
1139
+
1140
+ // exit - exit with code
1141
+ register('exit', async (args) => {
1142
+ const code = parseInt(args[0] || 0);
1143
+ if (globalShellSettings.errexit || code !== 0) {
1144
+ // For virtual commands, we simulate exit by returning the code
1145
+ return { stdout: '', code };
1146
+ }
1147
+ return { stdout: '', code: 0 };
1148
+ });
1149
+
1150
+ // env - print environment variables
1151
+ register('env', async (args, stdin, options) => {
1152
+ if (args.length === 0) {
1153
+ // Use custom env if provided, otherwise use process.env
1154
+ const env = options?.env || process.env;
1155
+ const output = Object.entries(env)
1156
+ .map(([key, value]) => `${key}=${value}`)
1157
+ .join('\n') + '\n';
1158
+ return { stdout: output, code: 0 };
1159
+ }
1160
+
1161
+ // TODO: Support env VAR=value command syntax
1162
+ return { stderr: 'env: command execution not yet supported', code: 1 };
1163
+ });
1164
+
1165
+ // cat - read and display file contents
1166
+ register('cat', async (args, stdin, options) => {
1167
+ if (args.length === 0) {
1168
+ // Read from stdin if no files specified
1169
+ return { stdout: stdin || '', code: 0 };
1170
+ }
1171
+
1172
+ try {
1173
+ const fs = await import('fs');
1174
+ const path = await import('path');
1175
+ let output = '';
1176
+
1177
+ for (const filename of args) {
1178
+ // Handle special flags
1179
+ if (filename === '-n') continue; // Line numbering (basic support)
1180
+
1181
+ try {
1182
+ // Resolve path relative to cwd if provided
1183
+ const basePath = options?.cwd || process.cwd();
1184
+ const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
1185
+
1186
+ const content = fs.readFileSync(fullPath, 'utf8');
1187
+ output += content;
1188
+ } catch (error) {
1189
+ // Format error message to match bash/sh style
1190
+ const errorMsg = error.code === 'ENOENT' ? 'No such file or directory' : error.message;
1191
+ return {
1192
+ stderr: `cat: ${filename}: ${errorMsg}`,
1193
+ stdout: output,
1194
+ code: 1
1195
+ };
1196
+ }
1197
+ }
1198
+
1199
+ return { stdout: output, code: 0 };
1200
+ } catch (error) {
1201
+ return { stderr: `cat: ${error.message}`, code: 1 };
1202
+ }
1203
+ });
1204
+
1205
+ // ls - list directory contents
1206
+ register('ls', async (args, stdin, options) => {
1207
+ try {
1208
+ const fs = await import('fs');
1209
+ const path = await import('path');
1210
+
1211
+ // Parse flags and paths
1212
+ const flags = args.filter(arg => arg.startsWith('-'));
1213
+ const paths = args.filter(arg => !arg.startsWith('-'));
1214
+ const isLongFormat = flags.includes('-l');
1215
+ const showAll = flags.includes('-a');
1216
+ const showAlmostAll = flags.includes('-A');
1217
+
1218
+ // Default to current directory if no paths specified
1219
+ const targetPaths = paths.length > 0 ? paths : ['.'];
1220
+
1221
+ let output = '';
1222
+
1223
+ for (const targetPath of targetPaths) {
1224
+ // Resolve path relative to cwd if provided
1225
+ const basePath = options?.cwd || process.cwd();
1226
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
1227
+
1228
+ try {
1229
+ const stat = fs.statSync(fullPath);
1230
+
1231
+ if (stat.isFile()) {
1232
+ // Just show the file name if it's a file
1233
+ output += path.basename(targetPath) + '\n';
1234
+ } else if (stat.isDirectory()) {
1235
+ const entries = fs.readdirSync(fullPath);
1236
+
1237
+ // Filter hidden files unless -a or -A is specified
1238
+ let filteredEntries = entries;
1239
+ if (!showAll && !showAlmostAll) {
1240
+ filteredEntries = entries.filter(entry => !entry.startsWith('.'));
1241
+ } else if (showAlmostAll) {
1242
+ filteredEntries = entries.filter(entry => entry !== '.' && entry !== '..');
1243
+ }
1244
+
1245
+ if (isLongFormat) {
1246
+ // Long format: permissions, links, owner, group, size, date, name
1247
+ for (const entry of filteredEntries) {
1248
+ const entryPath = path.join(fullPath, entry);
1249
+ try {
1250
+ const entryStat = fs.statSync(entryPath);
1251
+ const isDir = entryStat.isDirectory();
1252
+ const permissions = isDir ? 'drwxr-xr-x' : '-rw-r--r--';
1253
+ const size = entryStat.size.toString().padStart(8);
1254
+ const date = entryStat.mtime.toISOString().slice(0, 16).replace('T', ' ');
1255
+ output += `${permissions} 1 user group ${size} ${date} ${entry}\n`;
1256
+ } catch {
1257
+ output += `?????????? 1 user group ? ??? ?? ??:?? ${entry}\n`;
1258
+ }
1259
+ }
1260
+ } else {
1261
+ // Simple format: just names
1262
+ output += filteredEntries.join('\n') + (filteredEntries.length > 0 ? '\n' : '');
1263
+ }
1264
+ }
1265
+ } catch (error) {
1266
+ return {
1267
+ stderr: `ls: cannot access '${targetPath}': ${error.message}`,
1268
+ code: 2
1269
+ };
1270
+ }
1271
+ }
1272
+
1273
+ return { stdout: output, code: 0 };
1274
+ } catch (error) {
1275
+ return { stderr: `ls: ${error.message}`, code: 1 };
1276
+ }
1277
+ });
1278
+
1279
+ // mkdir - create directories
1280
+ register('mkdir', async (args, stdin, options) => {
1281
+ if (args.length === 0) {
1282
+ return { stderr: 'mkdir: missing operand', code: 1 };
1283
+ }
1284
+
1285
+ try {
1286
+ const fs = await import('fs');
1287
+ const path = await import('path');
1288
+
1289
+ const flags = args.filter(arg => arg.startsWith('-'));
1290
+ const dirs = args.filter(arg => !arg.startsWith('-'));
1291
+ const recursive = flags.includes('-p');
1292
+
1293
+ for (const dir of dirs) {
1294
+ try {
1295
+ const basePath = options?.cwd || process.cwd();
1296
+ const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
1297
+
1298
+ if (recursive) {
1299
+ fs.mkdirSync(fullPath, { recursive: true });
1300
+ } else {
1301
+ fs.mkdirSync(fullPath);
1302
+ }
1303
+ } catch (error) {
1304
+ return {
1305
+ stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
1306
+ code: 1
1307
+ };
1308
+ }
1309
+ }
1310
+
1311
+ return { stdout: '', code: 0 };
1312
+ } catch (error) {
1313
+ return { stderr: `mkdir: ${error.message}`, code: 1 };
1314
+ }
1315
+ });
1316
+
1317
+ // rm - remove files and directories
1318
+ register('rm', async (args, stdin, options) => {
1319
+ if (args.length === 0) {
1320
+ return { stderr: 'rm: missing operand', code: 1 };
1321
+ }
1322
+
1323
+ try {
1324
+ const fs = await import('fs');
1325
+ const path = await import('path');
1326
+
1327
+ const flags = args.filter(arg => arg.startsWith('-'));
1328
+ const targets = args.filter(arg => !arg.startsWith('-'));
1329
+ const recursive = flags.includes('-r') || flags.includes('-R');
1330
+ const force = flags.includes('-f');
1331
+
1332
+ for (const target of targets) {
1333
+ try {
1334
+ const basePath = options?.cwd || process.cwd();
1335
+ const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
1336
+
1337
+ const stat = fs.statSync(fullPath);
1338
+
1339
+ if (stat.isDirectory()) {
1340
+ if (!recursive) {
1341
+ return {
1342
+ stderr: `rm: cannot remove '${target}': Is a directory`,
1343
+ code: 1
1344
+ };
1345
+ }
1346
+ fs.rmSync(fullPath, { recursive: true, force });
1347
+ } else {
1348
+ fs.unlinkSync(fullPath);
1349
+ }
1350
+ } catch (error) {
1351
+ if (!force) {
1352
+ return {
1353
+ stderr: `rm: cannot remove '${target}': ${error.message}`,
1354
+ code: 1
1355
+ };
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ return { stdout: '', code: 0 };
1361
+ } catch (error) {
1362
+ return { stderr: `rm: ${error.message}`, code: 1 };
1363
+ }
1364
+ });
1365
+
1366
+ // mv - move/rename files and directories
1367
+ register('mv', async (args, stdin, options) => {
1368
+ if (args.length < 2) {
1369
+ return { stderr: 'mv: missing destination file operand', code: 1 };
1370
+ }
1371
+
1372
+ try {
1373
+ const fs = await import('fs');
1374
+ const path = await import('path');
1375
+
1376
+ const basePath = options?.cwd || process.cwd();
1377
+
1378
+ if (args.length === 2) {
1379
+ // Simple rename/move
1380
+ const [source, dest] = args;
1381
+ const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
1382
+ let destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
1383
+
1384
+ try {
1385
+ // Check if destination is an existing directory
1386
+ try {
1387
+ const destStat = fs.statSync(destPath);
1388
+ if (destStat.isDirectory()) {
1389
+ // Move file into the directory
1390
+ const fileName = path.basename(source);
1391
+ destPath = path.join(destPath, fileName);
1392
+ }
1393
+ } catch {
1394
+ // Destination doesn't exist, proceed with direct rename
1395
+ }
1396
+
1397
+ fs.renameSync(sourcePath, destPath);
1398
+ } catch (error) {
1399
+ return {
1400
+ stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
1401
+ code: 1
1402
+ };
1403
+ }
1404
+ } else {
1405
+ // Multiple sources to directory
1406
+ const sources = args.slice(0, -1);
1407
+ const dest = args[args.length - 1];
1408
+ const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
1409
+
1410
+ // Check if destination is a directory
1411
+ try {
1412
+ const destStat = fs.statSync(destPath);
1413
+ if (!destStat.isDirectory()) {
1414
+ return {
1415
+ stderr: `mv: target '${dest}' is not a directory`,
1416
+ code: 1
1417
+ };
1418
+ }
1419
+ } catch {
1420
+ return {
1421
+ stderr: `mv: cannot access '${dest}': No such file or directory`,
1422
+ code: 1
1423
+ };
1424
+ }
1425
+
1426
+ for (const source of sources) {
1427
+ try {
1428
+ const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
1429
+ const fileName = path.basename(source);
1430
+ const newDestPath = path.join(destPath, fileName);
1431
+ fs.renameSync(sourcePath, newDestPath);
1432
+ } catch (error) {
1433
+ return {
1434
+ stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
1435
+ code: 1
1436
+ };
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ return { stdout: '', code: 0 };
1442
+ } catch (error) {
1443
+ return { stderr: `mv: ${error.message}`, code: 1 };
1444
+ }
1445
+ });
1446
+
1447
+ // cp - copy files and directories
1448
+ register('cp', async (args, stdin, options) => {
1449
+ if (args.length < 2) {
1450
+ return { stderr: 'cp: missing destination file operand', code: 1 };
1451
+ }
1452
+
1453
+ try {
1454
+ const fs = await import('fs');
1455
+ const path = await import('path');
1456
+
1457
+ const flags = args.filter(arg => arg.startsWith('-'));
1458
+ const paths = args.filter(arg => !arg.startsWith('-'));
1459
+ const recursive = flags.includes('-r') || flags.includes('-R');
1460
+
1461
+ const basePath = options?.cwd || process.cwd();
1462
+
1463
+ if (paths.length === 2) {
1464
+ // Simple copy
1465
+ const [source, dest] = paths;
1466
+ const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
1467
+ const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
1468
+
1469
+ try {
1470
+ const sourceStat = fs.statSync(sourcePath);
1471
+
1472
+ if (sourceStat.isDirectory()) {
1473
+ if (!recursive) {
1474
+ return {
1475
+ stderr: `cp: -r not specified; omitting directory '${source}'`,
1476
+ code: 1
1477
+ };
1478
+ }
1479
+ fs.cpSync(sourcePath, destPath, { recursive: true });
1480
+ } else {
1481
+ fs.copyFileSync(sourcePath, destPath);
1482
+ }
1483
+ } catch (error) {
1484
+ return {
1485
+ stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
1486
+ code: 1
1487
+ };
1488
+ }
1489
+ } else {
1490
+ // Multiple sources to directory
1491
+ const sources = paths.slice(0, -1);
1492
+ const dest = paths[paths.length - 1];
1493
+ const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
1494
+
1495
+ // Check if destination is a directory
1496
+ try {
1497
+ const destStat = fs.statSync(destPath);
1498
+ if (!destStat.isDirectory()) {
1499
+ return {
1500
+ stderr: `cp: target '${dest}' is not a directory`,
1501
+ code: 1
1502
+ };
1503
+ }
1504
+ } catch {
1505
+ return {
1506
+ stderr: `cp: cannot access '${dest}': No such file or directory`,
1507
+ code: 1
1508
+ };
1509
+ }
1510
+
1511
+ for (const source of sources) {
1512
+ try {
1513
+ const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
1514
+ const fileName = path.basename(source);
1515
+ const newDestPath = path.join(destPath, fileName);
1516
+
1517
+ const sourceStat = fs.statSync(sourcePath);
1518
+ if (sourceStat.isDirectory()) {
1519
+ if (!recursive) {
1520
+ return {
1521
+ stderr: `cp: -r not specified; omitting directory '${source}'`,
1522
+ code: 1
1523
+ };
1524
+ }
1525
+ fs.cpSync(sourcePath, newDestPath, { recursive: true });
1526
+ } else {
1527
+ fs.copyFileSync(sourcePath, newDestPath);
1528
+ }
1529
+ } catch (error) {
1530
+ return {
1531
+ stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
1532
+ code: 1
1533
+ };
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ return { stdout: '', code: 0 };
1539
+ } catch (error) {
1540
+ return { stderr: `cp: ${error.message}`, code: 1 };
1541
+ }
1542
+ });
1543
+
1544
+ // touch - create or update file timestamps
1545
+ register('touch', async (args, stdin, options) => {
1546
+ if (args.length === 0) {
1547
+ return { stderr: 'touch: missing file operand', code: 1 };
1548
+ }
1549
+
1550
+ try {
1551
+ const fs = await import('fs');
1552
+ const path = await import('path');
1553
+
1554
+ const basePath = options?.cwd || process.cwd();
1555
+
1556
+ for (const file of args) {
1557
+ try {
1558
+ const fullPath = path.isAbsolute(file) ? file : path.join(basePath, file);
1559
+
1560
+ // Try to update timestamps if file exists
1561
+ try {
1562
+ const now = new Date();
1563
+ fs.utimesSync(fullPath, now, now);
1564
+ } catch {
1565
+ // File doesn't exist, create it
1566
+ fs.writeFileSync(fullPath, '', { flag: 'w' });
1567
+ }
1568
+ } catch (error) {
1569
+ return {
1570
+ stderr: `touch: cannot touch '${file}': ${error.message}`,
1571
+ code: 1
1572
+ };
1573
+ }
1574
+ }
1575
+
1576
+ return { stdout: '', code: 0 };
1577
+ } catch (error) {
1578
+ return { stderr: `touch: ${error.message}`, code: 1 };
1579
+ }
1580
+ });
1581
+
1582
+ // basename - extract filename from path
1583
+ register('basename', async (args) => {
1584
+ if (args.length === 0) {
1585
+ return { stderr: 'basename: missing operand', code: 1 };
1586
+ }
1587
+
1588
+ try {
1589
+ const path = await import('path');
1590
+
1591
+ const pathname = args[0];
1592
+ const suffix = args[1];
1593
+
1594
+ let result = path.basename(pathname);
1595
+
1596
+ // Remove suffix if provided
1597
+ if (suffix && result.endsWith(suffix)) {
1598
+ result = result.slice(0, -suffix.length);
1599
+ }
1600
+
1601
+ return { stdout: result + '\n', code: 0 };
1602
+ } catch (error) {
1603
+ return { stderr: `basename: ${error.message}`, code: 1 };
1604
+ }
1605
+ });
1606
+
1607
+ // dirname - extract directory from path
1608
+ register('dirname', async (args) => {
1609
+ if (args.length === 0) {
1610
+ return { stderr: 'dirname: missing operand', code: 1 };
1611
+ }
1612
+
1613
+ try {
1614
+ const path = await import('path');
1615
+
1616
+ const pathname = args[0];
1617
+ const result = path.dirname(pathname);
1618
+
1619
+ return { stdout: result + '\n', code: 0 };
1620
+ } catch (error) {
1621
+ return { stderr: `dirname: ${error.message}`, code: 1 };
1622
+ }
1623
+ });
1624
+
1625
+ // yes - output a string repeatedly
1626
+ register('yes', async function* (args) {
1627
+ const output = args.length > 0 ? args.join(' ') : 'y';
1628
+
1629
+ // Generate infinite stream of the output
1630
+ while (true) {
1631
+ yield output + '\n';
1632
+ // Small delay to prevent overwhelming the system
1633
+ await new Promise(resolve => setTimeout(resolve, 0));
1634
+ }
1635
+ });
1636
+
1637
+ // seq - generate sequence of numbers
1638
+ register('seq', async (args) => {
1639
+ if (args.length === 0) {
1640
+ return { stderr: 'seq: missing operand', code: 1 };
1641
+ }
1642
+
1643
+ try {
1644
+ let start, step, end;
1645
+
1646
+ if (args.length === 1) {
1647
+ start = 1;
1648
+ step = 1;
1649
+ end = parseInt(args[0]);
1650
+ } else if (args.length === 2) {
1651
+ start = parseInt(args[0]);
1652
+ step = 1;
1653
+ end = parseInt(args[1]);
1654
+ } else if (args.length === 3) {
1655
+ start = parseInt(args[0]);
1656
+ step = parseInt(args[1]);
1657
+ end = parseInt(args[2]);
1658
+ } else {
1659
+ return { stderr: 'seq: too many operands', code: 1 };
1660
+ }
1661
+
1662
+ if (isNaN(start) || isNaN(step) || isNaN(end)) {
1663
+ return { stderr: 'seq: invalid number', code: 1 };
1664
+ }
1665
+
1666
+ let output = '';
1667
+ if (step > 0) {
1668
+ for (let i = start; i <= end; i += step) {
1669
+ output += i + '\n';
1670
+ }
1671
+ } else if (step < 0) {
1672
+ for (let i = start; i >= end; i += step) {
1673
+ output += i + '\n';
1674
+ }
1675
+ } else {
1676
+ return { stderr: 'seq: invalid increment', code: 1 };
1677
+ }
1678
+
1679
+ return { stdout: output, code: 0 };
1680
+ } catch (error) {
1681
+ return { stderr: `seq: ${error.message}`, code: 1 };
1682
+ }
1683
+ });
1684
+
1685
+ // test - test file conditions (basic implementation)
1686
+ register('test', async (args) => {
1687
+ if (args.length === 0) {
1688
+ return { stdout: '', code: 1 };
1689
+ }
1690
+
1691
+ // Very basic test implementation
1692
+ const arg = args[0];
1693
+
1694
+ try {
1695
+ if (arg === '-d' && args[1]) {
1696
+ // Test if directory
1697
+ const stat = require('fs').statSync(args[1]);
1698
+ return { stdout: '', code: stat.isDirectory() ? 0 : 1 };
1699
+ } else if (arg === '-f' && args[1]) {
1700
+ // Test if file
1701
+ const stat = require('fs').statSync(args[1]);
1702
+ return { stdout: '', code: stat.isFile() ? 0 : 1 };
1703
+ } else if (arg === '-e' && args[1]) {
1704
+ // Test if exists
1705
+ require('fs').statSync(args[1]);
1706
+ return { stdout: '', code: 0 };
1707
+ }
1708
+ } catch {
1709
+ return { stdout: '', code: 1 };
1710
+ }
1711
+
1712
+ return { stdout: '', code: 1 };
1713
+ });
1714
+ }
1715
+
1716
+ // Initialize built-in commands
1717
+ registerBuiltins();
1718
+
1719
+ export { $tagged as $, sh, exec, run, quote, create, raw, ProcessRunner, shell, set, unset, register, unregister, listCommands, enableVirtualCommands, disableVirtualCommands };
1720
+ export default $tagged;