fauxnix-cli 0.9.2 → 0.11.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
@@ -58,6 +58,14 @@ Linux command line — file ops, text processing, process management, archives,
58
58
  maps cleanly onto PowerShell + .NET. fauxnix implements that subset faithfully and *fails loudly
59
59
  and helpfully* on what it can't translate, so the agent never gets silently-wrong results.
60
60
 
61
+ Labs now train computer-use agents on fleets of real desktops. Reporting in 2026 (*The
62
+ Information*, widely repeated) has OpenAI buying tens of thousands of Mac mini / Mac Studio
63
+ boxes — no screen, no keyboard — to reinforcement-learn agents that click, edit, test, and
64
+ run bash workflows, and Anthropic renting Mac minis through AWS for the same class of work.
65
+ That scoring environment is macOS. Windows users should not have to install a guest Unix to
66
+ keep up: the agent keeps writing bash; fauxnix makes the Windows box answer like the box the
67
+ agent was trained on. See [`docs/rfc-computer-use-windows.md`](docs/rfc-computer-use-windows.md).
68
+
61
69
  ## Install
62
70
 
63
71
  ```bash
@@ -90,6 +98,10 @@ fauxnix translate "find . -name '*.log' -mtime +7 -delete"
90
98
 
91
99
  # check your environment
92
100
  fauxnix check
101
+ fauxnix doctor # check + encoding, harness config, MCP
102
+
103
+ # write user-level MCP config (idempotent; also --codex/--opencode/--kimi/--qwen)
104
+ fauxnix install --claude
93
105
 
94
106
  # run the MCP stdio server (what agent harnesses connect to)
95
107
  fauxnix mcp
@@ -101,7 +113,11 @@ with argv-style quoting — no string re-parsing, no quoting bugs.
101
113
  ## Use with your agent harness
102
114
 
103
115
  fauxnix ships an MCP stdio server exposing a `bash` tool (plus `fauxnix_translate` and
104
- `fauxnix_session`). Point any MCP-capable harness at it:
116
+ `fauxnix_session`). Point any MCP-capable harness at it with
117
+ `fauxnix install --claude` (or `--codex` / `--opencode` / `--kimi` / `--qwen`).
118
+ Idempotent; prints what changed. Manual configs below.
119
+
120
+ - **Quickstarts** — copy-paste config + a 10-command smoke: [`docs/examples/`](docs/examples/)
105
121
 
106
122
  **Claude Code**
107
123
  ```bash
@@ -137,12 +153,23 @@ TOML config: `~/.kimi-code/mcp.json`
137
153
  }
138
154
  ```
139
155
 
156
+ **Qwen Code** (`~/.qwen/settings.json`)
157
+ ```json
158
+ {
159
+ "mcpServers": {
160
+ "fauxnix": { "command": "fauxnix", "args": ["mcp"] }
161
+ }
162
+ }
163
+ ```
164
+
140
165
  **Any MCP client** — stdio server: `fauxnix mcp`. The tool name is `bash` (override with
141
166
  `FAUXNIX_TOOL_NAME`). Tool description already teaches the model the supported subset, so no
142
167
  system-prompt changes are required.
143
168
 
144
- The MCP session persists `cwd`, environment variables, `export`/`unset` and `cd -`/OLDPWD across
145
- tool calls — it behaves like a logged-in shell, not a stateless `exec`.
169
+ The MCP session persists `cwd`, environment variables, `export`/`unset`, `cd -`/OLDPWD, and
170
+ positional parameters (`set --` / `$1` / `"$@"`) across tool calls — it behaves like a logged-in
171
+ shell, not a stateless `exec`. `$0` is the MCP tool name (`bash` / `FAUXNIX_TOOL_NAME`), not a
172
+ Windows path.
146
173
 
147
174
  ## What's translated
148
175
 
@@ -156,20 +183,24 @@ development:
156
183
 
157
184
  `cp` / `mv` / `rm` / `touch` / `du` / `ls` / `ll` / `mkdir` / `rmdir` / `mktemp` / `ln` /
158
185
  `readlink` / `realpath` / `basename` / `dirname` / `stat` / `file` / `df` / `chmod` / `chown` /
159
- `diff` / `tee` / `grep` / `head` carry a `CommandSpec`: unknown options fail with a GNU-style
186
+ `diff` / `tee` / `grep` / `head` / `echo` / `printf` / `cat` / `tail` / `wc` / `sort` / `uniq` /
187
+ `cut` / `tr` / `gzip` / `gunzip` / `zcat` / `zip` / `unzip` carry a `CommandSpec`: unknown options fail with a GNU-style
160
188
  usage error instead of being ignored (`find` stays unspec'd so predicates like `-name` still
161
- compile). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
189
+ compile; `sed`/`awk`/`egrep` stay unspec'd; `tar` stays unspec'd because it is fx-native to
190
+ `tar.exe` and unknown GNU flags must reach bsdtar). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
162
191
  `head --lines` / `du --max-depth`. `fauxnix list --json` and `docs/command-specs.md` dump the
163
192
  same metadata.
164
193
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
165
194
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
166
- timeout man history less more source . eval exit alias set`
195
+ timeout man history less more source . eval exit alias set shift`
167
196
  - **network**: `curl wget ping netstat ss ip ifconfig nslookup dig host`
168
197
  - **archives**: `tar gzip gunzip zcat zip unzip`
169
198
 
170
199
  Plus shell syntax: pipes, `&&` / `||` / `;`, redirections (`> >> 2> 2>&1 < &>`, `/dev/null`),
171
- quoting, `$VAR` `$(...)` command substitution, `VAR=x cmd` prefixes, `~` expansion, and
172
- POSIX-style path normalization (`/tmp`, `/d/foo` `D:\foo`).
200
+ quoting, `$VAR` `$1` `$#` `"$@"` `set --` `shift`, `${name:-word}` `${name//pat/str}`
201
+ `${name:off:len}` `${name[n]}` `${#name[@]}`, `A=(x y z)` array assignment, `$(...)` command
202
+ substitution, `VAR=x cmd` prefixes, `~` expansion, and POSIX-style path normalization
203
+ (`/tmp`, `/d/foo` → `D:\foo`).
173
204
 
174
205
  Exit codes follow bash conventions: 0 ok, 1 fail, 2 usage/serious, 127 command not found,
175
206
  124 timeout.
@@ -203,11 +234,15 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
203
234
  word expansion precedes the temporary environment).
204
235
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
205
236
  an unbounded `yes | head` would hang.
206
- - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
237
+ - `tail -f`, `eval`, `alias`, heredocs,
207
238
  `env -i`/`--ignore-environment`,
208
- and background `&` are rejected with actionable error messages instead of misbehaving.
209
- (`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
210
- dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
239
+ background `&`, and stdout redirects (`>` `>>` `&>` `&>>`) on a non-last
240
+ pipeline stage (`echo hi >f | cat`) are rejected with actionable error
241
+ messages instead of misbehaving.
242
+ (`if/then/elif/else/fi`, `for x in ...`, `while`/`until`, `case ... esac` (`;;` only; `;&`/`;;&` fail loud),
243
+ backtick substitution, `command -v`, pipeline `read`,
244
+ dotenv-style `source`, word-level `$((...))` arithmetic expansion, `A=(x y z)` arrays, and
245
+ `${name//pat/str}` / `${name:off:len}` are supported.)
211
246
  - `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
212
247
  exit codes and empty-result semantics match.
213
248
  - `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
@@ -237,6 +272,8 @@ npm run build
237
272
  npx tsx scratch/run.mjs "any bash command" # quick live check
238
273
  ```
239
274
 
275
+ Differential vs Git Bash is opt-in (`FAUXNIX_DIFF_ORACLE=1`; skips if unset or `bash.exe` is missing — Git Bash is not required). See [`test/differential/README.md`](test/differential/README.md). This is grown from the 40-case RFC C-7 scaffold, not the 200-case 1.0 gate. The weekly oracle is opt-in via the scheduled workflow (`.github/workflows/differential.yml`) when Git Bash is on the runner.
276
+
240
277
  Architecture map: `src/parser.ts` (bash subset → AST) · `src/translator.ts` (AST → PowerShell +
241
278
  executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence) ·
242
279
  `src/commands/*.ts` (per-command generators) · `src/mcp.ts` (MCP server) · `src/cli.ts`.
@@ -244,6 +281,11 @@ executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence)
244
281
  Roadmap: [docs/rfc-roadmap-to-1.0.md](docs/rfc-roadmap-to-1.0.md) — tracks, milestones,
245
282
  and the RFC process for proposing waves.
246
283
 
284
+ ## Security
285
+
286
+ Trust model, host protocol, kill semantics, network guard, and reporting:
287
+ [SECURITY.md](SECURITY.md).
288
+
247
289
  ## License
248
290
 
249
291
  MIT © 20000419
package/dist/ast.d.ts CHANGED
@@ -6,13 +6,16 @@
6
6
  * - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
- * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
9
+ * - variables: $VAR ${VAR} ${VAR[n]} ${name//pat/str} ${name:off:len}
10
+ * plus special cases ($HOME $USER $PATH $1 $# $@ $0 ...)
10
11
  * - command substitution: $(...) and `...` (recursively translated)
11
12
  * - arithmetic expansion: $((...)) (existing fx-arith engine)
12
13
  * - env assignment prefix: VAR=value cmd
14
+ * - control: if/then/elif/else/fi, for-in, while/until, case
15
+ * - array assignment: A=(x y z) (sidecar FAUXNIX_ARRS)
13
16
  *
14
17
  * Explicitly unsupported (parser throws a helpful FauxnixError):
15
- * heredocs, subshells (...), background &, while/until/case,
18
+ * heredocs, subshells (...), background &,
16
19
  * globs inside quotes, process substitution <(...).
17
20
  */
18
21
  export interface CommandList {
@@ -25,7 +28,7 @@ export interface ListSegment {
25
28
  /** ';' for the first segment, otherwise the operator seen before this one. */
26
29
  op: ';' | '&&' | '||';
27
30
  }
28
- export type ShellCommand = SimpleCommand | IfCommand | ForCommand;
31
+ export type ShellCommand = SimpleCommand | IfCommand | ForCommand | WhileCommand | CaseCommand;
29
32
  export interface Pipeline {
30
33
  kind: 'Pipeline';
31
34
  commands: ShellCommand[];
@@ -44,6 +47,24 @@ export interface ForCommand {
44
47
  body: CommandList;
45
48
  redirects: Redirect[];
46
49
  }
50
+ export interface WhileCommand {
51
+ kind: 'While';
52
+ /** true for `until TEST; do BODY; done`. */
53
+ until: boolean;
54
+ test: CommandList;
55
+ body: CommandList;
56
+ redirects: Redirect[];
57
+ }
58
+ export interface CaseArm {
59
+ patterns: Word[];
60
+ body: CommandList;
61
+ }
62
+ export interface CaseCommand {
63
+ kind: 'Case';
64
+ word: Word;
65
+ arms: CaseArm[];
66
+ redirects: Redirect[];
67
+ }
47
68
  export interface SimpleCommand {
48
69
  kind: 'SimpleCommand';
49
70
  /** `VAR=value` prefixes before the command name. */
@@ -55,7 +76,10 @@ export interface SimpleCommand {
55
76
  }
56
77
  export interface Assignment {
57
78
  name: string;
79
+ /** Scalar value; for arrays, first element (or empty Word) so export paths stay valid. */
58
80
  value: Word;
81
+ /** Set ⇒ bash array assignment `A=(x y z)`. */
82
+ values?: Word[];
59
83
  }
60
84
  export type RedirectOp = '>' | '>>' | '2>' | '2>>' | '&>' | '&>>' | '2>&1' | '1>&2' | '<';
61
85
  export interface Redirect {
@@ -86,6 +110,17 @@ export type WordPart = {
86
110
  };
87
111
  /** `${#name}` / `${#name[@]}` — string/array length expansion. */
88
112
  length?: boolean;
113
+ /** `${name/pat/str}` (first) / `${name//pat/str}` (global). Pattern is a bash glob. */
114
+ replace?: {
115
+ global: boolean;
116
+ pat: string;
117
+ repl: string;
118
+ };
119
+ /** `${name:offset}` / `${name:offset:length}` — scalar substring. */
120
+ slice?: {
121
+ offset: string;
122
+ length?: string;
123
+ };
89
124
  } | {
90
125
  kind: 'CmdSub';
91
126
  cmd: string;
package/dist/ast.js CHANGED
@@ -6,13 +6,16 @@
6
6
  * - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
- * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
9
+ * - variables: $VAR ${VAR} ${VAR[n]} ${name//pat/str} ${name:off:len}
10
+ * plus special cases ($HOME $USER $PATH $1 $# $@ $0 ...)
10
11
  * - command substitution: $(...) and `...` (recursively translated)
11
12
  * - arithmetic expansion: $((...)) (existing fx-arith engine)
12
13
  * - env assignment prefix: VAR=value cmd
14
+ * - control: if/then/elif/else/fi, for-in, while/until, case
15
+ * - array assignment: A=(x y z) (sidecar FAUXNIX_ARRS)
13
16
  *
14
17
  * Explicitly unsupported (parser throws a helpful FauxnixError):
15
- * heredocs, subshells (...), background &, while/until/case,
18
+ * heredocs, subshells (...), background &,
16
19
  * globs inside quotes, process substitution <(...).
17
20
  */
18
21
  export function wordToString(w) {
@@ -37,6 +40,23 @@ function partToString(p) {
37
40
  case 'DoubleQuoted':
38
41
  return p.parts.map(partToString).join('');
39
42
  case 'Var':
43
+ if (p.replace) {
44
+ const sep = p.replace.global ? '//' : '/';
45
+ return `\${${p.name}${sep}${p.replace.pat}/${p.replace.repl}}`;
46
+ }
47
+ if (p.slice) {
48
+ return p.slice.length !== undefined
49
+ ? `\${${p.name}:${p.slice.offset}:${p.slice.length}}`
50
+ : `\${${p.name}:${p.slice.offset}}`;
51
+ }
52
+ if (p.length) {
53
+ return p.index !== undefined
54
+ ? `\${#${p.name}[${p.index}]}`
55
+ : `\${#${p.name}}`;
56
+ }
57
+ if (p.param) {
58
+ return `\${${p.name}${p.param.op}${p.param.word}}`;
59
+ }
40
60
  return p.index !== undefined ? `\${${p.name}[${p.index}]}` : `$${p.name}`;
41
61
  case 'CmdSub':
42
62
  return '$(' + p.cmd + ')';
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import './commands/install-all.js';
2
+ export declare const USAGE = "fauxnix \u2014 run Linux-style commands on Windows via PowerShell translation\n\nUsage:\n fauxnix \"ls -la | head -5\" translate + execute a bash-style command\n fauxnix -c \"cmd\" same as above\n fauxnix translate \"cmd\" show the PowerShell translation only\n fauxnix mcp start the MCP stdio server (for agent harnesses)\n fauxnix list list translated commands\n fauxnix list --json same list as machine-readable capability metadata\n fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)\n fauxnix check verify the local PowerShell environment\n fauxnix doctor check + encoding, harness config, MCP readiness\n fauxnix install --claude|--codex|--opencode|--kimi|--qwen\n fauxnix --version\n\nNotes:\n Unknown commands (git, node, npm, python, cargo, ...) pass through and run natively.";
2
3
  export declare function runCli(argv: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -5,9 +5,11 @@ import { translateCommandList } from './translator.js';
5
5
  import { listCommandsJson, registeredNames, specsMarkdown } from './registry.js';
6
6
  import { encodeCommand } from './encoding.js';
7
7
  import { startMcpServer } from './mcp.js';
8
+ import { collectDoctorReport } from './doctor.js';
9
+ import { runInstall } from './install.js';
8
10
  import { packageVersion } from './version.js';
9
11
  import './commands/install-all.js';
10
- const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
12
+ export const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
11
13
 
12
14
  Usage:
13
15
  fauxnix "ls -la | head -5" translate + execute a bash-style command
@@ -18,6 +20,8 @@ Usage:
18
20
  fauxnix list --json same list as machine-readable capability metadata
19
21
  fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)
20
22
  fauxnix check verify the local PowerShell environment
23
+ fauxnix doctor check + encoding, harness config, MCP readiness
24
+ fauxnix install --claude|--codex|--opencode|--kimi|--qwen
21
25
  fauxnix --version
22
26
 
23
27
  Notes:
@@ -52,6 +56,18 @@ export async function runCli(argv) {
52
56
  await runCheck();
53
57
  return;
54
58
  }
59
+ if (verb === 'doctor') {
60
+ await runDoctor();
61
+ return;
62
+ }
63
+ if (verb === 'install') {
64
+ const result = runInstall(rest);
65
+ for (const line of result.lines)
66
+ console.log(line);
67
+ if (!result.ok)
68
+ process.exitCode = 1;
69
+ return;
70
+ }
55
71
  if (verb === 'mcp') {
56
72
  await startMcpServer();
57
73
  return;
@@ -80,6 +96,14 @@ export async function runCli(argv) {
80
96
  process.exit(result.exitCode);
81
97
  }
82
98
  const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
99
+ async function runDoctor() {
100
+ await runCheck();
101
+ const report = await collectDoctorReport();
102
+ for (const line of report.lines)
103
+ console.log(line);
104
+ if (!report.ok)
105
+ process.exitCode = 1;
106
+ }
83
107
  async function runCheck() {
84
108
  console.log('powershell : powershell.exe (Windows built-in)');
85
109
  const probeCmd = '$PSVersionTable.PSVersion.ToString()';
@@ -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>;
@@ -201,6 +201,8 @@ const gunzip = (args, ctx) => gzBlock(args, ctx, { decompress: true });
201
201
  const zcat = (args, ctx) => gzBlock(args, ctx, { decompress: true, stdout: true });
202
202
  /* ------------------------------------------------------------------ */
203
203
  /* tar — Windows 10+ ships bsdtar as tar.exe, pass everything through */
204
+ /* Intentionally unspec'd: registerSpec fail-loud would reject GNU */
205
+ /* flags bsdtar accepts (--numeric-owner, …). Same class as find. */
204
206
  /* ------------------------------------------------------------------ */
205
207
  const tar = (args) => {
206
208
  return [
@@ -214,8 +216,9 @@ const tar = (args) => {
214
216
  ' if ($fx_c) { $fx_tar = $fx_c.Source } else { $fx_tar = $null }',
215
217
  '}',
216
218
  'if ($fx_tar) {',
217
- ' & $fx_tar @($fx_args) | ForEach-Object { [string]$_ }',
218
- ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
219
+ // fx-native captures stdout as pipeline strings and sets fx_exit.
220
+ // [object[]]@(...) keeps an empty argv from unwrapping to $null on PS 5.1.
221
+ ' fx-native $fx_tar ([object[]]@($fx_args))',
219
222
  '} else {',
220
223
  " [Console]::Error.WriteLine('tar: fauxnix: tar.exe not found (Windows 10+ ships bsdtar as tar.exe)')",
221
224
  ' $script:fx_exit = 1',
@@ -373,8 +376,87 @@ const unzip = (args) => {
373
376
  ].join('\n');
374
377
  };
375
378
  /* ------------------------------------------------------------------ */
376
- /* exports */
379
+ /* CommandSpec (C-5 archive slice) */
380
+ /* tar stays unspec'd: fx-native to tar.exe; unknown GNU flags must */
381
+ /* reach bsdtar. gzip -f is a no-op today and the stdin-from-terminal */
382
+ /* message advertises it, so it is unsupported (force from terminal) */
383
+ /* rather than a silent ignore. */
377
384
  /* ------------------------------------------------------------------ */
385
+ const gzipOptions = [
386
+ { short: 'd', long: '--decompress', support: 'implemented' },
387
+ { long: '--uncompress', support: 'implemented' },
388
+ { short: 'k', long: '--keep', support: 'implemented' },
389
+ { short: 'c', long: '--stdout', support: 'implemented' },
390
+ { long: '--to-stdout', support: 'implemented' },
391
+ { short: 't', long: '--test', support: 'implemented' },
392
+ { short: '1', long: '--fast', support: 'implemented' },
393
+ { short: '2', support: 'implemented' },
394
+ { short: '3', support: 'implemented' },
395
+ { short: '4', support: 'implemented' },
396
+ { short: '5', support: 'implemented' },
397
+ { short: '6', support: 'implemented' },
398
+ { short: '7', support: 'implemented' },
399
+ { short: '8', support: 'implemented' },
400
+ { short: '9', long: '--best', support: 'implemented' },
401
+ { short: 'f', long: '--force', support: 'unsupported', reason: 'force from terminal' },
402
+ { short: 'q', long: '--quiet', support: 'unsupported', reason: 'quiet' },
403
+ { short: 'v', long: '--verbose', support: 'unsupported', reason: 'verbose' },
404
+ { short: 'n', long: '--no-name', support: 'unsupported', reason: 'no-name' },
405
+ { short: 'r', long: '--recursive', support: 'unsupported', reason: 'recursive' },
406
+ { short: 'S', long: '--suffix', takesValue: true, support: 'unsupported', reason: 'suffix' },
407
+ ];
408
+ export const specs = [
409
+ {
410
+ names: ['gzip'],
411
+ options: gzipOptions,
412
+ effects: ['read', 'write', 'delete'],
413
+ platform: 'windows-ps51',
414
+ dispatch: 'translated',
415
+ handler: gzip,
416
+ },
417
+ {
418
+ names: ['gunzip'],
419
+ options: gzipOptions,
420
+ effects: ['read', 'write', 'delete'],
421
+ platform: 'windows-ps51',
422
+ dispatch: 'translated',
423
+ handler: gunzip,
424
+ },
425
+ {
426
+ names: ['zcat'],
427
+ options: gzipOptions,
428
+ effects: ['read'],
429
+ platform: 'windows-ps51',
430
+ dispatch: 'translated',
431
+ handler: zcat,
432
+ },
433
+ {
434
+ names: ['zip'],
435
+ options: [
436
+ // Compress-Archive always recurses directory inputs; -q is already quiet.
437
+ { short: 'r', support: 'implemented' },
438
+ { short: 'q', support: 'implemented' },
439
+ { short: 'x', long: '--exclude', takesValue: true, support: 'unsupported', reason: 'exclude patterns' },
440
+ ],
441
+ effects: ['read', 'write'],
442
+ platform: 'windows-ps51',
443
+ dispatch: 'translated',
444
+ handler: zip,
445
+ },
446
+ {
447
+ names: ['unzip'],
448
+ options: [
449
+ { short: 'l', support: 'implemented' },
450
+ { short: 'o', support: 'implemented' },
451
+ { short: 'q', support: 'implemented' },
452
+ { short: 'd', long: '--directory', takesValue: true, support: 'implemented' },
453
+ ],
454
+ effects: ['read', 'write'],
455
+ platform: 'windows-ps51',
456
+ dispatch: 'translated',
457
+ handler: unzip,
458
+ },
459
+ ];
378
460
  export const handlers = {
379
461
  tar,
380
462
  gzip,
@@ -55,7 +55,7 @@ function psIsLink(it) {
55
55
  /* ls */
56
56
  /* ------------------------------------------------------------------ */
57
57
  const ls = (args) => {
58
- const { flags, longs, values, operandWords } = parseWords(args, [], ['--format']);
58
+ const { flags, longs, values, operandWords } = parseWords(args, [], ['--format', '--color']);
59
59
  const long = flags.has('l') || longs.has('--long') || values.get('--format') === 'long';
60
60
  const all = flags.has('a') || longs.has('--all');
61
61
  const almost = flags.has('A') || longs.has('--almost-all');
@@ -725,7 +725,7 @@ const find = (args) => {
725
725
  const preds = raw.slice(pathEnd);
726
726
  if (preds.includes('-exec') || preds.includes('-execdir')) {
727
727
  return ('[Console]::Error.WriteLine(' +
728
- psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
728
+ psStr('find: -exec is not supported by fauxnix; use `find . -name "*.log" -delete` or grep -r instead') +
729
729
  '); $script:fx_exit = 1');
730
730
  }
731
731
  const plan = parseFindPreds(preds);
@@ -967,6 +967,8 @@ export const specs = [
967
967
  opt('S', undefined),
968
968
  opt('r', undefined),
969
969
  opt('R', '--recursive', 'unsupported', { reason: 'recursive listing' }),
970
+ // GNU optional WHEN; takesValue so --color=auto is valid. No ANSI.
971
+ opt(undefined, '--color', 'implemented', { takesValue: true }),
970
972
  ], ls),
971
973
  fileSpec(['mkdir'], ['write'], [opt('p', '--parents'), opt('v', '--verbose')], mkdir),
972
974
  fileSpec(['rmdir'], ['delete'], [], rmdir),
@@ -4,7 +4,7 @@ import { handlers as textFilters, specs as textFilterSpecs } from './text-filter
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';
7
- import { handlers as archive } from './archive.js';
7
+ import { handlers as archive, specs as archiveSpecs } from './archive.js';
8
8
  /** Register every built-in Linux command translator. Side-effectful import. */
9
9
  export function installAll() {
10
10
  registerAll(files);
@@ -16,5 +16,6 @@ export function installAll() {
16
16
  registerSpecs(fileSpecs);
17
17
  registerSpecs(textIoSpecs);
18
18
  registerSpecs(textFilterSpecs);
19
+ registerSpecs(archiveSpecs);
19
20
  }
20
21
  installAll();
@@ -79,10 +79,9 @@ function synthWord(text) {
79
79
  }
80
80
  /** A native-exe invocation obeying the fauxnix contract (string lines + exit code). */
81
81
  function nativeCall(exe, argArray) {
82
- return [
83
- '& ' + psStr(exe) + ' @(' + argArray + ') | ForEach-Object { [string]$_ }',
84
- 'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
85
- ].join('\n');
82
+ // fx-native captures stdout as pipeline strings and sets fx_exit.
83
+ // [object[]]@(...) keeps an empty argv from unwrapping to $null on PS 5.1.
84
+ return 'fx-native ' + psStr(exe) + ' ([object[]]@(' + argArray + '))';
86
85
  }
87
86
  /* ------------------------------------------------------------------ */
88
87
  /* curl */
@@ -1,6 +1,6 @@
1
1
  import { FauxnixParseError, isUnquotedLiteral, wordToString } from '../ast.js';
2
2
  import { lookup, parseWords, psStr, registeredNames } from '../registry.js';
3
- import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, arithExpr, setArithHelperPreamble, } from '../translator.js';
3
+ import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, varExtraOf, arithExpr, setArithHelperPreamble, isSpecialShellVar, } from '../translator.js';
4
4
  import { handlers as textIoHandlers } from './text-io.js';
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Shared TS helpers */
@@ -210,7 +210,7 @@ const env = (args, ctx) => {
210
210
  }
211
211
  if (t === '-i' || t === '--ignore-environment') {
212
212
  return ('[Console]::Error.WriteLine(' +
213
- psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets)') +
213
+ psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets). Use env -u NAME or unset first instead.') +
214
214
  '); $script:fx_exit = 2');
215
215
  }
216
216
  if (t === '-u' || t === '--unset') {
@@ -1398,6 +1398,10 @@ function kshExprOfWord(w) {
1398
1398
  return arithExpr(expanded[0].parts);
1399
1399
  }
1400
1400
  if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
1401
+ const extra = varExtraOf(expanded[0]);
1402
+ if (extra && (extra.replace || extra.slice)) {
1403
+ return varExpr(expanded[0].name, expanded[0].index, expanded[0].param, expanded[0].length === true, extra);
1404
+ }
1401
1405
  if (expanded[0].param) {
1402
1406
  return paramExpr(expanded[0].name, expanded[0].param.op, expanded[0].param.word);
1403
1407
  }
@@ -1407,6 +1411,8 @@ function kshExprOfWord(w) {
1407
1411
  if (expanded[0].index !== undefined) {
1408
1412
  return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
1409
1413
  }
1414
+ if (isSpecialShellVar(expanded[0].name))
1415
+ return varExpr(expanded[0].name);
1410
1416
  return '(fx-envget ' + psStr(expanded[0].name) + ')';
1411
1417
  }
1412
1418
  const literal = !tilde && expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
@@ -1427,14 +1433,28 @@ function kshExprOfWord(w) {
1427
1433
  for (const q of p.parts)
1428
1434
  emitPart(q);
1429
1435
  break;
1430
- case 'Var':
1431
- out +=
1432
- p.param
1433
- ? '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')'
1434
- : p.index !== undefined
1435
- ? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
1436
- : '$(fx-envget ' + psStr(p.name) + ')';
1436
+ case 'Var': {
1437
+ const extra = varExtraOf(p);
1438
+ if (extra && (extra.replace || extra.slice)) {
1439
+ out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, extra) + ')';
1440
+ }
1441
+ else if (p.param) {
1442
+ out += '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')';
1443
+ }
1444
+ else if (p.length) {
1445
+ out += '$(' + varExpr(p.name, p.index, undefined, true) + ')';
1446
+ }
1447
+ else if (p.index !== undefined) {
1448
+ out += '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')';
1449
+ }
1450
+ else if (isSpecialShellVar(p.name)) {
1451
+ out += '$(' + varExpr(p.name) + ')';
1452
+ }
1453
+ else {
1454
+ out += '$(fx-envget ' + psStr(p.name) + ')';
1455
+ }
1437
1456
  break;
1457
+ }
1438
1458
  case 'CmdSub':
1439
1459
  // [[ ]] does not IFS-split, so keep the newline contract.
1440
1460
  out += '$(' + translateCmdSub(p.cmd, true) + ')';
@@ -2478,7 +2498,7 @@ const source = (args) => {
2478
2498
  ].join('\n');
2479
2499
  };
2480
2500
  const evalCmd = () => {
2481
- return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself'); $script:fx_exit = 1");
2501
+ return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself instead'); $script:fx_exit = 1");
2482
2502
  };
2483
2503
  const exitCmd = (args) => {
2484
2504
  const w = args[0];
@@ -2510,9 +2530,15 @@ const alias = (args) => {
2510
2530
  });
2511
2531
  if (!has)
2512
2532
  return '';
2513
- return "[Console]::Error.WriteLine('fauxnix: alias is not supported'); $script:fx_exit = 1";
2533
+ return "[Console]::Error.WriteLine('fauxnix: alias is not supported. Invoke the real command instead.'); $script:fx_exit = 1";
2514
2534
  };
2515
2535
  const set = (args) => {
2536
+ if (args.length > 0 && wordToString(args[0]) === '--') {
2537
+ return [
2538
+ '$fx_pv = [object[]](' + argListExpr(args.slice(1)) + ')',
2539
+ 'fx-posset $fx_pv',
2540
+ ].join('\n');
2541
+ }
2516
2542
  const raw = args.map(wordToString);
2517
2543
  const unsupported = raw.filter((t) => t === '-e' ||
2518
2544
  t === '-u' ||
@@ -2529,11 +2555,20 @@ const set = (args) => {
2529
2555
  /^-.*[eux]/.test(t));
2530
2556
  if (unsupported.length > 0) {
2531
2557
  return ('[Console]::Error.WriteLine(' +
2532
- psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit') +
2558
+ psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit instead') +
2533
2559
  '); $script:fx_exit = 2');
2534
2560
  }
2535
2561
  return '';
2536
2562
  };
2563
+ const shiftCmd = (args) => {
2564
+ if (args.length === 0)
2565
+ return 'fx-posshift 1';
2566
+ return [
2567
+ '$fx_sn = [string](' + exprOfWord(args[0]) + ')',
2568
+ "if ($fx_sn -notmatch '^-?[0-9]+$') { [Console]::Error.WriteLine('bash: shift: ' + $fx_sn + ': numeric argument required'); $script:fx_exit = 1 }",
2569
+ 'else { fx-posshift $fx_sn }',
2570
+ ].join('\n');
2571
+ };
2537
2572
  /* ------------------------------------------------------------------ */
2538
2573
  export const handlers = {
2539
2574
  cd,
@@ -2582,4 +2617,5 @@ export const handlers = {
2582
2617
  exit: exitCmd,
2583
2618
  alias,
2584
2619
  set,
2620
+ shift: shiftCmd,
2585
2621
  };
@@ -1,3 +1,4 @@
1
1
  import { CommandSpec, Handler } from '../registry.js';
2
2
  export declare const specs: CommandSpec[];
3
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
3
4
  export declare const handlers: Record<string, Handler>;