react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.937a7ca

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]="fd7e91f8-8458-50a2-99d9-a1ea8f8bfaff")}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";
@@ -39986,12 +39986,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
39986
39986
  if (!config) return [];
39987
39987
  return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
39988
39988
  };
39989
- const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
39989
+ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
39990
39990
  const seen = /* @__PURE__ */ new Set();
39991
39991
  const sources = [
39992
39992
  readIgnoreFile(path.join(rootDirectory, ".gitignore")),
39993
39993
  collectIgnorePatterns(rootDirectory),
39994
- userConfig?.ignore?.files ?? [],
39995
39994
  collectKnipPatterns(rootDirectory, "ignore")
39996
39995
  ];
39997
39996
  for (const source of sources) for (const pattern of source) seen.add(pattern);
@@ -40269,11 +40268,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
40269
40268
  });
40270
40269
  });
40271
40270
  const checkDeadCode = async (options) => {
40272
- const { userConfig } = options;
40273
40271
  const rootDirectory = toCanonicalPath(options.rootDirectory);
40274
40272
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40275
40273
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40276
- const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
40274
+ const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
40277
40275
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40278
40276
  rootDirectory,
40279
40277
  entryPatterns,
@@ -41322,8 +41320,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41322
41320
  }
41323
41321
  return enabled;
41324
41322
  };
41325
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41326
- 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);
41327
41325
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41328
41326
  const jsPlugins = [];
41329
41327
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -42020,9 +42018,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42020
42018
  try {
42021
42019
  parsed = JSON.parse(sanitizedStdout);
42022
42020
  } catch {
42023
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42021
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42024
42022
  }
42025
- 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) }) });
42026
42024
  const minifiedFileCache = /* @__PURE__ */ new Map();
42027
42025
  const isMinifiedDiagnosticFile = (filename) => {
42028
42026
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42313,6 +42311,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42313
42311
  NFS.closeSync(fileHandle);
42314
42312
  }
42315
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
+ };
42316
42336
  /**
42317
42337
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42318
42338
  *
@@ -42340,15 +42360,16 @@ const runOxlint = async (options) => {
42340
42360
  const pluginPath = resolvePluginPath();
42341
42361
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42342
42362
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42343
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42363
+ const buildConfig = (overrides) => createOxlintConfig({
42344
42364
  pluginPath,
42345
42365
  project,
42346
42366
  customRulesOnly,
42347
- extendsPaths: extendsForThisAttempt,
42367
+ extendsPaths: overrides.extendsPaths,
42348
42368
  ignoredTags,
42349
42369
  serverAuthFunctionNames,
42350
42370
  severityControls,
42351
- userPlugins
42371
+ userPlugins,
42372
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42352
42373
  });
42353
42374
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42354
42375
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42384,12 +42405,22 @@ const runOxlint = async (options) => {
42384
42405
  outputMaxBytes,
42385
42406
  concurrency: options.concurrency
42386
42407
  });
42387
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42408
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42388
42409
  try {
42389
42410
  return await runBatches();
42390
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
+ }
42391
42422
  if (extendsPaths.length === 0) throw error;
42392
- writeOxlintConfig(configPath, buildConfig([]));
42423
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42393
42424
  return await runBatches();
42394
42425
  }
42395
42426
  } finally {
@@ -43841,7 +43872,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43841
43872
  "false"
43842
43873
  ]);
43843
43874
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
43844
- 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);
43845
43876
  const detectCiProvider = () => {
43846
43877
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43847
43878
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43866,6 +43897,42 @@ const detectCodingAgent = () => {
43866
43897
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43867
43898
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43868
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
43869
43936
  //#region src/cli/utils/is-git-hook-environment.ts
43870
43937
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43871
43938
  //#endregion
@@ -43888,6 +43955,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43888
43955
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43889
43956
  //#endregion
43890
43957
  //#region src/cli/utils/constants.ts
43958
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43891
43959
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43892
43960
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43893
43961
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43972,7 +44040,7 @@ const makeNoopConsole = () => ({
43972
44040
  });
43973
44041
  //#endregion
43974
44042
  //#region src/cli/utils/version.ts
43975
- const VERSION = "0.5.6-dev.8908f98";
44043
+ const VERSION = "0.5.6-dev.937a7ca";
43976
44044
  //#endregion
43977
44045
  //#region src/cli/utils/json-mode.ts
43978
44046
  let context = null;
@@ -44122,6 +44190,7 @@ const buildRunContext = () => {
44122
44190
  viaAction: isOfficialGithubAction(),
44123
44191
  codingAgent: detectCodingAgent(),
44124
44192
  interactive: !isNonInteractiveEnvironment(),
44193
+ terminalKind: detectTerminalKind(),
44125
44194
  jsonMode: isJsonModeActive(),
44126
44195
  invokedVia: detectInvokedVia()
44127
44196
  };
@@ -44192,6 +44261,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44192
44261
  viaAction: runContext.viaAction,
44193
44262
  codingAgent: runContext.codingAgent,
44194
44263
  interactive: runContext.interactive,
44264
+ terminalKind: runContext.terminalKind,
44195
44265
  jsonMode: runContext.jsonMode,
44196
44266
  invokedVia: runContext.invokedVia,
44197
44267
  nodeMajor: runContext.nodeMajor
@@ -44330,13 +44400,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44330
44400
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44331
44401
  * standard `SENTRY_RELEASE` override.
44332
44402
  */
44333
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.8908f98`;
44403
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.937a7ca`;
44334
44404
  /**
44335
44405
  * Deployment environment shown in Sentry's environment filter. Defaults to
44336
44406
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44337
44407
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44338
44408
  */
44339
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.8908f98") ? "development" : "production");
44409
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.937a7ca") ? "development" : "production");
44340
44410
  /**
44341
44411
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44342
44412
  * (set to `0` to disable tracing) and falls back to
@@ -48118,7 +48188,7 @@ const AGENT_GUIDANCE_LINES = [
48118
48188
  "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
48119
48189
  "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
48120
48190
  "Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
48121
- "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.",
48122
48192
  "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
48123
48193
  "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
48124
48194
  "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
@@ -48193,6 +48263,15 @@ const boxText = (content, innerWidth) => {
48193
48263
  ].join("\n");
48194
48264
  };
48195
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
48196
48275
  //#region src/cli/utils/build-code-frame.ts
48197
48276
  /**
48198
48277
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48203,7 +48282,7 @@ const boxText = (content, innerWidth) => {
48203
48282
  */
48204
48283
  const buildCodeFrame = (input) => {
48205
48284
  if (input.line <= 0) return null;
48206
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48285
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48207
48286
  let source;
48208
48287
  try {
48209
48288
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48243,6 +48322,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48243
48322
  const DIVIDER_INDENT = " ";
48244
48323
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48245
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
48246
48335
  //#region src/cli/utils/indent-multiline-text.ts
48247
48336
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48248
48337
  //#endregion
@@ -48396,17 +48485,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48396
48485
  }
48397
48486
  return clusters;
48398
48487
  };
48399
- 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) => {
48400
48495
  const lead = cluster.diagnostics[0];
48401
48496
  const contextTag = formatFileContextTag(lead);
48402
- if (cluster.startLine <= 0) return `${lead.filePath}${contextTag}`;
48403
- if (cluster.endLine > cluster.startLine) return `${lead.filePath}:${cluster.startLine}-${cluster.endLine}${contextTag}`;
48404
- 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}`;
48405
48500
  };
48406
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48501
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48407
48502
  const lead = cluster.diagnostics[0];
48408
48503
  const isMultiSite = cluster.diagnostics.length > 1;
48409
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48504
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48410
48505
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48411
48506
  filePath: lead.filePath,
48412
48507
  line: cluster.startLine,
@@ -48425,7 +48520,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48425
48520
  }
48426
48521
  return lines;
48427
48522
  };
48428
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48523
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48429
48524
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48430
48525
  const { severity } = representative;
48431
48526
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48445,7 +48540,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48445
48540
  }
48446
48541
  const renderCodeFrame = severity === "error";
48447
48542
  const sites = renderEverySite ? ruleDiagnostics : [representative];
48448
- 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));
48449
48544
  return lines;
48450
48545
  };
48451
48546
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48458,7 +48553,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48458
48553
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48459
48554
  };
48460
48555
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48461
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48556
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48462
48557
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48463
48558
  if (topRuleGroups.length === 0) return {
48464
48559
  lines: [],
@@ -48468,7 +48563,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48468
48563
  const blockOffsets = [];
48469
48564
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48470
48565
  blockOffsets.push(lines.length);
48471
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48566
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48472
48567
  lines.push("");
48473
48568
  }
48474
48569
  return {
@@ -48506,18 +48601,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48506
48601
  * single Effect.forEach over Console.log so failures or fiber
48507
48602
  * interruption produce predictable partial output.
48508
48603
  */
48509
- 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* () {
48510
48605
  const sectionPause = onboarding.sectionPause ?? void_;
48511
48606
  const animateCountUp = onboarding.animateCountUp ?? false;
48512
48607
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48513
48608
  let detailLines;
48514
48609
  let topErrorBlockOffsets = [];
48515
48610
  if (!isVerbose) {
48516
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48611
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48517
48612
  detailLines = topErrors.lines;
48518
48613
  topErrorBlockOffsets = topErrors.blockOffsets;
48519
48614
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48520
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48615
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48521
48616
  });
48522
48617
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48523
48618
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48578,6 +48673,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48578
48673
  //#endregion
48579
48674
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48580
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();
48581
48718
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48582
48719
  const FALSY_FLAG_VALUES = new Set([
48583
48720
  "",
@@ -48597,10 +48734,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48597
48734
  };
48598
48735
  //#endregion
48599
48736
  //#region src/cli/utils/onboarding-state.ts
48600
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48601
48737
  const ONBOARDED_AT_KEY = "onboardedAt";
48602
48738
  const getOnboardingStore = (options = {}) => new Conf({
48603
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48739
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48604
48740
  cwd: options.cwd
48605
48741
  });
48606
48742
  const hasCompletedOnboarding = (options = {}) => {
@@ -49676,6 +49812,7 @@ const finalizeAndRender = (input) => gen(function* () {
49676
49812
  }
49677
49813
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49678
49814
  const pause = onboardingSectionPause(animateRender);
49815
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49679
49816
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49680
49817
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49681
49818
  if (printedDiagnostics.length === 0) {
@@ -49701,7 +49838,7 @@ const finalizeAndRender = (input) => gen(function* () {
49701
49838
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49702
49839
  sectionPause: pause,
49703
49840
  animateCountUp: animateRender
49704
- });
49841
+ }, useHyperlinks);
49705
49842
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49706
49843
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49707
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.`));
@@ -49894,16 +50031,7 @@ const buildHandoffPayload = (input) => {
49894
50031
  try {
49895
50032
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49896
50033
  } catch {}
49897
- const lines = [
49898
- "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:",
49899
- `- It's used by teams at ${CI_TRUST_COMPANIES}.`,
49900
- "- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
49901
- `- Setup guide: ${CI_URL}`,
49902
- "If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
49903
- "",
49904
- `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
- ""
49906
- ];
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.`, ""];
49907
50035
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49908
50036
  const representative = ruleDiagnostics[0];
49909
50037
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -50273,38 +50401,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50273
50401
  //#region src/cli/utils/hash-project-root.ts
50274
50402
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50275
50403
  //#endregion
50276
- //#region src/cli/utils/action-upgrade-prompt.ts
50277
- const GLOBAL_CONFIG_PROJECT_NAME$1 = "react-doctor";
50278
- const getActionUpgradeStore = (options = {}) => new Conf({
50279
- projectName: GLOBAL_CONFIG_PROJECT_NAME$1,
50280
- cwd: options.cwd
50281
- });
50282
- const hasHandledActionUpgrade = (projectRoot, storeOptions = {}) => {
50283
- try {
50284
- const upgrades = getActionUpgradeStore(storeOptions).get("actionUpgrades", {});
50285
- return Boolean(upgrades[hashProjectRoot(projectRoot)]);
50286
- } catch {
50287
- return true;
50288
- }
50289
- };
50290
- const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
50291
- try {
50292
- const store = getActionUpgradeStore(storeOptions);
50293
- const upgrades = store.get("actionUpgrades", {});
50294
- store.set("actionUpgrades", {
50295
- ...upgrades,
50296
- [hashProjectRoot(projectRoot)]: {
50297
- rootDirectory: Path.resolve(projectRoot),
50298
- outcome,
50299
- 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;
50300
50417
  }
50301
- });
50302
- return true;
50303
- } catch {
50304
- return false;
50305
- }
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
+ };
50306
50436
  };
50307
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
50308
50450
  //#region src/cli/utils/open-url.ts
50309
50451
  const resolveOpenCommand = (url) => {
50310
50452
  if (process$1.platform === "darwin") return {
@@ -50760,22 +50902,22 @@ const buildAgentHookScript = () => [
50760
50902
  "",
50761
50903
  "run_react_doctor() {",
50762
50904
  " if [ -x ./node_modules/.bin/react-doctor ]; then",
50763
- " ./node_modules/.bin/react-doctor --verbose --diff --blocking warning --no-score",
50905
+ " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
50764
50906
  " return",
50765
50907
  " fi",
50766
50908
  "",
50767
50909
  " if command -v react-doctor >/dev/null 2>&1; then",
50768
- " react-doctor --verbose --diff --blocking warning --no-score",
50910
+ " react-doctor --verbose --scope changed --blocking warning --no-score",
50769
50911
  " return",
50770
50912
  " fi",
50771
50913
  "",
50772
50914
  " if command -v pnpm >/dev/null 2>&1; then",
50773
- " pnpm dlx react-doctor@latest --verbose --diff --blocking warning --no-score",
50915
+ " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50774
50916
  " return",
50775
50917
  " fi",
50776
50918
  "",
50777
50919
  " if command -v npx >/dev/null 2>&1; then",
50778
- " npx --yes react-doctor@latest --verbose --diff --blocking warning --no-score",
50920
+ " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50779
50921
  " return",
50780
50922
  " fi",
50781
50923
  "",
@@ -51521,10 +51663,12 @@ const runInstallReactDoctor = async (options = {}) => {
51521
51663
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51522
51664
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51523
51665
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51524
- 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");
51525
51668
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51526
51669
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51527
51670
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51671
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51528
51672
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51529
51673
  type: "multiselect",
51530
51674
  name: "agents",
@@ -51771,18 +51915,24 @@ const handoffToAgent = async (input) => {
51771
51915
  if (!input.interactive || input.diagnostics.length === 0) return;
51772
51916
  cliLogger.break();
51773
51917
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51774
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51918
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51919
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51775
51920
  const ciOutcome = await askAddToGitHubActions();
51776
51921
  recordCount(METRIC.agentHandoff, 1, {
51777
51922
  outcome: `ci-${ciOutcome}`,
51778
51923
  diagnosticsCount: input.diagnostics.length
51779
51924
  });
51780
51925
  if (ciOutcome === "cancel") return;
51926
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51781
51927
  if (ciOutcome === "yes") {
51782
51928
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51783
51929
  cliLogger.break();
51784
51930
  }
51785
- } 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
+ });
51786
51936
  const { handoffTarget } = await prompts({
51787
51937
  type: "select",
51788
51938
  name: "handoffTarget",
@@ -52088,7 +52238,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52088
52238
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52089
52239
  if (displayDiagnostics.length > 0) {
52090
52240
  yield* log("");
52091
- 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));
52092
52242
  }
52093
52243
  const lowestScoredScan = findLowestScoredScan(completedScans);
52094
52244
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52126,9 +52276,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52126
52276
  });
52127
52277
  //#endregion
52128
52278
  //#region src/cli/utils/prompt-install-setup.ts
52129
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52130
52279
  const getSetupPromptStore = (options = {}) => new Conf({
52131
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52280
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52132
52281
  cwd: options.cwd
52133
52282
  });
52134
52283
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52139,6 +52288,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52139
52288
  return false;
52140
52289
  }
52141
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
+ };
52142
52309
  const resolveInstallSetupProjectRoot = (options) => {
52143
52310
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52144
52311
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52545,6 +52712,14 @@ const runExplain = async (fileLineArgument, context) => {
52545
52712
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52546
52713
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52547
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, " "));
52548
52723
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52549
52724
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52550
52725
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52941,6 +53116,7 @@ const inspectAction = async (directory, flags) => {
52941
53116
  })) {
52942
53117
  printAgentInstallHint();
52943
53118
  recordCount(METRIC.agentInstallHintShown, 1);
53119
+ disableSetupPrompt(setupProjectRoot);
52944
53120
  }
52945
53121
  }
52946
53122
  } catch (error) {
@@ -53872,7 +54048,7 @@ ${highlighter.dim("Examples:")}
53872
54048
  ${formatExampleLines([
53873
54049
  ["react-doctor", "scan the current project"],
53874
54050
  ["react-doctor ./apps/web", "scan a specific directory"],
53875
- ["react-doctor --diff main", "scan only files changed vs. main"],
54051
+ ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
53876
54052
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
53877
54053
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
53878
54054
  ["react-doctor --category Security", "show only one diagnostic category"],
@@ -53948,4 +54124,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53948
54124
  export {};
53949
54125
 
53950
54126
  //# sourceMappingURL=cli.js.map
53951
- //# debugId=fd7e91f8-8458-50a2-99d9-a1ea8f8bfaff
54127
+ //# debugId=ad091b20-c3e2-5c96-93ac-9a910745a035
package/dist/index.d.ts CHANGED
@@ -9280,9 +9280,10 @@ interface ReactDoctorConfig {
9280
9280
  * list, user-provided names are treated as distinctive and never
9281
9281
  * subject to receiver-object disambiguation.
9282
9282
  *
9283
- * Use this to teach react-doctor about custom auth guards in
9284
- * codebases that wrap their auth library — e.g. a project-local
9285
- * `requireWorkspaceMember` or `ensureSignedIn`.
9283
+ * Common guard conventions are already recognized automatically
9284
+ * (`requireAdmin`, `ensureSignedIn`, `getCurrentUser`, `hasRole`, …),
9285
+ * so this is only needed for domain-specific guards whose names carry
9286
+ * no auth noun — e.g. a project-local `requireWorkspaceMember`.
9286
9287
  */
9287
9288
  serverAuthFunctionNames?: string[];
9288
9289
  /**
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]="f55d58c3-c6f3-598b-bd99-d0abe85fd35e")}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";
@@ -36750,12 +36750,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
36750
36750
  if (!config) return [];
36751
36751
  return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
36752
36752
  };
36753
- const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
36753
+ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
36754
36754
  const seen = /* @__PURE__ */ new Set();
36755
36755
  const sources = [
36756
36756
  readIgnoreFile(path.join(rootDirectory, ".gitignore")),
36757
36757
  collectIgnorePatterns(rootDirectory),
36758
- userConfig?.ignore?.files ?? [],
36759
36758
  collectKnipPatterns(rootDirectory, "ignore")
36760
36759
  ];
36761
36760
  for (const source of sources) for (const pattern of source) seen.add(pattern);
@@ -37033,11 +37032,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
37033
37032
  });
37034
37033
  });
37035
37034
  const checkDeadCode = async (options) => {
37036
- const { userConfig } = options;
37037
37035
  const rootDirectory = toCanonicalPath(options.rootDirectory);
37038
37036
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37039
37037
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37040
- const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
37038
+ const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37041
37039
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37042
37040
  rootDirectory,
37043
37041
  entryPatterns,
@@ -38086,8 +38084,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38086
38084
  }
38087
38085
  return enabled;
38088
38086
  };
38089
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38090
- 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);
38091
38089
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38092
38090
  const jsPlugins = [];
38093
38091
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38784,9 +38782,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38784
38782
  try {
38785
38783
  parsed = JSON.parse(sanitizedStdout);
38786
38784
  } catch {
38787
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38785
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38788
38786
  }
38789
- 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) }) });
38790
38788
  const minifiedFileCache = /* @__PURE__ */ new Map();
38791
38789
  const isMinifiedDiagnosticFile = (filename) => {
38792
38790
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39077,6 +39075,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39077
39075
  NFS.closeSync(fileHandle);
39078
39076
  }
39079
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
+ };
39080
39100
  /**
39081
39101
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39082
39102
  *
@@ -39104,15 +39124,16 @@ const runOxlint = async (options) => {
39104
39124
  const pluginPath = resolvePluginPath();
39105
39125
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39106
39126
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39107
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39127
+ const buildConfig = (overrides) => createOxlintConfig({
39108
39128
  pluginPath,
39109
39129
  project,
39110
39130
  customRulesOnly,
39111
- extendsPaths: extendsForThisAttempt,
39131
+ extendsPaths: overrides.extendsPaths,
39112
39132
  ignoredTags,
39113
39133
  serverAuthFunctionNames,
39114
39134
  severityControls,
39115
- userPlugins
39135
+ userPlugins,
39136
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39116
39137
  });
39117
39138
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39118
39139
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39148,12 +39169,22 @@ const runOxlint = async (options) => {
39148
39169
  outputMaxBytes,
39149
39170
  concurrency: options.concurrency
39150
39171
  });
39151
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39172
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39152
39173
  try {
39153
39174
  return await runBatches();
39154
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
+ }
39155
39186
  if (extendsPaths.length === 0) throw error;
39156
- writeOxlintConfig(configPath, buildConfig([]));
39187
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39157
39188
  return await runBatches();
39158
39189
  }
39159
39190
  } finally {
@@ -40573,4 +40604,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40573
40604
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40574
40605
 
40575
40606
  //# sourceMappingURL=index.js.map
40576
- //# debugId=f55d58c3-c6f3-598b-bd99-d0abe85fd35e
40607
+ //# debugId=4e83db09-9255-525f-b6fd-405784826e7d
package/dist/lsp.js CHANGED
@@ -36736,12 +36736,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
36736
36736
  if (!config) return [];
36737
36737
  return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
36738
36738
  };
36739
- const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
36739
+ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
36740
36740
  const seen = /* @__PURE__ */ new Set();
36741
36741
  const sources = [
36742
36742
  readIgnoreFile(path.join(rootDirectory, ".gitignore")),
36743
36743
  collectIgnorePatterns(rootDirectory),
36744
- userConfig?.ignore?.files ?? [],
36745
36744
  collectKnipPatterns(rootDirectory, "ignore")
36746
36745
  ];
36747
36746
  for (const source of sources) for (const pattern of source) seen.add(pattern);
@@ -37019,11 +37018,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
37019
37018
  });
37020
37019
  });
37021
37020
  const checkDeadCode = async (options) => {
37022
- const { userConfig } = options;
37023
37021
  const rootDirectory = toCanonicalPath(options.rootDirectory);
37024
37022
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37025
37023
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37026
- const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
37024
+ const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37027
37025
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37028
37026
  rootDirectory,
37029
37027
  entryPatterns,
@@ -38072,8 +38070,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38072
38070
  }
38073
38071
  return enabled;
38074
38072
  };
38075
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38076
- 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);
38077
38075
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38078
38076
  const jsPlugins = [];
38079
38077
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38770,9 +38768,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38770
38768
  try {
38771
38769
  parsed = JSON.parse(sanitizedStdout);
38772
38770
  } catch {
38773
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
38771
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38774
38772
  }
38775
- 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) }) });
38776
38774
  const minifiedFileCache = /* @__PURE__ */ new Map();
38777
38775
  const isMinifiedDiagnosticFile = (filename) => {
38778
38776
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -39063,6 +39061,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39063
39061
  NFS.closeSync(fileHandle);
39064
39062
  }
39065
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
+ };
39066
39086
  /**
39067
39087
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39068
39088
  *
@@ -39090,15 +39110,16 @@ const runOxlint = async (options) => {
39090
39110
  const pluginPath = resolvePluginPath();
39091
39111
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39092
39112
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39093
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39113
+ const buildConfig = (overrides) => createOxlintConfig({
39094
39114
  pluginPath,
39095
39115
  project,
39096
39116
  customRulesOnly,
39097
- extendsPaths: extendsForThisAttempt,
39117
+ extendsPaths: overrides.extendsPaths,
39098
39118
  ignoredTags,
39099
39119
  serverAuthFunctionNames,
39100
39120
  severityControls,
39101
- userPlugins
39121
+ userPlugins,
39122
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39102
39123
  });
39103
39124
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39104
39125
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -39134,12 +39155,22 @@ const runOxlint = async (options) => {
39134
39155
  outputMaxBytes,
39135
39156
  concurrency: options.concurrency
39136
39157
  });
39137
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39158
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39138
39159
  try {
39139
39160
  return await runBatches();
39140
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
+ }
39141
39172
  if (extendsPaths.length === 0) throw error;
39142
- writeOxlintConfig(configPath, buildConfig([]));
39173
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39143
39174
  return await runBatches();
39144
39175
  }
39145
39176
  } finally {
@@ -42358,5 +42389,5 @@ const startLanguageServer = () => {
42358
42389
  };
42359
42390
  //#endregion
42360
42391
  export { startLanguageServer };
42361
- !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]="8882c2be-9063-5954-a6a3-fc287e8c715f")}catch(e){}}();
42362
- //# debugId=8882c2be-9063-5954-a6a3-fc287e8c715f
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.8908f98",
3
+ "version": "0.5.6-dev.937a7ca",
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.8908f98"
67
+ "oxlint-plugin-react-doctor": "0.5.6-dev.937a7ca"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.27.0",