react-doctor 0.9.2 → 0.9.3-dev.02e3188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1661 +0,0 @@
1
-
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d3f7c231-0bd2-5808-8ce4-be477335e722")}catch(e){}}();
3
- import { A as toForwardSlashes, B as highlighter, C as buildRulePriorityMap, D as resolveBlockingLevel, E as formatLearnMoreLine, F as isCiEnvironment, G as sync, H as mergeReactDoctorConfigs, I as DIAGNOSTIC_CATEGORY_BUCKETS, K as void_, L as Progress, M as isUnicodeSupported, N as METRIC, O as isReactDoctorWorkflowInstalled, P as TUI_REPORT_DETAIL_WIDTH_FRACTION, R as Reporter, S as hasLintHardFailure, T as formatFixRecipeLine, U as resolveScanTarget, V as mapWithConcurrency, W as runPromise, _ as canAnimateOnboarding, a as resolveCliInspectOptions, b as buildCodeFrame, c as detectLaunchableAgents, d as launchCliAgent, f as isShareOptedOut, g as doctorFace, h as easeOutCubic, i as selectProjects, j as findNearestPackageDirectory, k as recordCount, l as CLI_AGENT_BINARIES, m as printFooter, n as shouldFailScanGate, o as countUniqueScannedFiles, p as inspect, q as succeed, r as discoverWorkspacePackages, s as setUpGitHubActions, t as exitGracefully, u as copyToClipboard, v as computeProjectedScore, w as buildSortedRuleGroups, x as buildNoScoreMessage, y as formatElapsedTime, z as getCategoryImpact } from "./cli.js";
4
- import path from "node:path";
5
- import { performance } from "node:perf_hooks";
6
- import { getSkillAgentConfig } from "agent-install";
7
- import { Box, Text, render, useApp, useInput, useStdout } from "ink";
8
- import figures from "figures";
9
- import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
10
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
- import Spinner from "ink-spinner";
12
- //#region src/cli/ink/hooks/use-exit-on-ctrl-c.ts
13
- const SHOW_CURSOR = "\x1B[?25h";
14
- /**
15
- * Force-quits the whole CLI on Ctrl-C from any phase. Ink's built-in
16
- * `exitOnCtrlC` only unmounts the render — during a scan the in-flight
17
- * `inspect()` promise keeps the process alive, so Ctrl-C appears to do nothing.
18
- * Mounting this at the app root makes Ctrl-C always terminate: it restores the
19
- * terminal and then uses the CLI's shared SIGINT exit path so the in-flight
20
- * scan can't outlive the keystroke.
21
- */
22
- const useExitOnCtrlC = () => {
23
- const { exit } = useApp();
24
- useInput((input, key) => {
25
- if (key.ctrl && input === "c") {
26
- exit();
27
- process.stdin.setRawMode?.(false);
28
- process.stdout.write(SHOW_CURSOR);
29
- exitGracefully();
30
- }
31
- });
32
- };
33
- //#endregion
34
- //#region src/cli/ink/hooks/use-stdout-dimensions.ts
35
- const readDimensions = (stdout) => ({
36
- columns: stdout?.columns ?? 80,
37
- rows: stdout?.rows ?? 24
38
- });
39
- const useStdoutDimensions = () => {
40
- const { stdout } = useStdout();
41
- const [dimensions, setDimensions] = useState(() => readDimensions(stdout));
42
- useEffect(() => {
43
- if (!stdout) return;
44
- const onResize = () => setDimensions(readDimensions(stdout));
45
- stdout.on("resize", onResize);
46
- return () => {
47
- stdout.off("resize", onResize);
48
- };
49
- }, [stdout]);
50
- return dimensions;
51
- };
52
- //#endregion
53
- //#region src/cli/ink/lib/fuzzy-match.ts
54
- const isWordBoundaryBefore = (target, index) => {
55
- if (index === 0) return true;
56
- const previous = target[index - 1];
57
- return previous === "-" || previous === "_" || previous === "/" || previous === " ";
58
- };
59
- const fuzzyMatch = (query, target) => {
60
- if (query.length === 0) return {
61
- score: 0,
62
- matchedIndices: []
63
- };
64
- const lowerQuery = query.toLowerCase();
65
- const lowerTarget = target.toLowerCase();
66
- const matchedIndices = [];
67
- let score = 0;
68
- let queryIndex = 0;
69
- let previousMatchIndex = Number.NEGATIVE_INFINITY;
70
- for (let targetIndex = 0; targetIndex < lowerTarget.length && queryIndex < lowerQuery.length; targetIndex++) {
71
- if (lowerTarget[targetIndex] !== lowerQuery[queryIndex]) continue;
72
- matchedIndices.push(targetIndex);
73
- if (targetIndex === previousMatchIndex + 1) score += 5;
74
- if (isWordBoundaryBefore(target, targetIndex)) score += 10;
75
- previousMatchIndex = targetIndex;
76
- queryIndex++;
77
- }
78
- if (queryIndex < lowerQuery.length) return null;
79
- score -= matchedIndices[0] * 1;
80
- return {
81
- score,
82
- matchedIndices
83
- };
84
- };
85
- //#endregion
86
- //#region src/cli/ink/components/project-select.tsx
87
- const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
88
- const isPrintable = (input) => input.length > 0 && [...input].every((character) => character.charCodeAt(0) >= 32);
89
- const MatchedName = ({ name, matchedIndices, isSelected }) => {
90
- if (matchedIndices.length === 0) return /* @__PURE__ */ jsx(Text, {
91
- bold: isSelected,
92
- wrap: "truncate-end",
93
- children: name
94
- });
95
- const matched = new Set(matchedIndices);
96
- return /* @__PURE__ */ jsx(Text, {
97
- bold: isSelected,
98
- wrap: "truncate-end",
99
- children: [...name].map((char, index) => matched.has(index) ? /* @__PURE__ */ jsx(Text, {
100
- color: "yellow",
101
- children: char
102
- }, index) : char)
103
- });
104
- };
105
- const ProjectSelect = ({ packages, rootDirectory, onSubmit }) => {
106
- const { rows: terminalRows } = useStdoutDimensions();
107
- useExitOnCtrlC();
108
- const [mode, setMode] = useState("list");
109
- const [query, setQuery] = useState("");
110
- const [selectedIndex, setSelectedIndex] = useState(0);
111
- const [offset, setOffset] = useState(0);
112
- const [checked, setChecked] = useState(() => /* @__PURE__ */ new Set());
113
- const matches = useMemo(() => {
114
- const scored = packages.flatMap((workspacePackage) => {
115
- const result = fuzzyMatch(query, workspacePackage.name);
116
- return result ? [{
117
- workspacePackage,
118
- result
119
- }] : [];
120
- });
121
- if (query.length > 0) scored.sort((a, b) => b.result.score - a.result.score);
122
- return scored.map(({ workspacePackage, result }) => ({
123
- workspacePackage,
124
- matchedIndices: result.matchedIndices
125
- }));
126
- }, [packages, query]);
127
- const isSearching = mode === "search";
128
- const hasFilterLine = isSearching || query.length > 0;
129
- const listHeight = Math.max(1, Math.min(Math.max(matches.length, 1), terminalRows - 3 - (hasFilterLine ? 1 : 0)));
130
- const boundedSelected = matches.length === 0 ? 0 : clamp(selectedIndex, 0, matches.length - 1);
131
- const current = matches[boundedSelected]?.workspacePackage;
132
- const setFilter = (next) => {
133
- setQuery(next);
134
- setSelectedIndex(0);
135
- setOffset(0);
136
- };
137
- const move = (delta) => {
138
- if (matches.length === 0) return;
139
- const next = clamp(boundedSelected + delta, 0, matches.length - 1);
140
- setSelectedIndex(next);
141
- setOffset((current) => {
142
- if (next < current) return next;
143
- if (next >= current + listHeight) return next - listHeight + 1;
144
- return current;
145
- });
146
- };
147
- const toggleChecked = (directory) => {
148
- setChecked((current) => {
149
- const next = new Set(current);
150
- if (next.has(directory)) next.delete(directory);
151
- else next.add(directory);
152
- return next;
153
- });
154
- };
155
- const submit = (directories) => {
156
- onSubmit([...directories]);
157
- };
158
- const scanSelection = () => {
159
- if (checked.size > 0) {
160
- const selectedDirectories = [];
161
- for (const workspacePackage of packages) if (checked.has(workspacePackage.directory)) selectedDirectories.push(workspacePackage.directory);
162
- return submit(selectedDirectories);
163
- }
164
- if (current) submit([current.directory]);
165
- };
166
- useInput((input, key) => {
167
- if (isSearching) {
168
- if (key.return) return setMode("list");
169
- if (key.escape) {
170
- setFilter("");
171
- return setMode("list");
172
- }
173
- if (key.downArrow || key.ctrl && input === "n") return move(1);
174
- if (key.upArrow || key.ctrl && input === "p") return move(-1);
175
- if (key.backspace || key.delete) return setFilter(query.slice(0, -1));
176
- if (isPrintable(input) && !key.ctrl && !key.meta) setFilter(query + input);
177
- return;
178
- }
179
- if (input === "/") return setMode("search");
180
- if (input === " ") {
181
- if (current) toggleChecked(current.directory);
182
- return;
183
- }
184
- if (input === "a") {
185
- if (matches.length === 0) return;
186
- setChecked((current) => {
187
- const next = new Set(current);
188
- const shouldClearMatches = matches.every((match) => current.has(match.workspacePackage.directory));
189
- for (const match of matches) if (shouldClearMatches) next.delete(match.workspacePackage.directory);
190
- else next.add(match.workspacePackage.directory);
191
- return next;
192
- });
193
- return;
194
- }
195
- if (input === "q") return submit([]);
196
- if (key.escape) {
197
- if (query.length > 0) return setFilter("");
198
- if (checked.size > 0) return setChecked(/* @__PURE__ */ new Set());
199
- return submit([]);
200
- }
201
- if (key.return) return scanSelection();
202
- if (key.downArrow || input === "j") return move(1);
203
- if (key.upArrow || input === "k") return move(-1);
204
- if (key.pageDown) return move(listHeight);
205
- if (key.pageUp) return move(-listHeight);
206
- });
207
- const maxOffset = Math.max(0, matches.length - listHeight);
208
- const visibleStart = Math.min(offset, maxOffset);
209
- const visibleMatches = matches.slice(visibleStart, visibleStart + listHeight);
210
- const longestNameLength = Math.max(0, ...packages.map((workspacePackage) => workspacePackage.name.length));
211
- return /* @__PURE__ */ jsxs(Box, {
212
- flexDirection: "column",
213
- children: [
214
- /* @__PURE__ */ jsxs(Text, {
215
- wrap: "truncate-end",
216
- children: [/* @__PURE__ */ jsx(Text, {
217
- bold: true,
218
- children: "Select projects to scan"
219
- }), /* @__PURE__ */ jsxs(Text, {
220
- dimColor: true,
221
- children: [
222
- " ",
223
- checked.size,
224
- "/",
225
- packages.length
226
- ]
227
- })]
228
- }),
229
- isSearching ? /* @__PURE__ */ jsxs(Text, {
230
- wrap: "truncate-end",
231
- children: [
232
- /* @__PURE__ */ jsx(Text, {
233
- color: "cyan",
234
- children: "/ "
235
- }),
236
- query.length > 0 ? /* @__PURE__ */ jsx(Text, { children: query }) : null,
237
- /* @__PURE__ */ jsx(Text, {
238
- inverse: true,
239
- children: " "
240
- })
241
- ]
242
- }) : query.length > 0 ? /* @__PURE__ */ jsx(Text, {
243
- dimColor: true,
244
- wrap: "truncate-end",
245
- children: `filter: ${query}`
246
- }) : null,
247
- /* @__PURE__ */ jsx(Box, {
248
- flexDirection: "column",
249
- height: listHeight,
250
- children: matches.length === 0 ? /* @__PURE__ */ jsx(Text, {
251
- dimColor: true,
252
- children: "No matching projects"
253
- }) : visibleMatches.map((match, index) => {
254
- const isSelected = visibleStart + index === boundedSelected;
255
- const isChecked = checked.has(match.workspacePackage.directory);
256
- return /* @__PURE__ */ jsxs(Text, {
257
- wrap: "truncate-end",
258
- children: [
259
- /* @__PURE__ */ jsx(Text, {
260
- color: isSelected ? "cyan" : void 0,
261
- children: isSelected ? `${figures.pointer} ` : " "
262
- }),
263
- /* @__PURE__ */ jsx(Text, {
264
- color: isChecked ? "green" : void 0,
265
- children: isChecked ? `${figures.radioOn} ` : `${figures.radioOff} `
266
- }),
267
- /* @__PURE__ */ jsx(MatchedName, {
268
- name: match.workspacePackage.name,
269
- matchedIndices: match.matchedIndices,
270
- isSelected
271
- }),
272
- /* @__PURE__ */ jsxs(Text, {
273
- dimColor: true,
274
- children: [" ".repeat(longestNameLength - match.workspacePackage.name.length + 2), path.relative(rootDirectory, match.workspacePackage.directory) || "."]
275
- })
276
- ]
277
- }, match.workspacePackage.directory);
278
- })
279
- }),
280
- /* @__PURE__ */ jsx(Box, {
281
- marginTop: 1,
282
- children: /* @__PURE__ */ jsxs(Text, {
283
- dimColor: true,
284
- wrap: "truncate-end",
285
- children: [
286
- isSearching ? "type to filter · enter confirm · esc clear" : "space select · a all · / search · ",
287
- isSearching ? null : /* @__PURE__ */ jsx(Text, {
288
- color: "cyan",
289
- children: "enter"
290
- }),
291
- isSearching ? null : " to submit · q cancel"
292
- ]
293
- })
294
- })
295
- ]
296
- });
297
- };
298
- //#endregion
299
- //#region src/cli/utils/format-diagnostic-site.ts
300
- const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
301
- //#endregion
302
- //#region src/cli/ink/lib/diagnostic-rows.ts
303
- const pickRepresentative = (diagnostics) => diagnostics.find((diagnostic) => diagnostic.line > 0) ?? diagnostics[0];
304
- const buildDiagnosticRows = (diagnostics, scores) => {
305
- return buildSortedRuleGroups(diagnostics, buildRulePriorityMap(scores)).map(([ruleKey, ruleDiagnostics]) => {
306
- const representative = pickRepresentative(ruleDiagnostics);
307
- return {
308
- ruleKey,
309
- diagnostics: ruleDiagnostics,
310
- severity: representative.severity === "error" ? "error" : "warning",
311
- category: representative.category,
312
- title: representative.title ?? ruleKey,
313
- location: formatDiagnosticSite(representative),
314
- siteCount: ruleDiagnostics.length,
315
- representative,
316
- learnMore: formatLearnMoreLine(representative)
317
- };
318
- });
319
- };
320
- //#endregion
321
- //#region src/cli/ink/hooks/use-scroll-viewport.ts
322
- const useScrollViewport = (options) => {
323
- const { itemCount, height, isActive = true, isSelectable } = options;
324
- const canSelect = (index) => index >= 0 && index < itemCount && (isSelectable ? isSelectable(index) : true);
325
- const seekSelectable = (start, step) => {
326
- for (let index = start; index >= 0 && index < itemCount; index += step) if (canSelect(index)) return index;
327
- return -1;
328
- };
329
- const nearestSelectable = (target, step) => {
330
- const ahead = seekSelectable(target, step);
331
- if (ahead !== -1) return ahead;
332
- const behind = seekSelectable(target, -step);
333
- return behind === -1 ? target : behind;
334
- };
335
- const [selectedIndex, setSelectedIndex] = useState(() => {
336
- const first = seekSelectable(0, 1);
337
- return first === -1 ? 0 : first;
338
- });
339
- const [offset, setOffset] = useState(0);
340
- const awaitingSecondG = useRef(false);
341
- const clampIndex = (index) => Math.max(0, Math.min(itemCount - 1, index));
342
- const moveTo = (rawIndex, step) => {
343
- const next = nearestSelectable(clampIndex(rawIndex), step);
344
- setSelectedIndex(next);
345
- setOffset((current) => {
346
- if (next < current) return next;
347
- if (next >= current + height) return next - height + 1;
348
- return current;
349
- });
350
- };
351
- useInput((input, key) => {
352
- if (itemCount === 0) return;
353
- const isSecondG = awaitingSecondG.current && input === "g";
354
- if (input !== "g") awaitingSecondG.current = false;
355
- if (key.downArrow || input === "j") return moveTo(selectedIndex + 1, 1);
356
- if (key.upArrow || input === "k") return moveTo(selectedIndex - 1, -1);
357
- if (key.pageDown) return moveTo(selectedIndex + height, 1);
358
- if (key.pageUp) return moveTo(selectedIndex - height, -1);
359
- if (key.ctrl && input === "d") return moveTo(selectedIndex + Math.floor(height / 2), 1);
360
- if (key.ctrl && input === "u") return moveTo(selectedIndex - Math.floor(height / 2), -1);
361
- if (input === "G") return moveTo(itemCount - 1, -1);
362
- if (isSecondG) {
363
- awaitingSecondG.current = false;
364
- return moveTo(0, 1);
365
- }
366
- if (input === "g") awaitingSecondG.current = true;
367
- }, { isActive });
368
- const maxOffset = Math.max(0, itemCount - height);
369
- const visibleStart = Math.min(offset, maxOffset);
370
- return {
371
- selectedIndex: canSelect(selectedIndex) ? selectedIndex : nearestSelectable(clampIndex(selectedIndex), 1),
372
- visibleStart,
373
- visibleEnd: Math.min(itemCount, visibleStart + height)
374
- };
375
- };
376
- //#endregion
377
- //#region src/cli/ink/lib/diagnostic-list-entries.ts
378
- const CATEGORY_RANK = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category, index) => [category, index]));
379
- const rankOf = (category) => CATEGORY_RANK.get(category) ?? Number.MAX_SAFE_INTEGER;
380
- const buildDiagnosticListEntries = (rows) => {
381
- const rowsByCategory = /* @__PURE__ */ new Map();
382
- for (const row of rows) {
383
- const categoryRows = rowsByCategory.get(row.category) ?? [];
384
- categoryRows.push(row);
385
- rowsByCategory.set(row.category, categoryRows);
386
- }
387
- const orderedCategories = [...rowsByCategory.keys()].sort((categoryA, categoryB) => {
388
- const rankDelta = rankOf(categoryA) - rankOf(categoryB);
389
- return rankDelta !== 0 ? rankDelta : categoryA.localeCompare(categoryB);
390
- });
391
- const entries = [];
392
- for (const category of orderedCategories) {
393
- const categoryRows = rowsByCategory.get(category) ?? [];
394
- entries.push({
395
- kind: "header",
396
- category
397
- });
398
- for (const row of categoryRows) entries.push({
399
- kind: "item",
400
- row
401
- });
402
- }
403
- return entries;
404
- };
405
- //#endregion
406
- //#region src/cli/ink/lib/build-issue-prompt.ts
407
- const buildIssuePrompt = ({ row, projectName }) => {
408
- const { representative } = row;
409
- const severityLabel = row.severity === "error" ? "ERROR" : "WARN";
410
- const uniqueSites = [...new Set(row.diagnostics.map(formatDiagnosticSite))];
411
- const inlineSites = uniqueSites.slice(0, 8);
412
- const remainingSiteCount = uniqueSites.length - inlineSites.length;
413
- const lines = [
414
- `Fix exactly one React Doctor rule in ${projectName}:`,
415
- "",
416
- `${severityLabel} ${row.category}: ${row.title} (${row.ruleKey}, ×${row.siteCount})`,
417
- representative.message
418
- ];
419
- if (representative.help) lines.push("", `Suggested fix: ${representative.help}`);
420
- const fixRecipeLine = formatFixRecipeLine(representative);
421
- if (fixRecipeLine) lines.push("", fixRecipeLine);
422
- 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}`));
423
- if (remainingSiteCount > 0) lines.push(`- +${remainingSiteCount} more sites`);
424
- const learnMoreLine = formatLearnMoreLine(representative);
425
- if (learnMoreLine) lines.push("", learnMoreLine);
426
- lines.push("", `Verify with \`npx react-doctor@latest --verbose\` and confirm ${row.ruleKey} is gone before moving on.`);
427
- return lines.join("\n");
428
- };
429
- //#endregion
430
- //#region src/cli/ink/components/diagnostic-action-menu.tsx
431
- const MODAL_BACKGROUND = "black";
432
- const MODAL_SELECTED_BACKGROUND = "white";
433
- const DiagnosticActionMenu = ({ title, itemLabels, focusedIndex, maxWidth }) => {
434
- return /* @__PURE__ */ jsxs(Box, {
435
- width: Math.max(24, Math.min(44, maxWidth)),
436
- borderStyle: "round",
437
- borderColor: "gray",
438
- backgroundColor: MODAL_BACKGROUND,
439
- paddingX: 2,
440
- paddingY: 1,
441
- flexDirection: "column",
442
- children: [
443
- /* @__PURE__ */ jsxs(Text, {
444
- wrap: "truncate-end",
445
- children: [/* @__PURE__ */ jsx(Text, {
446
- dimColor: true,
447
- children: "Fix "
448
- }), /* @__PURE__ */ jsx(Text, {
449
- bold: true,
450
- children: title
451
- })]
452
- }),
453
- /* @__PURE__ */ jsx(Box, {
454
- marginTop: 1,
455
- flexDirection: "column",
456
- children: itemLabels.map((label, index) => {
457
- const isFocused = index === focusedIndex;
458
- return /* @__PURE__ */ jsx(Box, {
459
- backgroundColor: isFocused ? MODAL_SELECTED_BACKGROUND : void 0,
460
- children: /* @__PURE__ */ jsxs(Text, {
461
- wrap: "truncate-end",
462
- bold: isFocused,
463
- color: isFocused ? "black" : "gray",
464
- children: [isFocused ? "› " : " ", label]
465
- })
466
- }, label);
467
- })
468
- }),
469
- /* @__PURE__ */ jsx(Box, {
470
- marginTop: 1,
471
- children: /* @__PURE__ */ jsx(Text, {
472
- dimColor: true,
473
- wrap: "truncate-end",
474
- children: "↑/↓ select · enter run · esc close"
475
- })
476
- })
477
- ]
478
- });
479
- };
480
- //#endregion
481
- //#region src/cli/ink/lib/severity-variants.ts
482
- const ICONS = isUnicodeSupported() ? {
483
- error: "✖",
484
- warning: "⚠"
485
- } : {
486
- error: "x",
487
- warning: "!"
488
- };
489
- const severityVariant = (severity) => severity === "error" ? {
490
- color: "red",
491
- icon: ICONS.error,
492
- label: "error"
493
- } : {
494
- color: "yellow",
495
- icon: ICONS.warning,
496
- label: "warning"
497
- };
498
- //#endregion
499
- //#region src/cli/ink/components/diagnostic-detail.tsx
500
- const DiagnosticDetail = ({ row, rootDirectory }) => {
501
- const codeFrame = useMemo(() => {
502
- if (!row) return null;
503
- const { representative } = row;
504
- return buildCodeFrame({
505
- filePath: representative.filePath,
506
- line: representative.line,
507
- column: representative.column,
508
- rootDirectory
509
- });
510
- }, [row, rootDirectory]);
511
- if (!row) return null;
512
- const variant = severityVariant(row.severity);
513
- const { representative } = row;
514
- const impact = getCategoryImpact(row.category);
515
- return /* @__PURE__ */ jsxs(Box, {
516
- flexDirection: "column",
517
- children: [
518
- /* @__PURE__ */ jsxs(Text, {
519
- wrap: "truncate-end",
520
- children: [
521
- /* @__PURE__ */ jsxs(Text, {
522
- color: variant.color,
523
- children: [
524
- " ",
525
- variant.icon,
526
- " "
527
- ]
528
- }),
529
- /* @__PURE__ */ jsx(Text, {
530
- color: variant.color,
531
- bold: true,
532
- children: row.title
533
- }),
534
- row.siteCount > 1 ? /* @__PURE__ */ jsxs(Text, {
535
- dimColor: true,
536
- children: [" ×", row.siteCount]
537
- }) : null
538
- ]
539
- }),
540
- /* @__PURE__ */ jsxs(Box, {
541
- flexDirection: "column",
542
- paddingLeft: 4,
543
- children: [
544
- /* @__PURE__ */ jsxs(Text, {
545
- dimColor: true,
546
- wrap: "truncate-end",
547
- children: [
548
- row.category,
549
- " · ",
550
- variant.label
551
- ]
552
- }),
553
- impact ? /* @__PURE__ */ jsx(Text, {
554
- dimColor: true,
555
- wrap: "wrap",
556
- children: impact
557
- }) : null,
558
- /* @__PURE__ */ jsx(Text, {
559
- wrap: "wrap",
560
- children: representative.message
561
- }),
562
- /* @__PURE__ */ jsx(Text, {
563
- dimColor: true,
564
- wrap: "truncate-end",
565
- children: row.location
566
- })
567
- ]
568
- }),
569
- codeFrame ? /* @__PURE__ */ jsx(Box, {
570
- marginTop: 1,
571
- marginLeft: 4,
572
- borderStyle: "round",
573
- borderColor: "gray",
574
- paddingX: 1,
575
- alignSelf: "flex-start",
576
- children: /* @__PURE__ */ jsx(Text, { children: codeFrame })
577
- }) : null,
578
- representative.help ? /* @__PURE__ */ jsx(Box, {
579
- marginTop: 1,
580
- paddingLeft: 4,
581
- children: /* @__PURE__ */ jsxs(Text, {
582
- dimColor: true,
583
- wrap: "wrap",
584
- children: ["→ ", representative.help]
585
- })
586
- }) : null,
587
- row.learnMore ? /* @__PURE__ */ jsx(Box, {
588
- marginTop: 1,
589
- paddingLeft: 4,
590
- children: /* @__PURE__ */ jsx(Text, {
591
- color: "cyan",
592
- wrap: "truncate-end",
593
- children: row.learnMore
594
- })
595
- }) : null
596
- ]
597
- });
598
- };
599
- //#endregion
600
- //#region src/cli/ink/components/diagnostic-item.tsx
601
- const DiagnosticItem = ({ row, isSelected, isRead }) => {
602
- const variant = severityVariant(row.severity);
603
- return /* @__PURE__ */ jsxs(Text, {
604
- wrap: "truncate-end",
605
- dimColor: isRead,
606
- children: [
607
- /* @__PURE__ */ jsx(Text, {
608
- color: isSelected ? variant.color : void 0,
609
- children: isSelected ? "›" : " "
610
- }),
611
- /* @__PURE__ */ jsx(Text, {
612
- color: isRead ? void 0 : variant.color,
613
- children: isRead ? " " : " •"
614
- }),
615
- /* @__PURE__ */ jsx(Text, {
616
- color: isRead ? void 0 : variant.color,
617
- children: ` ${variant.icon} `
618
- }),
619
- /* @__PURE__ */ jsx(Text, {
620
- color: isRead ? void 0 : variant.color,
621
- bold: isSelected,
622
- children: row.title
623
- }),
624
- row.siteCount > 1 ? /* @__PURE__ */ jsxs(Text, {
625
- dimColor: true,
626
- children: [" ×", row.siteCount]
627
- }) : null
628
- ]
629
- });
630
- };
631
- //#endregion
632
- //#region src/cli/ink/components/status-bar.tsx
633
- const StatusBar = ({ total, errorCount, warningCount, position, groupCount, unreadCount, projectCount, keyHints = /* @__PURE__ */ jsx(Text, {
634
- dimColor: true,
635
- children: "↑/↓ to move"
636
- }), exitHint = "q to quit" }) => /* @__PURE__ */ jsxs(Text, {
637
- wrap: "truncate-end",
638
- children: [
639
- /* @__PURE__ */ jsxs(Text, {
640
- bold: true,
641
- children: [
642
- total,
643
- " ",
644
- total === 1 ? "issue" : "issues"
645
- ]
646
- }),
647
- /* @__PURE__ */ jsx(Text, {
648
- dimColor: true,
649
- children: " › "
650
- }),
651
- /* @__PURE__ */ jsxs(Text, {
652
- color: "red",
653
- children: [errorCount, " errors"]
654
- }),
655
- /* @__PURE__ */ jsx(Text, {
656
- dimColor: true,
657
- children: ", "
658
- }),
659
- /* @__PURE__ */ jsxs(Text, {
660
- color: "yellow",
661
- dimColor: true,
662
- children: [warningCount, " warnings"]
663
- }),
664
- unreadCount !== void 0 ? /* @__PURE__ */ jsxs(Text, {
665
- color: unreadCount > 0 ? "cyan" : void 0,
666
- dimColor: unreadCount === 0,
667
- children: [
668
- " · ",
669
- unreadCount,
670
- " unread"
671
- ]
672
- }) : null,
673
- projectCount !== void 0 ? /* @__PURE__ */ jsxs(Text, {
674
- dimColor: true,
675
- children: [
676
- " · ",
677
- projectCount,
678
- " ",
679
- projectCount === 1 ? "project" : "projects"
680
- ]
681
- }) : null,
682
- /* @__PURE__ */ jsxs(Text, {
683
- dimColor: true,
684
- children: [
685
- " ",
686
- position,
687
- "/",
688
- groupCount,
689
- " · ",
690
- keyHints,
691
- " · ",
692
- exitHint
693
- ]
694
- })
695
- ]
696
- });
697
- //#endregion
698
- //#region src/cli/ink/components/diagnostic-list.tsx
699
- const ADD_TO_CI_KEY = "a";
700
- const sumSites = (rows) => rows.reduce((total, row) => total + row.siteCount, 0);
701
- const renderEntry = (entry, entryIndex, selectedIndex, readRuleKeys) => {
702
- if (entry.kind === "header") return /* @__PURE__ */ jsx(Text, {
703
- bold: true,
704
- wrap: "truncate-end",
705
- children: entry.category
706
- }, `header:${entry.category}`);
707
- return /* @__PURE__ */ jsx(DiagnosticItem, {
708
- row: entry.row,
709
- isSelected: entryIndex === selectedIndex,
710
- isRead: readRuleKeys.has(entry.row.ruleKey)
711
- }, entry.row.ruleKey);
712
- };
713
- const DiagnosticList = ({ header, rows, width, listColumnWidth, detailColumnWidth, listHeight, detailHeight, layout, rootDirectory, projectName, launchableAgents, onHandoff, canAddToCi, onAddToCi, projectCount, onExit, exitHint }) => {
714
- const entries = useMemo(() => buildDiagnosticListEntries(rows), [rows]);
715
- const [isMenuOpen, setIsMenuOpen] = useState(false);
716
- const [menuIndex, setMenuIndex] = useState(0);
717
- const isSplit = layout === "split";
718
- const { rows: terminalRows } = useStdoutDimensions();
719
- const { selectedIndex, visibleStart, visibleEnd } = useScrollViewport({
720
- itemCount: entries.length,
721
- height: listHeight,
722
- isSelectable: (index) => entries[index]?.kind === "item",
723
- isActive: !isMenuOpen
724
- });
725
- const visibleEntries = entries.slice(visibleStart, visibleEnd);
726
- const selectedEntry = entries[selectedIndex];
727
- const selected = selectedEntry?.kind === "item" ? selectedEntry.row : null;
728
- const selectedRuleKey = selected?.ruleKey ?? null;
729
- const [readRuleKeys, setReadRuleKeys] = useState(() => /* @__PURE__ */ new Set());
730
- const [copiedRuleKey, setCopiedRuleKey] = useState(null);
731
- const menuLabels = useMemo(() => ["Copy prompt", ...launchableAgents.map((agentId) => getSkillAgentConfig(agentId).displayName)], [launchableAgents]);
732
- useEffect(() => {
733
- if (!selectedRuleKey) return;
734
- setReadRuleKeys((previous) => previous.has(selectedRuleKey) ? previous : new Set(previous).add(selectedRuleKey));
735
- }, [selectedRuleKey]);
736
- const copySelectedPrompt = () => {
737
- if (!selected) return;
738
- const prompt = buildIssuePrompt({
739
- row: selected,
740
- projectName
741
- });
742
- const ruleKey = selected.ruleKey;
743
- copyToClipboard(prompt).then((didCopy) => {
744
- if (didCopy) setCopiedRuleKey(ruleKey);
745
- });
746
- };
747
- const launchSelectedInAgent = (agentId) => {
748
- if (!selected || !onHandoff) return;
749
- onHandoff({
750
- agentId,
751
- prompt: buildIssuePrompt({
752
- row: selected,
753
- projectName
754
- })
755
- });
756
- onExit();
757
- };
758
- const runMenuItem = () => {
759
- if (menuIndex === 0) {
760
- copySelectedPrompt();
761
- setIsMenuOpen(false);
762
- return;
763
- }
764
- const agentId = launchableAgents[menuIndex - 1];
765
- if (agentId) launchSelectedInAgent(agentId);
766
- };
767
- const showCiCallout = Boolean(canAddToCi && onAddToCi);
768
- useInput((input, key) => {
769
- if (input === "q" || key.escape) return onExit();
770
- if (showCiCallout && input === ADD_TO_CI_KEY) {
771
- onAddToCi?.();
772
- return onExit();
773
- }
774
- if (key.return && selected) {
775
- setMenuIndex(0);
776
- setIsMenuOpen(true);
777
- }
778
- }, { isActive: !isMenuOpen });
779
- useInput((input, key) => {
780
- if (key.escape) return setIsMenuOpen(false);
781
- if (key.upArrow || input === "k") return setMenuIndex((index) => Math.max(0, index - 1));
782
- if (key.downArrow || input === "j") return setMenuIndex((index) => Math.min(menuLabels.length - 1, index + 1));
783
- if (key.return) return runMenuItem();
784
- }, { isActive: isMenuOpen });
785
- const errorRows = rows.filter((row) => row.severity === "error");
786
- const warningRows = rows.filter((row) => row.severity === "warning");
787
- const itemPosition = entries.slice(0, selectedIndex + 1).filter((entry) => entry.kind === "item").length;
788
- const unreadCount = rows.length - rows.filter((row) => readRuleKeys.has(row.ruleKey)).length;
789
- const listColumn = /* @__PURE__ */ jsx(Box, {
790
- flexDirection: "column",
791
- height: listHeight,
792
- width: isSplit ? listColumnWidth : width,
793
- children: visibleEntries.map((entry, index) => renderEntry(entry, visibleStart + index, selectedIndex, readRuleKeys))
794
- });
795
- const detailContent = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DiagnosticDetail, {
796
- row: selected,
797
- rootDirectory
798
- }), copiedRuleKey === selectedRuleKey ? /* @__PURE__ */ jsx(Box, {
799
- marginTop: 1,
800
- children: /* @__PURE__ */ jsx(Text, {
801
- color: "green",
802
- children: "✓ Copied fix prompt"
803
- })
804
- }) : null] });
805
- const keyHints = useMemo(() => isMenuOpen ? /* @__PURE__ */ jsxs(Fragment, { children: [
806
- /* @__PURE__ */ jsx(Text, {
807
- dimColor: true,
808
- children: "↑/↓ select · "
809
- }),
810
- /* @__PURE__ */ jsx(Text, {
811
- color: "cyan",
812
- children: "enter"
813
- }),
814
- /* @__PURE__ */ jsx(Text, {
815
- dimColor: true,
816
- children: " run · esc close"
817
- })
818
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
819
- /* @__PURE__ */ jsx(Text, {
820
- dimColor: true,
821
- children: "↑/↓ move · "
822
- }),
823
- /* @__PURE__ */ jsx(Text, {
824
- color: "cyan",
825
- children: "enter"
826
- }),
827
- /* @__PURE__ */ jsx(Text, {
828
- dimColor: true,
829
- children: " fix this"
830
- }),
831
- showCiCallout ? /* @__PURE__ */ jsxs(Fragment, { children: [
832
- /* @__PURE__ */ jsx(Text, {
833
- dimColor: true,
834
- children: " · "
835
- }),
836
- /* @__PURE__ */ jsx(Text, {
837
- color: "green",
838
- children: ADD_TO_CI_KEY
839
- }),
840
- /* @__PURE__ */ jsx(Text, {
841
- dimColor: true,
842
- children: " add CI"
843
- })
844
- ] }) : null
845
- ] }), [isMenuOpen, showCiCallout]);
846
- const statusBar = /* @__PURE__ */ jsx(Box, {
847
- marginTop: 1,
848
- children: /* @__PURE__ */ jsx(StatusBar, {
849
- total: sumSites(rows),
850
- errorCount: sumSites(errorRows),
851
- warningCount: sumSites(warningRows),
852
- position: rows.length === 0 ? 0 : itemPosition,
853
- groupCount: rows.length,
854
- unreadCount,
855
- projectCount,
856
- keyHints,
857
- exitHint
858
- })
859
- });
860
- const overlay = isMenuOpen && selected ? /* @__PURE__ */ jsx(Box, {
861
- position: "absolute",
862
- top: 0,
863
- left: 0,
864
- width,
865
- height: Math.max(6, terminalRows - 2),
866
- justifyContent: "center",
867
- alignItems: "center",
868
- children: /* @__PURE__ */ jsx(DiagnosticActionMenu, {
869
- title: selected.title,
870
- itemLabels: menuLabels,
871
- focusedIndex: menuIndex,
872
- maxWidth: width
873
- })
874
- }) : null;
875
- if (isSplit) return /* @__PURE__ */ jsxs(Box, {
876
- flexDirection: "column",
877
- width,
878
- position: "relative",
879
- children: [
880
- /* @__PURE__ */ jsxs(Box, {
881
- flexDirection: "row",
882
- children: [/* @__PURE__ */ jsxs(Box, {
883
- flexDirection: "column",
884
- width: listColumnWidth,
885
- marginRight: 1,
886
- children: [header, /* @__PURE__ */ jsx(Box, {
887
- marginTop: 1,
888
- children: listColumn
889
- })]
890
- }), /* @__PURE__ */ jsx(Box, {
891
- flexDirection: "column",
892
- width: detailColumnWidth,
893
- borderStyle: "single",
894
- borderColor: "gray",
895
- borderTop: false,
896
- borderRight: false,
897
- borderBottom: false,
898
- paddingLeft: 1,
899
- height: detailHeight,
900
- overflowY: "hidden",
901
- children: detailContent
902
- })]
903
- }),
904
- statusBar,
905
- overlay
906
- ]
907
- });
908
- return /* @__PURE__ */ jsxs(Box, {
909
- flexDirection: "column",
910
- width,
911
- position: "relative",
912
- children: [
913
- header,
914
- /* @__PURE__ */ jsx(Box, {
915
- marginTop: 1,
916
- children: listColumn
917
- }),
918
- /* @__PURE__ */ jsx(Text, {
919
- dimColor: true,
920
- children: "─".repeat(width)
921
- }),
922
- /* @__PURE__ */ jsx(Box, {
923
- flexDirection: "column",
924
- height: detailHeight,
925
- overflowY: "hidden",
926
- children: detailContent
927
- }),
928
- statusBar,
929
- overlay
930
- ]
931
- });
932
- };
933
- //#endregion
934
- //#region src/cli/ink/hooks/use-animated-score.ts
935
- const useAnimatedScore = ({ score, projectedScore, animate }) => {
936
- const projectionTarget = projectedScore ?? score;
937
- const hasProjection = projectionTarget > score;
938
- const [displayScore, setDisplayScore] = useState(animate ? 0 : score);
939
- const [displayProjectedScore, setDisplayProjectedScore] = useState(animate ? null : hasProjection ? projectionTarget : null);
940
- useEffect(() => {
941
- if (!animate) {
942
- setDisplayScore(score);
943
- setDisplayProjectedScore(hasProjection ? projectionTarget : null);
944
- return;
945
- }
946
- let timeoutId;
947
- const runProjection = () => {
948
- let frame = 1;
949
- const tick = () => {
950
- const progress = easeOutCubic(frame / 16);
951
- setDisplayProjectedScore(score + (projectionTarget - score) * progress);
952
- if (frame < 16) {
953
- frame += 1;
954
- timeoutId = setTimeout(tick, 35);
955
- } else setDisplayProjectedScore(projectionTarget);
956
- };
957
- tick();
958
- };
959
- let frame = 0;
960
- const tick = () => {
961
- const progress = easeOutCubic(frame / 40);
962
- setDisplayScore(Math.round(score * progress));
963
- if (frame < 40) {
964
- frame += 1;
965
- timeoutId = setTimeout(tick, 50);
966
- return;
967
- }
968
- setDisplayScore(score);
969
- if (hasProjection) runProjection();
970
- };
971
- tick();
972
- return () => clearTimeout(timeoutId);
973
- }, [
974
- animate,
975
- score,
976
- projectionTarget,
977
- hasProjection
978
- ]);
979
- return {
980
- displayScore,
981
- displayProjectedScore
982
- };
983
- };
984
- //#endregion
985
- //#region src/cli/ink/lib/score-color.ts
986
- const scoreColorName = (score) => {
987
- if (score >= 75) return "green";
988
- if (score >= 50) return "yellow";
989
- return "red";
990
- };
991
- //#endregion
992
- //#region src/cli/ink/components/score-header.tsx
993
- const BRANDING = "https://react.doctor";
994
- const ScoreHeader = ({ score, projectedScore, projectName, issueCount, noScoreMessage, width }) => {
995
- const { columns } = useStdoutDimensions();
996
- const availableWidth = width ?? columns;
997
- const { stdout } = useStdout();
998
- const animate = useMemo(() => canAnimateOnboarding(stdout ?? void 0), [stdout]);
999
- const { displayScore, displayProjectedScore } = useAnimatedScore({
1000
- score: score?.score ?? 0,
1001
- projectedScore,
1002
- animate: animate && score !== null
1003
- });
1004
- if (!score) return /* @__PURE__ */ jsxs(Box, {
1005
- flexDirection: "column",
1006
- paddingLeft: 2,
1007
- children: [/* @__PURE__ */ jsxs(Text, { children: ["React Doctor ", /* @__PURE__ */ jsxs(Text, {
1008
- dimColor: true,
1009
- children: [
1010
- "(",
1011
- BRANDING,
1012
- ")"
1013
- ]
1014
- })] }), /* @__PURE__ */ jsx(Text, {
1015
- dimColor: true,
1016
- children: noScoreMessage ?? `${issueCount} issues · ${projectName}`
1017
- })]
1018
- });
1019
- const color = scoreColorName(score.score);
1020
- const barWidth = Math.max(10, Math.min(50, availableWidth - 11 - 2));
1021
- const filled = Math.round(displayScore / 100 * barWidth);
1022
- const projectedFill = displayProjectedScore != null ? Math.min(barWidth, Math.round(displayProjectedScore / 100 * barWidth)) : filled;
1023
- const gain = Math.max(0, projectedFill - filled);
1024
- const empty = Math.max(0, barWidth - filled - gain);
1025
- const [eyes, mouth] = doctorFace(score.score);
1026
- return /* @__PURE__ */ jsxs(Box, {
1027
- flexDirection: "column",
1028
- children: [/* @__PURE__ */ jsxs(Box, {
1029
- paddingLeft: 2,
1030
- children: [/* @__PURE__ */ jsxs(Box, {
1031
- flexDirection: "column",
1032
- marginRight: 2,
1033
- children: [
1034
- /* @__PURE__ */ jsx(Text, {
1035
- color,
1036
- children: "┌─────┐"
1037
- }),
1038
- /* @__PURE__ */ jsxs(Text, {
1039
- color,
1040
- children: [
1041
- "│ ",
1042
- eyes,
1043
- " │"
1044
- ]
1045
- }),
1046
- /* @__PURE__ */ jsxs(Text, {
1047
- color,
1048
- children: [
1049
- "│ ",
1050
- mouth,
1051
- " │"
1052
- ]
1053
- }),
1054
- /* @__PURE__ */ jsx(Text, {
1055
- color,
1056
- children: "└─────┘"
1057
- })
1058
- ]
1059
- }), /* @__PURE__ */ jsxs(Box, {
1060
- flexDirection: "column",
1061
- children: [
1062
- /* @__PURE__ */ jsxs(Text, {
1063
- wrap: "truncate-end",
1064
- children: [
1065
- /* @__PURE__ */ jsx(Text, {
1066
- color,
1067
- bold: true,
1068
- children: displayScore
1069
- }),
1070
- /* @__PURE__ */ jsxs(Text, {
1071
- dimColor: true,
1072
- children: [
1073
- " / ",
1074
- 100,
1075
- " "
1076
- ]
1077
- }),
1078
- /* @__PURE__ */ jsx(Text, {
1079
- color,
1080
- children: score.label
1081
- }),
1082
- /* @__PURE__ */ jsxs(Text, {
1083
- dimColor: true,
1084
- children: [" · ", projectName]
1085
- })
1086
- ]
1087
- }),
1088
- /* @__PURE__ */ jsxs(Text, {
1089
- wrap: "truncate-end",
1090
- children: [
1091
- /* @__PURE__ */ jsx(Text, {
1092
- color,
1093
- children: "█".repeat(filled)
1094
- }),
1095
- /* @__PURE__ */ jsx(Text, {
1096
- color,
1097
- dimColor: true,
1098
- children: "▓".repeat(gain)
1099
- }),
1100
- /* @__PURE__ */ jsx(Text, {
1101
- dimColor: true,
1102
- children: "░".repeat(empty)
1103
- })
1104
- ]
1105
- }),
1106
- /* @__PURE__ */ jsxs(Text, { children: ["React Doctor ", /* @__PURE__ */ jsxs(Text, {
1107
- dimColor: true,
1108
- children: [
1109
- "(",
1110
- BRANDING,
1111
- ")"
1112
- ]
1113
- })] }),
1114
- /* @__PURE__ */ jsx(Text, { children: " " })
1115
- ]
1116
- })]
1117
- }), projectedScore != null && projectedScore > score.score ? /* @__PURE__ */ jsxs(Text, { children: [
1118
- /* @__PURE__ */ jsx(Text, {
1119
- dimColor: true,
1120
- children: " You could improve "
1121
- }),
1122
- /* @__PURE__ */ jsxs(Text, {
1123
- color: scoreColorName(projectedScore),
1124
- children: [
1125
- "+",
1126
- projectedScore - score.score,
1127
- "%"
1128
- ]
1129
- }),
1130
- /* @__PURE__ */ jsx(Text, {
1131
- dimColor: true,
1132
- children: ` by fixing the top 3 issues`
1133
- })
1134
- ] }) : null]
1135
- });
1136
- };
1137
- //#endregion
1138
- //#region src/cli/ink/components/report.tsx
1139
- const STACKED_FIXED_ROWS = 10;
1140
- const SPLIT_CHROME_ROWS = 9;
1141
- const EMPTY_LAUNCHABLE_AGENTS = [];
1142
- const Report = ({ report, onExit, launchableAgents = EMPTY_LAUNCHABLE_AGENTS, onHandoff, canAddToCi, onAddToCi, projectCount, priorityScores, exitHint = "q to quit" }) => {
1143
- const { rows: terminalRows, columns } = useStdoutDimensions();
1144
- const diagnosticRows = useMemo(() => buildDiagnosticRows(report.diagnostics, priorityScores ?? [report.score]), [
1145
- report.diagnostics,
1146
- report.score,
1147
- priorityScores
1148
- ]);
1149
- useInput((input, key) => {
1150
- if (input === "q" || key.escape) onExit();
1151
- }, { isActive: diagnosticRows.length === 0 });
1152
- const width = Math.max(24, columns - 2);
1153
- const isWide = columns >= 120 && terminalRows >= 22;
1154
- const detailHeight = isWide ? Math.max(0, terminalRows - 2) : Math.max(0, Math.min(15, terminalRows - STACKED_FIXED_ROWS - 3));
1155
- const listHeight = Math.max(3, terminalRows - (isWide ? SPLIT_CHROME_ROWS : STACKED_FIXED_ROWS + detailHeight));
1156
- const detailColumnWidth = Math.max(20, Math.floor(width * TUI_REPORT_DETAIL_WIDTH_FRACTION));
1157
- const listColumnWidth = Math.max(20, width - detailColumnWidth - 3);
1158
- const scoreHeader = /* @__PURE__ */ jsx(ScoreHeader, {
1159
- score: report.score,
1160
- projectedScore: report.projectedScore,
1161
- projectName: report.projectName,
1162
- issueCount: report.diagnostics.length,
1163
- noScoreMessage: report.noScoreMessage,
1164
- width: isWide ? listColumnWidth : width
1165
- });
1166
- if (diagnosticRows.length === 0) {
1167
- const lintFailureReason = report.lintFailureReason;
1168
- return /* @__PURE__ */ jsxs(Box, {
1169
- flexDirection: "column",
1170
- children: [
1171
- scoreHeader,
1172
- /* @__PURE__ */ jsx(Box, {
1173
- marginTop: 1,
1174
- children: lintFailureReason ? /* @__PURE__ */ jsxs(Text, {
1175
- color: "yellow",
1176
- children: ["⚠ Lint did not run: ", lintFailureReason]
1177
- }) : /* @__PURE__ */ jsx(Text, {
1178
- color: "green",
1179
- children: "✔ No issues found. Nice work."
1180
- })
1181
- }),
1182
- /* @__PURE__ */ jsx(Text, {
1183
- dimColor: true,
1184
- children: exitHint
1185
- })
1186
- ]
1187
- });
1188
- }
1189
- return /* @__PURE__ */ jsx(DiagnosticList, {
1190
- header: scoreHeader,
1191
- rows: diagnosticRows,
1192
- width,
1193
- listColumnWidth,
1194
- detailColumnWidth,
1195
- listHeight,
1196
- detailHeight,
1197
- layout: isWide ? "split" : "stacked",
1198
- rootDirectory: report.rootDirectory,
1199
- projectName: report.projectName,
1200
- launchableAgents,
1201
- onHandoff,
1202
- canAddToCi,
1203
- onAddToCi,
1204
- projectCount,
1205
- onExit,
1206
- exitHint
1207
- });
1208
- };
1209
- //#endregion
1210
- //#region src/cli/ink/components/scanning.tsx
1211
- const Scanning = ({ progressText, liveCount, recent }) => {
1212
- return /* @__PURE__ */ jsxs(Box, {
1213
- flexDirection: "column",
1214
- children: [/* @__PURE__ */ jsxs(Text, {
1215
- wrap: "truncate-end",
1216
- children: [
1217
- /* @__PURE__ */ jsx(Text, {
1218
- color: "cyan",
1219
- children: /* @__PURE__ */ jsx(Spinner, { type: "dots" })
1220
- }),
1221
- /* @__PURE__ */ jsxs(Text, { children: [" ", progressText ?? "Scanning…"] }),
1222
- liveCount > 0 ? /* @__PURE__ */ jsxs(Text, {
1223
- dimColor: true,
1224
- children: [
1225
- " · ",
1226
- liveCount,
1227
- " found"
1228
- ]
1229
- }) : null
1230
- ]
1231
- }), recent.map((diagnostic) => {
1232
- const variant = severityVariant(diagnostic.severity === "error" ? "error" : "warning");
1233
- const location = formatDiagnosticSite(diagnostic);
1234
- return /* @__PURE__ */ jsxs(Text, {
1235
- wrap: "truncate-end",
1236
- children: [
1237
- " ",
1238
- /* @__PURE__ */ jsx(Text, {
1239
- color: variant.color,
1240
- children: variant.icon
1241
- }),
1242
- /* @__PURE__ */ jsxs(Text, { children: [" ", diagnostic.title ?? `${diagnostic.plugin}/${diagnostic.rule}`] }),
1243
- /* @__PURE__ */ jsxs(Text, {
1244
- dimColor: true,
1245
- children: [" ", location]
1246
- })
1247
- ]
1248
- }, `${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.plugin}:${diagnostic.rule}`);
1249
- })]
1250
- });
1251
- };
1252
- //#endregion
1253
- //#region src/cli/ink/components/summary.tsx
1254
- const Summary = ({ summary, onExit, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
1255
- return /* @__PURE__ */ jsx(Report, {
1256
- report: {
1257
- diagnostics: summary.combinedDiagnostics,
1258
- score: summary.aggregateScore,
1259
- projectedScore: summary.projectedScore,
1260
- projectName: summary.projectName,
1261
- rootDirectory: summary.rootDirectory,
1262
- scannedFileCount: summary.scannedFileCount,
1263
- elapsedMilliseconds: summary.elapsedMilliseconds,
1264
- isOffline: summary.isOffline,
1265
- noScoreMessage: summary.noScoreMessage,
1266
- ...summary.lintFailureReason ? { lintFailureReason: summary.lintFailureReason } : {}
1267
- },
1268
- onExit,
1269
- launchableAgents,
1270
- onHandoff,
1271
- canAddToCi,
1272
- onAddToCi,
1273
- projectCount: summary.projects.length,
1274
- priorityScores: summary.projects.map((project) => project.score)
1275
- });
1276
- };
1277
- //#endregion
1278
- //#region src/cli/ink/hooks/use-scan-store.ts
1279
- const useScanStore = (store) => useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1280
- //#endregion
1281
- //#region src/cli/ink/scan-app.tsx
1282
- const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
1283
- const snapshot = useScanStore(store);
1284
- const { exit } = useApp();
1285
- useExitOnCtrlC();
1286
- if (snapshot.phase === "summary" && snapshot.summary) return /* @__PURE__ */ jsx(Summary, {
1287
- summary: snapshot.summary,
1288
- launchableAgents,
1289
- onHandoff,
1290
- canAddToCi,
1291
- onAddToCi,
1292
- onExit: () => exit()
1293
- });
1294
- if (snapshot.phase === "report" && snapshot.report) return /* @__PURE__ */ jsx(Report, {
1295
- report: snapshot.report,
1296
- launchableAgents,
1297
- onHandoff,
1298
- canAddToCi,
1299
- onAddToCi,
1300
- onExit: () => exit()
1301
- });
1302
- return /* @__PURE__ */ jsx(Scanning, {
1303
- progressText: snapshot.progress,
1304
- liveCount: snapshot.liveCount,
1305
- recent: snapshot.liveDiagnostics.slice(-5)
1306
- });
1307
- };
1308
- //#endregion
1309
- //#region src/cli/ink/scan-bridge-layers.ts
1310
- const reporterLayerForStore = (store) => succeed(Reporter, Reporter.of({
1311
- emit: (diagnostic) => sync(() => store.emitDiagnostic(diagnostic)),
1312
- finalize: void_
1313
- }));
1314
- const progressHandleForStore = (store) => (text) => {
1315
- store.setProgress(text);
1316
- return {
1317
- update: (displayText) => sync(() => store.setProgress(displayText)),
1318
- succeed: (displayText) => sync(() => store.setProgress(displayText)),
1319
- fail: (displayText) => sync(() => store.setProgress(displayText)),
1320
- stop: () => sync(() => store.setProgress(null))
1321
- };
1322
- };
1323
- const progressLayerForStore = (store) => Progress.layerOra(progressHandleForStore(store));
1324
- //#endregion
1325
- //#region src/cli/ink/scan-store.ts
1326
- const INITIAL_SNAPSHOT = {
1327
- phase: "scanning",
1328
- liveDiagnostics: [],
1329
- liveCount: 0,
1330
- progress: null,
1331
- report: null,
1332
- summary: null
1333
- };
1334
- const createScanStore = () => {
1335
- let snapshot = INITIAL_SNAPSHOT;
1336
- const listeners = /* @__PURE__ */ new Set();
1337
- const commit = (next) => {
1338
- snapshot = next;
1339
- for (const listener of listeners) listener();
1340
- };
1341
- return {
1342
- subscribe: (listener) => {
1343
- listeners.add(listener);
1344
- return () => {
1345
- listeners.delete(listener);
1346
- };
1347
- },
1348
- getSnapshot: () => snapshot,
1349
- emitDiagnostic: (diagnostic) => commit({
1350
- ...snapshot,
1351
- liveDiagnostics: [...snapshot.liveDiagnostics, diagnostic].slice(-25),
1352
- liveCount: snapshot.liveCount + 1
1353
- }),
1354
- setProgress: (progress) => commit({
1355
- ...snapshot,
1356
- progress
1357
- }),
1358
- setReport: (report) => commit({
1359
- ...snapshot,
1360
- report,
1361
- phase: "report"
1362
- }),
1363
- setSummary: (summary) => commit({
1364
- ...snapshot,
1365
- summary,
1366
- phase: "summary"
1367
- })
1368
- };
1369
- };
1370
- //#endregion
1371
- //#region src/cli/ink/run-scan-app.tsx
1372
- const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
1373
- const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
1374
- const config = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0);
1375
- const configSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
1376
- return {
1377
- directory: projectScanTarget.resolvedDirectory,
1378
- config,
1379
- configSourceDirectory
1380
- };
1381
- };
1382
- const qualifyDiagnosticPaths = (diagnostics, rootDirectory, projectDirectory) => {
1383
- const prefix = path.relative(rootDirectory, projectDirectory);
1384
- if (prefix === "" || prefix.startsWith("..")) return [...diagnostics];
1385
- return diagnostics.map((diagnostic) => path.isAbsolute(diagnostic.filePath) ? diagnostic : {
1386
- ...diagnostic,
1387
- filePath: toForwardSlashes(path.join(prefix, diagnostic.filePath))
1388
- });
1389
- };
1390
- const resolveScanPresentation = (input, projectScans) => {
1391
- const isScoreDisabled = input.options?.noScore === true || projectScans.some((projectScan) => projectScan.config?.noScore === true);
1392
- return {
1393
- isOffline: isCiEnvironment() || input.share === false || isShareOptedOut(projectScans, input.options?.noScore),
1394
- noScoreMessage: buildNoScoreMessage(isScoreDisabled)
1395
- };
1396
- };
1397
- const resolveTuiInspectOptions = (input, config) => {
1398
- const warnings = resolveCliInspectOptions({
1399
- blocking: input.blocking,
1400
- warnings: input.options?.warnings
1401
- }, config).warnings;
1402
- return warnings === void 0 ? { ...input.options } : {
1403
- ...input.options,
1404
- warnings
1405
- };
1406
- };
1407
- const resolveSelectedDirectories = async (rootDirectory, input) => {
1408
- const packages = discoverWorkspacePackages(rootDirectory);
1409
- 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);
1410
- return promptProjectSelection(packages, rootDirectory);
1411
- };
1412
- const promptProjectSelection = (packages, rootDirectory) => new Promise((resolve) => {
1413
- const instance = render(/* @__PURE__ */ jsx(ProjectSelect, {
1414
- packages,
1415
- rootDirectory,
1416
- onSubmit: (directories) => {
1417
- instance.clear();
1418
- instance.unmount();
1419
- resolve(directories);
1420
- }
1421
- }), { exitOnCtrlC: false });
1422
- });
1423
- const resolveLintFailureReason = (results) => {
1424
- for (const result of results) {
1425
- if (!hasLintHardFailure(result)) continue;
1426
- return result.skippedCheckReasons?.lint ?? "Lint failed before diagnostics were produced.";
1427
- }
1428
- return null;
1429
- };
1430
- const toScanReport = ({ result, rootDirectory, projectedScore, isOffline, noScoreMessage }) => {
1431
- const lintFailureReason = resolveLintFailureReason([result]);
1432
- return {
1433
- diagnostics: result.diagnostics,
1434
- score: result.score,
1435
- projectedScore,
1436
- projectName: result.project.projectName,
1437
- rootDirectory,
1438
- scannedFileCount: result.scannedFileCount ?? 0,
1439
- elapsedMilliseconds: result.elapsedMilliseconds,
1440
- isOffline,
1441
- noScoreMessage,
1442
- ...lintFailureReason ? { lintFailureReason } : {}
1443
- };
1444
- };
1445
- const findLowestScored = (reports) => {
1446
- let worst = null;
1447
- for (const report of reports) {
1448
- if (report.score === null) continue;
1449
- if (worst === null || report.score.score < worst.score.score) worst = {
1450
- score: report.score,
1451
- diagnostics: report.diagnostics
1452
- };
1453
- }
1454
- return worst;
1455
- };
1456
- const printExitFooter = async (input) => {
1457
- const fileLabel = input.scannedFileCount === 1 ? "file" : "files";
1458
- process.stdout.write(`${highlighter.success("✔")} Scanned ${input.scannedFileCount} ${fileLabel} in ${formatElapsedTime(input.elapsedMilliseconds)}\n`);
1459
- if (input.lintFailureReason !== null) process.stdout.write(`${highlighter.warn("⚠")} Lint did not run: ${input.lintFailureReason}\n`);
1460
- await runPromise(printFooter({
1461
- diagnostics: [...input.diagnostics],
1462
- scoreResult: input.scoreResult,
1463
- projectName: input.projectName,
1464
- isOffline: input.isOffline
1465
- }));
1466
- };
1467
- const performTuiHandoff = async (request, rootDirectory) => {
1468
- try {
1469
- await launchCliAgent(request.agentId, request.prompt, rootDirectory);
1470
- } catch {
1471
- process.stdout.write(`${highlighter.warn("⚠")} Couldn't launch ${CLI_AGENT_BINARIES[request.agentId]}. Here's the prompt instead:\n`);
1472
- process.stdout.write(`${highlighter.dim("──── Agent prompt ────")}\n`);
1473
- process.stdout.write(`${request.prompt}\n`);
1474
- process.stdout.write(`${highlighter.dim("──────────────────────")}\n`);
1475
- }
1476
- };
1477
- const isCiUnconfigured = (directory) => !isReactDoctorWorkflowInstalled(findNearestPackageDirectory(directory) ?? directory);
1478
- const performCiSetup = async (rootDirectory) => {
1479
- const didCreateWorkflow = await setUpGitHubActions({ rootDirectory });
1480
- recordCount(METRIC.agentHandoff, 1, {
1481
- outcome: "ci-yes",
1482
- source: "tui",
1483
- created: didCreateWorkflow
1484
- });
1485
- };
1486
- const mountScanApp = async (rootDirectory) => {
1487
- const store = createScanStore();
1488
- const launchableAgents = await detectLaunchableAgents();
1489
- const pending = {
1490
- handoff: null,
1491
- ciSetup: false
1492
- };
1493
- const instance = render(/* @__PURE__ */ jsx(ScanApp, {
1494
- store,
1495
- launchableAgents,
1496
- onHandoff: (request) => {
1497
- pending.handoff = request;
1498
- },
1499
- canAddToCi: isCiUnconfigured(rootDirectory),
1500
- onAddToCi: () => {
1501
- pending.ciSetup = true;
1502
- }
1503
- }), { exitOnCtrlC: false });
1504
- const settle = async () => {
1505
- if (pending.ciSetup) await performCiSetup(rootDirectory);
1506
- if (pending.handoff) await performTuiHandoff(pending.handoff, rootDirectory);
1507
- };
1508
- return {
1509
- store,
1510
- instance,
1511
- settle
1512
- };
1513
- };
1514
- const runMountedScan = async (rootDirectory, presentation, blockingLevel, executeScan) => {
1515
- const { store, instance, settle } = await mountScanApp(rootDirectory);
1516
- const context = {
1517
- store,
1518
- ...presentation
1519
- };
1520
- try {
1521
- const completedScan = await executeScan(context);
1522
- await instance.waitUntilExit();
1523
- await printExitFooter({
1524
- diagnostics: completedScan.diagnostics,
1525
- scoreResult: completedScan.scoreResult,
1526
- projectName: completedScan.projectName,
1527
- scannedFileCount: completedScan.scannedFileCount,
1528
- elapsedMilliseconds: completedScan.elapsedMilliseconds,
1529
- isOffline: context.isOffline,
1530
- lintFailureReason: resolveLintFailureReason(completedScan.scans.map(({ result }) => result))
1531
- });
1532
- await settle();
1533
- return { shouldFail: shouldFailScanGate({
1534
- scans: completedScan.scans,
1535
- blockingLevel
1536
- }) };
1537
- } catch (error) {
1538
- instance.unmount();
1539
- throw error;
1540
- }
1541
- };
1542
- const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blockingLevel) => {
1543
- const projectScan = await resolveProjectScan(rootScanTarget, projectDirectory);
1544
- const presentation = resolveScanPresentation(input, [projectScan]);
1545
- return runMountedScan(projectScan.directory, presentation, blockingLevel, async (context) => {
1546
- const result = await inspect(projectScan.directory, {
1547
- ...resolveTuiInspectOptions(input, projectScan.config),
1548
- isCi: isCiEnvironment(),
1549
- configOverride: projectScan.config,
1550
- configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
1551
- uiLayers: {
1552
- reporter: reporterLayerForStore(context.store),
1553
- progress: progressLayerForStore(context.store)
1554
- }
1555
- });
1556
- const projectedScore = result.score ? await computeProjectedScore([...result.diagnostics], [...result.diagnostics], result.score) : null;
1557
- context.store.setReport(toScanReport({
1558
- result,
1559
- rootDirectory: projectScan.directory,
1560
- projectedScore,
1561
- isOffline: context.isOffline,
1562
- noScoreMessage: context.noScoreMessage
1563
- }));
1564
- return {
1565
- scans: [{
1566
- result,
1567
- config: projectScan.config
1568
- }],
1569
- diagnostics: result.diagnostics,
1570
- scoreResult: result.score,
1571
- projectName: result.project.projectName,
1572
- scannedFileCount: result.scannedFileCount ?? 0,
1573
- elapsedMilliseconds: result.elapsedMilliseconds
1574
- };
1575
- });
1576
- };
1577
- const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel) => {
1578
- const rootDirectory = rootScanTarget.resolvedDirectory;
1579
- const projectScans = await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory));
1580
- return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans), blockingLevel, async (context) => {
1581
- const startTime = performance.now();
1582
- let finishedCount = 0;
1583
- context.store.setProgress(`Scanning ${directories.length} projects…`);
1584
- const results = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
1585
- const result = await inspect(projectScan.directory, {
1586
- ...resolveTuiInspectOptions(input, projectScan.config),
1587
- isCi: isCiEnvironment(),
1588
- configOverride: projectScan.config,
1589
- configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
1590
- uiLayers: { reporter: Reporter.layerNoop },
1591
- concurrentScan: true
1592
- });
1593
- finishedCount += 1;
1594
- context.store.setProgress(`Scanning ${directories.length} projects… (${finishedCount}/${directories.length})`);
1595
- return {
1596
- directory: projectScan.directory,
1597
- result,
1598
- config: projectScan.config
1599
- };
1600
- });
1601
- const projects = results.map(({ directory, result }) => toScanReport({
1602
- result,
1603
- rootDirectory: directory,
1604
- projectedScore: null,
1605
- isOffline: context.isOffline,
1606
- noScoreMessage: context.noScoreMessage
1607
- }));
1608
- const combinedDiagnostics = projects.flatMap((project) => qualifyDiagnosticPaths(project.diagnostics, rootDirectory, project.rootDirectory));
1609
- const worst = findLowestScored(projects);
1610
- const projectedScore = worst ? await computeProjectedScore(combinedDiagnostics, [...worst.diagnostics], worst.score) : null;
1611
- const scannedFileCount = countUniqueScannedFiles(results.map(({ result }) => result));
1612
- const elapsedMilliseconds = performance.now() - startTime;
1613
- const lintFailureReason = resolveLintFailureReason(results.map(({ result }) => result));
1614
- const summary = {
1615
- projects,
1616
- aggregateScore: worst?.score ?? null,
1617
- projectedScore,
1618
- combinedDiagnostics,
1619
- scannedFileCount,
1620
- elapsedMilliseconds,
1621
- projectName: path.basename(rootDirectory),
1622
- rootDirectory,
1623
- isOffline: context.isOffline,
1624
- noScoreMessage: context.noScoreMessage,
1625
- ...lintFailureReason ? { lintFailureReason } : {}
1626
- };
1627
- context.store.setSummary(summary);
1628
- return {
1629
- scans: results,
1630
- diagnostics: combinedDiagnostics,
1631
- scoreResult: summary.aggregateScore,
1632
- projectName: summary.projectName,
1633
- scannedFileCount,
1634
- elapsedMilliseconds
1635
- };
1636
- });
1637
- };
1638
- const runScanApp = async (input) => {
1639
- const scanTarget = await resolveScanTarget(input.directory, { allowAmbiguous: true });
1640
- const rootDirectory = scanTarget.resolvedDirectory;
1641
- const deadlineEpochMs = input.options?.deadlineEpochMs ?? (input.options?.maxDurationMs != null ? Date.now() + input.options.maxDurationMs : void 0);
1642
- const resolvedInput = {
1643
- ...input,
1644
- options: {
1645
- ...input.options,
1646
- deadlineEpochMs
1647
- },
1648
- configProjects: input.configProjects ?? scanTarget.userConfig?.projects,
1649
- share: input.share ?? scanTarget.userConfig?.share ?? true
1650
- };
1651
- const selectedDirectories = await resolveSelectedDirectories(rootDirectory, resolvedInput);
1652
- const blockingLevel = resolveBlockingLevel({ blocking: resolvedInput.blocking }, scanTarget.userConfig);
1653
- if (selectedDirectories.length === 0) return { shouldFail: false };
1654
- if (selectedDirectories.length === 1) return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, blockingLevel);
1655
- return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, blockingLevel);
1656
- };
1657
- //#endregion
1658
- export { runScanApp };
1659
-
1660
- //# sourceMappingURL=run-scan-app-DZ5jGWFf.js.map
1661
- //# debugId=d3f7c231-0bd2-5808-8ce4-be477335e722