react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.93b796d

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]="a06f0514-b1fa-5452-9c19-0140438862f8")}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";
@@ -36793,6 +36793,11 @@ const ES_TARGET_YEAR_BY_NAME = {
36793
36793
  esnext: 9999
36794
36794
  };
36795
36795
  /**
36796
+ * tsconfig filenames probed when resolving a project's TypeScript
36797
+ * compiler options — the root config first, then a monorepo base config.
36798
+ */
36799
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
36800
+ /**
36796
36801
  * Project-config files that `StagedFiles.materialize` copies into
36797
36802
  * the temp directory alongside staged sources so oxlint resolves
36798
36803
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -39941,8 +39946,8 @@ const collectIgnorePatterns = (rootDirectory) => {
39941
39946
  cachedPatternsByRoot.set(rootDirectory, patterns);
39942
39947
  return patterns;
39943
39948
  };
39949
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39944
39950
  const KNIP_JSON_FILENAME = "knip.json";
39945
- const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39946
39951
  const readJsonFileSafe = (filePath) => {
39947
39952
  let rawContents;
39948
39953
  try {
@@ -39958,10 +39963,10 @@ const readJsonFileSafe = (filePath) => {
39958
39963
  };
39959
39964
  const readKnipConfig = (rootDirectory) => {
39960
39965
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
39961
- if (isRecord$1$1(knipJson)) return knipJson;
39966
+ if (isRecord$2(knipJson)) return knipJson;
39962
39967
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
39963
- const packageKnipConfig = isRecord$1$1(packageJson) ? packageJson.knip : null;
39964
- return isRecord$1$1(packageKnipConfig) ? packageKnipConfig : null;
39968
+ const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
39969
+ return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
39965
39970
  };
39966
39971
  const normalizePatternList = (value) => {
39967
39972
  if (typeof value === "string" && value.length > 0) return [value];
@@ -39973,10 +39978,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
39973
39978
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
39974
39979
  };
39975
39980
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
39976
- if (!isRecord$1$1(workspaces)) return [];
39981
+ if (!isRecord$2(workspaces)) return [];
39977
39982
  const patterns = [];
39978
39983
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
39979
- if (!isRecord$1$1(workspaceConfig)) continue;
39984
+ if (!isRecord$2(workspaceConfig)) continue;
39980
39985
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
39981
39986
  }
39982
39987
  return patterns;
@@ -39986,12 +39991,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
39986
39991
  if (!config) return [];
39987
39992
  return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
39988
39993
  };
39989
- const collectDeadCodeIgnorePatterns = (rootDirectory, userConfig) => {
39994
+ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
39990
39995
  const seen = /* @__PURE__ */ new Set();
39991
39996
  const sources = [
39992
39997
  readIgnoreFile(path.join(rootDirectory, ".gitignore")),
39993
39998
  collectIgnorePatterns(rootDirectory),
39994
- userConfig?.ignore?.files ?? [],
39995
39999
  collectKnipPatterns(rootDirectory, "ignore")
39996
40000
  ];
39997
40001
  for (const source of sources) for (const pattern of source) seen.add(pattern);
@@ -40022,8 +40026,6 @@ const toCanonicalPath = (filePath) => {
40022
40026
  };
40023
40027
  const DEAD_CODE_PLUGIN = "deslop";
40024
40028
  const DEAD_CODE_CATEGORY = "Maintainability";
40025
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
40026
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40027
40029
  const DEAD_CODE_WORKER_SCRIPT = `
40028
40030
  const inputChunks = [];
40029
40031
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -40081,7 +40083,7 @@ process.stdin.on("end", () => {
40081
40083
  });
40082
40084
  `;
40083
40085
  const resolveTsConfigPath = (rootDirectory) => {
40084
- for (const filename of TSCONFIG_FILENAMES$1) {
40086
+ for (const filename of TSCONFIG_FILENAMES) {
40085
40087
  const candidate = Path.join(rootDirectory, filename);
40086
40088
  if (NFS.existsSync(candidate)) return candidate;
40087
40089
  }
@@ -40269,11 +40271,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
40269
40271
  });
40270
40272
  });
40271
40273
  const checkDeadCode = async (options) => {
40272
- const { userConfig } = options;
40273
40274
  const rootDirectory = toCanonicalPath(options.rootDirectory);
40274
40275
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40275
40276
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40276
- const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
40277
+ const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
40277
40278
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40278
40279
  rootDirectory,
40279
40280
  entryPatterns,
@@ -41322,8 +41323,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41322
41323
  }
41323
41324
  return enabled;
41324
41325
  };
41325
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41326
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41326
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
41327
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41327
41328
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41328
41329
  const jsPlugins = [];
41329
41330
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -41383,7 +41384,6 @@ const resolveOxlintBinary = () => {
41383
41384
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
41384
41385
  };
41385
41386
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
41386
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
41387
41387
  const resolveTsConfigRelativePath = (rootDirectory) => {
41388
41388
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
41389
41389
  return null;
@@ -41755,7 +41755,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
41755
41755
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
41756
41756
  let currentNode = identifier.parent;
41757
41757
  while (currentNode) {
41758
- if (isScopeNode(currentNode)) {
41758
+ if (isScopeBoundary(currentNode)) {
41759
41759
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
41760
41760
  }
41761
41761
  if (currentNode === sourceFile) return false;
@@ -41846,11 +41846,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
41846
41846
  });
41847
41847
  return resolution;
41848
41848
  };
41849
- const isScopeNode = isScopeBoundary;
41850
41849
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
41851
41850
  let currentNode = identifier.parent;
41852
41851
  while (currentNode) {
41853
- if (isScopeNode(currentNode)) {
41852
+ if (isScopeBoundary(currentNode)) {
41854
41853
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
41855
41854
  if (resolution) return resolution;
41856
41855
  }
@@ -42020,9 +42019,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42020
42019
  try {
42021
42020
  parsed = JSON.parse(sanitizedStdout);
42022
42021
  } catch {
42023
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42022
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42024
42023
  }
42025
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42024
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42026
42025
  const minifiedFileCache = /* @__PURE__ */ new Map();
42027
42026
  const isMinifiedDiagnosticFile = (filename) => {
42028
42027
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42313,6 +42312,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42313
42312
  NFS.closeSync(fileHandle);
42314
42313
  }
42315
42314
  };
42315
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
42316
+ /**
42317
+ * Detects an oxlint config-load crash caused by the optional
42318
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
42319
+ * builds the partial-failure note for it; returns `null` when the failure
42320
+ * was anything else.
42321
+ *
42322
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
42323
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
42324
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
42325
+ * config load on it, leaving the plugin in would drop every curated
42326
+ * react-doctor diagnostic too — so the caller retries with the plugin
42327
+ * stripped (issue #833). Both markers sit at the start of oxlint's
42328
+ * message, so they survive the `preview` slice even for deep pnpm paths.
42329
+ */
42330
+ const reactHooksJsPluginDropNote = (error) => {
42331
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
42332
+ const { preview } = error.reason;
42333
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
42334
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
42335
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
42336
+ };
42316
42337
  /**
42317
42338
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42318
42339
  *
@@ -42340,15 +42361,16 @@ const runOxlint = async (options) => {
42340
42361
  const pluginPath = resolvePluginPath();
42341
42362
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42342
42363
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42343
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42364
+ const buildConfig = (overrides) => createOxlintConfig({
42344
42365
  pluginPath,
42345
42366
  project,
42346
42367
  customRulesOnly,
42347
- extendsPaths: extendsForThisAttempt,
42368
+ extendsPaths: overrides.extendsPaths,
42348
42369
  ignoredTags,
42349
42370
  serverAuthFunctionNames,
42350
42371
  severityControls,
42351
- userPlugins
42372
+ userPlugins,
42373
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42352
42374
  });
42353
42375
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42354
42376
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42384,12 +42406,22 @@ const runOxlint = async (options) => {
42384
42406
  outputMaxBytes,
42385
42407
  concurrency: options.concurrency
42386
42408
  });
42387
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42409
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42388
42410
  try {
42389
42411
  return await runBatches();
42390
42412
  } catch (error) {
42413
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
42414
+ if (reactHooksJsDropNote !== null) {
42415
+ writeOxlintConfig(configPath, buildConfig({
42416
+ extendsPaths,
42417
+ disableReactHooksJsPlugin: true
42418
+ }));
42419
+ const diagnostics = await runBatches();
42420
+ onPartialFailure?.(reactHooksJsDropNote);
42421
+ return diagnostics;
42422
+ }
42391
42423
  if (extendsPaths.length === 0) throw error;
42392
- writeOxlintConfig(configPath, buildConfig([]));
42424
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42393
42425
  return await runBatches();
42394
42426
  }
42395
42427
  } finally {
@@ -43841,7 +43873,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43841
43873
  "false"
43842
43874
  ]);
43843
43875
  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);
43876
+ const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
43845
43877
  const detectCiProvider = () => {
43846
43878
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43847
43879
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43866,6 +43898,42 @@ const detectCodingAgent = () => {
43866
43898
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43867
43899
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43868
43900
  //#endregion
43901
+ //#region src/cli/utils/detect-terminal-kind.ts
43902
+ const TERMINAL_BY_TERM_PROGRAM = [
43903
+ ["vscode", "vscode"],
43904
+ ["iTerm.app", "iterm"],
43905
+ ["Apple_Terminal", "apple-terminal"],
43906
+ ["WezTerm", "wezterm"],
43907
+ ["ghostty", "ghostty"],
43908
+ ["Hyper", "hyper"],
43909
+ ["Tabby", "tabby"],
43910
+ ["rio", "rio"]
43911
+ ];
43912
+ /**
43913
+ * Best-effort label for the terminal emulator / editor hosting the CLI,
43914
+ * derived from terminal-identity env vars. Recorded as the `terminalKind` run
43915
+ * tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
43916
+ * …) — the split Sentry can't otherwise see. Low-cardinality and free of any
43917
+ * username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
43918
+ * win over the outer emulator because that's the surface a user is reading in;
43919
+ * "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
43920
+ */
43921
+ const detectTerminalKind = (env = process.env) => {
43922
+ if (env.NVIM) return "neovim";
43923
+ if (env.VIM_TERMINAL) return "vim";
43924
+ const termProgram = env.TERM_PROGRAM;
43925
+ if (termProgram) {
43926
+ for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
43927
+ }
43928
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
43929
+ if (env.WT_SESSION) return "windows-terminal";
43930
+ if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
43931
+ if (env.VTE_VERSION) return "vte";
43932
+ if (env.TMUX) return "tmux";
43933
+ if (isCiEnvironment(env)) return "ci";
43934
+ return "unknown";
43935
+ };
43936
+ //#endregion
43869
43937
  //#region src/cli/utils/is-git-hook-environment.ts
43870
43938
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43871
43939
  //#endregion
@@ -43888,6 +43956,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43888
43956
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43889
43957
  //#endregion
43890
43958
  //#region src/cli/utils/constants.ts
43959
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43891
43960
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43892
43961
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43893
43962
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43972,7 +44041,7 @@ const makeNoopConsole = () => ({
43972
44041
  });
43973
44042
  //#endregion
43974
44043
  //#region src/cli/utils/version.ts
43975
- const VERSION = "0.5.6-dev.8908f98";
44044
+ const VERSION = "0.5.6-dev.93b796d";
43976
44045
  //#endregion
43977
44046
  //#region src/cli/utils/json-mode.ts
43978
44047
  let context = null;
@@ -44122,6 +44191,7 @@ const buildRunContext = () => {
44122
44191
  viaAction: isOfficialGithubAction(),
44123
44192
  codingAgent: detectCodingAgent(),
44124
44193
  interactive: !isNonInteractiveEnvironment(),
44194
+ terminalKind: detectTerminalKind(),
44125
44195
  jsonMode: isJsonModeActive(),
44126
44196
  invokedVia: detectInvokedVia()
44127
44197
  };
@@ -44192,6 +44262,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44192
44262
  viaAction: runContext.viaAction,
44193
44263
  codingAgent: runContext.codingAgent,
44194
44264
  interactive: runContext.interactive,
44265
+ terminalKind: runContext.terminalKind,
44195
44266
  jsonMode: runContext.jsonMode,
44196
44267
  invokedVia: runContext.invokedVia,
44197
44268
  nodeMajor: runContext.nodeMajor
@@ -44330,13 +44401,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44330
44401
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44331
44402
  * standard `SENTRY_RELEASE` override.
44332
44403
  */
44333
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.8908f98`;
44404
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.93b796d`;
44334
44405
  /**
44335
44406
  * Deployment environment shown in Sentry's environment filter. Defaults to
44336
44407
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44337
44408
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44338
44409
  */
44339
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.8908f98") ? "development" : "production");
44410
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.93b796d") ? "development" : "production");
44340
44411
  /**
44341
44412
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44342
44413
  * (set to `0` to disable tracing) and falls back to
@@ -48118,7 +48189,7 @@ const AGENT_GUIDANCE_LINES = [
48118
48189
  "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
48119
48190
  "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
48120
48191
  "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.",
48192
+ "Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
48122
48193
  "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
48123
48194
  "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
48124
48195
  "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
@@ -48193,6 +48264,15 @@ const boxText = (content, innerWidth) => {
48193
48264
  ].join("\n");
48194
48265
  };
48195
48266
  //#endregion
48267
+ //#region src/cli/utils/resolve-absolute-path.ts
48268
+ /**
48269
+ * Resolves a diagnostic's `filePath` (relative to its project root, or
48270
+ * already absolute) to an absolute path. Shared by the code-frame reader and
48271
+ * the terminal hyperlink builder so both turn a relative path into the same
48272
+ * on-disk location.
48273
+ */
48274
+ const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
48275
+ //#endregion
48196
48276
  //#region src/cli/utils/build-code-frame.ts
48197
48277
  /**
48198
48278
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48203,7 +48283,7 @@ const boxText = (content, innerWidth) => {
48203
48283
  */
48204
48284
  const buildCodeFrame = (input) => {
48205
48285
  if (input.line <= 0) return null;
48206
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48286
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48207
48287
  let source;
48208
48288
  try {
48209
48289
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48243,6 +48323,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48243
48323
  const DIVIDER_INDENT = " ";
48244
48324
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48245
48325
  //#endregion
48326
+ //#region src/cli/utils/format-hyperlink.ts
48327
+ const OSC = "\x1B]";
48328
+ const ST = "\x1B\\";
48329
+ /**
48330
+ * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
48331
+ * are exactly `text`; the link is carried in escape sequences a capable
48332
+ * terminal turns into a click target.
48333
+ */
48334
+ const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
48335
+ //#endregion
48246
48336
  //#region src/cli/utils/indent-multiline-text.ts
48247
48337
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48248
48338
  //#endregion
@@ -48396,17 +48486,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48396
48486
  }
48397
48487
  return clusters;
48398
48488
  };
48399
- const formatClusterLocation = (cluster) => {
48489
+ const formatClusterLocationText = (cluster) => {
48490
+ const { filePath } = cluster.diagnostics[0];
48491
+ if (cluster.startLine <= 0) return filePath;
48492
+ if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
48493
+ return `${filePath}:${cluster.startLine}`;
48494
+ };
48495
+ const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
48400
48496
  const lead = cluster.diagnostics[0];
48401
48497
  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}`;
48498
+ const location = formatClusterLocationText(cluster);
48499
+ if (!hyperlinks) return `${location}${contextTag}`;
48500
+ return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
48405
48501
  };
48406
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48502
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48407
48503
  const lead = cluster.diagnostics[0];
48408
48504
  const isMultiSite = cluster.diagnostics.length > 1;
48409
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48505
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48410
48506
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48411
48507
  filePath: lead.filePath,
48412
48508
  line: cluster.startLine,
@@ -48425,7 +48521,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48425
48521
  }
48426
48522
  return lines;
48427
48523
  };
48428
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48524
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48429
48525
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48430
48526
  const { severity } = representative;
48431
48527
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48445,7 +48541,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48445
48541
  }
48446
48542
  const renderCodeFrame = severity === "error";
48447
48543
  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));
48544
+ if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
48449
48545
  return lines;
48450
48546
  };
48451
48547
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48458,7 +48554,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48458
48554
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48459
48555
  };
48460
48556
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48461
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48557
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48462
48558
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48463
48559
  if (topRuleGroups.length === 0) return {
48464
48560
  lines: [],
@@ -48468,7 +48564,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48468
48564
  const blockOffsets = [];
48469
48565
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48470
48566
  blockOffsets.push(lines.length);
48471
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48567
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48472
48568
  lines.push("");
48473
48569
  }
48474
48570
  return {
@@ -48506,18 +48602,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48506
48602
  * single Effect.forEach over Console.log so failures or fiber
48507
48603
  * interruption produce predictable partial output.
48508
48604
  */
48509
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
48605
+ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
48510
48606
  const sectionPause = onboarding.sectionPause ?? void_;
48511
48607
  const animateCountUp = onboarding.animateCountUp ?? false;
48512
48608
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48513
48609
  let detailLines;
48514
48610
  let topErrorBlockOffsets = [];
48515
48611
  if (!isVerbose) {
48516
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48612
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48517
48613
  detailLines = topErrors.lines;
48518
48614
  topErrorBlockOffsets = topErrors.blockOffsets;
48519
48615
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48520
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48616
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48521
48617
  });
48522
48618
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48523
48619
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48578,6 +48674,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48578
48674
  //#endregion
48579
48675
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48580
48676
  const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
48677
+ //#endregion
48678
+ //#region src/cli/utils/supports-hyperlinks.ts
48679
+ const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
48680
+ "iTerm.app",
48681
+ "WezTerm",
48682
+ "vscode",
48683
+ "Hyper",
48684
+ "ghostty",
48685
+ "Tabby",
48686
+ "rio"
48687
+ ]);
48688
+ const parseVteVersion = (raw) => {
48689
+ const parsed = Number.parseInt(raw ?? "", 10);
48690
+ return Number.isNaN(parsed) ? 0 : parsed;
48691
+ };
48692
+ /**
48693
+ * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
48694
+ * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
48695
+ * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
48696
+ * forces on), mirroring how the ecosystem's terminal libraries gate the same
48697
+ * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
48698
+ * raw escape rather than a link). Unknown terminals default to off.
48699
+ */
48700
+ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
48701
+ const forced = env.FORCE_HYPERLINK;
48702
+ if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
48703
+ if (stream.isTTY !== true) return false;
48704
+ if (env.TERM === "dumb") return false;
48705
+ if (isCiEnvironment(env)) return false;
48706
+ if (env.WT_SESSION) return true;
48707
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
48708
+ if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
48709
+ return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
48710
+ };
48711
+ //#endregion
48712
+ //#region src/cli/utils/should-render-hyperlinks.ts
48713
+ /**
48714
+ * Whether to emit OSC 8 clickable `file:line` locations for this run: a
48715
+ * hyperlink-capable terminal AND not a coding agent (whose output parsers
48716
+ * would choke on the escape sequences).
48717
+ */
48718
+ const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
48581
48719
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48582
48720
  const FALSY_FLAG_VALUES = new Set([
48583
48721
  "",
@@ -48597,10 +48735,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48597
48735
  };
48598
48736
  //#endregion
48599
48737
  //#region src/cli/utils/onboarding-state.ts
48600
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48601
48738
  const ONBOARDED_AT_KEY = "onboardedAt";
48602
48739
  const getOnboardingStore = (options = {}) => new Conf({
48603
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48740
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48604
48741
  cwd: options.cwd
48605
48742
  });
48606
48743
  const hasCompletedOnboarding = (options = {}) => {
@@ -49056,6 +49193,78 @@ const resolveCliCategories = (categoryFlag) => {
49056
49193
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
49057
49194
  };
49058
49195
  //#endregion
49196
+ //#region src/cli/utils/git-hook-shared.ts
49197
+ const HOOK_FILE_NAME = "pre-commit";
49198
+ const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49199
+ const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49200
+ const HUSKY_HOOKS_PATH = ".husky";
49201
+ const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49202
+ const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49203
+ const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49204
+ const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49205
+ const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49206
+ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49207
+ "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49208
+ `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49209
+ "rm -f \"$react_doctor_output\";",
49210
+ "else",
49211
+ "rm -f \"$react_doctor_output\";",
49212
+ `printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
49213
+ "fi"
49214
+ ].join(" ");
49215
+ const PACKAGE_JSON_FILE_NAME = "package.json";
49216
+ const runGit = (projectRoot, args) => {
49217
+ try {
49218
+ return execFileSync("git", [...args], {
49219
+ cwd: projectRoot,
49220
+ encoding: "utf8",
49221
+ stdio: [
49222
+ "ignore",
49223
+ "pipe",
49224
+ "ignore"
49225
+ ]
49226
+ }).trim();
49227
+ } catch {
49228
+ return null;
49229
+ }
49230
+ };
49231
+ const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
49232
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49233
+ const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
49234
+ const readPackageJson = (projectRoot) => {
49235
+ try {
49236
+ return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
49237
+ } catch {
49238
+ return null;
49239
+ }
49240
+ };
49241
+ const writeJsonFile$1 = (filePath, value) => {
49242
+ NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
49243
+ };
49244
+ const packageHasDependency = (projectRoot, dependencyName) => {
49245
+ const packageJson = readPackageJson(projectRoot);
49246
+ if (!isRecord$1(packageJson)) return false;
49247
+ return [
49248
+ "dependencies",
49249
+ "devDependencies",
49250
+ "optionalDependencies"
49251
+ ].some((fieldName) => {
49252
+ const dependencies = packageJson[fieldName];
49253
+ return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
49254
+ });
49255
+ };
49256
+ const packageHasRecordKey = (projectRoot, key) => {
49257
+ const packageJson = readPackageJson(projectRoot);
49258
+ return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
49259
+ };
49260
+ const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
49261
+ const packageJson = readPackageJson(projectRoot);
49262
+ if (!isRecord$1(packageJson)) return false;
49263
+ const value = packageJson[key];
49264
+ return isRecord$1(value) && isRecord$1(value[nestedKey]);
49265
+ };
49266
+ const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
49267
+ //#endregion
49059
49268
  //#region src/cli/utils/scan-result-cache.ts
49060
49269
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
49061
49270
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -49066,7 +49275,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
49066
49275
  "eslint-plugin-react-hooks/package.json"
49067
49276
  ];
49068
49277
  const bundledRequire = createRequire(import.meta.url);
49069
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49278
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49070
49279
  const normalizeForStableJson = (value) => {
49071
49280
  if (value === null) return null;
49072
49281
  if (value === void 0) return void 0;
@@ -49095,24 +49304,9 @@ const stringifyStableJson = (value) => {
49095
49304
  }
49096
49305
  };
49097
49306
  const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
49098
- const runGit$1 = (directory, args) => {
49099
- try {
49100
- return execFileSync("git", [...args], {
49101
- cwd: directory,
49102
- encoding: "utf8",
49103
- stdio: [
49104
- "ignore",
49105
- "pipe",
49106
- "ignore"
49107
- ]
49108
- }).trim();
49109
- } catch {
49110
- return null;
49111
- }
49112
- };
49113
- const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
49307
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
49114
49308
  const isWorktreeClean = (projectDirectory) => {
49115
- const status = runGit$1(projectDirectory, [
49309
+ const status = runGit(projectDirectory, [
49116
49310
  "status",
49117
49311
  "--porcelain=v1",
49118
49312
  "--untracked-files=normal"
@@ -49120,7 +49314,7 @@ const isWorktreeClean = (projectDirectory) => {
49120
49314
  return status !== null && status.length === 0;
49121
49315
  };
49122
49316
  const hasHiddenTrackedFileState = (projectDirectory) => {
49123
- const output = runGit$1(projectDirectory, ["ls-files", "-v"]);
49317
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
49124
49318
  if (output === null) return true;
49125
49319
  return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
49126
49320
  };
@@ -49133,7 +49327,7 @@ const resolveCacheFilePath = (projectDirectory) => {
49133
49327
  const readPersistedCache = (cacheFilePath) => {
49134
49328
  try {
49135
49329
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
49136
- if (!isRecord$1(parsed) || parsed.version !== 1) return {
49330
+ if (!isRecord(parsed) || parsed.version !== 1) return {
49137
49331
  version: 1,
49138
49332
  entries: []
49139
49333
  };
@@ -49143,8 +49337,8 @@ const readPersistedCache = (cacheFilePath) => {
49143
49337
  };
49144
49338
  const entries = [];
49145
49339
  for (const entry of parsed.entries) {
49146
- if (!isRecord$1(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49147
- if (!isRecord$1(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49340
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49341
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49148
49342
  entries.push(entry);
49149
49343
  }
49150
49344
  return {
@@ -49676,6 +49870,7 @@ const finalizeAndRender = (input) => gen(function* () {
49676
49870
  }
49677
49871
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49678
49872
  const pause = onboardingSectionPause(animateRender);
49873
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49679
49874
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49680
49875
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49681
49876
  if (printedDiagnostics.length === 0) {
@@ -49701,7 +49896,7 @@ const finalizeAndRender = (input) => gen(function* () {
49701
49896
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49702
49897
  sectionPause: pause,
49703
49898
  animateCountUp: animateRender
49704
- });
49899
+ }, useHyperlinks);
49705
49900
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49706
49901
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49707
49902
  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 +50089,7 @@ const buildHandoffPayload = (input) => {
49894
50089
  try {
49895
50090
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49896
50091
  } 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
- ];
50092
+ 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
50093
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49908
50094
  const representative = ruleDiagnostics[0];
49909
50095
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -49964,78 +50150,6 @@ const detectAvailableAgents = async () => {
49964
50150
  return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
49965
50151
  };
49966
50152
  //#endregion
49967
- //#region src/cli/utils/git-hook-shared.ts
49968
- const HOOK_FILE_NAME = "pre-commit";
49969
- const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49970
- const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49971
- const HUSKY_HOOKS_PATH = ".husky";
49972
- const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49973
- const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49974
- const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49975
- const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49976
- const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49977
- const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49978
- "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49979
- `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49980
- "rm -f \"$react_doctor_output\";",
49981
- "else",
49982
- "rm -f \"$react_doctor_output\";",
49983
- `printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
49984
- "fi"
49985
- ].join(" ");
49986
- const PACKAGE_JSON_FILE_NAME = "package.json";
49987
- const runGit = (projectRoot, args) => {
49988
- try {
49989
- return execFileSync("git", [...args], {
49990
- cwd: projectRoot,
49991
- encoding: "utf8",
49992
- stdio: [
49993
- "ignore",
49994
- "pipe",
49995
- "ignore"
49996
- ]
49997
- }).trim();
49998
- } catch {
49999
- return null;
50000
- }
50001
- };
50002
- const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
50003
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
50004
- const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
50005
- const readPackageJson = (projectRoot) => {
50006
- try {
50007
- return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
50008
- } catch {
50009
- return null;
50010
- }
50011
- };
50012
- const writeJsonFile$1 = (filePath, value) => {
50013
- NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
50014
- };
50015
- const packageHasDependency = (projectRoot, dependencyName) => {
50016
- const packageJson = readPackageJson(projectRoot);
50017
- if (!isRecord(packageJson)) return false;
50018
- return [
50019
- "dependencies",
50020
- "devDependencies",
50021
- "optionalDependencies"
50022
- ].some((fieldName) => {
50023
- const dependencies = packageJson[fieldName];
50024
- return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
50025
- });
50026
- };
50027
- const packageHasRecordKey = (projectRoot, key) => {
50028
- const packageJson = readPackageJson(projectRoot);
50029
- return isRecord(packageJson) && isRecord(packageJson[key]);
50030
- };
50031
- const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
50032
- const packageJson = readPackageJson(projectRoot);
50033
- if (!isRecord(packageJson)) return false;
50034
- const value = packageJson[key];
50035
- return isRecord(value) && isRecord(value[nestedKey]);
50036
- };
50037
- const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
50038
- //#endregion
50039
50153
  //#region src/cli/utils/install-doctor-script.ts
50040
50154
  const DOCTOR_SCRIPT_NAME = "doctor";
50041
50155
  const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
@@ -50061,31 +50175,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
50061
50175
  };
50062
50176
  const hasDoctorScript = (projectRoot) => {
50063
50177
  const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
50064
- if (!isRecord(packageJson)) return false;
50178
+ if (!isRecord$1(packageJson)) return false;
50065
50179
  const scripts = packageJson.scripts;
50066
- if (!isRecord(scripts)) return false;
50180
+ if (!isRecord$1(scripts)) return false;
50067
50181
  return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
50068
50182
  };
50069
50183
  const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
50070
50184
  const dependencies = packageJson[fieldName];
50071
- return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50185
+ return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50072
50186
  });
50073
50187
  const installDoctorScript = (options) => {
50074
50188
  const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
50075
50189
  const packageJsonPath = getPackageJsonPath(packageDirectory);
50076
50190
  const packageJson = readPackageJson(packageDirectory);
50077
- if (!isRecord(packageJson)) return {
50191
+ if (!isRecord$1(packageJson)) return {
50078
50192
  packageJsonPath,
50079
50193
  scriptStatus: "skipped",
50080
50194
  scriptReason: "missing-or-invalid-package-json"
50081
50195
  };
50082
50196
  const scripts = packageJson.scripts;
50083
50197
  const scriptTarget = (() => {
50084
- if (scripts !== void 0 && !isRecord(scripts)) return {
50198
+ if (scripts !== void 0 && !isRecord$1(scripts)) return {
50085
50199
  status: "skipped",
50086
50200
  reason: "invalid-scripts"
50087
50201
  };
50088
- const scriptRecord = isRecord(scripts) ? scripts : {};
50202
+ const scriptRecord = isRecord$1(scripts) ? scripts : {};
50089
50203
  if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
50090
50204
  scriptName: DOCTOR_SCRIPT_NAME,
50091
50205
  status: "existing"
@@ -50119,7 +50233,7 @@ const installDoctorScript = (options) => {
50119
50233
  if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
50120
50234
  ...packageJson,
50121
50235
  scripts: {
50122
- ...isRecord(scripts) ? scripts : {},
50236
+ ...isRecord$1(scripts) ? scripts : {},
50123
50237
  [scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
50124
50238
  }
50125
50239
  });
@@ -50273,38 +50387,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50273
50387
  //#region src/cli/utils/hash-project-root.ts
50274
50388
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50275
50389
  //#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()
50390
+ //#region src/cli/utils/project-decision-store.ts
50391
+ const createProjectDecisionStore = (storeKey) => {
50392
+ const getStore = (options = {}) => new Conf({
50393
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
50394
+ cwd: options.cwd
50395
+ });
50396
+ return {
50397
+ getConfigPath: (options = {}) => getStore(options).path,
50398
+ hasHandled: (projectRoot, options = {}) => {
50399
+ try {
50400
+ return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
50401
+ } catch {
50402
+ return true;
50300
50403
  }
50301
- });
50302
- return true;
50303
- } catch {
50304
- return false;
50305
- }
50404
+ },
50405
+ record: (projectRoot, outcome, options = {}) => {
50406
+ try {
50407
+ const store = getStore(options);
50408
+ store.set(storeKey, {
50409
+ ...store.get(storeKey, {}),
50410
+ [hashProjectRoot(projectRoot)]: {
50411
+ rootDirectory: Path.resolve(projectRoot),
50412
+ outcome,
50413
+ at: (/* @__PURE__ */ new Date()).toISOString()
50414
+ }
50415
+ });
50416
+ return true;
50417
+ } catch {
50418
+ return false;
50419
+ }
50420
+ }
50421
+ };
50306
50422
  };
50307
50423
  //#endregion
50424
+ //#region src/cli/utils/action-upgrade-prompt.ts
50425
+ const store$1 = createProjectDecisionStore("actionUpgrades");
50426
+ store$1.getConfigPath;
50427
+ const hasHandledActionUpgrade = store$1.hasHandled;
50428
+ const recordActionUpgradeDecision = store$1.record;
50429
+ //#endregion
50430
+ //#region src/cli/utils/ci-prompt-decision.ts
50431
+ const store = createProjectDecisionStore("ciPrompts");
50432
+ store.getConfigPath;
50433
+ const hasHandledCiPrompt = store.hasHandled;
50434
+ const recordCiPromptDecision = store.record;
50435
+ //#endregion
50308
50436
  //#region src/cli/utils/open-url.ts
50309
50437
  const resolveOpenCommand = (url) => {
50310
50438
  if (process$1.platform === "darwin") return {
@@ -50760,22 +50888,22 @@ const buildAgentHookScript = () => [
50760
50888
  "",
50761
50889
  "run_react_doctor() {",
50762
50890
  " if [ -x ./node_modules/.bin/react-doctor ]; then",
50763
- " ./node_modules/.bin/react-doctor --verbose --diff --blocking warning --no-score",
50891
+ " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
50764
50892
  " return",
50765
50893
  " fi",
50766
50894
  "",
50767
50895
  " if command -v react-doctor >/dev/null 2>&1; then",
50768
- " react-doctor --verbose --diff --blocking warning --no-score",
50896
+ " react-doctor --verbose --scope changed --blocking warning --no-score",
50769
50897
  " return",
50770
50898
  " fi",
50771
50899
  "",
50772
50900
  " if command -v pnpm >/dev/null 2>&1; then",
50773
- " pnpm dlx react-doctor@latest --verbose --diff --blocking warning --no-score",
50901
+ " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50774
50902
  " return",
50775
50903
  " fi",
50776
50904
  "",
50777
50905
  " if command -v npx >/dev/null 2>&1; then",
50778
- " npx --yes react-doctor@latest --verbose --diff --blocking warning --no-score",
50906
+ " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50779
50907
  " return",
50780
50908
  " fi",
50781
50909
  "",
@@ -50933,13 +51061,13 @@ const installPackageJsonHook = (options, strategy) => {
50933
51061
  const packageJsonPath = getPackageJsonPath(options.projectRoot);
50934
51062
  const didHookExist = NFS.existsSync(packageJsonPath);
50935
51063
  const packageJson = readPackageJson(options.projectRoot);
50936
- const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
51064
+ const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
50937
51065
  const parentKeys = strategy.path.slice(0, -1);
50938
51066
  const leafKey = strategy.path[strategy.path.length - 1];
50939
51067
  let parent = nextPackageJson;
50940
51068
  for (const key of parentKeys) {
50941
51069
  const existing = parent[key];
50942
- const cloned = isRecord(existing) ? { ...existing } : {};
51070
+ const cloned = isRecord$1(existing) ? { ...existing } : {};
50943
51071
  parent[key] = cloned;
50944
51072
  parent = cloned;
50945
51073
  }
@@ -51110,7 +51238,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
51110
51238
  const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
51111
51239
  const isSimpleGitHooksProject = (projectRoot) => {
51112
51240
  const packageJson = readPackageJson(projectRoot);
51113
- return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51241
+ return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51114
51242
  };
51115
51243
  const getLefthookConfigPath = (projectRoot) => {
51116
51244
  for (const fileName of LEFTHOOK_CONFIG_FILES) {
@@ -51276,7 +51404,7 @@ const detectPackageManager = (projectRoot) => {
51276
51404
  let currentDirectory = Path.resolve(projectRoot);
51277
51405
  while (true) {
51278
51406
  const packageJson = readPackageJson(currentDirectory);
51279
- if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
51407
+ if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
51280
51408
  const packageManagerName = packageJson.packageManager.split("@")[0];
51281
51409
  if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
51282
51410
  }
@@ -51352,12 +51480,12 @@ const isSupplyChainTrustError = (error) => {
51352
51480
  const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
51353
51481
  const installReactDoctorDependency = async (options) => {
51354
51482
  const packageJson = readPackageJson(options.projectRoot);
51355
- if (!isRecord(packageJson)) return {
51483
+ if (!isRecord$1(packageJson)) return {
51356
51484
  dependencyStatus: "skipped",
51357
51485
  dependencyReason: "missing-or-invalid-package-json"
51358
51486
  };
51359
51487
  if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
51360
- if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
51488
+ if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
51361
51489
  dependencyStatus: "skipped",
51362
51490
  dependencyReason: "invalid-dev-dependencies"
51363
51491
  };
@@ -51521,10 +51649,12 @@ const runInstallReactDoctor = async (options = {}) => {
51521
51649
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51522
51650
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51523
51651
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51524
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || !skipPrompts && await askAddToGitHubActions(prompt) === "yes");
51652
+ const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
51653
+ const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
51525
51654
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51526
51655
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51527
51656
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51657
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51528
51658
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51529
51659
  type: "multiselect",
51530
51660
  name: "agents",
@@ -51771,18 +51901,24 @@ const handoffToAgent = async (input) => {
51771
51901
  if (!input.interactive || input.diagnostics.length === 0) return;
51772
51902
  cliLogger.break();
51773
51903
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51774
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51904
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51905
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51775
51906
  const ciOutcome = await askAddToGitHubActions();
51776
51907
  recordCount(METRIC.agentHandoff, 1, {
51777
51908
  outcome: `ci-${ciOutcome}`,
51778
51909
  diagnosticsCount: input.diagnostics.length
51779
51910
  });
51780
51911
  if (ciOutcome === "cancel") return;
51912
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51781
51913
  if (ciOutcome === "yes") {
51782
51914
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51783
51915
  cliLogger.break();
51784
51916
  }
51785
- } else await maybeOfferActionUpgrade(projectRootForCi);
51917
+ } else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
51918
+ else recordCount(METRIC.agentHandoff, 1, {
51919
+ outcome: "ci-suppressed",
51920
+ diagnosticsCount: input.diagnostics.length
51921
+ });
51786
51922
  const { handoffTarget } = await prompts({
51787
51923
  type: "select",
51788
51924
  name: "handoffTarget",
@@ -52088,7 +52224,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52088
52224
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52089
52225
  if (displayDiagnostics.length > 0) {
52090
52226
  yield* log("");
52091
- yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
52227
+ yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
52092
52228
  }
52093
52229
  const lowestScoredScan = findLowestScoredScan(completedScans);
52094
52230
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52126,9 +52262,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52126
52262
  });
52127
52263
  //#endregion
52128
52264
  //#region src/cli/utils/prompt-install-setup.ts
52129
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52130
52265
  const getSetupPromptStore = (options = {}) => new Conf({
52131
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52266
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52132
52267
  cwd: options.cwd
52133
52268
  });
52134
52269
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52139,6 +52274,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52139
52274
  return false;
52140
52275
  }
52141
52276
  };
52277
+ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
52278
+ try {
52279
+ const store = getSetupPromptStore(storeOptions);
52280
+ const projects = store.get("projects", {});
52281
+ const projectKey = getSetupPromptProjectKey(projectRoot);
52282
+ store.set("projects", {
52283
+ ...projects,
52284
+ [projectKey]: {
52285
+ ...projects[projectKey] ?? {},
52286
+ rootDirectory: Path.resolve(projectRoot),
52287
+ setupPrompt: false
52288
+ }
52289
+ });
52290
+ return true;
52291
+ } catch {
52292
+ return false;
52293
+ }
52294
+ };
52142
52295
  const resolveInstallSetupProjectRoot = (options) => {
52143
52296
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52144
52297
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52545,6 +52698,14 @@ const runExplain = async (fileLineArgument, context) => {
52545
52698
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52546
52699
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52547
52700
  cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
52701
+ const codeFrame = buildCodeFrame({
52702
+ filePath: diagnostic.filePath,
52703
+ line: diagnostic.line,
52704
+ column: diagnostic.column,
52705
+ endLine: diagnostic.endLine,
52706
+ rootDirectory: targetDirectory
52707
+ });
52708
+ if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
52548
52709
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52549
52710
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52550
52711
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52941,6 +53102,7 @@ const inspectAction = async (directory, flags) => {
52941
53102
  })) {
52942
53103
  printAgentInstallHint();
52943
53104
  recordCount(METRIC.agentInstallHintShown, 1);
53105
+ disableSetupPrompt(setupProjectRoot);
52944
53106
  }
52945
53107
  }
52946
53108
  } catch (error) {
@@ -53872,7 +54034,7 @@ ${highlighter.dim("Examples:")}
53872
54034
  ${formatExampleLines([
53873
54035
  ["react-doctor", "scan the current project"],
53874
54036
  ["react-doctor ./apps/web", "scan a specific directory"],
53875
- ["react-doctor --diff main", "scan only files changed vs. main"],
54037
+ ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
53876
54038
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
53877
54039
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
53878
54040
  ["react-doctor --category Security", "show only one diagnostic category"],
@@ -53948,4 +54110,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53948
54110
  export {};
53949
54111
 
53950
54112
  //# sourceMappingURL=cli.js.map
53951
- //# debugId=fd7e91f8-8458-50a2-99d9-a1ea8f8bfaff
54113
+ //# debugId=a06f0514-b1fa-5452-9c19-0140438862f8