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,401 @@
1
+ // Global state management for command-stream
2
+ // Handles signal handlers, process tracking, and cleanup
3
+
4
+ import fs from 'fs';
5
+ import { trace } from './$.trace.mjs';
6
+ import { clearShellCache } from './$.shell.mjs';
7
+ import { resetAnsiConfig } from './$.ansi.mjs';
8
+
9
+ const isBun = typeof globalThis.Bun !== 'undefined';
10
+
11
+ // Save initial working directory for restoration
12
+ const initialWorkingDirectory = process.cwd();
13
+
14
+ // Track parent stream state for graceful shutdown
15
+ let parentStreamsMonitored = false;
16
+
17
+ // Set of active ProcessRunner instances
18
+ export const activeProcessRunners = new Set();
19
+
20
+ // Track if SIGINT handler has been installed
21
+ let sigintHandlerInstalled = false;
22
+ let sigintHandler = null; // Store reference to remove it later
23
+
24
+ // Global shell settings (use a proxy for modules that need direct property access)
25
+ const globalShellSettings = {
26
+ errexit: false, // set -e equivalent: exit on error
27
+ verbose: false, // set -v equivalent: print commands
28
+ xtrace: false, // set -x equivalent: trace execution
29
+ pipefail: false, // set -o pipefail equivalent: pipe failure detection
30
+ nounset: false, // set -u equivalent: error on undefined variables
31
+ };
32
+
33
+ // Export the globalShellSettings object
34
+ export { globalShellSettings };
35
+
36
+ // Virtual commands registry
37
+ export const virtualCommands = new Map();
38
+ let virtualCommandsEnabled = true;
39
+
40
+ /**
41
+ * Get the current shell settings
42
+ * @returns {object} Current shell settings
43
+ */
44
+ export function getShellSettings() {
45
+ return globalShellSettings;
46
+ }
47
+
48
+ /**
49
+ * Set shell settings
50
+ * @param {object} settings - Settings to apply
51
+ */
52
+ export function setShellSettings(settings) {
53
+ Object.assign(globalShellSettings, settings);
54
+ }
55
+
56
+ /**
57
+ * Reset shell settings to defaults
58
+ */
59
+ export function resetShellSettings() {
60
+ globalShellSettings.errexit = false;
61
+ globalShellSettings.verbose = false;
62
+ globalShellSettings.xtrace = false;
63
+ globalShellSettings.pipefail = false;
64
+ globalShellSettings.nounset = false;
65
+ globalShellSettings.noglob = false;
66
+ globalShellSettings.allexport = false;
67
+ }
68
+
69
+ /**
70
+ * Check if virtual commands are enabled
71
+ * @returns {boolean}
72
+ */
73
+ export function isVirtualCommandsEnabled() {
74
+ return virtualCommandsEnabled;
75
+ }
76
+
77
+ /**
78
+ * Enable virtual commands
79
+ */
80
+ export function enableVirtualCommands() {
81
+ trace('VirtualCommands', () => 'Enabling virtual commands');
82
+ virtualCommandsEnabled = true;
83
+ return virtualCommandsEnabled;
84
+ }
85
+
86
+ /**
87
+ * Disable virtual commands
88
+ */
89
+ export function disableVirtualCommands() {
90
+ trace('VirtualCommands', () => 'Disabling virtual commands');
91
+ virtualCommandsEnabled = false;
92
+ return virtualCommandsEnabled;
93
+ }
94
+
95
+ /**
96
+ * Find active runners (child processes and virtual commands)
97
+ * @returns {Array} Active runners
98
+ */
99
+ function findActiveRunners() {
100
+ const activeChildren = [];
101
+ for (const runner of activeProcessRunners) {
102
+ if (!runner.finished) {
103
+ if (runner.child && runner.child.pid) {
104
+ activeChildren.push(runner);
105
+ } else if (!runner.child) {
106
+ activeChildren.push(runner);
107
+ }
108
+ }
109
+ }
110
+ return activeChildren;
111
+ }
112
+
113
+ /**
114
+ * Send SIGINT to a child process
115
+ * @param {object} runner - ProcessRunner instance
116
+ */
117
+ function sendSigintToChild(runner) {
118
+ trace('ProcessRunner', () => `Sending SIGINT to child ${runner.child.pid}`);
119
+ if (isBun) {
120
+ runner.child.kill('SIGINT');
121
+ } else {
122
+ try {
123
+ process.kill(-runner.child.pid, 'SIGINT');
124
+ } catch (_err) {
125
+ process.kill(runner.child.pid, 'SIGINT');
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Forward SIGINT to all active runners
132
+ * @param {Array} activeChildren - Active runners to signal
133
+ */
134
+ function forwardSigintToRunners(activeChildren) {
135
+ for (const runner of activeChildren) {
136
+ try {
137
+ if (runner.child && runner.child.pid) {
138
+ sendSigintToChild(runner);
139
+ } else {
140
+ trace('ProcessRunner', () => 'Cancelling virtual command');
141
+ runner.kill('SIGINT');
142
+ }
143
+ } catch (err) {
144
+ trace('ProcessRunner', () => `Error forwarding SIGINT: ${err.message}`);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Handle exit after SIGINT forwarding
151
+ * @param {boolean} hasOtherHandlers - Whether other handlers exist
152
+ * @param {number} activeCount - Number of active children
153
+ */
154
+ function handleSigintExit(hasOtherHandlers, activeCount) {
155
+ trace('ProcessRunner', () => `SIGINT forwarded to ${activeCount} processes`);
156
+ if (!hasOtherHandlers) {
157
+ trace('ProcessRunner', () => 'No other handlers, exiting with code 130');
158
+ if (process.stdout && typeof process.stdout.write === 'function') {
159
+ process.stdout.write('', () => process.exit(130));
160
+ } else {
161
+ process.exit(130);
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Check if our handler is installed
168
+ * @returns {boolean}
169
+ */
170
+ function isOurHandlerInstalled() {
171
+ const currentListeners = process.listeners('SIGINT');
172
+ return currentListeners.some((l) => {
173
+ const str = l.toString();
174
+ // Look for our unique marker or helper function names
175
+ return (
176
+ str.includes('findActiveRunners') ||
177
+ str.includes('forwardSigintToRunners') ||
178
+ str.includes('handleSigintExit') ||
179
+ // Legacy detection for backwards compatibility
180
+ (str.includes('activeProcessRunners') &&
181
+ str.includes('ProcessRunner') &&
182
+ str.includes('activeChildren'))
183
+ );
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Install SIGINT handler for graceful shutdown
189
+ */
190
+ export function installSignalHandlers() {
191
+ const hasOurHandler = isOurHandlerInstalled();
192
+
193
+ if (sigintHandlerInstalled && hasOurHandler) {
194
+ return;
195
+ }
196
+
197
+ if (sigintHandlerInstalled && !hasOurHandler) {
198
+ sigintHandlerInstalled = false;
199
+ sigintHandler = null;
200
+ }
201
+
202
+ trace('SignalHandler', () => `Installing SIGINT handler`);
203
+ sigintHandlerInstalled = true;
204
+
205
+ sigintHandler = () => {
206
+ const hasOtherHandlers = process.listeners('SIGINT').length > 1;
207
+ const activeChildren = findActiveRunners();
208
+
209
+ if (activeChildren.length === 0) {
210
+ return;
211
+ }
212
+
213
+ forwardSigintToRunners(activeChildren);
214
+ handleSigintExit(hasOtherHandlers, activeChildren.length);
215
+ };
216
+
217
+ process.on('SIGINT', sigintHandler);
218
+ }
219
+
220
+ /**
221
+ * Uninstall SIGINT handler
222
+ */
223
+ export function uninstallSignalHandlers() {
224
+ if (!sigintHandlerInstalled || !sigintHandler) {
225
+ trace(
226
+ 'SignalHandler',
227
+ () => 'SIGINT handler not installed or missing, skipping removal'
228
+ );
229
+ return;
230
+ }
231
+
232
+ trace(
233
+ 'SignalHandler',
234
+ () =>
235
+ `Removing SIGINT handler | ${JSON.stringify({ activeRunners: activeProcessRunners.size })}`
236
+ );
237
+ process.removeListener('SIGINT', sigintHandler);
238
+ sigintHandlerInstalled = false;
239
+ sigintHandler = null;
240
+ }
241
+
242
+ /**
243
+ * Check if a listener is a command-stream SIGINT handler
244
+ * @param {Function} listener - Listener function
245
+ * @returns {boolean}
246
+ */
247
+ function isCommandStreamListener(listener) {
248
+ const str = listener.toString();
249
+ return (
250
+ str.includes('findActiveRunners') ||
251
+ str.includes('forwardSigintToRunners') ||
252
+ str.includes('handleSigintExit') ||
253
+ str.includes('activeProcessRunners') ||
254
+ str.includes('ProcessRunner') ||
255
+ str.includes('activeChildren')
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Force cleanup of all command-stream SIGINT handlers and state - for testing
261
+ */
262
+ export function forceCleanupAll() {
263
+ const sigintListeners = process.listeners('SIGINT');
264
+ const commandStreamListeners = sigintListeners.filter(
265
+ isCommandStreamListener
266
+ );
267
+
268
+ commandStreamListeners.forEach((listener) => {
269
+ process.removeListener('SIGINT', listener);
270
+ });
271
+
272
+ activeProcessRunners.clear();
273
+ sigintHandlerInstalled = false;
274
+ sigintHandler = null;
275
+
276
+ trace('SignalHandler', () => `Force cleanup completed`);
277
+ }
278
+
279
+ /**
280
+ * Monitor parent streams for graceful shutdown
281
+ */
282
+ export function monitorParentStreams() {
283
+ if (parentStreamsMonitored) {
284
+ trace('StreamMonitor', () => 'Parent streams already monitored, skipping');
285
+ return;
286
+ }
287
+ trace('StreamMonitor', () => 'Setting up parent stream monitoring');
288
+ parentStreamsMonitored = true;
289
+
290
+ const checkParentStream = (stream, name) => {
291
+ if (stream && typeof stream.on === 'function') {
292
+ stream.on('close', () => {
293
+ trace(
294
+ 'ProcessRunner',
295
+ () =>
296
+ `Parent ${name} closed - triggering graceful shutdown | ${JSON.stringify({ activeProcesses: activeProcessRunners.size }, null, 2)}`
297
+ );
298
+ for (const runner of activeProcessRunners) {
299
+ if (runner._handleParentStreamClosure) {
300
+ runner._handleParentStreamClosure();
301
+ }
302
+ }
303
+ });
304
+ }
305
+ };
306
+
307
+ checkParentStream(process.stdout, 'stdout');
308
+ checkParentStream(process.stderr, 'stderr');
309
+ }
310
+
311
+ /**
312
+ * Reset parent stream monitoring flag (for testing)
313
+ */
314
+ export function resetParentStreamMonitoring() {
315
+ parentStreamsMonitored = false;
316
+ }
317
+
318
+ /**
319
+ * Get a valid fallback directory
320
+ * @returns {string} Fallback directory path
321
+ */
322
+ function getFallbackDirectory() {
323
+ if (process.env.HOME && fs.existsSync(process.env.HOME)) {
324
+ return process.env.HOME;
325
+ }
326
+ if (fs.existsSync('/workspace/command-stream')) {
327
+ return '/workspace/command-stream';
328
+ }
329
+ return '/';
330
+ }
331
+
332
+ /**
333
+ * Restore working directory to initial or fallback
334
+ */
335
+ function restoreWorkingDirectory() {
336
+ try {
337
+ let currentDir;
338
+ try {
339
+ currentDir = process.cwd();
340
+ } catch (_e) {
341
+ currentDir = null;
342
+ }
343
+
344
+ if (currentDir && currentDir === initialWorkingDirectory) {
345
+ return;
346
+ }
347
+
348
+ if (fs.existsSync(initialWorkingDirectory)) {
349
+ process.chdir(initialWorkingDirectory);
350
+ } else {
351
+ const fallback = getFallbackDirectory();
352
+ process.chdir(fallback);
353
+ }
354
+ } catch (_e) {
355
+ try {
356
+ process.chdir(getFallbackDirectory());
357
+ } catch (e2) {
358
+ console.error('FATAL: Cannot set any working directory!', e2);
359
+ }
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Cleanup all active runners
365
+ */
366
+ function cleanupActiveRunners() {
367
+ for (const runner of activeProcessRunners) {
368
+ if (!runner) {
369
+ continue;
370
+ }
371
+ try {
372
+ if (!runner.started && runner._cleanup) {
373
+ runner._cleanup();
374
+ } else if (runner.kill) {
375
+ runner.kill();
376
+ }
377
+ } catch (_e) {
378
+ // Ignore errors
379
+ }
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Complete global state reset for testing - clears all library state
385
+ */
386
+ export function resetGlobalState() {
387
+ restoreWorkingDirectory();
388
+ cleanupActiveRunners();
389
+ forceCleanupAll();
390
+ clearShellCache();
391
+ parentStreamsMonitored = false;
392
+ resetShellSettings();
393
+ virtualCommandsEnabled = true;
394
+ resetAnsiConfig();
395
+
396
+ if (virtualCommands.size === 0) {
397
+ import('./commands/index.mjs').catch(() => {});
398
+ }
399
+
400
+ trace('GlobalState', () => 'Global state reset completed');
401
+ }
@@ -0,0 +1,111 @@
1
+ // EventEmitter-like implementation for stream events
2
+ // Provides a minimal event emitter for ProcessRunner
3
+
4
+ import { trace } from './$.trace.mjs';
5
+
6
+ /**
7
+ * Simple EventEmitter-like implementation for stream events
8
+ * Used as base class for ProcessRunner
9
+ */
10
+ export class StreamEmitter {
11
+ constructor() {
12
+ this.listeners = new Map();
13
+ }
14
+
15
+ /**
16
+ * Register a listener for an event
17
+ * @param {string} event - Event name
18
+ * @param {function} listener - Event handler
19
+ * @returns {this} For chaining
20
+ */
21
+ on(event, listener) {
22
+ trace(
23
+ 'StreamEmitter',
24
+ () =>
25
+ `on() called | ${JSON.stringify({
26
+ event,
27
+ hasExistingListeners: this.listeners.has(event),
28
+ listenerCount: this.listeners.get(event)?.length || 0,
29
+ })}`
30
+ );
31
+
32
+ if (!this.listeners.has(event)) {
33
+ this.listeners.set(event, []);
34
+ }
35
+ this.listeners.get(event).push(listener);
36
+
37
+ // No auto-start - explicit start() or await will start the process
38
+
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Register a one-time listener for an event
44
+ * @param {string} event - Event name
45
+ * @param {function} listener - Event handler
46
+ * @returns {this} For chaining
47
+ */
48
+ once(event, listener) {
49
+ trace('StreamEmitter', () => `once() called for event: ${event}`);
50
+ const onceWrapper = (...args) => {
51
+ this.off(event, onceWrapper);
52
+ listener(...args);
53
+ };
54
+ return this.on(event, onceWrapper);
55
+ }
56
+
57
+ /**
58
+ * Emit an event to all registered listeners
59
+ * @param {string} event - Event name
60
+ * @param {...*} args - Arguments to pass to listeners
61
+ * @returns {this} For chaining
62
+ */
63
+ emit(event, ...args) {
64
+ const eventListeners = this.listeners.get(event);
65
+ trace(
66
+ 'StreamEmitter',
67
+ () =>
68
+ `Emitting event | ${JSON.stringify({
69
+ event,
70
+ hasListeners: !!eventListeners,
71
+ listenerCount: eventListeners?.length || 0,
72
+ })}`
73
+ );
74
+ if (eventListeners) {
75
+ // Create a copy to avoid issues if listeners modify the array
76
+ const listenersToCall = [...eventListeners];
77
+ for (const listener of listenersToCall) {
78
+ listener(...args);
79
+ }
80
+ }
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * Remove a listener for an event
86
+ * @param {string} event - Event name
87
+ * @param {function} listener - Event handler to remove
88
+ * @returns {this} For chaining
89
+ */
90
+ off(event, listener) {
91
+ trace(
92
+ 'StreamEmitter',
93
+ () =>
94
+ `off() called | ${JSON.stringify({
95
+ event,
96
+ hasListeners: !!this.listeners.get(event),
97
+ listenerCount: this.listeners.get(event)?.length || 0,
98
+ })}`
99
+ );
100
+
101
+ const eventListeners = this.listeners.get(event);
102
+ if (eventListeners) {
103
+ const index = eventListeners.indexOf(listener);
104
+ if (index !== -1) {
105
+ eventListeners.splice(index, 1);
106
+ trace('StreamEmitter', () => `Removed listener at index ${index}`);
107
+ }
108
+ }
109
+ return this;
110
+ }
111
+ }