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