react-doctor 0.9.12-dev.a2b460d → 0.9.12-dev.ac87f7d

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