react-doctor 0.8.3-dev.3d7ea66 → 0.8.3-dev.416fdcc

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]="729213e0-f120-5159-8212-e0a881c2cf17")}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";
@@ -64964,6 +64964,14 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
64964
64964
  "Maintainability"
64965
64965
  ];
64966
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);
64967
64975
  const APP_ONLY_RULE_KEYS = new Set([
64968
64976
  "react-hooks-js/static-components",
64969
64977
  "react-doctor/no-render-prop-children",
@@ -65707,18 +65715,20 @@ const getReactCompilerFlagState = (expression, analysis) => {
65707
65715
  }
65708
65716
  return null;
65709
65717
  };
65710
- const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65718
+ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedExpressions = /* @__PURE__ */ new Set()) => {
65711
65719
  let resolvedExpression = expression;
65712
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);
65713
65723
  if (ts.isIdentifier(resolvedExpression)) {
65714
65724
  if (analysis.localBindings.has(resolvedExpression.text)) {
65715
65725
  const localInitializer = analysis.localBindings.get(resolvedExpression.text);
65716
- return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis) : null;
65726
+ return localInitializer ? getSelectedObjectProperty(localInitializer, propertyName, analysis, visitedExpressions) : null;
65717
65727
  }
65718
65728
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
65719
- 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;
65720
65730
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, resolvedExpression.text);
65721
- return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis) : null;
65731
+ return topLevelBinding && ts.isExpression(topLevelBinding) ? getSelectedObjectProperty(topLevelBinding, propertyName, analysis, visitedExpressions) : null;
65722
65732
  }
65723
65733
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return null;
65724
65734
  for (const property of [...resolvedExpression.properties].reverse()) {
@@ -65726,7 +65736,7 @@ const getSelectedObjectProperty = (expression, propertyName, analysis) => {
65726
65736
  if (ts.isMethodDeclaration(property) && getStaticPropertyName$1(property.name) === propertyName) return property;
65727
65737
  if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) return property.name;
65728
65738
  if (ts.isSpreadAssignment(property)) {
65729
- const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis);
65739
+ const spreadProperty = getSelectedObjectProperty(property.expression, propertyName, analysis, visitedExpressions);
65730
65740
  if (spreadProperty) return spreadProperty;
65731
65741
  }
65732
65742
  }
@@ -77307,6 +77317,7 @@ const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
77307
77317
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
77308
77318
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
77309
77319
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
77320
+ const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
77310
77321
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
77311
77322
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
77312
77323
  const CATEGORY_COUNTUP_SETTLE_HOLD_MS = 1e3;
@@ -77398,7 +77409,7 @@ const makeNoopConsole = () => ({
77398
77409
  });
77399
77410
  //#endregion
77400
77411
  //#region src/cli/utils/version.ts
77401
- const VERSION = "0.8.3-dev.3d7ea66";
77412
+ const VERSION = "0.8.3-dev.416fdcc";
77402
77413
  //#endregion
77403
77414
  //#region src/cli/utils/json-mode.ts
77404
77415
  let context = null;
@@ -77756,13 +77767,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
77756
77767
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
77757
77768
  * standard `SENTRY_RELEASE` override.
77758
77769
  */
77759
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.3-dev.3d7ea66`;
77770
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.8.3-dev.416fdcc`;
77760
77771
  /**
77761
77772
  * Deployment environment shown in Sentry's environment filter. Defaults to
77762
77773
  * `production` for tagged releases and `development` for dev/unbuilt versions,
77763
77774
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
77764
77775
  */
77765
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.3-dev.3d7ea66") ? "development" : "production");
77776
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.8.3-dev.416fdcc") ? "development" : "production");
77766
77777
  /**
77767
77778
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
77768
77779
  * (set to `0` to disable tracing) and falls back to
@@ -89609,12 +89620,13 @@ const buildRuntimeLayers = (input) => {
89609
89620
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
89610
89621
  const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
89611
89622
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
89612
- 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;
89613
89625
  const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
89614
89626
  config: input.userConfig,
89615
89627
  resolvedDirectory: input.directory,
89616
89628
  configSourceDirectory: input.configSourceDirectory
89617
- }) : 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);
89618
89630
  return input.oxlintConcurrency === void 0 ? baseLayers : mergeAll$1(baseLayers, succeed$3(OxlintConcurrency, input.oxlintConcurrency));
89619
89631
  };
89620
89632
  //#endregion
@@ -91449,6 +91461,9 @@ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
91449
91461
  * would choke on the escape sequences).
91450
91462
  */
91451
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;
91452
91467
  const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
91453
91468
  const FALSY_FLAG_VALUES = new Set([
91454
91469
  "",
@@ -91667,6 +91682,13 @@ const colorizeByScore = (text, score) => {
91667
91682
  return highlighter.error(text);
91668
91683
  };
91669
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
91670
91692
  //#region src/cli/utils/ease-out-cubic.ts
91671
91693
  const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
91672
91694
  //#endregion
@@ -91732,15 +91754,10 @@ const buildProjectedScoreBar = (currentScore, potentialScore) => {
91732
91754
  const emptyCount = Math.max(0, barWidth - currentFill - gainCount);
91733
91755
  return colorizeByScore("█".repeat(currentFill), currentScore) + highlighter.dim(colorizeByScore("▓".repeat(gainCount), currentScore)) + highlighter.dim("░".repeat(emptyCount));
91734
91756
  };
91735
- const getDoctorFace = (score) => {
91736
- if (score >= 75) return ["◠ ◠", " ▽ "];
91737
- if (score >= 50) return ["• •", " ─ "];
91738
- return ["x x", " ▽ "];
91739
- };
91740
91757
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
91741
91758
  const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
91742
91759
  const buildRawFaceLines = (score) => {
91743
- const [eyes, mouth] = getDoctorFace(score);
91760
+ const [eyes, mouth] = doctorFace(score);
91744
91761
  return [
91745
91762
  FACE_BOX_TOP_BORDER,
91746
91763
  `│ ${eyes} │`,
@@ -92074,7 +92091,8 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
92074
92091
  includeTagDefaults: inputOptions.includeTagDefaults ?? false,
92075
92092
  scoreDisabledMessage: inputOptions.scoreDisabledMessage,
92076
92093
  outputSurface: inputOptions.outputSurface ?? "cli",
92077
- suppressRendering: inputOptions.suppressRendering ?? false,
92094
+ suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
92095
+ uiLayers: inputOptions.uiLayers ?? null,
92078
92096
  concurrentScan: inputOptions.concurrentScan ?? false,
92079
92097
  concurrency: inputOptions.concurrency,
92080
92098
  maxDurationMs: inputOptions.maxDurationMs ?? null,
@@ -92298,7 +92316,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92298
92316
  shouldRunSupplyChain: options.supplyChain,
92299
92317
  shouldComputeScore: !options.noScore,
92300
92318
  shouldShowProgressSpinners,
92301
- oxlintConcurrency: options.concurrency
92319
+ oxlintConcurrency: options.concurrency,
92320
+ reporterLayer: options.uiLayers?.reporter,
92321
+ progressLayer: options.uiLayers?.progress
92302
92322
  });
92303
92323
  const program = runInspect({
92304
92324
  directory,
@@ -92336,7 +92356,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
92336
92356
  const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
92337
92357
  const didLintFail = lintBindingMissing || output.didLintFail;
92338
92358
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
92339
- 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`)));
92340
92360
  else runConsole(error$1(highlighter.error(lintFailureReason)));
92341
92361
  let inspectDiagnostics = output.diagnostics;
92342
92362
  let baselineDelta;
@@ -92571,7 +92591,7 @@ const finalizeAndRender = (input) => gen(function* () {
92571
92591
  const scoreDiagnostics = filterDiagnosticsForSurface([...diagnostics], "score", userConfig);
92572
92592
  const displayedScoreDiagnostics = filterDiagnosticsForSurface([...printedDiagnostics], "score", userConfig);
92573
92593
  const potentialScore = score ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, scoreDiagnostics, score)) : null;
92574
- const shouldShowShareLink = !options.noScore && options.share && !options.isCi;
92594
+ const showShareLink = shouldShowShareLink(options);
92575
92595
  yield* pause;
92576
92596
  yield* printSummary({
92577
92597
  diagnostics: [...printedDiagnostics],
@@ -92594,7 +92614,7 @@ const finalizeAndRender = (input) => gen(function* () {
92594
92614
  diagnostics: [...printedDiagnostics],
92595
92615
  scoreResult: score,
92596
92616
  projectName: project.projectName,
92597
- isOffline: !shouldShowShareLink
92617
+ isOffline: !showShareLink
92598
92618
  });
92599
92619
  return buildResult();
92600
92620
  });
@@ -92704,6 +92724,127 @@ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
92704
92724
  return detectedAgents.length === 1 ? [...detectedAgents] : [];
92705
92725
  };
92706
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
92707
92848
  //#region src/cli/utils/action-upgrade-prompt.ts
92708
92849
  const ACTION_UPGRADE_GATE = {
92709
92850
  id: ACTION_UPGRADE_EVENT,
@@ -93946,116 +94087,6 @@ const installReactDoctorSkillForAgent = async (agent, projectRoot) => {
93946
94087
  return result.skills.length > 0 && result.failed.length === 0;
93947
94088
  };
93948
94089
  //#endregion
93949
- //#region src/cli/utils/launch-agent.ts
93950
- const isWindows = process.platform === "win32";
93951
- const CLI_AGENT_BINARIES = {
93952
- "claude-code": "claude",
93953
- codex: "codex",
93954
- cursor: "cursor-agent"
93955
- };
93956
- const CLI_AGENT_AUTO_FLAGS = {
93957
- "claude-code": ["--dangerously-skip-permissions"],
93958
- codex: ["--yolo"],
93959
- cursor: ["--force"]
93960
- };
93961
- const resolveCursorBundledNode = () => {
93962
- if (!isWindows) return null;
93963
- const localAppData = process.env.LOCALAPPDATA;
93964
- if (!localAppData) return null;
93965
- const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
93966
- try {
93967
- const versions = NFS.readdirSync(cursorAgentRoot);
93968
- if (versions.length === 0) return null;
93969
- const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
93970
- for (const version of sortedVersions) try {
93971
- const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
93972
- const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
93973
- if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
93974
- command: bundledNodePath,
93975
- args: [entryScriptPath]
93976
- };
93977
- } catch {}
93978
- } catch {}
93979
- return null;
93980
- };
93981
- const resolveWindowsCmdEntryScript = (command) => {
93982
- if (!isWindows) return null;
93983
- const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
93984
- for (const directory of pathDirectories) {
93985
- const cmdFilePath = Path.join(directory, `${command}.cmd`);
93986
- try {
93987
- if (!NFS.statSync(cmdFilePath).isFile()) continue;
93988
- } catch {
93989
- continue;
93990
- }
93991
- try {
93992
- const entryScriptMatch = NFS.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
93993
- if (!entryScriptMatch) continue;
93994
- const resolvedScriptPath = Path.resolve(directory, entryScriptMatch[1]);
93995
- if (NFS.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
93996
- } catch {}
93997
- }
93998
- return null;
93999
- };
94000
- const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
94001
- const child = spawn(command, [...args], {
94002
- cwd,
94003
- stdio: "inherit"
94004
- });
94005
- child.on("error", reject);
94006
- child.on("close", (code) => resolve(code ?? 0));
94007
- });
94008
- const launchCliAgent = async (agentId, prompt, cwd) => {
94009
- const binary = CLI_AGENT_BINARIES[agentId];
94010
- const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
94011
- if (isWindows) {
94012
- if (agentId === "cursor") {
94013
- const cursorResolution = resolveCursorBundledNode();
94014
- if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
94015
- }
94016
- const entryScript = resolveWindowsCmdEntryScript(binary);
94017
- if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
94018
- }
94019
- try {
94020
- return await spawnAgent(binary, agentArgs, cwd);
94021
- } catch {
94022
- throw new Error(`Failed to launch ${binary}`);
94023
- }
94024
- };
94025
- const CLIPBOARD_COMMANDS = [
94026
- {
94027
- binary: "pbcopy",
94028
- args: []
94029
- },
94030
- {
94031
- binary: "wl-copy",
94032
- args: []
94033
- },
94034
- {
94035
- binary: "xclip",
94036
- args: ["-selection", "clipboard"]
94037
- },
94038
- {
94039
- binary: "xsel",
94040
- args: ["--clipboard", "--input"]
94041
- },
94042
- {
94043
- binary: "clip",
94044
- args: []
94045
- }
94046
- ];
94047
- const copyToClipboard = (text) => {
94048
- const command = CLIPBOARD_COMMANDS.find((candidate) => isCommandAvailable(candidate.binary));
94049
- if (!command) return Promise.resolve(false);
94050
- return new Promise((resolve) => {
94051
- const child = spawn(command.binary, command.args);
94052
- child.on("error", () => resolve(false));
94053
- child.on("close", (code) => resolve(code === 0));
94054
- child.stdin.on("error", () => resolve(false));
94055
- child.stdin.end(text);
94056
- });
94057
- };
94058
- //#endregion
94059
94090
  //#region src/cli/utils/handoff-to-agent.ts
94060
94091
  const CLIPBOARD_CHOICE = "clipboard";
94061
94092
  const SKIP_CHOICE = "skip";
@@ -94119,10 +94150,6 @@ const maybeOfferActionUpgrade = async (projectRoot) => {
94119
94150
  }
94120
94151
  if (await upgradeGitHubActionsWorkflow(workflow)) recordActionUpgradeDecision(projectRoot, "accepted");
94121
94152
  };
94122
- const detectLaunchableAgents = async () => {
94123
- const detected = new Set(await detectAvailableAgents());
94124
- return Object.keys(CLI_AGENT_BINARIES).filter((agentId) => detected.has(agentId) && isCommandAvailable(CLI_AGENT_BINARIES[agentId]));
94125
- };
94126
94153
  const handoffToAgent = async (input) => {
94127
94154
  if (!input.interactive || input.diagnostics.length === 0) return;
94128
94155
  cliLogger.break();
@@ -94437,15 +94464,29 @@ const readChangedFilesFrom = (filePath) => {
94437
94464
  return [...uniqueFiles];
94438
94465
  };
94439
94466
  //#endregion
94440
- //#region src/cli/utils/filter-scans-for-surface.ts
94441
- 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
+ };
94442
94478
  //#endregion
94443
- //#region src/cli/utils/render-multi-project-summary.ts
94444
- const getScoreLabel = (score) => {
94479
+ //#region src/cli/utils/score-band-label.ts
94480
+ const scoreBandLabel = (score) => {
94445
94481
  if (score >= 75) return "Great";
94446
94482
  if (score >= 50) return "OK";
94447
94483
  return "Needs work";
94448
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
94449
94490
  const buildSummaryLine = (entry, longestProjectNameLength) => {
94450
94491
  const paddedName = entry.projectName.padEnd(longestProjectNameLength);
94451
94492
  const nameRendering = entry.score !== null ? colorizeByScore(paddedName, entry.score) : highlighter.dim(paddedName);
@@ -94454,7 +94495,7 @@ const buildSummaryLine = (entry, longestProjectNameLength) => {
94454
94495
  return ` ${nameRendering} ${highlighter.dim("no score")} ${highlighter.dim(issueLabel)}`;
94455
94496
  }
94456
94497
  const scoreRendering = colorizeByScore(String(entry.score).padStart(3), entry.score);
94457
- const label = colorizeByScore(getScoreLabel(entry.score), entry.score);
94498
+ const label = colorizeByScore(scoreBandLabel(entry.score), entry.score);
94458
94499
  const issuesParts = [];
94459
94500
  if (entry.errorCount > 0) issuesParts.push(highlighter.error(`${entry.errorCount} ${entry.errorCount === 1 ? "error" : "errors"}`));
94460
94501
  const warningCount = entry.issueCount - entry.errorCount;
@@ -94476,14 +94517,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
94476
94517
  const projectRootByDiagnostic = /* @__PURE__ */ new WeakMap();
94477
94518
  for (const scan of completedScans) for (const diagnostic of scan.result.diagnostics) projectRootByDiagnostic.set(diagnostic, scan.result.project.rootDirectory);
94478
94519
  const resolveDiagnosticSourceRoot = (diagnostic) => projectRootByDiagnostic.get(diagnostic) ?? "";
94479
- const uniqueScannedFilePaths = /* @__PURE__ */ new Set();
94480
- let fileCountFromScansWithoutPaths = 0;
94481
- for (const scan of completedScans) {
94482
- const scannedFilePaths = scan.result.scannedFilePaths;
94483
- if (scannedFilePaths && scannedFilePaths.length > 0) for (const filePath of scannedFilePaths) uniqueScannedFilePaths.add(filePath);
94484
- else fileCountFromScansWithoutPaths += scan.result.scannedFileCount ?? scan.result.project.sourceFileCount;
94485
- }
94486
- const totalScannedFileCount = uniqueScannedFilePaths.size + fileCountFromScansWithoutPaths;
94520
+ const totalScannedFileCount = countUniqueScannedFiles(completedScans.map((scan) => scan.result));
94487
94521
  yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
94488
94522
  if (displayDiagnostics.length > 0) {
94489
94523
  yield* log("");
@@ -94755,10 +94789,14 @@ const parseFileLineArgument = (rawArgument) => {
94755
94789
  };
94756
94790
  //#endregion
94757
94791
  //#region src/cli/utils/select-projects.ts
94758
- const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
94792
+ const discoverWorkspacePackages = (rootDirectory) => {
94759
94793
  const hasRootPackageJson = isFile(Path.join(rootDirectory, "package.json"));
94760
- let packages = listWorkspacePackages(rootDirectory);
94761
- 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);
94762
94800
  if (projectFlag) return resolveProjectFlag(projectFlag, packages, rootDirectory);
94763
94801
  const configRequestedNames = (configProjects ?? []).map((requestedName) => requestedName.trim()).filter((requestedName) => requestedName.length > 0);
94764
94802
  if (configRequestedNames.length > 0) {
@@ -94887,6 +94925,14 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
94887
94925
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
94888
94926
  };
94889
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
94890
94936
  //#region src/cli/utils/warn-deprecated-fail-on.ts
94891
94937
  const warnDeprecatedFailOn = (flags, userConfig) => {
94892
94938
  const usedFlag = flags.failOn !== void 0;
@@ -95013,12 +95059,11 @@ const finalizeScans = (input) => {
95013
95059
  }));
95014
95060
  }
95015
95061
  const blockingLevel = resolveBlockingLevel(input.flags, input.userConfig);
95016
- if (input.completedScans.some(({ result }) => hasLintHardFailure(result)) && blockingLevel !== "none") {
95017
- process.exitCode = 1;
95018
- return;
95019
- }
95020
- if (input.isScoreOnly || baselineDegraded) return;
95021
- 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;
95022
95067
  };
95023
95068
  const buildChangedFilesDiffInfo = (changedFiles) => ({
95024
95069
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -95294,13 +95339,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
95294
95339
  completedScans.push(scanOutcome);
95295
95340
  }
95296
95341
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
95297
- const shouldShowShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95342
+ const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
95298
95343
  await runPromise(printMultiProjectSummary({
95299
95344
  completedScans,
95300
95345
  categoryFilters,
95301
95346
  verbose: Boolean(flags.verbose),
95302
95347
  outputDirectory: flags.outputDir,
95303
- isOffline: !shouldShowShareLink,
95348
+ isOffline: !showShareLink,
95304
95349
  projectName: Path.basename(resolvedDirectory),
95305
95350
  totalElapsedMilliseconds: performance$1.now() - scanLoopStartTime
95306
95351
  }));
@@ -96275,6 +96320,20 @@ const COMMAND_FLAG_SPECS = new Map([
96275
96320
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
96276
96321
  shortOptionsWithoutValues: new Set(["-h"]),
96277
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"])
96278
96337
  }]
96279
96338
  ]);
96280
96339
  const isFlagLike = (argument) => argument.startsWith("-") && argument !== "-";
@@ -96454,6 +96513,35 @@ rules.command("category <category> <severity>").description("Set the severity fo
96454
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()));
96455
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()));
96456
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
+ });
96457
96545
  process.stdout.on("error", (error) => {
96458
96546
  if (error.code === "EPIPE") process.exit(0);
96459
96547
  });
@@ -96480,7 +96568,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
96480
96568
  handleError(error, { sentryEventId });
96481
96569
  });
96482
96570
  //#endregion
96483
- 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 };
96484
96572
 
96485
96573
  //# sourceMappingURL=cli.js.map
96486
- //# debugId=729213e0-f120-5159-8212-e0a881c2cf17
96574
+ //# debugId=be7f877d-9463-5212-8e1f-dc5feea97da0