react-doctor 0.8.3-dev.84b9990 → 0.8.3-dev.9155bc0

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]="1ac2e899-2304-5ed1-be9d-6cd66a730b6e")}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]="be7f877d-9463-5212-8e1f-dc5feea97da0")}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",
@@ -65700,18 +65715,20 @@ const getReactCompilerFlagState = (expression, analysis) => {
65700
65715
  }
65701
65716
  return null;
65702
65717
  };
65703
- const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65718
+ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedExpressions = /* @__PURE__ */ new Set()) => {
65704
65719
  let resolvedExpression = expression;
65705
65720
  while (ts.isParenthesizedExpression(resolvedExpression) || ts.isAsExpression(resolvedExpression) || ts.isTypeAssertionExpression(resolvedExpression) || ts.isSatisfiesExpression(resolvedExpression) || ts.isNonNullExpression(resolvedExpression)) resolvedExpression = resolvedExpression.expression;
65721
+ if (visitedExpressions.has(resolvedExpression)) return null;
65722
+ visitedExpressions.add(resolvedExpression);
65706
65723
  if (ts.isIdentifier(resolvedExpression)) {
65707
65724
  if (analysis.localBindings.has(resolvedExpression.text)) {
65708
65725
  const localInitializer = analysis.localBindings.get(resolvedExpression.text);
65709
- return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis) : null;
65726
+ return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis, visitedExpressions) : null;
65710
65727
  }
65711
65728
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
65712
- if (scopedBinding.wasFound) return scopedBinding.initializer ? getSelectedObjectProperty(scopedBinding.initializer, propertyName, analysis) : null;
65729
+ if (scopedBinding.wasFound) return scopedBinding.initializer ? getSelectedObjectProperty(scopedBinding.initializer, propertyName, analysis, visitedExpressions) : null;
65713
65730
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, resolvedExpression.text);
65714
- return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis) : null;
65731
+ return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis, visitedExpressions) : null;
65715
65732
  }
65716
65733
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return null;
65717
65734
  for (const property of [...resolvedExpression.properties].reverse()) {
@@ -65719,7 +65736,7 @@ const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65719
65736
  if (ts.isMethodDeclaration(property) && getStaticPropertyName$1(property.name) === propertyName) return property;
65720
65737
  if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) return property.name;
65721
65738
  if (ts.isSpreadAssignment(property)) {
65722
- const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis);
65739
+ const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis, visitedExpressions);
65723
65740
  if (spreadProperty) return spreadProperty;
65724
65741
  }
65725
65742
  }
@@ -73188,13 +73205,13 @@ const decodeFileDiagnostics = (raw) => {
73188
73205
  }
73189
73206
  };
73190
73207
  const emptyCache$1 = () => ({
73191
- version: 1,
73208
+ version: 2,
73192
73209
  rulesets: {}
73193
73210
  });
73194
73211
  const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
73195
73212
  const entries = /* @__PURE__ */ new Map();
73196
73213
  const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
73197
- if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
73214
+ if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
73198
73215
  const bucket = persisted.rulesets[rulesetHash];
73199
73216
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
73200
73217
  for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
@@ -73214,7 +73231,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
73214
73231
  },
73215
73232
  persist: () => {
73216
73233
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
73217
- const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73234
+ const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73218
73235
  const existingBucket = rulesets[rulesetHash];
73219
73236
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
73220
73237
  const ourFiles = {};
@@ -73228,7 +73245,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
73228
73245
  files: Object.fromEntries(cappedEntries)
73229
73246
  };
73230
73247
  atomicWriteJson(cacheFilePath, {
73231
- version: 1,
73248
+ version: 2,
73232
73249
  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
73250
  });
73234
73251
  }
@@ -73317,13 +73334,13 @@ const encodeSidecarFileEntry = (entry) => ({
73317
73334
  diagnostics: entry.diagnostics
73318
73335
  });
73319
73336
  const emptyCache = () => ({
73320
- version: 2,
73337
+ version: 3,
73321
73338
  rulesets: {}
73322
73339
  });
73323
73340
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
73324
73341
  const entries = /* @__PURE__ */ new Map();
73325
73342
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
73326
- if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
73343
+ if (persisted.version !== 3 || !isRecord$1(persisted.rulesets)) return entries;
73327
73344
  const bucket = persisted.rulesets[rulesetHash];
73328
73345
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
73329
73346
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -73343,7 +73360,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
73343
73360
  },
73344
73361
  persist: () => {
73345
73362
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
73346
- const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73363
+ const rulesets = onDisk.version === 3 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
73347
73364
  const existingBucket = rulesets[rulesetHash];
73348
73365
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
73349
73366
  const ourFiles = {};
@@ -73357,7 +73374,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
73357
73374
  files: Object.fromEntries(cappedEntries)
73358
73375
  };
73359
73376
  atomicWriteJson(cacheFilePath, {
73360
- version: 2,
73377
+ version: 3,
73361
73378
  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
73379
  });
73363
73380
  }
@@ -74290,7 +74307,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
74290
74307
  minifiedFileCache.set(absolutePath, minified);
74291
74308
  return minified;
74292
74309
  };
74293
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
74310
+ 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
74311
  const { plugin, rule } = parseRuleCode(diagnostic.code);
74295
74312
  const primaryLabel = diagnostic.labels[0];
74296
74313
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -77300,6 +77317,7 @@ const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
77300
77317
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
77301
77318
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
77302
77319
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
77320
+ const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
77303
77321
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
77304
77322
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
77305
77323
  const CATEGORY_COUNTUP_SETTLE_HOLD_MS = 1e3;
@@ -77391,7 +77409,7 @@ const makeNoopConsole = () => ({
77391
77409
  });
77392
77410
  //#endregion
77393
77411
  //#region src/cli/utils/version.ts
77394
- const VERSION = "0.8.3-dev.84b9990";
77412
+ const VERSION = "0.8.3-dev.9155bc0";
77395
77413
  //#endregion
77396
77414
  //#region src/cli/utils/json-mode.ts
77397
77415
  let context = null;
@@ -77749,13 +77767,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
77749
77767
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
77750
77768
  * standard `SENTRY_RELEASE` override.
77751
77769
  */
77752
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.3-dev.84b9990`;
77770
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.3-dev.9155bc0`;
77753
77771
  /**
77754
77772
  * Deployment environment shown in Sentry's environment filter. Defaults to
77755
77773
  * `production` for tagged releases and `development` for dev/unbuilt versions,
77756
77774
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
77757
77775
  */
77758
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.3-dev.84b9990") ? "development" : "production");
77776
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.3-dev.9155bc0") ? "development" : "production");
77759
77777
  /**
77760
77778
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
77761
77779
  * (set to `0` to disable tracing) and falls back to
@@ -89602,12 +89620,13 @@ const buildRuntimeLayers = (input) => {
89602
89620
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
89603
89621
  const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
89604
89622
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
89605
- const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
89623
+ const progressLayer = input.progressLayer ?? (input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop);
89624
+ const reporterLayer = input.reporterLayer ?? Reporter.layerNoop;
89606
89625
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
89607
89626
  config: input.userConfig,
89608
89627
  resolvedDirectory: input.directory,
89609
89628
  configSourceDirectory: input.configSourceDirectory
89610
- }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, Reporter.layerNoop, scoreLayer, supplyChainLayer);
89629
+ }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
89611
89630
  return input.oxlintConcurrency === void 0 ? baseLayers : mergeAll$1(baseLayers, succeed$3(OxlintConcurrency, input.oxlintConcurrency));
89612
89631
  };
89613
89632
  //#endregion
@@ -89977,12 +89996,12 @@ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorC
89977
89996
  const readPersistedCache = (cacheFilePath) => {
89978
89997
  try {
89979
89998
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
89980
- if (!isRecord(parsed) || parsed.version !== 5) return {
89981
- version: 5,
89999
+ if (!isRecord(parsed) || parsed.version !== 6) return {
90000
+ version: 6,
89982
90001
  entries: []
89983
90002
  };
89984
90003
  if (!Array.isArray(parsed.entries)) return {
89985
- version: 5,
90004
+ version: 6,
89986
90005
  entries: []
89987
90006
  };
89988
90007
  const entries = [];
@@ -89992,12 +90011,12 @@ const readPersistedCache = (cacheFilePath) => {
89992
90011
  entries.push(entry);
89993
90012
  }
89994
90013
  return {
89995
- version: 5,
90014
+ version: 6,
89996
90015
  entries
89997
90016
  };
89998
90017
  } catch {
89999
90018
  return {
90000
- version: 5,
90019
+ version: 6,
90001
90020
  entries: []
90002
90021
  };
90003
90022
  }
@@ -90059,7 +90078,7 @@ const buildScanResultCacheKey = (input) => {
90059
90078
  if (headSha === null) return null;
90060
90079
  if (stringifyStableJson(input.userConfig) === null) return null;
90061
90080
  const cacheKeyJson = stringifyStableJson({
90062
- schemaVersion: 5,
90081
+ schemaVersion: 6,
90063
90082
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
90064
90083
  headSha,
90065
90084
  worktreeFingerprint,
@@ -90102,7 +90121,7 @@ const createScanResultCache = (projectDirectory) => {
90102
90121
  for (const entry of persistedCache.entries) entries.set(entry.key, entry);
90103
90122
  const persist = () => {
90104
90123
  writePersistedCache(cacheFilePath, {
90105
- version: 5,
90124
+ version: 6,
90106
90125
  entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
90107
90126
  });
90108
90127
  };
@@ -91442,6 +91461,9 @@ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
91442
91461
  * would choke on the escape sequences).
91443
91462
  */
91444
91463
  const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
91464
+ //#endregion
91465
+ //#region src/cli/utils/should-show-share-link.ts
91466
+ const shouldShowShareLink = ({ noScore, share, isCi }) => !noScore && share && !isCi;
91445
91467
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
91446
91468
  const FALSY_FLAG_VALUES = new Set([
91447
91469
  "",
@@ -91660,6 +91682,13 @@ const colorizeByScore = (text, score) => {
91660
91682
  return highlighter.error(text);
91661
91683
  };
91662
91684
  //#endregion
91685
+ //#region src/cli/utils/doctor-face.ts
91686
+ const doctorFace = (score) => {
91687
+ if (score >= 75) return ["◠ ◠", " ▽ "];
91688
+ if (score >= 50) return ["• •", " ─ "];
91689
+ return ["x x", " ▽ "];
91690
+ };
91691
+ //#endregion
91663
91692
  //#region src/cli/utils/ease-out-cubic.ts
91664
91693
  const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
91665
91694
  //#endregion
@@ -91725,15 +91754,10 @@ const buildProjectedScoreBar = (currentScore, potentialScore) => {
91725
91754
  const emptyCount = Math.max(0, barWidth - currentFill - gainCount);
91726
91755
  return colorizeByScore("█".repeat(currentFill), currentScore) + highlighter.dim(colorizeByScore("▓".repeat(gainCount), currentScore)) + highlighter.dim("░".repeat(emptyCount));
91727
91756
  };
91728
- const getDoctorFace = (score) => {
91729
- if (score >= 75) return ["◠ ◠", " ▽ "];
91730
- if (score >= 50) return ["• •", " ─ "];
91731
- return ["x x", " ▽ "];
91732
- };
91733
91757
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
91734
91758
  const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
91735
91759
  const buildRawFaceLines = (score) => {
91736
- const [eyes, mouth] = getDoctorFace(score);
91760
+ const [eyes, mouth] = doctorFace(score);
91737
91761
  return [
91738
91762
  FACE_BOX_TOP_BORDER,
91739
91763
  `│ ${eyes} │`,
@@ -92067,7 +92091,8 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
92067
92091
  includeTagDefaults: inputOptions.includeTagDefaults ?? false,
92068
92092
  scoreDisabledMessage: inputOptions.scoreDisabledMessage,
92069
92093
  outputSurface: inputOptions.outputSurface ?? "cli",
92070
- suppressRendering: inputOptions.suppressRendering ?? false,
92094
+ suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
92095
+ uiLayers: inputOptions.uiLayers ?? null,
92071
92096
  concurrentScan: inputOptions.concurrentScan ?? false,
92072
92097
  concurrency: inputOptions.concurrency,
92073
92098
  maxDurationMs: inputOptions.maxDurationMs ?? null,
@@ -92291,7 +92316,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92291
92316
  shouldRunSupplyChain: options.supplyChain,
92292
92317
  shouldComputeScore: !options.noScore,
92293
92318
  shouldShowProgressSpinners,
92294
- oxlintConcurrency: options.concurrency
92319
+ oxlintConcurrency: options.concurrency,
92320
+ reporterLayer: options.uiLayers?.reporter,
92321
+ progressLayer: options.uiLayers?.progress
92295
92322
  });
92296
92323
  const program = runInspect({
92297
92324
  directory,
@@ -92329,7 +92356,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92329
92356
  const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
92330
92357
  const didLintFail = lintBindingMissing || output.didLintFail;
92331
92358
  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`)));
92359
+ 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
92360
  else runConsole(error$1(highlighter.error(lintFailureReason)));
92334
92361
  let inspectDiagnostics = output.diagnostics;
92335
92362
  let baselineDelta;
@@ -92564,7 +92591,7 @@ const finalizeAndRender = (input) => gen(function* () {
92564
92591
  const scoreDiagnostics = filterDiagnosticsForSurface([...diagnostics], "score", userConfig);
92565
92592
  const displayedScoreDiagnostics = filterDiagnosticsForSurface([...printedDiagnostics], "score", userConfig);
92566
92593
  const potentialScore = score ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, scoreDiagnostics, score)) : null;
92567
- const shouldShowShareLink = !options.noScore && options.share && !options.isCi;
92594
+ const showShareLink = shouldShowShareLink(options);
92568
92595
  yield* pause;
92569
92596
  yield* printSummary({
92570
92597
  diagnostics: [...printedDiagnostics],
@@ -92587,7 +92614,7 @@ const finalizeAndRender = (input) => gen(function* () {
92587
92614
  diagnostics: [...printedDiagnostics],
92588
92615
  scoreResult: score,
92589
92616
  projectName: project.projectName,
92590
- isOffline: !shouldShowShareLink
92617
+ isOffline: !showShareLink
92591
92618
  });
92592
92619
  return buildResult();
92593
92620
  });
@@ -92697,6 +92724,127 @@ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
92697
92724
  return detectedAgents.length === 1 ? [...detectedAgents] : [];
92698
92725
  };
92699
92726
  //#endregion
92727
+ //#region src/cli/utils/launch-agent.ts
92728
+ const isWindows = process.platform === "win32";
92729
+ const CLI_AGENT_IDS = [
92730
+ "claude-code",
92731
+ "codex",
92732
+ "cursor"
92733
+ ];
92734
+ const CLI_AGENT_BINARIES = {
92735
+ "claude-code": "claude",
92736
+ codex: "codex",
92737
+ cursor: "cursor-agent"
92738
+ };
92739
+ const CLI_AGENT_AUTO_FLAGS = {
92740
+ "claude-code": ["--dangerously-skip-permissions"],
92741
+ codex: ["--yolo"],
92742
+ cursor: ["--force"]
92743
+ };
92744
+ const resolveCursorBundledNode = () => {
92745
+ if (!isWindows) return null;
92746
+ const localAppData = process.env.LOCALAPPDATA;
92747
+ if (!localAppData) return null;
92748
+ const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
92749
+ try {
92750
+ const versions = NFS.readdirSync(cursorAgentRoot);
92751
+ if (versions.length === 0) return null;
92752
+ const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
92753
+ for (const version of sortedVersions) try {
92754
+ const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
92755
+ const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
92756
+ if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
92757
+ command: bundledNodePath,
92758
+ args: [entryScriptPath]
92759
+ };
92760
+ } catch {}
92761
+ } catch {}
92762
+ return null;
92763
+ };
92764
+ const resolveWindowsCmdEntryScript = (command) => {
92765
+ if (!isWindows) return null;
92766
+ const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
92767
+ for (const directory of pathDirectories) {
92768
+ const cmdFilePath = Path.join(directory, `${command}.cmd`);
92769
+ try {
92770
+ if (!NFS.statSync(cmdFilePath).isFile()) continue;
92771
+ } catch {
92772
+ continue;
92773
+ }
92774
+ try {
92775
+ const entryScriptMatch = NFS.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
92776
+ if (!entryScriptMatch) continue;
92777
+ const resolvedScriptPath = Path.resolve(directory, entryScriptMatch[1]);
92778
+ if (NFS.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
92779
+ } catch {}
92780
+ }
92781
+ return null;
92782
+ };
92783
+ const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
92784
+ const child = spawn(command, [...args], {
92785
+ cwd,
92786
+ stdio: "inherit"
92787
+ });
92788
+ child.on("error", reject);
92789
+ child.on("close", (code) => resolve(code ?? 0));
92790
+ });
92791
+ const launchCliAgent = async (agentId, prompt, cwd) => {
92792
+ const binary = CLI_AGENT_BINARIES[agentId];
92793
+ const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
92794
+ if (isWindows) {
92795
+ if (agentId === "cursor") {
92796
+ const cursorResolution = resolveCursorBundledNode();
92797
+ if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
92798
+ }
92799
+ const entryScript = resolveWindowsCmdEntryScript(binary);
92800
+ if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
92801
+ }
92802
+ try {
92803
+ return await spawnAgent(binary, agentArgs, cwd);
92804
+ } catch {
92805
+ throw new Error(`Failed to launch ${binary}`);
92806
+ }
92807
+ };
92808
+ const CLIPBOARD_COMMANDS = [
92809
+ {
92810
+ binary: "pbcopy",
92811
+ args: []
92812
+ },
92813
+ {
92814
+ binary: "wl-copy",
92815
+ args: []
92816
+ },
92817
+ {
92818
+ binary: "xclip",
92819
+ args: ["-selection", "clipboard"]
92820
+ },
92821
+ {
92822
+ binary: "xsel",
92823
+ args: ["--clipboard", "--input"]
92824
+ },
92825
+ {
92826
+ binary: "clip",
92827
+ args: []
92828
+ }
92829
+ ];
92830
+ const copyToClipboard = (text) => {
92831
+ const command = CLIPBOARD_COMMANDS.find((candidate) => isCommandAvailable(candidate.binary));
92832
+ if (!command) return Promise.resolve(false);
92833
+ return new Promise((resolve) => {
92834
+ const child = spawn(command.binary, command.args);
92835
+ child.on("error", () => resolve(false));
92836
+ child.on("close", (code) => resolve(code === 0));
92837
+ child.stdin.on("error", () => resolve(false));
92838
+ child.stdin.end(text);
92839
+ });
92840
+ };
92841
+ //#endregion
92842
+ //#region src/cli/utils/detect-launchable-agents.ts
92843
+ const detectLaunchableAgents = async () => {
92844
+ const detected = new Set(await detectAvailableAgents());
92845
+ return CLI_AGENT_IDS.filter((agentId) => detected.has(agentId) && isCommandAvailable(CLI_AGENT_BINARIES[agentId]));
92846
+ };
92847
+ //#endregion
92700
92848
  //#region src/cli/utils/action-upgrade-prompt.ts
92701
92849
  const ACTION_UPGRADE_GATE = {
92702
92850
  id: ACTION_UPGRADE_EVENT,
@@ -93939,116 +94087,6 @@ const installReactDoctorSkillForAgent = async (agent, projectRoot) => {
93939
94087
  return result.skills.length > 0 && result.failed.length === 0;
93940
94088
  };
93941
94089
  //#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
94090
  //#region src/cli/utils/handoff-to-agent.ts
94053
94091
  const CLIPBOARD_CHOICE = "clipboard";
94054
94092
  const SKIP_CHOICE = "skip";
@@ -94112,10 +94150,6 @@ const maybeOfferActionUpgrade = async (projectRoot) => {
94112
94150
  }
94113
94151
  if (await upgradeGitHubActionsWorkflow(workflow)) recordActionUpgradeDecision(projectRoot, "accepted");
94114
94152
  };
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
94153
  const handoffToAgent = async (input) => {
94120
94154
  if (!input.interactive || input.diagnostics.length === 0) return;
94121
94155
  cliLogger.break();
@@ -94430,15 +94464,29 @@ const readChangedFilesFrom = (filePath) => {
94430
94464
  return [...uniqueFiles];
94431
94465
  };
94432
94466
  //#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));
94467
+ //#region src/cli/utils/count-unique-scanned-files.ts
94468
+ const countUniqueScannedFiles = (results) => {
94469
+ const uniqueScannedFilePaths = /* @__PURE__ */ new Set();
94470
+ let fileCountFromScansWithoutPaths = 0;
94471
+ for (const result of results) {
94472
+ const scannedFilePaths = result.scannedFilePaths;
94473
+ if (scannedFilePaths && scannedFilePaths.length > 0) for (const filePath of scannedFilePaths) uniqueScannedFilePaths.add(filePath);
94474
+ else fileCountFromScansWithoutPaths += result.scannedFileCount ?? result.project.sourceFileCount;
94475
+ }
94476
+ return uniqueScannedFilePaths.size + fileCountFromScansWithoutPaths;
94477
+ };
94435
94478
  //#endregion
94436
- //#region src/cli/utils/render-multi-project-summary.ts
94437
- const getScoreLabel = (score) => {
94479
+ //#region src/cli/utils/score-band-label.ts
94480
+ const scoreBandLabel = (score) => {
94438
94481
  if (score >= 75) return "Great";
94439
94482
  if (score >= 50) return "OK";
94440
94483
  return "Needs work";
94441
94484
  };
94485
+ //#endregion
94486
+ //#region src/cli/utils/filter-scans-for-surface.ts
94487
+ const filterScansForSurface = (completedScans, surface) => completedScans.flatMap((scan) => filterDiagnosticsForSurface([...scan.result.diagnostics], surface, scan.config));
94488
+ //#endregion
94489
+ //#region src/cli/utils/render-multi-project-summary.ts
94442
94490
  const buildSummaryLine = (entry, longestProjectNameLength) => {
94443
94491
  const paddedName = entry.projectName.padEnd(longestProjectNameLength);
94444
94492
  const nameRendering = entry.score !== null ? colorizeByScore(paddedName, entry.score) : highlighter.dim(paddedName);
@@ -94447,7 +94495,7 @@ const buildSummaryLine = (entry, longestProjectNameLength) => {
94447
94495
  return ` ${nameRendering} ${highlighter.dim("no score")} ${highlighter.dim(issueLabel)}`;
94448
94496
  }
94449
94497
  const scoreRendering = colorizeByScore(String(entry.score).padStart(3), entry.score);
94450
- const label = colorizeByScore(getScoreLabel(entry.score), entry.score);
94498
+ const label = colorizeByScore(scoreBandLabel(entry.score), entry.score);
94451
94499
  const issuesParts = [];
94452
94500
  if (entry.errorCount > 0) issuesParts.push(highlighter.error(`${entry.errorCount} ${entry.errorCount === 1 ? "error" : "errors"}`));
94453
94501
  const warningCount = entry.issueCount - entry.errorCount;
@@ -94469,14 +94517,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
94469
94517
  const projectRootByDiagnostic = /* @__PURE__ */ new WeakMap();
94470
94518
  for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
94471
94519
  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;
94520
+ const totalScannedFileCount = countUniqueScannedFiles(completedScans.map((scan) => scan.result));
94480
94521
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
94481
94522
  if (displayDiagnostics.length > 0) {
94482
94523
  yield* log("");
@@ -94748,10 +94789,14 @@ const parseFileLineArgument = (rawArgument) => {
94748
94789
  };
94749
94790
  //#endregion
94750
94791
  //#region src/cli/utils/select-projects.ts
94751
- const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
94792
+ const discoverWorkspacePackages = (rootDirectory) => {
94752
94793
  const hasRootPackageJson = isFile(Path.join(rootDirectory, "package.json"));
94753
- let packages = listWorkspacePackages(rootDirectory);
94754
- if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) packages = discoverReactSubprojects(rootDirectory);
94794
+ const packages = listWorkspacePackages(rootDirectory);
94795
+ if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) return discoverReactSubprojects(rootDirectory);
94796
+ return packages;
94797
+ };
94798
+ const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
94799
+ const packages = discoverWorkspacePackages(rootDirectory);
94755
94800
  if (projectFlag) return resolveProjectFlag(projectFlag, packages, rootDirectory);
94756
94801
  const configRequestedNames = (configProjects ?? []).map((requestedName) => requestedName.trim()).filter((requestedName) => requestedName.length > 0);
94757
94802
  if (configRequestedNames.length > 0) {
@@ -94880,6 +94925,14 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
94880
94925
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
94881
94926
  };
94882
94927
  //#endregion
94928
+ //#region src/cli/utils/should-fail-scan-gate.ts
94929
+ const shouldFailScanGate = (input) => {
94930
+ if (input.blockingLevel === "none") return false;
94931
+ if (input.scans.some(({ result }) => hasLintHardFailure(result))) return true;
94932
+ if (input.diagnosticsAreGateExempt === true) return false;
94933
+ return shouldBlockCi(filterScansForSurface(input.scans, "ciFailure"), input.blockingLevel);
94934
+ };
94935
+ //#endregion
94883
94936
  //#region src/cli/utils/warn-deprecated-fail-on.ts
94884
94937
  const warnDeprecatedFailOn = (flags, userConfig) => {
94885
94938
  const usedFlag = flags.failOn !== void 0;
@@ -95006,12 +95059,11 @@ const finalizeScans = (input) => {
95006
95059
  }));
95007
95060
  }
95008
95061
  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;
95062
+ if (shouldFailScanGate({
95063
+ scans: input.completedScans,
95064
+ blockingLevel,
95065
+ diagnosticsAreGateExempt: input.isScoreOnly || baselineDegraded
95066
+ })) process.exitCode = 1;
95015
95067
  };
95016
95068
  const buildChangedFilesDiffInfo = (changedFiles) => ({
95017
95069
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -95287,13 +95339,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
95287
95339
  completedScans.push(scanOutcome);
95288
95340
  }
95289
95341
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
95290
- const shouldShowShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95342
+ const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95291
95343
  await runPromise(printMultiProjectSummary({
95292
95344
  completedScans,
95293
95345
  categoryFilters,
95294
95346
  verbose: Boolean(flags.verbose),
95295
95347
  outputDirectory: flags.outputDir,
95296
- isOffline: !shouldShowShareLink,
95348
+ isOffline: !showShareLink,
95297
95349
  projectName: Path.basename(resolvedDirectory),
95298
95350
  totalElapsedMilliseconds: performance$1.now() - scanLoopStartTime
95299
95351
  }));
@@ -96268,6 +96320,20 @@ const COMMAND_FLAG_SPECS = new Map([
96268
96320
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
96269
96321
  shortOptionsWithoutValues: new Set(["-h"]),
96270
96322
  shortOptionsWithRequiredValues: new Set(["-c"])
96323
+ }],
96324
+ ["experimental-tui", {
96325
+ longOptionsWithoutValues: new Set([
96326
+ "--color",
96327
+ "--help",
96328
+ "--no-color",
96329
+ "--no-dead-code",
96330
+ "--no-score",
96331
+ "--yes"
96332
+ ]),
96333
+ longOptionsWithRequiredValues: new Set(["--blocking", "--project"]),
96334
+ longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
96335
+ shortOptionsWithoutValues: new Set(["-h", "-y"]),
96336
+ shortOptionsWithRequiredValues: new Set(["-p"])
96271
96337
  }]
96272
96338
  ]);
96273
96339
  const isFlagLike = (argument) => argument.startsWith("-") && argument !== "-";
@@ -96447,6 +96513,35 @@ rules.command("category <category> <severity>").description("Set the severity fo
96447
96513
  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
96514
  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
96515
  program.command("experimental-lsp", { hidden: false }).description("[experimental] run the React Doctor language server over stdio (for editors)").allowUnknownOption().action(() => {});
96516
+ 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) => {
96517
+ const options = command.optsWithGlobals();
96518
+ const deadCode = options.deadCode === false ? false : void 0;
96519
+ const noScore = options.score === false ? true : void 0;
96520
+ const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
96521
+ if (process.stdout.isTTY !== true || process.stdin.isTTY !== true || isNonInteractiveEnvironment() || nodeMajorVersion < 22) {
96522
+ await inspectAction(directory, {
96523
+ deadCode,
96524
+ score: options.score === false ? false : void 0,
96525
+ project: options.project,
96526
+ yes: options.yes,
96527
+ blocking: options.blocking
96528
+ });
96529
+ return;
96530
+ }
96531
+ recordCount(METRIC.cliInvoked, 1, { command: "experimental-tui" });
96532
+ const { runScanApp } = await import("./run-scan-app-DZ5jGWFf.js");
96533
+ const { shouldFail } = await runScanApp({
96534
+ directory,
96535
+ options: {
96536
+ deadCode,
96537
+ noScore
96538
+ },
96539
+ projectFlag: options.project,
96540
+ skipPrompts: options.yes ?? false,
96541
+ blocking: options.blocking
96542
+ });
96543
+ if (shouldFail) process.exitCode = 1;
96544
+ });
96450
96545
  process.stdout.on("error", (error) => {
96451
96546
  if (error.code === "EPIPE") process.exit(0);
96452
96547
  });
@@ -96473,7 +96568,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
96473
96568
  handleError(error, { sentryEventId });
96474
96569
  });
96475
96570
  //#endregion
96476
- export {};
96571
+ 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
96572
 
96478
96573
  //# sourceMappingURL=cli.js.map
96479
- //# debugId=1ac2e899-2304-5ed1-be9d-6cd66a730b6e
96574
+ //# debugId=be7f877d-9463-5212-8e1f-dc5feea97da0