react-doctor 0.9.2 → 0.9.3-dev.02e3188

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