react-doctor 0.0.40 → 0.0.42

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/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import { performance } from "node:perf_hooks";
13
13
  import { execSync, spawn, spawnSync } from "node:child_process";
14
14
  import { main } from "knip";
15
15
  import { createOptions } from "knip/session";
16
-
17
16
  //#region src/utils/detect-agents.ts
17
+ const AGENTS_SKILL_DIR = ".agents/skills";
18
18
  const SUPPORTED_AGENTS = {
19
19
  claude: {
20
20
  binaries: ["claude"],
@@ -24,37 +24,37 @@ const SUPPORTED_AGENTS = {
24
24
  codex: {
25
25
  binaries: ["codex"],
26
26
  displayName: "Codex",
27
- skillDir: ".codex/skills"
27
+ skillDir: AGENTS_SKILL_DIR
28
28
  },
29
29
  copilot: {
30
30
  binaries: ["copilot"],
31
31
  displayName: "GitHub Copilot",
32
- skillDir: ".github/copilot/skills"
32
+ skillDir: AGENTS_SKILL_DIR
33
33
  },
34
34
  gemini: {
35
35
  binaries: ["gemini"],
36
36
  displayName: "Gemini CLI",
37
- skillDir: ".gemini/skills"
37
+ skillDir: AGENTS_SKILL_DIR
38
38
  },
39
39
  cursor: {
40
40
  binaries: ["cursor", "agent"],
41
41
  displayName: "Cursor",
42
- skillDir: ".cursor/skills"
42
+ skillDir: AGENTS_SKILL_DIR
43
43
  },
44
44
  opencode: {
45
45
  binaries: ["opencode"],
46
46
  displayName: "OpenCode",
47
- skillDir: ".opencode/skills"
47
+ skillDir: AGENTS_SKILL_DIR
48
48
  },
49
49
  droid: {
50
50
  binaries: ["droid"],
51
51
  displayName: "Factory Droid",
52
- skillDir: ".droid/skills"
52
+ skillDir: ".factory/skills"
53
53
  },
54
54
  pi: {
55
55
  binaries: ["pi", "omegon"],
56
56
  displayName: "Pi",
57
- skillDir: ".pi/skills"
57
+ skillDir: AGENTS_SKILL_DIR
58
58
  }
59
59
  };
60
60
  const ALL_SUPPORTED_AGENTS = Object.keys(SUPPORTED_AGENTS);
@@ -74,7 +74,6 @@ const isCommandAvailable = (command) => {
74
74
  const detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
75
75
  const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
76
76
  const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
77
-
78
77
  //#endregion
79
78
  //#region src/utils/highlighter.ts
80
79
  const highlighter = {
@@ -84,11 +83,11 @@ const highlighter = {
84
83
  success: pc.green,
85
84
  dim: pc.dim
86
85
  };
87
-
88
86
  //#endregion
89
87
  //#region src/utils/install-skill-for-agent.ts
90
- const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName) => {
88
+ const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName, alreadyInstalledDirectories) => {
91
89
  const installedSkillDirectory = path.join(projectRoot, toSkillDir(agent), skillName);
90
+ if (alreadyInstalledDirectories?.has(installedSkillDirectory)) return installedSkillDirectory;
92
91
  rmSync(installedSkillDirectory, {
93
92
  recursive: true,
94
93
  force: true
@@ -97,7 +96,6 @@ const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillNam
97
96
  cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
98
97
  return installedSkillDirectory;
99
98
  };
100
-
101
99
  //#endregion
102
100
  //#region src/utils/logger.ts
103
101
  const logger = {
@@ -123,7 +121,6 @@ const logger = {
123
121
  console.log("");
124
122
  }
125
123
  };
126
-
127
124
  //#endregion
128
125
  //#region src/utils/should-auto-select-current-choice.ts
129
126
  const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
@@ -131,13 +128,11 @@ const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
131
128
  const currentChoice = choiceStates[cursor];
132
129
  return Boolean(currentChoice) && !currentChoice.disabled;
133
130
  };
134
-
135
131
  //#endregion
136
132
  //#region src/utils/should-select-all-choices.ts
137
133
  const shouldSelectAllChoices = (choiceStates) => {
138
134
  return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
139
135
  };
140
-
141
136
  //#endregion
142
137
  //#region src/utils/prompts.ts
143
138
  const require = createRequire(import.meta.url);
@@ -203,7 +198,6 @@ const prompts = (questions) => {
203
198
  patchSelectBanner();
204
199
  return basePrompts(questions, { onCancel });
205
200
  };
206
-
207
201
  //#endregion
208
202
  //#region src/utils/spinner.ts
209
203
  let sharedInstance = null;
@@ -234,7 +228,6 @@ const spinner = (text) => ({ start() {
234
228
  fail: (displayText) => finalize("fail", text, displayText)
235
229
  };
236
230
  } });
237
-
238
231
  //#endregion
239
232
  //#region src/install-skill.ts
240
233
  const SKILL_NAME = "react-doctor";
@@ -271,35 +264,37 @@ const runInstallSkill = async (options = {}) => {
271
264
  })).agents ?? [];
272
265
  if (selectedAgents.length === 0) return;
273
266
  const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
274
- for (const agent of selectedAgents) installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME);
267
+ const installedDirectories = /* @__PURE__ */ new Set();
268
+ for (const agent of selectedAgents) {
269
+ const installedDirectory = installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME, installedDirectories);
270
+ installedDirectories.add(installedDirectory);
271
+ }
275
272
  installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
276
273
  };
277
-
278
274
  //#endregion
279
275
  //#region src/constants.ts
280
276
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
281
277
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
282
278
  const MILLISECONDS_PER_SECOND = 1e3;
283
- const ERROR_PREVIEW_LENGTH_CHARS = 200;
284
- const PERFECT_SCORE = 100;
285
- const SCORE_GOOD_THRESHOLD = 75;
286
- const SCORE_OK_THRESHOLD = 50;
287
- const SCORE_BAR_WIDTH_CHARS = 50;
288
- const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
289
- const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
290
279
  const SCORE_API_URL = "https://www.react.doctor/api/score";
291
280
  const SHARE_BASE_URL = "https://www.react.doctor/share";
292
281
  const FETCH_TIMEOUT_MS = 1e4;
293
282
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
294
- const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
295
- const OXLINT_MAX_FILES_PER_BATCH = 500;
296
283
  const OFFLINE_MESSAGE = "Score calculated locally (offline mode).";
297
284
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
298
285
  const ERROR_RULE_PENALTY = 1.5;
299
286
  const WARNING_RULE_PENALTY = .75;
300
- const MAX_KNIP_RETRIES = 5;
287
+ const KNIP_CONFIG_LOCATIONS = [
288
+ "knip.json",
289
+ "knip.jsonc",
290
+ ".knip.json",
291
+ ".knip.jsonc",
292
+ "knip.ts",
293
+ "knip.js",
294
+ "knip.config.ts",
295
+ "knip.config.js"
296
+ ];
301
297
  const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
302
- const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
303
298
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
304
299
  const IGNORED_DIRECTORIES = new Set([
305
300
  "node_modules",
@@ -307,12 +302,11 @@ const IGNORED_DIRECTORIES = new Set([
307
302
  "build",
308
303
  "coverage"
309
304
  ]);
310
-
311
305
  //#endregion
312
306
  //#region src/core/calculate-score-locally.ts
313
307
  const getScoreLabel = (score) => {
314
- if (score >= SCORE_GOOD_THRESHOLD) return "Great";
315
- if (score >= SCORE_OK_THRESHOLD) return "Needs work";
308
+ if (score >= 75) return "Great";
309
+ if (score >= 50) return "Needs work";
316
310
  return "Critical";
317
311
  };
318
312
  const countUniqueRules = (diagnostics) => {
@@ -330,7 +324,7 @@ const countUniqueRules = (diagnostics) => {
330
324
  };
331
325
  const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
332
326
  const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
333
- return Math.max(0, Math.round(PERFECT_SCORE - penalty));
327
+ return Math.max(0, Math.round(100 - penalty));
334
328
  };
335
329
  const calculateScoreLocally = (diagnostics) => {
336
330
  const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
@@ -340,7 +334,6 @@ const calculateScoreLocally = (diagnostics) => {
340
334
  label: getScoreLabel(score)
341
335
  };
342
336
  };
343
-
344
337
  //#endregion
345
338
  //#region src/core/try-score-from-api.ts
346
339
  const parseScoreResult = (value) => {
@@ -372,7 +365,6 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
372
365
  clearTimeout(timeoutId);
373
366
  }
374
367
  };
375
-
376
368
  //#endregion
377
369
  //#region src/utils/proxy-fetch.ts
378
370
  const getGlobalProcess = () => {
@@ -415,7 +407,6 @@ const proxyFetch = async (url, init) => {
415
407
  clearTimeout(timeoutId);
416
408
  }
417
409
  };
418
-
419
410
  //#endregion
420
411
  //#region src/utils/calculate-score-node.ts
421
412
  const calculateScore = async (diagnostics) => {
@@ -423,19 +414,16 @@ const calculateScore = async (diagnostics) => {
423
414
  if (apiScore) return apiScore;
424
415
  return calculateScoreLocally(diagnostics);
425
416
  };
426
-
427
417
  //#endregion
428
418
  //#region src/utils/colorize-by-score.ts
429
419
  const colorizeByScore = (text, score) => {
430
- if (score >= SCORE_GOOD_THRESHOLD) return highlighter.success(text);
431
- if (score >= SCORE_OK_THRESHOLD) return highlighter.warn(text);
420
+ if (score >= 75) return highlighter.success(text);
421
+ if (score >= 50) return highlighter.warn(text);
432
422
  return highlighter.error(text);
433
423
  };
434
-
435
424
  //#endregion
436
425
  //#region src/plugin/constants.ts
437
426
  const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
438
-
439
427
  //#endregion
440
428
  //#region src/utils/is-file.ts
441
429
  const isFile = (filePath) => {
@@ -445,7 +433,6 @@ const isFile = (filePath) => {
445
433
  return false;
446
434
  }
447
435
  };
448
-
449
436
  //#endregion
450
437
  //#region src/utils/read-package-json.ts
451
438
  const readPackageJson = (packageJsonPath) => {
@@ -460,7 +447,6 @@ const readPackageJson = (packageJsonPath) => {
460
447
  throw error;
461
448
  }
462
449
  };
463
-
464
450
  //#endregion
465
451
  //#region src/utils/check-reduced-motion.ts
466
452
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
@@ -502,7 +488,6 @@ const checkReducedMotion = (rootDirectory) => {
502
488
  return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
503
489
  }
504
490
  };
505
-
506
491
  //#endregion
507
492
  //#region src/utils/read-file-lines-node.ts
508
493
  const createNodeReadFileLinesSync = (rootDirectory) => {
@@ -515,7 +500,6 @@ const createNodeReadFileLinesSync = (rootDirectory) => {
515
500
  }
516
501
  };
517
502
  };
518
-
519
503
  //#endregion
520
504
  //#region src/utils/match-glob-pattern.ts
521
505
  const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
@@ -543,7 +527,6 @@ const compileGlobPattern = (pattern) => {
543
527
  regexSource += "$";
544
528
  return new RegExp(regexSource);
545
529
  };
546
-
547
530
  //#endregion
548
531
  //#region src/utils/is-ignored-file.ts
549
532
  const toRelativePath = (filePath, rootDirectory) => {
@@ -558,7 +541,6 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
558
541
  const relativePath = toRelativePath(filePath, rootDirectory);
559
542
  return patterns.some((pattern) => pattern.test(relativePath));
560
543
  };
561
-
562
544
  //#endregion
563
545
  //#region src/utils/filter-diagnostics.ts
564
546
  const resolveCandidateReadPath = (rootDirectory, filePath) => {
@@ -632,17 +614,14 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
632
614
  return true;
633
615
  });
634
616
  };
635
-
636
617
  //#endregion
637
618
  //#region src/utils/merge-and-filter-diagnostics.ts
638
619
  const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
639
620
  return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
640
621
  };
641
-
642
622
  //#endregion
643
623
  //#region src/utils/jsx-include-paths.ts
644
624
  const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
645
-
646
625
  //#endregion
647
626
  //#region src/utils/combine-diagnostics.ts
648
627
  const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isDiffMode, userConfig, readFileLinesSync = createNodeReadFileLinesSync(directory), includeEnvironmentChecks = true) => {
@@ -653,7 +632,6 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
653
632
  ...extraDiagnostics
654
633
  ], directory, userConfig, readFileLinesSync);
655
634
  };
656
-
657
635
  //#endregion
658
636
  //#region src/utils/find-monorepo-root.ts
659
637
  const isMonorepoRoot = (directory) => {
@@ -672,11 +650,9 @@ const findMonorepoRoot = (startDirectory) => {
672
650
  }
673
651
  return null;
674
652
  };
675
-
676
653
  //#endregion
677
654
  //#region src/utils/is-plain-object.ts
678
655
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
679
-
680
656
  //#endregion
681
657
  //#region src/utils/discover-project.ts
682
658
  const REACT_COMPILER_PACKAGES = new Set([
@@ -1097,7 +1073,22 @@ const discoverProject = (directory) => {
1097
1073
  sourceFileCount
1098
1074
  };
1099
1075
  };
1100
-
1076
+ //#endregion
1077
+ //#region src/utils/format-error-chain.ts
1078
+ const collectErrorChain = (rootError) => {
1079
+ const errorChain = [];
1080
+ const visitedErrors = /* @__PURE__ */ new Set();
1081
+ let currentError = rootError;
1082
+ while (currentError !== void 0 && !visitedErrors.has(currentError)) {
1083
+ visitedErrors.add(currentError);
1084
+ errorChain.push(currentError);
1085
+ currentError = currentError instanceof Error ? currentError.cause : void 0;
1086
+ }
1087
+ return errorChain;
1088
+ };
1089
+ const formatErrorMessage = (error) => error instanceof Error ? error.message || error.name : String(error);
1090
+ const formatErrorChain = (rootError) => collectErrorChain(rootError).map(formatErrorMessage).join(" → ");
1091
+ const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
1101
1092
  //#endregion
1102
1093
  //#region src/utils/framed-box.ts
1103
1094
  const createFramedLine = (plainText, renderedText = plainText) => ({
@@ -1107,10 +1098,10 @@ const createFramedLine = (plainText, renderedText = plainText) => ({
1107
1098
  const renderFramedBoxString = (framedLines) => {
1108
1099
  if (framedLines.length === 0) return "";
1109
1100
  const borderColorizer = highlighter.dim;
1110
- const outerIndent = " ".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);
1111
- const horizontalPadding = " ".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);
1101
+ const outerIndent = " ".repeat(2);
1102
+ const horizontalPadding = " ".repeat(1);
1112
1103
  const maximumLineLength = Math.max(...framedLines.map((framedLine) => framedLine.plainText.length));
1113
- const borderLine = "─".repeat(maximumLineLength + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2);
1104
+ const borderLine = "─".repeat(maximumLineLength + 2);
1114
1105
  const lines = [];
1115
1106
  lines.push(`${outerIndent}${borderColorizer(`┌${borderLine}┐`)}`);
1116
1107
  for (const framedLine of framedLines) {
@@ -1124,7 +1115,6 @@ const printFramedBox = (framedLines) => {
1124
1115
  const rendered = renderFramedBoxString(framedLines);
1125
1116
  if (rendered) logger.log(rendered);
1126
1117
  };
1127
-
1128
1118
  //#endregion
1129
1119
  //#region src/utils/group-by.ts
1130
1120
  const groupBy = (items, keyFn) => {
@@ -1137,11 +1127,9 @@ const groupBy = (items, keyFn) => {
1137
1127
  }
1138
1128
  return groups;
1139
1129
  };
1140
-
1141
1130
  //#endregion
1142
1131
  //#region src/utils/indent-multiline-text.ts
1143
1132
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
1144
-
1145
1133
  //#endregion
1146
1134
  //#region src/utils/load-config.ts
1147
1135
  const CONFIG_FILENAME = "react-doctor.config.json";
@@ -1177,7 +1165,6 @@ const loadConfig = (rootDirectory) => {
1177
1165
  }
1178
1166
  return null;
1179
1167
  };
1180
-
1181
1168
  //#endregion
1182
1169
  //#region src/utils/resolve-compatible-node.ts
1183
1170
  const parseNodeVersion = (versionString) => {
@@ -1230,7 +1217,7 @@ const installNodeViaNvm = () => {
1230
1217
  const nvmScript = path.join(nvmDirectory, "nvm.sh");
1231
1218
  if (!existsSync(nvmScript)) return false;
1232
1219
  try {
1233
- execSync(`bash -c ". '${nvmScript}' && nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}"`, { stdio: "inherit" });
1220
+ execSync(`bash -c ". '${nvmScript}' && nvm install 24"`, { stdio: "inherit" });
1234
1221
  return findCompatibleNvmBinary() !== null;
1235
1222
  } catch {
1236
1223
  return false;
@@ -1252,7 +1239,6 @@ const resolveNodeForOxlint = () => {
1252
1239
  version
1253
1240
  };
1254
1241
  };
1255
-
1256
1242
  //#endregion
1257
1243
  //#region src/utils/resolve-lint-include-paths.ts
1258
1244
  const listSourceFilesViaGit = (rootDirectory) => {
@@ -1295,7 +1281,6 @@ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
1295
1281
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
1296
1282
  });
1297
1283
  };
1298
-
1299
1284
  //#endregion
1300
1285
  //#region src/utils/collect-unused-file-paths.ts
1301
1286
  const collectUnusedFilePaths = (filesIssues) => {
@@ -1309,7 +1294,19 @@ const collectUnusedFilePaths = (filesIssues) => {
1309
1294
  }
1310
1295
  return unusedFilePaths;
1311
1296
  };
1312
-
1297
+ //#endregion
1298
+ //#region src/utils/extract-failed-plugin-name.ts
1299
+ const PLUGIN_CONFIG_PATTERN = /(?:^|[/\\\s])([a-z][a-z0-9-]*)\.config\./i;
1300
+ const extractFailedPluginName = (error) => {
1301
+ for (const errorMessage of getErrorChainMessages(error)) {
1302
+ const pluginNameMatch = errorMessage.match(PLUGIN_CONFIG_PATTERN);
1303
+ if (pluginNameMatch?.[1]) return pluginNameMatch[1].toLowerCase();
1304
+ }
1305
+ return null;
1306
+ };
1307
+ //#endregion
1308
+ //#region src/utils/has-knip-config.ts
1309
+ const hasKnipConfig = (directory) => KNIP_CONFIG_LOCATIONS.some((configFilename) => isFile(path.join(directory, configFilename)));
1313
1310
  //#endregion
1314
1311
  //#region src/utils/run-knip.ts
1315
1312
  const KNIP_CATEGORY_MAP = {
@@ -1364,12 +1361,15 @@ const silenced = async (fn) => {
1364
1361
  console.error = originalError;
1365
1362
  }
1366
1363
  };
1367
- const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
1368
- const extractFailedPluginName = (error) => {
1369
- return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
1370
- };
1371
1364
  const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
1372
1365
  const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
1366
+ const tryDisableFailedPlugin = (error, parsedConfig, disabledPlugins) => {
1367
+ const failedPlugin = extractFailedPluginName(error);
1368
+ if (!failedPlugin || !(failedPlugin in parsedConfig) || disabledPlugins.has(failedPlugin)) return false;
1369
+ disabledPlugins.add(failedPlugin);
1370
+ parsedConfig[failedPlugin] = false;
1371
+ return true;
1372
+ };
1373
1373
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
1374
1374
  const tsConfigFile = resolveTsConfigFile(knipCwd);
1375
1375
  const options = await silenced(() => createOptions({
@@ -1379,33 +1379,36 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
1379
1379
  ...tsConfigFile ? { tsConfigFile } : {}
1380
1380
  }));
1381
1381
  const parsedConfig = options.parsedConfig;
1382
- for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
1382
+ const disabledPlugins = /* @__PURE__ */ new Set();
1383
+ let lastKnipError;
1384
+ for (let attempt = 0; attempt <= 5; attempt++) try {
1383
1385
  return await silenced(() => main(options));
1384
1386
  } catch (error) {
1385
- const failedPlugin = extractFailedPluginName(error);
1386
- if (!failedPlugin || attempt === MAX_KNIP_RETRIES) throw error;
1387
- parsedConfig[failedPlugin] = false;
1387
+ lastKnipError = error;
1388
+ if (!tryDisableFailedPlugin(error, parsedConfig, disabledPlugins)) throw error;
1388
1389
  }
1389
- throw new Error("Unreachable");
1390
+ throw lastKnipError;
1390
1391
  };
1391
1392
  const hasNodeModules = (directory) => {
1392
1393
  const nodeModulesPath = path.join(directory, "node_modules");
1393
1394
  return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
1394
1395
  };
1396
+ const resolveWorkspaceName = (rootDirectory) => {
1397
+ const packageJsonPath = path.join(rootDirectory, "package.json");
1398
+ return (isFile(packageJsonPath) ? readPackageJson(packageJsonPath) : {}).name ?? path.basename(rootDirectory);
1399
+ };
1400
+ const runKnipForProject = async (rootDirectory, monorepoRoot) => {
1401
+ if (!monorepoRoot || hasKnipConfig(rootDirectory)) return runKnipWithOptions(rootDirectory);
1402
+ try {
1403
+ return await runKnipWithOptions(monorepoRoot, resolveWorkspaceName(rootDirectory));
1404
+ } catch {
1405
+ return runKnipWithOptions(rootDirectory);
1406
+ }
1407
+ };
1395
1408
  const runKnip = async (rootDirectory) => {
1396
1409
  const monorepoRoot = findMonorepoRoot(rootDirectory);
1397
1410
  if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
1398
- let knipResult;
1399
- if (monorepoRoot) {
1400
- const packageJsonPath = path.join(rootDirectory, "package.json");
1401
- const workspaceName = (isFile(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
1402
- try {
1403
- knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
1404
- } catch {
1405
- knipResult = await runKnipWithOptions(rootDirectory);
1406
- }
1407
- } else knipResult = await runKnipWithOptions(rootDirectory);
1408
- const { issues } = knipResult;
1411
+ const { issues } = await runKnipForProject(rootDirectory, monorepoRoot);
1409
1412
  const diagnostics = [];
1410
1413
  for (const unusedFilePath of collectUnusedFilePaths(issues.files)) diagnostics.push({
1411
1414
  filePath: path.relative(rootDirectory, unusedFilePath),
@@ -1426,7 +1429,6 @@ const runKnip = async (rootDirectory) => {
1426
1429
  ]) diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));
1427
1430
  return diagnostics;
1428
1431
  };
1429
-
1430
1432
  //#endregion
1431
1433
  //#region src/oxlint-config.ts
1432
1434
  const esmRequire$1 = createRequire(import.meta.url);
@@ -1610,7 +1612,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
1610
1612
  ...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
1611
1613
  }
1612
1614
  });
1613
-
1614
1615
  //#endregion
1615
1616
  //#region src/utils/neutralize-disable-directives.ts
1616
1617
  const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
@@ -1653,7 +1654,6 @@ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
1653
1654
  for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
1654
1655
  };
1655
1656
  };
1656
-
1657
1657
  //#endregion
1658
1658
  //#region src/utils/run-oxlint.ts
1659
1659
  const esmRequire = createRequire(import.meta.url);
@@ -1913,8 +1913,8 @@ const batchIncludePaths = (baseArgs, includePaths) => {
1913
1913
  let currentBatchLength = baseArgsLength;
1914
1914
  for (const filePath of includePaths) {
1915
1915
  const entryLength = filePath.length + 1;
1916
- const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > SPAWN_ARGS_MAX_LENGTH_CHARS;
1917
- const exceedsFileCount = currentBatch.length >= OXLINT_MAX_FILES_PER_BATCH;
1916
+ const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
1917
+ const exceedsFileCount = currentBatch.length >= 500;
1918
1918
  if (exceedsArgLength || exceedsFileCount) {
1919
1919
  batches.push(currentBatch);
1920
1920
  currentBatch = [];
@@ -1958,7 +1958,7 @@ const parseOxlintOutput = (stdout) => {
1958
1958
  try {
1959
1959
  output = JSON.parse(stdout);
1960
1960
  } catch {
1961
- throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, ERROR_PREVIEW_LENGTH_CHARS)}`);
1961
+ throw new Error(`Failed to parse oxlint output: ${stdout.slice(0, 200)}`);
1962
1962
  }
1963
1963
  return output.diagnostics.filter((diagnostic) => diagnostic.code && JSX_FILE_PATTERN.test(diagnostic.filename)).map((diagnostic) => {
1964
1964
  const { plugin, rule } = parseRuleCode(diagnostic.code);
@@ -2009,7 +2009,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
2009
2009
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
2010
2010
  }
2011
2011
  };
2012
-
2013
2012
  //#endregion
2014
2013
  //#region src/scan.ts
2015
2014
  const SEVERITY_ORDER = {
@@ -2048,7 +2047,7 @@ const printDiagnostics = (diagnostics, isVerbose) => {
2048
2047
  }
2049
2048
  };
2050
2049
  const formatElapsedTime = (elapsedMilliseconds) => {
2051
- if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) return `${Math.round(elapsedMilliseconds)}ms`;
2050
+ if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
2052
2051
  return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
2053
2052
  };
2054
2053
  const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
@@ -2077,8 +2076,8 @@ const writeDiagnosticsDirectory = (diagnostics) => {
2077
2076
  return outputDirectory;
2078
2077
  };
2079
2078
  const buildScoreBarSegments = (score) => {
2080
- const filledCount = Math.round(score / PERFECT_SCORE * SCORE_BAR_WIDTH_CHARS);
2081
- const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;
2079
+ const filledCount = Math.round(score / 100 * 50);
2080
+ const emptyCount = 50 - filledCount;
2082
2081
  return {
2083
2082
  filledSegment: "█".repeat(filledCount),
2084
2083
  emptySegment: "░".repeat(emptyCount)
@@ -2095,14 +2094,14 @@ const buildScoreBar = (score) => {
2095
2094
  const printScoreGauge = (score, label) => {
2096
2095
  const scoreDisplay = colorizeByScore(`${score}`, score);
2097
2096
  const labelDisplay = colorizeByScore(label, score);
2098
- logger.log(` ${scoreDisplay} / ${PERFECT_SCORE} ${labelDisplay}`);
2097
+ logger.log(` ${scoreDisplay} / 100 ${labelDisplay}`);
2099
2098
  logger.break();
2100
2099
  logger.log(` ${buildScoreBar(score)}`);
2101
2100
  logger.break();
2102
2101
  };
2103
2102
  const getDoctorFace = (score) => {
2104
- if (score >= SCORE_GOOD_THRESHOLD) return ["◠ ◠", " ▽ "];
2105
- if (score >= SCORE_OK_THRESHOLD) return ["• •", " ─ "];
2103
+ if (score >= 75) return ["◠ ◠", " ▽ "];
2104
+ if (score >= 50) return ["• •", " ─ "];
2106
2105
  return ["x x", " ▽ "];
2107
2106
  };
2108
2107
  const printBranding = (score) => {
@@ -2140,8 +2139,8 @@ const buildBrandingLines = (scoreResult, noScoreMessage) => {
2140
2139
  lines.push(createFramedLine("└─────┘", scoreColorizer("└─────┘")));
2141
2140
  lines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
2142
2141
  lines.push(createFramedLine(""));
2143
- const scoreLinePlainText = `${scoreResult.score} / ${PERFECT_SCORE} ${scoreResult.label}`;
2144
- const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / ${PERFECT_SCORE} ${colorizeByScore(scoreResult.label, scoreResult.score)}`;
2142
+ const scoreLinePlainText = `${scoreResult.score} / 100 ${scoreResult.label}`;
2143
+ const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / 100 ${colorizeByScore(scoreResult.label, scoreResult.score)}`;
2145
2144
  lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));
2146
2145
  lines.push(createFramedLine(""));
2147
2146
  lines.push(createFramedLine(buildPlainScoreBar(scoreResult.score), buildScoreBar(scoreResult.score)));
@@ -2208,7 +2207,7 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
2208
2207
  const { shouldInstallNode } = await prompts({
2209
2208
  type: "confirm",
2210
2209
  name: "shouldInstallNode",
2211
- message: `Install Node ${OXLINT_RECOMMENDED_NODE_MAJOR} via nvm to enable lint checks?`,
2210
+ message: `Install Node 24 via nvm to enable lint checks?`,
2212
2211
  initial: true
2213
2212
  });
2214
2213
  if (shouldInstallNode) {
@@ -2225,8 +2224,8 @@ const resolveOxlintNode = async (isLintEnabled, isScoreOnly) => {
2225
2224
  logger.break();
2226
2225
  return null;
2227
2226
  }
2228
- } else if (isNvmInstalled()) logger.dim(` Run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
2229
- else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install ${OXLINT_RECOMMENDED_NODE_MAJOR}`);
2227
+ } else if (isNvmInstalled()) logger.dim(` Run: nvm install 24`);
2228
+ else logger.dim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
2230
2229
  logger.break();
2231
2230
  return null;
2232
2231
  };
@@ -2279,13 +2278,13 @@ const scan = async (directory, inputOptions = {}) => {
2279
2278
  } catch (error) {
2280
2279
  didLintFail = true;
2281
2280
  if (!options.scoreOnly) {
2282
- const errorMessage = error instanceof Error ? error.message : String(error);
2283
- if (errorMessage.includes("native binding")) {
2281
+ const lintErrorChain = formatErrorChain(error);
2282
+ if (lintErrorChain.includes("native binding")) {
2284
2283
  lintSpinner?.fail(`Lint checks failed — oxlint native binding not found (Node ${process.version}).`);
2285
2284
  logger.dim(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`);
2286
2285
  } else {
2287
2286
  lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
2288
- logger.error(errorMessage);
2287
+ logger.error(lintErrorChain);
2289
2288
  }
2290
2289
  }
2291
2290
  return [];
@@ -2301,7 +2300,7 @@ const scan = async (directory, inputOptions = {}) => {
2301
2300
  didDeadCodeFail = true;
2302
2301
  if (!options.scoreOnly) {
2303
2302
  deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
2304
- logger.error(String(error));
2303
+ logger.error(formatErrorChain(error));
2305
2304
  }
2306
2305
  return [];
2307
2306
  }
@@ -2358,7 +2357,6 @@ const scan = async (directory, inputOptions = {}) => {
2358
2357
  skippedChecks
2359
2358
  };
2360
2359
  };
2361
-
2362
2360
  //#endregion
2363
2361
  //#region src/utils/get-diff-files.ts
2364
2362
  const getCurrentBranch = (directory) => {
@@ -2438,7 +2436,6 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
2438
2436
  };
2439
2437
  };
2440
2438
  const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
2441
-
2442
2439
  //#endregion
2443
2440
  //#region src/utils/get-staged-files.ts
2444
2441
  const getStagedFilePaths = (directory) => {
@@ -2500,7 +2497,6 @@ const materializeStagedFiles = (directory, stagedFiles, tempDirectory) => {
2500
2497
  }
2501
2498
  };
2502
2499
  };
2503
-
2504
2500
  //#endregion
2505
2501
  //#region src/utils/handle-error.ts
2506
2502
  const DEFAULT_HANDLE_ERROR_OPTIONS = { shouldExit: true };
@@ -2514,7 +2510,6 @@ const handleError = (error, options = DEFAULT_HANDLE_ERROR_OPTIONS) => {
2514
2510
  if (options.shouldExit) process.exit(1);
2515
2511
  process.exitCode = 1;
2516
2512
  };
2517
-
2518
2513
  //#endregion
2519
2514
  //#region src/utils/select-projects.ts
2520
2515
  const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
@@ -2562,10 +2557,9 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
2562
2557
  });
2563
2558
  return selectedDirectories;
2564
2559
  };
2565
-
2566
2560
  //#endregion
2567
2561
  //#region src/cli.ts
2568
- const VERSION = "0.0.40";
2562
+ const VERSION = "0.0.42";
2569
2563
  const VALID_FAIL_ON_LEVELS = new Set([
2570
2564
  "error",
2571
2565
  "warning",
@@ -2734,7 +2728,7 @@ const main$1 = async () => {
2734
2728
  await program.parseAsync();
2735
2729
  };
2736
2730
  main$1();
2737
-
2738
2731
  //#endregion
2739
- export { };
2732
+ export {};
2733
+
2740
2734
  //# sourceMappingURL=cli.js.map