react-doctor 0.5.8-dev.80e3093 → 0.5.8-dev.8232e96

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]="38109950-d718-564f-8c23-83e407ef77e9")}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]="56ca0cbd-9f70-5a50-8496-43abdf078431")}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";
@@ -22465,6 +22465,15 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
22465
22465
  ok: Boolean$1,
22466
22466
  directory: String$1,
22467
22467
  mode: JsonReportMode,
22468
+ /**
22469
+ * True when a `changed` (compare) run was intended but the base couldn't be
22470
+ * resolved — a shallow CI checkout with no merge base, or a failed base/head
22471
+ * lint. The report then lists every finding in the changed files (mode
22472
+ * downgrades to `diff`, the `baseline` block is dropped, the CI gate is
22473
+ * skipped) rather than only the ones the change introduced. Absent on a
22474
+ * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
22475
+ */
22476
+ baselineDegraded: optional(Boolean$1),
22468
22477
  diff: NullOr(JsonReportDiffInfo),
22469
22478
  projects: ArraySchema(JsonReportProjectEntry),
22470
22479
  diagnostics: ArraySchema(Diagnostic),
@@ -35948,12 +35957,31 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
35948
35957
  const MINIFIED_SNIFF_BYTES = 65536;
35949
35958
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
35950
35959
  const IGNORED_DIRECTORIES = new Set([
35960
+ ".angular",
35961
+ ".astro",
35962
+ ".cache",
35963
+ ".contentlayer",
35964
+ ".direnv",
35965
+ ".docusaurus",
35966
+ ".expo",
35967
+ ".firebase",
35951
35968
  ".git",
35969
+ ".gradle",
35970
+ ".hg",
35952
35971
  ".next",
35953
35972
  ".nuxt",
35973
+ ".nx",
35954
35974
  ".output",
35975
+ ".parcel-cache",
35976
+ ".serverless",
35955
35977
  ".svelte-kit",
35978
+ ".svn",
35979
+ ".terraform",
35956
35980
  ".turbo",
35981
+ ".venv",
35982
+ ".vercel",
35983
+ ".wrangler",
35984
+ ".yarn",
35957
35985
  "build",
35958
35986
  "coverage",
35959
35987
  "dist",
@@ -35961,6 +35989,152 @@ const IGNORED_DIRECTORIES = new Set([
35961
35989
  "out",
35962
35990
  "storybook-static"
35963
35991
  ]);
35992
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
35993
+ const MILLISECONDS_PER_SECOND = 1e3;
35994
+ const SCORE_API_URL = "https://www.react.doctor/api/score";
35995
+ const ENTERPRISE_CONTACT_URL = "https://react.doctor/enterprise";
35996
+ const SHARE_BASE_URL = "https://react.doctor/share";
35997
+ const CI_URL = "https://react.doctor/ci";
35998
+ const GITHUB_ACTIONS_SETUP_URL = "https://www.react.doctor/docs/ci-and-prs/github-actions-setup";
35999
+ const DOCS_URL = "https://react.doctor/docs";
36000
+ const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
36001
+ const FETCH_TIMEOUT_MS = 1e4;
36002
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
36003
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
36004
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
36005
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
36006
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
36007
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
36008
+ const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
36009
+ const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
36010
+ const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
36011
+ const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
36012
+ const ES_TARGET_YEAR_BY_NAME = {
36013
+ es3: 1999,
36014
+ es5: 2009,
36015
+ es6: 2015,
36016
+ es2015: 2015,
36017
+ es2016: 2016,
36018
+ es2017: 2017,
36019
+ es2018: 2018,
36020
+ es2019: 2019,
36021
+ es2020: 2020,
36022
+ es2021: 2021,
36023
+ es2022: 2022,
36024
+ es2023: 2023,
36025
+ es2024: 2024,
36026
+ es2025: 2025,
36027
+ esnext: 9999
36028
+ };
36029
+ /**
36030
+ * tsconfig filenames probed when resolving a project's TypeScript
36031
+ * compiler options — the root config first, then a monorepo base config.
36032
+ */
36033
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
36034
+ /**
36035
+ * Project-config files that `StagedFiles.materialize` copies into
36036
+ * the temp directory alongside staged sources so oxlint resolves
36037
+ * `tsconfig` / `package.json` / lint configs the same way it would
36038
+ * in the working tree. Hoisted out of the staged-files helper so
36039
+ * the constant lives next to the rest of the IO budget knobs.
36040
+ */
36041
+ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
36042
+ "tsconfig.json",
36043
+ "tsconfig.base.json",
36044
+ "package.json",
36045
+ "doctor.config.ts",
36046
+ "doctor.config.mts",
36047
+ "doctor.config.cts",
36048
+ "doctor.config.js",
36049
+ "doctor.config.mjs",
36050
+ "doctor.config.cjs",
36051
+ "doctor.config.json",
36052
+ "doctor.config.jsonc",
36053
+ "oxlint.json",
36054
+ ".oxlintrc.json"
36055
+ ];
36056
+ const CONFIG_FINGERPRINT_FILENAMES = [
36057
+ "doctor.config.ts",
36058
+ "doctor.config.mts",
36059
+ "doctor.config.cts",
36060
+ "doctor.config.js",
36061
+ "doctor.config.mjs",
36062
+ "doctor.config.cjs",
36063
+ "doctor.config.json",
36064
+ "doctor.config.jsonc",
36065
+ "react-doctor.config.json",
36066
+ "package.json",
36067
+ "pnpm-workspace.yaml",
36068
+ "tsconfig.json",
36069
+ "tsconfig.base.json",
36070
+ ".oxlintrc.json",
36071
+ ".eslintrc.json",
36072
+ "pnpm-lock.yaml",
36073
+ "package-lock.json",
36074
+ "yarn.lock",
36075
+ "bun.lock",
36076
+ "bun.lockb",
36077
+ ".gitignore"
36078
+ ];
36079
+ const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
36080
+ const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
36081
+ const SKILL_NAME = "react-doctor";
36082
+ const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
36083
+ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
36084
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
36085
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
36086
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
36087
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36088
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
36089
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
36090
+ const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
36091
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
36092
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
36093
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
36094
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
36095
+ const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
36096
+ const REACT_SERVER_DOM_PACKAGES = [
36097
+ "react-server-dom-webpack",
36098
+ "react-server-dom-parcel",
36099
+ "react-server-dom-turbopack"
36100
+ ];
36101
+ const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
36102
+ const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
36103
+ const DIAGNOSTIC_CATEGORY_BUCKETS = [
36104
+ "Security",
36105
+ "Bugs",
36106
+ "Performance",
36107
+ "Accessibility",
36108
+ "Maintainability"
36109
+ ];
36110
+ const APP_ONLY_RULE_KEYS = new Set([
36111
+ "react-hooks-js/static-components",
36112
+ "react-doctor/no-render-prop-children",
36113
+ "react-doctor/prefer-explicit-variants"
36114
+ ]);
36115
+ const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
36116
+ const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
36117
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
36118
+ "react-doctor/no-derived-state",
36119
+ "react-doctor/no-derived-state-effect",
36120
+ "react-doctor/no-derived-useState",
36121
+ "react-doctor/no-adjust-state-on-prop-change",
36122
+ "react-doctor/no-reset-all-state-on-prop-change"
36123
+ ]);
36124
+ const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
36125
+ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
36126
+ const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
36127
+ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36128
+ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36129
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36130
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36131
+ const SUPPLY_CHAIN_PLUGIN = "socket";
36132
+ const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36133
+ const SUPPLY_CHAIN_CATEGORY = "Security";
36134
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36135
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36136
+ const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36137
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
35964
36138
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
35965
36139
  const isMinifiedSource = (absolutePath) => {
35966
36140
  let fileDescriptor;
@@ -36014,23 +36188,38 @@ const readDirectoryEntries = (directoryPath) => {
36014
36188
  throw error;
36015
36189
  }
36016
36190
  };
36017
- const countSourceFilesViaFilesystem = (rootDirectory) => {
36018
- let count = 0;
36191
+ function* walkSourceTreeFiles(rootDirectory) {
36019
36192
  const stack = [rootDirectory];
36020
36193
  while (stack.length > 0) {
36021
36194
  const currentDirectory = stack.pop();
36022
- const entries = readDirectoryEntries(currentDirectory);
36023
- for (const entry of entries) {
36195
+ if (currentDirectory === void 0) continue;
36196
+ for (const entry of readDirectoryEntries(currentDirectory)) {
36197
+ const absolutePath = Path.join(currentDirectory, entry.name);
36024
36198
  if (entry.isDirectory()) {
36025
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(Path.join(currentDirectory, entry.name));
36199
+ if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
36026
36200
  continue;
36027
36201
  }
36028
- if (entry.isFile() && isLintableSourceFile(entry.name) && !isLargeMinifiedFile(Path.join(currentDirectory, entry.name))) count++;
36202
+ if (entry.isFile()) yield {
36203
+ absolutePath,
36204
+ name: entry.name
36205
+ };
36029
36206
  }
36030
36207
  }
36031
- return count;
36208
+ }
36209
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
36210
+ const entries = [];
36211
+ for (const relativePath of relativePaths) {
36212
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
36213
+ const sizeBytes = statSourceFileSize(absolutePath);
36214
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
36215
+ entries.push({
36216
+ path: relativePath,
36217
+ sizeBytes: sizeBytes ?? 0
36218
+ });
36219
+ }
36220
+ return entries;
36032
36221
  };
36033
- const countSourceFilesViaGit = (rootDirectory) => {
36222
+ const listSourceFilesViaGit = (rootDirectory) => {
36034
36223
  const result = spawnSync("git", [
36035
36224
  "ls-files",
36036
36225
  "-z",
@@ -36043,9 +36232,16 @@ const countSourceFilesViaGit = (rootDirectory) => {
36043
36232
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
36044
36233
  });
36045
36234
  if (result.error || result.status !== 0) return null;
36046
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !isLargeMinifiedFile(Path.resolve(rootDirectory, filePath))).length;
36235
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
36236
+ };
36237
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
36238
+ const filePaths = [];
36239
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
36240
+ return filePaths;
36047
36241
  };
36048
- const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
36242
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36243
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36244
+ const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36049
36245
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36050
36246
  const readPackageJsonUncached = (packageJsonPath) => {
36051
36247
  try {
@@ -36851,151 +37047,6 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
36851
37047
  if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
36852
37048
  return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
36853
37049
  };
36854
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
36855
- const MILLISECONDS_PER_SECOND = 1e3;
36856
- const SCORE_API_URL = "https://www.react.doctor/api/score";
36857
- const ENTERPRISE_CONTACT_URL = "https://react.doctor/enterprise";
36858
- const SHARE_BASE_URL = "https://react.doctor/share";
36859
- const CI_URL = "https://react.doctor/ci";
36860
- const GITHUB_ACTIONS_SETUP_URL = "https://www.react.doctor/docs/ci-and-prs/github-actions-setup";
36861
- const DOCS_URL = "https://react.doctor/docs";
36862
- const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
36863
- const FETCH_TIMEOUT_MS = 1e4;
36864
- const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
36865
- const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
36866
- const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
36867
- const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
36868
- const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
36869
- const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
36870
- const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
36871
- const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
36872
- const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
36873
- const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
36874
- const ES_TARGET_YEAR_BY_NAME = {
36875
- es3: 1999,
36876
- es5: 2009,
36877
- es6: 2015,
36878
- es2015: 2015,
36879
- es2016: 2016,
36880
- es2017: 2017,
36881
- es2018: 2018,
36882
- es2019: 2019,
36883
- es2020: 2020,
36884
- es2021: 2021,
36885
- es2022: 2022,
36886
- es2023: 2023,
36887
- es2024: 2024,
36888
- es2025: 2025,
36889
- esnext: 9999
36890
- };
36891
- /**
36892
- * tsconfig filenames probed when resolving a project's TypeScript
36893
- * compiler options — the root config first, then a monorepo base config.
36894
- */
36895
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
36896
- /**
36897
- * Project-config files that `StagedFiles.materialize` copies into
36898
- * the temp directory alongside staged sources so oxlint resolves
36899
- * `tsconfig` / `package.json` / lint configs the same way it would
36900
- * in the working tree. Hoisted out of the staged-files helper so
36901
- * the constant lives next to the rest of the IO budget knobs.
36902
- */
36903
- const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
36904
- "tsconfig.json",
36905
- "tsconfig.base.json",
36906
- "package.json",
36907
- "doctor.config.ts",
36908
- "doctor.config.mts",
36909
- "doctor.config.cts",
36910
- "doctor.config.js",
36911
- "doctor.config.mjs",
36912
- "doctor.config.cjs",
36913
- "doctor.config.json",
36914
- "doctor.config.jsonc",
36915
- "oxlint.json",
36916
- ".oxlintrc.json"
36917
- ];
36918
- const CONFIG_FINGERPRINT_FILENAMES = [
36919
- "doctor.config.ts",
36920
- "doctor.config.mts",
36921
- "doctor.config.cts",
36922
- "doctor.config.js",
36923
- "doctor.config.mjs",
36924
- "doctor.config.cjs",
36925
- "doctor.config.json",
36926
- "doctor.config.jsonc",
36927
- "react-doctor.config.json",
36928
- "package.json",
36929
- "pnpm-workspace.yaml",
36930
- "tsconfig.json",
36931
- "tsconfig.base.json",
36932
- ".oxlintrc.json",
36933
- ".eslintrc.json",
36934
- "pnpm-lock.yaml",
36935
- "package-lock.json",
36936
- "yarn.lock",
36937
- "bun.lock",
36938
- "bun.lockb",
36939
- ".gitignore"
36940
- ];
36941
- const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
36942
- const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
36943
- const SKILL_NAME = "react-doctor";
36944
- const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
36945
- const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
36946
- const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
36947
- const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
36948
- const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
36949
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36950
- const LINT_PHASE_TIMEOUT_MS = 3e5;
36951
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
36952
- const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
36953
- const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
36954
- const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
36955
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
36956
- const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
36957
- const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
36958
- const REACT_SERVER_DOM_PACKAGES = [
36959
- "react-server-dom-webpack",
36960
- "react-server-dom-parcel",
36961
- "react-server-dom-turbopack"
36962
- ];
36963
- const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
36964
- const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
36965
- const DIAGNOSTIC_CATEGORY_BUCKETS = [
36966
- "Security",
36967
- "Bugs",
36968
- "Performance",
36969
- "Accessibility",
36970
- "Maintainability"
36971
- ];
36972
- const APP_ONLY_RULE_KEYS = new Set([
36973
- "react-hooks-js/static-components",
36974
- "react-doctor/no-render-prop-children",
36975
- "react-doctor/prefer-explicit-variants"
36976
- ]);
36977
- const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
36978
- const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
36979
- const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
36980
- "react-doctor/no-derived-state",
36981
- "react-doctor/no-derived-state-effect",
36982
- "react-doctor/no-derived-useState",
36983
- "react-doctor/no-adjust-state-on-prop-change",
36984
- "react-doctor/no-reset-all-state-on-prop-change"
36985
- ]);
36986
- const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
36987
- const CONFIG_CACHE_TTL_MS = 300 * 1e3;
36988
- const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
36989
- const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36990
- const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36991
- const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36992
- const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36993
- const SUPPLY_CHAIN_PLUGIN = "socket";
36994
- const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36995
- const SUPPLY_CHAIN_CATEGORY = "Security";
36996
- const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36997
- const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36998
- const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36999
37050
  const TSCONFIG_FILENAME = "tsconfig.json";
37000
37051
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
37001
37052
  const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
@@ -39975,14 +40026,17 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
39975
40026
  return diagnostics;
39976
40027
  };
39977
40028
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
39978
- const REDUCED_MOTION_FILE_GLOBS = [
39979
- "*.ts",
39980
- "*.tsx",
39981
- "*.js",
39982
- "*.jsx",
39983
- "*.css",
39984
- "*.scss"
39985
- ];
40029
+ const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
40030
+ const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
40031
+ ".ts",
40032
+ ".tsx",
40033
+ ".js",
40034
+ ".jsx",
40035
+ ".css",
40036
+ ".scss"
40037
+ ]);
40038
+ const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
40039
+ const GIT_GREP_NO_MATCH_STATUS = 1;
39986
40040
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
39987
40041
  filePath: "package.json",
39988
40042
  plugin: "react-doctor",
@@ -39994,6 +40048,19 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
39994
40048
  column: 0,
39995
40049
  category: "Accessibility"
39996
40050
  };
40051
+ const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
40052
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40053
+ if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
40054
+ let content;
40055
+ try {
40056
+ content = NFS.readFileSync(absolutePath, "utf-8");
40057
+ } catch {
40058
+ continue;
40059
+ }
40060
+ if (REDUCED_MOTION_PATTERN.test(content)) return true;
40061
+ }
40062
+ return false;
40063
+ };
39997
40064
  const checkReducedMotion = (rootDirectory) => {
39998
40065
  const packageJsonPath = Path.join(rootDirectory, "package.json");
39999
40066
  if (!isFile(packageJsonPath)) return [];
@@ -40012,22 +40079,21 @@ const checkReducedMotion = (rootDirectory) => {
40012
40079
  const result = spawnSync("git", [
40013
40080
  "grep",
40014
40081
  "--untracked",
40015
- "-ql",
40016
- "-E",
40082
+ "-lE",
40017
40083
  REDUCED_MOTION_GREP_PATTERN,
40018
40084
  "--",
40019
40085
  ...REDUCED_MOTION_FILE_GLOBS
40020
40086
  ], {
40021
40087
  cwd: rootDirectory,
40088
+ encoding: "utf-8",
40022
40089
  stdio: [
40023
40090
  "ignore",
40024
40091
  "pipe",
40025
40092
  "pipe"
40026
40093
  ]
40027
40094
  });
40028
- if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40029
- if (result.status === 0) return [];
40030
- return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40095
+ if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40096
+ return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40031
40097
  };
40032
40098
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
40033
40099
  filePath: relativePath,
@@ -40411,7 +40477,7 @@ const collectIgnorePatterns = (rootDirectory) => {
40411
40477
  cachedPatternsByRoot.set(rootDirectory, patterns);
40412
40478
  return patterns;
40413
40479
  };
40414
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40480
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40415
40481
  const KNIP_JSON_FILENAME = "knip.json";
40416
40482
  const readJsonFileSafe = (filePath) => {
40417
40483
  let rawContents;
@@ -40428,10 +40494,10 @@ const readJsonFileSafe = (filePath) => {
40428
40494
  };
40429
40495
  const readKnipConfig = (rootDirectory) => {
40430
40496
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
40431
- if (isRecord$2(knipJson)) return knipJson;
40497
+ if (isRecord$1(knipJson)) return knipJson;
40432
40498
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
40433
- const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
40434
- return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
40499
+ const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
40500
+ return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
40435
40501
  };
40436
40502
  const normalizePatternList = (value) => {
40437
40503
  if (typeof value === "string" && value.length > 0) return [value];
@@ -40443,10 +40509,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
40443
40509
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
40444
40510
  };
40445
40511
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
40446
- if (!isRecord$2(workspaces)) return [];
40512
+ if (!isRecord$1(workspaces)) return [];
40447
40513
  const patterns = [];
40448
40514
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
40449
- if (!isRecord$2(workspaceConfig)) continue;
40515
+ if (!isRecord$1(workspaceConfig)) continue;
40450
40516
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
40451
40517
  }
40452
40518
  return patterns;
@@ -40669,7 +40735,7 @@ const parseUnusedFiles = (value) => {
40669
40735
  const values = parseArray(value, "unusedFiles");
40670
40736
  const unusedFiles = [];
40671
40737
  for (const [index, entry] of values.entries()) {
40672
- if (!isRecord$2(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
40738
+ if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
40673
40739
  unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
40674
40740
  }
40675
40741
  return unusedFiles;
@@ -40678,7 +40744,7 @@ const parseUnusedExports = (value) => {
40678
40744
  const values = parseArray(value, "unusedExports");
40679
40745
  const unusedExports = [];
40680
40746
  for (const [index, entry] of values.entries()) {
40681
- if (!isRecord$2(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
40747
+ if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
40682
40748
  unusedExports.push({
40683
40749
  path: parseString(entry.path, `unusedExports[${index}].path`),
40684
40750
  name: parseString(entry.name, `unusedExports[${index}].name`),
@@ -40693,7 +40759,7 @@ const parseUnusedDependencies = (value) => {
40693
40759
  const values = parseArray(value, "unusedDependencies");
40694
40760
  const unusedDependencies = [];
40695
40761
  for (const [index, entry] of values.entries()) {
40696
- if (!isRecord$2(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
40762
+ if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
40697
40763
  unusedDependencies.push({
40698
40764
  name: parseString(entry.name, `unusedDependencies[${index}].name`),
40699
40765
  isDevDependency: parseBoolean$1(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
@@ -40705,13 +40771,13 @@ const parseCircularDependencies = (value) => {
40705
40771
  const values = parseArray(value, "circularDependencies");
40706
40772
  const circularDependencies = [];
40707
40773
  for (const [index, entry] of values.entries()) {
40708
- if (!isRecord$2(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
40774
+ if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
40709
40775
  circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
40710
40776
  }
40711
40777
  return circularDependencies;
40712
40778
  };
40713
40779
  const parseDeadCodeWorkerResult = (value) => {
40714
- if (!isRecord$2(value)) throw new Error("Dead-code worker returned an invalid result.");
40780
+ if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
40715
40781
  return {
40716
40782
  unusedFiles: parseUnusedFiles(value.unusedFiles),
40717
40783
  unusedExports: parseUnusedExports(value.unusedExports),
@@ -40720,7 +40786,7 @@ const parseDeadCodeWorkerResult = (value) => {
40720
40786
  };
40721
40787
  };
40722
40788
  const parseDeadCodeWorkerError = (value) => {
40723
- if (!isRecord$2(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
40789
+ if (!isRecord$1(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
40724
40790
  return {
40725
40791
  ...typeof value.name === "string" ? { name: value.name } : {},
40726
40792
  message: value.message,
@@ -40728,7 +40794,7 @@ const parseDeadCodeWorkerError = (value) => {
40728
40794
  };
40729
40795
  };
40730
40796
  const parseDeadCodeWorkerMessage = (value) => {
40731
- if (!isRecord$2(value)) throw new Error("Dead-code worker returned an invalid message.");
40797
+ if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid message.");
40732
40798
  if (value.ok === true) return {
40733
40799
  ok: true,
40734
40800
  result: value.result
@@ -40939,6 +41005,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
40939
41005
  return true;
40940
41006
  };
40941
41007
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
41008
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
40942
41009
  /**
40943
41010
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
40944
41011
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -40957,53 +41024,6 @@ const resolveDeadCodeTimeout = (input) => {
40957
41024
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
40958
41025
  };
40959
41026
  };
40960
- const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
40961
- const entries = [];
40962
- for (const relativePath of relativePaths) {
40963
- const absolutePath = Path.resolve(rootDirectory, relativePath);
40964
- const sizeBytes = statSourceFileSize(absolutePath);
40965
- if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
40966
- entries.push({
40967
- path: relativePath,
40968
- sizeBytes: sizeBytes ?? 0
40969
- });
40970
- }
40971
- return entries;
40972
- };
40973
- const listSourceFilesViaGit = (rootDirectory) => {
40974
- const result = spawnSync("git", [
40975
- "ls-files",
40976
- "-z",
40977
- "--cached",
40978
- "--others",
40979
- "--exclude-standard"
40980
- ], {
40981
- cwd: rootDirectory,
40982
- encoding: "utf-8",
40983
- maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
40984
- });
40985
- if (result.error || result.status !== 0) return null;
40986
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
40987
- };
40988
- const listSourceFilesViaFilesystem = (rootDirectory) => {
40989
- const filePaths = [];
40990
- const stack = [rootDirectory];
40991
- while (stack.length > 0) {
40992
- const currentDirectory = stack.pop();
40993
- const entries = readDirectoryEntries(currentDirectory);
40994
- for (const entry of entries) {
40995
- const absolutePath = Path.join(currentDirectory, entry.name);
40996
- if (entry.isDirectory()) {
40997
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
40998
- continue;
40999
- }
41000
- if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
41001
- }
41002
- }
41003
- return filePaths;
41004
- };
41005
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
41006
- const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
41007
41027
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
41008
41028
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
41009
41029
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -41700,12 +41720,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
41700
41720
  return null;
41701
41721
  }
41702
41722
  if (grepResult === null) return null;
41703
- return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
41723
+ return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
41704
41724
  };
41705
41725
  const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
41706
41726
  const matches = [];
41707
41727
  const checkFile = (relativePath) => {
41708
- if (!isLintableSourceFile(relativePath)) return;
41728
+ if (!isLintableSourceFile(relativePath) || hasIgnoredPathSegment(relativePath)) return;
41709
41729
  const absolutePath = Path.join(rootDirectory, relativePath);
41710
41730
  let content;
41711
41731
  try {
@@ -41719,22 +41739,7 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
41719
41739
  for (const candidate of includePaths) checkFile(candidate);
41720
41740
  return matches;
41721
41741
  }
41722
- const stack = [rootDirectory];
41723
- while (stack.length > 0) {
41724
- const current = stack.pop();
41725
- if (current === void 0) continue;
41726
- const entries = readDirectoryEntries(current);
41727
- for (const entry of entries) {
41728
- if (entry.isDirectory()) {
41729
- if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
41730
- stack.push(Path.join(current, entry.name));
41731
- continue;
41732
- }
41733
- if (!entry.isFile()) continue;
41734
- const absolute = Path.join(current, entry.name);
41735
- checkFile(Path.relative(rootDirectory, absolute));
41736
- }
41737
- }
41742
+ for (const { absolutePath } of walkSourceTreeFiles(rootDirectory)) checkFile(Path.relative(rootDirectory, absolutePath));
41738
41743
  return matches;
41739
41744
  };
41740
41745
  const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
@@ -41951,12 +41956,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
41951
41956
  if (customRulesOnly && registryEntry.originallyExternal) continue;
41952
41957
  if (rule.framework !== "global" && !rule.requires) continue;
41953
41958
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
41954
- const explicitSeverity = resolveRuleSeverityOverride({
41959
+ const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
41960
+ if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
41961
+ const severity = resolveRuleSeverityOverride({
41955
41962
  ruleKey: registryEntry.key,
41956
41963
  category: rule.category
41957
- }, severityControls);
41958
- if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
41959
- const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
41964
+ }, severityControls) ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
41960
41965
  if (severity === "off") continue;
41961
41966
  enabledReactDoctorRules[registryEntry.key] = severity;
41962
41967
  }
@@ -42025,9 +42030,9 @@ const emptyCache = () => ({
42025
42030
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42026
42031
  const entries = /* @__PURE__ */ new Map();
42027
42032
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42028
- if (persisted.version !== 1 || !isRecord$2(persisted.rulesets)) return entries;
42033
+ if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42029
42034
  const bucket = persisted.rulesets[rulesetHash];
42030
- if (!isRecord$2(bucket) || !isRecord$2(bucket.files)) return entries;
42035
+ if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42031
42036
  for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
42032
42037
  const decoded = decodeFileDiagnostics(rawDiagnostics);
42033
42038
  if (decoded !== null) entries.set(fileKey, decoded);
@@ -42045,9 +42050,9 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42045
42050
  },
42046
42051
  persist: () => {
42047
42052
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42048
- const rulesets = onDisk.version === 1 && isRecord$2(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42053
+ const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42049
42054
  const existingBucket = rulesets[rulesetHash];
42050
- const existingFiles = isRecord$2(existingBucket) && isRecord$2(existingBucket.files) ? existingBucket.files : {};
42055
+ const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42051
42056
  const ourFiles = {};
42052
42057
  for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
42053
42058
  const cappedEntries = Object.entries({
@@ -42060,7 +42065,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42060
42065
  };
42061
42066
  atomicWriteJson(cacheFilePath, {
42062
42067
  version: 1,
42063
- rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$2(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42068
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42064
42069
  });
42065
42070
  }
42066
42071
  };
@@ -42938,21 +42943,33 @@ const isParallelismRelatedSpawnError = (error) => {
42938
42943
  * loop with a slimmer config in that case.
42939
42944
  */
42940
42945
  const spawnLintBatches = async (input) => {
42941
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
42946
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
42942
42947
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
42943
42948
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
42944
42949
  const runBatchPass = async (concurrency) => {
42945
42950
  const allDiagnostics = [];
42946
42951
  const droppedFiles = [];
42952
+ const deadlineSkippedFiles = [];
42953
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
42947
42954
  let firstDropReason = null;
42948
- const spawnLintBatch = async (batch, depth, splitBudget) => {
42955
+ const spawnLintBatch = async (batch, depth, batchState) => {
42956
+ if (isPastDeadline()) {
42957
+ deadlineSkippedFiles.push(...batch);
42958
+ batchState.deadlineSkippedFileCount += batch.length;
42959
+ return [];
42960
+ }
42949
42961
  const batchArgs = [...baseArgs, ...batch];
42950
42962
  try {
42951
42963
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
42952
42964
  } catch (error) {
42953
42965
  if (!isSplittableReactDoctorError(error)) throw error;
42954
- splitBudget.deadlineMs ??= Date.now() + splitTotalBudgetMs;
42955
- const isBudgetElapsed = Date.now() >= splitBudget.deadlineMs;
42966
+ if (isPastDeadline()) {
42967
+ deadlineSkippedFiles.push(...batch);
42968
+ batchState.deadlineSkippedFileCount += batch.length;
42969
+ return [];
42970
+ }
42971
+ batchState.deadlineMs ??= Date.now() + splitTotalBudgetMs;
42972
+ const isBudgetElapsed = Date.now() >= batchState.deadlineMs;
42956
42973
  const isDepthCapReached = depth >= splitMaxDepth;
42957
42974
  if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
42958
42975
  droppedFiles.push(...batch);
@@ -42965,7 +42982,7 @@ const spawnLintBatches = async (input) => {
42965
42982
  return [];
42966
42983
  }
42967
42984
  const splitIndex = Math.ceil(batch.length / 2);
42968
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, splitBudget), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, splitBudget)];
42985
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, batchState), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, batchState)];
42969
42986
  }
42970
42987
  };
42971
42988
  let startedFileCount = 0;
@@ -42981,9 +42998,17 @@ const spawnLintBatches = async (input) => {
42981
42998
  progressTimer?.unref?.();
42982
42999
  try {
42983
43000
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
43001
+ if (isPastDeadline()) {
43002
+ deadlineSkippedFiles.push(...batch);
43003
+ return [];
43004
+ }
42984
43005
  startedFileCount += batch.length;
42985
- const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
42986
- scannedFileCount += batch.length;
43006
+ const batchState = {
43007
+ deadlineMs: null,
43008
+ deadlineSkippedFileCount: 0
43009
+ };
43010
+ const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
43011
+ scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
42987
43012
  if (onFileProgress) {
42988
43013
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
42989
43014
  onFileProgress(displayedFileCount, totalFileCount);
@@ -42994,12 +43019,13 @@ const spawnLintBatches = async (input) => {
42994
43019
  } finally {
42995
43020
  if (progressTimer !== null) clearInterval(progressTimer);
42996
43021
  }
42997
- if (droppedFiles.length > 0 && onPartialFailure) {
42998
- const previewFiles = droppedFiles.slice(0, 3).join(", ");
42999
- const remainderHint = droppedFiles.length > 3 ? `, +${droppedFiles.length - 3} more` : "";
43000
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43001
- onPartialFailure(`${droppedFiles.length} file(s) failed to lint and were skipped (${previewFiles}${remainderHint})${reasonHint}`);
43002
- }
43022
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
43023
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
43024
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
43025
+ };
43026
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
43027
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
43028
+ reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
43003
43029
  return allDiagnostics;
43004
43030
  };
43005
43031
  let diagnostics;
@@ -43203,7 +43229,8 @@ const runOxlint = async (options) => {
43203
43229
  spawnTimeoutMs,
43204
43230
  outputMaxBytes,
43205
43231
  concurrency: options.concurrency,
43206
- signal: options.signal
43232
+ signal: options.signal,
43233
+ deadlineEpochMs: options.deadlineEpochMs
43207
43234
  });
43208
43235
  writeOxlintConfig(passConfigPath, buildConfigForPass({}));
43209
43236
  try {
@@ -43302,7 +43329,8 @@ const runOxlint = async (options) => {
43302
43329
  spawnTimeoutMs,
43303
43330
  outputMaxBytes,
43304
43331
  concurrency: options.concurrency,
43305
- signal: options.signal
43332
+ signal: options.signal,
43333
+ deadlineEpochMs: options.deadlineEpochMs
43306
43334
  });
43307
43335
  writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
43308
43336
  try {
@@ -43406,7 +43434,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43406
43434
  outputMaxBytes,
43407
43435
  concurrency,
43408
43436
  signal,
43409
- lintBatchOrdering
43437
+ lintBatchOrdering,
43438
+ deadlineEpochMs: input.deadlineEpochMs
43410
43439
  }),
43411
43440
  catch: ensureReactDoctorError
43412
43441
  });
@@ -44166,6 +44195,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44166
44195
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
44167
44196
  const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
44168
44197
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
44198
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
44169
44199
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
44170
44200
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
44171
44201
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
@@ -44195,7 +44225,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44195
44225
  });
44196
44226
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
44197
44227
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
44198
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
44228
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
44199
44229
  })) : null;
44200
44230
  const scanProgress = yield* progressService.start("Scanning...");
44201
44231
  const scanStartTime = Date.now();
@@ -44220,7 +44250,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44220
44250
  onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
44221
44251
  lintCacheHitFileCount = cacheHitFileCount;
44222
44252
  lintCacheTotalFileCount = totalConsideredFileCount;
44223
- }
44253
+ },
44254
+ deadlineEpochMs: input.deadlineEpochMs
44224
44255
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44225
44256
  yield* set(lintFailure, {
44226
44257
  didFail: true,
@@ -44247,7 +44278,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44247
44278
  let deadCodeCollected = [];
44248
44279
  if (lintFailureState.didFail) {
44249
44280
  if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
44250
- } else if (shouldRunDeadCode) {
44281
+ } else if (shouldRunDeadCode) if (input.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.deadlineEpochMs) === 0) {
44282
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
44283
+ yield* set(deadCodeFailure, {
44284
+ didFail: true,
44285
+ reason: "Dead-code analysis skipped — max scan duration reached."
44286
+ });
44287
+ } else {
44251
44288
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
44252
44289
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
44253
44290
  sourceFileCount: totalFileCount,
@@ -44256,7 +44293,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44256
44293
  });
44257
44294
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
44258
44295
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
44259
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
44296
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
44260
44297
  });
44261
44298
  }
44262
44299
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -44294,7 +44331,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44294
44331
  };
44295
44332
  const scoreSurface = input.scoreSurface ?? "score";
44296
44333
  const scoreDiagnostics = filterDiagnosticsForSurface([...finalDiagnostics], scoreSurface, resolvedConfig.config);
44297
- const score = lintFailureState.didFail ? null : yield* scoreService.compute({
44334
+ const score = lintFailureState.didFail || deadCodeFailureState.didFail ? null : yield* scoreService.compute({
44298
44335
  diagnostics: scoreDiagnostics,
44299
44336
  isCi: input.isCi,
44300
44337
  metadata: scoreMetadata
@@ -44687,6 +44724,7 @@ const buildJsonReport = (input) => {
44687
44724
  return {
44688
44725
  schemaVersion: 1,
44689
44726
  mode: input.mode,
44727
+ ...input.baselineDegraded ? { baselineDegraded: true } : {},
44690
44728
  ...shared
44691
44729
  };
44692
44730
  };
@@ -45194,7 +45232,7 @@ const makeNoopConsole = () => ({
45194
45232
  });
45195
45233
  //#endregion
45196
45234
  //#region src/cli/utils/version.ts
45197
- const VERSION = "0.5.8-dev.80e3093";
45235
+ const VERSION = "0.5.8-dev.8232e96";
45198
45236
  //#endregion
45199
45237
  //#region src/cli/utils/json-mode.ts
45200
45238
  let context = null;
@@ -45563,13 +45601,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45563
45601
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45564
45602
  * standard `SENTRY_RELEASE` override.
45565
45603
  */
45566
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.80e3093`;
45604
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.8232e96`;
45567
45605
  /**
45568
45606
  * Deployment environment shown in Sentry's environment filter. Defaults to
45569
45607
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45570
45608
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45571
45609
  */
45572
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.80e3093") ? "development" : "production");
45610
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.8232e96") ? "development" : "production");
45573
45611
  /**
45574
45612
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45575
45613
  * (set to `0` to disable tracing) and falls back to
@@ -45777,7 +45815,7 @@ const runGit = (projectRoot, args) => {
45777
45815
  }
45778
45816
  };
45779
45817
  const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
45780
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
45818
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
45781
45819
  const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
45782
45820
  const readPackageJson = (projectRoot) => {
45783
45821
  try {
@@ -45791,25 +45829,25 @@ const writeJsonFile = (filePath, value) => {
45791
45829
  };
45792
45830
  const packageHasDependency = (projectRoot, dependencyName) => {
45793
45831
  const packageJson = readPackageJson(projectRoot);
45794
- if (!isRecord$1(packageJson)) return false;
45832
+ if (!isRecord(packageJson)) return false;
45795
45833
  return [
45796
45834
  "dependencies",
45797
45835
  "devDependencies",
45798
45836
  "optionalDependencies"
45799
45837
  ].some((fieldName) => {
45800
45838
  const dependencies = packageJson[fieldName];
45801
- return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
45839
+ return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
45802
45840
  });
45803
45841
  };
45804
45842
  const packageHasRecordKey = (projectRoot, key) => {
45805
45843
  const packageJson = readPackageJson(projectRoot);
45806
- return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
45844
+ return isRecord(packageJson) && isRecord(packageJson[key]);
45807
45845
  };
45808
45846
  const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
45809
45847
  const packageJson = readPackageJson(projectRoot);
45810
- if (!isRecord$1(packageJson)) return false;
45848
+ if (!isRecord(packageJson)) return false;
45811
45849
  const value = packageJson[key];
45812
- return isRecord$1(value) && isRecord$1(value[nestedKey]);
45850
+ return isRecord(value) && isRecord(value[nestedKey]);
45813
45851
  };
45814
45852
  const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
45815
45853
  //#endregion
@@ -48849,31 +48887,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
48849
48887
  };
48850
48888
  const hasDoctorScript = (projectRoot) => {
48851
48889
  const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
48852
- if (!isRecord$1(packageJson)) return false;
48890
+ if (!isRecord(packageJson)) return false;
48853
48891
  const scripts = packageJson.scripts;
48854
- if (!isRecord$1(scripts)) return false;
48892
+ if (!isRecord(scripts)) return false;
48855
48893
  return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
48856
48894
  };
48857
48895
  const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
48858
48896
  const dependencies = packageJson[fieldName];
48859
- return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
48897
+ return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
48860
48898
  });
48861
48899
  const installDoctorScript = (options) => {
48862
48900
  const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
48863
48901
  const packageJsonPath = getPackageJsonPath(packageDirectory);
48864
48902
  const packageJson = readPackageJson(packageDirectory);
48865
- if (!isRecord$1(packageJson)) return {
48903
+ if (!isRecord(packageJson)) return {
48866
48904
  packageJsonPath,
48867
48905
  scriptStatus: "skipped",
48868
48906
  scriptReason: "missing-or-invalid-package-json"
48869
48907
  };
48870
48908
  const scripts = packageJson.scripts;
48871
48909
  const scriptTarget = (() => {
48872
- if (scripts !== void 0 && !isRecord$1(scripts)) return {
48910
+ if (scripts !== void 0 && !isRecord(scripts)) return {
48873
48911
  status: "skipped",
48874
48912
  reason: "invalid-scripts"
48875
48913
  };
48876
- const scriptRecord = isRecord$1(scripts) ? scripts : {};
48914
+ const scriptRecord = isRecord(scripts) ? scripts : {};
48877
48915
  if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
48878
48916
  scriptName: DOCTOR_SCRIPT_NAME,
48879
48917
  status: "existing"
@@ -48907,7 +48945,7 @@ const installDoctorScript = (options) => {
48907
48945
  if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
48908
48946
  ...packageJson,
48909
48947
  scripts: {
48910
- ...isRecord$1(scripts) ? scripts : {},
48948
+ ...isRecord(scripts) ? scripts : {},
48911
48949
  [scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
48912
48950
  }
48913
48951
  });
@@ -55991,7 +56029,10 @@ jobs:
55991
56029
  react-doctor:
55992
56030
  runs-on: ubuntu-latest
55993
56031
  steps:
56032
+ # fetch-depth: 0 gives React Doctor the full git history it needs to find the merge base with the target branch. Without it a shallow checkout has no merge base, so PR runs can't compare against the base and fall back to reporting every issue in the changed files (pre-existing ones included) instead of only the ones the PR introduced.
55994
56033
  - uses: actions/checkout@v5
56034
+ with:
56035
+ fetch-depth: 0
55995
56036
 
55996
56037
  - uses: millionco/react-doctor@${actionRef}
55997
56038
  # Advisory by default: React Doctor reports findings on every PR — a
@@ -56289,7 +56330,10 @@ jobs:
56289
56330
  react-doctor:
56290
56331
  runs-on: ubuntu-latest
56291
56332
  steps:
56333
+ # fetch-depth: 0 gives React Doctor the full git history it needs to find the merge base with the target branch. Without it a shallow checkout has no merge base, so PR runs can't compare against the base and fall back to reporting every issue in the changed files (pre-existing ones included) instead of only the ones the PR introduced.
56292
56334
  - uses: actions/checkout@v5
56335
+ with:
56336
+ fetch-depth: 0
56293
56337
 
56294
56338
  - uses: millionco/react-doctor@${actionRef}
56295
56339
  ${WITH_INDENT}with:
@@ -56332,12 +56376,8 @@ const parseGate$1 = (content) => {
56332
56376
  };
56333
56377
  };
56334
56378
  const surgicalApplyGate = (content, gate) => {
56335
- let doc;
56336
- try {
56337
- doc = import_dist.parseDocument(content);
56338
- } catch {
56339
- return null;
56340
- }
56379
+ const doc = import_dist.parseDocument(content);
56380
+ if (doc.errors.length > 0) return null;
56341
56381
  const located = findReactDoctorStep(doc);
56342
56382
  if (located === null) return null;
56343
56383
  let withMap = located.withMap;
@@ -56745,23 +56785,20 @@ const promptForGate = async (provider, currentGate, prompt) => {
56745
56785
  const openCiPullRequest = async (params) => {
56746
56786
  const pullRequestSpinner = spinner("Opening a pull request for review...").start();
56747
56787
  const result = await openWorkflowPullRequest(params);
56748
- if (result.status === "pr-opened") {
56749
- pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(result.url)}`);
56750
- return "pr";
56751
- }
56752
- if (result.status === "pr-exists") {
56753
- pullRequestSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(result.url)}`);
56754
- return "pr";
56755
- }
56756
- if (result.status === "branch-pushed") {
56757
- pullRequestSpinner.warn(`Pushed ${highlighter.bold(result.branch)} but couldn't open a PR. Open one with: gh pr create --head ${result.branch}`);
56758
- return "pr";
56788
+ if (result.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(result.url)}`);
56789
+ else if (result.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(result.url)}`);
56790
+ else if (result.status === "branch-pushed") pullRequestSpinner.warn(`Pushed ${highlighter.bold(result.branch)} but couldn't open a PR. Open one with: gh pr create --head ${result.branch}`);
56791
+ else {
56792
+ pullRequestSpinner.stop();
56793
+ const didStage = await stageWorkflowFile({
56794
+ workflowPath: params.workflowPath,
56795
+ run: params.run
56796
+ });
56797
+ cliLogger.dim(didStage ? " Staged the change. Commit it to apply." : " Review and commit the change to apply it.");
56759
56798
  }
56760
- pullRequestSpinner.stop();
56761
- const didStage = await stageWorkflowFile({ workflowPath: params.workflowPath });
56762
- cliLogger.dim(didStage ? " Staged the change. Commit it to apply." : " Review and commit the change to apply it.");
56763
- return "tree";
56799
+ return result.status;
56764
56800
  };
56801
+ const pullRequestMode = (status) => status === "not-attempted" ? "tree" : "pr";
56765
56802
  const runCiInstall = async (options = {}) => {
56766
56803
  const projectRoot = resolveProjectRoot$1(options);
56767
56804
  const prompt = options.prompt ?? prompts;
@@ -56800,10 +56837,12 @@ const runCiInstall = async (options = {}) => {
56800
56837
  }
56801
56838
  scaffoldSpinner.succeed(`Added ${provider.fileLabel}.`);
56802
56839
  let mode = "tree";
56803
- if (provider.supportsPullRequest && options.pr) mode = await openCiPullRequest({
56840
+ if (provider.supportsPullRequest && options.pr) mode = pullRequestMode(await openCiPullRequest({
56804
56841
  workflowPath: result.path,
56805
- baseBranch: defaultBranch
56806
- });
56842
+ baseBranch: defaultBranch,
56843
+ run: options.run,
56844
+ checkCommandAvailable: options.checkCommandAvailable
56845
+ }));
56807
56846
  else cliLogger.dim(" Review and commit it to start scanning every pull request.");
56808
56847
  printGateSummary(provider, gate);
56809
56848
  cliLogger.log(` Learn more: ${highlighter.info(setupDocsUrl(provider))}`);
@@ -56849,11 +56888,28 @@ const runCiUpgrade = async (options = {}) => {
56849
56888
  process.exitCode = 1;
56850
56889
  return;
56851
56890
  }
56852
- mode = await openCiPullRequest({
56891
+ const status = await openCiPullRequest({
56853
56892
  workflowPath: workflow.path,
56854
56893
  baseBranch: await detectDefaultBranch(projectRoot, run) ?? "main",
56855
- ...upgradeCopy
56894
+ ...upgradeCopy,
56895
+ run: options.run,
56896
+ checkCommandAvailable: options.checkCommandAvailable
56856
56897
  });
56898
+ if (status === "pr-exists") {
56899
+ try {
56900
+ NFS.writeFileSync(workflow.path, workflow.content);
56901
+ } catch {
56902
+ cliLogger.dim(" The upgrade edit is still in your working tree — review and commit it.");
56903
+ return;
56904
+ }
56905
+ cliLogger.dim(` Merge or close that PR, then re-run ${highlighter.info("react-doctor ci upgrade")} if the workflow still needs it.`);
56906
+ recordCount(METRIC.ciUpgraded, 1, {
56907
+ provider: provider.id,
56908
+ mode: "pr-exists"
56909
+ });
56910
+ return;
56911
+ }
56912
+ mode = pullRequestMode(status);
56857
56913
  } else {
56858
56914
  const upgradeSpinner = spinner("Upgrading workflow to @v2...").start();
56859
56915
  try {
@@ -56951,7 +57007,6 @@ const runCiConfig = async (options = {}) => {
56951
57007
  };
56952
57008
  //#endregion
56953
57009
  //#region src/cli/utils/is-environment-error.ts
56954
- const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
56955
57010
  const ENVIRONMENT_ERROR_CODES = new Set([
56956
57011
  "ENOSPC",
56957
57012
  "EIO",
@@ -56961,12 +57016,12 @@ const ENVIRONMENT_ERROR_CODES = new Set([
56961
57016
  "ENOTDIR"
56962
57017
  ]);
56963
57018
  const isEnvironmentError = (error) => {
56964
- if (!isNodeSystemError(error)) return false;
57019
+ if (!isErrnoException(error) || typeof error.code !== "string") return false;
56965
57020
  if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
56966
- return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
57021
+ return ENVIRONMENT_ERROR_CODES.has(error.code);
56967
57022
  };
56968
57023
  const formatEnvironmentError = (error) => {
56969
- if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
57024
+ if (!isErrnoException(error)) return messageFromUnknown(error);
56970
57025
  switch (error.code) {
56971
57026
  case "ENOSPC": return "No space left on device. Free up disk space and try again.";
56972
57027
  case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
@@ -57069,7 +57124,7 @@ const handleError = (error, options = {}) => {
57069
57124
  */
57070
57125
  const handleUserError = (error, options = {}) => {
57071
57126
  const isEnvError = isEnvironmentError(error);
57072
- if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
57127
+ if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: (isErrnoException(error) ? error.code : void 0) ?? "unknown" });
57073
57128
  const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
57074
57129
  runSync(gen(function* () {
57075
57130
  yield* error$1("");
@@ -57583,7 +57638,7 @@ const recordScanMetrics = (input) => {
57583
57638
  });
57584
57639
  if (result.diagnostics.length === 0 && !hasSkippedChecks) recordCount(METRIC.scanClean, 1, { mode: input.mode });
57585
57640
  if (result.score) recordDistribution(METRIC.scanScore, result.score.score, { attributes: { mode: input.mode } });
57586
- else if (!input.noScore && !input.didLintFail) recordCount(METRIC.scoreUnavailable, 1, { mode: input.mode });
57641
+ else if (!input.noScore && !input.didLintFail && !input.didDeadCodeFail) recordCount(METRIC.scoreUnavailable, 1, { mode: input.mode });
57587
57642
  if (input.baselineDegraded) recordCount(METRIC.baselineDegraded, 1);
57588
57643
  if (input.didLintFail) recordCount(METRIC.lintFailed, 1, { reasonKind: input.lintFailureReasonKind });
57589
57644
  if (input.didDeadCodeFail) recordCount(METRIC.deadCodeFailed, 1);
@@ -57772,6 +57827,7 @@ const buildOutcomeAttributes = (input) => {
57772
57827
  failureReasonKind: input.lintFailureReasonKind ?? null,
57773
57828
  partialFailureCount: input.lintPartialFailureCount ?? null,
57774
57829
  droppedFileCount: input.lintDroppedFileCount ?? null,
57830
+ deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
57775
57831
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
57776
57832
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
57777
57833
  cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
@@ -57819,6 +57875,7 @@ const buildScanAttributes = (input) => {
57819
57875
  scope: input.scope,
57820
57876
  parallel: input.parallel,
57821
57877
  workerCount: input.workerCount ?? null,
57878
+ maxDurationMs: input.maxDurationMs,
57822
57879
  lint: input.lint,
57823
57880
  deadCode: input.deadCode,
57824
57881
  scoreOnly: input.scoreOnly,
@@ -57884,13 +57941,20 @@ const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
57884
57941
  };
57885
57942
  };
57886
57943
  //#endregion
57887
- //#region src/cli/utils/count-dropped-lint-files.ts
57888
- const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
57889
- const countDroppedLintFiles = (lintPartialFailures) => lintPartialFailures.reduce((total, message) => {
57890
- const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
57944
+ //#region src/cli/utils/sum-file-counts-matching.ts
57945
+ const sumFileCountsMatching = (messages, countPattern) => messages.reduce((total, message) => {
57946
+ const match = countPattern.exec(message);
57891
57947
  return match ? total + Number(match[1]) : total;
57892
57948
  }, 0);
57893
57949
  //#endregion
57950
+ //#region src/cli/utils/count-deadline-skipped-files.ts
57951
+ const DEADLINE_SKIPPED_MESSAGE_PATTERN = /^(\d+) file\(s\) skipped — max scan duration reached/;
57952
+ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DEADLINE_SKIPPED_MESSAGE_PATTERN);
57953
+ //#endregion
57954
+ //#region src/cli/utils/count-dropped-lint-files.ts
57955
+ const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
57956
+ const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
57957
+ //#endregion
57894
57958
  //#region src/cli/utils/materialize-baseline-files.ts
57895
57959
  /**
57896
57960
  * Materializes the `ref` version of `files` into `tempDirectory` (via
@@ -58529,7 +58593,7 @@ const foldLegacyDecisions = (projects, legacy, eventId) => {
58529
58593
  }
58530
58594
  };
58531
58595
  const migrateCliState = (state) => {
58532
- if (state.schemaVersion === 2) return state;
58596
+ if (typeof state.schemaVersion === "number" && state.schemaVersion >= 2) return state;
58533
58597
  const projects = {};
58534
58598
  for (const [hash, record] of Object.entries(state.projects ?? {})) {
58535
58599
  const carried = {
@@ -58567,7 +58631,9 @@ const openStore = (options = {}) => new Conf({
58567
58631
  });
58568
58632
  const openMigratedStore = (options) => {
58569
58633
  const store = openStore(options);
58570
- if (store.store.schemaVersion !== 2) store.store = migrateCliState(store.store);
58634
+ const state = store.store;
58635
+ const migrated = migrateCliState(state);
58636
+ if (migrated !== state) store.store = migrated;
58571
58637
  return store;
58572
58638
  };
58573
58639
  const readCliState = (select, fallback, options = {}) => {
@@ -59062,7 +59128,6 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
59062
59128
  "eslint-plugin-react-hooks/package.json"
59063
59129
  ];
59064
59130
  const bundledRequire = createRequire(import.meta.url);
59065
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
59066
59131
  const normalizeForStableJson = (value) => {
59067
59132
  if (value === null) return null;
59068
59133
  if (value === void 0) return void 0;
@@ -59075,10 +59140,10 @@ const normalizeForStableJson = (value) => {
59075
59140
  });
59076
59141
  const prototype = Object.getPrototypeOf(value);
59077
59142
  if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
59078
- const record = value;
59143
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
59079
59144
  const normalizedRecord = {};
59080
- for (const key of Object.keys(record).sort()) {
59081
- const normalized = normalizeForStableJson(record[key]);
59145
+ for (const key of Object.keys(value).sort()) {
59146
+ const normalized = normalizeForStableJson(value[key]);
59082
59147
  if (normalized !== void 0) normalizedRecord[key] = normalized;
59083
59148
  }
59084
59149
  return normalizedRecord;
@@ -59296,6 +59361,7 @@ const mergeInspectOptions = (inputOptions, userConfig) => ({
59296
59361
  suppressRendering: inputOptions.suppressRendering ?? false,
59297
59362
  concurrentScan: inputOptions.concurrentScan ?? false,
59298
59363
  concurrency: inputOptions.concurrency,
59364
+ maxDurationMs: inputOptions.maxDurationMs ?? null,
59299
59365
  baseline: inputOptions.baseline ?? null,
59300
59366
  changedLineRanges: inputOptions.changedLineRanges ?? null,
59301
59367
  supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
@@ -59311,6 +59377,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
59311
59377
  scope: deriveScope(options),
59312
59378
  parallel,
59313
59379
  workerCount,
59380
+ maxDurationMs: options.maxDurationMs,
59314
59381
  lint: options.lint,
59315
59382
  deadCode: options.deadCode,
59316
59383
  scoreOnly: options.scoreOnly,
@@ -59325,6 +59392,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
59325
59392
  };
59326
59393
  const inspect = async (directory, inputOptions = {}) => {
59327
59394
  const startTime = performance$1.now();
59395
+ const deadlineEpochMs = inputOptions.deadlineEpochMs ?? (inputOptions.maxDurationMs != null ? Date.now() + inputOptions.maxDurationMs : null);
59328
59396
  const isConcurrentScan = inputOptions.concurrentScan === true;
59329
59397
  if (!isConcurrentScan) resetSentryRunState();
59330
59398
  const hasConfigOverride = inputOptions.configOverride !== void 0;
@@ -59348,7 +59416,7 @@ const inspect = async (directory, inputOptions = {}) => {
59348
59416
  try {
59349
59417
  const result = await withSentryRunSpan(async (rootSentrySpan) => {
59350
59418
  try {
59351
- return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, rootSentrySpan);
59419
+ return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan);
59352
59420
  } catch (error) {
59353
59421
  recordRunEvent(rootSentrySpan, {
59354
59422
  ...buildRunEventConfig(options, userConfig, userConfig !== null),
@@ -59364,6 +59432,7 @@ const inspect = async (directory, inputOptions = {}) => {
59364
59432
  if (ownsSpinnerSilence) setSpinnerSilent(wasSpinnerSilent);
59365
59433
  }
59366
59434
  };
59435
+ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
59367
59436
  /**
59368
59437
  * Runs a second, lint-only scan over the changed files as they existed at the
59369
59438
  * baseline ref (materialized into a temp tree with head's config) and diffs it
@@ -59390,7 +59459,7 @@ const runBaselineComparison = async (params) => {
59390
59459
  directory: snapshot.tempDirectory,
59391
59460
  hasConfigOverride: true,
59392
59461
  userConfig: params.userConfig,
59393
- configSourceDirectory: null,
59462
+ configSourceDirectory: params.configSourceDirectory,
59394
59463
  projectInfoOverride: params.headProjectInfo,
59395
59464
  shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
59396
59465
  shouldRunDeadCode: false,
@@ -59413,9 +59482,10 @@ const runBaselineComparison = async (params) => {
59413
59482
  runId: getRunId(),
59414
59483
  resolveLocalGithubViewerPermission: false,
59415
59484
  suppressScanSummary: true,
59416
- supplyChainManifestChanged: params.options.supplyChainManifestChanged
59485
+ supplyChainManifestChanged: params.options.supplyChainManifestChanged,
59486
+ deadlineEpochMs: params.deadlineEpochMs ?? void 0
59417
59487
  }, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
59418
- if (baseOutput.didLintFail) return null;
59488
+ if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
59419
59489
  const delta = computeDiagnosticDelta({
59420
59490
  headDiagnostics: params.headDiagnostics,
59421
59491
  baseDiagnostics: baseOutput.diagnostics,
@@ -59434,7 +59504,7 @@ const runBaselineComparison = async (params) => {
59434
59504
  snapshot.cleanup();
59435
59505
  }
59436
59506
  };
59437
- const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, rootSentrySpan) => {
59507
+ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan) => {
59438
59508
  const isDiffMode = options.includePaths.length > 0;
59439
59509
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
59440
59510
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
@@ -59500,7 +59570,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59500
59570
  resolveLocalGithubViewerPermission: !options.noScore,
59501
59571
  suppressScanSummary: options.suppressRendering,
59502
59572
  supplyChainManifestChanged: options.supplyChainManifestChanged,
59503
- concurrentScan: options.concurrentScan
59573
+ concurrentScan: options.concurrentScan,
59574
+ deadlineEpochMs: deadlineEpochMs ?? void 0
59504
59575
  }, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
59505
59576
  recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
59506
59577
  recordCount(METRIC.projectDetected, 1);
@@ -59521,15 +59592,17 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59521
59592
  else runConsole(error$1(highlighter.error(lintFailureReason)));
59522
59593
  let inspectDiagnostics = output.diagnostics;
59523
59594
  let baselineDelta;
59524
- if (options.baseline && isDiffMode && !didLintFail) {
59595
+ if (options.baseline && isDiffMode && !didLintFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
59525
59596
  const comparison = await runBaselineComparison({
59526
59597
  directory,
59527
59598
  options,
59528
59599
  userConfig,
59600
+ configSourceDirectory,
59529
59601
  headProjectInfo: output.project,
59530
59602
  headDiagnostics: output.diagnostics,
59531
59603
  resolvedNodeBinaryPath,
59532
- baselineRef: options.baseline.ref
59604
+ baselineRef: options.baseline.ref,
59605
+ deadlineEpochMs
59533
59606
  });
59534
59607
  if (comparison) {
59535
59608
  inspectDiagnostics = comparison.displayDiagnostics;
@@ -59563,7 +59636,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59563
59636
  securityScanFailed: output.securityScanFailed,
59564
59637
  suppressedRuleCounts: output.suppressedRuleCounts
59565
59638
  };
59566
- if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
59639
+ if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
59567
59640
  const result = await renderAndRecordScan({
59568
59641
  payload,
59569
59642
  options,
@@ -59637,6 +59710,7 @@ const renderAndRecordScan = async (input) => {
59637
59710
  lintFailureReasonKind: input.payload.lintFailureReasonKind,
59638
59711
  lintPartialFailureCount: input.payload.lintPartialFailures.length,
59639
59712
  lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
59713
+ lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
59640
59714
  didDeadCodeFail: input.payload.didDeadCodeFail,
59641
59715
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
59642
59716
  securityScanFailed: input.payload.securityScanFailed,
@@ -59752,7 +59826,7 @@ const getStagedSourceFiles = async (directory) => {
59752
59826
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
59753
59827
  }).pipe(provide(stagedFilesLayer)))];
59754
59828
  } catch (error) {
59755
- cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
59829
+ cliLogger.warn(`Failed to discover staged files: ${messageFromUnknown(error)}`);
59756
59830
  return [];
59757
59831
  }
59758
59832
  };
@@ -59771,6 +59845,9 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
59771
59845
  };
59772
59846
  };
59773
59847
  //#endregion
59848
+ //#region src/cli/utils/is-share-opted-out.ts
59849
+ const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
59850
+ //#endregion
59774
59851
  //#region src/cli/utils/build-handoff-payload.ts
59775
59852
  const buildHandoffPayload = (input) => {
59776
59853
  const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
@@ -60053,6 +60130,8 @@ const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
60053
60130
  const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
60054
60131
  const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
60055
60132
  const CURSOR_HOOKS_SCHEMA_VERSION = 1;
60133
+ const LEGACY_HOOK_SCRIPT_PATHS = [".claude/hooks/react-doctor.sh", ".cursor/hooks/react-doctor.sh"];
60134
+ const isLegacyHookCommand = (command) => typeof command === "string" && LEGACY_HOOK_SCRIPT_PATHS.some((legacyPath) => command.includes(legacyPath));
60056
60135
  const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
60057
60136
  const readJsonFile = (filePath, fallback) => {
60058
60137
  if (!NFS.existsSync(filePath)) return fallback;
@@ -60065,11 +60144,24 @@ const readJsonFile = (filePath, fallback) => {
60065
60144
  throw error;
60066
60145
  }
60067
60146
  };
60147
+ const readJsonFileSafely = (filePath, fallback) => {
60148
+ try {
60149
+ return readJsonFile(filePath, fallback);
60150
+ } catch {
60151
+ return fallback;
60152
+ }
60153
+ };
60154
+ const findAgentsWithLegacyShellHooks = (projectRoot) => {
60155
+ const agents = [];
60156
+ if ((readJsonFileSafely(Path.join(projectRoot, CLAUDE_SETTINGS_RELATIVE_PATH), {}).hooks?.PostToolBatch ?? []).some((group) => (group.hooks ?? []).some((hook) => isLegacyHookCommand(hook.command)))) agents.push(CLAUDE_AGENT);
60157
+ if ((readJsonFileSafely(Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH), {}).hooks?.postToolUse ?? []).some((handler) => isLegacyHookCommand(handler.command))) agents.push(CURSOR_AGENT);
60158
+ return agents;
60159
+ };
60068
60160
  const ensureDirectoryExists = (directoryPath) => {
60069
60161
  try {
60070
60162
  NFS.mkdirSync(directoryPath, { recursive: true });
60071
60163
  } catch (error) {
60072
- const code = error.code;
60164
+ const code = isErrnoException(error) ? error.code : void 0;
60073
60165
  if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
60074
60166
  if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
60075
60167
  throw error;
@@ -60082,6 +60174,9 @@ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
60082
60174
  const writeHookScript = (filePath) => {
60083
60175
  ensureDirectoryExists(Path.dirname(filePath));
60084
60176
  NFS.writeFileSync(filePath, buildAgentHookScript());
60177
+ try {
60178
+ NFS.rmSync(filePath.replace(/\.mjs$/, ".sh"), { force: true });
60179
+ } catch {}
60085
60180
  };
60086
60181
  const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
60087
60182
  const installClaudeHook = (projectRoot) => {
@@ -60089,7 +60184,15 @@ const installClaudeHook = (projectRoot) => {
60089
60184
  const hookPath = Path.join(projectRoot, CLAUDE_HOOK_RELATIVE_PATH);
60090
60185
  const settings = readJsonFile(settingsPath, {});
60091
60186
  const hooks = { ...settings.hooks ?? {} };
60092
- const postToolBatchHooks = [...hooks.PostToolBatch ?? []];
60187
+ const postToolBatchHooks = (hooks.PostToolBatch ?? []).flatMap((group) => {
60188
+ const groupHooks = group.hooks ?? [];
60189
+ const keptHooks = groupHooks.filter((hook) => !isLegacyHookCommand(hook.command));
60190
+ if (keptHooks.length === groupHooks.length) return [group];
60191
+ return keptHooks.length > 0 ? [{
60192
+ ...group,
60193
+ hooks: keptHooks
60194
+ }] : [];
60195
+ });
60093
60196
  if (!hasClaudeHookCommand(postToolBatchHooks)) postToolBatchHooks.push({ hooks: [{
60094
60197
  type: "command",
60095
60198
  command: CLAUDE_HOOK_COMMAND
@@ -60108,7 +60211,7 @@ const installCursorHook = (projectRoot) => {
60108
60211
  const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
60109
60212
  const config = readJsonFile(configPath, {});
60110
60213
  const hooks = { ...config.hooks ?? {} };
60111
- const postToolUseHooks = [...hooks.postToolUse ?? []];
60214
+ const postToolUseHooks = (hooks.postToolUse ?? []).filter((handler) => !isLegacyHookCommand(handler.command));
60112
60215
  if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
60113
60216
  command: CURSOR_HOOK_COMMAND,
60114
60217
  matcher: CURSOR_HOOK_MATCHER,
@@ -60124,7 +60227,7 @@ const installCursorHook = (projectRoot) => {
60124
60227
  return [configPath, hookPath];
60125
60228
  };
60126
60229
  const buildAgentHookScript = () => [
60127
- "import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
60230
+ "import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
60128
60231
  "import { tmpdir } from 'node:os';",
60129
60232
  "import { join, dirname } from 'node:path';",
60130
60233
  "import { fileURLToPath } from 'node:url';",
@@ -60133,11 +60236,14 @@ const buildAgentHookScript = () => [
60133
60236
  "const __filename = fileURLToPath(import.meta.url);",
60134
60237
  "const __dirname = dirname(__filename);",
60135
60238
  "",
60239
+ "// --verbose scans on large diffs can exceed spawnSync's 1 MiB default.",
60240
+ "const SPAWN_MAX_BUFFER_BYTES = 16 * 1024 * 1024;",
60241
+ "",
60136
60242
  "const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
60137
60243
  "",
60138
- "const readStdin = () => {",
60244
+ "const readFileOrEmpty = (source) => {",
60139
60245
  " try {",
60140
- " return readFileSync(0, 'utf8');",
60246
+ " return readFileSync(source, 'utf8');",
60141
60247
  " } catch {",
60142
60248
  " return '';",
60143
60249
  " }",
@@ -60157,18 +60263,25 @@ const buildAgentHookScript = () => [
60157
60263
  " // Each candidate is a single shell command string (not an args array):",
60158
60264
  " // `shell: true` is required to run the Windows `.cmd` shims, and an args",
60159
60265
  " // array with `shell: true` trips Node's DEP0190. A missing command exits",
60160
- " // 127 via the shell (no ENOENT error), so fall through on that. With no",
60161
- " // runner found, exit 0 silently — stdout is parsed as the hook's JSON.",
60266
+ " // 127 via a POSIX shell (no ENOENT error) and 9009 via cmd.exe, so fall",
60267
+ " // through on those. The local bin is probed with existsSync (its `./`",
60268
+ " // prefix form is not runnable by cmd.exe at all). With no runner found,",
60269
+ " // exit 0 silently — stdout is parsed as the hook's JSON.",
60270
+ " const localBin = process.platform === 'win32'",
60271
+ " ? 'node_modules\\\\.bin\\\\react-doctor.cmd'",
60272
+ " : './node_modules/.bin/react-doctor';",
60162
60273
  " const commands = [",
60163
- " './node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score',",
60274
+ " ...(existsSync(localBin)",
60275
+ " ? [localBin + ' --verbose --scope changed --blocking warning --no-score']",
60276
+ " : []),",
60164
60277
  " 'react-doctor --verbose --scope changed --blocking warning --no-score',",
60165
60278
  " 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
60166
60279
  " 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
60167
60280
  " ];",
60168
60281
  "",
60169
60282
  " for (const command of commands) {",
60170
- " const result = spawnSync(command, { encoding: 'utf8', shell: true });",
60171
- " if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
60283
+ " const result = spawnSync(command, { encoding: 'utf8', shell: true, maxBuffer: SPAWN_MAX_BUFFER_BYTES });",
60284
+ " if (result.error?.code === 'ENOENT' || result.status === 127 || result.status === 9009) continue;",
60172
60285
  " try {",
60173
60286
  " writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
60174
60287
  " } catch {}",
@@ -60187,8 +60300,7 @@ const buildAgentHookScript = () => [
60187
60300
  "const main = () => {",
60188
60301
  " let input;",
60189
60302
  " try {",
60190
- " const stdinContent = readStdin();",
60191
- " input = JSON.parse(stdinContent || '{}');",
60303
+ " input = JSON.parse(readFileOrEmpty(0) || '{}');",
60192
60304
  " } catch {",
60193
60305
  " input = {};",
60194
60306
  " }",
@@ -60212,7 +60324,9 @@ const buildAgentHookScript = () => [
60212
60324
  " process.exit(0);",
60213
60325
  " }",
60214
60326
  "",
60215
- " const scanOutput = readFileSync(outputPath, 'utf8').trim();",
60327
+ " // The write above is best-effort (unwritable tmpdir), so the read is too",
60328
+ " // — a hook must never crash the agent loop with a stack trace.",
60329
+ " const scanOutput = readFileOrEmpty(outputPath).trim();",
60216
60330
  " cleanup(outputPath);",
60217
60331
  "",
60218
60332
  " if (!scanOutput) {",
@@ -60253,7 +60367,9 @@ const installReactDoctorAgentHooks = (options) => {
60253
60367
  //#region src/cli/utils/install-git-hook-file.ts
60254
60368
  const REACT_DOCTOR_BLOCK_START = "# react-doctor hook start";
60255
60369
  const REACT_DOCTOR_BLOCK_END = "# react-doctor hook end";
60256
- const REACT_DOCTOR_BLOCK_PATTERN = new RegExp(`(?:${REACT_DOCTOR_BLOCK_START}[\\s\\S]*?${REACT_DOCTOR_BLOCK_END}\\n?|# react-doctor hook launcher start[\\s\\S]*?# react-doctor hook launcher end\\n?)`);
60370
+ const REACT_DOCTOR_BLOCK_PATTERN_SOURCE = `(?:${REACT_DOCTOR_BLOCK_START}[\\s\\S]*?${REACT_DOCTOR_BLOCK_END}\\n?|# react-doctor hook launcher start[\\s\\S]*?# react-doctor hook launcher end\\n?)`;
60371
+ const REACT_DOCTOR_BLOCK_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE);
60372
+ const ALL_REACT_DOCTOR_BLOCKS_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE, "g");
60257
60373
  const SHEBANG = "#!/bin/sh";
60258
60374
  const SHEBANG_PREFIX = "#!";
60259
60375
  const LOCAL_REACT_DOCTOR_BIN = "./node_modules/.bin/react-doctor";
@@ -60297,7 +60413,16 @@ const buildReactDoctorHookBlock = () => [
60297
60413
  ].join("\n");
60298
60414
  const mergeHookContent = (existingContent) => {
60299
60415
  const hookBlock = `${buildReactDoctorHookBlock()}\n`;
60300
- if (REACT_DOCTOR_BLOCK_PATTERN.test(existingContent)) return ensureTrailingNewline(existingContent.replace(REACT_DOCTOR_BLOCK_PATTERN, hookBlock));
60416
+ if (REACT_DOCTOR_BLOCK_PATTERN.test(existingContent)) {
60417
+ let isFirstManagedBlock = true;
60418
+ return ensureTrailingNewline(existingContent.replace(ALL_REACT_DOCTOR_BLOCKS_PATTERN, () => {
60419
+ if (isFirstManagedBlock) {
60420
+ isFirstManagedBlock = false;
60421
+ return hookBlock;
60422
+ }
60423
+ return "";
60424
+ }));
60425
+ }
60301
60426
  if (existingContent.length === 0) return `${SHEBANG}\n\n${hookBlock}`;
60302
60427
  const normalizedExistingContent = ensureTrailingNewline(existingContent);
60303
60428
  if (normalizedExistingContent.startsWith(SHEBANG_PREFIX)) {
@@ -60350,13 +60475,13 @@ const installPackageJsonHook = (options, strategy) => {
60350
60475
  const packageJsonPath = getPackageJsonPath(options.projectRoot);
60351
60476
  const didHookExist = NFS.existsSync(packageJsonPath);
60352
60477
  const packageJson = readPackageJson(options.projectRoot);
60353
- const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
60478
+ const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
60354
60479
  const parentKeys = strategy.path.slice(0, -1);
60355
60480
  const leafKey = strategy.path[strategy.path.length - 1];
60356
60481
  let parent = nextPackageJson;
60357
60482
  for (const key of parentKeys) {
60358
60483
  const existing = parent[key];
60359
- const cloned = isRecord$1(existing) ? { ...existing } : {};
60484
+ const cloned = isRecord(existing) ? { ...existing } : {};
60360
60485
  parent[key] = cloned;
60361
60486
  parent = cloned;
60362
60487
  }
@@ -60527,7 +60652,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
60527
60652
  const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
60528
60653
  const isSimpleGitHooksProject = (projectRoot) => {
60529
60654
  const packageJson = readPackageJson(projectRoot);
60530
- return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
60655
+ return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
60531
60656
  };
60532
60657
  const getLefthookConfigPath = (projectRoot) => {
60533
60658
  for (const fileName of LEFTHOOK_CONFIG_FILES) {
@@ -60690,7 +60815,7 @@ const detectPackageManager = (projectRoot) => {
60690
60815
  let currentDirectory = Path.resolve(projectRoot);
60691
60816
  while (true) {
60692
60817
  const packageJson = readPackageJson(currentDirectory);
60693
- if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
60818
+ if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
60694
60819
  const packageManagerName = packageJson.packageManager.split("@")[0];
60695
60820
  if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
60696
60821
  }
@@ -60766,12 +60891,12 @@ const isSupplyChainTrustError = (error) => {
60766
60891
  const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
60767
60892
  const installReactDoctorDependency = async (options) => {
60768
60893
  const packageJson = readPackageJson(options.projectRoot);
60769
- if (!isRecord$1(packageJson)) return {
60894
+ if (!isRecord(packageJson)) return {
60770
60895
  dependencyStatus: "skipped",
60771
60896
  dependencyReason: "missing-or-invalid-package-json"
60772
60897
  };
60773
60898
  if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
60774
- if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
60899
+ if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
60775
60900
  dependencyStatus: "skipped",
60776
60901
  dependencyReason: "invalid-dev-dependencies"
60777
60902
  };
@@ -61310,7 +61435,7 @@ const handoffToAgent = async (input) => {
61310
61435
  //#region src/cli/utils/migrate-action-pin.ts
61311
61436
  const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
61312
61437
  const RECOMMENDED_ACTION_REF = "v2";
61313
- const MUTABLE_ACTION_REF = /(uses:\s*[\w.-]+\/react-doctor@)(?:main|master)\b/g;
61438
+ const MUTABLE_ACTION_REF = /(uses:\s*millionco\/react-doctor@)(?:main|master)\b/gi;
61314
61439
  const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
61315
61440
  /**
61316
61441
  * Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
@@ -61412,34 +61537,54 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
61412
61537
  NFS.rmSync(legacy.legacyFilePath, { force: true });
61413
61538
  return targetPath;
61414
61539
  };
61415
- const PROJECT_MIGRATIONS = [{
61416
- id: "config-json-to-ts",
61417
- scope: "project",
61418
- run: ({ projectRoot }) => {
61419
- if (projectRoot === void 0) return false;
61420
- const legacyConfig = findLegacyConfig(projectRoot);
61421
- if (!legacyConfig) return false;
61422
- const migratedPath = migrateLegacyConfig(legacyConfig);
61423
- if (!migratedPath) return false;
61424
- cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
61425
- cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
61426
- cliLogger.break();
61427
- return true;
61428
- }
61429
- }, {
61430
- id: "action-pin-main-to-v2",
61431
- scope: "project",
61432
- run: ({ projectRoot }) => {
61433
- if (projectRoot === void 0) return false;
61434
- const rewrittenFiles = migrateActionPin(projectRoot);
61435
- if (rewrittenFiles.length === 0) return false;
61436
- const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
61437
- cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
61438
- cliLogger.dim(" An unpinned @main reference runs whatever the action's HEAD points to (a supply-chain risk). Review and commit the change — or revert it if you intentionally track main.");
61439
- cliLogger.break();
61440
- return true;
61540
+ const PROJECT_MIGRATIONS = [
61541
+ {
61542
+ id: "config-json-to-ts",
61543
+ scope: "project",
61544
+ run: ({ projectRoot }) => {
61545
+ if (projectRoot === void 0) return false;
61546
+ const legacyConfig = findLegacyConfig(projectRoot);
61547
+ if (!legacyConfig) return false;
61548
+ const migratedPath = migrateLegacyConfig(legacyConfig);
61549
+ if (!migratedPath) return false;
61550
+ cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
61551
+ cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
61552
+ cliLogger.break();
61553
+ return true;
61554
+ }
61555
+ },
61556
+ {
61557
+ id: "action-pin-main-to-v2",
61558
+ scope: "project",
61559
+ run: ({ projectRoot }) => {
61560
+ if (projectRoot === void 0) return false;
61561
+ const rewrittenFiles = migrateActionPin(projectRoot);
61562
+ if (rewrittenFiles.length === 0) return false;
61563
+ const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
61564
+ cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
61565
+ cliLogger.dim(" An unpinned @main reference runs whatever the action's HEAD points to (a supply-chain risk). Review and commit the change — or revert it if you intentionally track main.");
61566
+ cliLogger.break();
61567
+ return true;
61568
+ }
61569
+ },
61570
+ {
61571
+ id: "agent-hooks-sh-to-mjs",
61572
+ scope: "project",
61573
+ run: ({ projectRoot }) => {
61574
+ if (projectRoot === void 0) return false;
61575
+ const agents = findAgentsWithLegacyShellHooks(projectRoot);
61576
+ if (agents.length === 0) return false;
61577
+ installReactDoctorAgentHooks({
61578
+ projectRoot,
61579
+ agents
61580
+ });
61581
+ cliLogger.success(`Upgraded the legacy react-doctor.sh agent hook to the Node hook (${agents.join(", ")})`);
61582
+ cliLogger.dim(" The shell hook can't run on Windows and would double-scan next to the current hook. Review and commit the change.");
61583
+ cliLogger.break();
61584
+ return true;
61585
+ }
61441
61586
  }
61442
- }];
61587
+ ];
61443
61588
  const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
61444
61589
  //#endregion
61445
61590
  //#region src/cli/utils/render-welcome.ts
@@ -61499,7 +61644,7 @@ const readChangedFilesFrom = (filePath) => {
61499
61644
  try {
61500
61645
  raw = NFS.readFileSync(filePath, "utf8");
61501
61646
  } catch (error) {
61502
- const errorCode = error?.code;
61647
+ const errorCode = isErrnoException(error) ? error.code : void 0;
61503
61648
  throw new CliInputError(`Could not read the --changed-files-from file "${filePath}"${errorCode ? ` (${errorCode})` : ""}. Pass a path to a readable text file that lists changed files, one per line.`);
61504
61649
  }
61505
61650
  const uniqueFiles = /* @__PURE__ */ new Set();
@@ -61642,6 +61787,17 @@ const printAgentInstallHint = (writeLine = defaultWriteLine) => {
61642
61787
  for (const line of AGENT_INSTALL_HINT_LINES) writeLine(line);
61643
61788
  };
61644
61789
  //#endregion
61790
+ //#region src/cli/utils/resolve-max-duration-flag.ts
61791
+ const resolveMaxDurationFlag = (maxDuration) => {
61792
+ if (maxDuration === void 0) return void 0;
61793
+ const seconds = Number(maxDuration);
61794
+ if (!Number.isFinite(seconds) || seconds <= 0) {
61795
+ cliLogger.warn(`Invalid --max-duration "${maxDuration}". Expected a positive number of seconds; running without a budget.`);
61796
+ return;
61797
+ }
61798
+ return seconds * MILLISECONDS_PER_SECOND;
61799
+ };
61800
+ //#endregion
61645
61801
  //#region src/cli/utils/resolve-parallel-flag.ts
61646
61802
  /**
61647
61803
  * Maps the `--no-parallel` flag to an `InspectOptions.concurrency` value.
@@ -61655,14 +61811,12 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
61655
61811
  //#region src/cli/utils/resolve-cli-inspect-options.ts
61656
61812
  /**
61657
61813
  * Translates CLI flags into the `InspectOptions` contract `inspect()`
61658
- * accepts. Flag-specific computed fields (`scoreOnly`, `noScore`,
61659
- * `silent`, `isCi`) live here — there's no `userConfig` knob for them,
61660
- * only flag derivation. The plain boolean knobs (`lint`, `deadCode`,
61661
- * `verbose`) pass through unchanged: `inspect()` owns the
61662
- * userConfig-fallback layer so the merge logic isn't duplicated. The
61663
- * shell still hands `userConfig` in via `configOverride` and `noScore`
61664
- * so this resolver can apply the one flag-and-config rule that flags own
61665
- * (`--score false` wins, otherwise inherit `userConfig.noScore`).
61814
+ * accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
61815
+ * `isCi`): every `userConfig` fallback — including `noScore` — lives in
61816
+ * `inspect()`'s merge layer, which sees each project's module-merged
61817
+ * config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
61818
+ * through unchanged for the same reason. This resolver reads
61819
+ * `userConfig` only to decide the `--blocking warning` gate.
61666
61820
  */
61667
61821
  const resolveCliInspectOptions = (flags, userConfig) => {
61668
61822
  const wantsWarningGate = pickBlockingLevel(flags, userConfig) === "warning";
@@ -61674,10 +61828,11 @@ const resolveCliInspectOptions = (flags, userConfig) => {
61674
61828
  respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
61675
61829
  warnings: wantsWarningGate ? true : flags.warnings,
61676
61830
  scoreOnly: flags.score === true,
61677
- noScore: flags.score === false || flags.telemetry === false || (userConfig?.noScore ?? false),
61831
+ noScore: flags.score === false || flags.telemetry === false ? true : void 0,
61678
61832
  isCi: isCiEnvironment(),
61679
61833
  silent: Boolean(flags.json),
61680
61834
  concurrency: resolveParallelFlag(flags.parallel),
61835
+ maxDurationMs: resolveMaxDurationFlag(flags.maxDuration),
61681
61836
  categoryFilters: resolveCliCategories(flags.category)
61682
61837
  };
61683
61838
  };
@@ -62012,7 +62167,8 @@ const finalizeScans = (input) => {
62012
62167
  diff: input.diff,
62013
62168
  scans: jsonCompletedScans,
62014
62169
  totalElapsedMilliseconds: performance$1.now() - input.startTime,
62015
- baseline
62170
+ baseline,
62171
+ baselineDegraded
62016
62172
  }));
62017
62173
  }
62018
62174
  if (input.isScoreOnly || baselineDegraded) return;
@@ -62082,6 +62238,7 @@ const inspectAction = async (directory, flags) => {
62082
62238
  if (!isQuiet) if (!flags.verbose && canAnimateOnboarding(process.stdout)) await runPromise(playWelcomeScene({ speedMultiplier: !isOnboardingForced() && hasCompletedOnboarding() ? 2 : 1 }));
62083
62239
  else runSync(printBrandedHeader);
62084
62240
  const scanOptions = resolveCliInspectOptions(flags, userConfig);
62241
+ const scanDeadlineEpochMs = scanOptions.maxDurationMs !== void 0 ? Date.now() + scanOptions.maxDurationMs : void 0;
62085
62242
  const categoryFilters = new Set(scanOptions.categoryFilters ?? []);
62086
62243
  const skipPrompts = shouldSkipPrompts({
62087
62244
  yes: flags.yes,
@@ -62127,8 +62284,10 @@ const inspectAction = async (directory, flags) => {
62127
62284
  try {
62128
62285
  const scanResult = await inspect(snapshot.tempDirectory, {
62129
62286
  ...scanOptions,
62287
+ deadlineEpochMs: scanDeadlineEpochMs,
62130
62288
  includePaths: snapshot.stagedFiles,
62131
62289
  configOverride: userConfig,
62290
+ configSourceDirectory: scanTarget.configSourceDirectory ?? void 0,
62132
62291
  changedLineRanges: stagedLineRanges ?? void 0
62133
62292
  });
62134
62293
  const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
@@ -62228,6 +62387,7 @@ const inspectAction = async (directory, flags) => {
62228
62387
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
62229
62388
  const scanResult = await inspect(scanDirectory, {
62230
62389
  ...scanOptions,
62390
+ deadlineEpochMs: scanDeadlineEpochMs,
62231
62391
  includePaths,
62232
62392
  configOverride: projectConfig,
62233
62393
  configSourceDirectory: projectConfigSourceDirectory ?? void 0,
@@ -62268,7 +62428,7 @@ const inspectAction = async (directory, flags) => {
62268
62428
  completedScans.push(scanOutcome);
62269
62429
  }
62270
62430
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
62271
- const shouldShowShareLink = !scanOptions.noScore && (userConfig?.share ?? true) && !scanOptions.isCi;
62431
+ const shouldShowShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
62272
62432
  await runPromise(printMultiProjectSummary({
62273
62433
  completedScans,
62274
62434
  categoryFilters,
@@ -62602,6 +62762,10 @@ const resolveEffectiveRuleSeverity = (config, entry) => {
62602
62762
  source: "rule"
62603
62763
  };
62604
62764
  }
62765
+ if (!entry.defaultEnabled) return {
62766
+ value: "off",
62767
+ source: "default"
62768
+ };
62605
62769
  const categoryOverride = config?.categories?.[entry.category];
62606
62770
  if (categoryOverride !== void 0) return {
62607
62771
  value: categoryOverride,
@@ -63089,7 +63253,7 @@ const printDebugTrace = () => {
63089
63253
  //#endregion
63090
63254
  //#region src/cli/utils/removed-cli-flags.ts
63091
63255
  const REMOVED_FLAGS = new Map([
63092
- ["--full", "use `--diff false` to force a full scan"],
63256
+ ["--full", "use `--scope full` to force a full scan"],
63093
63257
  ["--explain", "use the `why <file>:<line>` command"],
63094
63258
  ["--why", "use the `why <file>:<line>` command"],
63095
63259
  ["--pr-comment", "the GitHub Action posts the PR comment for you; remove this flag"]
@@ -63139,6 +63303,7 @@ const ROOT_FLAG_SPEC = {
63139
63303
  "--changed-files-from",
63140
63304
  "--blocking",
63141
63305
  "--json-out",
63306
+ "--max-duration",
63142
63307
  "--output-dir",
63143
63308
  "--fail-on",
63144
63309
  "--project",
@@ -63375,7 +63540,7 @@ ${highlighter.dim("Learn more:")}
63375
63540
  ${highlighter.info(CI_URL)}
63376
63541
  `;
63377
63542
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
63378
- 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("--json-out <path>", "with --json, write the report to a file instead of stdout").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);
63543
+ 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("--json-out <path>", "with --json, write the report to a file instead of stdout").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("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").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);
63379
63544
  program.action(inspectAction);
63380
63545
  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));
63381
63546
  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);
@@ -63425,4 +63590,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63425
63590
  export {};
63426
63591
 
63427
63592
  //# sourceMappingURL=cli.js.map
63428
- //# debugId=38109950-d718-564f-8c23-83e407ef77e9
63593
+ //# debugId=56ca0cbd-9f70-5a50-8496-43abdf078431