fauxnix-cli 0.9.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -26
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -20
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +169 -25
- package/dist/commands/install-all.js +4 -2
- package/dist/commands/net.d.ts +9 -0
- package/dist/commands/net.js +51 -15
- package/dist/commands/sysinfo.d.ts +2 -1
- package/dist/commands/sysinfo.js +610 -82
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +99 -13
- package/dist/commands/text-io.js +72 -43
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +292 -0
- package/dist/errors.js +14 -4
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +194 -51
- package/dist/install.d.ts +15 -0
- package/dist/install.js +247 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +49 -26
- package/dist/parser.js +432 -35
- package/dist/powershell.d.ts +22 -0
- package/dist/powershell.js +129 -0
- package/dist/ps-host.d.ts +41 -13
- package/dist/ps-host.js +302 -40
- package/dist/qwen-launch.d.ts +12 -0
- package/dist/qwen-launch.js +29 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +109 -1
- package/dist/translator.d.ts +42 -9
- package/dist/translator.js +697 -95
- package/package.json +3 -1
|
@@ -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 === '
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2293
|
-
const
|
|
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,
|
package/dist/commands/text-io.js
CHANGED
|
@@ -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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
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
ADDED
|
@@ -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
|
+
}
|