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/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
- const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
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 powershell.exe 5.1 process. Frames are UTF-8 JSON lines;
46
- * command stdout/stderr come back as base64 so PS 5.1's UTF-16LE pipe
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: powershell host stopped'));
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 line = encodeHostRequest(id, script, env, this.protocol === 2
130
- ? { v: 2, stdoutLimit: limits?.stdoutLimit, stderrLimit: limits?.stderrLimit }
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: native.length ? Buffer.concat([msg.stderr, native]) : msg.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
- if (cancelled || signal?.aborted)
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(PS_MISSING_MESSAGE, 'utf8'),
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(PS_MISSING_MESSAGE, 'utf8'),
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 powershell.exe: ' + err.message + '\n', 'utf8'),
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('powershell.exe', [...PS_ARGS, '-File', this.hostFile], {
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.stderrChunks.push(d);
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 native = Buffer.alloc(0);
510
+ let nativeBytes = 0;
511
+ let markerTimer;
434
512
  try {
435
- native = Buffer.from(await this.waitNativeMarker(id, 2000));
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
- native = Buffer.from(this.drainNativeStderr());
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: native.length ? Buffer.from(Buffer.concat([capturedErr, native])) : capturedErr,
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
- async waitNativeMarker(id, timeoutMs) {
452
- const needle = Buffer.from('FAUXNIX_ERR_END:' + id + '\n', 'utf8');
453
- const deadline = Date.now() + timeoutMs;
454
- while (Date.now() < deadline) {
455
- const buf = Buffer.concat(this.stderrChunks);
456
- const idx = buf.indexOf(needle);
457
- if (idx >= 0) {
458
- const before = buf.subarray(0, idx);
459
- const after = buf.subarray(idx + needle.length);
460
- this.stderrChunks = after.length ? [Buffer.from(after)] : [];
461
- return Buffer.from(before);
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
+ }
@@ -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;