fauxnix-cli 0.9.3 → 0.12.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 +84 -26
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -20
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +169 -25
- package/dist/commands/install-all.js +4 -2
- package/dist/commands/net.d.ts +9 -0
- package/dist/commands/net.js +51 -15
- package/dist/commands/sysinfo.d.ts +2 -1
- package/dist/commands/sysinfo.js +610 -82
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +99 -13
- package/dist/commands/text-io.js +72 -43
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +292 -0
- package/dist/errors.js +14 -4
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +194 -51
- package/dist/install.d.ts +15 -0
- package/dist/install.js +247 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +49 -26
- package/dist/parser.js +432 -35
- package/dist/powershell.d.ts +22 -0
- package/dist/powershell.js +129 -0
- package/dist/ps-host.d.ts +41 -13
- package/dist/ps-host.js +302 -40
- package/dist/qwen-launch.d.ts +12 -0
- package/dist/qwen-launch.js +29 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +109 -1
- package/dist/translator.d.ts +42 -9
- package/dist/translator.js +697 -95
- package/package.json +3 -1
package/dist/ps-host.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readSync, rmSync, statSync, writeFileSync, writeSync, } from 'node:fs';
|
|
3
3
|
import { hostBootstrapScript } from './translator.js';
|
|
4
|
-
|
|
4
|
+
import { POWERSHELL_ARGS, powerShellMissingMessage, resolvePowerShell, } from './powershell.js';
|
|
5
|
+
class NativeStderrSpoolError extends Error {
|
|
6
|
+
constructor(operation, spoolPath, cause) {
|
|
7
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
8
|
+
const code = cause?.code;
|
|
9
|
+
super('fauxnix: native stderr spool ' +
|
|
10
|
+
operation +
|
|
11
|
+
' failed' +
|
|
12
|
+
(code ? ' (' + code + ')' : '') +
|
|
13
|
+
(spoolPath ? ' for ' + spoolPath : '') +
|
|
14
|
+
': ' +
|
|
15
|
+
detail);
|
|
16
|
+
this.name = 'NativeStderrSpoolError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
5
19
|
const READY_TIMEOUT_MS = 30_000;
|
|
6
|
-
export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
7
|
-
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
8
|
-
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
20
|
export const DEFAULT_STDOUT_LIMIT = 8_388_608;
|
|
10
21
|
export const DEFAULT_STDERR_LIMIT = 1_048_576;
|
|
11
22
|
export function encodeHostRequest(id, script, env, opts) {
|
|
@@ -19,6 +30,12 @@ export function encodeHostRequest(id, script, env, opts) {
|
|
|
19
30
|
body.type = 'run';
|
|
20
31
|
body.stdoutLimit = opts.stdoutLimit ?? DEFAULT_STDOUT_LIMIT;
|
|
21
32
|
body.stderrLimit = opts.stderrLimit ?? DEFAULT_STDERR_LIMIT;
|
|
33
|
+
body.stdoutMode = opts.stdoutMode ?? 'capture';
|
|
34
|
+
body.stderrMode = opts.stderrMode ?? 'capture';
|
|
35
|
+
if (opts.stdoutSpoolPath)
|
|
36
|
+
body.stdoutSpoolPath = opts.stdoutSpoolPath;
|
|
37
|
+
if (opts.stderrSpoolPath)
|
|
38
|
+
body.stderrSpoolPath = opts.stderrSpoolPath;
|
|
22
39
|
}
|
|
23
40
|
return JSON.stringify(body);
|
|
24
41
|
}
|
|
@@ -42,27 +59,32 @@ export function decodeHostResponse(line) {
|
|
|
42
59
|
};
|
|
43
60
|
}
|
|
44
61
|
/**
|
|
45
|
-
* One resident
|
|
46
|
-
* command stdout/stderr come back as base64 so
|
|
47
|
-
* encoding cannot scramble the payload.
|
|
62
|
+
* One resident selected PowerShell process. Frames are UTF-8 JSON lines;
|
|
63
|
+
* command stdout/stderr come back as base64 so Windows PowerShell 5.1's
|
|
64
|
+
* UTF-16LE pipe encoding cannot scramble the payload.
|
|
48
65
|
*/
|
|
49
66
|
export class PowerShellHost {
|
|
50
67
|
hostFile;
|
|
51
68
|
envFn;
|
|
69
|
+
powerShell;
|
|
70
|
+
nativeSpoolWrite;
|
|
52
71
|
proc = null;
|
|
53
72
|
stdoutBuf = Buffer.alloc(0);
|
|
54
73
|
queuedLines = [];
|
|
55
74
|
waiters = [];
|
|
56
75
|
stderrChunks = [];
|
|
76
|
+
nativeCapture = null;
|
|
57
77
|
closeCode;
|
|
58
78
|
closeErr = null;
|
|
59
79
|
closed = false;
|
|
60
80
|
startLock = null;
|
|
61
81
|
invokeLock = Promise.resolve();
|
|
62
82
|
protocol = 1;
|
|
63
|
-
constructor(hostFile, envFn) {
|
|
83
|
+
constructor(hostFile, envFn, powerShell = resolvePowerShell(), nativeSpoolWrite = (fd, buffer, offset, length) => writeSync(fd, buffer, offset, length)) {
|
|
64
84
|
this.hostFile = hostFile;
|
|
65
85
|
this.envFn = envFn;
|
|
86
|
+
this.powerShell = powerShell;
|
|
87
|
+
this.nativeSpoolWrite = nativeSpoolWrite;
|
|
66
88
|
}
|
|
67
89
|
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
68
90
|
async ready() {
|
|
@@ -81,10 +103,11 @@ export class PowerShellHost {
|
|
|
81
103
|
return b;
|
|
82
104
|
}
|
|
83
105
|
async stop() {
|
|
106
|
+
this.cancelNativeCapture();
|
|
84
107
|
const proc = this.proc;
|
|
85
108
|
this.proc = null;
|
|
86
109
|
this.closed = true;
|
|
87
|
-
this.failWaiters(new Error('fauxnix:
|
|
110
|
+
this.failWaiters(new Error('fauxnix: PowerShell host stopped'));
|
|
88
111
|
if (!proc)
|
|
89
112
|
return;
|
|
90
113
|
try {
|
|
@@ -126,14 +149,31 @@ export class PowerShellHost {
|
|
|
126
149
|
if (started)
|
|
127
150
|
return { ...started, cancelled: false, truncated: false };
|
|
128
151
|
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
129
|
-
const
|
|
130
|
-
|
|
152
|
+
const resolvedLimits = { ...limits };
|
|
153
|
+
if (resolvedLimits.stdoutMode === 'spool') {
|
|
154
|
+
resolvedLimits.stdoutSpoolPath = this.hostFile + '.' + id + '.stdout';
|
|
155
|
+
}
|
|
156
|
+
if (resolvedLimits.stderrMode === 'spool') {
|
|
157
|
+
resolvedLimits.stderrSpoolPath = this.hostFile + '.' + id + '.stderr';
|
|
158
|
+
}
|
|
159
|
+
if (resolvedLimits.stderrMode !== 'discard') {
|
|
160
|
+
resolvedLimits.nativeStderrSpoolPath = this.hostFile + '.' + id + '.native-stderr';
|
|
161
|
+
}
|
|
162
|
+
const nativeSpoolDir = this.hostFile + '.' + id + '.native';
|
|
163
|
+
mkdirSync(nativeSpoolDir);
|
|
164
|
+
const requestEnv = { ...env, FAUXNIX_NATIVE_SPOOL_DIR: nativeSpoolDir };
|
|
165
|
+
const line = encodeHostRequest(id, script, requestEnv, this.protocol === 2
|
|
166
|
+
? {
|
|
167
|
+
v: 2,
|
|
168
|
+
...resolvedLimits,
|
|
169
|
+
}
|
|
131
170
|
: undefined);
|
|
132
171
|
try {
|
|
133
172
|
this.proc.stdin.write(line + '\n');
|
|
134
173
|
}
|
|
135
174
|
catch (e) {
|
|
136
175
|
await this.deadRestart();
|
|
176
|
+
rmSync(nativeSpoolDir, { recursive: true, force: true });
|
|
137
177
|
return {
|
|
138
178
|
stdout: Buffer.alloc(0),
|
|
139
179
|
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
|
|
@@ -152,14 +192,15 @@ export class PowerShellHost {
|
|
|
152
192
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
153
193
|
try {
|
|
154
194
|
if (this.protocol === 2) {
|
|
155
|
-
return await this.collectV2(id, timeoutMs);
|
|
195
|
+
return await this.collectV2(id, timeoutMs, resolvedLimits);
|
|
156
196
|
}
|
|
157
197
|
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
158
198
|
const msg = decodeHostResponse(raw);
|
|
159
199
|
const native = this.drainNativeStderr();
|
|
160
200
|
return {
|
|
161
201
|
stdout: msg.stdout,
|
|
162
|
-
stderr:
|
|
202
|
+
stderr: msg.stderr,
|
|
203
|
+
nativeStderr: native,
|
|
163
204
|
exitCode: msg.exitCode,
|
|
164
205
|
timedOut: false,
|
|
165
206
|
cancelled: false,
|
|
@@ -168,9 +209,12 @@ export class PowerShellHost {
|
|
|
168
209
|
}
|
|
169
210
|
catch (e) {
|
|
170
211
|
const timedOut = e.timedOut === true;
|
|
212
|
+
const wasCancelled = cancelled || signal?.aborted === true;
|
|
213
|
+
const nativeSpoolError = e instanceof NativeStderrSpoolError ? e : null;
|
|
171
214
|
await this.stop();
|
|
172
215
|
this.drainNativeStderr();
|
|
173
|
-
|
|
216
|
+
this.removeRequestSpools(resolvedLimits);
|
|
217
|
+
if (wasCancelled)
|
|
174
218
|
return this.cancelledResult();
|
|
175
219
|
if (timedOut) {
|
|
176
220
|
return {
|
|
@@ -182,10 +226,20 @@ export class PowerShellHost {
|
|
|
182
226
|
truncated: false,
|
|
183
227
|
};
|
|
184
228
|
}
|
|
229
|
+
if (nativeSpoolError) {
|
|
230
|
+
return {
|
|
231
|
+
stdout: Buffer.alloc(0),
|
|
232
|
+
stderr: Buffer.from(nativeSpoolError.message + '\n', 'utf8'),
|
|
233
|
+
exitCode: 1,
|
|
234
|
+
timedOut: false,
|
|
235
|
+
cancelled: false,
|
|
236
|
+
truncated: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
185
239
|
if (this.closeErr && this.closeErr.code === 'ENOENT') {
|
|
186
240
|
return {
|
|
187
241
|
stdout: Buffer.alloc(0),
|
|
188
|
-
stderr: Buffer.from(
|
|
242
|
+
stderr: Buffer.from(powerShellMissingMessage(this.powerShell), 'utf8'),
|
|
189
243
|
exitCode: 127,
|
|
190
244
|
timedOut: false,
|
|
191
245
|
cancelled: false,
|
|
@@ -207,9 +261,27 @@ export class PowerShellHost {
|
|
|
207
261
|
}
|
|
208
262
|
finally {
|
|
209
263
|
signal?.removeEventListener('abort', onAbort);
|
|
264
|
+
try {
|
|
265
|
+
rmSync(nativeSpoolDir, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* best effort; never mask the request result */
|
|
269
|
+
}
|
|
210
270
|
}
|
|
211
271
|
}
|
|
212
272
|
async ensureStarted() {
|
|
273
|
+
if (this.powerShell.error) {
|
|
274
|
+
return {
|
|
275
|
+
stdout: Buffer.alloc(0),
|
|
276
|
+
stderr: Buffer.from(this.powerShell.error + '\n', 'utf8'),
|
|
277
|
+
exitCode: 127,
|
|
278
|
+
timedOut: false,
|
|
279
|
+
cancelled: false,
|
|
280
|
+
truncated: false,
|
|
281
|
+
spawnError: 'START',
|
|
282
|
+
spawnMessage: this.powerShell.error,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
213
285
|
if (this.proc && !this.closed)
|
|
214
286
|
return null;
|
|
215
287
|
if (!this.startLock)
|
|
@@ -223,7 +295,7 @@ export class PowerShellHost {
|
|
|
223
295
|
if (err.code === 'ENOENT') {
|
|
224
296
|
return {
|
|
225
297
|
stdout: Buffer.alloc(0),
|
|
226
|
-
stderr: Buffer.from(
|
|
298
|
+
stderr: Buffer.from(powerShellMissingMessage(this.powerShell), 'utf8'),
|
|
227
299
|
exitCode: 127,
|
|
228
300
|
timedOut: false,
|
|
229
301
|
cancelled: false,
|
|
@@ -233,7 +305,7 @@ export class PowerShellHost {
|
|
|
233
305
|
}
|
|
234
306
|
return {
|
|
235
307
|
stdout: Buffer.alloc(0),
|
|
236
|
-
stderr: Buffer.from('fauxnix: failed to start
|
|
308
|
+
stderr: Buffer.from('fauxnix: failed to start ' + this.powerShell.executable + ': ' + err.message + '\n', 'utf8'),
|
|
237
309
|
exitCode: 127,
|
|
238
310
|
timedOut: false,
|
|
239
311
|
cancelled: false,
|
|
@@ -259,7 +331,7 @@ export class PowerShellHost {
|
|
|
259
331
|
await this.deadRestart();
|
|
260
332
|
this.closed = false;
|
|
261
333
|
writeFileSync(this.hostFile, '\ufeff' + hostBootstrapScript(), 'utf8');
|
|
262
|
-
const child = spawn(
|
|
334
|
+
const child = spawn(this.powerShell.executable, [...POWERSHELL_ARGS, '-File', this.hostFile], {
|
|
263
335
|
env: this.envFn(),
|
|
264
336
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
265
337
|
windowsHide: true,
|
|
@@ -273,7 +345,7 @@ export class PowerShellHost {
|
|
|
273
345
|
child.stderr.on('data', (d) => {
|
|
274
346
|
if (this.proc !== child)
|
|
275
347
|
return;
|
|
276
|
-
this.
|
|
348
|
+
this.onNativeStderr(d);
|
|
277
349
|
});
|
|
278
350
|
child.on('error', (e) => {
|
|
279
351
|
if (this.proc !== child)
|
|
@@ -393,13 +465,18 @@ export class PowerShellHost {
|
|
|
393
465
|
err.timedOut = true;
|
|
394
466
|
throw err;
|
|
395
467
|
}
|
|
396
|
-
async collectV2(id, timeoutMs) {
|
|
468
|
+
async collectV2(id, timeoutMs, limits) {
|
|
397
469
|
const deadline = Date.now() + timeoutMs;
|
|
398
470
|
const out = [];
|
|
399
471
|
const err = [];
|
|
400
472
|
let outSeq = 0;
|
|
401
473
|
let errSeq = 0;
|
|
402
474
|
let end = null;
|
|
475
|
+
const nativePromise = this.beginNativeCapture(id, limits?.stderrMode ?? 'capture', limits?.stderrLimit ?? DEFAULT_STDERR_LIMIT, limits?.nativeStderrSpoolPath, timeoutMs + 2000);
|
|
476
|
+
// collectV2 can leave through a frame timeout before it reaches the await
|
|
477
|
+
// below. Attach a handler immediately so teardown never reports a stray
|
|
478
|
+
// promise rejection; the awaited path still receives the same failure.
|
|
479
|
+
void nativePromise.catch(() => undefined);
|
|
403
480
|
while (!end) {
|
|
404
481
|
const line = await this.nextLine(Math.max(1, deadline - Date.now()));
|
|
405
482
|
if (!line.trim())
|
|
@@ -430,39 +507,224 @@ export class PowerShellHost {
|
|
|
430
507
|
end = f;
|
|
431
508
|
}
|
|
432
509
|
}
|
|
433
|
-
let
|
|
510
|
+
let nativeBytes = 0;
|
|
511
|
+
let markerTimer;
|
|
434
512
|
try {
|
|
435
|
-
|
|
513
|
+
const captured = await Promise.race([
|
|
514
|
+
nativePromise,
|
|
515
|
+
new Promise((_, reject) => {
|
|
516
|
+
markerTimer = setTimeout(() => reject(new Error('fauxnix: native stderr marker missing')), 2000);
|
|
517
|
+
}),
|
|
518
|
+
]);
|
|
519
|
+
nativeBytes = captured.bytesSeen;
|
|
436
520
|
}
|
|
437
|
-
catch {
|
|
438
|
-
|
|
521
|
+
catch (e) {
|
|
522
|
+
this.cancelNativeCapture();
|
|
523
|
+
if (e instanceof NativeStderrSpoolError)
|
|
524
|
+
throw e;
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
if (markerTimer)
|
|
528
|
+
clearTimeout(markerTimer);
|
|
439
529
|
}
|
|
440
530
|
const capturedErr = Buffer.from(Buffer.concat(err));
|
|
531
|
+
let native = Buffer.alloc(0);
|
|
532
|
+
let nativeTruncated = false;
|
|
533
|
+
const nativeSpool = limits?.nativeStderrSpoolPath;
|
|
534
|
+
if ((limits?.stderrMode ?? 'capture') === 'capture' && nativeSpool) {
|
|
535
|
+
const remaining = Math.max(0, (limits?.stderrLimit ?? DEFAULT_STDERR_LIMIT) - capturedErr.length);
|
|
536
|
+
const clipped = this.readUtf8Prefix(nativeSpool, remaining, nativeBytes);
|
|
537
|
+
native = Buffer.from(clipped.data);
|
|
538
|
+
nativeTruncated = clipped.truncated;
|
|
539
|
+
rmSync(nativeSpool, { force: true });
|
|
540
|
+
}
|
|
541
|
+
if (limits?.stderrMode === 'spool' && nativeSpool && nativeBytes === 0) {
|
|
542
|
+
rmSync(nativeSpool, { force: true });
|
|
543
|
+
}
|
|
441
544
|
const n = Number(end.exitCode);
|
|
442
545
|
return {
|
|
443
546
|
stdout: Buffer.from(Buffer.concat(out)),
|
|
444
|
-
stderr:
|
|
547
|
+
stderr: capturedErr,
|
|
548
|
+
nativeStderr: native,
|
|
445
549
|
exitCode: Number.isFinite(n) ? n : 0,
|
|
446
550
|
timedOut: end.timedOut === true,
|
|
447
551
|
cancelled: end.cancelled === true,
|
|
448
|
-
truncated: end.truncated === true,
|
|
552
|
+
truncated: end.truncated === true || nativeTruncated,
|
|
553
|
+
stdoutTruncated: end.stdoutTruncated === true,
|
|
554
|
+
stderrTruncated: end.stderrTruncated === true || nativeTruncated,
|
|
555
|
+
stdoutSpool: limits?.stdoutSpoolPath,
|
|
556
|
+
stderrSpool: limits?.stderrSpoolPath,
|
|
557
|
+
nativeStderrSpool: limits?.stderrMode === 'spool' && nativeBytes > 0 ? nativeSpool : undefined,
|
|
449
558
|
};
|
|
450
559
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
this.
|
|
461
|
-
|
|
560
|
+
beginNativeCapture(id, mode, limit, spoolPath, timeoutMs) {
|
|
561
|
+
this.cancelNativeCapture();
|
|
562
|
+
return new Promise((resolve, reject) => {
|
|
563
|
+
const timer = setTimeout(() => {
|
|
564
|
+
if (this.nativeCapture?.id !== id)
|
|
565
|
+
return;
|
|
566
|
+
const state = this.nativeCapture;
|
|
567
|
+
const failure = state.ioError ?? new Error('fauxnix: native stderr marker missing');
|
|
568
|
+
this.closeNativeSpool(state);
|
|
569
|
+
this.nativeCapture = null;
|
|
570
|
+
reject(failure);
|
|
571
|
+
}, timeoutMs);
|
|
572
|
+
let spoolFd;
|
|
573
|
+
let ioError;
|
|
574
|
+
if (mode !== 'discard' && spoolPath) {
|
|
575
|
+
try {
|
|
576
|
+
spoolFd = openSync(spoolPath, 'w');
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
ioError = new NativeStderrSpoolError('open', spoolPath, e);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
this.nativeCapture = {
|
|
583
|
+
id,
|
|
584
|
+
needle: Buffer.from('FAUXNIX_ERR_END:' + id + '\n', 'utf8'),
|
|
585
|
+
mode,
|
|
586
|
+
limit: Math.max(0, limit),
|
|
587
|
+
tail: Buffer.alloc(0),
|
|
588
|
+
spoolPath,
|
|
589
|
+
spoolFd,
|
|
590
|
+
bytesWritten: 0,
|
|
591
|
+
bytesSeen: 0,
|
|
592
|
+
ioError,
|
|
593
|
+
resolve,
|
|
594
|
+
reject,
|
|
595
|
+
timer,
|
|
596
|
+
};
|
|
597
|
+
if (this.stderrChunks.length) {
|
|
598
|
+
const pending = Buffer.concat(this.stderrChunks);
|
|
599
|
+
this.stderrChunks = [];
|
|
600
|
+
this.onNativeStderr(pending);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
appendNativePayload(state, data) {
|
|
605
|
+
if (!data.length)
|
|
606
|
+
return;
|
|
607
|
+
state.bytesSeen += data.length;
|
|
608
|
+
if (state.ioError || state.mode === 'discard' || state.spoolFd === undefined)
|
|
609
|
+
return;
|
|
610
|
+
let retained = data;
|
|
611
|
+
if (state.mode === 'capture') {
|
|
612
|
+
const remaining = Math.max(0, state.limit + 3 - state.bytesWritten);
|
|
613
|
+
retained = data.subarray(0, remaining);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
let offset = 0;
|
|
617
|
+
while (offset < retained.length) {
|
|
618
|
+
const remaining = retained.length - offset;
|
|
619
|
+
const written = this.nativeSpoolWrite(state.spoolFd, retained, offset, remaining);
|
|
620
|
+
if (!Number.isInteger(written) || written <= 0 || written > remaining) {
|
|
621
|
+
throw new Error('short write to native stderr spool');
|
|
622
|
+
}
|
|
623
|
+
offset += written;
|
|
624
|
+
state.bytesWritten += written;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
state.ioError = new NativeStderrSpoolError('write', state.spoolPath, e);
|
|
629
|
+
this.closeNativeSpool(state);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
onNativeStderr(chunk) {
|
|
633
|
+
const state = this.nativeCapture;
|
|
634
|
+
if (!state) {
|
|
635
|
+
// Startup/teardown noise is diagnostic only. Keep a bounded tail so a
|
|
636
|
+
// noisy host cannot grow the resident Node process indefinitely.
|
|
637
|
+
const max = DEFAULT_STDERR_LIMIT;
|
|
638
|
+
const combined = Buffer.concat([...this.stderrChunks, chunk]);
|
|
639
|
+
this.stderrChunks = [Buffer.from(combined.subarray(Math.max(0, combined.length - max)))];
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const combined = state.tail.length ? Buffer.concat([state.tail, chunk]) : chunk;
|
|
643
|
+
const idx = combined.indexOf(state.needle);
|
|
644
|
+
if (idx >= 0) {
|
|
645
|
+
this.appendNativePayload(state, combined.subarray(0, idx));
|
|
646
|
+
const after = combined.subarray(idx + state.needle.length);
|
|
647
|
+
const ioError = state.ioError;
|
|
648
|
+
clearTimeout(state.timer);
|
|
649
|
+
this.closeNativeSpool(state);
|
|
650
|
+
this.nativeCapture = null;
|
|
651
|
+
if (after.length)
|
|
652
|
+
this.onNativeStderr(after);
|
|
653
|
+
if (ioError)
|
|
654
|
+
state.reject(ioError);
|
|
655
|
+
else
|
|
656
|
+
state.resolve({ bytesSeen: state.bytesSeen });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const keep = Math.min(state.needle.length - 1, combined.length);
|
|
660
|
+
const emit = combined.length - keep;
|
|
661
|
+
if (emit > 0)
|
|
662
|
+
this.appendNativePayload(state, combined.subarray(0, emit));
|
|
663
|
+
state.tail = Buffer.from(combined.subarray(emit));
|
|
664
|
+
}
|
|
665
|
+
cancelNativeCapture() {
|
|
666
|
+
const state = this.nativeCapture;
|
|
667
|
+
if (!state)
|
|
668
|
+
return;
|
|
669
|
+
clearTimeout(state.timer);
|
|
670
|
+
this.appendNativePayload(state, state.tail);
|
|
671
|
+
const ioError = state.ioError;
|
|
672
|
+
this.closeNativeSpool(state);
|
|
673
|
+
this.nativeCapture = null;
|
|
674
|
+
state.reject(ioError ?? new Error('fauxnix: native stderr capture cancelled'));
|
|
675
|
+
}
|
|
676
|
+
closeNativeSpool(state) {
|
|
677
|
+
if (state.spoolFd === undefined)
|
|
678
|
+
return;
|
|
679
|
+
try {
|
|
680
|
+
closeSync(state.spoolFd);
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
/* already closed */
|
|
684
|
+
}
|
|
685
|
+
state.spoolFd = undefined;
|
|
686
|
+
}
|
|
687
|
+
readUtf8Prefix(file, limit, totalBytes = statSync(file).size) {
|
|
688
|
+
const size = statSync(file).size;
|
|
689
|
+
const wanted = Math.min(size, Math.max(0, limit) + 3);
|
|
690
|
+
const buf = Buffer.alloc(wanted);
|
|
691
|
+
if (wanted > 0) {
|
|
692
|
+
const fd = openSync(file, 'r');
|
|
693
|
+
try {
|
|
694
|
+
let offset = 0;
|
|
695
|
+
while (offset < wanted) {
|
|
696
|
+
const n = readSync(fd, buf, offset, wanted - offset, offset);
|
|
697
|
+
if (n <= 0)
|
|
698
|
+
break;
|
|
699
|
+
offset += n;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
closeSync(fd);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
let use = Math.min(limit, buf.length);
|
|
707
|
+
if (use < buf.length && use > 0 && (buf[use] & 0xc0) === 0x80) {
|
|
708
|
+
while (use > 0 && (buf[use] & 0xc0) === 0x80)
|
|
709
|
+
use--;
|
|
710
|
+
}
|
|
711
|
+
return { data: Buffer.from(buf.subarray(0, use)), truncated: totalBytes > use };
|
|
712
|
+
}
|
|
713
|
+
removeRequestSpools(limits) {
|
|
714
|
+
for (const file of [
|
|
715
|
+
limits.stdoutSpoolPath,
|
|
716
|
+
limits.stderrSpoolPath,
|
|
717
|
+
limits.nativeStderrSpoolPath,
|
|
718
|
+
]) {
|
|
719
|
+
if (!file)
|
|
720
|
+
continue;
|
|
721
|
+
try {
|
|
722
|
+
rmSync(file, { force: true });
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
/* best effort; preserve the original transport error */
|
|
462
726
|
}
|
|
463
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
464
727
|
}
|
|
465
|
-
throw new Error('fauxnix: native stderr marker missing');
|
|
466
728
|
}
|
|
467
729
|
failWaiters(err) {
|
|
468
730
|
const ws = this.waiters.splice(0);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type QwenLaunchTuple = {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function resolveQwenLaunchTuple(): {
|
|
6
|
+
ok: true;
|
|
7
|
+
value: QwenLaunchTuple;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function sameQwenLaunchTuple(value: unknown, expected: QwenLaunchTuple): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function resolveQwenLaunchTuple() {
|
|
5
|
+
const command = process.execPath;
|
|
6
|
+
const entry = fileURLToPath(new URL('../dist/index.js', import.meta.url));
|
|
7
|
+
if (!isAbsolute(command) || !existsSync(command)) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
reason: 'cannot locate the Node.js executable; reinstall Node.js, then retry `fauxnix install --qwen`',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (!isAbsolute(entry) || !existsSync(entry)) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
reason: `cannot locate the built fauxnix entry at ${entry}; run \`npm run build\` or reinstall fauxnix-cli, then retry \`fauxnix install --qwen\``,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, value: { command, args: [entry, 'mcp'] } };
|
|
20
|
+
}
|
|
21
|
+
export function sameQwenLaunchTuple(value, expected) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
return false;
|
|
24
|
+
const rec = value;
|
|
25
|
+
return (rec.command === expected.command &&
|
|
26
|
+
Array.isArray(rec.args) &&
|
|
27
|
+
rec.args.length === expected.args.length &&
|
|
28
|
+
rec.args.every((part, index) => part === expected.args[index]));
|
|
29
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface PipelineCtx {
|
|
|
32
32
|
position: 'first' | 'middle' | 'last';
|
|
33
33
|
/** True when stdin is available (piped input or `< file` redirect). */
|
|
34
34
|
hasStdin: boolean;
|
|
35
|
+
/** Whether translation is preparing an executable plan or only rendering it. */
|
|
36
|
+
translationMode?: 'execute' | 'pure';
|
|
35
37
|
}
|
|
36
38
|
export type Handler = (args: Word[], ctx: PipelineCtx) => string;
|
|
37
39
|
export declare function register(name: string, handler: Handler): void;
|
|
@@ -103,6 +105,26 @@ export interface CommandSpec {
|
|
|
103
105
|
leadingOptions?: boolean;
|
|
104
106
|
handler: Handler;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* C-5's curated agent-daily command set. Keep this explicit: a raw count of
|
|
110
|
+
* CommandSpecs is not enough to prove that the commands agents use every day
|
|
111
|
+
* are the ones protected by fail-loud option validation.
|
|
112
|
+
*/
|
|
113
|
+
export declare const AGENT_DAILY_60: readonly ["basename", "cat", "cd", "chmod", "chown", "clear", "command", "cp", "cut", "date", "df", "diff", "dirname", "du", "echo", "env", "export", "file", "free", "grep", "groups", "gunzip", "gzip", "head", "hostname", "id", "ll", "ln", "ls", "mkdir", "mktemp", "mv", "nproc", "printenv", "printf", "ps", "pwd", "readlink", "realpath", "rm", "rmdir", "sleep", "sort", "stat", "tail", "tee", "timeout", "touch", "tr", "type", "uname", "uniq", "unset", "unzip", "uptime", "wc", "which", "whoami", "zcat", "zip"];
|
|
114
|
+
/** Commands deliberately kept outside generic CommandSpec option walking. */
|
|
115
|
+
export declare const COMMAND_SPEC_EXCLUSIONS: readonly [{
|
|
116
|
+
readonly names: readonly ["find"];
|
|
117
|
+
readonly reason: "option-looking predicates are parsed by the find expression compiler; generic short-option bundling would misread -name";
|
|
118
|
+
}, {
|
|
119
|
+
readonly names: readonly ["sed", "awk"];
|
|
120
|
+
readonly reason: "program text and option grammar require command-specific parsing; any remaining unchecked options must be fixed there rather than treated as generic flags";
|
|
121
|
+
}, {
|
|
122
|
+
readonly names: readonly ["egrep"];
|
|
123
|
+
readonly reason: "semantic alias injects grep -E through its own handler; it must not be wrapped as an independent generic option parser";
|
|
124
|
+
}, {
|
|
125
|
+
readonly names: readonly ["tar"];
|
|
126
|
+
readonly reason: "argv is passed to Windows bsdtar; rejecting unlisted GNU/bsdtar options before native dispatch would reduce compatibility";
|
|
127
|
+
}];
|
|
106
128
|
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
|
107
129
|
export declare function registerSpec(spec: CommandSpec): void;
|
|
108
130
|
export declare function registerSpecs(list: CommandSpec[]): void;
|