fauxnix-cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,9 +1,13 @@
1
1
  export declare const PS_MISSING_MESSAGE: string;
2
+ export declare const DEFAULT_STDOUT_LIMIT = 8388608;
3
+ export declare const DEFAULT_STDERR_LIMIT = 1048576;
2
4
  export interface HostInvokeResult {
3
5
  stdout: Buffer;
4
6
  stderr: Buffer;
5
7
  exitCode: number;
6
8
  timedOut: boolean;
9
+ cancelled: boolean;
10
+ truncated: boolean;
7
11
  spawnError?: 'ENOENT' | 'START';
8
12
  spawnMessage?: string;
9
13
  }
@@ -39,8 +43,10 @@ export declare class PowerShellHost {
39
43
  constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
40
44
  /** Start the resident process and wait for the ready handshake (B1 prewarm). */
41
45
  ready(): Promise<HostInvokeResult | null>;
42
- invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
46
+ invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal): Promise<HostInvokeResult>;
47
+ drainNativeStderr(): Buffer;
43
48
  stop(): Promise<void>;
49
+ private cancelledResult;
44
50
  private invokeSerial;
45
51
  private ensureStarted;
46
52
  private deadRestart;
package/dist/ps-host.js CHANGED
@@ -6,6 +6,8 @@ const READY_TIMEOUT_MS = 30_000;
6
6
  export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
7
7
  'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
8
8
  'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
9
+ export const DEFAULT_STDOUT_LIMIT = 8_388_608;
10
+ export const DEFAULT_STDERR_LIMIT = 1_048_576;
9
11
  export function encodeHostRequest(id, script, env) {
10
12
  return JSON.stringify({
11
13
  id,
@@ -50,11 +52,18 @@ export class PowerShellHost {
50
52
  async ready() {
51
53
  return this.ensureStarted();
52
54
  }
53
- async invoke(script, env, timeoutMs) {
54
- const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
55
+ async invoke(script, env, timeoutMs, signal) {
56
+ const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal));
55
57
  this.invokeLock = run.then(() => undefined, () => undefined);
56
58
  return run;
57
59
  }
60
+ drainNativeStderr() {
61
+ if (!this.stderrChunks.length)
62
+ return Buffer.alloc(0);
63
+ const b = Buffer.concat(this.stderrChunks);
64
+ this.stderrChunks = [];
65
+ return b;
66
+ }
58
67
  async stop() {
59
68
  const proc = this.proc;
60
69
  this.proc = null;
@@ -82,10 +91,24 @@ export class PowerShellHost {
82
91
  });
83
92
  });
84
93
  }
85
- async invokeSerial(script, env, timeoutMs) {
94
+ cancelledResult() {
95
+ return {
96
+ stdout: Buffer.alloc(0),
97
+ stderr: Buffer.alloc(0),
98
+ exitCode: 130,
99
+ timedOut: false,
100
+ cancelled: true,
101
+ truncated: false,
102
+ };
103
+ }
104
+ async invokeSerial(script, env, timeoutMs, signal) {
105
+ if (signal?.aborted) {
106
+ await this.stop();
107
+ return this.cancelledResult();
108
+ }
86
109
  const started = await this.ensureStarted();
87
110
  if (started)
88
- return started;
111
+ return { ...started, cancelled: false, truncated: false };
89
112
  const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
90
113
  const line = encodeHostRequest(id, script, env);
91
114
  try {
@@ -98,24 +121,45 @@ export class PowerShellHost {
98
121
  stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
99
122
  exitCode: 1,
100
123
  timedOut: false,
124
+ cancelled: false,
125
+ truncated: false,
101
126
  spawnMessage: e.message,
102
127
  };
103
128
  }
129
+ let cancelled = false;
130
+ const onAbort = () => {
131
+ cancelled = true;
132
+ void this.stop();
133
+ };
134
+ signal?.addEventListener('abort', onAbort, { once: true });
104
135
  try {
105
136
  const raw = await this.nextJsonLine(timeoutMs, id);
106
137
  const msg = decodeHostResponse(raw);
138
+ const native = this.drainNativeStderr();
107
139
  return {
108
140
  stdout: msg.stdout,
109
- stderr: msg.stderr,
141
+ stderr: native.length ? Buffer.concat([msg.stderr, native]) : msg.stderr,
110
142
  exitCode: msg.exitCode,
111
143
  timedOut: false,
144
+ cancelled: false,
145
+ truncated: false,
112
146
  };
113
147
  }
114
148
  catch (e) {
115
149
  const timedOut = e.timedOut === true;
116
150
  await this.stop();
151
+ this.drainNativeStderr();
152
+ if (cancelled || signal?.aborted)
153
+ return this.cancelledResult();
117
154
  if (timedOut) {
118
- return { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 124, timedOut: true };
155
+ return {
156
+ stdout: Buffer.alloc(0),
157
+ stderr: Buffer.alloc(0),
158
+ exitCode: 124,
159
+ timedOut: true,
160
+ cancelled: false,
161
+ truncated: false,
162
+ };
119
163
  }
120
164
  if (this.closeErr && this.closeErr.code === 'ENOENT') {
121
165
  return {
@@ -123,6 +167,8 @@ export class PowerShellHost {
123
167
  stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
124
168
  exitCode: 127,
125
169
  timedOut: false,
170
+ cancelled: false,
171
+ truncated: false,
126
172
  spawnError: 'ENOENT',
127
173
  };
128
174
  }
@@ -134,8 +180,13 @@ export class PowerShellHost {
134
180
  '\n', 'utf8'),
135
181
  exitCode: code === 0 ? 1 : code,
136
182
  timedOut: false,
183
+ cancelled: false,
184
+ truncated: false,
137
185
  };
138
186
  }
187
+ finally {
188
+ signal?.removeEventListener('abort', onAbort);
189
+ }
139
190
  }
140
191
  async ensureStarted() {
141
192
  if (this.proc && !this.closed)
@@ -154,6 +205,8 @@ export class PowerShellHost {
154
205
  stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
155
206
  exitCode: 127,
156
207
  timedOut: false,
208
+ cancelled: false,
209
+ truncated: false,
157
210
  spawnError: 'ENOENT',
158
211
  };
159
212
  }
@@ -162,6 +215,8 @@ export class PowerShellHost {
162
215
  stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
163
216
  exitCode: 127,
164
217
  timedOut: false,
218
+ cancelled: false,
219
+ truncated: false,
165
220
  spawnError: 'START',
166
221
  spawnMessage: err.message,
167
222
  };
@@ -189,14 +244,26 @@ export class PowerShellHost {
189
244
  windowsHide: true,
190
245
  });
191
246
  this.proc = child;
192
- child.stdout.on('data', (d) => this.onStdout(d));
193
- child.stderr.on('data', (d) => this.stderrChunks.push(d));
247
+ child.stdout.on('data', (d) => {
248
+ if (this.proc !== child)
249
+ return;
250
+ this.onStdout(d);
251
+ });
252
+ child.stderr.on('data', (d) => {
253
+ if (this.proc !== child)
254
+ return;
255
+ this.stderrChunks.push(d);
256
+ });
194
257
  child.on('error', (e) => {
258
+ if (this.proc !== child)
259
+ return;
195
260
  this.closeErr = e;
196
261
  this.closed = true;
197
262
  this.failWaiters(e);
198
263
  });
199
264
  child.on('close', (c) => {
265
+ if (this.proc !== child)
266
+ return;
200
267
  this.closeCode = c;
201
268
  this.closed = true;
202
269
  this.proc = null;
@@ -77,3 +77,53 @@ export interface WordArgs {
77
77
  * shortValues: single-char options that consume a value (e.g. ['n']).
78
78
  */
79
79
  export declare function parseWords(args: Word[], shortValues?: string[], longValues?: string[]): WordArgs;
80
+ export type CommandEffect = 'read' | 'write' | 'delete' | 'network' | 'process';
81
+ export type OptionSupport = 'implemented' | 'unsupported';
82
+ /** One short/long alias group. Unknown options on a spec'd command fail loud. */
83
+ export interface OptionSpec {
84
+ /** Short flag letter without dash (e.g. `'n'`). */
85
+ short?: string;
86
+ /** Long option including dashes (e.g. `'--no-clobber'`). */
87
+ long?: string;
88
+ /** Consumes a following argument (`-n 5`, `--lines=5`). */
89
+ takesValue?: boolean;
90
+ support: OptionSupport;
91
+ /** Extra phrase for unsupported options (`interactive prompt`). */
92
+ reason?: string;
93
+ }
94
+ export interface CommandSpec {
95
+ names: string[];
96
+ options: OptionSpec[];
97
+ effects: CommandEffect[];
98
+ platform?: 'windows-ps51' | 'portable-translate';
99
+ dispatch?: 'translated' | 'native' | 'dynamic';
100
+ handler: Handler;
101
+ }
102
+ /** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
103
+ export declare function registerSpec(spec: CommandSpec): void;
104
+ export declare function registerSpecs(list: CommandSpec[]): void;
105
+ export declare function lookupSpec(name: string): CommandSpec | undefined;
106
+ /** Unique specs in registration order. */
107
+ export declare function registeredSpecs(): CommandSpec[];
108
+ export interface ListedCommand {
109
+ name: string;
110
+ spec: null | {
111
+ options: Array<{
112
+ short?: string;
113
+ long?: string;
114
+ takesValue: boolean;
115
+ support: OptionSupport;
116
+ reason?: string;
117
+ }>;
118
+ effects: CommandEffect[];
119
+ platform: 'windows-ps51' | 'portable-translate';
120
+ dispatch: 'translated' | 'native' | 'dynamic';
121
+ };
122
+ }
123
+ /** Capability dump for `fauxnix list --json` / MCP introspection. */
124
+ export declare function listCommandsJson(): ListedCommand[];
125
+ /**
126
+ * Walk argv against a CommandSpec. Returns a PowerShell error script, or
127
+ * null when every option is recognized and implemented.
128
+ */
129
+ export declare function specOptionError(spec: CommandSpec, args: Word[], cmdName: string): string | null;
package/dist/registry.js CHANGED
@@ -153,3 +153,145 @@ export function parseWords(args, shortValues = [], longValues = []) {
153
153
  }
154
154
  return { flags, longs, values, missingValue, operandWords };
155
155
  }
156
+ const specs = new Map();
157
+ /** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
158
+ export function registerSpec(spec) {
159
+ for (const name of spec.names) {
160
+ const wrapped = (args, ctx) => {
161
+ const err = specOptionError(spec, args, name);
162
+ if (err)
163
+ return err;
164
+ return spec.handler(args, ctx);
165
+ };
166
+ registry.set(name, wrapped);
167
+ specs.set(name, spec);
168
+ }
169
+ }
170
+ export function registerSpecs(list) {
171
+ for (const spec of list)
172
+ registerSpec(spec);
173
+ }
174
+ export function lookupSpec(name) {
175
+ return specs.get(name);
176
+ }
177
+ /** Unique specs in registration order. */
178
+ export function registeredSpecs() {
179
+ const seen = new Set();
180
+ const out = [];
181
+ for (const spec of specs.values()) {
182
+ if (seen.has(spec))
183
+ continue;
184
+ seen.add(spec);
185
+ out.push(spec);
186
+ }
187
+ return out;
188
+ }
189
+ /** Capability dump for `fauxnix list --json` / MCP introspection. */
190
+ export function listCommandsJson() {
191
+ return registeredNames().map((name) => {
192
+ const spec = lookupSpec(name);
193
+ if (!spec)
194
+ return { name, spec: null };
195
+ return {
196
+ name,
197
+ spec: {
198
+ options: spec.options.map((o) => ({
199
+ ...(o.short ? { short: o.short } : {}),
200
+ ...(o.long ? { long: o.long } : {}),
201
+ takesValue: o.takesValue === true,
202
+ support: o.support,
203
+ ...(o.reason ? { reason: o.reason } : {}),
204
+ })),
205
+ effects: spec.effects,
206
+ platform: spec.platform ?? 'windows-ps51',
207
+ dispatch: spec.dispatch ?? 'translated',
208
+ },
209
+ };
210
+ });
211
+ }
212
+ /**
213
+ * Walk argv against a CommandSpec. Returns a PowerShell error script, or
214
+ * null when every option is recognized and implemented.
215
+ */
216
+ export function specOptionError(spec, args, cmdName) {
217
+ const shorts = new Map();
218
+ const longs = new Map();
219
+ for (const o of spec.options) {
220
+ if (o.short)
221
+ shorts.set(o.short, o);
222
+ if (o.long)
223
+ longs.set(o.long, o);
224
+ }
225
+ let i = 0;
226
+ let onlyOperands = false;
227
+ while (i < args.length) {
228
+ const t = wordToString(args[i]);
229
+ if (onlyOperands) {
230
+ i++;
231
+ continue;
232
+ }
233
+ if (t === '--') {
234
+ onlyOperands = true;
235
+ i++;
236
+ continue;
237
+ }
238
+ if (t.startsWith('--')) {
239
+ const eq = t.indexOf('=');
240
+ const name = eq >= 0 ? t.slice(0, eq) : t;
241
+ const opt = longs.get(name);
242
+ if (!opt)
243
+ return optionFail(cmdName, "unrecognized option '" + name + "'");
244
+ if (opt.support === 'unsupported') {
245
+ return optionFail(cmdName, unsupportedMsg(opt, name));
246
+ }
247
+ if (!opt.takesValue && eq >= 0) {
248
+ return optionFail(cmdName, "option '" + name + "' doesn't allow an argument");
249
+ }
250
+ if (opt.takesValue && eq < 0) {
251
+ if (i + 1 < args.length)
252
+ i++;
253
+ else
254
+ return optionFail(cmdName, "option '" + name + "' requires an argument");
255
+ }
256
+ i++;
257
+ continue;
258
+ }
259
+ if (t.startsWith('-') && t.length > 1 && !/^-?\d/.test(t.slice(1, 2))) {
260
+ const body = t.slice(1);
261
+ for (let c = 0; c < body.length; c++) {
262
+ const ch = body[c];
263
+ const opt = shorts.get(ch);
264
+ if (!opt)
265
+ return optionFail(cmdName, "invalid option -- '" + ch + "'");
266
+ if (opt.support === 'unsupported') {
267
+ return optionFail(cmdName, unsupportedMsg(opt, '-' + ch));
268
+ }
269
+ if (opt.takesValue) {
270
+ const rest = body.slice(c + 1);
271
+ if (!rest) {
272
+ if (i + 1 < args.length)
273
+ i++;
274
+ else
275
+ return optionFail(cmdName, "option requires an argument -- '" + ch + "'");
276
+ }
277
+ break;
278
+ }
279
+ }
280
+ i++;
281
+ continue;
282
+ }
283
+ i++;
284
+ }
285
+ return null;
286
+ }
287
+ function unsupportedMsg(opt, shown) {
288
+ const reason = opt.reason ? ' (' + opt.reason + ')' : '';
289
+ return "option '" + shown + "' is not supported by fauxnix" + reason;
290
+ }
291
+ function optionFail(cmd, msg) {
292
+ return ('[Console]::Error.WriteLine(' +
293
+ psStr(cmd + ': ' + msg) +
294
+ '); [Console]::Error.WriteLine(' +
295
+ psStr("Try '" + cmd + " --help' for more information.") +
296
+ '); $script:fx_exit = 1');
297
+ }
@@ -719,6 +719,8 @@ export function wrapTempEnv(sets, body, extra) {
719
719
  }
720
720
  /** Unique suffix for generated stage functions (nested pipelines included). */
721
721
  let stageSeq = 0;
722
+ /** Unique suffix for generated pipeline wrappers and their local status arrays. */
723
+ let pipelineSeq = 0;
722
724
  /**
723
725
  * Pipeline body. A lone command runs as a plain script-block expression;
724
726
  * multi-command pipelines become generated functions chained with `|`
@@ -784,6 +786,12 @@ function translateFor(cmd) {
784
786
  return lines.join('\n');
785
787
  }
786
788
  export function translatePipelineBody(p) {
789
+ // Every pipeline stage needs its own status slot. Handlers deliberately use
790
+ // `$script:fx_exit` because their helper functions run in child scopes; in a
791
+ // pipeline that shared flag lets an earlier failure leak into a successful
792
+ // last stage. Reserve the wrapper id before translating bodies so nested
793
+ // command substitutions cannot reuse it.
794
+ const pipelineId = p.commands.length > 1 ? pipelineSeq++ : -1;
787
795
  const bodies = [];
788
796
  for (let i = 0; i < p.commands.length; i++) {
789
797
  const c = p.commands[i];
@@ -801,16 +809,36 @@ export function translatePipelineBody(p) {
801
809
  }
802
810
  const names = [];
803
811
  const defs = [];
812
+ const statusVar = '$fx_pipe_status' + pipelineId;
804
813
  for (let i = 0; i < bodies.length; i++) {
805
814
  const name = '__fx_s' + stageSeq++;
806
815
  names.push(name);
807
- const indented = bodies[i]
816
+ const isolatedBody = bodies[i].split('$script:fx_exit').join(statusVar + '[' + i + ']');
817
+ const indented = isolatedBody
808
818
  .split('\n')
809
819
  .map((l) => (l ? ' ' + l : l))
810
820
  .join('\n');
811
821
  defs.push('function ' + name + ' {\n' + indented + '\n}');
812
822
  }
813
- return { defs: defs.join('\n'), call: names.join(' | ') };
823
+ const pipelineName = '__fx_p' + pipelineId;
824
+ const statuses = bodies.map(() => '0').join(', ');
825
+ const pipelineCall = names.join(' | ');
826
+ defs.push([
827
+ 'function ' + pipelineName + ' {',
828
+ ' ' + statusVar + ' = @(' + statuses + ')',
829
+ ' try {',
830
+ // The wrapper forwards redirect input to stage zero. With no input,
831
+ // PowerShell still invokes a regular function once, which preserves the
832
+ // existing no-stdin pipeline behavior on Windows PowerShell 5.1.
833
+ ' $input | ' + pipelineCall,
834
+ ' } finally {',
835
+ // Bash defaults to pipefail off: only the last stage controls the list
836
+ // status used by a following && / || segment.
837
+ ' $script:fx_exit = [int]' + statusVar + '[' + (bodies.length - 1) + ']',
838
+ ' }',
839
+ '}',
840
+ ].join('\n'));
841
+ return { defs: defs.join('\n'), call: pipelineName };
814
842
  }
815
843
  export function translateCommandList(list) {
816
844
  const plans = [];
@@ -1240,6 +1268,13 @@ while ($true) {
1240
1268
  $fx_code = 0
1241
1269
  try { $fx_code = [int]$script:fx_exit } catch { $fx_code = 1 }
1242
1270
  $fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
1243
- $fx_proto.WriteLine(($fx_res | ConvertTo-Json -Compress))
1271
+ try {
1272
+ $fx_json = $fx_res | ConvertTo-Json -Compress
1273
+ } catch {
1274
+ $fx_msg = 'fauxnix: host result exceeded ConvertTo-Json MaxJsonLength (~2MB)'
1275
+ $fx_res = @{ id = $fx_id; stdoutB64 = ''; stderrB64 = [Convert]::ToBase64String($fx_utf8.GetBytes($fx_msg)); exitCode = 1 }
1276
+ $fx_json = $fx_res | ConvertTo-Json -Compress
1277
+ }
1278
+ $fx_proto.WriteLine($fx_json)
1244
1279
  }
1245
1280
  `.trim();
@@ -0,0 +1 @@
1
+ export declare const packageVersion: string;
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from 'node:fs';
2
+ // src/ and dist/ are both one level below the package root, so package.json is
3
+ // the runtime source of truth in development and in the published tarball.
4
+ const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
5
+ if (typeof metadata.version !== 'string' || metadata.version.length === 0) {
6
+ throw new Error('fauxnix: package.json does not contain a valid version');
7
+ }
8
+ export const packageVersion = metadata.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxnix-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Fauxnix — run Linux-style commands on Windows via deterministic PowerShell translation. No VM, no WSL. MCP server + CLI for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,9 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc",
16
+ "prepare": "npm run build",
16
17
  "test": "vitest run",
18
+ "test:package": "node scripts/package-smoke.mjs",
17
19
  "test:watch": "vitest",
18
20
  "typecheck": "tsc --noEmit",
19
21
  "dev": "tsx src/index.ts"