fauxnix-cli 0.2.0 → 0.3.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.
- package/README.md +203 -199
- package/dist/ast.d.ts +7 -1
- package/dist/ast.js +10 -0
- package/dist/cli.js +13 -13
- package/dist/commands/sysinfo.js +1300 -48
- package/dist/encoding.d.ts +9 -0
- package/dist/encoding.js +19 -0
- package/dist/executor.js +230 -105
- package/dist/mcp.js +11 -11
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +420 -15
- package/dist/registry.js +10 -0
- package/dist/translator.d.ts +16 -1
- package/dist/translator.js +271 -11
- package/package.json +58 -58
package/dist/encoding.d.ts
CHANGED
|
@@ -14,5 +14,14 @@ export declare function resolveNativePref(): NativeEncodingPref;
|
|
|
14
14
|
* setting (GBK decoding is lenient and cannot be validity-tested).
|
|
15
15
|
*/
|
|
16
16
|
export declare function decodeOutput(buf: Buffer, prefer?: NativeEncodingPref): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert PowerShell-host CRLF line endings to LF without destroying
|
|
19
|
+
* intentional CR/CRLF from exact writers (`fx-write`, `printf`, `echo -n`).
|
|
20
|
+
*
|
|
21
|
+
* The console host terminates each Write-Output object with CRLF and a
|
|
22
|
+
* final newline. Exact writers do not. So we only rewrite when every LF
|
|
23
|
+
* is part of a CRLF pair *and* the buffer ends with a newline.
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeHostNewlines(s: string): string;
|
|
17
26
|
/** Encode a PowerShell script for -EncodedCommand (UTF-16LE base64). */
|
|
18
27
|
export declare function encodeCommand(script: string): string;
|
package/dist/encoding.js
CHANGED
|
@@ -33,6 +33,25 @@ export function decodeOutput(buf, prefer = 'utf8') {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert PowerShell-host CRLF line endings to LF without destroying
|
|
38
|
+
* intentional CR/CRLF from exact writers (`fx-write`, `printf`, `echo -n`).
|
|
39
|
+
*
|
|
40
|
+
* The console host terminates each Write-Output object with CRLF and a
|
|
41
|
+
* final newline. Exact writers do not. So we only rewrite when every LF
|
|
42
|
+
* is part of a CRLF pair *and* the buffer ends with a newline.
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeHostNewlines(s) {
|
|
45
|
+
if (s.length === 0 || !s.includes('\n'))
|
|
46
|
+
return s;
|
|
47
|
+
if (!s.endsWith('\n'))
|
|
48
|
+
return s;
|
|
49
|
+
for (let i = 0; i < s.length; i++) {
|
|
50
|
+
if (s[i] === '\n' && (i === 0 || s[i - 1] !== '\r'))
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
return s.replace(/\r\n/g, '\n');
|
|
54
|
+
}
|
|
36
55
|
/** Encode a PowerShell script for -EncodedCommand (UTF-16LE base64). */
|
|
37
56
|
export function encodeCommand(script) {
|
|
38
57
|
return Buffer.from(script, 'utf16le').toString('base64');
|
package/dist/executor.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { promises as fs, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { promises as fs, readFileSync, writeFileSync, existsSync, openSync, closeSync, writeSync } from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { normalizeLiteralPath } from './translator.js';
|
|
7
|
-
import { decodeOutput, encodeCommand, resolveNativePref } from './encoding.js';
|
|
7
|
+
import { decodeOutput, encodeCommand, normalizeHostNewlines, resolveNativePref } from './encoding.js';
|
|
8
8
|
import { normalizeStderr } from './errors.js';
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
10
|
const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
|
|
@@ -17,6 +17,67 @@ function winTarget(target) {
|
|
|
17
17
|
return path.join(os.tmpdir(), p.slice('$env:TEMP\\'.length));
|
|
18
18
|
return p;
|
|
19
19
|
}
|
|
20
|
+
function emitToPrepDest(dest, msg, fds, fallback) {
|
|
21
|
+
if (dest.kind === 'nul')
|
|
22
|
+
return;
|
|
23
|
+
if (dest.kind === 'file') {
|
|
24
|
+
try {
|
|
25
|
+
writeToPrepFd(fds, dest.path, msg);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* fall through to caller */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
fallback(msg);
|
|
33
|
+
}
|
|
34
|
+
function writeAllSync(fd, data) {
|
|
35
|
+
let off = 0;
|
|
36
|
+
while (off < data.length) {
|
|
37
|
+
const n = writeSync(fd, data, off, data.length - off);
|
|
38
|
+
if (n <= 0)
|
|
39
|
+
throw new Error('fauxnix: short write to redirect');
|
|
40
|
+
off += n;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeToPrepFd(fds, file, data) {
|
|
44
|
+
const fd = fds.get(file);
|
|
45
|
+
if (fd === undefined)
|
|
46
|
+
throw new Error('fauxnix: redirect fd missing for ' + file);
|
|
47
|
+
writeAllSync(fd, Buffer.from(data, 'utf8'));
|
|
48
|
+
}
|
|
49
|
+
function closePrepFds(fds) {
|
|
50
|
+
for (const fd of fds.values()) {
|
|
51
|
+
try {
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* already closed */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
fds.clear();
|
|
59
|
+
}
|
|
60
|
+
function prepareRedirectFile(file, append, fds) {
|
|
61
|
+
try {
|
|
62
|
+
const prev = fds.get(file);
|
|
63
|
+
if (prev !== undefined) {
|
|
64
|
+
closeSync(prev);
|
|
65
|
+
fds.delete(file);
|
|
66
|
+
}
|
|
67
|
+
fds.set(file, openSync(file, append ? 'a' : 'w'));
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const err = e;
|
|
72
|
+
if (err.code === 'ENOENT')
|
|
73
|
+
return file + ': No such file or directory';
|
|
74
|
+
if (err.code === 'EACCES' || err.code === 'EPERM')
|
|
75
|
+
return file + ': Permission denied';
|
|
76
|
+
if (err.code === 'EISDIR')
|
|
77
|
+
return file + ': Is a directory';
|
|
78
|
+
return file + ': cannot create: ' + err.message;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
20
81
|
function planRedirects(redirects) {
|
|
21
82
|
const r = {
|
|
22
83
|
stdinFile: null,
|
|
@@ -169,9 +230,12 @@ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
|
|
|
169
230
|
// skips b AND c when a fails. chainOk models the value of the current
|
|
170
231
|
// &&/|| chain; `;` segments always run and restart the chain.
|
|
171
232
|
let chainOk = true;
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
233
|
+
// Redirect targets are relative to the *current* session cwd, not this
|
|
234
|
+
// Node process. Re-read after every segment so `cd src && echo x > out.txt`
|
|
235
|
+
// writes under src (same as two separate session calls). A one-shot
|
|
236
|
+
// capture of the entry cwd would land the file in the old directory.
|
|
237
|
+
let currentDir = opts.cwd ?? session.cwd ?? process.cwd();
|
|
238
|
+
const resolveTarget = (t) => path.isAbsolute(t) || /^[A-Za-z]:[\\/]/.test(t) ? t : path.resolve(currentDir, t);
|
|
175
239
|
for (const plan of plans) {
|
|
176
240
|
if (plan.op === '&&' && !chainOk)
|
|
177
241
|
continue;
|
|
@@ -181,118 +245,179 @@ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
|
|
|
181
245
|
red.stdinFile = red.stdinFile ? resolveTarget(red.stdinFile) : null;
|
|
182
246
|
red.stdoutFile = red.stdoutFile ? resolveTarget(red.stdoutFile) : null;
|
|
183
247
|
red.stderrFile = red.stderrFile ? resolveTarget(red.stderrFile) : null;
|
|
184
|
-
// bash
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
psArgs = [...PS_ARGS, '-File', scriptFile];
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
psArgs = [...PS_ARGS, '-EncodedCommand', encoded];
|
|
203
|
-
}
|
|
204
|
-
const child = spawn('powershell.exe', psArgs, {
|
|
205
|
-
env: session.childEnv(opts.cwd, red.stdinFile),
|
|
206
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
207
|
-
windowsHide: true,
|
|
208
|
-
});
|
|
209
|
-
const running = { proc: child, killed: false };
|
|
210
|
-
const outBufs = [];
|
|
211
|
-
const errBufs = [];
|
|
212
|
-
child.stdout.on('data', (d) => outBufs.push(d));
|
|
213
|
-
child.stderr.on('data', (d) => errBufs.push(d));
|
|
214
|
-
child.stdin.end();
|
|
215
|
-
const timer = setTimeout(() => {
|
|
216
|
-
running.killed = true;
|
|
217
|
-
// Node-native termination — no external kill process, nothing injectable.
|
|
218
|
-
// Grandchildren of a timed-out script may survive; the `kill -9`/`pkill`
|
|
219
|
-
// builtins remain available for explicit Windows tree kills.
|
|
220
|
-
try {
|
|
221
|
-
child.kill();
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
/* best effort */
|
|
225
|
-
}
|
|
226
|
-
}, timeoutMs);
|
|
227
|
-
const code = await new Promise((resolve) => {
|
|
228
|
-
child.on('error', (e) => {
|
|
229
|
-
stderr += 'fauxnix: failed to start powershell.exe: ' + e.message + '\n';
|
|
230
|
-
resolve(127);
|
|
248
|
+
// bash applies redirects left-to-right *before* the command runs.
|
|
249
|
+
// Walk the parsed list in source order so a failing earlier redirect
|
|
250
|
+
// (e.g. `2>nosuch/err >important.txt`) does not truncate a later file,
|
|
251
|
+
// and a failing redirected `cd` cannot change cwd. Setup errors after
|
|
252
|
+
// an earlier `2>file` go to that file, not the caller (bash already
|
|
253
|
+
// applied the stderr redirect).
|
|
254
|
+
const prepFds = new Map();
|
|
255
|
+
try {
|
|
256
|
+
let redirectPrepFailed = false;
|
|
257
|
+
// Snapshot fd destinations as we walk. `2>&1` copies stdout *at that
|
|
258
|
+
// moment*; a later `>file` must not drag stderr along (bash fd dup).
|
|
259
|
+
let prepStdout = { kind: 'caller' };
|
|
260
|
+
let prepStderr = { kind: 'caller' };
|
|
261
|
+
const emitPrepError = (msg) => emitToPrepDest(prepStderr, msg, prepFds, (s) => {
|
|
262
|
+
stderr += s;
|
|
231
263
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const decodePref = resolveNativePref();
|
|
237
|
-
let segOut = decodeOutput(Buffer.concat(outBufs), decodePref);
|
|
238
|
-
let segErr = normalizeStderr(decodeOutput(Buffer.concat(errBufs), decodePref));
|
|
239
|
-
if (running.killed) {
|
|
240
|
-
segErr += '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
|
|
241
|
-
}
|
|
242
|
-
if (red.mergeStderr) {
|
|
243
|
-
segOut += (segOut && !segOut.endsWith('\n') && segErr ? '\n' : '') + segErr;
|
|
244
|
-
segErr = '';
|
|
245
|
-
}
|
|
246
|
-
const stdoutToStderr = red.stdoutToStderr;
|
|
247
|
-
if (stdoutToStderr) {
|
|
248
|
-
segErr += segOut;
|
|
249
|
-
segOut = '';
|
|
250
|
-
}
|
|
251
|
-
const swallowStderr = red.swallowStderr;
|
|
252
|
-
if (swallowStderr)
|
|
253
|
-
segErr = '';
|
|
254
|
-
// redirect stdout to file instead of the result stream
|
|
255
|
-
if (red.stdoutFile) {
|
|
256
|
-
try {
|
|
257
|
-
if (red.appendStdout) {
|
|
258
|
-
const prev = existsSync(red.stdoutFile) ? readFileSync(red.stdoutFile) : Buffer.alloc(0);
|
|
259
|
-
writeFileSync(red.stdoutFile, Buffer.concat([prev, Buffer.from(segOut, 'utf8')]));
|
|
264
|
+
for (const r of plan.redirects) {
|
|
265
|
+
if (r.op === '2>&1') {
|
|
266
|
+
prepStderr = prepStdout;
|
|
267
|
+
continue;
|
|
260
268
|
}
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
if (r.op === '1>&2') {
|
|
270
|
+
prepStdout = prepStderr;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const target = resolveTarget(winTarget(r.target));
|
|
274
|
+
if (r.op === '<') {
|
|
275
|
+
if (!existsSync(target)) {
|
|
276
|
+
emitPrepError('bash: ' + target + ': No such file or directory\n');
|
|
277
|
+
redirectPrepFailed = true;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (target === 'NUL') {
|
|
283
|
+
if (r.op === '>' || r.op === '>>')
|
|
284
|
+
prepStdout = { kind: 'nul' };
|
|
285
|
+
else if (r.op === '2>' || r.op === '2>>')
|
|
286
|
+
prepStderr = { kind: 'nul' };
|
|
287
|
+
else if (r.op === '&>' || r.op === '&>>') {
|
|
288
|
+
prepStdout = { kind: 'nul' };
|
|
289
|
+
prepStderr = { kind: 'nul' };
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const append = r.op === '>>' || r.op === '2>>' || r.op === '&>>';
|
|
294
|
+
const fail = prepareRedirectFile(target, append, prepFds);
|
|
295
|
+
if (fail) {
|
|
296
|
+
emitPrepError('bash: ' + fail + '\n');
|
|
297
|
+
redirectPrepFailed = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
const fileDest = { kind: 'file', path: target };
|
|
301
|
+
if (r.op === '>' || r.op === '>>')
|
|
302
|
+
prepStdout = fileDest;
|
|
303
|
+
else if (r.op === '2>' || r.op === '2>>')
|
|
304
|
+
prepStderr = fileDest;
|
|
305
|
+
else if (r.op === '&>' || r.op === '&>>') {
|
|
306
|
+
prepStdout = fileDest;
|
|
307
|
+
prepStderr = fileDest;
|
|
263
308
|
}
|
|
264
|
-
segOut = '';
|
|
265
309
|
}
|
|
266
|
-
|
|
267
|
-
segErr += 'bash: ' + red.stdoutFile + ': cannot create: ' + e.message + '\n';
|
|
310
|
+
if (redirectPrepFailed) {
|
|
268
311
|
exitCode = 1;
|
|
312
|
+
session.prevExit = exitCode;
|
|
313
|
+
chainOk = false;
|
|
314
|
+
continue;
|
|
269
315
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
316
|
+
const encoded = encodeCommand(plan.script);
|
|
317
|
+
// -EncodedCommand is capped by the ~32K command-line limit; heavy
|
|
318
|
+
// pipelines fall back to a UTF-8 BOM temp script (PS 5.1 honors the BOM
|
|
319
|
+
// regardless of the console codepage, so non-ASCII stays intact).
|
|
320
|
+
let psArgs;
|
|
321
|
+
if (encoded.length > 28000) {
|
|
322
|
+
writeFileSync(scriptFile, '\ufeff' + plan.script, 'utf8');
|
|
323
|
+
psArgs = [...PS_ARGS, '-File', scriptFile];
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
psArgs = [...PS_ARGS, '-EncodedCommand', encoded];
|
|
327
|
+
}
|
|
328
|
+
const child = spawn('powershell.exe', psArgs, {
|
|
329
|
+
env: session.childEnv(currentDir, red.stdinFile),
|
|
330
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
331
|
+
windowsHide: true,
|
|
332
|
+
});
|
|
333
|
+
const running = { proc: child, killed: false };
|
|
334
|
+
const outBufs = [];
|
|
335
|
+
const errBufs = [];
|
|
336
|
+
child.stdout.on('data', (d) => outBufs.push(d));
|
|
337
|
+
child.stderr.on('data', (d) => errBufs.push(d));
|
|
338
|
+
child.stdin.end();
|
|
339
|
+
const timer = setTimeout(() => {
|
|
340
|
+
running.killed = true;
|
|
341
|
+
// Node-native termination — no external kill process, nothing injectable.
|
|
342
|
+
// Grandchildren of a timed-out script may survive; the `kill -9`/`pkill`
|
|
343
|
+
// builtins remain available for explicit Windows tree kills.
|
|
344
|
+
try {
|
|
345
|
+
child.kill();
|
|
281
346
|
}
|
|
282
|
-
|
|
283
|
-
|
|
347
|
+
catch {
|
|
348
|
+
/* best effort */
|
|
284
349
|
}
|
|
350
|
+
}, timeoutMs);
|
|
351
|
+
const code = await new Promise((resolve) => {
|
|
352
|
+
child.on('error', (e) => {
|
|
353
|
+
stderr += 'fauxnix: failed to start powershell.exe: ' + e.message + '\n';
|
|
354
|
+
resolve(127);
|
|
355
|
+
});
|
|
356
|
+
child.on('close', (c) => resolve(running.killed ? 124 : (c ?? 0)));
|
|
357
|
+
});
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
afterSegment();
|
|
360
|
+
const decodePref = resolveNativePref();
|
|
361
|
+
// GNU line discipline: the PS host terminates Write-Output lines with
|
|
362
|
+
// CRLF. Exact writers (fx-write / printf / echo -n) must keep embedded
|
|
363
|
+
// CR so `printf 'a\r\nb' > out` stays 4 bytes.
|
|
364
|
+
let segOut = normalizeHostNewlines(decodeOutput(Buffer.concat(outBufs), decodePref));
|
|
365
|
+
let segErr = normalizeHostNewlines(normalizeStderr(decodeOutput(Buffer.concat(errBufs), decodePref)));
|
|
366
|
+
if (running.killed) {
|
|
367
|
+
segErr += '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
|
|
368
|
+
}
|
|
369
|
+
if (red.mergeStderr) {
|
|
370
|
+
segOut += (segOut && !segOut.endsWith('\n') && segErr ? '\n' : '') + segErr;
|
|
371
|
+
segErr = '';
|
|
372
|
+
}
|
|
373
|
+
const stdoutToStderr = red.stdoutToStderr;
|
|
374
|
+
if (stdoutToStderr) {
|
|
375
|
+
segErr += segOut;
|
|
376
|
+
segOut = '';
|
|
377
|
+
}
|
|
378
|
+
const swallowStderr = red.swallowStderr;
|
|
379
|
+
if (swallowStderr)
|
|
285
380
|
segErr = '';
|
|
381
|
+
// Write captured streams through the fds opened during preflight
|
|
382
|
+
// (bash: the redirect refers to the open file, not the path). Reopening
|
|
383
|
+
// the path would recreate a file the command just unlinked
|
|
384
|
+
// (`rm out.txt > out.txt`).
|
|
385
|
+
let redirectOk = true;
|
|
386
|
+
if (red.stdoutFile) {
|
|
387
|
+
try {
|
|
388
|
+
writeToPrepFd(prepFds, red.stdoutFile, segOut);
|
|
389
|
+
segOut = '';
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
segErr += 'bash: ' + red.stdoutFile + ': cannot create: ' + e.message + '\n';
|
|
393
|
+
exitCode = 1;
|
|
394
|
+
redirectOk = false;
|
|
395
|
+
}
|
|
286
396
|
}
|
|
287
|
-
|
|
288
|
-
|
|
397
|
+
if (red.stderrFile) {
|
|
398
|
+
try {
|
|
399
|
+
const body = red.stderrFile === red.stdoutFile ? segOut + segErr : segErr;
|
|
400
|
+
writeToPrepFd(prepFds, red.stderrFile, body);
|
|
401
|
+
segErr = '';
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
/* best effort */
|
|
405
|
+
}
|
|
289
406
|
}
|
|
407
|
+
stdout += segOut;
|
|
408
|
+
stderr += segErr;
|
|
409
|
+
exitCode = code ?? 0;
|
|
410
|
+
session.prevExit = exitCode;
|
|
411
|
+
chainOk = exitCode === 0;
|
|
412
|
+
// Only inherit cwd from a segment that actually ran and whose
|
|
413
|
+
// output redirects succeeded. A failed `cd dir > missing/out` must
|
|
414
|
+
// not move later relative redirects.
|
|
415
|
+
if (redirectOk && session.cwd)
|
|
416
|
+
currentDir = session.cwd;
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
closePrepFds(prepFds);
|
|
290
420
|
}
|
|
291
|
-
stdout += segOut;
|
|
292
|
-
stderr += segErr;
|
|
293
|
-
exitCode = code ?? 0;
|
|
294
|
-
session.prevExit = exitCode;
|
|
295
|
-
chainOk = exitCode === 0;
|
|
296
421
|
}
|
|
297
422
|
return { stdout, stderr, exitCode };
|
|
298
423
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -7,19 +7,19 @@ import { translateCommandList, wrapScript, translatePipelineBody } from './trans
|
|
|
7
7
|
import { registeredNames } from './registry.js';
|
|
8
8
|
import './commands/install-all.js';
|
|
9
9
|
const TOOL_NAME = process.env.FAUXNIX_TOOL_NAME || 'bash';
|
|
10
|
-
const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
|
|
11
|
-
|
|
12
|
-
Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
|
|
13
|
-
Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), errors look like bash errors, and text encoding (UTF-8/GBK) is handled automatically.
|
|
14
|
-
|
|
15
|
-
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
16
|
-
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
17
|
-
Not supported: heredocs, backticks, control flow (if/for/while), background jobs.
|
|
18
|
-
|
|
19
|
-
CWD, environment variables, export/unset and cd persist across calls within this session.
|
|
10
|
+
const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
|
|
11
|
+
|
|
12
|
+
Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
|
|
13
|
+
Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), errors look like bash errors, and text encoding (UTF-8/GBK) is handled automatically.
|
|
14
|
+
|
|
15
|
+
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
16
|
+
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
17
|
+
Not supported: heredocs, backticks, control flow (if/for/while), background jobs.
|
|
18
|
+
|
|
19
|
+
CWD, environment variables, export/unset and cd persist across calls within this session — but prefer COMBINING related commands in one call with ; or && (e.g. 'cd src && ls | wc -l'); each call is a fresh translation+process, so batching is faster than many tiny calls.
|
|
20
20
|
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).`;
|
|
21
21
|
export async function startMcpServer() {
|
|
22
|
-
const server = new McpServer({ name: 'fauxnix', version: '0.
|
|
22
|
+
const server = new McpServer({ name: 'fauxnix', version: '0.3.0' }, { capabilities: { tools: {} } });
|
|
23
23
|
const session = new FauxnixSession();
|
|
24
24
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
25
25
|
command: z.string().describe('The bash-style command line to run'),
|
package/dist/parser.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ interface Token {
|
|
|
5
5
|
/** For WORD: the parsed parts. For OP: the operator text. */
|
|
6
6
|
op?: string;
|
|
7
7
|
parts?: WordPart[];
|
|
8
|
+
/** True when this token was not preceded by whitespace. */
|
|
9
|
+
tightLeft?: boolean;
|
|
8
10
|
}
|
|
9
11
|
export declare function tokenize(input: string): Token[];
|
|
10
12
|
export declare function parseCommand(input: string): CommandList;
|