fauxnix-cli 0.7.1 → 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/README.md CHANGED
@@ -153,6 +153,10 @@ development:
153
153
  - **text filters**: `grep egrep sed awk sort uniq cut tr` — sed/awk scripts are parsed at
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
+
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.
156
160
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
157
161
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
158
162
  timeout man history less more source . eval exit alias set`
@@ -196,6 +200,7 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
196
200
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
197
201
  an unbounded `yes | head` would hang.
198
202
  - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
203
+ `env -i`/`--ignore-environment`,
199
204
  and background `&` are rejected with actionable error messages instead of misbehaving.
200
205
  (`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
201
206
  dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
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 { registeredNames } from './registry.js';
5
+ import { listCommandsJson, registeredNames } from './registry.js';
6
6
  import { encodeCommand } from './encoding.js';
7
7
  import { startMcpServer } from './mcp.js';
8
8
  import { packageVersion } from './version.js';
@@ -15,6 +15,7 @@ Usage:
15
15
  fauxnix translate "cmd" show the PowerShell translation only
16
16
  fauxnix mcp start the MCP stdio server (for agent harnesses)
17
17
  fauxnix list list translated commands
18
+ fauxnix list --json same list as machine-readable capability metadata
18
19
  fauxnix check verify the local PowerShell environment
19
20
  fauxnix --version
20
21
 
@@ -31,6 +32,10 @@ export async function runCli(argv) {
31
32
  return;
32
33
  }
33
34
  if (verb === 'list') {
35
+ if (rest[0] === '--json') {
36
+ console.log(JSON.stringify(listCommandsJson(), null, 2));
37
+ return;
38
+ }
34
39
  const names = registeredNames();
35
40
  console.log(names.length + ' translated commands:');
36
41
  for (const n of names)
@@ -1,2 +1,5 @@
1
- import { Handler } from '../registry.js';
1
+ import { CommandSpec, Handler } from '../registry.js';
2
+ /** Destructive file commands migrated first (#130). Unspec'd handlers stay unchecked.
3
+ * cp/mv `-f`/`--force` is always-on overwrite (Copy-Item/Move-Item -Force); listed so `cp -rf` stays valid. */
4
+ export declare const specs: CommandSpec[];
2
5
  export declare const handlers: Record<string, Handler>;
@@ -1,5 +1,5 @@
1
- import { wordToString } from '../ast.js';
2
- import { parseWords, psStr } from '../registry.js';
1
+ import { FauxnixParseError, wordToString } from '../ast.js';
2
+ import { parseWords, psStr, } from '../registry.js';
3
3
  import { argListExpr, exprOfWord, operandExpr } from '../translator.js';
4
4
  /* ------------------------------------------------------------------ */
5
5
  /* Shared PS snippets */
@@ -124,6 +124,7 @@ const cp = (args) => {
124
124
  const { flags, longs, operandWords } = parseWords(args);
125
125
  const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
126
126
  const verbose = flags.has('v') || longs.has('--verbose');
127
+ const noclobber = flags.has('n') || longs.has('--no-clobber');
127
128
  return [
128
129
  PS_GLOB_FN,
129
130
  '$fx_all = ' + argListExpr(operandWords),
@@ -138,6 +139,7 @@ const cp = (args) => {
138
139
  ' if ($fx_isdir -and ' + (recurse ? '$false' : '$true') + ') { [Console]::Error.WriteLine("cp: -r not specified; omitting directory \'" + $fx_g + "\'"); $script:fx_exit = 1; continue }',
139
140
  ' $fx_target = $fx_dst',
140
141
  ' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
142
+ ' if (' + (noclobber ? '$true' : '$false') + ' -and (Test-Path -LiteralPath $fx_target)) { continue }',
141
143
  ' try {',
142
144
  ' Copy-Item -LiteralPath $fx_g -Destination $fx_target -Recurse:' + (recurse ? '$true' : '$false') + ' -Force',
143
145
  ' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("\'" + $fx_g + "\' -> \'" + $fx_target + "\'") }',
@@ -150,6 +152,7 @@ const cp = (args) => {
150
152
  const mv = (args) => {
151
153
  const { flags, longs, operandWords } = parseWords(args);
152
154
  const verbose = flags.has('v') || longs.has('--verbose');
155
+ const noclobber = flags.has('n') || longs.has('--no-clobber');
153
156
  return [
154
157
  PS_GLOB_FN,
155
158
  '$fx_all = ' + argListExpr(operandWords),
@@ -163,6 +166,7 @@ const mv = (args) => {
163
166
  ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("mv: cannot stat \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
164
167
  ' $fx_target = $fx_dst',
165
168
  ' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
169
+ ' if (' + (noclobber ? '$true' : '$false') + ' -and (Test-Path -LiteralPath $fx_target)) { continue }',
166
170
  ' try {',
167
171
  ' if (Test-Path -LiteralPath $fx_target) { Remove-Item -LiteralPath $fx_target -Recurse -Force }',
168
172
  ' Move-Item -LiteralPath $fx_g -Destination $fx_target -Force',
@@ -177,7 +181,7 @@ const rm = (args) => {
177
181
  const { flags, longs, operandWords } = parseWords(args);
178
182
  const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
179
183
  const force = flags.has('f') || longs.has('--force');
180
- const verbose = flags.has('v');
184
+ const verbose = flags.has('v') || longs.has('--verbose');
181
185
  return [
182
186
  PS_GLOB_FN,
183
187
  '$fx_files = ' + psArray(operandWords),
@@ -233,7 +237,8 @@ const rmdir = (args) => {
233
237
  ].join('\n');
234
238
  };
235
239
  const touch = (args) => {
236
- const { operandWords } = parseWords(args);
240
+ const { flags, longs, operandWords } = parseWords(args);
241
+ const noCreate = flags.has('c') || longs.has('--no-create');
237
242
  return [
238
243
  '$fx_files = ' + psArray(operandWords),
239
244
  "if ($fx_files.Count -eq 0) { [Console]::Error.WriteLine('touch: missing file operand'); $script:fx_exit = 1 }",
@@ -241,6 +246,7 @@ const touch = (args) => {
241
246
  ' if (Test-Path -LiteralPath $fx_f) {',
242
247
  ' try { (Get-Item -LiteralPath $fx_f).LastWriteTime = Get-Date } catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': Permission denied"); $script:fx_exit = 1 }',
243
248
  ' } else {',
249
+ ' if (' + (noCreate ? '$true' : '$false') + ') { continue }',
244
250
  ' try { New-Item -ItemType File -Path $fx_f | Out-Null }',
245
251
  ' catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': No such file or directory"); $script:fx_exit = 1 }',
246
252
  ' }',
@@ -465,15 +471,215 @@ const df = (args) => {
465
471
  '}',
466
472
  ].join('\n');
467
473
  };
468
- /* ------------------------------------------------------------------ */
469
- /* find */
470
- /* ------------------------------------------------------------------ */
474
+ function findFail(msg) {
475
+ throw new FauxnixParseError(msg);
476
+ }
477
+ function canStartFindFactor(t) {
478
+ return (t === '!' ||
479
+ t === '-not' ||
480
+ t === '(' ||
481
+ t === '-name' ||
482
+ t === '-iname' ||
483
+ t === '-type' ||
484
+ t === '-size' ||
485
+ t === '-mtime' ||
486
+ t === '-delete' ||
487
+ t === '-print' ||
488
+ t === '-maxdepth' ||
489
+ t === '-mindepth');
490
+ }
491
+ /** GNU find expression: `!` tightest, juxtaposition/`-a` next, `-o` loosest. */
492
+ function parseFindPreds(preds) {
493
+ const plan = {
494
+ ast: { kind: 'true' },
495
+ maxDepth: null,
496
+ minDepth: null,
497
+ hasDelete: false,
498
+ hasPrint: false,
499
+ hasSize: false,
500
+ };
501
+ let i = 0;
502
+ const peek = () => (i < preds.length ? preds[i] : null);
503
+ const take = () => preds[i++];
504
+ const needArg = (opt) => {
505
+ if (i >= preds.length)
506
+ findFail("find: missing argument to '" + opt + "'");
507
+ return take();
508
+ };
509
+ const parsePrimary = () => {
510
+ const t = take();
511
+ if (t === '-name' || t === '-iname') {
512
+ return { kind: 'name', pat: needArg(t), ci: t === '-iname' };
513
+ }
514
+ if (t === '-type') {
515
+ const v = needArg(t);
516
+ if (v !== 'f' && v !== 'd' && v !== 'l')
517
+ findFail('find: Unknown argument to -type: ' + v);
518
+ return { kind: 'type', t: v };
519
+ }
520
+ if (t === '-size') {
521
+ const v = needArg(t);
522
+ const ps = sizeOf(v);
523
+ if (!ps)
524
+ findFail("find: Invalid argument '" + v + "' to -size");
525
+ plan.hasSize = true;
526
+ return { kind: 'size', ps };
527
+ }
528
+ if (t === '-mtime') {
529
+ const v = needArg(t);
530
+ const ps = mtimeOf(v);
531
+ if (!ps)
532
+ findFail("find: Invalid argument '" + v + "' to -mtime");
533
+ return { kind: 'mtime', ps };
534
+ }
535
+ if (t === '-delete') {
536
+ plan.hasDelete = true;
537
+ return { kind: 'delete' };
538
+ }
539
+ if (t === '-print') {
540
+ plan.hasPrint = true;
541
+ return { kind: 'print' };
542
+ }
543
+ if (t === '-maxdepth' || t === '-mindepth') {
544
+ const v = needArg(t);
545
+ if (!/^\d+$/.test(v)) {
546
+ findFail('find: expected a non-negative decimal integer argument to ' +
547
+ t +
548
+ ", but got '" +
549
+ v +
550
+ "'");
551
+ }
552
+ if (t === '-maxdepth' && plan.maxDepth === null)
553
+ plan.maxDepth = v;
554
+ if (t === '-mindepth' && plan.minDepth === null)
555
+ plan.minDepth = v;
556
+ return { kind: 'true' };
557
+ }
558
+ if (t.startsWith('-'))
559
+ findFail("find: unknown predicate '" + t + "'");
560
+ findFail("find: paths must precede expression: '" + t + "'");
561
+ };
562
+ const parseFactor = () => {
563
+ const t = peek();
564
+ if (t === null)
565
+ findFail('find: expected an expression');
566
+ if (t === '-o' || t === '-or' || t === '-a' || t === '-and') {
567
+ findFail("find: invalid expression; you have used a binary operator '" +
568
+ t +
569
+ "' with nothing before it.");
570
+ }
571
+ if (t === '!' || t === '-not') {
572
+ take();
573
+ if (peek() === null)
574
+ findFail("find: expected an expression after '" + t + "'");
575
+ if (peek() === ')')
576
+ findFail("find: expected an expression between '" + t + "' and ')'");
577
+ return { kind: 'not', inner: parseFactor() };
578
+ }
579
+ if (t === '(') {
580
+ take();
581
+ if (peek() === ')')
582
+ findFail('find: invalid expression; empty parentheses are not allowed.');
583
+ if (peek() === null) {
584
+ findFail("find: invalid expression; expected to find a ')' but didn't see one.");
585
+ }
586
+ const inner = parseOr();
587
+ if (peek() !== ')') {
588
+ findFail("find: invalid expression; expected to find a ')' but didn't see one.");
589
+ }
590
+ take();
591
+ return inner;
592
+ }
593
+ if (t === ')')
594
+ findFail("find: invalid expression; you have too many ')'");
595
+ return parsePrimary();
596
+ };
597
+ const parseAnd = () => {
598
+ let left = parseFactor();
599
+ while (true) {
600
+ const t = peek();
601
+ if (t === null || t === ')' || t === '-o' || t === '-or')
602
+ break;
603
+ if (t === '-a' || t === '-and') {
604
+ take();
605
+ if (peek() === null || peek() === ')') {
606
+ findFail("find: expected an expression after '" + t + "'");
607
+ }
608
+ left = { kind: 'and', left, right: parseFactor() };
609
+ continue;
610
+ }
611
+ if (canStartFindFactor(t)) {
612
+ left = { kind: 'and', left, right: parseFactor() };
613
+ continue;
614
+ }
615
+ findFail("find: paths must precede expression: '" + t + "'");
616
+ }
617
+ return left;
618
+ };
619
+ const parseOr = () => {
620
+ let left = parseAnd();
621
+ while (peek() === '-o' || peek() === '-or') {
622
+ const op = take();
623
+ if (peek() === null || peek() === ')') {
624
+ findFail("find: expected an expression after '" + op + "'");
625
+ }
626
+ left = { kind: 'or', left, right: parseAnd() };
627
+ }
628
+ return left;
629
+ };
630
+ if (preds.length === 0) {
631
+ plan.ast = { kind: 'print' };
632
+ plan.hasPrint = true;
633
+ return plan;
634
+ }
635
+ plan.ast = parseOr();
636
+ if (i < preds.length) {
637
+ if (preds[i] === ')')
638
+ findFail("find: invalid expression; you have too many ')'");
639
+ findFail("find: paths must precede expression: '" + preds[i] + "'");
640
+ }
641
+ if (!plan.hasDelete && !plan.hasPrint) {
642
+ plan.ast = { kind: 'and', left: plan.ast, right: { kind: 'print' } };
643
+ plan.hasPrint = true;
644
+ }
645
+ return plan;
646
+ }
647
+ function emitFind(n) {
648
+ switch (n.kind) {
649
+ case 'true':
650
+ return '$true';
651
+ case 'and':
652
+ return '(' + emitFind(n.left) + ' -and ' + emitFind(n.right) + ')';
653
+ case 'or':
654
+ return '(' + emitFind(n.left) + ' -or ' + emitFind(n.right) + ')';
655
+ case 'not':
656
+ return '(-not ' + emitFind(n.inner) + ')';
657
+ case 'name':
658
+ if (n.ci)
659
+ return "($fx_i.Name.ToLower() -like '" + likeOf(n.pat).toLowerCase() + "')";
660
+ return "($fx_i.Name -clike '" + likeOf(n.pat) + "')";
661
+ case 'type':
662
+ if (n.t === 'f')
663
+ return '(-not $fx_i.PSIsContainer)';
664
+ if (n.t === 'd')
665
+ return '($fx_i.PSIsContainer)';
666
+ return '([bool]$fx_i.LinkType)';
667
+ case 'size':
668
+ return '(' + n.ps + ')';
669
+ case 'mtime':
670
+ return '(' + n.ps + ')';
671
+ case 'delete':
672
+ return '(fx-find-delete)';
673
+ case 'print':
674
+ return '(fx-find-print)';
675
+ }
676
+ }
471
677
  const find = (args) => {
472
678
  const raw = args.map((w) => wordToString(w));
473
679
  let pathEnd = 0;
474
680
  while (pathEnd < raw.length &&
475
681
  !raw[pathEnd].startsWith('-') &&
476
- !['(', ')', '!', '-a', '-o'].includes(raw[pathEnd])) {
682
+ !['(', ')', '!', '-a', '-o', '-not', '-and', '-or'].includes(raw[pathEnd])) {
477
683
  pathEnd++;
478
684
  }
479
685
  const pathWords = args.slice(0, pathEnd);
@@ -483,81 +689,41 @@ const find = (args) => {
483
689
  psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
484
690
  '); $script:fx_exit = 1');
485
691
  }
486
- const namePat = extractValue(preds, ['-name']);
487
- const inamePat = extractValue(preds, ['-iname']);
488
- const typeV = extractValue(preds, ['-type']);
489
- const maxDepthS = extractValue(preds, ['-maxdepth']);
490
- const minDepthS = extractValue(preds, ['-mindepth']);
491
- const sizeExpr = extractValue(preds, ['-size']);
492
- const mtimeExpr = extractValue(preds, ['-mtime']);
493
- const wantDelete = preds.includes('-delete');
692
+ const plan = parseFindPreds(preds);
693
+ const expr = emitFind(plan.ast);
494
694
  const paths = pathWords.length ? argListExpr(pathWords) : "@('.')";
495
- for (const option of ['-maxdepth', '-mindepth']) {
496
- const optionIndex = preds.indexOf(option);
497
- if (optionIndex < 0)
498
- continue;
499
- if (optionIndex + 1 >= preds.length) {
500
- return ('[Console]::Error.WriteLine(' +
501
- psStr(`find: missing argument to '${option}'`) +
502
- '); $script:fx_exit = 1');
503
- }
504
- const value = preds[optionIndex + 1];
505
- if (!/^\d+$/.test(value)) {
506
- return ('[Console]::Error.WriteLine(' +
507
- psStr(`find: expected a non-negative decimal integer argument to ${option}, but got '${value}'`) +
508
- '); $script:fx_exit = 1');
509
- }
510
- }
511
- const conditions = [];
512
- if (namePat !== null)
513
- conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
514
- if (inamePat !== null)
515
- conditions.push("($fx_i.Name.ToLower() -like '" + likeOf(inamePat).toLowerCase() + "')");
516
- if (typeV === 'f')
517
- conditions.push('(-not $fx_i.PSIsContainer)');
518
- if (typeV === 'd')
519
- conditions.push('($fx_i.PSIsContainer)');
520
- if (typeV === 'l')
521
- conditions.push('([bool]$fx_i.LinkType)');
522
- const cond = conditions.length ? conditions.join(' -and ') : '$true';
523
- const sizeCond = sizeOf(sizeExpr);
524
- const mtimeCond = mtimeOf(mtimeExpr);
525
695
  return [
696
+ plan.hasPrint
697
+ ? 'function fx-find-print { $script:fx_find_print = $true; $true }'
698
+ : '',
699
+ plan.hasDelete
700
+ ? 'function fx-find-delete { try { Remove-Item -LiteralPath $fx_i.FullName -Force -ErrorAction SilentlyContinue | Out-Null } catch {}; $true }'
701
+ : '',
526
702
  '$fx_paths = ' + paths,
527
703
  'foreach ($fx_p in $fx_paths) {',
528
704
  ' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
529
705
  ' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
530
706
  ' $fx_all = @(Get-Item -LiteralPath $fx_p -Force)',
531
707
  ' $fx_all += @(Get-ChildItem -LiteralPath $fx_p -Recurse -Force -ErrorAction SilentlyContinue)',
708
+ plan.hasDelete ? ' [array]::Reverse($fx_all)' : '',
532
709
  ' foreach ($fx_i in $fx_all) {',
533
710
  " $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
534
711
  " if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
535
712
  ' $fx_depth = 0; if ($fx_rel -ne \'\') { $fx_depth = 1; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } } }',
536
- ' if ($fx_depth -lt ' + (minDepthS ?? '0') + ') { continue }',
537
- maxDepthS !== null ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
538
- ' if (-not (' + cond + ')) { continue }',
539
- sizeCond ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }' : '',
540
- sizeCond ? ' if (-not (' + sizeCond + ')) { continue }' : '',
541
- mtimeCond ? ' if (-not (' + mtimeCond + ')) { continue }' : '',
542
- ' if (' + (wantDelete ? '$true' : '$false') + ') {',
543
- ' try { Remove-Item -LiteralPath $fx_i.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}',
544
- ' } else {',
545
- ' $fx_disp',
546
- ' }',
713
+ ' if ($fx_depth -lt ' + (plan.minDepth ?? '0') + ') { continue }',
714
+ plan.maxDepth !== null ? ' if ($fx_depth -gt ' + plan.maxDepth + ') { continue }' : '',
715
+ plan.hasSize
716
+ ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }'
717
+ : '',
718
+ plan.hasPrint ? ' $script:fx_find_print = $false' : '',
719
+ ' if (' + expr + ') { }',
720
+ plan.hasPrint ? ' if ($script:fx_find_print) { $fx_disp }' : '',
547
721
  ' }',
548
722
  '}',
549
723
  ]
550
724
  .filter((l) => l !== '')
551
725
  .join('\n');
552
726
  };
553
- function extractValue(preds, names) {
554
- for (const n of names) {
555
- const i = preds.indexOf(n);
556
- if (i >= 0 && i + 1 < preds.length)
557
- return preds[i + 1];
558
- }
559
- return null;
560
- }
561
727
  /** fnmatch glob → PowerShell -like pattern (same semantics for * and ?). */
562
728
  function likeOf(glob) {
563
729
  return glob.replace(/'/g, "''");
@@ -700,15 +866,56 @@ const diff = (args) => {
700
866
  '}',
701
867
  ].join('\n');
702
868
  };
869
+ function opt(short, long, support = 'implemented', extra = {}) {
870
+ const o = { support };
871
+ if (short)
872
+ o.short = short;
873
+ if (long)
874
+ o.long = long;
875
+ return extra.takesValue || extra.reason ? { ...o, ...extra } : o;
876
+ }
877
+ function fileSpec(names, effects, options, handler) {
878
+ return {
879
+ names,
880
+ options,
881
+ effects,
882
+ platform: 'windows-ps51',
883
+ dispatch: 'translated',
884
+ handler,
885
+ };
886
+ }
887
+ const INTERACTIVE = { reason: 'interactive prompt' };
888
+ /** Destructive file commands migrated first (#130). Unspec'd handlers stay unchecked.
889
+ * cp/mv `-f`/`--force` is always-on overwrite (Copy-Item/Move-Item -Force); listed so `cp -rf` stays valid. */
890
+ export const specs = [
891
+ fileSpec(['cp'], ['read', 'write'], [
892
+ opt('r', undefined),
893
+ opt('R', '--recursive'),
894
+ opt('v', '--verbose'),
895
+ opt('n', '--no-clobber'),
896
+ opt('f', '--force'),
897
+ opt('i', '--interactive', 'unsupported', INTERACTIVE),
898
+ ], cp),
899
+ fileSpec(['mv'], ['read', 'write', 'delete'], [
900
+ opt('v', '--verbose'),
901
+ opt('n', '--no-clobber'),
902
+ opt('f', '--force'),
903
+ opt('i', '--interactive', 'unsupported', INTERACTIVE),
904
+ ], mv),
905
+ fileSpec(['rm'], ['delete'], [
906
+ opt('r', undefined),
907
+ opt('R', '--recursive'),
908
+ opt('f', '--force'),
909
+ opt('v', '--verbose'),
910
+ opt('i', '--interactive', 'unsupported', INTERACTIVE),
911
+ ], rm),
912
+ fileSpec(['touch'], ['write'], [opt('c', '--no-create')], touch),
913
+ ];
703
914
  export const handlers = {
704
915
  ls,
705
916
  ll: ls, // common alias
706
- cp,
707
- mv,
708
- rm,
709
917
  mkdir,
710
918
  rmdir,
711
- touch,
712
919
  mktemp,
713
920
  ln,
714
921
  readlink,
@@ -1,7 +1,7 @@
1
- import { registerAll } from '../registry.js';
2
- import { handlers as files } from './files.js';
1
+ import { registerAll, registerSpecs } from '../registry.js';
2
+ import { handlers as files, specs as fileSpecs } from './files.js';
3
3
  import { handlers as textFilters } from './text-filters.js';
4
- import { handlers as textIo } from './text-io.js';
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';
7
7
  import { handlers as archive } from './archive.js';
@@ -13,5 +13,7 @@ export function installAll() {
13
13
  registerAll(sysinfo);
14
14
  registerAll(net);
15
15
  registerAll(archive);
16
+ registerSpecs(fileSpecs);
17
+ registerSpecs(textIoSpecs);
16
18
  }
17
19
  installAll();
@@ -209,8 +209,9 @@ const env = (args, ctx) => {
209
209
  break;
210
210
  }
211
211
  if (t === '-i' || t === '--ignore-environment') {
212
- i++; // best-effort: the flag is accepted and ignored
213
- continue;
212
+ return ('[Console]::Error.WriteLine(' +
213
+ psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets)') +
214
+ '); $script:fx_exit = 2');
214
215
  }
215
216
  if (t === '-u' || t === '--unset') {
216
217
  if (i + 1 < raw.length)
@@ -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>;
@@ -691,8 +691,8 @@ const wc = (args) => {
691
691
  /* tee */
692
692
  /* ------------------------------------------------------------------ */
693
693
  const tee = (args, ctx) => {
694
- const { flags, operandWords } = parseWords(args);
695
- const append = flags.has('a') || flags.has('append');
694
+ const { flags, longs, operandWords } = parseWords(args);
695
+ const append = flags.has('a') || longs.has('--append');
696
696
  return [
697
697
  PS_WRITE_FN,
698
698
  fxTermLine(ctx.position),
@@ -1142,6 +1142,18 @@ const xargs = (args) => {
1142
1142
  ].join('\n');
1143
1143
  };
1144
1144
  /* ------------------------------------------------------------------ */
1145
+ export const specs = [
1146
+ {
1147
+ names: ['tee'],
1148
+ options: [
1149
+ { short: 'a', long: '--append', support: 'implemented' },
1150
+ ],
1151
+ effects: ['read', 'write'],
1152
+ platform: 'windows-ps51',
1153
+ dispatch: 'translated',
1154
+ handler: tee,
1155
+ },
1156
+ ];
1145
1157
  export const handlers = {
1146
1158
  echo,
1147
1159
  printf,
@@ -1149,7 +1161,6 @@ export const handlers = {
1149
1161
  head,
1150
1162
  tail,
1151
1163
  wc,
1152
- tee,
1153
1164
  nl,
1154
1165
  tac,
1155
1166
  md5sum,
@@ -3,14 +3,22 @@ export interface ExecResult {
3
3
  stdout: string;
4
4
  stderr: string;
5
5
  exitCode: number;
6
+ timedOut: boolean;
7
+ cancelled: boolean;
8
+ truncated: boolean;
9
+ spawnError?: 'ENOENT' | 'START';
6
10
  }
7
11
  export interface ExecOptions {
8
12
  timeoutMs?: number;
9
13
  /** Extra environment layered over the session (used by MCP per-call cwd). */
10
14
  cwd?: string;
15
+ signal?: AbortSignal;
16
+ stdoutLimit?: number;
17
+ stderrLimit?: number;
11
18
  }
12
19
  /** Session persists cwd and env across segments, like a real shell. */
13
20
  export declare class FauxnixSession {
21
+ id: string;
14
22
  cwd: string | null;
15
23
  env: Record<string, string>;
16
24
  /** Exit code of the previous segment — powers bash's `$?`. */
@@ -20,14 +28,18 @@ export declare class FauxnixSession {
20
28
  private scriptFile;
21
29
  private hostFile;
22
30
  private host;
23
- private runLock;
31
+ private lifecycleLock;
24
32
  constructor();
25
33
  private bindFiles;
34
+ private withLock;
26
35
  private syncFromDisk;
27
36
  private ensureHost;
28
37
  /** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
29
38
  prewarm(): Promise<void>;
30
39
  dispose(): Promise<void>;
40
+ /** Kill the host and re-prewarm the same session object (no second FauxnixSession). */
41
+ reset(): Promise<void>;
42
+ private disposeUnlocked;
31
43
  /** env for the child powershell process. */
32
44
  childEnv(cwdOverride?: string, stdinFile?: string | null): NodeJS.ProcessEnv;
33
45
  run(plans: SegmentPlan[], opts?: ExecOptions): Promise<ExecResult>;