command-stream 0.0.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/$.mjs +1720 -0
- package/LICENSE +24 -0
- package/README.md +776 -0
- package/package.json +50 -0
package/$.mjs
ADDED
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
// Enhanced $ shell utilities with streaming, async iteration, and EventEmitter support
|
|
2
|
+
// Usage patterns:
|
|
3
|
+
// 1. Classic await: const result = await $`command`
|
|
4
|
+
// 2. Async iteration: for await (const chunk of $`command`.stream()) { ... }
|
|
5
|
+
// 3. EventEmitter: $`command`.on('data', chunk => ...).on('end', result => ...)
|
|
6
|
+
// 4. Stream access: $`command`.stdout, $`command`.stderr
|
|
7
|
+
|
|
8
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
9
|
+
|
|
10
|
+
// Global shell settings (like bash set -e / set +e)
|
|
11
|
+
let globalShellSettings = {
|
|
12
|
+
errexit: false, // set -e equivalent: exit on error
|
|
13
|
+
verbose: false, // set -v equivalent: print commands
|
|
14
|
+
xtrace: false, // set -x equivalent: trace execution
|
|
15
|
+
pipefail: false, // set -o pipefail equivalent: pipe failure detection
|
|
16
|
+
nounset: false // set -u equivalent: error on undefined variables
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Virtual command registry - unified system for all commands
|
|
20
|
+
const virtualCommands = new Map();
|
|
21
|
+
|
|
22
|
+
// Global flag to enable/disable virtual commands (for backward compatibility)
|
|
23
|
+
let virtualCommandsEnabled = true;
|
|
24
|
+
|
|
25
|
+
// EventEmitter-like implementation
|
|
26
|
+
class StreamEmitter {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.listeners = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
on(event, listener) {
|
|
32
|
+
if (!this.listeners.has(event)) {
|
|
33
|
+
this.listeners.set(event, []);
|
|
34
|
+
}
|
|
35
|
+
this.listeners.get(event).push(listener);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emit(event, ...args) {
|
|
40
|
+
const eventListeners = this.listeners.get(event);
|
|
41
|
+
if (eventListeners) {
|
|
42
|
+
for (const listener of eventListeners) {
|
|
43
|
+
listener(...args);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
off(event, listener) {
|
|
50
|
+
const eventListeners = this.listeners.get(event);
|
|
51
|
+
if (eventListeners) {
|
|
52
|
+
const index = eventListeners.indexOf(listener);
|
|
53
|
+
if (index !== -1) {
|
|
54
|
+
eventListeners.splice(index, 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function quote(value) {
|
|
62
|
+
if (value == null) return "''";
|
|
63
|
+
if (Array.isArray(value)) return value.map(quote).join(' ');
|
|
64
|
+
if (typeof value !== 'string') value = String(value);
|
|
65
|
+
if (value === '') return "''";
|
|
66
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildShellCommand(strings, values) {
|
|
70
|
+
let out = '';
|
|
71
|
+
for (let i = 0; i < strings.length; i++) {
|
|
72
|
+
out += strings[i];
|
|
73
|
+
if (i < values.length) {
|
|
74
|
+
const v = values[i];
|
|
75
|
+
if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
|
|
76
|
+
out += String(v.raw);
|
|
77
|
+
} else {
|
|
78
|
+
out += quote(v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function asBuffer(chunk) {
|
|
86
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
87
|
+
if (typeof chunk === 'string') return Buffer.from(chunk);
|
|
88
|
+
return Buffer.from(chunk);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function pumpReadable(readable, onChunk) {
|
|
92
|
+
if (!readable) return;
|
|
93
|
+
for await (const chunk of readable) {
|
|
94
|
+
await onChunk(asBuffer(chunk));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Enhanced process runner with streaming capabilities
|
|
99
|
+
class ProcessRunner extends StreamEmitter {
|
|
100
|
+
constructor(spec, options = {}) {
|
|
101
|
+
super();
|
|
102
|
+
this.spec = spec;
|
|
103
|
+
this.options = {
|
|
104
|
+
mirror: true,
|
|
105
|
+
capture: true,
|
|
106
|
+
stdin: 'inherit',
|
|
107
|
+
cwd: undefined,
|
|
108
|
+
env: undefined,
|
|
109
|
+
...options
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
this.outChunks = this.options.capture ? [] : null;
|
|
113
|
+
this.errChunks = this.options.capture ? [] : null;
|
|
114
|
+
this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
|
|
115
|
+
this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
|
|
116
|
+
[Buffer.from(this.options.stdin)] : [];
|
|
117
|
+
|
|
118
|
+
this.result = null;
|
|
119
|
+
this.child = null;
|
|
120
|
+
this.started = false;
|
|
121
|
+
this.finished = false;
|
|
122
|
+
|
|
123
|
+
// Promise for awaiting final result
|
|
124
|
+
this.promise = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async _start() {
|
|
128
|
+
if (this.started) return;
|
|
129
|
+
this.started = true;
|
|
130
|
+
|
|
131
|
+
const { cwd, env, stdin } = this.options;
|
|
132
|
+
|
|
133
|
+
// Handle programmatic pipeline mode
|
|
134
|
+
if (this.spec.mode === 'pipeline') {
|
|
135
|
+
return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if this is a virtual command first
|
|
139
|
+
if (this.spec.mode === 'shell') {
|
|
140
|
+
// Parse the command to check for virtual commands or pipelines
|
|
141
|
+
const parsed = this._parseCommand(this.spec.command);
|
|
142
|
+
if (parsed) {
|
|
143
|
+
if (parsed.type === 'pipeline') {
|
|
144
|
+
return await this._runPipeline(parsed.commands);
|
|
145
|
+
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
146
|
+
return await this._runVirtual(parsed.cmd, parsed.args);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const spawnBun = (argv) => {
|
|
152
|
+
return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
|
|
153
|
+
};
|
|
154
|
+
const spawnNode = async (argv) => {
|
|
155
|
+
const cp = await import('child_process');
|
|
156
|
+
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
160
|
+
|
|
161
|
+
// Shell tracing (set -x equivalent)
|
|
162
|
+
if (globalShellSettings.xtrace) {
|
|
163
|
+
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
164
|
+
console.log(`+ ${traceCmd}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Verbose mode (set -v equivalent)
|
|
168
|
+
if (globalShellSettings.verbose) {
|
|
169
|
+
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
170
|
+
console.log(verboseCmd);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
|
|
174
|
+
const preferNodeForInput = isBun && needsExplicitPipe;
|
|
175
|
+
this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
|
|
176
|
+
|
|
177
|
+
// Setup stdout streaming
|
|
178
|
+
const outPump = pumpReadable(this.child.stdout, async (buf) => {
|
|
179
|
+
if (this.options.capture) this.outChunks.push(buf);
|
|
180
|
+
if (this.options.mirror) process.stdout.write(buf);
|
|
181
|
+
|
|
182
|
+
// Emit chunk events
|
|
183
|
+
this.emit('stdout', buf);
|
|
184
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Setup stderr streaming
|
|
188
|
+
const errPump = pumpReadable(this.child.stderr, async (buf) => {
|
|
189
|
+
if (this.options.capture) this.errChunks.push(buf);
|
|
190
|
+
if (this.options.mirror) process.stderr.write(buf);
|
|
191
|
+
|
|
192
|
+
// Emit chunk events
|
|
193
|
+
this.emit('stderr', buf);
|
|
194
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Handle stdin
|
|
198
|
+
let stdinPumpPromise = Promise.resolve();
|
|
199
|
+
if (stdin === 'inherit') {
|
|
200
|
+
const isPipedIn = process.stdin && process.stdin.isTTY === false;
|
|
201
|
+
if (isPipedIn) {
|
|
202
|
+
stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
|
|
203
|
+
} else {
|
|
204
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') {
|
|
205
|
+
try { this.child.stdin.end(); } catch {}
|
|
206
|
+
} else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
|
|
207
|
+
try { const w = this.child.stdin.getWriter(); await w.close(); } catch {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else if (stdin === 'ignore') {
|
|
211
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') this.child.stdin.end();
|
|
212
|
+
} else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
|
|
213
|
+
const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
|
|
214
|
+
if (this.options.capture && this.inChunks) this.inChunks.push(Buffer.from(buf));
|
|
215
|
+
stdinPumpPromise = this._writeToStdin(buf);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const exited = isBun ? this.child.exited : new Promise((resolve) => this.child.on('close', resolve));
|
|
219
|
+
const code = await exited;
|
|
220
|
+
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
221
|
+
|
|
222
|
+
this.result = {
|
|
223
|
+
code,
|
|
224
|
+
stdout: this.options.capture ? Buffer.concat(this.outChunks).toString('utf8') : undefined,
|
|
225
|
+
stderr: this.options.capture ? Buffer.concat(this.errChunks).toString('utf8') : undefined,
|
|
226
|
+
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
227
|
+
child: this.child
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
this.finished = true;
|
|
231
|
+
this.emit('end', this.result);
|
|
232
|
+
this.emit('exit', this.result.code);
|
|
233
|
+
|
|
234
|
+
// Handle shell settings (set -e equivalent)
|
|
235
|
+
if (globalShellSettings.errexit && this.result.code !== 0) {
|
|
236
|
+
const error = new Error(`Command failed with exit code ${this.result.code}`);
|
|
237
|
+
error.code = this.result.code;
|
|
238
|
+
error.stdout = this.result.stdout;
|
|
239
|
+
error.stderr = this.result.stderr;
|
|
240
|
+
error.result = this.result;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return this.result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async _pumpStdinTo(child, captureChunks) {
|
|
248
|
+
if (!child.stdin) return;
|
|
249
|
+
const bunWriter = isBun && child.stdin && typeof child.stdin.getWriter === 'function' ? child.stdin.getWriter() : null;
|
|
250
|
+
for await (const chunk of process.stdin) {
|
|
251
|
+
const buf = asBuffer(chunk);
|
|
252
|
+
captureChunks && captureChunks.push(buf);
|
|
253
|
+
if (bunWriter) await bunWriter.write(buf);
|
|
254
|
+
else if (typeof child.stdin.write === 'function') child.stdin.write(buf);
|
|
255
|
+
else if (isBun && typeof Bun.write === 'function') await Bun.write(child.stdin, buf);
|
|
256
|
+
}
|
|
257
|
+
if (bunWriter) await bunWriter.close();
|
|
258
|
+
else if (typeof child.stdin.end === 'function') child.stdin.end();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async _writeToStdin(buf) {
|
|
262
|
+
if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
|
|
263
|
+
const w = this.child.stdin.getWriter();
|
|
264
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
265
|
+
await w.write(bytes);
|
|
266
|
+
await w.close();
|
|
267
|
+
} else if (this.child.stdin && typeof this.child.stdin.write === 'function') {
|
|
268
|
+
this.child.stdin.end(buf);
|
|
269
|
+
} else if (isBun && typeof Bun.write === 'function') {
|
|
270
|
+
await Bun.write(this.child.stdin, buf);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_parseCommand(command) {
|
|
275
|
+
const trimmed = command.trim();
|
|
276
|
+
if (!trimmed) return null;
|
|
277
|
+
|
|
278
|
+
// Check for pipes
|
|
279
|
+
if (trimmed.includes('|')) {
|
|
280
|
+
return this._parsePipeline(trimmed);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Simple command parsing
|
|
284
|
+
const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
285
|
+
if (parts.length === 0) return null;
|
|
286
|
+
|
|
287
|
+
const cmd = parts[0];
|
|
288
|
+
const args = parts.slice(1).map(arg => {
|
|
289
|
+
// Remove quotes if present
|
|
290
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
291
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
292
|
+
return arg.slice(1, -1);
|
|
293
|
+
}
|
|
294
|
+
return arg;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return { cmd, args, type: 'simple' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_parsePipeline(command) {
|
|
301
|
+
// Split by pipe, respecting quotes
|
|
302
|
+
const segments = [];
|
|
303
|
+
let current = '';
|
|
304
|
+
let inQuotes = false;
|
|
305
|
+
let quoteChar = '';
|
|
306
|
+
|
|
307
|
+
for (let i = 0; i < command.length; i++) {
|
|
308
|
+
const char = command[i];
|
|
309
|
+
|
|
310
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
311
|
+
inQuotes = true;
|
|
312
|
+
quoteChar = char;
|
|
313
|
+
current += char;
|
|
314
|
+
} else if (inQuotes && char === quoteChar) {
|
|
315
|
+
inQuotes = false;
|
|
316
|
+
quoteChar = '';
|
|
317
|
+
current += char;
|
|
318
|
+
} else if (!inQuotes && char === '|') {
|
|
319
|
+
segments.push(current.trim());
|
|
320
|
+
current = '';
|
|
321
|
+
} else {
|
|
322
|
+
current += char;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (current.trim()) {
|
|
327
|
+
segments.push(current.trim());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Parse each segment as a simple command
|
|
331
|
+
const commands = segments.map(segment => {
|
|
332
|
+
const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
333
|
+
if (parts.length === 0) return null;
|
|
334
|
+
|
|
335
|
+
const cmd = parts[0];
|
|
336
|
+
const args = parts.slice(1).map(arg => {
|
|
337
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
338
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
339
|
+
return arg.slice(1, -1);
|
|
340
|
+
}
|
|
341
|
+
return arg;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return { cmd, args };
|
|
345
|
+
}).filter(Boolean);
|
|
346
|
+
|
|
347
|
+
return { type: 'pipeline', commands };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async _runVirtual(cmd, args) {
|
|
351
|
+
const handler = virtualCommands.get(cmd);
|
|
352
|
+
if (!handler) {
|
|
353
|
+
throw new Error(`Virtual command not found: ${cmd}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Prepare stdin
|
|
358
|
+
let stdinData = '';
|
|
359
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
360
|
+
stdinData = this.options.stdin;
|
|
361
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
362
|
+
stdinData = this.options.stdin.toString('utf8');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Shell tracing for virtual commands
|
|
366
|
+
if (globalShellSettings.xtrace) {
|
|
367
|
+
console.log(`+ ${cmd} ${args.join(' ')}`);
|
|
368
|
+
}
|
|
369
|
+
if (globalShellSettings.verbose) {
|
|
370
|
+
console.log(`${cmd} ${args.join(' ')}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Execute the virtual command
|
|
374
|
+
let result;
|
|
375
|
+
|
|
376
|
+
// Check if handler is async generator (streaming)
|
|
377
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
378
|
+
// Handle streaming virtual command
|
|
379
|
+
const chunks = [];
|
|
380
|
+
for await (const chunk of handler(args, stdinData, this.options)) {
|
|
381
|
+
const buf = Buffer.from(chunk);
|
|
382
|
+
chunks.push(buf);
|
|
383
|
+
|
|
384
|
+
if (this.options.mirror) {
|
|
385
|
+
process.stdout.write(buf);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.emit('stdout', buf);
|
|
389
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
result = {
|
|
393
|
+
code: 0,
|
|
394
|
+
stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
|
|
395
|
+
stderr: this.options.capture ? '' : undefined,
|
|
396
|
+
stdin: this.options.capture ? stdinData : undefined
|
|
397
|
+
};
|
|
398
|
+
} else {
|
|
399
|
+
// Regular async function
|
|
400
|
+
result = await handler(args, stdinData, this.options);
|
|
401
|
+
|
|
402
|
+
// Ensure result has required fields, respecting capture option
|
|
403
|
+
result = {
|
|
404
|
+
code: result.code ?? 0,
|
|
405
|
+
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
406
|
+
stderr: this.options.capture ? (result.stderr ?? '') : undefined,
|
|
407
|
+
stdin: this.options.capture ? stdinData : undefined,
|
|
408
|
+
...result
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Mirror and emit output
|
|
412
|
+
if (result.stdout) {
|
|
413
|
+
const buf = Buffer.from(result.stdout);
|
|
414
|
+
if (this.options.mirror) {
|
|
415
|
+
process.stdout.write(buf);
|
|
416
|
+
}
|
|
417
|
+
this.emit('stdout', buf);
|
|
418
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (result.stderr) {
|
|
422
|
+
const buf = Buffer.from(result.stderr);
|
|
423
|
+
if (this.options.mirror) {
|
|
424
|
+
process.stderr.write(buf);
|
|
425
|
+
}
|
|
426
|
+
this.emit('stderr', buf);
|
|
427
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Store result
|
|
432
|
+
this.result = result;
|
|
433
|
+
this.finished = true;
|
|
434
|
+
|
|
435
|
+
// Emit completion events
|
|
436
|
+
this.emit('end', result);
|
|
437
|
+
this.emit('exit', result.code);
|
|
438
|
+
|
|
439
|
+
// Handle shell settings
|
|
440
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
441
|
+
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
442
|
+
error.code = result.code;
|
|
443
|
+
error.stdout = result.stdout;
|
|
444
|
+
error.stderr = result.stderr;
|
|
445
|
+
error.result = result;
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return result;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
// Handle errors from virtual commands
|
|
452
|
+
const result = {
|
|
453
|
+
code: error.code ?? 1,
|
|
454
|
+
stdout: error.stdout ?? '',
|
|
455
|
+
stderr: error.stderr ?? error.message,
|
|
456
|
+
stdin: ''
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
this.result = result;
|
|
460
|
+
this.finished = true;
|
|
461
|
+
|
|
462
|
+
if (result.stderr) {
|
|
463
|
+
const buf = Buffer.from(result.stderr);
|
|
464
|
+
if (this.options.mirror) {
|
|
465
|
+
process.stderr.write(buf);
|
|
466
|
+
}
|
|
467
|
+
this.emit('stderr', buf);
|
|
468
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.emit('end', result);
|
|
472
|
+
this.emit('exit', result.code);
|
|
473
|
+
|
|
474
|
+
if (globalShellSettings.errexit) {
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async _runPipeline(commands) {
|
|
483
|
+
if (commands.length === 0) {
|
|
484
|
+
return { code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let currentOutput = '';
|
|
488
|
+
let currentInput = '';
|
|
489
|
+
|
|
490
|
+
// Get initial stdin from options
|
|
491
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
492
|
+
currentInput = this.options.stdin;
|
|
493
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
494
|
+
currentInput = this.options.stdin.toString('utf8');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Execute each command in the pipeline
|
|
498
|
+
for (let i = 0; i < commands.length; i++) {
|
|
499
|
+
const command = commands[i];
|
|
500
|
+
const { cmd, args } = command;
|
|
501
|
+
|
|
502
|
+
// Check if this is a virtual command
|
|
503
|
+
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
504
|
+
// Run virtual command with current input
|
|
505
|
+
const handler = virtualCommands.get(cmd);
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
// Shell tracing for virtual commands
|
|
509
|
+
if (globalShellSettings.xtrace) {
|
|
510
|
+
console.log(`+ ${cmd} ${args.join(' ')}`);
|
|
511
|
+
}
|
|
512
|
+
if (globalShellSettings.verbose) {
|
|
513
|
+
console.log(`${cmd} ${args.join(' ')}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let result;
|
|
517
|
+
|
|
518
|
+
// Check if handler is async generator (streaming)
|
|
519
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
520
|
+
const chunks = [];
|
|
521
|
+
for await (const chunk of handler(args, currentInput, this.options)) {
|
|
522
|
+
chunks.push(Buffer.from(chunk));
|
|
523
|
+
}
|
|
524
|
+
result = {
|
|
525
|
+
code: 0,
|
|
526
|
+
stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
|
|
527
|
+
stderr: this.options.capture ? '' : undefined,
|
|
528
|
+
stdin: this.options.capture ? currentInput : undefined
|
|
529
|
+
};
|
|
530
|
+
} else {
|
|
531
|
+
// Regular async function
|
|
532
|
+
result = await handler(args, currentInput, this.options);
|
|
533
|
+
result = {
|
|
534
|
+
code: result.code ?? 0,
|
|
535
|
+
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
536
|
+
stderr: this.options.capture ? (result.stderr ?? '') : undefined,
|
|
537
|
+
stdin: this.options.capture ? currentInput : undefined,
|
|
538
|
+
...result
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// If this isn't the last command, pass stdout as stdin to next command
|
|
543
|
+
if (i < commands.length - 1) {
|
|
544
|
+
currentInput = result.stdout;
|
|
545
|
+
} else {
|
|
546
|
+
// This is the last command - emit output and store final result
|
|
547
|
+
currentOutput = result.stdout;
|
|
548
|
+
|
|
549
|
+
// Mirror and emit output for final command
|
|
550
|
+
if (result.stdout) {
|
|
551
|
+
const buf = Buffer.from(result.stdout);
|
|
552
|
+
if (this.options.mirror) {
|
|
553
|
+
process.stdout.write(buf);
|
|
554
|
+
}
|
|
555
|
+
this.emit('stdout', buf);
|
|
556
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (result.stderr) {
|
|
560
|
+
const buf = Buffer.from(result.stderr);
|
|
561
|
+
if (this.options.mirror) {
|
|
562
|
+
process.stderr.write(buf);
|
|
563
|
+
}
|
|
564
|
+
this.emit('stderr', buf);
|
|
565
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Store final result
|
|
569
|
+
const finalResult = {
|
|
570
|
+
code: result.code,
|
|
571
|
+
stdout: currentOutput,
|
|
572
|
+
stderr: result.stderr,
|
|
573
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
574
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
this.result = finalResult;
|
|
578
|
+
this.finished = true;
|
|
579
|
+
|
|
580
|
+
// Emit completion events
|
|
581
|
+
this.emit('end', finalResult);
|
|
582
|
+
this.emit('exit', finalResult.code);
|
|
583
|
+
|
|
584
|
+
// Handle shell settings
|
|
585
|
+
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
586
|
+
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
587
|
+
error.code = finalResult.code;
|
|
588
|
+
error.stdout = finalResult.stdout;
|
|
589
|
+
error.stderr = finalResult.stderr;
|
|
590
|
+
error.result = finalResult;
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return finalResult;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle errors from intermediate commands
|
|
598
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
599
|
+
const error = new Error(`Pipeline command failed with exit code ${result.code}`);
|
|
600
|
+
error.code = result.code;
|
|
601
|
+
error.stdout = result.stdout;
|
|
602
|
+
error.stderr = result.stderr;
|
|
603
|
+
error.result = result;
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
// Handle errors from virtual commands in pipeline
|
|
608
|
+
const result = {
|
|
609
|
+
code: error.code ?? 1,
|
|
610
|
+
stdout: currentOutput,
|
|
611
|
+
stderr: error.stderr ?? error.message,
|
|
612
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
613
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
this.result = result;
|
|
617
|
+
this.finished = true;
|
|
618
|
+
|
|
619
|
+
if (result.stderr) {
|
|
620
|
+
const buf = Buffer.from(result.stderr);
|
|
621
|
+
if (this.options.mirror) {
|
|
622
|
+
process.stderr.write(buf);
|
|
623
|
+
}
|
|
624
|
+
this.emit('stderr', buf);
|
|
625
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this.emit('end', result);
|
|
629
|
+
this.emit('exit', result.code);
|
|
630
|
+
|
|
631
|
+
if (globalShellSettings.errexit) {
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
// For system commands in pipeline, we would need to spawn processes
|
|
639
|
+
// For now, return an error indicating this isn't supported
|
|
640
|
+
const result = {
|
|
641
|
+
code: 1,
|
|
642
|
+
stdout: currentOutput,
|
|
643
|
+
stderr: `Pipeline with system command '${cmd}' not yet supported`,
|
|
644
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
645
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
this.result = result;
|
|
649
|
+
this.finished = true;
|
|
650
|
+
|
|
651
|
+
const buf = Buffer.from(result.stderr);
|
|
652
|
+
if (this.options.mirror) {
|
|
653
|
+
process.stderr.write(buf);
|
|
654
|
+
}
|
|
655
|
+
this.emit('stderr', buf);
|
|
656
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
657
|
+
|
|
658
|
+
this.emit('end', result);
|
|
659
|
+
this.emit('exit', result.code);
|
|
660
|
+
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Run programmatic pipeline (.pipe() method)
|
|
667
|
+
async _runProgrammaticPipeline(source, destination) {
|
|
668
|
+
try {
|
|
669
|
+
// Execute the source command first
|
|
670
|
+
const sourceResult = await source;
|
|
671
|
+
|
|
672
|
+
if (sourceResult.code !== 0) {
|
|
673
|
+
// If source failed, return its result
|
|
674
|
+
return sourceResult;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Set the destination's stdin to the source's stdout
|
|
678
|
+
destination.options = {
|
|
679
|
+
...destination.options,
|
|
680
|
+
stdin: sourceResult.stdout
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Execute the destination command
|
|
684
|
+
const destResult = await destination;
|
|
685
|
+
|
|
686
|
+
// Return the final result with combined information
|
|
687
|
+
return {
|
|
688
|
+
code: destResult.code,
|
|
689
|
+
stdout: destResult.stdout,
|
|
690
|
+
stderr: sourceResult.stderr + destResult.stderr,
|
|
691
|
+
stdin: sourceResult.stdin
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
} catch (error) {
|
|
695
|
+
const result = {
|
|
696
|
+
code: error.code ?? 1,
|
|
697
|
+
stdout: '',
|
|
698
|
+
stderr: error.message || 'Pipeline execution failed',
|
|
699
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
700
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
this.result = result;
|
|
704
|
+
this.finished = true;
|
|
705
|
+
|
|
706
|
+
const buf = Buffer.from(result.stderr);
|
|
707
|
+
if (this.options.mirror) {
|
|
708
|
+
process.stderr.write(buf);
|
|
709
|
+
}
|
|
710
|
+
this.emit('stderr', buf);
|
|
711
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
712
|
+
|
|
713
|
+
this.emit('end', result);
|
|
714
|
+
this.emit('exit', result.code);
|
|
715
|
+
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Async iteration support
|
|
721
|
+
async* stream() {
|
|
722
|
+
if (!this.started) {
|
|
723
|
+
this._start(); // Start but don't await
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let buffer = [];
|
|
727
|
+
let resolve, reject;
|
|
728
|
+
let ended = false;
|
|
729
|
+
|
|
730
|
+
const onData = (chunk) => {
|
|
731
|
+
buffer.push(chunk);
|
|
732
|
+
if (resolve) {
|
|
733
|
+
resolve();
|
|
734
|
+
resolve = reject = null;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const onEnd = () => {
|
|
739
|
+
ended = true;
|
|
740
|
+
if (resolve) {
|
|
741
|
+
resolve();
|
|
742
|
+
resolve = reject = null;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
this.on('data', onData);
|
|
747
|
+
this.on('end', onEnd);
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
while (!ended || buffer.length > 0) {
|
|
751
|
+
if (buffer.length > 0) {
|
|
752
|
+
yield buffer.shift();
|
|
753
|
+
} else if (!ended) {
|
|
754
|
+
await new Promise((res, rej) => {
|
|
755
|
+
resolve = res;
|
|
756
|
+
reject = rej;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} finally {
|
|
761
|
+
this.off('data', onData);
|
|
762
|
+
this.off('end', onEnd);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Programmatic piping support
|
|
767
|
+
pipe(destination) {
|
|
768
|
+
// If destination is a ProcessRunner, create a pipeline
|
|
769
|
+
if (destination instanceof ProcessRunner) {
|
|
770
|
+
// Create a new ProcessRunner that represents the piped operation
|
|
771
|
+
const pipeSpec = {
|
|
772
|
+
mode: 'pipeline',
|
|
773
|
+
source: this,
|
|
774
|
+
destination: destination
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
return new ProcessRunner(pipeSpec, {
|
|
778
|
+
...this.options,
|
|
779
|
+
capture: destination.options.capture ?? true
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// If destination is a template literal result (from $`command`), use its spec
|
|
784
|
+
if (destination && destination.spec) {
|
|
785
|
+
const destRunner = new ProcessRunner(destination.spec, destination.options);
|
|
786
|
+
return this.pipe(destRunner);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Promise interface (for await)
|
|
793
|
+
then(onFulfilled, onRejected) {
|
|
794
|
+
if (!this.promise) {
|
|
795
|
+
this.promise = this._start();
|
|
796
|
+
}
|
|
797
|
+
return this.promise.then(onFulfilled, onRejected);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
catch(onRejected) {
|
|
801
|
+
if (!this.promise) {
|
|
802
|
+
this.promise = this._start();
|
|
803
|
+
}
|
|
804
|
+
return this.promise.catch(onRejected);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
finally(onFinally) {
|
|
808
|
+
if (!this.promise) {
|
|
809
|
+
this.promise = this._start();
|
|
810
|
+
}
|
|
811
|
+
return this.promise.finally(onFinally);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Synchronous execution
|
|
815
|
+
sync() {
|
|
816
|
+
if (this.started) {
|
|
817
|
+
throw new Error('Command already started - cannot run sync after async start');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const { cwd, env, stdin } = this.options;
|
|
821
|
+
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
822
|
+
|
|
823
|
+
// Shell tracing (set -x equivalent)
|
|
824
|
+
if (globalShellSettings.xtrace) {
|
|
825
|
+
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
826
|
+
console.log(`+ ${traceCmd}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Verbose mode (set -v equivalent)
|
|
830
|
+
if (globalShellSettings.verbose) {
|
|
831
|
+
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
832
|
+
console.log(verboseCmd);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let result;
|
|
836
|
+
|
|
837
|
+
if (isBun) {
|
|
838
|
+
// Use Bun's synchronous spawn
|
|
839
|
+
const proc = Bun.spawnSync(argv, {
|
|
840
|
+
cwd,
|
|
841
|
+
env,
|
|
842
|
+
stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
|
|
843
|
+
Buffer.isBuffer(stdin) ? stdin :
|
|
844
|
+
stdin === 'ignore' ? undefined : undefined,
|
|
845
|
+
stdout: 'pipe',
|
|
846
|
+
stderr: 'pipe'
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
result = {
|
|
850
|
+
code: proc.exitCode || 0,
|
|
851
|
+
stdout: proc.stdout?.toString('utf8') || '',
|
|
852
|
+
stderr: proc.stderr?.toString('utf8') || '',
|
|
853
|
+
stdin: typeof stdin === 'string' ? stdin :
|
|
854
|
+
Buffer.isBuffer(stdin) ? stdin.toString('utf8') : '',
|
|
855
|
+
child: proc
|
|
856
|
+
};
|
|
857
|
+
} else {
|
|
858
|
+
// Use Node's synchronous spawn
|
|
859
|
+
const cp = require('child_process');
|
|
860
|
+
const proc = cp.spawnSync(argv[0], argv.slice(1), {
|
|
861
|
+
cwd,
|
|
862
|
+
env,
|
|
863
|
+
input: typeof stdin === 'string' ? stdin :
|
|
864
|
+
Buffer.isBuffer(stdin) ? stdin : undefined,
|
|
865
|
+
encoding: 'utf8',
|
|
866
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
result = {
|
|
870
|
+
code: proc.status || 0,
|
|
871
|
+
stdout: proc.stdout || '',
|
|
872
|
+
stderr: proc.stderr || '',
|
|
873
|
+
stdin: typeof stdin === 'string' ? stdin :
|
|
874
|
+
Buffer.isBuffer(stdin) ? stdin.toString('utf8') : '',
|
|
875
|
+
child: proc
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Mirror output if requested (but always capture for result)
|
|
880
|
+
if (this.options.mirror) {
|
|
881
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
882
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Store chunks for events (batched after completion)
|
|
886
|
+
this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
|
|
887
|
+
this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
|
|
888
|
+
|
|
889
|
+
this.result = result;
|
|
890
|
+
this.finished = true;
|
|
891
|
+
|
|
892
|
+
// Emit batched events after completion
|
|
893
|
+
if (result.stdout) {
|
|
894
|
+
const stdoutBuf = Buffer.from(result.stdout);
|
|
895
|
+
this.emit('stdout', stdoutBuf);
|
|
896
|
+
this.emit('data', { type: 'stdout', data: stdoutBuf });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (result.stderr) {
|
|
900
|
+
const stderrBuf = Buffer.from(result.stderr);
|
|
901
|
+
this.emit('stderr', stderrBuf);
|
|
902
|
+
this.emit('data', { type: 'stderr', data: stderrBuf });
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
this.emit('end', result);
|
|
906
|
+
this.emit('exit', result.code);
|
|
907
|
+
|
|
908
|
+
// Handle shell settings (set -e equivalent)
|
|
909
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
910
|
+
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
911
|
+
error.code = result.code;
|
|
912
|
+
error.stdout = result.stdout;
|
|
913
|
+
error.stderr = result.stderr;
|
|
914
|
+
error.result = result;
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Stream properties
|
|
922
|
+
get stdout() {
|
|
923
|
+
return this.child?.stdout;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
get stderr() {
|
|
927
|
+
return this.child?.stderr;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
get stdin() {
|
|
931
|
+
return this.child?.stdin;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Public APIs
|
|
936
|
+
async function sh(commandString, options = {}) {
|
|
937
|
+
const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
|
|
938
|
+
return runner._start();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function exec(file, args = [], options = {}) {
|
|
942
|
+
const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
|
|
943
|
+
return runner._start();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function run(commandOrTokens, options = {}) {
|
|
947
|
+
if (typeof commandOrTokens === 'string') {
|
|
948
|
+
return sh(commandOrTokens, { ...options, mirror: false, capture: true });
|
|
949
|
+
}
|
|
950
|
+
const [file, ...args] = commandOrTokens;
|
|
951
|
+
return exec(file, args, { ...options, mirror: false, capture: true });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Enhanced tagged template that returns ProcessRunner
|
|
955
|
+
function $tagged(strings, ...values) {
|
|
956
|
+
const cmd = buildShellCommand(strings, values);
|
|
957
|
+
return new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function create(defaultOptions = {}) {
|
|
961
|
+
const tagged = (strings, ...values) => {
|
|
962
|
+
const cmd = buildShellCommand(strings, values);
|
|
963
|
+
return new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
|
|
964
|
+
};
|
|
965
|
+
return tagged;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function raw(value) {
|
|
969
|
+
return { raw: String(value) };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Shell setting control functions (like bash set/unset)
|
|
973
|
+
function set(option) {
|
|
974
|
+
const mapping = {
|
|
975
|
+
'e': 'errexit', // set -e: exit on error
|
|
976
|
+
'errexit': 'errexit',
|
|
977
|
+
'v': 'verbose', // set -v: verbose
|
|
978
|
+
'verbose': 'verbose',
|
|
979
|
+
'x': 'xtrace', // set -x: trace execution
|
|
980
|
+
'xtrace': 'xtrace',
|
|
981
|
+
'u': 'nounset', // set -u: error on unset vars
|
|
982
|
+
'nounset': 'nounset',
|
|
983
|
+
'o pipefail': 'pipefail', // set -o pipefail
|
|
984
|
+
'pipefail': 'pipefail'
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
if (mapping[option]) {
|
|
988
|
+
globalShellSettings[mapping[option]] = true;
|
|
989
|
+
if (globalShellSettings.verbose) {
|
|
990
|
+
console.log(`+ set -${option}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return globalShellSettings;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function unset(option) {
|
|
997
|
+
const mapping = {
|
|
998
|
+
'e': 'errexit',
|
|
999
|
+
'errexit': 'errexit',
|
|
1000
|
+
'v': 'verbose',
|
|
1001
|
+
'verbose': 'verbose',
|
|
1002
|
+
'x': 'xtrace',
|
|
1003
|
+
'xtrace': 'xtrace',
|
|
1004
|
+
'u': 'nounset',
|
|
1005
|
+
'nounset': 'nounset',
|
|
1006
|
+
'o pipefail': 'pipefail',
|
|
1007
|
+
'pipefail': 'pipefail'
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (mapping[option]) {
|
|
1011
|
+
globalShellSettings[mapping[option]] = false;
|
|
1012
|
+
if (globalShellSettings.verbose) {
|
|
1013
|
+
console.log(`+ set +${option}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return globalShellSettings;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Convenience functions for common patterns
|
|
1020
|
+
const shell = {
|
|
1021
|
+
set,
|
|
1022
|
+
unset,
|
|
1023
|
+
settings: () => ({ ...globalShellSettings }),
|
|
1024
|
+
|
|
1025
|
+
// Bash-like shortcuts
|
|
1026
|
+
errexit: (enable = true) => enable ? set('e') : unset('e'),
|
|
1027
|
+
verbose: (enable = true) => enable ? set('v') : unset('v'),
|
|
1028
|
+
xtrace: (enable = true) => enable ? set('x') : unset('x'),
|
|
1029
|
+
pipefail: (enable = true) => enable ? set('o pipefail') : unset('o pipefail'),
|
|
1030
|
+
nounset: (enable = true) => enable ? set('u') : unset('u'),
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// Virtual command registration API
|
|
1034
|
+
function register(name, handler) {
|
|
1035
|
+
virtualCommands.set(name, handler);
|
|
1036
|
+
return virtualCommands;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function unregister(name) {
|
|
1040
|
+
return virtualCommands.delete(name);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function listCommands() {
|
|
1044
|
+
return Array.from(virtualCommands.keys());
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function enableVirtualCommands() {
|
|
1048
|
+
virtualCommandsEnabled = true;
|
|
1049
|
+
return virtualCommandsEnabled;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function disableVirtualCommands() {
|
|
1053
|
+
virtualCommandsEnabled = false;
|
|
1054
|
+
return virtualCommandsEnabled;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Built-in commands that match Bun.$ functionality
|
|
1058
|
+
function registerBuiltins() {
|
|
1059
|
+
// cd - change directory
|
|
1060
|
+
register('cd', async (args) => {
|
|
1061
|
+
const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
|
|
1062
|
+
try {
|
|
1063
|
+
process.chdir(target);
|
|
1064
|
+
return { stdout: process.cwd(), code: 0 };
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// pwd - print working directory
|
|
1071
|
+
register('pwd', async (args, stdin, options) => {
|
|
1072
|
+
// If cwd option is provided, return that instead of process.cwd()
|
|
1073
|
+
const dir = options?.cwd || process.cwd();
|
|
1074
|
+
return { stdout: dir, code: 0 };
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// echo - print arguments
|
|
1078
|
+
register('echo', async (args) => {
|
|
1079
|
+
let output = args.join(' ');
|
|
1080
|
+
if (args.includes('-n')) {
|
|
1081
|
+
// Don't add newline
|
|
1082
|
+
output = args.filter(arg => arg !== '-n').join(' ');
|
|
1083
|
+
} else {
|
|
1084
|
+
output += '\n';
|
|
1085
|
+
}
|
|
1086
|
+
return { stdout: output, code: 0 };
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// sleep - wait for specified time
|
|
1090
|
+
register('sleep', async (args) => {
|
|
1091
|
+
const seconds = parseFloat(args[0] || 0);
|
|
1092
|
+
if (isNaN(seconds) || seconds < 0) {
|
|
1093
|
+
return { stderr: 'sleep: invalid time interval', code: 1 };
|
|
1094
|
+
}
|
|
1095
|
+
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
1096
|
+
return { stdout: '', code: 0 };
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// true - always succeed
|
|
1100
|
+
register('true', async () => {
|
|
1101
|
+
return { stdout: '', code: 0 };
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// false - always fail
|
|
1105
|
+
register('false', async () => {
|
|
1106
|
+
return { stdout: '', code: 1 };
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// which - locate command
|
|
1110
|
+
register('which', async (args) => {
|
|
1111
|
+
if (args.length === 0) {
|
|
1112
|
+
return { stderr: 'which: missing operand', code: 1 };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const cmd = args[0];
|
|
1116
|
+
|
|
1117
|
+
// Check virtual commands first
|
|
1118
|
+
if (virtualCommands.has(cmd)) {
|
|
1119
|
+
return { stdout: `${cmd}: shell builtin\n`, code: 0 };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Check PATH for system commands
|
|
1123
|
+
const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
|
|
1124
|
+
const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
|
|
1125
|
+
|
|
1126
|
+
for (const path of paths) {
|
|
1127
|
+
for (const ext of extensions) {
|
|
1128
|
+
const fullPath = require('path').join(path, cmd + ext);
|
|
1129
|
+
try {
|
|
1130
|
+
if (require('fs').statSync(fullPath).isFile()) {
|
|
1131
|
+
return { stdout: fullPath, code: 0 };
|
|
1132
|
+
}
|
|
1133
|
+
} catch {}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return { stderr: `which: no ${cmd} in PATH`, code: 1 };
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
// exit - exit with code
|
|
1141
|
+
register('exit', async (args) => {
|
|
1142
|
+
const code = parseInt(args[0] || 0);
|
|
1143
|
+
if (globalShellSettings.errexit || code !== 0) {
|
|
1144
|
+
// For virtual commands, we simulate exit by returning the code
|
|
1145
|
+
return { stdout: '', code };
|
|
1146
|
+
}
|
|
1147
|
+
return { stdout: '', code: 0 };
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// env - print environment variables
|
|
1151
|
+
register('env', async (args, stdin, options) => {
|
|
1152
|
+
if (args.length === 0) {
|
|
1153
|
+
// Use custom env if provided, otherwise use process.env
|
|
1154
|
+
const env = options?.env || process.env;
|
|
1155
|
+
const output = Object.entries(env)
|
|
1156
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1157
|
+
.join('\n') + '\n';
|
|
1158
|
+
return { stdout: output, code: 0 };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// TODO: Support env VAR=value command syntax
|
|
1162
|
+
return { stderr: 'env: command execution not yet supported', code: 1 };
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// cat - read and display file contents
|
|
1166
|
+
register('cat', async (args, stdin, options) => {
|
|
1167
|
+
if (args.length === 0) {
|
|
1168
|
+
// Read from stdin if no files specified
|
|
1169
|
+
return { stdout: stdin || '', code: 0 };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
const fs = await import('fs');
|
|
1174
|
+
const path = await import('path');
|
|
1175
|
+
let output = '';
|
|
1176
|
+
|
|
1177
|
+
for (const filename of args) {
|
|
1178
|
+
// Handle special flags
|
|
1179
|
+
if (filename === '-n') continue; // Line numbering (basic support)
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
// Resolve path relative to cwd if provided
|
|
1183
|
+
const basePath = options?.cwd || process.cwd();
|
|
1184
|
+
const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
|
|
1185
|
+
|
|
1186
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
1187
|
+
output += content;
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
// Format error message to match bash/sh style
|
|
1190
|
+
const errorMsg = error.code === 'ENOENT' ? 'No such file or directory' : error.message;
|
|
1191
|
+
return {
|
|
1192
|
+
stderr: `cat: ${filename}: ${errorMsg}`,
|
|
1193
|
+
stdout: output,
|
|
1194
|
+
code: 1
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return { stdout: output, code: 0 };
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
return { stderr: `cat: ${error.message}`, code: 1 };
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// ls - list directory contents
|
|
1206
|
+
register('ls', async (args, stdin, options) => {
|
|
1207
|
+
try {
|
|
1208
|
+
const fs = await import('fs');
|
|
1209
|
+
const path = await import('path');
|
|
1210
|
+
|
|
1211
|
+
// Parse flags and paths
|
|
1212
|
+
const flags = args.filter(arg => arg.startsWith('-'));
|
|
1213
|
+
const paths = args.filter(arg => !arg.startsWith('-'));
|
|
1214
|
+
const isLongFormat = flags.includes('-l');
|
|
1215
|
+
const showAll = flags.includes('-a');
|
|
1216
|
+
const showAlmostAll = flags.includes('-A');
|
|
1217
|
+
|
|
1218
|
+
// Default to current directory if no paths specified
|
|
1219
|
+
const targetPaths = paths.length > 0 ? paths : ['.'];
|
|
1220
|
+
|
|
1221
|
+
let output = '';
|
|
1222
|
+
|
|
1223
|
+
for (const targetPath of targetPaths) {
|
|
1224
|
+
// Resolve path relative to cwd if provided
|
|
1225
|
+
const basePath = options?.cwd || process.cwd();
|
|
1226
|
+
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const stat = fs.statSync(fullPath);
|
|
1230
|
+
|
|
1231
|
+
if (stat.isFile()) {
|
|
1232
|
+
// Just show the file name if it's a file
|
|
1233
|
+
output += path.basename(targetPath) + '\n';
|
|
1234
|
+
} else if (stat.isDirectory()) {
|
|
1235
|
+
const entries = fs.readdirSync(fullPath);
|
|
1236
|
+
|
|
1237
|
+
// Filter hidden files unless -a or -A is specified
|
|
1238
|
+
let filteredEntries = entries;
|
|
1239
|
+
if (!showAll && !showAlmostAll) {
|
|
1240
|
+
filteredEntries = entries.filter(entry => !entry.startsWith('.'));
|
|
1241
|
+
} else if (showAlmostAll) {
|
|
1242
|
+
filteredEntries = entries.filter(entry => entry !== '.' && entry !== '..');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (isLongFormat) {
|
|
1246
|
+
// Long format: permissions, links, owner, group, size, date, name
|
|
1247
|
+
for (const entry of filteredEntries) {
|
|
1248
|
+
const entryPath = path.join(fullPath, entry);
|
|
1249
|
+
try {
|
|
1250
|
+
const entryStat = fs.statSync(entryPath);
|
|
1251
|
+
const isDir = entryStat.isDirectory();
|
|
1252
|
+
const permissions = isDir ? 'drwxr-xr-x' : '-rw-r--r--';
|
|
1253
|
+
const size = entryStat.size.toString().padStart(8);
|
|
1254
|
+
const date = entryStat.mtime.toISOString().slice(0, 16).replace('T', ' ');
|
|
1255
|
+
output += `${permissions} 1 user group ${size} ${date} ${entry}\n`;
|
|
1256
|
+
} catch {
|
|
1257
|
+
output += `?????????? 1 user group ? ??? ?? ??:?? ${entry}\n`;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
// Simple format: just names
|
|
1262
|
+
output += filteredEntries.join('\n') + (filteredEntries.length > 0 ? '\n' : '');
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
return {
|
|
1267
|
+
stderr: `ls: cannot access '${targetPath}': ${error.message}`,
|
|
1268
|
+
code: 2
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return { stdout: output, code: 0 };
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
return { stderr: `ls: ${error.message}`, code: 1 };
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// mkdir - create directories
|
|
1280
|
+
register('mkdir', async (args, stdin, options) => {
|
|
1281
|
+
if (args.length === 0) {
|
|
1282
|
+
return { stderr: 'mkdir: missing operand', code: 1 };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
const fs = await import('fs');
|
|
1287
|
+
const path = await import('path');
|
|
1288
|
+
|
|
1289
|
+
const flags = args.filter(arg => arg.startsWith('-'));
|
|
1290
|
+
const dirs = args.filter(arg => !arg.startsWith('-'));
|
|
1291
|
+
const recursive = flags.includes('-p');
|
|
1292
|
+
|
|
1293
|
+
for (const dir of dirs) {
|
|
1294
|
+
try {
|
|
1295
|
+
const basePath = options?.cwd || process.cwd();
|
|
1296
|
+
const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
|
|
1297
|
+
|
|
1298
|
+
if (recursive) {
|
|
1299
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
1300
|
+
} else {
|
|
1301
|
+
fs.mkdirSync(fullPath);
|
|
1302
|
+
}
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
return {
|
|
1305
|
+
stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
|
|
1306
|
+
code: 1
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return { stdout: '', code: 0 };
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return { stderr: `mkdir: ${error.message}`, code: 1 };
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// rm - remove files and directories
|
|
1318
|
+
register('rm', async (args, stdin, options) => {
|
|
1319
|
+
if (args.length === 0) {
|
|
1320
|
+
return { stderr: 'rm: missing operand', code: 1 };
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
const fs = await import('fs');
|
|
1325
|
+
const path = await import('path');
|
|
1326
|
+
|
|
1327
|
+
const flags = args.filter(arg => arg.startsWith('-'));
|
|
1328
|
+
const targets = args.filter(arg => !arg.startsWith('-'));
|
|
1329
|
+
const recursive = flags.includes('-r') || flags.includes('-R');
|
|
1330
|
+
const force = flags.includes('-f');
|
|
1331
|
+
|
|
1332
|
+
for (const target of targets) {
|
|
1333
|
+
try {
|
|
1334
|
+
const basePath = options?.cwd || process.cwd();
|
|
1335
|
+
const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
|
|
1336
|
+
|
|
1337
|
+
const stat = fs.statSync(fullPath);
|
|
1338
|
+
|
|
1339
|
+
if (stat.isDirectory()) {
|
|
1340
|
+
if (!recursive) {
|
|
1341
|
+
return {
|
|
1342
|
+
stderr: `rm: cannot remove '${target}': Is a directory`,
|
|
1343
|
+
code: 1
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
fs.rmSync(fullPath, { recursive: true, force });
|
|
1347
|
+
} else {
|
|
1348
|
+
fs.unlinkSync(fullPath);
|
|
1349
|
+
}
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (!force) {
|
|
1352
|
+
return {
|
|
1353
|
+
stderr: `rm: cannot remove '${target}': ${error.message}`,
|
|
1354
|
+
code: 1
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return { stdout: '', code: 0 };
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
return { stderr: `rm: ${error.message}`, code: 1 };
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// mv - move/rename files and directories
|
|
1367
|
+
register('mv', async (args, stdin, options) => {
|
|
1368
|
+
if (args.length < 2) {
|
|
1369
|
+
return { stderr: 'mv: missing destination file operand', code: 1 };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
try {
|
|
1373
|
+
const fs = await import('fs');
|
|
1374
|
+
const path = await import('path');
|
|
1375
|
+
|
|
1376
|
+
const basePath = options?.cwd || process.cwd();
|
|
1377
|
+
|
|
1378
|
+
if (args.length === 2) {
|
|
1379
|
+
// Simple rename/move
|
|
1380
|
+
const [source, dest] = args;
|
|
1381
|
+
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
1382
|
+
let destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
1383
|
+
|
|
1384
|
+
try {
|
|
1385
|
+
// Check if destination is an existing directory
|
|
1386
|
+
try {
|
|
1387
|
+
const destStat = fs.statSync(destPath);
|
|
1388
|
+
if (destStat.isDirectory()) {
|
|
1389
|
+
// Move file into the directory
|
|
1390
|
+
const fileName = path.basename(source);
|
|
1391
|
+
destPath = path.join(destPath, fileName);
|
|
1392
|
+
}
|
|
1393
|
+
} catch {
|
|
1394
|
+
// Destination doesn't exist, proceed with direct rename
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
fs.renameSync(sourcePath, destPath);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
return {
|
|
1400
|
+
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
1401
|
+
code: 1
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
// Multiple sources to directory
|
|
1406
|
+
const sources = args.slice(0, -1);
|
|
1407
|
+
const dest = args[args.length - 1];
|
|
1408
|
+
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
1409
|
+
|
|
1410
|
+
// Check if destination is a directory
|
|
1411
|
+
try {
|
|
1412
|
+
const destStat = fs.statSync(destPath);
|
|
1413
|
+
if (!destStat.isDirectory()) {
|
|
1414
|
+
return {
|
|
1415
|
+
stderr: `mv: target '${dest}' is not a directory`,
|
|
1416
|
+
code: 1
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
} catch {
|
|
1420
|
+
return {
|
|
1421
|
+
stderr: `mv: cannot access '${dest}': No such file or directory`,
|
|
1422
|
+
code: 1
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
for (const source of sources) {
|
|
1427
|
+
try {
|
|
1428
|
+
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
1429
|
+
const fileName = path.basename(source);
|
|
1430
|
+
const newDestPath = path.join(destPath, fileName);
|
|
1431
|
+
fs.renameSync(sourcePath, newDestPath);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
return {
|
|
1434
|
+
stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
|
|
1435
|
+
code: 1
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return { stdout: '', code: 0 };
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
return { stderr: `mv: ${error.message}`, code: 1 };
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
// cp - copy files and directories
|
|
1448
|
+
register('cp', async (args, stdin, options) => {
|
|
1449
|
+
if (args.length < 2) {
|
|
1450
|
+
return { stderr: 'cp: missing destination file operand', code: 1 };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
try {
|
|
1454
|
+
const fs = await import('fs');
|
|
1455
|
+
const path = await import('path');
|
|
1456
|
+
|
|
1457
|
+
const flags = args.filter(arg => arg.startsWith('-'));
|
|
1458
|
+
const paths = args.filter(arg => !arg.startsWith('-'));
|
|
1459
|
+
const recursive = flags.includes('-r') || flags.includes('-R');
|
|
1460
|
+
|
|
1461
|
+
const basePath = options?.cwd || process.cwd();
|
|
1462
|
+
|
|
1463
|
+
if (paths.length === 2) {
|
|
1464
|
+
// Simple copy
|
|
1465
|
+
const [source, dest] = paths;
|
|
1466
|
+
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
1467
|
+
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
1468
|
+
|
|
1469
|
+
try {
|
|
1470
|
+
const sourceStat = fs.statSync(sourcePath);
|
|
1471
|
+
|
|
1472
|
+
if (sourceStat.isDirectory()) {
|
|
1473
|
+
if (!recursive) {
|
|
1474
|
+
return {
|
|
1475
|
+
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
1476
|
+
code: 1
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
fs.cpSync(sourcePath, destPath, { recursive: true });
|
|
1480
|
+
} else {
|
|
1481
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
1482
|
+
}
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
return {
|
|
1485
|
+
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
1486
|
+
code: 1
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
} else {
|
|
1490
|
+
// Multiple sources to directory
|
|
1491
|
+
const sources = paths.slice(0, -1);
|
|
1492
|
+
const dest = paths[paths.length - 1];
|
|
1493
|
+
const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
|
|
1494
|
+
|
|
1495
|
+
// Check if destination is a directory
|
|
1496
|
+
try {
|
|
1497
|
+
const destStat = fs.statSync(destPath);
|
|
1498
|
+
if (!destStat.isDirectory()) {
|
|
1499
|
+
return {
|
|
1500
|
+
stderr: `cp: target '${dest}' is not a directory`,
|
|
1501
|
+
code: 1
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
} catch {
|
|
1505
|
+
return {
|
|
1506
|
+
stderr: `cp: cannot access '${dest}': No such file or directory`,
|
|
1507
|
+
code: 1
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
for (const source of sources) {
|
|
1512
|
+
try {
|
|
1513
|
+
const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
|
|
1514
|
+
const fileName = path.basename(source);
|
|
1515
|
+
const newDestPath = path.join(destPath, fileName);
|
|
1516
|
+
|
|
1517
|
+
const sourceStat = fs.statSync(sourcePath);
|
|
1518
|
+
if (sourceStat.isDirectory()) {
|
|
1519
|
+
if (!recursive) {
|
|
1520
|
+
return {
|
|
1521
|
+
stderr: `cp: -r not specified; omitting directory '${source}'`,
|
|
1522
|
+
code: 1
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
fs.cpSync(sourcePath, newDestPath, { recursive: true });
|
|
1526
|
+
} else {
|
|
1527
|
+
fs.copyFileSync(sourcePath, newDestPath);
|
|
1528
|
+
}
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
return {
|
|
1531
|
+
stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
|
|
1532
|
+
code: 1
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return { stdout: '', code: 0 };
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
return { stderr: `cp: ${error.message}`, code: 1 };
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
// touch - create or update file timestamps
|
|
1545
|
+
register('touch', async (args, stdin, options) => {
|
|
1546
|
+
if (args.length === 0) {
|
|
1547
|
+
return { stderr: 'touch: missing file operand', code: 1 };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
const fs = await import('fs');
|
|
1552
|
+
const path = await import('path');
|
|
1553
|
+
|
|
1554
|
+
const basePath = options?.cwd || process.cwd();
|
|
1555
|
+
|
|
1556
|
+
for (const file of args) {
|
|
1557
|
+
try {
|
|
1558
|
+
const fullPath = path.isAbsolute(file) ? file : path.join(basePath, file);
|
|
1559
|
+
|
|
1560
|
+
// Try to update timestamps if file exists
|
|
1561
|
+
try {
|
|
1562
|
+
const now = new Date();
|
|
1563
|
+
fs.utimesSync(fullPath, now, now);
|
|
1564
|
+
} catch {
|
|
1565
|
+
// File doesn't exist, create it
|
|
1566
|
+
fs.writeFileSync(fullPath, '', { flag: 'w' });
|
|
1567
|
+
}
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
return {
|
|
1570
|
+
stderr: `touch: cannot touch '${file}': ${error.message}`,
|
|
1571
|
+
code: 1
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
return { stdout: '', code: 0 };
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
return { stderr: `touch: ${error.message}`, code: 1 };
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// basename - extract filename from path
|
|
1583
|
+
register('basename', async (args) => {
|
|
1584
|
+
if (args.length === 0) {
|
|
1585
|
+
return { stderr: 'basename: missing operand', code: 1 };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
try {
|
|
1589
|
+
const path = await import('path');
|
|
1590
|
+
|
|
1591
|
+
const pathname = args[0];
|
|
1592
|
+
const suffix = args[1];
|
|
1593
|
+
|
|
1594
|
+
let result = path.basename(pathname);
|
|
1595
|
+
|
|
1596
|
+
// Remove suffix if provided
|
|
1597
|
+
if (suffix && result.endsWith(suffix)) {
|
|
1598
|
+
result = result.slice(0, -suffix.length);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
return { stdout: result + '\n', code: 0 };
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
return { stderr: `basename: ${error.message}`, code: 1 };
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// dirname - extract directory from path
|
|
1608
|
+
register('dirname', async (args) => {
|
|
1609
|
+
if (args.length === 0) {
|
|
1610
|
+
return { stderr: 'dirname: missing operand', code: 1 };
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
try {
|
|
1614
|
+
const path = await import('path');
|
|
1615
|
+
|
|
1616
|
+
const pathname = args[0];
|
|
1617
|
+
const result = path.dirname(pathname);
|
|
1618
|
+
|
|
1619
|
+
return { stdout: result + '\n', code: 0 };
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return { stderr: `dirname: ${error.message}`, code: 1 };
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// yes - output a string repeatedly
|
|
1626
|
+
register('yes', async function* (args) {
|
|
1627
|
+
const output = args.length > 0 ? args.join(' ') : 'y';
|
|
1628
|
+
|
|
1629
|
+
// Generate infinite stream of the output
|
|
1630
|
+
while (true) {
|
|
1631
|
+
yield output + '\n';
|
|
1632
|
+
// Small delay to prevent overwhelming the system
|
|
1633
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
// seq - generate sequence of numbers
|
|
1638
|
+
register('seq', async (args) => {
|
|
1639
|
+
if (args.length === 0) {
|
|
1640
|
+
return { stderr: 'seq: missing operand', code: 1 };
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
try {
|
|
1644
|
+
let start, step, end;
|
|
1645
|
+
|
|
1646
|
+
if (args.length === 1) {
|
|
1647
|
+
start = 1;
|
|
1648
|
+
step = 1;
|
|
1649
|
+
end = parseInt(args[0]);
|
|
1650
|
+
} else if (args.length === 2) {
|
|
1651
|
+
start = parseInt(args[0]);
|
|
1652
|
+
step = 1;
|
|
1653
|
+
end = parseInt(args[1]);
|
|
1654
|
+
} else if (args.length === 3) {
|
|
1655
|
+
start = parseInt(args[0]);
|
|
1656
|
+
step = parseInt(args[1]);
|
|
1657
|
+
end = parseInt(args[2]);
|
|
1658
|
+
} else {
|
|
1659
|
+
return { stderr: 'seq: too many operands', code: 1 };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (isNaN(start) || isNaN(step) || isNaN(end)) {
|
|
1663
|
+
return { stderr: 'seq: invalid number', code: 1 };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
let output = '';
|
|
1667
|
+
if (step > 0) {
|
|
1668
|
+
for (let i = start; i <= end; i += step) {
|
|
1669
|
+
output += i + '\n';
|
|
1670
|
+
}
|
|
1671
|
+
} else if (step < 0) {
|
|
1672
|
+
for (let i = start; i >= end; i += step) {
|
|
1673
|
+
output += i + '\n';
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
return { stderr: 'seq: invalid increment', code: 1 };
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return { stdout: output, code: 0 };
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
return { stderr: `seq: ${error.message}`, code: 1 };
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
// test - test file conditions (basic implementation)
|
|
1686
|
+
register('test', async (args) => {
|
|
1687
|
+
if (args.length === 0) {
|
|
1688
|
+
return { stdout: '', code: 1 };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Very basic test implementation
|
|
1692
|
+
const arg = args[0];
|
|
1693
|
+
|
|
1694
|
+
try {
|
|
1695
|
+
if (arg === '-d' && args[1]) {
|
|
1696
|
+
// Test if directory
|
|
1697
|
+
const stat = require('fs').statSync(args[1]);
|
|
1698
|
+
return { stdout: '', code: stat.isDirectory() ? 0 : 1 };
|
|
1699
|
+
} else if (arg === '-f' && args[1]) {
|
|
1700
|
+
// Test if file
|
|
1701
|
+
const stat = require('fs').statSync(args[1]);
|
|
1702
|
+
return { stdout: '', code: stat.isFile() ? 0 : 1 };
|
|
1703
|
+
} else if (arg === '-e' && args[1]) {
|
|
1704
|
+
// Test if exists
|
|
1705
|
+
require('fs').statSync(args[1]);
|
|
1706
|
+
return { stdout: '', code: 0 };
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
return { stdout: '', code: 1 };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
return { stdout: '', code: 1 };
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Initialize built-in commands
|
|
1717
|
+
registerBuiltins();
|
|
1718
|
+
|
|
1719
|
+
export { $tagged as $, sh, exec, run, quote, create, raw, ProcessRunner, shell, set, unset, register, unregister, listCommands, enableVirtualCommands, disableVirtualCommands };
|
|
1720
|
+
export default $tagged;
|