fauxnix-cli 0.7.1 → 0.8.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 +5 -0
- package/dist/cli.js +6 -1
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +276 -69
- package/dist/commands/install-all.js +5 -3
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +14 -3
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +83 -13
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +79 -18
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +7 -1
- package/dist/ps-host.js +75 -8
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +142 -0
- package/dist/translator.js +8 -1
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { normalizeLiteralPath, wrapScript } from './translator.js';
|
|
6
6
|
import { decodeOutput, normalizeHostNewlines, resolveNativePref } from './encoding.js';
|
|
7
7
|
import { normalizeStderr } from './errors.js';
|
|
8
|
-
import { PowerShellHost, PS_MISSING_MESSAGE } from './ps-host.js';
|
|
8
|
+
import { DEFAULT_STDERR_LIMIT, DEFAULT_STDOUT_LIMIT, PowerShellHost, PS_MISSING_MESSAGE, } from './ps-host.js';
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
10
|
const DEFAULT_WINDOWS_PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
11
11
|
function hasEnvKey(env, name) {
|
|
@@ -153,6 +153,7 @@ function planRedirects(redirects) {
|
|
|
153
153
|
}
|
|
154
154
|
/** Session persists cwd and env across segments, like a real shell. */
|
|
155
155
|
export class FauxnixSession {
|
|
156
|
+
id;
|
|
156
157
|
cwd = null;
|
|
157
158
|
env = {};
|
|
158
159
|
/** Exit code of the previous segment — powers bash's `$?`. */
|
|
@@ -162,16 +163,23 @@ export class FauxnixSession {
|
|
|
162
163
|
scriptFile;
|
|
163
164
|
hostFile;
|
|
164
165
|
host = null;
|
|
165
|
-
|
|
166
|
+
lifecycleLock = Promise.resolve();
|
|
166
167
|
constructor() {
|
|
167
|
-
this.
|
|
168
|
+
this.id = randomUUID().slice(0, 8);
|
|
169
|
+
this.bindFiles(this.id);
|
|
168
170
|
}
|
|
169
171
|
bindFiles(id) {
|
|
172
|
+
this.id = id;
|
|
170
173
|
this.cwdFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-cwd.txt');
|
|
171
174
|
this.envFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-env.json');
|
|
172
175
|
this.scriptFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-script.ps1');
|
|
173
176
|
this.hostFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-host.ps1');
|
|
174
177
|
}
|
|
178
|
+
withLock(fn) {
|
|
179
|
+
const done = this.lifecycleLock.then(fn, fn);
|
|
180
|
+
this.lifecycleLock = done.then(() => undefined, () => undefined);
|
|
181
|
+
return done;
|
|
182
|
+
}
|
|
175
183
|
syncFromDisk() {
|
|
176
184
|
try {
|
|
177
185
|
if (existsSync(this.cwdFile)) {
|
|
@@ -201,10 +209,22 @@ export class FauxnixSession {
|
|
|
201
209
|
return this.host;
|
|
202
210
|
}
|
|
203
211
|
/** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
prewarm() {
|
|
213
|
+
return this.withLock(async () => {
|
|
214
|
+
await this.ensureHost().ready();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
dispose() {
|
|
218
|
+
return this.withLock(() => this.disposeUnlocked());
|
|
219
|
+
}
|
|
220
|
+
/** Kill the host and re-prewarm the same session object (no second FauxnixSession). */
|
|
221
|
+
reset() {
|
|
222
|
+
return this.withLock(async () => {
|
|
223
|
+
await this.disposeUnlocked();
|
|
224
|
+
await this.ensureHost().ready();
|
|
225
|
+
});
|
|
206
226
|
}
|
|
207
|
-
async
|
|
227
|
+
async disposeUnlocked() {
|
|
208
228
|
if (this.host) {
|
|
209
229
|
await this.host.stop();
|
|
210
230
|
this.host = null;
|
|
@@ -255,18 +275,22 @@ export class FauxnixSession {
|
|
|
255
275
|
return env;
|
|
256
276
|
}
|
|
257
277
|
run(plans, opts = {}) {
|
|
258
|
-
|
|
259
|
-
this.runLock = done.then(() => undefined, () => undefined);
|
|
260
|
-
return done;
|
|
278
|
+
return this.withLock(() => runPlans(plans, this, opts, () => this.syncFromDisk(), () => this.ensureHost()));
|
|
261
279
|
}
|
|
262
280
|
}
|
|
263
281
|
async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
264
282
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
265
283
|
const deadline = Date.now() + timeoutMs;
|
|
284
|
+
const stdoutLimit = opts.stdoutLimit ?? DEFAULT_STDOUT_LIMIT;
|
|
285
|
+
const stderrLimit = opts.stderrLimit ?? DEFAULT_STDERR_LIMIT;
|
|
266
286
|
const timeoutMessage = '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
|
|
267
287
|
let stdout = '';
|
|
268
288
|
let stderr = '';
|
|
269
289
|
let exitCode = 0;
|
|
290
|
+
let timedOut = false;
|
|
291
|
+
let cancelled = false;
|
|
292
|
+
let truncated = false;
|
|
293
|
+
let spawnError;
|
|
270
294
|
// bash list semantics: `a && b ; c` runs c regardless of a; `a && b && c`
|
|
271
295
|
// skips b AND c when a fails. chainOk models the value of the current
|
|
272
296
|
// &&/|| chain; `;` segments always run and restart the chain.
|
|
@@ -282,9 +306,16 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
282
306
|
continue;
|
|
283
307
|
if (plan.op === '||' && chainOk)
|
|
284
308
|
continue;
|
|
309
|
+
if (opts.signal?.aborted) {
|
|
310
|
+
cancelled = true;
|
|
311
|
+
exitCode = 130;
|
|
312
|
+
session.prevExit = exitCode;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
285
315
|
if (Date.now() >= deadline) {
|
|
286
316
|
stderr += timeoutMessage;
|
|
287
317
|
exitCode = 124;
|
|
318
|
+
timedOut = true;
|
|
288
319
|
session.prevExit = exitCode;
|
|
289
320
|
break;
|
|
290
321
|
}
|
|
@@ -361,9 +392,16 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
361
392
|
continue;
|
|
362
393
|
}
|
|
363
394
|
const remainingMs = deadline - Date.now();
|
|
395
|
+
if (opts.signal?.aborted) {
|
|
396
|
+
cancelled = true;
|
|
397
|
+
exitCode = 130;
|
|
398
|
+
session.prevExit = exitCode;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
364
401
|
if (remainingMs <= 0) {
|
|
365
402
|
stderr += timeoutMessage;
|
|
366
403
|
exitCode = 124;
|
|
404
|
+
timedOut = true;
|
|
367
405
|
session.prevExit = exitCode;
|
|
368
406
|
break;
|
|
369
407
|
}
|
|
@@ -372,14 +410,22 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
372
410
|
FAUXNIX_CWD: currentDir,
|
|
373
411
|
FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
|
|
374
412
|
FAUXNIX_STDIN_FILE: red.stdinFile || '',
|
|
375
|
-
}, remainingMs);
|
|
376
|
-
if (inv.spawnError === 'ENOENT') {
|
|
413
|
+
}, remainingMs, opts.signal);
|
|
414
|
+
if (inv.spawnError === 'ENOENT' || inv.spawnError === 'START') {
|
|
377
415
|
stderr += inv.stderr.toString('utf8') || PS_MISSING_MESSAGE;
|
|
378
416
|
exitCode = 127;
|
|
417
|
+
spawnError = inv.spawnError;
|
|
379
418
|
session.prevExit = exitCode;
|
|
380
419
|
chainOk = false;
|
|
381
420
|
continue;
|
|
382
421
|
}
|
|
422
|
+
if (inv.cancelled) {
|
|
423
|
+
cancelled = true;
|
|
424
|
+
exitCode = 130;
|
|
425
|
+
session.prevExit = exitCode;
|
|
426
|
+
chainOk = false;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
383
429
|
afterSegment();
|
|
384
430
|
const decodePref = resolveNativePref();
|
|
385
431
|
// GNU line discipline: the PS host terminates Write-Output lines with
|
|
@@ -430,7 +476,11 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
430
476
|
}
|
|
431
477
|
stdout += segOut;
|
|
432
478
|
stderr += segErr;
|
|
433
|
-
|
|
479
|
+
if (inv.truncated)
|
|
480
|
+
truncated = true;
|
|
481
|
+
exitCode = inv.timedOut ? 124 : inv.cancelled ? 130 : inv.exitCode;
|
|
482
|
+
if (inv.timedOut)
|
|
483
|
+
timedOut = true;
|
|
434
484
|
session.prevExit = exitCode;
|
|
435
485
|
chainOk = exitCode === 0;
|
|
436
486
|
// Only inherit cwd from a segment that actually ran and whose
|
|
@@ -445,5 +495,25 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
445
495
|
closePrepFds(prepFds);
|
|
446
496
|
}
|
|
447
497
|
}
|
|
448
|
-
|
|
498
|
+
const clippedOut = clipUtf8(stdout, stdoutLimit);
|
|
499
|
+
const clippedErr = clipUtf8(stderr, stderrLimit);
|
|
500
|
+
if (clippedOut.truncated || clippedErr.truncated)
|
|
501
|
+
truncated = true;
|
|
502
|
+
return {
|
|
503
|
+
stdout: clippedOut.text,
|
|
504
|
+
stderr: clippedErr.text,
|
|
505
|
+
exitCode,
|
|
506
|
+
timedOut,
|
|
507
|
+
cancelled,
|
|
508
|
+
truncated,
|
|
509
|
+
spawnError,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function clipUtf8(text, limit) {
|
|
513
|
+
if (Buffer.byteLength(text, 'utf8') <= limit)
|
|
514
|
+
return { text, truncated: false };
|
|
515
|
+
let end = text.length;
|
|
516
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), 'utf8') > limit)
|
|
517
|
+
end--;
|
|
518
|
+
return { text: text.slice(0, end), truncated: true };
|
|
449
519
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import { ExecResult } from './executor.js';
|
|
1
2
|
import { translatePipelineBody } from './translator.js';
|
|
2
3
|
import './commands/install-all.js';
|
|
4
|
+
export declare function formatBashText(r: Pick<ExecResult, 'stdout' | 'stderr' | 'exitCode' | 'timedOut' | 'cancelled'>): string;
|
|
5
|
+
export declare function bashToolResult(r: ExecResult, sessionId: string, infra: boolean): {
|
|
6
|
+
isError?: true | undefined;
|
|
7
|
+
content: {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}[];
|
|
11
|
+
structuredContent: {
|
|
12
|
+
schemaVersion: 1;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
cancelled: boolean;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
3
22
|
export declare function startMcpServer(): Promise<void>;
|
|
4
23
|
export { translatePipelineBody };
|
package/dist/mcp.js
CHANGED
|
@@ -33,15 +33,46 @@ Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), e
|
|
|
33
33
|
|
|
34
34
|
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
35
35
|
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
36
|
-
Not supported: heredocs, while/until/case, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
|
|
36
|
+
Not supported: heredocs, while/until/case, env -i/--ignore-environment, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
|
|
37
37
|
|
|
38
38
|
CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is started when the MCP session begins (and after reset), so the first bash tool call is already warm.
|
|
39
|
-
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
39
|
+
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout, 130 cancelled). The tool also returns structuredContent (schemaVersion 1) with stdout/stderr/exitCode/timedOut/cancelled/truncated/sessionId.
|
|
40
40
|
|
|
41
41
|
Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
|
|
42
|
+
export function formatBashText(r) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (r.stdout.length)
|
|
45
|
+
parts.push(r.stdout.replace(/\n$/, ''));
|
|
46
|
+
if (r.stderr.length)
|
|
47
|
+
parts.push(r.stderr.replace(/\n$/, ''));
|
|
48
|
+
if (r.cancelled)
|
|
49
|
+
parts.push('Cancelled');
|
|
50
|
+
else if (r.timedOut)
|
|
51
|
+
parts.push('Exit code: 124');
|
|
52
|
+
else if (r.exitCode !== 0)
|
|
53
|
+
parts.push('Exit code: ' + r.exitCode);
|
|
54
|
+
return parts.length ? parts.join('\n') : '(no output)';
|
|
55
|
+
}
|
|
56
|
+
export function bashToolResult(r, sessionId, infra) {
|
|
57
|
+
const structuredContent = {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
stdout: r.stdout,
|
|
60
|
+
stderr: r.stderr,
|
|
61
|
+
exitCode: r.exitCode,
|
|
62
|
+
timedOut: r.timedOut,
|
|
63
|
+
cancelled: r.cancelled,
|
|
64
|
+
truncated: r.truncated,
|
|
65
|
+
sessionId,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: formatBashText(r) }],
|
|
69
|
+
structuredContent,
|
|
70
|
+
...(infra ? { isError: true } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
42
73
|
export async function startMcpServer() {
|
|
43
74
|
const server = new McpServer({ name: 'fauxnix', version: packageVersion }, { capabilities: { tools: {} } });
|
|
44
|
-
|
|
75
|
+
const session = new FauxnixSession();
|
|
45
76
|
await session.prewarm();
|
|
46
77
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
47
78
|
command: z.string().describe('The bash-style command line to run'),
|
|
@@ -52,23 +83,26 @@ export async function startMcpServer() {
|
|
|
52
83
|
.max(600_000)
|
|
53
84
|
.optional()
|
|
54
85
|
.describe('Timeout in milliseconds (default 120000)'),
|
|
55
|
-
}, EXEC_ANNOTATIONS, async ({ command, timeout_ms }) => {
|
|
86
|
+
}, EXEC_ANNOTATIONS, async ({ command, timeout_ms }, extra) => {
|
|
56
87
|
try {
|
|
57
88
|
const plans = translateCommandList(parseCommand(command));
|
|
58
|
-
const result = await session.run(plans, {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (result.exitCode !== 0)
|
|
65
|
-
parts.push('Exit code: ' + result.exitCode);
|
|
66
|
-
const text = parts.length ? parts.join('\n') : '(no output)';
|
|
67
|
-
return { content: [{ type: 'text', text }] };
|
|
89
|
+
const result = await session.run(plans, {
|
|
90
|
+
timeoutMs: timeout_ms,
|
|
91
|
+
signal: extra.signal,
|
|
92
|
+
});
|
|
93
|
+
const infra = result.spawnError === 'ENOENT' || result.spawnError === 'START';
|
|
94
|
+
return bashToolResult(result, session.id, infra);
|
|
68
95
|
}
|
|
69
96
|
catch (e) {
|
|
70
97
|
const msg = e instanceof Error ? e.message : String(e);
|
|
71
|
-
return {
|
|
98
|
+
return bashToolResult({
|
|
99
|
+
stdout: '',
|
|
100
|
+
stderr: msg,
|
|
101
|
+
exitCode: 2,
|
|
102
|
+
timedOut: false,
|
|
103
|
+
cancelled: false,
|
|
104
|
+
truncated: false,
|
|
105
|
+
}, session.id, true);
|
|
72
106
|
}
|
|
73
107
|
});
|
|
74
108
|
server.tool('fauxnix_translate', 'Translate a bash-style command into the equivalent PowerShell script WITHOUT executing it. Useful for learning/debugging what fauxnix does under the hood.', { command: z.string().describe('The bash-style command line to translate (never executed)') }, TRANSLATE_ANNOTATIONS, async ({ command }) => {
|
|
@@ -90,18 +124,45 @@ export async function startMcpServer() {
|
|
|
90
124
|
.describe('"status" shows the session state (cwd, tracked env keys); "reset" clears it back to a fresh shell'),
|
|
91
125
|
}, SESSION_ANNOTATIONS, async ({ action }) => {
|
|
92
126
|
if (action === 'reset') {
|
|
93
|
-
await session.
|
|
94
|
-
session = new FauxnixSession();
|
|
95
|
-
await session.prewarm();
|
|
127
|
+
await session.reset();
|
|
96
128
|
return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
|
|
97
129
|
}
|
|
98
130
|
const envKeys = Object.keys(session.env).sort();
|
|
99
131
|
const text = 'cwd: ' + (session.cwd ?? '(inherit from server start)') +
|
|
100
132
|
'\nenv keys: ' + (envKeys.length ? envKeys.join(', ') : '(none tracked)') +
|
|
133
|
+
'\nsession: ' + session.id +
|
|
101
134
|
'\ncommands registered: ' + registeredNames().length;
|
|
102
135
|
return { content: [{ type: 'text', text }] };
|
|
103
136
|
});
|
|
104
137
|
const transport = new StdioServerTransport();
|
|
138
|
+
let shuttingDown = false;
|
|
139
|
+
const shutdown = async () => {
|
|
140
|
+
if (shuttingDown)
|
|
141
|
+
return;
|
|
142
|
+
shuttingDown = true;
|
|
143
|
+
await session.dispose();
|
|
144
|
+
try {
|
|
145
|
+
await server.close();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* ignore */
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
process.stdin.on('end', () => {
|
|
152
|
+
void shutdown();
|
|
153
|
+
});
|
|
154
|
+
process.stdin.on('close', () => {
|
|
155
|
+
void shutdown();
|
|
156
|
+
});
|
|
157
|
+
process.on('SIGINT', () => {
|
|
158
|
+
void shutdown();
|
|
159
|
+
});
|
|
160
|
+
process.on('SIGTERM', () => {
|
|
161
|
+
void shutdown();
|
|
162
|
+
});
|
|
163
|
+
transport.onclose = () => {
|
|
164
|
+
void shutdown();
|
|
165
|
+
};
|
|
105
166
|
await server.connect(transport);
|
|
106
167
|
}
|
|
107
168
|
// keep referenced for tree-shaking clarity
|
package/dist/parser.js
CHANGED
|
@@ -586,28 +586,62 @@ export function parseCommand(input) {
|
|
|
586
586
|
let pos = 0;
|
|
587
587
|
const peek = () => tokens[pos];
|
|
588
588
|
const next = () => tokens[pos++];
|
|
589
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
590
|
+
/** Consume `;` / newline / `&&` / `||`. Trailing `&&`/`||` and `;;` fail loud (bash). */
|
|
591
|
+
const consumeListOp = (stops) => {
|
|
592
|
+
const t = peek();
|
|
593
|
+
if (t.type !== 'OP')
|
|
594
|
+
return null;
|
|
595
|
+
if (!(t.op === '&&' || t.op === '||' || isListSep(t.op)))
|
|
596
|
+
return null;
|
|
597
|
+
if (t.op === ';') {
|
|
598
|
+
next();
|
|
599
|
+
if (peek().type === 'OP' && peek().op === ';') {
|
|
600
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
601
|
+
}
|
|
602
|
+
return ';';
|
|
603
|
+
}
|
|
604
|
+
if (t.op === '\n') {
|
|
605
|
+
next();
|
|
606
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
607
|
+
next();
|
|
608
|
+
return ';';
|
|
609
|
+
}
|
|
610
|
+
const sep = t.op;
|
|
611
|
+
next();
|
|
612
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
613
|
+
next();
|
|
614
|
+
const n = peek();
|
|
615
|
+
const kw = peekKw();
|
|
616
|
+
if (n.type === 'OP' && n.op === ';') {
|
|
617
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
618
|
+
}
|
|
619
|
+
if (n.type === 'EOF' || (stops && kw && stops.has(kw))) {
|
|
620
|
+
throw new FauxnixParseError(n.type === 'EOF'
|
|
621
|
+
? 'fauxnix: syntax error: unexpected end of file after `' + sep + "'"
|
|
622
|
+
: "fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
623
|
+
}
|
|
624
|
+
return sep;
|
|
625
|
+
};
|
|
589
626
|
const parseList = () => {
|
|
590
627
|
const segments = [];
|
|
591
628
|
let op = ';';
|
|
592
|
-
|
|
593
|
-
|
|
629
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
630
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
631
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
632
|
+
}
|
|
594
633
|
next();
|
|
634
|
+
}
|
|
595
635
|
while (peek().type !== 'EOF') {
|
|
596
636
|
const pipeline = parsePipeline();
|
|
597
637
|
segments.push({ pipeline, op });
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
603
|
-
next();
|
|
604
|
-
}
|
|
605
|
-
else if (t.type === 'EOF') {
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
638
|
+
const nextOp = consumeListOp();
|
|
639
|
+
if (nextOp === null) {
|
|
640
|
+
if (peek().type === 'EOF')
|
|
641
|
+
break;
|
|
609
642
|
throw new FauxnixParseError('fauxnix: unexpected token after pipeline');
|
|
610
643
|
}
|
|
644
|
+
op = nextOp;
|
|
611
645
|
}
|
|
612
646
|
if (segments.length === 0)
|
|
613
647
|
throw new FauxnixParseError('fauxnix: empty command');
|
|
@@ -672,25 +706,22 @@ export function parseCommand(input) {
|
|
|
672
706
|
const stop = new Set(stops);
|
|
673
707
|
const segments = [];
|
|
674
708
|
let op = ';';
|
|
675
|
-
|
|
676
|
-
|
|
709
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
710
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
711
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
712
|
+
}
|
|
677
713
|
next();
|
|
714
|
+
}
|
|
678
715
|
while (peek().type !== 'EOF') {
|
|
679
716
|
const kw = peekKw();
|
|
680
717
|
if (kw && stop.has(kw))
|
|
681
718
|
break;
|
|
682
719
|
const pipeline = parsePipeline();
|
|
683
720
|
segments.push({ pipeline, op });
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
op = t.op === '\n' ? ';' : t.op;
|
|
687
|
-
next();
|
|
688
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
689
|
-
next();
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
721
|
+
const nextOp = consumeListOp(stop);
|
|
722
|
+
if (nextOp === null)
|
|
692
723
|
break;
|
|
693
|
-
|
|
724
|
+
op = nextOp;
|
|
694
725
|
}
|
|
695
726
|
if (segments.length === 0) {
|
|
696
727
|
throw new FauxnixParseError('fauxnix: empty command');
|
package/dist/ps-host.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export declare const PS_MISSING_MESSAGE: string;
|
|
2
|
+
export declare const DEFAULT_STDOUT_LIMIT = 8388608;
|
|
3
|
+
export declare const DEFAULT_STDERR_LIMIT = 1048576;
|
|
2
4
|
export interface HostInvokeResult {
|
|
3
5
|
stdout: Buffer;
|
|
4
6
|
stderr: Buffer;
|
|
5
7
|
exitCode: number;
|
|
6
8
|
timedOut: boolean;
|
|
9
|
+
cancelled: boolean;
|
|
10
|
+
truncated: boolean;
|
|
7
11
|
spawnError?: 'ENOENT' | 'START';
|
|
8
12
|
spawnMessage?: string;
|
|
9
13
|
}
|
|
@@ -39,8 +43,10 @@ export declare class PowerShellHost {
|
|
|
39
43
|
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
40
44
|
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
41
45
|
ready(): Promise<HostInvokeResult | null>;
|
|
42
|
-
invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
|
|
46
|
+
invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal): Promise<HostInvokeResult>;
|
|
47
|
+
drainNativeStderr(): Buffer;
|
|
43
48
|
stop(): Promise<void>;
|
|
49
|
+
private cancelledResult;
|
|
44
50
|
private invokeSerial;
|
|
45
51
|
private ensureStarted;
|
|
46
52
|
private deadRestart;
|
package/dist/ps-host.js
CHANGED
|
@@ -6,6 +6,8 @@ const READY_TIMEOUT_MS = 30_000;
|
|
|
6
6
|
export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
7
7
|
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
8
8
|
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
|
+
export const DEFAULT_STDOUT_LIMIT = 8_388_608;
|
|
10
|
+
export const DEFAULT_STDERR_LIMIT = 1_048_576;
|
|
9
11
|
export function encodeHostRequest(id, script, env) {
|
|
10
12
|
return JSON.stringify({
|
|
11
13
|
id,
|
|
@@ -50,11 +52,18 @@ export class PowerShellHost {
|
|
|
50
52
|
async ready() {
|
|
51
53
|
return this.ensureStarted();
|
|
52
54
|
}
|
|
53
|
-
async invoke(script, env, timeoutMs) {
|
|
54
|
-
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
|
|
55
|
+
async invoke(script, env, timeoutMs, signal) {
|
|
56
|
+
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal));
|
|
55
57
|
this.invokeLock = run.then(() => undefined, () => undefined);
|
|
56
58
|
return run;
|
|
57
59
|
}
|
|
60
|
+
drainNativeStderr() {
|
|
61
|
+
if (!this.stderrChunks.length)
|
|
62
|
+
return Buffer.alloc(0);
|
|
63
|
+
const b = Buffer.concat(this.stderrChunks);
|
|
64
|
+
this.stderrChunks = [];
|
|
65
|
+
return b;
|
|
66
|
+
}
|
|
58
67
|
async stop() {
|
|
59
68
|
const proc = this.proc;
|
|
60
69
|
this.proc = null;
|
|
@@ -82,10 +91,24 @@ export class PowerShellHost {
|
|
|
82
91
|
});
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
|
-
|
|
94
|
+
cancelledResult() {
|
|
95
|
+
return {
|
|
96
|
+
stdout: Buffer.alloc(0),
|
|
97
|
+
stderr: Buffer.alloc(0),
|
|
98
|
+
exitCode: 130,
|
|
99
|
+
timedOut: false,
|
|
100
|
+
cancelled: true,
|
|
101
|
+
truncated: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async invokeSerial(script, env, timeoutMs, signal) {
|
|
105
|
+
if (signal?.aborted) {
|
|
106
|
+
await this.stop();
|
|
107
|
+
return this.cancelledResult();
|
|
108
|
+
}
|
|
86
109
|
const started = await this.ensureStarted();
|
|
87
110
|
if (started)
|
|
88
|
-
return started;
|
|
111
|
+
return { ...started, cancelled: false, truncated: false };
|
|
89
112
|
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
90
113
|
const line = encodeHostRequest(id, script, env);
|
|
91
114
|
try {
|
|
@@ -98,24 +121,45 @@ export class PowerShellHost {
|
|
|
98
121
|
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
|
|
99
122
|
exitCode: 1,
|
|
100
123
|
timedOut: false,
|
|
124
|
+
cancelled: false,
|
|
125
|
+
truncated: false,
|
|
101
126
|
spawnMessage: e.message,
|
|
102
127
|
};
|
|
103
128
|
}
|
|
129
|
+
let cancelled = false;
|
|
130
|
+
const onAbort = () => {
|
|
131
|
+
cancelled = true;
|
|
132
|
+
void this.stop();
|
|
133
|
+
};
|
|
134
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
135
|
try {
|
|
105
136
|
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
106
137
|
const msg = decodeHostResponse(raw);
|
|
138
|
+
const native = this.drainNativeStderr();
|
|
107
139
|
return {
|
|
108
140
|
stdout: msg.stdout,
|
|
109
|
-
stderr: msg.stderr,
|
|
141
|
+
stderr: native.length ? Buffer.concat([msg.stderr, native]) : msg.stderr,
|
|
110
142
|
exitCode: msg.exitCode,
|
|
111
143
|
timedOut: false,
|
|
144
|
+
cancelled: false,
|
|
145
|
+
truncated: false,
|
|
112
146
|
};
|
|
113
147
|
}
|
|
114
148
|
catch (e) {
|
|
115
149
|
const timedOut = e.timedOut === true;
|
|
116
150
|
await this.stop();
|
|
151
|
+
this.drainNativeStderr();
|
|
152
|
+
if (cancelled || signal?.aborted)
|
|
153
|
+
return this.cancelledResult();
|
|
117
154
|
if (timedOut) {
|
|
118
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
stdout: Buffer.alloc(0),
|
|
157
|
+
stderr: Buffer.alloc(0),
|
|
158
|
+
exitCode: 124,
|
|
159
|
+
timedOut: true,
|
|
160
|
+
cancelled: false,
|
|
161
|
+
truncated: false,
|
|
162
|
+
};
|
|
119
163
|
}
|
|
120
164
|
if (this.closeErr && this.closeErr.code === 'ENOENT') {
|
|
121
165
|
return {
|
|
@@ -123,6 +167,8 @@ export class PowerShellHost {
|
|
|
123
167
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
124
168
|
exitCode: 127,
|
|
125
169
|
timedOut: false,
|
|
170
|
+
cancelled: false,
|
|
171
|
+
truncated: false,
|
|
126
172
|
spawnError: 'ENOENT',
|
|
127
173
|
};
|
|
128
174
|
}
|
|
@@ -134,8 +180,13 @@ export class PowerShellHost {
|
|
|
134
180
|
'\n', 'utf8'),
|
|
135
181
|
exitCode: code === 0 ? 1 : code,
|
|
136
182
|
timedOut: false,
|
|
183
|
+
cancelled: false,
|
|
184
|
+
truncated: false,
|
|
137
185
|
};
|
|
138
186
|
}
|
|
187
|
+
finally {
|
|
188
|
+
signal?.removeEventListener('abort', onAbort);
|
|
189
|
+
}
|
|
139
190
|
}
|
|
140
191
|
async ensureStarted() {
|
|
141
192
|
if (this.proc && !this.closed)
|
|
@@ -154,6 +205,8 @@ export class PowerShellHost {
|
|
|
154
205
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
155
206
|
exitCode: 127,
|
|
156
207
|
timedOut: false,
|
|
208
|
+
cancelled: false,
|
|
209
|
+
truncated: false,
|
|
157
210
|
spawnError: 'ENOENT',
|
|
158
211
|
};
|
|
159
212
|
}
|
|
@@ -162,6 +215,8 @@ export class PowerShellHost {
|
|
|
162
215
|
stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
|
|
163
216
|
exitCode: 127,
|
|
164
217
|
timedOut: false,
|
|
218
|
+
cancelled: false,
|
|
219
|
+
truncated: false,
|
|
165
220
|
spawnError: 'START',
|
|
166
221
|
spawnMessage: err.message,
|
|
167
222
|
};
|
|
@@ -189,14 +244,26 @@ export class PowerShellHost {
|
|
|
189
244
|
windowsHide: true,
|
|
190
245
|
});
|
|
191
246
|
this.proc = child;
|
|
192
|
-
child.stdout.on('data', (d) =>
|
|
193
|
-
|
|
247
|
+
child.stdout.on('data', (d) => {
|
|
248
|
+
if (this.proc !== child)
|
|
249
|
+
return;
|
|
250
|
+
this.onStdout(d);
|
|
251
|
+
});
|
|
252
|
+
child.stderr.on('data', (d) => {
|
|
253
|
+
if (this.proc !== child)
|
|
254
|
+
return;
|
|
255
|
+
this.stderrChunks.push(d);
|
|
256
|
+
});
|
|
194
257
|
child.on('error', (e) => {
|
|
258
|
+
if (this.proc !== child)
|
|
259
|
+
return;
|
|
195
260
|
this.closeErr = e;
|
|
196
261
|
this.closed = true;
|
|
197
262
|
this.failWaiters(e);
|
|
198
263
|
});
|
|
199
264
|
child.on('close', (c) => {
|
|
265
|
+
if (this.proc !== child)
|
|
266
|
+
return;
|
|
200
267
|
this.closeCode = c;
|
|
201
268
|
this.closed = true;
|
|
202
269
|
this.proc = null;
|