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/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
- let session = new FauxnixSession();
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, { timeoutMs: timeout_ms });
59
- const parts = [];
60
- if (result.stdout.trim())
61
- parts.push(result.stdout.replace(/\n$/, ''));
62
- if (result.stderr.trim())
63
- parts.push(result.stderr.replace(/\n$/, ''));
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 { content: [{ type: 'text', text: msg }], isError: true };
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.dispose();
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
- const isListSep = (o) => o === ';' || o === '\n';
593
- while (peek().type === 'OP' && isListSep(peek().op))
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 t = peek();
599
- if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
600
- op = t.op === '\n' ? ';' : t.op;
601
- next();
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
- const isListSep = (o) => o === ';' || o === '\n';
676
- while (peek().type === 'OP' && isListSep(peek().op))
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 t = peek();
685
- if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
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,16 +1,52 @@
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
  }
10
14
  export interface HostRequestEnv {
11
15
  [key: string]: string;
12
16
  }
13
- export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv): string;
17
+ export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv, opts?: {
18
+ v?: number;
19
+ stdoutLimit?: number;
20
+ stderrLimit?: number;
21
+ }): string;
22
+ export type HostV2Frame = {
23
+ v: 2;
24
+ type: 'ready';
25
+ capabilities?: {
26
+ cancel?: boolean;
27
+ maxChunkBytes?: number;
28
+ stderrMarker?: boolean;
29
+ };
30
+ } | {
31
+ v: 2;
32
+ type: 'stdout' | 'stderr';
33
+ id: string;
34
+ seq: number;
35
+ dataB64: string;
36
+ } | {
37
+ v: 2;
38
+ type: 'end';
39
+ id: string;
40
+ exitCode: number;
41
+ timedOut?: boolean;
42
+ cancelled?: boolean;
43
+ truncated?: boolean;
44
+ };
45
+ export declare function parseHostLine(line: string): {
46
+ v1Ready?: boolean;
47
+ v2?: HostV2Frame;
48
+ v1?: ReturnType<typeof decodeHostResponse>;
49
+ };
14
50
  export declare function decodeHostResponse(line: string): {
15
51
  id: string;
16
52
  stdout: Buffer;
@@ -36,11 +72,17 @@ export declare class PowerShellHost {
36
72
  private closed;
37
73
  private startLock;
38
74
  private invokeLock;
75
+ protocol: 1 | 2;
39
76
  constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
40
77
  /** Start the resident process and wait for the ready handshake (B1 prewarm). */
41
78
  ready(): Promise<HostInvokeResult | null>;
42
- invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
79
+ invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: {
80
+ stdoutLimit?: number;
81
+ stderrLimit?: number;
82
+ }): Promise<HostInvokeResult>;
83
+ drainNativeStderr(): Buffer;
43
84
  stop(): Promise<void>;
85
+ private cancelledResult;
44
86
  private invokeSerial;
45
87
  private ensureStarted;
46
88
  private deadRestart;
@@ -49,5 +91,7 @@ export declare class PowerShellHost {
49
91
  private nextLine;
50
92
  private nextReadyLine;
51
93
  private nextJsonLine;
94
+ private collectV2;
95
+ private waitNativeMarker;
52
96
  private failWaiters;
53
97
  }
package/dist/ps-host.js CHANGED
@@ -6,12 +6,29 @@ 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 function encodeHostRequest(id, script, env) {
10
- return JSON.stringify({
9
+ export const DEFAULT_STDOUT_LIMIT = 8_388_608;
10
+ export const DEFAULT_STDERR_LIMIT = 1_048_576;
11
+ export function encodeHostRequest(id, script, env, opts) {
12
+ const body = {
11
13
  id,
12
14
  scriptB64: Buffer.from(script, 'utf8').toString('base64'),
13
15
  env,
14
- });
16
+ };
17
+ if (opts?.v === 2) {
18
+ body.v = 2;
19
+ body.type = 'run';
20
+ body.stdoutLimit = opts.stdoutLimit ?? DEFAULT_STDOUT_LIMIT;
21
+ body.stderrLimit = opts.stderrLimit ?? DEFAULT_STDERR_LIMIT;
22
+ }
23
+ return JSON.stringify(body);
24
+ }
25
+ export function parseHostLine(line) {
26
+ const j = JSON.parse(line);
27
+ if (j && j.v === 2 && typeof j.type === 'string')
28
+ return { v2: j };
29
+ if (j && j.ready === true)
30
+ return { v1Ready: true };
31
+ return { v1: decodeHostResponse(line) };
15
32
  }
16
33
  export function decodeHostResponse(line) {
17
34
  const j = JSON.parse(line);
@@ -42,6 +59,7 @@ export class PowerShellHost {
42
59
  closed = false;
43
60
  startLock = null;
44
61
  invokeLock = Promise.resolve();
62
+ protocol = 1;
45
63
  constructor(hostFile, envFn) {
46
64
  this.hostFile = hostFile;
47
65
  this.envFn = envFn;
@@ -50,11 +68,18 @@ export class PowerShellHost {
50
68
  async ready() {
51
69
  return this.ensureStarted();
52
70
  }
53
- async invoke(script, env, timeoutMs) {
54
- const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
71
+ async invoke(script, env, timeoutMs, signal, limits) {
72
+ const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal, limits));
55
73
  this.invokeLock = run.then(() => undefined, () => undefined);
56
74
  return run;
57
75
  }
76
+ drainNativeStderr() {
77
+ if (!this.stderrChunks.length)
78
+ return Buffer.alloc(0);
79
+ const b = Buffer.concat(this.stderrChunks);
80
+ this.stderrChunks = [];
81
+ return b;
82
+ }
58
83
  async stop() {
59
84
  const proc = this.proc;
60
85
  this.proc = null;
@@ -82,12 +107,28 @@ export class PowerShellHost {
82
107
  });
83
108
  });
84
109
  }
85
- async invokeSerial(script, env, timeoutMs) {
110
+ cancelledResult() {
111
+ return {
112
+ stdout: Buffer.alloc(0),
113
+ stderr: Buffer.alloc(0),
114
+ exitCode: 130,
115
+ timedOut: false,
116
+ cancelled: true,
117
+ truncated: false,
118
+ };
119
+ }
120
+ async invokeSerial(script, env, timeoutMs, signal, limits) {
121
+ if (signal?.aborted) {
122
+ await this.stop();
123
+ return this.cancelledResult();
124
+ }
86
125
  const started = await this.ensureStarted();
87
126
  if (started)
88
- return started;
127
+ return { ...started, cancelled: false, truncated: false };
89
128
  const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
90
- const line = encodeHostRequest(id, script, env);
129
+ const line = encodeHostRequest(id, script, env, this.protocol === 2
130
+ ? { v: 2, stdoutLimit: limits?.stdoutLimit, stderrLimit: limits?.stderrLimit }
131
+ : undefined);
91
132
  try {
92
133
  this.proc.stdin.write(line + '\n');
93
134
  }
@@ -98,24 +139,48 @@ export class PowerShellHost {
98
139
  stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
99
140
  exitCode: 1,
100
141
  timedOut: false,
142
+ cancelled: false,
143
+ truncated: false,
101
144
  spawnMessage: e.message,
102
145
  };
103
146
  }
147
+ let cancelled = false;
148
+ const onAbort = () => {
149
+ cancelled = true;
150
+ void this.stop();
151
+ };
152
+ signal?.addEventListener('abort', onAbort, { once: true });
104
153
  try {
154
+ if (this.protocol === 2) {
155
+ return await this.collectV2(id, timeoutMs);
156
+ }
105
157
  const raw = await this.nextJsonLine(timeoutMs, id);
106
158
  const msg = decodeHostResponse(raw);
159
+ const native = this.drainNativeStderr();
107
160
  return {
108
161
  stdout: msg.stdout,
109
- stderr: msg.stderr,
162
+ stderr: native.length ? Buffer.concat([msg.stderr, native]) : msg.stderr,
110
163
  exitCode: msg.exitCode,
111
164
  timedOut: false,
165
+ cancelled: false,
166
+ truncated: false,
112
167
  };
113
168
  }
114
169
  catch (e) {
115
170
  const timedOut = e.timedOut === true;
116
171
  await this.stop();
172
+ this.drainNativeStderr();
173
+ if (cancelled || signal?.aborted)
174
+ return this.cancelledResult();
117
175
  if (timedOut) {
118
- return { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 124, timedOut: true };
176
+ return {
177
+ stdout: Buffer.alloc(0),
178
+ stderr: Buffer.alloc(0),
179
+ exitCode: 124,
180
+ timedOut: true,
181
+ cancelled: false,
182
+ truncated: false,
183
+ };
119
184
  }
120
185
  if (this.closeErr && this.closeErr.code === 'ENOENT') {
121
186
  return {
@@ -123,6 +188,8 @@ export class PowerShellHost {
123
188
  stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
124
189
  exitCode: 127,
125
190
  timedOut: false,
191
+ cancelled: false,
192
+ truncated: false,
126
193
  spawnError: 'ENOENT',
127
194
  };
128
195
  }
@@ -134,8 +201,13 @@ export class PowerShellHost {
134
201
  '\n', 'utf8'),
135
202
  exitCode: code === 0 ? 1 : code,
136
203
  timedOut: false,
204
+ cancelled: false,
205
+ truncated: false,
137
206
  };
138
207
  }
208
+ finally {
209
+ signal?.removeEventListener('abort', onAbort);
210
+ }
139
211
  }
140
212
  async ensureStarted() {
141
213
  if (this.proc && !this.closed)
@@ -154,6 +226,8 @@ export class PowerShellHost {
154
226
  stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
155
227
  exitCode: 127,
156
228
  timedOut: false,
229
+ cancelled: false,
230
+ truncated: false,
157
231
  spawnError: 'ENOENT',
158
232
  };
159
233
  }
@@ -162,6 +236,8 @@ export class PowerShellHost {
162
236
  stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
163
237
  exitCode: 127,
164
238
  timedOut: false,
239
+ cancelled: false,
240
+ truncated: false,
165
241
  spawnError: 'START',
166
242
  spawnMessage: err.message,
167
243
  };
@@ -189,14 +265,26 @@ export class PowerShellHost {
189
265
  windowsHide: true,
190
266
  });
191
267
  this.proc = child;
192
- child.stdout.on('data', (d) => this.onStdout(d));
193
- child.stderr.on('data', (d) => this.stderrChunks.push(d));
268
+ child.stdout.on('data', (d) => {
269
+ if (this.proc !== child)
270
+ return;
271
+ this.onStdout(d);
272
+ });
273
+ child.stderr.on('data', (d) => {
274
+ if (this.proc !== child)
275
+ return;
276
+ this.stderrChunks.push(d);
277
+ });
194
278
  child.on('error', (e) => {
279
+ if (this.proc !== child)
280
+ return;
195
281
  this.closeErr = e;
196
282
  this.closed = true;
197
283
  this.failWaiters(e);
198
284
  });
199
285
  child.on('close', (c) => {
286
+ if (this.proc !== child)
287
+ return;
200
288
  this.closeCode = c;
201
289
  this.closed = true;
202
290
  this.proc = null;
@@ -204,10 +292,13 @@ export class PowerShellHost {
204
292
  });
205
293
  try {
206
294
  const readyLine = await this.nextReadyLine(READY_TIMEOUT_MS);
207
- const msg = decodeHostResponse(readyLine);
208
- if (!msg.ready) {
295
+ const parsed = parseHostLine(readyLine);
296
+ if (parsed.v2?.type === 'ready')
297
+ this.protocol = 2;
298
+ else if (parsed.v1Ready || decodeHostResponse(readyLine).ready)
299
+ this.protocol = 1;
300
+ else
209
301
  throw new Error('fauxnix: powershell host handshake failed');
210
- }
211
302
  }
212
303
  catch (e) {
213
304
  await this.stop();
@@ -267,8 +358,10 @@ export class PowerShellHost {
267
358
  if (!line.trim())
268
359
  continue;
269
360
  try {
270
- const msg = decodeHostResponse(line);
271
- if (msg.ready)
361
+ const parsed = parseHostLine(line);
362
+ if (parsed.v2?.type === 'ready' || parsed.v1Ready)
363
+ return line;
364
+ if (parsed.v1?.ready)
272
365
  return line;
273
366
  }
274
367
  catch {
@@ -300,6 +393,77 @@ export class PowerShellHost {
300
393
  err.timedOut = true;
301
394
  throw err;
302
395
  }
396
+ async collectV2(id, timeoutMs) {
397
+ const deadline = Date.now() + timeoutMs;
398
+ const out = [];
399
+ const err = [];
400
+ let outSeq = 0;
401
+ let errSeq = 0;
402
+ let end = null;
403
+ while (!end) {
404
+ const line = await this.nextLine(Math.max(1, deadline - Date.now()));
405
+ if (!line.trim())
406
+ continue;
407
+ let parsed;
408
+ try {
409
+ parsed = parseHostLine(line);
410
+ }
411
+ catch {
412
+ continue;
413
+ }
414
+ const f = parsed.v2;
415
+ if (!f)
416
+ continue;
417
+ if (f.type === 'stdout' && f.id === id) {
418
+ if (f.seq !== outSeq)
419
+ throw new Error('fauxnix: host stdout seq gap');
420
+ out.push(Buffer.from(f.dataB64 ?? '', 'base64'));
421
+ outSeq++;
422
+ }
423
+ else if (f.type === 'stderr' && f.id === id) {
424
+ if (f.seq !== errSeq)
425
+ throw new Error('fauxnix: host stderr seq gap');
426
+ err.push(Buffer.from(f.dataB64 ?? '', 'base64'));
427
+ errSeq++;
428
+ }
429
+ else if (f.type === 'end' && f.id === id) {
430
+ end = f;
431
+ }
432
+ }
433
+ let native = Buffer.alloc(0);
434
+ try {
435
+ native = Buffer.from(await this.waitNativeMarker(id, 2000));
436
+ }
437
+ catch {
438
+ native = Buffer.from(this.drainNativeStderr());
439
+ }
440
+ const capturedErr = Buffer.from(Buffer.concat(err));
441
+ const n = Number(end.exitCode);
442
+ return {
443
+ stdout: Buffer.from(Buffer.concat(out)),
444
+ stderr: native.length ? Buffer.from(Buffer.concat([capturedErr, native])) : capturedErr,
445
+ exitCode: Number.isFinite(n) ? n : 0,
446
+ timedOut: end.timedOut === true,
447
+ cancelled: end.cancelled === true,
448
+ truncated: end.truncated === true,
449
+ };
450
+ }
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);
462
+ }
463
+ await new Promise((r) => setTimeout(r, 15));
464
+ }
465
+ throw new Error('fauxnix: native stderr marker missing');
466
+ }
303
467
  failWaiters(err) {
304
468
  const ws = this.waiters.splice(0);
305
469
  for (const w of ws) {