command-stream 0.5.0 → 0.7.0

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.
@@ -8,9 +8,10 @@ export default async function cd({ args }) {
8
8
  process.chdir(target);
9
9
  const newDir = process.cwd();
10
10
  trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
11
- return VirtualUtils.success(newDir);
11
+ // cd command should not output anything on success, just like real cd
12
+ return VirtualUtils.success('');
12
13
  } catch (error) {
13
14
  trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
14
- return { stderr: `cd: ${error.message}`, code: 1 };
15
+ return { stderr: `cd: ${error.message}\n`, code: 1 };
15
16
  }
16
17
  }
@@ -4,5 +4,5 @@ export default async function pwd({ args, stdin, cwd }) {
4
4
  // If cwd option is provided, return that instead of process.cwd()
5
5
  const dir = cwd || process.cwd();
6
6
  trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
7
- return VirtualUtils.success(dir);
7
+ return VirtualUtils.success(dir + '\n');
8
8
  }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Enhanced shell command parser that handles &&, ||, ;, and () operators
3
+ * This allows virtual commands to work properly with shell operators
4
+ */
5
+
6
+ import { trace } from './$.utils.mjs';
7
+
8
+ /**
9
+ * Token types for the parser
10
+ */
11
+ const TokenType = {
12
+ WORD: 'word',
13
+ AND: '&&',
14
+ OR: '||',
15
+ SEMICOLON: ';',
16
+ PIPE: '|',
17
+ LPAREN: '(',
18
+ RPAREN: ')',
19
+ REDIRECT_OUT: '>',
20
+ REDIRECT_APPEND: '>>',
21
+ REDIRECT_IN: '<',
22
+ EOF: 'eof'
23
+ };
24
+
25
+ /**
26
+ * Tokenize a shell command string
27
+ */
28
+ function tokenize(command) {
29
+ const tokens = [];
30
+ let i = 0;
31
+
32
+ while (i < command.length) {
33
+ // Skip whitespace
34
+ while (i < command.length && /\s/.test(command[i])) {
35
+ i++;
36
+ }
37
+
38
+ if (i >= command.length) break;
39
+
40
+ // Check for operators
41
+ if (command[i] === '&' && command[i + 1] === '&') {
42
+ tokens.push({ type: TokenType.AND, value: '&&' });
43
+ i += 2;
44
+ } else if (command[i] === '|' && command[i + 1] === '|') {
45
+ tokens.push({ type: TokenType.OR, value: '||' });
46
+ i += 2;
47
+ } else if (command[i] === '|') {
48
+ tokens.push({ type: TokenType.PIPE, value: '|' });
49
+ i++;
50
+ } else if (command[i] === ';') {
51
+ tokens.push({ type: TokenType.SEMICOLON, value: ';' });
52
+ i++;
53
+ } else if (command[i] === '(') {
54
+ tokens.push({ type: TokenType.LPAREN, value: '(' });
55
+ i++;
56
+ } else if (command[i] === ')') {
57
+ tokens.push({ type: TokenType.RPAREN, value: ')' });
58
+ i++;
59
+ } else if (command[i] === '>' && command[i + 1] === '>') {
60
+ tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
61
+ i += 2;
62
+ } else if (command[i] === '>') {
63
+ tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
64
+ i++;
65
+ } else if (command[i] === '<') {
66
+ tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
67
+ i++;
68
+ } else {
69
+ // Parse word (respecting quotes)
70
+ let word = '';
71
+ let inQuote = false;
72
+ let quoteChar = '';
73
+
74
+ while (i < command.length) {
75
+ const char = command[i];
76
+
77
+ if (!inQuote) {
78
+ if (char === '"' || char === "'") {
79
+ inQuote = true;
80
+ quoteChar = char;
81
+ word += char;
82
+ i++;
83
+ } else if (/\s/.test(char) ||
84
+ '&|;()<>'.includes(char)) {
85
+ break;
86
+ } else if (char === '\\' && i + 1 < command.length) {
87
+ // Handle escape sequences
88
+ word += char;
89
+ i++;
90
+ if (i < command.length) {
91
+ word += command[i];
92
+ i++;
93
+ }
94
+ } else {
95
+ word += char;
96
+ i++;
97
+ }
98
+ } else {
99
+ if (char === quoteChar && command[i - 1] !== '\\') {
100
+ inQuote = false;
101
+ quoteChar = '';
102
+ word += char;
103
+ i++;
104
+ } else if (char === '\\' && i + 1 < command.length &&
105
+ (command[i + 1] === quoteChar || command[i + 1] === '\\')) {
106
+ // Handle escaped quotes and backslashes inside quotes
107
+ word += char;
108
+ i++;
109
+ if (i < command.length) {
110
+ word += command[i];
111
+ i++;
112
+ }
113
+ } else {
114
+ word += char;
115
+ i++;
116
+ }
117
+ }
118
+ }
119
+
120
+ if (word) {
121
+ tokens.push({ type: TokenType.WORD, value: word });
122
+ }
123
+ }
124
+ }
125
+
126
+ tokens.push({ type: TokenType.EOF, value: '' });
127
+ return tokens;
128
+ }
129
+
130
+ /**
131
+ * Parse a sequence of commands with operators
132
+ */
133
+ class ShellParser {
134
+ constructor(command) {
135
+ this.tokens = tokenize(command);
136
+ this.pos = 0;
137
+ }
138
+
139
+ current() {
140
+ return this.tokens[this.pos] || { type: TokenType.EOF, value: '' };
141
+ }
142
+
143
+ peek() {
144
+ return this.tokens[this.pos + 1] || { type: TokenType.EOF, value: '' };
145
+ }
146
+
147
+ consume() {
148
+ const token = this.current();
149
+ this.pos++;
150
+ return token;
151
+ }
152
+
153
+ /**
154
+ * Parse the main command sequence
155
+ */
156
+ parse() {
157
+ return this.parseSequence();
158
+ }
159
+
160
+ /**
161
+ * Parse a sequence of commands connected by &&, ||, ;
162
+ */
163
+ parseSequence() {
164
+ const commands = [];
165
+ const operators = [];
166
+
167
+ // Parse first command
168
+ let cmd = this.parsePipeline();
169
+ if (cmd) {
170
+ commands.push(cmd);
171
+ }
172
+
173
+ // Parse additional commands with operators
174
+ while (this.current().type !== TokenType.EOF &&
175
+ this.current().type !== TokenType.RPAREN) {
176
+ const op = this.current();
177
+
178
+ if (op.type === TokenType.AND ||
179
+ op.type === TokenType.OR ||
180
+ op.type === TokenType.SEMICOLON) {
181
+ operators.push(op.type);
182
+ this.consume();
183
+
184
+ cmd = this.parsePipeline();
185
+ if (cmd) {
186
+ commands.push(cmd);
187
+ }
188
+ } else {
189
+ break;
190
+ }
191
+ }
192
+
193
+ if (commands.length === 1 && operators.length === 0) {
194
+ return commands[0];
195
+ }
196
+
197
+ return {
198
+ type: 'sequence',
199
+ commands,
200
+ operators
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Parse a pipeline (commands connected by |)
206
+ */
207
+ parsePipeline() {
208
+ const commands = [];
209
+
210
+ let cmd = this.parseCommand();
211
+ if (cmd) {
212
+ commands.push(cmd);
213
+ }
214
+
215
+ while (this.current().type === TokenType.PIPE) {
216
+ this.consume();
217
+ cmd = this.parseCommand();
218
+ if (cmd) {
219
+ commands.push(cmd);
220
+ }
221
+ }
222
+
223
+ if (commands.length === 1) {
224
+ return commands[0];
225
+ }
226
+
227
+ return {
228
+ type: 'pipeline',
229
+ commands
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Parse a single command or subshell
235
+ */
236
+ parseCommand() {
237
+ // Check for subshell
238
+ if (this.current().type === TokenType.LPAREN) {
239
+ this.consume(); // consume (
240
+ const subshell = this.parseSequence();
241
+
242
+ if (this.current().type === TokenType.RPAREN) {
243
+ this.consume(); // consume )
244
+ }
245
+
246
+ return {
247
+ type: 'subshell',
248
+ command: subshell
249
+ };
250
+ }
251
+
252
+ // Parse simple command
253
+ return this.parseSimpleCommand();
254
+ }
255
+
256
+ /**
257
+ * Parse a simple command (command + args + redirections)
258
+ */
259
+ parseSimpleCommand() {
260
+ const words = [];
261
+ const redirects = [];
262
+
263
+ while (this.current().type !== TokenType.EOF) {
264
+ const token = this.current();
265
+
266
+ if (token.type === TokenType.WORD) {
267
+ words.push(token.value);
268
+ this.consume();
269
+ } else if (token.type === TokenType.REDIRECT_OUT ||
270
+ token.type === TokenType.REDIRECT_APPEND ||
271
+ token.type === TokenType.REDIRECT_IN) {
272
+ this.consume();
273
+ const target = this.current();
274
+ if (target.type === TokenType.WORD) {
275
+ redirects.push({
276
+ type: token.type,
277
+ target: target.value
278
+ });
279
+ this.consume();
280
+ }
281
+ } else {
282
+ break;
283
+ }
284
+ }
285
+
286
+ if (words.length === 0) {
287
+ return null;
288
+ }
289
+
290
+ const cmd = words[0];
291
+ const args = words.slice(1).map(word => {
292
+ // Remove quotes if present
293
+ if ((word.startsWith('"') && word.endsWith('"')) ||
294
+ (word.startsWith("'") && word.endsWith("'"))) {
295
+ return {
296
+ value: word.slice(1, -1),
297
+ quoted: true,
298
+ quoteChar: word[0]
299
+ };
300
+ }
301
+ return {
302
+ value: word,
303
+ quoted: false
304
+ };
305
+ });
306
+
307
+ const result = {
308
+ type: 'simple',
309
+ cmd,
310
+ args
311
+ };
312
+
313
+ if (redirects.length > 0) {
314
+ result.redirects = redirects;
315
+ }
316
+
317
+ return result;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Parse a shell command with support for &&, ||, ;, and ()
323
+ */
324
+ export function parseShellCommand(command) {
325
+ try {
326
+ const parser = new ShellParser(command);
327
+ const result = parser.parse();
328
+
329
+ trace('ShellParser', () => `Parsed command | ${JSON.stringify({
330
+ input: command.slice(0, 100),
331
+ result
332
+ }, null, 2)}`);
333
+
334
+ return result;
335
+ } catch (error) {
336
+ trace('ShellParser', () => `Parse error | ${JSON.stringify({
337
+ command: command.slice(0, 100),
338
+ error: error.message
339
+ }, null, 2)}`);
340
+
341
+ // Return null to fallback to sh -c
342
+ return null;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Check if a command needs shell features we don't handle
348
+ */
349
+ export function needsRealShell(command) {
350
+ // Check for features we don't handle yet
351
+ const unsupported = [
352
+ '`', // Command substitution
353
+ '$(', // Command substitution
354
+ '${', // Variable expansion
355
+ '~', // Home expansion (at start of word)
356
+ '*', // Glob patterns
357
+ '?', // Glob patterns
358
+ '[', // Glob patterns
359
+ '2>', // stderr redirection
360
+ '&>', // Combined redirection
361
+ '>&', // File descriptor duplication
362
+ '<<', // Here documents
363
+ '<<<', // Here strings
364
+ ];
365
+
366
+ for (const feature of unsupported) {
367
+ if (command.includes(feature)) {
368
+ return true;
369
+ }
370
+ }
371
+
372
+ return false;
373
+ }
374
+
375
+ export default { parseShellCommand, needsRealShell };