react-doctor 0.5.6-dev.6b8e756 → 0.5.6-dev.740211c

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]="54905d6a-f06f-5f80-9fb6-7d4ab857d553")}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]="f029f05b-4c71-52d3-b523-0834d67de2d4")}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";
@@ -22400,6 +22400,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
22400
22400
  score: Unknown,
22401
22401
  skippedChecks: ArraySchema(String$1),
22402
22402
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
22403
+ scannedFileCount: optional(Number$1),
22403
22404
  elapsedMilliseconds: Number$1
22404
22405
  }) {};
22405
22406
  /**
@@ -35893,6 +35894,7 @@ const isLargeMinifiedFile = (absolutePath) => {
35893
35894
  if (sizeBytes < 2e4) return false;
35894
35895
  return isMinifiedSource(absolutePath);
35895
35896
  };
35897
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
35896
35898
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
35897
35899
  "EACCES",
35898
35900
  "EPERM",
@@ -35902,11 +35904,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
35902
35904
  "ELOOP",
35903
35905
  "ENAMETOOLONG"
35904
35906
  ]);
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
- };
35907
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
35910
35908
  const readDirectoryEntries = (directoryPath) => {
35911
35909
  try {
35912
35910
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -35953,7 +35951,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
35953
35951
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
35954
35952
  } catch (error) {
35955
35953
  if (error instanceof SyntaxError) return {};
35956
- if (error instanceof Error && "code" in error) {
35954
+ if (isErrnoException(error)) {
35957
35955
  const { code } = error;
35958
35956
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
35959
35957
  }
@@ -36678,17 +36676,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
36678
36676
  return false;
36679
36677
  };
36680
36678
  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;
36679
+ const getDependencySpec = (packageJson, packageName) => {
36680
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
36683
36681
  return typeof spec === "string" ? spec : null;
36684
36682
  };
36685
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
36683
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
36686
36684
  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);
36685
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
36692
36686
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
36693
36687
  if (version === null || !isCatalogReference(version)) return version;
36694
36688
  const catalogName = extractCatalogName(version);
@@ -36700,11 +36694,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
36700
36694
  if (!isFile(monorepoPackageJsonPath)) return version;
36701
36695
  return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
36702
36696
  };
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);
36697
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
36708
36698
  const getPreactVersion = (packageJson) => {
36709
36699
  return {
36710
36700
  ...packageJson.peerDependencies,
@@ -36793,6 +36783,11 @@ const ES_TARGET_YEAR_BY_NAME = {
36793
36783
  esnext: 9999
36794
36784
  };
36795
36785
  /**
36786
+ * tsconfig filenames probed when resolving a project's TypeScript
36787
+ * compiler options — the root config first, then a monorepo base config.
36788
+ */
36789
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
36790
+ /**
36796
36791
  * Project-config files that `StagedFiles.materialize` copies into
36797
36792
  * the temp directory alongside staged sources so oxlint resolves
36798
36793
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -37314,6 +37309,7 @@ const isTailwindAtLeast = (detected, required) => {
37314
37309
  if (detected.major !== required.major) return detected.major > required.major;
37315
37310
  return detected.minor >= required.minor;
37316
37311
  };
37312
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
37317
37313
  var InvalidGlobPatternError = class extends Error {
37318
37314
  pattern;
37319
37315
  reason;
@@ -37342,7 +37338,7 @@ const compileGlobPattern = (rawPattern) => {
37342
37338
  try {
37343
37339
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
37344
37340
  } catch (caughtError) {
37345
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
37341
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
37346
37342
  }
37347
37343
  };
37348
37344
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -38520,7 +38516,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
38520
38516
  const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
38521
38517
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
38522
38518
  const jiti = createJiti(import.meta.url);
38523
- const formatError = (error) => error instanceof Error ? error.message : String(error);
38524
38519
  const importDefaultExport = async (jitiInstance, filePath) => {
38525
38520
  const imported = await jitiInstance.import(filePath);
38526
38521
  return imported?.default ?? imported;
@@ -38552,7 +38547,7 @@ const loadModuleConfig = async (filePath) => {
38552
38547
  try {
38553
38548
  return await importDefaultExport(aliasJiti, filePath);
38554
38549
  } 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 });
38550
+ 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
38551
  }
38557
38552
  }
38558
38553
  };
@@ -38601,7 +38596,7 @@ const loadLegacyConfig = (directory) => {
38601
38596
  }
38602
38597
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
38603
38598
  } catch (error) {
38604
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
38599
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
38605
38600
  }
38606
38601
  return {
38607
38602
  status: "invalid",
@@ -38628,7 +38623,7 @@ const loadConfigFromDirectory = async (directory) => {
38628
38623
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
38629
38624
  sawBrokenConfigFile = true;
38630
38625
  } catch (error) {
38631
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
38626
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
38632
38627
  sawBrokenConfigFile = true;
38633
38628
  }
38634
38629
  }
@@ -39692,15 +39687,10 @@ const buildCapabilities = (project) => {
39692
39687
  }
39693
39688
  if (project.tailwindVersion !== null) {
39694
39689
  capabilities.add("tailwind");
39695
- const tailwind = parseTailwindMajorMinor(project.tailwindVersion);
39696
- if (isTailwindAtLeast(tailwind, {
39690
+ if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
39697
39691
  major: 3,
39698
39692
  minor: 4
39699
39693
  })) capabilities.add("tailwind:3.4");
39700
- if (tailwind !== null && isTailwindAtLeast(tailwind, {
39701
- major: 4,
39702
- minor: 0
39703
- })) capabilities.add("tailwind:4");
39704
39694
  }
39705
39695
  if (project.zodVersion !== null) {
39706
39696
  capabilities.add("zod");
@@ -39908,7 +39898,7 @@ const readIgnoreFile = (filePath) => {
39908
39898
  try {
39909
39899
  content = NFS.readFileSync(filePath, "utf-8");
39910
39900
  } catch (error) {
39911
- const errnoCode = error?.code;
39901
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
39912
39902
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
39913
39903
  return [];
39914
39904
  }
@@ -39946,8 +39936,8 @@ const collectIgnorePatterns = (rootDirectory) => {
39946
39936
  cachedPatternsByRoot.set(rootDirectory, patterns);
39947
39937
  return patterns;
39948
39938
  };
39939
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39949
39940
  const KNIP_JSON_FILENAME = "knip.json";
39950
- const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39951
39941
  const readJsonFileSafe = (filePath) => {
39952
39942
  let rawContents;
39953
39943
  try {
@@ -39963,10 +39953,10 @@ const readJsonFileSafe = (filePath) => {
39963
39953
  };
39964
39954
  const readKnipConfig = (rootDirectory) => {
39965
39955
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
39966
- if (isRecord$1$1(knipJson)) return knipJson;
39956
+ if (isRecord$2(knipJson)) return knipJson;
39967
39957
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
39968
- const packageKnipConfig = isRecord$1$1(packageJson) ? packageJson.knip : null;
39969
- return isRecord$1$1(packageKnipConfig) ? packageKnipConfig : null;
39958
+ const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
39959
+ return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
39970
39960
  };
39971
39961
  const normalizePatternList = (value) => {
39972
39962
  if (typeof value === "string" && value.length > 0) return [value];
@@ -39978,10 +39968,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
39978
39968
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
39979
39969
  };
39980
39970
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
39981
- if (!isRecord$1$1(workspaces)) return [];
39971
+ if (!isRecord$2(workspaces)) return [];
39982
39972
  const patterns = [];
39983
39973
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
39984
- if (!isRecord$1$1(workspaceConfig)) continue;
39974
+ if (!isRecord$2(workspaceConfig)) continue;
39985
39975
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
39986
39976
  }
39987
39977
  return patterns;
@@ -40026,8 +40016,6 @@ const toCanonicalPath = (filePath) => {
40026
40016
  };
40027
40017
  const DEAD_CODE_PLUGIN = "deslop";
40028
40018
  const DEAD_CODE_CATEGORY = "Maintainability";
40029
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
40030
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40031
40019
  const DEAD_CODE_WORKER_SCRIPT = `
40032
40020
  const inputChunks = [];
40033
40021
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -40085,7 +40073,7 @@ process.stdin.on("end", () => {
40085
40073
  });
40086
40074
  `;
40087
40075
  const resolveTsConfigPath = (rootDirectory) => {
40088
- for (const filename of TSCONFIG_FILENAMES$1) {
40076
+ for (const filename of TSCONFIG_FILENAMES) {
40089
40077
  const candidate = Path.join(rootDirectory, filename);
40090
40078
  if (NFS.existsSync(candidate)) return candidate;
40091
40079
  }
@@ -40466,15 +40454,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
40466
40454
  })()) }));
40467
40455
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
40468
40456
  };
40469
- const createNodeReadFileLinesSync = (rootDirectory) => {
40470
- return (filePath) => {
40471
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
40472
- try {
40473
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
40474
- } catch {
40475
- return null;
40476
- }
40477
- };
40457
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
40458
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
40459
+ try {
40460
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
40461
+ } catch {
40462
+ return null;
40463
+ }
40478
40464
  };
40479
40465
  var Files = class Files extends Service()("react-doctor/Files") {
40480
40466
  static layerNode = succeed$3(Files, Files.of({
@@ -40685,7 +40671,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
40685
40671
  directory: input.directory,
40686
40672
  cause
40687
40673
  }) });
40688
- }));
40674
+ }), withSpan("git.exec", { attributes: {
40675
+ "git.command": input.command,
40676
+ "git.subcommand": input.args[0] ?? ""
40677
+ } }));
40689
40678
  const runGit = (directory, args) => runCommand({
40690
40679
  command: "git",
40691
40680
  args,
@@ -40713,7 +40702,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40713
40702
  ]);
40714
40703
  if (candidates.status !== 0) return null;
40715
40704
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
40716
- });
40705
+ }).pipe(withSpan("Git.defaultBranch"));
40717
40706
  const branchExists = (directory, branch) => runGit(directory, [
40718
40707
  "rev-parse",
40719
40708
  "--verify",
@@ -40760,7 +40749,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40760
40749
  const result = resultOption.value;
40761
40750
  if (result.status !== 0) return null;
40762
40751
  return parseGithubViewerPermission(result.stdout);
40763
- }).pipe(catch_$1(() => succeed$2(null)));
40752
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
40764
40753
  /**
40765
40754
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
40766
40755
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -40874,7 +40863,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40874
40863
  changedFiles: splitNullSeparated(diff.stdout),
40875
40864
  isCurrentChanges: false
40876
40865
  };
40877
- }),
40866
+ }).pipe(withSpan("Git.diffSelection")),
40878
40867
  stagedFilePaths: (directory) => runGit(directory, [
40879
40868
  "diff",
40880
40869
  "--cached",
@@ -40916,7 +40905,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40916
40905
  status: result.status,
40917
40906
  stdout: result.stdout
40918
40907
  };
40919
- }),
40908
+ }).pipe(withSpan("Git.grep")),
40920
40909
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
40921
40910
  if (files.length === 0) return [];
40922
40911
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -40932,7 +40921,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
40932
40921
  ]);
40933
40922
  if (result.status !== 0) return null;
40934
40923
  return parseChangedLineRanges(result.stdout);
40935
- })
40924
+ }).pipe(withSpan("Git.changedLineRanges"))
40936
40925
  });
40937
40926
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
40938
40927
  /**
@@ -41147,7 +41136,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
41147
41136
  for (const [absolutePath, originalContent] of originalContents) try {
41148
41137
  NFS.writeFileSync(absolutePath, originalContent);
41149
41138
  } catch (error) {
41150
- 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`);
41139
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
41151
41140
  }
41152
41141
  };
41153
41142
  const onExit = () => restore();
@@ -41253,7 +41242,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
41253
41242
  try {
41254
41243
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
41255
41244
  } catch (error) {
41256
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
41245
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
41257
41246
  return null;
41258
41247
  }
41259
41248
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -41325,8 +41314,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41325
41314
  }
41326
41315
  return enabled;
41327
41316
  };
41328
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
41329
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41317
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
41318
+ const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41330
41319
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41331
41320
  const jsPlugins = [];
41332
41321
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -41386,7 +41375,6 @@ const resolveOxlintBinary = () => {
41386
41375
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
41387
41376
  };
41388
41377
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
41389
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
41390
41378
  const resolveTsConfigRelativePath = (rootDirectory) => {
41391
41379
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
41392
41380
  return null;
@@ -41758,7 +41746,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
41758
41746
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
41759
41747
  let currentNode = identifier.parent;
41760
41748
  while (currentNode) {
41761
- if (isScopeNode(currentNode)) {
41749
+ if (isScopeBoundary(currentNode)) {
41762
41750
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
41763
41751
  }
41764
41752
  if (currentNode === sourceFile) return false;
@@ -41849,11 +41837,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
41849
41837
  });
41850
41838
  return resolution;
41851
41839
  };
41852
- const isScopeNode = isScopeBoundary;
41853
41840
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
41854
41841
  let currentNode = identifier.parent;
41855
41842
  while (currentNode) {
41856
- if (isScopeNode(currentNode)) {
41843
+ if (isScopeBoundary(currentNode)) {
41857
41844
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
41858
41845
  if (resolution) return resolution;
41859
41846
  }
@@ -42023,9 +42010,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
42023
42010
  try {
42024
42011
  parsed = JSON.parse(sanitizedStdout);
42025
42012
  } catch {
42026
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42013
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42027
42014
  }
42028
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
42015
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
42029
42016
  const minifiedFileCache = /* @__PURE__ */ new Map();
42030
42017
  const isMinifiedDiagnosticFile = (filename) => {
42031
42018
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -42101,7 +42088,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
42101
42088
  child.kill("SIGKILL");
42102
42089
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
42103
42090
  kind: "timeout",
42104
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
42091
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
42105
42092
  }) }));
42106
42093
  }, spawnTimeoutMs);
42107
42094
  timeoutHandle.unref?.();
@@ -42316,6 +42303,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
42316
42303
  NFS.closeSync(fileHandle);
42317
42304
  }
42318
42305
  };
42306
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
42307
+ /**
42308
+ * Detects an oxlint config-load crash caused by the optional
42309
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
42310
+ * builds the partial-failure note for it; returns `null` when the failure
42311
+ * was anything else.
42312
+ *
42313
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
42314
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
42315
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
42316
+ * config load on it, leaving the plugin in would drop every curated
42317
+ * react-doctor diagnostic too — so the caller retries with the plugin
42318
+ * stripped (issue #833). Both markers sit at the start of oxlint's
42319
+ * message, so they survive the `preview` slice even for deep pnpm paths.
42320
+ */
42321
+ const reactHooksJsPluginDropNote = (error) => {
42322
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
42323
+ const { preview } = error.reason;
42324
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
42325
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
42326
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
42327
+ };
42319
42328
  /**
42320
42329
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
42321
42330
  *
@@ -42343,15 +42352,16 @@ const runOxlint = async (options) => {
42343
42352
  const pluginPath = resolvePluginPath();
42344
42353
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
42345
42354
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
42346
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
42355
+ const buildConfig = (overrides) => createOxlintConfig({
42347
42356
  pluginPath,
42348
42357
  project,
42349
42358
  customRulesOnly,
42350
- extendsPaths: extendsForThisAttempt,
42359
+ extendsPaths: overrides.extendsPaths,
42351
42360
  ignoredTags,
42352
42361
  serverAuthFunctionNames,
42353
42362
  severityControls,
42354
- userPlugins
42363
+ userPlugins,
42364
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
42355
42365
  });
42356
42366
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
42357
42367
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -42387,12 +42397,22 @@ const runOxlint = async (options) => {
42387
42397
  outputMaxBytes,
42388
42398
  concurrency: options.concurrency
42389
42399
  });
42390
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
42400
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
42391
42401
  try {
42392
42402
  return await runBatches();
42393
42403
  } catch (error) {
42404
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
42405
+ if (reactHooksJsDropNote !== null) {
42406
+ writeOxlintConfig(configPath, buildConfig({
42407
+ extendsPaths,
42408
+ disableReactHooksJsPlugin: true
42409
+ }));
42410
+ const diagnostics = await runBatches();
42411
+ onPartialFailure?.(reactHooksJsDropNote);
42412
+ return diagnostics;
42413
+ }
42394
42414
  if (extendsPaths.length === 0) throw error;
42395
- writeOxlintConfig(configPath, buildConfig([]));
42415
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
42396
42416
  return await runBatches();
42397
42417
  }
42398
42418
  } finally {
@@ -43190,7 +43210,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43190
43210
  }))))))));
43191
43211
  const deadCodeFailureState = yield* get$2(deadCodeFailure);
43192
43212
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
43193
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
43213
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
43194
43214
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
43195
43215
  else if (input.suppressScanSummary) yield* scanProgress.stop();
43196
43216
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
@@ -43427,7 +43447,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
43427
43447
  static layerNode = effect(StagedFiles, gen(function* () {
43428
43448
  const git = yield* Git;
43429
43449
  return StagedFiles.of({
43430
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
43450
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
43431
43451
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
43432
43452
  directory,
43433
43453
  files: stagedFiles,
@@ -43437,7 +43457,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
43437
43457
  tempDirectory: tree.tempDirectory,
43438
43458
  stagedFiles: tree.materializedFiles,
43439
43459
  cleanup: tree.cleanup
43440
- })))
43460
+ })), withSpan("StagedFiles.materialize"))
43441
43461
  });
43442
43462
  }));
43443
43463
  /**
@@ -43570,6 +43590,7 @@ const buildJsonReport = (input) => {
43570
43590
  score: result.score,
43571
43591
  skippedChecks: result.skippedChecks,
43572
43592
  ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
43593
+ ...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
43573
43594
  elapsedMilliseconds: result.elapsedMilliseconds
43574
43595
  }));
43575
43596
  const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
@@ -43844,7 +43865,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
43844
43865
  "false"
43845
43866
  ]);
43846
43867
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
43847
- const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(process.env[environmentVariable])) || isCiFlagSet(process.env.CI);
43868
+ const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
43848
43869
  const detectCiProvider = () => {
43849
43870
  for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
43850
43871
  return isCiFlagSet(process.env.CI) ? "unknown" : null;
@@ -43869,6 +43890,53 @@ const detectCodingAgent = () => {
43869
43890
  const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
43870
43891
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
43871
43892
  //#endregion
43893
+ //#region src/cli/utils/detect-terminal-kind.ts
43894
+ const TERMINAL_BY_TERM_PROGRAM = [
43895
+ ["vscode", "vscode"],
43896
+ ["iTerm.app", "iterm"],
43897
+ ["Apple_Terminal", "apple-terminal"],
43898
+ ["WezTerm", "wezterm"],
43899
+ ["ghostty", "ghostty"],
43900
+ ["Hyper", "hyper"],
43901
+ ["Tabby", "tabby"],
43902
+ ["rio", "rio"]
43903
+ ];
43904
+ /**
43905
+ * Best-effort label for the terminal emulator / editor hosting the CLI,
43906
+ * derived from terminal-identity env vars. Recorded as the `terminalKind` run
43907
+ * tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
43908
+ * …) — the split Sentry can't otherwise see. Low-cardinality and free of any
43909
+ * username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
43910
+ * win over the outer emulator because that's the surface a user is reading in;
43911
+ * "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
43912
+ */
43913
+ const detectTerminalKind = (env = process.env) => {
43914
+ if (env.NVIM) return "neovim";
43915
+ if (env.VIM_TERMINAL) return "vim";
43916
+ const termProgram = env.TERM_PROGRAM;
43917
+ if (termProgram) {
43918
+ for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
43919
+ }
43920
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
43921
+ if (env.WT_SESSION) return "windows-terminal";
43922
+ if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
43923
+ if (env.VTE_VERSION) return "vte";
43924
+ if (env.TMUX) return "tmux";
43925
+ if (isCiEnvironment(env)) return "ci";
43926
+ return "unknown";
43927
+ };
43928
+ //#endregion
43929
+ //#region src/cli/utils/is-debug-flag.ts
43930
+ /**
43931
+ * Whether the user passed `--debug` (surface the run's Sentry trace id, and
43932
+ * force performance tracing on so there's a trace to surface). Read straight
43933
+ * from argv rather than Commander's parsed flags because `initializeSentry()`
43934
+ * runs before Commander parses — the same reason `shouldEnableSentry()` reads
43935
+ * `--no-score` from argv. Sharing this one reader keeps the init-time sampling
43936
+ * override and the end-of-run print in agreement.
43937
+ */
43938
+ const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
43939
+ //#endregion
43872
43940
  //#region src/cli/utils/is-git-hook-environment.ts
43873
43941
  const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
43874
43942
  //#endregion
@@ -43891,6 +43959,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
43891
43959
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
43892
43960
  //#endregion
43893
43961
  //#region src/cli/utils/constants.ts
43962
+ const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
43894
43963
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
43895
43964
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
43896
43965
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
@@ -43975,7 +44044,7 @@ const makeNoopConsole = () => ({
43975
44044
  });
43976
44045
  //#endregion
43977
44046
  //#region src/cli/utils/version.ts
43978
- const VERSION = "0.5.6-dev.6b8e756";
44047
+ const VERSION = "0.5.6-dev.740211c";
43979
44048
  //#endregion
43980
44049
  //#region src/cli/utils/json-mode.ts
43981
44050
  let context = null;
@@ -44125,7 +44194,9 @@ const buildRunContext = () => {
44125
44194
  viaAction: isOfficialGithubAction(),
44126
44195
  codingAgent: detectCodingAgent(),
44127
44196
  interactive: !isNonInteractiveEnvironment(),
44197
+ terminalKind: detectTerminalKind(),
44128
44198
  jsonMode: isJsonModeActive(),
44199
+ debug: isDebugFlagEnabled(),
44129
44200
  invokedVia: detectInvokedVia()
44130
44201
  };
44131
44202
  };
@@ -44195,7 +44266,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
44195
44266
  viaAction: runContext.viaAction,
44196
44267
  codingAgent: runContext.codingAgent,
44197
44268
  interactive: runContext.interactive,
44269
+ terminalKind: runContext.terminalKind,
44198
44270
  jsonMode: runContext.jsonMode,
44271
+ debug: runContext.debug,
44199
44272
  invokedVia: runContext.invokedVia,
44200
44273
  nodeMajor: runContext.nodeMajor
44201
44274
  };
@@ -44333,13 +44406,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
44333
44406
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
44334
44407
  * standard `SENTRY_RELEASE` override.
44335
44408
  */
44336
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.6b8e756`;
44409
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.740211c`;
44337
44410
  /**
44338
44411
  * Deployment environment shown in Sentry's environment filter. Defaults to
44339
44412
  * `production` for tagged releases and `development` for dev/unbuilt versions,
44340
44413
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
44341
44414
  */
44342
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.6b8e756") ? "development" : "production");
44415
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.740211c") ? "development" : "production");
44343
44416
  /**
44344
44417
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
44345
44418
  * (set to `0` to disable tracing) and falls back to
@@ -44403,7 +44476,7 @@ const flushSentry = async () => {
44403
44476
  const initializeSentry = () => {
44404
44477
  if (isInitialized || !shouldEnableSentry()) return;
44405
44478
  isInitialized = true;
44406
- resolvedTracesSampleRate = resolveTracesSampleRate();
44479
+ resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
44407
44480
  const { tags, contexts } = buildSentryScope();
44408
44481
  Sentry.init({
44409
44482
  dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
@@ -47619,6 +47692,11 @@ const setActiveRunTrace = (trace) => {
47619
47692
  activeRunTrace = trace;
47620
47693
  };
47621
47694
  const getActiveRunTrace = () => activeRunTrace;
47695
+ let lastRunTraceId = null;
47696
+ const recordRunTraceId = (traceId) => {
47697
+ lastRunTraceId = traceId;
47698
+ };
47699
+ const getLastRunTraceId = () => lastRunTraceId;
47622
47700
  //#endregion
47623
47701
  //#region src/cli/utils/to-span-attributes.ts
47624
47702
  /**
@@ -47681,14 +47759,13 @@ const withSentryRunSpan = (run, options = {}) => {
47681
47759
  op: "cli.inspect",
47682
47760
  attributes: toSpanAttributes(tags)
47683
47761
  }, (rootSpan) => {
47684
- if (options.concurrentScan !== true) {
47685
- const spanContext = rootSpan.spanContext();
47686
- setActiveRunTrace({
47687
- traceId: spanContext.traceId,
47688
- spanId: spanContext.spanId,
47689
- sampled: (spanContext.traceFlags & 1) === 1
47690
- });
47691
- }
47762
+ const spanContext = rootSpan.spanContext();
47763
+ recordRunTraceId(spanContext.traceId);
47764
+ if (options.concurrentScan !== true) setActiveRunTrace({
47765
+ traceId: spanContext.traceId,
47766
+ spanId: spanContext.spanId,
47767
+ sampled: (spanContext.traceFlags & 1) === 1
47768
+ });
47692
47769
  return run(rootSpan);
47693
47770
  });
47694
47771
  };
@@ -48196,6 +48273,15 @@ const boxText = (content, innerWidth) => {
48196
48273
  ].join("\n");
48197
48274
  };
48198
48275
  //#endregion
48276
+ //#region src/cli/utils/resolve-absolute-path.ts
48277
+ /**
48278
+ * Resolves a diagnostic's `filePath` (relative to its project root, or
48279
+ * already absolute) to an absolute path. Shared by the code-frame reader and
48280
+ * the terminal hyperlink builder so both turn a relative path into the same
48281
+ * on-disk location.
48282
+ */
48283
+ const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
48284
+ //#endregion
48199
48285
  //#region src/cli/utils/build-code-frame.ts
48200
48286
  /**
48201
48287
  * Renders a syntax-highlighted source excerpt around a diagnostic site
@@ -48206,7 +48292,7 @@ const boxText = (content, innerWidth) => {
48206
48292
  */
48207
48293
  const buildCodeFrame = (input) => {
48208
48294
  if (input.line <= 0) return null;
48209
- const absolutePath = Path.isAbsolute(input.filePath) ? input.filePath : Path.resolve(input.rootDirectory || ".", input.filePath);
48295
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
48210
48296
  let source;
48211
48297
  try {
48212
48298
  source = NFS.readFileSync(absolutePath, "utf8");
@@ -48246,6 +48332,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
48246
48332
  const DIVIDER_INDENT = " ";
48247
48333
  const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
48248
48334
  //#endregion
48335
+ //#region src/cli/utils/format-hyperlink.ts
48336
+ const OSC = "\x1B]";
48337
+ const ST = "\x1B\\";
48338
+ /**
48339
+ * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
48340
+ * are exactly `text`; the link is carried in escape sequences a capable
48341
+ * terminal turns into a click target.
48342
+ */
48343
+ const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
48344
+ //#endregion
48249
48345
  //#region src/cli/utils/indent-multiline-text.ts
48250
48346
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
48251
48347
  //#endregion
@@ -48399,17 +48495,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
48399
48495
  }
48400
48496
  return clusters;
48401
48497
  };
48402
- const formatClusterLocation = (cluster) => {
48498
+ const formatClusterLocationText = (cluster) => {
48499
+ const { filePath } = cluster.diagnostics[0];
48500
+ if (cluster.startLine <= 0) return filePath;
48501
+ if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
48502
+ return `${filePath}:${cluster.startLine}`;
48503
+ };
48504
+ const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
48403
48505
  const lead = cluster.diagnostics[0];
48404
48506
  const contextTag = formatFileContextTag(lead);
48405
- if (cluster.startLine <= 0) return `${lead.filePath}${contextTag}`;
48406
- if (cluster.endLine > cluster.startLine) return `${lead.filePath}:${cluster.startLine}-${cluster.endLine}${contextTag}`;
48407
- return `${lead.filePath}:${cluster.startLine}${contextTag}`;
48507
+ const location = formatClusterLocationText(cluster);
48508
+ if (!hyperlinks) return `${location}${contextTag}`;
48509
+ return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
48408
48510
  };
48409
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
48511
+ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
48410
48512
  const lead = cluster.diagnostics[0];
48411
48513
  const isMultiSite = cluster.diagnostics.length > 1;
48412
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
48514
+ const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
48413
48515
  const codeFrame = renderCodeFrame ? buildCodeFrame({
48414
48516
  filePath: lead.filePath,
48415
48517
  line: cluster.startLine,
@@ -48428,7 +48530,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
48428
48530
  }
48429
48531
  return lines;
48430
48532
  };
48431
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
48533
+ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
48432
48534
  const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
48433
48535
  const { severity } = representative;
48434
48536
  const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
@@ -48448,7 +48550,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
48448
48550
  }
48449
48551
  const renderCodeFrame = severity === "error";
48450
48552
  const sites = renderEverySite ? ruleDiagnostics : [representative];
48451
- if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
48553
+ if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
48452
48554
  return lines;
48453
48555
  };
48454
48556
  const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
@@ -48461,7 +48563,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
48461
48563
  return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
48462
48564
  };
48463
48565
  const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
48464
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
48566
+ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
48465
48567
  const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
48466
48568
  if (topRuleGroups.length === 0) return {
48467
48569
  lines: [],
@@ -48471,7 +48573,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
48471
48573
  const blockOffsets = [];
48472
48574
  for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
48473
48575
  blockOffsets.push(lines.length);
48474
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
48576
+ lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
48475
48577
  lines.push("");
48476
48578
  }
48477
48579
  return {
@@ -48509,18 +48611,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
48509
48611
  * single Effect.forEach over Console.log so failures or fiber
48510
48612
  * interruption produce predictable partial output.
48511
48613
  */
48512
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
48614
+ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
48513
48615
  const sectionPause = onboarding.sectionPause ?? void_;
48514
48616
  const animateCountUp = onboarding.animateCountUp ?? false;
48515
48617
  const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
48516
48618
  let detailLines;
48517
48619
  let topErrorBlockOffsets = [];
48518
48620
  if (!isVerbose) {
48519
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
48621
+ const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
48520
48622
  detailLines = topErrors.lines;
48521
48623
  topErrorBlockOffsets = topErrors.blockOffsets;
48522
48624
  } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
48523
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
48625
+ return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
48524
48626
  });
48525
48627
  const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
48526
48628
  const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
@@ -48581,6 +48683,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
48581
48683
  //#endregion
48582
48684
  //#region src/cli/utils/filter-diagnostics-by-categories.ts
48583
48685
  const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
48686
+ //#endregion
48687
+ //#region src/cli/utils/supports-hyperlinks.ts
48688
+ const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
48689
+ "iTerm.app",
48690
+ "WezTerm",
48691
+ "vscode",
48692
+ "Hyper",
48693
+ "ghostty",
48694
+ "Tabby",
48695
+ "rio"
48696
+ ]);
48697
+ const parseVteVersion = (raw) => {
48698
+ const parsed = Number.parseInt(raw ?? "", 10);
48699
+ return Number.isNaN(parsed) ? 0 : parsed;
48700
+ };
48701
+ /**
48702
+ * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
48703
+ * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
48704
+ * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
48705
+ * forces on), mirroring how the ecosystem's terminal libraries gate the same
48706
+ * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
48707
+ * raw escape rather than a link). Unknown terminals default to off.
48708
+ */
48709
+ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
48710
+ const forced = env.FORCE_HYPERLINK;
48711
+ if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
48712
+ if (stream.isTTY !== true) return false;
48713
+ if (env.TERM === "dumb") return false;
48714
+ if (isCiEnvironment(env)) return false;
48715
+ if (env.WT_SESSION) return true;
48716
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
48717
+ if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
48718
+ return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
48719
+ };
48720
+ //#endregion
48721
+ //#region src/cli/utils/should-render-hyperlinks.ts
48722
+ /**
48723
+ * Whether to emit OSC 8 clickable `file:line` locations for this run: a
48724
+ * hyperlink-capable terminal AND not a coding agent (whose output parsers
48725
+ * would choke on the escape sequences).
48726
+ */
48727
+ const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
48584
48728
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
48585
48729
  const FALSY_FLAG_VALUES = new Set([
48586
48730
  "",
@@ -48600,10 +48744,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
48600
48744
  };
48601
48745
  //#endregion
48602
48746
  //#region src/cli/utils/onboarding-state.ts
48603
- const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
48604
48747
  const ONBOARDED_AT_KEY = "onboardedAt";
48605
48748
  const getOnboardingStore = (options = {}) => new Conf({
48606
- projectName: GLOBAL_CONFIG_PROJECT_NAME$2,
48749
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
48607
48750
  cwd: options.cwd
48608
48751
  });
48609
48752
  const hasCompletedOnboarding = (options = {}) => {
@@ -49059,6 +49202,78 @@ const resolveCliCategories = (categoryFlag) => {
49059
49202
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
49060
49203
  };
49061
49204
  //#endregion
49205
+ //#region src/cli/utils/git-hook-shared.ts
49206
+ const HOOK_FILE_NAME = "pre-commit";
49207
+ const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49208
+ const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49209
+ const HUSKY_HOOKS_PATH = ".husky";
49210
+ const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49211
+ const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49212
+ const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49213
+ const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49214
+ const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49215
+ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49216
+ "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49217
+ `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49218
+ "rm -f \"$react_doctor_output\";",
49219
+ "else",
49220
+ "rm -f \"$react_doctor_output\";",
49221
+ `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;`,
49222
+ "fi"
49223
+ ].join(" ");
49224
+ const PACKAGE_JSON_FILE_NAME = "package.json";
49225
+ const runGit = (projectRoot, args) => {
49226
+ try {
49227
+ return execFileSync("git", [...args], {
49228
+ cwd: projectRoot,
49229
+ encoding: "utf8",
49230
+ stdio: [
49231
+ "ignore",
49232
+ "pipe",
49233
+ "ignore"
49234
+ ]
49235
+ }).trim();
49236
+ } catch {
49237
+ return null;
49238
+ }
49239
+ };
49240
+ const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
49241
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49242
+ const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
49243
+ const readPackageJson = (projectRoot) => {
49244
+ try {
49245
+ return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
49246
+ } catch {
49247
+ return null;
49248
+ }
49249
+ };
49250
+ const writeJsonFile$1 = (filePath, value) => {
49251
+ NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
49252
+ };
49253
+ const packageHasDependency = (projectRoot, dependencyName) => {
49254
+ const packageJson = readPackageJson(projectRoot);
49255
+ if (!isRecord$1(packageJson)) return false;
49256
+ return [
49257
+ "dependencies",
49258
+ "devDependencies",
49259
+ "optionalDependencies"
49260
+ ].some((fieldName) => {
49261
+ const dependencies = packageJson[fieldName];
49262
+ return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
49263
+ });
49264
+ };
49265
+ const packageHasRecordKey = (projectRoot, key) => {
49266
+ const packageJson = readPackageJson(projectRoot);
49267
+ return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
49268
+ };
49269
+ const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
49270
+ const packageJson = readPackageJson(projectRoot);
49271
+ if (!isRecord$1(packageJson)) return false;
49272
+ const value = packageJson[key];
49273
+ return isRecord$1(value) && isRecord$1(value[nestedKey]);
49274
+ };
49275
+ const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
49276
+ //#endregion
49062
49277
  //#region src/cli/utils/scan-result-cache.ts
49063
49278
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
49064
49279
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -49069,7 +49284,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
49069
49284
  "eslint-plugin-react-hooks/package.json"
49070
49285
  ];
49071
49286
  const bundledRequire = createRequire(import.meta.url);
49072
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49287
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49073
49288
  const normalizeForStableJson = (value) => {
49074
49289
  if (value === null) return null;
49075
49290
  if (value === void 0) return void 0;
@@ -49098,24 +49313,9 @@ const stringifyStableJson = (value) => {
49098
49313
  }
49099
49314
  };
49100
49315
  const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
49101
- const runGit$1 = (directory, args) => {
49102
- try {
49103
- return execFileSync("git", [...args], {
49104
- cwd: directory,
49105
- encoding: "utf8",
49106
- stdio: [
49107
- "ignore",
49108
- "pipe",
49109
- "ignore"
49110
- ]
49111
- }).trim();
49112
- } catch {
49113
- return null;
49114
- }
49115
- };
49116
- const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
49316
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
49117
49317
  const isWorktreeClean = (projectDirectory) => {
49118
- const status = runGit$1(projectDirectory, [
49318
+ const status = runGit(projectDirectory, [
49119
49319
  "status",
49120
49320
  "--porcelain=v1",
49121
49321
  "--untracked-files=normal"
@@ -49123,7 +49323,7 @@ const isWorktreeClean = (projectDirectory) => {
49123
49323
  return status !== null && status.length === 0;
49124
49324
  };
49125
49325
  const hasHiddenTrackedFileState = (projectDirectory) => {
49126
- const output = runGit$1(projectDirectory, ["ls-files", "-v"]);
49326
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
49127
49327
  if (output === null) return true;
49128
49328
  return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
49129
49329
  };
@@ -49136,7 +49336,7 @@ const resolveCacheFilePath = (projectDirectory) => {
49136
49336
  const readPersistedCache = (cacheFilePath) => {
49137
49337
  try {
49138
49338
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
49139
- if (!isRecord$1(parsed) || parsed.version !== 1) return {
49339
+ if (!isRecord(parsed) || parsed.version !== 1) return {
49140
49340
  version: 1,
49141
49341
  entries: []
49142
49342
  };
@@ -49146,8 +49346,8 @@ const readPersistedCache = (cacheFilePath) => {
49146
49346
  };
49147
49347
  const entries = [];
49148
49348
  for (const entry of parsed.entries) {
49149
- if (!isRecord$1(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49150
- if (!isRecord$1(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49349
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
49350
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
49151
49351
  entries.push(entry);
49152
49352
  }
49153
49353
  return {
@@ -49679,6 +49879,7 @@ const finalizeAndRender = (input) => gen(function* () {
49679
49879
  }
49680
49880
  const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
49681
49881
  const pause = onboardingSectionPause(animateRender);
49882
+ const useHyperlinks = shouldRenderHyperlinks(process.stdout);
49682
49883
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
49683
49884
  const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
49684
49885
  if (printedDiagnostics.length === 0) {
@@ -49704,7 +49905,7 @@ const finalizeAndRender = (input) => gen(function* () {
49704
49905
  yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
49705
49906
  sectionPause: pause,
49706
49907
  animateCountUp: animateRender
49707
- });
49908
+ }, useHyperlinks);
49708
49909
  if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
49709
49910
  if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
49710
49911
  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.`));
@@ -49897,16 +50098,7 @@ const buildHandoffPayload = (input) => {
49897
50098
  try {
49898
50099
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
49899
50100
  } catch {}
49900
- const lines = [
49901
- "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:",
49902
- `- It's used by teams at ${CI_TRUST_COMPANIES}.`,
49903
- "- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
49904
- `- Setup guide: ${CI_URL}`,
49905
- "If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
49906
- "",
49907
- `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.`,
49908
- ""
49909
- ];
50101
+ 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.`, ""];
49910
50102
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
49911
50103
  const representative = ruleDiagnostics[0];
49912
50104
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -49967,78 +50159,6 @@ const detectAvailableAgents = async () => {
49967
50159
  return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
49968
50160
  };
49969
50161
  //#endregion
49970
- //#region src/cli/utils/git-hook-shared.ts
49971
- const HOOK_FILE_NAME = "pre-commit";
49972
- const HOOK_RELATIVE_PATH = "hooks/pre-commit";
49973
- const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
49974
- const HUSKY_HOOKS_PATH = ".husky";
49975
- const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
49976
- const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
49977
- const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
49978
- const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
49979
- const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
49980
- const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
49981
- "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
49982
- `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
49983
- "rm -f \"$react_doctor_output\";",
49984
- "else",
49985
- "rm -f \"$react_doctor_output\";",
49986
- `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;`,
49987
- "fi"
49988
- ].join(" ");
49989
- const PACKAGE_JSON_FILE_NAME = "package.json";
49990
- const runGit = (projectRoot, args) => {
49991
- try {
49992
- return execFileSync("git", [...args], {
49993
- cwd: projectRoot,
49994
- encoding: "utf8",
49995
- stdio: [
49996
- "ignore",
49997
- "pipe",
49998
- "ignore"
49999
- ]
50000
- }).trim();
50001
- } catch {
50002
- return null;
50003
- }
50004
- };
50005
- const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
50006
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
50007
- const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
50008
- const readPackageJson = (projectRoot) => {
50009
- try {
50010
- return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
50011
- } catch {
50012
- return null;
50013
- }
50014
- };
50015
- const writeJsonFile$1 = (filePath, value) => {
50016
- NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
50017
- };
50018
- const packageHasDependency = (projectRoot, dependencyName) => {
50019
- const packageJson = readPackageJson(projectRoot);
50020
- if (!isRecord(packageJson)) return false;
50021
- return [
50022
- "dependencies",
50023
- "devDependencies",
50024
- "optionalDependencies"
50025
- ].some((fieldName) => {
50026
- const dependencies = packageJson[fieldName];
50027
- return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
50028
- });
50029
- };
50030
- const packageHasRecordKey = (projectRoot, key) => {
50031
- const packageJson = readPackageJson(projectRoot);
50032
- return isRecord(packageJson) && isRecord(packageJson[key]);
50033
- };
50034
- const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
50035
- const packageJson = readPackageJson(projectRoot);
50036
- if (!isRecord(packageJson)) return false;
50037
- const value = packageJson[key];
50038
- return isRecord(value) && isRecord(value[nestedKey]);
50039
- };
50040
- const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
50041
- //#endregion
50042
50162
  //#region src/cli/utils/install-doctor-script.ts
50043
50163
  const DOCTOR_SCRIPT_NAME = "doctor";
50044
50164
  const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
@@ -50064,31 +50184,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
50064
50184
  };
50065
50185
  const hasDoctorScript = (projectRoot) => {
50066
50186
  const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
50067
- if (!isRecord(packageJson)) return false;
50187
+ if (!isRecord$1(packageJson)) return false;
50068
50188
  const scripts = packageJson.scripts;
50069
- if (!isRecord(scripts)) return false;
50189
+ if (!isRecord$1(scripts)) return false;
50070
50190
  return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
50071
50191
  };
50072
50192
  const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
50073
50193
  const dependencies = packageJson[fieldName];
50074
- return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50194
+ return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
50075
50195
  });
50076
50196
  const installDoctorScript = (options) => {
50077
50197
  const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
50078
50198
  const packageJsonPath = getPackageJsonPath(packageDirectory);
50079
50199
  const packageJson = readPackageJson(packageDirectory);
50080
- if (!isRecord(packageJson)) return {
50200
+ if (!isRecord$1(packageJson)) return {
50081
50201
  packageJsonPath,
50082
50202
  scriptStatus: "skipped",
50083
50203
  scriptReason: "missing-or-invalid-package-json"
50084
50204
  };
50085
50205
  const scripts = packageJson.scripts;
50086
50206
  const scriptTarget = (() => {
50087
- if (scripts !== void 0 && !isRecord(scripts)) return {
50207
+ if (scripts !== void 0 && !isRecord$1(scripts)) return {
50088
50208
  status: "skipped",
50089
50209
  reason: "invalid-scripts"
50090
50210
  };
50091
- const scriptRecord = isRecord(scripts) ? scripts : {};
50211
+ const scriptRecord = isRecord$1(scripts) ? scripts : {};
50092
50212
  if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
50093
50213
  scriptName: DOCTOR_SCRIPT_NAME,
50094
50214
  status: "existing"
@@ -50122,7 +50242,7 @@ const installDoctorScript = (options) => {
50122
50242
  if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
50123
50243
  ...packageJson,
50124
50244
  scripts: {
50125
- ...isRecord(scripts) ? scripts : {},
50245
+ ...isRecord$1(scripts) ? scripts : {},
50126
50246
  [scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
50127
50247
  }
50128
50248
  });
@@ -50276,38 +50396,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
50276
50396
  //#region src/cli/utils/hash-project-root.ts
50277
50397
  const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
50278
50398
  //#endregion
50279
- //#region src/cli/utils/action-upgrade-prompt.ts
50280
- const GLOBAL_CONFIG_PROJECT_NAME$1 = "react-doctor";
50281
- const getActionUpgradeStore = (options = {}) => new Conf({
50282
- projectName: GLOBAL_CONFIG_PROJECT_NAME$1,
50283
- cwd: options.cwd
50284
- });
50285
- const hasHandledActionUpgrade = (projectRoot, storeOptions = {}) => {
50286
- try {
50287
- const upgrades = getActionUpgradeStore(storeOptions).get("actionUpgrades", {});
50288
- return Boolean(upgrades[hashProjectRoot(projectRoot)]);
50289
- } catch {
50290
- return true;
50291
- }
50292
- };
50293
- const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
50294
- try {
50295
- const store = getActionUpgradeStore(storeOptions);
50296
- const upgrades = store.get("actionUpgrades", {});
50297
- store.set("actionUpgrades", {
50298
- ...upgrades,
50299
- [hashProjectRoot(projectRoot)]: {
50300
- rootDirectory: Path.resolve(projectRoot),
50301
- outcome,
50302
- at: (/* @__PURE__ */ new Date()).toISOString()
50399
+ //#region src/cli/utils/project-decision-store.ts
50400
+ const createProjectDecisionStore = (storeKey) => {
50401
+ const getStore = (options = {}) => new Conf({
50402
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
50403
+ cwd: options.cwd
50404
+ });
50405
+ return {
50406
+ getConfigPath: (options = {}) => getStore(options).path,
50407
+ hasHandled: (projectRoot, options = {}) => {
50408
+ try {
50409
+ return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
50410
+ } catch {
50411
+ return true;
50303
50412
  }
50304
- });
50305
- return true;
50306
- } catch {
50307
- return false;
50308
- }
50413
+ },
50414
+ record: (projectRoot, outcome, options = {}) => {
50415
+ try {
50416
+ const store = getStore(options);
50417
+ store.set(storeKey, {
50418
+ ...store.get(storeKey, {}),
50419
+ [hashProjectRoot(projectRoot)]: {
50420
+ rootDirectory: Path.resolve(projectRoot),
50421
+ outcome,
50422
+ at: (/* @__PURE__ */ new Date()).toISOString()
50423
+ }
50424
+ });
50425
+ return true;
50426
+ } catch {
50427
+ return false;
50428
+ }
50429
+ }
50430
+ };
50309
50431
  };
50310
50432
  //#endregion
50433
+ //#region src/cli/utils/action-upgrade-prompt.ts
50434
+ const store$1 = createProjectDecisionStore("actionUpgrades");
50435
+ store$1.getConfigPath;
50436
+ const hasHandledActionUpgrade = store$1.hasHandled;
50437
+ const recordActionUpgradeDecision = store$1.record;
50438
+ //#endregion
50439
+ //#region src/cli/utils/ci-prompt-decision.ts
50440
+ const store = createProjectDecisionStore("ciPrompts");
50441
+ store.getConfigPath;
50442
+ const hasHandledCiPrompt = store.hasHandled;
50443
+ const recordCiPromptDecision = store.record;
50444
+ //#endregion
50311
50445
  //#region src/cli/utils/open-url.ts
50312
50446
  const resolveOpenCommand = (url) => {
50313
50447
  if (process$1.platform === "darwin") return {
@@ -50936,13 +51070,13 @@ const installPackageJsonHook = (options, strategy) => {
50936
51070
  const packageJsonPath = getPackageJsonPath(options.projectRoot);
50937
51071
  const didHookExist = NFS.existsSync(packageJsonPath);
50938
51072
  const packageJson = readPackageJson(options.projectRoot);
50939
- const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
51073
+ const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
50940
51074
  const parentKeys = strategy.path.slice(0, -1);
50941
51075
  const leafKey = strategy.path[strategy.path.length - 1];
50942
51076
  let parent = nextPackageJson;
50943
51077
  for (const key of parentKeys) {
50944
51078
  const existing = parent[key];
50945
- const cloned = isRecord(existing) ? { ...existing } : {};
51079
+ const cloned = isRecord$1(existing) ? { ...existing } : {};
50946
51080
  parent[key] = cloned;
50947
51081
  parent = cloned;
50948
51082
  }
@@ -51113,7 +51247,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
51113
51247
  const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
51114
51248
  const isSimpleGitHooksProject = (projectRoot) => {
51115
51249
  const packageJson = readPackageJson(projectRoot);
51116
- return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51250
+ return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
51117
51251
  };
51118
51252
  const getLefthookConfigPath = (projectRoot) => {
51119
51253
  for (const fileName of LEFTHOOK_CONFIG_FILES) {
@@ -51279,7 +51413,7 @@ const detectPackageManager = (projectRoot) => {
51279
51413
  let currentDirectory = Path.resolve(projectRoot);
51280
51414
  while (true) {
51281
51415
  const packageJson = readPackageJson(currentDirectory);
51282
- if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
51416
+ if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
51283
51417
  const packageManagerName = packageJson.packageManager.split("@")[0];
51284
51418
  if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
51285
51419
  }
@@ -51355,12 +51489,12 @@ const isSupplyChainTrustError = (error) => {
51355
51489
  const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
51356
51490
  const installReactDoctorDependency = async (options) => {
51357
51491
  const packageJson = readPackageJson(options.projectRoot);
51358
- if (!isRecord(packageJson)) return {
51492
+ if (!isRecord$1(packageJson)) return {
51359
51493
  dependencyStatus: "skipped",
51360
51494
  dependencyReason: "missing-or-invalid-package-json"
51361
51495
  };
51362
51496
  if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
51363
- if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
51497
+ if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
51364
51498
  dependencyStatus: "skipped",
51365
51499
  dependencyReason: "invalid-dev-dependencies"
51366
51500
  };
@@ -51524,10 +51658,12 @@ const runInstallReactDoctor = async (options = {}) => {
51524
51658
  const existingWorkflow = readReactDoctorWorkflow(projectRoot);
51525
51659
  const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
51526
51660
  const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
51527
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || !skipPrompts && await askAddToGitHubActions(prompt) === "yes");
51661
+ const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
51662
+ const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
51528
51663
  const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
51529
51664
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
51530
51665
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
51666
+ if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
51531
51667
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
51532
51668
  type: "multiselect",
51533
51669
  name: "agents",
@@ -51774,18 +51910,24 @@ const handoffToAgent = async (input) => {
51774
51910
  if (!input.interactive || input.diagnostics.length === 0) return;
51775
51911
  cliLogger.break();
51776
51912
  const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
51777
- if (!isReactDoctorWorkflowInstalled(projectRootForCi)) {
51913
+ const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
51914
+ if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
51778
51915
  const ciOutcome = await askAddToGitHubActions();
51779
51916
  recordCount(METRIC.agentHandoff, 1, {
51780
51917
  outcome: `ci-${ciOutcome}`,
51781
51918
  diagnosticsCount: input.diagnostics.length
51782
51919
  });
51783
51920
  if (ciOutcome === "cancel") return;
51921
+ recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
51784
51922
  if (ciOutcome === "yes") {
51785
51923
  await setUpGitHubActions({ rootDirectory: input.rootDirectory });
51786
51924
  cliLogger.break();
51787
51925
  }
51788
- } else await maybeOfferActionUpgrade(projectRootForCi);
51926
+ } else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
51927
+ else recordCount(METRIC.agentHandoff, 1, {
51928
+ outcome: "ci-suppressed",
51929
+ diagnosticsCount: input.diagnostics.length
51930
+ });
51789
51931
  const { handoffTarget } = await prompts({
51790
51932
  type: "select",
51791
51933
  name: "handoffTarget",
@@ -52008,6 +52150,7 @@ const reportErrorToSentry = async (error) => {
52008
52150
  sampled: runTrace.sampled,
52009
52151
  sampleRand: Math.random()
52010
52152
  });
52153
+ recordRunTraceId(scope.getPropagationContext().traceId);
52011
52154
  return Sentry.captureException(error);
52012
52155
  });
52013
52156
  await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
@@ -52091,7 +52234,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
52091
52234
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
52092
52235
  if (displayDiagnostics.length > 0) {
52093
52236
  yield* log("");
52094
- yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
52237
+ yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
52095
52238
  }
52096
52239
  const lowestScoredScan = findLowestScoredScan(completedScans);
52097
52240
  const aggregateScore = lowestScoredScan?.result.score ?? null;
@@ -52129,9 +52272,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
52129
52272
  });
52130
52273
  //#endregion
52131
52274
  //#region src/cli/utils/prompt-install-setup.ts
52132
- const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
52133
52275
  const getSetupPromptStore = (options = {}) => new Conf({
52134
- projectName: GLOBAL_CONFIG_PROJECT_NAME,
52276
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
52135
52277
  cwd: options.cwd
52136
52278
  });
52137
52279
  const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
@@ -52142,6 +52284,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
52142
52284
  return false;
52143
52285
  }
52144
52286
  };
52287
+ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
52288
+ try {
52289
+ const store = getSetupPromptStore(storeOptions);
52290
+ const projects = store.get("projects", {});
52291
+ const projectKey = getSetupPromptProjectKey(projectRoot);
52292
+ store.set("projects", {
52293
+ ...projects,
52294
+ [projectKey]: {
52295
+ ...projects[projectKey] ?? {},
52296
+ rootDirectory: Path.resolve(projectRoot),
52297
+ setupPrompt: false
52298
+ }
52299
+ });
52300
+ return true;
52301
+ } catch {
52302
+ return false;
52303
+ }
52304
+ };
52145
52305
  const resolveInstallSetupProjectRoot = (options) => {
52146
52306
  if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
52147
52307
  const packageDirectories = /* @__PURE__ */ new Set();
@@ -52548,6 +52708,14 @@ const runExplain = async (fileLineArgument, context) => {
52548
52708
  const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
52549
52709
  const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
52550
52710
  cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
52711
+ const codeFrame = buildCodeFrame({
52712
+ filePath: diagnostic.filePath,
52713
+ line: diagnostic.line,
52714
+ column: diagnostic.column,
52715
+ endLine: diagnostic.endLine,
52716
+ rootDirectory: targetDirectory
52717
+ });
52718
+ if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
52551
52719
  if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
52552
52720
  if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
52553
52721
  cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
@@ -52597,6 +52765,10 @@ const validateModeFlags = (flags) => {
52597
52765
  if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
52598
52766
  if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
52599
52767
  if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
52768
+ if (flags.debug && (flags.score === false || flags.telemetry === false)) {
52769
+ const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
52770
+ throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
52771
+ }
52600
52772
  };
52601
52773
  //#endregion
52602
52774
  //#region src/cli/commands/inspect.ts
@@ -52944,11 +53116,13 @@ const inspectAction = async (directory, flags) => {
52944
53116
  })) {
52945
53117
  printAgentInstallHint();
52946
53118
  recordCount(METRIC.agentInstallHintShown, 1);
53119
+ disableSetupPrompt(setupProjectRoot);
52947
53120
  }
52948
53121
  }
52949
53122
  } catch (error) {
52950
53123
  const isUserError = isExpectedUserError(error);
52951
53124
  const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
53125
+ if (isDebugFlagEnabled()) await flushSentry();
52952
53126
  if (isJsonMode) {
52953
53127
  writeJsonErrorReport(error, sentryEventId);
52954
53128
  process.exitCode = 1;
@@ -53671,6 +53845,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
53671
53845
  return [...nodeArguments, "--help"];
53672
53846
  };
53673
53847
  //#endregion
53848
+ //#region src/cli/utils/print-debug-trace.ts
53849
+ /**
53850
+ * The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
53851
+ * Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
53852
+ * reporting") so users learn one habit for both paths. A `null` trace says why,
53853
+ * so `--debug` never silently does nothing.
53854
+ */
53855
+ const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
53856
+ /**
53857
+ * Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
53858
+ * maintainers can pull the full trace from a pasted id. Runs from the process
53859
+ * `exit` handler, so it's the last line on both the success path and the error
53860
+ * funnels (which `process.exit()` before the promise chain could resume).
53861
+ *
53862
+ * Writes straight to `process.stderr` (not `Console`) for three reasons: the
53863
+ * exit handler is synchronous, JSON mode patches the global console to no-ops —
53864
+ * a diagnostic the user explicitly asked for must survive that — and stderr
53865
+ * keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
53866
+ * a diagnostic must never throw out of an exit handler.
53867
+ */
53868
+ const printDebugTrace = () => {
53869
+ if (!Sentry.isInitialized()) return;
53870
+ try {
53871
+ process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
53872
+ } catch {}
53873
+ };
53874
+ //#endregion
53674
53875
  //#region src/cli/utils/removed-cli-flags.ts
53675
53876
  const REMOVED_FLAGS = new Map([
53676
53877
  ["--full", "use `--diff false` to force a full scan"],
@@ -53697,6 +53898,7 @@ const ROOT_FLAG_SPEC = {
53697
53898
  longOptionsWithoutValues: new Set([
53698
53899
  "--color",
53699
53900
  "--dead-code",
53901
+ "--debug",
53700
53902
  "--help",
53701
53903
  "--json",
53702
53904
  "--json-compact",
@@ -53864,6 +54066,9 @@ const stripUnknownCliFlags = (argv) => {
53864
54066
  initializeSentry();
53865
54067
  process.on("SIGINT", exitGracefully);
53866
54068
  process.on("SIGTERM", exitGracefully);
54069
+ process.on("exit", () => {
54070
+ if (isDebugFlagEnabled()) printDebugTrace();
54071
+ });
53867
54072
  unrefStdin();
53868
54073
  guardStdin();
53869
54074
  const formatExampleLines = (examples) => {
@@ -53908,7 +54113,7 @@ ${highlighter.dim("Learn more:")}
53908
54113
  ${highlighter.info(CANONICAL_GITHUB_URL)}
53909
54114
  `;
53910
54115
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
53911
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
54116
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
53912
54117
  program.action(inspectAction);
53913
54118
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
53914
54119
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -53951,4 +54156,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
53951
54156
  export {};
53952
54157
 
53953
54158
  //# sourceMappingURL=cli.js.map
53954
- //# debugId=54905d6a-f06f-5f80-9fb6-7d4ab857d553
54159
+ //# debugId=f029f05b-4c71-52d3-b523-0834d67de2d4