fauxnix-cli 0.9.3 → 0.12.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
@@ -85,6 +85,20 @@ npm install -g .
85
85
 
86
86
  Requires: Windows with PowerShell 5.1+ (built-in) and Node.js ≥ 18.
87
87
 
88
+ PowerShell 7 is an opt-in, CI-tested tier:
89
+
90
+ ```powershell
91
+ $env:FAUXNIX_PS = 'pwsh'
92
+ fauxnix check # edition: Core
93
+ ```
94
+
95
+ Set the variable before starting `fauxnix` or its MCP harness; restart the
96
+ harness after changing it. Windows PowerShell 5.1 remains the default. Invalid
97
+ values and a missing selected executable fail loudly rather than falling back.
98
+ The default is pinned below `SystemRoot`; `pwsh.exe` is resolved once from
99
+ absolute `PATH` directories, excluding the current working directory.
100
+ See [PowerShell 7 support](docs/powershell-7.md).
101
+
88
102
  ## Quick start
89
103
 
90
104
  ```bash
@@ -98,18 +112,32 @@ fauxnix translate "find . -name '*.log' -mtime +7 -delete"
98
112
 
99
113
  # check your environment
100
114
  fauxnix check
115
+ fauxnix doctor # check + encoding, harness config, MCP
116
+
117
+ # write user-level MCP config (idempotent; also --codex/--opencode/--kimi/--qwen)
118
+ fauxnix install --claude
101
119
 
102
120
  # run the MCP stdio server (what agent harnesses connect to)
103
121
  fauxnix mcp
104
122
  ```
105
123
 
124
+ `translate` only renders a script and does not read command operands. Because
125
+ `sed -f` needs a script file, translate-only mode asks you to use `-e` with
126
+ inline script text; normal command execution continues to support `sed -f`.
127
+
106
128
  Unknown commands (git, node, npm, python, cargo, gh, docker, ...) are **passed through natively**
107
- with argv-style quoting no string re-parsing, no quoting bugs.
129
+ with argv-style quoting. Windows `.cmd`/`.bat` shims necessarily pass through `cmd.exe`; fauxnix
130
+ preserves its supported punctuation and fails loudly for `%`, embedded double quotes, NUL, and
131
+ line breaks rather than passing a different argument.
108
132
 
109
133
  ## Use with your agent harness
110
134
 
111
135
  fauxnix ships an MCP stdio server exposing a `bash` tool (plus `fauxnix_translate` and
112
- `fauxnix_session`). Point any MCP-capable harness at it:
136
+ `fauxnix_session`). Point any MCP-capable harness at it with
137
+ `fauxnix install --claude` (or `--codex` / `--opencode` / `--kimi` / `--qwen`).
138
+ Idempotent; prints what changed. Manual configs below.
139
+
140
+ - **Quickstarts** — copy-paste config + a 10-command smoke: [`docs/examples/`](docs/examples/)
113
141
 
114
142
  **Claude Code**
115
143
  ```bash
@@ -145,12 +173,23 @@ TOML config: `~/.kimi-code/mcp.json`
145
173
  }
146
174
  ```
147
175
 
176
+ **Qwen Code** (`~/.qwen/settings.json`)
177
+ ```bash
178
+ fauxnix install --qwen
179
+ ```
180
+ The installer preserves the rest of `settings.json` and writes an absolute
181
+ Node + package-entry launcher so Qwen startup does not depend on its working
182
+ directory or `PATH` order. See [the Qwen example](docs/examples/qwen.md) for
183
+ the generated JSON shape.
184
+
148
185
  **Any MCP client** — stdio server: `fauxnix mcp`. The tool name is `bash` (override with
149
186
  `FAUXNIX_TOOL_NAME`). Tool description already teaches the model the supported subset, so no
150
187
  system-prompt changes are required.
151
188
 
152
- The MCP session persists `cwd`, environment variables, `export`/`unset` and `cd -`/OLDPWD across
153
- tool calls — it behaves like a logged-in shell, not a stateless `exec`.
189
+ The MCP session persists `cwd`, environment variables, `export`/`unset`, `cd -`/OLDPWD, and
190
+ positional parameters (`set --` / `$1` / `"$@"`) across tool calls — it behaves like a logged-in
191
+ shell, not a stateless `exec`. `$0` is the MCP tool name (`bash` / `FAUXNIX_TOOL_NAME`), not a
192
+ Windows path.
154
193
 
155
194
  ## What's translated
156
195
 
@@ -158,26 +197,30 @@ tool calls — it behaves like a logged-in shell, not a stateless `exec`.
158
197
  development:
159
198
 
160
199
  - **files**: `ls cp mv rm mkdir rmdir touch mktemp ln readlink realpath basename dirname stat file du df find chmod chown diff`
161
- - **text filters**: `grep egrep sed awk sort uniq cut tr` — sed/awk scripts are parsed at
162
- translate time (unsupported constructs throw named errors, never silently misbehave)
200
+ - **text filters**: `grep egrep sed awk sort uniq cut tr` — sed/awk scripts are parsed while
201
+ preparing an executable plan (unsupported constructs throw named errors, never silently
202
+ misbehave); inspect-only `translate` keeps `sed -f` file reads out of that path
163
203
  - **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
164
204
 
165
- `cp` / `mv` / `rm` / `touch` / `du` / `ls` / `ll` / `mkdir` / `rmdir` / `mktemp` / `ln` /
166
- `readlink` / `realpath` / `basename` / `dirname` / `stat` / `file` / `df` / `chmod` / `chown` /
167
- `diff` / `tee` / `grep` / `head` / `echo` / `printf` / `cat` / `tail` / `wc` carry a `CommandSpec`: unknown options fail with a GNU-style
168
- usage error instead of being ignored (`find` stays unspec'd so predicates like `-name` still
169
- compile). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
170
- `head --lines` / `du --max-depth`. `fauxnix list --json` and `docs/command-specs.md` dump the
171
- same metadata.
205
+ The curated **agent-daily 60** carry a `CommandSpec`: unknown options fail with a GNU-style
206
+ usage error instead of being ignored. The generated [`docs/command-specs.md`](docs/command-specs.md)
207
+ is the exact list, coverage count, option table, and exclusion rationale; `fauxnix list --json`
208
+ exposes the same per-command metadata. `find` stays unspec'd so predicates like `-name` still
209
+ compile; `sed`/`awk`/`egrep` keep their command-specific parsers; `tar` remains native to
210
+ `tar.exe` so supported bsdtar options reach the executable. Implemented GNU holes include
211
+ `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` / `head --lines` /
212
+ `du --max-depth` / `env -u` / `ps -f` / `command -V` / `date --date=@SECONDS`.
172
213
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
173
214
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
174
- timeout man history less more source . eval exit alias set`
215
+ timeout man history less more source . eval exit alias set shift`
175
216
  - **network**: `curl wget ping netstat ss ip ifconfig nslookup dig host`
176
217
  - **archives**: `tar gzip gunzip zcat zip unzip`
177
218
 
178
219
  Plus shell syntax: pipes, `&&` / `||` / `;`, redirections (`> >> 2> 2>&1 < &>`, `/dev/null`),
179
- quoting, `$VAR` `$(...)` command substitution, `VAR=x cmd` prefixes, `~` expansion, and
180
- POSIX-style path normalization (`/tmp`, `/d/foo` `D:\foo`).
220
+ quoting, `$VAR` `$1` `$#` `"$@"` `set --` `shift`, `${name:-word}` `${name//pat/str}`
221
+ `${name:off:len}` `${name[n]}` `${#name[@]}`, `A=(x y z)` array assignment, `$(...)` command
222
+ substitution, `VAR=x cmd` prefixes, `~` expansion, and POSIX-style path normalization
223
+ (`/tmp`, `/d/foo` → `D:\foo`).
181
224
 
182
225
  Exit codes follow bash conventions: 0 ok, 1 fail, 2 usage/serious, 127 command not found,
183
226
  124 timeout.
@@ -185,9 +228,9 @@ Exit codes follow bash conventions: 0 ok, 1 fail, 2 usage/serious, 127 command n
185
228
  ## How it works
186
229
 
187
230
  ```
188
- bash command ──parser──▶ AST ──translator──▶ PowerShell script ──executor──▶ powershell.exe
231
+ bash command ──parser──▶ AST ──translator──▶ PowerShell script ──executor──▶ selected PowerShell
189
232
 
190
- agent ◀── GNU-style output, bash-style errors ◀── decoder (UTF-8 GBK fallback) ◀┘
233
+ agent ◀── GNU-style output, bash-style errors ◀── UTF-8 framed host protocol ◀┘
191
234
  ```
192
235
 
193
236
  - **Deterministic translation, zero LLM calls** at runtime.
@@ -195,9 +238,10 @@ agent ◀── GNU-style output, bash-style errors ◀── decoder (UTF-8 →
195
238
  "Fauxnix contract": string-per-line stdout, `[Console]::Error.WriteLine` for bash-style
196
239
  stderr, `$script:fx_exit` for exit codes, `$input` for stdin.
197
240
  - The executor wraps every script with UTF-8 enforcement (`[Console]::OutputEncoding`,
198
- `$OutputEncoding`, `chcp 65001`), decodes output as strict-UTF-8 with a GBK(936) fallback for
199
- legacy native tools, strips CLIXML serialization and PowerShell noise from stderr, and rewrites
200
- common PowerShell errors (including zh-CN locale messages) into bash phrasing.
241
+ `$OutputEncoding`, `chcp 65001`), decodes native output at the process boundary (UTF-8 by
242
+ default or GBK(936) in `ansi` mode), keeps host frames UTF-8, strips CLIXML serialization and
243
+ PowerShell noise from stderr, and rewrites common PowerShell errors (including zh-CN locale
244
+ messages) into bash phrasing.
201
245
  - Scripts run via `-EncodedCommand` (UTF-16LE) and transparently fall back to a temp `.ps1` file
202
246
  when the 32 KB command-line limit would be exceeded.
203
247
 
@@ -211,11 +255,17 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
211
255
  word expansion precedes the temporary environment).
212
256
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
213
257
  an unbounded `yes | head` would hang.
214
- - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
258
+ - `tail -f`, `eval`, `alias`, heredocs,
215
259
  `env -i`/`--ignore-environment`,
216
- and background `&` are rejected with actionable error messages instead of misbehaving.
217
- (`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
218
- dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
260
+ background `&`, and output/fd redirects (`>` `>>` `2>` `2>>` `&>` `&>>`
261
+ `2>&1` `1>&2`) on a non-last pipeline stage (`echo hi >f | cat`) are
262
+ rejected with operation-specific, actionable error messages instead of
263
+ misbehaving. Per-stage `<` remains supported; fully routed per-stage output
264
+ fds are still tracked by #157.
265
+ (`if/then/elif/else/fi`, `for x in ...`, `while`/`until`, `case ... esac` (`;;` only; `;&`/`;;&` fail loud),
266
+ backtick substitution, `command -v`, pipeline `read`,
267
+ dotenv-style `source`, word-level `$((...))` arithmetic expansion, `A=(x y z)` arrays, and
268
+ `${name//pat/str}` / `${name:off:len}` are supported.)
219
269
  - `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
220
270
  exit codes and empty-result semantics match.
221
271
  - `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
@@ -238,13 +288,16 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
238
288
 
239
289
  ## Development
240
290
 
241
- ```bash
291
+ ```powershell
242
292
  npm install
243
293
  npm test # unit + real-PowerShell integration suite (Windows only, auto-skipped elsewhere)
294
+ $env:FAUXNIX_PS = 'pwsh'; npm test # same suite through PowerShell 7
244
295
  npm run build
245
296
  npx tsx scratch/run.mjs "any bash command" # quick live check
246
297
  ```
247
298
 
299
+ 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). The 253-case corpus enforces the RFC C-7 minimum of 200 cases and a 95% identity gate. The weekly oracle runs from `.github/workflows/differential.yml`; two consecutive green **scheduled** runs are still required release evidence after this gate lands.
300
+
248
301
  Architecture map: `src/parser.ts` (bash subset → AST) · `src/translator.ts` (AST → PowerShell +
249
302
  executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence) ·
250
303
  `src/commands/*.ts` (per-command generators) · `src/mcp.ts` (MCP server) · `src/cli.ts`.
@@ -252,6 +305,11 @@ executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence)
252
305
  Roadmap: [docs/rfc-roadmap-to-1.0.md](docs/rfc-roadmap-to-1.0.md) — tracks, milestones,
253
306
  and the RFC process for proposing waves.
254
307
 
308
+ ## Security
309
+
310
+ Trust model, host protocol, kill semantics, network guard, and reporting:
311
+ [SECURITY.md](SECURITY.md).
312
+
255
313
  ## License
256
314
 
257
315
  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,4 @@
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 FAUXNIX_PS=pwsh selects the opt-in PowerShell 7 host (5.1 is the default).\n Unknown commands (git, node, npm, python, cargo, ...) pass through and run natively.";
2
3
  export declare function runCli(argv: string[]): Promise<void>;
4
+ export declare function runCheck(): Promise<boolean>;
package/dist/cli.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { FauxnixSession } from './executor.js';
3
3
  import { parseCommand } from './parser.js';
4
- import { translateCommandList } from './translator.js';
4
+ import { EXECUTE_TRANSLATION, PURE_TRANSLATION, 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';
11
+ import { POWERSHELL_ARGS, powerShellDisplay, powerShellMissingMessage, resolvePowerShell, } from './powershell.js';
9
12
  import './commands/install-all.js';
10
- const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
13
+ export const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
11
14
 
12
15
  Usage:
13
16
  fauxnix "ls -la | head -5" translate + execute a bash-style command
@@ -18,9 +21,12 @@ Usage:
18
21
  fauxnix list --json same list as machine-readable capability metadata
19
22
  fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)
20
23
  fauxnix check verify the local PowerShell environment
24
+ fauxnix doctor check + encoding, harness config, MCP readiness
25
+ fauxnix install --claude|--codex|--opencode|--kimi|--qwen
21
26
  fauxnix --version
22
27
 
23
28
  Notes:
29
+ FAUXNIX_PS=pwsh selects the opt-in PowerShell 7 host (5.1 is the default).
24
30
  Unknown commands (git, node, npm, python, cargo, ...) pass through and run natively.`;
25
31
  export async function runCli(argv) {
26
32
  if (argv.length === 0) {
@@ -52,6 +58,18 @@ export async function runCli(argv) {
52
58
  await runCheck();
53
59
  return;
54
60
  }
61
+ if (verb === 'doctor') {
62
+ await runDoctor();
63
+ return;
64
+ }
65
+ if (verb === 'install') {
66
+ const result = runInstall(rest);
67
+ for (const line of result.lines)
68
+ console.log(line);
69
+ if (!result.ok)
70
+ process.exitCode = 1;
71
+ return;
72
+ }
55
73
  if (verb === 'mcp') {
56
74
  await startMcpServer();
57
75
  return;
@@ -59,7 +77,7 @@ export async function runCli(argv) {
59
77
  if (verb === 'translate') {
60
78
  const cmd = rest.join(' ');
61
79
  const list = parseCommand(cmd);
62
- const plans = translateCommandList(list);
80
+ const plans = translateCommandList(list, PURE_TRANSLATION);
63
81
  console.log(plans.map((p) => p.script).join('\n# ---- next segment ----\n'));
64
82
  return;
65
83
  }
@@ -69,7 +87,7 @@ export async function runCli(argv) {
69
87
  return;
70
88
  }
71
89
  const list = parseCommand(cmd);
72
- const plans = translateCommandList(list);
90
+ const plans = translateCommandList(list, EXECUTE_TRANSLATION);
73
91
  const session = new FauxnixSession();
74
92
  const result = await session.run(plans);
75
93
  if (result.stdout)
@@ -79,28 +97,74 @@ export async function runCli(argv) {
79
97
  await session.dispose();
80
98
  process.exit(result.exitCode);
81
99
  }
82
- const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
83
- async function runCheck() {
84
- console.log('powershell : powershell.exe (Windows built-in)');
85
- const probeCmd = '$PSVersionTable.PSVersion.ToString()';
86
- const probe = spawn('powershell.exe', [...PS_ARGS, '-EncodedCommand', encodeCommand(probeCmd)], {
100
+ async function runDoctor() {
101
+ const checkOk = await runCheck();
102
+ const report = await collectDoctorReport();
103
+ for (const line of report.lines)
104
+ console.log(line);
105
+ if (!checkOk || !report.ok)
106
+ process.exitCode = 1;
107
+ }
108
+ export async function runCheck() {
109
+ const selection = resolvePowerShell();
110
+ console.log('powershell : ' + powerShellDisplay(selection));
111
+ if (selection.error) {
112
+ console.error('status : FAILED');
113
+ console.error(selection.error);
114
+ process.exitCode = 1;
115
+ return false;
116
+ }
117
+ const probeCmd = '[Console]::Out.WriteLine($PSVersionTable.PSVersion.ToString()); ' +
118
+ '[Console]::Out.WriteLine([string]$PSVersionTable.PSEdition)';
119
+ const probe = spawn(selection.executable, [...POWERSHELL_ARGS, '-EncodedCommand', encodeCommand(probeCmd)], {
87
120
  stdio: ['ignore', 'pipe', 'pipe'],
88
121
  windowsHide: true,
89
122
  });
90
- probe.on('error', (e) => {
91
- console.error('status : FAILED to run powershell.exe: ' + e.message);
92
- process.exit(1);
93
- });
94
123
  let out = '';
124
+ let err = '';
95
125
  probe.stdout.on('data', (d) => (out += d.toString('utf8')));
96
- const code = await new Promise((resolve) => probe.on('close', (c) => resolve(c ?? 1)));
97
- if (code === 0) {
98
- console.log('version : ' + out.trim());
99
- console.log('commands : ' + registeredNames().length + ' translated, others pass through');
100
- console.log('status : OK');
126
+ probe.stderr.on('data', (d) => (err += d.toString('utf8')));
127
+ const outcome = await new Promise((resolve) => {
128
+ let settled = false;
129
+ const done = (value) => {
130
+ if (settled)
131
+ return;
132
+ settled = true;
133
+ resolve(value);
134
+ };
135
+ probe.once('error', (error) => done({ code: 127, error }));
136
+ probe.once('close', (code) => done({ code: code ?? 1 }));
137
+ });
138
+ if (outcome.error) {
139
+ console.error('status : FAILED');
140
+ if (outcome.error.code === 'ENOENT') {
141
+ console.error(powerShellMissingMessage(selection).trimEnd());
142
+ }
143
+ else {
144
+ console.error(`fauxnix: failed to start ${selection.executable}: ${outcome.error.message}`);
145
+ }
146
+ process.exitCode = 1;
147
+ return false;
148
+ }
149
+ if (outcome.code !== 0) {
150
+ console.error(`status : FAILED to run ${selection.executable}`);
151
+ if (err.trim())
152
+ console.error(err.trim());
153
+ process.exitCode = 1;
154
+ return false;
101
155
  }
102
- else {
103
- console.error('status : FAILED to run powershell.exe');
156
+ const lines = out.trim().split(/\r?\n/);
157
+ const version = lines[0] ?? '';
158
+ const edition = lines[1] ?? '';
159
+ console.log('version : ' + version);
160
+ console.log('edition : ' + edition);
161
+ if (edition !== selection.expectedEdition) {
162
+ console.error(`status : FAILED: ${selection.executable} reported ${edition || 'no edition'}; ` +
163
+ `expected ${selection.expectedEdition}`);
104
164
  process.exitCode = 1;
165
+ return false;
105
166
  }
167
+ console.log('commands : ' + registeredNames().length + ' translated, others pass through');
168
+ console.log('status : OK');
169
+ return true;
106
170
  }
@@ -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>;