fauxnix-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -154,9 +154,10 @@ development:
154
154
  translate time (unsupported constructs throw named errors, never silently misbehave)
155
155
  - **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
156
156
 
157
- `cp` / `mv` / `rm` / `touch` / `tee` carry a `CommandSpec`: unknown options fail with a GNU-style
158
- usage error instead of being ignored. `cp -n` / `mv -n` / `touch -c` / `tee --append` match GNU
159
- (no clobber / no-create / append). `fauxnix list --json` dumps the same capability metadata.
157
+ `cp` / `mv` / `rm` / `touch` / `tee` / `grep` / `head` / `du` carry a `CommandSpec`: unknown
158
+ options fail with a GNU-style usage error instead of being ignored. Implemented GNU holes:
159
+ `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` / `head --lines` / `du --max-depth`.
160
+ `fauxnix list --json` and `docs/command-specs.md` dump the same metadata.
160
161
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
161
162
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
162
163
  timeout man history less more source . eval exit alias set`
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { FauxnixSession } from './executor.js';
3
3
  import { parseCommand } from './parser.js';
4
4
  import { translateCommandList } from './translator.js';
5
- import { listCommandsJson, registeredNames } from './registry.js';
5
+ import { listCommandsJson, registeredNames, specsMarkdown } from './registry.js';
6
6
  import { encodeCommand } from './encoding.js';
7
7
  import { startMcpServer } from './mcp.js';
8
8
  import { packageVersion } from './version.js';
@@ -16,6 +16,7 @@ Usage:
16
16
  fauxnix mcp start the MCP stdio server (for agent harnesses)
17
17
  fauxnix list list translated commands
18
18
  fauxnix list --json same list as machine-readable capability metadata
19
+ fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)
19
20
  fauxnix check verify the local PowerShell environment
20
21
  fauxnix --version
21
22
 
@@ -36,6 +37,10 @@ export async function runCli(argv) {
36
37
  console.log(JSON.stringify(listCommandsJson(), null, 2));
37
38
  return;
38
39
  }
40
+ if (rest[0] === '--markdown') {
41
+ console.log(specsMarkdown());
42
+ return;
43
+ }
39
44
  const names = registeredNames();
40
45
  console.log(names.length + ' translated commands:');
41
46
  for (const n of names)
@@ -51,8 +51,8 @@ function psArray(words, fn = operandExpr) {
51
51
  /* ls */
52
52
  /* ------------------------------------------------------------------ */
53
53
  const ls = (args) => {
54
- const { flags, longs, operandWords } = parseWords(args);
55
- const long = flags.has('l') || longs.has('--format=long') || longs.has('--long');
54
+ const { flags, longs, values, operandWords } = parseWords(args, [], ['--format']);
55
+ const long = flags.has('l') || longs.has('--long') || values.get('--format') === 'long';
56
56
  const all = flags.has('a') || longs.has('--all');
57
57
  const almost = flags.has('A') || longs.has('--almost-all');
58
58
  const dirOnly = flags.has('d') || longs.has('--directory');
@@ -208,7 +208,7 @@ const rm = (args) => {
208
208
  const mkdir = (args) => {
209
209
  const { flags, longs, operandWords } = parseWords(args);
210
210
  const parents = flags.has('p') || longs.has('--parents');
211
- const verbose = flags.has('v');
211
+ const verbose = flags.has('v') || longs.has('--verbose');
212
212
  return [
213
213
  '$fx_dirs = ' + psArray(operandWords),
214
214
  "if ($fx_dirs.Count -eq 0) { [Console]::Error.WriteLine('mkdir: missing operand'); $script:fx_exit = 1 }",
@@ -254,8 +254,8 @@ const touch = (args) => {
254
254
  ].join('\n');
255
255
  };
256
256
  const mktemp = (args) => {
257
- const { flags } = parseWords(args);
258
- const dir = flags.has('d');
257
+ const { flags, longs } = parseWords(args);
258
+ const dir = flags.has('d') || longs.has('--directory');
259
259
  return [
260
260
  'try {',
261
261
  ' if (' + (dir ? '$true' : '$false') + ') {',
@@ -272,8 +272,8 @@ const mktemp = (args) => {
272
272
  /* ln / readlink / realpath */
273
273
  /* ------------------------------------------------------------------ */
274
274
  const ln = (args) => {
275
- const { flags, operandWords } = parseWords(args);
276
- const sym = flags.has('s');
275
+ const { flags, longs, operandWords } = parseWords(args);
276
+ const sym = flags.has('s') || longs.has('--symbolic');
277
277
  const kind = sym ? 'SymbolicLink' : 'HardLink';
278
278
  const label = sym ? 'symbolic link' : 'hard link';
279
279
  return [
@@ -288,8 +288,8 @@ const ln = (args) => {
288
288
  ].join('\n');
289
289
  };
290
290
  const readlink = (args) => {
291
- const { flags, operandWords } = parseWords(args);
292
- const canon = flags.has('f');
291
+ const { flags, longs, operandWords } = parseWords(args);
292
+ const canon = flags.has('f') || longs.has('--canonicalize');
293
293
  return [
294
294
  '$fx_p = ' + (operandWords.length ? operandExpr(operandWords[0]) : "''"),
295
295
  "if ($fx_p -eq '') { [Console]::Error.WriteLine('readlink: missing operand'); $script:fx_exit = 1 }",
@@ -422,10 +422,31 @@ const file = (args) => {
422
422
  /* du / df */
423
423
  /* ------------------------------------------------------------------ */
424
424
  const du = (args) => {
425
- const { flags, longs, operandWords } = parseWords(args, [], ['--max-depth']);
425
+ const { flags, longs, values, missingValue, operandWords } = parseWords(args, ['d'], ['--max-depth']);
426
426
  const sum = flags.has('s') || longs.has('--summarize');
427
427
  const human = flags.has('h') || longs.has('--human-readable');
428
+ if (missingValue.includes('-d') || missingValue.includes('--max-depth')) {
429
+ return ('[Console]::Error.WriteLine(' +
430
+ psStr("du: option requires an argument -- 'max-depth'") +
431
+ '); $script:fx_exit = 1');
432
+ }
433
+ const maxDepthRaw = values.get('-d') ?? values.get('--max-depth');
434
+ let maxDepth = null;
435
+ if (maxDepthRaw !== undefined) {
436
+ if (!/^\d+$/.test(maxDepthRaw)) {
437
+ return ('[Console]::Error.WriteLine(' +
438
+ psStr("du: invalid maximum depth '" + maxDepthRaw + "'") +
439
+ '); $script:fx_exit = 1');
440
+ }
441
+ maxDepth = parseInt(maxDepthRaw, 10);
442
+ }
443
+ if (sum && maxDepth !== null && maxDepth !== 0) {
444
+ return ('[Console]::Error.WriteLine(' +
445
+ psStr('du: summarizing conflicts with --max-depth=' + String(maxDepth)) +
446
+ '); $script:fx_exit = 1');
447
+ }
428
448
  const targets = operandWords.length ? argListExpr(operandWords) : "@('.')";
449
+ const onlyRoot = sum || maxDepth === 0;
429
450
  return [
430
451
  PS_HSIZE_FN,
431
452
  'function fx-size($p) {',
@@ -436,7 +457,7 @@ const du = (args) => {
436
457
  '$fx_ts = ' + targets,
437
458
  'foreach ($fx_t in $fx_ts) {',
438
459
  ' if (-not (Test-Path -LiteralPath $fx_t)) { [Console]::Error.WriteLine("du: cannot access \'" + $fx_t + "\': No such file or directory"); $script:fx_exit = 1; continue }',
439
- ' if (' + (sum ? '$true' : '$false') + ') {',
460
+ ' if (' + (onlyRoot ? '$true' : '$false') + ') {',
440
461
  ' $fx_kb = fx-size $fx_t',
441
462
  ' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_t } else { "{0}`t{1}" -f $fx_kb, $fx_t }',
442
463
  ' } else {',
@@ -444,6 +465,11 @@ const du = (args) => {
444
465
  ' foreach ($fx_d in @(Get-ChildItem -LiteralPath $fx_t -Recurse -Force -Directory -ErrorAction SilentlyContinue)) {',
445
466
  ' $fx_kb = fx-size $fx_d.FullName',
446
467
  " $fx_rel = './' + $fx_d.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
468
+ maxDepth !== null && maxDepth > 0
469
+ ? " $fx_ddepth = @($fx_rel.ToCharArray() | Where-Object { $_ -eq '/' }).Count; if ($fx_ddepth -gt " +
470
+ maxDepth +
471
+ ') { continue }'
472
+ : '',
447
473
  ' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_rel } else { "{0}`t{1}" -f $fx_kb, $fx_rel }',
448
474
  ' }',
449
475
  ' $fx_kb = fx-size $fx_t',
@@ -453,8 +479,8 @@ const du = (args) => {
453
479
  ].join('\n');
454
480
  };
455
481
  const df = (args) => {
456
- const { flags } = parseWords(args);
457
- const human = flags.has('h') || flags.has('H');
482
+ const { flags, longs } = parseWords(args);
483
+ const human = flags.has('h') || flags.has('H') || longs.has('--human-readable');
458
484
  return [
459
485
  PS_HSIZE_FN,
460
486
  '"Filesystem Size Used Avail Use% Mounted on"',
@@ -801,9 +827,9 @@ const chown = () => {
801
827
  /* diff — LCS-based, GNU normal format (+ -q, -u) */
802
828
  /* ------------------------------------------------------------------ */
803
829
  const diff = (args) => {
804
- const { flags, operandWords } = parseWords(args);
805
- const unified = flags.has('u') || flags.has('U');
806
- const brief = flags.has('q') || flags.has('brief');
830
+ const { flags, longs, operandWords } = parseWords(args);
831
+ const unified = flags.has('u') || flags.has('U') || longs.has('--unified');
832
+ const brief = flags.has('q') || longs.has('--brief');
807
833
  void unified;
808
834
  return [
809
835
  PS_READTEXT_FN,
@@ -910,6 +936,43 @@ export const specs = [
910
936
  opt('i', '--interactive', 'unsupported', INTERACTIVE),
911
937
  ], rm),
912
938
  fileSpec(['touch'], ['write'], [opt('c', '--no-create')], touch),
939
+ fileSpec(['du'], ['read'], [
940
+ opt('s', '--summarize'),
941
+ opt('h', '--human-readable'),
942
+ opt('d', '--max-depth', 'implemented', { takesValue: true }),
943
+ ], du),
944
+ fileSpec(['ls', 'll'], ['read'], [
945
+ opt('l', '--long'),
946
+ opt(undefined, '--format', 'implemented', { takesValue: true }),
947
+ opt('a', '--all'),
948
+ opt('A', '--almost-all'),
949
+ opt('d', '--directory'),
950
+ opt('h', '--human-readable'),
951
+ opt('F', '--classify'),
952
+ opt('p', undefined),
953
+ opt('t', undefined),
954
+ opt('S', undefined),
955
+ opt('r', undefined),
956
+ opt('R', '--recursive', 'unsupported', { reason: 'recursive listing' }),
957
+ ], ls),
958
+ fileSpec(['mkdir'], ['write'], [opt('p', '--parents'), opt('v', '--verbose')], mkdir),
959
+ fileSpec(['rmdir'], ['delete'], [], rmdir),
960
+ fileSpec(['mktemp'], ['write'], [opt('d', '--directory')], mktemp),
961
+ fileSpec(['ln'], ['read', 'write'], [opt('s', '--symbolic')], ln),
962
+ fileSpec(['readlink'], ['read'], [opt('f', '--canonicalize')], readlink),
963
+ fileSpec(['realpath'], ['read'], [], realpath),
964
+ fileSpec(['basename'], ['read'], [], basename),
965
+ fileSpec(['dirname'], ['read'], [], dirname),
966
+ fileSpec(['stat'], ['read'], [
967
+ opt('c', undefined, 'implemented', { takesValue: true }),
968
+ opt(undefined, '--format', 'implemented', { takesValue: true }),
969
+ opt(undefined, '--printf', 'implemented', { takesValue: true }),
970
+ ], stat),
971
+ fileSpec(['file'], ['read'], [], file),
972
+ fileSpec(['df'], ['read'], [opt('h', '--human-readable'), opt('H', undefined)], df),
973
+ fileSpec(['chmod'], ['write'], [opt('R', '--recursive', 'unsupported', { reason: 'recursive chmod' })], chmod),
974
+ fileSpec(['chown'], ['write'], [], chown),
975
+ fileSpec(['diff'], ['read'], [opt('q', '--brief'), opt('u', '--unified'), opt('U', undefined)], diff),
913
976
  ];
914
977
  export const handlers = {
915
978
  ls,
@@ -924,10 +987,7 @@ export const handlers = {
924
987
  dirname,
925
988
  stat,
926
989
  file,
927
- du,
928
990
  df,
991
+ du,
929
992
  find,
930
- chmod,
931
- chown,
932
- diff,
933
993
  };
@@ -1,6 +1,6 @@
1
1
  import { registerAll, registerSpecs } from '../registry.js';
2
2
  import { handlers as files, specs as fileSpecs } from './files.js';
3
- import { handlers as textFilters } from './text-filters.js';
3
+ import { handlers as textFilters, specs as textFilterSpecs } from './text-filters.js';
4
4
  import { handlers as textIo, specs as textIoSpecs } from './text-io.js';
5
5
  import { handlers as sysinfo } from './sysinfo.js';
6
6
  import { handlers as net } from './net.js';
@@ -15,5 +15,6 @@ export function installAll() {
15
15
  registerAll(archive);
16
16
  registerSpecs(fileSpecs);
17
17
  registerSpecs(textIoSpecs);
18
+ registerSpecs(textFilterSpecs);
18
19
  }
19
20
  installAll();
@@ -1,2 +1,3 @@
1
- import { Handler } from '../registry.js';
1
+ import { CommandSpec, Handler } from '../registry.js';
2
+ export declare const specs: CommandSpec[];
2
3
  export declare const handlers: Record<string, Handler>;
@@ -268,13 +268,23 @@ const grep = (args) => {
268
268
  const excludeDirGlobs = filterOptions
269
269
  .filter((o) => o.name === '--exclude-dir')
270
270
  .map((o) => o.value.replace(/[\\/]+$/, ''));
271
- const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C'], filterOptionNames);
272
- const missingFilterOption = missingValue.find((o) => filterOptionNames.includes(o));
271
+ const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C', 'm', 'e'], [...filterOptionNames, '--max-count', '--regexp']);
272
+ const missingFilterOption = missingValue.find((o) => [...filterOptionNames, '-m', '--max-count', '-e', '--regexp'].includes(o));
273
273
  if (missingFilterOption) {
274
274
  return ('[Console]::Error.WriteLine(' +
275
275
  psStr("grep: option '" + missingFilterOption + "' requires an argument") +
276
276
  '); $script:fx_exit = 2');
277
277
  }
278
+ const maxCountRaw = values.get('-m') ?? values.get('--max-count');
279
+ let maxCount = null;
280
+ if (maxCountRaw !== undefined) {
281
+ if (!/^\d+$/.test(maxCountRaw)) {
282
+ return ('[Console]::Error.WriteLine(' +
283
+ psStr("grep: invalid max count '" + maxCountRaw + "'") +
284
+ '); $script:fx_exit = 2');
285
+ }
286
+ maxCount = parseInt(maxCountRaw, 10);
287
+ }
278
288
  const ci = flags.has('i');
279
289
  const inv = flags.has('v');
280
290
  const num = flags.has('n');
@@ -294,11 +304,12 @@ const grep = (args) => {
294
304
  };
295
305
  const ctxA = Math.max(toInt(values.get('-A')), toInt(values.get('-C')));
296
306
  const ctxB = Math.max(toInt(values.get('-B')), toInt(values.get('-C')));
297
- if (operandWords.length === 0) {
307
+ const ePat = values.get('-e') ?? values.get('--regexp');
308
+ if (ePat === undefined && operandWords.length === 0) {
298
309
  return ("[Console]::Error.WriteLine('usage: grep [OPTION]... PATTERN [FILE]...'); $script:fx_exit = 2");
299
310
  }
300
- const patternWord = operandWords[0];
301
- const fileWords = operandWords.slice(1);
311
+ const patternWord = ePat !== undefined ? [{ kind: 'Text', text: ePat }] : operandWords[0];
312
+ const fileWords = ePat !== undefined ? operandWords : operandWords.slice(1);
302
313
  const patLit = literalOfWord(patternWord);
303
314
  let patExpr;
304
315
  if (fixed || patLit === null) {
@@ -435,10 +446,16 @@ const grep = (args) => {
435
446
  lines.push('}');
436
447
  // --- per-source scan body ----------------------------------------------
437
448
  const scan = [];
449
+ if (maxCount !== null)
450
+ scan.push('$fx_mleft = ' + maxCount);
438
451
  if (cntMode) {
439
452
  scan.push('$fx_c = 0');
440
453
  scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
441
- scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_c++ }');
454
+ if (maxCount !== null)
455
+ scan.push(' if ($fx_mleft -le 0) { break }');
456
+ scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_c++' +
457
+ (maxCount !== null ? '; $fx_mleft--; if ($fx_mleft -le 0) { break }' : '') +
458
+ ' }');
442
459
  scan.push('}');
443
460
  scan.push('if ($fx_c -gt 0) { $fx_any = $true }');
444
461
  scan.push('if ($fx_pre) { $fx_disp + \':\' + [string]$fx_c } else { [string]$fx_c }');
@@ -446,21 +463,29 @@ const grep = (args) => {
446
463
  else if (listMode) {
447
464
  scan.push('$fx_hit1 = $false');
448
465
  scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
466
+ if (maxCount !== null)
467
+ scan.push(' if ($fx_mleft -le 0) { break }');
449
468
  scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_hit1 = $true; break }');
450
469
  scan.push('}');
451
470
  scan.push('if ($fx_hit1) { $fx_any = $true; $fx_disp }');
452
471
  }
453
472
  else if (quiet) {
454
473
  scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
474
+ if (maxCount !== null)
475
+ scan.push(' if ($fx_mleft -le 0) { break }');
455
476
  scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_any = $true; break }');
456
477
  scan.push('}');
457
478
  }
458
479
  else {
459
480
  scan.push('$fx_hits = @()');
460
481
  scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
482
+ if (maxCount !== null)
483
+ scan.push(' if ($fx_mleft -le 0) { break }');
461
484
  scan.push(' $fx_l = $fx_ls[$fx_i]');
462
485
  scan.push(' if (fx-gmatch $fx_l) {');
463
486
  scan.push(' $fx_any = $true');
487
+ if (maxCount !== null)
488
+ scan.push(' $fx_mleft--');
464
489
  if (onlyMatch && !inv) {
465
490
  if (fixed) {
466
491
  if (ci) {
@@ -492,6 +517,8 @@ const grep = (args) => {
492
517
  else if (!onlyMatch) {
493
518
  scan.push(' $fx_hits += $fx_i');
494
519
  }
520
+ if (maxCount !== null)
521
+ scan.push(' if ($fx_mleft -le 0) { break }');
495
522
  scan.push(' }');
496
523
  scan.push('}');
497
524
  if (!onlyMatch) {
@@ -2347,8 +2374,41 @@ const tr = (args) => {
2347
2374
  return lines.join('\n');
2348
2375
  };
2349
2376
  /* ------------------------------------------------------------------ */
2377
+ export const specs = [
2378
+ {
2379
+ names: ['grep'],
2380
+ options: [
2381
+ { short: 'i', support: 'implemented' },
2382
+ { short: 'v', support: 'implemented' },
2383
+ { short: 'n', support: 'implemented' },
2384
+ { short: 'c', support: 'implemented' },
2385
+ { short: 'l', support: 'implemented' },
2386
+ { short: 'r', support: 'implemented' },
2387
+ { short: 'R', support: 'implemented' },
2388
+ { short: 'E', support: 'implemented' },
2389
+ { short: 'F', support: 'implemented' },
2390
+ { short: 'w', support: 'implemented' },
2391
+ { short: 'q', support: 'implemented' },
2392
+ { short: 'o', support: 'implemented' },
2393
+ { short: 'h', support: 'implemented' },
2394
+ { short: 'H', support: 'implemented' },
2395
+ { short: 'A', takesValue: true, support: 'implemented' },
2396
+ { short: 'B', takesValue: true, support: 'implemented' },
2397
+ { short: 'C', takesValue: true, support: 'implemented' },
2398
+ { short: 'm', long: '--max-count', takesValue: true, support: 'implemented' },
2399
+ { short: 'e', long: '--regexp', takesValue: true, support: 'implemented' },
2400
+ { long: '--include', takesValue: true, support: 'implemented' },
2401
+ { long: '--exclude', takesValue: true, support: 'implemented' },
2402
+ { long: '--exclude-dir', takesValue: true, support: 'implemented' },
2403
+ ],
2404
+ effects: ['read'],
2405
+ platform: 'windows-ps51',
2406
+ dispatch: 'translated',
2407
+ usageExit: 2,
2408
+ handler: grep,
2409
+ },
2410
+ ];
2350
2411
  export const handlers = {
2351
- grep,
2352
2412
  egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
2353
2413
  sed,
2354
2414
  awk,
@@ -419,6 +419,37 @@ const head = (args, ctx) => {
419
419
  continue;
420
420
  }
421
421
  if (t.startsWith('--')) {
422
+ const eq = t.indexOf('=');
423
+ const name = eq >= 0 ? t.slice(0, eq) : t;
424
+ const inline = eq >= 0 ? t.slice(eq + 1) : null;
425
+ if (name === '--lines' || name === '--bytes') {
426
+ let val = inline;
427
+ if (val === null) {
428
+ if (i + 1 >= args.length) {
429
+ return psErrExpr(psStr('head: option requires an argument -- ' + name.slice(2)));
430
+ }
431
+ val = wordToString(args[i + 1]);
432
+ i += 2;
433
+ }
434
+ else {
435
+ i++;
436
+ }
437
+ if (name === '--bytes')
438
+ nBytes = val;
439
+ else
440
+ nLines = val;
441
+ continue;
442
+ }
443
+ if (name === '--quiet' || name === '--silent') {
444
+ quiet = true;
445
+ i++;
446
+ continue;
447
+ }
448
+ if (name === '--verbose') {
449
+ verbose = true;
450
+ i++;
451
+ continue;
452
+ }
422
453
  i++;
423
454
  continue;
424
455
  }
@@ -464,6 +495,9 @@ const head = (args, ctx) => {
464
495
  }
465
496
  const bytesMode = nBytes !== null;
466
497
  const countLit = bytesMode ? nBytes : nLines !== null ? nLines : '10';
498
+ if (!/^[+-]?\d+$/.test(countLit)) {
499
+ return psErrExpr(psStr("head: invalid number of " + (bytesMode ? 'bytes' : 'lines') + ": '" + countLit + "'"));
500
+ }
467
501
  const lines = [
468
502
  PS_GLOB_FN,
469
503
  PS_READTEXT_FN,
@@ -516,6 +550,39 @@ const tail = (args, ctx) => {
516
550
  continue;
517
551
  }
518
552
  if (t.startsWith('--')) {
553
+ const eq = t.indexOf('=');
554
+ const name = eq >= 0 ? t.slice(0, eq) : t;
555
+ const inline = eq >= 0 ? t.slice(eq + 1) : null;
556
+ if (name === '--lines' || name === '--bytes') {
557
+ let val = inline;
558
+ if (val === null) {
559
+ if (i + 1 >= args.length) {
560
+ return psErrExpr(psStr('tail: option requires an argument -- ' + name.slice(2)));
561
+ }
562
+ val = wordToString(args[i + 1]);
563
+ i += 2;
564
+ }
565
+ else {
566
+ i++;
567
+ }
568
+ if (name === '--bytes')
569
+ nBytes = val;
570
+ else if (val.startsWith('+'))
571
+ fromLine = val.slice(1);
572
+ else
573
+ nLines = val.replace(/^-/, '');
574
+ continue;
575
+ }
576
+ if (name === '--quiet' || name === '--silent') {
577
+ quiet = true;
578
+ i++;
579
+ continue;
580
+ }
581
+ if (name === '--verbose') {
582
+ verbose = true;
583
+ i++;
584
+ continue;
585
+ }
519
586
  i++;
520
587
  continue;
521
588
  }
@@ -1153,12 +1220,25 @@ export const specs = [
1153
1220
  dispatch: 'translated',
1154
1221
  handler: tee,
1155
1222
  },
1223
+ {
1224
+ names: ['head'],
1225
+ options: [
1226
+ { short: 'n', long: '--lines', takesValue: true, support: 'implemented' },
1227
+ { short: 'c', long: '--bytes', takesValue: true, support: 'implemented' },
1228
+ { short: 'q', long: '--quiet', support: 'implemented' },
1229
+ { long: '--silent', support: 'implemented' },
1230
+ { short: 'v', long: '--verbose', support: 'implemented' },
1231
+ ],
1232
+ effects: ['read'],
1233
+ platform: 'windows-ps51',
1234
+ dispatch: 'translated',
1235
+ handler: head,
1236
+ },
1156
1237
  ];
1157
1238
  export const handlers = {
1158
1239
  echo,
1159
1240
  printf,
1160
1241
  cat,
1161
- head,
1162
1242
  tail,
1163
1243
  wc,
1164
1244
  nl,
package/dist/executor.js CHANGED
@@ -410,7 +410,7 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
410
410
  FAUXNIX_CWD: currentDir,
411
411
  FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
412
412
  FAUXNIX_STDIN_FILE: red.stdinFile || '',
413
- }, remainingMs, opts.signal);
413
+ }, remainingMs, opts.signal, { stdoutLimit, stderrLimit });
414
414
  if (inv.spawnError === 'ENOENT' || inv.spawnError === 'START') {
415
415
  stderr += inv.stderr.toString('utf8') || PS_MISSING_MESSAGE;
416
416
  exitCode = 127;
package/dist/ps-host.d.ts CHANGED
@@ -14,7 +14,39 @@ export interface HostInvokeResult {
14
14
  export interface HostRequestEnv {
15
15
  [key: string]: string;
16
16
  }
17
- 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
+ };
18
50
  export declare function decodeHostResponse(line: string): {
19
51
  id: string;
20
52
  stdout: Buffer;
@@ -40,10 +72,14 @@ export declare class PowerShellHost {
40
72
  private closed;
41
73
  private startLock;
42
74
  private invokeLock;
75
+ protocol: 1 | 2;
43
76
  constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
44
77
  /** Start the resident process and wait for the ready handshake (B1 prewarm). */
45
78
  ready(): Promise<HostInvokeResult | null>;
46
- invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal): Promise<HostInvokeResult>;
79
+ invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: {
80
+ stdoutLimit?: number;
81
+ stderrLimit?: number;
82
+ }): Promise<HostInvokeResult>;
47
83
  drainNativeStderr(): Buffer;
48
84
  stop(): Promise<void>;
49
85
  private cancelledResult;
@@ -55,5 +91,7 @@ export declare class PowerShellHost {
55
91
  private nextLine;
56
92
  private nextReadyLine;
57
93
  private nextJsonLine;
94
+ private collectV2;
95
+ private waitNativeMarker;
58
96
  private failWaiters;
59
97
  }
package/dist/ps-host.js CHANGED
@@ -8,12 +8,27 @@ export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix
8
8
  'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
9
9
  export const DEFAULT_STDOUT_LIMIT = 8_388_608;
10
10
  export const DEFAULT_STDERR_LIMIT = 1_048_576;
11
- export function encodeHostRequest(id, script, env) {
12
- return JSON.stringify({
11
+ export function encodeHostRequest(id, script, env, opts) {
12
+ const body = {
13
13
  id,
14
14
  scriptB64: Buffer.from(script, 'utf8').toString('base64'),
15
15
  env,
16
- });
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) };
17
32
  }
18
33
  export function decodeHostResponse(line) {
19
34
  const j = JSON.parse(line);
@@ -44,6 +59,7 @@ export class PowerShellHost {
44
59
  closed = false;
45
60
  startLock = null;
46
61
  invokeLock = Promise.resolve();
62
+ protocol = 1;
47
63
  constructor(hostFile, envFn) {
48
64
  this.hostFile = hostFile;
49
65
  this.envFn = envFn;
@@ -52,8 +68,8 @@ export class PowerShellHost {
52
68
  async ready() {
53
69
  return this.ensureStarted();
54
70
  }
55
- async invoke(script, env, timeoutMs, signal) {
56
- const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal));
71
+ async invoke(script, env, timeoutMs, signal, limits) {
72
+ const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal, limits));
57
73
  this.invokeLock = run.then(() => undefined, () => undefined);
58
74
  return run;
59
75
  }
@@ -101,7 +117,7 @@ export class PowerShellHost {
101
117
  truncated: false,
102
118
  };
103
119
  }
104
- async invokeSerial(script, env, timeoutMs, signal) {
120
+ async invokeSerial(script, env, timeoutMs, signal, limits) {
105
121
  if (signal?.aborted) {
106
122
  await this.stop();
107
123
  return this.cancelledResult();
@@ -110,7 +126,9 @@ export class PowerShellHost {
110
126
  if (started)
111
127
  return { ...started, cancelled: false, truncated: false };
112
128
  const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
113
- 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);
114
132
  try {
115
133
  this.proc.stdin.write(line + '\n');
116
134
  }
@@ -133,6 +151,9 @@ export class PowerShellHost {
133
151
  };
134
152
  signal?.addEventListener('abort', onAbort, { once: true });
135
153
  try {
154
+ if (this.protocol === 2) {
155
+ return await this.collectV2(id, timeoutMs);
156
+ }
136
157
  const raw = await this.nextJsonLine(timeoutMs, id);
137
158
  const msg = decodeHostResponse(raw);
138
159
  const native = this.drainNativeStderr();
@@ -271,10 +292,13 @@ export class PowerShellHost {
271
292
  });
272
293
  try {
273
294
  const readyLine = await this.nextReadyLine(READY_TIMEOUT_MS);
274
- const msg = decodeHostResponse(readyLine);
275
- 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
276
301
  throw new Error('fauxnix: powershell host handshake failed');
277
- }
278
302
  }
279
303
  catch (e) {
280
304
  await this.stop();
@@ -334,8 +358,10 @@ export class PowerShellHost {
334
358
  if (!line.trim())
335
359
  continue;
336
360
  try {
337
- const msg = decodeHostResponse(line);
338
- if (msg.ready)
361
+ const parsed = parseHostLine(line);
362
+ if (parsed.v2?.type === 'ready' || parsed.v1Ready)
363
+ return line;
364
+ if (parsed.v1?.ready)
339
365
  return line;
340
366
  }
341
367
  catch {
@@ -367,6 +393,77 @@ export class PowerShellHost {
367
393
  err.timedOut = true;
368
394
  throw err;
369
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
+ }
370
467
  failWaiters(err) {
371
468
  const ws = this.waiters.splice(0);
372
469
  for (const w of ws) {
@@ -97,6 +97,8 @@ export interface CommandSpec {
97
97
  effects: CommandEffect[];
98
98
  platform?: 'windows-ps51' | 'portable-translate';
99
99
  dispatch?: 'translated' | 'native' | 'dynamic';
100
+ /** GNU usage/syntax exit (grep uses 2; cp/mv/rm use 1). */
101
+ usageExit?: number;
100
102
  handler: Handler;
101
103
  }
102
104
  /** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
@@ -105,6 +107,8 @@ export declare function registerSpecs(list: CommandSpec[]): void;
105
107
  export declare function lookupSpec(name: string): CommandSpec | undefined;
106
108
  /** Unique specs in registration order. */
107
109
  export declare function registeredSpecs(): CommandSpec[];
110
+ /** Markdown dump of every CommandSpec — source for docs/command-specs.md. */
111
+ export declare function specsMarkdown(): string;
108
112
  export interface ListedCommand {
109
113
  name: string;
110
114
  spec: null | {
package/dist/registry.js CHANGED
@@ -186,6 +186,43 @@ export function registeredSpecs() {
186
186
  }
187
187
  return out;
188
188
  }
189
+ /** Markdown dump of every CommandSpec — source for docs/command-specs.md. */
190
+ export function specsMarkdown() {
191
+ const lines = [
192
+ '# Command specs',
193
+ '',
194
+ 'Generated from `CommandSpec`. Unlisted commands still use unchecked `parseWords` (unknown flags ignored). Spec\'d commands fail loud on unknown or unsupported options.',
195
+ '',
196
+ ];
197
+ for (const spec of registeredSpecs()) {
198
+ lines.push('## `' + spec.names.join('` / `') + '`');
199
+ lines.push('');
200
+ lines.push('Effects: ' + spec.effects.map((e) => '`' + e + '`').join(', '));
201
+ lines.push('');
202
+ if (!spec.options.length) {
203
+ lines.push('No options declared.');
204
+ }
205
+ else {
206
+ lines.push('| Option | Value | Support |');
207
+ lines.push('| --- | --- | --- |');
208
+ for (const o of spec.options) {
209
+ const names = [o.short ? '`-' + o.short + '`' : '', o.long ? '`' + o.long + '`' : '']
210
+ .filter((s) => s !== '')
211
+ .join(', ');
212
+ const val = o.takesValue ? 'required' : 'flag';
213
+ const extra = o.reason ? ' (' + o.reason + ')' : '';
214
+ lines.push('| ' + names + ' | ' + val + ' | ' + o.support + extra + ' |');
215
+ }
216
+ }
217
+ lines.push('');
218
+ }
219
+ const unspec = registeredNames().filter((n) => !lookupSpec(n));
220
+ lines.push('## Unspec\'d commands');
221
+ lines.push('');
222
+ lines.push(unspec.map((n) => '`' + n + '`').join(', '));
223
+ lines.push('');
224
+ return lines.join('\n');
225
+ }
189
226
  /** Capability dump for `fauxnix list --json` / MCP introspection. */
190
227
  export function listCommandsJson() {
191
228
  return registeredNames().map((name) => {
@@ -214,6 +251,8 @@ export function listCommandsJson() {
214
251
  * null when every option is recognized and implemented.
215
252
  */
216
253
  export function specOptionError(spec, args, cmdName) {
254
+ const usageExit = spec.usageExit ?? 1;
255
+ const fail = (msg) => optionFail(cmdName, msg, usageExit);
217
256
  const shorts = new Map();
218
257
  const longs = new Map();
219
258
  for (const o of spec.options) {
@@ -240,18 +279,18 @@ export function specOptionError(spec, args, cmdName) {
240
279
  const name = eq >= 0 ? t.slice(0, eq) : t;
241
280
  const opt = longs.get(name);
242
281
  if (!opt)
243
- return optionFail(cmdName, "unrecognized option '" + name + "'");
282
+ return fail("unrecognized option '" + name + "'");
244
283
  if (opt.support === 'unsupported') {
245
- return optionFail(cmdName, unsupportedMsg(opt, name));
284
+ return fail(unsupportedMsg(opt, name));
246
285
  }
247
286
  if (!opt.takesValue && eq >= 0) {
248
- return optionFail(cmdName, "option '" + name + "' doesn't allow an argument");
287
+ return fail("option '" + name + "' doesn't allow an argument");
249
288
  }
250
289
  if (opt.takesValue && eq < 0) {
251
290
  if (i + 1 < args.length)
252
291
  i++;
253
292
  else
254
- return optionFail(cmdName, "option '" + name + "' requires an argument");
293
+ return fail("option '" + name + "' requires an argument");
255
294
  }
256
295
  i++;
257
296
  continue;
@@ -262,9 +301,9 @@ export function specOptionError(spec, args, cmdName) {
262
301
  const ch = body[c];
263
302
  const opt = shorts.get(ch);
264
303
  if (!opt)
265
- return optionFail(cmdName, "invalid option -- '" + ch + "'");
304
+ return fail("invalid option -- '" + ch + "'");
266
305
  if (opt.support === 'unsupported') {
267
- return optionFail(cmdName, unsupportedMsg(opt, '-' + ch));
306
+ return fail(unsupportedMsg(opt, '-' + ch));
268
307
  }
269
308
  if (opt.takesValue) {
270
309
  const rest = body.slice(c + 1);
@@ -272,7 +311,7 @@ export function specOptionError(spec, args, cmdName) {
272
311
  if (i + 1 < args.length)
273
312
  i++;
274
313
  else
275
- return optionFail(cmdName, "option requires an argument -- '" + ch + "'");
314
+ return fail("option requires an argument -- '" + ch + "'");
276
315
  }
277
316
  break;
278
317
  }
@@ -288,10 +327,11 @@ function unsupportedMsg(opt, shown) {
288
327
  const reason = opt.reason ? ' (' + opt.reason + ')' : '';
289
328
  return "option '" + shown + "' is not supported by fauxnix" + reason;
290
329
  }
291
- function optionFail(cmd, msg) {
330
+ function optionFail(cmd, msg, code = 1) {
292
331
  return ('[Console]::Error.WriteLine(' +
293
332
  psStr(cmd + ': ' + msg) +
294
333
  '); [Console]::Error.WriteLine(' +
295
334
  psStr("Try '" + cmd + " --help' for more information.") +
296
- '); $script:fx_exit = 1');
335
+ '); $script:fx_exit = ' +
336
+ String(code));
297
337
  }
@@ -1209,12 +1209,36 @@ $fx_reader = New-Object System.IO.StreamReader($fx_in, $fx_utf8, $true, 8192, $t
1209
1209
  $fx_proto = New-Object System.IO.StreamWriter($fx_out, $fx_utf8, 8192, $true)
1210
1210
  $fx_proto.NewLine = [string][char]10
1211
1211
  $fx_proto.AutoFlush = $true
1212
- $fx_proto.WriteLine('{"ready":true}')
1212
+ $fx_proto.WriteLine('{"v":2,"type":"ready","capabilities":{"cancel":false,"maxChunkBytes":65536,"stderrMarker":true}}')
1213
+ function fx-b64([byte[]]$bytes, $off, $len) {
1214
+ if ($len -le 0) { return '' }
1215
+ $slice = New-Object byte[] $len
1216
+ [Array]::Copy($bytes, $off, $slice, 0, $len)
1217
+ return [Convert]::ToBase64String($slice)
1218
+ }
1219
+ function fx-emit-chunks($type, $id, [byte[]]$bytes, $limit, [ref]$seq) {
1220
+ $n = 0
1221
+ if ($null -ne $bytes) { $n = $bytes.Length }
1222
+ $use = $n
1223
+ $trunc = $false
1224
+ if ($use -gt $limit) { $use = $limit; $trunc = $true }
1225
+ $off = 0
1226
+ while ($off -lt $use) {
1227
+ $len = $use - $off
1228
+ if ($len -gt 65536) { $len = 65536 }
1229
+ $b64 = fx-b64 $bytes $off $len
1230
+ $fx_proto.WriteLine('{"v":2,"type":"' + $type + '","id":"' + $id + '","seq":' + $seq.Value + ',"dataB64":"' + $b64 + '"}')
1231
+ $seq.Value = $seq.Value + 1
1232
+ $off += $len
1233
+ }
1234
+ return $trunc
1235
+ }
1213
1236
  while ($true) {
1214
1237
  $fx_line = $fx_reader.ReadLine()
1215
1238
  if ($null -eq $fx_line) { break }
1216
1239
  if ($fx_line -eq '') { continue }
1217
1240
  $fx_id = ''
1241
+ $fx_req = $null
1218
1242
  $fx_msOut = $null
1219
1243
  $fx_msErr = $null
1220
1244
  $fx_outW = $null
@@ -1261,20 +1285,47 @@ while ($true) {
1261
1285
  try { [Console]::SetOut($fx_oldOut) } catch {}
1262
1286
  try { [Console]::SetError($fx_oldErr) } catch {}
1263
1287
  }
1264
- $fx_outB64 = ''
1265
- $fx_errB64 = ''
1266
- if ($null -ne $fx_msOut) { $fx_outB64 = [Convert]::ToBase64String($fx_msOut.ToArray()) }
1267
- if ($null -ne $fx_msErr) { $fx_errB64 = [Convert]::ToBase64String($fx_msErr.ToArray()) }
1268
1288
  $fx_code = 0
1269
1289
  try { $fx_code = [int]$script:fx_exit } catch { $fx_code = 1 }
1270
- $fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
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
1290
+ $fx_v2 = $false
1291
+ if ($null -ne $fx_req -and $null -ne $fx_req.PSObject.Properties['v']) {
1292
+ if ([int]$fx_req.v -eq 2) { $fx_v2 = $true }
1293
+ }
1294
+ $fx_outBytes = New-Object byte[] 0
1295
+ $fx_errBytes = New-Object byte[] 0
1296
+ if ($null -ne $fx_msOut) { $fx_outBytes = $fx_msOut.ToArray() }
1297
+ if ($null -ne $fx_msErr) { $fx_errBytes = $fx_msErr.ToArray() }
1298
+ if ($fx_v2) {
1299
+ $fx_outLimit = 8388608
1300
+ $fx_errLimit = 1048576
1301
+ if ($null -ne $fx_req.PSObject.Properties['stdoutLimit']) { $fx_outLimit = [int]$fx_req.stdoutLimit }
1302
+ if ($null -ne $fx_req.PSObject.Properties['stderrLimit']) { $fx_errLimit = [int]$fx_req.stderrLimit }
1303
+ $fx_outSeq = 0
1304
+ $fx_errSeq = 0
1305
+ $fx_trunc = $false
1306
+ if (fx-emit-chunks 'stdout' $fx_id $fx_outBytes $fx_outLimit ([ref]$fx_outSeq)) { $fx_trunc = $true }
1307
+ if (fx-emit-chunks 'stderr' $fx_id $fx_errBytes $fx_errLimit ([ref]$fx_errSeq)) { $fx_trunc = $true }
1308
+ $fx_nativeErr = [Console]::OpenStandardError()
1309
+ $fx_mark = $fx_utf8.GetBytes(('FAUXNIX_ERR_END:' + $fx_id + [char]10))
1310
+ $fx_nativeErr.Write($fx_mark, 0, $fx_mark.Length)
1311
+ $fx_nativeErr.Flush()
1312
+ $fx_end = '{"v":2,"type":"end","id":"' + $fx_id + '","exitCode":' + $fx_code + ',"timedOut":false,"cancelled":false,"truncated":'
1313
+ if ($fx_trunc) { $fx_end = $fx_end + 'true}' } else { $fx_end = $fx_end + 'false}' }
1314
+ $fx_proto.WriteLine($fx_end)
1315
+ } else {
1316
+ $fx_outB64 = ''
1317
+ $fx_errB64 = ''
1318
+ if ($fx_outBytes.Length -gt 0) { $fx_outB64 = [Convert]::ToBase64String($fx_outBytes) }
1319
+ if ($fx_errBytes.Length -gt 0) { $fx_errB64 = [Convert]::ToBase64String($fx_errBytes) }
1320
+ $fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
1321
+ try {
1322
+ $fx_json = $fx_res | ConvertTo-Json -Compress
1323
+ } catch {
1324
+ $fx_msg = 'fauxnix: host result exceeded ConvertTo-Json MaxJsonLength (~2MB)'
1325
+ $fx_res = @{ id = $fx_id; stdoutB64 = ''; stderrB64 = [Convert]::ToBase64String($fx_utf8.GetBytes($fx_msg)); exitCode = 1 }
1326
+ $fx_json = $fx_res | ConvertTo-Json -Compress
1327
+ }
1328
+ $fx_proto.WriteLine($fx_json)
1277
1329
  }
1278
- $fx_proto.WriteLine($fx_json)
1279
1330
  }
1280
1331
  `.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxnix-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {