react-doctor 0.9.4-dev.da0647e → 0.9.5

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.
@@ -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]="07705956-10ec-5419-acea-25f57c6a08c5")}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]="b58d69eb-938f-5abd-9831-40b3dfe65ff2")}catch(e){}}();
3
3
  import { n as __require, r as __toESM$1, t as __commonJSMin$1 } from "./chunk-q7NCDQ7-.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NodeChildProcess from "node:child_process";
@@ -15,7 +15,7 @@ import crypto, { createHash, randomUUID } from "node:crypto";
15
15
  import { ResolverFactory } from "oxc-resolver";
16
16
  import ts from "typescript";
17
17
  import * as NodeUrl from "node:url";
18
- import { fileURLToPath, pathToFileURL } from "node:url";
18
+ import { fileURLToPath } from "node:url";
19
19
  import * as OS from "node:os";
20
20
  import os, { tmpdir } from "node:os";
21
21
  import { parseJSON5 } from "confbox";
@@ -28,9 +28,9 @@ import { performance as performance$1 } from "node:perf_hooks";
28
28
  import { promisify, stripVTControlCharacters } from "node:util";
29
29
  import tty from "node:tty";
30
30
  import basePrompts from "prompts";
31
- import { codeFrameColumns } from "@babel/code-frame";
32
- import Conf from "conf";
33
31
  import { detectInstalledSkillAgents, getSkillAgentTypes } from "agent-install";
32
+ import Conf from "conf";
33
+ import { codeFrameColumns } from "@babel/code-frame";
34
34
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
35
35
  /**
36
36
  * The `Pipeable` module defines the shared interface and implementation helpers
@@ -62453,7 +62453,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
62453
62453
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
62454
62454
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
62455
62455
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
62456
- const CORE_PACKAGE_VERSION = "0.9.4";
62456
+ const CORE_PACKAGE_VERSION = "0.9.5";
62457
62457
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
62458
62458
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
62459
62459
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -76357,8 +76357,7 @@ const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
76357
76357
  const TUI_REPORT_COMPACT_MAX_ROWS = 23;
76358
76358
  const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
76359
76359
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
76360
- const WELCOME_EXPLANATION_HOLD_MS = 1e3;
76361
- const CATEGORY_COUNTUP_SETTLE_HOLD_MS = 1e3;
76360
+ const MINIMUM_VTE_VERSION_FOR_HYPERLINKS = 5e3;
76362
76361
  const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":3,\"version\":\"unknown\",\"ok\":false,\"directory\":\"\",\"mode\":\"full\",\"diff\":null,\"projects\":[],\"diagnostics\":[],\"summary\":{\"errorCount\":0,\"warningCount\":0,\"affectedFileCount\":0,\"totalDiagnosticCount\":0,\"score\":null,\"scoreLabel\":null},\"elapsedMilliseconds\":0,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
76363
76362
  const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
76364
76363
  const NANOSECONDS_PER_SECOND = 1000000000n;
@@ -76412,7 +76411,7 @@ const METRIC = {
76412
76411
  tuiFindingNavigated: "tui.finding_navigated",
76413
76412
  tuiIssueStreamShown: "tui.issue_stream_shown",
76414
76413
  tuiProjectPathContextShown: "tui.project_path_context_shown",
76415
- tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
76414
+ tuiProjectSelectShown: "tui.project_select_shown",
76416
76415
  tuiReportActionSelected: "tui.report_action_selected",
76417
76416
  tuiCancelled: "tui.cancelled",
76418
76417
  tuiScanInlineShown: "tui.scan_inline_shown",
@@ -76461,7 +76460,7 @@ const makeNoopConsole = () => ({
76461
76460
  });
76462
76461
  //#endregion
76463
76462
  //#region src/cli/utils/version.ts
76464
- const VERSION = "0.9.4-dev.da0647e";
76463
+ const VERSION = "0.9.5";
76465
76464
  //#endregion
76466
76465
  //#region src/cli/utils/json-mode.ts
76467
76466
  let context = null;
@@ -76922,13 +76921,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
76922
76921
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
76923
76922
  * standard `SENTRY_RELEASE` override.
76924
76923
  */
76925
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.4-dev.da0647e`;
76924
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.5`;
76926
76925
  /**
76927
76926
  * Deployment environment shown in Sentry's environment filter. Defaults to
76928
76927
  * `production` for tagged releases and `development` for dev/unbuilt versions,
76929
76928
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
76930
76929
  */
76931
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.4-dev.da0647e") ? "development" : "production");
76930
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.5") ? "development" : "production");
76932
76931
  /**
76933
76932
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
76934
76933
  * (set to `0` to disable tracing) and falls back to
@@ -82449,88 +82448,68 @@ const printAgentGuidance = () => gen(function* () {
82449
82448
  yield* log("");
82450
82449
  });
82451
82450
  //#endregion
82452
- //#region src/cli/utils/box-text.ts
82453
- const ESCAPE = String.fromCharCode(27);
82454
- const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
82455
- const ANSI_LEADING_ESCAPE_PATTERN = new RegExp(`^${ESCAPE}\\[[0-9;]*m`);
82456
- const ANSI_RESET = `${ESCAPE}[0m`;
82457
- const ELLIPSIS = "…";
82458
- const visibleWidth = (line) => line.replace(ANSI_ESCAPE_PATTERN, "").length;
82459
- const truncateToVisibleWidth = (line, maxVisibleWidth) => {
82460
- if (visibleWidth(line) <= maxVisibleWidth) return line;
82461
- const keepWidth = Math.max(0, maxVisibleWidth - 1);
82462
- let result = "";
82463
- let keptVisible = 0;
82464
- let cursor = 0;
82465
- while (cursor < line.length && keptVisible < keepWidth) {
82466
- const escapeMatch = ANSI_LEADING_ESCAPE_PATTERN.exec(line.slice(cursor));
82467
- if (escapeMatch) {
82468
- result += escapeMatch[0];
82469
- cursor += escapeMatch[0].length;
82470
- continue;
82471
- }
82472
- result += line[cursor];
82473
- keptVisible += 1;
82474
- cursor += 1;
82451
+ //#region src/cli/utils/filter-diagnostics-by-categories.ts
82452
+ const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
82453
+ //#endregion
82454
+ //#region src/cli/utils/format-diagnostic-site.ts
82455
+ const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
82456
+ //#endregion
82457
+ //#region src/cli/utils/format-rule-summary.ts
82458
+ const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
82459
+ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
82460
+ const firstDiagnostic = ruleDiagnostics[0];
82461
+ if (firstDiagnostic === void 0) return "";
82462
+ const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
82463
+ const sections = [
82464
+ `Rule: ${ruleKey}`,
82465
+ `Severity: ${firstDiagnostic.severity}`,
82466
+ `Category: ${firstDiagnostic.category}`,
82467
+ `Count: ${ruleDiagnostics.length}`,
82468
+ "",
82469
+ distinctMessages.join("\n\n")
82470
+ ];
82471
+ if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
82472
+ if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
82473
+ const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
82474
+ if (fixRecipeLine) sections.push("", fixRecipeLine);
82475
+ sections.push("", "Files:");
82476
+ for (const diagnostic of ruleDiagnostics) {
82477
+ sections.push(` ${formatDiagnosticSite(diagnostic)}${formatFileContextTag(diagnostic)}`);
82478
+ if (diagnostic.suppressionHint) sections.push(` ${diagnostic.suppressionHint}`);
82475
82479
  }
82476
- return `${result}${ANSI_RESET}${ELLIPSIS}`;
82477
- };
82478
- const boxText = (content, innerWidth) => {
82479
- const horizontalRule = highlighter.dim("─".repeat(innerWidth + 2));
82480
- const side = highlighter.dim("│");
82481
- const body = content.split("\n").map((rawLine) => {
82482
- const line = truncateToVisibleWidth(rawLine, innerWidth);
82483
- return `${side} ${line}${" ".repeat(innerWidth - visibleWidth(line))} ${side}`;
82484
- });
82485
- return [
82486
- `${highlighter.dim("┌")}${horizontalRule}${highlighter.dim("┐")}`,
82487
- ...body,
82488
- `${highlighter.dim("└")}${horizontalRule}${highlighter.dim("┘")}`
82489
- ].join("\n");
82480
+ return `${sections.join("\n")}\n`;
82490
82481
  };
82491
82482
  //#endregion
82492
- //#region src/cli/utils/resolve-absolute-path.ts
82493
- /**
82494
- * Resolves a diagnostic's `filePath` (relative to its project root, or
82495
- * already absolute) to an absolute path. Shared by the code-frame reader and
82496
- * the terminal hyperlink builder so both turn a relative path into the same
82497
- * on-disk location.
82498
- */
82499
- const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
82500
- //#endregion
82501
- //#region src/cli/utils/build-code-frame.ts
82502
- /**
82503
- * Renders a syntax-highlighted source excerpt around a diagnostic site
82504
- * with a caret pointing at the offending column. Returns null when the
82505
- * file can't be read (e.g. multi-project summaries where paths are
82506
- * resolved against a different cwd), so callers can fall back to the
82507
- * bare `file:line` reference instead of failing the whole render.
82508
- */
82509
- const buildCodeFrame = (input) => {
82510
- if (input.line <= 0) return null;
82511
- const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
82512
- let source;
82483
+ //#region src/cli/utils/write-diagnostics-directory.ts
82484
+ const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
82485
+ const readPreviousRuleDumpFileNames = (directory) => {
82513
82486
  try {
82514
- source = NFS.readFileSync(absolutePath, "utf8");
82487
+ const previous = JSON.parse(NFS.readFileSync(Path.join(directory, "diagnostics.json"), "utf8"));
82488
+ return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
82515
82489
  } catch {
82516
- return null;
82490
+ return /* @__PURE__ */ new Set();
82517
82491
  }
82518
- if ((source.split("\n", input.line)[input.line - 1] ?? "").length > 200) return null;
82519
- const location = input.endLine != null && input.endLine > input.line ? {
82520
- start: { line: input.line },
82521
- end: { line: input.endLine }
82522
- } : { start: {
82523
- line: input.line,
82524
- column: input.column > 0 ? input.column : void 0
82525
- } };
82526
- return codeFrameColumns(source, location, {
82527
- highlightCode: true,
82528
- linesAbove: 1,
82529
- linesBelow: 1,
82530
- ...input.message ? { message: input.message } : {}
82531
- });
82492
+ };
82493
+ const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
82494
+ const directory = outputDirectory ? Path.resolve(outputDirectory) : Path.join(tmpdir(), `react-doctor-${randomUUID()}`);
82495
+ if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) NFS.rmSync(Path.join(directory, fileName), { force: true });
82496
+ NFS.mkdirSync(directory, { recursive: true });
82497
+ for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) NFS.writeFileSync(Path.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
82498
+ NFS.writeFileSync(Path.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
82499
+ return directory;
82532
82500
  };
82533
82501
  //#endregion
82502
+ //#region src/cli/utils/print-diagnostics-dump.ts
82503
+ const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
82504
+ const writtenDirectory = yield* try_({
82505
+ try: () => writeDiagnosticsDirectory(diagnostics, outputDirectory),
82506
+ catch: (cause) => cause
82507
+ }).pipe(orElseSucceed(() => null));
82508
+ if (writtenDirectory === null || !verbose && !outputDirectory) return;
82509
+ const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
82510
+ yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
82511
+ });
82512
+ //#endregion
82534
82513
  //#region src/cli/utils/resolve-measure-width.ts
82535
82514
  const resolveClampedWidth = (input) => {
82536
82515
  const terminalColumns = process.stdout.columns;
@@ -82544,20 +82523,6 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
82544
82523
  minWidth: 24
82545
82524
  });
82546
82525
  //#endregion
82547
- //#region src/cli/utils/build-section-divider.ts
82548
- const DIVIDER_INDENT = " ";
82549
- const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
82550
- //#endregion
82551
- //#region src/cli/utils/format-hyperlink.ts
82552
- const OSC = "\x1B]";
82553
- const ST = "\x1B\\";
82554
- /**
82555
- * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
82556
- * are exactly `text`; the link is carried in escape sequences a capable
82557
- * terminal turns into a click target.
82558
- */
82559
- const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
82560
- //#endregion
82561
82526
  //#region src/cli/utils/indent-multiline-text.ts
82562
82527
  const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
82563
82528
  //#endregion
@@ -82597,793 +82562,6 @@ const wrapTextToWidth = (text, width, options = {}) => {
82597
82562
  return text.split("\n").flatMap((lineText) => wrapLine(lineText, width, breakLongWords));
82598
82563
  };
82599
82564
  //#endregion
82600
- //#region src/cli/utils/write-stdout.ts
82601
- const writeStdout = (text) => sync(() => {
82602
- process.stdout.write(text);
82603
- });
82604
- //#endregion
82605
- //#region src/cli/utils/render-diagnostics.ts
82606
- const POINTER = isUnicodeSupported() ? "›" : ">";
82607
- const colorizeBySeverity = (text, severity) => severity === "error" ? highlighter.error(text) : highlighter.warn(text);
82608
- const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
82609
- const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
82610
- const buildVerboseFileEntries = (diagnostics) => {
82611
- const fileEntries = /* @__PURE__ */ new Map();
82612
- for (const diagnostic of diagnostics) {
82613
- let entry = fileEntries.get(diagnostic.filePath);
82614
- if (entry === void 0) {
82615
- entry = {
82616
- contextTag: formatFileContextTag(diagnostic),
82617
- sites: []
82618
- };
82619
- fileEntries.set(diagnostic.filePath, entry);
82620
- }
82621
- if (diagnostic.line > 0) entry.sites.push({
82622
- line: diagnostic.line,
82623
- suppressionHint: diagnostic.suppressionHint
82624
- });
82625
- }
82626
- return fileEntries;
82627
- };
82628
- const formatSiteCountBadge = (count) => count > 1 ? `×${count}` : "";
82629
- const formatTrailingSiteBadge = (count) => {
82630
- const badge = formatSiteCountBadge(count);
82631
- return badge.length > 0 ? ` ${highlighter.gray(badge)}` : "";
82632
- };
82633
- const CATEGORY_DISPLAY_RANK = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category, index) => [category, index]));
82634
- const compareCategoriesByDisplayRank = (categoryA, categoryB) => {
82635
- const rankA = CATEGORY_DISPLAY_RANK.get(categoryA);
82636
- const rankB = CATEGORY_DISPLAY_RANK.get(categoryB);
82637
- if (rankA !== void 0 && rankB !== void 0) return rankA - rankB;
82638
- if (rankA !== void 0) return -1;
82639
- if (rankB !== void 0) return 1;
82640
- return categoryA.localeCompare(categoryB);
82641
- };
82642
- const buildCategoryDiagnosticGroups = (diagnostics, rulePriority) => {
82643
- return [...groupBy(diagnostics, (diagnostic) => diagnostic.category).entries()].map(([category, categoryDiagnostics]) => ({
82644
- category,
82645
- diagnostics: categoryDiagnostics,
82646
- ruleGroups: buildSortedRuleGroups(categoryDiagnostics, rulePriority)
82647
- })).toSorted((categoryGroupA, categoryGroupB) => compareCategoriesByDisplayRank(categoryGroupA.category, categoryGroupB.category));
82648
- };
82649
- const formatCategoryTallyLine = (tally, errorShown, warningShown) => {
82650
- const parts = [];
82651
- if (tally.errorCount > 0) parts.push(highlighter.error(`${errorShown} ${errorShown === 1 ? "error" : "errors"}`));
82652
- if (tally.warningCount > 0) parts.push(highlighter.warn(highlighter.dim(`${warningShown} ${warningShown === 1 ? "warning" : "warnings"}`)));
82653
- return ` ${highlighter.bold(tally.category)} ${highlighter.dim(POINTER)} ${parts.join(highlighter.dim(", "))}`;
82654
- };
82655
- const buildCategoryTally = (categoryGroup) => ({
82656
- category: categoryGroup.category,
82657
- errorCount: categoryGroup.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
82658
- warningCount: categoryGroup.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
82659
- });
82660
- const buildCategoryTallyLines = (tallies) => tallies.map((tally) => formatCategoryTallyLine(tally, tally.errorCount, tally.warningCount));
82661
- const buildPartiallyRevealedTallyLines = (tallies, revealedUnitCount) => {
82662
- let remainingToReveal = revealedUnitCount;
82663
- return tallies.map((tally) => {
82664
- const errorShown = Math.min(tally.errorCount, remainingToReveal);
82665
- remainingToReveal -= errorShown;
82666
- const warningShown = Math.min(tally.warningCount, remainingToReveal);
82667
- remainingToReveal -= warningShown;
82668
- return formatCategoryTallyLine(tally, errorShown, warningShown);
82669
- });
82670
- };
82671
- const printCategoryCountUp = (tallies) => gen(function* () {
82672
- const totalUnitCount = tallies.reduce((sum, tally) => sum + tally.errorCount + tally.warningCount, 0);
82673
- const unitsPerStep = Math.max(1, Math.ceil(totalUnitCount / 24));
82674
- for (let revealedUnitCount = 0; revealedUnitCount < totalUnitCount; revealedUnitCount += unitsPerStep) {
82675
- const lines = buildPartiallyRevealedTallyLines(tallies, revealedUnitCount);
82676
- yield* writeStdout(`${revealedUnitCount === 0 ? "" : `\x1b[${tallies.length}A`}\r${lines.join("\n\r")}\n`);
82677
- yield* sleep(70);
82678
- }
82679
- yield* writeStdout(`${totalUnitCount === 0 ? "" : `\x1b[${tallies.length}A`}\r${buildCategoryTallyLines(tallies).join("\n\r")}\n`);
82680
- yield* sleep(CATEGORY_COUNTUP_SETTLE_HOLD_MS);
82681
- });
82682
- const TOP_ERROR_DETAIL_INDENT = " ";
82683
- const pickRepresentativeDiagnostic = (ruleDiagnostics) => ruleDiagnostics.find((diagnostic) => diagnostic.line > 0) ?? ruleDiagnostics[0];
82684
- const hasIndistinctSiteLocations = (ruleDiagnostics) => {
82685
- const firstDiagnostic = ruleDiagnostics[0];
82686
- if (firstDiagnostic === void 0) return false;
82687
- return ruleDiagnostics.every((diagnostic) => diagnostic.line <= 0 && diagnostic.filePath === firstDiagnostic.filePath);
82688
- };
82689
- const FRAME_CONTEXT_REACH_LINES = 3;
82690
- const clusterNearbyDiagnostics = (diagnostics) => {
82691
- const byFile = groupBy(diagnostics, (diagnostic) => diagnostic.filePath);
82692
- const clusters = [];
82693
- for (const fileDiagnostics of byFile.values()) {
82694
- const sorted = [...fileDiagnostics].sort((left, right) => left.line - right.line);
82695
- let current = [];
82696
- const flush = () => {
82697
- if (current.length === 0) return;
82698
- clusters.push({
82699
- diagnostics: current,
82700
- startLine: current[0].line,
82701
- endLine: current[current.length - 1].line
82702
- });
82703
- current = [];
82704
- };
82705
- for (const diagnostic of sorted) {
82706
- const previous = current[current.length - 1];
82707
- if (previous != null && (diagnostic.line - previous.line > FRAME_CONTEXT_REACH_LINES || diagnostic.line - current[0].line > 20)) flush();
82708
- current.push(diagnostic);
82709
- }
82710
- flush();
82711
- }
82712
- return clusters;
82713
- };
82714
- const formatClusterLocationText = (cluster) => {
82715
- const { filePath } = cluster.diagnostics[0];
82716
- if (cluster.startLine <= 0) return filePath;
82717
- if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
82718
- return `${filePath}:${cluster.startLine}`;
82719
- };
82720
- const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
82721
- const lead = cluster.diagnostics[0];
82722
- const contextTag = formatFileContextTag(lead);
82723
- const location = formatClusterLocationText(cluster);
82724
- if (!hyperlinks) return `${location}${contextTag}`;
82725
- return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
82726
- };
82727
- const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, resolveFrameSourceRoot, renderCodeFrame, hyperlinks) => {
82728
- const lead = cluster.diagnostics[0];
82729
- const isMultiSite = cluster.diagnostics.length > 1;
82730
- const sourceRoot = resolveSourceRoot(lead);
82731
- const frameSourceRoot = resolveFrameSourceRoot(lead);
82732
- const frameFilePath = Path.isAbsolute(lead.filePath) && sourceRoot !== frameSourceRoot ? Path.relative(sourceRoot, lead.filePath) : lead.filePath;
82733
- const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
82734
- const codeFrame = renderCodeFrame ? buildCodeFrame({
82735
- filePath: frameFilePath,
82736
- line: cluster.startLine,
82737
- column: isMultiSite ? 0 : lead.column,
82738
- endLine: isMultiSite ? cluster.endLine : void 0,
82739
- rootDirectory: frameSourceRoot
82740
- }) : null;
82741
- if (codeFrame) {
82742
- const boxInnerWidth = resolveMeasureWidth(8);
82743
- lines.push(indentMultilineText(boxText(codeFrame, boxInnerWidth), TOP_ERROR_DETAIL_INDENT));
82744
- }
82745
- const seenHints = /* @__PURE__ */ new Set();
82746
- for (const diagnostic of cluster.diagnostics) if (diagnostic.suppressionHint && !seenHints.has(diagnostic.suppressionHint)) {
82747
- seenHints.add(diagnostic.suppressionHint);
82748
- lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}↳ ${diagnostic.suppressionHint}`));
82749
- }
82750
- return lines;
82751
- };
82752
- const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
82753
- const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
82754
- const { severity } = representative;
82755
- const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
82756
- const headline = colorizeBySeverity(`${representative.category}: ${representative.title ?? ruleKey}`, severity);
82757
- const lines = [` ${colorizeBySeverity(severity === "error" ? "✖" : "⚠", severity)} ${headline}${trailingBadge}`];
82758
- if (renderEverySite && !isAgentEnvironment) {
82759
- const learnMoreLine = formatLearnMoreLine(representative);
82760
- if (learnMoreLine) lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.info(learnMoreLine)}`);
82761
- }
82762
- const isCollapsedWarningGroup = severity === "warning" && hasIndistinctSiteLocations(ruleDiagnostics);
82763
- const impactMessages = isCollapsedWarningGroup ? [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))] : [representative.message];
82764
- for (const impactMessage of impactMessages) for (const explanationLine of wrapTextToWidth(impactMessage, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(`${TOP_ERROR_DETAIL_INDENT}${explanationLine}`);
82765
- if (representative.help) for (const fixLine of wrapTextToWidth(`→ ${representative.help}`, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${fixLine}`));
82766
- const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
82767
- if (sharedFixSiteCount > 0) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}↳ One fix clears all ${sharedFixSiteCount} findings.`));
82768
- if (renderEverySite && isAgentEnvironment) {
82769
- const fixRecipeLine = formatFixRecipeLine(representative);
82770
- if (fixRecipeLine) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${fixRecipeLine}`));
82771
- }
82772
- const renderCodeFrame = severity === "error";
82773
- const sites = renderEverySite ? ruleDiagnostics : [representative];
82774
- if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, resolveFrameSourceRoot, renderCodeFrame, hyperlinks));
82775
- return lines;
82776
- };
82777
- const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
82778
- const selectTopErrorRuleGroups = (diagnostics, limit, rulePriority) => selectErrorRuleGroups(diagnostics, rulePriority).slice(0, limit);
82779
- const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
82780
- const errorRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority);
82781
- const shownErrorRuleCount = Math.min(3, errorRuleGroups.length);
82782
- if (diagnostics.length <= shownErrorRuleCount) return void 0;
82783
- const command = highlighter.bold(highlighter.info("npx react-doctor@latest --verbose"));
82784
- return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
82785
- };
82786
- const formatMigrationBucketLine = (bucket) => `${TOP_ERROR_DETAIL_INDENT}${bucket.title} ${highlighter.gray(`×${bucket.siteCount} across ${bucket.fileCount} files`)}`;
82787
- const buildMigrationScaleAdvisoryLines = (diagnostics) => {
82788
- const buckets = findMigrationScaleBuckets(diagnostics);
82789
- if (buckets.length === 0) return [];
82790
- const shownBuckets = buckets.slice(0, 3);
82791
- const lines = [` ${highlighter.warn("⚠")} ${highlighter.bold("Migration-scale change")}${highlighter.dim(": sample before you sweep")}`, ...shownBuckets.map(formatMigrationBucketLine)];
82792
- const remainingBuckets = buckets.length - shownBuckets.length;
82793
- if (remainingBuckets > 0) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}+${remainingBuckets} more ${remainingBuckets === 1 ? "rule" : "rules"} at this scale`));
82794
- for (const guidanceLine of wrapTextToWidth("Fixing all of them at once is hard to review and prone to subtle mistakes across the whole repo. Fix a representative few first and confirm the recipe holds. Then get the code owner's sign-off before changing the rest.", resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${guidanceLine}`));
82795
- const command = highlighter.info("npx react-doctor@latest <path>");
82796
- lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.dim("Scope it down one area at a time:")} ${command}`);
82797
- return lines;
82798
- };
82799
- const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
82800
- const buildTopErrorsSection = (diagnostics, resolveSourceRoot, resolveFrameSourceRoot, hyperlinks, rulePriority) => {
82801
- const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
82802
- if (topRuleGroups.length === 0) return {
82803
- lines: [],
82804
- blockOffsets: []
82805
- };
82806
- const lines = [` ${highlighter.bold(`Top ${topRuleGroups.length} ${topRuleGroups.length === 1 ? "error" : "errors"} you should fix`)}`, ""];
82807
- const blockOffsets = [];
82808
- for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
82809
- blockOffsets.push(lines.length);
82810
- lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, false, false, hyperlinks));
82811
- lines.push("");
82812
- }
82813
- return {
82814
- lines,
82815
- blockOffsets
82816
- };
82817
- };
82818
- const joinSections = (...sections) => {
82819
- const lines = [];
82820
- const sectionStarts = [];
82821
- for (const section of sections) {
82822
- if (section.length === 0) {
82823
- sectionStarts.push(null);
82824
- continue;
82825
- }
82826
- if (lines.length > 0) lines.push("");
82827
- sectionStarts.push(lines.length);
82828
- lines.push(...section);
82829
- }
82830
- if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
82831
- return {
82832
- lines,
82833
- sectionStarts
82834
- };
82835
- };
82836
- const buildOverviewHeaderLines = (diagnostics) => {
82837
- const totalIssueCount = diagnostics.length;
82838
- if (totalIssueCount === 0) return [];
82839
- const issueNoun = totalIssueCount === 1 ? "issue" : "issues";
82840
- return [` ${highlighter.bold(`All ${totalIssueCount} ${issueNoun}`)}`];
82841
- };
82842
- /**
82843
- * Effect-typed diagnostics renderer. Internal helpers build the
82844
- * line array purely; the IO happens once at the boundary with a
82845
- * single Effect.forEach over Console.log so failures or fiber
82846
- * interruption produce predictable partial output.
82847
- */
82848
- const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false, frameSourceRoot = sourceRoot) => gen(function* () {
82849
- const sectionPause = onboarding.sectionPause ?? void_;
82850
- const animateCountUp = onboarding.animateCountUp ?? false;
82851
- const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
82852
- const resolveFrameSourceRoot = typeof frameSourceRoot === "function" ? frameSourceRoot : () => frameSourceRoot;
82853
- let detailLines;
82854
- let topErrorBlockOffsets = [];
82855
- if (!isVerbose) {
82856
- const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, resolveFrameSourceRoot, hyperlinks, rulePriority);
82857
- detailLines = topErrors.lines;
82858
- topErrorBlockOffsets = topErrors.blockOffsets;
82859
- } else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
82860
- return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, resolveFrameSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
82861
- });
82862
- const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
82863
- const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
82864
- const categoryLines = buildCategoryTallyLines(categoryTallies);
82865
- const overviewDividerLines = detailLines.length > 0 && categoryLines.length > 0 ? [buildSectionDivider()] : [];
82866
- const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : [], buildMigrationScaleAdvisoryLines(diagnostics));
82867
- const [detailStart, , , categoryStart] = sectionStarts;
82868
- const pauseBeforeLineIndices = detailStart == null ? /* @__PURE__ */ new Set() : new Set(topErrorBlockOffsets.map((offset) => detailStart + offset));
82869
- let lineIndex = 0;
82870
- while (lineIndex < lines.length) {
82871
- if (animateCountUp && lineIndex === categoryStart && categoryLines.length > 0) {
82872
- yield* printCategoryCountUp(categoryTallies);
82873
- lineIndex += categoryLines.length;
82874
- continue;
82875
- }
82876
- if (pauseBeforeLineIndices.has(lineIndex)) yield* sectionPause;
82877
- yield* log(lines[lineIndex]);
82878
- lineIndex += 1;
82879
- }
82880
- });
82881
- const formatElapsedTime = (elapsedMilliseconds) => {
82882
- if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
82883
- return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
82884
- };
82885
- const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
82886
- const firstDiagnostic = ruleDiagnostics[0];
82887
- const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
82888
- const sections = [
82889
- `Rule: ${ruleKey}`,
82890
- `Severity: ${firstDiagnostic.severity}`,
82891
- `Category: ${firstDiagnostic.category}`,
82892
- `Count: ${ruleDiagnostics.length}`,
82893
- "",
82894
- distinctMessages.join("\n\n")
82895
- ];
82896
- if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
82897
- if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
82898
- const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
82899
- if (fixRecipeLine) sections.push("", fixRecipeLine);
82900
- sections.push("", "Files:");
82901
- const fileEntries = buildVerboseFileEntries(ruleDiagnostics);
82902
- for (const [filePath, { contextTag, sites }] of fileEntries) if (sites.length > 0) for (const site of sites) {
82903
- sections.push(` ${filePath}:${site.line}${contextTag}`);
82904
- if (site.suppressionHint) sections.push(` ${site.suppressionHint}`);
82905
- }
82906
- else sections.push(` ${filePath}${contextTag}`);
82907
- return sections.join("\n") + "\n";
82908
- };
82909
- //#endregion
82910
- //#region src/cli/utils/compute-score-projection.ts
82911
- const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore) => {
82912
- const topErrorRuleKeys = getTopErrorRuleKeys(topErrorSource, 3, buildRulePriorityMap([currentScore]));
82913
- if (topErrorRuleKeys.size === 0) return null;
82914
- const potentialScore = await calculateScore(rescoreSource.filter((diagnostic) => !topErrorRuleKeys.has(`${diagnostic.plugin}/${diagnostic.rule}`)));
82915
- if (!potentialScore || potentialScore.score <= currentScore.score) return null;
82916
- return potentialScore.score;
82917
- };
82918
- //#endregion
82919
- //#region src/cli/utils/filter-diagnostics-by-categories.ts
82920
- const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
82921
- //#endregion
82922
- //#region src/cli/utils/supports-hyperlinks.ts
82923
- const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
82924
- "iTerm.app",
82925
- "WezTerm",
82926
- "vscode",
82927
- "Hyper",
82928
- "ghostty",
82929
- "Tabby",
82930
- "rio"
82931
- ]);
82932
- const parseVteVersion = (raw) => {
82933
- const parsed = Number.parseInt(raw ?? "", 10);
82934
- return Number.isNaN(parsed) ? 0 : parsed;
82935
- };
82936
- /**
82937
- * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
82938
- * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
82939
- * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
82940
- * forces on), mirroring how the ecosystem's terminal libraries gate the same
82941
- * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
82942
- * raw escape rather than a link). Unknown terminals default to off.
82943
- */
82944
- const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
82945
- const forced = env.FORCE_HYPERLINK;
82946
- if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
82947
- if (stream.isTTY !== true) return false;
82948
- if (env.TERM === "dumb") return false;
82949
- if (isCiEnvironment(env)) return false;
82950
- if (env.WT_SESSION) return true;
82951
- if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
82952
- if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
82953
- return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
82954
- };
82955
- //#endregion
82956
- //#region src/cli/utils/should-render-hyperlinks.ts
82957
- /**
82958
- * Whether to emit OSC 8 clickable `file:line` locations for this run: a
82959
- * hyperlink-capable terminal AND not a coding agent (whose output parsers
82960
- * would choke on the escape sequences).
82961
- */
82962
- const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
82963
- //#endregion
82964
- //#region src/cli/utils/should-show-share-link.ts
82965
- const shouldShowShareLink = ({ noScore, share, isCi }) => !noScore && share && !isCi;
82966
- const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
82967
- const FALSY_FLAG_VALUES = new Set([
82968
- "",
82969
- "0",
82970
- "false"
82971
- ]);
82972
- const isOnboardingForced = (environment = process.env) => {
82973
- const value = environment[FORCE_ONBOARDING_ENV_VAR];
82974
- return value !== void 0 && !FALSY_FLAG_VALUES.has(value.toLowerCase());
82975
- };
82976
- const onboardingSectionPause = (shouldPace) => shouldPace ? sleep(850) : void_;
82977
- const shouldRecordOnboarding = (input) => input.paceOnboardingSections && !input.forceOnboarding && !input.verbose && !input.isNonInteractiveEnvironment;
82978
- const canAnimateOnboarding = (stream = process.stdout) => {
82979
- if (!(stream.isTTY === true && (stream.columns ?? 0) > 0 && process.env.TERM !== "dumb")) return false;
82980
- if (isOnboardingForced()) return true;
82981
- return !isGitHookEnvironment() && !isCiEnvironment();
82982
- };
82983
- //#endregion
82984
- //#region src/cli/utils/now-iso.ts
82985
- const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
82986
- const ONBOARDING_EVENT = "onboarding";
82987
- const CI_PITCH_EVENT = "ci-pitch";
82988
- const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
82989
- const SETUP_HINT_EVENT = "setup-hint";
82990
- const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
82991
- const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
82992
- const foldLegacyDecisions = (projects, legacy, eventId) => {
82993
- for (const [hash, record] of Object.entries(legacy ?? {})) {
82994
- const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
82995
- projects[hash] = {
82996
- ...existing,
82997
- events: {
82998
- ...existing.events,
82999
- [eventId]: {
83000
- firedAt: record.at ?? nowIso(),
83001
- version: 1,
83002
- ...record.outcome ? { outcome: record.outcome } : {}
83003
- }
83004
- }
83005
- };
83006
- }
83007
- };
83008
- const migrateCliState = (state) => {
83009
- if (typeof state.schemaVersion === "number" && state.schemaVersion >= 2) return state;
83010
- const projects = {};
83011
- for (const [hash, record] of Object.entries(state.projects ?? {})) {
83012
- const carried = {
83013
- rootDirectory: record.rootDirectory,
83014
- ...record.events ? { events: record.events } : {},
83015
- ...record.migrations ? { migrations: record.migrations } : {}
83016
- };
83017
- projects[hash] = record.setupPrompt === false ? {
83018
- ...carried,
83019
- events: {
83020
- ...carried.events,
83021
- [SETUP_HINT_EVENT]: {
83022
- firedAt: nowIso(),
83023
- version: 1,
83024
- outcome: "declined"
83025
- }
83026
- }
83027
- } : carried;
83028
- }
83029
- foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
83030
- foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
83031
- return {
83032
- schemaVersion: 2,
83033
- global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
83034
- firedAt: state.onboardedAt,
83035
- version: 1
83036
- } } } : {},
83037
- projects
83038
- };
83039
- };
83040
- const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
83041
- const openStore = (options = {}) => new Conf({
83042
- projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
83043
- cwd: resolveConfigDir(options)
83044
- });
83045
- const openMigratedStore = (options) => {
83046
- const store = openStore(options);
83047
- const state = store.store;
83048
- const migrated = migrateCliState(state);
83049
- if (migrated !== state) store.store = migrated;
83050
- return store;
83051
- };
83052
- const readCliState = (select, fallback, options = {}) => {
83053
- try {
83054
- return select(openMigratedStore(options).store);
83055
- } catch {
83056
- return fallback;
83057
- }
83058
- };
83059
- const updateCliState = (update, options = {}) => {
83060
- try {
83061
- const store = openMigratedStore(options);
83062
- store.store = update(store.store);
83063
- return true;
83064
- } catch {
83065
- return false;
83066
- }
83067
- };
83068
- //#endregion
83069
- //#region src/cli/utils/hash-project-root.ts
83070
- const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
83071
- //#endregion
83072
- //#region src/cli/utils/cli-lifecycle.ts
83073
- const versionOf = (item) => item.version ?? 1;
83074
- const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
83075
- const updateScope = (state, scoped, projectRoot, updateScopeState) => {
83076
- if (scoped.scope === "global") return {
83077
- ...state,
83078
- global: updateScopeState(state.global ?? {})
83079
- };
83080
- if (projectRoot === void 0) return state;
83081
- const projectKey = hashProjectRoot(projectRoot);
83082
- const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
83083
- return {
83084
- ...state,
83085
- projects: {
83086
- ...state.projects,
83087
- [projectKey]: {
83088
- ...base,
83089
- ...updateScopeState(base)
83090
- }
83091
- }
83092
- };
83093
- };
83094
- const isGatePending = (gate, target = {}, options = {}) => {
83095
- if (gate.scope === "project" && target.projectRoot === void 0) return false;
83096
- return readCliState((state) => {
83097
- const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
83098
- return !record || record.version < versionOf(gate);
83099
- }, gate.fireWhenUnknown ?? false, options);
83100
- };
83101
- const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
83102
- ...scope,
83103
- events: {
83104
- ...scope.events,
83105
- [gate.id]: {
83106
- firedAt: nowIso(),
83107
- version: versionOf(gate),
83108
- ...target.outcome ? { outcome: target.outcome } : {}
83109
- }
83110
- }
83111
- })), options);
83112
- const readPreference = (preference, target = {}, options = {}) => readCliState((state) => selectScope(state, preference, target.projectRoot)?.preferences?.[preference.id] ?? null, null, options);
83113
- const writePreference = (preference, value, target = {}, options = {}) => updateCliState((state) => updateScope(state, preference, target.projectRoot, (scope) => ({
83114
- ...scope,
83115
- preferences: {
83116
- ...scope.preferences,
83117
- [preference.id]: value
83118
- }
83119
- })), options);
83120
- const isMigrationPending = (migration, target = {}, options = {}) => {
83121
- if (migration.scope === "project" && target.projectRoot === void 0) return false;
83122
- return readCliState((state) => {
83123
- const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
83124
- return !record || record.version < versionOf(migration);
83125
- }, false, options);
83126
- };
83127
- const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
83128
- ...scope,
83129
- migrations: {
83130
- ...scope.migrations,
83131
- [migration.id]: record
83132
- }
83133
- })), options);
83134
- const runMigrations = async (migrations, target = {}, options = {}) => {
83135
- const results = [];
83136
- for (const migration of migrations) {
83137
- if (!isMigrationPending(migration, target, options)) {
83138
- results.push({
83139
- id: migration.id,
83140
- ran: false,
83141
- applied: true
83142
- });
83143
- continue;
83144
- }
83145
- let applied = false;
83146
- try {
83147
- applied = await migration.run({ projectRoot: target.projectRoot });
83148
- } catch {
83149
- applied = false;
83150
- }
83151
- if (applied) recordMigration(migration, target.projectRoot, {
83152
- ranAt: nowIso(),
83153
- version: versionOf(migration)
83154
- }, options);
83155
- results.push({
83156
- id: migration.id,
83157
- ran: true,
83158
- applied
83159
- });
83160
- }
83161
- return results;
83162
- };
83163
- //#endregion
83164
- //#region src/cli/utils/onboarding-state.ts
83165
- const ONBOARDING_GATE = {
83166
- id: ONBOARDING_EVENT,
83167
- scope: "global"
83168
- };
83169
- const hasCompletedOnboarding = (options = {}) => !isGatePending(ONBOARDING_GATE, {}, options);
83170
- const markOnboardingComplete = (options = {}) => {
83171
- if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
83172
- };
83173
- //#endregion
83174
- //#region src/cli/utils/colorize-by-score.ts
83175
- const colorizeByScore = (text, score) => {
83176
- if (score >= 75) return highlighter.success(text);
83177
- if (score >= 50) return highlighter.warn(text);
83178
- return highlighter.error(text);
83179
- };
83180
- //#endregion
83181
- //#region src/cli/utils/doctor-face.ts
83182
- const doctorFace = (score) => {
83183
- if (score >= 75) return ["◠ ◠", " ▽ "];
83184
- if (score >= 50) return ["• •", " ─ "];
83185
- return ["x x", " ▽ "];
83186
- };
83187
- //#endregion
83188
- //#region src/cli/utils/ease-out-cubic.ts
83189
- const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
83190
- //#endregion
83191
- //#region src/cli/utils/render-score-header.ts
83192
- const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
83193
- const RAINBOW_GRADIENT_WIDTH = 80;
83194
- const RAINBOW_OKLCH_LIGHTNESS = .638;
83195
- const RAINBOW_OKLCH_CHROMA = .129;
83196
- const FACE_BOX_TOP_BORDER = "┌─────┐";
83197
- const SCORE_RIGHT_COLUMN_OFFSET = 11;
83198
- const getScoreBarWidth = () => resolveClampedWidth({
83199
- reservedColumns: SCORE_RIGHT_COLUMN_OFFSET + 1,
83200
- fullWidth: 50,
83201
- minWidth: 10
83202
- });
83203
- const buildScoreBarSegments = (filledCount) => {
83204
- const emptyCount = Math.max(0, getScoreBarWidth() - filledCount);
83205
- return {
83206
- filledSegment: "█".repeat(filledCount),
83207
- emptySegment: "░".repeat(emptyCount)
83208
- };
83209
- };
83210
- const getFilledCount = (score) => Math.round(score / 100 * getScoreBarWidth());
83211
- const joinScoreHeaderFrame = (lines) => `${lines[0]}\n\r${lines[1]}\n\r${lines[2]}\n\r${lines[3]}\n`;
83212
- const buildRawScoreBar = (displayScore) => {
83213
- const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
83214
- return filledSegment + emptySegment;
83215
- };
83216
- const buildScoreHeaderLine = (faceLine, rightColumnContent) => {
83217
- return ` ${faceLine}${rightColumnContent.length > 0 ? " " : ""}${rightColumnContent}`;
83218
- };
83219
- const getRightColumnOffset = (faceLine) => ` ${faceLine} `.length;
83220
- const clampColorChannel = (value) => Math.max(0, Math.min(255, Math.round(value)));
83221
- const encodeSrgb = (value) => value <= .0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - .055;
83222
- const oklchToRgb = (lightness, chroma, hue) => {
83223
- const hueRadians = hue * Math.PI / 180;
83224
- const labA = chroma * Math.cos(hueRadians);
83225
- const labB = chroma * Math.sin(hueRadians);
83226
- const longCone = (lightness + .3963377774 * labA + .2158037573 * labB) ** 3;
83227
- const mediumCone = (lightness - .1055613458 * labA - .0638541728 * labB) ** 3;
83228
- const shortCone = (lightness - .0894841775 * labA - 1.291485548 * labB) ** 3;
83229
- return {
83230
- red: clampColorChannel(encodeSrgb(4.0767416621 * longCone - 3.3077115913 * mediumCone + .2309699292 * shortCone) * 255),
83231
- green: clampColorChannel(encodeSrgb(-1.2684380046 * longCone + 2.6097574011 * mediumCone - .3413193965 * shortCone) * 255),
83232
- blue: clampColorChannel(encodeSrgb(-.0041960863 * longCone - .7034186147 * mediumCone + 1.707614701 * shortCone) * 255)
83233
- };
83234
- };
83235
- const colorizeTrueColor = (text, { red, green, blue }) => `\x1b[38;2;${red};${green};${blue}m${text}\x1b[39m`;
83236
- const colorizeRainbowText = (text, frame, offset = 0) => [...text].map((character, index) => {
83237
- if (character === " ") return character;
83238
- return colorizeTrueColor(character, oklchToRgb(RAINBOW_OKLCH_LIGHTNESS, RAINBOW_OKLCH_CHROMA, ((index + offset) / RAINBOW_GRADIENT_WIDTH * 360 + frame * RAINBOW_HUE_SHIFT_PER_FRAME) % 360));
83239
- }).join("");
83240
- const buildRainbowHeaderLine = (faceLine, rightColumnContent, frame) => colorizeRainbowText(buildScoreHeaderLine(faceLine, rightColumnContent), frame);
83241
- const buildScoreBar = (displayScore, colorScore = displayScore) => {
83242
- const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
83243
- return colorizeByScore(filledSegment, colorScore) + highlighter.dim(emptySegment);
83244
- };
83245
- const buildProjectedScoreBar = (currentScore, potentialScore) => {
83246
- const barWidth = getScoreBarWidth();
83247
- const currentFill = getFilledCount(currentScore);
83248
- const potentialFill = Math.min(getFilledCount(potentialScore), barWidth);
83249
- const gainCount = Math.max(0, potentialFill - currentFill);
83250
- const emptyCount = Math.max(0, barWidth - currentFill - gainCount);
83251
- return colorizeByScore("█".repeat(currentFill), currentScore) + highlighter.dim(colorizeByScore("▓".repeat(gainCount), currentScore)) + highlighter.dim("░".repeat(emptyCount));
83252
- };
83253
- const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
83254
- const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
83255
- const buildRawFaceLines = (score) => {
83256
- const [eyes, mouth] = doctorFace(score);
83257
- return [
83258
- FACE_BOX_TOP_BORDER,
83259
- `│ ${eyes} │`,
83260
- `│ ${mouth} │`,
83261
- "└─────┘"
83262
- ];
83263
- };
83264
- const buildFaceRenderedLines = (score) => {
83265
- const colorize = (text) => colorizeByScore(text, score);
83266
- return buildRawFaceLines(score).map(colorize);
83267
- };
83268
- const buildScoreLine = (displayScore, finalScore, label) => {
83269
- const scoreNumber = colorizeByScore(`${displayScore}`, finalScore);
83270
- const scoreLabel = colorizeByScore(label, finalScore);
83271
- return `${scoreNumber} ${highlighter.dim(`/ 100`)} ${scoreLabel}`;
83272
- };
83273
- const buildRawScoreLine = (displayScore, label) => `${displayScore} / 100 ${label}`;
83274
- const buildRainbowScoreHeaderFrame = ({ score, displayScore, label, frame }) => {
83275
- const rawFaceLines = buildRawFaceLines(score);
83276
- return joinScoreHeaderFrame([
83277
- buildRainbowHeaderLine(rawFaceLines[0] ?? "", buildRawScoreLine(displayScore, label), frame),
83278
- buildRainbowHeaderLine(rawFaceLines[1] ?? "", buildRawScoreBar(displayScore), frame),
83279
- buildRainbowHeaderLine(rawFaceLines[2] ?? "", RAW_BRANDING_LINE, frame),
83280
- buildRainbowHeaderLine(rawFaceLines[3] ?? "", "", frame)
83281
- ]);
83282
- };
83283
- const buildFinalPerfectScoreHeaderFrame = (score, label, frame) => {
83284
- const rawFaceLines = buildRawFaceLines(score);
83285
- const renderedFaceLines = buildFaceRenderedLines(score);
83286
- const rainbowBarLine = colorizeRainbowText(buildRawScoreBar(score), frame, getRightColumnOffset(rawFaceLines[1] ?? ""));
83287
- return joinScoreHeaderFrame([
83288
- buildScoreHeaderLine(renderedFaceLines[0] ?? "", buildScoreLine(score, score, label)),
83289
- buildScoreHeaderLine(renderedFaceLines[1] ?? "", rainbowBarLine),
83290
- buildScoreHeaderLine(renderedFaceLines[2] ?? "", BRANDING_LINE),
83291
- buildScoreHeaderLine(renderedFaceLines[3] ?? "", "")
83292
- ]);
83293
- };
83294
- const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex, renderedFaceLine, rawFaceLine, rightColumnContent, rawRightColumnContent, score }) => {
83295
- if (!isPerfectScore) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
83296
- if (shouldAnimate) return buildRainbowHeaderLine(rawFaceLine, rawRightColumnContent, 0);
83297
- if (lineIndex !== 1) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
83298
- return buildScoreHeaderLine(renderedFaceLine, colorizeRainbowText(buildRawScoreBar(score), 0, getRightColumnOffset(rawFaceLine)));
83299
- };
83300
- const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, potentialScore) => gen(function* () {
83301
- const isPerfectScore = score === 100;
83302
- for (let frame = 0; frame <= 40; frame += 1) {
83303
- const progress = easeOutCubic(frame / 40);
83304
- const animatedScore = Math.round(score * progress);
83305
- if (isPerfectScore) {
83306
- yield* writeStdout(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
83307
- score,
83308
- displayScore: animatedScore,
83309
- label,
83310
- frame
83311
- })}`);
83312
- if (frame < 40) yield* sleep(50);
83313
- continue;
83314
- }
83315
- const animatedScoreLine = buildScoreLine(animatedScore, score, label);
83316
- const animatedBarLine = frame === 40 && potentialScore !== void 0 ? buildProjectedScoreBar(score, potentialScore) : buildScoreBar(animatedScore, score);
83317
- yield* writeStdout(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
83318
- if (frame < 40) yield* sleep(50);
83319
- }
83320
- if (!isPerfectScore) return;
83321
- for (let frame = 0; frame < 16; frame += 1) {
83322
- yield* writeStdout(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
83323
- score,
83324
- displayScore: score,
83325
- label,
83326
- frame
83327
- })}`);
83328
- yield* sleep(50);
83329
- }
83330
- yield* writeStdout(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16)}\x1b[2A`);
83331
- });
83332
- const printScoreHeader = (scoreResult, potentialScore) => gen(function* () {
83333
- const isPerfectScore = scoreResult.score === 100;
83334
- const renderedFaceLines = buildFaceRenderedLines(scoreResult.score);
83335
- const rawFaceLines = buildRawFaceLines(scoreResult.score);
83336
- const shouldAnimate = !isSpinnerSilent() && canAnimateOnboarding(process.stdout);
83337
- const displayScore = shouldAnimate ? 0 : scoreResult.score;
83338
- const rightColumnLines = [
83339
- buildScoreLine(displayScore, scoreResult.score, scoreResult.label),
83340
- shouldAnimate ? buildScoreBar(0, scoreResult.score) : potentialScore !== void 0 ? buildProjectedScoreBar(scoreResult.score, potentialScore) : buildScoreBar(scoreResult.score),
83341
- BRANDING_LINE,
83342
- ""
83343
- ];
83344
- const rawRightColumnLines = [
83345
- buildRawScoreLine(displayScore, scoreResult.label),
83346
- buildRawScoreBar(displayScore),
83347
- RAW_BRANDING_LINE,
83348
- ""
83349
- ];
83350
- for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) yield* log(buildInitialScoreHeaderLine({
83351
- isPerfectScore,
83352
- shouldAnimate,
83353
- lineIndex,
83354
- renderedFaceLine: renderedFaceLines[lineIndex] ?? "",
83355
- rawFaceLine: rawFaceLines[lineIndex] ?? "",
83356
- rightColumnContent: rightColumnLines[lineIndex] ?? "",
83357
- rawRightColumnContent: rawRightColumnLines[lineIndex] ?? "",
83358
- score: scoreResult.score
83359
- }));
83360
- yield* log("");
83361
- if (shouldAnimate) {
83362
- yield* writeStdout("\x1B[5A");
83363
- yield* printAnimatedScore(renderedFaceLines[0], renderedFaceLines[1], scoreResult.score, scoreResult.label, potentialScore);
83364
- yield* writeStdout("\x1B[3B");
83365
- }
83366
- });
83367
- const animateScoreProjection = (scoreResult, potentialScore, linesBelowBar) => gen(function* () {
83368
- if (scoreResult.score === 100 || potentialScore <= scoreResult.score) return;
83369
- const barFaceLine = buildFaceRenderedLines(scoreResult.score)[1] ?? "";
83370
- for (let frame = 1; frame <= 16; frame += 1) {
83371
- const progress = easeOutCubic(frame / 16);
83372
- const displayedPotential = scoreResult.score + (potentialScore - scoreResult.score) * progress;
83373
- yield* writeStdout(`\x1b[${linesBelowBar}A\r${buildScoreHeaderLine(barFaceLine, buildProjectedScoreBar(scoreResult.score, displayedPotential))}\x1b[${linesBelowBar}B\r`);
83374
- if (frame < 16) yield* sleep(35);
83375
- }
83376
- });
83377
- const printBrandingOnlyHeader = gen(function* () {
83378
- yield* log(` ${BRANDING_LINE}`);
83379
- yield* log("");
83380
- });
83381
- const printNoScoreHeader = (noScoreMessage) => gen(function* () {
83382
- yield* log(` ${BRANDING_LINE}`);
83383
- yield* log(` ${highlighter.gray(noScoreMessage)}`);
83384
- yield* log("");
83385
- });
83386
- //#endregion
83387
82565
  //#region src/cli/utils/build-footer-description-lines.ts
83388
82566
  const FOOTER_DESCRIPTION_INDENT = " ";
83389
82567
  const buildFooterDescriptionLines = (description) => {
@@ -83404,67 +82582,90 @@ const buildFooterLinkLines = ({ shareUrl }) => [
83404
82582
  ...buildFooterDescriptionLines("Report issues and star the repository!")
83405
82583
  ];
83406
82584
  //#endregion
83407
- //#region src/cli/utils/write-diagnostics-directory.ts
83408
- const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
83409
- const readPreviousRuleDumpFileNames = (directory) => {
83410
- try {
83411
- const previous = JSON.parse(NFS.readFileSync(Path.join(directory, "diagnostics.json"), "utf8"));
83412
- return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
83413
- } catch {
83414
- return /* @__PURE__ */ new Set();
83415
- }
83416
- };
83417
- const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
83418
- const directory = outputDirectory ? Path.resolve(outputDirectory) : Path.join(tmpdir(), `react-doctor-${randomUUID()}`);
83419
- if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) NFS.rmSync(Path.join(directory, fileName), { force: true });
83420
- NFS.mkdirSync(directory, { recursive: true });
83421
- for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) NFS.writeFileSync(Path.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
83422
- NFS.writeFileSync(Path.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
83423
- return directory;
83424
- };
82585
+ //#region src/cli/utils/build-section-divider.ts
82586
+ const DIVIDER_INDENT = " ";
82587
+ const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
82588
+ //#endregion
82589
+ //#region src/cli/utils/collect-affected-files.ts
82590
+ const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
83425
82591
  //#endregion
83426
- //#region src/cli/utils/render-summary.ts
83427
- const buildShareUrl = (diagnostics, scoreResult, projectName) => {
83428
- const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
83429
- const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
83430
- const affectedFileCount = collectAffectedFiles(diagnostics).size;
83431
- const params = new URLSearchParams();
83432
- params.set("p", projectName);
83433
- if (scoreResult) params.set("s", String(scoreResult.score));
83434
- if (errorCount > 0) params.set("e", String(errorCount));
83435
- if (warningCount > 0) params.set("w", String(warningCount));
83436
- if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
83437
- return `${SHARE_BASE_URL}?${params.toString()}`;
82592
+ //#region src/cli/utils/print-footer.ts
82593
+ const buildShareUrl = (input) => {
82594
+ const errorCount = input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
82595
+ const warningCount = input.diagnostics.length - errorCount;
82596
+ const affectedFileCount = collectAffectedFiles(input.diagnostics).size;
82597
+ const parameters = new URLSearchParams({ p: input.projectName });
82598
+ if (input.scoreResult) parameters.set("s", String(input.scoreResult.score));
82599
+ if (errorCount > 0) parameters.set("e", String(errorCount));
82600
+ if (warningCount > 0) parameters.set("w", String(warningCount));
82601
+ if (affectedFileCount > 0) parameters.set("f", String(affectedFileCount));
82602
+ return `${SHARE_BASE_URL}?${parameters.toString()}`;
83438
82603
  };
83439
82604
  const printFooter = (input) => gen(function* () {
83440
82605
  yield* log("");
83441
82606
  yield* log(buildSectionDivider());
83442
82607
  yield* log("");
83443
- const shareUrl = input.isOffline ? null : buildShareUrl(input.diagnostics, input.scoreResult, input.projectName);
82608
+ const shareUrl = input.isOffline ? null : buildShareUrl(input);
83444
82609
  for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
83445
82610
  });
83446
- const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
83447
- const writtenDirectory = yield* try_({
83448
- try: () => writeDiagnosticsDirectory(diagnostics, outputDirectory),
83449
- catch: (cause) => cause
83450
- }).pipe(orElseSucceed(() => null));
83451
- if (writtenDirectory !== null && (verbose || outputDirectory)) {
83452
- const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
83453
- yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
82611
+ //#endregion
82612
+ //#region src/cli/utils/format-elapsed-time.ts
82613
+ const formatElapsedTime = (elapsedMilliseconds) => {
82614
+ if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
82615
+ return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
82616
+ };
82617
+ //#endregion
82618
+ //#region src/cli/utils/pluralize.ts
82619
+ const pluralize = (count, singularNoun) => `${count} ${singularNoun}${count === 1 ? "" : "s"}`;
82620
+ //#endregion
82621
+ //#region src/cli/utils/print-headless-report.ts
82622
+ const formatContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
82623
+ const printHeadlessReport = (input) => gen(function* () {
82624
+ yield* log(`${highlighter.success("✔")} Scanned ${pluralize(input.scannedFileCount, "file")} in ${formatElapsedTime(input.elapsedMilliseconds)}`);
82625
+ yield* log("");
82626
+ yield* log(highlighter.bold(`React Doctor — ${input.projectName}`));
82627
+ if (input.scoreResult) yield* log(`Score: ${input.scoreResult.score} / 100 ${input.scoreResult.label}`);
82628
+ else yield* log(highlighter.gray(input.noScoreMessage));
82629
+ if (input.diagnostics.length === 0) {
82630
+ yield* log("");
82631
+ yield* log(highlighter.success(`✔ ${input.emptyStateMessage}`));
82632
+ } else {
82633
+ yield* log("");
82634
+ yield* log(highlighter.bold(pluralize(input.diagnostics.length, "issue")));
82635
+ const categoryCounts = /* @__PURE__ */ new Map();
82636
+ for (const diagnostic of input.diagnostics) {
82637
+ const counts = categoryCounts.get(diagnostic.category) ?? {
82638
+ error: 0,
82639
+ warning: 0
82640
+ };
82641
+ categoryCounts.set(diagnostic.category, {
82642
+ error: counts.error + Number(diagnostic.severity === "error"),
82643
+ warning: counts.warning + Number(diagnostic.severity === "warning")
82644
+ });
82645
+ }
82646
+ for (const [category, counts] of categoryCounts) {
82647
+ const countParts = [];
82648
+ if (counts.error > 0) countParts.push(pluralize(counts.error, "error"));
82649
+ if (counts.warning > 0) countParts.push(pluralize(counts.warning, "warning"));
82650
+ yield* log(`${category}: ${countParts.join(", ")}`);
82651
+ }
82652
+ yield* log("");
82653
+ for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(input.diagnostics)) {
82654
+ const representativeDiagnostic = ruleDiagnostics[0];
82655
+ if (representativeDiagnostic === void 0) continue;
82656
+ const severityIcon = representativeDiagnostic.severity === "error" ? "✖" : "⚠";
82657
+ const colorize = representativeDiagnostic.severity === "error" ? highlighter.error : highlighter.warn;
82658
+ const occurrenceSuffix = ruleDiagnostics.length > 1 ? ` ×${ruleDiagnostics.length}` : "";
82659
+ yield* log(colorize(`${severityIcon} ${representativeDiagnostic.title ?? ruleKey}${occurrenceSuffix}`));
82660
+ yield* log(highlighter.gray(` ${ruleKey}`));
82661
+ for (const diagnostic of ruleDiagnostics) yield* log(` ${formatDiagnosticSite(diagnostic)}${formatContextTag(diagnostic)}`);
82662
+ }
82663
+ }
82664
+ if (input.skippedChecks.length > 0) {
82665
+ yield* log("");
82666
+ yield* warn$1(highlighter.warn(`Results are incomplete: ${input.skippedChecks.join(" and ")} checks failed.`));
83454
82667
  }
83455
82668
  });
83456
- const printSummary = (input) => gen(function* () {
83457
- if (input.scoreResult) {
83458
- const animateProjection = Boolean(input.animateProjection) && input.potentialScore != null && !input.verbose;
83459
- yield* printScoreHeader(input.scoreResult, animateProjection ? void 0 : input.potentialScore ?? void 0);
83460
- if (input.potentialScore != null) {
83461
- const improvement = input.potentialScore - input.scoreResult.score;
83462
- yield* log(highlighter.gray(" You could improve ") + colorizeByScore(`+${improvement}%`, input.potentialScore) + highlighter.gray(` by fixing the top 3 issues`));
83463
- if (animateProjection) yield* animateScoreProjection(input.scoreResult, input.potentialScore, 5);
83464
- }
83465
- } else yield* printNoScoreHeader(input.noScoreMessage);
83466
- yield* printDiagnosticsDump(input.diagnostics, input.outputDirectory, input.verbose);
83467
- });
83468
82669
  //#endregion
83469
82670
  //#region src/cli/utils/resolve-oxlint-node.ts
83470
82671
  const consoleWarn = (message) => warn$1(highlighter.warn(message));
@@ -83538,15 +82739,6 @@ const resolveCliCategories = (categoryFlag) => {
83538
82739
  //#endregion
83539
82740
  //#region src/inspect.ts
83540
82741
  const silentConsole = makeNoopConsole();
83541
- const recordOnboardingCompletion = (options) => {
83542
- const forceOnboarding = isOnboardingForced();
83543
- if (shouldRecordOnboarding({
83544
- paceOnboardingSections: !options.silent && !options.scoreOnly && !options.suppressRendering && !options.verbose && canAnimateOnboarding(process.stdout) && (forceOnboarding || !hasCompletedOnboarding()),
83545
- forceOnboarding,
83546
- verbose: options.verbose,
83547
- isNonInteractiveEnvironment: options.isNonInteractiveEnvironment
83548
- })) markOnboardingComplete();
83549
- };
83550
82742
  const buildChangedLineMatcher = (directory, changedLineRanges) => {
83551
82743
  const rangesByFile = /* @__PURE__ */ new Map();
83552
82744
  for (const entry of changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
@@ -83826,7 +83018,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
83826
83018
  recordSentryProjectContext(cachedPayload.project, rootSentrySpan, { concurrentScan: options.concurrentScan });
83827
83019
  recordCount(METRIC.projectDetected, 1);
83828
83020
  const baselineDegraded = Boolean(options.baseline) && isDiffMode && cachedPayload.baselineDelta === void 0;
83829
- const result = await renderAndRecordScan({
83021
+ return await renderAndRecordScan({
83830
83022
  payload: cachedPayload,
83831
83023
  options,
83832
83024
  userConfig,
@@ -83837,8 +83029,6 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
83837
83029
  baselineDegraded,
83838
83030
  wholeRepoCacheHit: true
83839
83031
  });
83840
- recordOnboardingCompletion(options);
83841
- return result;
83842
83032
  }
83843
83033
  const shouldShowProgressSpinners = !options.isCiOrCodingAgentEnvironment && !options.silent && !options.scoreOnly && !options.suppressRendering && options.lint && Boolean(resolvedNodeBinaryPath);
83844
83034
  const layers = buildRuntimeLayers({
@@ -83941,7 +83131,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
83941
83131
  suppressedRuleCounts: output.suppressedRuleCounts
83942
83132
  };
83943
83133
  if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
83944
- const result = await renderAndRecordScan({
83134
+ return await renderAndRecordScan({
83945
83135
  payload,
83946
83136
  options,
83947
83137
  userConfig,
@@ -83959,8 +83149,6 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
83959
83149
  deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
83960
83150
  deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
83961
83151
  });
83962
- recordOnboardingCompletion(options);
83963
- return result;
83964
83152
  };
83965
83153
  const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
83966
83154
  const renderAndRecordScan = async (input) => {
@@ -83979,7 +83167,6 @@ const renderAndRecordScan = async (input) => {
83979
83167
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
83980
83168
  securityScanFailed: input.payload.securityScanFailed ?? false,
83981
83169
  securityScanFailureReason: input.payload.securityScanFailureReason ?? null,
83982
- directory: input.payload.directory,
83983
83170
  scannedFileCount: input.payload.scannedFileCount,
83984
83171
  scannedFilePaths: input.payload.scannedFilePaths,
83985
83172
  analyzedFiles: input.payload.analyzedFiles ?? [],
@@ -84030,7 +83217,7 @@ const renderAndRecordScan = async (input) => {
84030
83217
  return result;
84031
83218
  };
84032
83219
  const finalizeAndRender = (input) => gen(function* () {
84033
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, securityScanFailureReason, directory, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
83220
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, securityScanFailureReason, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
84034
83221
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
84035
83222
  didLintFail,
84036
83223
  lintFailureReason,
@@ -84082,69 +83269,32 @@ const finalizeAndRender = (input) => gen(function* () {
84082
83269
  else yield* error$1(highlighter.gray(noScoreMessage));
84083
83270
  return buildResult();
84084
83271
  }
84085
- const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
84086
- const pause = onboardingSectionPause(animateRender);
84087
- const useHyperlinks = shouldRenderHyperlinks(process.stdout);
84088
83272
  const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
84089
- const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
84090
- if (printedDiagnostics.length === 0) {
84091
- yield* pause;
84092
- if (hasSkippedChecks) {
84093
- const skippedLabel = skippedChecks.join(" and ");
84094
- yield* warn$1(highlighter.warn(`No issues detected, but ${skippedLabel} checks failed — results are incomplete.`));
84095
- } else yield* log(highlighter.success(buildEmptyReportMessage({
83273
+ if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
83274
+ yield* printHeadlessReport({
83275
+ diagnostics: printedDiagnostics,
83276
+ elapsedMilliseconds,
83277
+ emptyStateMessage: buildEmptyReportMessage({
84096
83278
  categoryFilters: options.categoryFilters,
84097
83279
  demotedDiagnosticCount,
84098
83280
  outputSurface: options.outputSurface
84099
- })));
84100
- yield* log("");
84101
- yield* pause;
84102
- if (hasSkippedChecks) {
84103
- yield* printBrandingOnlyHeader;
84104
- yield* log(highlighter.gray(" Score not shown — some checks could not complete."));
84105
- } else if (score) yield* printScoreHeader(score);
84106
- else yield* printNoScoreHeader(noScoreMessage);
84107
- if (options.outputDirectory !== null) yield* printDiagnosticsDump(printedDiagnostics, options.outputDirectory);
84108
- return buildResult();
84109
- }
84110
- yield* pause;
84111
- yield* log("");
84112
- yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
84113
- sectionPause: pause,
84114
- animateCountUp: animateRender
84115
- }, useHyperlinks);
84116
- if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
84117
- if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
84118
- yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
84119
- yield* log("");
84120
- }
84121
- const scoreDiagnostics = filterDiagnosticsForSurface([...diagnostics], "score", userConfig);
84122
- const displayedScoreDiagnostics = filterDiagnosticsForSurface([...printedDiagnostics], "score", userConfig);
84123
- const potentialScore = score ? yield* promise(() => computeProjectedScore(displayedScoreDiagnostics, scoreDiagnostics, score)) : null;
84124
- const showShareLink = shouldShowShareLink(options);
84125
- yield* pause;
84126
- yield* printSummary({
84127
- diagnostics: [...printedDiagnostics],
84128
- elapsedMilliseconds,
84129
- scoreResult: score,
84130
- potentialScore,
84131
- totalSourceFileCount: lintSourceFileCount,
83281
+ }),
84132
83282
  noScoreMessage,
84133
- verbose: options.verbose,
84134
- outputDirectory: options.outputDirectory,
84135
- animateProjection: animateRender
83283
+ projectName: project.projectName,
83284
+ scannedFileCount,
83285
+ scoreResult: hasSkippedChecks ? null : score,
83286
+ skippedChecks
84136
83287
  });
84137
- if (hasSkippedChecks) {
84138
- const skippedLabel = skippedChecks.join(" and ");
83288
+ if (options.outputDirectory !== null || options.verbose) yield* printDiagnosticsDump(printedDiagnostics, options.outputDirectory, options.verbose);
83289
+ if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
83290
+ yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
84139
83291
  yield* log("");
84140
- yield* warn$1(highlighter.warn(` Note: ${skippedLabel} checks failed — score may be incomplete.`));
84141
83292
  }
84142
- yield* pause;
84143
83293
  yield* printFooter({
84144
- diagnostics: [...printedDiagnostics],
83294
+ diagnostics: printedDiagnostics,
84145
83295
  scoreResult: score,
84146
83296
  projectName: project.projectName,
84147
- isOffline: !showShareLink
83297
+ isOffline: options.isCi || !options.share || score === null
84148
83298
  });
84149
83299
  return buildResult();
84150
83300
  });
@@ -84175,9 +83325,6 @@ const collectProjectSourceFileCounts = async (rootDirectory, projectDirectories)
84175
83325
  //#region src/cli/utils/format-skipped-projects-message.ts
84176
83326
  const formatSkippedProjectsMessage = (projectCount) => `${projectCount} ${projectCount === 1 ? "project was" : "projects were"} skipped because the max scan duration was reached.`;
84177
83327
  //#endregion
84178
- //#region src/cli/utils/is-share-opted-out.ts
84179
- const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
84180
- //#endregion
84181
83328
  //#region src/cli/utils/build-handoff-payload.ts
84182
83329
  const buildHandoffPayload = (input) => {
84183
83330
  const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
@@ -84374,6 +83521,186 @@ const detectLaunchableAgents = async () => {
84374
83521
  return CLI_AGENT_IDS.filter((agentId) => detected.has(agentId) && isCommandAvailable(CLI_AGENT_BINARIES[agentId]));
84375
83522
  };
84376
83523
  //#endregion
83524
+ //#region src/cli/utils/now-iso.ts
83525
+ const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
83526
+ const ONBOARDING_EVENT = "onboarding";
83527
+ const CI_PITCH_EVENT = "ci-pitch";
83528
+ const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
83529
+ const SETUP_HINT_EVENT = "setup-hint";
83530
+ const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
83531
+ const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
83532
+ const foldLegacyDecisions = (projects, legacy, eventId) => {
83533
+ for (const [hash, record] of Object.entries(legacy ?? {})) {
83534
+ const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
83535
+ projects[hash] = {
83536
+ ...existing,
83537
+ events: {
83538
+ ...existing.events,
83539
+ [eventId]: {
83540
+ firedAt: record.at ?? nowIso(),
83541
+ version: 1,
83542
+ ...record.outcome ? { outcome: record.outcome } : {}
83543
+ }
83544
+ }
83545
+ };
83546
+ }
83547
+ };
83548
+ const migrateCliState = (state) => {
83549
+ if (typeof state.schemaVersion === "number" && state.schemaVersion >= 2) return state;
83550
+ const projects = {};
83551
+ for (const [hash, record] of Object.entries(state.projects ?? {})) {
83552
+ const carried = {
83553
+ rootDirectory: record.rootDirectory,
83554
+ ...record.events ? { events: record.events } : {},
83555
+ ...record.migrations ? { migrations: record.migrations } : {}
83556
+ };
83557
+ projects[hash] = record.setupPrompt === false ? {
83558
+ ...carried,
83559
+ events: {
83560
+ ...carried.events,
83561
+ [SETUP_HINT_EVENT]: {
83562
+ firedAt: nowIso(),
83563
+ version: 1,
83564
+ outcome: "declined"
83565
+ }
83566
+ }
83567
+ } : carried;
83568
+ }
83569
+ foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
83570
+ foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
83571
+ return {
83572
+ schemaVersion: 2,
83573
+ global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
83574
+ firedAt: state.onboardedAt,
83575
+ version: 1
83576
+ } } } : {},
83577
+ projects
83578
+ };
83579
+ };
83580
+ const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
83581
+ const openStore = (options = {}) => new Conf({
83582
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
83583
+ cwd: resolveConfigDir(options)
83584
+ });
83585
+ const openMigratedStore = (options) => {
83586
+ const store = openStore(options);
83587
+ const state = store.store;
83588
+ const migrated = migrateCliState(state);
83589
+ if (migrated !== state) store.store = migrated;
83590
+ return store;
83591
+ };
83592
+ const readCliState = (select, fallback, options = {}) => {
83593
+ try {
83594
+ return select(openMigratedStore(options).store);
83595
+ } catch {
83596
+ return fallback;
83597
+ }
83598
+ };
83599
+ const updateCliState = (update, options = {}) => {
83600
+ try {
83601
+ const store = openMigratedStore(options);
83602
+ store.store = update(store.store);
83603
+ return true;
83604
+ } catch {
83605
+ return false;
83606
+ }
83607
+ };
83608
+ //#endregion
83609
+ //#region src/cli/utils/hash-project-root.ts
83610
+ const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
83611
+ //#endregion
83612
+ //#region src/cli/utils/cli-lifecycle.ts
83613
+ const versionOf = (item) => item.version ?? 1;
83614
+ const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
83615
+ const updateScope = (state, scoped, projectRoot, updateScopeState) => {
83616
+ if (scoped.scope === "global") return {
83617
+ ...state,
83618
+ global: updateScopeState(state.global ?? {})
83619
+ };
83620
+ if (projectRoot === void 0) return state;
83621
+ const projectKey = hashProjectRoot(projectRoot);
83622
+ const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
83623
+ return {
83624
+ ...state,
83625
+ projects: {
83626
+ ...state.projects,
83627
+ [projectKey]: {
83628
+ ...base,
83629
+ ...updateScopeState(base)
83630
+ }
83631
+ }
83632
+ };
83633
+ };
83634
+ const isGatePending = (gate, target = {}, options = {}) => {
83635
+ if (gate.scope === "project" && target.projectRoot === void 0) return false;
83636
+ return readCliState((state) => {
83637
+ const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
83638
+ return !record || record.version < versionOf(gate);
83639
+ }, gate.fireWhenUnknown ?? false, options);
83640
+ };
83641
+ const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
83642
+ ...scope,
83643
+ events: {
83644
+ ...scope.events,
83645
+ [gate.id]: {
83646
+ firedAt: nowIso(),
83647
+ version: versionOf(gate),
83648
+ ...target.outcome ? { outcome: target.outcome } : {}
83649
+ }
83650
+ }
83651
+ })), options);
83652
+ const readPreference = (preference, target = {}, options = {}) => readCliState((state) => selectScope(state, preference, target.projectRoot)?.preferences?.[preference.id] ?? null, null, options);
83653
+ const writePreference = (preference, value, target = {}, options = {}) => updateCliState((state) => updateScope(state, preference, target.projectRoot, (scope) => ({
83654
+ ...scope,
83655
+ preferences: {
83656
+ ...scope.preferences,
83657
+ [preference.id]: value
83658
+ }
83659
+ })), options);
83660
+ const isMigrationPending = (migration, target = {}, options = {}) => {
83661
+ if (migration.scope === "project" && target.projectRoot === void 0) return false;
83662
+ return readCliState((state) => {
83663
+ const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
83664
+ return !record || record.version < versionOf(migration);
83665
+ }, false, options);
83666
+ };
83667
+ const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
83668
+ ...scope,
83669
+ migrations: {
83670
+ ...scope.migrations,
83671
+ [migration.id]: record
83672
+ }
83673
+ })), options);
83674
+ const runMigrations = async (migrations, target = {}, options = {}) => {
83675
+ const results = [];
83676
+ for (const migration of migrations) {
83677
+ if (!isMigrationPending(migration, target, options)) {
83678
+ results.push({
83679
+ id: migration.id,
83680
+ ran: false,
83681
+ applied: true
83682
+ });
83683
+ continue;
83684
+ }
83685
+ let applied = false;
83686
+ try {
83687
+ applied = await migration.run({ projectRoot: target.projectRoot });
83688
+ } catch {
83689
+ applied = false;
83690
+ }
83691
+ if (applied) recordMigration(migration, target.projectRoot, {
83692
+ ranAt: nowIso(),
83693
+ version: versionOf(migration)
83694
+ }, options);
83695
+ results.push({
83696
+ id: migration.id,
83697
+ ran: true,
83698
+ applied
83699
+ });
83700
+ }
83701
+ return results;
83702
+ };
83703
+ //#endregion
84377
83704
  //#region src/cli/utils/open-url.ts
84378
83705
  const resolveOpenCommand = (url) => {
84379
83706
  if (process$1.platform === "darwin") return {
@@ -84619,6 +83946,48 @@ const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
84619
83946
  };
84620
83947
  };
84621
83948
  //#endregion
83949
+ //#region src/cli/utils/resolve-absolute-path.ts
83950
+ /**
83951
+ * Resolves a diagnostic's `filePath` (relative to its project root, or
83952
+ * already absolute) to an absolute path. Shared by the code-frame reader and
83953
+ * the terminal hyperlink builder so both turn a relative path into the same
83954
+ * on-disk location.
83955
+ */
83956
+ const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
83957
+ //#endregion
83958
+ //#region src/cli/utils/build-code-frame.ts
83959
+ /**
83960
+ * Renders a syntax-highlighted source excerpt around a diagnostic site
83961
+ * with a caret pointing at the offending column. Returns null when the
83962
+ * file can't be read (e.g. multi-project summaries where paths are
83963
+ * resolved against a different cwd), so callers can fall back to the
83964
+ * bare `file:line` reference instead of failing the whole render.
83965
+ */
83966
+ const buildCodeFrame = (input) => {
83967
+ if (input.line <= 0) return null;
83968
+ const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
83969
+ let source;
83970
+ try {
83971
+ source = NFS.readFileSync(absolutePath, "utf8");
83972
+ } catch {
83973
+ return null;
83974
+ }
83975
+ if ((source.split("\n", input.line)[input.line - 1] ?? "").length > 200) return null;
83976
+ const location = input.endLine != null && input.endLine > input.line ? {
83977
+ start: { line: input.line },
83978
+ end: { line: input.endLine }
83979
+ } : { start: {
83980
+ line: input.line,
83981
+ column: input.column > 0 ? input.column : void 0
83982
+ } };
83983
+ return codeFrameColumns(source, location, {
83984
+ highlightCode: true,
83985
+ linesAbove: 1,
83986
+ linesBelow: 1,
83987
+ ...input.message ? { message: input.message } : {}
83988
+ });
83989
+ };
83990
+ //#endregion
84622
83991
  //#region src/cli/utils/select-projects.ts
84623
83992
  const discoverWorkspacePackages = (rootDirectory) => {
84624
83993
  const hasRootPackageJson = isFile(Path.join(rootDirectory, "package.json"));
@@ -84738,10 +84107,10 @@ const registerActiveTuiRenderer = (renderer) => {
84738
84107
  if (activeTuiRenderer === renderer) activeTuiRenderer = null;
84739
84108
  };
84740
84109
  };
84741
- const clearActiveTuiRenderer = () => {
84110
+ const preserveActiveTuiRendererOutput = () => {
84742
84111
  const renderer = activeTuiRenderer;
84743
84112
  activeTuiRenderer = null;
84744
- renderer?.clear();
84113
+ renderer?.preserveOutput();
84745
84114
  };
84746
84115
  //#endregion
84747
84116
  //#region src/cli/utils/exit-gracefully.ts
@@ -84750,7 +84119,7 @@ const exitGracefully = () => {
84750
84119
  if (didStartExiting) return process.exit(130);
84751
84120
  didStartExiting = true;
84752
84121
  activeScanAbortRegistry.abortAll();
84753
- clearActiveTuiRenderer();
84122
+ preserveActiveTuiRendererOutput();
84754
84123
  try {
84755
84124
  if (isJsonModeActive()) writeJsonErrorReport(/* @__PURE__ */ new Error("Scan cancelled by user (SIGINT/SIGTERM)"));
84756
84125
  else console.log([
@@ -84765,7 +84134,7 @@ const exitGracefully = () => {
84765
84134
  flushSentry().finally(() => process.exit(130));
84766
84135
  };
84767
84136
  //#endregion
84768
- export { canAnimateOnboarding as $, isDebugFlagEnabled as $n, provide as $r, isFullWidth as $t, formatSkippedProjectsMessage as A, flushSentry as An, getEquivalentRuleKeys as Ar, buildWorkflowContent as At, colorizeByScore as B, writeJsonErrorReport as Bn, loadConfigWithSource as Br, prompts as Bt, CLI_AGENT_BINARIES as C, resolveGitPath as Cn, findLegacyConfig as Cr, recordRunTraceId as Ct, detectAvailableAgents as D, detectDefaultBranch as Dn, getCategoryImpact as Dr, warnDeprecatedDiff as Dt, computeDefaultSelectedAgents as E, writeJsonFile as En, getBaselineDiffPlan as Er, resolveScope as Et, printDiagnosticsDump as F, buildSentryScope as Fn, isPathInsideDirectory as Fr, upgradeReactDoctorWorkflowInPlace as Ft, recordGate as G, SENTRY_FLUSH_TIMEOUT_MS as Gn, setColorEnabled as Gr, DOCTOR_PACKAGE_NAME as Gt, markOnboardingComplete as H, VERSION as Hn, messageFromUnknown as Hr, openWorkflowPullRequest as Ht, printFooter as I, enableJsonMode as In, isPlainObject as Ir, upgradeWorkflowActionToV2 as It, ACTION_UPGRADE_EVENT as J, TUI_REPORT_COMPACT_MAX_ROWS as Jn, yieldToEventLoop as Jr, hasDoctorScript as Jt, runMigrations as K, STAGED_FILES_TEMP_DIR_PREFIX as Kn, toRelativePath as Kr, findNearestPackageDirectory as Kt, printSummary as L, isJsonModeActive as Ln, isReactDoctorError as Lr, workflowUsesV1Action as Lt, deduplicateProjectScans as M, isExpectedUserError as Mn, hasReactRuntime as Mr, installReactDoctorWorkflow as Mt, createInvocationInspect as N, formatEnvironmentError as Nn, highlighter as Nr, isReactDoctorWorkflowInstalled as Nt, buildHandoffPayload as O, runCommand as On, getChangedLineRanges as Or, isValidBlockingLevel as Ot, inspect as P, isEnvironmentError as Pn, isErrnoException as Pr, readReactDoctorWorkflow as Pt, SETUP_HINT_EVENT as Q, isNonInteractiveEnvironment as Qn, promise as Qr, spinner as Qt, easeOutCubic as R, setJsonReportDirectory as Rn, isSameRuleKey as Rr, recordCount as Rt, detectLaunchableAgents as S, readPackageJson as Sn, discoverReactSubprojects as Sr, getLastRunTraceId as St, launchCliAgent as T, runGitRaw as Tn, formatReactDoctorError as Tr, isScopeValue as Tt, isGatePending as U, CI_TRUST_COMPANIES as Un, remainingDeadlineBudgetMs as Ur, stageWorkflowFile as Ut, hasCompletedOnboarding as V, writeJsonReport as Vn, mapWithConcurrency as Vr, unrefStdin as Vt, readPreference as W, METRIC as Wn, resolveScanTarget as Wr, toForwardSlashes as Wt, HANDOFF_TARGET_PREFERENCE_ID as X, WELCOME_EXPLANATION_HOLD_MS as Xn, log as Xr, isSpinnerSilent as Xt, CI_PITCH_EVENT as Y, TUI_REPORT_DETAIL_WIDTH_FRACTION as Yn, error$1 as Yr, installReactDoctorScriptStep as Yt, INSTALL_AGENTS_PREFERENCE_ID as Z, CliInputError as Zn, gen as Zr, setSpinnerSilent as Zt, filterScansForSurface as _, getPackageJsonPath as _n, StagedFiles as _r, buildRulePriorityMap as _t, shouldFailScanGate as a, provide$2 as ai, HOOK_RELATIVE_PATH as an, CI_URL as ar, computeProjectedScore as at, reportWorkflowResult as b, packageHasNestedRecordKey as bn, clearConfigCache as br, formatLearnMoreLine as bt, selectProjects as c, LEGACY_HOOK_RUNNER_RELATIVE_PATH as cn, CONFIG_SCHEMA_URL as cr, writeStdout as ct, resolveProjectDiffIncludePaths as d, PRE_COMMIT_CONFIG_FILE as dn, Git as dr, buildCodeFrame as dt, runPromise as ei, isWide as en, isCiEnvironment as er, isOnboardingForced as et, resolveProjectSourceFilePaths as f, REACT_DOCTOR_COMMAND as fn, LEGACY_CONFIG_FILENAME as fr, buildEmptyReportMessage as ft, resolveCliInspectOptions as g, ensureTrailingNewline as gn, STAGED_FILES_PROJECT_CONFIG_FILENAMES as gr, hasLintHardFailure as gt, resolveWorkspaceDeadCodeOwner as h, VITE_PLUS_HOOKS_PATH as hn, SKILL_NAME as hr, resolveMergeBaseRef as ht, validateModeFlags as i, void_ as ii, HOOK_FILE_NAME as in, CANONICAL_GITHUB_URL as ir, filterDiagnosticsByCategories as it, collectProjectSourceFileCounts as j, initializeSentry as jn, hasPublishedFixRecipe as jr, getReactDoctorWorkflowPath as jt, isShareOptedOut as k, cliLogger as kn, getDiffInfo as kr, resolveBlockingLevel as kt, resolveProjectScan as l, NON_BLOCKING_REACT_DOCTOR_COMMAND as ln, DIAGNOSTIC_CATEGORY_BUCKETS as lr, indentMultilineText as lt, retryMissingProjectScores as m, SIMPLE_GIT_HOOKS_PACKAGE_JSON_KEY as mn, Reporter as mr, buildNoScoreMessage as mt, registerActiveTuiRenderer as n, sleep as ni, isUnicodeSupported as nn, isCodingAgentEnvironment as nr, shouldRenderHyperlinks as nt, projectManifestChanged as o, succeed$3 as oi, HUSKY_HOOKS_PATH as on, COMPILER_CLEANUP_BUCKET as or, formatElapsedTime as ot, resolveProjectRelativeDirectory as p, SIMPLE_GIT_HOOKS_CONFIG_FILE as pn, Progress as pr, hasIncompleteScoreAnalysis as pt, writePreference as q, STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES as qn, validateConfigTypes as qr, hasDoctorDependency as qt, validateIncludeUntrackedScope as r, sync as ri, chalk as rn, CANONICAL_DISCORD_URL as rr, supportsHyperlinks as rt, discoverWorkspacePackages as s, LEFTHOOK_CONFIG_FILES as sn, COMPILER_CLEANUP_RULE_KEYS as sr, printDiagnostics as st, exitGracefully as t, runSync as ti, stripAnsi as tn, isCiOrCodingAgentEnvironment as tr, shouldRecordOnboarding as tt, resolveProjectChangedLineRanges as u, OVERCOMMIT_CONFIG_FILE as un, GITHUB_ACTIONS_SETUP_URL as ur, formatHyperlink as ut, countUniqueScannedFiles as v, isRecord as vn, buildJsonReport as vr, buildSortedRuleGroups as vt, copyToClipboard as w, runGit as wn, formatErrorChain as wr, finalizeScope as wt, openUrl as x, packageHasRecordKey as xn, detectAiTrainingEnvironment as xr, getActiveRunTrace as xt, setUpGitHubActions as y, packageHasDependency as yn, buildRuleDocsUrl as yr, formatFixRecipeLine as yt, doctorFace as z, setJsonReportMode as zn, listWorkspacePackages as zr, recordDistribution as zt };
84137
+ export { filterDiagnosticsByCategories as $, GITHUB_ACTIONS_SETUP_URL as $n, PRE_COMMIT_CONFIG_FILE as $t, HANDOFF_TARGET_PREFERENCE_ID as A, VERSION as An, loadConfigWithSource as Ar, openWorkflowPullRequest as At, buildHandoffPayload as B, isNonInteractiveEnvironment as Bn, log as Br, spinner as Bt, isGatePending as C, buildSentryScope as Cn, highlighter as Cr, upgradeReactDoctorWorkflowInPlace as Ct, writePreference as D, setJsonReportMode as Dn, isReactDoctorError as Dr, recordDistribution as Dt, runMigrations as E, setJsonReportDirectory as En, isPlainObject as Er, recordCount as Et, CLI_AGENT_BINARIES as F, STAGED_FILES_TEMP_DIR_PREFIX as Fn, setColorEnabled as Fr, hasDoctorDependency as Ft, inspect as G, isCodingAgentEnvironment as Gn, sync as Gr, chalk as Gt, collectProjectSourceFileCounts as H, isDebugFlagEnabled as Hn, provide as Hr, isWide as Ht, copyToClipboard as I, STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES as In, toRelativePath as Ir, hasDoctorScript as It, formatElapsedTime as J, CI_URL as Jn, succeed$3 as Jr, HUSKY_HOOKS_PATH as Jt, printHeadlessReport as K, CANONICAL_DISCORD_URL as Kn, void_ as Kr, HOOK_FILE_NAME as Kt, launchCliAgent as L, TUI_REPORT_COMPACT_MAX_ROWS as Ln, validateConfigTypes as Lr, installReactDoctorScriptStep as Lt, ONBOARDING_EVENT as M, METRIC as Mn, messageFromUnknown as Mr, toForwardSlashes as Mt, SETUP_HINT_EVENT as N, MINIMUM_VTE_VERSION_FOR_HYPERLINKS as Nn, remainingDeadlineBudgetMs as Nr, DOCTOR_PACKAGE_NAME as Nt, ACTION_UPGRADE_EVENT as O, writeJsonErrorReport as On, isSameRuleKey as Or, prompts as Ot, detectLaunchableAgents as P, SENTRY_FLUSH_TIMEOUT_MS as Pn, resolveScanTarget as Pr, findNearestPackageDirectory as Pt, formatDiagnosticSite as Q, DIAGNOSTIC_CATEGORY_BUCKETS as Qn, OVERCOMMIT_CONFIG_FILE as Qt, computeDefaultSelectedAgents as R, TUI_REPORT_DETAIL_WIDTH_FRACTION as Rn, yieldToEventLoop as Rr, isSpinnerSilent as Rt, openUrl as S, isEnvironmentError as Sn, hasReactRuntime as Sr, readReactDoctorWorkflow as St, recordGate as T, isJsonModeActive as Tn, isPathInsideDirectory as Tr, workflowUsesV1Action as Tt, deduplicateProjectScans as U, isCiEnvironment as Un, runPromise as Ur, stripAnsi as Ut, formatSkippedProjectsMessage as V, isGitHookEnvironment as Vn, gen as Vr, isFullWidth as Vt, createInvocationInspect as W, isCiOrCodingAgentEnvironment as Wn, runSync as Wr, isUnicodeSupported as Wt, indentMultilineText as X, COMPILER_CLEANUP_RULE_KEYS as Xn, LEGACY_HOOK_RUNNER_RELATIVE_PATH as Xt, printFooter as Y, COMPILER_CLEANUP_BUCKET as Yn, LEFTHOOK_CONFIG_FILES as Yt, printDiagnosticsDump as Z, CONFIG_SCHEMA_URL as Zn, NON_BLOCKING_REACT_DOCTOR_COMMAND as Zt, resolveCliInspectOptions as _, cliLogger as _n, getCategoryImpact as _r, resolveBlockingLevel as _t, shouldFailScanGate as a, getPackageJsonPath as an, STAGED_FILES_PROJECT_CONFIG_FILENAMES as ar, buildRulePriorityMap as at, setUpGitHubActions as b, isExpectedUserError as bn, getEquivalentRuleKeys as br, installReactDoctorWorkflow as bt, selectProjects as c, packageHasNestedRecordKey as cn, buildRuleDocsUrl as cr, formatLearnMoreLine as ct, resolveProjectChangedLineRanges as d, resolveGitPath as dn, detectAiTrainingEnvironment as dr, recordRunTraceId as dt, REACT_DOCTOR_COMMAND as en, Git as er, buildEmptyReportMessage as et, resolveProjectDiffIncludePaths as f, runGit as fn, discoverReactSubprojects as fr, finalizeScope as ft, resolveWorkspaceDeadCodeOwner as g, runCommand as gn, getBaselineDiffPlan as gr, isValidBlockingLevel as gt, retryMissingProjectScores as h, detectDefaultBranch as hn, formatReactDoctorError as hr, warnDeprecatedDiff as ht, validateModeFlags as i, ensureTrailingNewline as in, SKILL_NAME as ir, hasLintHardFailure as it, INSTALL_AGENTS_PREFERENCE_ID as j, CI_TRUST_COMPANIES as jn, mapWithConcurrency as jr, stageWorkflowFile as jt, CI_PITCH_EVENT as k, writeJsonReport as kn, listWorkspacePackages as kr, unrefStdin as kt, buildCodeFrame as l, packageHasRecordKey as ln, calculateScore as lr, getActiveRunTrace as lt, resolveProjectRelativeDirectory as m, writeJsonFile as mn, formatErrorChain as mr, resolveScope as mt, registerActiveTuiRenderer as n, SIMPLE_GIT_HOOKS_PACKAGE_JSON_KEY as nn, Progress as nr, buildNoScoreMessage as nt, projectManifestChanged as o, isRecord as on, StagedFiles as or, buildSortedRuleGroups as ot, resolveProjectSourceFilePaths as p, runGitRaw as pn, findLegacyConfig as pr, isScopeValue as pt, pluralize as q, CANONICAL_GITHUB_URL as qn, provide$2 as qr, HOOK_RELATIVE_PATH as qt, validateIncludeUntrackedScope as r, VITE_PLUS_HOOKS_PATH as rn, Reporter as rr, resolveMergeBaseRef as rt, discoverWorkspacePackages as s, packageHasDependency as sn, buildJsonReport as sr, formatFixRecipeLine as st, exitGracefully as t, SIMPLE_GIT_HOOKS_CONFIG_FILE as tn, LEGACY_CONFIG_FILENAME as tr, hasIncompleteScoreAnalysis as tt, resolveProjectScan as u, readPackageJson as un, clearConfigCache as ur, getLastRunTraceId as ut, filterScansForSurface as v, flushSentry as vn, getChangedLineRanges as vr, buildWorkflowContent as vt, readPreference as w, enableJsonMode as wn, isErrnoException as wr, upgradeWorkflowActionToV2 as wt, reportWorkflowResult as x, formatEnvironmentError as xn, hasPublishedFixRecipe as xr, isReactDoctorWorkflowInstalled as xt, countUniqueScannedFiles as y, initializeSentry as yn, getDiffInfo as yr, getReactDoctorWorkflowPath as yt, detectAvailableAgents as z, CliInputError as zn, error$1 as zr, setSpinnerSilent as zt };
84769
84138
 
84770
- //# sourceMappingURL=exit-gracefully-BN2OivAV.js.map
84771
- //# debugId=07705956-10ec-5419-acea-25f57c6a08c5
84139
+ //# sourceMappingURL=exit-gracefully-B18ck0kt.js.map
84140
+ //# debugId=b58d69eb-938f-5abd-9831-40b3dfe65ff2