fauxnix-cli 0.4.2 → 0.5.1

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
@@ -193,8 +193,13 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
193
193
  word expansion precedes the temporary environment).
194
194
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
195
195
  an unbounded `yes | head` would hang.
196
- - `tail -f`, `source`, `eval`, `alias`, heredocs, backticks, shell control flow (`if`/`for`/`while`)
196
+ - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`, word-level
197
+ `$((...))` arithmetic expansion
197
198
  and background `&` are rejected with actionable error messages instead of misbehaving.
199
+ (`if/then/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
200
+ and dotenv-style `source` are supported.)
201
+ - `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
202
+ exit codes and empty-result semantics match.
198
203
  - `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
199
204
  (as in Git Bash).
200
205
  - `ps aux` columns are approximations (no per-process CPU% accounting, USER shows `?`).
package/dist/ast.d.ts CHANGED
@@ -7,12 +7,13 @@
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
9
  * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
10
- * - command substitution: $(...) (recursively translated)
10
+ * - command substitution: $(...) and `...` (recursively translated)
11
11
  * - env assignment prefix: VAR=value cmd
12
12
  *
13
13
  * Explicitly unsupported (parser throws a helpful FauxnixError):
14
- * heredocs, backticks, subshells (...), background &, control flow
15
- * (if/for/while), globs inside quotes, process substitution <(...).
14
+ * heredocs, subshells (...), background &, while/until/case,
15
+ * word-level $((...)) arithmetic expansion, globs inside quotes,
16
+ * process substitution <(...).
16
17
  */
17
18
  export interface CommandList {
18
19
  kind: 'CommandList';
@@ -24,9 +25,24 @@ export interface ListSegment {
24
25
  /** ';' for the first segment, otherwise the operator seen before this one. */
25
26
  op: ';' | '&&' | '||';
26
27
  }
28
+ export type ShellCommand = SimpleCommand | IfCommand | ForCommand;
27
29
  export interface Pipeline {
28
30
  kind: 'Pipeline';
29
- commands: SimpleCommand[];
31
+ commands: ShellCommand[];
32
+ }
33
+ export interface IfCommand {
34
+ kind: 'If';
35
+ test: CommandList;
36
+ then: CommandList;
37
+ else?: CommandList;
38
+ redirects: Redirect[];
39
+ }
40
+ export interface ForCommand {
41
+ kind: 'For';
42
+ name: string;
43
+ words: Word[];
44
+ body: CommandList;
45
+ redirects: Redirect[];
30
46
  }
31
47
  export interface SimpleCommand {
32
48
  kind: 'SimpleCommand';
@@ -63,6 +79,13 @@ export type WordPart = {
63
79
  kind: 'Var';
64
80
  name: string;
65
81
  index?: string;
82
+ /** `${name:-word}` / `${name:+word}` / `${name:?word}` (and non-colon). */
83
+ param?: {
84
+ op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?';
85
+ word: string;
86
+ };
87
+ /** `${#name}` / `${#name[@]}` — string/array length expansion. */
88
+ length?: boolean;
66
89
  } | {
67
90
  kind: 'CmdSub';
68
91
  cmd: string;
package/dist/ast.js CHANGED
@@ -7,12 +7,13 @@
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
9
  * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
10
- * - command substitution: $(...) (recursively translated)
10
+ * - command substitution: $(...) and `...` (recursively translated)
11
11
  * - env assignment prefix: VAR=value cmd
12
12
  *
13
13
  * Explicitly unsupported (parser throws a helpful FauxnixError):
14
- * heredocs, backticks, subshells (...), background &, control flow
15
- * (if/for/while), globs inside quotes, process substitution <(...).
14
+ * heredocs, subshells (...), background &, while/until/case,
15
+ * word-level $((...)) arithmetic expansion, globs inside quotes,
16
+ * process substitution <(...).
16
17
  */
17
18
  export function wordToString(w) {
18
19
  return w.map(partToString).join('');
@@ -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, } from '../translator.js';
3
+ import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, } from '../translator.js';
4
4
  import { handlers as textIoHandlers } from './text-io.js';
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Shared TS helpers */
@@ -545,6 +545,55 @@ const type = (args) => {
545
545
  '}',
546
546
  ].join('\n');
547
547
  };
548
+ /** `command -v name` (and bare `command name args` as a no-alias run). */
549
+ const commandCmd = (args, ctx) => {
550
+ let i = 0;
551
+ let identify = false;
552
+ while (i < args.length) {
553
+ const t = wordToString(args[i]);
554
+ if (t === '-v' || t === '-V') {
555
+ identify = true;
556
+ i++;
557
+ continue;
558
+ }
559
+ if (t === '--') {
560
+ i++;
561
+ break;
562
+ }
563
+ if (t.startsWith('-')) {
564
+ i++;
565
+ continue;
566
+ }
567
+ break;
568
+ }
569
+ const rest = args.slice(i);
570
+ if (identify) {
571
+ if (rest.length === 0)
572
+ return '';
573
+ return [
574
+ PS_WHICH_FN,
575
+ '$fx_b = @(' + builtinNames().map(psStr).join(', ') + ')',
576
+ '$fx_ns = ' + textArgs(rest),
577
+ 'foreach ($fx_n in $fx_ns) {',
578
+ " if ($fx_b -contains $fx_n) { '/usr/bin/' + $fx_n }",
579
+ ' else {',
580
+ ' $fx_w = fx-which $fx_n',
581
+ " if ($fx_w -eq '') { $script:fx_exit = 1 }",
582
+ " else { $fx_w.Replace('\\', '/') }",
583
+ ' }',
584
+ '}',
585
+ ].join('\n');
586
+ }
587
+ if (rest.length === 0)
588
+ return '';
589
+ return translateSimple({
590
+ kind: 'SimpleCommand',
591
+ assignments: [],
592
+ name: rest[0],
593
+ args: rest.slice(1),
594
+ redirects: [],
595
+ }, ctx.position, ctx.hasStdin);
596
+ };
548
597
  /* ------------------------------------------------------------------ */
549
598
  /* whoami / id / groups */
550
599
  /* ------------------------------------------------------------------ */
@@ -1344,6 +1393,12 @@ function kshExprOfWord(w) {
1344
1393
  if (tilde && expanded.length === 0)
1345
1394
  return '(fx-home)';
1346
1395
  if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
1396
+ if (expanded[0].param) {
1397
+ return paramExpr(expanded[0].name, expanded[0].param.op, expanded[0].param.word);
1398
+ }
1399
+ if (expanded[0].length) {
1400
+ return varExpr(expanded[0].name, expanded[0].index, undefined, true);
1401
+ }
1347
1402
  if (expanded[0].index !== undefined) {
1348
1403
  return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
1349
1404
  }
@@ -1369,9 +1424,11 @@ function kshExprOfWord(w) {
1369
1424
  break;
1370
1425
  case 'Var':
1371
1426
  out +=
1372
- p.index !== undefined
1373
- ? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
1374
- : '$(fx-envget ' + psStr(p.name) + ')';
1427
+ p.param
1428
+ ? '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')'
1429
+ : p.index !== undefined
1430
+ ? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
1431
+ : '$(fx-envget ' + psStr(p.name) + ')';
1375
1432
  break;
1376
1433
  case 'CmdSub':
1377
1434
  // [[ ]] does not IFS-split, so keep the newline contract.
@@ -2310,12 +2367,107 @@ const less = (args, ctx) => {
2310
2367
  return cat(files, ctx);
2311
2368
  };
2312
2369
  /* ------------------------------------------------------------------ */
2370
+ /* read */
2371
+ /* ------------------------------------------------------------------ */
2372
+ const readCmd = (args, ctx) => {
2373
+ const names = [];
2374
+ for (const w of args) {
2375
+ const t = wordToString(w);
2376
+ if (t === '-r' || t === '-a' || t.startsWith('-'))
2377
+ continue;
2378
+ if (!NAME_RE.test(t)) {
2379
+ return ('[Console]::Error.WriteLine(' +
2380
+ psStr('bash: read: `' + t + "': not a valid identifier") +
2381
+ '); $script:fx_exit = 2');
2382
+ }
2383
+ names.push(t);
2384
+ }
2385
+ if (names.length === 0)
2386
+ names.push('REPLY');
2387
+ const first = names[0];
2388
+ return [
2389
+ ctx.hasStdin
2390
+ ? "$fx_line = [string]((@($input) | Select-Object -First 1))"
2391
+ : "$fx_line = ''",
2392
+ ctx.hasStdin ? '' : '$script:fx_exit = 1',
2393
+ names.length === 1
2394
+ ? [
2395
+ "Set-Item -LiteralPath ('Env:\\' + " + psStr(first) + ') -Value $fx_line',
2396
+ " $n = " + psStr(first),
2397
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
2398
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
2399
+ ' fx-arrdrop $n',
2400
+ ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
2401
+ ' $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_line)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
2402
+ ].join('\n')
2403
+ : [
2404
+ "$fx_fs = @($fx_line -split '\\s+', " + names.length + ')',
2405
+ 'foreach ($fx_i in 0..' + (names.length - 1) + ') {',
2406
+ ' $n = @(' + names.map(psStr).join(',') + ')[$fx_i]',
2407
+ " $fx_v = $(if ($fx_i -lt $fx_fs.Count) { [string]$fx_fs[$fx_i] } else { '' })",
2408
+ " Set-Item -LiteralPath ('Env:\\' + $n) -Value $fx_v",
2409
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
2410
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
2411
+ ' fx-arrdrop $n',
2412
+ ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
2413
+ ' $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_v)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
2414
+ '}',
2415
+ ].join('\n'),
2416
+ ]
2417
+ .filter((l) => l !== '')
2418
+ .join('\n');
2419
+ };
2420
+ /* ------------------------------------------------------------------ */
2313
2421
  /* source / . / eval / exit / alias / set */
2314
2422
  /* ------------------------------------------------------------------ */
2315
- const source = () => {
2316
- return ('[Console]::Error.WriteLine(' +
2317
- psStr('fauxnix: source/. requires a persistent shell; fauxnix persists cwd and env across calls but not shell functions') +
2318
- '); $script:fx_exit = 1');
2423
+ const source = (args) => {
2424
+ const files = stripFlags(args);
2425
+ if (files.length === 0) {
2426
+ return ('[Console]::Error.WriteLine(' +
2427
+ psStr('bash: source: filename argument required') +
2428
+ '); $script:fx_exit = 2');
2429
+ }
2430
+ const q = (c) => '[char]' + c.charCodeAt(0);
2431
+ return [
2432
+ '$fx_files = ' + argListExpr(files, operandExpr),
2433
+ 'foreach ($fx_f in $fx_files) {',
2434
+ ' if (-not (Test-Path -LiteralPath $fx_f -PathType Leaf)) {',
2435
+ " [Console]::Error.WriteLine('bash: source: ' + $fx_f + ': No such file or directory'); $script:fx_exit = 1; continue",
2436
+ ' }',
2437
+ ' $fx_raw = [IO.File]::ReadAllText($fx_f) -replace ([string][char]13 + [string][char]10), [string][char]10',
2438
+ ' foreach ($fx_line in @($fx_raw -split [string][char]10)) {',
2439
+ ' $fx_t = $fx_line.Trim()',
2440
+ " if ($fx_t -eq '' -or $fx_t.StartsWith([string][char]35)) { continue }",
2441
+ ' if ($fx_t.StartsWith(' +
2442
+ psStr('export ') +
2443
+ ') -or $fx_t.StartsWith(' +
2444
+ psStr('export\t') +
2445
+ ')) { $fx_t = $fx_t.Substring(7).Trim() }',
2446
+ ' $fx_eq = $fx_t.IndexOf([char]61)',
2447
+ ' if ($fx_eq -lt 1) { [Console]::Error.WriteLine(' +
2448
+ psStr('fauxnix: source: only NAME=VALUE lines are supported') +
2449
+ '); $script:fx_exit = 1; continue }',
2450
+ ' $fx_n = $fx_t.Substring(0, $fx_eq)',
2451
+ " if ($fx_n -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { [Console]::Error.WriteLine('bash: source: ' + $fx_n + ': not a valid identifier'); $script:fx_exit = 1; continue }",
2452
+ ' $fx_v = $fx_t.Substring($fx_eq + 1)',
2453
+ ' if ($fx_v.Length -ge 2 -and (($fx_v[0] -eq ' +
2454
+ q('"') +
2455
+ ' -and $fx_v[$fx_v.Length-1] -eq ' +
2456
+ q('"') +
2457
+ ') -or ($fx_v[0] -eq ' +
2458
+ q("'") +
2459
+ ' -and $fx_v[$fx_v.Length-1] -eq ' +
2460
+ q("'") +
2461
+ '))) { $fx_v = $fx_v.Substring(1, $fx_v.Length - 2) }',
2462
+ " Set-Item -LiteralPath ('Env:\\' + $fx_n) -Value $fx_v",
2463
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) + $fx_n) -join ';')",
2464
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) -join ';')",
2465
+ ' fx-arrdrop $fx_n',
2466
+ ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq2 = $fx_pair.IndexOf([char]61); if ($fx_eq2 -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq2) -cne $fx_n) { $fx_sv += $fx_pair } }',
2467
+ ' $fx_sv += ($fx_n + [string][char]61 + (fx-svenc $fx_v)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
2468
+ ' }',
2469
+ '}',
2470
+ ].join('\n');
2319
2471
  };
2320
2472
  const evalCmd = () => {
2321
2473
  return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself'); $script:fx_exit = 1");
@@ -2352,7 +2504,28 @@ const alias = (args) => {
2352
2504
  return '';
2353
2505
  return "[Console]::Error.WriteLine('fauxnix: alias is not supported'); $script:fx_exit = 1";
2354
2506
  };
2355
- const set = () => ''; // silently ignore (`set -e`, `set --` ... no-op)
2507
+ const set = (args) => {
2508
+ const raw = args.map(wordToString);
2509
+ const unsupported = raw.filter((t) => t === '-e' ||
2510
+ t === '-u' ||
2511
+ t === '-x' ||
2512
+ t === '-o' ||
2513
+ t === '+e' ||
2514
+ t === '+u' ||
2515
+ t === '+x' ||
2516
+ t.startsWith('-o') ||
2517
+ t.startsWith('+o') ||
2518
+ t === '-eu' ||
2519
+ t === '-ue' ||
2520
+ t === '-eux' ||
2521
+ /^-.*[eux]/.test(t));
2522
+ if (unsupported.length > 0) {
2523
+ return ('[Console]::Error.WriteLine(' +
2524
+ psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit') +
2525
+ '); $script:fx_exit = 2');
2526
+ }
2527
+ return '';
2528
+ };
2356
2529
  /* ------------------------------------------------------------------ */
2357
2530
  export const handlers = {
2358
2531
  cd,
@@ -2368,6 +2541,7 @@ export const handlers = {
2368
2541
  sleep,
2369
2542
  which,
2370
2543
  type,
2544
+ command: commandCmd,
2371
2545
  whoami,
2372
2546
  id,
2373
2547
  groups,
@@ -2393,6 +2567,7 @@ export const handlers = {
2393
2567
  history,
2394
2568
  less,
2395
2569
  more: less,
2570
+ read: readCmd,
2396
2571
  source,
2397
2572
  '.': source,
2398
2573
  eval: evalCmd,
package/dist/mcp.js CHANGED
@@ -37,7 +37,7 @@ Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), e
37
37
 
38
38
  Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
39
39
  Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
40
- Not supported: heredocs, backticks, control flow (if/for/while), background jobs.
40
+ Not supported: heredocs, while/until/case, word-level \$((...)) arithmetic expansion, background jobs. if/then/else/fi and for-in loops are supported.
41
41
 
42
42
  CWD, environment variables, export/unset and cd persist across calls within this session — but prefer COMBINING related commands in one call with ; or && (e.g. 'cd src && ls | wc -l'); each call is a fresh translation+process, so batching is faster than many tiny calls.
43
43
  Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
package/dist/parser.js CHANGED
@@ -127,6 +127,16 @@ export function tokenize(input) {
127
127
  continue;
128
128
  }
129
129
  }
130
+ if (c === '`') {
131
+ const v = readBacktick(input, i);
132
+ if (buf) {
133
+ parts.push({ kind: 'Text', text: buf });
134
+ buf = '';
135
+ }
136
+ parts.push(v.part);
137
+ i = v.next;
138
+ continue;
139
+ }
130
140
  buf += c;
131
141
  i++;
132
142
  }
@@ -153,7 +163,11 @@ export function tokenize(input) {
153
163
  continue;
154
164
  }
155
165
  if (ch === '`') {
156
- throw new FauxnixParseError('fauxnix: backticks are not supported. Use $(...) command substitution instead.');
166
+ const v = readBacktick(input, i);
167
+ beginWordPart();
168
+ cur.push(v.part);
169
+ i = v.next;
170
+ continue;
157
171
  }
158
172
  // escape outside quotes — keep the escape so [[ =~ ]] / == can
159
173
  // treat `\*` as a literal rather than a metacharacter
@@ -186,6 +200,23 @@ function isNameStart(c) {
186
200
  function isNameChar(c) {
187
201
  return /[A-Za-z0-9_]/.test(c);
188
202
  }
203
+ /** Parse `cmd` as command substitution (same AST as $(cmd)). */
204
+ function readBacktick(input, i) {
205
+ if (input[i] !== '`')
206
+ throw new FauxnixParseError('fauxnix: expected backtick');
207
+ let k = i + 1;
208
+ while (k < input.length) {
209
+ if (input[k] === '\\' && k + 1 < input.length) {
210
+ k += 2;
211
+ continue;
212
+ }
213
+ if (input[k] === '`') {
214
+ return { part: { kind: 'CmdSub', cmd: input.slice(i + 1, k) }, next: k + 1 };
215
+ }
216
+ k++;
217
+ }
218
+ throw new FauxnixParseError('fauxnix: unclosed backtick');
219
+ }
189
220
  /** Parse $VAR, ${VAR}, $(cmd substitution). Returns null when not a valid dollar construct. */
190
221
  function readDollar(input, i) {
191
222
  const n = input.length;
@@ -204,12 +235,33 @@ function readDollar(input, i) {
204
235
  if (sub) {
205
236
  return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: end + 1 };
206
237
  }
238
+ const pm = name.match(/^([A-Za-z_][A-Za-z0-9_]*)(:?[-+?])(.*)$/);
239
+ if (pm) {
240
+ const op = pm[2];
241
+ return {
242
+ part: { kind: 'Var', name: pm[1], param: { op, word: pm[3] } },
243
+ next: end + 1,
244
+ };
245
+ }
246
+ const hash = name.match(/^#([A-Za-z_][A-Za-z0-9_]*)(\[([0-9]+|@|\*)\])?$/);
247
+ if (hash) {
248
+ return {
249
+ part: { kind: 'Var', name: hash[1], index: hash[3], length: true },
250
+ next: end + 1,
251
+ };
252
+ }
207
253
  if (!name || !isNameStart(name[0]) || !name.split('').every(isNameChar)) {
208
- // ${VAR:-default} etc. unsupported, kept as raw text
254
+ // ${VAR:=default} etc. still raw text
209
255
  return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: end + 1 };
210
256
  }
211
257
  return { part: { kind: 'Var', name }, next: end + 1 };
212
258
  }
259
+ // $((...)) is arithmetic expansion, not command substitution of a
260
+ // parenthesized body. Today it was parsed as $( (expr) ) and became an
261
+ // empty/confusing command. Reject loudly until word-level arith lands.
262
+ if (input[j] === '(' && j + 1 < n && input[j + 1] === '(') {
263
+ throw new FauxnixParseError('fauxnix: $((...)) arithmetic expansion is not supported; compute the value in the agent or compare with [[ $n -eq m ]]');
264
+ }
213
265
  // $(cmd substitution) — captured with balanced parens; the translator
214
266
  // recursively translates this text before embedding it.
215
267
  if (input[j] === '(') {
@@ -509,7 +561,22 @@ export function parseCommand(input) {
509
561
  const parsePipeline = () => {
510
562
  const commands = [];
511
563
  for (;;) {
512
- commands.push(parseSimple());
564
+ const kw = peekKw();
565
+ if (kw === 'if') {
566
+ if (commands.length > 0) {
567
+ throw new FauxnixParseError('fauxnix: if in a pipeline is not supported');
568
+ }
569
+ commands.push(parseIf());
570
+ }
571
+ else if (kw === 'for') {
572
+ if (commands.length > 0) {
573
+ throw new FauxnixParseError('fauxnix: for in a pipeline is not supported');
574
+ }
575
+ commands.push(parseFor());
576
+ }
577
+ else {
578
+ commands.push(parseSimple());
579
+ }
513
580
  const t = peek();
514
581
  if (t.type === 'OP' && t.op === '|') {
515
582
  next();
@@ -519,6 +586,108 @@ export function parseCommand(input) {
519
586
  }
520
587
  return { kind: 'Pipeline', commands };
521
588
  };
589
+ const peekKw = () => {
590
+ const t = peek();
591
+ if (t.type !== 'WORD' || !t.parts)
592
+ return null;
593
+ const s = wordToString(t.parts);
594
+ if (!isUnquotedLiteral(t.parts, s))
595
+ return null;
596
+ if (s === 'if' ||
597
+ s === 'then' ||
598
+ s === 'else' ||
599
+ s === 'elif' ||
600
+ s === 'fi' ||
601
+ s === 'for' ||
602
+ s === 'in' ||
603
+ s === 'do' ||
604
+ s === 'done' ||
605
+ s === 'while') {
606
+ return s;
607
+ }
608
+ return null;
609
+ };
610
+ const expectKw = (k) => {
611
+ if (peekKw() !== k) {
612
+ throw new FauxnixParseError('fauxnix: expected `' + k + "'");
613
+ }
614
+ next();
615
+ };
616
+ const parseListUntil = (stops) => {
617
+ const stop = new Set(stops);
618
+ const segments = [];
619
+ let op = ';';
620
+ const isListSep = (o) => o === ';' || o === '\n';
621
+ while (peek().type === 'OP' && isListSep(peek().op))
622
+ next();
623
+ while (peek().type !== 'EOF') {
624
+ const kw = peekKw();
625
+ if (kw && stop.has(kw))
626
+ break;
627
+ const pipeline = parsePipeline();
628
+ segments.push({ pipeline, op });
629
+ const t = peek();
630
+ if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
631
+ op = t.op === '\n' ? ';' : t.op;
632
+ next();
633
+ while (peek().type === 'OP' && isListSep(peek().op))
634
+ next();
635
+ }
636
+ else {
637
+ break;
638
+ }
639
+ }
640
+ if (segments.length === 0) {
641
+ throw new FauxnixParseError('fauxnix: empty command');
642
+ }
643
+ return { kind: 'CommandList', segments };
644
+ };
645
+ const parseIf = () => {
646
+ expectKw('if');
647
+ const test = parseListUntil(['then']);
648
+ expectKw('then');
649
+ const thenL = parseListUntil(['else', 'elif', 'fi']);
650
+ let elseL;
651
+ if (peekKw() === 'elif') {
652
+ throw new FauxnixParseError('fauxnix: elif is not supported yet; use else + if');
653
+ }
654
+ if (peekKw() === 'else') {
655
+ next();
656
+ elseL = parseListUntil(['fi']);
657
+ }
658
+ expectKw('fi');
659
+ return { kind: 'If', test, then: thenL, else: elseL, redirects: [] };
660
+ };
661
+ const parseFor = () => {
662
+ expectKw('for');
663
+ const nt = peek();
664
+ if (nt.type !== 'WORD' || !nt.parts) {
665
+ throw new FauxnixParseError('fauxnix: `for` expected a name');
666
+ }
667
+ const name = wordToString(nt.parts);
668
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !isUnquotedLiteral(nt.parts, name)) {
669
+ throw new FauxnixParseError('fauxnix: `for` name must be an identifier');
670
+ }
671
+ next();
672
+ expectKw('in');
673
+ const words = [];
674
+ for (;;) {
675
+ while (peek().type === 'OP' && (peek().op === ';' || peek().op === '\n'))
676
+ next();
677
+ if (peekKw() === 'do')
678
+ break;
679
+ const t = peek();
680
+ if (t.type !== 'WORD' || !t.parts) {
681
+ throw new FauxnixParseError("fauxnix: `for` expected `do`");
682
+ }
683
+ words.push(t.parts);
684
+ next();
685
+ }
686
+ expectKw('do');
687
+ const body = parseListUntil(['done']);
688
+ expectKw('done');
689
+ return { kind: 'For', name, words, body, redirects: [] };
690
+ };
522
691
  const parseSimple = () => {
523
692
  const assignments = [];
524
693
  let redirects = [];
@@ -1,7 +1,12 @@
1
- import { Assignment, CommandList, Redirect, SimpleCommand, Word } from './ast.js';
1
+ import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, Word } from './ast.js';
2
2
  import { PipelineCtx } from './registry.js';
3
+ /** `${name:-word}` and friends using case-exact fx-scalar0. */
4
+ export declare function paramExpr(name: string, op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?', word: string): string;
3
5
  /** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
4
- export declare function varExpr(name: string, index?: string): string;
6
+ export declare function varExpr(name: string, index?: string, param?: {
7
+ op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?';
8
+ word: string;
9
+ }, length?: boolean): string;
5
10
  /** Escape text destined for the inside of a PS double-quoted string. */
6
11
  export declare function escapeDq(s: string): string;
7
12
  /** Normalize a literal POSIX-ish path to its Windows equivalent. */
@@ -66,13 +71,8 @@ export interface PipelineParts {
66
71
  /** The pipeline invocation itself. */
67
72
  call: string;
68
73
  }
69
- /**
70
- * Pipeline body. A lone command runs as a plain script-block expression;
71
- * multi-command pipelines become generated functions chained with `|`
72
- * (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
73
- */
74
74
  export declare function translatePipelineBody(p: {
75
- commands: SimpleCommand[];
75
+ commands: Array<SimpleCommand | IfCommand | ForCommand>;
76
76
  }): PipelineParts;
77
77
  export interface SegmentPlan {
78
78
  op: ';' | '&&' | '||';
@@ -85,5 +85,7 @@ export declare function translateCommandList(list: CommandList): SegmentPlan[];
85
85
  /**
86
86
  * Wrap a pipeline body with the Fauxnix executor contract:
87
87
  * UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
88
+ * Only the fx- helpers the body actually calls are emitted — the full
89
+ * catalog is ~170 lines and was paid on every `echo hi`.
88
90
  */
89
91
  export declare function wrapScript(body: string): string;
@@ -4,8 +4,55 @@ import { lookup, psStr } from './registry.js';
4
4
  /* ------------------------------------------------------------------ */
5
5
  /* Variable mapping */
6
6
  /* ------------------------------------------------------------------ */
7
+ function paramWordExpr(word) {
8
+ if (word.startsWith('$') && /^\$[A-Za-z_][A-Za-z0-9_]*$/.test(word)) {
9
+ return varExpr(word.slice(1));
10
+ }
11
+ return psStr(word);
12
+ }
13
+ /** `${name:-word}` and friends using case-exact fx-scalar0. */
14
+ export function paramExpr(name, op, word) {
15
+ const alt = paramWordExpr(word);
16
+ const get = '(fx-scalar0 ' + psStr(name) + ')';
17
+ const empty = '($null -eq $fx_pv -or [string]$fx_pv -eq \'\')';
18
+ const unset = '($null -eq $fx_pv)';
19
+ if (op === ':-') {
20
+ return '$( $fx_pv = ' + get + '; if (' + empty + ') { ' + alt + ' } else { $fx_pv } )';
21
+ }
22
+ if (op === '-') {
23
+ return '$( $fx_pv = ' + get + '; if (' + unset + ') { ' + alt + ' } else { $fx_pv } )';
24
+ }
25
+ if (op === ':+') {
26
+ return '$( $fx_pv = ' + get + '; if (' + empty + ') { \'\' } else { ' + alt + ' } )';
27
+ }
28
+ if (op === '+') {
29
+ return '$( $fx_pv = ' + get + '; if (' + unset + ') { \'\' } else { ' + alt + ' } )';
30
+ }
31
+ const msg = word === '' ? name + ': parameter null or not set' : name + ': ' + word;
32
+ const cond = op === ':?' ? empty : unset;
33
+ return ('$( $fx_pv = ' +
34
+ get +
35
+ '; if (' +
36
+ cond +
37
+ ') { [Console]::Error.WriteLine(' +
38
+ psStr('bash: ' + msg) +
39
+ '); $script:fx_exit = 1; \'\' } else { $fx_pv } )');
40
+ }
7
41
  /** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
8
- export function varExpr(name, index) {
42
+ export function varExpr(name, index, param, length = false) {
43
+ if (param)
44
+ return paramExpr(name, param.op, param.word);
45
+ if (length) {
46
+ if (index === '@' || index === '*') {
47
+ return '@(fx-arrload ' + psStr(name) + ').Count';
48
+ }
49
+ if (index !== undefined) {
50
+ return '([string](fx-subget ' + psStr(name) + ' ' + psStr(index) + ')).Length';
51
+ }
52
+ return ('([string]$(if ($null -eq ($fx_pv = fx-scalar0 ' +
53
+ psStr(name) +
54
+ ')) { \'\' } else { $fx_pv })).Length');
55
+ }
9
56
  // Indexed reads always go through fx-subget → fx-arrload → fx-scalar0 so
10
57
  // ${PWD[0]} keeps the special mapping and ${bash_rematch[0]} stays
11
58
  // case-exact (a `$env:name` fallback would alias BASH_REMATCH on Windows).
@@ -106,7 +153,8 @@ export function exprOfWord(w, opts) {
106
153
  }
107
154
  // single bare variable → bare expression
108
155
  if (expanded.length === 1 && expanded[0].kind === 'Var') {
109
- return varExpr(expanded[0].name, expanded[0].index);
156
+ const v = expanded[0];
157
+ return varExpr(v.name, v.index, v.param, v.length === true);
110
158
  }
111
159
  // Bare `$(...)` must not sit inside a PS expandable string: the
112
160
  // substitution body contains `"` / `$_` that would break interpolation.
@@ -133,7 +181,7 @@ export function exprOfWord(w, opts) {
133
181
  emitPart(q, true);
134
182
  break;
135
183
  case 'Var':
136
- out += '$(' + varExpr(p.name, p.index) + ')';
184
+ out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
137
185
  break;
138
186
  case 'CmdSub':
139
187
  out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
@@ -189,7 +237,9 @@ export function splatSpec(w) {
189
237
  let suffix = '';
190
238
  let seen = false;
191
239
  for (const { part: p, quoted } of parts) {
192
- const splat = p.kind === 'Var' && (p.index === '@' || (p.index === '*' && !quoted));
240
+ const splat = p.kind === 'Var' &&
241
+ !p.length &&
242
+ (p.index === '@' || (p.index === '*' && !quoted));
193
243
  if (splat) {
194
244
  if (seen)
195
245
  return null;
@@ -258,6 +308,12 @@ export function translateCmdSub(cmdText, keepNl = false) {
258
308
  /* ------------------------------------------------------------------ */
259
309
  /* Simple command translation */
260
310
  /* ------------------------------------------------------------------ */
311
+ function indentBlock(s) {
312
+ return s
313
+ .split('\n')
314
+ .map((l) => (l ? ' ' + l : l))
315
+ .join('\n');
316
+ }
261
317
  export function translateSimple(cmd, position, hasStdin) {
262
318
  // assignment-only segment (`X=1; cmd`): bash semantics are "set for the
263
319
  // rest of the shell". Reuse the export code path — persist + env shadow —
@@ -276,20 +332,35 @@ export function translateSimple(cmd, position, hasStdin) {
276
332
  let body;
277
333
  if (nameSplat) {
278
334
  const invoke = '& $fx_cmd @fx_na';
279
- body = [
335
+ const hasAffix = !!(nameSplat.prefix || nameSplat.suffix);
336
+ const promoted = cmd.args.length === 0
337
+ ? ''
338
+ : translateSimple({
339
+ kind: 'SimpleCommand',
340
+ assignments: [],
341
+ name: cmd.args[0],
342
+ args: cmd.args.slice(1),
343
+ redirects: cmd.redirects,
344
+ }, position, hasStdin);
345
+ const emptyCmdLines = [
280
346
  '$fx_cw = @(fx-arrload ' + psStr(nameSplat.name) + ')',
281
- 'if ($fx_cw.Count -eq 0) { $fx_cw = @(' + psStr(nameSplat.prefix + nameSplat.suffix) + ') }',
282
- 'else { $fx_cw[0] = ' +
347
+ ];
348
+ if (hasAffix) {
349
+ emptyCmdLines.push('if ($fx_cw.Count -eq 0) { $fx_cw = @(' +
350
+ psStr(nameSplat.prefix + nameSplat.suffix) +
351
+ ') } else { $fx_cw[0] = ' +
283
352
  psStr(nameSplat.prefix) +
284
353
  ' + $fx_cw[0]; $fx_cw[$fx_cw.Count-1] = $fx_cw[$fx_cw.Count-1] + ' +
285
354
  psStr(nameSplat.suffix) +
286
- ' }',
287
- '$fx_cmd = [string]$fx_cw[0]',
288
- '$fx_na = ' + argListExpr(cmd.args),
289
- 'if ($fx_cw.Count -gt 1) { $fx_na = @($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na }',
290
- (hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
291
- 'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
292
- ].join('\n');
355
+ ' }');
356
+ }
357
+ emptyCmdLines.push('if ($fx_cw.Count -eq 0) {',
358
+ // No words left bash null command (exit 0). Remaining words are
359
+ // known at compile time, so reuse translateSimple (handlers, not `&`).
360
+ promoted ? indentBlock(promoted) : ' ', '} else {', ' $fx_na = [object[]]@(' + argListExpr(cmd.args) + ')', ' $fx_cmd = [string]$fx_cw[0]', ' if ($fx_cw.Count -gt 1) { $fx_na = @($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na }', ' ' +
361
+ (hasStdin ? '($input | ' + invoke + ')' : invoke) +
362
+ ' | ForEach-Object { [string]$_ }', ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }', '}');
363
+ body = emptyCmdLines.join('\n');
293
364
  }
294
365
  else if (nameLit !== null) {
295
366
  const handler = lookup(nameLit);
@@ -591,12 +662,77 @@ let stageSeq = 0;
591
662
  * multi-command pipelines become generated functions chained with `|`
592
663
  * (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
593
664
  */
665
+ function translateListInline(list) {
666
+ const chunks = [];
667
+ for (const seg of list.segments) {
668
+ const { defs, call } = translatePipelineBody(seg.pipeline);
669
+ const body = (defs ? defs + '\n' : '') + call;
670
+ if (seg.op === '&&') {
671
+ chunks.push('if ($script:fx_exit -eq 0) {\n' + body + '\n}');
672
+ }
673
+ else if (seg.op === '||') {
674
+ chunks.push('if ($script:fx_exit -ne 0) {\n' + body + '\n}');
675
+ }
676
+ else {
677
+ chunks.push(body);
678
+ }
679
+ }
680
+ return chunks.join('\n');
681
+ }
682
+ function translateIf(cmd) {
683
+ // Branch bodies reset fx_exit first: the compound's exit status must come
684
+ // from the taken branch's last command (bash semantics), not leak the test's
685
+ // failure — `if false; then A; else B; fi` exits 0 in bash.
686
+ const lines = [translateListInline(cmd.test), 'if ($script:fx_exit -eq 0) {', ' $script:fx_exit = 0'];
687
+ for (const l of translateListInline(cmd.then).split('\n'))
688
+ lines.push(l ? ' ' + l : l);
689
+ lines.push('} else {', ' $script:fx_exit = 0');
690
+ if (cmd.else) {
691
+ for (const l of translateListInline(cmd.else).split('\n'))
692
+ lines.push(l ? ' ' + l : l);
693
+ }
694
+ lines.push('}');
695
+ return lines.join('\n');
696
+ }
697
+ function translateFor(cmd) {
698
+ const n = cmd.name.replace(/'/g, "''");
699
+ const lines = [
700
+ '$fx_for = ' + argListExpr(cmd.words),
701
+ 'foreach ($fx_it in @($fx_for)) {',
702
+ " Set-Item -LiteralPath ('Env:\\' + '" + n + "') -Value ([string]$fx_it)",
703
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
704
+ n +
705
+ "' }) + '" +
706
+ n +
707
+ "') -join ';')",
708
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
709
+ n +
710
+ "' }) -join ';')",
711
+ ' fx-arrdrop ' + psStr(cmd.name),
712
+ " $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne '" +
713
+ n +
714
+ "') { $fx_sv += $fx_pair } }",
715
+ " $fx_sv += ('" +
716
+ n +
717
+ "' + [string][char]61 + (fx-svenc ([string]$fx_it))); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
718
+ ];
719
+ for (const l of translateListInline(cmd.body).split('\n'))
720
+ lines.push(l ? ' ' + l : l);
721
+ lines.push('}');
722
+ return lines.join('\n');
723
+ }
594
724
  export function translatePipelineBody(p) {
595
725
  const bodies = [];
596
726
  for (let i = 0; i < p.commands.length; i++) {
597
- const hasStdin = i > 0 || p.commands[i].redirects.some((r) => r.op === '<');
727
+ const c = p.commands[i];
728
+ const hasStdin = i > 0 || c.redirects.some((r) => r.op === '<');
598
729
  const position = i === 0 ? 'first' : i === p.commands.length - 1 ? 'last' : 'middle';
599
- bodies.push(translateSimple(p.commands[i], position, hasStdin));
730
+ if (c.kind === 'If')
731
+ bodies.push(translateIf(c));
732
+ else if (c.kind === 'For')
733
+ bodies.push(translateFor(c));
734
+ else
735
+ bodies.push(translateSimple(c, position, hasStdin));
600
736
  }
601
737
  if (bodies.length === 1) {
602
738
  return { defs: '', call: '(& {\n' + bodies[0] + '\n})' };
@@ -639,11 +775,72 @@ export function translateCommandList(list) {
639
775
  }
640
776
  return plans;
641
777
  }
778
+ const WRAP_HELPER_ORDER = [
779
+ 'fx-readlines',
780
+ 'fx-csub',
781
+ 'fx-svenc',
782
+ 'fx-svdec',
783
+ 'fx-arrload',
784
+ 'fx-scalar0',
785
+ 'fx-ifs1',
786
+ 'fx-arrdrop',
787
+ 'fx-arrhas',
788
+ 'fx-arrpackget',
789
+ 'fx-arrpackset',
790
+ 'fx-arrput',
791
+ 'fx-arrclr',
792
+ 'fx-subget',
793
+ ];
794
+ const WRAP_HELPER_DEPS = {
795
+ 'fx-readlines': [],
796
+ 'fx-csub': [],
797
+ 'fx-svenc': [],
798
+ 'fx-svdec': [],
799
+ 'fx-arrload': ['fx-scalar0', 'fx-svdec'],
800
+ 'fx-scalar0': ['fx-svdec'],
801
+ 'fx-ifs1': ['fx-scalar0'],
802
+ 'fx-arrdrop': [],
803
+ 'fx-arrhas': [],
804
+ 'fx-arrpackget': [],
805
+ 'fx-arrpackset': ['fx-arrdrop'],
806
+ 'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
807
+ 'fx-arrclr': ['fx-arrdrop'],
808
+ 'fx-subget': ['fx-arrload', 'fx-ifs1'],
809
+ };
810
+ /** Helpers the body calls that wrapScript still has to emit (not already defined there). */
811
+ function wrapHelpersNeeded(body) {
812
+ const defined = new Set();
813
+ const defRe = /function\s+(fx-[A-Za-z0-9]+)/g;
814
+ let m;
815
+ while ((m = defRe.exec(body)))
816
+ defined.add(m[1]);
817
+ const seeds = [];
818
+ const callRe = /\b(fx-[A-Za-z0-9]+)\b/g;
819
+ while ((m = callRe.exec(body))) {
820
+ const n = m[1];
821
+ if (!defined.has(n) && n in WRAP_HELPER_DEPS)
822
+ seeds.push(n);
823
+ }
824
+ const needed = new Set();
825
+ const stack = seeds.slice();
826
+ while (stack.length) {
827
+ const n = stack.pop();
828
+ if (needed.has(n) || defined.has(n))
829
+ continue;
830
+ needed.add(n);
831
+ for (const d of WRAP_HELPER_DEPS[n])
832
+ stack.push(d);
833
+ }
834
+ return needed;
835
+ }
642
836
  /**
643
837
  * Wrap a pipeline body with the Fauxnix executor contract:
644
838
  * UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
839
+ * Only the fx- helpers the body actually calls are emitted — the full
840
+ * catalog is ~170 lines and was paid on every `echo hi`.
645
841
  */
646
842
  export function wrapScript(body) {
843
+ const needed = wrapHelpersNeeded(body);
647
844
  const lines = [
648
845
  '$ErrorActionPreference = "Continue"',
649
846
  "$ProgressPreference = 'SilentlyContinue'",
@@ -667,175 +864,191 @@ export function wrapScript(body) {
667
864
  // .NET APIs (ReadAllBytes & friends) resolve relative paths against the
668
865
  // process working directory, NOT the PS location — keep them in sync.
669
866
  'try { [Environment]::CurrentDirectory = (Get-Location).ProviderPath } catch {}',
670
- // byte-sniffing line reader for `< file` stdin redirects (UTF-8 → GBK)
671
- 'function fx-readlines($p) {',
672
- ' $b = [IO.File]::ReadAllBytes($p)',
673
- ' $t = $null',
674
- ' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
675
- " if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
676
- ' $t = $t -replace "`r`n", "`n"',
677
- ' $t = $t -replace "`r", "`n"',
678
- ' $parts = @($t.Split("`n"))',
679
- " if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
680
- " if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
681
- ' return $parts',
682
- '}',
683
- 'function fx-csub([scriptblock]$b) {',
684
- ' $fx_prevcs = $script:fx_csub',
685
- ' $script:fx_csub = $true',
686
- ' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
687
- ' finally { $script:fx_csub = $fx_prevcs }',
688
- ' $fx_s = ($fx_o -join [string][char]10)',
689
- ' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
690
- ' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
691
- ' }',
692
- ' return $fx_s',
693
- '}',
694
- 'function fx-svenc($s) {',
695
- ' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
696
- '}',
697
- 'function fx-svdec($s) {',
698
- ' $s = [string]$s',
699
- ' $sb = New-Object System.Text.StringBuilder',
700
- ' $i = 0',
701
- ' while ($i -lt $s.Length) {',
702
- ' $c = $s[$i]',
703
- ' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
704
- ' $n2 = $s[$i + 1]',
705
- ' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
706
- ' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
707
- ' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
708
- ' }',
709
- ' [void]$sb.Append($c)',
710
- ' $i++',
711
- ' }',
712
- ' return [string]$sb',
713
- '}',
714
- 'function fx-arrload($n) {',
715
- ' $n = [string]$n',
716
- ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
717
- ' $fx_eq = $fx_pair.IndexOf([char]61)',
718
- ' if ($fx_eq -lt 1) { continue }',
719
- ' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
720
- ' $out = @()',
721
- ' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
722
- ' return $out',
723
- ' }',
724
- ' $s0 = fx-scalar0 $n',
725
- ' if ($null -eq $s0) { return @() }',
726
- ' return @([string]$s0)',
727
- '}',
728
- 'function fx-scalar0($n) {',
729
- ' $n = [string]$n',
730
- " if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
731
- ' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
732
- ' $fx_eq = $fx_pair.IndexOf([char]61)',
733
- ' if ($fx_eq -lt 1) { continue }',
734
- ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
735
- ' }',
736
- " if ($n -ceq 'HOME') { return [string]$HOME }",
737
- " if ($n -ceq 'PWD') { return [string]$PWD.Path }",
738
- " if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
739
- " if ($n -ceq 'PATH') { return [string]$env:PATH }",
740
- " if ($n -ceq 'SHELL') { return 'powershell' }",
741
- " if ($n -ceq 'TERM') { return 'xterm-256color' }",
742
- " if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
743
- " if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
744
- ' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
745
- ' if ($ev) { return [string]$ev.Value }',
746
- ' return $null',
747
- '}',
748
- 'function fx-ifs1 {',
749
- " $s = fx-scalar0 'IFS'",
750
- " if ($null -eq $s) { return ' ' }",
751
- " if ([string]$s -eq '') { return '' }",
752
- ' return [string]$s[0]',
753
- '}',
754
- 'function fx-arrdrop($n) {',
755
- ' $n = [string]$n',
756
- ' $fx_sm = @()',
757
- ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
758
- ' $fx_eq = $fx_pair.IndexOf([char]61)',
759
- ' if ($fx_eq -lt 1) { continue }',
760
- ' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
761
- ' }',
762
- ' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
763
- '}',
764
- 'function fx-arrhas($n) {',
765
- ' $n = [string]$n',
766
- ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
767
- ' $fx_eq = $fx_pair.IndexOf([char]61)',
768
- ' if ($fx_eq -lt 1) { continue }',
769
- ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
770
- ' }',
771
- ' return $false',
772
- '}',
773
- 'function fx-arrpackget($n) {',
774
- ' $n = [string]$n',
775
- ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
776
- ' $fx_eq = $fx_pair.IndexOf([char]61)',
777
- ' if ($fx_eq -lt 1) { continue }',
778
- ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
779
- ' }',
780
- ' return $null',
781
- '}',
782
- 'function fx-arrpackset($n, $pay) {',
783
- ' fx-arrdrop $n',
784
- ' if ($null -eq $pay) { return }',
785
- " $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
786
- '}',
787
- 'function fx-arrput($n, $vals) {',
788
- ' $n = [string]$n',
789
- ' $vals = @($vals)',
790
- ' fx-arrdrop $n',
791
- ' if ($vals.Count -eq 0) { } else {',
792
- ' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
793
- " $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
794
- ' }',
795
- " $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
796
- ' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
797
- " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
798
- " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
799
- ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
800
- " $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
801
- '}',
802
- 'function fx-arrclr($n) {',
803
- ' $n = [string]$n',
804
- ' fx-arrdrop $n',
805
- " Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
806
- " $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
807
- " $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
808
- ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
809
- '}',
810
- 'function fx-subget($n, $ix) {',
811
- ' $arr = @(fx-arrload $n)',
812
- ' $ix = [string]$ix',
813
- // argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
814
- " if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
815
- " if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
816
- ' $i = 0',
817
- ' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
818
- " if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
819
- ' return [string]$arr[$i]',
820
- '}',
821
- 'try {',
822
- ...body.split('\n').map((l) => ' ' + l),
823
- '} catch [System.Management.Automation.CommandNotFoundException] {',
824
- " [Console]::Error.WriteLine('bash: ' + $_.Exception.TargetName + ': command not found')",
825
- ' $script:fx_exit = 127',
826
- '} catch {',
827
- ' [Console]::Error.WriteLine(($_.Exception.Message).Split("`n")[0])',
828
- ' $script:fx_exit = 1',
829
- '}',
830
- '# persist session cwd and environment for the next segment',
831
- 'try { [IO.File]::WriteAllText($env:FAUXNIX_CWD_FILE, (Get-Location).Path) } catch {}',
832
- 'if ((Get-Location).Path -ne $fx_oldcwd) { $env:FAUXNIX_OLDPWD = $fx_oldcwd }',
833
- 'try {',
834
- ' $envObj = @{}',
835
- ' Get-ChildItem Env: | ForEach-Object { $envObj[$_.Name] = $_.Value }',
836
- ' [IO.File]::WriteAllText($env:FAUXNIX_ENV_FILE, (ConvertTo-Json $envObj -Compress))',
837
- '} catch {}',
838
- 'exit $script:fx_exit',
839
867
  ];
868
+ const helpers = {
869
+ 'fx-readlines': [
870
+ 'function fx-readlines($p) {',
871
+ ' $b = [IO.File]::ReadAllBytes($p)',
872
+ ' $t = $null',
873
+ ' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
874
+ " if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
875
+ ' $t = $t -replace "`r`n", "`n"',
876
+ ' $t = $t -replace "`r", "`n"',
877
+ ' $parts = @($t.Split("`n"))',
878
+ " if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
879
+ " if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
880
+ ' return $parts',
881
+ '}',
882
+ ],
883
+ 'fx-csub': [
884
+ 'function fx-csub([scriptblock]$b) {',
885
+ ' $fx_prevcs = $script:fx_csub',
886
+ ' $script:fx_csub = $true',
887
+ ' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
888
+ ' finally { $script:fx_csub = $fx_prevcs }',
889
+ ' $fx_s = ($fx_o -join [string][char]10)',
890
+ ' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
891
+ ' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
892
+ ' }',
893
+ ' return $fx_s',
894
+ '}',
895
+ ],
896
+ 'fx-svenc': [
897
+ 'function fx-svenc($s) {',
898
+ ' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
899
+ '}',
900
+ ],
901
+ 'fx-svdec': [
902
+ 'function fx-svdec($s) {',
903
+ ' $s = [string]$s',
904
+ ' $sb = New-Object System.Text.StringBuilder',
905
+ ' $i = 0',
906
+ ' while ($i -lt $s.Length) {',
907
+ ' $c = $s[$i]',
908
+ ' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
909
+ ' $n2 = $s[$i + 1]',
910
+ ' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
911
+ ' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
912
+ ' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
913
+ ' }',
914
+ ' [void]$sb.Append($c)',
915
+ ' $i++',
916
+ ' }',
917
+ ' return [string]$sb',
918
+ '}',
919
+ ],
920
+ 'fx-arrload': [
921
+ 'function fx-arrload($n) {',
922
+ ' $n = [string]$n',
923
+ ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
924
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
925
+ ' if ($fx_eq -lt 1) { continue }',
926
+ ' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
927
+ ' $out = @()',
928
+ ' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
929
+ ' return $out',
930
+ ' }',
931
+ ' $s0 = fx-scalar0 $n',
932
+ ' if ($null -eq $s0) { return @() }',
933
+ ' return @([string]$s0)',
934
+ '}',
935
+ ],
936
+ 'fx-scalar0': [
937
+ 'function fx-scalar0($n) {',
938
+ ' $n = [string]$n',
939
+ " if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
940
+ ' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
941
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
942
+ ' if ($fx_eq -lt 1) { continue }',
943
+ ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
944
+ ' }',
945
+ " if ($n -ceq 'HOME') { return [string]$HOME }",
946
+ " if ($n -ceq 'PWD') { return [string]$PWD.Path }",
947
+ " if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
948
+ " if ($n -ceq 'PATH') { return [string]$env:PATH }",
949
+ " if ($n -ceq 'SHELL') { return 'powershell' }",
950
+ " if ($n -ceq 'TERM') { return 'xterm-256color' }",
951
+ " if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
952
+ " if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
953
+ ' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
954
+ ' if ($ev) { return [string]$ev.Value }',
955
+ ' return $null',
956
+ '}',
957
+ ],
958
+ 'fx-ifs1': [
959
+ 'function fx-ifs1 {',
960
+ " $s = fx-scalar0 'IFS'",
961
+ " if ($null -eq $s) { return ' ' }",
962
+ " if ([string]$s -eq '') { return '' }",
963
+ ' return [string]$s[0]',
964
+ '}',
965
+ ],
966
+ 'fx-arrdrop': [
967
+ 'function fx-arrdrop($n) {',
968
+ ' $n = [string]$n',
969
+ ' $fx_sm = @()',
970
+ ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
971
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
972
+ ' if ($fx_eq -lt 1) { continue }',
973
+ ' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
974
+ ' }',
975
+ ' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
976
+ '}',
977
+ ],
978
+ 'fx-arrhas': [
979
+ 'function fx-arrhas($n) {',
980
+ ' $n = [string]$n',
981
+ ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
982
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
983
+ ' if ($fx_eq -lt 1) { continue }',
984
+ ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
985
+ ' }',
986
+ ' return $false',
987
+ '}',
988
+ ],
989
+ 'fx-arrpackget': [
990
+ 'function fx-arrpackget($n) {',
991
+ ' $n = [string]$n',
992
+ ' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
993
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
994
+ ' if ($fx_eq -lt 1) { continue }',
995
+ ' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
996
+ ' }',
997
+ ' return $null',
998
+ '}',
999
+ ],
1000
+ 'fx-arrpackset': [
1001
+ 'function fx-arrpackset($n, $pay) {',
1002
+ ' fx-arrdrop $n',
1003
+ ' if ($null -eq $pay) { return }',
1004
+ " $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
1005
+ '}',
1006
+ ],
1007
+ 'fx-arrput': [
1008
+ 'function fx-arrput($n, $vals) {',
1009
+ ' $n = [string]$n',
1010
+ ' $vals = @($vals)',
1011
+ ' fx-arrdrop $n',
1012
+ ' if ($vals.Count -eq 0) { } else {',
1013
+ ' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
1014
+ " $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
1015
+ ' }',
1016
+ " $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
1017
+ ' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
1018
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
1019
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
1020
+ ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
1021
+ " $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
1022
+ '}',
1023
+ ],
1024
+ 'fx-arrclr': [
1025
+ 'function fx-arrclr($n) {',
1026
+ ' $n = [string]$n',
1027
+ ' fx-arrdrop $n',
1028
+ " Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
1029
+ " $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
1030
+ " $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
1031
+ ' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
1032
+ '}',
1033
+ ],
1034
+ 'fx-subget': [
1035
+ 'function fx-subget($n, $ix) {',
1036
+ ' $arr = @(fx-arrload $n)',
1037
+ ' $ix = [string]$ix',
1038
+ // argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
1039
+ " if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
1040
+ " if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
1041
+ ' $i = 0',
1042
+ ' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
1043
+ " if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
1044
+ ' return [string]$arr[$i]',
1045
+ '}',
1046
+ ],
1047
+ };
1048
+ for (const name of WRAP_HELPER_ORDER) {
1049
+ if (needed.has(name))
1050
+ lines.push(...helpers[name]);
1051
+ }
1052
+ lines.push('try {', ...body.split('\n').map((l) => ' ' + l), '} catch [System.Management.Automation.CommandNotFoundException] {', " [Console]::Error.WriteLine('bash: ' + $_.Exception.TargetName + ': command not found')", ' $script:fx_exit = 127', '} catch {', ' [Console]::Error.WriteLine(($_.Exception.Message).Split("`n")[0])', ' $script:fx_exit = 1', '}', '# persist session cwd and environment for the next segment', 'try { [IO.File]::WriteAllText($env:FAUXNIX_CWD_FILE, (Get-Location).Path) } catch {}', 'if ((Get-Location).Path -ne $fx_oldcwd) { $env:FAUXNIX_OLDPWD = $fx_oldcwd }', 'try {', ' $envObj = @{}', ' Get-ChildItem Env: | ForEach-Object { $envObj[$_.Name] = $_.Value }', ' [IO.File]::WriteAllText($env:FAUXNIX_ENV_FILE, (ConvertTo-Json $envObj -Compress))', '} catch {}', 'exit $script:fx_exit');
840
1053
  return lines.join('\n');
841
1054
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxnix-cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Fauxnix — run Linux-style commands on Windows via deterministic PowerShell translation. No VM, no WSL. MCP server + CLI for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {