fauxnix-cli 0.11.0 → 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.
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { FauxnixParseError, wordToString } from '../ast.js';
5
5
  import { parseWords, psStr } from '../registry.js';
6
- import { argListExpr, exprOfWord, literalOfWord, operandExpr } from '../translator.js';
6
+ import { argListExpr, exprOfWord, literalOfWord, operandExpr, PURE_SED_FILE_MESSAGE, } from '../translator.js';
7
7
  /* ------------------------------------------------------------------ */
8
8
  /* Shared PS snippets (same shape as files.ts) */
9
9
  /* ------------------------------------------------------------------ */
@@ -981,7 +981,7 @@ function parseSedScript(src, isEre) {
981
981
  }
982
982
  return out;
983
983
  }
984
- const sed = (args) => {
984
+ const sed = (args, ctx) => {
985
985
  // custom argv parse: -i takes an ATTACHED suffix; -e/-f take attached or next
986
986
  const raw = args.map((w) => wordToString(w));
987
987
  let noPrint = false;
@@ -1030,6 +1030,9 @@ const sed = (args) => {
1030
1030
  throw new FauxnixParseError('fauxnix: sed -' + ch + ' requires an argument');
1031
1031
  }
1032
1032
  if (ch === 'f') {
1033
+ if (ctx.translationMode === 'pure') {
1034
+ throw new FauxnixParseError(PURE_SED_FILE_MESSAGE);
1035
+ }
1033
1036
  try {
1034
1037
  val = readFileSync(nodePathOf(val), 'utf8');
1035
1038
  }
@@ -1775,20 +1778,42 @@ const awk = (args) => {
1775
1778
  const { values, operandWords } = parseWords(args, ['F', 'v']);
1776
1779
  // -v may repeat; collect all of them
1777
1780
  const vvars = [];
1781
+ const addVVar = (nv) => {
1782
+ const eq = nv.indexOf('=');
1783
+ if (eq < 1) {
1784
+ throw new FauxnixParseError("fauxnix: awk invalid -v assignment '" + nv + "'");
1785
+ }
1786
+ const name = nv.slice(0, eq);
1787
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1788
+ throw new FauxnixParseError("fauxnix: awk invalid variable name '" + name + "'");
1789
+ }
1790
+ vvars.push([name, nv.slice(eq + 1)]);
1791
+ };
1792
+ let onlyOperands = false;
1778
1793
  for (let i = 0; i < args.length; i++) {
1779
1794
  const t = wordToString(args[i]);
1780
- if (t === '-v' && i + 1 < args.length) {
1781
- const nv = wordToString(args[i + 1]);
1782
- const eq = nv.indexOf('=');
1783
- if (eq > 0)
1784
- vvars.push([nv.slice(0, eq), nv.slice(eq + 1)]);
1795
+ if (t === '--') {
1796
+ onlyOperands = true;
1797
+ continue;
1798
+ }
1799
+ if (onlyOperands)
1800
+ continue;
1801
+ if (t === '-F') {
1802
+ if (i + 1 < args.length)
1803
+ i++;
1804
+ continue;
1805
+ }
1806
+ if (t.startsWith('-F') && t.length > 2)
1807
+ continue;
1808
+ if (t === '-v') {
1809
+ if (i + 1 >= args.length) {
1810
+ throw new FauxnixParseError('fauxnix: awk -v requires an argument');
1811
+ }
1812
+ addVVar(wordToString(args[i + 1]));
1785
1813
  i++;
1786
1814
  }
1787
1815
  else if (t.startsWith('-v') && t.length > 2) {
1788
- const nv = t.slice(2);
1789
- const eq = nv.indexOf('=');
1790
- if (eq > 0)
1791
- vvars.push([nv.slice(0, eq), nv.slice(eq + 1)]);
1816
+ addVVar(t.slice(2));
1792
1817
  }
1793
1818
  }
1794
1819
  if (operandWords.length === 0) {
@@ -523,6 +523,20 @@ const head = (args, ctx) => {
523
523
  /* ------------------------------------------------------------------ */
524
524
  /* tail */
525
525
  /* ------------------------------------------------------------------ */
526
+ /** A count embedded in generated PS must be a canonical Int32 literal. */
527
+ function tailCountLiteral(raw) {
528
+ if (!/^[+-]?\d+$/.test(raw))
529
+ return null;
530
+ try {
531
+ const n = BigInt(raw.replace(/^[+-]/, ''));
532
+ if (n > 2147483647n)
533
+ return null;
534
+ return n.toString();
535
+ }
536
+ catch {
537
+ return null;
538
+ }
539
+ }
526
540
  const tail = (args, ctx) => {
527
541
  const pre = parseWords(args);
528
542
  if (pre.flags.has('f') || pre.flags.has('F')) {
@@ -533,9 +547,35 @@ const tail = (args, ctx) => {
533
547
  let nLines = null;
534
548
  let fromLine = null;
535
549
  let nBytes = null;
550
+ let fromByte = false;
536
551
  const operandWords = [];
537
552
  let quiet = false;
538
553
  let verbose = false;
554
+ const setCount = (kind, raw) => {
555
+ const literal = tailCountLiteral(raw);
556
+ if (literal === null) {
557
+ return psErrExpr(psStr("tail: invalid number of " + kind + ": '" + raw + "'"));
558
+ }
559
+ if (kind === 'bytes') {
560
+ nBytes = literal;
561
+ fromByte = raw.startsWith('+');
562
+ nLines = null;
563
+ fromLine = null;
564
+ }
565
+ else {
566
+ nBytes = null;
567
+ fromByte = false;
568
+ if (raw.startsWith('+')) {
569
+ fromLine = literal;
570
+ nLines = null;
571
+ }
572
+ else {
573
+ nLines = literal;
574
+ fromLine = null;
575
+ }
576
+ }
577
+ return null;
578
+ };
539
579
  {
540
580
  let i = 0;
541
581
  let onlyOps = false;
@@ -567,12 +607,9 @@ const tail = (args, ctx) => {
567
607
  else {
568
608
  i++;
569
609
  }
570
- if (name === '--bytes')
571
- nBytes = val;
572
- else if (val.startsWith('+'))
573
- fromLine = val.slice(1);
574
- else
575
- nLines = val.replace(/^-/, '');
610
+ const err = setCount(name === '--bytes' ? 'bytes' : 'lines', val);
611
+ if (err !== null)
612
+ return err;
576
613
  continue;
577
614
  }
578
615
  if (name === '--quiet' || name === '--silent') {
@@ -589,49 +626,45 @@ const tail = (args, ctx) => {
589
626
  continue;
590
627
  }
591
628
  let m;
592
- if (t === '-n' || t === '-c') {
593
- const val = i + 1 < args.length ? wordToString(args[i + 1]) : null;
594
- if (val === null) {
595
- return psErrExpr(psStr('tail: option requires an argument -- ' + t.slice(1)));
596
- }
597
- if (t === '-c')
598
- nBytes = val;
599
- else if (val.startsWith('+'))
600
- fromLine = val.slice(1);
601
- else
602
- nLines = val.replace(/^-/, ''); // -n -N ≡ -n N (last N)
603
- i += 2;
604
- continue;
605
- }
606
- if ((m = t.match(/^-[nc](.*)$/)) !== null) {
607
- const val = m[1];
608
- if (t[1] === 'c')
609
- nBytes = val;
610
- else if (val.startsWith('+'))
611
- fromLine = val.slice(1);
612
- else
613
- nLines = val.replace(/^-/, '');
614
- i++;
615
- continue;
616
- }
617
629
  if ((m = t.match(/^-(\d+)$/)) !== null) {
618
- nLines = m[1];
630
+ const err = setCount('lines', t);
631
+ if (err !== null)
632
+ return err;
619
633
  i++;
620
634
  continue;
621
635
  }
622
636
  if ((m = t.match(/^\+(\d+)$/)) !== null) {
623
- fromLine = m[1];
637
+ const err = setCount('lines', t);
638
+ if (err !== null)
639
+ return err;
624
640
  i++;
625
641
  continue;
626
642
  }
627
643
  if (t.startsWith('-') && t.length > 1) {
628
- for (const ch of t.slice(1)) {
644
+ const body = t.slice(1);
645
+ let usedNext = false;
646
+ for (let c = 0; c < body.length; c++) {
647
+ const ch = body[c];
629
648
  if (ch === 'q')
630
649
  quiet = true;
631
650
  else if (ch === 'v')
632
651
  verbose = true;
652
+ else if (ch === 'n' || ch === 'c') {
653
+ let val = body.slice(c + 1);
654
+ if (val === '') {
655
+ if (i + 1 >= args.length) {
656
+ return psErrExpr(psStr('tail: option requires an argument -- ' + ch));
657
+ }
658
+ val = wordToString(args[i + 1]);
659
+ usedNext = true;
660
+ }
661
+ const err = setCount(ch === 'c' ? 'bytes' : 'lines', val);
662
+ if (err !== null)
663
+ return err;
664
+ break;
665
+ }
633
666
  }
634
- i++;
667
+ i += usedNext ? 2 : 1;
635
668
  continue;
636
669
  }
637
670
  operandWords.push(args[i]);
@@ -639,13 +672,7 @@ const tail = (args, ctx) => {
639
672
  }
640
673
  }
641
674
  const bytesMode = nBytes !== null;
642
- const countLit = bytesMode
643
- ? nBytes
644
- : fromLine !== null
645
- ? fromLine
646
- : nLines !== null
647
- ? nLines
648
- : '10';
675
+ const countLit = bytesMode ? nBytes : fromLine ?? nLines ?? '10';
649
676
  const lines = [
650
677
  PS_GLOB_FN,
651
678
  PS_READTEXT_FN,
@@ -659,7 +686,9 @@ const tail = (args, ctx) => {
659
686
  lines.push(STDIN_INLINES);
660
687
  lines.push(...psCollectFiles(operandWords, (g) => qErr('tail', g, 'No such file or directory'), (g) => qErr('tail', g, 'Is a directory', 'error reading ')), '$fx_count = [int](' + countLit + ')', '$fx_from = ' + pb(fromLine !== null && !bytesMode), '$fx_hdr = ((($fx_srcs.Count -gt 1) -and ' + pb(!quiet) + ') -or ' + pb(verbose) + ')', '$fx_first = $true');
661
688
  if (bytesMode) {
662
- lines.push('$fx_out = New-Object System.Text.StringBuilder', 'for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_txt = (fx-stdinraw $fx_items); $fx_disp = 'standard input' }", ' else { $fx_txt = fx-read $fx_g; $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', ' if (-not $fx_first) { [void]$fx_out.Append([string][char]10) }', " [void]$fx_out.Append('==> ' + $fx_disp + ' <==' + [string][char]10)", ' }', ' $fx_first = $false', ' $fx_st = $fx_txt.Length - $fx_count', ' if ($fx_st -lt 0) { $fx_st = 0 }', ' if ($fx_st -lt $fx_txt.Length) { [void]$fx_out.Append($fx_txt.Substring($fx_st)) }', '}', 'fx-write $fx_out.ToString() $fx_term');
689
+ lines.push('$fx_out = New-Object System.Text.StringBuilder', 'for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_txt = (fx-stdinraw $fx_items); $fx_disp = 'standard input' }", ' else { $fx_txt = fx-read $fx_g; $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', ' if (-not $fx_first) { [void]$fx_out.Append([string][char]10) }', " [void]$fx_out.Append('==> ' + $fx_disp + ' <==' + [string][char]10)", ' }', ' $fx_first = $false', fromByte
690
+ ? ' $fx_st = $fx_count - 1'
691
+ : ' $fx_st = $fx_txt.Length - $fx_count', ' if ($fx_st -lt 0) { $fx_st = 0 }', ' if ($fx_st -lt $fx_txt.Length) { [void]$fx_out.Append($fx_txt.Substring($fx_st)) }', '}', 'fx-write $fx_out.ToString() $fx_term');
663
692
  }
664
693
  else {
665
694
  lines.push('for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_ls = @($fx_in); $fx_disp = 'standard input' }", ' else { $fx_ls = @(fx-splitlines (fx-read $fx_g)); $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', " if (-not $fx_first) { '' }", " '==> ' + $fx_disp + ' <=='", ' }', ' $fx_first = $false', ' if ($fx_from) {', ' $fx_st = $fx_count - 1', ' if ($fx_st -lt 0) { $fx_st = 0 }', ' } else {', ' $fx_st = $fx_ls.Count - $fx_count', ' if ($fx_st -lt 0) { $fx_st = 0 }', ' }', ' for ($fx_i = $fx_st; $fx_i -lt $fx_ls.Count; $fx_i++) { $fx_ls[$fx_i] }', '}');
package/dist/doctor.d.ts CHANGED
@@ -18,3 +18,4 @@ export declare function hasCodexFauxnix(text: string): boolean;
18
18
  export declare function hasOpenCodeFauxnix(data: unknown): boolean;
19
19
  export declare function isServerMap(value: unknown): boolean;
20
20
  export declare function serverMapHasFauxnix(value: unknown): boolean;
21
+ export declare function fauxnixServerNames(value: unknown): string[];
package/dist/doctor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { resolveQwenLaunchTuple, sameQwenLaunchTuple } from './qwen-launch.js';
4
5
  const VALUE_INDENT = ' ';
5
6
  export async function collectDoctorReport(opts = {}) {
6
7
  const home = opts.home ?? homedir();
@@ -13,6 +14,7 @@ export async function collectDoctorReport(opts = {}) {
13
14
  lines.push(field('claude', detectClaude(home, cwd, env)));
14
15
  lines.push(field('codex', detectCodex(home, env)));
15
16
  lines.push(field('opencode', detectOpenCode(home, env)));
17
+ lines.push(field('qwen', detectQwen(home)));
16
18
  lines.push('');
17
19
  const mcp = await mcpLines(nodeVersion, opts.loadMcp);
18
20
  lines.push(...mcp.lines);
@@ -159,6 +161,41 @@ function detectOpenCode(home, env) {
159
161
  return `fauxnix MCP configured (${path})`;
160
162
  return `found ${path}, fauxnix MCP not listed — add mcp.fauxnix (see README)`;
161
163
  }
164
+ function detectQwen(home) {
165
+ const path = join(home, '.qwen', 'settings.json');
166
+ if (!existsSync(path))
167
+ return 'not detected — see README';
168
+ const text = readText(path);
169
+ if (text === undefined)
170
+ return `found ${path} (unreadable) — see README`;
171
+ let data;
172
+ try {
173
+ data = JSON.parse(stripBom(text));
174
+ }
175
+ catch {
176
+ return `found ${path} (unreadable JSON) — see README`;
177
+ }
178
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
179
+ return `found ${path} (not a JSON object) — see README`;
180
+ }
181
+ const servers = data.mcpServers;
182
+ if (!isServerMap(servers)) {
183
+ return `found ${path}, fauxnix MCP not listed — run: fauxnix install --qwen`;
184
+ }
185
+ const extraNames = fauxnixServerNames(servers).filter((name) => name !== 'fauxnix');
186
+ if (extraNames.length) {
187
+ return `found ${path}, additional fauxnix MCP entries (${extraNames.join(', ')}) — remove them, then run: fauxnix install --qwen`;
188
+ }
189
+ const config = servers.fauxnix;
190
+ const expected = resolveQwenLaunchTuple();
191
+ if (expected.ok && sameQwenLaunchTuple(config, expected.value)) {
192
+ return `fauxnix MCP configured with an absolute launcher (${path})`;
193
+ }
194
+ if (config != null || serverMapHasFauxnix(servers)) {
195
+ return `found ${path}, fauxnix MCP launcher does not match this installation — run: fauxnix install --qwen`;
196
+ }
197
+ return `found ${path}, fauxnix MCP not listed — run: fauxnix install --qwen`;
198
+ }
162
199
  export function hasOpenCodeFauxnix(data) {
163
200
  if (!data || typeof data !== 'object' || Array.isArray(data))
164
201
  return false;
@@ -174,15 +211,19 @@ export function isServerMap(value) {
174
211
  return !!value && typeof value === 'object' && !Array.isArray(value);
175
212
  }
176
213
  export function serverMapHasFauxnix(value) {
214
+ return fauxnixServerNames(value).length > 0;
215
+ }
216
+ export function fauxnixServerNames(value) {
177
217
  if (!isServerMap(value))
178
- return false;
218
+ return [];
219
+ const names = [];
179
220
  for (const [name, cfg] of Object.entries(value)) {
180
221
  if (name === 'servers')
181
222
  continue;
182
223
  if (looksLikeFauxnixServer(name, cfg))
183
- return true;
224
+ names.push(name);
184
225
  }
185
- return false;
226
+ return names;
186
227
  }
187
228
  function looksLikeFauxnixServer(name, cfg) {
188
229
  if (/^fauxnix(-cli)?$/i.test(name))
package/dist/errors.js CHANGED
@@ -43,7 +43,7 @@ function unescapeClixml(t) {
43
43
  .trim();
44
44
  }
45
45
  /**
46
- * When stderr is redirected, powershell.exe serializes error records as
46
+ * When stderr is redirected, PowerShell can serialize error records as
47
47
  * CLIXML (`#< CLIXML` + XML). Unwrap the real message lines and drop
48
48
  * progress records, so agents see plain bash-style text.
49
49
  */
@@ -76,8 +76,10 @@ export function normalizeStderr(stderr) {
76
76
  let m = line.match(/^The term '(.+?)' is not recognized/);
77
77
  if (m)
78
78
  return commandNotFound(m[1]);
79
- // zh-CN: 无法将"x"项识别为 cmdlet、函数、脚本文件或可运行程序的名称
80
- m = line.match(/^无法将["'”]?([^"'”]+)["'”]?项识别为/);
79
+ // zh-CN: & : 无法将“x”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
80
+ // Require the PowerShell-specific suffix so ordinary Chinese prose that
81
+ // happens to contain “无法将…项识别为” is not rewritten.
82
+ m = line.match(/^(?:\S+\s*:\s*)?无法将[“"'‘]?(.+?)[”"'’]?项识别为\s*cmdlet、函数、脚本文件或可运行程序的名称(?:。|$)/);
81
83
  if (m)
82
84
  return commandNotFound(m[1]);
83
85
  // "x : The term 'y' is not recognized ..." (with source prefix)
@@ -94,18 +96,26 @@ export function normalizeStderr(stderr) {
94
96
  ': No such file or directory');
95
97
  }
96
98
  // zh-CN: "Get-Content : 找不到路径“X”,因为该路径不存在。"
97
- m = line.match(/^(\S+)\s*:\s*找不到路径[“'"](.+?)[”'"],因为该路径不存在\.?$/);
99
+ m = line.match(/^(\S+)\s*:\s*找不到路径[“"'](.+?)[”"'],因为该路径不存在[。.]?$/);
98
100
  if (m) {
99
101
  return m[1].toLowerCase() + ': ' + m[2].replace(/\\/g, '/') + ': No such file or directory';
100
102
  }
101
103
  // "cat : Cannot find drive. A drive with the name 'z' does not exist."
102
104
  m = line.match(/^(\S+)\s*:\s*Cannot find drive\..*name '(.+?)'.*$/);
105
+ if (m)
106
+ return m[1].toLowerCase() + ': ' + m[2] + ': No such file or directory';
107
+ // zh-CN: "Get-Content : 找不到驱动器。名为“Z”的驱动器不存在。"
108
+ m = line.match(/^(\S+)\s*:\s*找不到驱动器[。.]\s*名为[“"'‘](.+?)[”"'’]的驱动器不存在[。.]?$/);
103
109
  if (m)
104
110
  return m[1].toLowerCase() + ': ' + m[2] + ': No such file or directory';
105
111
  // "rm : Cannot remove item ... Access is denied"
106
112
  m = line.match(/^(\S+)\s*:\s*(.*)Access to the path '(.+?)' is denied\.?$/);
107
113
  if (m)
108
114
  return m[1].toLowerCase() + ': cannot remove \'' + m[3] + '\': Permission denied';
115
+ // zh-CN: "rm : 对路径“C:\\protected”的访问被拒绝。"
116
+ m = line.match(/^(\S+)\s*:\s*对路径[“"'‘](.+?)[”"'’]的访问被拒绝[。.]?$/);
117
+ if (m)
118
+ return m[1].toLowerCase() + ': cannot remove \'' + m[2] + '\': Permission denied';
109
119
  // leftover PS not-recognized lines that the rewrites above did not catch
110
120
  if (/\.sh'?/.test(line) && /is not recognized/.test(line)) {
111
121
  return line + SH_SCRIPT_WINDOWS_HINT;
@@ -29,12 +29,15 @@ export declare class FauxnixSession {
29
29
  private hostFile;
30
30
  private host;
31
31
  private lifecycleLock;
32
+ /** True once `env` was loaded from the host's complete environment snapshot. */
33
+ private hasEnvSnapshot;
34
+ private readonly powerShell;
32
35
  constructor();
33
36
  private bindFiles;
34
37
  private withLock;
35
38
  private syncFromDisk;
36
39
  private ensureHost;
37
- /** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
40
+ /** Boot the selected PowerShell now so the first run() is not a cold start. */
38
41
  prewarm(): Promise<void>;
39
42
  dispose(): Promise<void>;
40
43
  /** Kill the host and re-prewarm the same session object (no second FauxnixSession). */