react-doctor 0.0.30 → 0.0.32

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
@@ -32,7 +32,7 @@ const OPEN_BASE_URL = "https://www.react.doctor/open";
32
32
  const FETCH_TIMEOUT_MS = 1e4;
33
33
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
34
34
  const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
35
- const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
35
+ const OFFLINE_MESSAGE = "Score calculated locally (offline mode).";
36
36
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
37
37
  const ERROR_RULE_PENALTY = 1.5;
38
38
  const WARNING_RULE_PENALTY = .75;
@@ -41,6 +41,12 @@ const WARNING_ESTIMATED_FIX_RATE = .8;
41
41
  const MAX_KNIP_RETRIES = 5;
42
42
  const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
43
43
  const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
44
+ const IGNORED_DIRECTORIES = new Set([
45
+ "node_modules",
46
+ "dist",
47
+ "build",
48
+ "coverage"
49
+ ]);
44
50
  const AMI_WEBSITE_URL = "https://ami.dev";
45
51
  const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
46
52
  const AMI_RELEASES_URL = "https://github.com/millionco/ami-releases/releases";
@@ -278,17 +284,31 @@ const compileGlobPattern = (pattern) => {
278
284
  return new RegExp(regexSource);
279
285
  };
280
286
 
287
+ //#endregion
288
+ //#region src/utils/is-ignored-file.ts
289
+ const toRelativePath = (filePath, rootDirectory) => {
290
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
291
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
292
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
293
+ return normalizedFilePath.replace(/^\.\//, "");
294
+ };
295
+ const compileIgnoredFilePatterns = (userConfig) => Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
296
+ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
297
+ if (patterns.length === 0) return false;
298
+ const relativePath = toRelativePath(filePath, rootDirectory);
299
+ return patterns.some((pattern) => pattern.test(relativePath));
300
+ };
301
+
281
302
  //#endregion
282
303
  //#region src/utils/filter-diagnostics.ts
283
- const filterIgnoredDiagnostics = (diagnostics, config) => {
304
+ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
284
305
  const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
285
- const ignoredFilePatterns = Array.isArray(config.ignore?.files) ? config.ignore.files.map(compileGlobPattern) : [];
306
+ const ignoredFilePatterns = compileIgnoredFilePatterns(config);
286
307
  if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
287
308
  return diagnostics.filter((diagnostic) => {
288
309
  const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
289
310
  if (ignoredRules.has(ruleIdentifier)) return false;
290
- const normalizedPath = diagnostic.filePath.replace(/\\/g, "/").replace(/^\.\//, "");
291
- if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) return false;
311
+ if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
292
312
  return true;
293
313
  });
294
314
  };
@@ -343,7 +363,7 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
343
363
  ...deadCodeDiagnostics,
344
364
  ...isDiffMode ? [] : checkReducedMotion(directory)
345
365
  ];
346
- return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig) : merged, directory);
366
+ return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig, directory) : merged, directory);
347
367
  };
348
368
 
349
369
  //#endregion
@@ -422,12 +442,6 @@ const FRAMEWORK_DISPLAY_NAMES = {
422
442
  unknown: "React"
423
443
  };
424
444
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
425
- const IGNORED_DIRECTORIES = new Set([
426
- "node_modules",
427
- "dist",
428
- "build",
429
- "coverage"
430
- ]);
431
445
  const countSourceFilesViaFilesystem = (rootDirectory) => {
432
446
  let count = 0;
433
447
  const stack = [rootDirectory];
@@ -469,23 +483,115 @@ const detectFramework = (dependencies) => {
469
483
  return "unknown";
470
484
  };
471
485
  const isCatalogReference = (version) => version.startsWith("catalog:");
486
+ const extractCatalogName = (version) => {
487
+ if (!isCatalogReference(version)) return null;
488
+ const name = version.slice(8).trim();
489
+ return name.length > 0 ? name : null;
490
+ };
472
491
  const resolveVersionFromCatalog = (catalog, packageName) => {
473
492
  const version = catalog[packageName];
474
493
  if (typeof version === "string" && !isCatalogReference(version)) return version;
475
494
  return null;
476
495
  };
477
- const resolveCatalogVersion = (packageJson, packageName) => {
496
+ const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
497
+ const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
498
+ if (!isFile(workspacePath)) return {
499
+ defaultCatalog: {},
500
+ namedCatalogs: {}
501
+ };
502
+ const content = fs.readFileSync(workspacePath, "utf-8");
503
+ const defaultCatalog = {};
504
+ const namedCatalogs = {};
505
+ let currentSection = "none";
506
+ let currentCatalogName = "";
507
+ for (const line of content.split("\n")) {
508
+ const trimmed = line.trim();
509
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
510
+ const indentLevel = line.search(/\S/);
511
+ if (indentLevel === 0 && trimmed === "catalog:") {
512
+ currentSection = "catalog";
513
+ continue;
514
+ }
515
+ if (indentLevel === 0 && trimmed === "catalogs:") {
516
+ currentSection = "catalogs";
517
+ continue;
518
+ }
519
+ if (indentLevel === 0) {
520
+ currentSection = "none";
521
+ continue;
522
+ }
523
+ if (currentSection === "catalog" && indentLevel > 0) {
524
+ const colonIndex = trimmed.indexOf(":");
525
+ if (colonIndex > 0) {
526
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
527
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
528
+ if (key && value) defaultCatalog[key] = value;
529
+ }
530
+ continue;
531
+ }
532
+ if (currentSection === "catalogs" && indentLevel > 0) {
533
+ if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
534
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
535
+ currentSection = "named-catalog";
536
+ namedCatalogs[currentCatalogName] = {};
537
+ continue;
538
+ }
539
+ }
540
+ if (currentSection === "named-catalog" && indentLevel > 0) {
541
+ if (indentLevel <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
542
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
543
+ namedCatalogs[currentCatalogName] = {};
544
+ continue;
545
+ }
546
+ const colonIndex = trimmed.indexOf(":");
547
+ if (colonIndex > 0 && currentCatalogName) {
548
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
549
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
550
+ if (key && value) namedCatalogs[currentCatalogName][key] = value;
551
+ }
552
+ }
553
+ }
554
+ return {
555
+ defaultCatalog,
556
+ namedCatalogs
557
+ };
558
+ };
559
+ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogReference) => {
560
+ if (catalogReference) {
561
+ const namedCatalog = catalogs.namedCatalogs[catalogReference];
562
+ if (namedCatalog?.[packageName]) return namedCatalog[packageName];
563
+ }
564
+ if (catalogs.defaultCatalog[packageName]) return catalogs.defaultCatalog[packageName];
565
+ for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
566
+ return null;
567
+ };
568
+ const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
569
+ const rawVersion = collectAllDependencies(packageJson)[packageName];
570
+ const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
478
571
  const raw = packageJson;
479
572
  if (isPlainObject(raw.catalog)) {
480
573
  const version = resolveVersionFromCatalog(raw.catalog, packageName);
481
574
  if (version) return version;
482
575
  }
483
576
  if (isPlainObject(raw.catalogs)) {
577
+ if (catalogName && isPlainObject(raw.catalogs[catalogName])) {
578
+ const version = resolveVersionFromCatalog(raw.catalogs[catalogName], packageName);
579
+ if (version) return version;
580
+ }
484
581
  for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
485
582
  const version = resolveVersionFromCatalog(catalogEntries, packageName);
486
583
  if (version) return version;
487
584
  }
488
585
  }
586
+ const workspaces = packageJson.workspaces;
587
+ if (workspaces && !Array.isArray(workspaces) && isPlainObject(workspaces.catalog)) {
588
+ const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
589
+ if (version) return version;
590
+ }
591
+ if (rootDirectory) {
592
+ const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
593
+ if (pnpmVersion) return pnpmVersion;
594
+ }
489
595
  return null;
490
596
  };
491
597
  const extractDependencyInfo = (packageJson) => {
@@ -546,7 +652,7 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
546
652
  };
547
653
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
548
654
  const rootInfo = extractDependencyInfo(rootPackageJson);
549
- const catalogVersion = resolveCatalogVersion(rootPackageJson, "react");
655
+ const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot);
550
656
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
551
657
  return {
552
658
  reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
@@ -612,9 +718,17 @@ const discoverReactSubprojects = (rootDirectory) => {
612
718
  const listWorkspacePackages = (rootDirectory) => {
613
719
  const packageJsonPath = path.join(rootDirectory, "package.json");
614
720
  if (!isFile(packageJsonPath)) return [];
615
- const patterns = getWorkspacePatterns(rootDirectory, readPackageJson(packageJsonPath));
721
+ const packageJson = readPackageJson(packageJsonPath);
722
+ const patterns = getWorkspacePatterns(rootDirectory, packageJson);
616
723
  if (patterns.length === 0) return [];
617
724
  const packages = [];
725
+ if (hasReactDependency(packageJson)) {
726
+ const rootName = packageJson.name ?? path.basename(rootDirectory);
727
+ packages.push({
728
+ name: rootName,
729
+ directory: rootDirectory
730
+ });
731
+ }
618
732
  for (const pattern of patterns) {
619
733
  const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
620
734
  for (const workspaceDirectory of directories) {
@@ -660,7 +774,14 @@ const discoverProject = (directory) => {
660
774
  if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
661
775
  const packageJson = readPackageJson(packageJsonPath);
662
776
  let { reactVersion, framework } = extractDependencyInfo(packageJson);
663
- if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react");
777
+ if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory);
778
+ if (!reactVersion) {
779
+ const monorepoRoot = findMonorepoRoot(directory);
780
+ if (monorepoRoot) {
781
+ const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
782
+ if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
783
+ }
784
+ }
664
785
  if (!reactVersion || framework === "unknown") {
665
786
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
666
787
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
@@ -942,6 +1063,49 @@ const resolveNodeForOxlint = () => {
942
1063
  };
943
1064
  };
944
1065
 
1066
+ //#endregion
1067
+ //#region src/utils/resolve-lint-include-paths.ts
1068
+ const listSourceFilesViaGit = (rootDirectory) => {
1069
+ const result = spawnSync("git", [
1070
+ "ls-files",
1071
+ "--cached",
1072
+ "--others",
1073
+ "--exclude-standard"
1074
+ ], {
1075
+ cwd: rootDirectory,
1076
+ encoding: "utf-8",
1077
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
1078
+ });
1079
+ if (result.error || result.status !== 0) return null;
1080
+ return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
1081
+ };
1082
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
1083
+ const filePaths = [];
1084
+ const stack = [rootDirectory];
1085
+ while (stack.length > 0) {
1086
+ const currentDirectory = stack.pop();
1087
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
1088
+ for (const entry of entries) {
1089
+ const absolutePath = path.join(currentDirectory, entry.name);
1090
+ if (entry.isDirectory()) {
1091
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
1092
+ continue;
1093
+ }
1094
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
1095
+ }
1096
+ }
1097
+ return filePaths;
1098
+ };
1099
+ const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
1100
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
1101
+ if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
1102
+ const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
1103
+ return listSourceFiles(rootDirectory).filter((filePath) => {
1104
+ if (!JSX_FILE_PATTERN.test(filePath)) return false;
1105
+ return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
1106
+ });
1107
+ };
1108
+
945
1109
  //#endregion
946
1110
  //#region src/utils/run-knip.ts
947
1111
  const KNIP_CATEGORY_MAP = {
@@ -1200,24 +1364,27 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
1200
1364
 
1201
1365
  //#endregion
1202
1366
  //#region src/utils/neutralize-disable-directives.ts
1203
- const findFilesWithDisableDirectives = (rootDirectory) => {
1204
- const result = spawnSync("git", [
1367
+ const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
1368
+ const grepArgs = [
1205
1369
  "grep",
1206
1370
  "-l",
1207
1371
  "--untracked",
1208
1372
  "-E",
1209
1373
  "(eslint|oxlint)-disable"
1210
- ], {
1374
+ ];
1375
+ if (includePaths && includePaths.length > 0) grepArgs.push("--", ...includePaths);
1376
+ const result = spawnSync("git", grepArgs, {
1211
1377
  cwd: rootDirectory,
1212
1378
  encoding: "utf-8",
1213
1379
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
1214
1380
  });
1215
1381
  if (result.error || result.status === null) return [];
1382
+ if (result.status !== 0 && result.stdout.trim().length === 0) return [];
1216
1383
  return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
1217
1384
  };
1218
1385
  const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
1219
- const neutralizeDisableDirectives = (rootDirectory) => {
1220
- const filePaths = findFilesWithDisableDirectives(rootDirectory);
1386
+ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
1387
+ const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
1221
1388
  const originalContents = /* @__PURE__ */ new Map();
1222
1389
  for (const relativePath of filePaths) {
1223
1390
  const absolutePath = path.join(rootDirectory, relativePath);
@@ -1493,7 +1660,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
1493
1660
  framework,
1494
1661
  hasReactCompiler
1495
1662
  });
1496
- const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
1663
+ const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
1497
1664
  try {
1498
1665
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1499
1666
  const baseArgs = [
@@ -1780,7 +1947,7 @@ const mergeScanOptions = (inputOptions, userConfig) => ({
1780
1947
  offline: inputOptions.offline ?? false,
1781
1948
  includePaths: inputOptions.includePaths ?? []
1782
1949
  });
1783
- const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths) => {
1950
+ const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount) => {
1784
1951
  const frameworkLabel = formatFrameworkName(projectInfo.framework);
1785
1952
  const languageLabel = projectInfo.hasTypeScript ? "TypeScript" : "JavaScript";
1786
1953
  const completeStep = (message) => {
@@ -1791,7 +1958,7 @@ const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths
1791
1958
  completeStep(`Detecting language. Found ${highlighter.info(languageLabel)}.`);
1792
1959
  completeStep(`Detecting React Compiler. ${projectInfo.hasReactCompiler ? highlighter.info("Found React Compiler.") : "Not found."}`);
1793
1960
  if (isDiffMode) completeStep(`Scanning ${highlighter.info(`${includePaths.length}`)} changed source files.`);
1794
- else completeStep(`Found ${highlighter.info(`${projectInfo.sourceFileCount}`)} source files.`);
1961
+ else completeStep(`Found ${highlighter.info(`${lintSourceFileCount ?? projectInfo.sourceFileCount}`)} source files.`);
1795
1962
  if (userConfig) completeStep(`Loaded ${highlighter.info("react-doctor config")}.`);
1796
1963
  logger.break();
1797
1964
  };
@@ -1803,8 +1970,9 @@ const scan = async (directory, inputOptions = {}) => {
1803
1970
  const { includePaths } = options;
1804
1971
  const isDiffMode = includePaths.length > 0;
1805
1972
  if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
1806
- if (!options.scoreOnly) printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths);
1807
- const jsxIncludePaths = computeJsxIncludePaths(includePaths);
1973
+ const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(directory, userConfig);
1974
+ const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
1975
+ if (!options.scoreOnly) printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount);
1808
1976
  let didLintFail = false;
1809
1977
  let didDeadCodeFail = false;
1810
1978
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly);
@@ -1812,7 +1980,7 @@ const scan = async (directory, inputOptions = {}) => {
1812
1980
  const lintPromise = resolvedNodeBinaryPath ? (async () => {
1813
1981
  const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
1814
1982
  try {
1815
- const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths, resolvedNodeBinaryPath);
1983
+ const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths, resolvedNodeBinaryPath);
1816
1984
  lintSpinner?.succeed("Running lint checks.");
1817
1985
  return lintDiagnostics;
1818
1986
  } catch (error) {
@@ -1883,7 +2051,7 @@ const scan = async (directory, inputOptions = {}) => {
1883
2051
  };
1884
2052
  }
1885
2053
  printDiagnostics(diagnostics, options.verbose);
1886
- const displayedSourceFileCount = isDiffMode ? includePaths.length : projectInfo.sourceFileCount;
2054
+ const displayedSourceFileCount = isDiffMode ? includePaths.length : lintSourceFileCount;
1887
2055
  printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage, options.offline);
1888
2056
  if (hasSkippedChecks) {
1889
2057
  const skippedLabel = skippedChecks.join(" and ");
@@ -2211,7 +2379,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
2211
2379
 
2212
2380
  //#endregion
2213
2381
  //#region src/cli.ts
2214
- const VERSION = "0.0.30";
2382
+ const VERSION = "0.0.32";
2215
2383
  const VALID_FAIL_ON_LEVELS = new Set([
2216
2384
  "error",
2217
2385
  "warning",
@@ -2264,7 +2432,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
2264
2432
  if (effectiveDiff === false || !diffInfo) return false;
2265
2433
  const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
2266
2434
  if (changedSourceFiles.length === 0) return false;
2267
- if (shouldSkipPrompts) return true;
2435
+ if (shouldSkipPrompts) return false;
2268
2436
  if (isScoreOnly) return false;
2269
2437
  const { shouldScanChangedOnly } = await prompts({
2270
2438
  type: "confirm",