react-doctor 0.9.3 → 0.9.4-dev.8b97fdc
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.
- package/dist/cli.js +1143 -559
- package/dist/index.d.ts +9 -2
- package/dist/index.js +643 -349
- package/dist/lsp.js +655 -327
- package/dist/run-scan-app-QbtG43NL.js +2326 -0
- package/package.json +7 -6
- package/dist/run-scan-app-DHRvqfkX.js +0 -1733
|
@@ -1,1733 +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]="898bdb3e-afed-5fca-92a2-f14d1e8ca6fb")}catch(e){}}();
|
|
3
|
-
import { A as toForwardSlashes, B as getCategoryImpact, C as buildRulePriorityMap, D as resolveBlockingLevel, E as formatLearnMoreLine, F as TUI_REPORT_DETAIL_WIDTH_FRACTION, G as runPromise, H as mapWithConcurrency, I as isCiEnvironment, J as succeed, K as sync, L as DIAGNOSTIC_CATEGORY_BUCKETS, M as isUnicodeSupported, N as METRIC, O as isReactDoctorWorkflowInstalled, P as TUI_REPORT_COMPACT_MAX_ROWS, R as Progress, S as hasLintHardFailure, T as formatFixRecipeLine, U as mergeReactDoctorConfigs, V as highlighter, W as resolveScanTarget, _ 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 createInvocationInspect, q as void_, 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 Reporter } 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 DiagnosticActionMenu = ({ title, itemLabels, focusedIndex, maxWidth }) => {
|
|
432
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
433
|
-
width: Math.max(24, Math.min(44, maxWidth)),
|
|
434
|
-
borderStyle: "round",
|
|
435
|
-
borderColor: "cyan",
|
|
436
|
-
paddingX: 2,
|
|
437
|
-
paddingY: 1,
|
|
438
|
-
flexDirection: "column",
|
|
439
|
-
children: [
|
|
440
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
441
|
-
wrap: "truncate-end",
|
|
442
|
-
children: [/* @__PURE__ */ jsx(Text, {
|
|
443
|
-
dimColor: true,
|
|
444
|
-
children: "Fix "
|
|
445
|
-
}), /* @__PURE__ */ jsx(Text, {
|
|
446
|
-
bold: true,
|
|
447
|
-
children: title
|
|
448
|
-
})]
|
|
449
|
-
}),
|
|
450
|
-
/* @__PURE__ */ jsx(Box, {
|
|
451
|
-
marginTop: 1,
|
|
452
|
-
flexDirection: "column",
|
|
453
|
-
children: itemLabels.map((label, index) => {
|
|
454
|
-
const isFocused = index === focusedIndex;
|
|
455
|
-
return /* @__PURE__ */ jsxs(Text, {
|
|
456
|
-
wrap: "truncate-end",
|
|
457
|
-
bold: isFocused,
|
|
458
|
-
color: isFocused ? "cyan" : void 0,
|
|
459
|
-
dimColor: !isFocused,
|
|
460
|
-
children: [isFocused ? "› " : " ", label]
|
|
461
|
-
}, label);
|
|
462
|
-
})
|
|
463
|
-
}),
|
|
464
|
-
/* @__PURE__ */ jsx(Box, {
|
|
465
|
-
marginTop: 1,
|
|
466
|
-
children: /* @__PURE__ */ jsx(Text, {
|
|
467
|
-
dimColor: true,
|
|
468
|
-
wrap: "truncate-end",
|
|
469
|
-
children: "↑/↓ select · enter run · esc close"
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
]
|
|
473
|
-
});
|
|
474
|
-
};
|
|
475
|
-
//#endregion
|
|
476
|
-
//#region src/cli/ink/lib/severity-variants.ts
|
|
477
|
-
const ICONS = isUnicodeSupported() ? {
|
|
478
|
-
error: "✖",
|
|
479
|
-
warning: "⚠"
|
|
480
|
-
} : {
|
|
481
|
-
error: "x",
|
|
482
|
-
warning: "!"
|
|
483
|
-
};
|
|
484
|
-
const severityVariant = (severity) => severity === "error" ? {
|
|
485
|
-
color: "red",
|
|
486
|
-
icon: ICONS.error,
|
|
487
|
-
label: "error"
|
|
488
|
-
} : {
|
|
489
|
-
color: "yellow",
|
|
490
|
-
icon: ICONS.warning,
|
|
491
|
-
label: "warning"
|
|
492
|
-
};
|
|
493
|
-
//#endregion
|
|
494
|
-
//#region src/cli/ink/components/diagnostic-detail.tsx
|
|
495
|
-
const DiagnosticDetail = ({ row, rootDirectory }) => {
|
|
496
|
-
const codeFrame = useMemo(() => {
|
|
497
|
-
if (!row) return null;
|
|
498
|
-
const { representative } = row;
|
|
499
|
-
return buildCodeFrame({
|
|
500
|
-
filePath: representative.filePath,
|
|
501
|
-
line: representative.line,
|
|
502
|
-
column: representative.column,
|
|
503
|
-
rootDirectory
|
|
504
|
-
});
|
|
505
|
-
}, [row, rootDirectory]);
|
|
506
|
-
if (!row) return null;
|
|
507
|
-
const variant = severityVariant(row.severity);
|
|
508
|
-
const { representative } = row;
|
|
509
|
-
const impact = getCategoryImpact(row.category);
|
|
510
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
511
|
-
flexDirection: "column",
|
|
512
|
-
children: [/* @__PURE__ */ jsxs(Text, {
|
|
513
|
-
wrap: "truncate-end",
|
|
514
|
-
children: [
|
|
515
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
516
|
-
color: variant.color,
|
|
517
|
-
children: [variant.icon, " "]
|
|
518
|
-
}),
|
|
519
|
-
/* @__PURE__ */ jsx(Text, {
|
|
520
|
-
color: variant.color,
|
|
521
|
-
bold: true,
|
|
522
|
-
children: row.title
|
|
523
|
-
}),
|
|
524
|
-
row.siteCount > 1 ? /* @__PURE__ */ jsxs(Text, {
|
|
525
|
-
dimColor: true,
|
|
526
|
-
children: [" ×", row.siteCount]
|
|
527
|
-
}) : null
|
|
528
|
-
]
|
|
529
|
-
}), /* @__PURE__ */ jsxs(Box, {
|
|
530
|
-
flexDirection: "column",
|
|
531
|
-
paddingLeft: 2,
|
|
532
|
-
children: [
|
|
533
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
534
|
-
dimColor: true,
|
|
535
|
-
wrap: "truncate-end",
|
|
536
|
-
children: [
|
|
537
|
-
row.category,
|
|
538
|
-
" · ",
|
|
539
|
-
variant.label,
|
|
540
|
-
" · ",
|
|
541
|
-
row.location
|
|
542
|
-
]
|
|
543
|
-
}),
|
|
544
|
-
impact ? /* @__PURE__ */ jsx(Text, {
|
|
545
|
-
dimColor: true,
|
|
546
|
-
wrap: "wrap",
|
|
547
|
-
children: impact
|
|
548
|
-
}) : null,
|
|
549
|
-
/* @__PURE__ */ jsx(Text, {
|
|
550
|
-
wrap: "wrap",
|
|
551
|
-
children: representative.message
|
|
552
|
-
}),
|
|
553
|
-
codeFrame ? /* @__PURE__ */ jsx(Box, {
|
|
554
|
-
marginTop: 1,
|
|
555
|
-
children: /* @__PURE__ */ jsx(Text, { children: codeFrame })
|
|
556
|
-
}) : null,
|
|
557
|
-
representative.help ? /* @__PURE__ */ jsx(Box, {
|
|
558
|
-
marginTop: 1,
|
|
559
|
-
children: /* @__PURE__ */ jsxs(Text, {
|
|
560
|
-
wrap: "wrap",
|
|
561
|
-
children: [/* @__PURE__ */ jsx(Text, {
|
|
562
|
-
color: "cyan",
|
|
563
|
-
children: "Fix "
|
|
564
|
-
}), representative.help]
|
|
565
|
-
})
|
|
566
|
-
}) : null,
|
|
567
|
-
row.learnMore ? /* @__PURE__ */ jsx(Box, {
|
|
568
|
-
marginTop: 1,
|
|
569
|
-
children: /* @__PURE__ */ jsx(Text, {
|
|
570
|
-
color: "blue",
|
|
571
|
-
wrap: "truncate-end",
|
|
572
|
-
children: row.learnMore
|
|
573
|
-
})
|
|
574
|
-
}) : null
|
|
575
|
-
]
|
|
576
|
-
})]
|
|
577
|
-
});
|
|
578
|
-
};
|
|
579
|
-
//#endregion
|
|
580
|
-
//#region src/cli/ink/components/diagnostic-item.tsx
|
|
581
|
-
const DiagnosticItem = ({ row, isSelected, isRead }) => {
|
|
582
|
-
const variant = severityVariant(row.severity);
|
|
583
|
-
const shouldHighlightSeverity = isSelected || !isRead;
|
|
584
|
-
let marker = "• ";
|
|
585
|
-
if (isRead) marker = " ";
|
|
586
|
-
if (isSelected) marker = "› ";
|
|
587
|
-
let markerColor = shouldHighlightSeverity ? variant.color : void 0;
|
|
588
|
-
if (isSelected) markerColor = "cyan";
|
|
589
|
-
return /* @__PURE__ */ jsxs(Text, {
|
|
590
|
-
wrap: "truncate-end",
|
|
591
|
-
dimColor: isRead && !isSelected,
|
|
592
|
-
children: [
|
|
593
|
-
/* @__PURE__ */ jsx(Text, {
|
|
594
|
-
color: markerColor,
|
|
595
|
-
children: marker
|
|
596
|
-
}),
|
|
597
|
-
/* @__PURE__ */ jsx(Text, {
|
|
598
|
-
color: shouldHighlightSeverity ? variant.color : void 0,
|
|
599
|
-
children: `${variant.icon} `
|
|
600
|
-
}),
|
|
601
|
-
/* @__PURE__ */ jsx(Text, {
|
|
602
|
-
color: shouldHighlightSeverity ? variant.color : void 0,
|
|
603
|
-
bold: isSelected,
|
|
604
|
-
children: row.title
|
|
605
|
-
}),
|
|
606
|
-
row.siteCount > 1 ? /* @__PURE__ */ jsxs(Text, {
|
|
607
|
-
dimColor: true,
|
|
608
|
-
children: [" ×", row.siteCount]
|
|
609
|
-
}) : null
|
|
610
|
-
]
|
|
611
|
-
});
|
|
612
|
-
};
|
|
613
|
-
//#endregion
|
|
614
|
-
//#region src/cli/ink/components/status-bar.tsx
|
|
615
|
-
const StatusBar = ({ total, errorCount, warningCount, position, groupCount, unreadCount, projectCount, keyHints = /* @__PURE__ */ jsx(Text, {
|
|
616
|
-
dimColor: true,
|
|
617
|
-
children: "↑/↓ to move"
|
|
618
|
-
}), exitHint = "q to quit", compact = false }) => {
|
|
619
|
-
const counts = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
620
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
621
|
-
bold: true,
|
|
622
|
-
children: [
|
|
623
|
-
total,
|
|
624
|
-
" ",
|
|
625
|
-
total === 1 ? "issue" : "issues"
|
|
626
|
-
]
|
|
627
|
-
}),
|
|
628
|
-
/* @__PURE__ */ jsx(Text, {
|
|
629
|
-
dimColor: true,
|
|
630
|
-
children: " "
|
|
631
|
-
}),
|
|
632
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
633
|
-
color: "red",
|
|
634
|
-
children: [errorCount, " errors"]
|
|
635
|
-
}),
|
|
636
|
-
/* @__PURE__ */ jsx(Text, {
|
|
637
|
-
dimColor: true,
|
|
638
|
-
children: " "
|
|
639
|
-
}),
|
|
640
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
641
|
-
color: "yellow",
|
|
642
|
-
children: [warningCount, " warnings"]
|
|
643
|
-
})
|
|
644
|
-
] });
|
|
645
|
-
if (compact) return /* @__PURE__ */ jsxs(Text, {
|
|
646
|
-
wrap: "truncate-end",
|
|
647
|
-
children: [
|
|
648
|
-
counts,
|
|
649
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
650
|
-
dimColor: true,
|
|
651
|
-
children: [
|
|
652
|
-
" · ",
|
|
653
|
-
position,
|
|
654
|
-
"/",
|
|
655
|
-
groupCount,
|
|
656
|
-
" · "
|
|
657
|
-
]
|
|
658
|
-
}),
|
|
659
|
-
keyHints,
|
|
660
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
661
|
-
dimColor: true,
|
|
662
|
-
children: [" · ", exitHint]
|
|
663
|
-
})
|
|
664
|
-
]
|
|
665
|
-
});
|
|
666
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
667
|
-
flexDirection: "column",
|
|
668
|
-
children: [/* @__PURE__ */ jsxs(Text, {
|
|
669
|
-
wrap: "truncate-end",
|
|
670
|
-
children: [
|
|
671
|
-
counts,
|
|
672
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
673
|
-
dimColor: true,
|
|
674
|
-
children: [
|
|
675
|
-
" · ",
|
|
676
|
-
position,
|
|
677
|
-
"/",
|
|
678
|
-
groupCount
|
|
679
|
-
]
|
|
680
|
-
}),
|
|
681
|
-
projectCount !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
682
|
-
dimColor: true,
|
|
683
|
-
children: [
|
|
684
|
-
" · ",
|
|
685
|
-
projectCount,
|
|
686
|
-
" ",
|
|
687
|
-
projectCount === 1 ? "project" : "projects"
|
|
688
|
-
]
|
|
689
|
-
}) : null
|
|
690
|
-
]
|
|
691
|
-
}), /* @__PURE__ */ jsxs(Text, {
|
|
692
|
-
wrap: "truncate-end",
|
|
693
|
-
children: [
|
|
694
|
-
unreadCount !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
695
|
-
color: unreadCount > 0 ? "cyan" : void 0,
|
|
696
|
-
dimColor: unreadCount === 0,
|
|
697
|
-
children: [
|
|
698
|
-
unreadCount,
|
|
699
|
-
" unread",
|
|
700
|
-
" · "
|
|
701
|
-
]
|
|
702
|
-
}) : null,
|
|
703
|
-
keyHints,
|
|
704
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
705
|
-
dimColor: true,
|
|
706
|
-
children: [" · ", exitHint]
|
|
707
|
-
})
|
|
708
|
-
]
|
|
709
|
-
})]
|
|
710
|
-
});
|
|
711
|
-
};
|
|
712
|
-
//#endregion
|
|
713
|
-
//#region src/cli/ink/components/diagnostic-list.tsx
|
|
714
|
-
const ADD_TO_CI_KEY = "a";
|
|
715
|
-
const sumSites = (rows) => rows.reduce((total, row) => total + row.siteCount, 0);
|
|
716
|
-
const renderEntry = (entry, entryIndex, selectedIndex, readRuleKeys) => {
|
|
717
|
-
if (entry.kind === "header") return /* @__PURE__ */ jsx(Text, {
|
|
718
|
-
bold: true,
|
|
719
|
-
wrap: "truncate-end",
|
|
720
|
-
children: entry.category
|
|
721
|
-
}, `header:${entry.category}`);
|
|
722
|
-
return /* @__PURE__ */ jsx(DiagnosticItem, {
|
|
723
|
-
row: entry.row,
|
|
724
|
-
isSelected: entryIndex === selectedIndex,
|
|
725
|
-
isRead: readRuleKeys.has(entry.row.ruleKey)
|
|
726
|
-
}, entry.row.ruleKey);
|
|
727
|
-
};
|
|
728
|
-
const DiagnosticList = ({ header, rows, width, listColumnWidth, detailColumnWidth, listHeight, detailHeight, layout, rootDirectory, projectName, launchableAgents, onHandoff, canAddToCi, onAddToCi, projectCount, onExit, exitHint }) => {
|
|
729
|
-
const entries = useMemo(() => buildDiagnosticListEntries(rows), [rows]);
|
|
730
|
-
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
731
|
-
const [menuIndex, setMenuIndex] = useState(0);
|
|
732
|
-
const isSplit = layout === "split";
|
|
733
|
-
const isCompact = layout === "compact";
|
|
734
|
-
const { rows: terminalRows } = useStdoutDimensions();
|
|
735
|
-
const { selectedIndex, visibleStart, visibleEnd } = useScrollViewport({
|
|
736
|
-
itemCount: entries.length,
|
|
737
|
-
height: listHeight,
|
|
738
|
-
isSelectable: (index) => entries[index]?.kind === "item",
|
|
739
|
-
isActive: !isMenuOpen
|
|
740
|
-
});
|
|
741
|
-
const visibleEntries = entries.slice(visibleStart, visibleEnd);
|
|
742
|
-
const selectedEntry = entries[selectedIndex];
|
|
743
|
-
const selected = selectedEntry?.kind === "item" ? selectedEntry.row : null;
|
|
744
|
-
const selectedRuleKey = selected?.ruleKey ?? null;
|
|
745
|
-
const initialSelectedRuleKey = useRef(selectedRuleKey);
|
|
746
|
-
const didRecordFindingNavigation = useRef(false);
|
|
747
|
-
const [readRuleKeys, setReadRuleKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
748
|
-
const [copiedRuleKey, setCopiedRuleKey] = useState(null);
|
|
749
|
-
const menuLabels = useMemo(() => ["Copy prompt", ...launchableAgents.map((agentId) => getSkillAgentConfig(agentId).displayName)], [launchableAgents]);
|
|
750
|
-
useEffect(() => {
|
|
751
|
-
if (!selectedRuleKey) return;
|
|
752
|
-
setReadRuleKeys((previous) => previous.has(selectedRuleKey) ? previous : new Set(previous).add(selectedRuleKey));
|
|
753
|
-
}, [selectedRuleKey]);
|
|
754
|
-
useEffect(() => {
|
|
755
|
-
if (!selectedRuleKey || selectedRuleKey === initialSelectedRuleKey.current || didRecordFindingNavigation.current) return;
|
|
756
|
-
didRecordFindingNavigation.current = true;
|
|
757
|
-
recordCount(METRIC.tuiFindingNavigated);
|
|
758
|
-
}, [selectedRuleKey]);
|
|
759
|
-
const copySelectedPrompt = () => {
|
|
760
|
-
if (!selected) return;
|
|
761
|
-
const prompt = buildIssuePrompt({
|
|
762
|
-
row: selected,
|
|
763
|
-
projectName
|
|
764
|
-
});
|
|
765
|
-
const ruleKey = selected.ruleKey;
|
|
766
|
-
copyToClipboard(prompt).then((didCopy) => {
|
|
767
|
-
if (didCopy) setCopiedRuleKey(ruleKey);
|
|
768
|
-
});
|
|
769
|
-
};
|
|
770
|
-
const launchSelectedInAgent = (agentId) => {
|
|
771
|
-
if (!selected || !onHandoff) return;
|
|
772
|
-
onHandoff({
|
|
773
|
-
agentId,
|
|
774
|
-
prompt: buildIssuePrompt({
|
|
775
|
-
row: selected,
|
|
776
|
-
projectName
|
|
777
|
-
})
|
|
778
|
-
});
|
|
779
|
-
onExit();
|
|
780
|
-
};
|
|
781
|
-
const runMenuItem = () => {
|
|
782
|
-
if (menuIndex === 0) {
|
|
783
|
-
copySelectedPrompt();
|
|
784
|
-
setIsMenuOpen(false);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const agentId = launchableAgents[menuIndex - 1];
|
|
788
|
-
if (agentId) launchSelectedInAgent(agentId);
|
|
789
|
-
};
|
|
790
|
-
const showCiCallout = Boolean(canAddToCi && onAddToCi);
|
|
791
|
-
useInput((input, key) => {
|
|
792
|
-
if (input === "q" || key.escape) return onExit();
|
|
793
|
-
if (showCiCallout && input === ADD_TO_CI_KEY) {
|
|
794
|
-
onAddToCi?.();
|
|
795
|
-
return onExit();
|
|
796
|
-
}
|
|
797
|
-
if (key.return && selected) {
|
|
798
|
-
recordCount(METRIC.tuiActionMenuOpened);
|
|
799
|
-
setMenuIndex(0);
|
|
800
|
-
setIsMenuOpen(true);
|
|
801
|
-
}
|
|
802
|
-
}, { isActive: !isMenuOpen });
|
|
803
|
-
useInput((input, key) => {
|
|
804
|
-
if (key.escape) return setIsMenuOpen(false);
|
|
805
|
-
if (key.upArrow || input === "k") return setMenuIndex((index) => Math.max(0, index - 1));
|
|
806
|
-
if (key.downArrow || input === "j") return setMenuIndex((index) => Math.min(menuLabels.length - 1, index + 1));
|
|
807
|
-
if (key.return) return runMenuItem();
|
|
808
|
-
}, { isActive: isMenuOpen });
|
|
809
|
-
if (isMenuOpen && selected) return /* @__PURE__ */ jsx(Box, {
|
|
810
|
-
width,
|
|
811
|
-
height: Math.max(6, terminalRows - 2),
|
|
812
|
-
justifyContent: "center",
|
|
813
|
-
alignItems: "center",
|
|
814
|
-
children: /* @__PURE__ */ jsx(DiagnosticActionMenu, {
|
|
815
|
-
title: selected.title,
|
|
816
|
-
itemLabels: menuLabels,
|
|
817
|
-
focusedIndex: menuIndex,
|
|
818
|
-
maxWidth: width
|
|
819
|
-
})
|
|
820
|
-
});
|
|
821
|
-
const errorRows = rows.filter((row) => row.severity === "error");
|
|
822
|
-
const warningRows = rows.filter((row) => row.severity === "warning");
|
|
823
|
-
const itemPosition = entries.slice(0, selectedIndex + 1).filter((entry) => entry.kind === "item").length;
|
|
824
|
-
const unreadCount = rows.length - rows.filter((row) => readRuleKeys.has(row.ruleKey)).length;
|
|
825
|
-
const listColumn = /* @__PURE__ */ jsx(Box, {
|
|
826
|
-
flexDirection: "column",
|
|
827
|
-
height: listHeight,
|
|
828
|
-
width: isSplit ? listColumnWidth : width,
|
|
829
|
-
children: visibleEntries.map((entry, index) => renderEntry(entry, visibleStart + index, selectedIndex, readRuleKeys))
|
|
830
|
-
});
|
|
831
|
-
const detailContent = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DiagnosticDetail, {
|
|
832
|
-
row: selected,
|
|
833
|
-
rootDirectory
|
|
834
|
-
}), copiedRuleKey === selectedRuleKey ? /* @__PURE__ */ jsx(Box, {
|
|
835
|
-
marginTop: 1,
|
|
836
|
-
children: /* @__PURE__ */ jsx(Text, {
|
|
837
|
-
color: "green",
|
|
838
|
-
children: "✓ Copied fix prompt"
|
|
839
|
-
})
|
|
840
|
-
}) : null] });
|
|
841
|
-
const keyHints = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
842
|
-
/* @__PURE__ */ jsx(Text, {
|
|
843
|
-
dimColor: true,
|
|
844
|
-
children: "↑/↓ move · "
|
|
845
|
-
}),
|
|
846
|
-
/* @__PURE__ */ jsx(Text, {
|
|
847
|
-
color: "cyan",
|
|
848
|
-
children: "enter"
|
|
849
|
-
}),
|
|
850
|
-
/* @__PURE__ */ jsx(Text, {
|
|
851
|
-
dimColor: true,
|
|
852
|
-
children: " fix this"
|
|
853
|
-
}),
|
|
854
|
-
showCiCallout ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
855
|
-
/* @__PURE__ */ jsx(Text, {
|
|
856
|
-
dimColor: true,
|
|
857
|
-
children: " · "
|
|
858
|
-
}),
|
|
859
|
-
/* @__PURE__ */ jsx(Text, {
|
|
860
|
-
color: "green",
|
|
861
|
-
children: ADD_TO_CI_KEY
|
|
862
|
-
}),
|
|
863
|
-
/* @__PURE__ */ jsx(Text, {
|
|
864
|
-
dimColor: true,
|
|
865
|
-
children: " add CI"
|
|
866
|
-
})
|
|
867
|
-
] }) : null
|
|
868
|
-
] });
|
|
869
|
-
const statusBar = /* @__PURE__ */ jsx(Box, {
|
|
870
|
-
marginTop: isCompact ? 0 : 1,
|
|
871
|
-
children: /* @__PURE__ */ jsx(StatusBar, {
|
|
872
|
-
total: sumSites(rows),
|
|
873
|
-
errorCount: sumSites(errorRows),
|
|
874
|
-
warningCount: sumSites(warningRows),
|
|
875
|
-
position: rows.length === 0 ? 0 : itemPosition,
|
|
876
|
-
groupCount: rows.length,
|
|
877
|
-
unreadCount,
|
|
878
|
-
projectCount,
|
|
879
|
-
keyHints: isCompact && copiedRuleKey === selectedRuleKey ? /* @__PURE__ */ jsx(Text, {
|
|
880
|
-
color: "green",
|
|
881
|
-
children: "✓ Copied fix prompt"
|
|
882
|
-
}) : keyHints,
|
|
883
|
-
exitHint,
|
|
884
|
-
compact: isCompact
|
|
885
|
-
})
|
|
886
|
-
});
|
|
887
|
-
if (isSplit) return /* @__PURE__ */ jsxs(Box, {
|
|
888
|
-
flexDirection: "column",
|
|
889
|
-
width,
|
|
890
|
-
children: [/* @__PURE__ */ jsxs(Box, {
|
|
891
|
-
flexDirection: "row",
|
|
892
|
-
children: [/* @__PURE__ */ jsxs(Box, {
|
|
893
|
-
flexDirection: "column",
|
|
894
|
-
width: listColumnWidth,
|
|
895
|
-
marginRight: 1,
|
|
896
|
-
children: [header, /* @__PURE__ */ jsx(Box, {
|
|
897
|
-
marginTop: 1,
|
|
898
|
-
children: listColumn
|
|
899
|
-
})]
|
|
900
|
-
}), /* @__PURE__ */ jsx(Box, {
|
|
901
|
-
flexDirection: "column",
|
|
902
|
-
width: detailColumnWidth,
|
|
903
|
-
borderStyle: "single",
|
|
904
|
-
borderColor: "gray",
|
|
905
|
-
borderTop: false,
|
|
906
|
-
borderRight: false,
|
|
907
|
-
borderBottom: false,
|
|
908
|
-
paddingLeft: 1,
|
|
909
|
-
height: detailHeight,
|
|
910
|
-
overflowY: "hidden",
|
|
911
|
-
children: detailContent
|
|
912
|
-
})]
|
|
913
|
-
}), statusBar]
|
|
914
|
-
});
|
|
915
|
-
if (isCompact) return /* @__PURE__ */ jsxs(Box, {
|
|
916
|
-
flexDirection: "column",
|
|
917
|
-
width,
|
|
918
|
-
children: [
|
|
919
|
-
header,
|
|
920
|
-
listColumn,
|
|
921
|
-
statusBar
|
|
922
|
-
]
|
|
923
|
-
});
|
|
924
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
925
|
-
flexDirection: "column",
|
|
926
|
-
width,
|
|
927
|
-
children: [
|
|
928
|
-
header,
|
|
929
|
-
/* @__PURE__ */ jsx(Box, {
|
|
930
|
-
marginTop: 1,
|
|
931
|
-
children: listColumn
|
|
932
|
-
}),
|
|
933
|
-
/* @__PURE__ */ jsx(Text, {
|
|
934
|
-
dimColor: true,
|
|
935
|
-
children: "─".repeat(width)
|
|
936
|
-
}),
|
|
937
|
-
/* @__PURE__ */ jsx(Box, {
|
|
938
|
-
flexDirection: "column",
|
|
939
|
-
height: detailHeight,
|
|
940
|
-
overflowY: "hidden",
|
|
941
|
-
children: detailContent
|
|
942
|
-
}),
|
|
943
|
-
statusBar
|
|
944
|
-
]
|
|
945
|
-
});
|
|
946
|
-
};
|
|
947
|
-
//#endregion
|
|
948
|
-
//#region src/cli/ink/hooks/use-animated-score.ts
|
|
949
|
-
const useAnimatedScore = ({ score, projectedScore, animate }) => {
|
|
950
|
-
const projectionTarget = projectedScore ?? score;
|
|
951
|
-
const hasProjection = projectionTarget > score;
|
|
952
|
-
const [displayScore, setDisplayScore] = useState(animate ? 0 : score);
|
|
953
|
-
const [displayProjectedScore, setDisplayProjectedScore] = useState(animate ? null : hasProjection ? projectionTarget : null);
|
|
954
|
-
useEffect(() => {
|
|
955
|
-
if (!animate) {
|
|
956
|
-
setDisplayScore(score);
|
|
957
|
-
setDisplayProjectedScore(hasProjection ? projectionTarget : null);
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
let timeoutId;
|
|
961
|
-
const runProjection = () => {
|
|
962
|
-
let frame = 1;
|
|
963
|
-
const tick = () => {
|
|
964
|
-
const progress = easeOutCubic(frame / 16);
|
|
965
|
-
setDisplayProjectedScore(score + (projectionTarget - score) * progress);
|
|
966
|
-
if (frame < 16) {
|
|
967
|
-
frame += 1;
|
|
968
|
-
timeoutId = setTimeout(tick, 35);
|
|
969
|
-
} else setDisplayProjectedScore(projectionTarget);
|
|
970
|
-
};
|
|
971
|
-
tick();
|
|
972
|
-
};
|
|
973
|
-
let frame = 0;
|
|
974
|
-
const tick = () => {
|
|
975
|
-
const progress = easeOutCubic(frame / 40);
|
|
976
|
-
setDisplayScore(Math.round(score * progress));
|
|
977
|
-
if (frame < 40) {
|
|
978
|
-
frame += 1;
|
|
979
|
-
timeoutId = setTimeout(tick, 50);
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
setDisplayScore(score);
|
|
983
|
-
if (hasProjection) runProjection();
|
|
984
|
-
};
|
|
985
|
-
tick();
|
|
986
|
-
return () => clearTimeout(timeoutId);
|
|
987
|
-
}, [
|
|
988
|
-
animate,
|
|
989
|
-
score,
|
|
990
|
-
projectionTarget,
|
|
991
|
-
hasProjection
|
|
992
|
-
]);
|
|
993
|
-
return {
|
|
994
|
-
displayScore,
|
|
995
|
-
displayProjectedScore
|
|
996
|
-
};
|
|
997
|
-
};
|
|
998
|
-
//#endregion
|
|
999
|
-
//#region src/cli/ink/lib/score-color.ts
|
|
1000
|
-
const scoreColorName = (score) => {
|
|
1001
|
-
if (score >= 75) return "green";
|
|
1002
|
-
if (score >= 50) return "yellow";
|
|
1003
|
-
return "red";
|
|
1004
|
-
};
|
|
1005
|
-
//#endregion
|
|
1006
|
-
//#region src/cli/ink/components/score-header.tsx
|
|
1007
|
-
const BRANDING = "https://react.doctor";
|
|
1008
|
-
const ScoreHeader = ({ score, projectedScore, projectName, issueCount, noScoreMessage, width, compact = false }) => {
|
|
1009
|
-
const { columns } = useStdoutDimensions();
|
|
1010
|
-
const availableWidth = width ?? columns;
|
|
1011
|
-
const { stdout } = useStdout();
|
|
1012
|
-
const animate = useMemo(() => canAnimateOnboarding(stdout ?? void 0), [stdout]);
|
|
1013
|
-
const { displayScore, displayProjectedScore } = useAnimatedScore({
|
|
1014
|
-
score: score?.score ?? 0,
|
|
1015
|
-
projectedScore,
|
|
1016
|
-
animate: animate && score !== null
|
|
1017
|
-
});
|
|
1018
|
-
if (compact) return score ? /* @__PURE__ */ jsxs(Text, {
|
|
1019
|
-
wrap: "truncate-end",
|
|
1020
|
-
children: [
|
|
1021
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1022
|
-
color: scoreColorName(score.score),
|
|
1023
|
-
bold: true,
|
|
1024
|
-
children: displayScore
|
|
1025
|
-
}),
|
|
1026
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1027
|
-
dimColor: true,
|
|
1028
|
-
children: [
|
|
1029
|
-
" / ",
|
|
1030
|
-
100,
|
|
1031
|
-
" "
|
|
1032
|
-
]
|
|
1033
|
-
}),
|
|
1034
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1035
|
-
color: scoreColorName(score.score),
|
|
1036
|
-
children: score.label
|
|
1037
|
-
}),
|
|
1038
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1039
|
-
dimColor: true,
|
|
1040
|
-
children: [" · ", projectName]
|
|
1041
|
-
})
|
|
1042
|
-
]
|
|
1043
|
-
}) : /* @__PURE__ */ jsxs(Text, {
|
|
1044
|
-
wrap: "truncate-end",
|
|
1045
|
-
children: [
|
|
1046
|
-
"React Doctor",
|
|
1047
|
-
" ",
|
|
1048
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1049
|
-
dimColor: true,
|
|
1050
|
-
children: ["· ", noScoreMessage ?? `${issueCount} issues · ${projectName}`]
|
|
1051
|
-
})
|
|
1052
|
-
]
|
|
1053
|
-
});
|
|
1054
|
-
if (!score) return /* @__PURE__ */ jsxs(Box, {
|
|
1055
|
-
flexDirection: "column",
|
|
1056
|
-
paddingLeft: 2,
|
|
1057
|
-
children: [/* @__PURE__ */ jsxs(Text, { children: ["React Doctor ", /* @__PURE__ */ jsxs(Text, {
|
|
1058
|
-
dimColor: true,
|
|
1059
|
-
children: [
|
|
1060
|
-
"(",
|
|
1061
|
-
BRANDING,
|
|
1062
|
-
")"
|
|
1063
|
-
]
|
|
1064
|
-
})] }), /* @__PURE__ */ jsx(Text, {
|
|
1065
|
-
dimColor: true,
|
|
1066
|
-
children: noScoreMessage ?? `${issueCount} issues · ${projectName}`
|
|
1067
|
-
})]
|
|
1068
|
-
});
|
|
1069
|
-
const color = scoreColorName(score.score);
|
|
1070
|
-
const barWidth = Math.max(10, Math.min(50, availableWidth - 11 - 2));
|
|
1071
|
-
const filled = Math.round(displayScore / 100 * barWidth);
|
|
1072
|
-
const projectedFill = displayProjectedScore != null ? Math.min(barWidth, Math.round(displayProjectedScore / 100 * barWidth)) : filled;
|
|
1073
|
-
const gain = Math.max(0, projectedFill - filled);
|
|
1074
|
-
const empty = Math.max(0, barWidth - filled - gain);
|
|
1075
|
-
const [eyes, mouth] = doctorFace(score.score);
|
|
1076
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
1077
|
-
flexDirection: "column",
|
|
1078
|
-
children: [/* @__PURE__ */ jsxs(Box, {
|
|
1079
|
-
paddingLeft: 2,
|
|
1080
|
-
children: [/* @__PURE__ */ jsxs(Box, {
|
|
1081
|
-
flexDirection: "column",
|
|
1082
|
-
marginRight: 2,
|
|
1083
|
-
children: [
|
|
1084
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1085
|
-
color,
|
|
1086
|
-
children: "┌─────┐"
|
|
1087
|
-
}),
|
|
1088
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1089
|
-
color,
|
|
1090
|
-
children: [
|
|
1091
|
-
"│ ",
|
|
1092
|
-
eyes,
|
|
1093
|
-
" │"
|
|
1094
|
-
]
|
|
1095
|
-
}),
|
|
1096
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1097
|
-
color,
|
|
1098
|
-
children: [
|
|
1099
|
-
"│ ",
|
|
1100
|
-
mouth,
|
|
1101
|
-
" │"
|
|
1102
|
-
]
|
|
1103
|
-
}),
|
|
1104
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1105
|
-
color,
|
|
1106
|
-
children: "└─────┘"
|
|
1107
|
-
})
|
|
1108
|
-
]
|
|
1109
|
-
}), /* @__PURE__ */ jsxs(Box, {
|
|
1110
|
-
flexDirection: "column",
|
|
1111
|
-
children: [
|
|
1112
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1113
|
-
wrap: "truncate-end",
|
|
1114
|
-
children: [
|
|
1115
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1116
|
-
color,
|
|
1117
|
-
bold: true,
|
|
1118
|
-
children: displayScore
|
|
1119
|
-
}),
|
|
1120
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1121
|
-
dimColor: true,
|
|
1122
|
-
children: [
|
|
1123
|
-
" / ",
|
|
1124
|
-
100,
|
|
1125
|
-
" "
|
|
1126
|
-
]
|
|
1127
|
-
}),
|
|
1128
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1129
|
-
color,
|
|
1130
|
-
children: score.label
|
|
1131
|
-
}),
|
|
1132
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1133
|
-
dimColor: true,
|
|
1134
|
-
children: [" · ", projectName]
|
|
1135
|
-
})
|
|
1136
|
-
]
|
|
1137
|
-
}),
|
|
1138
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1139
|
-
wrap: "truncate-end",
|
|
1140
|
-
children: [
|
|
1141
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1142
|
-
color,
|
|
1143
|
-
children: "█".repeat(filled)
|
|
1144
|
-
}),
|
|
1145
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1146
|
-
color,
|
|
1147
|
-
dimColor: true,
|
|
1148
|
-
children: "▓".repeat(gain)
|
|
1149
|
-
}),
|
|
1150
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1151
|
-
dimColor: true,
|
|
1152
|
-
children: "░".repeat(empty)
|
|
1153
|
-
})
|
|
1154
|
-
]
|
|
1155
|
-
}),
|
|
1156
|
-
/* @__PURE__ */ jsxs(Text, { children: ["React Doctor ", /* @__PURE__ */ jsxs(Text, {
|
|
1157
|
-
dimColor: true,
|
|
1158
|
-
children: [
|
|
1159
|
-
"(",
|
|
1160
|
-
BRANDING,
|
|
1161
|
-
")"
|
|
1162
|
-
]
|
|
1163
|
-
})] }),
|
|
1164
|
-
/* @__PURE__ */ jsx(Text, { children: " " })
|
|
1165
|
-
]
|
|
1166
|
-
})]
|
|
1167
|
-
}), projectedScore != null && projectedScore > score.score ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
1168
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1169
|
-
dimColor: true,
|
|
1170
|
-
children: " You could improve "
|
|
1171
|
-
}),
|
|
1172
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1173
|
-
color: scoreColorName(projectedScore),
|
|
1174
|
-
children: [
|
|
1175
|
-
"+",
|
|
1176
|
-
projectedScore - score.score,
|
|
1177
|
-
"%"
|
|
1178
|
-
]
|
|
1179
|
-
}),
|
|
1180
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1181
|
-
dimColor: true,
|
|
1182
|
-
children: ` by fixing the top 3 issues`
|
|
1183
|
-
})
|
|
1184
|
-
] }) : null]
|
|
1185
|
-
});
|
|
1186
|
-
};
|
|
1187
|
-
//#endregion
|
|
1188
|
-
//#region src/cli/ink/components/report.tsx
|
|
1189
|
-
const STACKED_FIXED_ROWS = 11;
|
|
1190
|
-
const SPLIT_CHROME_ROWS = 10;
|
|
1191
|
-
const EMPTY_LAUNCHABLE_AGENTS = [];
|
|
1192
|
-
const Report = ({ report, onExit, launchableAgents = EMPTY_LAUNCHABLE_AGENTS, onHandoff, canAddToCi, onAddToCi, projectCount, priorityScores, exitHint = "q to quit" }) => {
|
|
1193
|
-
const { rows: terminalRows, columns } = useStdoutDimensions();
|
|
1194
|
-
const diagnosticRows = useMemo(() => buildDiagnosticRows(report.diagnostics, priorityScores ?? [report.score]), [
|
|
1195
|
-
report.diagnostics,
|
|
1196
|
-
report.score,
|
|
1197
|
-
priorityScores
|
|
1198
|
-
]);
|
|
1199
|
-
useInput((input, key) => {
|
|
1200
|
-
if (input === "q" || key.escape) onExit();
|
|
1201
|
-
}, { isActive: diagnosticRows.length === 0 });
|
|
1202
|
-
const width = Math.max(24, columns - 2);
|
|
1203
|
-
const isWide = columns >= 120 && terminalRows >= 22;
|
|
1204
|
-
const isCompact = !isWide && terminalRows <= TUI_REPORT_COMPACT_MAX_ROWS;
|
|
1205
|
-
const didRecordCompactReport = useRef(false);
|
|
1206
|
-
const didRecordStackedReportCap = useRef(false);
|
|
1207
|
-
useEffect(() => {
|
|
1208
|
-
if (!isCompact || didRecordCompactReport.current) return;
|
|
1209
|
-
didRecordCompactReport.current = true;
|
|
1210
|
-
recordCount(METRIC.tuiCompactReportShown);
|
|
1211
|
-
}, [isCompact]);
|
|
1212
|
-
const detailHeight = isWide ? Math.max(0, terminalRows - 3) : Math.max(0, Math.min(15, terminalRows - STACKED_FIXED_ROWS - 3));
|
|
1213
|
-
const availableListHeight = Math.max(3, terminalRows - (isWide ? SPLIT_CHROME_ROWS : STACKED_FIXED_ROWS + detailHeight));
|
|
1214
|
-
const isStackedReportCapped = !isCompact && !isWide && availableListHeight > 16;
|
|
1215
|
-
useEffect(() => {
|
|
1216
|
-
if (!isStackedReportCapped || didRecordStackedReportCap.current) return;
|
|
1217
|
-
didRecordStackedReportCap.current = true;
|
|
1218
|
-
recordCount(METRIC.tuiStackedReportCapped);
|
|
1219
|
-
}, [isStackedReportCapped]);
|
|
1220
|
-
let listHeight = availableListHeight;
|
|
1221
|
-
if (isCompact) listHeight = Math.max(3, terminalRows - 1 - 1);
|
|
1222
|
-
else if (!isWide) listHeight = Math.min(16, availableListHeight);
|
|
1223
|
-
const detailColumnWidth = Math.max(20, Math.floor(width * TUI_REPORT_DETAIL_WIDTH_FRACTION));
|
|
1224
|
-
const listColumnWidth = Math.max(20, width - detailColumnWidth - 3);
|
|
1225
|
-
let layout = "stacked";
|
|
1226
|
-
if (isCompact) layout = "compact";
|
|
1227
|
-
else if (isWide) layout = "split";
|
|
1228
|
-
const scoreHeader = /* @__PURE__ */ jsx(ScoreHeader, {
|
|
1229
|
-
score: report.score,
|
|
1230
|
-
projectedScore: report.projectedScore,
|
|
1231
|
-
projectName: report.projectName,
|
|
1232
|
-
issueCount: report.diagnostics.length,
|
|
1233
|
-
noScoreMessage: report.noScoreMessage,
|
|
1234
|
-
width: isWide ? listColumnWidth : width,
|
|
1235
|
-
compact: isCompact
|
|
1236
|
-
});
|
|
1237
|
-
if (diagnosticRows.length === 0) {
|
|
1238
|
-
const lintFailureReason = report.lintFailureReason;
|
|
1239
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
1240
|
-
flexDirection: "column",
|
|
1241
|
-
children: [
|
|
1242
|
-
scoreHeader,
|
|
1243
|
-
/* @__PURE__ */ jsx(Box, {
|
|
1244
|
-
marginTop: 1,
|
|
1245
|
-
children: lintFailureReason ? /* @__PURE__ */ jsxs(Text, {
|
|
1246
|
-
color: "yellow",
|
|
1247
|
-
children: ["⚠ Lint did not run: ", lintFailureReason]
|
|
1248
|
-
}) : /* @__PURE__ */ jsx(Text, {
|
|
1249
|
-
color: "green",
|
|
1250
|
-
children: "✔ No issues found. Nice work."
|
|
1251
|
-
})
|
|
1252
|
-
}),
|
|
1253
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1254
|
-
dimColor: true,
|
|
1255
|
-
children: exitHint
|
|
1256
|
-
})
|
|
1257
|
-
]
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
return /* @__PURE__ */ jsx(DiagnosticList, {
|
|
1261
|
-
header: scoreHeader,
|
|
1262
|
-
rows: diagnosticRows,
|
|
1263
|
-
width,
|
|
1264
|
-
listColumnWidth,
|
|
1265
|
-
detailColumnWidth,
|
|
1266
|
-
listHeight,
|
|
1267
|
-
detailHeight,
|
|
1268
|
-
layout,
|
|
1269
|
-
rootDirectory: report.rootDirectory,
|
|
1270
|
-
projectName: report.projectName,
|
|
1271
|
-
launchableAgents,
|
|
1272
|
-
onHandoff,
|
|
1273
|
-
canAddToCi,
|
|
1274
|
-
onAddToCi,
|
|
1275
|
-
projectCount,
|
|
1276
|
-
onExit,
|
|
1277
|
-
exitHint
|
|
1278
|
-
});
|
|
1279
|
-
};
|
|
1280
|
-
//#endregion
|
|
1281
|
-
//#region src/cli/ink/components/scanning.tsx
|
|
1282
|
-
const Scanning = ({ progressText, liveCount, recent }) => {
|
|
1283
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
1284
|
-
flexDirection: "column",
|
|
1285
|
-
children: [/* @__PURE__ */ jsxs(Text, {
|
|
1286
|
-
wrap: "truncate-end",
|
|
1287
|
-
children: [
|
|
1288
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1289
|
-
color: "cyan",
|
|
1290
|
-
children: /* @__PURE__ */ jsx(Spinner, { type: "dots" })
|
|
1291
|
-
}),
|
|
1292
|
-
/* @__PURE__ */ jsxs(Text, { children: [" ", progressText ?? "Scanning…"] }),
|
|
1293
|
-
liveCount > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
1294
|
-
dimColor: true,
|
|
1295
|
-
children: [
|
|
1296
|
-
" · ",
|
|
1297
|
-
liveCount,
|
|
1298
|
-
" found"
|
|
1299
|
-
]
|
|
1300
|
-
}) : null
|
|
1301
|
-
]
|
|
1302
|
-
}), recent.map((diagnostic, diagnosticIndex) => {
|
|
1303
|
-
const variant = severityVariant(diagnostic.severity === "error" ? "error" : "warning");
|
|
1304
|
-
const location = formatDiagnosticSite(diagnostic);
|
|
1305
|
-
return /* @__PURE__ */ jsxs(Text, {
|
|
1306
|
-
wrap: "truncate-end",
|
|
1307
|
-
children: [
|
|
1308
|
-
" ",
|
|
1309
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1310
|
-
color: variant.color,
|
|
1311
|
-
children: variant.icon
|
|
1312
|
-
}),
|
|
1313
|
-
/* @__PURE__ */ jsxs(Text, { children: [" ", diagnostic.title ?? `${diagnostic.plugin}/${diagnostic.rule}`] }),
|
|
1314
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
1315
|
-
dimColor: true,
|
|
1316
|
-
children: [" ", location]
|
|
1317
|
-
})
|
|
1318
|
-
]
|
|
1319
|
-
}, `${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.plugin}:${diagnostic.rule}:${diagnosticIndex}`);
|
|
1320
|
-
})]
|
|
1321
|
-
});
|
|
1322
|
-
};
|
|
1323
|
-
//#endregion
|
|
1324
|
-
//#region src/cli/ink/components/summary.tsx
|
|
1325
|
-
const Summary = ({ summary, onExit, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
|
|
1326
|
-
return /* @__PURE__ */ jsx(Report, {
|
|
1327
|
-
report: {
|
|
1328
|
-
diagnostics: summary.combinedDiagnostics,
|
|
1329
|
-
score: summary.aggregateScore,
|
|
1330
|
-
projectedScore: summary.projectedScore,
|
|
1331
|
-
projectName: summary.projectName,
|
|
1332
|
-
rootDirectory: summary.rootDirectory,
|
|
1333
|
-
scannedFileCount: summary.scannedFileCount,
|
|
1334
|
-
elapsedMilliseconds: summary.elapsedMilliseconds,
|
|
1335
|
-
isOffline: summary.isOffline,
|
|
1336
|
-
noScoreMessage: summary.noScoreMessage,
|
|
1337
|
-
...summary.lintFailureReason ? { lintFailureReason: summary.lintFailureReason } : {}
|
|
1338
|
-
},
|
|
1339
|
-
onExit,
|
|
1340
|
-
launchableAgents,
|
|
1341
|
-
onHandoff,
|
|
1342
|
-
canAddToCi,
|
|
1343
|
-
onAddToCi,
|
|
1344
|
-
projectCount: summary.projects.length,
|
|
1345
|
-
priorityScores: summary.projects.map((project) => project.score)
|
|
1346
|
-
});
|
|
1347
|
-
};
|
|
1348
|
-
//#endregion
|
|
1349
|
-
//#region src/cli/ink/hooks/use-scan-store.ts
|
|
1350
|
-
const useScanStore = (store) => useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1351
|
-
//#endregion
|
|
1352
|
-
//#region src/cli/ink/scan-app.tsx
|
|
1353
|
-
const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
|
|
1354
|
-
const snapshot = useScanStore(store);
|
|
1355
|
-
const { exit } = useApp();
|
|
1356
|
-
useExitOnCtrlC();
|
|
1357
|
-
if (snapshot.phase === "summary" && snapshot.summary) return /* @__PURE__ */ jsx(Summary, {
|
|
1358
|
-
summary: snapshot.summary,
|
|
1359
|
-
launchableAgents,
|
|
1360
|
-
onHandoff,
|
|
1361
|
-
canAddToCi,
|
|
1362
|
-
onAddToCi,
|
|
1363
|
-
onExit: () => exit()
|
|
1364
|
-
});
|
|
1365
|
-
if (snapshot.phase === "report" && snapshot.report) return /* @__PURE__ */ jsx(Report, {
|
|
1366
|
-
report: snapshot.report,
|
|
1367
|
-
launchableAgents,
|
|
1368
|
-
onHandoff,
|
|
1369
|
-
canAddToCi,
|
|
1370
|
-
onAddToCi,
|
|
1371
|
-
onExit: () => exit()
|
|
1372
|
-
});
|
|
1373
|
-
return /* @__PURE__ */ jsx(Scanning, {
|
|
1374
|
-
progressText: snapshot.progress,
|
|
1375
|
-
liveCount: snapshot.liveCount,
|
|
1376
|
-
recent: snapshot.liveDiagnostics.slice(-5)
|
|
1377
|
-
});
|
|
1378
|
-
};
|
|
1379
|
-
//#endregion
|
|
1380
|
-
//#region src/cli/ink/scan-bridge-layers.ts
|
|
1381
|
-
const reporterLayerForStore = (store) => succeed(Reporter, Reporter.of({
|
|
1382
|
-
emit: (diagnostic) => sync(() => store.emitDiagnostic(diagnostic)),
|
|
1383
|
-
finalize: void_
|
|
1384
|
-
}));
|
|
1385
|
-
const progressHandleForStore = (store) => (text) => {
|
|
1386
|
-
store.setProgress(text);
|
|
1387
|
-
return {
|
|
1388
|
-
update: (displayText) => sync(() => store.setProgress(displayText)),
|
|
1389
|
-
succeed: (displayText) => sync(() => store.setProgress(displayText)),
|
|
1390
|
-
fail: (displayText) => sync(() => store.setProgress(displayText)),
|
|
1391
|
-
stop: () => sync(() => store.setProgress(null))
|
|
1392
|
-
};
|
|
1393
|
-
};
|
|
1394
|
-
const progressLayerForStore = (store) => Progress.layerOra(progressHandleForStore(store));
|
|
1395
|
-
//#endregion
|
|
1396
|
-
//#region src/cli/ink/scan-store.ts
|
|
1397
|
-
const INITIAL_SNAPSHOT = {
|
|
1398
|
-
phase: "scanning",
|
|
1399
|
-
liveDiagnostics: [],
|
|
1400
|
-
liveCount: 0,
|
|
1401
|
-
progress: null,
|
|
1402
|
-
report: null,
|
|
1403
|
-
summary: null
|
|
1404
|
-
};
|
|
1405
|
-
const createScanStore = () => {
|
|
1406
|
-
let snapshot = INITIAL_SNAPSHOT;
|
|
1407
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1408
|
-
const commit = (next) => {
|
|
1409
|
-
snapshot = next;
|
|
1410
|
-
for (const listener of listeners) listener();
|
|
1411
|
-
};
|
|
1412
|
-
return {
|
|
1413
|
-
subscribe: (listener) => {
|
|
1414
|
-
listeners.add(listener);
|
|
1415
|
-
return () => {
|
|
1416
|
-
listeners.delete(listener);
|
|
1417
|
-
};
|
|
1418
|
-
},
|
|
1419
|
-
getSnapshot: () => snapshot,
|
|
1420
|
-
emitDiagnostic: (diagnostic) => commit({
|
|
1421
|
-
...snapshot,
|
|
1422
|
-
liveDiagnostics: [...snapshot.liveDiagnostics, diagnostic].slice(-25),
|
|
1423
|
-
liveCount: snapshot.liveCount + 1
|
|
1424
|
-
}),
|
|
1425
|
-
setProgress: (progress) => commit({
|
|
1426
|
-
...snapshot,
|
|
1427
|
-
progress
|
|
1428
|
-
}),
|
|
1429
|
-
setReport: (report) => commit({
|
|
1430
|
-
...snapshot,
|
|
1431
|
-
report,
|
|
1432
|
-
phase: "report"
|
|
1433
|
-
}),
|
|
1434
|
-
setSummary: (summary) => commit({
|
|
1435
|
-
...snapshot,
|
|
1436
|
-
summary,
|
|
1437
|
-
phase: "summary"
|
|
1438
|
-
})
|
|
1439
|
-
};
|
|
1440
|
-
};
|
|
1441
|
-
//#endregion
|
|
1442
|
-
//#region src/cli/ink/run-scan-app.tsx
|
|
1443
|
-
const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
|
|
1444
|
-
const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
|
|
1445
|
-
const config = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0);
|
|
1446
|
-
const configSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
|
|
1447
|
-
return {
|
|
1448
|
-
directory: projectScanTarget.resolvedDirectory,
|
|
1449
|
-
config,
|
|
1450
|
-
configSourceDirectory
|
|
1451
|
-
};
|
|
1452
|
-
};
|
|
1453
|
-
const qualifyDiagnosticPaths = (diagnostics, rootDirectory, projectDirectory) => {
|
|
1454
|
-
const prefix = path.relative(rootDirectory, projectDirectory);
|
|
1455
|
-
if (prefix === "" || prefix.startsWith("..")) return [...diagnostics];
|
|
1456
|
-
return diagnostics.map((diagnostic) => path.isAbsolute(diagnostic.filePath) ? diagnostic : {
|
|
1457
|
-
...diagnostic,
|
|
1458
|
-
filePath: toForwardSlashes(path.join(prefix, diagnostic.filePath))
|
|
1459
|
-
});
|
|
1460
|
-
};
|
|
1461
|
-
const resolveScanPresentation = (input, projectScans) => {
|
|
1462
|
-
const isScoreDisabled = input.options?.noScore === true || projectScans.some((projectScan) => projectScan.config?.noScore === true);
|
|
1463
|
-
return {
|
|
1464
|
-
isOffline: isCiEnvironment() || input.share === false || isShareOptedOut(projectScans, input.options?.noScore),
|
|
1465
|
-
noScoreMessage: buildNoScoreMessage(isScoreDisabled)
|
|
1466
|
-
};
|
|
1467
|
-
};
|
|
1468
|
-
const resolveTuiInspectOptions = (input, config) => {
|
|
1469
|
-
const warnings = resolveCliInspectOptions({
|
|
1470
|
-
blocking: input.blocking,
|
|
1471
|
-
warnings: input.options?.warnings
|
|
1472
|
-
}, config).warnings;
|
|
1473
|
-
return warnings === void 0 ? { ...input.options } : {
|
|
1474
|
-
...input.options,
|
|
1475
|
-
warnings
|
|
1476
|
-
};
|
|
1477
|
-
};
|
|
1478
|
-
const resolveSelectedDirectories = async (rootDirectory, input) => {
|
|
1479
|
-
const packages = discoverWorkspacePackages(rootDirectory);
|
|
1480
|
-
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);
|
|
1481
|
-
return promptProjectSelection(packages, rootDirectory);
|
|
1482
|
-
};
|
|
1483
|
-
const promptProjectSelection = (packages, rootDirectory) => new Promise((resolve) => {
|
|
1484
|
-
const instance = render(/* @__PURE__ */ jsx(ProjectSelect, {
|
|
1485
|
-
packages,
|
|
1486
|
-
rootDirectory,
|
|
1487
|
-
onSubmit: (directories) => {
|
|
1488
|
-
instance.clear();
|
|
1489
|
-
instance.unmount();
|
|
1490
|
-
resolve(directories);
|
|
1491
|
-
}
|
|
1492
|
-
}), { exitOnCtrlC: false });
|
|
1493
|
-
});
|
|
1494
|
-
const resolveLintFailureReason = (results) => {
|
|
1495
|
-
for (const result of results) {
|
|
1496
|
-
if (!hasLintHardFailure(result)) continue;
|
|
1497
|
-
return result.skippedCheckReasons?.lint ?? "Lint failed before diagnostics were produced.";
|
|
1498
|
-
}
|
|
1499
|
-
return null;
|
|
1500
|
-
};
|
|
1501
|
-
const toScanReport = ({ result, rootDirectory, projectedScore, isOffline, noScoreMessage }) => {
|
|
1502
|
-
const lintFailureReason = resolveLintFailureReason([result]);
|
|
1503
|
-
return {
|
|
1504
|
-
diagnostics: result.diagnostics,
|
|
1505
|
-
score: result.score,
|
|
1506
|
-
projectedScore,
|
|
1507
|
-
projectName: result.project.projectName,
|
|
1508
|
-
rootDirectory,
|
|
1509
|
-
scannedFileCount: result.scannedFileCount ?? 0,
|
|
1510
|
-
elapsedMilliseconds: result.elapsedMilliseconds,
|
|
1511
|
-
isOffline,
|
|
1512
|
-
noScoreMessage,
|
|
1513
|
-
...lintFailureReason ? { lintFailureReason } : {}
|
|
1514
|
-
};
|
|
1515
|
-
};
|
|
1516
|
-
const findLowestScored = (reports) => {
|
|
1517
|
-
let worst = null;
|
|
1518
|
-
for (const report of reports) {
|
|
1519
|
-
if (report.score === null) continue;
|
|
1520
|
-
if (worst === null || report.score.score < worst.score.score) worst = {
|
|
1521
|
-
score: report.score,
|
|
1522
|
-
diagnostics: report.diagnostics
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
return worst;
|
|
1526
|
-
};
|
|
1527
|
-
const printExitFooter = async (input) => {
|
|
1528
|
-
const fileLabel = input.scannedFileCount === 1 ? "file" : "files";
|
|
1529
|
-
process.stdout.write(`${highlighter.success("✔")} Scanned ${input.scannedFileCount} ${fileLabel} in ${formatElapsedTime(input.elapsedMilliseconds)}\n`);
|
|
1530
|
-
if (input.lintFailureReason !== null) process.stdout.write(`${highlighter.warn("⚠")} Lint did not run: ${input.lintFailureReason}\n`);
|
|
1531
|
-
await runPromise(printFooter({
|
|
1532
|
-
diagnostics: [...input.diagnostics],
|
|
1533
|
-
scoreResult: input.scoreResult,
|
|
1534
|
-
projectName: input.projectName,
|
|
1535
|
-
isOffline: input.isOffline
|
|
1536
|
-
}));
|
|
1537
|
-
};
|
|
1538
|
-
const performTuiHandoff = async (request, rootDirectory) => {
|
|
1539
|
-
try {
|
|
1540
|
-
await launchCliAgent(request.agentId, request.prompt, rootDirectory);
|
|
1541
|
-
} catch {
|
|
1542
|
-
process.stdout.write(`${highlighter.warn("⚠")} Couldn't launch ${CLI_AGENT_BINARIES[request.agentId]}. Here's the prompt instead:\n`);
|
|
1543
|
-
process.stdout.write(`${highlighter.dim("──── Agent prompt ────")}\n`);
|
|
1544
|
-
process.stdout.write(`${request.prompt}\n`);
|
|
1545
|
-
process.stdout.write(`${highlighter.dim("──────────────────────")}\n`);
|
|
1546
|
-
}
|
|
1547
|
-
};
|
|
1548
|
-
const isCiUnconfigured = (directory) => !isReactDoctorWorkflowInstalled(findNearestPackageDirectory(directory) ?? directory);
|
|
1549
|
-
const performCiSetup = async (rootDirectory) => {
|
|
1550
|
-
const didCreateWorkflow = await setUpGitHubActions({ rootDirectory });
|
|
1551
|
-
recordCount(METRIC.agentHandoff, 1, {
|
|
1552
|
-
outcome: "ci-yes",
|
|
1553
|
-
source: "tui",
|
|
1554
|
-
created: didCreateWorkflow
|
|
1555
|
-
});
|
|
1556
|
-
};
|
|
1557
|
-
const mountScanApp = async (rootDirectory) => {
|
|
1558
|
-
const store = createScanStore();
|
|
1559
|
-
const launchableAgents = await detectLaunchableAgents();
|
|
1560
|
-
const pending = {
|
|
1561
|
-
handoff: null,
|
|
1562
|
-
ciSetup: false
|
|
1563
|
-
};
|
|
1564
|
-
const instance = render(/* @__PURE__ */ jsx(ScanApp, {
|
|
1565
|
-
store,
|
|
1566
|
-
launchableAgents,
|
|
1567
|
-
onHandoff: (request) => {
|
|
1568
|
-
pending.handoff = request;
|
|
1569
|
-
},
|
|
1570
|
-
canAddToCi: isCiUnconfigured(rootDirectory),
|
|
1571
|
-
onAddToCi: () => {
|
|
1572
|
-
pending.ciSetup = true;
|
|
1573
|
-
}
|
|
1574
|
-
}), { exitOnCtrlC: false });
|
|
1575
|
-
const settle = async () => {
|
|
1576
|
-
if (pending.ciSetup) await performCiSetup(rootDirectory);
|
|
1577
|
-
if (pending.handoff) await performTuiHandoff(pending.handoff, rootDirectory);
|
|
1578
|
-
};
|
|
1579
|
-
return {
|
|
1580
|
-
store,
|
|
1581
|
-
instance,
|
|
1582
|
-
settle
|
|
1583
|
-
};
|
|
1584
|
-
};
|
|
1585
|
-
const runMountedScan = async (rootDirectory, presentation, blockingLevel, executeScan) => {
|
|
1586
|
-
const { store, instance, settle } = await mountScanApp(rootDirectory);
|
|
1587
|
-
const context = {
|
|
1588
|
-
store,
|
|
1589
|
-
...presentation
|
|
1590
|
-
};
|
|
1591
|
-
try {
|
|
1592
|
-
const completedScan = await executeScan(context);
|
|
1593
|
-
await instance.waitUntilExit();
|
|
1594
|
-
await printExitFooter({
|
|
1595
|
-
diagnostics: completedScan.diagnostics,
|
|
1596
|
-
scoreResult: completedScan.scoreResult,
|
|
1597
|
-
projectName: completedScan.projectName,
|
|
1598
|
-
scannedFileCount: completedScan.scannedFileCount,
|
|
1599
|
-
elapsedMilliseconds: completedScan.elapsedMilliseconds,
|
|
1600
|
-
isOffline: context.isOffline,
|
|
1601
|
-
lintFailureReason: resolveLintFailureReason(completedScan.scans.map(({ result }) => result))
|
|
1602
|
-
});
|
|
1603
|
-
await settle();
|
|
1604
|
-
return { shouldFail: shouldFailScanGate({
|
|
1605
|
-
scans: completedScan.scans,
|
|
1606
|
-
blockingLevel
|
|
1607
|
-
}) };
|
|
1608
|
-
} catch (error) {
|
|
1609
|
-
instance.unmount();
|
|
1610
|
-
throw error;
|
|
1611
|
-
}
|
|
1612
|
-
};
|
|
1613
|
-
const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blockingLevel, inspectProject) => {
|
|
1614
|
-
const projectScan = await resolveProjectScan(rootScanTarget, projectDirectory);
|
|
1615
|
-
const presentation = resolveScanPresentation(input, [projectScan]);
|
|
1616
|
-
return runMountedScan(projectScan.directory, presentation, blockingLevel, async (context) => {
|
|
1617
|
-
const result = await inspectProject(projectScan.directory, {
|
|
1618
|
-
...resolveTuiInspectOptions(input, projectScan.config),
|
|
1619
|
-
isCi: isCiEnvironment(),
|
|
1620
|
-
configOverride: projectScan.config,
|
|
1621
|
-
configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
|
|
1622
|
-
uiLayers: {
|
|
1623
|
-
reporter: reporterLayerForStore(context.store),
|
|
1624
|
-
progress: progressLayerForStore(context.store)
|
|
1625
|
-
}
|
|
1626
|
-
});
|
|
1627
|
-
const projectedScore = result.score ? await computeProjectedScore([...result.diagnostics], [...result.diagnostics], result.score) : null;
|
|
1628
|
-
context.store.setReport(toScanReport({
|
|
1629
|
-
result,
|
|
1630
|
-
rootDirectory: projectScan.directory,
|
|
1631
|
-
projectedScore,
|
|
1632
|
-
isOffline: context.isOffline,
|
|
1633
|
-
noScoreMessage: context.noScoreMessage
|
|
1634
|
-
}));
|
|
1635
|
-
return {
|
|
1636
|
-
scans: [{
|
|
1637
|
-
result,
|
|
1638
|
-
config: projectScan.config
|
|
1639
|
-
}],
|
|
1640
|
-
diagnostics: result.diagnostics,
|
|
1641
|
-
scoreResult: result.score,
|
|
1642
|
-
projectName: result.project.projectName,
|
|
1643
|
-
scannedFileCount: result.scannedFileCount ?? 0,
|
|
1644
|
-
elapsedMilliseconds: result.elapsedMilliseconds
|
|
1645
|
-
};
|
|
1646
|
-
});
|
|
1647
|
-
};
|
|
1648
|
-
const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel, inspectProject) => {
|
|
1649
|
-
const rootDirectory = rootScanTarget.resolvedDirectory;
|
|
1650
|
-
const projectScans = await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory));
|
|
1651
|
-
return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans), blockingLevel, async (context) => {
|
|
1652
|
-
const startTime = performance.now();
|
|
1653
|
-
let finishedCount = 0;
|
|
1654
|
-
context.store.setProgress(`Scanning ${directories.length} projects…`);
|
|
1655
|
-
const results = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
|
|
1656
|
-
const result = await inspectProject(projectScan.directory, {
|
|
1657
|
-
...resolveTuiInspectOptions(input, projectScan.config),
|
|
1658
|
-
isCi: isCiEnvironment(),
|
|
1659
|
-
configOverride: projectScan.config,
|
|
1660
|
-
configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
|
|
1661
|
-
uiLayers: { reporter: Reporter.layerNoop },
|
|
1662
|
-
concurrentScan: true
|
|
1663
|
-
});
|
|
1664
|
-
finishedCount += 1;
|
|
1665
|
-
context.store.setProgress(`Scanning ${directories.length} projects… (${finishedCount}/${directories.length})`);
|
|
1666
|
-
return {
|
|
1667
|
-
directory: projectScan.directory,
|
|
1668
|
-
result,
|
|
1669
|
-
config: projectScan.config
|
|
1670
|
-
};
|
|
1671
|
-
});
|
|
1672
|
-
const projects = results.map(({ directory, result }) => toScanReport({
|
|
1673
|
-
result,
|
|
1674
|
-
rootDirectory: directory,
|
|
1675
|
-
projectedScore: null,
|
|
1676
|
-
isOffline: context.isOffline,
|
|
1677
|
-
noScoreMessage: context.noScoreMessage
|
|
1678
|
-
}));
|
|
1679
|
-
const combinedDiagnostics = projects.flatMap((project) => qualifyDiagnosticPaths(project.diagnostics, rootDirectory, project.rootDirectory));
|
|
1680
|
-
const worst = findLowestScored(projects);
|
|
1681
|
-
const projectedScore = worst ? await computeProjectedScore(combinedDiagnostics, [...worst.diagnostics], worst.score) : null;
|
|
1682
|
-
const scannedFileCount = countUniqueScannedFiles(results.map(({ result }) => result));
|
|
1683
|
-
const elapsedMilliseconds = performance.now() - startTime;
|
|
1684
|
-
const lintFailureReason = resolveLintFailureReason(results.map(({ result }) => result));
|
|
1685
|
-
const summary = {
|
|
1686
|
-
projects,
|
|
1687
|
-
aggregateScore: worst?.score ?? null,
|
|
1688
|
-
projectedScore,
|
|
1689
|
-
combinedDiagnostics,
|
|
1690
|
-
scannedFileCount,
|
|
1691
|
-
elapsedMilliseconds,
|
|
1692
|
-
projectName: path.basename(rootDirectory),
|
|
1693
|
-
rootDirectory,
|
|
1694
|
-
isOffline: context.isOffline,
|
|
1695
|
-
noScoreMessage: context.noScoreMessage,
|
|
1696
|
-
...lintFailureReason ? { lintFailureReason } : {}
|
|
1697
|
-
};
|
|
1698
|
-
context.store.setSummary(summary);
|
|
1699
|
-
return {
|
|
1700
|
-
scans: results,
|
|
1701
|
-
diagnostics: combinedDiagnostics,
|
|
1702
|
-
scoreResult: summary.aggregateScore,
|
|
1703
|
-
projectName: summary.projectName,
|
|
1704
|
-
scannedFileCount,
|
|
1705
|
-
elapsedMilliseconds
|
|
1706
|
-
};
|
|
1707
|
-
});
|
|
1708
|
-
};
|
|
1709
|
-
const runScanApp = async (input) => {
|
|
1710
|
-
const scanTarget = await resolveScanTarget(input.directory, { allowAmbiguous: true });
|
|
1711
|
-
const rootDirectory = scanTarget.resolvedDirectory;
|
|
1712
|
-
const deadlineEpochMs = input.options?.deadlineEpochMs ?? (input.options?.maxDurationMs != null ? Date.now() + input.options.maxDurationMs : void 0);
|
|
1713
|
-
const resolvedInput = {
|
|
1714
|
-
...input,
|
|
1715
|
-
options: {
|
|
1716
|
-
...input.options,
|
|
1717
|
-
deadlineEpochMs
|
|
1718
|
-
},
|
|
1719
|
-
configProjects: input.configProjects ?? scanTarget.userConfig?.projects,
|
|
1720
|
-
share: input.share ?? scanTarget.userConfig?.share ?? true
|
|
1721
|
-
};
|
|
1722
|
-
const selectedDirectories = await resolveSelectedDirectories(rootDirectory, resolvedInput);
|
|
1723
|
-
const inspectProject = createInvocationInspect(input.options?.concurrency);
|
|
1724
|
-
const blockingLevel = resolveBlockingLevel({ blocking: resolvedInput.blocking }, scanTarget.userConfig);
|
|
1725
|
-
if (selectedDirectories.length === 0) return { shouldFail: false };
|
|
1726
|
-
if (selectedDirectories.length === 1) return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, blockingLevel, inspectProject);
|
|
1727
|
-
return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, blockingLevel, inspectProject);
|
|
1728
|
-
};
|
|
1729
|
-
//#endregion
|
|
1730
|
-
export { runScanApp };
|
|
1731
|
-
|
|
1732
|
-
//# sourceMappingURL=run-scan-app-DHRvqfkX.js.map
|
|
1733
|
-
//# debugId=898bdb3e-afed-5fca-92a2-f14d1e8ca6fb
|