command-stream 0.9.0 → 0.9.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.
- package/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
package/js/src/shell-parser.mjs
CHANGED
|
@@ -22,6 +22,119 @@ const TokenType = {
|
|
|
22
22
|
EOF: 'eof',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse a word token from the command string, handling quotes and escapes
|
|
27
|
+
* @param {string} command - The command string
|
|
28
|
+
* @param {number} startIndex - Starting position
|
|
29
|
+
* @returns {{word: string, endIndex: number}} Parsed word and end position
|
|
30
|
+
*/
|
|
31
|
+
function parseWord(command, startIndex) {
|
|
32
|
+
let word = '';
|
|
33
|
+
let i = startIndex;
|
|
34
|
+
let inQuote = false;
|
|
35
|
+
let quoteChar = '';
|
|
36
|
+
|
|
37
|
+
while (i < command.length) {
|
|
38
|
+
const char = command[i];
|
|
39
|
+
|
|
40
|
+
if (!inQuote) {
|
|
41
|
+
const result = parseUnquotedChar(command, i, char, word);
|
|
42
|
+
if (result.done) {
|
|
43
|
+
return { word: result.word, endIndex: i };
|
|
44
|
+
}
|
|
45
|
+
word = result.word;
|
|
46
|
+
i = result.index;
|
|
47
|
+
if (result.startQuote) {
|
|
48
|
+
inQuote = true;
|
|
49
|
+
quoteChar = result.startQuote;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
const result = parseQuotedChar(command, i, char, word, quoteChar);
|
|
53
|
+
word = result.word;
|
|
54
|
+
i = result.index;
|
|
55
|
+
if (result.endQuote) {
|
|
56
|
+
inQuote = false;
|
|
57
|
+
quoteChar = '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { word, endIndex: i };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a character when not inside quotes
|
|
67
|
+
*/
|
|
68
|
+
function parseUnquotedChar(command, i, char, word) {
|
|
69
|
+
if (char === '"' || char === "'") {
|
|
70
|
+
return { word: word + char, index: i + 1, startQuote: char };
|
|
71
|
+
}
|
|
72
|
+
if (/\s/.test(char) || '&|;()<>'.includes(char)) {
|
|
73
|
+
return { word, done: true };
|
|
74
|
+
}
|
|
75
|
+
if (char === '\\' && i + 1 < command.length) {
|
|
76
|
+
const escaped = command[i + 1];
|
|
77
|
+
return { word: word + char + escaped, index: i + 2 };
|
|
78
|
+
}
|
|
79
|
+
return { word: word + char, index: i + 1 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a character when inside quotes
|
|
84
|
+
*/
|
|
85
|
+
function parseQuotedChar(command, i, char, word, quoteChar) {
|
|
86
|
+
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
87
|
+
return { word: word + char, index: i + 1, endQuote: true };
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
char === '\\' &&
|
|
91
|
+
i + 1 < command.length &&
|
|
92
|
+
(command[i + 1] === quoteChar || command[i + 1] === '\\')
|
|
93
|
+
) {
|
|
94
|
+
const escaped = command[i + 1];
|
|
95
|
+
return { word: word + char + escaped, index: i + 2 };
|
|
96
|
+
}
|
|
97
|
+
return { word: word + char, index: i + 1 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Try to match an operator at the current position
|
|
102
|
+
* @returns {{type: string, value: string, length: number} | null}
|
|
103
|
+
*/
|
|
104
|
+
function matchOperator(command, i) {
|
|
105
|
+
const twoChar = command.slice(i, i + 2);
|
|
106
|
+
const oneChar = command[i];
|
|
107
|
+
|
|
108
|
+
if (twoChar === '&&') {
|
|
109
|
+
return { type: TokenType.AND, value: '&&', length: 2 };
|
|
110
|
+
}
|
|
111
|
+
if (twoChar === '||') {
|
|
112
|
+
return { type: TokenType.OR, value: '||', length: 2 };
|
|
113
|
+
}
|
|
114
|
+
if (twoChar === '>>') {
|
|
115
|
+
return { type: TokenType.REDIRECT_APPEND, value: '>>', length: 2 };
|
|
116
|
+
}
|
|
117
|
+
if (oneChar === '|') {
|
|
118
|
+
return { type: TokenType.PIPE, value: '|', length: 1 };
|
|
119
|
+
}
|
|
120
|
+
if (oneChar === ';') {
|
|
121
|
+
return { type: TokenType.SEMICOLON, value: ';', length: 1 };
|
|
122
|
+
}
|
|
123
|
+
if (oneChar === '(') {
|
|
124
|
+
return { type: TokenType.LPAREN, value: '(', length: 1 };
|
|
125
|
+
}
|
|
126
|
+
if (oneChar === ')') {
|
|
127
|
+
return { type: TokenType.RPAREN, value: ')', length: 1 };
|
|
128
|
+
}
|
|
129
|
+
if (oneChar === '>') {
|
|
130
|
+
return { type: TokenType.REDIRECT_OUT, value: '>', length: 1 };
|
|
131
|
+
}
|
|
132
|
+
if (oneChar === '<') {
|
|
133
|
+
return { type: TokenType.REDIRECT_IN, value: '<', length: 1 };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
25
138
|
/**
|
|
26
139
|
* Tokenize a shell command string
|
|
27
140
|
*/
|
|
@@ -40,90 +153,19 @@ function tokenize(command) {
|
|
|
40
153
|
}
|
|
41
154
|
|
|
42
155
|
// Check for operators
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else if (command[i] === '|') {
|
|
50
|
-
tokens.push({ type: TokenType.PIPE, value: '|' });
|
|
51
|
-
i++;
|
|
52
|
-
} else if (command[i] === ';') {
|
|
53
|
-
tokens.push({ type: TokenType.SEMICOLON, value: ';' });
|
|
54
|
-
i++;
|
|
55
|
-
} else if (command[i] === '(') {
|
|
56
|
-
tokens.push({ type: TokenType.LPAREN, value: '(' });
|
|
57
|
-
i++;
|
|
58
|
-
} else if (command[i] === ')') {
|
|
59
|
-
tokens.push({ type: TokenType.RPAREN, value: ')' });
|
|
60
|
-
i++;
|
|
61
|
-
} else if (command[i] === '>' && command[i + 1] === '>') {
|
|
62
|
-
tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
|
|
63
|
-
i += 2;
|
|
64
|
-
} else if (command[i] === '>') {
|
|
65
|
-
tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
|
|
66
|
-
i++;
|
|
67
|
-
} else if (command[i] === '<') {
|
|
68
|
-
tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
|
|
69
|
-
i++;
|
|
70
|
-
} else {
|
|
71
|
-
// Parse word (respecting quotes)
|
|
72
|
-
let word = '';
|
|
73
|
-
let inQuote = false;
|
|
74
|
-
let quoteChar = '';
|
|
75
|
-
|
|
76
|
-
while (i < command.length) {
|
|
77
|
-
const char = command[i];
|
|
78
|
-
|
|
79
|
-
if (!inQuote) {
|
|
80
|
-
if (char === '"' || char === "'") {
|
|
81
|
-
inQuote = true;
|
|
82
|
-
quoteChar = char;
|
|
83
|
-
word += char;
|
|
84
|
-
i++;
|
|
85
|
-
} else if (/\s/.test(char) || '&|;()<>'.includes(char)) {
|
|
86
|
-
break;
|
|
87
|
-
} else if (char === '\\' && i + 1 < command.length) {
|
|
88
|
-
// Handle escape sequences
|
|
89
|
-
word += char;
|
|
90
|
-
i++;
|
|
91
|
-
if (i < command.length) {
|
|
92
|
-
word += command[i];
|
|
93
|
-
i++;
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
word += char;
|
|
97
|
-
i++;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
101
|
-
inQuote = false;
|
|
102
|
-
quoteChar = '';
|
|
103
|
-
word += char;
|
|
104
|
-
i++;
|
|
105
|
-
} else if (
|
|
106
|
-
char === '\\' &&
|
|
107
|
-
i + 1 < command.length &&
|
|
108
|
-
(command[i + 1] === quoteChar || command[i + 1] === '\\')
|
|
109
|
-
) {
|
|
110
|
-
// Handle escaped quotes and backslashes inside quotes
|
|
111
|
-
word += char;
|
|
112
|
-
i++;
|
|
113
|
-
if (i < command.length) {
|
|
114
|
-
word += command[i];
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
word += char;
|
|
119
|
-
i++;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
156
|
+
const operator = matchOperator(command, i);
|
|
157
|
+
if (operator) {
|
|
158
|
+
tokens.push({ type: operator.type, value: operator.value });
|
|
159
|
+
i += operator.length;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
// Parse word (respecting quotes)
|
|
164
|
+
const { word, endIndex } = parseWord(command, i);
|
|
165
|
+
i = endIndex;
|
|
166
|
+
|
|
167
|
+
if (word) {
|
|
168
|
+
tokens.push({ type: TokenType.WORD, value: word });
|
|
127
169
|
}
|
|
128
170
|
}
|
|
129
171
|
|
|
@@ -8,19 +8,27 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
10
10
|
|
|
11
|
+
// Helper to check if a listener is a command-stream SIGINT handler
|
|
12
|
+
function isCommandStreamListener(l) {
|
|
13
|
+
const str = l.toString();
|
|
14
|
+
return (
|
|
15
|
+
str.includes('findActiveRunners') ||
|
|
16
|
+
str.includes('forwardSigintToRunners') ||
|
|
17
|
+
str.includes('handleSigintExit') ||
|
|
18
|
+
str.includes('activeProcessRunners') ||
|
|
19
|
+
str.includes('ProcessRunner') ||
|
|
20
|
+
str.includes('activeChildren')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
// Helper to access internal state for testing
|
|
12
25
|
// This is a testing-only approach to verify cleanup
|
|
13
26
|
function getInternalState() {
|
|
14
27
|
// We'll use process listeners as a proxy for internal state
|
|
15
28
|
const sigintListeners = process.listeners('SIGINT');
|
|
16
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
str.includes('activeProcessRunners') ||
|
|
20
|
-
str.includes('ProcessRunner') ||
|
|
21
|
-
str.includes('activeChildren')
|
|
22
|
-
);
|
|
23
|
-
});
|
|
29
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
30
|
+
isCommandStreamListener
|
|
31
|
+
);
|
|
24
32
|
|
|
25
33
|
return {
|
|
26
34
|
sigintHandlerCount: commandStreamListeners.length,
|
|
@@ -50,14 +58,9 @@ describe('Resource Cleanup Internal Verification', () => {
|
|
|
50
58
|
|
|
51
59
|
// Force remove any command-stream SIGINT listeners
|
|
52
60
|
const sigintListeners = process.listeners('SIGINT');
|
|
53
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
str.includes('activeProcessRunners') ||
|
|
57
|
-
str.includes('ProcessRunner') ||
|
|
58
|
-
str.includes('activeChildren')
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
62
|
+
isCommandStreamListener
|
|
63
|
+
);
|
|
61
64
|
|
|
62
65
|
commandStreamListeners.forEach((listener) => {
|
|
63
66
|
process.removeListener('SIGINT', listener);
|
|
@@ -431,14 +434,9 @@ describe('Resource Cleanup Internal Verification', () => {
|
|
|
431
434
|
`Pipeline error test left behind ${state.sigintHandlerCount - initialState.sigintHandlerCount} handlers, forcing cleanup...`
|
|
432
435
|
);
|
|
433
436
|
const sigintListeners = process.listeners('SIGINT');
|
|
434
|
-
const commandStreamListeners = sigintListeners.filter(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
str.includes('activeProcessRunners') ||
|
|
438
|
-
str.includes('ProcessRunner') ||
|
|
439
|
-
str.includes('activeChildren')
|
|
440
|
-
);
|
|
441
|
-
});
|
|
437
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
438
|
+
isCommandStreamListener
|
|
439
|
+
);
|
|
442
440
|
|
|
443
441
|
commandStreamListeners.forEach((listener) => {
|
|
444
442
|
process.removeListener('SIGINT', listener);
|
|
@@ -82,6 +82,9 @@ describe.skipIf(isWindows)('SIGINT Handler Cleanup Tests', () => {
|
|
|
82
82
|
const ourListeners = process.listeners('SIGINT').filter((l) => {
|
|
83
83
|
const str = l.toString();
|
|
84
84
|
return (
|
|
85
|
+
str.includes('findActiveRunners') ||
|
|
86
|
+
str.includes('forwardSigintToRunners') ||
|
|
87
|
+
str.includes('handleSigintExit') ||
|
|
85
88
|
str.includes('activeProcessRunners') ||
|
|
86
89
|
str.includes('ProcessRunner') ||
|
|
87
90
|
str.includes('activeChildren')
|
package/package.json
CHANGED