react-doctor 0.9.12-dev.ffc2d14 → 0.9.13-dev.d030b11

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.
Files changed (47) hide show
  1. package/README.md +33 -0
  2. package/dist/active-run-trace-dlIZeEvb.js +18 -0
  3. package/dist/active-scan-abort-registry-DwG6tSPy.js +28 -0
  4. package/dist/{build-section-divider-BuOtLrzK.js → build-section-divider-Dhl7C8kp.js} +8 -22
  5. package/dist/{ci-DVVsf25h.js → ci-CPJtbXo4.js} +12 -11
  6. package/dist/cli.js +116 -35
  7. package/dist/{design-Y1pcA2uj.js → design-CI6oM-NB.js} +4 -4
  8. package/dist/{devtools-D8NTVj81.js → devtools-CC2PAZ85.js} +3 -3
  9. package/dist/{exit-gracefully-DnXGONjj.js → exit-gracefully-Zhbpfy2i.js} +15 -9
  10. package/dist/get-east-asian-width-Bv2TD3Xj.js +444 -0
  11. package/dist/{report-error-MGKyeIns.js → handle-error-LYTGcmFr.js} +18 -68
  12. package/dist/index.d.ts +32 -0
  13. package/dist/index.js +593 -286
  14. package/dist/{inspect-g3_lzs3l.js → inspect-BZ-i49rK.js} +38 -28
  15. package/dist/{install-02Lq6wKC.js → install-Cf-tjrbB.js} +9 -8
  16. package/dist/{install-doctor-script-DmEWQpVJ.js → install-doctor-script-BGdm_niA.js} +7 -449
  17. package/dist/{install-github-workflow-Dx6Pl9Wl.js → install-github-workflow-CwcDLQCy.js} +10 -6
  18. package/dist/{install-react-doctor-CMCXgenA.js → install-react-doctor-DcJ90NzQ.js} +8 -8
  19. package/dist/{run-scan-app-CDDu7P32.js → jsx-runtime-Cq-_Fvkx.js} +280 -2774
  20. package/dist/path-format-DnHpmJwG.js +9 -0
  21. package/dist/{print-branded-header-kbvWYuGv.js → print-branded-header-DM0JRJnn.js} +4 -4
  22. package/dist/project-analysis-worker.js +59 -54
  23. package/dist/prompt-runtime-scan-url-BpHApg0d.js +240 -0
  24. package/dist/{read-object-file-Cwhr9OIi.js → read-object-file-DMUSluLA.js} +4 -4
  25. package/dist/{record-metric-CFfsgcVs.js → record-metric-BRc98SXW.js} +556 -235
  26. package/dist/report-error-BpWTkd5U.js +64 -0
  27. package/dist/{report-workflow-result-DGg9xopH.js → report-workflow-result-DXSpYstq.js} +5 -5
  28. package/dist/{resolve-scope-CXS-5jLR.js → resolve-scope-CJXv5ePP.js} +8 -10
  29. package/dist/{rules-CThlosPx.js → rules-CZk5ywQe.js} +6 -6
  30. package/dist/run-scan-app-BtY0jXpd.js +2511 -0
  31. package/dist/runtime-scan/browser-probe.iife.js +855 -0
  32. package/dist/runtime-scan-iztAEKV3.js +770 -0
  33. package/dist/sanitize-runtime-url-DbrHDuC1.js +76 -0
  34. package/dist/scan-GPvogJT-.js +47 -0
  35. package/dist/{should-fail-scan-gate-C0Bo5Gqy.js → should-fail-scan-gate-Uk7rJiMQ.js} +20 -112
  36. package/dist/{should-skip-prompts-BqrtVX4G.js → should-skip-prompts-DzpvkZcL.js} +4 -4
  37. package/dist/should-use-tui-CH5oI2Vu.js +25 -0
  38. package/dist/skills/improve-threejs/SKILL.md +102 -0
  39. package/dist/skills/performance/SKILL.md +113 -0
  40. package/dist/skills/react-doctor/SKILL.md +6 -0
  41. package/dist/{unref-stdin-BxyvqsOy.js → telemetry-runtime-hAsPFFEx.js} +55 -25
  42. package/dist/unref-stdin-SK3WfwxS.js +12 -0
  43. package/dist/{version-S-8vvWFi.js → version-BiWDQQgs.js} +4 -4
  44. package/dist/{why-DXfx0oL9.js → why-B8sLOroq.js} +4 -4
  45. package/dist/with-run-span-BWmpgEhA.js +110 -0
  46. package/package.json +9 -6
  47. package/dist/scan-4I1mvxBO.js +0 -53
@@ -0,0 +1,2511 @@
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]="043306a4-5f17-588e-9ac0-086eff86ab98")}catch(e){}}();
3
+ import { Bn as sync, Bt as getDiffInfo, C as CI_TRUST_COMPANIES, G as isCiEnvironment, Hn as void_, Jt as isPathInsideDirectory, Kn as succeed, Kt as highlighter, L as TUI_REPORT_COMPACT_MAX_ROWS, Lt as getBaselineDiffPlan, R as TUI_REPORT_DETAIL_WIDTH_FRACTION, Rn as runPromise, Rt as getChangedLineRanges, T as METRIC, Ut as hasPublishedFixRecipe, V as isGitHookEnvironment, ar as DIAGNOSTIC_CATEGORY_BUCKETS, at as Progress, cr as GITHUB_ACTIONS_SETUP_URL, fn as resolveScanTarget, gt as buildRuleDocsUrl, in as mapWithConcurrency, n as recordDistribution, sn as remainingDeadlineBudgetMs, st as Reporter, t as recordCount, vr as getCategoryImpact, xn as yieldToEventLoop, yt as calculateScore } from "./record-metric-BRc98SXW.js";
4
+ import { t as toForwardSlashes } from "./path-format-DnHpmJwG.js";
5
+ import { M as cliLogger, n as findNearestPackageDirectory } from "./install-doctor-script-BGdm_niA.js";
6
+ import { i as isReactDoctorWorkflowInstalled } from "./install-github-workflow-CwcDLQCy.js";
7
+ import { r as resolveScope, s as resolveBlockingLevel, t as finalizeScope } from "./resolve-scope-CJXv5ePP.js";
8
+ import { B as filterDiagnosticsByCategories, C as CLI_AGENT_BINARIES, D as collectProjectSourceFileCounts, E as buildHandoffPayload, F as formatElapsedTime, G as buildSortedRuleGroups, H as buildEmptyReportMessage, I as printFooter, K as formatFixRecipeLine, L as printDiagnosticsDump, N as pluralize, O as deduplicateProjectScans, P as formatSkippedCheckLabel, R as formatDiagnosticSite, S as detectLaunchableAgents, T as launchCliAgent, U as hasLintHardFailure, V as buildNoScoreMessage, W as buildRulePriorityMap, _ as resolveWorkspaceDeadCodeOwner, b as countUniqueScannedFiles, c as discoverWorkspacePackages, d as resolveProjectScan, f as resolveProjectChangedLineRanges, g as retryMissingProjectScores, i as validateIncludeUntrackedScope, j as resolveMergeBaseRef, k as createInvocationInspect, l as selectProjects, m as resolveProjectSourceFilePaths, n as formatSkippedProjectsMessage, o as projectManifestChanged, p as resolveProjectDiffIncludePaths, q as formatLearnMoreLine, s as partitionProjectScanOutcomes, t as shouldFailScanGate, u as buildCodeFrame, v as resolveCliInspectOptions, w as copyToClipboard, x as setUpGitHubActions, y as filterScansForSurface, z as hasIncompleteScoreAnalysis } from "./should-fail-scan-gate-Uk7rJiMQ.js";
9
+ import { a as recordGate, f as ONBOARDING_EVENT, n as openUrl, r as isGatePending } from "./report-workflow-result-DXSpYstq.js";
10
+ import { a as useExitOnCtrlC, c as Spinner, d as useInput, f as Transform, h as Box, i as import_react, l as useStdout, m as Text, n as registerMountedTuiRenderer, o as terminalSymbols, p as render, r as useStdoutDimensions, s as isPrintableInput, t as require_jsx_runtime, u as useApp } from "./jsx-runtime-Cq-_Fvkx.js";
11
+ import path from "node:path";
12
+ import { performance } from "node:perf_hooks";
13
+ import isUnicodeSupported from "is-unicode-supported";
14
+ import { getSkillAgentConfig } from "agent-install";
15
+ //#region src/cli/utils/get-top-error-rule-keys.ts
16
+ const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority).slice(0, limit).map(([ruleKey]) => ruleKey));
17
+ //#endregion
18
+ //#region src/cli/utils/compute-score-projection.ts
19
+ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore) => {
20
+ const topErrorRuleKeys = getTopErrorRuleKeys(topErrorSource, 3, buildRulePriorityMap([currentScore]));
21
+ if (topErrorRuleKeys.size === 0) return null;
22
+ const potentialScore = await calculateScore(rescoreSource.filter((diagnostic) => !topErrorRuleKeys.has(`${diagnostic.plugin}/${diagnostic.rule}`)));
23
+ if (!potentialScore || potentialScore.score <= currentScore.score) return null;
24
+ return potentialScore.score;
25
+ };
26
+ //#endregion
27
+ //#region src/cli/utils/is-share-opted-out.ts
28
+ const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
29
+ //#endregion
30
+ //#region src/cli/utils/resolve-project-tui-scan-scope.ts
31
+ const resolveProjectTuiScanScope = (input) => {
32
+ if (input.plan.scope === "full") return {};
33
+ if (input.plan.diffInfo === null) return null;
34
+ const changedSourceFiles = resolveProjectDiffIncludePaths(input.rootDirectory, input.projectDirectory, input.plan.diffInfo);
35
+ const baselineBaseFiles = input.plan.baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(input.rootDirectory, input.projectDirectory, input.plan.baselineDiffPlan.baseFiles);
36
+ const baselineHeadFiles = input.plan.baselineDiffPlan === null ? null : resolveProjectSourceFilePaths(input.rootDirectory, input.projectDirectory, input.plan.baselineDiffPlan.headFiles);
37
+ const supplyChainManifestChanged = input.supplyChainEnabled && projectManifestChanged(input.rootDirectory, input.projectDirectory, input.plan.diffInfo);
38
+ const hasBaselineOnlyFiles = (baselineBaseFiles?.length ?? 0) > 0;
39
+ if (!supplyChainManifestChanged && changedSourceFiles.length === 0 && !hasBaselineOnlyFiles) return null;
40
+ const includePaths = [...changedSourceFiles];
41
+ if (includePaths.length === 0 && hasBaselineOnlyFiles) includePaths.push(...baselineBaseFiles ?? []);
42
+ if (supplyChainManifestChanged) includePaths.push("package.json");
43
+ return {
44
+ includePaths,
45
+ baseline: input.plan.baselineRef !== null && baselineBaseFiles !== null && baselineHeadFiles !== null ? {
46
+ ref: input.plan.baselineRef,
47
+ baseFiles: baselineBaseFiles,
48
+ headFiles: baselineHeadFiles
49
+ } : void 0,
50
+ changedLineRanges: input.plan.scope === "lines" && input.plan.changedLineRanges !== null ? resolveProjectChangedLineRanges(input.rootDirectory, input.projectDirectory, input.plan.changedLineRanges) : void 0,
51
+ supplyChainManifestChanged
52
+ };
53
+ };
54
+ //#endregion
55
+ //#region src/cli/utils/resolve-tui-scan-scope.ts
56
+ const resolveTuiScanScope = async (input) => {
57
+ const requestedScope = resolveScope(input.flags, input.userConfig);
58
+ const includeUntracked = input.flags.includeUntracked ?? false;
59
+ validateIncludeUntrackedScope(includeUntracked, requestedScope.scope);
60
+ const diffInfo = requestedScope.scope !== void 0 && requestedScope.scope !== "full" ? await getDiffInfo(input.directory, requestedScope.base, includeUntracked) : null;
61
+ const scope = await finalizeScope({
62
+ requested: requestedScope,
63
+ diffInfo,
64
+ skipPrompts: true,
65
+ isQuiet: false
66
+ });
67
+ let comparisonBaseRef = null;
68
+ if (scope !== "full" && diffInfo !== null && !diffInfo.isCurrentChanges) if (diffInfo.baseSha !== void 0) comparisonBaseRef = await resolveMergeBaseRef(input.directory, diffInfo.baseSha);
69
+ else if (diffInfo.diffBaseRef !== void 0) comparisonBaseRef = diffInfo.diffBaseRef;
70
+ else comparisonBaseRef = await resolveMergeBaseRef(input.directory, diffInfo.baseBranch);
71
+ const baselineRef = scope === "changed" ? comparisonBaseRef : null;
72
+ const baselineDiffPlan = baselineRef === null ? null : await getBaselineDiffPlan(input.directory, baselineRef);
73
+ const linesBaseRef = diffInfo?.isCurrentChanges ? "HEAD" : comparisonBaseRef;
74
+ const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) ? await getChangedLineRanges({
75
+ directory: input.directory,
76
+ baseRef: linesBaseRef ?? void 0,
77
+ files: [...diffInfo.changedFiles],
78
+ includeUntracked
79
+ }) : null;
80
+ if (scope === "lines" && changedLineRanges === null) {
81
+ cliLogger.warn("Could not determine changed lines (no base ref or git diff failed); reporting all issues in changed files.");
82
+ cliLogger.break();
83
+ }
84
+ return {
85
+ baselineDiffPlan,
86
+ baselineIntended: scope === "changed" && diffInfo !== null && !diffInfo.isCurrentChanges,
87
+ baselineRef,
88
+ changedLineRanges,
89
+ diffInfo,
90
+ scope
91
+ };
92
+ };
93
+ //#endregion
94
+ //#region src/cli/utils/select-report-diagnostics.ts
95
+ const selectReportDiagnostics = (input) => {
96
+ const surfaceDiagnostics = filterScansForSurface([input.scan], input.surface ?? "cli");
97
+ return {
98
+ diagnostics: filterDiagnosticsByCategories(surfaceDiagnostics, new Set(input.categoryFilters)),
99
+ demotedDiagnosticCount: input.scan.result.diagnostics.length - surfaceDiagnostics.length
100
+ };
101
+ };
102
+ //#endregion
103
+ //#region src/cli/utils/clamp-number.ts
104
+ const clampNumber = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, value));
105
+ //#endregion
106
+ //#region src/cli/utils/resolve-visible-start.ts
107
+ const resolveVisibleStart = ({ itemCount, offset, selectedIndex, viewportHeight }) => {
108
+ if (viewportHeight <= 0) return 0;
109
+ const boundedOffset = clampNumber(offset, 0, Math.max(0, itemCount - viewportHeight));
110
+ if (selectedIndex < boundedOffset) return selectedIndex;
111
+ if (selectedIndex >= boundedOffset + viewportHeight) return Math.max(0, selectedIndex - viewportHeight + 1);
112
+ return boundedOffset;
113
+ };
114
+ //#endregion
115
+ //#region src/cli/ink/lib/fuzzy-match.ts
116
+ const fuzzyMatch = (query, target) => {
117
+ if (query.length === 0) return {
118
+ score: 0,
119
+ matchedIndices: []
120
+ };
121
+ const lowerQuery = query.toLowerCase();
122
+ const lowerTarget = target.toLowerCase();
123
+ const matchedIndices = [];
124
+ let score = 0;
125
+ let queryIndex = 0;
126
+ let previousMatchIndex = Number.NEGATIVE_INFINITY;
127
+ for (let targetIndex = 0; targetIndex < lowerTarget.length && queryIndex < lowerQuery.length; targetIndex++) {
128
+ if (lowerTarget[targetIndex] !== lowerQuery[queryIndex]) continue;
129
+ matchedIndices.push(targetIndex);
130
+ if (targetIndex === previousMatchIndex + 1) score += 5;
131
+ const previousCharacter = target[targetIndex - 1];
132
+ if (targetIndex === 0 || previousCharacter === "-" || previousCharacter === "_" || previousCharacter === "/" || previousCharacter === " ") score += 10;
133
+ previousMatchIndex = targetIndex;
134
+ queryIndex++;
135
+ }
136
+ if (queryIndex < lowerQuery.length) return null;
137
+ score -= matchedIndices[0] * 1;
138
+ return {
139
+ score,
140
+ matchedIndices
141
+ };
142
+ };
143
+ //#endregion
144
+ //#region src/cli/ink/components/project-select.tsx
145
+ var import_jsx_runtime = require_jsx_runtime();
146
+ const MatchedName = ({ name, matchedIndices, isSelected }) => {
147
+ if (matchedIndices.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
148
+ bold: isSelected,
149
+ wrap: "truncate-end",
150
+ children: name
151
+ });
152
+ const matchedIndexSet = new Set(matchedIndices);
153
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
154
+ bold: isSelected,
155
+ wrap: "truncate-end",
156
+ children: [...name].map((character, index) => matchedIndexSet.has(index) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
157
+ color: "yellow",
158
+ children: character
159
+ }, index) : character)
160
+ });
161
+ };
162
+ const ProjectSelect = ({ packages, rootDirectory, onSubmit }) => {
163
+ const { rows: terminalRows } = useStdoutDimensions();
164
+ useExitOnCtrlC();
165
+ const [selectionMode, setSelectionMode] = (0, import_react.useState)("list");
166
+ const [searchQuery, setSearchQuery] = (0, import_react.useState)("");
167
+ const [selectedIndex, setSelectedIndex] = (0, import_react.useState)(0);
168
+ const [scrollOffset, setScrollOffset] = (0, import_react.useState)(0);
169
+ const [checkedDirectories, setCheckedDirectories] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
170
+ const didRecordProjectPathContext = (0, import_react.useRef)(false);
171
+ const matchedPackages = (0, import_react.useMemo)(() => {
172
+ const scoredPackages = packages.flatMap((workspacePackage) => {
173
+ const result = fuzzyMatch(searchQuery, workspacePackage.name);
174
+ return result ? [{
175
+ workspacePackage,
176
+ result
177
+ }] : [];
178
+ });
179
+ if (searchQuery.length > 0) scoredPackages.sort((leftPackage, rightPackage) => rightPackage.result.score - leftPackage.result.score);
180
+ return scoredPackages.map(({ workspacePackage, result }) => ({
181
+ workspacePackage,
182
+ matchedIndices: result.matchedIndices,
183
+ relativeDirectory: path.relative(rootDirectory, workspacePackage.directory) || "."
184
+ }));
185
+ }, [
186
+ packages,
187
+ rootDirectory,
188
+ searchQuery
189
+ ]);
190
+ const hasProjectPathContext = matchedPackages.some(({ relativeDirectory, workspacePackage }) => relativeDirectory !== workspacePackage.name);
191
+ (0, import_react.useEffect)(() => {
192
+ if (!hasProjectPathContext || didRecordProjectPathContext.current) return;
193
+ didRecordProjectPathContext.current = true;
194
+ recordCount(METRIC.tuiProjectPathContextShown);
195
+ }, [hasProjectPathContext]);
196
+ const isSearching = selectionMode === "search";
197
+ const hasFilterLine = isSearching || searchQuery.length > 0;
198
+ const listHeight = Math.max(1, Math.min(Math.max(matchedPackages.length, 1), terminalRows - 3 - (hasFilterLine ? 1 : 0)));
199
+ const selectedPackageIndex = matchedPackages.length === 0 ? 0 : clampNumber(selectedIndex, 0, matchedPackages.length - 1);
200
+ const selectedWorkspacePackage = matchedPackages[selectedPackageIndex]?.workspacePackage;
201
+ const updateFilter = (nextSearchQuery) => {
202
+ setSearchQuery(nextSearchQuery);
203
+ setSelectedIndex(0);
204
+ setScrollOffset(0);
205
+ };
206
+ const moveSelection = (indexDelta) => {
207
+ if (matchedPackages.length === 0) return;
208
+ const nextSelectedIndex = clampNumber(selectedPackageIndex + indexDelta, 0, matchedPackages.length - 1);
209
+ setSelectedIndex(nextSelectedIndex);
210
+ setScrollOffset((currentOffset) => {
211
+ if (nextSelectedIndex < currentOffset) return nextSelectedIndex;
212
+ if (nextSelectedIndex >= currentOffset + listHeight) return nextSelectedIndex - listHeight + 1;
213
+ return currentOffset;
214
+ });
215
+ };
216
+ const toggleCheckedDirectory = (directory) => {
217
+ setCheckedDirectories((currentCheckedDirectories) => {
218
+ const updatedCheckedDirectories = new Set(currentCheckedDirectories);
219
+ if (updatedCheckedDirectories.has(directory)) updatedCheckedDirectories.delete(directory);
220
+ else updatedCheckedDirectories.add(directory);
221
+ return updatedCheckedDirectories;
222
+ });
223
+ };
224
+ const submitDirectories = (directories) => {
225
+ onSubmit([...directories]);
226
+ };
227
+ const submitSelection = () => {
228
+ if (checkedDirectories.size > 0) {
229
+ const selectedDirectories = [];
230
+ for (const workspacePackage of packages) if (checkedDirectories.has(workspacePackage.directory)) selectedDirectories.push(workspacePackage.directory);
231
+ return submitDirectories(selectedDirectories);
232
+ }
233
+ if (selectedWorkspacePackage) submitDirectories([selectedWorkspacePackage.directory]);
234
+ };
235
+ useInput((input, key) => {
236
+ if (isSearching) {
237
+ if (key.return) return setSelectionMode("list");
238
+ if (key.escape) {
239
+ updateFilter("");
240
+ return setSelectionMode("list");
241
+ }
242
+ if (key.downArrow || key.ctrl && input === "n") return moveSelection(1);
243
+ if (key.upArrow || key.ctrl && input === "p") return moveSelection(-1);
244
+ if (key.backspace || key.delete) return updateFilter(searchQuery.slice(0, -1));
245
+ if (isPrintableInput(input) && !key.ctrl && !key.meta) updateFilter(searchQuery + input);
246
+ return;
247
+ }
248
+ if (input === "/") return setSelectionMode("search");
249
+ if (input === " ") {
250
+ if (selectedWorkspacePackage) toggleCheckedDirectory(selectedWorkspacePackage.directory);
251
+ return;
252
+ }
253
+ if (input === "a") {
254
+ if (matchedPackages.length === 0) return;
255
+ setCheckedDirectories((currentCheckedDirectories) => {
256
+ const updatedCheckedDirectories = new Set(currentCheckedDirectories);
257
+ const shouldClearMatches = matchedPackages.every((matchedPackage) => currentCheckedDirectories.has(matchedPackage.workspacePackage.directory));
258
+ for (const matchedPackage of matchedPackages) if (shouldClearMatches) updatedCheckedDirectories.delete(matchedPackage.workspacePackage.directory);
259
+ else updatedCheckedDirectories.add(matchedPackage.workspacePackage.directory);
260
+ return updatedCheckedDirectories;
261
+ });
262
+ return;
263
+ }
264
+ if (input === "q") return submitDirectories([]);
265
+ if (key.escape) {
266
+ if (searchQuery.length > 0) return updateFilter("");
267
+ if (checkedDirectories.size > 0) return setCheckedDirectories(/* @__PURE__ */ new Set());
268
+ return submitDirectories([]);
269
+ }
270
+ if (key.return) return submitSelection();
271
+ if (key.downArrow || input === "j") return moveSelection(1);
272
+ if (key.upArrow || input === "k") return moveSelection(-1);
273
+ if (key.pageDown) return moveSelection(listHeight);
274
+ if (key.pageUp) return moveSelection(-listHeight);
275
+ });
276
+ const visibleStart = resolveVisibleStart({
277
+ itemCount: matchedPackages.length,
278
+ offset: scrollOffset,
279
+ selectedIndex: selectedPackageIndex,
280
+ viewportHeight: listHeight
281
+ });
282
+ const visibleMatches = matchedPackages.slice(visibleStart, visibleStart + listHeight);
283
+ const longestNameLength = Math.max(0, ...packages.map((workspacePackage) => workspacePackage.name.length));
284
+ let filterLine = null;
285
+ if (isSearching) filterLine = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
286
+ wrap: "truncate-end",
287
+ children: [
288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
289
+ color: "cyan",
290
+ children: "/ "
291
+ }),
292
+ searchQuery.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: searchQuery }) : null,
293
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
294
+ inverse: true,
295
+ children: " "
296
+ })
297
+ ]
298
+ });
299
+ else if (searchQuery.length > 0) filterLine = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
300
+ dimColor: true,
301
+ wrap: "truncate-end",
302
+ children: `filter: ${searchQuery}`
303
+ });
304
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
305
+ flexDirection: "column",
306
+ children: [
307
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
308
+ wrap: "truncate-end",
309
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
310
+ bold: true,
311
+ children: "Select projects to scan"
312
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
313
+ dimColor: true,
314
+ children: [
315
+ " ",
316
+ checkedDirectories.size,
317
+ "/",
318
+ packages.length
319
+ ]
320
+ })]
321
+ }),
322
+ filterLine,
323
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
324
+ flexDirection: "column",
325
+ height: listHeight,
326
+ children: matchedPackages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
327
+ dimColor: true,
328
+ children: "No matching projects"
329
+ }) : visibleMatches.map((matchedPackage, visiblePackageIndex) => {
330
+ const isSelected = visibleStart + visiblePackageIndex === selectedPackageIndex;
331
+ const isChecked = checkedDirectories.has(matchedPackage.workspacePackage.directory);
332
+ const shouldShowRelativeDirectory = matchedPackage.relativeDirectory !== matchedPackage.workspacePackage.name;
333
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
334
+ wrap: "truncate-end",
335
+ children: [
336
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
337
+ color: isSelected ? "cyan" : void 0,
338
+ children: isSelected ? `${terminalSymbols.pointer} ` : " "
339
+ }),
340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
341
+ color: isChecked ? "green" : void 0,
342
+ children: isChecked ? `${terminalSymbols.radioOn} ` : `${terminalSymbols.radioOff} `
343
+ }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MatchedName, {
345
+ name: matchedPackage.workspacePackage.name,
346
+ matchedIndices: matchedPackage.matchedIndices,
347
+ isSelected
348
+ }),
349
+ shouldShowRelativeDirectory ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
350
+ dimColor: true,
351
+ children: [" ".repeat(longestNameLength - matchedPackage.workspacePackage.name.length + 2), matchedPackage.relativeDirectory]
352
+ }) : null
353
+ ]
354
+ }, matchedPackage.workspacePackage.directory);
355
+ })
356
+ }),
357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
358
+ marginTop: 1,
359
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
360
+ dimColor: true,
361
+ wrap: "truncate-end",
362
+ children: isSearching ? "type to filter · enter confirm · esc clear" : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
363
+ "space select · a all · / search · ",
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
365
+ color: "cyan",
366
+ children: "enter"
367
+ }),
368
+ " to submit · q cancel"
369
+ ] })
370
+ })
371
+ })
372
+ ]
373
+ });
374
+ };
375
+ //#endregion
376
+ //#region src/cli/utils/onboarding-pacing.ts
377
+ const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
378
+ const FALSY_FLAG_VALUES = new Set([
379
+ "",
380
+ "0",
381
+ "false"
382
+ ]);
383
+ const isOnboardingForced = (environment = process.env) => {
384
+ const value = environment[FORCE_ONBOARDING_ENV_VAR];
385
+ return value !== void 0 && !FALSY_FLAG_VALUES.has(value.toLowerCase());
386
+ };
387
+ const shouldRecordOnboarding = (input) => input.paceOnboardingSections && !input.forceOnboarding && !input.verbose && !input.isNonInteractiveEnvironment;
388
+ const canAnimateOnboarding = (stream = process.stdout) => {
389
+ if (!(stream.isTTY === true && (stream.columns ?? 0) > 0 && process.env.TERM !== "dumb")) return false;
390
+ if (isOnboardingForced()) return true;
391
+ return !isGitHookEnvironment() && !isCiEnvironment();
392
+ };
393
+ //#endregion
394
+ //#region src/cli/utils/onboarding-state.ts
395
+ const ONBOARDING_GATE = {
396
+ id: ONBOARDING_EVENT,
397
+ scope: "global"
398
+ };
399
+ const markOnboardingComplete = (options = {}) => {
400
+ if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
401
+ };
402
+ //#endregion
403
+ //#region src/cli/ink/hooks/use-report-reveal.ts
404
+ const useReportReveal = ({ issueCount, onRevealComplete }) => {
405
+ const { stdout } = useStdout();
406
+ const shouldAnimate = issueCount > 0 && canAnimateOnboarding(stdout ?? void 0);
407
+ const streamStepCount = Math.min(12, Math.max(issueCount, 8));
408
+ const [streamStep, setStreamStep] = (0, import_react.useState)(0);
409
+ const [didRevealActions, setDidRevealActions] = (0, import_react.useState)(false);
410
+ const didNotifyRevealComplete = (0, import_react.useRef)(false);
411
+ const isStreaming = shouldAnimate && streamStep < streamStepCount;
412
+ const didReachActions = shouldAnimate ? didRevealActions : stdout?.isTTY === true;
413
+ (0, import_react.useEffect)(() => {
414
+ if (!shouldAnimate) return;
415
+ let nextStreamStep = 0;
416
+ const intervalId = setInterval(() => {
417
+ nextStreamStep = Math.min(streamStepCount, nextStreamStep + 1);
418
+ setStreamStep(nextStreamStep);
419
+ if (nextStreamStep === streamStepCount) clearInterval(intervalId);
420
+ }, 125);
421
+ return () => clearInterval(intervalId);
422
+ }, [shouldAnimate, streamStepCount]);
423
+ (0, import_react.useEffect)(() => {
424
+ if (!shouldAnimate || isStreaming) return;
425
+ const timeoutId = setTimeout(() => setDidRevealActions(true), 850);
426
+ return () => clearTimeout(timeoutId);
427
+ }, [isStreaming, shouldAnimate]);
428
+ (0, import_react.useEffect)(() => {
429
+ if (!didReachActions || didNotifyRevealComplete.current) return;
430
+ didNotifyRevealComplete.current = true;
431
+ onRevealComplete?.();
432
+ }, [didReachActions, onRevealComplete]);
433
+ if (!shouldAnimate) return {
434
+ phase: "actions",
435
+ streamSelectedIndex: 0
436
+ };
437
+ if (isStreaming) return {
438
+ phase: "streaming",
439
+ streamSelectedIndex: streamStep % issueCount
440
+ };
441
+ return {
442
+ phase: didRevealActions ? "actions" : "score",
443
+ streamSelectedIndex: 0
444
+ };
445
+ };
446
+ //#endregion
447
+ //#region src/cli/ink/lib/diagnostic-list-entries.ts
448
+ const CATEGORY_RANK = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category, index) => [category, index]));
449
+ const buildDiagnosticListEntries = (rows) => {
450
+ const rowsByCategory = /* @__PURE__ */ new Map();
451
+ for (const [rowIndex, row] of rows.entries()) {
452
+ const categoryRows = rowsByCategory.get(row.category) ?? [];
453
+ categoryRows.push({
454
+ row,
455
+ rowIndex
456
+ });
457
+ rowsByCategory.set(row.category, categoryRows);
458
+ }
459
+ const orderedCategories = [...rowsByCategory.keys()].sort((categoryA, categoryB) => {
460
+ const rankDelta = (CATEGORY_RANK.get(categoryA) ?? Number.MAX_SAFE_INTEGER) - (CATEGORY_RANK.get(categoryB) ?? Number.MAX_SAFE_INTEGER);
461
+ return rankDelta !== 0 ? rankDelta : categoryA.localeCompare(categoryB);
462
+ });
463
+ const entries = [];
464
+ for (const category of orderedCategories) {
465
+ const categoryRows = rowsByCategory.get(category) ?? [];
466
+ entries.push({
467
+ kind: "header",
468
+ category
469
+ });
470
+ for (const indexedRow of categoryRows) entries.push({
471
+ kind: "item",
472
+ ...indexedRow
473
+ });
474
+ }
475
+ return entries;
476
+ };
477
+ //#endregion
478
+ //#region src/cli/ink/lib/diagnostic-rows.ts
479
+ const buildDiagnosticRows = (diagnostics, scores) => {
480
+ return buildSortedRuleGroups(diagnostics, buildRulePriorityMap(scores)).map(([ruleKey, ruleDiagnostics]) => {
481
+ const representative = ruleDiagnostics.find((diagnostic) => diagnostic.line > 0) ?? ruleDiagnostics[0];
482
+ return {
483
+ ruleKey,
484
+ diagnostics: ruleDiagnostics,
485
+ severity: representative.severity === "error" ? "error" : "warning",
486
+ category: representative.category,
487
+ title: representative.title ?? ruleKey,
488
+ location: formatDiagnosticSite(representative),
489
+ siteCount: ruleDiagnostics.length,
490
+ representative,
491
+ ruleGuideUrl: hasPublishedFixRecipe(representative) ? buildRuleDocsUrl(representative.plugin, representative.rule) : null
492
+ };
493
+ });
494
+ };
495
+ //#endregion
496
+ //#region src/cli/ink/lib/resolve-report-layout.ts
497
+ const STACKED_FIXED_ROWS = 9;
498
+ const resolveReportLayout = ({ columns, diagnosticEntryCount, terminalRows }) => {
499
+ const width = Math.max(1, columns - 2);
500
+ const reportRows = Math.max(0, terminalRows - 1);
501
+ const isWide = columns >= 120 && terminalRows >= 22;
502
+ const isCompact = !isWide && terminalRows <= TUI_REPORT_COMPACT_MAX_ROWS;
503
+ const showsViewerScoreHeader = !isCompact || reportRows >= 8;
504
+ const detailHeight = Math.max(0, isWide ? reportRows - 3 : Math.min(15, reportRows - STACKED_FIXED_ROWS - 3));
505
+ const availableListHeight = isWide ? Math.max(3, detailHeight - 4 - 1) : Math.max(3, reportRows - STACKED_FIXED_ROWS - detailHeight);
506
+ const isStackedReportCapped = !isCompact && !isWide && availableListHeight > 16;
507
+ let listHeight = availableListHeight;
508
+ if (isCompact) {
509
+ const viewerScoreHeaderRows = showsViewerScoreHeader ? 4 : 0;
510
+ listHeight = Math.min(diagnosticEntryCount, Math.max(3, reportRows - 1 - viewerScoreHeaderRows));
511
+ } else if (!isWide) listHeight = Math.min(diagnosticEntryCount, 16, availableListHeight);
512
+ const detailColumnWidth = Math.max(20, Math.floor(width * TUI_REPORT_DETAIL_WIDTH_FRACTION));
513
+ const listColumnWidth = Math.max(20, width - detailColumnWidth - 3);
514
+ let layout = "stacked";
515
+ if (isCompact) layout = "compact";
516
+ else if (isWide) layout = "split";
517
+ return {
518
+ detailColumnWidth,
519
+ detailHeight,
520
+ isStackedReportCapped,
521
+ layout,
522
+ listColumnWidth,
523
+ listHeight,
524
+ showsViewerScoreHeader,
525
+ width
526
+ };
527
+ };
528
+ //#endregion
529
+ //#region src/cli/ink/components/action-menu.tsx
530
+ const ActionMenu = ({ actions, selectedIndex: controlledSelectedIndex, onSelectionChange, onEscape, onQuit }) => {
531
+ const [internalSelectedIndex, setInternalSelectedIndex] = (0, import_react.useState)(0);
532
+ const selectedIndex = controlledSelectedIndex ?? internalSelectedIndex;
533
+ const changeSelection = onSelectionChange ?? setInternalSelectedIndex;
534
+ useInput((input, key) => {
535
+ if (input === "q") return onQuit();
536
+ if (key.escape) return onEscape();
537
+ if (actions.length === 0) return;
538
+ if (key.upArrow || input === "k") return changeSelection(Math.max(0, selectedIndex - 1));
539
+ if (key.downArrow || input === "j") return changeSelection(Math.min(actions.length - 1, selectedIndex + 1));
540
+ if (key.return) actions[selectedIndex]?.onSelect();
541
+ });
542
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
543
+ flexDirection: "column",
544
+ marginTop: 1,
545
+ children: actions.map((action, index) => {
546
+ const isSelected = index === selectedIndex;
547
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
548
+ flexDirection: "column",
549
+ marginBottom: index < actions.length - 1 ? 1 : void 0,
550
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
551
+ color: isSelected ? "cyan" : void 0,
552
+ bold: isSelected,
553
+ children: [
554
+ isSelected ? terminalSymbols.pointer : terminalSymbols.pointerSmall,
555
+ " ",
556
+ action.label
557
+ ]
558
+ }), isSelected ? action.description : null]
559
+ }, action.id);
560
+ })
561
+ });
562
+ };
563
+ //#endregion
564
+ //#region src/cli/ink/components/agent-handoff.tsx
565
+ const AgentHandoff = ({ agents, onSelect, onCopyPrompt, onBack, onQuit }) => {
566
+ const actions = [...agents.map((agentId) => ({
567
+ id: agentId,
568
+ label: getSkillAgentConfig(agentId).displayName,
569
+ onSelect: () => onSelect(agentId)
570
+ })), {
571
+ id: "copy-prompt",
572
+ label: "Copy prompt",
573
+ description: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
574
+ dimColor: true,
575
+ children: "Paste into any agent or edit it first"
576
+ }),
577
+ onSelect: onCopyPrompt
578
+ }];
579
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
580
+ flexDirection: "column",
581
+ children: [
582
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
583
+ paddingLeft: 2,
584
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
585
+ bold: true,
586
+ children: "Choose how to continue"
587
+ })
588
+ }),
589
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionMenu, {
590
+ actions,
591
+ onEscape: onBack,
592
+ onQuit
593
+ }),
594
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
595
+ marginTop: 1,
596
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
597
+ dimColor: true,
598
+ children: "↑/↓ move · enter select · esc back · q quit"
599
+ })
600
+ })
601
+ ]
602
+ });
603
+ };
604
+ //#endregion
605
+ //#region src/cli/utils/format-hyperlink.ts
606
+ const OSC = "\x1B]";
607
+ const ST = "\x1B\\";
608
+ /**
609
+ * Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
610
+ * are exactly `text`; the link is carried in escape sequences a capable
611
+ * terminal turns into a click target.
612
+ */
613
+ const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
614
+ //#endregion
615
+ //#region src/cli/utils/supports-hyperlinks.ts
616
+ const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
617
+ "iTerm.app",
618
+ "WezTerm",
619
+ "vscode",
620
+ "Hyper",
621
+ "ghostty",
622
+ "Tabby",
623
+ "rio"
624
+ ]);
625
+ const parseVteVersion = (raw) => {
626
+ const parsed = Number.parseInt(raw ?? "", 10);
627
+ return Number.isNaN(parsed) ? 0 : parsed;
628
+ };
629
+ /**
630
+ * Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
631
+ * from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
632
+ * overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
633
+ * forces on), mirroring how the ecosystem's terminal libraries gate the same
634
+ * feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
635
+ * raw escape rather than a link). Unknown terminals default to off.
636
+ */
637
+ const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
638
+ const forced = env.FORCE_HYPERLINK;
639
+ if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
640
+ if (stream.isTTY !== true) return false;
641
+ if (env.TERM === "dumb") return false;
642
+ if (isCiEnvironment(env)) return false;
643
+ if (env.WT_SESSION) return true;
644
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
645
+ if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
646
+ return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
647
+ };
648
+ //#endregion
649
+ //#region src/cli/ink/components/tui-link.tsx
650
+ const TuiLink = ({ children, url }) => {
651
+ const shouldFormatHyperlink = supportsHyperlinks();
652
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Transform, {
653
+ transform: (text) => shouldFormatHyperlink ? formatHyperlink(text, url) : text,
654
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children })
655
+ });
656
+ };
657
+ //#endregion
658
+ //#region src/cli/ink/components/ci-justification.tsx
659
+ const CiJustification = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
660
+ flexDirection: "column",
661
+ paddingLeft: 2,
662
+ children: [
663
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
664
+ dimColor: true,
665
+ children: "Scan every pull request to prevent new React issues while you fix the backlog."
666
+ }),
667
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
668
+ dimColor: true,
669
+ children: [
670
+ "Used by teams at ",
671
+ CI_TRUST_COMPANIES,
672
+ "."
673
+ ]
674
+ }),
675
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TuiLink, {
676
+ url: GITHUB_ACTIONS_SETUP_URL,
677
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
678
+ color: "cyan",
679
+ children: GITHUB_ACTIONS_SETUP_URL
680
+ })
681
+ })
682
+ ]
683
+ });
684
+ //#endregion
685
+ //#region src/cli/ink/components/ci-setup.tsx
686
+ const CiSetup = ({ feedback, onConfirm, onLearnMore, onBack, onQuit }) => {
687
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
688
+ flexDirection: "column",
689
+ children: [
690
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
691
+ bold: true,
692
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
693
+ color: "cyan",
694
+ children: "?"
695
+ }), " Add React Doctor to GitHub Actions?"]
696
+ }),
697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CiJustification, {}),
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionMenu, {
699
+ actions: [{
700
+ id: "confirm",
701
+ label: "Yes, add the workflow",
702
+ onSelect: onConfirm
703
+ }, {
704
+ id: "learn-more",
705
+ label: "Open the GitHub Actions guide",
706
+ onSelect: onLearnMore
707
+ }],
708
+ onEscape: onBack,
709
+ onQuit
710
+ }),
711
+ feedback ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
712
+ color: feedback.didSucceed ? "green" : "yellow",
713
+ children: feedback.didSucceed ? feedback.message : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TuiLink, {
714
+ url: GITHUB_ACTIONS_SETUP_URL,
715
+ children: feedback.message
716
+ })
717
+ }) : null,
718
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
719
+ marginTop: 1,
720
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
721
+ dimColor: true,
722
+ children: "↑/↓ move · enter select · esc back · q quit"
723
+ })
724
+ })
725
+ ]
726
+ });
727
+ };
728
+ //#endregion
729
+ //#region src/cli/ink/hooks/use-scroll-viewport.ts
730
+ const useScrollViewport = (options) => {
731
+ const { itemCount, height, initialSelectedIndex = 0, isActive = true, isSelectable, onSelectedIndexChange } = options;
732
+ const canSelect = (index) => index >= 0 && index < itemCount && (isSelectable ? isSelectable(index) : true);
733
+ const seekSelectable = (start, step) => {
734
+ for (let index = start; index >= 0 && index < itemCount; index += step) if (canSelect(index)) return index;
735
+ return -1;
736
+ };
737
+ const nearestSelectable = (target, step) => {
738
+ const ahead = seekSelectable(target, step);
739
+ if (ahead !== -1) return ahead;
740
+ const behind = seekSelectable(target, -step);
741
+ return behind === -1 ? target : behind;
742
+ };
743
+ const [selectedIndex, setSelectedIndex] = (0, import_react.useState)(() => {
744
+ const first = seekSelectable(clampNumber(initialSelectedIndex, 0, itemCount - 1), 1);
745
+ return first === -1 ? 0 : first;
746
+ });
747
+ const [offset, setOffset] = (0, import_react.useState)(0);
748
+ const awaitingSecondG = (0, import_react.useRef)(false);
749
+ const moveTo = (rawIndex, step) => {
750
+ const nextSelectedIndex = nearestSelectable(clampNumber(rawIndex, 0, itemCount - 1), step);
751
+ setSelectedIndex(nextSelectedIndex);
752
+ onSelectedIndexChange?.(nextSelectedIndex);
753
+ setOffset((currentOffset) => {
754
+ if (nextSelectedIndex < currentOffset) return nextSelectedIndex;
755
+ if (nextSelectedIndex >= currentOffset + height) return nextSelectedIndex - height + 1;
756
+ return currentOffset;
757
+ });
758
+ };
759
+ useInput((input, key) => {
760
+ if (itemCount === 0) return;
761
+ const isSecondG = awaitingSecondG.current && input === "g";
762
+ if (input !== "g") awaitingSecondG.current = false;
763
+ if (key.downArrow || input === "j") return moveTo(selectedIndex + 1, 1);
764
+ if (key.upArrow || input === "k") return moveTo(selectedIndex - 1, -1);
765
+ if (key.pageDown) return moveTo(selectedIndex + height, 1);
766
+ if (key.pageUp) return moveTo(selectedIndex - height, -1);
767
+ if (key.ctrl && input === "d") return moveTo(selectedIndex + Math.floor(height / 2), 1);
768
+ if (key.ctrl && input === "u") return moveTo(selectedIndex - Math.floor(height / 2), -1);
769
+ if (input === "G") return moveTo(itemCount - 1, -1);
770
+ if (isSecondG) {
771
+ awaitingSecondG.current = false;
772
+ return moveTo(0, 1);
773
+ }
774
+ if (input === "g") awaitingSecondG.current = true;
775
+ }, { isActive });
776
+ const resolvedSelected = canSelect(selectedIndex) ? selectedIndex : nearestSelectable(clampNumber(selectedIndex, 0, itemCount - 1), 1);
777
+ const visibleStart = resolveVisibleStart({
778
+ itemCount,
779
+ offset,
780
+ selectedIndex: resolvedSelected,
781
+ viewportHeight: height
782
+ });
783
+ return {
784
+ selectedIndex: resolvedSelected,
785
+ visibleStart,
786
+ visibleEnd: Math.min(itemCount, visibleStart + height)
787
+ };
788
+ };
789
+ //#endregion
790
+ //#region src/cli/ink/lib/build-issue-prompt.ts
791
+ const buildIssuePrompt = ({ row, projectName }) => {
792
+ const { representative } = row;
793
+ const severityLabel = row.severity === "error" ? "ERROR" : "WARN";
794
+ const uniqueSites = [...new Set(row.diagnostics.map(formatDiagnosticSite))];
795
+ const inlineSites = uniqueSites.slice(0, 8);
796
+ const remainingSiteCount = uniqueSites.length - inlineSites.length;
797
+ const lines = [
798
+ `Fix exactly one React Doctor rule in ${projectName}:`,
799
+ "",
800
+ `${severityLabel} ${row.category}: ${row.title} (${row.ruleKey}, ×${row.siteCount})`,
801
+ representative.message
802
+ ];
803
+ if (representative.help) lines.push("", `Suggested fix: ${representative.help}`);
804
+ const fixRecipeLine = formatFixRecipeLine(representative);
805
+ if (fixRecipeLine) lines.push("", fixRecipeLine);
806
+ lines.push("", "Scope:", `- Fix only ${row.ruleKey}.`, "- Fix the root cause; do not suppress, disable, or silence the rule.", "- Keep unrelated refactors out of this pass.", "", "Affected sites:", ...inlineSites.map((site) => `- ${site}`));
807
+ if (remainingSiteCount > 0) lines.push(`- +${remainingSiteCount} more sites`);
808
+ const learnMoreLine = formatLearnMoreLine(representative);
809
+ if (learnMoreLine) lines.push("", learnMoreLine);
810
+ lines.push("", `Verify with \`npx react-doctor@latest --verbose\` and confirm ${row.ruleKey} is gone before moving on.`);
811
+ return lines.join("\n");
812
+ };
813
+ //#endregion
814
+ //#region src/cli/ink/lib/severity-variants.ts
815
+ const ICONS = isUnicodeSupported() ? {
816
+ error: "✖",
817
+ warning: "⚠"
818
+ } : {
819
+ error: "x",
820
+ warning: "!"
821
+ };
822
+ const severityVariant = (severity) => severity === "error" ? {
823
+ color: "red",
824
+ icon: ICONS.error,
825
+ label: "error"
826
+ } : {
827
+ color: "yellow",
828
+ icon: ICONS.warning,
829
+ label: "warning"
830
+ };
831
+ //#endregion
832
+ //#region src/cli/ink/components/diagnostic-detail.tsx
833
+ const DiagnosticDetail = ({ row, rootDirectory }) => {
834
+ const codeFrame = (0, import_react.useMemo)(() => {
835
+ if (!row) return null;
836
+ const { representative } = row;
837
+ return buildCodeFrame({
838
+ filePath: representative.filePath,
839
+ line: representative.line,
840
+ column: representative.column,
841
+ rootDirectory
842
+ });
843
+ }, [row, rootDirectory]);
844
+ if (!row) return null;
845
+ const variant = severityVariant(row.severity);
846
+ const { representative } = row;
847
+ const impact = getCategoryImpact(row.category);
848
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
849
+ flexDirection: "column",
850
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
851
+ wrap: "truncate-end",
852
+ children: [
853
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
854
+ color: variant.color,
855
+ children: [variant.icon, " "]
856
+ }),
857
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
858
+ color: variant.color,
859
+ bold: true,
860
+ children: row.title
861
+ }),
862
+ row.siteCount > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
863
+ dimColor: true,
864
+ children: [" ×", row.siteCount]
865
+ }) : null
866
+ ]
867
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
868
+ flexDirection: "column",
869
+ paddingLeft: 2,
870
+ children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
872
+ dimColor: true,
873
+ wrap: "truncate-end",
874
+ children: [
875
+ row.category,
876
+ " · ",
877
+ variant.label,
878
+ " · ",
879
+ row.location,
880
+ representative.fileContext ? ` · ${representative.fileContext} file` : ""
881
+ ]
882
+ }),
883
+ impact ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
884
+ marginTop: 1,
885
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
886
+ wrap: "wrap",
887
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
888
+ color: "cyan",
889
+ children: "Impact "
890
+ }), impact]
891
+ })
892
+ }) : null,
893
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
894
+ marginTop: 1,
895
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
896
+ wrap: "wrap",
897
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
898
+ color: "cyan",
899
+ children: "Why "
900
+ }), representative.message]
901
+ })
902
+ }),
903
+ codeFrame ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
904
+ marginTop: 1,
905
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: codeFrame })
906
+ }) : null,
907
+ representative.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
908
+ marginTop: 1,
909
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
910
+ wrap: "wrap",
911
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
912
+ color: "cyan",
913
+ children: "Fix "
914
+ }), representative.help]
915
+ })
916
+ }) : null,
917
+ row.ruleGuideUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
918
+ marginTop: 1,
919
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
920
+ color: "blue",
921
+ wrap: "truncate-end",
922
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TuiLink, {
923
+ url: row.ruleGuideUrl,
924
+ children: ["Rule guide: ", row.ruleGuideUrl]
925
+ })
926
+ })
927
+ }) : null
928
+ ]
929
+ })]
930
+ });
931
+ };
932
+ //#endregion
933
+ //#region src/cli/ink/components/diagnostic-item.tsx
934
+ const DiagnosticItem = ({ row, isSelected, isRead }) => {
935
+ const variant = severityVariant(row.severity);
936
+ const shouldHighlightSeverity = isSelected || !isRead;
937
+ let marker = "• ";
938
+ if (isRead) marker = " ";
939
+ if (isSelected) marker = "› ";
940
+ let markerColor = shouldHighlightSeverity ? variant.color : void 0;
941
+ if (isSelected) markerColor = "cyan";
942
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
943
+ wrap: "truncate-end",
944
+ dimColor: isRead && !isSelected,
945
+ children: [
946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
947
+ color: markerColor,
948
+ children: marker
949
+ }),
950
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
951
+ color: shouldHighlightSeverity ? variant.color : void 0,
952
+ children: `${variant.icon} `
953
+ }),
954
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
955
+ color: shouldHighlightSeverity ? variant.color : void 0,
956
+ bold: isSelected,
957
+ children: row.title
958
+ }),
959
+ row.siteCount > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
960
+ dimColor: true,
961
+ children: [" ×", row.siteCount]
962
+ }) : null
963
+ ]
964
+ });
965
+ };
966
+ //#endregion
967
+ //#region src/cli/ink/components/status-bar.tsx
968
+ const StatusBar = ({ total, errorCount, warningCount, position, issueCount, unreadCount, projectCount, keyHints = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
969
+ dimColor: true,
970
+ children: "↑/↓ to move"
971
+ }), exitHint = "q to quit", compact = false }) => {
972
+ const counts = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
973
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
974
+ bold: true,
975
+ children: pluralize(total, "finding")
976
+ }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
978
+ dimColor: true,
979
+ children: " "
980
+ }),
981
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
982
+ color: "red",
983
+ children: pluralize(errorCount, "error")
984
+ }),
985
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
986
+ dimColor: true,
987
+ children: " "
988
+ }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
990
+ color: "yellow",
991
+ children: pluralize(warningCount, "warning")
992
+ })
993
+ ] });
994
+ if (compact) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
995
+ wrap: "truncate-end",
996
+ children: [
997
+ counts,
998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
999
+ dimColor: true,
1000
+ children: [
1001
+ " · ",
1002
+ "issue ",
1003
+ position,
1004
+ "/",
1005
+ issueCount,
1006
+ " · "
1007
+ ]
1008
+ }),
1009
+ keyHints,
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1011
+ dimColor: true,
1012
+ children: [" · ", exitHint]
1013
+ })
1014
+ ]
1015
+ });
1016
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1017
+ flexDirection: "column",
1018
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1019
+ wrap: "truncate-end",
1020
+ children: [
1021
+ counts,
1022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1023
+ dimColor: true,
1024
+ children: [
1025
+ " · ",
1026
+ "issue ",
1027
+ position,
1028
+ "/",
1029
+ issueCount
1030
+ ]
1031
+ }),
1032
+ projectCount !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1033
+ dimColor: true,
1034
+ children: [" · ", pluralize(projectCount, "project")]
1035
+ }) : null
1036
+ ]
1037
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1038
+ wrap: "truncate-end",
1039
+ children: [
1040
+ unreadCount !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1041
+ color: unreadCount > 0 ? "cyan" : void 0,
1042
+ dimColor: unreadCount === 0,
1043
+ children: [
1044
+ pluralize(unreadCount, "issue"),
1045
+ " unread",
1046
+ " · "
1047
+ ]
1048
+ }) : null,
1049
+ keyHints,
1050
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1051
+ dimColor: true,
1052
+ children: [" · ", exitHint]
1053
+ })
1054
+ ]
1055
+ })]
1056
+ });
1057
+ };
1058
+ //#endregion
1059
+ //#region src/cli/ink/components/diagnostic-list.tsx
1060
+ const DIAGNOSTIC_KEY_HINTS = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1062
+ dimColor: true,
1063
+ children: "↑/↓ move · "
1064
+ }),
1065
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1066
+ color: "cyan",
1067
+ children: "enter"
1068
+ }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1070
+ dimColor: true,
1071
+ children: " copy context"
1072
+ })
1073
+ ] });
1074
+ const DiagnosticList = ({ header, rows, entries, width, listColumnWidth, detailColumnWidth, listHeight, detailHeight, layout, rootDirectory, projectName, projectCount, initialSelectedRowIndex, readRuleKeys, onSelectedRowIndexChange, onQuit, onBack = onQuit, exitHint }) => {
1075
+ const isSplit = layout === "split";
1076
+ const isCompact = layout === "compact";
1077
+ const initialSelectedEntryIndex = initialSelectedRowIndex === null ? 0 : Math.max(0, entries.findIndex((entry) => entry.kind === "item" && entry.rowIndex === initialSelectedRowIndex));
1078
+ const { selectedIndex: selectedEntryIndex, visibleStart: visibleEntryStart, visibleEnd: visibleEntryEnd } = useScrollViewport({
1079
+ itemCount: entries.length,
1080
+ height: listHeight,
1081
+ initialSelectedIndex: initialSelectedEntryIndex,
1082
+ isSelectable: (entryIndex) => entries[entryIndex]?.kind === "item",
1083
+ onSelectedIndexChange: (entryIndex) => {
1084
+ const entry = entries[entryIndex];
1085
+ if (entry?.kind === "item") onSelectedRowIndexChange(entry.rowIndex);
1086
+ }
1087
+ });
1088
+ const visibleEntries = entries.slice(visibleEntryStart, visibleEntryEnd);
1089
+ const selectedEntry = entries[selectedEntryIndex];
1090
+ const selected = selectedEntry?.kind === "item" ? selectedEntry.row : null;
1091
+ const selectedRuleKey = selected?.ruleKey ?? null;
1092
+ const initialSelectedRuleKey = (0, import_react.useRef)(selectedRuleKey);
1093
+ const didRecordFindingNavigation = (0, import_react.useRef)(false);
1094
+ const [copiedRuleKey, setCopiedRuleKey] = (0, import_react.useState)(null);
1095
+ const [copyFailedRuleKey, setCopyFailedRuleKey] = (0, import_react.useState)(null);
1096
+ const effectiveReadRuleKeys = (0, import_react.useMemo)(() => {
1097
+ if (!selectedRuleKey || readRuleKeys.has(selectedRuleKey)) return readRuleKeys;
1098
+ return new Set(readRuleKeys).add(selectedRuleKey);
1099
+ }, [readRuleKeys, selectedRuleKey]);
1100
+ (0, import_react.useEffect)(() => {
1101
+ if (!selectedRuleKey || selectedRuleKey === initialSelectedRuleKey.current || didRecordFindingNavigation.current) return;
1102
+ didRecordFindingNavigation.current = true;
1103
+ recordCount(METRIC.tuiFindingNavigated);
1104
+ }, [selectedRuleKey]);
1105
+ const copySelectedIssueContext = () => {
1106
+ if (!selected) return;
1107
+ const issuePrompt = buildIssuePrompt({
1108
+ row: selected,
1109
+ projectName
1110
+ });
1111
+ const ruleKey = selected.ruleKey;
1112
+ copyToClipboard(issuePrompt).then((didCopyIssueContext) => {
1113
+ if (didCopyIssueContext) {
1114
+ setCopiedRuleKey(ruleKey);
1115
+ setCopyFailedRuleKey(null);
1116
+ return;
1117
+ }
1118
+ recordCount(METRIC.tuiReportActionSelected, 1, { action: "copy-issue-context-failed" });
1119
+ setCopiedRuleKey(null);
1120
+ setCopyFailedRuleKey(ruleKey);
1121
+ });
1122
+ };
1123
+ useInput((input, key) => {
1124
+ if (input === "q") return onQuit();
1125
+ if (key.escape) return onBack();
1126
+ if (key.return && selected) {
1127
+ recordCount(METRIC.tuiReportActionSelected, 1, { action: "copy-issue-context" });
1128
+ copySelectedIssueContext();
1129
+ }
1130
+ });
1131
+ let totalFindingCount = 0;
1132
+ let errorFindingCount = 0;
1133
+ let warningFindingCount = 0;
1134
+ for (const row of rows) {
1135
+ totalFindingCount += row.siteCount;
1136
+ if (row.severity === "error") errorFindingCount += row.siteCount;
1137
+ else warningFindingCount += row.siteCount;
1138
+ }
1139
+ const selectedIssuePosition = entries.slice(0, selectedEntryIndex + 1).filter((entry) => entry.kind === "item").length;
1140
+ const unreadIssueCount = rows.length - rows.filter((row) => effectiveReadRuleKeys.has(row.ruleKey)).length;
1141
+ const listColumn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1142
+ flexDirection: "column",
1143
+ height: listHeight,
1144
+ width: isSplit ? listColumnWidth : width,
1145
+ children: visibleEntries.map((entry, index) => {
1146
+ const entryIndex = visibleEntryStart + index;
1147
+ if (entry.kind === "header") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1148
+ bold: true,
1149
+ children: entry.category
1150
+ }, `header-${entry.category}`);
1151
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiagnosticItem, {
1152
+ row: entry.row,
1153
+ isSelected: entryIndex === selectedEntryIndex,
1154
+ isRead: effectiveReadRuleKeys.has(entry.row.ruleKey)
1155
+ }, entry.row.ruleKey);
1156
+ })
1157
+ });
1158
+ let copyFeedback = null;
1159
+ if (copiedRuleKey === selectedRuleKey) copyFeedback = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1160
+ marginTop: 1,
1161
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1162
+ color: "green",
1163
+ children: "✓ Copied issue context"
1164
+ })
1165
+ });
1166
+ else if (copyFailedRuleKey === selectedRuleKey) copyFeedback = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1167
+ marginTop: 1,
1168
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1169
+ color: "yellow",
1170
+ children: "Couldn't copy issue context. Press Enter to try again."
1171
+ })
1172
+ });
1173
+ const detailContent = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiagnosticDetail, {
1174
+ row: selected,
1175
+ rootDirectory
1176
+ }), copyFeedback] });
1177
+ let visibleKeyHints = DIAGNOSTIC_KEY_HINTS;
1178
+ if (isCompact && copiedRuleKey === selectedRuleKey) visibleKeyHints = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1179
+ color: "green",
1180
+ children: "✓ Copied issue context"
1181
+ });
1182
+ else if (isCompact && copyFailedRuleKey === selectedRuleKey) visibleKeyHints = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1183
+ color: "yellow",
1184
+ children: "Copy failed · enter retry"
1185
+ });
1186
+ const statusBar = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1187
+ marginTop: isCompact ? 0 : 1,
1188
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBar, {
1189
+ total: totalFindingCount,
1190
+ errorCount: errorFindingCount,
1191
+ warningCount: warningFindingCount,
1192
+ position: selectedIssuePosition,
1193
+ issueCount: rows.length,
1194
+ unreadCount: unreadIssueCount,
1195
+ projectCount,
1196
+ keyHints: visibleKeyHints,
1197
+ exitHint,
1198
+ compact: isCompact
1199
+ })
1200
+ });
1201
+ if (isCompact) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1202
+ flexDirection: "column",
1203
+ width,
1204
+ children: [
1205
+ header,
1206
+ listColumn,
1207
+ statusBar
1208
+ ]
1209
+ });
1210
+ if (isSplit) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1211
+ flexDirection: "column",
1212
+ width,
1213
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1214
+ flexDirection: "row",
1215
+ maxHeight: detailHeight,
1216
+ overflowY: "hidden",
1217
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1218
+ flexDirection: "column",
1219
+ width: listColumnWidth,
1220
+ marginRight: 1,
1221
+ children: [header, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1222
+ marginTop: 1,
1223
+ children: listColumn
1224
+ })]
1225
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1226
+ flexDirection: "column",
1227
+ width: detailColumnWidth,
1228
+ borderStyle: "single",
1229
+ borderColor: "gray",
1230
+ borderTop: false,
1231
+ borderRight: false,
1232
+ borderBottom: false,
1233
+ paddingLeft: 1,
1234
+ overflowY: "hidden",
1235
+ children: detailContent
1236
+ })]
1237
+ }), statusBar]
1238
+ });
1239
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1240
+ flexDirection: "column",
1241
+ width,
1242
+ children: [
1243
+ header,
1244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1245
+ marginTop: 1,
1246
+ children: listColumn
1247
+ }),
1248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1249
+ dimColor: true,
1250
+ children: "─".repeat(width)
1251
+ }),
1252
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1253
+ flexDirection: "column",
1254
+ maxHeight: detailHeight,
1255
+ overflowY: "hidden",
1256
+ children: detailContent
1257
+ }),
1258
+ statusBar
1259
+ ]
1260
+ });
1261
+ };
1262
+ //#endregion
1263
+ //#region src/cli/ink/components/handoff-ci-recommendation.tsx
1264
+ const HandoffCiRecommendation = ({ onAddToCi, onContinue, onQuit }) => {
1265
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1266
+ flexDirection: "column",
1267
+ children: [
1268
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1269
+ paddingLeft: 2,
1270
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1271
+ bold: true,
1272
+ children: "Add React Doctor to GitHub Actions first"
1273
+ })
1274
+ }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CiJustification, {}),
1276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionMenu, {
1277
+ actions: [{
1278
+ id: "add-to-ci",
1279
+ label: "Add to GitHub Actions first (Recommended)",
1280
+ onSelect: onAddToCi
1281
+ }, {
1282
+ id: "continue",
1283
+ label: "Continue without GitHub Actions",
1284
+ onSelect: onContinue
1285
+ }],
1286
+ onEscape: onContinue,
1287
+ onQuit
1288
+ }),
1289
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1290
+ marginTop: 1,
1291
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1292
+ dimColor: true,
1293
+ children: "↑/↓ move · enter select · esc skip · q quit"
1294
+ })
1295
+ })
1296
+ ]
1297
+ });
1298
+ };
1299
+ //#endregion
1300
+ //#region src/cli/ink/components/report-landing.tsx
1301
+ const ReportStatus = ({ issueCount, emptyStateMessage, lintFailureReason, skippedChecks, incompleteMessage }) => {
1302
+ const skippedCheckLabel = skippedChecks?.flatMap((skippedCheck) => skippedCheck === "lint" && lintFailureReason ? [] : [formatSkippedCheckLabel(skippedCheck)]).join(" and ");
1303
+ const hasIncompleteResult = Boolean(incompleteMessage || lintFailureReason || skippedCheckLabel);
1304
+ if (!hasIncompleteResult && issueCount > 0) return null;
1305
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1306
+ flexDirection: "column",
1307
+ marginTop: 1,
1308
+ children: [
1309
+ incompleteMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1310
+ color: "yellow",
1311
+ children: ["⚠ ", incompleteMessage]
1312
+ }) : null,
1313
+ lintFailureReason ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1314
+ color: "yellow",
1315
+ children: ["⚠ Lint did not run: ", lintFailureReason]
1316
+ }) : null,
1317
+ skippedCheckLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1318
+ color: "yellow",
1319
+ children: [
1320
+ "⚠ ",
1321
+ issueCount === 0 ? "No issues detected, but " : "",
1322
+ skippedCheckLabel,
1323
+ " checks failed — results are incomplete."
1324
+ ]
1325
+ }) : null,
1326
+ issueCount === 0 && !hasIncompleteResult ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1327
+ color: "green",
1328
+ children: ["✔ ", emptyStateMessage ?? "No issues found. Nice work."]
1329
+ }) : null
1330
+ ]
1331
+ });
1332
+ };
1333
+ const ReportLanding = ({ header, phase, issueCount, emptyStateMessage, lintFailureReason, skippedChecks, incompleteMessage, actions, selectedIndex, onSelectionChange, onQuit }) => {
1334
+ const showScore = phase === "actions" || phase === "score";
1335
+ useInput((input) => {
1336
+ if (input === "q") onQuit();
1337
+ }, { isActive: phase !== "actions" });
1338
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1339
+ flexDirection: "column",
1340
+ children: [
1341
+ showScore ? header : null,
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportStatus, {
1343
+ issueCount,
1344
+ emptyStateMessage,
1345
+ lintFailureReason,
1346
+ skippedChecks,
1347
+ incompleteMessage
1348
+ }),
1349
+ phase === "actions" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionMenu, {
1350
+ actions,
1351
+ selectedIndex,
1352
+ onSelectionChange,
1353
+ onEscape: onQuit,
1354
+ onQuit
1355
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1356
+ marginTop: 1,
1357
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1358
+ dimColor: true,
1359
+ children: actions.length === 0 ? "q quit" : "↑/↓ move · enter select · q quit"
1360
+ })
1361
+ })] }) : null
1362
+ ]
1363
+ });
1364
+ };
1365
+ //#endregion
1366
+ //#region src/cli/ink/components/report-issue-stream.tsx
1367
+ const ReportIssueStream = ({ rows, selectedIndex, width }) => {
1368
+ const visibleRowCount = Math.min(rows.length, 3);
1369
+ const visibleRows = Array.from({ length: visibleRowCount }, (_, visibleRowIndex) => {
1370
+ const rowIndex = (selectedIndex - visibleRowCount + visibleRowIndex + 1 + rows.length) % rows.length;
1371
+ return {
1372
+ row: rows[rowIndex],
1373
+ rowIndex
1374
+ };
1375
+ });
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1377
+ flexDirection: "column",
1378
+ width: Math.min(width, 60),
1379
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1380
+ bold: true,
1381
+ children: "Reviewing issues"
1382
+ }), visibleRows.map(({ row, rowIndex }) => {
1383
+ if (!row) return null;
1384
+ const variant = severityVariant(row.severity);
1385
+ const isSelected = rowIndex === selectedIndex;
1386
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1387
+ dimColor: !isSelected,
1388
+ wrap: "truncate-end",
1389
+ children: [
1390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1391
+ color: isSelected ? variant.color : void 0,
1392
+ children: [isSelected ? "›" : " ", " "]
1393
+ }),
1394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1395
+ color: isSelected ? variant.color : void 0,
1396
+ bold: isSelected,
1397
+ children: [
1398
+ row.category,
1399
+ ": ",
1400
+ row.title
1401
+ ]
1402
+ }),
1403
+ row.siteCount > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1404
+ dimColor: true,
1405
+ children: [" ×", row.siteCount]
1406
+ }) : null
1407
+ ]
1408
+ }, row.ruleKey);
1409
+ })]
1410
+ });
1411
+ };
1412
+ //#endregion
1413
+ //#region src/cli/utils/doctor-face.ts
1414
+ const doctorFace = (score) => {
1415
+ if (score >= 75) return ["◠ ◠", " ▽ "];
1416
+ if (score >= 50) return ["• •", " ─ "];
1417
+ return ["x x", " ▽ "];
1418
+ };
1419
+ //#endregion
1420
+ //#region src/cli/utils/ease-out-cubic.ts
1421
+ const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
1422
+ //#endregion
1423
+ //#region src/cli/ink/hooks/use-animated-score.ts
1424
+ const useAnimatedScore = ({ score, projectedScore, shouldAnimate }) => {
1425
+ const projectionTarget = projectedScore ?? score;
1426
+ const hasProjection = projectionTarget > score;
1427
+ const initialProjectedScore = !shouldAnimate && hasProjection ? projectionTarget : null;
1428
+ const [displayScore, setDisplayScore] = (0, import_react.useState)(shouldAnimate ? 0 : score);
1429
+ const [displayProjectedScore, setDisplayProjectedScore] = (0, import_react.useState)(initialProjectedScore);
1430
+ (0, import_react.useEffect)(() => {
1431
+ if (!shouldAnimate) {
1432
+ setDisplayScore(score);
1433
+ setDisplayProjectedScore(hasProjection ? projectionTarget : null);
1434
+ return;
1435
+ }
1436
+ let timeoutId;
1437
+ const runProjection = () => {
1438
+ let projectionFrame = 1;
1439
+ const renderProjectionFrame = () => {
1440
+ const progress = easeOutCubic(projectionFrame / 16);
1441
+ setDisplayProjectedScore(score + (projectionTarget - score) * progress);
1442
+ if (projectionFrame < 16) {
1443
+ projectionFrame += 1;
1444
+ timeoutId = setTimeout(renderProjectionFrame, 35);
1445
+ } else setDisplayProjectedScore(projectionTarget);
1446
+ };
1447
+ renderProjectionFrame();
1448
+ };
1449
+ let scoreFrame = 0;
1450
+ const renderScoreFrame = () => {
1451
+ const progress = easeOutCubic(scoreFrame / 40);
1452
+ setDisplayScore(Math.round(score * progress));
1453
+ if (scoreFrame < 40) {
1454
+ scoreFrame += 1;
1455
+ timeoutId = setTimeout(renderScoreFrame, 50);
1456
+ return;
1457
+ }
1458
+ setDisplayScore(score);
1459
+ if (hasProjection) runProjection();
1460
+ };
1461
+ renderScoreFrame();
1462
+ return () => {
1463
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
1464
+ };
1465
+ }, [
1466
+ shouldAnimate,
1467
+ score,
1468
+ projectionTarget,
1469
+ hasProjection
1470
+ ]);
1471
+ return {
1472
+ displayScore,
1473
+ displayProjectedScore
1474
+ };
1475
+ };
1476
+ //#endregion
1477
+ //#region src/cli/ink/lib/score-color.ts
1478
+ const scoreColorName = (score) => {
1479
+ if (score >= 75) return "green";
1480
+ if (score >= 50) return "yellow";
1481
+ return "red";
1482
+ };
1483
+ //#endregion
1484
+ //#region src/cli/ink/components/score-header.tsx
1485
+ const REACT_DOCTOR_URL = "https://react.doctor";
1486
+ const ReactDoctorLink = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TuiLink, {
1487
+ url: REACT_DOCTOR_URL,
1488
+ children: ["React Doctor ", /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1489
+ dimColor: true,
1490
+ children: [
1491
+ "(",
1492
+ REACT_DOCTOR_URL,
1493
+ ")"
1494
+ ]
1495
+ })]
1496
+ });
1497
+ const ScoreHeader = ({ variant, score, projectedScore, projectName, issueCount, noScoreMessage, width }) => {
1498
+ const { columns } = useStdoutDimensions();
1499
+ const availableWidth = width ?? columns;
1500
+ const { stdout } = useStdout();
1501
+ const shouldAnimateScore = canAnimateOnboarding(stdout ?? void 0);
1502
+ const visibleProjectedScore = variant === "landing" ? projectedScore : null;
1503
+ const { displayScore, displayProjectedScore } = useAnimatedScore({
1504
+ score: score?.score ?? 0,
1505
+ projectedScore: visibleProjectedScore,
1506
+ shouldAnimate: variant === "landing" && shouldAnimateScore && score !== null
1507
+ });
1508
+ if (!score) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1509
+ flexDirection: "column",
1510
+ paddingLeft: 2,
1511
+ width: availableWidth,
1512
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1513
+ wrap: "truncate-end",
1514
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactDoctorLink, {})
1515
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1516
+ dimColor: true,
1517
+ wrap: "wrap",
1518
+ children: noScoreMessage ?? `${pluralize(issueCount, "finding")} · ${projectName}`
1519
+ })]
1520
+ });
1521
+ const scoreColor = scoreColorName(score.score);
1522
+ const scoreSummaryLine = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1523
+ wrap: "truncate-end",
1524
+ children: [
1525
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1526
+ color: scoreColor,
1527
+ bold: true,
1528
+ children: displayScore
1529
+ }),
1530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1531
+ dimColor: true,
1532
+ children: [
1533
+ " / ",
1534
+ 100,
1535
+ " "
1536
+ ]
1537
+ }),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1539
+ color: scoreColor,
1540
+ children: score.label
1541
+ }),
1542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1543
+ dimColor: true,
1544
+ children: [" · ", projectName]
1545
+ })
1546
+ ]
1547
+ });
1548
+ if (availableWidth < 23) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1549
+ flexDirection: "column",
1550
+ width: availableWidth,
1551
+ children: [scoreSummaryLine, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1552
+ wrap: "truncate-end",
1553
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactDoctorLink, {})
1554
+ })]
1555
+ });
1556
+ const barWidth = Math.max(10, Math.min(50, availableWidth - 11 - 2));
1557
+ const filledBarWidth = Math.round(displayScore / 100 * barWidth);
1558
+ const projectedBarWidth = displayProjectedScore != null ? Math.min(barWidth, Math.round(displayProjectedScore / 100 * barWidth)) : filledBarWidth;
1559
+ const projectedGainWidth = Math.max(0, projectedBarWidth - filledBarWidth);
1560
+ const emptyBarWidth = Math.max(0, barWidth - filledBarWidth - projectedGainWidth);
1561
+ const [eyes, mouth] = doctorFace(score.score);
1562
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1563
+ flexDirection: "column",
1564
+ width: availableWidth,
1565
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1566
+ paddingLeft: 2,
1567
+ width: availableWidth,
1568
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1569
+ flexDirection: "column",
1570
+ flexShrink: 0,
1571
+ width: 7,
1572
+ marginRight: 2,
1573
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1574
+ color: scoreColor,
1575
+ children: `┌─────┐\n│ ${eyes} │\n│ ${mouth} │\n└─────┘`
1576
+ })
1577
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1578
+ flexDirection: "column",
1579
+ width: availableWidth - 11,
1580
+ children: [
1581
+ scoreSummaryLine,
1582
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1583
+ wrap: "truncate-end",
1584
+ children: [
1585
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1586
+ color: scoreColor,
1587
+ children: "█".repeat(filledBarWidth)
1588
+ }),
1589
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1590
+ color: scoreColor,
1591
+ dimColor: true,
1592
+ children: "▓".repeat(projectedGainWidth)
1593
+ }),
1594
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1595
+ dimColor: true,
1596
+ children: "░".repeat(emptyBarWidth)
1597
+ })
1598
+ ]
1599
+ }),
1600
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1601
+ wrap: "truncate-end",
1602
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactDoctorLink, {})
1603
+ }),
1604
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " })
1605
+ ]
1606
+ })]
1607
+ }), variant === "landing" && projectedScore != null && projectedScore > score.score ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1608
+ wrap: "truncate-end",
1609
+ children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1611
+ dimColor: true,
1612
+ children: " Potential score "
1613
+ }),
1614
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1615
+ color: scoreColorName(projectedScore),
1616
+ children: projectedScore
1617
+ }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1619
+ dimColor: true,
1620
+ children: " after priority fixes "
1621
+ }),
1622
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1623
+ color: scoreColorName(projectedScore),
1624
+ children: ["+", projectedScore - score.score]
1625
+ })
1626
+ ]
1627
+ }) : null]
1628
+ });
1629
+ };
1630
+ //#endregion
1631
+ //#region src/cli/ink/components/report.tsx
1632
+ const EMPTY_LAUNCHABLE_AGENTS = [];
1633
+ const recordReportAction = (action) => {
1634
+ recordCount(METRIC.tuiReportActionSelected, 1, { action });
1635
+ };
1636
+ const completeReportOnboarding = () => {
1637
+ if (shouldRecordOnboarding({
1638
+ paceOnboardingSections: true,
1639
+ forceOnboarding: isOnboardingForced(),
1640
+ verbose: false,
1641
+ isNonInteractiveEnvironment: false
1642
+ })) markOnboardingComplete();
1643
+ };
1644
+ const Report = ({ report, onExit, onQuit = onExit, launchableAgents = EMPTY_LAUNCHABLE_AGENTS, onHandoff, canAddToCi, onAddToCi, projectCount, priorityScores, exitHint = "q to quit" }) => {
1645
+ const { rows: terminalRows, columns } = useStdoutDimensions();
1646
+ const diagnosticRows = (0, import_react.useMemo)(() => buildDiagnosticRows(report.diagnostics, priorityScores ?? [report.score]), [
1647
+ report.diagnostics,
1648
+ report.score,
1649
+ priorityScores
1650
+ ]);
1651
+ const diagnosticListEntries = (0, import_react.useMemo)(() => buildDiagnosticListEntries(diagnosticRows), [diagnosticRows]);
1652
+ const reportLayout = resolveReportLayout({
1653
+ columns,
1654
+ diagnosticEntryCount: diagnosticListEntries.length,
1655
+ terminalRows
1656
+ });
1657
+ const reportReveal = useReportReveal({
1658
+ issueCount: diagnosticRows.length,
1659
+ onRevealComplete: completeReportOnboarding
1660
+ });
1661
+ const [activeReportScreen, setActiveReportScreen] = (0, import_react.useState)("landing");
1662
+ const [ciSetupFeedback, setCiSetupFeedback] = (0, import_react.useState)();
1663
+ const [landingSelectedIndex, setLandingSelectedIndex] = (0, import_react.useState)(0);
1664
+ const [viewerSelectedRowIndex, setViewerSelectedRowIndex] = (0, import_react.useState)(null);
1665
+ const [viewerReadRuleKeys, setViewerReadRuleKeys] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
1666
+ const [isCiSetupQueued, setIsCiSetupQueued] = (0, import_react.useState)(false);
1667
+ const [shouldShowIssueStream, setShouldShowIssueStream] = (0, import_react.useState)(true);
1668
+ const didRecordCompactReport = (0, import_react.useRef)(false);
1669
+ const didRecordIssueStream = (0, import_react.useRef)(false);
1670
+ const didRecordStackedReportCap = (0, import_react.useRef)(false);
1671
+ const isCompact = reportLayout.layout === "compact";
1672
+ const isWide = reportLayout.layout === "split";
1673
+ (0, import_react.useEffect)(() => {
1674
+ if (!isCompact || didRecordCompactReport.current) return;
1675
+ didRecordCompactReport.current = true;
1676
+ recordCount(METRIC.tuiCompactReportShown);
1677
+ }, [isCompact]);
1678
+ (0, import_react.useEffect)(() => {
1679
+ if (!reportLayout.isStackedReportCapped || didRecordStackedReportCap.current) return;
1680
+ didRecordStackedReportCap.current = true;
1681
+ recordCount(METRIC.tuiStackedReportCapped);
1682
+ }, [reportLayout.isStackedReportCapped]);
1683
+ (0, import_react.useEffect)(() => {
1684
+ if (reportReveal.phase !== "streaming" || didRecordIssueStream.current) return;
1685
+ didRecordIssueStream.current = true;
1686
+ recordCount(METRIC.tuiIssueStreamShown);
1687
+ }, [reportReveal.phase]);
1688
+ const scoreHeaderProps = {
1689
+ score: report.score,
1690
+ projectedScore: report.projectedScore,
1691
+ projectName: report.projectName,
1692
+ issueCount: report.diagnostics.length,
1693
+ noScoreMessage: report.noScoreMessage,
1694
+ width: isWide ? reportLayout.listColumnWidth : reportLayout.width
1695
+ };
1696
+ const isCiSetupAvailable = Boolean(canAddToCi && onAddToCi && !isCiSetupQueued);
1697
+ const isHandoffAvailable = diagnosticRows.length > 0 && Boolean(onHandoff);
1698
+ const completeHandoff = (destination) => {
1699
+ if (!onHandoff) return;
1700
+ onHandoff({
1701
+ destination,
1702
+ prompt: buildHandoffPayload({
1703
+ diagnostics: report.diagnostics,
1704
+ projectName: report.projectName
1705
+ })
1706
+ });
1707
+ onExit();
1708
+ };
1709
+ const firstDiagnosticEntry = diagnosticListEntries.find((entry) => entry.kind === "item");
1710
+ const resolvedViewerSelectedRowIndex = viewerSelectedRowIndex ?? (firstDiagnosticEntry?.kind === "item" ? firstDiagnosticEntry.rowIndex : null);
1711
+ const markViewerRuleRead = (index) => {
1712
+ const ruleKey = diagnosticRows[index]?.ruleKey;
1713
+ if (!ruleKey) return;
1714
+ setViewerReadRuleKeys((previous) => previous.has(ruleKey) ? previous : new Set(previous).add(ruleKey));
1715
+ };
1716
+ const handleViewerSelectionChange = (index) => {
1717
+ setViewerSelectedRowIndex(index);
1718
+ markViewerRuleRead(index);
1719
+ };
1720
+ const openReportScreen = (nextScreen) => {
1721
+ setShouldShowIssueStream(false);
1722
+ setCiSetupFeedback(void 0);
1723
+ setActiveReportScreen(nextScreen);
1724
+ };
1725
+ const landingActions = [];
1726
+ if (report.diagnostics.length > 0) landingActions.push({
1727
+ id: "view-issues",
1728
+ label: `Review ${pluralize(diagnosticRows.length, "issue")}`,
1729
+ onSelect: () => {
1730
+ recordReportAction("view-issues");
1731
+ if (resolvedViewerSelectedRowIndex !== null) {
1732
+ setViewerSelectedRowIndex(resolvedViewerSelectedRowIndex);
1733
+ markViewerRuleRead(resolvedViewerSelectedRowIndex);
1734
+ }
1735
+ openReportScreen("issues");
1736
+ }
1737
+ });
1738
+ if (isCiSetupAvailable) landingActions.push({
1739
+ id: "add-to-ci",
1740
+ label: "Add to GitHub Actions (Recommended)",
1741
+ description: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CiJustification, {}),
1742
+ onSelect: () => {
1743
+ recordReportAction("add-to-ci");
1744
+ openReportScreen("ci");
1745
+ }
1746
+ });
1747
+ if (isHandoffAvailable) landingActions.push({
1748
+ id: "handoff",
1749
+ label: "Hand off to an agent",
1750
+ onSelect: () => {
1751
+ recordReportAction("handoff");
1752
+ openReportScreen(isCiSetupAvailable ? "handoff-ci" : "handoff");
1753
+ }
1754
+ });
1755
+ const postReviewLandingActionIndex = landingActions.findIndex((action) => action.id !== "view-issues");
1756
+ const resolvedLandingSelectedIndex = Math.min(landingSelectedIndex, Math.max(0, landingActions.length - 1));
1757
+ const issueStream = reportReveal.phase === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportIssueStream, {
1758
+ rows: diagnosticRows,
1759
+ selectedIndex: reportReveal.streamSelectedIndex,
1760
+ width: reportLayout.width
1761
+ }) : null;
1762
+ const screenContent = {
1763
+ ci: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CiSetup, {
1764
+ feedback: ciSetupFeedback,
1765
+ onConfirm: () => {
1766
+ onAddToCi?.();
1767
+ onExit();
1768
+ },
1769
+ onLearnMore: () => {
1770
+ recordReportAction("ci-learn-more");
1771
+ openUrl(GITHUB_ACTIONS_SETUP_URL).then((didOpen) => {
1772
+ setCiSetupFeedback({
1773
+ didSucceed: didOpen,
1774
+ message: didOpen ? "✓ Opened the GitHub Actions guide in your browser" : `Couldn't open a browser. Visit ${GITHUB_ACTIONS_SETUP_URL}`
1775
+ });
1776
+ });
1777
+ },
1778
+ onBack: () => {
1779
+ setCiSetupFeedback(void 0);
1780
+ setActiveReportScreen("landing");
1781
+ },
1782
+ onQuit
1783
+ }),
1784
+ handoff: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentHandoff, {
1785
+ agents: launchableAgents,
1786
+ onSelect: completeHandoff,
1787
+ onCopyPrompt: () => completeHandoff("clipboard"),
1788
+ onBack: () => setActiveReportScreen("landing"),
1789
+ onQuit
1790
+ }),
1791
+ "handoff-ci": /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HandoffCiRecommendation, {
1792
+ onAddToCi: () => {
1793
+ onAddToCi?.();
1794
+ setIsCiSetupQueued(true);
1795
+ setActiveReportScreen("handoff");
1796
+ },
1797
+ onContinue: () => setActiveReportScreen("handoff"),
1798
+ onQuit
1799
+ }),
1800
+ landing: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportLanding, {
1801
+ header: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScoreHeader, {
1802
+ variant: "landing",
1803
+ ...scoreHeaderProps
1804
+ }),
1805
+ phase: reportReveal.phase,
1806
+ issueCount: report.diagnostics.length,
1807
+ emptyStateMessage: report.emptyStateMessage,
1808
+ lintFailureReason: report.lintFailureReason,
1809
+ skippedChecks: report.skippedChecks,
1810
+ incompleteMessage: report.incompleteMessage,
1811
+ actions: landingActions,
1812
+ selectedIndex: resolvedLandingSelectedIndex,
1813
+ onSelectionChange: setLandingSelectedIndex,
1814
+ onQuit
1815
+ }),
1816
+ issues: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiagnosticList, {
1817
+ header: reportLayout.showsViewerScoreHeader ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScoreHeader, {
1818
+ variant: "viewer",
1819
+ ...scoreHeaderProps
1820
+ }) : null,
1821
+ rows: diagnosticRows,
1822
+ entries: diagnosticListEntries,
1823
+ width: reportLayout.width,
1824
+ listColumnWidth: reportLayout.listColumnWidth,
1825
+ detailColumnWidth: reportLayout.detailColumnWidth,
1826
+ listHeight: reportLayout.listHeight,
1827
+ detailHeight: reportLayout.detailHeight,
1828
+ layout: reportLayout.layout,
1829
+ rootDirectory: report.rootDirectory,
1830
+ projectName: report.projectName,
1831
+ projectCount,
1832
+ initialSelectedRowIndex: resolvedViewerSelectedRowIndex,
1833
+ readRuleKeys: viewerReadRuleKeys,
1834
+ onSelectedRowIndexChange: handleViewerSelectionChange,
1835
+ onQuit,
1836
+ onBack: () => {
1837
+ setLandingSelectedIndex(Math.max(0, postReviewLandingActionIndex));
1838
+ setActiveReportScreen("landing");
1839
+ },
1840
+ exitHint: `esc back · ${exitHint}`
1841
+ })
1842
+ };
1843
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [activeReportScreen === "landing" && shouldShowIssueStream ? issueStream : null, screenContent[activeReportScreen]] });
1844
+ };
1845
+ //#endregion
1846
+ //#region src/cli/ink/components/scanning.tsx
1847
+ const Scanning = ({ progressText, recent }) => {
1848
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1849
+ flexDirection: "column",
1850
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1851
+ wrap: "truncate-end",
1852
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1853
+ color: "cyan",
1854
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { type: "dots" })
1855
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [" ", progressText ?? "Scanning…"] })]
1856
+ }), recent.map((diagnostic, diagnosticIndex) => {
1857
+ const variant = severityVariant(diagnostic.severity === "error" ? "error" : "warning");
1858
+ const location = formatDiagnosticSite(diagnostic);
1859
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1860
+ wrap: "truncate-end",
1861
+ children: [
1862
+ " ",
1863
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1864
+ color: variant.color,
1865
+ children: variant.icon
1866
+ }),
1867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [" ", diagnostic.title ?? `${diagnostic.plugin}/${diagnostic.rule}`] }),
1868
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1869
+ dimColor: true,
1870
+ children: [" ", location]
1871
+ })
1872
+ ]
1873
+ }, `${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.plugin}:${diagnostic.rule}:${diagnosticIndex}`);
1874
+ })]
1875
+ });
1876
+ };
1877
+ //#endregion
1878
+ //#region src/cli/ink/components/summary.tsx
1879
+ const Summary = ({ summary, onExit, onQuit, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
1880
+ const skippedProjects = summary.skippedProjects ?? [];
1881
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Report, {
1882
+ report: {
1883
+ diagnostics: summary.combinedDiagnostics,
1884
+ score: summary.aggregateScore,
1885
+ projectedScore: summary.projectedScore,
1886
+ projectName: summary.projectName,
1887
+ rootDirectory: summary.rootDirectory,
1888
+ scannedFileCount: summary.scannedFileCount,
1889
+ elapsedMilliseconds: summary.elapsedMilliseconds,
1890
+ isOffline: summary.isOffline,
1891
+ noScoreMessage: summary.noScoreMessage,
1892
+ skippedChecks: [...new Set(summary.projects.flatMap((project) => project.skippedChecks ?? []))],
1893
+ ...skippedProjects.length > 0 ? { incompleteMessage: formatSkippedProjectsMessage(skippedProjects.length) } : {},
1894
+ ...summary.emptyStateMessage ? { emptyStateMessage: summary.emptyStateMessage } : {},
1895
+ ...summary.lintFailureReason ? { lintFailureReason: summary.lintFailureReason } : {}
1896
+ },
1897
+ onExit,
1898
+ onQuit,
1899
+ launchableAgents,
1900
+ onHandoff,
1901
+ canAddToCi,
1902
+ onAddToCi,
1903
+ projectCount: summary.projects.length + skippedProjects.length,
1904
+ priorityScores: summary.projects.map((project) => project.score)
1905
+ });
1906
+ };
1907
+ //#endregion
1908
+ //#region src/cli/ink/hooks/use-scan-store.ts
1909
+ const useScanStore = (store) => (0, import_react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
1910
+ //#endregion
1911
+ //#region src/cli/ink/scan-app.tsx
1912
+ const ScanApp = ({ store, displayMode = "report", launchableAgents, onHandoff, canAddToCi, onAddToCi, onQuit }) => {
1913
+ const snapshot = useScanStore(store);
1914
+ const { exit } = useApp();
1915
+ useExitOnCtrlC(onQuit);
1916
+ const handleQuit = () => {
1917
+ onQuit?.();
1918
+ exit();
1919
+ };
1920
+ if (displayMode === "report" && snapshot.phase === "summary") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Summary, {
1921
+ summary: snapshot.summary,
1922
+ launchableAgents,
1923
+ onHandoff,
1924
+ canAddToCi,
1925
+ onAddToCi,
1926
+ onExit: exit,
1927
+ onQuit: handleQuit
1928
+ });
1929
+ if (displayMode === "report" && snapshot.phase === "report") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Report, {
1930
+ report: snapshot.report,
1931
+ launchableAgents,
1932
+ onHandoff,
1933
+ canAddToCi,
1934
+ onAddToCi,
1935
+ onExit: exit,
1936
+ onQuit: handleQuit
1937
+ });
1938
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scanning, {
1939
+ progressText: snapshot.progress,
1940
+ recent: snapshot.liveDiagnostics.slice(-5)
1941
+ });
1942
+ };
1943
+ //#endregion
1944
+ //#region src/cli/ink/scan-bridge-layers.ts
1945
+ const reporterLayerForStore = (store) => succeed(Reporter, Reporter.of({
1946
+ emit: (diagnostic) => sync(() => store.emitDiagnostic(diagnostic)),
1947
+ finalize: void_
1948
+ }));
1949
+ const progressHandleForStore = (store, options = {}) => (text) => {
1950
+ const transformText = options.transformText ?? ((displayText) => displayText);
1951
+ store.setProgress(transformText(text));
1952
+ return {
1953
+ update: (displayText) => sync(() => store.scheduleProgress(transformText(displayText))),
1954
+ succeed: (displayText) => sync(() => store.setProgress(transformText(displayText))),
1955
+ fail: (displayText) => sync(() => store.setProgress(transformText(displayText))),
1956
+ stop: () => sync(() => store.setProgress(options.shouldClearOnStop === false ? transformText(text) : null))
1957
+ };
1958
+ };
1959
+ const progressLayerForStore = (store, options = {}) => Progress.layerOra(progressHandleForStore(store, options));
1960
+ //#endregion
1961
+ //#region src/cli/ink/scan-store.ts
1962
+ const INITIAL_SNAPSHOT = {
1963
+ phase: "scanning",
1964
+ liveDiagnostics: [],
1965
+ progress: null
1966
+ };
1967
+ const createScanStore = () => {
1968
+ let snapshot = INITIAL_SNAPSHOT;
1969
+ const listeners = /* @__PURE__ */ new Set();
1970
+ let pendingProgress = null;
1971
+ let progressTimer = null;
1972
+ const commit = (next) => {
1973
+ snapshot = next;
1974
+ for (const listener of [...listeners]) listener();
1975
+ };
1976
+ const cancelPendingProgress = () => {
1977
+ pendingProgress = null;
1978
+ if (progressTimer === null) return;
1979
+ clearTimeout(progressTimer);
1980
+ progressTimer = null;
1981
+ };
1982
+ const setProgress = (progress) => {
1983
+ cancelPendingProgress();
1984
+ commit({
1985
+ ...snapshot,
1986
+ progress
1987
+ });
1988
+ };
1989
+ return {
1990
+ subscribe: (listener) => {
1991
+ listeners.add(listener);
1992
+ return () => {
1993
+ listeners.delete(listener);
1994
+ };
1995
+ },
1996
+ getSnapshot: () => snapshot,
1997
+ emitDiagnostic: (diagnostic) => commit({
1998
+ ...snapshot,
1999
+ liveDiagnostics: [...snapshot.liveDiagnostics, diagnostic].slice(-25)
2000
+ }),
2001
+ scheduleProgress: (progress) => {
2002
+ pendingProgress = progress;
2003
+ if (progressTimer !== null) return;
2004
+ progressTimer = setTimeout(() => {
2005
+ progressTimer = null;
2006
+ const progressToCommit = pendingProgress;
2007
+ pendingProgress = null;
2008
+ if (progressToCommit !== null) commit({
2009
+ ...snapshot,
2010
+ progress: progressToCommit
2011
+ });
2012
+ }, 250);
2013
+ progressTimer.unref?.();
2014
+ },
2015
+ setProgress,
2016
+ setReport: (report) => {
2017
+ cancelPendingProgress();
2018
+ commit({
2019
+ phase: "report",
2020
+ liveDiagnostics: snapshot.liveDiagnostics,
2021
+ progress: snapshot.progress,
2022
+ report
2023
+ });
2024
+ },
2025
+ setSummary: (summary) => {
2026
+ cancelPendingProgress();
2027
+ commit({
2028
+ phase: "summary",
2029
+ liveDiagnostics: snapshot.liveDiagnostics,
2030
+ progress: snapshot.progress,
2031
+ summary
2032
+ });
2033
+ }
2034
+ };
2035
+ };
2036
+ //#endregion
2037
+ //#region src/cli/ink/run-scan-app.tsx
2038
+ const qualifyDiagnosticPaths = (diagnostics, rootDirectory, projectDirectory) => {
2039
+ const prefix = path.relative(rootDirectory, projectDirectory);
2040
+ if (prefix === "" || prefix.startsWith("..")) return [...diagnostics];
2041
+ return diagnostics.map((diagnostic) => path.isAbsolute(diagnostic.filePath) ? diagnostic : {
2042
+ ...diagnostic,
2043
+ filePath: toForwardSlashes(path.join(prefix, diagnostic.filePath))
2044
+ });
2045
+ };
2046
+ const resolveScanPresentation = (input, projectScans, rootDirectory) => {
2047
+ const isScoreDisabled = input.options?.noScore === true || projectScans.some((projectScan) => projectScan.config?.noScore === true);
2048
+ return {
2049
+ isOffline: isCiEnvironment() || input.share === false || isShareOptedOut(projectScans, input.options?.noScore),
2050
+ initialProgress: projectScans.length > 1 ? "Indexing workspace files…" : "Scanning project…",
2051
+ noScoreMessage: buildNoScoreMessage({ isScoreDisabled }),
2052
+ outputDirectory: input.options?.outputDirectory,
2053
+ shouldRecommendCi: isCiUnconfigured(rootDirectory),
2054
+ verbose: input.options?.verbose === true
2055
+ };
2056
+ };
2057
+ const resolveTuiInspectOptions = (input, config) => {
2058
+ const warnings = resolveCliInspectOptions({
2059
+ blocking: input.blocking,
2060
+ warnings: input.options?.warnings
2061
+ }, config).warnings;
2062
+ return warnings === void 0 ? { ...input.options } : {
2063
+ ...input.options,
2064
+ warnings
2065
+ };
2066
+ };
2067
+ const resolveSelectedDirectories = async (rootDirectory, input) => {
2068
+ const packages = discoverWorkspacePackages(rootDirectory);
2069
+ if (!(packages.length > 1 && !input.projectFlag && !input.skipPrompts && (input.configProjects ?? []).length === 0 && process.stdin.isTTY === true)) return selectProjects(rootDirectory, input.projectFlag, input.skipPrompts ?? false, input.configProjects);
2070
+ return promptProjectSelection(packages, rootDirectory);
2071
+ };
2072
+ const promptProjectSelection = (packages, rootDirectory) => new Promise((resolve) => {
2073
+ let disposeRenderer = () => {};
2074
+ recordCount(METRIC.tuiProjectSelectShown);
2075
+ disposeRenderer = registerMountedTuiRenderer(render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProjectSelect, {
2076
+ packages,
2077
+ rootDirectory,
2078
+ onSubmit: (directories) => {
2079
+ disposeRenderer();
2080
+ resolve(directories);
2081
+ }
2082
+ }), {
2083
+ alternateScreen: true,
2084
+ exitOnCtrlC: false
2085
+ }));
2086
+ });
2087
+ const resolveLintFailureReason = (results) => {
2088
+ for (const result of results) {
2089
+ if (!hasLintHardFailure(result)) continue;
2090
+ return result.skippedCheckReasons?.lint ?? "Lint failed before diagnostics were produced.";
2091
+ }
2092
+ return null;
2093
+ };
2094
+ const toScanReport = ({ result, diagnostics, rootDirectory, projectedScore, isOffline, noScoreMessage, emptyStateMessage }) => {
2095
+ const lintFailureReason = resolveLintFailureReason([result]);
2096
+ return {
2097
+ diagnostics: diagnostics ?? result.diagnostics,
2098
+ score: result.score,
2099
+ projectedScore,
2100
+ projectName: result.project.projectName,
2101
+ rootDirectory,
2102
+ scannedFileCount: result.scannedFileCount ?? 0,
2103
+ elapsedMilliseconds: result.elapsedMilliseconds,
2104
+ isOffline,
2105
+ noScoreMessage,
2106
+ emptyStateMessage,
2107
+ skippedChecks: result.skippedChecks,
2108
+ ...lintFailureReason ? { lintFailureReason } : {}
2109
+ };
2110
+ };
2111
+ const findLowestScoredReport = (reports) => {
2112
+ let lowestScoredReport = null;
2113
+ for (const report of reports) {
2114
+ if (report.score === null) continue;
2115
+ if (lowestScoredReport === null || report.score.score < lowestScoredReport.score.score) lowestScoredReport = {
2116
+ score: report.score,
2117
+ scoreDiagnostics: report.scoreDiagnostics
2118
+ };
2119
+ }
2120
+ return lowestScoredReport;
2121
+ };
2122
+ const resolveEmptyStateMessage = (input, demotedDiagnosticCount) => buildEmptyReportMessage({
2123
+ categoryFilters: input.options?.categoryFilters ?? [],
2124
+ demotedDiagnosticCount,
2125
+ outputSurface: input.options?.outputSurface ?? "cli"
2126
+ });
2127
+ const printExitFooter = async (input) => {
2128
+ process.stdout.write(`${highlighter.success("✔")} Scanned ${pluralize(input.scannedFileCount, "file")} in ${formatElapsedTime(input.elapsedMilliseconds)}\n`);
2129
+ if (input.lintFailureReason !== null) process.stdout.write(`${highlighter.warn("⚠")} Lint did not run: ${input.lintFailureReason}\n`);
2130
+ await runPromise(printFooter({
2131
+ diagnostics: [...input.diagnostics],
2132
+ scoreResult: input.scoreResult,
2133
+ projectName: input.projectName,
2134
+ isOffline: input.isOffline
2135
+ }));
2136
+ };
2137
+ const performTuiHandoff = async (request, rootDirectory) => {
2138
+ let failureMessage = null;
2139
+ if (request.destination === "clipboard") {
2140
+ const didCopy = await copyToClipboard(request.prompt);
2141
+ recordCount(METRIC.agentHandoff, 1, {
2142
+ outcome: "clipboard",
2143
+ source: "tui",
2144
+ copied: didCopy
2145
+ });
2146
+ if (didCopy) {
2147
+ process.stdout.write(`${highlighter.success("✔")} Copied the prompt to your clipboard.\n`);
2148
+ return;
2149
+ }
2150
+ failureMessage = "Couldn't access the clipboard. Here's the prompt instead:";
2151
+ } else try {
2152
+ await launchCliAgent(request.destination, request.prompt, rootDirectory);
2153
+ return;
2154
+ } catch {
2155
+ failureMessage = `Couldn't launch ${CLI_AGENT_BINARIES[request.destination]}. Here's the prompt instead:`;
2156
+ }
2157
+ process.stdout.write(`${highlighter.warn("⚠")} ${failureMessage}\n`);
2158
+ process.stdout.write(`${highlighter.dim("──── Agent prompt ────")}\n`);
2159
+ process.stdout.write(`${request.prompt}\n`);
2160
+ process.stdout.write(`${highlighter.dim("──────────────────────")}\n`);
2161
+ };
2162
+ const isCiUnconfigured = (directory) => !isReactDoctorWorkflowInstalled(findNearestPackageDirectory(directory) ?? directory);
2163
+ const performCiSetup = async (rootDirectory) => {
2164
+ const didCreateWorkflow = await setUpGitHubActions({ rootDirectory });
2165
+ recordCount(METRIC.agentHandoff, 1, {
2166
+ outcome: "ci-yes",
2167
+ source: "tui",
2168
+ created: didCreateWorkflow
2169
+ });
2170
+ };
2171
+ const mountScanApp = async (rootDirectory, shouldRecommendCi, initialProgress) => {
2172
+ const store = createScanStore();
2173
+ store.setProgress(initialProgress);
2174
+ const launchableAgents = await detectLaunchableAgents();
2175
+ const pendingActions = {
2176
+ handoffRequest: null,
2177
+ shouldSetUpCi: false,
2178
+ didQuit: false
2179
+ };
2180
+ const mountRenderer = (displayMode) => {
2181
+ const instance = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScanApp, {
2182
+ store,
2183
+ displayMode,
2184
+ launchableAgents,
2185
+ onHandoff: (request) => {
2186
+ pendingActions.handoffRequest = request;
2187
+ },
2188
+ canAddToCi: shouldRecommendCi,
2189
+ onAddToCi: () => {
2190
+ pendingActions.shouldSetUpCi = true;
2191
+ },
2192
+ onQuit: () => {
2193
+ pendingActions.didQuit = true;
2194
+ }
2195
+ }), {
2196
+ alternateScreen: false,
2197
+ exitOnCtrlC: false
2198
+ });
2199
+ return {
2200
+ instance,
2201
+ dispose: registerMountedTuiRenderer(instance)
2202
+ };
2203
+ };
2204
+ const executePendingActions = async () => {
2205
+ if (pendingActions.shouldSetUpCi) await performCiSetup(rootDirectory);
2206
+ if (pendingActions.didQuit) return;
2207
+ if (pendingActions.handoffRequest) await performTuiHandoff(pendingActions.handoffRequest, rootDirectory);
2208
+ };
2209
+ return {
2210
+ store,
2211
+ pendingActions,
2212
+ mountRenderer,
2213
+ executePendingActions
2214
+ };
2215
+ };
2216
+ const runMountedScan = async (rootDirectory, presentation, blockingLevel, executeScan) => {
2217
+ const { store, pendingActions, mountRenderer, executePendingActions } = await mountScanApp(rootDirectory, presentation.shouldRecommendCi, presentation.initialProgress);
2218
+ let mountedRenderer = mountRenderer("scan");
2219
+ recordCount(METRIC.tuiScanInlineShown);
2220
+ const context = {
2221
+ store,
2222
+ ...presentation
2223
+ };
2224
+ try {
2225
+ const completedScan = await executeScan(context);
2226
+ mountedRenderer.dispose();
2227
+ mountedRenderer = mountRenderer("report");
2228
+ if (presentation.shouldRecommendCi) recordCount(METRIC.tuiCiRecommendationShown);
2229
+ await mountedRenderer.instance.waitUntilExit();
2230
+ mountedRenderer.dispose();
2231
+ if (presentation.outputDirectory !== void 0 || presentation.verbose) await runPromise(printDiagnosticsDump([...completedScan.diagnostics], presentation.outputDirectory, presentation.verbose));
2232
+ if (!pendingActions.didQuit) await printExitFooter({
2233
+ diagnostics: completedScan.diagnostics,
2234
+ scoreResult: completedScan.scoreResult,
2235
+ projectName: completedScan.projectName,
2236
+ scannedFileCount: completedScan.scannedFileCount,
2237
+ elapsedMilliseconds: completedScan.elapsedMilliseconds,
2238
+ isOffline: context.isOffline,
2239
+ lintFailureReason: resolveLintFailureReason(completedScan.scans.map(({ result }) => result))
2240
+ });
2241
+ await executePendingActions();
2242
+ return { shouldFail: shouldFailScanGate({
2243
+ scans: completedScan.scans,
2244
+ blockingLevel,
2245
+ diagnosticsAreGateExempt: completedScan.diagnosticsAreGateExempt
2246
+ }) };
2247
+ } catch (error) {
2248
+ mountedRenderer.dispose();
2249
+ throw error;
2250
+ }
2251
+ };
2252
+ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, scopePlan, blockingLevel, inspectProject) => {
2253
+ const projectScan = await resolveProjectScan(rootScanTarget, projectDirectory);
2254
+ const isSupplyChainEnabled = input.options?.supplyChain ?? projectScan.config?.supplyChain?.enabled ?? true;
2255
+ const scopeOptions = resolveProjectTuiScanScope({
2256
+ plan: scopePlan,
2257
+ projectDirectory: projectScan.directory,
2258
+ rootDirectory: rootScanTarget.resolvedDirectory,
2259
+ supplyChainEnabled: isSupplyChainEnabled
2260
+ });
2261
+ if (scopeOptions === null) {
2262
+ process.stdout.write("No changed source files in the selected project.\n");
2263
+ return { shouldFail: false };
2264
+ }
2265
+ const presentation = resolveScanPresentation(input, [projectScan], rootScanTarget.resolvedDirectory);
2266
+ return runMountedScan(projectScan.directory, presentation, blockingLevel, async (context) => {
2267
+ const result = await inspectProject(projectScan.directory, {
2268
+ ...resolveTuiInspectOptions(input, projectScan.config),
2269
+ ...scopeOptions,
2270
+ isCi: isCiEnvironment(),
2271
+ configOverride: projectScan.config,
2272
+ configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
2273
+ uiLayers: {
2274
+ reporter: reporterLayerForStore(context.store),
2275
+ progress: progressLayerForStore(context.store)
2276
+ }
2277
+ });
2278
+ const reportSelection = selectReportDiagnostics({
2279
+ scan: {
2280
+ result,
2281
+ config: projectScan.config
2282
+ },
2283
+ categoryFilters: input.options?.categoryFilters,
2284
+ surface: input.options?.outputSurface
2285
+ });
2286
+ const scoreDiagnostics = filterScansForSurface([{
2287
+ result,
2288
+ config: projectScan.config
2289
+ }], "score");
2290
+ const projectedScore = result.score ? await computeProjectedScore([...reportSelection.diagnostics], scoreDiagnostics, result.score) : null;
2291
+ context.store.setReport(toScanReport({
2292
+ result,
2293
+ diagnostics: reportSelection.diagnostics,
2294
+ rootDirectory: projectScan.directory,
2295
+ projectedScore,
2296
+ isOffline: context.isOffline,
2297
+ noScoreMessage: buildNoScoreMessage({
2298
+ isScoreDisabled: input.options?.noScore === true || projectScan.config?.noScore === true,
2299
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2300
+ disabledMessage: input.options?.scoreDisabledMessage
2301
+ }),
2302
+ emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2303
+ }));
2304
+ return {
2305
+ scans: [{
2306
+ result,
2307
+ config: projectScan.config
2308
+ }],
2309
+ diagnostics: reportSelection.diagnostics,
2310
+ diagnosticsAreGateExempt: scopePlan.baselineIntended && result.baselineDelta === void 0,
2311
+ scoreResult: result.score,
2312
+ projectName: result.project.projectName,
2313
+ scannedFileCount: result.scannedFileCount ?? 0,
2314
+ elapsedMilliseconds: result.elapsedMilliseconds
2315
+ };
2316
+ });
2317
+ };
2318
+ const runMultiProjectScan = async (rootScanTarget, directories, input, scopePlan, blockingLevel, inspectProject) => {
2319
+ const feedbackStartTime = performance.now();
2320
+ const rootDirectory = rootScanTarget.resolvedDirectory;
2321
+ const discoveredProjectScans = deduplicateProjectScans(await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory)));
2322
+ const projectScans = discoveredProjectScans.flatMap((projectScan) => {
2323
+ const isSupplyChainEnabled = input.options?.supplyChain ?? projectScan.config?.supplyChain?.enabled ?? true;
2324
+ const scopeOptions = resolveProjectTuiScanScope({
2325
+ plan: scopePlan,
2326
+ projectDirectory: projectScan.directory,
2327
+ rootDirectory,
2328
+ supplyChainEnabled: isSupplyChainEnabled
2329
+ });
2330
+ return scopeOptions === null ? [] : [{
2331
+ projectScan,
2332
+ scopeOptions
2333
+ }];
2334
+ });
2335
+ if (projectScans.length === 0) {
2336
+ process.stdout.write("No changed source files in the selected projects.\n");
2337
+ return { shouldFail: false };
2338
+ }
2339
+ const projectCount = projectScans.length;
2340
+ const rootProjectScan = discoveredProjectScans.find((projectScan) => path.resolve(projectScan.directory) === path.resolve(rootDirectory));
2341
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
2342
+ rootDirectory,
2343
+ projectDirectories: discoveredProjectScans.map((projectScan) => projectScan.directory),
2344
+ isRootDeadCodeEnabled: input.options?.deadCode ?? rootProjectScan?.config?.deadCode ?? true
2345
+ });
2346
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceMaintainabilityShared, 1, { projectCount: discoveredProjectScans.length });
2347
+ return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans.map(({ projectScan }) => projectScan), rootDirectory), blockingLevel, async (context) => {
2348
+ const startTime = performance.now();
2349
+ let finishedCount = 0;
2350
+ recordDistribution(METRIC.scanFeedbackDelay, performance.now() - feedbackStartTime, {
2351
+ unit: "millisecond",
2352
+ attributes: {
2353
+ surface: "tui",
2354
+ projectCount
2355
+ }
2356
+ });
2357
+ context.store.setProgress(`Scanning ${projectCount} projects…`);
2358
+ await yieldToEventLoop();
2359
+ const precomputedSourceFileCounts = scopePlan.scope === "full" ? await collectProjectSourceFileCounts(rootDirectory, projectScans.map(({ projectScan }) => projectScan.directory)) : null;
2360
+ const { completedScans, skippedScans } = partitionProjectScanOutcomes(await mapWithConcurrency(projectScans, 4, async ({ projectScan, scopeOptions }) => {
2361
+ if (input.options?.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.options.deadlineEpochMs) === 0) {
2362
+ finishedCount += 1;
2363
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2364
+ return {
2365
+ status: "skipped",
2366
+ value: {
2367
+ directory: projectScan.directory,
2368
+ reason: "max-duration"
2369
+ }
2370
+ };
2371
+ }
2372
+ const projectLabel = path.relative(rootDirectory, projectScan.directory) || path.basename(rootDirectory);
2373
+ const formatProjectProgress = (displayText) => `Scanning ${projectCount} projects… (${finishedCount}/${projectCount}) · ${projectLabel}: ${displayText}`;
2374
+ const inspectOptions = resolveTuiInspectOptions(input, projectScan.config);
2375
+ const ownsWorkspaceDeadCode = projectScan.directory === workspaceDeadCodeOwner;
2376
+ const result = await inspectProject(projectScan.directory, {
2377
+ ...inspectOptions,
2378
+ ...scopeOptions,
2379
+ deadCode: workspaceDeadCodeOwner === null ? inspectOptions.deadCode : ownsWorkspaceDeadCode,
2380
+ isCi: isCiEnvironment(),
2381
+ configOverride: projectScan.config,
2382
+ configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
2383
+ precomputedSourceFileCount: precomputedSourceFileCounts?.get(projectScan.directory),
2384
+ uiLayers: {
2385
+ reporter: Reporter.layerNoop,
2386
+ progress: progressLayerForStore(context.store, {
2387
+ transformText: formatProjectProgress,
2388
+ shouldClearOnStop: false
2389
+ })
2390
+ },
2391
+ concurrentScan: true,
2392
+ excludedProjectDirectories: discoveredProjectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, projectScan.directory)).map((candidateProjectScan) => candidateProjectScan.directory),
2393
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode
2394
+ });
2395
+ finishedCount += 1;
2396
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2397
+ await yieldToEventLoop();
2398
+ return {
2399
+ status: "completed",
2400
+ value: {
2401
+ directory: projectScan.directory,
2402
+ result,
2403
+ config: projectScan.config
2404
+ }
2405
+ };
2406
+ }));
2407
+ const results = await retryMissingProjectScores(completedScans.map((completedScan) => ({
2408
+ ...completedScan,
2409
+ isScoreDisabled: input.options?.noScore ?? completedScan.config?.noScore ?? false
2410
+ })));
2411
+ const skippedProjects = skippedScans.sort((left, right) => left.directory.localeCompare(right.directory));
2412
+ if (skippedProjects.length > 0) recordCount(METRIC.scanProjectSkipped, skippedProjects.length, { reason: "max-duration" });
2413
+ const projectEntries = results.map(({ directory, result, config }) => {
2414
+ const reportSelection = selectReportDiagnostics({
2415
+ scan: {
2416
+ result,
2417
+ config
2418
+ },
2419
+ categoryFilters: input.options?.categoryFilters,
2420
+ surface: input.options?.outputSurface
2421
+ });
2422
+ return {
2423
+ report: toScanReport({
2424
+ result,
2425
+ diagnostics: reportSelection.diagnostics,
2426
+ rootDirectory: directory,
2427
+ projectedScore: null,
2428
+ isOffline: context.isOffline,
2429
+ noScoreMessage: buildNoScoreMessage({
2430
+ isScoreDisabled: input.options?.noScore === true || config?.noScore === true,
2431
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2432
+ disabledMessage: input.options?.scoreDisabledMessage
2433
+ }),
2434
+ emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2435
+ }),
2436
+ score: result.score,
2437
+ scoreDiagnostics: filterScansForSurface([{
2438
+ result,
2439
+ config
2440
+ }], "score"),
2441
+ demotedDiagnosticCount: reportSelection.demotedDiagnosticCount
2442
+ };
2443
+ });
2444
+ const projects = projectEntries.map(({ report }) => report);
2445
+ const combinedDiagnostics = projects.flatMap((project) => qualifyDiagnosticPaths(project.diagnostics, rootDirectory, project.rootDirectory));
2446
+ const lowestScoredReport = findLowestScoredReport(projectEntries);
2447
+ const projectedScore = lowestScoredReport ? await computeProjectedScore(combinedDiagnostics, [...lowestScoredReport.scoreDiagnostics], lowestScoredReport.score) : null;
2448
+ const scannedFileCount = countUniqueScannedFiles(results.map(({ result }) => result));
2449
+ const elapsedMilliseconds = performance.now() - startTime;
2450
+ const lintFailureReason = resolveLintFailureReason(results.map(({ result }) => result));
2451
+ const summary = {
2452
+ projects,
2453
+ skippedProjects,
2454
+ aggregateScore: lowestScoredReport?.score ?? null,
2455
+ projectedScore,
2456
+ combinedDiagnostics,
2457
+ scannedFileCount,
2458
+ elapsedMilliseconds,
2459
+ projectName: path.basename(rootDirectory),
2460
+ rootDirectory,
2461
+ isOffline: context.isOffline,
2462
+ noScoreMessage: buildNoScoreMessage({
2463
+ isScoreDisabled: input.options?.noScore === true || results.some(({ config }) => config?.noScore === true),
2464
+ isAnalysisIncomplete: results.some(({ result }) => hasIncompleteScoreAnalysis(result.skippedChecks)),
2465
+ disabledMessage: input.options?.scoreDisabledMessage
2466
+ }),
2467
+ emptyStateMessage: resolveEmptyStateMessage(input, projectEntries.reduce((total, projectEntry) => total + projectEntry.demotedDiagnosticCount, 0)),
2468
+ ...lintFailureReason ? { lintFailureReason } : {}
2469
+ };
2470
+ context.store.setSummary(summary);
2471
+ return {
2472
+ scans: results,
2473
+ diagnostics: combinedDiagnostics,
2474
+ diagnosticsAreGateExempt: scopePlan.baselineIntended && (skippedProjects.length > 0 || results.length === 0 || results.some(({ result }) => result.baselineDelta === void 0)),
2475
+ scoreResult: summary.aggregateScore,
2476
+ projectName: summary.projectName,
2477
+ scannedFileCount,
2478
+ elapsedMilliseconds
2479
+ };
2480
+ });
2481
+ };
2482
+ const runScanApp = async (input) => {
2483
+ const scanTarget = input.scanTarget ?? await resolveScanTarget(input.directory, { allowAmbiguous: true });
2484
+ const rootDirectory = scanTarget.resolvedDirectory;
2485
+ const scopePlan = await resolveTuiScanScope({
2486
+ directory: rootDirectory,
2487
+ flags: input.flags ?? {},
2488
+ userConfig: scanTarget.userConfig
2489
+ });
2490
+ const deadlineEpochMs = input.options?.deadlineEpochMs ?? (input.options?.maxDurationMs != null ? Date.now() + input.options.maxDurationMs : void 0);
2491
+ const resolvedInput = {
2492
+ ...input,
2493
+ options: {
2494
+ ...input.options,
2495
+ deadlineEpochMs
2496
+ },
2497
+ configProjects: input.configProjects ?? scanTarget.userConfig?.projects,
2498
+ share: input.share ?? scanTarget.userConfig?.share ?? true
2499
+ };
2500
+ const selectedDirectories = await resolveSelectedDirectories(rootDirectory, resolvedInput);
2501
+ const inspectProject = createInvocationInspect(input.options?.concurrency);
2502
+ const blockingLevel = resolveBlockingLevel({ blocking: resolvedInput.blocking }, scanTarget.userConfig);
2503
+ if (selectedDirectories.length === 0) return { shouldFail: false };
2504
+ if (selectedDirectories.length === 1) return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, scopePlan, blockingLevel, inspectProject);
2505
+ return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, scopePlan, blockingLevel, inspectProject);
2506
+ };
2507
+ //#endregion
2508
+ export { runScanApp };
2509
+
2510
+ //# sourceMappingURL=run-scan-app-BtY0jXpd.js.map
2511
+ //# debugId=043306a4-5f17-588e-9ac0-086eff86ab98