react-doctor 0.2.9 → 0.2.11-dev.15e5fec

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
@@ -1,9 +1,9 @@
1
1
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
2
- import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-BliQX9s8.js";
2
+ import { A as isMonorepoRoot, C as filterDiagnosticsForSurface, D as getDiffInfo, E as formatReactDoctorError, F as restoreLegacyThrow, I as runInspect, L as toRelativePath, M as layerOtlp, N as listWorkspacePackages, O as groupBy, P as resolveScanTarget, S as discoverReactSubprojects, T as formatErrorChain, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as isReactDoctorError, k as highlighter, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as filterSourceFiles, x as buildRulePromptUrl, y as buildJsonReport } from "./cli-logger-CSZagq1E.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, execSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
6
- import { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
6
+ import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import process$1 from "node:process";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Layer from "effect/Layer";
@@ -6220,6 +6220,8 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
6220
6220
  return ruleName + " ".repeat(columnWidth - ruleName.length);
6221
6221
  };
6222
6222
  const grayLine = (text) => highlighter.gray(text);
6223
+ const FETCH_FIX_RECIPE_LABEL = "Fetch & follow the canonical fix recipe before fixing";
6224
+ const formatFixRecipeLine = (diagnostic) => `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}`;
6223
6225
  const buildCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
6224
6226
  const firstDiagnostic = ruleDiagnostics[0];
6225
6227
  const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
@@ -6255,6 +6257,7 @@ const buildVerboseRuleGroupLines = (ruleKey, ruleDiagnostics, ruleNameColumnWidt
6255
6257
  const firstDiagnostic = ruleDiagnostics[0];
6256
6258
  lines.push(grayLine(indentMultilineText(firstDiagnostic.message, " ")));
6257
6259
  if (firstDiagnostic.help) lines.push(grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " ")));
6260
+ lines.push(grayLine(` ${formatFixRecipeLine(firstDiagnostic)}`));
6258
6261
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
6259
6262
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
6260
6263
  lines.push(grayLine(` ${filePath}:${site.line}`));
@@ -6299,6 +6302,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
6299
6302
  ];
6300
6303
  if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
6301
6304
  if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
6305
+ sections.push("", formatFixRecipeLine(firstDiagnostic));
6302
6306
  sections.push("", "Files:");
6303
6307
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
6304
6308
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
@@ -6320,11 +6324,11 @@ const colorizeByScore = (text, score) => {
6320
6324
  return highlighter.error(text);
6321
6325
  };
6322
6326
  //#endregion
6327
+ //#region src/cli/utils/constants.ts
6328
+ const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6329
+ const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6330
+ //#endregion
6323
6331
  //#region src/cli/utils/render-score-header.ts
6324
- const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
6325
- const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
6326
- const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
6327
- const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
6328
6332
  const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
6329
6333
  const RAINBOW_GRADIENT_WIDTH = 80;
6330
6334
  const RAINBOW_OKLCH_LIGHTNESS = .638;
@@ -6433,8 +6437,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
6433
6437
  };
6434
6438
  const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
6435
6439
  const isPerfectScore = score === 100;
6436
- for (let frame = 0; frame <= SCORE_BAR_ANIMATION_FRAME_COUNT; frame += 1) {
6437
- const progress = easeOutCubic(frame / SCORE_BAR_ANIMATION_FRAME_COUNT);
6440
+ for (let frame = 0; frame <= 40; frame += 1) {
6441
+ const progress = easeOutCubic(frame / 40);
6438
6442
  const animatedScore = Math.round(score * progress);
6439
6443
  if (isPerfectScore) {
6440
6444
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
@@ -6444,16 +6448,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6444
6448
  frame,
6445
6449
  projectName
6446
6450
  })}`);
6447
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6451
+ if (frame < 40) yield* sleep(50);
6448
6452
  continue;
6449
6453
  }
6450
6454
  const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
6451
6455
  const animatedBarLine = buildScoreBar(animatedScore, score);
6452
6456
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
6453
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6457
+ if (frame < 40) yield* sleep(50);
6454
6458
  }
6455
6459
  if (!isPerfectScore) return;
6456
- for (let frame = 0; frame < PERFECT_SCORE_RAINBOW_FRAME_COUNT; frame += 1) {
6460
+ for (let frame = 0; frame < 16; frame += 1) {
6457
6461
  yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
6458
6462
  score,
6459
6463
  displayScore: score,
@@ -6461,9 +6465,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6461
6465
  frame,
6462
6466
  projectName
6463
6467
  })}`);
6464
- yield* sleep(PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS);
6468
+ yield* sleep(50);
6465
6469
  }
6466
- yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, PERFECT_SCORE_RAINBOW_FRAME_COUNT, projectName)}\x1b[2A`);
6470
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
6467
6471
  });
6468
6472
  const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
6469
6473
  const isPerfectScore = scoreResult.score === 100;
@@ -6539,7 +6543,11 @@ const printCountsSummaryLine = (diagnostics, isVerbose) => Effect.gen(function*
6539
6543
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
6540
6544
  const issueText = (errorCount > 0 ? highlighter.error : warningCount > 0 ? highlighter.warn : highlighter.dim)(`${totalIssueCount} ${totalIssueCount === 1 ? "issue" : "issues"}`);
6541
6545
  yield* Console.log(` ${issueText}`);
6542
- if (!isVerbose && totalIssueCount > 0) yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to see details`));
6546
+ if (!isVerbose && totalIssueCount > 0) {
6547
+ const exampleDiagnostic = diagnostics.find((diagnostic) => diagnostic.severity === "error") ?? diagnostics[0];
6548
+ yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to list every issue with its fix-recipe URL`));
6549
+ yield* Console.log(highlighter.dim(` Each rule links a canonical fix recipe to fetch & follow before fixing, e.g. ${highlighter.info(buildRulePromptUrl(exampleDiagnostic.plugin, exampleDiagnostic.rule))}`));
6550
+ }
6543
6551
  });
6544
6552
  const printSummary = (input) => Effect.gen(function* () {
6545
6553
  if (input.scoreResult) yield* printScoreHeader(input.scoreResult, input.projectName);
@@ -6666,7 +6674,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6666
6674
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6667
6675
  //#endregion
6668
6676
  //#region src/cli/utils/version.ts
6669
- const VERSION = "0.2.9";
6677
+ const VERSION = "0.2.11-dev.15e5fec";
6670
6678
  //#endregion
6671
6679
  //#region src/inspect.ts
6672
6680
  const silentConsole = makeNoopConsole();
@@ -6764,9 +6772,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
6764
6772
  const output = await Effect.runPromise(restoreLegacyThrow(programWithLayers));
6765
6773
  const didLintFail = lintBindingMissing || output.didLintFail;
6766
6774
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
6767
- const lintFailureReasonTag = output.lintFailureReasonTag;
6768
- const isNativeBindingFailure = lintFailureReasonTag === "OxlintUnavailable" || lintFailureReasonTag === "OxlintSpawnFailed";
6769
- if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (isNativeBindingFailure && /native binding/.test(lintFailureReason)) runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
6775
+ if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
6770
6776
  else runConsole(Console.error(highlighter.error(lintFailureReason)));
6771
6777
  const inspectDiagnostics = output.diagnostics;
6772
6778
  const score = didLintFail ? null : output.score;
@@ -6854,10 +6860,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
6854
6860
  return buildResult();
6855
6861
  });
6856
6862
  //#endregion
6857
- //#region src/cli/utils/constants.ts
6858
- const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6859
- const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6860
- //#endregion
6861
6863
  //#region src/cli/utils/get-staged-files.ts
6862
6864
  const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
6863
6865
  const getStagedSourceFiles = async (directory) => {
@@ -7072,15 +7074,16 @@ const buildIssuesSummary = (input) => {
7072
7074
  if (input.score) lines.push(`Score: ${input.score.score}/100`);
7073
7075
  lines.push(`${input.diagnostics.length} issues found`);
7074
7076
  lines.push("");
7075
- const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => diagnostic.rule).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
7077
+ const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
7076
7078
  const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
7077
- for (const [rule, ruleDiagnostics] of visibleRules) {
7079
+ for (const [ruleKey, ruleDiagnostics] of visibleRules) {
7078
7080
  const severity = ruleDiagnostics[0].severity;
7079
7081
  const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
7080
7082
  const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
7081
7083
  const remainingFileCount = uniqueFiles.length - shownFiles.length;
7082
- lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${rule} (×${ruleDiagnostics.length})`);
7084
+ lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
7083
7085
  lines.push(` ${ruleDiagnostics[0].message}`);
7086
+ lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
7084
7087
  for (const filePath of shownFiles) {
7085
7088
  const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
7086
7089
  lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
@@ -7100,11 +7103,12 @@ const buildIssuesSummary = (input) => {
7100
7103
  lines.push("");
7101
7104
  lines.push("## How to fix");
7102
7105
  lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
7103
- lines.push("2. Fix errors first, then warnings. Start with high-count rules.");
7104
- lines.push("3. Read the code before acting. Treat findings as hypotheses, not commands.");
7105
- lines.push("4. Fix root causes, not symptoms. Don't suppress rules without evidence.");
7106
- lines.push("5. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
7107
- lines.push("6. Split unrelated fixes into separate PRs.");
7106
+ lines.push("2. For each rule above, fetch & follow its canonical fix recipe URL before fixing.");
7107
+ lines.push("3. Fix errors first, then warnings. Start with high-count rules.");
7108
+ lines.push("4. Read the code before acting. Treat findings as hypotheses, not commands.");
7109
+ lines.push("5. Fix root causes, not symptoms. Don't suppress rules without evidence.");
7110
+ lines.push("6. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
7111
+ lines.push("7. Split unrelated fixes into separate PRs.");
7108
7112
  return lines.join("\n");
7109
7113
  };
7110
7114
  const copyToClipboard = (text) => {
@@ -7159,6 +7163,29 @@ const promptCopyIssues = async (input) => {
7159
7163
  else cliLogger.log(issuesSummary);
7160
7164
  };
7161
7165
  //#endregion
7166
+ //#region src/cli/utils/path-format.ts
7167
+ const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
7168
+ //#endregion
7169
+ //#region src/cli/utils/read-changed-files-from.ts
7170
+ const isSafeRelativePath = (filePath) => {
7171
+ if (filePath.length === 0) return false;
7172
+ if (filePath.includes("\0")) return false;
7173
+ if (path.isAbsolute(filePath)) return false;
7174
+ const normalized = path.posix.normalize(filePath);
7175
+ if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
7176
+ return normalized === filePath;
7177
+ };
7178
+ const readChangedFilesFrom = (filePath) => {
7179
+ const raw = fs.readFileSync(filePath, "utf8");
7180
+ const uniqueFiles = /* @__PURE__ */ new Set();
7181
+ for (const line of raw.split(/\r?\n/)) {
7182
+ const candidate = toForwardSlashes(line.trim());
7183
+ if (!isSafeRelativePath(candidate)) continue;
7184
+ uniqueFiles.add(candidate);
7185
+ }
7186
+ return [...uniqueFiles];
7187
+ };
7188
+ //#endregion
7162
7189
  //#region src/cli/utils/render-multi-project-summary.ts
7163
7190
  const SUMMARY_BAR_WIDTH_CHARS = 20;
7164
7191
  const buildMiniBar = (score) => {
@@ -7221,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
7221
7248
  };
7222
7249
  });
7223
7250
  const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
7251
+ yield* Console.log("");
7224
7252
  for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
7225
7253
  yield* Console.log("");
7226
7254
  });
@@ -7416,7 +7444,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
7416
7444
  return true;
7417
7445
  };
7418
7446
  const shouldPromptInstallSetup = (options) => {
7419
- if (!options.hasScoredScan) return false;
7447
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7420
7448
  if (options.isJsonMode) return false;
7421
7449
  if (options.isScoreOnly) return false;
7422
7450
  if (options.isStaged) return false;
@@ -7426,13 +7454,13 @@ const shouldPromptInstallSetup = (options) => {
7426
7454
  return !hasDoctorScript(options.projectRoot);
7427
7455
  };
7428
7456
  const resolveInstallSetupProjectRoot = (options) => {
7429
- if (options.completedScanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7457
+ if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7430
7458
  const packageDirectories = /* @__PURE__ */ new Set();
7431
- for (const scanDirectory of options.completedScanDirectories) {
7459
+ for (const scanDirectory of options.scanDirectories) {
7432
7460
  const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
7433
7461
  packageDirectories.add(packageDirectory);
7434
7462
  }
7435
- if (packageDirectories.size !== 1) return null;
7463
+ if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
7436
7464
  return [...packageDirectories][0] ?? null;
7437
7465
  };
7438
7466
  const defaultWait = (milliseconds) => new Promise((resolve) => {
@@ -7467,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
7467
7495
  return;
7468
7496
  }
7469
7497
  try {
7470
- const { cliLogger } = await import("./cli-logger-BliQX9s8.js").then((n) => n.n);
7498
+ const { cliLogger } = await import("./cli-logger-CSZagq1E.js").then((n) => n.n);
7471
7499
  cliLogger.warn(message);
7472
7500
  } catch {}
7473
7501
  };
@@ -7483,7 +7511,7 @@ const promptInstallSetup = async (options) => {
7483
7511
  writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
7484
7512
  return;
7485
7513
  }
7486
- const install = options.install ?? (await Promise.resolve().then(() => install_skill_exports)).runInstallSkill;
7514
+ const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
7487
7515
  const previousExitCode = process.exitCode;
7488
7516
  let setupExitCode;
7489
7517
  try {
@@ -7502,7 +7530,7 @@ const promptInstallSetup = async (options) => {
7502
7530
  }
7503
7531
  };
7504
7532
  const shouldShowAgentInstallHint = (options) => {
7505
- if (!options.hasScoredScan) return false;
7533
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7506
7534
  if (options.isJsonMode) return false;
7507
7535
  if (options.isScoreOnly) return false;
7508
7536
  if (options.isStaged) return false;
@@ -7567,15 +7595,17 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
7567
7595
  const { scanScope } = await prompts({
7568
7596
  type: "select",
7569
7597
  name: "scanScope",
7570
- message: "Select",
7598
+ message: "Choose what to scan",
7571
7599
  choices: [{
7572
7600
  title: "Full codebase",
7601
+ description: "Scan every source file",
7573
7602
  value: "full"
7574
7603
  }, {
7575
- title: `Changed files (${changedSourceFiles.length})`,
7604
+ title: diffInfo.isCurrentChanges ? `Uncommitted changes (${changedSourceFiles.length})` : `Changed files on ${diffInfo.currentBranch ?? "this branch"} (${changedSourceFiles.length})`,
7605
+ description: diffInfo.isCurrentChanges ? "Compare working tree changes against HEAD" : `Compare against ${diffInfo.baseBranch} from the branch merge-base`,
7576
7606
  value: "branch"
7577
7607
  }],
7578
- initial: 0
7608
+ initial: diffInfo.isCurrentChanges ? 0 : 1
7579
7609
  });
7580
7610
  return scanScope === "branch";
7581
7611
  };
@@ -7605,17 +7635,16 @@ const VALID_FAIL_ON_LEVELS = new Set([
7605
7635
  "warning",
7606
7636
  "none"
7607
7637
  ]);
7608
- const DEFAULT_FAIL_ON_LEVEL = "error";
7638
+ const DEFAULT_FAIL_ON_LEVEL = "none";
7609
7639
  const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
7610
7640
  const resolveFailOnLevel = (flags, userConfig) => {
7611
7641
  const sourceValue = flags.failOn ?? userConfig?.failOn ?? DEFAULT_FAIL_ON_LEVEL;
7612
7642
  if (isValidFailOnLevel(sourceValue)) return sourceValue;
7613
- cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "none".`);
7614
- return "none";
7643
+ cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "${DEFAULT_FAIL_ON_LEVEL}".`);
7644
+ return DEFAULT_FAIL_ON_LEVEL;
7615
7645
  };
7616
7646
  //#endregion
7617
7647
  //#region src/cli/utils/resolve-project-diff-include-paths.ts
7618
- const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
7619
7648
  const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
7620
7649
  const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
7621
7650
  const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
@@ -7709,7 +7738,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
7709
7738
  }
7710
7739
  if (packages.length === 0) return [rootDirectory];
7711
7740
  if (packages.length === 1) {
7712
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages[0].name}`);
7741
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
7713
7742
  return [packages[0].directory];
7714
7743
  }
7715
7744
  if (projectFlag) return resolveProjectFlag(projectFlag, packages);
@@ -7733,13 +7762,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
7733
7762
  return resolvedDirectories;
7734
7763
  };
7735
7764
  const printDiscoveredProjects = (packages) => {
7736
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7765
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7737
7766
  };
7738
7767
  const promptProjectSelection = async (workspacePackages, rootDirectory) => {
7739
7768
  const { selectedDirectories } = await prompts({
7740
7769
  type: "multiselect",
7741
7770
  name: "selectedDirectories",
7742
- message: "Select projects to scan",
7771
+ message: "Select projects",
7743
7772
  choices: workspacePackages.map((workspacePackage) => ({
7744
7773
  title: workspacePackage.name,
7745
7774
  description: path.relative(rootDirectory, workspacePackage.directory),
@@ -7814,7 +7843,7 @@ const validateModeFlags = (flags) => {
7814
7843
  if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
7815
7844
  if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
7816
7845
  if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
7817
- if (flags.annotations && (flags.json || flags.score)) throw new Error("--annotations cannot be combined with --json or --score.");
7846
+ if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
7818
7847
  if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
7819
7848
  if ((flags.explain ?? flags.why) !== void 0 && (flags.json || flags.score || flags.annotations || flags.staged)) throw new Error("--explain cannot be combined with --json, --score, --annotations, or --staged.");
7820
7849
  };
@@ -7840,6 +7869,12 @@ const finalizeScans = (input) => {
7840
7869
  const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
7841
7870
  if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
7842
7871
  };
7872
+ const buildChangedFilesDiffInfo = (changedFiles) => ({
7873
+ currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
7874
+ baseBranch: process.env.GITHUB_BASE_REF?.trim() || "pull request target",
7875
+ changedFiles,
7876
+ isCurrentChanges: false
7877
+ });
7843
7878
  const inspectAction = async (directory, flags) => {
7844
7879
  const isScoreOnly = Boolean(flags.score);
7845
7880
  const isJsonMode = Boolean(flags.json);
@@ -7896,7 +7931,14 @@ const inspectAction = async (directory, flags) => {
7896
7931
  cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
7897
7932
  cliLogger.break();
7898
7933
  }
7899
- const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, mkdtempSync(path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX)));
7934
+ const tempDirectory = mkdtempSync(path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
7935
+ const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory).catch((error) => {
7936
+ rmSync(tempDirectory, {
7937
+ recursive: true,
7938
+ force: true
7939
+ });
7940
+ throw error;
7941
+ });
7900
7942
  try {
7901
7943
  const scanResult = await inspect(snapshot.tempDirectory, {
7902
7944
  ...scanOptions,
@@ -7905,7 +7947,7 @@ const inspectAction = async (directory, flags) => {
7905
7947
  });
7906
7948
  const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
7907
7949
  ...diagnostic,
7908
- filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, resolvedDirectory) : diagnostic.filePath
7950
+ filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, () => resolvedDirectory) : diagnostic.filePath
7909
7951
  }));
7910
7952
  finalizeScans({
7911
7953
  diagnostics: remappedDiagnostics,
@@ -7935,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
7935
7977
  return;
7936
7978
  }
7937
7979
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
7980
+ const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
7938
7981
  const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
7939
- const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
7940
- const isDiffMode = await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
7982
+ const diffInfo = changedFilesDiffInfo ?? (changedFilesDiffInfo === null && (effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet) ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null);
7983
+ const isDiffMode = changedFilesDiffInfo !== null || await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
7941
7984
  setJsonReportMode(isDiffMode ? "diff" : "full");
7942
7985
  if (isDiffMode && diffInfo && !isQuiet) {
7943
7986
  if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
@@ -7996,13 +8039,13 @@ const inspectAction = async (directory, flags) => {
7996
8039
  });
7997
8040
  const setupProjectRoot = resolveInstallSetupProjectRoot({
7998
8041
  scanRoot: resolvedDirectory,
7999
- completedScanDirectories: completedScans.map((scan) => scan.directory)
8042
+ scanDirectories: projectDirectories
8000
8043
  });
8001
8044
  if (setupProjectRoot !== null) {
8002
- const hasScoredScan = completedScans.some((scan) => scan.result.score !== null);
8045
+ const hasCompletedScan = completedScans.length > 0;
8003
8046
  await promptInstallSetup({
8004
8047
  projectRoot: setupProjectRoot,
8005
- hasScoredScan,
8048
+ hasCompletedScan,
8006
8049
  issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
8007
8050
  isJsonMode,
8008
8051
  isScoreOnly,
@@ -8011,7 +8054,7 @@ const inspectAction = async (directory, flags) => {
8011
8054
  });
8012
8055
  if (shouldShowAgentInstallHint({
8013
8056
  projectRoot: setupProjectRoot,
8014
- hasScoredScan,
8057
+ hasCompletedScan,
8015
8058
  isJsonMode,
8016
8059
  isScoreOnly,
8017
8060
  isStaged: Boolean(flags.staged)
@@ -8643,8 +8686,12 @@ const installReactDoctorGitHook = (options) => {
8643
8686
  return installDirectGitHook(options);
8644
8687
  };
8645
8688
  //#endregion
8646
- //#region src/cli/utils/install-skill.ts
8647
- var install_skill_exports = /* @__PURE__ */ __exportAll({ runInstallSkill: () => runInstallSkill });
8689
+ //#region src/cli/utils/install-react-doctor.ts
8690
+ var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
8691
+ const SETUP_OPTION_GIT_HOOK = "git-hook";
8692
+ const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
8693
+ const SETUP_OPTION_WORKFLOW = "workflow";
8694
+ const SETUP_OPTION_SKIP = "skip";
8648
8695
  const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
8649
8696
  "ghooks",
8650
8697
  "git-hooks-js",
@@ -8825,7 +8872,32 @@ const getSkillSourceDirectory = () => {
8825
8872
  const distDirectory = path.dirname(fileURLToPath(import.meta.url));
8826
8873
  return path.join(distDirectory, "skills", SKILL_NAME);
8827
8874
  };
8828
- const runInstallSkill = async (options = {}) => {
8875
+ const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
8876
+ const buildWorkflowContent = () => [
8877
+ "name: React Doctor",
8878
+ "",
8879
+ "on:",
8880
+ " pull_request:",
8881
+ " types: [opened, synchronize, reopened, ready_for_review]",
8882
+ "",
8883
+ "permissions:",
8884
+ " contents: read",
8885
+ " pull-requests: write",
8886
+ " issues: write",
8887
+ "",
8888
+ "concurrency:",
8889
+ " group: react-doctor-${{ github.event.pull_request.number || github.ref }}",
8890
+ " cancel-in-progress: true",
8891
+ "",
8892
+ "jobs:",
8893
+ " react-doctor:",
8894
+ " runs-on: ubuntu-latest",
8895
+ " steps:",
8896
+ " - uses: actions/checkout@v5",
8897
+ " - uses: millionco/react-doctor@main",
8898
+ ""
8899
+ ].join("\n");
8900
+ const runInstallReactDoctor = async (options = {}) => {
8829
8901
  const requestedProjectRoot = options.projectRoot ?? process.cwd();
8830
8902
  const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
8831
8903
  const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
@@ -8846,7 +8918,8 @@ const runInstallSkill = async (options = {}) => {
8846
8918
  const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
8847
8919
  const gitHookPath = gitHookTarget?.hookPath;
8848
8920
  const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
8849
- const selectedAgents = skipPrompts ? detectedAgents : (await prompts({
8921
+ const prompt = options.prompt ?? prompts;
8922
+ const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
8850
8923
  type: "multiselect",
8851
8924
  name: "agents",
8852
8925
  message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
@@ -8859,13 +8932,48 @@ const runInstallSkill = async (options = {}) => {
8859
8932
  min: 1
8860
8933
  }, promptOptions)).agents ?? [];
8861
8934
  if (selectedAgents.length === 0) return;
8862
- const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !skipPrompts && Boolean((await prompts({
8863
- type: "confirm",
8864
- name: "installGitHook",
8865
- message: "Check for issues before each commit?",
8866
- initial: true
8867
- }, promptOptions)).installGitHook));
8868
- const shouldInstallAgentHooks = Boolean(options.agentHooks);
8935
+ const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8936
+ const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8937
+ const hasExistingWorkflows = existsSync(workflowsDirectory);
8938
+ const canInstallWorkflow = !existsSync(workflowTargetPath);
8939
+ const setupActionChoices = [
8940
+ ...gitHookPath === null || gitHookPath === void 0 ? [] : [{
8941
+ title: "Pre-commit hook",
8942
+ description: "Check staged changes before each commit",
8943
+ value: SETUP_OPTION_GIT_HOOK,
8944
+ selected: true
8945
+ }],
8946
+ ...canInstallNativeAgentHooks(selectedAgents) ? [{
8947
+ title: "Agent hooks",
8948
+ description: "Ask Claude Code or Cursor to scan after code edits",
8949
+ value: SETUP_OPTION_AGENT_HOOKS,
8950
+ selected: Boolean(options.agentHooks)
8951
+ }] : [],
8952
+ ...canInstallWorkflow ? [{
8953
+ title: "GitHub Actions workflow",
8954
+ description: "Scan pull requests in CI",
8955
+ value: SETUP_OPTION_WORKFLOW,
8956
+ selected: hasExistingWorkflows
8957
+ }] : []
8958
+ ];
8959
+ const setupChoices = setupActionChoices.length === 0 ? [] : [{
8960
+ title: "Skip optional setup",
8961
+ description: "Install only the agent skill and package setup",
8962
+ value: SETUP_OPTION_SKIP,
8963
+ selected: false
8964
+ }, ...setupActionChoices];
8965
+ const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
8966
+ type: "multiselect",
8967
+ name: "setupOptions",
8968
+ message: "Select additional React Doctor setup:",
8969
+ choices: setupChoices,
8970
+ instructions: false
8971
+ }, promptOptions)).setupOptions ?? [];
8972
+ const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
8973
+ const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
8974
+ const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
8975
+ const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
8976
+ const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
8869
8977
  if (options.dryRun) {
8870
8978
  cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
8871
8979
  for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
@@ -8874,6 +8982,7 @@ const runInstallSkill = async (options = {}) => {
8874
8982
  cliLogger.dim(" Dev dependency: react-doctor");
8875
8983
  if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
8876
8984
  if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
8985
+ if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
8877
8986
  return;
8878
8987
  }
8879
8988
  const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
@@ -8921,47 +9030,15 @@ const runInstallSkill = async (options = {}) => {
8921
9030
  throw error;
8922
9031
  }
8923
9032
  }
8924
- const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8925
- const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8926
- if (!existsSync(workflowTargetPath) && !skipPrompts) {
8927
- const hasExistingWorkflows = existsSync(workflowsDirectory);
8928
- const { shouldInstallWorkflow } = await prompts({
8929
- type: "confirm",
8930
- name: "shouldInstallWorkflow",
8931
- message: "Add a GitHub Actions workflow to scan PRs?",
8932
- initial: hasExistingWorkflows
8933
- }, promptOptions);
8934
- if (shouldInstallWorkflow) {
8935
- if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8936
- const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8937
- try {
8938
- writeFileSync(workflowTargetPath, [
8939
- "name: React Doctor",
8940
- "",
8941
- "on:",
8942
- " pull_request:",
8943
- " branches: [main]",
8944
- "",
8945
- "permissions:",
8946
- " contents: read",
8947
- " pull-requests: write",
8948
- "",
8949
- "jobs:",
8950
- " react-doctor:",
8951
- " runs-on: ubuntu-latest",
8952
- " steps:",
8953
- " - uses: actions/checkout@v4",
8954
- " - uses: millionco/react-doctor@main",
8955
- " with:",
8956
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8957
- " diff: main",
8958
- ""
8959
- ].join("\n"));
8960
- workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8961
- } catch (error) {
8962
- workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8963
- throw error;
8964
- }
9033
+ if (shouldInstallWorkflow) {
9034
+ if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
9035
+ const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
9036
+ try {
9037
+ writeFileSync(workflowTargetPath, buildWorkflowContent());
9038
+ workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
9039
+ } catch (error) {
9040
+ workflowSpinner.fail("Failed to add GitHub Actions workflow.");
9041
+ throw error;
8965
9042
  }
8966
9043
  }
8967
9044
  };
@@ -8971,7 +9048,7 @@ const installAction = async (options, command) => {
8971
9048
  Effect.runSync(printBrandedHeader);
8972
9049
  try {
8973
9050
  const parentOptions = command?.parent?.opts?.();
8974
- await runInstallSkill({
9051
+ await runInstallReactDoctor({
8975
9052
  yes: options.yes ?? parentOptions?.yes,
8976
9053
  dryRun: options.dryRun,
8977
9054
  agentHooks: options.agentHooks,
@@ -9015,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
9015
9092
  "--yes"
9016
9093
  ]),
9017
9094
  longOptionsWithRequiredValues: new Set([
9095
+ "--changed-files-from",
9018
9096
  "--explain",
9019
9097
  "--fail-on",
9020
9098
  "--project",
@@ -9108,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
9108
9186
  ];
9109
9187
  };
9110
9188
  //#endregion
9189
+ //#region src/cli/utils/unref-stdin.ts
9190
+ const unrefStdin = () => {
9191
+ process.stdin.unref?.();
9192
+ };
9193
+ //#endregion
9111
9194
  //#region src/cli/index.ts
9112
9195
  process.on("SIGINT", exitGracefully);
9113
9196
  process.on("SIGTERM", exitGracefully);
9114
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: error)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
9197
+ unrefStdin();
9198
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
9115
9199
  ${highlighter.dim("Configuration:")}
9116
9200
  Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
9117
9201
  CLI flags always override config values. See the README for the full schema.