react-doctor 0.5.6-dev.b08ca1c → 0.5.6-dev.b8170f8

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,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bb93aaad-ea85-5f1d-b2db-584f48671f66")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ad091b20-c3e2-5c96-93ac-9a910745a035")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -14,7 +14,7 @@ import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
16
16
  import * as NodeUrl from "node:url";
17
- import { fileURLToPath } from "node:url";
17
+ import { fileURLToPath, pathToFileURL } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
20
  import crypto, { createHash, randomUUID } from "node:crypto";
@@ -41320,8 +41320,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41320
41320
  }
41321
41321
  return enabled;
41322
41322
  };
41323
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41324
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41323
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
41324
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41325
41325
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41326
41326
  const jsPlugins = [];
41327
41327
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -42018,9 +42018,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42018
42018
  try {
42019
42019
  parsed = JSON.parse(sanitizedStdout);
42020
42020
  } catch {
42021
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42021
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42022
42022
  }
42023
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42023
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42024
42024
  const minifiedFileCache = /* @__PURE__ */ new Map();
42025
42025
  const isMinifiedDiagnosticFile = (filename) => {
42026
42026
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42311,6 +42311,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42311
42311
  NFS.closeSync(fileHandle);
42312
42312
  }
42313
42313
  };
42314
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
42315
+ /**
42316
+ * Detects an oxlint config-load crash caused by the optional
42317
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
42318
+ * builds the partial-failure note for it; returns `null` when the failure
42319
+ * was anything else.
42320
+ *
42321
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
42322
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
42323
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
42324
+ * config load on it, leaving the plugin in would drop every curated
42325
+ * react-doctor diagnostic too — so the caller retries with the plugin
42326
+ * stripped (issue #833). Both markers sit at the start of oxlint's
42327
+ * message, so they survive the `preview` slice even for deep pnpm paths.
42328
+ */
42329
+ const reactHooksJsPluginDropNote = (error) => {
42330
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
42331
+ const { preview } = error.reason;
42332
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
42333
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
42334
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
42335
+ };
42314
42336
  /**
42315
42337
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42316
42338
  *
@@ -42338,15 +42360,16 @@ const runOxlint = async (options) => {
42338
42360
  const pluginPath = resolvePluginPath();
42339
42361
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42340
42362
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42341
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42363
+ const buildConfig = (overrides) => createOxlintConfig({
42342
42364
  pluginPath,
42343
42365
  project,
42344
42366
  customRulesOnly,
42345
- extendsPaths: extendsForThisAttempt,
42367
+ extendsPaths: overrides.extendsPaths,
42346
42368
  ignoredTags,
42347
42369
  serverAuthFunctionNames,
42348
42370
  severityControls,
42349
- userPlugins
42371
+ userPlugins,
42372
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42350
42373
  });
42351
42374
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42352
42375
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42382,12 +42405,22 @@ const runOxlint = async (options) => {
42382
42405
  outputMaxBytes,
42383
42406
  concurrency: options.concurrency
42384
42407
  });
42385
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42408
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42386
42409
  try {
42387
42410
  return await runBatches();
42388
42411
  } catch (error) {
42412
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
42413
+ if (reactHooksJsDropNote !== null) {
42414
+ writeOxlintConfig(configPath, buildConfig({
42415
+ extendsPaths,
42416
+ disableReactHooksJsPlugin: true
42417
+ }));
42418
+ const diagnostics = await runBatches();
42419
+ onPartialFailure?.(reactHooksJsDropNote);
42420
+ return diagnostics;
42421
+ }
42389
42422
  if (extendsPaths.length === 0) throw error;
42390
- writeOxlintConfig(configPath, buildConfig([]));
42423
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42391
42424
  return await runBatches();
42392
42425
  }
42393
42426
  } finally {
@@ -43839,7 +43872,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43839
43872
  "false"
43840
43873
  ]);
43841
43874
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
43842
- const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(process.env[environmentVariable])) || isCiFlagSet(process.env.CI);
43875
+ const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
43843
43876
  const detectCiProvider = () => {
43844
43877
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43845
43878
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43864,6 +43897,42 @@ const detectCodingAgent = () => {
43864
43897
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43865
43898
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43866
43899
  //#endregion
43900
+ //#region src/cli/utils/detect-terminal-kind.ts
43901
+ const TERMINAL_BY_TERM_PROGRAM = [
43902
+ ["vscode", "vscode"],
43903
+ ["iTerm.app", "iterm"],
43904
+ ["Apple_Terminal", "apple-terminal"],
43905
+ ["WezTerm", "wezterm"],
43906
+ ["ghostty", "ghostty"],
43907
+ ["Hyper", "hyper"],
43908
+ ["Tabby", "tabby"],
43909
+ ["rio", "rio"]
43910
+ ];
43911
+ /**
43912
+ * Best-effort label for the terminal emulator / editor hosting the CLI,
43913
+ * derived from terminal-identity env vars. Recorded as the `terminalKind` run
43914
+ * tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
43915
+ * …) — the split Sentry can't otherwise see. Low-cardinality and free of any
43916
+ * username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
43917
+ * win over the outer emulator because that's the surface a user is reading in;
43918
+ * "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
43919
+ */
43920
+ const detectTerminalKind = (env = process.env) => {
43921
+ if (env.NVIM) return "neovim";
43922
+ if (env.VIM_TERMINAL) return "vim";
43923
+ const termProgram = env.TERM_PROGRAM;
43924
+ if (termProgram) {
43925
+ for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
43926
+ }
43927
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
43928
+ if (env.WT_SESSION) return "windows-terminal";
43929
+ if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
43930
+ if (env.VTE_VERSION) return "vte";
43931
+ if (env.TMUX) return "tmux";
43932
+ if (isCiEnvironment(env)) return "ci";
43933
+ return "unknown";
43934
+ };
43935
+ //#endregion
43867
43936
  //#region src/cli/utils/is-git-hook-environment.ts
43868
43937
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43869
43938
  //#endregion
@@ -43886,6 +43955,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43886
43955
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43887
43956
  //#endregion
43888
43957
  //#region src/cli/utils/constants.ts
43958
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43889
43959
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43890
43960
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43891
43961
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43970,7 +44040,7 @@ const makeNoopConsole = () => ({
43970
44040
  });
43971
44041
  //#endregion
43972
44042
  //#region src/cli/utils/version.ts
43973
- const VERSION = "0.5.6-dev.b08ca1c";
44043
+ const VERSION = "0.5.6-dev.b8170f8";
43974
44044
  //#endregion
43975
44045
  //#region src/cli/utils/json-mode.ts
43976
44046
  let context = null;
@@ -44120,6 +44190,7 @@ const buildRunContext = () => {
44120
44190
  viaAction: isOfficialGithubAction(),
44121
44191
  codingAgent: detectCodingAgent(),
44122
44192
  interactive: !isNonInteractiveEnvironment(),
44193
+ terminalKind: detectTerminalKind(),
44123
44194
  jsonMode: isJsonModeActive(),
44124
44195
  invokedVia: detectInvokedVia()
44125
44196
  };
@@ -44190,6 +44261,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44190
44261
  viaAction: runContext.viaAction,
44191
44262
  codingAgent: runContext.codingAgent,
44192
44263
  interactive: runContext.interactive,
44264
+ terminalKind: runContext.terminalKind,
44193
44265
  jsonMode: runContext.jsonMode,
44194
44266
  invokedVia: runContext.invokedVia,
44195
44267
  nodeMajor: runContext.nodeMajor
@@ -44328,13 +44400,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44328
44400
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44329
44401
  * standard `SENTRY_RELEASE` override.
44330
44402
  */
44331
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.b08ca1c`;
44403
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.b8170f8`;
44332
44404
  /**
44333
44405
  * Deployment environment shown in Sentry's environment filter. Defaults to
44334
44406
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44335
44407
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44336
44408
  */
44337
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.b08ca1c") ? "development" : "production");
44409
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.b8170f8") ? "development" : "production");
44338
44410
  /**
44339
44411
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44340
44412
  * (set to `0` to disable tracing) and falls back to
@@ -48116,7 +48188,7 @@ const AGENT_GUIDANCE_LINES = [
48116
48188
  "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
48117
48189
  "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
48118
48190
  "Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
48119
- "Run `npx react-doctor@latest --verbose --diff` before and after changes, plus relevant tests after each focused batch.",
48191
+ "Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
48120
48192
  "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
48121
48193
  "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
48122
48194
  "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
@@ -48191,6 +48263,15 @@ const boxText = (content, innerWidth) => {
48191
48263
  ].join("\n");
48192
48264
  };
48193
48265
  //#endregion
48266
+ //#region src/cli/utils/resolve-absolute-path.ts
48267
+ /**
48268
+ * Resolves a diagnostic's `filePath` (relative to its project root, or
48269
+ * already absolute) to an absolute path. Shared by the code-frame reader and
48270
+ * the terminal hyperlink builder so both turn a relative path into the same
48271
+ * on-disk location.
48272
+ */
48273
+ const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
48274
+ //#endregion
48194
48275
  //#region src/cli/utils/build-code-frame.ts
48195
48276
  /**
48196
48277
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48201,7 +48282,7 @@ const boxText = (content, innerWidth) => {
48201
48282
  */
48202
48283
  const buildCodeFrame = (input) => {
48203
48284
  if (input.line <= 0) return null;
48204
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48285
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48205
48286
  let source;
48206
48287
  try {
48207
48288
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48241,6 +48322,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48241
48322
  const DIVIDER_INDENT = " ";
48242
48323
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48243
48324
  //#endregion
48325
+ //#region src/cli/utils/format-hyperlink.ts
48326
+ const OSC = "\x1B]";
48327
+ const ST = "\x1B\\";
48328
+ /**
48329
+ * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
48330
+ * are exactly `text`; the link is carried in escape sequences a capable
48331
+ * terminal turns into a click target.
48332
+ */
48333
+ const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
48334
+ //#endregion
48244
48335
  //#region src/cli/utils/indent-multiline-text.ts
48245
48336
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48246
48337
  //#endregion
@@ -48394,17 +48485,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48394
48485
  }
48395
48486
  return clusters;
48396
48487
  };
48397
- const formatClusterLocation = (cluster) => {
48488
+ const formatClusterLocationText = (cluster) => {
48489
+ const { filePath } = cluster.diagnostics[0];
48490
+ if (cluster.startLine <= 0) return filePath;
48491
+ if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
48492
+ return `${filePath}:${cluster.startLine}`;
48493
+ };
48494
+ const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
48398
48495
  const lead = cluster.diagnostics[0];
48399
48496
  const contextTag = formatFileContextTag(lead);
48400
- if (cluster.startLine <= 0) return `${lead.filePath}${contextTag}`;
48401
- if (cluster.endLine > cluster.startLine) return `${lead.filePath}:${cluster.startLine}-${cluster.endLine}${contextTag}`;
48402
- return `${lead.filePath}:${cluster.startLine}${contextTag}`;
48497
+ const location = formatClusterLocationText(cluster);
48498
+ if (!hyperlinks) return `${location}${contextTag}`;
48499
+ return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
48403
48500
  };
48404
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48501
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48405
48502
  const lead = cluster.diagnostics[0];
48406
48503
  const isMultiSite = cluster.diagnostics.length > 1;
48407
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48504
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48408
48505
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48409
48506
  filePath: lead.filePath,
48410
48507
  line: cluster.startLine,
@@ -48423,7 +48520,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48423
48520
  }
48424
48521
  return lines;
48425
48522
  };
48426
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48523
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48427
48524
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48428
48525
  const { severity } = representative;
48429
48526
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48443,7 +48540,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48443
48540
  }
48444
48541
  const renderCodeFrame = severity === "error";
48445
48542
  const sites = renderEverySite ? ruleDiagnostics : [representative];
48446
- if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
48543
+ if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
48447
48544
  return lines;
48448
48545
  };
48449
48546
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48456,7 +48553,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48456
48553
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48457
48554
  };
48458
48555
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48459
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48556
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48460
48557
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48461
48558
  if (topRuleGroups.length === 0) return {
48462
48559
  lines: [],
@@ -48466,7 +48563,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48466
48563
  const blockOffsets = [];
48467
48564
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48468
48565
  blockOffsets.push(lines.length);
48469
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48566
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48470
48567
  lines.push("");
48471
48568
  }
48472
48569
  return {
@@ -48504,18 +48601,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48504
48601
  * single Effect.forEach over Console.log so failures or fiber
48505
48602
  * interruption produce predictable partial output.
48506
48603
  */
48507
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
48604
+ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
48508
48605
  const sectionPause = onboarding.sectionPause ?? void_;
48509
48606
  const animateCountUp = onboarding.animateCountUp ?? false;
48510
48607
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48511
48608
  let detailLines;
48512
48609
  let topErrorBlockOffsets = [];
48513
48610
  if (!isVerbose) {
48514
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48611
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48515
48612
  detailLines = topErrors.lines;
48516
48613
  topErrorBlockOffsets = topErrors.blockOffsets;
48517
48614
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48518
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48615
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48519
48616
  });
48520
48617
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48521
48618
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48576,6 +48673,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48576
48673
  //#endregion
48577
48674
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48578
48675
  const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
48676
+ //#endregion
48677
+ //#region src/cli/utils/supports-hyperlinks.ts
48678
+ const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
48679
+ "iTerm.app",
48680
+ "WezTerm",
48681
+ "vscode",
48682
+ "Hyper",
48683
+ "ghostty",
48684
+ "Tabby",
48685
+ "rio"
48686
+ ]);
48687
+ const parseVteVersion = (raw) => {
48688
+ const parsed = Number.parseInt(raw ?? "", 10);
48689
+ return Number.isNaN(parsed) ? 0 : parsed;
48690
+ };
48691
+ /**
48692
+ * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
48693
+ * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
48694
+ * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
48695
+ * forces on), mirroring how the ecosystem's terminal libraries gate the same
48696
+ * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
48697
+ * raw escape rather than a link). Unknown terminals default to off.
48698
+ */
48699
+ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
48700
+ const forced = env.FORCE_HYPERLINK;
48701
+ if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
48702
+ if (stream.isTTY !== true) return false;
48703
+ if (env.TERM === "dumb") return false;
48704
+ if (isCiEnvironment(env)) return false;
48705
+ if (env.WT_SESSION) return true;
48706
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
48707
+ if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
48708
+ return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
48709
+ };
48710
+ //#endregion
48711
+ //#region src/cli/utils/should-render-hyperlinks.ts
48712
+ /**
48713
+ * Whether to emit OSC 8 clickable `file:line` locations for this run: a
48714
+ * hyperlink-capable terminal AND not a coding agent (whose output parsers
48715
+ * would choke on the escape sequences).
48716
+ */
48717
+ const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
48579
48718
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48580
48719
  const FALSY_FLAG_VALUES = new Set([
48581
48720
  "",
@@ -48595,10 +48734,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48595
48734
  };
48596
48735
  //#endregion
48597
48736
  //#region src/cli/utils/onboarding-state.ts
48598
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48599
48737
  const ONBOARDED_AT_KEY = "onboardedAt";
48600
48738
  const getOnboardingStore = (options = {}) => new Conf({
48601
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48739
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48602
48740
  cwd: options.cwd
48603
48741
  });
48604
48742
  const hasCompletedOnboarding = (options = {}) => {
@@ -49674,6 +49812,7 @@ const finalizeAndRender = (input) => gen(function* () {
49674
49812
  }
49675
49813
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49676
49814
  const pause = onboardingSectionPause(animateRender);
49815
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49677
49816
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49678
49817
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49679
49818
  if (printedDiagnostics.length === 0) {
@@ -49699,7 +49838,7 @@ const finalizeAndRender = (input) => gen(function* () {
49699
49838
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49700
49839
  sectionPause: pause,
49701
49840
  animateCountUp: animateRender
49702
- });
49841
+ }, useHyperlinks);
49703
49842
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49704
49843
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49705
49844
  yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
@@ -49892,16 +50031,7 @@ const buildHandoffPayload = (input) => {
49892
50031
  try {
49893
50032
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49894
50033
  } catch {}
49895
- const lines = [
49896
- "Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
49897
- `- It's used by teams at ${CI_TRUST_COMPANIES}.`,
49898
- "- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
49899
- `- Setup guide: ${CI_URL}`,
49900
- "If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
49901
- "",
49902
- `Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
49903
- ""
49904
- ];
50034
+ const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
49905
50035
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49906
50036
  const representative = ruleDiagnostics[0];
49907
50037
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -50271,38 +50401,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50271
50401
  //#region src/cli/utils/hash-project-root.ts
50272
50402
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50273
50403
  //#endregion
50274
- //#region src/cli/utils/action-upgrade-prompt.ts
50275
- const GLOBAL_CONFIG_PROJECT_NAME$1 = "react-doctor";
50276
- const getActionUpgradeStore = (options = {}) => new Conf({
50277
- projectName: GLOBAL_CONFIG_PROJECT_NAME$1,
50278
- cwd: options.cwd
50279
- });
50280
- const hasHandledActionUpgrade = (projectRoot, storeOptions = {}) => {
50281
- try {
50282
- const upgrades = getActionUpgradeStore(storeOptions).get("actionUpgrades", {});
50283
- return Boolean(upgrades[hashProjectRoot(projectRoot)]);
50284
- } catch {
50285
- return true;
50286
- }
50287
- };
50288
- const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
50289
- try {
50290
- const store = getActionUpgradeStore(storeOptions);
50291
- const upgrades = store.get("actionUpgrades", {});
50292
- store.set("actionUpgrades", {
50293
- ...upgrades,
50294
- [hashProjectRoot(projectRoot)]: {
50295
- rootDirectory: Path.resolve(projectRoot),
50296
- outcome,
50297
- at: (/* @__PURE__ */ new Date()).toISOString()
50404
+ //#region src/cli/utils/project-decision-store.ts
50405
+ const createProjectDecisionStore = (storeKey) => {
50406
+ const getStore = (options = {}) => new Conf({
50407
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
50408
+ cwd: options.cwd
50409
+ });
50410
+ return {
50411
+ getConfigPath: (options = {}) => getStore(options).path,
50412
+ hasHandled: (projectRoot, options = {}) => {
50413
+ try {
50414
+ return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
50415
+ } catch {
50416
+ return true;
50298
50417
  }
50299
- });
50300
- return true;
50301
- } catch {
50302
- return false;
50303
- }
50418
+ },
50419
+ record: (projectRoot, outcome, options = {}) => {
50420
+ try {
50421
+ const store = getStore(options);
50422
+ store.set(storeKey, {
50423
+ ...store.get(storeKey, {}),
50424
+ [hashProjectRoot(projectRoot)]: {
50425
+ rootDirectory: Path.resolve(projectRoot),
50426
+ outcome,
50427
+ at: (/* @__PURE__ */ new Date()).toISOString()
50428
+ }
50429
+ });
50430
+ return true;
50431
+ } catch {
50432
+ return false;
50433
+ }
50434
+ }
50435
+ };
50304
50436
  };
50305
50437
  //#endregion
50438
+ //#region src/cli/utils/action-upgrade-prompt.ts
50439
+ const store$1 = createProjectDecisionStore("actionUpgrades");
50440
+ store$1.getConfigPath;
50441
+ const hasHandledActionUpgrade = store$1.hasHandled;
50442
+ const recordActionUpgradeDecision = store$1.record;
50443
+ //#endregion
50444
+ //#region src/cli/utils/ci-prompt-decision.ts
50445
+ const store = createProjectDecisionStore("ciPrompts");
50446
+ store.getConfigPath;
50447
+ const hasHandledCiPrompt = store.hasHandled;
50448
+ const recordCiPromptDecision = store.record;
50449
+ //#endregion
50306
50450
  //#region src/cli/utils/open-url.ts
50307
50451
  const resolveOpenCommand = (url) => {
50308
50452
  if (process$1.platform === "darwin") return {
@@ -50758,22 +50902,22 @@ const buildAgentHookScript = () => [
50758
50902
  "",
50759
50903
  "run_react_doctor() {",
50760
50904
  " if [ -x ./node_modules/.bin/react-doctor ]; then",
50761
- " ./node_modules/.bin/react-doctor --verbose --diff --blocking warning --no-score",
50905
+ " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
50762
50906
  " return",
50763
50907
  " fi",
50764
50908
  "",
50765
50909
  " if command -v react-doctor >/dev/null 2>&1; then",
50766
- " react-doctor --verbose --diff --blocking warning --no-score",
50910
+ " react-doctor --verbose --scope changed --blocking warning --no-score",
50767
50911
  " return",
50768
50912
  " fi",
50769
50913
  "",
50770
50914
  " if command -v pnpm >/dev/null 2>&1; then",
50771
- " pnpm dlx react-doctor@latest --verbose --diff --blocking warning --no-score",
50915
+ " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50772
50916
  " return",
50773
50917
  " fi",
50774
50918
  "",
50775
50919
  " if command -v npx >/dev/null 2>&1; then",
50776
- " npx --yes react-doctor@latest --verbose --diff --blocking warning --no-score",
50920
+ " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50777
50921
  " return",
50778
50922
  " fi",
50779
50923
  "",
@@ -51519,10 +51663,12 @@ const runInstallReactDoctor = async (options = {}) => {
51519
51663
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51520
51664
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51521
51665
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51522
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || !skipPrompts && await askAddToGitHubActions(prompt) === "yes");
51666
+ const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
51667
+ const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
51523
51668
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51524
51669
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51525
51670
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51671
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51526
51672
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51527
51673
  type: "multiselect",
51528
51674
  name: "agents",
@@ -51769,18 +51915,24 @@ const handoffToAgent = async (input) => {
51769
51915
  if (!input.interactive || input.diagnostics.length === 0) return;
51770
51916
  cliLogger.break();
51771
51917
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51772
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51918
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51919
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51773
51920
  const ciOutcome = await askAddToGitHubActions();
51774
51921
  recordCount(METRIC.agentHandoff, 1, {
51775
51922
  outcome: `ci-${ciOutcome}`,
51776
51923
  diagnosticsCount: input.diagnostics.length
51777
51924
  });
51778
51925
  if (ciOutcome === "cancel") return;
51926
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51779
51927
  if (ciOutcome === "yes") {
51780
51928
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51781
51929
  cliLogger.break();
51782
51930
  }
51783
- } else await maybeOfferActionUpgrade(projectRootForCi);
51931
+ } else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
51932
+ else recordCount(METRIC.agentHandoff, 1, {
51933
+ outcome: "ci-suppressed",
51934
+ diagnosticsCount: input.diagnostics.length
51935
+ });
51784
51936
  const { handoffTarget } = await prompts({
51785
51937
  type: "select",
51786
51938
  name: "handoffTarget",
@@ -52086,7 +52238,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52086
52238
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52087
52239
  if (displayDiagnostics.length > 0) {
52088
52240
  yield* log("");
52089
- yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
52241
+ yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
52090
52242
  }
52091
52243
  const lowestScoredScan = findLowestScoredScan(completedScans);
52092
52244
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52124,9 +52276,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52124
52276
  });
52125
52277
  //#endregion
52126
52278
  //#region src/cli/utils/prompt-install-setup.ts
52127
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52128
52279
  const getSetupPromptStore = (options = {}) => new Conf({
52129
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52280
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52130
52281
  cwd: options.cwd
52131
52282
  });
52132
52283
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52137,6 +52288,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52137
52288
  return false;
52138
52289
  }
52139
52290
  };
52291
+ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
52292
+ try {
52293
+ const store = getSetupPromptStore(storeOptions);
52294
+ const projects = store.get("projects", {});
52295
+ const projectKey = getSetupPromptProjectKey(projectRoot);
52296
+ store.set("projects", {
52297
+ ...projects,
52298
+ [projectKey]: {
52299
+ ...projects[projectKey] ?? {},
52300
+ rootDirectory: Path.resolve(projectRoot),
52301
+ setupPrompt: false
52302
+ }
52303
+ });
52304
+ return true;
52305
+ } catch {
52306
+ return false;
52307
+ }
52308
+ };
52140
52309
  const resolveInstallSetupProjectRoot = (options) => {
52141
52310
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52142
52311
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52543,6 +52712,14 @@ const runExplain = async (fileLineArgument, context) => {
52543
52712
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52544
52713
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52545
52714
  cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
52715
+ const codeFrame = buildCodeFrame({
52716
+ filePath: diagnostic.filePath,
52717
+ line: diagnostic.line,
52718
+ column: diagnostic.column,
52719
+ endLine: diagnostic.endLine,
52720
+ rootDirectory: targetDirectory
52721
+ });
52722
+ if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
52546
52723
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52547
52724
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52548
52725
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52939,6 +53116,7 @@ const inspectAction = async (directory, flags) => {
52939
53116
  })) {
52940
53117
  printAgentInstallHint();
52941
53118
  recordCount(METRIC.agentInstallHintShown, 1);
53119
+ disableSetupPrompt(setupProjectRoot);
52942
53120
  }
52943
53121
  }
52944
53122
  } catch (error) {
@@ -53870,7 +54048,7 @@ ${highlighter.dim("Examples:")}
53870
54048
  ${formatExampleLines([
53871
54049
  ["react-doctor", "scan the current project"],
53872
54050
  ["react-doctor ./apps/web", "scan a specific directory"],
53873
- ["react-doctor --diff main", "scan only files changed vs. main"],
54051
+ ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
53874
54052
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
53875
54053
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
53876
54054
  ["react-doctor --category Security", "show only one diagnostic category"],
@@ -53946,4 +54124,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53946
54124
  export {};
53947
54125
 
53948
54126
  //# sourceMappingURL=cli.js.map
53949
- //# debugId=bb93aaad-ea85-5f1d-b2db-584f48671f66
54127
+ //# debugId=ad091b20-c3e2-5c96-93ac-9a910745a035
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4e83db09-9255-525f-b6fd-405784826e7d")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -38084,8 +38084,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38084
38084
  }
38085
38085
  return enabled;
38086
38086
  };
38087
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38088
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38087
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38088
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38089
38089
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38090
38090
  const jsPlugins = [];
38091
38091
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38782,9 +38782,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38782
38782
  try {
38783
38783
  parsed = JSON.parse(sanitizedStdout);
38784
38784
  } catch {
38785
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38785
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38786
38786
  }
38787
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38787
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38788
38788
  const minifiedFileCache = /* @__PURE__ */ new Map();
38789
38789
  const isMinifiedDiagnosticFile = (filename) => {
38790
38790
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39075,6 +39075,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39075
39075
  NFS.closeSync(fileHandle);
39076
39076
  }
39077
39077
  };
39078
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39079
+ /**
39080
+ * Detects an oxlint config-load crash caused by the optional
39081
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39082
+ * builds the partial-failure note for it; returns `null` when the failure
39083
+ * was anything else.
39084
+ *
39085
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39086
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39087
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39088
+ * config load on it, leaving the plugin in would drop every curated
39089
+ * react-doctor diagnostic too — so the caller retries with the plugin
39090
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39091
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39092
+ */
39093
+ const reactHooksJsPluginDropNote = (error) => {
39094
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39095
+ const { preview } = error.reason;
39096
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39097
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39098
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39099
+ };
39078
39100
  /**
39079
39101
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39080
39102
  *
@@ -39102,15 +39124,16 @@ const runOxlint = async (options) => {
39102
39124
  const pluginPath = resolvePluginPath();
39103
39125
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39104
39126
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39105
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39127
+ const buildConfig = (overrides) => createOxlintConfig({
39106
39128
  pluginPath,
39107
39129
  project,
39108
39130
  customRulesOnly,
39109
- extendsPaths: extendsForThisAttempt,
39131
+ extendsPaths: overrides.extendsPaths,
39110
39132
  ignoredTags,
39111
39133
  serverAuthFunctionNames,
39112
39134
  severityControls,
39113
- userPlugins
39135
+ userPlugins,
39136
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39114
39137
  });
39115
39138
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39116
39139
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39146,12 +39169,22 @@ const runOxlint = async (options) => {
39146
39169
  outputMaxBytes,
39147
39170
  concurrency: options.concurrency
39148
39171
  });
39149
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39172
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39150
39173
  try {
39151
39174
  return await runBatches();
39152
39175
  } catch (error) {
39176
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39177
+ if (reactHooksJsDropNote !== null) {
39178
+ writeOxlintConfig(configPath, buildConfig({
39179
+ extendsPaths,
39180
+ disableReactHooksJsPlugin: true
39181
+ }));
39182
+ const diagnostics = await runBatches();
39183
+ onPartialFailure?.(reactHooksJsDropNote);
39184
+ return diagnostics;
39185
+ }
39153
39186
  if (extendsPaths.length === 0) throw error;
39154
- writeOxlintConfig(configPath, buildConfig([]));
39187
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39155
39188
  return await runBatches();
39156
39189
  }
39157
39190
  } finally {
@@ -40571,4 +40604,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40571
40604
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40572
40605
 
40573
40606
  //# sourceMappingURL=index.js.map
40574
- //# debugId=a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd
40607
+ //# debugId=4e83db09-9255-525f-b6fd-405784826e7d
package/dist/lsp.js CHANGED
@@ -38070,8 +38070,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38070
38070
  }
38071
38071
  return enabled;
38072
38072
  };
38073
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38074
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38073
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38074
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38075
38075
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38076
38076
  const jsPlugins = [];
38077
38077
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38768,9 +38768,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38768
38768
  try {
38769
38769
  parsed = JSON.parse(sanitizedStdout);
38770
38770
  } catch {
38771
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38771
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38772
38772
  }
38773
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38773
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38774
38774
  const minifiedFileCache = /* @__PURE__ */ new Map();
38775
38775
  const isMinifiedDiagnosticFile = (filename) => {
38776
38776
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39061,6 +39061,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39061
39061
  NFS.closeSync(fileHandle);
39062
39062
  }
39063
39063
  };
39064
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39065
+ /**
39066
+ * Detects an oxlint config-load crash caused by the optional
39067
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39068
+ * builds the partial-failure note for it; returns `null` when the failure
39069
+ * was anything else.
39070
+ *
39071
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39072
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39073
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39074
+ * config load on it, leaving the plugin in would drop every curated
39075
+ * react-doctor diagnostic too — so the caller retries with the plugin
39076
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39077
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39078
+ */
39079
+ const reactHooksJsPluginDropNote = (error) => {
39080
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39081
+ const { preview } = error.reason;
39082
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39083
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39084
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39085
+ };
39064
39086
  /**
39065
39087
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39066
39088
  *
@@ -39088,15 +39110,16 @@ const runOxlint = async (options) => {
39088
39110
  const pluginPath = resolvePluginPath();
39089
39111
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39090
39112
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39091
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39113
+ const buildConfig = (overrides) => createOxlintConfig({
39092
39114
  pluginPath,
39093
39115
  project,
39094
39116
  customRulesOnly,
39095
- extendsPaths: extendsForThisAttempt,
39117
+ extendsPaths: overrides.extendsPaths,
39096
39118
  ignoredTags,
39097
39119
  serverAuthFunctionNames,
39098
39120
  severityControls,
39099
- userPlugins
39121
+ userPlugins,
39122
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39100
39123
  });
39101
39124
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39102
39125
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39132,12 +39155,22 @@ const runOxlint = async (options) => {
39132
39155
  outputMaxBytes,
39133
39156
  concurrency: options.concurrency
39134
39157
  });
39135
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39158
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39136
39159
  try {
39137
39160
  return await runBatches();
39138
39161
  } catch (error) {
39162
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39163
+ if (reactHooksJsDropNote !== null) {
39164
+ writeOxlintConfig(configPath, buildConfig({
39165
+ extendsPaths,
39166
+ disableReactHooksJsPlugin: true
39167
+ }));
39168
+ const diagnostics = await runBatches();
39169
+ onPartialFailure?.(reactHooksJsDropNote);
39170
+ return diagnostics;
39171
+ }
39139
39172
  if (extendsPaths.length === 0) throw error;
39140
- writeOxlintConfig(configPath, buildConfig([]));
39173
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39141
39174
  return await runBatches();
39142
39175
  }
39143
39176
  } finally {
@@ -42356,5 +42389,5 @@ const startLanguageServer = () => {
42356
42389
  };
42357
42390
  //#endregion
42358
42391
  export { startLanguageServer };
42359
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="03349093-b1b2-5f21-bad3-7e212e5a7f91")}catch(e){}}();
42360
- //# debugId=03349093-b1b2-5f21-bad3-7e212e5a7f91
42392
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7deadb2d-94c2-54e2-bd0d-808ee8d4c380")}catch(e){}}();
42393
+ //# debugId=7deadb2d-94c2-54e2-bd0d-808ee8d4c380
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.6-dev.b08ca1c",
3
+ "version": "0.5.6-dev.b8170f8",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -64,7 +64,7 @@
64
64
  "vscode-languageserver": "^9.0.1",
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
- "oxlint-plugin-react-doctor": "0.5.6-dev.b08ca1c"
67
+ "oxlint-plugin-react-doctor": "0.5.6-dev.b8170f8"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.27.0",
@@ -72,9 +72,9 @@
72
72
  "@xterm/headless": "^6.0.0",
73
73
  "commander": "^14.0.3",
74
74
  "ora": "^9.4.0",
75
- "@react-doctor/language-server": "0.5.6",
76
75
  "@react-doctor/api": "0.5.6",
77
- "@react-doctor/core": "0.5.6"
76
+ "@react-doctor/core": "0.5.6",
77
+ "@react-doctor/language-server": "0.5.6"
78
78
  },
79
79
  "engines": {
80
80
  "node": "^20.19.0 || >=22.13.0"