react-doctor 0.2.11 → 0.2.12-dev.269ca17

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-pbFEieEc.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) {
@@ -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.11";
6677
+ const VERSION = "0.2.12-dev.269ca17";
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;
@@ -7068,15 +7074,16 @@ const buildIssuesSummary = (input) => {
7068
7074
  if (input.score) lines.push(`Score: ${input.score.score}/100`);
7069
7075
  lines.push(`${input.diagnostics.length} issues found`);
7070
7076
  lines.push("");
7071
- 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);
7072
7078
  const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
7073
- for (const [rule, ruleDiagnostics] of visibleRules) {
7079
+ for (const [ruleKey, ruleDiagnostics] of visibleRules) {
7074
7080
  const severity = ruleDiagnostics[0].severity;
7075
7081
  const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
7076
7082
  const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
7077
7083
  const remainingFileCount = uniqueFiles.length - shownFiles.length;
7078
- lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${rule} (×${ruleDiagnostics.length})`);
7084
+ lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
7079
7085
  lines.push(` ${ruleDiagnostics[0].message}`);
7086
+ lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
7080
7087
  for (const filePath of shownFiles) {
7081
7088
  const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
7082
7089
  lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
@@ -7096,11 +7103,12 @@ const buildIssuesSummary = (input) => {
7096
7103
  lines.push("");
7097
7104
  lines.push("## How to fix");
7098
7105
  lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
7099
- lines.push("2. Fix errors first, then warnings. Start with high-count rules.");
7100
- lines.push("3. Read the code before acting. Treat findings as hypotheses, not commands.");
7101
- lines.push("4. Fix root causes, not symptoms. Don't suppress rules without evidence.");
7102
- lines.push("5. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
7103
- 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.");
7104
7112
  return lines.join("\n");
7105
7113
  };
7106
7114
  const copyToClipboard = (text) => {
@@ -7155,6 +7163,29 @@ const promptCopyIssues = async (input) => {
7155
7163
  else cliLogger.log(issuesSummary);
7156
7164
  };
7157
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
7158
7189
  //#region src/cli/utils/render-multi-project-summary.ts
7159
7190
  const SUMMARY_BAR_WIDTH_CHARS = 20;
7160
7191
  const buildMiniBar = (score) => {
@@ -7217,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
7217
7248
  };
7218
7249
  });
7219
7250
  const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
7251
+ yield* Console.log("");
7220
7252
  for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
7221
7253
  yield* Console.log("");
7222
7254
  });
@@ -7463,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
7463
7495
  return;
7464
7496
  }
7465
7497
  try {
7466
- const { cliLogger } = await import("./cli-logger-pbFEieEc.js").then((n) => n.n);
7498
+ const { cliLogger } = await import("./cli-logger-CSZagq1E.js").then((n) => n.n);
7467
7499
  cliLogger.warn(message);
7468
7500
  } catch {}
7469
7501
  };
@@ -7566,12 +7598,14 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
7566
7598
  message: "Choose what to scan",
7567
7599
  choices: [{
7568
7600
  title: "Full codebase",
7601
+ description: "Scan every source file",
7569
7602
  value: "full"
7570
7603
  }, {
7571
- 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`,
7572
7606
  value: "branch"
7573
7607
  }],
7574
- initial: 0
7608
+ initial: diffInfo.isCurrentChanges ? 0 : 1
7575
7609
  });
7576
7610
  return scanScope === "branch";
7577
7611
  };
@@ -7601,17 +7635,16 @@ const VALID_FAIL_ON_LEVELS = new Set([
7601
7635
  "warning",
7602
7636
  "none"
7603
7637
  ]);
7604
- const DEFAULT_FAIL_ON_LEVEL = "error";
7638
+ const DEFAULT_FAIL_ON_LEVEL = "none";
7605
7639
  const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
7606
7640
  const resolveFailOnLevel = (flags, userConfig) => {
7607
7641
  const sourceValue = flags.failOn ?? userConfig?.failOn ?? DEFAULT_FAIL_ON_LEVEL;
7608
7642
  if (isValidFailOnLevel(sourceValue)) return sourceValue;
7609
- cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "none".`);
7610
- 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;
7611
7645
  };
7612
7646
  //#endregion
7613
7647
  //#region src/cli/utils/resolve-project-diff-include-paths.ts
7614
- const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
7615
7648
  const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
7616
7649
  const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
7617
7650
  const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
@@ -7810,7 +7843,7 @@ const validateModeFlags = (flags) => {
7810
7843
  if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
7811
7844
  if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
7812
7845
  if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
7813
- 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.");
7814
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.");
7815
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.");
7816
7849
  };
@@ -7836,6 +7869,12 @@ const finalizeScans = (input) => {
7836
7869
  const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
7837
7870
  if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
7838
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
+ });
7839
7878
  const inspectAction = async (directory, flags) => {
7840
7879
  const isScoreOnly = Boolean(flags.score);
7841
7880
  const isJsonMode = Boolean(flags.json);
@@ -7892,7 +7931,14 @@ const inspectAction = async (directory, flags) => {
7892
7931
  cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
7893
7932
  cliLogger.break();
7894
7933
  }
7895
- 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
+ });
7896
7942
  try {
7897
7943
  const scanResult = await inspect(snapshot.tempDirectory, {
7898
7944
  ...scanOptions,
@@ -7901,7 +7947,7 @@ const inspectAction = async (directory, flags) => {
7901
7947
  });
7902
7948
  const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
7903
7949
  ...diagnostic,
7904
- 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
7905
7951
  }));
7906
7952
  finalizeScans({
7907
7953
  diagnostics: remappedDiagnostics,
@@ -7931,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
7931
7977
  return;
7932
7978
  }
7933
7979
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
7980
+ const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
7934
7981
  const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
7935
- const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
7936
- 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);
7937
7984
  setJsonReportMode(isDiffMode ? "diff" : "full");
7938
7985
  if (isDiffMode && diffInfo && !isQuiet) {
7939
7986
  if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
@@ -8831,21 +8878,23 @@ const buildWorkflowContent = () => [
8831
8878
  "",
8832
8879
  "on:",
8833
8880
  " pull_request:",
8834
- " branches: [main]",
8881
+ " types: [opened, synchronize, reopened, ready_for_review]",
8835
8882
  "",
8836
8883
  "permissions:",
8837
8884
  " contents: read",
8838
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",
8839
8891
  "",
8840
8892
  "jobs:",
8841
8893
  " react-doctor:",
8842
8894
  " runs-on: ubuntu-latest",
8843
8895
  " steps:",
8844
- " - uses: actions/checkout@v4",
8896
+ " - uses: actions/checkout@v5",
8845
8897
  " - uses: millionco/react-doctor@main",
8846
- " with:",
8847
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8848
- " diff: main",
8849
8898
  ""
8850
8899
  ].join("\n");
8851
8900
  const runInstallReactDoctor = async (options = {}) => {
@@ -9043,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
9043
9092
  "--yes"
9044
9093
  ]),
9045
9094
  longOptionsWithRequiredValues: new Set([
9095
+ "--changed-files-from",
9046
9096
  "--explain",
9047
9097
  "--fail-on",
9048
9098
  "--project",
@@ -9136,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
9136
9186
  ];
9137
9187
  };
9138
9188
  //#endregion
9189
+ //#region src/cli/utils/unref-stdin.ts
9190
+ const unrefStdin = () => {
9191
+ process.stdin.unref?.();
9192
+ };
9193
+ //#endregion
9139
9194
  //#region src/cli/index.ts
9140
9195
  process.on("SIGINT", exitGracefully);
9141
9196
  process.on("SIGTERM", exitGracefully);
9142
- 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", `
9143
9199
  ${highlighter.dim("Configuration:")}
9144
9200
  Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
9145
9201
  CLI flags always override config values. See the README for the full schema.
package/dist/index.d.ts CHANGED
@@ -305,20 +305,25 @@ interface ProjectInfo {
305
305
  reactVersion: string | null;
306
306
  reactMajorVersion: number | null;
307
307
  tailwindVersion: string | null;
308
+ zodVersion: string | null;
309
+ /** Parsed major from `zodVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
310
+ zodMajorVersion: number | null;
308
311
  framework: Framework;
309
312
  hasTypeScript: boolean;
310
313
  hasReactCompiler: boolean;
311
314
  hasTanStackQuery: boolean;
312
315
  /**
313
- * `true` when `preact` is declared anywhere in the project's
314
- * dependency manifest. Drives the `preact` capability in
315
- * `buildCapabilities`, which gates every `preact-*` rule. Modeled
316
- * on `hasTanStackQuery` rather than the `framework` field because
317
- * the dominant Preact setup today is Preact-on-Vite — those
318
- * projects classify as `framework: "vite"` for build-tool reasons
319
- * but still need Preact-specific rules to fire.
316
+ * The declared `preact` version spec, or `null` when Preact isn't a
317
+ * dependency. Parallels `reactVersion` so a React-compatible runtime is
318
+ * modeled the same way React is. Drives the `preact` capability in
319
+ * `buildCapabilities` (which gates every `preact-*` rule) — keyed off
320
+ * this rather than `framework` because the dominant Preact setup
321
+ * (Preact-on-Vite) classifies as `framework: "vite"` but still needs
322
+ * Preact rules to fire.
320
323
  */
321
- hasPreact: boolean;
324
+ preactVersion: string | null;
325
+ /** Parsed major from `preactVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
326
+ preactMajorVersion: number | null;
322
327
  /**
323
328
  * `true` when the project (or any of its workspace packages) declares
324
329
  * React Native or Expo as a dependency. Enables the `react-native`