react-doctor 0.8.3 → 0.9.0-dev.30369b6

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]="0dac14aa-8d32-5d22-8e5e-1d8d793e05c0")}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]="b50e5764-57f0-54b7-81f8-530e6656066f")}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";
@@ -64656,6 +64656,13 @@ const IGNORED_DIRECTORIES = new Set([
64656
64656
  "storybook-static"
64657
64657
  ]);
64658
64658
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
64659
+ const TYPESCRIPT_DECLARATION_FILE_PATTERN = /\.d\.(?:ts|mts|cts)$/;
64660
+ const OXLINT_IGNORED_DECLARATION_DIAGNOSTIC_CODES = new Set([
64661
+ "TS(1036)",
64662
+ "TS(1038)",
64663
+ "TS(1183)",
64664
+ "TS(1319)"
64665
+ ]);
64659
64666
  const MILLISECONDS_PER_SECOND = 1e3;
64660
64667
  const REACT_ROUTER_CAPABILITY_THRESHOLDS = [
64661
64668
  {
@@ -64957,6 +64964,14 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
64957
64964
  "Maintainability"
64958
64965
  ];
64959
64966
  const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
64967
+ const CATEGORY_IMPACT_BY_NAME = new Map(Object.entries({
64968
+ Security: "An attacker can read user data, act as your users, or run code you never shipped.",
64969
+ Bugs: "Real users hit crashes, wrong output, or state that silently corrupts.",
64970
+ Performance: "Users feel extra latency and wasted renders on every interaction.",
64971
+ Accessibility: "Users on screen readers, keyboards, or assistive tech get locked out.",
64972
+ Maintainability: "Every future change gets slower and riskier to make."
64973
+ }));
64974
+ const getCategoryImpact = (category) => CATEGORY_IMPACT_BY_NAME.get(category);
64960
64975
  const APP_ONLY_RULE_KEYS = new Set([
64961
64976
  "react-hooks-js/static-components",
64962
64977
  "react-doctor/no-render-prop-children",
@@ -64979,7 +64994,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
64979
64994
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
64980
64995
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
64981
64996
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
64982
- const CORE_PACKAGE_VERSION = "0.8.3";
64997
+ const CORE_PACKAGE_VERSION = "0.9.0";
64983
64998
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
64984
64999
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
64985
65000
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -65364,6 +65379,7 @@ const frameworkMergeRank = (framework) => {
65364
65379
  return MOBILE_FRAMEWORKS.has(framework) ? 2 : 1;
65365
65380
  };
65366
65381
  const REACT_COMPILER_LINT_PACKAGES = new Set(["eslint-plugin-react-compiler"]);
65382
+ const REACT_COMPILER_RUNTIME_PACKAGES = new Set(["react-compiler-runtime"]);
65367
65383
  const NEXT_CONFIG_FILENAMES = [
65368
65384
  "next.config.js",
65369
65385
  "next.config.mjs",
@@ -65700,18 +65716,20 @@ const getReactCompilerFlagState = (expression, analysis) => {
65700
65716
  }
65701
65717
  return null;
65702
65718
  };
65703
- const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65719
+ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedExpressions = /* @__PURE__ */ new Set()) => {
65704
65720
  let resolvedExpression = expression;
65705
65721
  while (ts.isParenthesizedExpression(resolvedExpression) || ts.isAsExpression(resolvedExpression) || ts.isTypeAssertionExpression(resolvedExpression) || ts.isSatisfiesExpression(resolvedExpression) || ts.isNonNullExpression(resolvedExpression)) resolvedExpression = resolvedExpression.expression;
65722
+ if (visitedExpressions.has(resolvedExpression)) return null;
65723
+ visitedExpressions.add(resolvedExpression);
65706
65724
  if (ts.isIdentifier(resolvedExpression)) {
65707
65725
  if (analysis.localBindings.has(resolvedExpression.text)) {
65708
65726
  const localInitializer = analysis.localBindings.get(resolvedExpression.text);
65709
- return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis) : null;
65727
+ return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis, visitedExpressions) : null;
65710
65728
  }
65711
65729
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
65712
- if (scopedBinding.wasFound) return scopedBinding.initializer ? getSelectedObjectProperty(scopedBinding.initializer, propertyName, analysis) : null;
65730
+ if (scopedBinding.wasFound) return scopedBinding.initializer ? getSelectedObjectProperty(scopedBinding.initializer, propertyName, analysis, visitedExpressions) : null;
65713
65731
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, resolvedExpression.text);
65714
- return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis) : null;
65732
+ return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis, visitedExpressions) : null;
65715
65733
  }
65716
65734
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return null;
65717
65735
  for (const property of [...resolvedExpression.properties].reverse()) {
@@ -65719,7 +65737,7 @@ const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65719
65737
  if (ts.isMethodDeclaration(property) && getStaticPropertyName$1(property.name) === propertyName) return property;
65720
65738
  if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) return property.name;
65721
65739
  if (ts.isSpreadAssignment(property)) {
65722
- const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis);
65740
+ const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis, visitedExpressions);
65723
65741
  if (spreadProperty) return spreadProperty;
65724
65742
  }
65725
65743
  }
@@ -66068,7 +66086,7 @@ const hasCompilerConfigurationInAncestors = (directory) => {
66068
66086
  }
66069
66087
  return false;
66070
66088
  };
66071
- const detectReactCompiler = (directory, packageJson) => hasCompilerConfiguration(directory, packageJson) || hasCompilerConfigurationInAncestors(directory);
66089
+ const detectReactCompiler = (directory, packageJson) => hasCompilerPackage(packageJson, REACT_COMPILER_RUNTIME_PACKAGES) || hasCompilerPackageInAncestors(directory, REACT_COMPILER_RUNTIME_PACKAGES) || hasCompilerConfiguration(directory, packageJson) || hasCompilerConfigurationInAncestors(directory);
66072
66090
  const detectReactCompilerLintPlugin = (directory, packageJson) => hasCompilerPackage(packageJson, REACT_COMPILER_LINT_PACKAGES) || hasCompilerPackageInAncestors(directory, REACT_COMPILER_LINT_PACKAGES);
66073
66091
  const detectNextjsStaticExport = (directory) => NEXT_CONFIG_FILENAMES.some((filename) => {
66074
66092
  const filePath = Path.join(directory, filename);
@@ -73188,13 +73206,13 @@ const decodeFileDiagnostics = (raw) => {
73188
73206
  }
73189
73207
  };
73190
73208
  const emptyCache$1 = () => ({
73191
- version: 1,
73209
+ version: 2,
73192
73210
  rulesets: {}
73193
73211
  });
73194
73212
  const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
73195
73213
  const entries = /* @__PURE__ */ new Map();
73196
73214
  const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
73197
- if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
73215
+ if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
73198
73216
  const bucket = persisted.rulesets[rulesetHash];
73199
73217
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
73200
73218
  for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
@@ -73214,7 +73232,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
73214
73232
  },
73215
73233
  persist: () => {
73216
73234
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
73217
- const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73235
+ const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73218
73236
  const existingBucket = rulesets[rulesetHash];
73219
73237
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
73220
73238
  const ourFiles = {};
@@ -73228,7 +73246,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
73228
73246
  files: Object.fromEntries(cappedEntries)
73229
73247
  };
73230
73248
  atomicWriteJson(cacheFilePath, {
73231
- version: 1,
73249
+ version: 2,
73232
73250
  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))
73233
73251
  });
73234
73252
  }
@@ -73317,13 +73335,13 @@ const encodeSidecarFileEntry = (entry) => ({
73317
73335
  diagnostics: entry.diagnostics
73318
73336
  });
73319
73337
  const emptyCache = () => ({
73320
- version: 2,
73338
+ version: 3,
73321
73339
  rulesets: {}
73322
73340
  });
73323
73341
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
73324
73342
  const entries = /* @__PURE__ */ new Map();
73325
73343
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
73326
- if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
73344
+ if (persisted.version !== 3 || !isRecord$1(persisted.rulesets)) return entries;
73327
73345
  const bucket = persisted.rulesets[rulesetHash];
73328
73346
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
73329
73347
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -73343,7 +73361,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
73343
73361
  },
73344
73362
  persist: () => {
73345
73363
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
73346
- const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73364
+ const rulesets = onDisk.version === 3 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73347
73365
  const existingBucket = rulesets[rulesetHash];
73348
73366
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
73349
73367
  const ourFiles = {};
@@ -73357,7 +73375,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
73357
73375
  files: Object.fromEntries(cappedEntries)
73358
73376
  };
73359
73377
  atomicWriteJson(cacheFilePath, {
73360
- version: 2,
73378
+ version: 3,
73361
73379
  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))
73362
73380
  });
73363
73381
  }
@@ -74290,7 +74308,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
74290
74308
  minifiedFileCache.set(absolutePath, minified);
74291
74309
  return minified;
74292
74310
  };
74293
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
74311
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !(TYPESCRIPT_DECLARATION_FILE_PATTERN.test(diagnostic.filename) && OXLINT_IGNORED_DECLARATION_DIAGNOSTIC_CODES.has(diagnostic.code)) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
74294
74312
  const { plugin, rule } = parseRuleCode(diagnostic.code);
74295
74313
  const primaryLabel = diagnostic.labels[0];
74296
74314
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -77300,6 +77318,7 @@ const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
77300
77318
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
77301
77319
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
77302
77320
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
77321
+ const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
77303
77322
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
77304
77323
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
77305
77324
  const CATEGORY_COUNTUP_SETTLE_HOLD_MS = 1e3;
@@ -77391,7 +77410,7 @@ const makeNoopConsole = () => ({
77391
77410
  });
77392
77411
  //#endregion
77393
77412
  //#region src/cli/utils/version.ts
77394
- const VERSION = "0.8.3";
77413
+ const VERSION = "0.9.0-dev.30369b6";
77395
77414
  //#endregion
77396
77415
  //#region src/cli/utils/json-mode.ts
77397
77416
  let context = null;
@@ -77749,13 +77768,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
77749
77768
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
77750
77769
  * standard `SENTRY_RELEASE` override.
77751
77770
  */
77752
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.3`;
77771
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.0-dev.30369b6`;
77753
77772
  /**
77754
77773
  * Deployment environment shown in Sentry's environment filter. Defaults to
77755
77774
  * `production` for tagged releases and `development` for dev/unbuilt versions,
77756
77775
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
77757
77776
  */
77758
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.3") ? "development" : "production");
77777
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.0-dev.30369b6") ? "development" : "production");
77759
77778
  /**
77760
77779
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
77761
77780
  * (set to `0` to disable tracing) and falls back to
@@ -89602,12 +89621,13 @@ const buildRuntimeLayers = (input) => {
89602
89621
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
89603
89622
  const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
89604
89623
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
89605
- const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
89624
+ const progressLayer = input.progressLayer ?? (input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop);
89625
+ const reporterLayer = input.reporterLayer ?? Reporter.layerNoop;
89606
89626
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
89607
89627
  config: input.userConfig,
89608
89628
  resolvedDirectory: input.directory,
89609
89629
  configSourceDirectory: input.configSourceDirectory
89610
- }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, Reporter.layerNoop, scoreLayer, supplyChainLayer);
89630
+ }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
89611
89631
  return input.oxlintConcurrency === void 0 ? baseLayers : mergeAll$1(baseLayers, succeed$3(OxlintConcurrency, input.oxlintConcurrency));
89612
89632
  };
89613
89633
  //#endregion
@@ -89977,12 +89997,12 @@ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorC
89977
89997
  const readPersistedCache = (cacheFilePath) => {
89978
89998
  try {
89979
89999
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
89980
- if (!isRecord(parsed) || parsed.version !== 5) return {
89981
- version: 5,
90000
+ if (!isRecord(parsed) || parsed.version !== 6) return {
90001
+ version: 6,
89982
90002
  entries: []
89983
90003
  };
89984
90004
  if (!Array.isArray(parsed.entries)) return {
89985
- version: 5,
90005
+ version: 6,
89986
90006
  entries: []
89987
90007
  };
89988
90008
  const entries = [];
@@ -89992,12 +90012,12 @@ const readPersistedCache = (cacheFilePath) => {
89992
90012
  entries.push(entry);
89993
90013
  }
89994
90014
  return {
89995
- version: 5,
90015
+ version: 6,
89996
90016
  entries
89997
90017
  };
89998
90018
  } catch {
89999
90019
  return {
90000
- version: 5,
90020
+ version: 6,
90001
90021
  entries: []
90002
90022
  };
90003
90023
  }
@@ -90059,7 +90079,7 @@ const buildScanResultCacheKey = (input) => {
90059
90079
  if (headSha === null) return null;
90060
90080
  if (stringifyStableJson(input.userConfig) === null) return null;
90061
90081
  const cacheKeyJson = stringifyStableJson({
90062
- schemaVersion: 5,
90082
+ schemaVersion: 6,
90063
90083
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
90064
90084
  headSha,
90065
90085
  worktreeFingerprint,
@@ -90102,7 +90122,7 @@ const createScanResultCache = (projectDirectory) => {
90102
90122
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
90103
90123
  const persist = () => {
90104
90124
  writePersistedCache(cacheFilePath, {
90105
- version: 5,
90125
+ version: 6,
90106
90126
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
90107
90127
  });
90108
90128
  };
@@ -91442,6 +91462,9 @@ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
91442
91462
  * would choke on the escape sequences).
91443
91463
  */
91444
91464
  const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
91465
+ //#endregion
91466
+ //#region src/cli/utils/should-show-share-link.ts
91467
+ const shouldShowShareLink = ({ noScore, share, isCi }) => !noScore && share && !isCi;
91445
91468
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
91446
91469
  const FALSY_FLAG_VALUES = new Set([
91447
91470
  "",
@@ -91660,6 +91683,13 @@ const colorizeByScore = (text, score) => {
91660
91683
  return highlighter.error(text);
91661
91684
  };
91662
91685
  //#endregion
91686
+ //#region src/cli/utils/doctor-face.ts
91687
+ const doctorFace = (score) => {
91688
+ if (score >= 75) return ["◠ ◠", " ▽ "];
91689
+ if (score >= 50) return ["• •", " ─ "];
91690
+ return ["x x", " ▽ "];
91691
+ };
91692
+ //#endregion
91663
91693
  //#region src/cli/utils/ease-out-cubic.ts
91664
91694
  const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
91665
91695
  //#endregion
@@ -91725,15 +91755,10 @@ const buildProjectedScoreBar = (currentScore, potentialScore) => {
91725
91755
  const emptyCount = Math.max(0, barWidth - currentFill - gainCount);
91726
91756
  return colorizeByScore("█".repeat(currentFill), currentScore) + highlighter.dim(colorizeByScore("▓".repeat(gainCount), currentScore)) + highlighter.dim("░".repeat(emptyCount));
91727
91757
  };
91728
- const getDoctorFace = (score) => {
91729
- if (score >= 75) return ["◠ ◠", " ▽ "];
91730
- if (score >= 50) return ["• •", " ─ "];
91731
- return ["x x", " ▽ "];
91732
- };
91733
91758
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
91734
91759
  const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
91735
91760
  const buildRawFaceLines = (score) => {
91736
- const [eyes, mouth] = getDoctorFace(score);
91761
+ const [eyes, mouth] = doctorFace(score);
91737
91762
  return [
91738
91763
  FACE_BOX_TOP_BORDER,
91739
91764
  `│ ${eyes} │`,
@@ -92067,7 +92092,8 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
92067
92092
  includeTagDefaults: inputOptions.includeTagDefaults ?? false,
92068
92093
  scoreDisabledMessage: inputOptions.scoreDisabledMessage,
92069
92094
  outputSurface: inputOptions.outputSurface ?? "cli",
92070
- suppressRendering: inputOptions.suppressRendering ?? false,
92095
+ suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
92096
+ uiLayers: inputOptions.uiLayers ?? null,
92071
92097
  concurrentScan: inputOptions.concurrentScan ?? false,
92072
92098
  concurrency: inputOptions.concurrency,
92073
92099
  maxDurationMs: inputOptions.maxDurationMs ?? null,
@@ -92291,7 +92317,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92291
92317
  shouldRunSupplyChain: options.supplyChain,
92292
92318
  shouldComputeScore: !options.noScore,
92293
92319
  shouldShowProgressSpinners,
92294
- oxlintConcurrency: options.concurrency
92320
+ oxlintConcurrency: options.concurrency,
92321
+ reporterLayer: options.uiLayers?.reporter,
92322
+ progressLayer: options.uiLayers?.progress
92295
92323
  });
92296
92324
  const program = runInspect({
92297
92325
  directory,
@@ -92329,7 +92357,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92329
92357
  const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
92330
92358
  const didLintFail = lintBindingMissing || output.didLintFail;
92331
92359
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
92332
- if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
92360
+ if (!options.scoreOnly && !options.uiLayers && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
92333
92361
  else runConsole(error$1(highlighter.error(lintFailureReason)));
92334
92362
  let inspectDiagnostics = output.diagnostics;
92335
92363
  let baselineDelta;
@@ -92564,7 +92592,7 @@ const finalizeAndRender = (input) => gen(function* () {
92564
92592
  const scoreDiagnostics = filterDiagnosticsForSurface([...diagnostics], "score", userConfig);
92565
92593
  const displayedScoreDiagnostics = filterDiagnosticsForSurface([...printedDiagnostics], "score", userConfig);
92566
92594
  const potentialScore = score ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, scoreDiagnostics, score)) : null;
92567
- const shouldShowShareLink = !options.noScore && options.share && !options.isCi;
92595
+ const showShareLink = shouldShowShareLink(options);
92568
92596
  yield* pause;
92569
92597
  yield* printSummary({
92570
92598
  diagnostics: [...printedDiagnostics],
@@ -92587,7 +92615,7 @@ const finalizeAndRender = (input) => gen(function* () {
92587
92615
  diagnostics: [...printedDiagnostics],
92588
92616
  scoreResult: score,
92589
92617
  projectName: project.projectName,
92590
- isOffline: !shouldShowShareLink
92618
+ isOffline: !showShareLink
92591
92619
  });
92592
92620
  return buildResult();
92593
92621
  });
@@ -92697,6 +92725,127 @@ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
92697
92725
  return detectedAgents.length === 1 ? [...detectedAgents] : [];
92698
92726
  };
92699
92727
  //#endregion
92728
+ //#region src/cli/utils/launch-agent.ts
92729
+ const isWindows = process.platform === "win32";
92730
+ const CLI_AGENT_IDS = [
92731
+ "claude-code",
92732
+ "codex",
92733
+ "cursor"
92734
+ ];
92735
+ const CLI_AGENT_BINARIES = {
92736
+ "claude-code": "claude",
92737
+ codex: "codex",
92738
+ cursor: "cursor-agent"
92739
+ };
92740
+ const CLI_AGENT_AUTO_FLAGS = {
92741
+ "claude-code": ["--dangerously-skip-permissions"],
92742
+ codex: ["--yolo"],
92743
+ cursor: ["--force"]
92744
+ };
92745
+ const resolveCursorBundledNode = () => {
92746
+ if (!isWindows) return null;
92747
+ const localAppData = process.env.LOCALAPPDATA;
92748
+ if (!localAppData) return null;
92749
+ const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
92750
+ try {
92751
+ const versions = NFS.readdirSync(cursorAgentRoot);
92752
+ if (versions.length === 0) return null;
92753
+ const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
92754
+ for (const version of sortedVersions) try {
92755
+ const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
92756
+ const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
92757
+ if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
92758
+ command: bundledNodePath,
92759
+ args: [entryScriptPath]
92760
+ };
92761
+ } catch {}
92762
+ } catch {}
92763
+ return null;
92764
+ };
92765
+ const resolveWindowsCmdEntryScript = (command) => {
92766
+ if (!isWindows) return null;
92767
+ const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
92768
+ for (const directory of pathDirectories) {
92769
+ const cmdFilePath = Path.join(directory, `${command}.cmd`);
92770
+ try {
92771
+ if (!NFS.statSync(cmdFilePath).isFile()) continue;
92772
+ } catch {
92773
+ continue;
92774
+ }
92775
+ try {
92776
+ const entryScriptMatch = NFS.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
92777
+ if (!entryScriptMatch) continue;
92778
+ const resolvedScriptPath = Path.resolve(directory, entryScriptMatch[1]);
92779
+ if (NFS.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
92780
+ } catch {}
92781
+ }
92782
+ return null;
92783
+ };
92784
+ const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
92785
+ const child = spawn(command, [...args], {
92786
+ cwd,
92787
+ stdio: "inherit"
92788
+ });
92789
+ child.on("error", reject);
92790
+ child.on("close", (code) => resolve(code ?? 0));
92791
+ });
92792
+ const launchCliAgent = async (agentId, prompt, cwd) => {
92793
+ const binary = CLI_AGENT_BINARIES[agentId];
92794
+ const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
92795
+ if (isWindows) {
92796
+ if (agentId === "cursor") {
92797
+ const cursorResolution = resolveCursorBundledNode();
92798
+ if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
92799
+ }
92800
+ const entryScript = resolveWindowsCmdEntryScript(binary);
92801
+ if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
92802
+ }
92803
+ try {
92804
+ return await spawnAgent(binary, agentArgs, cwd);
92805
+ } catch {
92806
+ throw new Error(`Failed to launch ${binary}`);
92807
+ }
92808
+ };
92809
+ const CLIPBOARD_COMMANDS = [
92810
+ {
92811
+ binary: "pbcopy",
92812
+ args: []
92813
+ },
92814
+ {
92815
+ binary: "wl-copy",
92816
+ args: []
92817
+ },
92818
+ {
92819
+ binary: "xclip",
92820
+ args: ["-selection", "clipboard"]
92821
+ },
92822
+ {
92823
+ binary: "xsel",
92824
+ args: ["--clipboard", "--input"]
92825
+ },
92826
+ {
92827
+ binary: "clip",
92828
+ args: []
92829
+ }
92830
+ ];
92831
+ const copyToClipboard = (text) => {
92832
+ const command = CLIPBOARD_COMMANDS.find((candidate) => isCommandAvailable(candidate.binary));
92833
+ if (!command) return Promise.resolve(false);
92834
+ return new Promise((resolve) => {
92835
+ const child = spawn(command.binary, command.args);
92836
+ child.on("error", () => resolve(false));
92837
+ child.on("close", (code) => resolve(code === 0));
92838
+ child.stdin.on("error", () => resolve(false));
92839
+ child.stdin.end(text);
92840
+ });
92841
+ };
92842
+ //#endregion
92843
+ //#region src/cli/utils/detect-launchable-agents.ts
92844
+ const detectLaunchableAgents = async () => {
92845
+ const detected = new Set(await detectAvailableAgents());
92846
+ return CLI_AGENT_IDS.filter((agentId) => detected.has(agentId) && isCommandAvailable(CLI_AGENT_BINARIES[agentId]));
92847
+ };
92848
+ //#endregion
92700
92849
  //#region src/cli/utils/action-upgrade-prompt.ts
92701
92850
  const ACTION_UPGRADE_GATE = {
92702
92851
  id: ACTION_UPGRADE_EVENT,
@@ -93939,116 +94088,6 @@ const installReactDoctorSkillForAgent = async (agent, projectRoot) => {
93939
94088
  return result.skills.length > 0 && result.failed.length === 0;
93940
94089
  };
93941
94090
  //#endregion
93942
- //#region src/cli/utils/launch-agent.ts
93943
- const isWindows = process.platform === "win32";
93944
- const CLI_AGENT_BINARIES = {
93945
- "claude-code": "claude",
93946
- codex: "codex",
93947
- cursor: "cursor-agent"
93948
- };
93949
- const CLI_AGENT_AUTO_FLAGS = {
93950
- "claude-code": ["--dangerously-skip-permissions"],
93951
- codex: ["--yolo"],
93952
- cursor: ["--force"]
93953
- };
93954
- const resolveCursorBundledNode = () => {
93955
- if (!isWindows) return null;
93956
- const localAppData = process.env.LOCALAPPDATA;
93957
- if (!localAppData) return null;
93958
- const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
93959
- try {
93960
- const versions = NFS.readdirSync(cursorAgentRoot);
93961
- if (versions.length === 0) return null;
93962
- const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
93963
- for (const version of sortedVersions) try {
93964
- const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
93965
- const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
93966
- if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
93967
- command: bundledNodePath,
93968
- args: [entryScriptPath]
93969
- };
93970
- } catch {}
93971
- } catch {}
93972
- return null;
93973
- };
93974
- const resolveWindowsCmdEntryScript = (command) => {
93975
- if (!isWindows) return null;
93976
- const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
93977
- for (const directory of pathDirectories) {
93978
- const cmdFilePath = Path.join(directory, `${command}.cmd`);
93979
- try {
93980
- if (!NFS.statSync(cmdFilePath).isFile()) continue;
93981
- } catch {
93982
- continue;
93983
- }
93984
- try {
93985
- const entryScriptMatch = NFS.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
93986
- if (!entryScriptMatch) continue;
93987
- const resolvedScriptPath = Path.resolve(directory, entryScriptMatch[1]);
93988
- if (NFS.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
93989
- } catch {}
93990
- }
93991
- return null;
93992
- };
93993
- const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
93994
- const child = spawn(command, [...args], {
93995
- cwd,
93996
- stdio: "inherit"
93997
- });
93998
- child.on("error", reject);
93999
- child.on("close", (code) => resolve(code ?? 0));
94000
- });
94001
- const launchCliAgent = async (agentId, prompt, cwd) => {
94002
- const binary = CLI_AGENT_BINARIES[agentId];
94003
- const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
94004
- if (isWindows) {
94005
- if (agentId === "cursor") {
94006
- const cursorResolution = resolveCursorBundledNode();
94007
- if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
94008
- }
94009
- const entryScript = resolveWindowsCmdEntryScript(binary);
94010
- if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
94011
- }
94012
- try {
94013
- return await spawnAgent(binary, agentArgs, cwd);
94014
- } catch {
94015
- throw new Error(`Failed to launch ${binary}`);
94016
- }
94017
- };
94018
- const CLIPBOARD_COMMANDS = [
94019
- {
94020
- binary: "pbcopy",
94021
- args: []
94022
- },
94023
- {
94024
- binary: "wl-copy",
94025
- args: []
94026
- },
94027
- {
94028
- binary: "xclip",
94029
- args: ["-selection", "clipboard"]
94030
- },
94031
- {
94032
- binary: "xsel",
94033
- args: ["--clipboard", "--input"]
94034
- },
94035
- {
94036
- binary: "clip",
94037
- args: []
94038
- }
94039
- ];
94040
- const copyToClipboard = (text) => {
94041
- const command = CLIPBOARD_COMMANDS.find((candidate) => isCommandAvailable(candidate.binary));
94042
- if (!command) return Promise.resolve(false);
94043
- return new Promise((resolve) => {
94044
- const child = spawn(command.binary, command.args);
94045
- child.on("error", () => resolve(false));
94046
- child.on("close", (code) => resolve(code === 0));
94047
- child.stdin.on("error", () => resolve(false));
94048
- child.stdin.end(text);
94049
- });
94050
- };
94051
- //#endregion
94052
94091
  //#region src/cli/utils/handoff-to-agent.ts
94053
94092
  const CLIPBOARD_CHOICE = "clipboard";
94054
94093
  const SKIP_CHOICE = "skip";
@@ -94112,10 +94151,6 @@ const maybeOfferActionUpgrade = async (projectRoot) => {
94112
94151
  }
94113
94152
  if (await upgradeGitHubActionsWorkflow(workflow)) recordActionUpgradeDecision(projectRoot, "accepted");
94114
94153
  };
94115
- const detectLaunchableAgents = async () => {
94116
- const detected = new Set(await detectAvailableAgents());
94117
- return Object.keys(CLI_AGENT_BINARIES).filter((agentId) => detected.has(agentId) && isCommandAvailable(CLI_AGENT_BINARIES[agentId]));
94118
- };
94119
94154
  const handoffToAgent = async (input) => {
94120
94155
  if (!input.interactive || input.diagnostics.length === 0) return;
94121
94156
  cliLogger.break();
@@ -94430,15 +94465,29 @@ const readChangedFilesFrom = (filePath) => {
94430
94465
  return [...uniqueFiles];
94431
94466
  };
94432
94467
  //#endregion
94433
- //#region src/cli/utils/filter-scans-for-surface.ts
94434
- const filterScansForSurface = (completedScans, surface) => completedScans.flatMap((scan) => filterDiagnosticsForSurface([...scan.result.diagnostics], surface, scan.config));
94468
+ //#region src/cli/utils/count-unique-scanned-files.ts
94469
+ const countUniqueScannedFiles = (results) => {
94470
+ const uniqueScannedFilePaths = /* @__PURE__ */ new Set();
94471
+ let fileCountFromScansWithoutPaths = 0;
94472
+ for (const result of results) {
94473
+ const scannedFilePaths = result.scannedFilePaths;
94474
+ if (scannedFilePaths && scannedFilePaths.length > 0) for (const filePath of scannedFilePaths) uniqueScannedFilePaths.add(filePath);
94475
+ else fileCountFromScansWithoutPaths += result.scannedFileCount ?? result.project.sourceFileCount;
94476
+ }
94477
+ return uniqueScannedFilePaths.size + fileCountFromScansWithoutPaths;
94478
+ };
94435
94479
  //#endregion
94436
- //#region src/cli/utils/render-multi-project-summary.ts
94437
- const getScoreLabel = (score) => {
94480
+ //#region src/cli/utils/score-band-label.ts
94481
+ const scoreBandLabel = (score) => {
94438
94482
  if (score >= 75) return "Great";
94439
94483
  if (score >= 50) return "OK";
94440
94484
  return "Needs work";
94441
94485
  };
94486
+ //#endregion
94487
+ //#region src/cli/utils/filter-scans-for-surface.ts
94488
+ const filterScansForSurface = (completedScans, surface) => completedScans.flatMap((scan) => filterDiagnosticsForSurface([...scan.result.diagnostics], surface, scan.config));
94489
+ //#endregion
94490
+ //#region src/cli/utils/render-multi-project-summary.ts
94442
94491
  const buildSummaryLine = (entry, longestProjectNameLength) => {
94443
94492
  const paddedName = entry.projectName.padEnd(longestProjectNameLength);
94444
94493
  const nameRendering = entry.score !== null ? colorizeByScore(paddedName, entry.score) : highlighter.dim(paddedName);
@@ -94447,7 +94496,7 @@ const buildSummaryLine = (entry, longestProjectNameLength) => {
94447
94496
  return ` ${nameRendering} ${highlighter.dim("no score")} ${highlighter.dim(issueLabel)}`;
94448
94497
  }
94449
94498
  const scoreRendering = colorizeByScore(String(entry.score).padStart(3), entry.score);
94450
- const label = colorizeByScore(getScoreLabel(entry.score), entry.score);
94499
+ const label = colorizeByScore(scoreBandLabel(entry.score), entry.score);
94451
94500
  const issuesParts = [];
94452
94501
  if (entry.errorCount > 0) issuesParts.push(highlighter.error(`${entry.errorCount} ${entry.errorCount === 1 ? "error" : "errors"}`));
94453
94502
  const warningCount = entry.issueCount - entry.errorCount;
@@ -94469,14 +94518,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
94469
94518
  const projectRootByDiagnostic = /* @__PURE__ */ new WeakMap();
94470
94519
  for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
94471
94520
  const resolveDiagnosticSourceRoot = (diagnostic) => projectRootByDiagnostic.get(diagnostic) ?? "";
94472
- const uniqueScannedFilePaths = /* @__PURE__ */ new Set();
94473
- let fileCountFromScansWithoutPaths = 0;
94474
- for (const scan of completedScans) {
94475
- const scannedFilePaths = scan.result.scannedFilePaths;
94476
- if (scannedFilePaths && scannedFilePaths.length > 0) for (const filePath of scannedFilePaths) uniqueScannedFilePaths.add(filePath);
94477
- else fileCountFromScansWithoutPaths += scan.result.scannedFileCount ?? scan.result.project.sourceFileCount;
94478
- }
94479
- const totalScannedFileCount = uniqueScannedFilePaths.size + fileCountFromScansWithoutPaths;
94521
+ const totalScannedFileCount = countUniqueScannedFiles(completedScans.map((scan) => scan.result));
94480
94522
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
94481
94523
  if (displayDiagnostics.length > 0) {
94482
94524
  yield* log("");
@@ -94748,10 +94790,14 @@ const parseFileLineArgument = (rawArgument) => {
94748
94790
  };
94749
94791
  //#endregion
94750
94792
  //#region src/cli/utils/select-projects.ts
94751
- const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
94793
+ const discoverWorkspacePackages = (rootDirectory) => {
94752
94794
  const hasRootPackageJson = isFile(Path.join(rootDirectory, "package.json"));
94753
- let packages = listWorkspacePackages(rootDirectory);
94754
- if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) packages = discoverReactSubprojects(rootDirectory);
94795
+ const packages = listWorkspacePackages(rootDirectory);
94796
+ if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) return discoverReactSubprojects(rootDirectory);
94797
+ return packages;
94798
+ };
94799
+ const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
94800
+ const packages = discoverWorkspacePackages(rootDirectory);
94755
94801
  if (projectFlag) return resolveProjectFlag(projectFlag, packages, rootDirectory);
94756
94802
  const configRequestedNames = (configProjects ?? []).map((requestedName) => requestedName.trim()).filter((requestedName) => requestedName.length > 0);
94757
94803
  if (configRequestedNames.length > 0) {
@@ -94880,6 +94926,14 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
94880
94926
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
94881
94927
  };
94882
94928
  //#endregion
94929
+ //#region src/cli/utils/should-fail-scan-gate.ts
94930
+ const shouldFailScanGate = (input) => {
94931
+ if (input.blockingLevel === "none") return false;
94932
+ if (input.scans.some(({ result }) => hasLintHardFailure(result))) return true;
94933
+ if (input.diagnosticsAreGateExempt === true) return false;
94934
+ return shouldBlockCi(filterScansForSurface(input.scans, "ciFailure"), input.blockingLevel);
94935
+ };
94936
+ //#endregion
94883
94937
  //#region src/cli/utils/warn-deprecated-fail-on.ts
94884
94938
  const warnDeprecatedFailOn = (flags, userConfig) => {
94885
94939
  const usedFlag = flags.failOn !== void 0;
@@ -95006,12 +95060,11 @@ const finalizeScans = (input) => {
95006
95060
  }));
95007
95061
  }
95008
95062
  const blockingLevel = resolveBlockingLevel(input.flags, input.userConfig);
95009
- if (input.completedScans.some(({ result }) => hasLintHardFailure(result)) && blockingLevel !== "none") {
95010
- process.exitCode = 1;
95011
- return;
95012
- }
95013
- if (input.isScoreOnly || baselineDegraded) return;
95014
- if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), blockingLevel)) process.exitCode = 1;
95063
+ if (shouldFailScanGate({
95064
+ scans: input.completedScans,
95065
+ blockingLevel,
95066
+ diagnosticsAreGateExempt: input.isScoreOnly || baselineDegraded
95067
+ })) process.exitCode = 1;
95015
95068
  };
95016
95069
  const buildChangedFilesDiffInfo = (changedFiles) => ({
95017
95070
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -95287,13 +95340,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
95287
95340
  completedScans.push(scanOutcome);
95288
95341
  }
95289
95342
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
95290
- const shouldShowShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95343
+ const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95291
95344
  await runPromise(printMultiProjectSummary({
95292
95345
  completedScans,
95293
95346
  categoryFilters,
95294
95347
  verbose: Boolean(flags.verbose),
95295
95348
  outputDirectory: flags.outputDir,
95296
- isOffline: !shouldShowShareLink,
95349
+ isOffline: !showShareLink,
95297
95350
  projectName: Path.basename(resolvedDirectory),
95298
95351
  totalElapsedMilliseconds: performance$1.now() - scanLoopStartTime
95299
95352
  }));
@@ -96268,6 +96321,20 @@ const COMMAND_FLAG_SPECS = new Map([
96268
96321
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
96269
96322
  shortOptionsWithoutValues: new Set(["-h"]),
96270
96323
  shortOptionsWithRequiredValues: new Set(["-c"])
96324
+ }],
96325
+ ["experimental-tui", {
96326
+ longOptionsWithoutValues: new Set([
96327
+ "--color",
96328
+ "--help",
96329
+ "--no-color",
96330
+ "--no-dead-code",
96331
+ "--no-score",
96332
+ "--yes"
96333
+ ]),
96334
+ longOptionsWithRequiredValues: new Set(["--blocking", "--project"]),
96335
+ longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
96336
+ shortOptionsWithoutValues: new Set(["-h", "-y"]),
96337
+ shortOptionsWithRequiredValues: new Set(["-p"])
96271
96338
  }]
96272
96339
  ]);
96273
96340
  const isFlagLike = (argument) => argument.startsWith("-") && argument !== "-";
@@ -96402,7 +96469,7 @@ ${formatExampleLines([
96402
96469
  ])}
96403
96470
 
96404
96471
  ${highlighter.dim("Scope:")}
96405
- Runs only rules tagged ${highlighter.info("design")}, including focused rules that stay opt-in during a general health scan.
96472
+ Runs every rule tagged ${highlighter.info("design")}; all design rules stay opt-in during a general health scan.
96406
96473
  Dead-code, supply-chain, external lint-config, custom-plugin, and health-score passes are skipped.
96407
96474
  Standard scan flags such as ${highlighter.info("--scope")}, ${highlighter.info("--project")}, ${highlighter.info("--verbose")}, and ${highlighter.info("--json")} still work.
96408
96475
  `;
@@ -96447,6 +96514,35 @@ rules.command("category <category> <severity>").description("Set the severity fo
96447
96514
  rules.command("ignore-tag <tag>").description("Skip a whole rule family by tag before linting (e.g. design)").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesIgnoreTagAction(tag, command.optsWithGlobals()));
96448
96515
  rules.command("unignore-tag <tag>").description("Stop ignoring a tag previously skipped via ignore-tag").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesUnignoreTagAction(tag, command.optsWithGlobals()));
96449
96516
  program.command("experimental-lsp", { hidden: false }).description("[experimental] run the React Doctor language server over stdio (for editors)").allowUnknownOption().action(() => {});
96517
+ program.command("experimental-tui [directory]", { hidden: true }).description("[experimental] interactive, scrollable scan report").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").option("--no-dead-code", "skip dead-code analysis").option("--no-score", "skip the score API, the share URL, and crash reporting").option("-p, --project <names>", "scan specific workspace projects (comma-separated, or *)").option("-y, --yes", "skip the project prompt and scan every discovered project").action(async (directory = ".", _localOptions, command) => {
96518
+ const options = command.optsWithGlobals();
96519
+ const deadCode = options.deadCode === false ? false : void 0;
96520
+ const noScore = options.score === false ? true : void 0;
96521
+ const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
96522
+ if (process.stdout.isTTY !== true || process.stdin.isTTY !== true || isNonInteractiveEnvironment() || nodeMajorVersion < 22) {
96523
+ await inspectAction(directory, {
96524
+ deadCode,
96525
+ score: options.score === false ? false : void 0,
96526
+ project: options.project,
96527
+ yes: options.yes,
96528
+ blocking: options.blocking
96529
+ });
96530
+ return;
96531
+ }
96532
+ recordCount(METRIC.cliInvoked, 1, { command: "experimental-tui" });
96533
+ const { runScanApp } = await import("./run-scan-app-DZ5jGWFf.js");
96534
+ const { shouldFail } = await runScanApp({
96535
+ directory,
96536
+ options: {
96537
+ deadCode,
96538
+ noScore
96539
+ },
96540
+ projectFlag: options.project,
96541
+ skipPrompts: options.yes ?? false,
96542
+ blocking: options.blocking
96543
+ });
96544
+ if (shouldFail) process.exitCode = 1;
96545
+ });
96450
96546
  process.stdout.on("error", (error) => {
96451
96547
  if (error.code === "EPIPE") process.exit(0);
96452
96548
  });
@@ -96473,7 +96569,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
96473
96569
  handleError(error, { sentryEventId });
96474
96570
  });
96475
96571
  //#endregion
96476
- export {};
96572
+ export { toForwardSlashes as A, highlighter as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, isCiEnvironment as F, sync as G, mergeReactDoctorConfigs as H, DIAGNOSTIC_CATEGORY_BUCKETS as I, void_ as K, Progress as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_DETAIL_WIDTH_FRACTION as P, Reporter as R, hasLintHardFailure as S, formatFixRecipeLine as T, resolveScanTarget as U, mapWithConcurrency as V, runPromise as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, inspect as p, succeed$3 as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, getCategoryImpact as z };
96477
96573
 
96478
96574
  //# sourceMappingURL=cli.js.map
96479
- //# debugId=0dac14aa-8d32-5d22-8e5e-1d8d793e05c0
96575
+ //# debugId=b50e5764-57f0-54b7-81f8-530e6656066f