react-doctor 0.9.12-dev.28a1a9f → 0.9.12-dev.3eda608

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