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/README.md CHANGED
@@ -67,7 +67,9 @@ npm install -g fauxnix-cli
67
67
  Or from source:
68
68
 
69
69
  ```bash
70
- git clone https://github.com/20000419/fauxnix && cd fauxnix && npm install -g .
70
+ git clone https://github.com/20000419/fauxnix && cd fauxnix
71
+ npm ci
72
+ npm install -g .
71
73
  ```
72
74
 
73
75
  > npm package name is `fauxnix-cli` (the `fauxnix` name on npm belongs to an
@@ -151,6 +153,10 @@ development:
151
153
  - **text filters**: `grep egrep sed awk sort uniq cut tr` — sed/awk scripts are parsed at
152
154
  translate time (unsupported constructs throw named errors, never silently misbehave)
153
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.
154
160
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
155
161
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
156
162
  timeout man history less more source . eval exit alias set`
@@ -194,6 +200,7 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
194
200
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
195
201
  an unbounded `yes | head` would hang.
196
202
  - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
203
+ `env -i`/`--ignore-environment`,
197
204
  and background `&` are rejected with actionable error messages instead of misbehaving.
198
205
  (`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
199
206
  dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
package/dist/cli.js CHANGED
@@ -2,9 +2,10 @@ 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
+ import { packageVersion } from './version.js';
8
9
  import './commands/install-all.js';
9
10
  const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
10
11
 
@@ -14,6 +15,7 @@ Usage:
14
15
  fauxnix translate "cmd" show the PowerShell translation only
15
16
  fauxnix mcp start the MCP stdio server (for agent harnesses)
16
17
  fauxnix list list translated commands
18
+ fauxnix list --json same list as machine-readable capability metadata
17
19
  fauxnix check verify the local PowerShell environment
18
20
  fauxnix --version
19
21
 
@@ -26,10 +28,14 @@ export async function runCli(argv) {
26
28
  }
27
29
  const [verb, ...rest] = argv;
28
30
  if (verb === '--version' || verb === '-v') {
29
- console.log('fauxnix 0.4.0');
31
+ console.log(`fauxnix ${packageVersion}`);
30
32
  return;
31
33
  }
32
34
  if (verb === 'list') {
35
+ if (rest[0] === '--json') {
36
+ console.log(JSON.stringify(listCommandsJson(), null, 2));
37
+ return;
38
+ }
33
39
  const names = registeredNames();
34
40
  console.log(names.length + ' translated commands:');
35
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,65 +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
- const conditions = [];
496
- if (namePat !== null)
497
- conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
498
- if (inamePat !== null)
499
- conditions.push("($fx_i.Name.ToLower() -like '" + likeOf(inamePat).toLowerCase() + "')");
500
- if (typeV === 'f')
501
- conditions.push('(-not $fx_i.PSIsContainer)');
502
- if (typeV === 'd')
503
- conditions.push('($fx_i.PSIsContainer)');
504
- if (typeV === 'l')
505
- conditions.push('([bool]$fx_i.LinkType)');
506
- const cond = conditions.length ? conditions.join(' -and ') : '$true';
507
- const sizeCond = sizeOf(sizeExpr);
508
- const mtimeCond = mtimeOf(mtimeExpr);
509
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
+ : '',
510
702
  '$fx_paths = ' + paths,
511
703
  'foreach ($fx_p in $fx_paths) {',
512
704
  ' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
513
705
  ' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
514
706
  ' $fx_all = @(Get-Item -LiteralPath $fx_p -Force)',
515
707
  ' $fx_all += @(Get-ChildItem -LiteralPath $fx_p -Recurse -Force -ErrorAction SilentlyContinue)',
708
+ plan.hasDelete ? ' [array]::Reverse($fx_all)' : '',
516
709
  ' foreach ($fx_i in $fx_all) {',
517
710
  " $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
518
711
  " if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
519
- ' $fx_depth = 0; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } }',
520
- ' if ($fx_depth -lt ' + (minDepthS && /^\d+$/.test(minDepthS) ? minDepthS : '0') + ') { continue }',
521
- maxDepthS && /^\d+$/.test(maxDepthS) ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
522
- ' if (-not (' + cond + ')) { continue }',
523
- sizeCond ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }' : '',
524
- sizeCond ? ' if (-not (' + sizeCond + ')) { continue }' : '',
525
- mtimeCond ? ' if (-not (' + mtimeCond + ')) { continue }' : '',
526
- ' if (' + (wantDelete ? '$true' : '$false') + ') {',
527
- ' try { Remove-Item -LiteralPath $fx_i.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}',
528
- ' } else {',
529
- ' $fx_disp',
530
- ' }',
712
+ ' $fx_depth = 0; if ($fx_rel -ne \'\') { $fx_depth = 1; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } } }',
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 }' : '',
531
721
  ' }',
532
722
  '}',
533
723
  ]
534
724
  .filter((l) => l !== '')
535
725
  .join('\n');
536
726
  };
537
- function extractValue(preds, names) {
538
- for (const n of names) {
539
- const i = preds.indexOf(n);
540
- if (i >= 0 && i + 1 < preds.length)
541
- return preds[i + 1];
542
- }
543
- return null;
544
- }
545
727
  /** fnmatch glob → PowerShell -like pattern (same semantics for * and ?). */
546
728
  function likeOf(glob) {
547
729
  return glob.replace(/'/g, "''");
@@ -684,15 +866,56 @@ const diff = (args) => {
684
866
  '}',
685
867
  ].join('\n');
686
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
+ ];
687
914
  export const handlers = {
688
915
  ls,
689
916
  ll: ls, // common alias
690
- cp,
691
- mv,
692
- rm,
693
917
  mkdir,
694
918
  rmdir,
695
- touch,
696
919
  mktemp,
697
920
  ln,
698
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)