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.
@@ -1,3 +1,4 @@
1
1
  import { CommandSpec, Handler } from '../registry.js';
2
2
  export declare const specs: CommandSpec[];
3
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
3
4
  export declare const handlers: Record<string, Handler>;
@@ -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) {
@@ -2289,8 +2314,9 @@ function parseCutList(list) {
2289
2314
  const cut = (args) => {
2290
2315
  const { flags, longs, values, operandWords } = parseWords(args, ['d', 'f', 'c', 'b'], []);
2291
2316
  const complement = longs.has('--complement');
2292
- const charsMode = flags.has('c') || flags.has('b');
2293
- const fieldsMode = flags.has('f');
2317
+ // -f/-c/-b are value-taking; parseWords records them in `values`, not `flags`.
2318
+ const charsMode = values.has('-c') || values.has('-b');
2319
+ const fieldsMode = values.has('-f');
2294
2320
  const suppress = flags.has('s');
2295
2321
  if (charsMode && fieldsMode) {
2296
2322
  throw new FauxnixParseError('fauxnix: cut only one type of list may be specified');
@@ -2527,7 +2553,67 @@ export const specs = [
2527
2553
  usageExit: 2,
2528
2554
  handler: grep,
2529
2555
  },
2556
+ {
2557
+ names: ['sort'],
2558
+ options: [
2559
+ { short: 'r', long: '--reverse', support: 'implemented' },
2560
+ { short: 'n', long: '--numeric-sort', support: 'implemented' },
2561
+ { short: 'u', long: '--unique', support: 'implemented' },
2562
+ { short: 'f', long: '--ignore-case', support: 'implemented' },
2563
+ { short: 'b', long: '--ignore-leading-blanks', support: 'implemented' },
2564
+ { short: 't', takesValue: true, support: 'implemented' },
2565
+ { short: 'k', takesValue: true, support: 'implemented' },
2566
+ { short: 'z', long: '--zero-terminated', support: 'unsupported', reason: 'NUL-terminated records' },
2567
+ ],
2568
+ effects: ['read'],
2569
+ platform: 'windows-ps51',
2570
+ dispatch: 'translated',
2571
+ usageExit: 2,
2572
+ handler: sort,
2573
+ },
2574
+ {
2575
+ names: ['uniq'],
2576
+ options: [
2577
+ { short: 'c', support: 'implemented' },
2578
+ { short: 'd', support: 'implemented' },
2579
+ { short: 'u', support: 'implemented' },
2580
+ { short: 'i', support: 'implemented' },
2581
+ ],
2582
+ effects: ['read'],
2583
+ platform: 'windows-ps51',
2584
+ dispatch: 'translated',
2585
+ handler: uniq,
2586
+ },
2587
+ {
2588
+ names: ['cut'],
2589
+ options: [
2590
+ { short: 'd', takesValue: true, support: 'implemented' },
2591
+ { short: 'f', takesValue: true, support: 'implemented' },
2592
+ { short: 'c', takesValue: true, support: 'implemented' },
2593
+ { short: 'b', takesValue: true, support: 'implemented' },
2594
+ { short: 's', support: 'implemented' },
2595
+ { long: '--complement', support: 'implemented' },
2596
+ ],
2597
+ effects: ['read'],
2598
+ platform: 'windows-ps51',
2599
+ dispatch: 'translated',
2600
+ handler: cut,
2601
+ },
2602
+ {
2603
+ names: ['tr'],
2604
+ options: [
2605
+ { short: 'd', support: 'implemented' },
2606
+ { short: 's', support: 'implemented' },
2607
+ { short: 'c', long: '--complement', support: 'unsupported', reason: 'complement' },
2608
+ { short: 'C', support: 'unsupported', reason: 'complement' },
2609
+ ],
2610
+ effects: ['read'],
2611
+ platform: 'windows-ps51',
2612
+ dispatch: 'translated',
2613
+ handler: tr,
2614
+ },
2530
2615
  ];
2616
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
2531
2617
  export const handlers = {
2532
2618
  egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
2533
2619
  sed,
@@ -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] }', '}');
@@ -0,0 +1,21 @@
1
+ export type DoctorOptions = {
2
+ home?: string;
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ nodeVersion?: string;
6
+ /** Injected MCP-module loader. Default: dynamic import of ./mcp.js (does not start the server). */
7
+ loadMcp?: () => Promise<unknown>;
8
+ };
9
+ export type DoctorReport = {
10
+ lines: string[];
11
+ ok: boolean;
12
+ };
13
+ export declare function collectDoctorReport(opts?: DoctorOptions): Promise<DoctorReport>;
14
+ export declare function claudeUserConfigPath(home: string, env: NodeJS.ProcessEnv): string;
15
+ export declare function codexConfigPath(home: string, env: NodeJS.ProcessEnv): string;
16
+ export declare function openCodeConfigPath(home: string, env: NodeJS.ProcessEnv): string;
17
+ export declare function hasCodexFauxnix(text: string): boolean;
18
+ export declare function hasOpenCodeFauxnix(data: unknown): boolean;
19
+ export declare function isServerMap(value: unknown): boolean;
20
+ export declare function serverMapHasFauxnix(value: unknown): boolean;
21
+ export declare function fauxnixServerNames(value: unknown): string[];
package/dist/doctor.js ADDED
@@ -0,0 +1,292 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { resolveQwenLaunchTuple, sameQwenLaunchTuple } from './qwen-launch.js';
5
+ const VALUE_INDENT = ' ';
6
+ export async function collectDoctorReport(opts = {}) {
7
+ const home = opts.home ?? homedir();
8
+ const cwd = opts.cwd ?? process.cwd();
9
+ const env = opts.env ?? process.env;
10
+ const nodeVersion = opts.nodeVersion ?? process.version;
11
+ const lines = [''];
12
+ lines.push(...encodingLines(env));
13
+ lines.push('');
14
+ lines.push(field('claude', detectClaude(home, cwd, env)));
15
+ lines.push(field('codex', detectCodex(home, env)));
16
+ lines.push(field('opencode', detectOpenCode(home, env)));
17
+ lines.push(field('qwen', detectQwen(home)));
18
+ lines.push('');
19
+ const mcp = await mcpLines(nodeVersion, opts.loadMcp);
20
+ lines.push(...mcp.lines);
21
+ return { lines, ok: mcp.ok };
22
+ }
23
+ function field(label, value) {
24
+ return `${label.padEnd(10)} : ${value}`;
25
+ }
26
+ function encodingLines(env) {
27
+ const raw = env.FAUXNIX_NATIVE_ENCODING;
28
+ const current = raw === undefined || raw === ''
29
+ ? 'unset → utf8 (default)'
30
+ : raw === 'ansi'
31
+ ? 'ansi → GBK-native admin tools'
32
+ : `${raw} → utf8 (only ansi selects GBK)`;
33
+ return [
34
+ field('encoding', 'UTF-8 default for native-tool pipelines'),
35
+ VALUE_INDENT + `current FAUXNIX_NATIVE_ENCODING=${current}`,
36
+ VALUE_INDENT + 'set FAUXNIX_NATIVE_ENCODING=ansi for GBK-native admin tools (ipconfig, tasklist)',
37
+ ];
38
+ }
39
+ function detectClaude(home, cwd, env) {
40
+ const userPath = claudeUserConfigPath(home, env);
41
+ const projectPath = join(cwd, '.mcp.json');
42
+ const userExists = existsSync(userPath);
43
+ const projectExists = existsSync(projectPath);
44
+ let user;
45
+ let project;
46
+ if (userExists)
47
+ user = inspectClaudeJson(userPath);
48
+ if (projectExists) {
49
+ const inspected = inspectClaudeJson(projectPath);
50
+ // Project-scope Claude MCP is always a top-level mcpServers object.
51
+ if (!inspected.parseError && inspected.hasTopLevelMcpServers)
52
+ project = inspected;
53
+ }
54
+ if (!userExists && !project)
55
+ return 'not detected — see README';
56
+ if (user?.hasFauxnix)
57
+ return `fauxnix MCP configured (${userPath})`;
58
+ if (project?.hasFauxnix)
59
+ return `fauxnix MCP configured (${projectPath})`;
60
+ if (userExists && user?.parseError) {
61
+ return `found ${userPath} (unreadable JSON) — see README`;
62
+ }
63
+ if (userExists) {
64
+ return `found ${userPath}, fauxnix MCP not listed — run: claude mcp add fauxnix -- fauxnix mcp`;
65
+ }
66
+ return `found ${projectPath}, fauxnix MCP not listed — see README`;
67
+ }
68
+ export function claudeUserConfigPath(home, env) {
69
+ const dir = env.CLAUDE_CONFIG_DIR?.trim();
70
+ if (dir)
71
+ return join(dir, '.claude.json');
72
+ return join(home, '.claude.json');
73
+ }
74
+ export function codexConfigPath(home, env) {
75
+ const codexHome = env.CODEX_HOME?.trim() || join(home, '.codex');
76
+ return join(codexHome, 'config.toml');
77
+ }
78
+ export function openCodeConfigPath(home, env) {
79
+ const xdg = env.XDG_CONFIG_HOME?.trim() || join(home, '.config');
80
+ return join(xdg, 'opencode', 'opencode.json');
81
+ }
82
+ function inspectClaudeJson(path) {
83
+ const text = readText(path);
84
+ if (text === undefined) {
85
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
86
+ }
87
+ let data;
88
+ try {
89
+ data = JSON.parse(stripBom(text));
90
+ }
91
+ catch {
92
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
93
+ }
94
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
95
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
96
+ }
97
+ const rec = data;
98
+ const hasTopLevelMcpServers = isServerMap(rec.mcpServers);
99
+ let hasFauxnix = hasTopLevelMcpServers && serverMapHasFauxnix(rec.mcpServers);
100
+ const projects = rec.projects;
101
+ if (projects && typeof projects === 'object' && !Array.isArray(projects)) {
102
+ for (const proj of Object.values(projects)) {
103
+ if (!proj || typeof proj !== 'object' || Array.isArray(proj))
104
+ continue;
105
+ const servers = proj.mcpServers;
106
+ if (isServerMap(servers) && serverMapHasFauxnix(servers))
107
+ hasFauxnix = true;
108
+ }
109
+ }
110
+ return { parseError: false, hasTopLevelMcpServers, hasFauxnix };
111
+ }
112
+ function detectCodex(home, env) {
113
+ const path = codexConfigPath(home, env);
114
+ if (!existsSync(path))
115
+ return 'not detected — see README';
116
+ const text = readText(path);
117
+ if (text === undefined)
118
+ return `found ${path} (unreadable) — see README`;
119
+ if (hasCodexFauxnix(stripBom(text)))
120
+ return `fauxnix MCP configured (${path})`;
121
+ return `found ${path}, fauxnix MCP not listed — run: codex mcp add fauxnix -- fauxnix mcp`;
122
+ }
123
+ export function hasCodexFauxnix(text) {
124
+ if (/^\s*\[mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')\]/im.test(text))
125
+ return true;
126
+ const tables = text.split(/^\s*\[/m);
127
+ for (const table of tables) {
128
+ if (!/^mcp_servers\./i.test(table))
129
+ continue;
130
+ const header = (table.split(/[\]\r\n]/, 1)[0] ?? '').trim();
131
+ if (/^mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')$/i.test(header))
132
+ return true;
133
+ const cmd = /^\s*command\s*=\s*(?:"([^"]*)"|'([^']*)')/im.exec(table);
134
+ const command = cmd?.[1] ?? cmd?.[2];
135
+ if (command && isFauxnixExecutable(command))
136
+ return true;
137
+ const args = /^\s*args\s*=\s*\[([^\]]*)\]/im.exec(table);
138
+ if (args) {
139
+ const items = [...args[1].matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? '');
140
+ if (items.some(isFauxnixExecutable))
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ function detectOpenCode(home, env) {
147
+ const path = openCodeConfigPath(home, env);
148
+ if (!existsSync(path))
149
+ return 'not detected — see README';
150
+ const text = readText(path);
151
+ if (text === undefined)
152
+ return `found ${path} (unreadable) — see README`;
153
+ let data;
154
+ try {
155
+ data = JSON.parse(stripBom(text));
156
+ }
157
+ catch {
158
+ return `found ${path} (unreadable JSON) — see README`;
159
+ }
160
+ if (hasOpenCodeFauxnix(data))
161
+ return `fauxnix MCP configured (${path})`;
162
+ return `found ${path}, fauxnix MCP not listed — add mcp.fauxnix (see README)`;
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
+ }
199
+ export function hasOpenCodeFauxnix(data) {
200
+ if (!data || typeof data !== 'object' || Array.isArray(data))
201
+ return false;
202
+ const mcp = data.mcp;
203
+ if (!isServerMap(mcp))
204
+ return false;
205
+ if (serverMapHasFauxnix(mcp))
206
+ return true;
207
+ const nested = mcp.servers;
208
+ return isServerMap(nested) && serverMapHasFauxnix(nested);
209
+ }
210
+ export function isServerMap(value) {
211
+ return !!value && typeof value === 'object' && !Array.isArray(value);
212
+ }
213
+ export function serverMapHasFauxnix(value) {
214
+ return fauxnixServerNames(value).length > 0;
215
+ }
216
+ export function fauxnixServerNames(value) {
217
+ if (!isServerMap(value))
218
+ return [];
219
+ const names = [];
220
+ for (const [name, cfg] of Object.entries(value)) {
221
+ if (name === 'servers')
222
+ continue;
223
+ if (looksLikeFauxnixServer(name, cfg))
224
+ names.push(name);
225
+ }
226
+ return names;
227
+ }
228
+ function looksLikeFauxnixServer(name, cfg) {
229
+ if (/^fauxnix(-cli)?$/i.test(name))
230
+ return true;
231
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
232
+ return false;
233
+ const rec = cfg;
234
+ const chunks = [];
235
+ if (typeof rec.command === 'string')
236
+ chunks.push(rec.command);
237
+ if (Array.isArray(rec.command)) {
238
+ for (const part of rec.command)
239
+ if (typeof part === 'string')
240
+ chunks.push(part);
241
+ }
242
+ if (Array.isArray(rec.args)) {
243
+ for (const part of rec.args)
244
+ if (typeof part === 'string')
245
+ chunks.push(part);
246
+ }
247
+ return chunks.some(isFauxnixExecutable);
248
+ }
249
+ function isFauxnixExecutable(s) {
250
+ const base = s.replace(/\\/g, '/').split('/').pop()?.trim() ?? '';
251
+ return /^fauxnix(-cli)?(\.cmd|\.exe)?$/i.test(base);
252
+ }
253
+ async function mcpLines(nodeVersion, loadMcp) {
254
+ const major = nodeMajor(nodeVersion);
255
+ const nodeOk = major >= 18;
256
+ let moduleOk = false;
257
+ let moduleDetail = '';
258
+ try {
259
+ const mod = await (loadMcp ?? defaultLoadMcp)();
260
+ moduleOk =
261
+ !!mod && typeof mod.startMcpServer === 'function';
262
+ if (!moduleOk)
263
+ moduleDetail = 'startMcpServer export missing';
264
+ }
265
+ catch (e) {
266
+ moduleDetail = e instanceof Error ? e.message : String(e);
267
+ }
268
+ const lines = [
269
+ field('node', `${nodeVersion.startsWith('v') ? nodeVersion : 'v' + nodeVersion}${nodeOk ? ' (>=18 required)' : ' FAILED (requires >=18)'}`),
270
+ field('mcp', moduleOk ? 'module loads' : `FAILED to load${moduleDetail ? ': ' + moduleDetail : ''}`),
271
+ VALUE_INDENT + 'start with: fauxnix mcp',
272
+ ];
273
+ return { lines, ok: nodeOk && moduleOk };
274
+ }
275
+ async function defaultLoadMcp() {
276
+ return import('./mcp.js');
277
+ }
278
+ function nodeMajor(version) {
279
+ const m = /^v?(\d+)/.exec(version);
280
+ return m ? Number(m[1]) : 0;
281
+ }
282
+ function stripBom(text) {
283
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
284
+ }
285
+ function readText(path) {
286
+ try {
287
+ return readFileSync(path, 'utf8');
288
+ }
289
+ catch {
290
+ return undefined;
291
+ }
292
+ }