react-doctor 0.9.4 → 0.9.5

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