react-doctor 0.5.6-dev.451beeb → 0.5.6-dev.5d1347e

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.
Files changed (4) hide show
  1. package/dist/cli.js +404 -250
  2. package/dist/index.js +94 -70
  3. package/dist/lsp.js +113 -92
  4. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bb93aaad-ea85-5f1d-b2db-584f48671f66")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="37f4074d-83ec-54d9-a9f3-5a8967531f6b")}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";
@@ -35893,6 +35893,7 @@ const isLargeMinifiedFile = (absolutePath) => {
35893
35893
  if (sizeBytes < 2e4) return false;
35894
35894
  return isMinifiedSource(absolutePath);
35895
35895
  };
35896
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
35896
35897
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
35897
35898
  "EACCES",
35898
35899
  "EPERM",
@@ -35902,11 +35903,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
35902
35903
  "ELOOP",
35903
35904
  "ENAMETOOLONG"
35904
35905
  ]);
35905
- const isIgnorableReaddirError = (error) => {
35906
- if (typeof error !== "object" || error === null) return false;
35907
- const errorCode = error.code;
35908
- return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
35909
- };
35906
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
35910
35907
  const readDirectoryEntries = (directoryPath) => {
35911
35908
  try {
35912
35909
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -35953,7 +35950,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
35953
35950
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
35954
35951
  } catch (error) {
35955
35952
  if (error instanceof SyntaxError) return {};
35956
- if (error instanceof Error && "code" in error) {
35953
+ if (isErrnoException(error)) {
35957
35954
  const { code } = error;
35958
35955
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
35959
35956
  }
@@ -36678,17 +36675,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
36678
36675
  return false;
36679
36676
  };
36680
36677
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
36681
- const getExpoDependencySpec = (packageJson) => {
36682
- const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
36678
+ const getDependencySpec = (packageJson, packageName) => {
36679
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36683
36680
  return typeof spec === "string" ? spec : null;
36684
36681
  };
36685
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
36682
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
36686
36683
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
36687
- const getShopifyFlashListDependencySpec = (packageJson) => {
36688
- const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
36689
- return typeof spec === "string" ? spec : null;
36690
- };
36691
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
36684
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
36692
36685
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
36693
36686
  if (version === null || !isCatalogReference(version)) return version;
36694
36687
  const catalogName = extractCatalogName(version);
@@ -36700,11 +36693,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
36700
36693
  if (!isFile(monorepoPackageJsonPath)) return version;
36701
36694
  return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
36702
36695
  };
36703
- const getNextjsDependencySpec = (packageJson) => {
36704
- const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
36705
- return typeof spec === "string" ? spec : null;
36706
- };
36707
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
36696
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
36708
36697
  const getPreactVersion = (packageJson) => {
36709
36698
  return {
36710
36699
  ...packageJson.peerDependencies,
@@ -36793,6 +36782,11 @@ const ES_TARGET_YEAR_BY_NAME = {
36793
36782
  esnext: 9999
36794
36783
  };
36795
36784
  /**
36785
+ * tsconfig filenames probed when resolving a project's TypeScript
36786
+ * compiler options — the root config first, then a monorepo base config.
36787
+ */
36788
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
36789
+ /**
36796
36790
  * Project-config files that `StagedFiles.materialize` copies into
36797
36791
  * the temp directory alongside staged sources so oxlint resolves
36798
36792
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -37314,6 +37308,7 @@ const isTailwindAtLeast = (detected, required) => {
37314
37308
  if (detected.major !== required.major) return detected.major > required.major;
37315
37309
  return detected.minor >= required.minor;
37316
37310
  };
37311
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37317
37312
  var InvalidGlobPatternError = class extends Error {
37318
37313
  pattern;
37319
37314
  reason;
@@ -37342,7 +37337,7 @@ const compileGlobPattern = (rawPattern) => {
37342
37337
  try {
37343
37338
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
37344
37339
  } catch (caughtError) {
37345
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
37340
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
37346
37341
  }
37347
37342
  };
37348
37343
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -38520,7 +38515,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
38520
38515
  const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
38521
38516
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
38522
38517
  const jiti = createJiti(import.meta.url);
38523
- const formatError = (error) => error instanceof Error ? error.message : String(error);
38524
38518
  const importDefaultExport = async (jitiInstance, filePath) => {
38525
38519
  const imported = await jitiInstance.import(filePath);
38526
38520
  return imported?.default ?? imported;
@@ -38552,7 +38546,7 @@ const loadModuleConfig = async (filePath) => {
38552
38546
  try {
38553
38547
  return await importDefaultExport(aliasJiti, filePath);
38554
38548
  } catch (retryError) {
38555
- throw new Error(`${formatError(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${formatError(retryError)})`, { cause: retryError });
38549
+ throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
38556
38550
  }
38557
38551
  }
38558
38552
  };
@@ -38601,7 +38595,7 @@ const loadLegacyConfig = (directory) => {
38601
38595
  }
38602
38596
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
38603
38597
  } catch (error) {
38604
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
38598
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
38605
38599
  }
38606
38600
  return {
38607
38601
  status: "invalid",
@@ -38628,7 +38622,7 @@ const loadConfigFromDirectory = async (directory) => {
38628
38622
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
38629
38623
  sawBrokenConfigFile = true;
38630
38624
  } catch (error) {
38631
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
38625
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
38632
38626
  sawBrokenConfigFile = true;
38633
38627
  }
38634
38628
  }
@@ -39903,7 +39897,7 @@ const readIgnoreFile = (filePath) => {
39903
39897
  try {
39904
39898
  content = NFS.readFileSync(filePath, "utf-8");
39905
39899
  } catch (error) {
39906
- const errnoCode = error?.code;
39900
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
39907
39901
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
39908
39902
  return [];
39909
39903
  }
@@ -39941,8 +39935,8 @@ const collectIgnorePatterns = (rootDirectory) => {
39941
39935
  cachedPatternsByRoot.set(rootDirectory, patterns);
39942
39936
  return patterns;
39943
39937
  };
39938
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39944
39939
  const KNIP_JSON_FILENAME = "knip.json";
39945
- const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39946
39940
  const readJsonFileSafe = (filePath) => {
39947
39941
  let rawContents;
39948
39942
  try {
@@ -39958,10 +39952,10 @@ const readJsonFileSafe = (filePath) => {
39958
39952
  };
39959
39953
  const readKnipConfig = (rootDirectory) => {
39960
39954
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
39961
- if (isRecord$1$1(knipJson)) return knipJson;
39955
+ if (isRecord$2(knipJson)) return knipJson;
39962
39956
  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;
39957
+ const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
39958
+ return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
39965
39959
  };
39966
39960
  const normalizePatternList = (value) => {
39967
39961
  if (typeof value === "string" && value.length > 0) return [value];
@@ -39973,10 +39967,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
39973
39967
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
39974
39968
  };
39975
39969
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
39976
- if (!isRecord$1$1(workspaces)) return [];
39970
+ if (!isRecord$2(workspaces)) return [];
39977
39971
  const patterns = [];
39978
39972
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
39979
- if (!isRecord$1$1(workspaceConfig)) continue;
39973
+ if (!isRecord$2(workspaceConfig)) continue;
39980
39974
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
39981
39975
  }
39982
39976
  return patterns;
@@ -40021,8 +40015,6 @@ const toCanonicalPath = (filePath) => {
40021
40015
  };
40022
40016
  const DEAD_CODE_PLUGIN = "deslop";
40023
40017
  const DEAD_CODE_CATEGORY = "Maintainability";
40024
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
40025
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40026
40018
  const DEAD_CODE_WORKER_SCRIPT = `
40027
40019
  const inputChunks = [];
40028
40020
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -40080,7 +40072,7 @@ process.stdin.on("end", () => {
40080
40072
  });
40081
40073
  `;
40082
40074
  const resolveTsConfigPath = (rootDirectory) => {
40083
- for (const filename of TSCONFIG_FILENAMES$1) {
40075
+ for (const filename of TSCONFIG_FILENAMES) {
40084
40076
  const candidate = Path.join(rootDirectory, filename);
40085
40077
  if (NFS.existsSync(candidate)) return candidate;
40086
40078
  }
@@ -40461,15 +40453,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
40461
40453
  })()) }));
40462
40454
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
40463
40455
  };
40464
- const createNodeReadFileLinesSync = (rootDirectory) => {
40465
- return (filePath) => {
40466
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
40467
- try {
40468
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
40469
- } catch {
40470
- return null;
40471
- }
40472
- };
40456
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
40457
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
40458
+ try {
40459
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
40460
+ } catch {
40461
+ return null;
40462
+ }
40473
40463
  };
40474
40464
  var Files = class Files extends Service()("react-doctor/Files") {
40475
40465
  static layerNode = succeed$3(Files, Files.of({
@@ -40680,7 +40670,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
40680
40670
  directory: input.directory,
40681
40671
  cause
40682
40672
  }) });
40683
- }));
40673
+ }), withSpan("git.exec", { attributes: {
40674
+ "git.command": input.command,
40675
+ "git.subcommand": input.args[0] ?? ""
40676
+ } }));
40684
40677
  const runGit = (directory, args) => runCommand({
40685
40678
  command: "git",
40686
40679
  args,
@@ -40708,7 +40701,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40708
40701
  ]);
40709
40702
  if (candidates.status !== 0) return null;
40710
40703
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
40711
- });
40704
+ }).pipe(withSpan("Git.defaultBranch"));
40712
40705
  const branchExists = (directory, branch) => runGit(directory, [
40713
40706
  "rev-parse",
40714
40707
  "--verify",
@@ -40755,7 +40748,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40755
40748
  const result = resultOption.value;
40756
40749
  if (result.status !== 0) return null;
40757
40750
  return parseGithubViewerPermission(result.stdout);
40758
- }).pipe(catch_$1(() => succeed$2(null)));
40751
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
40759
40752
  /**
40760
40753
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
40761
40754
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -40869,7 +40862,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40869
40862
  changedFiles: splitNullSeparated(diff.stdout),
40870
40863
  isCurrentChanges: false
40871
40864
  };
40872
- }),
40865
+ }).pipe(withSpan("Git.diffSelection")),
40873
40866
  stagedFilePaths: (directory) => runGit(directory, [
40874
40867
  "diff",
40875
40868
  "--cached",
@@ -40911,7 +40904,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40911
40904
  status: result.status,
40912
40905
  stdout: result.stdout
40913
40906
  };
40914
- }),
40907
+ }).pipe(withSpan("Git.grep")),
40915
40908
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
40916
40909
  if (files.length === 0) return [];
40917
40910
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -40927,7 +40920,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40927
40920
  ]);
40928
40921
  if (result.status !== 0) return null;
40929
40922
  return parseChangedLineRanges(result.stdout);
40930
- })
40923
+ }).pipe(withSpan("Git.changedLineRanges"))
40931
40924
  });
40932
40925
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
40933
40926
  /**
@@ -41142,7 +41135,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
41142
41135
  for (const [absolutePath, originalContent] of originalContents) try {
41143
41136
  NFS.writeFileSync(absolutePath, originalContent);
41144
41137
  } catch (error) {
41145
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
41138
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
41146
41139
  }
41147
41140
  };
41148
41141
  const onExit = () => restore();
@@ -41248,7 +41241,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
41248
41241
  try {
41249
41242
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
41250
41243
  } catch (error) {
41251
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
41244
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
41252
41245
  return null;
41253
41246
  }
41254
41247
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -41320,8 +41313,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41320
41313
  }
41321
41314
  return enabled;
41322
41315
  };
41323
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41324
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41316
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
41317
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41325
41318
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41326
41319
  const jsPlugins = [];
41327
41320
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -41381,7 +41374,6 @@ const resolveOxlintBinary = () => {
41381
41374
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
41382
41375
  };
41383
41376
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
41384
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
41385
41377
  const resolveTsConfigRelativePath = (rootDirectory) => {
41386
41378
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
41387
41379
  return null;
@@ -41753,7 +41745,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
41753
41745
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
41754
41746
  let currentNode = identifier.parent;
41755
41747
  while (currentNode) {
41756
- if (isScopeNode(currentNode)) {
41748
+ if (isScopeBoundary(currentNode)) {
41757
41749
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
41758
41750
  }
41759
41751
  if (currentNode === sourceFile) return false;
@@ -41844,11 +41836,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
41844
41836
  });
41845
41837
  return resolution;
41846
41838
  };
41847
- const isScopeNode = isScopeBoundary;
41848
41839
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
41849
41840
  let currentNode = identifier.parent;
41850
41841
  while (currentNode) {
41851
- if (isScopeNode(currentNode)) {
41842
+ if (isScopeBoundary(currentNode)) {
41852
41843
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
41853
41844
  if (resolution) return resolution;
41854
41845
  }
@@ -42018,9 +42009,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42018
42009
  try {
42019
42010
  parsed = JSON.parse(sanitizedStdout);
42020
42011
  } catch {
42021
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42012
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42022
42013
  }
42023
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42014
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42024
42015
  const minifiedFileCache = /* @__PURE__ */ new Map();
42025
42016
  const isMinifiedDiagnosticFile = (filename) => {
42026
42017
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42096,7 +42087,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
42096
42087
  child.kill("SIGKILL");
42097
42088
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
42098
42089
  kind: "timeout",
42099
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
42090
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
42100
42091
  }) }));
42101
42092
  }, spawnTimeoutMs);
42102
42093
  timeoutHandle.unref?.();
@@ -42311,6 +42302,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42311
42302
  NFS.closeSync(fileHandle);
42312
42303
  }
42313
42304
  };
42305
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
42306
+ /**
42307
+ * Detects an oxlint config-load crash caused by the optional
42308
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
42309
+ * builds the partial-failure note for it; returns `null` when the failure
42310
+ * was anything else.
42311
+ *
42312
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
42313
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
42314
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
42315
+ * config load on it, leaving the plugin in would drop every curated
42316
+ * react-doctor diagnostic too — so the caller retries with the plugin
42317
+ * stripped (issue #833). Both markers sit at the start of oxlint's
42318
+ * message, so they survive the `preview` slice even for deep pnpm paths.
42319
+ */
42320
+ const reactHooksJsPluginDropNote = (error) => {
42321
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
42322
+ const { preview } = error.reason;
42323
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
42324
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
42325
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
42326
+ };
42314
42327
  /**
42315
42328
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42316
42329
  *
@@ -42338,15 +42351,16 @@ const runOxlint = async (options) => {
42338
42351
  const pluginPath = resolvePluginPath();
42339
42352
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42340
42353
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42341
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42354
+ const buildConfig = (overrides) => createOxlintConfig({
42342
42355
  pluginPath,
42343
42356
  project,
42344
42357
  customRulesOnly,
42345
- extendsPaths: extendsForThisAttempt,
42358
+ extendsPaths: overrides.extendsPaths,
42346
42359
  ignoredTags,
42347
42360
  serverAuthFunctionNames,
42348
42361
  severityControls,
42349
- userPlugins
42362
+ userPlugins,
42363
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42350
42364
  });
42351
42365
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42352
42366
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42382,12 +42396,22 @@ const runOxlint = async (options) => {
42382
42396
  outputMaxBytes,
42383
42397
  concurrency: options.concurrency
42384
42398
  });
42385
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42399
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42386
42400
  try {
42387
42401
  return await runBatches();
42388
42402
  } catch (error) {
42403
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
42404
+ if (reactHooksJsDropNote !== null) {
42405
+ writeOxlintConfig(configPath, buildConfig({
42406
+ extendsPaths,
42407
+ disableReactHooksJsPlugin: true
42408
+ }));
42409
+ const diagnostics = await runBatches();
42410
+ onPartialFailure?.(reactHooksJsDropNote);
42411
+ return diagnostics;
42412
+ }
42389
42413
  if (extendsPaths.length === 0) throw error;
42390
- writeOxlintConfig(configPath, buildConfig([]));
42414
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42391
42415
  return await runBatches();
42392
42416
  }
42393
42417
  } finally {
@@ -43185,7 +43209,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43185
43209
  }))))))));
43186
43210
  const deadCodeFailureState = yield* get$2(deadCodeFailure);
43187
43211
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
43188
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
43212
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
43189
43213
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
43190
43214
  else if (input.suppressScanSummary) yield* scanProgress.stop();
43191
43215
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
@@ -43422,7 +43446,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
43422
43446
  static layerNode = effect(StagedFiles, gen(function* () {
43423
43447
  const git = yield* Git;
43424
43448
  return StagedFiles.of({
43425
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
43449
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
43426
43450
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
43427
43451
  directory,
43428
43452
  files: stagedFiles,
@@ -43432,7 +43456,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
43432
43456
  tempDirectory: tree.tempDirectory,
43433
43457
  stagedFiles: tree.materializedFiles,
43434
43458
  cleanup: tree.cleanup
43435
- })))
43459
+ })), withSpan("StagedFiles.materialize"))
43436
43460
  });
43437
43461
  }));
43438
43462
  /**
@@ -43839,7 +43863,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43839
43863
  "false"
43840
43864
  ]);
43841
43865
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
43842
- const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(process.env[environmentVariable])) || isCiFlagSet(process.env.CI);
43866
+ const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
43843
43867
  const detectCiProvider = () => {
43844
43868
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43845
43869
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43864,6 +43888,42 @@ const detectCodingAgent = () => {
43864
43888
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43865
43889
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43866
43890
  //#endregion
43891
+ //#region src/cli/utils/detect-terminal-kind.ts
43892
+ const TERMINAL_BY_TERM_PROGRAM = [
43893
+ ["vscode", "vscode"],
43894
+ ["iTerm.app", "iterm"],
43895
+ ["Apple_Terminal", "apple-terminal"],
43896
+ ["WezTerm", "wezterm"],
43897
+ ["ghostty", "ghostty"],
43898
+ ["Hyper", "hyper"],
43899
+ ["Tabby", "tabby"],
43900
+ ["rio", "rio"]
43901
+ ];
43902
+ /**
43903
+ * Best-effort label for the terminal emulator / editor hosting the CLI,
43904
+ * derived from terminal-identity env vars. Recorded as the `terminalKind` run
43905
+ * tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
43906
+ * …) — the split Sentry can't otherwise see. Low-cardinality and free of any
43907
+ * username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
43908
+ * win over the outer emulator because that's the surface a user is reading in;
43909
+ * "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
43910
+ */
43911
+ const detectTerminalKind = (env = process.env) => {
43912
+ if (env.NVIM) return "neovim";
43913
+ if (env.VIM_TERMINAL) return "vim";
43914
+ const termProgram = env.TERM_PROGRAM;
43915
+ if (termProgram) {
43916
+ for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
43917
+ }
43918
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
43919
+ if (env.WT_SESSION) return "windows-terminal";
43920
+ if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
43921
+ if (env.VTE_VERSION) return "vte";
43922
+ if (env.TMUX) return "tmux";
43923
+ if (isCiEnvironment(env)) return "ci";
43924
+ return "unknown";
43925
+ };
43926
+ //#endregion
43867
43927
  //#region src/cli/utils/is-git-hook-environment.ts
43868
43928
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43869
43929
  //#endregion
@@ -43886,6 +43946,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43886
43946
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43887
43947
  //#endregion
43888
43948
  //#region src/cli/utils/constants.ts
43949
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43889
43950
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43890
43951
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43891
43952
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43970,7 +44031,7 @@ const makeNoopConsole = () => ({
43970
44031
  });
43971
44032
  //#endregion
43972
44033
  //#region src/cli/utils/version.ts
43973
- const VERSION = "0.5.6-dev.451beeb";
44034
+ const VERSION = "0.5.6-dev.5d1347e";
43974
44035
  //#endregion
43975
44036
  //#region src/cli/utils/json-mode.ts
43976
44037
  let context = null;
@@ -44120,6 +44181,7 @@ const buildRunContext = () => {
44120
44181
  viaAction: isOfficialGithubAction(),
44121
44182
  codingAgent: detectCodingAgent(),
44122
44183
  interactive: !isNonInteractiveEnvironment(),
44184
+ terminalKind: detectTerminalKind(),
44123
44185
  jsonMode: isJsonModeActive(),
44124
44186
  invokedVia: detectInvokedVia()
44125
44187
  };
@@ -44190,6 +44252,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44190
44252
  viaAction: runContext.viaAction,
44191
44253
  codingAgent: runContext.codingAgent,
44192
44254
  interactive: runContext.interactive,
44255
+ terminalKind: runContext.terminalKind,
44193
44256
  jsonMode: runContext.jsonMode,
44194
44257
  invokedVia: runContext.invokedVia,
44195
44258
  nodeMajor: runContext.nodeMajor
@@ -44328,13 +44391,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44328
44391
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44329
44392
  * standard `SENTRY_RELEASE` override.
44330
44393
  */
44331
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.451beeb`;
44394
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.5d1347e`;
44332
44395
  /**
44333
44396
  * Deployment environment shown in Sentry's environment filter. Defaults to
44334
44397
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44335
44398
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44336
44399
  */
44337
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.451beeb") ? "development" : "production");
44400
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.5d1347e") ? "development" : "production");
44338
44401
  /**
44339
44402
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44340
44403
  * (set to `0` to disable tracing) and falls back to
@@ -48116,7 +48179,7 @@ const AGENT_GUIDANCE_LINES = [
48116
48179
  "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
48117
48180
  "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
48118
48181
  "Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
48119
- "Run `npx react-doctor@latest --verbose --diff` before and after changes, plus relevant tests after each focused batch.",
48182
+ "Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
48120
48183
  "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
48121
48184
  "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
48122
48185
  "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
@@ -48191,6 +48254,15 @@ const boxText = (content, innerWidth) => {
48191
48254
  ].join("\n");
48192
48255
  };
48193
48256
  //#endregion
48257
+ //#region src/cli/utils/resolve-absolute-path.ts
48258
+ /**
48259
+ * Resolves a diagnostic's `filePath` (relative to its project root, or
48260
+ * already absolute) to an absolute path. Shared by the code-frame reader and
48261
+ * the terminal hyperlink builder so both turn a relative path into the same
48262
+ * on-disk location.
48263
+ */
48264
+ const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
48265
+ //#endregion
48194
48266
  //#region src/cli/utils/build-code-frame.ts
48195
48267
  /**
48196
48268
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48201,7 +48273,7 @@ const boxText = (content, innerWidth) => {
48201
48273
  */
48202
48274
  const buildCodeFrame = (input) => {
48203
48275
  if (input.line <= 0) return null;
48204
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48276
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48205
48277
  let source;
48206
48278
  try {
48207
48279
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48241,6 +48313,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48241
48313
  const DIVIDER_INDENT = " ";
48242
48314
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48243
48315
  //#endregion
48316
+ //#region src/cli/utils/format-hyperlink.ts
48317
+ const OSC = "\x1B]";
48318
+ const ST = "\x1B\\";
48319
+ /**
48320
+ * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
48321
+ * are exactly `text`; the link is carried in escape sequences a capable
48322
+ * terminal turns into a click target.
48323
+ */
48324
+ const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
48325
+ //#endregion
48244
48326
  //#region src/cli/utils/indent-multiline-text.ts
48245
48327
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48246
48328
  //#endregion
@@ -48394,17 +48476,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48394
48476
  }
48395
48477
  return clusters;
48396
48478
  };
48397
- const formatClusterLocation = (cluster) => {
48479
+ const formatClusterLocationText = (cluster) => {
48480
+ const { filePath } = cluster.diagnostics[0];
48481
+ if (cluster.startLine <= 0) return filePath;
48482
+ if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
48483
+ return `${filePath}:${cluster.startLine}`;
48484
+ };
48485
+ const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
48398
48486
  const lead = cluster.diagnostics[0];
48399
48487
  const contextTag = formatFileContextTag(lead);
48400
- if (cluster.startLine <= 0) return `${lead.filePath}${contextTag}`;
48401
- if (cluster.endLine > cluster.startLine) return `${lead.filePath}:${cluster.startLine}-${cluster.endLine}${contextTag}`;
48402
- return `${lead.filePath}:${cluster.startLine}${contextTag}`;
48488
+ const location = formatClusterLocationText(cluster);
48489
+ if (!hyperlinks) return `${location}${contextTag}`;
48490
+ return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
48403
48491
  };
48404
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48492
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48405
48493
  const lead = cluster.diagnostics[0];
48406
48494
  const isMultiSite = cluster.diagnostics.length > 1;
48407
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48495
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48408
48496
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48409
48497
  filePath: lead.filePath,
48410
48498
  line: cluster.startLine,
@@ -48423,7 +48511,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48423
48511
  }
48424
48512
  return lines;
48425
48513
  };
48426
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48514
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48427
48515
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48428
48516
  const { severity } = representative;
48429
48517
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48443,7 +48531,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48443
48531
  }
48444
48532
  const renderCodeFrame = severity === "error";
48445
48533
  const sites = renderEverySite ? ruleDiagnostics : [representative];
48446
- if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
48534
+ if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
48447
48535
  return lines;
48448
48536
  };
48449
48537
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48456,7 +48544,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48456
48544
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48457
48545
  };
48458
48546
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48459
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48547
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48460
48548
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48461
48549
  if (topRuleGroups.length === 0) return {
48462
48550
  lines: [],
@@ -48466,7 +48554,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48466
48554
  const blockOffsets = [];
48467
48555
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48468
48556
  blockOffsets.push(lines.length);
48469
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48557
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48470
48558
  lines.push("");
48471
48559
  }
48472
48560
  return {
@@ -48504,18 +48592,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48504
48592
  * single Effect.forEach over Console.log so failures or fiber
48505
48593
  * interruption produce predictable partial output.
48506
48594
  */
48507
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
48595
+ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
48508
48596
  const sectionPause = onboarding.sectionPause ?? void_;
48509
48597
  const animateCountUp = onboarding.animateCountUp ?? false;
48510
48598
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48511
48599
  let detailLines;
48512
48600
  let topErrorBlockOffsets = [];
48513
48601
  if (!isVerbose) {
48514
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48602
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48515
48603
  detailLines = topErrors.lines;
48516
48604
  topErrorBlockOffsets = topErrors.blockOffsets;
48517
48605
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48518
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48606
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48519
48607
  });
48520
48608
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48521
48609
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48576,6 +48664,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48576
48664
  //#endregion
48577
48665
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48578
48666
  const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
48667
+ //#endregion
48668
+ //#region src/cli/utils/supports-hyperlinks.ts
48669
+ const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
48670
+ "iTerm.app",
48671
+ "WezTerm",
48672
+ "vscode",
48673
+ "Hyper",
48674
+ "ghostty",
48675
+ "Tabby",
48676
+ "rio"
48677
+ ]);
48678
+ const parseVteVersion = (raw) => {
48679
+ const parsed = Number.parseInt(raw ?? "", 10);
48680
+ return Number.isNaN(parsed) ? 0 : parsed;
48681
+ };
48682
+ /**
48683
+ * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
48684
+ * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
48685
+ * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
48686
+ * forces on), mirroring how the ecosystem's terminal libraries gate the same
48687
+ * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
48688
+ * raw escape rather than a link). Unknown terminals default to off.
48689
+ */
48690
+ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
48691
+ const forced = env.FORCE_HYPERLINK;
48692
+ if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
48693
+ if (stream.isTTY !== true) return false;
48694
+ if (env.TERM === "dumb") return false;
48695
+ if (isCiEnvironment(env)) return false;
48696
+ if (env.WT_SESSION) return true;
48697
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
48698
+ if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
48699
+ return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
48700
+ };
48701
+ //#endregion
48702
+ //#region src/cli/utils/should-render-hyperlinks.ts
48703
+ /**
48704
+ * Whether to emit OSC 8 clickable `file:line` locations for this run: a
48705
+ * hyperlink-capable terminal AND not a coding agent (whose output parsers
48706
+ * would choke on the escape sequences).
48707
+ */
48708
+ const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
48579
48709
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48580
48710
  const FALSY_FLAG_VALUES = new Set([
48581
48711
  "",
@@ -48595,10 +48725,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48595
48725
  };
48596
48726
  //#endregion
48597
48727
  //#region src/cli/utils/onboarding-state.ts
48598
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48599
48728
  const ONBOARDED_AT_KEY = "onboardedAt";
48600
48729
  const getOnboardingStore = (options = {}) => new Conf({
48601
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48730
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48602
48731
  cwd: options.cwd
48603
48732
  });
48604
48733
  const hasCompletedOnboarding = (options = {}) => {
@@ -49054,6 +49183,78 @@ const resolveCliCategories = (categoryFlag) => {
49054
49183
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
49055
49184
  };
49056
49185
  //#endregion
49186
+ //#region src/cli/utils/git-hook-shared.ts
49187
+ const HOOK_FILE_NAME = "pre-commit";
49188
+ const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49189
+ const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49190
+ const HUSKY_HOOKS_PATH = ".husky";
49191
+ const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49192
+ const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49193
+ const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49194
+ const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49195
+ const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49196
+ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49197
+ "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49198
+ `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49199
+ "rm -f \"$react_doctor_output\";",
49200
+ "else",
49201
+ "rm -f \"$react_doctor_output\";",
49202
+ `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;`,
49203
+ "fi"
49204
+ ].join(" ");
49205
+ const PACKAGE_JSON_FILE_NAME = "package.json";
49206
+ const runGit = (projectRoot, args) => {
49207
+ try {
49208
+ return execFileSync("git", [...args], {
49209
+ cwd: projectRoot,
49210
+ encoding: "utf8",
49211
+ stdio: [
49212
+ "ignore",
49213
+ "pipe",
49214
+ "ignore"
49215
+ ]
49216
+ }).trim();
49217
+ } catch {
49218
+ return null;
49219
+ }
49220
+ };
49221
+ const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
49222
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49223
+ const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
49224
+ const readPackageJson = (projectRoot) => {
49225
+ try {
49226
+ return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
49227
+ } catch {
49228
+ return null;
49229
+ }
49230
+ };
49231
+ const writeJsonFile$1 = (filePath, value) => {
49232
+ NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
49233
+ };
49234
+ const packageHasDependency = (projectRoot, dependencyName) => {
49235
+ const packageJson = readPackageJson(projectRoot);
49236
+ if (!isRecord$1(packageJson)) return false;
49237
+ return [
49238
+ "dependencies",
49239
+ "devDependencies",
49240
+ "optionalDependencies"
49241
+ ].some((fieldName) => {
49242
+ const dependencies = packageJson[fieldName];
49243
+ return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
49244
+ });
49245
+ };
49246
+ const packageHasRecordKey = (projectRoot, key) => {
49247
+ const packageJson = readPackageJson(projectRoot);
49248
+ return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
49249
+ };
49250
+ const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
49251
+ const packageJson = readPackageJson(projectRoot);
49252
+ if (!isRecord$1(packageJson)) return false;
49253
+ const value = packageJson[key];
49254
+ return isRecord$1(value) && isRecord$1(value[nestedKey]);
49255
+ };
49256
+ const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
49257
+ //#endregion
49057
49258
  //#region src/cli/utils/scan-result-cache.ts
49058
49259
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
49059
49260
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -49064,7 +49265,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
49064
49265
  "eslint-plugin-react-hooks/package.json"
49065
49266
  ];
49066
49267
  const bundledRequire = createRequire(import.meta.url);
49067
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49268
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49068
49269
  const normalizeForStableJson = (value) => {
49069
49270
  if (value === null) return null;
49070
49271
  if (value === void 0) return void 0;
@@ -49093,24 +49294,9 @@ const stringifyStableJson = (value) => {
49093
49294
  }
49094
49295
  };
49095
49296
  const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
49096
- const runGit$1 = (directory, args) => {
49097
- try {
49098
- return execFileSync("git", [...args], {
49099
- cwd: directory,
49100
- encoding: "utf8",
49101
- stdio: [
49102
- "ignore",
49103
- "pipe",
49104
- "ignore"
49105
- ]
49106
- }).trim();
49107
- } catch {
49108
- return null;
49109
- }
49110
- };
49111
- const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
49297
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
49112
49298
  const isWorktreeClean = (projectDirectory) => {
49113
- const status = runGit$1(projectDirectory, [
49299
+ const status = runGit(projectDirectory, [
49114
49300
  "status",
49115
49301
  "--porcelain=v1",
49116
49302
  "--untracked-files=normal"
@@ -49118,7 +49304,7 @@ const isWorktreeClean = (projectDirectory) => {
49118
49304
  return status !== null && status.length === 0;
49119
49305
  };
49120
49306
  const hasHiddenTrackedFileState = (projectDirectory) => {
49121
- const output = runGit$1(projectDirectory, ["ls-files", "-v"]);
49307
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
49122
49308
  if (output === null) return true;
49123
49309
  return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
49124
49310
  };
@@ -49131,7 +49317,7 @@ const resolveCacheFilePath = (projectDirectory) => {
49131
49317
  const readPersistedCache = (cacheFilePath) => {
49132
49318
  try {
49133
49319
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
49134
- if (!isRecord$1(parsed) || parsed.version !== 1) return {
49320
+ if (!isRecord(parsed) || parsed.version !== 1) return {
49135
49321
  version: 1,
49136
49322
  entries: []
49137
49323
  };
@@ -49141,8 +49327,8 @@ const readPersistedCache = (cacheFilePath) => {
49141
49327
  };
49142
49328
  const entries = [];
49143
49329
  for (const entry of parsed.entries) {
49144
- if (!isRecord$1(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49145
- if (!isRecord$1(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49330
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49331
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49146
49332
  entries.push(entry);
49147
49333
  }
49148
49334
  return {
@@ -49674,6 +49860,7 @@ const finalizeAndRender = (input) => gen(function* () {
49674
49860
  }
49675
49861
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49676
49862
  const pause = onboardingSectionPause(animateRender);
49863
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49677
49864
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49678
49865
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49679
49866
  if (printedDiagnostics.length === 0) {
@@ -49699,7 +49886,7 @@ const finalizeAndRender = (input) => gen(function* () {
49699
49886
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49700
49887
  sectionPause: pause,
49701
49888
  animateCountUp: animateRender
49702
- });
49889
+ }, useHyperlinks);
49703
49890
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49704
49891
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49705
49892
  yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
@@ -49892,16 +50079,7 @@ const buildHandoffPayload = (input) => {
49892
50079
  try {
49893
50080
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49894
50081
  } catch {}
49895
- const lines = [
49896
- "Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
49897
- `- It's used by teams at ${CI_TRUST_COMPANIES}.`,
49898
- "- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
49899
- `- Setup guide: ${CI_URL}`,
49900
- "If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
49901
- "",
49902
- `Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
49903
- ""
49904
- ];
50082
+ const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
49905
50083
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49906
50084
  const representative = ruleDiagnostics[0];
49907
50085
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -49962,78 +50140,6 @@ const detectAvailableAgents = async () => {
49962
50140
  return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
49963
50141
  };
49964
50142
  //#endregion
49965
- //#region src/cli/utils/git-hook-shared.ts
49966
- const HOOK_FILE_NAME = "pre-commit";
49967
- const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49968
- const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49969
- const HUSKY_HOOKS_PATH = ".husky";
49970
- const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49971
- const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49972
- const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49973
- const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49974
- const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49975
- const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49976
- "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49977
- `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49978
- "rm -f \"$react_doctor_output\";",
49979
- "else",
49980
- "rm -f \"$react_doctor_output\";",
49981
- `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;`,
49982
- "fi"
49983
- ].join(" ");
49984
- const PACKAGE_JSON_FILE_NAME = "package.json";
49985
- const runGit = (projectRoot, args) => {
49986
- try {
49987
- return execFileSync("git", [...args], {
49988
- cwd: projectRoot,
49989
- encoding: "utf8",
49990
- stdio: [
49991
- "ignore",
49992
- "pipe",
49993
- "ignore"
49994
- ]
49995
- }).trim();
49996
- } catch {
49997
- return null;
49998
- }
49999
- };
50000
- const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
50001
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
50002
- const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
50003
- const readPackageJson = (projectRoot) => {
50004
- try {
50005
- return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
50006
- } catch {
50007
- return null;
50008
- }
50009
- };
50010
- const writeJsonFile$1 = (filePath, value) => {
50011
- NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
50012
- };
50013
- const packageHasDependency = (projectRoot, dependencyName) => {
50014
- const packageJson = readPackageJson(projectRoot);
50015
- if (!isRecord(packageJson)) return false;
50016
- return [
50017
- "dependencies",
50018
- "devDependencies",
50019
- "optionalDependencies"
50020
- ].some((fieldName) => {
50021
- const dependencies = packageJson[fieldName];
50022
- return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
50023
- });
50024
- };
50025
- const packageHasRecordKey = (projectRoot, key) => {
50026
- const packageJson = readPackageJson(projectRoot);
50027
- return isRecord(packageJson) && isRecord(packageJson[key]);
50028
- };
50029
- const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
50030
- const packageJson = readPackageJson(projectRoot);
50031
- if (!isRecord(packageJson)) return false;
50032
- const value = packageJson[key];
50033
- return isRecord(value) && isRecord(value[nestedKey]);
50034
- };
50035
- const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
50036
- //#endregion
50037
50143
  //#region src/cli/utils/install-doctor-script.ts
50038
50144
  const DOCTOR_SCRIPT_NAME = "doctor";
50039
50145
  const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
@@ -50059,31 +50165,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
50059
50165
  };
50060
50166
  const hasDoctorScript = (projectRoot) => {
50061
50167
  const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
50062
- if (!isRecord(packageJson)) return false;
50168
+ if (!isRecord$1(packageJson)) return false;
50063
50169
  const scripts = packageJson.scripts;
50064
- if (!isRecord(scripts)) return false;
50170
+ if (!isRecord$1(scripts)) return false;
50065
50171
  return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
50066
50172
  };
50067
50173
  const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
50068
50174
  const dependencies = packageJson[fieldName];
50069
- return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50175
+ return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50070
50176
  });
50071
50177
  const installDoctorScript = (options) => {
50072
50178
  const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
50073
50179
  const packageJsonPath = getPackageJsonPath(packageDirectory);
50074
50180
  const packageJson = readPackageJson(packageDirectory);
50075
- if (!isRecord(packageJson)) return {
50181
+ if (!isRecord$1(packageJson)) return {
50076
50182
  packageJsonPath,
50077
50183
  scriptStatus: "skipped",
50078
50184
  scriptReason: "missing-or-invalid-package-json"
50079
50185
  };
50080
50186
  const scripts = packageJson.scripts;
50081
50187
  const scriptTarget = (() => {
50082
- if (scripts !== void 0 && !isRecord(scripts)) return {
50188
+ if (scripts !== void 0 && !isRecord$1(scripts)) return {
50083
50189
  status: "skipped",
50084
50190
  reason: "invalid-scripts"
50085
50191
  };
50086
- const scriptRecord = isRecord(scripts) ? scripts : {};
50192
+ const scriptRecord = isRecord$1(scripts) ? scripts : {};
50087
50193
  if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
50088
50194
  scriptName: DOCTOR_SCRIPT_NAME,
50089
50195
  status: "existing"
@@ -50117,7 +50223,7 @@ const installDoctorScript = (options) => {
50117
50223
  if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
50118
50224
  ...packageJson,
50119
50225
  scripts: {
50120
- ...isRecord(scripts) ? scripts : {},
50226
+ ...isRecord$1(scripts) ? scripts : {},
50121
50227
  [scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
50122
50228
  }
50123
50229
  });
@@ -50271,38 +50377,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50271
50377
  //#region src/cli/utils/hash-project-root.ts
50272
50378
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50273
50379
  //#endregion
50274
- //#region src/cli/utils/action-upgrade-prompt.ts
50275
- const GLOBAL_CONFIG_PROJECT_NAME$1 = "react-doctor";
50276
- const getActionUpgradeStore = (options = {}) => new Conf({
50277
- projectName: GLOBAL_CONFIG_PROJECT_NAME$1,
50278
- cwd: options.cwd
50279
- });
50280
- const hasHandledActionUpgrade = (projectRoot, storeOptions = {}) => {
50281
- try {
50282
- const upgrades = getActionUpgradeStore(storeOptions).get("actionUpgrades", {});
50283
- return Boolean(upgrades[hashProjectRoot(projectRoot)]);
50284
- } catch {
50285
- return true;
50286
- }
50287
- };
50288
- const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
50289
- try {
50290
- const store = getActionUpgradeStore(storeOptions);
50291
- const upgrades = store.get("actionUpgrades", {});
50292
- store.set("actionUpgrades", {
50293
- ...upgrades,
50294
- [hashProjectRoot(projectRoot)]: {
50295
- rootDirectory: Path.resolve(projectRoot),
50296
- outcome,
50297
- at: (/* @__PURE__ */ new Date()).toISOString()
50380
+ //#region src/cli/utils/project-decision-store.ts
50381
+ const createProjectDecisionStore = (storeKey) => {
50382
+ const getStore = (options = {}) => new Conf({
50383
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
50384
+ cwd: options.cwd
50385
+ });
50386
+ return {
50387
+ getConfigPath: (options = {}) => getStore(options).path,
50388
+ hasHandled: (projectRoot, options = {}) => {
50389
+ try {
50390
+ return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
50391
+ } catch {
50392
+ return true;
50298
50393
  }
50299
- });
50300
- return true;
50301
- } catch {
50302
- return false;
50303
- }
50394
+ },
50395
+ record: (projectRoot, outcome, options = {}) => {
50396
+ try {
50397
+ const store = getStore(options);
50398
+ store.set(storeKey, {
50399
+ ...store.get(storeKey, {}),
50400
+ [hashProjectRoot(projectRoot)]: {
50401
+ rootDirectory: Path.resolve(projectRoot),
50402
+ outcome,
50403
+ at: (/* @__PURE__ */ new Date()).toISOString()
50404
+ }
50405
+ });
50406
+ return true;
50407
+ } catch {
50408
+ return false;
50409
+ }
50410
+ }
50411
+ };
50304
50412
  };
50305
50413
  //#endregion
50414
+ //#region src/cli/utils/action-upgrade-prompt.ts
50415
+ const store$1 = createProjectDecisionStore("actionUpgrades");
50416
+ store$1.getConfigPath;
50417
+ const hasHandledActionUpgrade = store$1.hasHandled;
50418
+ const recordActionUpgradeDecision = store$1.record;
50419
+ //#endregion
50420
+ //#region src/cli/utils/ci-prompt-decision.ts
50421
+ const store = createProjectDecisionStore("ciPrompts");
50422
+ store.getConfigPath;
50423
+ const hasHandledCiPrompt = store.hasHandled;
50424
+ const recordCiPromptDecision = store.record;
50425
+ //#endregion
50306
50426
  //#region src/cli/utils/open-url.ts
50307
50427
  const resolveOpenCommand = (url) => {
50308
50428
  if (process$1.platform === "darwin") return {
@@ -50758,22 +50878,22 @@ const buildAgentHookScript = () => [
50758
50878
  "",
50759
50879
  "run_react_doctor() {",
50760
50880
  " if [ -x ./node_modules/.bin/react-doctor ]; then",
50761
- " ./node_modules/.bin/react-doctor --verbose --diff --blocking warning --no-score",
50881
+ " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
50762
50882
  " return",
50763
50883
  " fi",
50764
50884
  "",
50765
50885
  " if command -v react-doctor >/dev/null 2>&1; then",
50766
- " react-doctor --verbose --diff --blocking warning --no-score",
50886
+ " react-doctor --verbose --scope changed --blocking warning --no-score",
50767
50887
  " return",
50768
50888
  " fi",
50769
50889
  "",
50770
50890
  " if command -v pnpm >/dev/null 2>&1; then",
50771
- " pnpm dlx react-doctor@latest --verbose --diff --blocking warning --no-score",
50891
+ " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50772
50892
  " return",
50773
50893
  " fi",
50774
50894
  "",
50775
50895
  " if command -v npx >/dev/null 2>&1; then",
50776
- " npx --yes react-doctor@latest --verbose --diff --blocking warning --no-score",
50896
+ " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
50777
50897
  " return",
50778
50898
  " fi",
50779
50899
  "",
@@ -50931,13 +51051,13 @@ const installPackageJsonHook = (options, strategy) => {
50931
51051
  const packageJsonPath = getPackageJsonPath(options.projectRoot);
50932
51052
  const didHookExist = NFS.existsSync(packageJsonPath);
50933
51053
  const packageJson = readPackageJson(options.projectRoot);
50934
- const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
51054
+ const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
50935
51055
  const parentKeys = strategy.path.slice(0, -1);
50936
51056
  const leafKey = strategy.path[strategy.path.length - 1];
50937
51057
  let parent = nextPackageJson;
50938
51058
  for (const key of parentKeys) {
50939
51059
  const existing = parent[key];
50940
- const cloned = isRecord(existing) ? { ...existing } : {};
51060
+ const cloned = isRecord$1(existing) ? { ...existing } : {};
50941
51061
  parent[key] = cloned;
50942
51062
  parent = cloned;
50943
51063
  }
@@ -51108,7 +51228,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
51108
51228
  const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
51109
51229
  const isSimpleGitHooksProject = (projectRoot) => {
51110
51230
  const packageJson = readPackageJson(projectRoot);
51111
- return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51231
+ return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51112
51232
  };
51113
51233
  const getLefthookConfigPath = (projectRoot) => {
51114
51234
  for (const fileName of LEFTHOOK_CONFIG_FILES) {
@@ -51274,7 +51394,7 @@ const detectPackageManager = (projectRoot) => {
51274
51394
  let currentDirectory = Path.resolve(projectRoot);
51275
51395
  while (true) {
51276
51396
  const packageJson = readPackageJson(currentDirectory);
51277
- if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
51397
+ if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
51278
51398
  const packageManagerName = packageJson.packageManager.split("@")[0];
51279
51399
  if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
51280
51400
  }
@@ -51350,12 +51470,12 @@ const isSupplyChainTrustError = (error) => {
51350
51470
  const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
51351
51471
  const installReactDoctorDependency = async (options) => {
51352
51472
  const packageJson = readPackageJson(options.projectRoot);
51353
- if (!isRecord(packageJson)) return {
51473
+ if (!isRecord$1(packageJson)) return {
51354
51474
  dependencyStatus: "skipped",
51355
51475
  dependencyReason: "missing-or-invalid-package-json"
51356
51476
  };
51357
51477
  if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
51358
- if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
51478
+ if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
51359
51479
  dependencyStatus: "skipped",
51360
51480
  dependencyReason: "invalid-dev-dependencies"
51361
51481
  };
@@ -51519,10 +51639,12 @@ const runInstallReactDoctor = async (options = {}) => {
51519
51639
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51520
51640
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51521
51641
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51522
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || !skipPrompts && await askAddToGitHubActions(prompt) === "yes");
51642
+ const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
51643
+ const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
51523
51644
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51524
51645
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51525
51646
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51647
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51526
51648
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51527
51649
  type: "multiselect",
51528
51650
  name: "agents",
@@ -51769,18 +51891,24 @@ const handoffToAgent = async (input) => {
51769
51891
  if (!input.interactive || input.diagnostics.length === 0) return;
51770
51892
  cliLogger.break();
51771
51893
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51772
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51894
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51895
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51773
51896
  const ciOutcome = await askAddToGitHubActions();
51774
51897
  recordCount(METRIC.agentHandoff, 1, {
51775
51898
  outcome: `ci-${ciOutcome}`,
51776
51899
  diagnosticsCount: input.diagnostics.length
51777
51900
  });
51778
51901
  if (ciOutcome === "cancel") return;
51902
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51779
51903
  if (ciOutcome === "yes") {
51780
51904
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51781
51905
  cliLogger.break();
51782
51906
  }
51783
- } else await maybeOfferActionUpgrade(projectRootForCi);
51907
+ } else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
51908
+ else recordCount(METRIC.agentHandoff, 1, {
51909
+ outcome: "ci-suppressed",
51910
+ diagnosticsCount: input.diagnostics.length
51911
+ });
51784
51912
  const { handoffTarget } = await prompts({
51785
51913
  type: "select",
51786
51914
  name: "handoffTarget",
@@ -52086,7 +52214,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52086
52214
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52087
52215
  if (displayDiagnostics.length > 0) {
52088
52216
  yield* log("");
52089
- yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
52217
+ yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
52090
52218
  }
52091
52219
  const lowestScoredScan = findLowestScoredScan(completedScans);
52092
52220
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52124,9 +52252,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52124
52252
  });
52125
52253
  //#endregion
52126
52254
  //#region src/cli/utils/prompt-install-setup.ts
52127
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52128
52255
  const getSetupPromptStore = (options = {}) => new Conf({
52129
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52256
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52130
52257
  cwd: options.cwd
52131
52258
  });
52132
52259
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52137,6 +52264,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52137
52264
  return false;
52138
52265
  }
52139
52266
  };
52267
+ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
52268
+ try {
52269
+ const store = getSetupPromptStore(storeOptions);
52270
+ const projects = store.get("projects", {});
52271
+ const projectKey = getSetupPromptProjectKey(projectRoot);
52272
+ store.set("projects", {
52273
+ ...projects,
52274
+ [projectKey]: {
52275
+ ...projects[projectKey] ?? {},
52276
+ rootDirectory: Path.resolve(projectRoot),
52277
+ setupPrompt: false
52278
+ }
52279
+ });
52280
+ return true;
52281
+ } catch {
52282
+ return false;
52283
+ }
52284
+ };
52140
52285
  const resolveInstallSetupProjectRoot = (options) => {
52141
52286
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52142
52287
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52543,6 +52688,14 @@ const runExplain = async (fileLineArgument, context) => {
52543
52688
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52544
52689
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52545
52690
  cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
52691
+ const codeFrame = buildCodeFrame({
52692
+ filePath: diagnostic.filePath,
52693
+ line: diagnostic.line,
52694
+ column: diagnostic.column,
52695
+ endLine: diagnostic.endLine,
52696
+ rootDirectory: targetDirectory
52697
+ });
52698
+ if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
52546
52699
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52547
52700
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52548
52701
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52939,6 +53092,7 @@ const inspectAction = async (directory, flags) => {
52939
53092
  })) {
52940
53093
  printAgentInstallHint();
52941
53094
  recordCount(METRIC.agentInstallHintShown, 1);
53095
+ disableSetupPrompt(setupProjectRoot);
52942
53096
  }
52943
53097
  }
52944
53098
  } catch (error) {
@@ -53870,7 +54024,7 @@ ${highlighter.dim("Examples:")}
53870
54024
  ${formatExampleLines([
53871
54025
  ["react-doctor", "scan the current project"],
53872
54026
  ["react-doctor ./apps/web", "scan a specific directory"],
53873
- ["react-doctor --diff main", "scan only files changed vs. main"],
54027
+ ["react-doctor --scope changed --base main", "scan only new issues vs. main"],
53874
54028
  ["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
53875
54029
  ["react-doctor --staged", "scan staged files (pre-commit hook)"],
53876
54030
  ["react-doctor --category Security", "show only one diagnostic category"],
@@ -53946,4 +54100,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53946
54100
  export {};
53947
54101
 
53948
54102
  //# sourceMappingURL=cli.js.map
53949
- //# debugId=bb93aaad-ea85-5f1d-b2db-584f48671f66
54103
+ //# debugId=37f4074d-83ec-54d9-a9f3-5a8967531f6b