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,390 @@
1
+ // Stream utility functions for safe operations and error handling
2
+ // Provides cross-runtime compatible stream operations
3
+
4
+ import { trace } from './$.trace.mjs';
5
+
6
+ const isBun = typeof globalThis.Bun !== 'undefined';
7
+
8
+ // Stream utility functions for safe operations and error handling
9
+ export const StreamUtils = {
10
+ /**
11
+ * Check if a stream is safe to write to
12
+ * @param {object} stream - The stream to check
13
+ * @returns {boolean} Whether the stream is writable
14
+ */
15
+ isStreamWritable(stream) {
16
+ return stream && stream.writable && !stream.destroyed && !stream.closed;
17
+ },
18
+
19
+ /**
20
+ * Add standardized error handler to stdin streams
21
+ * @param {object} stream - The stream to add handler to
22
+ * @param {string} contextName - Name for trace logging
23
+ * @param {function} onNonEpipeError - Optional callback for non-EPIPE errors
24
+ */
25
+ addStdinErrorHandler(stream, contextName = 'stdin', onNonEpipeError = null) {
26
+ if (stream && typeof stream.on === 'function') {
27
+ stream.on('error', (error) => {
28
+ const handled = this.handleStreamError(
29
+ error,
30
+ `${contextName} error event`,
31
+ false
32
+ );
33
+ if (!handled && onNonEpipeError) {
34
+ onNonEpipeError(error);
35
+ }
36
+ });
37
+ }
38
+ },
39
+
40
+ /**
41
+ * Safely write to a stream with comprehensive error handling
42
+ * @param {object} stream - The stream to write to
43
+ * @param {Buffer|string} data - The data to write
44
+ * @param {string} contextName - Name for trace logging
45
+ * @returns {boolean} Whether the write was successful
46
+ */
47
+ safeStreamWrite(stream, data, contextName = 'stream') {
48
+ if (!this.isStreamWritable(stream)) {
49
+ trace(
50
+ 'ProcessRunner',
51
+ () =>
52
+ `${contextName} write skipped - not writable | ${JSON.stringify(
53
+ {
54
+ hasStream: !!stream,
55
+ writable: stream?.writable,
56
+ destroyed: stream?.destroyed,
57
+ closed: stream?.closed,
58
+ },
59
+ null,
60
+ 2
61
+ )}`
62
+ );
63
+ return false;
64
+ }
65
+
66
+ try {
67
+ const result = stream.write(data);
68
+ trace(
69
+ 'ProcessRunner',
70
+ () =>
71
+ `${contextName} write successful | ${JSON.stringify(
72
+ {
73
+ dataLength: data?.length || 0,
74
+ },
75
+ null,
76
+ 2
77
+ )}`
78
+ );
79
+ return result;
80
+ } catch (error) {
81
+ if (error.code !== 'EPIPE') {
82
+ trace(
83
+ 'ProcessRunner',
84
+ () =>
85
+ `${contextName} write error | ${JSON.stringify(
86
+ {
87
+ error: error.message,
88
+ code: error.code,
89
+ isEPIPE: false,
90
+ },
91
+ null,
92
+ 2
93
+ )}`
94
+ );
95
+ throw error; // Re-throw non-EPIPE errors
96
+ } else {
97
+ trace(
98
+ 'ProcessRunner',
99
+ () =>
100
+ `${contextName} EPIPE error (ignored) | ${JSON.stringify(
101
+ {
102
+ error: error.message,
103
+ code: error.code,
104
+ isEPIPE: true,
105
+ },
106
+ null,
107
+ 2
108
+ )}`
109
+ );
110
+ }
111
+ return false;
112
+ }
113
+ },
114
+
115
+ /**
116
+ * Safely end a stream with error handling
117
+ * @param {object} stream - The stream to end
118
+ * @param {string} contextName - Name for trace logging
119
+ * @returns {boolean} Whether the end was successful
120
+ */
121
+ safeStreamEnd(stream, contextName = 'stream') {
122
+ if (!this.isStreamWritable(stream) || typeof stream.end !== 'function') {
123
+ trace(
124
+ 'ProcessRunner',
125
+ () =>
126
+ `${contextName} end skipped - not available | ${JSON.stringify(
127
+ {
128
+ hasStream: !!stream,
129
+ hasEnd: stream && typeof stream.end === 'function',
130
+ writable: stream?.writable,
131
+ },
132
+ null,
133
+ 2
134
+ )}`
135
+ );
136
+ return false;
137
+ }
138
+
139
+ try {
140
+ stream.end();
141
+ trace('ProcessRunner', () => `${contextName} ended successfully`);
142
+ return true;
143
+ } catch (error) {
144
+ if (error.code !== 'EPIPE') {
145
+ trace(
146
+ 'ProcessRunner',
147
+ () =>
148
+ `${contextName} end error | ${JSON.stringify(
149
+ {
150
+ error: error.message,
151
+ code: error.code,
152
+ },
153
+ null,
154
+ 2
155
+ )}`
156
+ );
157
+ } else {
158
+ trace(
159
+ 'ProcessRunner',
160
+ () =>
161
+ `${contextName} EPIPE on end (ignored) | ${JSON.stringify(
162
+ {
163
+ error: error.message,
164
+ code: error.code,
165
+ },
166
+ null,
167
+ 2
168
+ )}`
169
+ );
170
+ }
171
+ return false;
172
+ }
173
+ },
174
+
175
+ /**
176
+ * Setup comprehensive stdin handling (error handler + safe operations)
177
+ * @param {object} stream - The stream to setup
178
+ * @param {string} contextName - Name for trace logging
179
+ * @returns {{ write: function, end: function, isWritable: function }}
180
+ */
181
+ setupStdinHandling(stream, contextName = 'stdin') {
182
+ this.addStdinErrorHandler(stream, contextName);
183
+
184
+ return {
185
+ write: (data) => this.safeStreamWrite(stream, data, contextName),
186
+ end: () => this.safeStreamEnd(stream, contextName),
187
+ isWritable: () => this.isStreamWritable(stream),
188
+ };
189
+ },
190
+
191
+ /**
192
+ * Handle stream errors with consistent EPIPE behavior
193
+ * @param {Error} error - The error to handle
194
+ * @param {string} contextName - Name for trace logging
195
+ * @param {boolean} shouldThrow - Whether to throw non-EPIPE errors
196
+ * @returns {boolean} Whether the error was an EPIPE (handled gracefully)
197
+ */
198
+ handleStreamError(error, contextName, shouldThrow = true) {
199
+ if (error.code !== 'EPIPE') {
200
+ trace(
201
+ 'ProcessRunner',
202
+ () =>
203
+ `${contextName} error | ${JSON.stringify(
204
+ {
205
+ error: error.message,
206
+ code: error.code,
207
+ isEPIPE: false,
208
+ },
209
+ null,
210
+ 2
211
+ )}`
212
+ );
213
+ if (shouldThrow) {
214
+ throw error;
215
+ }
216
+ return false;
217
+ } else {
218
+ trace(
219
+ 'ProcessRunner',
220
+ () =>
221
+ `${contextName} EPIPE error (ignored) | ${JSON.stringify(
222
+ {
223
+ error: error.message,
224
+ code: error.code,
225
+ isEPIPE: true,
226
+ },
227
+ null,
228
+ 2
229
+ )}`
230
+ );
231
+ return true; // EPIPE handled gracefully
232
+ }
233
+ },
234
+
235
+ /**
236
+ * Detect if stream supports Bun-style writing
237
+ * @param {object} stream - The stream to check
238
+ * @returns {boolean}
239
+ */
240
+ isBunStream(stream) {
241
+ return isBun && stream && typeof stream.getWriter === 'function';
242
+ },
243
+
244
+ /**
245
+ * Detect if stream supports Node.js-style writing
246
+ * @param {object} stream - The stream to check
247
+ * @returns {boolean}
248
+ */
249
+ isNodeStream(stream) {
250
+ return stream && typeof stream.write === 'function';
251
+ },
252
+
253
+ /**
254
+ * Write to either Bun or Node.js style stream
255
+ * @param {object} stream - The stream to write to
256
+ * @param {Buffer|string} data - The data to write
257
+ * @param {string} contextName - Name for trace logging
258
+ * @returns {Promise<boolean>} Whether the write was successful
259
+ */
260
+ async writeToStream(stream, data, contextName = 'stream') {
261
+ if (this.isBunStream(stream)) {
262
+ try {
263
+ const writer = stream.getWriter();
264
+ await writer.write(data);
265
+ writer.releaseLock();
266
+ return true;
267
+ } catch (error) {
268
+ return this.handleStreamError(
269
+ error,
270
+ `${contextName} Bun writer`,
271
+ false
272
+ );
273
+ }
274
+ } else if (this.isNodeStream(stream)) {
275
+ try {
276
+ stream.write(data);
277
+ return true;
278
+ } catch (error) {
279
+ return this.handleStreamError(
280
+ error,
281
+ `${contextName} Node writer`,
282
+ false
283
+ );
284
+ }
285
+ }
286
+ return false;
287
+ },
288
+ };
289
+
290
+ /**
291
+ * Safe write to a stream with parent stream monitoring
292
+ * @param {object} stream - The stream to write to
293
+ * @param {Buffer|string} data - The data to write
294
+ * @param {object} processRunner - Optional ProcessRunner for parent stream handling
295
+ * @param {function} monitorParentStreams - Function to call for monitoring
296
+ * @returns {boolean} Whether the write was successful
297
+ */
298
+ export function safeWrite(
299
+ stream,
300
+ data,
301
+ processRunner = null,
302
+ monitorParentStreams = null
303
+ ) {
304
+ if (monitorParentStreams) {
305
+ monitorParentStreams();
306
+ }
307
+
308
+ if (!StreamUtils.isStreamWritable(stream)) {
309
+ trace(
310
+ 'ProcessRunner',
311
+ () =>
312
+ `safeWrite skipped - stream not writable | ${JSON.stringify(
313
+ {
314
+ hasStream: !!stream,
315
+ writable: stream?.writable,
316
+ destroyed: stream?.destroyed,
317
+ closed: stream?.closed,
318
+ },
319
+ null,
320
+ 2
321
+ )}`
322
+ );
323
+
324
+ if (
325
+ processRunner &&
326
+ processRunner._handleParentStreamClosure &&
327
+ (stream === process.stdout || stream === process.stderr)
328
+ ) {
329
+ processRunner._handleParentStreamClosure();
330
+ }
331
+
332
+ return false;
333
+ }
334
+
335
+ try {
336
+ return stream.write(data);
337
+ } catch (error) {
338
+ trace(
339
+ 'ProcessRunner',
340
+ () =>
341
+ `safeWrite error | ${JSON.stringify(
342
+ {
343
+ error: error.message,
344
+ code: error.code,
345
+ writable: stream.writable,
346
+ destroyed: stream.destroyed,
347
+ },
348
+ null,
349
+ 2
350
+ )}`
351
+ );
352
+
353
+ if (
354
+ error.code === 'EPIPE' &&
355
+ processRunner &&
356
+ processRunner._handleParentStreamClosure &&
357
+ (stream === process.stdout || stream === process.stderr)
358
+ ) {
359
+ processRunner._handleParentStreamClosure();
360
+ }
361
+
362
+ return false;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Convert data to Buffer
368
+ * @param {Buffer|string|object} chunk - Data to convert
369
+ * @returns {Buffer} The data as a Buffer
370
+ */
371
+ export function asBuffer(chunk) {
372
+ if (chunk === null || chunk === undefined) {
373
+ return Buffer.alloc(0);
374
+ }
375
+ if (Buffer.isBuffer(chunk)) {
376
+ return chunk;
377
+ }
378
+ if (typeof chunk === 'string') {
379
+ return Buffer.from(chunk, 'utf8');
380
+ }
381
+ // Handle ArrayBuffer and other views
382
+ if (chunk instanceof Uint8Array || chunk instanceof ArrayBuffer) {
383
+ return Buffer.from(chunk);
384
+ }
385
+ // Handle objects with toString
386
+ if (typeof chunk.toString === 'function') {
387
+ return Buffer.from(chunk.toString(), 'utf8');
388
+ }
389
+ return Buffer.from(String(chunk), 'utf8');
390
+ }
@@ -0,0 +1,36 @@
1
+ // Trace function for verbose logging
2
+ // Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars
3
+ // Can be disabled per-command via trace: false option
4
+ // CI environment no longer auto-enables tracing
5
+
6
+ /**
7
+ * Log trace messages for debugging
8
+ * @param {string} category - The category of the trace message
9
+ * @param {string|function} messageOrFunc - The message or a function that returns the message
10
+ * @param {object} runner - Optional runner object to check for trace option
11
+ */
12
+ export function trace(category, messageOrFunc, runner = null) {
13
+ // Check if runner explicitly disabled tracing
14
+ if (runner && runner.options && runner.options.trace === false) {
15
+ return;
16
+ }
17
+
18
+ // Check global trace setting (evaluated dynamically for runtime changes)
19
+ const TRACE_ENV = process.env.COMMAND_STREAM_TRACE;
20
+ const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true';
21
+
22
+ // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true
23
+ // COMMAND_STREAM_TRACE=true explicitly enables tracing
24
+ // Otherwise, use COMMAND_STREAM_VERBOSE
25
+ const VERBOSE =
26
+ TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV;
27
+
28
+ if (!VERBOSE) {
29
+ return;
30
+ }
31
+
32
+ const message =
33
+ typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
34
+ const timestamp = new Date().toISOString();
35
+ console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
36
+ }
@@ -1,28 +1,7 @@
1
1
  import path from 'path';
2
2
 
3
- // Trace function for verbose logging - consistent with src/$.mjs
4
- // Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars
5
- // CI environment no longer auto-enables tracing
6
- export function trace(category, messageOrFunc) {
7
- // Check global trace setting (evaluated dynamically for runtime changes)
8
- const TRACE_ENV = process.env.COMMAND_STREAM_TRACE;
9
- const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true';
10
-
11
- // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true
12
- // COMMAND_STREAM_TRACE=true explicitly enables tracing
13
- // Otherwise, use COMMAND_STREAM_VERBOSE
14
- const VERBOSE =
15
- TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV;
16
-
17
- if (!VERBOSE) {
18
- return;
19
- }
20
-
21
- const message =
22
- typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
23
- const timestamp = new Date().toISOString();
24
- console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
25
- }
3
+ // Re-export trace from the dedicated trace module for consistency
4
+ export { trace } from './$.trace.mjs';
26
5
 
27
6
  export const VirtualUtils = {
28
7
  /**
@@ -0,0 +1,113 @@
1
+ // Virtual commands registration and management
2
+ // Handles registration of built-in and custom virtual commands
3
+
4
+ import { trace } from './$.trace.mjs';
5
+ import { virtualCommands, getShellSettings } from './$.state.mjs';
6
+
7
+ // Import virtual command implementations
8
+ import cdCommand from './commands/$.cd.mjs';
9
+ import pwdCommand from './commands/$.pwd.mjs';
10
+ import echoCommand from './commands/$.echo.mjs';
11
+ import sleepCommand from './commands/$.sleep.mjs';
12
+ import trueCommand from './commands/$.true.mjs';
13
+ import falseCommand from './commands/$.false.mjs';
14
+ import createWhichCommand from './commands/$.which.mjs';
15
+ import createExitCommand from './commands/$.exit.mjs';
16
+ import envCommand from './commands/$.env.mjs';
17
+ import catCommand from './commands/$.cat.mjs';
18
+ import lsCommand from './commands/$.ls.mjs';
19
+ import mkdirCommand from './commands/$.mkdir.mjs';
20
+ import rmCommand from './commands/$.rm.mjs';
21
+ import mvCommand from './commands/$.mv.mjs';
22
+ import cpCommand from './commands/$.cp.mjs';
23
+ import touchCommand from './commands/$.touch.mjs';
24
+ import basenameCommand from './commands/$.basename.mjs';
25
+ import dirnameCommand from './commands/$.dirname.mjs';
26
+ import yesCommand from './commands/$.yes.mjs';
27
+ import seqCommand from './commands/$.seq.mjs';
28
+ import testCommand from './commands/$.test.mjs';
29
+
30
+ /**
31
+ * Register a virtual command
32
+ * @param {string} name - Command name
33
+ * @param {function} handler - Command handler function
34
+ * @returns {Map} The virtual commands map
35
+ */
36
+ export function register(name, handler) {
37
+ trace(
38
+ 'VirtualCommands',
39
+ () => `register ENTER | ${JSON.stringify({ name }, null, 2)}`
40
+ );
41
+ virtualCommands.set(name, handler);
42
+ trace(
43
+ 'VirtualCommands',
44
+ () => `register EXIT | ${JSON.stringify({ registered: true }, null, 2)}`
45
+ );
46
+ return virtualCommands;
47
+ }
48
+
49
+ /**
50
+ * Unregister a virtual command
51
+ * @param {string} name - Command name to remove
52
+ * @returns {boolean} Whether the command was removed
53
+ */
54
+ export function unregister(name) {
55
+ trace(
56
+ 'VirtualCommands',
57
+ () => `unregister ENTER | ${JSON.stringify({ name }, null, 2)}`
58
+ );
59
+ const deleted = virtualCommands.delete(name);
60
+ trace(
61
+ 'VirtualCommands',
62
+ () => `unregister EXIT | ${JSON.stringify({ deleted }, null, 2)}`
63
+ );
64
+ return deleted;
65
+ }
66
+
67
+ /**
68
+ * List all registered virtual commands
69
+ * @returns {string[]} Array of command names
70
+ */
71
+ export function listCommands() {
72
+ const commands = Array.from(virtualCommands.keys());
73
+ trace(
74
+ 'VirtualCommands',
75
+ () => `listCommands() returning ${commands.length} commands`
76
+ );
77
+ return commands;
78
+ }
79
+
80
+ /**
81
+ * Register all built-in virtual commands
82
+ */
83
+ export function registerBuiltins() {
84
+ trace(
85
+ 'VirtualCommands',
86
+ () => 'registerBuiltins() called - registering all built-in commands'
87
+ );
88
+
89
+ const globalShellSettings = getShellSettings();
90
+
91
+ // Register all imported commands
92
+ register('cd', cdCommand);
93
+ register('pwd', pwdCommand);
94
+ register('echo', echoCommand);
95
+ register('sleep', sleepCommand);
96
+ register('true', trueCommand);
97
+ register('false', falseCommand);
98
+ register('which', createWhichCommand(virtualCommands));
99
+ register('exit', createExitCommand(globalShellSettings));
100
+ register('env', envCommand);
101
+ register('cat', catCommand);
102
+ register('ls', lsCommand);
103
+ register('mkdir', mkdirCommand);
104
+ register('rm', rmCommand);
105
+ register('mv', mvCommand);
106
+ register('cp', cpCommand);
107
+ register('touch', touchCommand);
108
+ register('basename', basenameCommand);
109
+ register('dirname', dirnameCommand);
110
+ register('yes', yesCommand);
111
+ register('seq', seqCommand);
112
+ register('test', testCommand);
113
+ }
@@ -28,7 +28,9 @@ export default function createWhichCommand(virtualCommands) {
28
28
  if (fs.statSync(fullPath).isFile()) {
29
29
  return VirtualUtils.success(`${fullPath}\n`);
30
30
  }
31
- } catch {}
31
+ } catch {
32
+ // File doesn't exist or isn't accessible, continue searching
33
+ }
32
34
  }
33
35
  }
34
36
 
@@ -0,0 +1,24 @@
1
+ // Command module index
2
+ // Re-exports all built-in virtual commands
3
+
4
+ export { default as cd } from './$.cd.mjs';
5
+ export { default as pwd } from './$.pwd.mjs';
6
+ export { default as echo } from './$.echo.mjs';
7
+ export { default as sleep } from './$.sleep.mjs';
8
+ export { default as trueCmd } from './$.true.mjs';
9
+ export { default as falseCmd } from './$.false.mjs';
10
+ export { default as createWhich } from './$.which.mjs';
11
+ export { default as createExit } from './$.exit.mjs';
12
+ export { default as env } from './$.env.mjs';
13
+ export { default as cat } from './$.cat.mjs';
14
+ export { default as ls } from './$.ls.mjs';
15
+ export { default as mkdir } from './$.mkdir.mjs';
16
+ export { default as rm } from './$.rm.mjs';
17
+ export { default as mv } from './$.mv.mjs';
18
+ export { default as cp } from './$.cp.mjs';
19
+ export { default as touch } from './$.touch.mjs';
20
+ export { default as basename } from './$.basename.mjs';
21
+ export { default as dirname } from './$.dirname.mjs';
22
+ export { default as yes } from './$.yes.mjs';
23
+ export { default as seq } from './$.seq.mjs';
24
+ export { default as test } from './$.test.mjs';