fauxnix-cli 0.9.2 → 0.11.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.
@@ -583,48 +583,45 @@ const grep = (args) => {
583
583
  scan.push(' $fx_mleft--');
584
584
  if (onlyMatch && !inv) {
585
585
  if (fixed) {
586
+ // GNU -o: emit leftmost-longest matches in input order, not per-needle.
586
587
  if (ci)
587
588
  scan.push(' $lx = $fx_l.ToLower()');
588
589
  const hay = ci ? '$lx' : '$fx_l';
589
- const emitFixedHits = (needle, indent) => {
590
- scan.push(indent + '$p = ' + hay + '.IndexOf(' + needle + ')');
591
- scan.push(indent + 'while ($p -ge 0) {');
592
- scan.push(indent + ' $ok = $true');
593
- if (word) {
594
- scan.push(indent +
595
- " if ($p -gt 0) { $c = " +
596
- hay +
597
- "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
598
- scan.push(indent +
599
- ' if ($ok) { $e = $p + ' +
600
- needle +
601
- '.Length; if ($e -lt ' +
602
- hay +
603
- '.Length) { $c = ' +
604
- hay +
605
- "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
606
- scan.push(indent +
607
- ' if ($ok) { fx-emitline $fx_i (' +
608
- hay +
609
- '.Substring($p, ' +
610
- needle +
611
- '.Length)) }');
612
- }
613
- else {
614
- scan.push(indent + ' fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length))');
615
- }
616
- scan.push(indent + ' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
617
- scan.push(indent + '}');
618
- };
619
- if (multiFixed) {
620
- const arr = ci ? '$fx_needles_ll' : '$fx_needles';
621
- scan.push(' foreach ($fx_needle in ' + arr + ') {');
622
- emitFixedHits('$fx_needle', ' ');
623
- scan.push(' }');
590
+ const needleArr = multiFixed
591
+ ? ci
592
+ ? '$fx_needles_ll'
593
+ : '$fx_needles'
594
+ : '@(' + (ci ? '$fx_needle_ll' : '$fx_needle') + ')';
595
+ scan.push(' $fx_cands = New-Object System.Collections.Generic.List[object]');
596
+ scan.push(' foreach ($fx_needle in ' + needleArr + ') {');
597
+ scan.push(' if ($fx_needle.Length -lt 1) { continue }');
598
+ scan.push(' $p = ' + hay + '.IndexOf($fx_needle)');
599
+ scan.push(' while ($p -ge 0) {');
600
+ if (word) {
601
+ scan.push(' $ok = $true');
602
+ scan.push(" if ($p -gt 0) { $c = " +
603
+ hay +
604
+ "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
605
+ scan.push(' if ($ok) { $e = $p + $fx_needle.Length; if ($e -lt ' +
606
+ hay +
607
+ '.Length) { $c = ' +
608
+ hay +
609
+ "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
610
+ scan.push(' if ($ok) { [void]$fx_cands.Add([pscustomobject]@{ Start = $p; Len = $fx_needle.Length }) }');
624
611
  }
625
612
  else {
626
- emitFixedHits(ci ? '$fx_needle_ll' : '$fx_needle', ' ');
613
+ scan.push(' [void]$fx_cands.Add([pscustomobject]@{ Start = $p; Len = $fx_needle.Length })');
627
614
  }
615
+ scan.push(' $p = ' + hay + '.IndexOf($fx_needle, $p + 1)');
616
+ scan.push(' }');
617
+ scan.push(' }');
618
+ scan.push(' $fx_end = 0');
619
+ scan.push(' foreach ($fx_c in @($fx_cands | Sort-Object Start, @{ Expression = { $_.Len }; Descending = $true })) {');
620
+ scan.push(' if ($fx_c.Start -ge $fx_end) {');
621
+ scan.push(' fx-emitline $fx_i (' + hay + '.Substring($fx_c.Start, $fx_c.Len))');
622
+ scan.push(' $fx_end = $fx_c.Start + $fx_c.Len');
623
+ scan.push(' }');
624
+ scan.push(' }');
628
625
  }
629
626
  else {
630
627
  scan.push(' foreach ($fx_m in $fx_re.Matches($fx_l)) { fx-emitline $fx_i $fx_m.Value }');
@@ -2292,8 +2289,9 @@ function parseCutList(list) {
2292
2289
  const cut = (args) => {
2293
2290
  const { flags, longs, values, operandWords } = parseWords(args, ['d', 'f', 'c', 'b'], []);
2294
2291
  const complement = longs.has('--complement');
2295
- const charsMode = flags.has('c') || flags.has('b');
2296
- const fieldsMode = flags.has('f');
2292
+ // -f/-c/-b are value-taking; parseWords records them in `values`, not `flags`.
2293
+ const charsMode = values.has('-c') || values.has('-b');
2294
+ const fieldsMode = values.has('-f');
2297
2295
  const suppress = flags.has('s');
2298
2296
  if (charsMode && fieldsMode) {
2299
2297
  throw new FauxnixParseError('fauxnix: cut only one type of list may be specified');
@@ -2530,7 +2528,67 @@ export const specs = [
2530
2528
  usageExit: 2,
2531
2529
  handler: grep,
2532
2530
  },
2531
+ {
2532
+ names: ['sort'],
2533
+ options: [
2534
+ { short: 'r', long: '--reverse', support: 'implemented' },
2535
+ { short: 'n', long: '--numeric-sort', support: 'implemented' },
2536
+ { short: 'u', long: '--unique', support: 'implemented' },
2537
+ { short: 'f', long: '--ignore-case', support: 'implemented' },
2538
+ { short: 'b', long: '--ignore-leading-blanks', support: 'implemented' },
2539
+ { short: 't', takesValue: true, support: 'implemented' },
2540
+ { short: 'k', takesValue: true, support: 'implemented' },
2541
+ { short: 'z', long: '--zero-terminated', support: 'unsupported', reason: 'NUL-terminated records' },
2542
+ ],
2543
+ effects: ['read'],
2544
+ platform: 'windows-ps51',
2545
+ dispatch: 'translated',
2546
+ usageExit: 2,
2547
+ handler: sort,
2548
+ },
2549
+ {
2550
+ names: ['uniq'],
2551
+ options: [
2552
+ { short: 'c', support: 'implemented' },
2553
+ { short: 'd', support: 'implemented' },
2554
+ { short: 'u', support: 'implemented' },
2555
+ { short: 'i', support: 'implemented' },
2556
+ ],
2557
+ effects: ['read'],
2558
+ platform: 'windows-ps51',
2559
+ dispatch: 'translated',
2560
+ handler: uniq,
2561
+ },
2562
+ {
2563
+ names: ['cut'],
2564
+ options: [
2565
+ { short: 'd', takesValue: true, support: 'implemented' },
2566
+ { short: 'f', takesValue: true, support: 'implemented' },
2567
+ { short: 'c', takesValue: true, support: 'implemented' },
2568
+ { short: 'b', takesValue: true, support: 'implemented' },
2569
+ { short: 's', support: 'implemented' },
2570
+ { long: '--complement', support: 'implemented' },
2571
+ ],
2572
+ effects: ['read'],
2573
+ platform: 'windows-ps51',
2574
+ dispatch: 'translated',
2575
+ handler: cut,
2576
+ },
2577
+ {
2578
+ names: ['tr'],
2579
+ options: [
2580
+ { short: 'd', support: 'implemented' },
2581
+ { short: 's', support: 'implemented' },
2582
+ { short: 'c', long: '--complement', support: 'unsupported', reason: 'complement' },
2583
+ { short: 'C', support: 'unsupported', reason: 'complement' },
2584
+ ],
2585
+ effects: ['read'],
2586
+ platform: 'windows-ps51',
2587
+ dispatch: 'translated',
2588
+ handler: tr,
2589
+ },
2533
2590
  ];
2591
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
2534
2592
  export const handlers = {
2535
2593
  egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
2536
2594
  sed,
@@ -1093,6 +1093,9 @@ const xargs = (args) => {
1093
1093
  noRunIfEmpty = true;
1094
1094
  else if (ch === 't')
1095
1095
  trace = true;
1096
+ else if (ch === '0') {
1097
+ return psErrExpr(psStr('xargs: -0 is not supported by fauxnix'));
1098
+ }
1096
1099
  else if (ch === 'n' || ch === 'I' || ch === 'L') {
1097
1100
  const restv = body.slice(c + 1);
1098
1101
  let val;
@@ -1140,12 +1143,12 @@ const xargs = (args) => {
1140
1143
  }
1141
1144
  const n = chunkN !== null && Number.isFinite(chunkN) && chunkN > 0 ? chunkN : 0;
1142
1145
  const replExpr = repl !== null ? psStr(repl) : null;
1146
+ // fx-native already records $script:fx_exit from ExitCode.
1143
1147
  const invoke = [
1144
1148
  ' if (' +
1145
1149
  pb(trace) +
1146
1150
  ") { [Console]::Error.WriteLine(((@($fx_cmd) + @($fx_argv)) -join ' ')) }",
1147
- ' & $fx_cmd @fx_argv',
1148
- ' if ($LASTEXITCODE -ne 0 -and $script:fx_exit -eq 0) { $script:fx_exit = $LASTEXITCODE }',
1151
+ ' fx-native $fx_cmd $fx_argv',
1149
1152
  ];
1150
1153
  let dispatch;
1151
1154
  const guard = ' if (' + pb(noRunIfEmpty) + ' -and $fx_args.Count -eq 0) { }' + '\n' + ' else {';
@@ -1154,7 +1157,7 @@ const xargs = (args) => {
1154
1157
  dispatch = [
1155
1158
  guard,
1156
1159
  ' foreach ($fx_l in $fx_args) {',
1157
- ' $fx_argv = @()',
1160
+ ' $fx_argv = [object[]]@()',
1158
1161
  ' $fx_hit = $false',
1159
1162
  ' foreach ($fx_a in $fx_base) {',
1160
1163
  ' if ($fx_a.Contains(' + replExpr + ')) {',
@@ -1175,7 +1178,7 @@ const xargs = (args) => {
1175
1178
  ' $fx_i = 0',
1176
1179
  ' $fx_ran = $false',
1177
1180
  ' while ($fx_i -lt $fx_args.Count) {',
1178
- ' $fx_argv = @($fx_base)',
1181
+ ' $fx_argv = [object[]]@($fx_base)',
1179
1182
  ' $fx_j = 0',
1180
1183
  ' while ($fx_j -lt ' + n + ' -and $fx_i -lt $fx_args.Count) {',
1181
1184
  ' $fx_argv += $fx_args[$fx_i]',
@@ -1185,10 +1188,8 @@ const xargs = (args) => {
1185
1188
  ...invoke,
1186
1189
  ' }',
1187
1190
  ' if (-not $fx_ran) {',
1188
- ' $fx_argv = @($fx_base)',
1189
- ' ' + invoke[0],
1190
- ' ' + invoke[1],
1191
- ' ' + invoke[2],
1191
+ ' $fx_argv = [object[]]@($fx_base)',
1192
+ ...invoke.map((line) => ' ' + line),
1192
1193
  ' }',
1193
1194
  ' }',
1194
1195
  ];
@@ -1196,17 +1197,27 @@ const xargs = (args) => {
1196
1197
  else {
1197
1198
  dispatch = [
1198
1199
  guard,
1199
- ' $fx_argv = @($fx_base) + @($fx_args)',
1200
+ ' $fx_argv = [object[]](@($fx_base) + @($fx_args))',
1200
1201
  ...invoke,
1201
1202
  ' }',
1202
1203
  ];
1203
1204
  }
1205
+ // Default GNU xargs splits on blanks; -I keeps whole lines as one item.
1206
+ const collectArgs = replExpr !== null
1207
+ ? "$fx_args = @($fx_in | Where-Object { $_ -ne '' })"
1208
+ : [
1209
+ '$fx_args = @()',
1210
+ 'foreach ($fx_l in $fx_in) {',
1211
+ " if ($fx_l -eq '') { continue }",
1212
+ " $fx_args += @($fx_l -split '[ \\t]+' | Where-Object { $_ -ne '' })",
1213
+ '}',
1214
+ ].join('\n');
1204
1215
  return [
1205
1216
  PS_SPLITLINES_FN,
1206
1217
  STDIN_INLINES,
1207
1218
  '$fx_tg = ' + argListExpr(target, exprOfWord),
1208
1219
  "if ($fx_tg.Count -eq 0) { $fx_cmd = ''; $fx_base = @() } else { $fx_cmd = [string]$fx_tg[0]; $fx_base = $(if ($fx_tg.Count -gt 1) { @($fx_tg[1..($fx_tg.Count - 1)]) } else { @() }) }",
1209
- "$fx_args = @($fx_in | Where-Object { $_ -ne '' })",
1220
+ collectArgs,
1210
1221
  ...dispatch,
1211
1222
  ].join('\n');
1212
1223
  };
@@ -1236,6 +1247,74 @@ export const specs = [
1236
1247
  dispatch: 'translated',
1237
1248
  handler: head,
1238
1249
  },
1250
+ {
1251
+ names: ['echo'],
1252
+ options: [
1253
+ { short: 'n', support: 'implemented' },
1254
+ { short: 'e', support: 'implemented' },
1255
+ { short: 'E', support: 'implemented' },
1256
+ ],
1257
+ effects: [],
1258
+ platform: 'windows-ps51',
1259
+ dispatch: 'translated',
1260
+ usageExit: 2,
1261
+ leadingOptions: true,
1262
+ handler: echo,
1263
+ },
1264
+ {
1265
+ names: ['printf'],
1266
+ options: [],
1267
+ effects: [],
1268
+ platform: 'windows-ps51',
1269
+ dispatch: 'translated',
1270
+ usageExit: 2,
1271
+ leadingOptions: true,
1272
+ handler: printf,
1273
+ },
1274
+ {
1275
+ names: ['cat'],
1276
+ options: [
1277
+ { short: 'n', support: 'implemented' },
1278
+ { short: 'b', support: 'implemented' },
1279
+ { short: 's', support: 'implemented' },
1280
+ { short: 'E', support: 'implemented' },
1281
+ { short: 'T', support: 'implemented' },
1282
+ { short: 'A', support: 'implemented' },
1283
+ ],
1284
+ effects: ['read'],
1285
+ platform: 'windows-ps51',
1286
+ dispatch: 'translated',
1287
+ handler: cat,
1288
+ },
1289
+ {
1290
+ names: ['tail'],
1291
+ options: [
1292
+ { short: 'n', long: '--lines', takesValue: true, support: 'implemented' },
1293
+ { short: 'c', long: '--bytes', takesValue: true, support: 'implemented' },
1294
+ { short: 'q', long: '--quiet', support: 'implemented' },
1295
+ { long: '--silent', support: 'implemented' },
1296
+ { short: 'v', long: '--verbose', support: 'implemented' },
1297
+ { short: 'f', support: 'unsupported', reason: 'no persistent tty' },
1298
+ { short: 'F', support: 'unsupported', reason: 'no persistent tty' },
1299
+ ],
1300
+ effects: ['read'],
1301
+ platform: 'windows-ps51',
1302
+ dispatch: 'translated',
1303
+ handler: tail,
1304
+ },
1305
+ {
1306
+ names: ['wc'],
1307
+ options: [
1308
+ { short: 'l', support: 'implemented' },
1309
+ { short: 'w', support: 'implemented' },
1310
+ { short: 'c', support: 'implemented' },
1311
+ { short: 'm', support: 'implemented' },
1312
+ ],
1313
+ effects: ['read'],
1314
+ platform: 'windows-ps51',
1315
+ dispatch: 'translated',
1316
+ handler: wc,
1317
+ },
1239
1318
  ];
1240
1319
  export const handlers = {
1241
1320
  echo,
@@ -0,0 +1,20 @@
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;
package/dist/doctor.js ADDED
@@ -0,0 +1,251 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const VALUE_INDENT = ' ';
5
+ export async function collectDoctorReport(opts = {}) {
6
+ const home = opts.home ?? homedir();
7
+ const cwd = opts.cwd ?? process.cwd();
8
+ const env = opts.env ?? process.env;
9
+ const nodeVersion = opts.nodeVersion ?? process.version;
10
+ const lines = [''];
11
+ lines.push(...encodingLines(env));
12
+ lines.push('');
13
+ lines.push(field('claude', detectClaude(home, cwd, env)));
14
+ lines.push(field('codex', detectCodex(home, env)));
15
+ lines.push(field('opencode', detectOpenCode(home, env)));
16
+ lines.push('');
17
+ const mcp = await mcpLines(nodeVersion, opts.loadMcp);
18
+ lines.push(...mcp.lines);
19
+ return { lines, ok: mcp.ok };
20
+ }
21
+ function field(label, value) {
22
+ return `${label.padEnd(10)} : ${value}`;
23
+ }
24
+ function encodingLines(env) {
25
+ const raw = env.FAUXNIX_NATIVE_ENCODING;
26
+ const current = raw === undefined || raw === ''
27
+ ? 'unset → utf8 (default)'
28
+ : raw === 'ansi'
29
+ ? 'ansi → GBK-native admin tools'
30
+ : `${raw} → utf8 (only ansi selects GBK)`;
31
+ return [
32
+ field('encoding', 'UTF-8 default for native-tool pipelines'),
33
+ VALUE_INDENT + `current FAUXNIX_NATIVE_ENCODING=${current}`,
34
+ VALUE_INDENT + 'set FAUXNIX_NATIVE_ENCODING=ansi for GBK-native admin tools (ipconfig, tasklist)',
35
+ ];
36
+ }
37
+ function detectClaude(home, cwd, env) {
38
+ const userPath = claudeUserConfigPath(home, env);
39
+ const projectPath = join(cwd, '.mcp.json');
40
+ const userExists = existsSync(userPath);
41
+ const projectExists = existsSync(projectPath);
42
+ let user;
43
+ let project;
44
+ if (userExists)
45
+ user = inspectClaudeJson(userPath);
46
+ if (projectExists) {
47
+ const inspected = inspectClaudeJson(projectPath);
48
+ // Project-scope Claude MCP is always a top-level mcpServers object.
49
+ if (!inspected.parseError && inspected.hasTopLevelMcpServers)
50
+ project = inspected;
51
+ }
52
+ if (!userExists && !project)
53
+ return 'not detected — see README';
54
+ if (user?.hasFauxnix)
55
+ return `fauxnix MCP configured (${userPath})`;
56
+ if (project?.hasFauxnix)
57
+ return `fauxnix MCP configured (${projectPath})`;
58
+ if (userExists && user?.parseError) {
59
+ return `found ${userPath} (unreadable JSON) — see README`;
60
+ }
61
+ if (userExists) {
62
+ return `found ${userPath}, fauxnix MCP not listed — run: claude mcp add fauxnix -- fauxnix mcp`;
63
+ }
64
+ return `found ${projectPath}, fauxnix MCP not listed — see README`;
65
+ }
66
+ export function claudeUserConfigPath(home, env) {
67
+ const dir = env.CLAUDE_CONFIG_DIR?.trim();
68
+ if (dir)
69
+ return join(dir, '.claude.json');
70
+ return join(home, '.claude.json');
71
+ }
72
+ export function codexConfigPath(home, env) {
73
+ const codexHome = env.CODEX_HOME?.trim() || join(home, '.codex');
74
+ return join(codexHome, 'config.toml');
75
+ }
76
+ export function openCodeConfigPath(home, env) {
77
+ const xdg = env.XDG_CONFIG_HOME?.trim() || join(home, '.config');
78
+ return join(xdg, 'opencode', 'opencode.json');
79
+ }
80
+ function inspectClaudeJson(path) {
81
+ const text = readText(path);
82
+ if (text === undefined) {
83
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
84
+ }
85
+ let data;
86
+ try {
87
+ data = JSON.parse(stripBom(text));
88
+ }
89
+ catch {
90
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
91
+ }
92
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
93
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
94
+ }
95
+ const rec = data;
96
+ const hasTopLevelMcpServers = isServerMap(rec.mcpServers);
97
+ let hasFauxnix = hasTopLevelMcpServers && serverMapHasFauxnix(rec.mcpServers);
98
+ const projects = rec.projects;
99
+ if (projects && typeof projects === 'object' && !Array.isArray(projects)) {
100
+ for (const proj of Object.values(projects)) {
101
+ if (!proj || typeof proj !== 'object' || Array.isArray(proj))
102
+ continue;
103
+ const servers = proj.mcpServers;
104
+ if (isServerMap(servers) && serverMapHasFauxnix(servers))
105
+ hasFauxnix = true;
106
+ }
107
+ }
108
+ return { parseError: false, hasTopLevelMcpServers, hasFauxnix };
109
+ }
110
+ function detectCodex(home, env) {
111
+ const path = codexConfigPath(home, env);
112
+ if (!existsSync(path))
113
+ return 'not detected — see README';
114
+ const text = readText(path);
115
+ if (text === undefined)
116
+ return `found ${path} (unreadable) — see README`;
117
+ if (hasCodexFauxnix(stripBom(text)))
118
+ return `fauxnix MCP configured (${path})`;
119
+ return `found ${path}, fauxnix MCP not listed — run: codex mcp add fauxnix -- fauxnix mcp`;
120
+ }
121
+ export function hasCodexFauxnix(text) {
122
+ if (/^\s*\[mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')\]/im.test(text))
123
+ return true;
124
+ const tables = text.split(/^\s*\[/m);
125
+ for (const table of tables) {
126
+ if (!/^mcp_servers\./i.test(table))
127
+ continue;
128
+ const header = (table.split(/[\]\r\n]/, 1)[0] ?? '').trim();
129
+ if (/^mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')$/i.test(header))
130
+ return true;
131
+ const cmd = /^\s*command\s*=\s*(?:"([^"]*)"|'([^']*)')/im.exec(table);
132
+ const command = cmd?.[1] ?? cmd?.[2];
133
+ if (command && isFauxnixExecutable(command))
134
+ return true;
135
+ const args = /^\s*args\s*=\s*\[([^\]]*)\]/im.exec(table);
136
+ if (args) {
137
+ const items = [...args[1].matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? '');
138
+ if (items.some(isFauxnixExecutable))
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+ function detectOpenCode(home, env) {
145
+ const path = openCodeConfigPath(home, env);
146
+ if (!existsSync(path))
147
+ return 'not detected — see README';
148
+ const text = readText(path);
149
+ if (text === undefined)
150
+ return `found ${path} (unreadable) — see README`;
151
+ let data;
152
+ try {
153
+ data = JSON.parse(stripBom(text));
154
+ }
155
+ catch {
156
+ return `found ${path} (unreadable JSON) — see README`;
157
+ }
158
+ if (hasOpenCodeFauxnix(data))
159
+ return `fauxnix MCP configured (${path})`;
160
+ return `found ${path}, fauxnix MCP not listed — add mcp.fauxnix (see README)`;
161
+ }
162
+ export function hasOpenCodeFauxnix(data) {
163
+ if (!data || typeof data !== 'object' || Array.isArray(data))
164
+ return false;
165
+ const mcp = data.mcp;
166
+ if (!isServerMap(mcp))
167
+ return false;
168
+ if (serverMapHasFauxnix(mcp))
169
+ return true;
170
+ const nested = mcp.servers;
171
+ return isServerMap(nested) && serverMapHasFauxnix(nested);
172
+ }
173
+ export function isServerMap(value) {
174
+ return !!value && typeof value === 'object' && !Array.isArray(value);
175
+ }
176
+ export function serverMapHasFauxnix(value) {
177
+ if (!isServerMap(value))
178
+ return false;
179
+ for (const [name, cfg] of Object.entries(value)) {
180
+ if (name === 'servers')
181
+ continue;
182
+ if (looksLikeFauxnixServer(name, cfg))
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+ function looksLikeFauxnixServer(name, cfg) {
188
+ if (/^fauxnix(-cli)?$/i.test(name))
189
+ return true;
190
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
191
+ return false;
192
+ const rec = cfg;
193
+ const chunks = [];
194
+ if (typeof rec.command === 'string')
195
+ chunks.push(rec.command);
196
+ if (Array.isArray(rec.command)) {
197
+ for (const part of rec.command)
198
+ if (typeof part === 'string')
199
+ chunks.push(part);
200
+ }
201
+ if (Array.isArray(rec.args)) {
202
+ for (const part of rec.args)
203
+ if (typeof part === 'string')
204
+ chunks.push(part);
205
+ }
206
+ return chunks.some(isFauxnixExecutable);
207
+ }
208
+ function isFauxnixExecutable(s) {
209
+ const base = s.replace(/\\/g, '/').split('/').pop()?.trim() ?? '';
210
+ return /^fauxnix(-cli)?(\.cmd|\.exe)?$/i.test(base);
211
+ }
212
+ async function mcpLines(nodeVersion, loadMcp) {
213
+ const major = nodeMajor(nodeVersion);
214
+ const nodeOk = major >= 18;
215
+ let moduleOk = false;
216
+ let moduleDetail = '';
217
+ try {
218
+ const mod = await (loadMcp ?? defaultLoadMcp)();
219
+ moduleOk =
220
+ !!mod && typeof mod.startMcpServer === 'function';
221
+ if (!moduleOk)
222
+ moduleDetail = 'startMcpServer export missing';
223
+ }
224
+ catch (e) {
225
+ moduleDetail = e instanceof Error ? e.message : String(e);
226
+ }
227
+ const lines = [
228
+ field('node', `${nodeVersion.startsWith('v') ? nodeVersion : 'v' + nodeVersion}${nodeOk ? ' (>=18 required)' : ' FAILED (requires >=18)'}`),
229
+ field('mcp', moduleOk ? 'module loads' : `FAILED to load${moduleDetail ? ': ' + moduleDetail : ''}`),
230
+ VALUE_INDENT + 'start with: fauxnix mcp',
231
+ ];
232
+ return { lines, ok: nodeOk && moduleOk };
233
+ }
234
+ async function defaultLoadMcp() {
235
+ return import('./mcp.js');
236
+ }
237
+ function nodeMajor(version) {
238
+ const m = /^v?(\d+)/.exec(version);
239
+ return m ? Number(m[1]) : 0;
240
+ }
241
+ function stripBom(text) {
242
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
243
+ }
244
+ function readText(path) {
245
+ try {
246
+ return readFileSync(path, 'utf8');
247
+ }
248
+ catch {
249
+ return undefined;
250
+ }
251
+ }
package/dist/errors.d.ts CHANGED
@@ -2,4 +2,8 @@
2
2
  * Error normalization — make PowerShell failures look like bash failures
3
3
  * so agents can pattern-match on familiar Linux error styles.
4
4
  */
5
+ /** Missing `python3` on Windows (Mac/Linux agents emit this; do not alias). */
6
+ export declare const PYTHON3_WINDOWS_HINT = " (fauxnix: try `python` or `py` on Windows)";
7
+ /** `.sh` cannot be CreateProcess'd; fx-native never emits the PS not-recognized line. */
8
+ export declare const SH_SCRIPT_WINDOWS_HINT = " (fauxnix: .sh scripts cannot run natively on Windows)";
5
9
  export declare function normalizeStderr(stderr: string): string;
package/dist/errors.js CHANGED
@@ -2,6 +2,19 @@
2
2
  * Error normalization — make PowerShell failures look like bash failures
3
3
  * so agents can pattern-match on familiar Linux error styles.
4
4
  */
5
+ /** Missing `python3` on Windows (Mac/Linux agents emit this; do not alias). */
6
+ export const PYTHON3_WINDOWS_HINT = ' (fauxnix: try `python` or `py` on Windows)';
7
+ /** `.sh` cannot be CreateProcess'd; fx-native never emits the PS not-recognized line. */
8
+ export const SH_SCRIPT_WINDOWS_HINT = ' (fauxnix: .sh scripts cannot run natively on Windows)';
9
+ function commandNotFound(name) {
10
+ const msg = 'bash: ' + name + ': command not found';
11
+ const base = name.replace(/^.*[/\\]/, '');
12
+ if (/^python3(\.exe)?$/i.test(base))
13
+ return msg + PYTHON3_WINDOWS_HINT;
14
+ if (/\.sh$/i.test(name))
15
+ return msg + SH_SCRIPT_WINDOWS_HINT;
16
+ return msg;
17
+ }
5
18
  /** Lines produced by PowerShell error formatting that bash would never show. */
6
19
  const PS_NOISE = [
7
20
  /^\s*\+ CategoryInfo\s*:/,
@@ -62,15 +75,15 @@ export function normalizeStderr(stderr) {
62
75
  // "The term 'x' is not recognized as a name of a cmdlet, function, ..."
63
76
  let m = line.match(/^The term '(.+?)' is not recognized/);
64
77
  if (m)
65
- return 'bash: ' + m[1] + ': command not found';
78
+ return commandNotFound(m[1]);
66
79
  // zh-CN: 无法将"x"项识别为 cmdlet、函数、脚本文件或可运行程序的名称
67
80
  m = line.match(/^无法将["'”]?([^"'”]+)["'”]?项识别为/);
68
81
  if (m)
69
- return 'bash: ' + m[1] + ': command not found';
82
+ return commandNotFound(m[1]);
70
83
  // "x : The term 'y' is not recognized ..." (with source prefix)
71
84
  m = line.match(/^(\S+)\s*:\s*The term '(.+?)' is not recognized/);
72
85
  if (m)
73
- return 'bash: ' + m[2] + ': command not found';
86
+ return commandNotFound(m[2]);
74
87
  // "cat : Cannot find path 'D:\x' because it does not exist."
75
88
  m = line.match(/^(\S+)\s*:\s*Cannot find path '(.+?)' because it does not exist\.?$/);
76
89
  if (m) {
@@ -93,9 +106,9 @@ export function normalizeStderr(stderr) {
93
106
  m = line.match(/^(\S+)\s*:\s*(.*)Access to the path '(.+?)' is denied\.?$/);
94
107
  if (m)
95
108
  return m[1].toLowerCase() + ': cannot remove \'' + m[3] + '\': Permission denied';
96
- // helpful hint for bash scripts
109
+ // leftover PS not-recognized lines that the rewrites above did not catch
97
110
  if (/\.sh'?/.test(line) && /is not recognized/.test(line)) {
98
- return line + ' (fauxnix: .sh scripts cannot run natively on Windows)';
111
+ return line + SH_SCRIPT_WINDOWS_HINT;
99
112
  }
100
113
  return line;
101
114
  });