command-stream 0.9.0 → 0.9.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.
Files changed (39) hide show
  1. package/js/src/$.ansi.mjs +147 -0
  2. package/js/src/$.mjs +49 -6382
  3. package/js/src/$.process-runner-base.mjs +563 -0
  4. package/js/src/$.process-runner-execution.mjs +1497 -0
  5. package/js/src/$.process-runner-orchestration.mjs +250 -0
  6. package/js/src/$.process-runner-pipeline.mjs +1162 -0
  7. package/js/src/$.process-runner-stream-kill.mjs +312 -0
  8. package/js/src/$.process-runner-virtual.mjs +297 -0
  9. package/js/src/$.quote.mjs +161 -0
  10. package/js/src/$.result.mjs +23 -0
  11. package/js/src/$.shell-settings.mjs +84 -0
  12. package/js/src/$.shell.mjs +157 -0
  13. package/js/src/$.state.mjs +401 -0
  14. package/js/src/$.stream-emitter.mjs +111 -0
  15. package/js/src/$.stream-utils.mjs +390 -0
  16. package/js/src/$.trace.mjs +36 -0
  17. package/js/src/$.utils.mjs +2 -23
  18. package/js/src/$.virtual-commands.mjs +113 -0
  19. package/js/src/commands/$.which.mjs +3 -1
  20. package/js/src/commands/index.mjs +24 -0
  21. package/js/src/shell-parser.mjs +125 -83
  22. package/js/tests/resource-cleanup-internals.test.mjs +22 -24
  23. package/js/tests/sigint-cleanup.test.mjs +3 -0
  24. package/package.json +1 -1
  25. package/rust/src/ansi.rs +194 -0
  26. package/rust/src/events.rs +305 -0
  27. package/rust/src/lib.rs +71 -60
  28. package/rust/src/macros.rs +165 -0
  29. package/rust/src/pipeline.rs +411 -0
  30. package/rust/src/quote.rs +161 -0
  31. package/rust/src/state.rs +333 -0
  32. package/rust/src/stream.rs +369 -0
  33. package/rust/src/trace.rs +152 -0
  34. package/rust/src/utils.rs +53 -158
  35. package/rust/tests/events.rs +207 -0
  36. package/rust/tests/macros.rs +77 -0
  37. package/rust/tests/pipeline.rs +93 -0
  38. package/rust/tests/state.rs +207 -0
  39. package/rust/tests/stream.rs +102 -0
@@ -0,0 +1,563 @@
1
+ // ProcessRunner base class - core constructor, properties, and lifecycle methods
2
+ // Part of the modular ProcessRunner architecture
3
+
4
+ import { trace } from './$.trace.mjs';
5
+ import {
6
+ activeProcessRunners,
7
+ virtualCommands,
8
+ installSignalHandlers,
9
+ monitorParentStreams,
10
+ uninstallSignalHandlers,
11
+ } from './$.state.mjs';
12
+ import { StreamEmitter } from './$.stream-emitter.mjs';
13
+ import { processOutput } from './$.ansi.mjs';
14
+
15
+ const isBun = typeof globalThis.Bun !== 'undefined';
16
+
17
+ /**
18
+ * Wait for child stream to become available
19
+ * @param {object} self - ProcessRunner instance
20
+ * @param {string} streamName - Name of stream (stdin, stdout, stderr)
21
+ * @returns {Promise<object|null>}
22
+ */
23
+ function waitForChildStream(self, streamName) {
24
+ return new Promise((resolve) => {
25
+ const checkForChild = () => {
26
+ if (self.child && self.child[streamName]) {
27
+ resolve(self.child[streamName]);
28
+ } else if (self.finished || self._virtualGenerator) {
29
+ resolve(null);
30
+ } else {
31
+ setImmediate(checkForChild);
32
+ }
33
+ };
34
+ setImmediate(checkForChild);
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Check if command is a virtual command
40
+ * @param {object} self - ProcessRunner instance
41
+ * @returns {boolean}
42
+ */
43
+ function isVirtualCommand(self) {
44
+ return (
45
+ self._virtualGenerator ||
46
+ (self.spec &&
47
+ self.spec.command &&
48
+ virtualCommands.has(self.spec.command.split(' ')[0]))
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Get stream from child or wait for it
54
+ * @param {object} self - ProcessRunner instance
55
+ * @param {string} streamName - Name of stream
56
+ * @param {boolean} checkVirtual - Whether to check for virtual commands
57
+ * @returns {object|Promise|null}
58
+ */
59
+ function getOrWaitForStream(self, streamName, checkVirtual = true) {
60
+ self._autoStartIfNeeded(`streams.${streamName} access`);
61
+
62
+ if (self.child && self.child[streamName]) {
63
+ return self.child[streamName];
64
+ }
65
+ if (self.finished) {
66
+ return null;
67
+ }
68
+ if (checkVirtual && isVirtualCommand(self)) {
69
+ return null;
70
+ }
71
+ if (!self.started) {
72
+ self._startAsync();
73
+ return waitForChildStream(self, streamName);
74
+ }
75
+ if (self.promise && !self.child) {
76
+ return waitForChildStream(self, streamName);
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Get stdin stream with special handling for pipe mode
83
+ * @param {object} self - ProcessRunner instance
84
+ * @returns {object|Promise|null}
85
+ */
86
+ function getStdinStream(self) {
87
+ self._autoStartIfNeeded('streams.stdin access');
88
+
89
+ if (self.child && self.child.stdin) {
90
+ return self.child.stdin;
91
+ }
92
+ if (self.finished) {
93
+ return null;
94
+ }
95
+
96
+ const isVirtual = isVirtualCommand(self);
97
+ const willFallbackToReal = isVirtual && self.options.stdin === 'pipe';
98
+
99
+ if (isVirtual && !willFallbackToReal) {
100
+ return null;
101
+ }
102
+ if (!self.started) {
103
+ self._startAsync();
104
+ return waitForChildStream(self, 'stdin');
105
+ }
106
+ if (self.promise && !self.child) {
107
+ return waitForChildStream(self, 'stdin');
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Cleanup abort controller
114
+ * @param {object} runner - ProcessRunner instance
115
+ */
116
+ function cleanupAbortController(runner) {
117
+ if (!runner._abortController) {
118
+ return;
119
+ }
120
+ trace('ProcessRunner', () => 'Cleaning up abort controller');
121
+ try {
122
+ runner._abortController.abort();
123
+ } catch (e) {
124
+ trace('ProcessRunner', () => `Error aborting controller: ${e.message}`);
125
+ }
126
+ runner._abortController = null;
127
+ }
128
+
129
+ /**
130
+ * Cleanup child process reference
131
+ * @param {object} runner - ProcessRunner instance
132
+ */
133
+ function cleanupChildProcess(runner) {
134
+ if (!runner.child) {
135
+ return;
136
+ }
137
+ trace('ProcessRunner', () => `Cleaning up child process ${runner.child.pid}`);
138
+ try {
139
+ runner.child.removeAllListeners?.();
140
+ } catch (e) {
141
+ trace('ProcessRunner', () => `Error removing listeners: ${e.message}`);
142
+ }
143
+ runner.child = null;
144
+ }
145
+
146
+ /**
147
+ * Cleanup virtual generator
148
+ * @param {object} runner - ProcessRunner instance
149
+ */
150
+ function cleanupGenerator(runner) {
151
+ if (!runner._virtualGenerator) {
152
+ return;
153
+ }
154
+ trace('ProcessRunner', () => 'Cleaning up virtual generator');
155
+ try {
156
+ if (runner._virtualGenerator.return) {
157
+ runner._virtualGenerator.return();
158
+ }
159
+ } catch (e) {
160
+ trace('ProcessRunner', () => `Error closing generator: ${e.message}`);
161
+ }
162
+ runner._virtualGenerator = null;
163
+ }
164
+
165
+ /**
166
+ * Cleanup pipeline components
167
+ * @param {object} runner - ProcessRunner instance
168
+ */
169
+ function cleanupPipeline(runner) {
170
+ if (runner.spec?.mode !== 'pipeline') {
171
+ return;
172
+ }
173
+ trace('ProcessRunner', () => 'Cleaning up pipeline components');
174
+ if (runner.spec.source && typeof runner.spec.source._cleanup === 'function') {
175
+ runner.spec.source._cleanup();
176
+ }
177
+ if (
178
+ runner.spec.destination &&
179
+ typeof runner.spec.destination._cleanup === 'function'
180
+ ) {
181
+ runner.spec.destination._cleanup();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * ProcessRunner - Enhanced process runner with streaming capabilities
187
+ * Extends StreamEmitter for event-based output handling
188
+ */
189
+ class ProcessRunner extends StreamEmitter {
190
+ constructor(spec, options = {}) {
191
+ super();
192
+
193
+ trace(
194
+ 'ProcessRunner',
195
+ () =>
196
+ `constructor ENTER | ${JSON.stringify(
197
+ {
198
+ spec:
199
+ typeof spec === 'object'
200
+ ? { ...spec, command: spec.command?.slice(0, 100) }
201
+ : spec,
202
+ options,
203
+ },
204
+ null,
205
+ 2
206
+ )}`
207
+ );
208
+
209
+ this.spec = spec;
210
+ this.options = {
211
+ mirror: true,
212
+ capture: true,
213
+ stdin: 'inherit',
214
+ cwd: undefined,
215
+ env: undefined,
216
+ interactive: false,
217
+ shellOperators: true,
218
+ ...options,
219
+ };
220
+
221
+ this.outChunks = this.options.capture ? [] : null;
222
+ this.errChunks = this.options.capture ? [] : null;
223
+ this.inChunks =
224
+ this.options.capture && this.options.stdin === 'inherit'
225
+ ? []
226
+ : this.options.capture &&
227
+ (typeof this.options.stdin === 'string' ||
228
+ Buffer.isBuffer(this.options.stdin))
229
+ ? [Buffer.from(this.options.stdin)]
230
+ : [];
231
+
232
+ this.result = null;
233
+ this.child = null;
234
+ this.started = false;
235
+ this.finished = false;
236
+
237
+ this.promise = null;
238
+ this._mode = null;
239
+
240
+ this._cancelled = false;
241
+ this._cancellationSignal = null;
242
+ this._virtualGenerator = null;
243
+ this._abortController = new AbortController();
244
+
245
+ activeProcessRunners.add(this);
246
+ monitorParentStreams();
247
+
248
+ trace(
249
+ 'ProcessRunner',
250
+ () =>
251
+ `Added to activeProcessRunners | ${JSON.stringify(
252
+ {
253
+ command: this.spec?.command || 'unknown',
254
+ totalActive: activeProcessRunners.size,
255
+ },
256
+ null,
257
+ 2
258
+ )}`
259
+ );
260
+ installSignalHandlers();
261
+
262
+ this.finished = false;
263
+ }
264
+
265
+ // Stream property getters
266
+ get stdout() {
267
+ trace(
268
+ 'ProcessRunner',
269
+ () =>
270
+ `stdout getter accessed | ${JSON.stringify(
271
+ {
272
+ hasChild: !!this.child,
273
+ hasStdout: !!(this.child && this.child.stdout),
274
+ },
275
+ null,
276
+ 2
277
+ )}`
278
+ );
279
+ return this.child ? this.child.stdout : null;
280
+ }
281
+
282
+ get stderr() {
283
+ trace(
284
+ 'ProcessRunner',
285
+ () =>
286
+ `stderr getter accessed | ${JSON.stringify(
287
+ {
288
+ hasChild: !!this.child,
289
+ hasStderr: !!(this.child && this.child.stderr),
290
+ },
291
+ null,
292
+ 2
293
+ )}`
294
+ );
295
+ return this.child ? this.child.stderr : null;
296
+ }
297
+
298
+ get stdin() {
299
+ trace(
300
+ 'ProcessRunner',
301
+ () =>
302
+ `stdin getter accessed | ${JSON.stringify(
303
+ {
304
+ hasChild: !!this.child,
305
+ hasStdin: !!(this.child && this.child.stdin),
306
+ },
307
+ null,
308
+ 2
309
+ )}`
310
+ );
311
+ return this.child ? this.child.stdin : null;
312
+ }
313
+
314
+ _autoStartIfNeeded(reason) {
315
+ if (!this.started && !this.finished) {
316
+ trace('ProcessRunner', () => `Auto-starting process due to ${reason}`);
317
+ this.start({
318
+ mode: 'async',
319
+ stdin: 'pipe',
320
+ stdout: 'pipe',
321
+ stderr: 'pipe',
322
+ });
323
+ }
324
+ }
325
+
326
+ get streams() {
327
+ const self = this;
328
+ return {
329
+ get stdin() {
330
+ trace('ProcessRunner.streams', () => `stdin access`);
331
+ return getStdinStream(self);
332
+ },
333
+ get stdout() {
334
+ trace('ProcessRunner.streams', () => `stdout access`);
335
+ return getOrWaitForStream(self, 'stdout');
336
+ },
337
+ get stderr() {
338
+ trace('ProcessRunner.streams', () => `stderr access`);
339
+ return getOrWaitForStream(self, 'stderr');
340
+ },
341
+ };
342
+ }
343
+
344
+ get buffers() {
345
+ const self = this;
346
+ return {
347
+ get stdin() {
348
+ self._autoStartIfNeeded('buffers.stdin access');
349
+ if (self.finished && self.result) {
350
+ return Buffer.from(self.result.stdin || '', 'utf8');
351
+ }
352
+ return self.then
353
+ ? self.then((result) => Buffer.from(result.stdin || '', 'utf8'))
354
+ : Promise.resolve(Buffer.alloc(0));
355
+ },
356
+ get stdout() {
357
+ self._autoStartIfNeeded('buffers.stdout access');
358
+ if (self.finished && self.result) {
359
+ return Buffer.from(self.result.stdout || '', 'utf8');
360
+ }
361
+ return self.then
362
+ ? self.then((result) => Buffer.from(result.stdout || '', 'utf8'))
363
+ : Promise.resolve(Buffer.alloc(0));
364
+ },
365
+ get stderr() {
366
+ self._autoStartIfNeeded('buffers.stderr access');
367
+ if (self.finished && self.result) {
368
+ return Buffer.from(self.result.stderr || '', 'utf8');
369
+ }
370
+ return self.then
371
+ ? self.then((result) => Buffer.from(result.stderr || '', 'utf8'))
372
+ : Promise.resolve(Buffer.alloc(0));
373
+ },
374
+ };
375
+ }
376
+
377
+ get strings() {
378
+ const self = this;
379
+ return {
380
+ get stdin() {
381
+ self._autoStartIfNeeded('strings.stdin access');
382
+ if (self.finished && self.result) {
383
+ return self.result.stdin || '';
384
+ }
385
+ return self.then
386
+ ? self.then((result) => result.stdin || '')
387
+ : Promise.resolve('');
388
+ },
389
+ get stdout() {
390
+ self._autoStartIfNeeded('strings.stdout access');
391
+ if (self.finished && self.result) {
392
+ return self.result.stdout || '';
393
+ }
394
+ return self.then
395
+ ? self.then((result) => result.stdout || '')
396
+ : Promise.resolve('');
397
+ },
398
+ get stderr() {
399
+ self._autoStartIfNeeded('strings.stderr access');
400
+ if (self.finished && self.result) {
401
+ return self.result.stderr || '';
402
+ }
403
+ return self.then
404
+ ? self.then((result) => result.stderr || '')
405
+ : Promise.resolve('');
406
+ },
407
+ };
408
+ }
409
+
410
+ // Centralized method to properly finish a process with correct event emission order
411
+ finish(result) {
412
+ trace(
413
+ 'ProcessRunner',
414
+ () =>
415
+ `finish() called | ${JSON.stringify(
416
+ {
417
+ alreadyFinished: this.finished,
418
+ resultCode: result?.code,
419
+ hasStdout: !!result?.stdout,
420
+ hasStderr: !!result?.stderr,
421
+ command: this.spec?.command?.slice(0, 50),
422
+ },
423
+ null,
424
+ 2
425
+ )}`
426
+ );
427
+
428
+ if (this.finished) {
429
+ trace(
430
+ 'ProcessRunner',
431
+ () => `Already finished, returning existing result`
432
+ );
433
+ return this.result || result;
434
+ }
435
+
436
+ this.result = result;
437
+ trace('ProcessRunner', () => `Result stored, about to emit events`);
438
+
439
+ this.emit('end', result);
440
+ trace('ProcessRunner', () => `'end' event emitted`);
441
+ this.emit('exit', result.code);
442
+ trace(
443
+ 'ProcessRunner',
444
+ () => `'exit' event emitted with code ${result.code}`
445
+ );
446
+
447
+ this.finished = true;
448
+ trace('ProcessRunner', () => `Marked as finished, calling cleanup`);
449
+
450
+ this._cleanup();
451
+ trace('ProcessRunner', () => `Cleanup completed`);
452
+
453
+ return result;
454
+ }
455
+
456
+ _emitProcessedData(type, buf) {
457
+ if (this._cancelled) {
458
+ trace(
459
+ 'ProcessRunner',
460
+ () => 'Skipping data emission - process cancelled'
461
+ );
462
+ return;
463
+ }
464
+ const processedBuf = processOutput(buf, this.options.ansi);
465
+ this.emit(type, processedBuf);
466
+ this.emit('data', { type, data: processedBuf });
467
+ }
468
+
469
+ _handleParentStreamClosure() {
470
+ if (this.finished || this._cancelled) {
471
+ trace(
472
+ 'ProcessRunner',
473
+ () =>
474
+ `Parent stream closure ignored | ${JSON.stringify({
475
+ finished: this.finished,
476
+ cancelled: this._cancelled,
477
+ })}`
478
+ );
479
+ return;
480
+ }
481
+
482
+ trace(
483
+ 'ProcessRunner',
484
+ () =>
485
+ `Handling parent stream closure | ${JSON.stringify(
486
+ {
487
+ started: this.started,
488
+ hasChild: !!this.child,
489
+ command: this.spec.command?.slice(0, 50) || this.spec.file,
490
+ },
491
+ null,
492
+ 2
493
+ )}`
494
+ );
495
+
496
+ this._cancelled = true;
497
+
498
+ if (this._abortController) {
499
+ this._abortController.abort();
500
+ }
501
+
502
+ if (this.child) {
503
+ try {
504
+ if (this.child.stdin && typeof this.child.stdin.end === 'function') {
505
+ this.child.stdin.end();
506
+ } else if (
507
+ isBun &&
508
+ this.child.stdin &&
509
+ typeof this.child.stdin.getWriter === 'function'
510
+ ) {
511
+ const writer = this.child.stdin.getWriter();
512
+ writer.close().catch(() => {});
513
+ }
514
+
515
+ setImmediate(() => {
516
+ if (this.child && !this.finished) {
517
+ trace(
518
+ 'ProcessRunner',
519
+ () => 'Terminating child process after parent stream closure'
520
+ );
521
+ if (typeof this.child.kill === 'function') {
522
+ this.child.kill('SIGTERM');
523
+ }
524
+ }
525
+ });
526
+ } catch (error) {
527
+ trace(
528
+ 'ProcessRunner',
529
+ () =>
530
+ `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`
531
+ );
532
+ }
533
+ }
534
+
535
+ this._cleanup();
536
+ }
537
+
538
+ _cleanup() {
539
+ trace(
540
+ 'ProcessRunner',
541
+ () => `_cleanup() | active=${activeProcessRunners.size}`
542
+ );
543
+
544
+ activeProcessRunners.delete(this);
545
+ cleanupPipeline(this);
546
+
547
+ if (activeProcessRunners.size === 0) {
548
+ uninstallSignalHandlers();
549
+ }
550
+
551
+ if (this.listeners) {
552
+ this.listeners.clear();
553
+ }
554
+
555
+ cleanupAbortController(this);
556
+ cleanupChildProcess(this);
557
+ cleanupGenerator(this);
558
+
559
+ trace('ProcessRunner', () => `_cleanup() completed`);
560
+ }
561
+ }
562
+
563
+ export { ProcessRunner, isBun };