fauxnix-cli 0.7.1 → 0.9.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 +6 -0
- package/dist/cli.js +11 -1
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +356 -89
- package/dist/commands/install-all.js +7 -4
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.d.ts +2 -1
- package/dist/commands/text-filters.js +67 -7
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +95 -4
- 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 +46 -2
- package/dist/ps-host.js +181 -17
- package/dist/registry.d.ts +54 -0
- package/dist/registry.js +182 -0
- package/dist/translator.js +65 -7
- package/package.json +1 -1
|
@@ -268,13 +268,23 @@ const grep = (args) => {
|
|
|
268
268
|
const excludeDirGlobs = filterOptions
|
|
269
269
|
.filter((o) => o.name === '--exclude-dir')
|
|
270
270
|
.map((o) => o.value.replace(/[\\/]+$/, ''));
|
|
271
|
-
const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C'], filterOptionNames);
|
|
272
|
-
const missingFilterOption = missingValue.find((o) => filterOptionNames.includes(o));
|
|
271
|
+
const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C', 'm', 'e'], [...filterOptionNames, '--max-count', '--regexp']);
|
|
272
|
+
const missingFilterOption = missingValue.find((o) => [...filterOptionNames, '-m', '--max-count', '-e', '--regexp'].includes(o));
|
|
273
273
|
if (missingFilterOption) {
|
|
274
274
|
return ('[Console]::Error.WriteLine(' +
|
|
275
275
|
psStr("grep: option '" + missingFilterOption + "' requires an argument") +
|
|
276
276
|
'); $script:fx_exit = 2');
|
|
277
277
|
}
|
|
278
|
+
const maxCountRaw = values.get('-m') ?? values.get('--max-count');
|
|
279
|
+
let maxCount = null;
|
|
280
|
+
if (maxCountRaw !== undefined) {
|
|
281
|
+
if (!/^\d+$/.test(maxCountRaw)) {
|
|
282
|
+
return ('[Console]::Error.WriteLine(' +
|
|
283
|
+
psStr("grep: invalid max count '" + maxCountRaw + "'") +
|
|
284
|
+
'); $script:fx_exit = 2');
|
|
285
|
+
}
|
|
286
|
+
maxCount = parseInt(maxCountRaw, 10);
|
|
287
|
+
}
|
|
278
288
|
const ci = flags.has('i');
|
|
279
289
|
const inv = flags.has('v');
|
|
280
290
|
const num = flags.has('n');
|
|
@@ -294,11 +304,12 @@ const grep = (args) => {
|
|
|
294
304
|
};
|
|
295
305
|
const ctxA = Math.max(toInt(values.get('-A')), toInt(values.get('-C')));
|
|
296
306
|
const ctxB = Math.max(toInt(values.get('-B')), toInt(values.get('-C')));
|
|
297
|
-
|
|
307
|
+
const ePat = values.get('-e') ?? values.get('--regexp');
|
|
308
|
+
if (ePat === undefined && operandWords.length === 0) {
|
|
298
309
|
return ("[Console]::Error.WriteLine('usage: grep [OPTION]... PATTERN [FILE]...'); $script:fx_exit = 2");
|
|
299
310
|
}
|
|
300
|
-
const patternWord = operandWords[0];
|
|
301
|
-
const fileWords = operandWords.slice(1);
|
|
311
|
+
const patternWord = ePat !== undefined ? [{ kind: 'Text', text: ePat }] : operandWords[0];
|
|
312
|
+
const fileWords = ePat !== undefined ? operandWords : operandWords.slice(1);
|
|
302
313
|
const patLit = literalOfWord(patternWord);
|
|
303
314
|
let patExpr;
|
|
304
315
|
if (fixed || patLit === null) {
|
|
@@ -435,10 +446,16 @@ const grep = (args) => {
|
|
|
435
446
|
lines.push('}');
|
|
436
447
|
// --- per-source scan body ----------------------------------------------
|
|
437
448
|
const scan = [];
|
|
449
|
+
if (maxCount !== null)
|
|
450
|
+
scan.push('$fx_mleft = ' + maxCount);
|
|
438
451
|
if (cntMode) {
|
|
439
452
|
scan.push('$fx_c = 0');
|
|
440
453
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
441
|
-
|
|
454
|
+
if (maxCount !== null)
|
|
455
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
456
|
+
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_c++' +
|
|
457
|
+
(maxCount !== null ? '; $fx_mleft--; if ($fx_mleft -le 0) { break }' : '') +
|
|
458
|
+
' }');
|
|
442
459
|
scan.push('}');
|
|
443
460
|
scan.push('if ($fx_c -gt 0) { $fx_any = $true }');
|
|
444
461
|
scan.push('if ($fx_pre) { $fx_disp + \':\' + [string]$fx_c } else { [string]$fx_c }');
|
|
@@ -446,21 +463,29 @@ const grep = (args) => {
|
|
|
446
463
|
else if (listMode) {
|
|
447
464
|
scan.push('$fx_hit1 = $false');
|
|
448
465
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
466
|
+
if (maxCount !== null)
|
|
467
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
449
468
|
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_hit1 = $true; break }');
|
|
450
469
|
scan.push('}');
|
|
451
470
|
scan.push('if ($fx_hit1) { $fx_any = $true; $fx_disp }');
|
|
452
471
|
}
|
|
453
472
|
else if (quiet) {
|
|
454
473
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
474
|
+
if (maxCount !== null)
|
|
475
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
455
476
|
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_any = $true; break }');
|
|
456
477
|
scan.push('}');
|
|
457
478
|
}
|
|
458
479
|
else {
|
|
459
480
|
scan.push('$fx_hits = @()');
|
|
460
481
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
482
|
+
if (maxCount !== null)
|
|
483
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
461
484
|
scan.push(' $fx_l = $fx_ls[$fx_i]');
|
|
462
485
|
scan.push(' if (fx-gmatch $fx_l) {');
|
|
463
486
|
scan.push(' $fx_any = $true');
|
|
487
|
+
if (maxCount !== null)
|
|
488
|
+
scan.push(' $fx_mleft--');
|
|
464
489
|
if (onlyMatch && !inv) {
|
|
465
490
|
if (fixed) {
|
|
466
491
|
if (ci) {
|
|
@@ -492,6 +517,8 @@ const grep = (args) => {
|
|
|
492
517
|
else if (!onlyMatch) {
|
|
493
518
|
scan.push(' $fx_hits += $fx_i');
|
|
494
519
|
}
|
|
520
|
+
if (maxCount !== null)
|
|
521
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
495
522
|
scan.push(' }');
|
|
496
523
|
scan.push('}');
|
|
497
524
|
if (!onlyMatch) {
|
|
@@ -2347,8 +2374,41 @@ const tr = (args) => {
|
|
|
2347
2374
|
return lines.join('\n');
|
|
2348
2375
|
};
|
|
2349
2376
|
/* ------------------------------------------------------------------ */
|
|
2377
|
+
export const specs = [
|
|
2378
|
+
{
|
|
2379
|
+
names: ['grep'],
|
|
2380
|
+
options: [
|
|
2381
|
+
{ short: 'i', support: 'implemented' },
|
|
2382
|
+
{ short: 'v', support: 'implemented' },
|
|
2383
|
+
{ short: 'n', support: 'implemented' },
|
|
2384
|
+
{ short: 'c', support: 'implemented' },
|
|
2385
|
+
{ short: 'l', support: 'implemented' },
|
|
2386
|
+
{ short: 'r', support: 'implemented' },
|
|
2387
|
+
{ short: 'R', support: 'implemented' },
|
|
2388
|
+
{ short: 'E', support: 'implemented' },
|
|
2389
|
+
{ short: 'F', support: 'implemented' },
|
|
2390
|
+
{ short: 'w', support: 'implemented' },
|
|
2391
|
+
{ short: 'q', support: 'implemented' },
|
|
2392
|
+
{ short: 'o', support: 'implemented' },
|
|
2393
|
+
{ short: 'h', support: 'implemented' },
|
|
2394
|
+
{ short: 'H', support: 'implemented' },
|
|
2395
|
+
{ short: 'A', takesValue: true, support: 'implemented' },
|
|
2396
|
+
{ short: 'B', takesValue: true, support: 'implemented' },
|
|
2397
|
+
{ short: 'C', takesValue: true, support: 'implemented' },
|
|
2398
|
+
{ short: 'm', long: '--max-count', takesValue: true, support: 'implemented' },
|
|
2399
|
+
{ short: 'e', long: '--regexp', takesValue: true, support: 'implemented' },
|
|
2400
|
+
{ long: '--include', takesValue: true, support: 'implemented' },
|
|
2401
|
+
{ long: '--exclude', takesValue: true, support: 'implemented' },
|
|
2402
|
+
{ long: '--exclude-dir', takesValue: true, support: 'implemented' },
|
|
2403
|
+
],
|
|
2404
|
+
effects: ['read'],
|
|
2405
|
+
platform: 'windows-ps51',
|
|
2406
|
+
dispatch: 'translated',
|
|
2407
|
+
usageExit: 2,
|
|
2408
|
+
handler: grep,
|
|
2409
|
+
},
|
|
2410
|
+
];
|
|
2350
2411
|
export const handlers = {
|
|
2351
|
-
grep,
|
|
2352
2412
|
egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
|
|
2353
2413
|
sed,
|
|
2354
2414
|
awk,
|
package/dist/commands/text-io.js
CHANGED
|
@@ -419,6 +419,37 @@ const head = (args, ctx) => {
|
|
|
419
419
|
continue;
|
|
420
420
|
}
|
|
421
421
|
if (t.startsWith('--')) {
|
|
422
|
+
const eq = t.indexOf('=');
|
|
423
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
424
|
+
const inline = eq >= 0 ? t.slice(eq + 1) : null;
|
|
425
|
+
if (name === '--lines' || name === '--bytes') {
|
|
426
|
+
let val = inline;
|
|
427
|
+
if (val === null) {
|
|
428
|
+
if (i + 1 >= args.length) {
|
|
429
|
+
return psErrExpr(psStr('head: option requires an argument -- ' + name.slice(2)));
|
|
430
|
+
}
|
|
431
|
+
val = wordToString(args[i + 1]);
|
|
432
|
+
i += 2;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
i++;
|
|
436
|
+
}
|
|
437
|
+
if (name === '--bytes')
|
|
438
|
+
nBytes = val;
|
|
439
|
+
else
|
|
440
|
+
nLines = val;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (name === '--quiet' || name === '--silent') {
|
|
444
|
+
quiet = true;
|
|
445
|
+
i++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (name === '--verbose') {
|
|
449
|
+
verbose = true;
|
|
450
|
+
i++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
422
453
|
i++;
|
|
423
454
|
continue;
|
|
424
455
|
}
|
|
@@ -464,6 +495,9 @@ const head = (args, ctx) => {
|
|
|
464
495
|
}
|
|
465
496
|
const bytesMode = nBytes !== null;
|
|
466
497
|
const countLit = bytesMode ? nBytes : nLines !== null ? nLines : '10';
|
|
498
|
+
if (!/^[+-]?\d+$/.test(countLit)) {
|
|
499
|
+
return psErrExpr(psStr("head: invalid number of " + (bytesMode ? 'bytes' : 'lines') + ": '" + countLit + "'"));
|
|
500
|
+
}
|
|
467
501
|
const lines = [
|
|
468
502
|
PS_GLOB_FN,
|
|
469
503
|
PS_READTEXT_FN,
|
|
@@ -516,6 +550,39 @@ const tail = (args, ctx) => {
|
|
|
516
550
|
continue;
|
|
517
551
|
}
|
|
518
552
|
if (t.startsWith('--')) {
|
|
553
|
+
const eq = t.indexOf('=');
|
|
554
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
555
|
+
const inline = eq >= 0 ? t.slice(eq + 1) : null;
|
|
556
|
+
if (name === '--lines' || name === '--bytes') {
|
|
557
|
+
let val = inline;
|
|
558
|
+
if (val === null) {
|
|
559
|
+
if (i + 1 >= args.length) {
|
|
560
|
+
return psErrExpr(psStr('tail: option requires an argument -- ' + name.slice(2)));
|
|
561
|
+
}
|
|
562
|
+
val = wordToString(args[i + 1]);
|
|
563
|
+
i += 2;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
i++;
|
|
567
|
+
}
|
|
568
|
+
if (name === '--bytes')
|
|
569
|
+
nBytes = val;
|
|
570
|
+
else if (val.startsWith('+'))
|
|
571
|
+
fromLine = val.slice(1);
|
|
572
|
+
else
|
|
573
|
+
nLines = val.replace(/^-/, '');
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (name === '--quiet' || name === '--silent') {
|
|
577
|
+
quiet = true;
|
|
578
|
+
i++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (name === '--verbose') {
|
|
582
|
+
verbose = true;
|
|
583
|
+
i++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
519
586
|
i++;
|
|
520
587
|
continue;
|
|
521
588
|
}
|
|
@@ -691,8 +758,8 @@ const wc = (args) => {
|
|
|
691
758
|
/* tee */
|
|
692
759
|
/* ------------------------------------------------------------------ */
|
|
693
760
|
const tee = (args, ctx) => {
|
|
694
|
-
const { flags, operandWords } = parseWords(args);
|
|
695
|
-
const append = flags.has('a') ||
|
|
761
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
762
|
+
const append = flags.has('a') || longs.has('--append');
|
|
696
763
|
return [
|
|
697
764
|
PS_WRITE_FN,
|
|
698
765
|
fxTermLine(ctx.position),
|
|
@@ -1142,14 +1209,38 @@ const xargs = (args) => {
|
|
|
1142
1209
|
].join('\n');
|
|
1143
1210
|
};
|
|
1144
1211
|
/* ------------------------------------------------------------------ */
|
|
1212
|
+
export const specs = [
|
|
1213
|
+
{
|
|
1214
|
+
names: ['tee'],
|
|
1215
|
+
options: [
|
|
1216
|
+
{ short: 'a', long: '--append', support: 'implemented' },
|
|
1217
|
+
],
|
|
1218
|
+
effects: ['read', 'write'],
|
|
1219
|
+
platform: 'windows-ps51',
|
|
1220
|
+
dispatch: 'translated',
|
|
1221
|
+
handler: tee,
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
names: ['head'],
|
|
1225
|
+
options: [
|
|
1226
|
+
{ short: 'n', long: '--lines', takesValue: true, support: 'implemented' },
|
|
1227
|
+
{ short: 'c', long: '--bytes', takesValue: true, support: 'implemented' },
|
|
1228
|
+
{ short: 'q', long: '--quiet', support: 'implemented' },
|
|
1229
|
+
{ long: '--silent', support: 'implemented' },
|
|
1230
|
+
{ short: 'v', long: '--verbose', support: 'implemented' },
|
|
1231
|
+
],
|
|
1232
|
+
effects: ['read'],
|
|
1233
|
+
platform: 'windows-ps51',
|
|
1234
|
+
dispatch: 'translated',
|
|
1235
|
+
handler: head,
|
|
1236
|
+
},
|
|
1237
|
+
];
|
|
1145
1238
|
export const handlers = {
|
|
1146
1239
|
echo,
|
|
1147
1240
|
printf,
|
|
1148
1241
|
cat,
|
|
1149
|
-
head,
|
|
1150
1242
|
tail,
|
|
1151
1243
|
wc,
|
|
1152
|
-
tee,
|
|
1153
1244
|
nl,
|
|
1154
1245
|
tac,
|
|
1155
1246
|
md5sum,
|
package/dist/executor.d.ts
CHANGED
|
@@ -3,14 +3,22 @@ export interface ExecResult {
|
|
|
3
3
|
stdout: string;
|
|
4
4
|
stderr: string;
|
|
5
5
|
exitCode: number;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
cancelled: boolean;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
spawnError?: 'ENOENT' | 'START';
|
|
6
10
|
}
|
|
7
11
|
export interface ExecOptions {
|
|
8
12
|
timeoutMs?: number;
|
|
9
13
|
/** Extra environment layered over the session (used by MCP per-call cwd). */
|
|
10
14
|
cwd?: string;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
stdoutLimit?: number;
|
|
17
|
+
stderrLimit?: number;
|
|
11
18
|
}
|
|
12
19
|
/** Session persists cwd and env across segments, like a real shell. */
|
|
13
20
|
export declare class FauxnixSession {
|
|
21
|
+
id: string;
|
|
14
22
|
cwd: string | null;
|
|
15
23
|
env: Record<string, string>;
|
|
16
24
|
/** Exit code of the previous segment — powers bash's `$?`. */
|
|
@@ -20,14 +28,18 @@ export declare class FauxnixSession {
|
|
|
20
28
|
private scriptFile;
|
|
21
29
|
private hostFile;
|
|
22
30
|
private host;
|
|
23
|
-
private
|
|
31
|
+
private lifecycleLock;
|
|
24
32
|
constructor();
|
|
25
33
|
private bindFiles;
|
|
34
|
+
private withLock;
|
|
26
35
|
private syncFromDisk;
|
|
27
36
|
private ensureHost;
|
|
28
37
|
/** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
|
|
29
38
|
prewarm(): Promise<void>;
|
|
30
39
|
dispose(): Promise<void>;
|
|
40
|
+
/** Kill the host and re-prewarm the same session object (no second FauxnixSession). */
|
|
41
|
+
reset(): Promise<void>;
|
|
42
|
+
private disposeUnlocked;
|
|
31
43
|
/** env for the child powershell process. */
|
|
32
44
|
childEnv(cwdOverride?: string, stdinFile?: string | null): NodeJS.ProcessEnv;
|
|
33
45
|
run(plans: SegmentPlan[], opts?: ExecOptions): Promise<ExecResult>;
|
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, { stdoutLimit, stderrLimit });
|
|
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 };
|