@jonathan0823/pi-sonarqube 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +38 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +367 -0
- package/dist/api.js.map +1 -0
- package/dist/commands.d.ts +54 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +192 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +278 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -975
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +31 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +193 -0
- package/dist/ui.js.map +1 -0
- package/package.json +1 -1
- package/src/api.ts +476 -0
- package/src/commands.ts +248 -0
- package/src/config.ts +340 -0
- package/src/index.ts +274 -1262
- package/src/types.ts +79 -0
- package/src/ui.ts +271 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
1
4
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
-
import { basename, dirname, relative, resolve } from "node:path";
|
|
6
5
|
import { Type } from "typebox";
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { slugify, normalizeServerUrl, sonarProjectPropertiesPath, projectConfigPath, resolveInitTarget, loadProjectConfig, saveProjectConfig, ensureDefaultSonarProjectProperties, saveWorkspaceRegistry, resolveConfig, } from "./config.js";
|
|
7
|
+
import { runScanner, readReportTask, waitForAnalysis, normalizeIssueFilters, fetchIssues, createAnalysisState, issueFilterLabel, } from "./api.js";
|
|
8
|
+
import { STATE_TYPE, sonarArgumentCompletions, parseCommandArgs, splitSonarArgumentContext, mergeAutocompleteSuggestions, formatIssue, formatSummary, formatReport, helpText, targetLabel, sonarErrorMessage, } from "./commands.js";
|
|
9
|
+
import { startAnalysisUi, showIssueBrowser, buildIssuePreview, openIssuePreview, resolveTargetState, } from "./ui.js";
|
|
9
10
|
const SonarToolParams = Type.Object({
|
|
10
11
|
action: StringEnum(["analyze", "issues", "open"]),
|
|
11
12
|
path: Type.Optional(Type.String({ description: "Target alias or project directory to analyze or inspect" })),
|
|
@@ -15,616 +16,7 @@ const SonarToolParams = Type.Object({
|
|
|
15
16
|
types: Type.Optional(Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" }))),
|
|
16
17
|
rules: Type.Optional(Type.Array(Type.String({ description: "Rule keys to fetch" }))),
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
-
{ value: "analyze", label: "analyze", description: "run analysis and fetch issues" },
|
|
20
|
-
{ value: "issues", label: "issues", description: "browse the latest issues" },
|
|
21
|
-
{ value: "open", label: "open", description: "preview a specific issue" },
|
|
22
|
-
{ value: "init", label: "init", description: "configure a project target" },
|
|
23
|
-
];
|
|
24
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
25
|
-
function nowIso() {
|
|
26
|
-
return new Date().toISOString();
|
|
27
|
-
}
|
|
28
|
-
function slugify(input) {
|
|
29
|
-
return (input
|
|
30
|
-
.toLowerCase()
|
|
31
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
32
|
-
.replace(/^-+|-+$/g, "")
|
|
33
|
-
.slice(0, 80) || "sonarqube");
|
|
34
|
-
}
|
|
35
|
-
function normalizePath(input, cwd) {
|
|
36
|
-
const trimmed = input.trim().replace(/^@/, "");
|
|
37
|
-
return resolve(cwd, trimmed);
|
|
38
|
-
}
|
|
39
|
-
function parseProperties(text) {
|
|
40
|
-
const props = {};
|
|
41
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
42
|
-
const line = rawLine.trim();
|
|
43
|
-
if (!line || line.startsWith("#") || line.startsWith("!"))
|
|
44
|
-
continue;
|
|
45
|
-
const sep = line.indexOf("=");
|
|
46
|
-
if (sep < 0)
|
|
47
|
-
continue;
|
|
48
|
-
const key = line.slice(0, sep).trim();
|
|
49
|
-
const value = line.slice(sep + 1).trim();
|
|
50
|
-
if (key)
|
|
51
|
-
props[key] = value;
|
|
52
|
-
}
|
|
53
|
-
return props;
|
|
54
|
-
}
|
|
55
|
-
function normalizeServerUrl(url) {
|
|
56
|
-
const fallback = "http://localhost:9000";
|
|
57
|
-
if (!url?.trim())
|
|
58
|
-
return fallback;
|
|
59
|
-
return url.trim().replace(/\/+$/, "");
|
|
60
|
-
}
|
|
61
|
-
const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
|
|
62
|
-
const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"];
|
|
63
|
-
const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"];
|
|
64
|
-
function createAutocompleteItem(value, description) {
|
|
65
|
-
return description ? { value, label: value, description } : { value, label: value };
|
|
66
|
-
}
|
|
67
|
-
function filterAutocompleteItems(items, prefix) {
|
|
68
|
-
const query = prefix.trim().toLowerCase();
|
|
69
|
-
const filtered = query
|
|
70
|
-
? items.filter((item) => item.value.toLowerCase().startsWith(query) || item.label.toLowerCase().startsWith(query))
|
|
71
|
-
: [...items];
|
|
72
|
-
return filtered.map((item) => ({ value: item.value, label: item.label, description: item.description }));
|
|
73
|
-
}
|
|
74
|
-
function mergeAutocompleteSuggestions(prefix, current, extraItems) {
|
|
75
|
-
const merged = new Map();
|
|
76
|
-
for (const item of current?.items ?? [])
|
|
77
|
-
merged.set(item.value, item);
|
|
78
|
-
for (const item of extraItems)
|
|
79
|
-
merged.set(item.value, item);
|
|
80
|
-
const items = [...merged.values()];
|
|
81
|
-
return items.length > 0 ? { prefix, items } : current;
|
|
82
|
-
}
|
|
83
|
-
function splitSonarArgumentContext(argumentText) {
|
|
84
|
-
const trimmedLeft = argumentText.replace(/^\s+/, "");
|
|
85
|
-
if (!trimmedLeft)
|
|
86
|
-
return { command: "", current: "", tokens: [] };
|
|
87
|
-
const tokens = trimmedLeft.split(/\s+/);
|
|
88
|
-
const current = /\s$/.test(argumentText) ? "" : tokens.pop() ?? "";
|
|
89
|
-
return { command: tokens[0] ?? current, current, tokens: tokens.length > 0 ? tokens : current ? [current] : [] };
|
|
90
|
-
}
|
|
91
|
-
function createFilterCompletionList() {
|
|
92
|
-
return [
|
|
93
|
-
...SONAR_SEVERITIES.flatMap((value) => [
|
|
94
|
-
createAutocompleteItem(`severity:${value}`, "severity"),
|
|
95
|
-
createAutocompleteItem(value, "severity"),
|
|
96
|
-
]),
|
|
97
|
-
...SONAR_STATUSES.flatMap((value) => [
|
|
98
|
-
createAutocompleteItem(`status:${value}`, "status"),
|
|
99
|
-
createAutocompleteItem(value, "status"),
|
|
100
|
-
]),
|
|
101
|
-
...SONAR_TYPES.flatMap((value) => [
|
|
102
|
-
createAutocompleteItem(`type:${value}`, "type"),
|
|
103
|
-
createAutocompleteItem(value, "type"),
|
|
104
|
-
]),
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
function sonarArgumentCompletions(argumentText, issues) {
|
|
108
|
-
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
109
|
-
const lowerCommand = command.toLowerCase();
|
|
110
|
-
const commandMatches = SONAR_COMMANDS.filter((item) => item.value.startsWith(lowerCommand));
|
|
111
|
-
if (!command || commandMatches.length > 1 || !SONAR_COMMANDS.some((item) => item.value === lowerCommand)) {
|
|
112
|
-
return commandMatches.length > 0 ? commandMatches.map((item) => createAutocompleteItem(item.value, item.description)) : null;
|
|
113
|
-
}
|
|
114
|
-
if (lowerCommand === "open") {
|
|
115
|
-
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => createAutocompleteItem(String(index + 1), `${issue.severity} ${issue.filePath}${issue.line ? `:${issue.line}` : ""}`));
|
|
116
|
-
const suggestions = [...issueItems, ...createFilterCompletionList()];
|
|
117
|
-
const filtered = filterAutocompleteItems(suggestions, current);
|
|
118
|
-
return filtered.length > 0 ? filtered : null;
|
|
119
|
-
}
|
|
120
|
-
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
121
|
-
const suggestions = createFilterCompletionList();
|
|
122
|
-
const filtered = filterAutocompleteItems(suggestions, current);
|
|
123
|
-
return filtered.length > 0 ? filtered : null;
|
|
124
|
-
}
|
|
125
|
-
if (lowerCommand === "init") {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
return tokens.length === 0 ? filterAutocompleteItems(SONAR_COMMANDS, current) : null;
|
|
129
|
-
}
|
|
130
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
131
|
-
function resolveProjectKey(baseDir, props) {
|
|
132
|
-
const fromProps = props["sonar.projectKey"]?.trim();
|
|
133
|
-
if (fromProps)
|
|
134
|
-
return fromProps;
|
|
135
|
-
return slugify(basename(baseDir));
|
|
136
|
-
}
|
|
137
|
-
async function readOptionalText(path) {
|
|
138
|
-
try {
|
|
139
|
-
return await readFile(path, "utf8");
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function readOptionalJson(path) {
|
|
146
|
-
try {
|
|
147
|
-
const text = await readFile(path, "utf8");
|
|
148
|
-
return JSON.parse(text);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function projectConfigPath(baseDir) {
|
|
155
|
-
return resolve(baseDir, CONFIG_DIR_NAME, "sonarqube.json");
|
|
156
|
-
}
|
|
157
|
-
function sonarqubeConfigDir(baseDir) {
|
|
158
|
-
return resolve(baseDir, CONFIG_DIR_NAME);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Load project-local config from `.pi/sonarqube.json`.
|
|
162
|
-
* Returns undefined when the file doesn't exist or is invalid.
|
|
163
|
-
*/
|
|
164
|
-
async function loadProjectConfig(baseDir) {
|
|
165
|
-
return readOptionalJson(projectConfigPath(baseDir));
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Save project-local config to `.pi/sonarqube.json`.
|
|
169
|
-
*/
|
|
170
|
-
async function saveProjectConfig(baseDir, config) {
|
|
171
|
-
const dir = sonarqubeConfigDir(baseDir);
|
|
172
|
-
await mkdir(dir, { recursive: true });
|
|
173
|
-
await writeFile(projectConfigPath(baseDir), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
174
|
-
}
|
|
175
|
-
const WORKSPACE_REGISTRY_FILE = "sonarqube.workspaces.json";
|
|
176
|
-
function workspaceRegistryPath(repoRoot) {
|
|
177
|
-
return resolve(repoRoot, CONFIG_DIR_NAME, WORKSPACE_REGISTRY_FILE);
|
|
178
|
-
}
|
|
179
|
-
async function hasGitRootMarker(dir) {
|
|
180
|
-
try {
|
|
181
|
-
const entry = await stat(resolve(dir, ".git"));
|
|
182
|
-
return entry.isDirectory() || entry.isFile();
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
async function findRepoRoot(startDir) {
|
|
189
|
-
let dir = resolve(startDir);
|
|
190
|
-
for (;;) {
|
|
191
|
-
if (await hasGitRootMarker(dir))
|
|
192
|
-
return dir;
|
|
193
|
-
const parent = dirname(dir);
|
|
194
|
-
if (parent === dir)
|
|
195
|
-
return resolve(startDir);
|
|
196
|
-
dir = parent;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function loadWorkspaceRegistry(startDir) {
|
|
200
|
-
const repoRoot = await findRepoRoot(startDir);
|
|
201
|
-
const raw = await readOptionalJson(workspaceRegistryPath(repoRoot));
|
|
202
|
-
const workspaces = raw?.workspaces && typeof raw.workspaces === "object" ? { ...raw.workspaces } : {};
|
|
203
|
-
return { repoRoot, registry: { version: 1, workspaces: workspaces } };
|
|
204
|
-
}
|
|
205
|
-
async function saveWorkspaceRegistry(startDir, alias, targetDir) {
|
|
206
|
-
const { repoRoot, registry } = await loadWorkspaceRegistry(startDir);
|
|
207
|
-
registry.workspaces[alias] = relative(repoRoot, targetDir) || ".";
|
|
208
|
-
await mkdir(sonarqubeConfigDir(repoRoot), { recursive: true });
|
|
209
|
-
await writeFile(workspaceRegistryPath(repoRoot), JSON.stringify(registry, null, 2) + "\n", "utf8");
|
|
210
|
-
}
|
|
211
|
-
function looksLikePath(token) {
|
|
212
|
-
return (token === "." ||
|
|
213
|
-
token === ".." ||
|
|
214
|
-
token.startsWith("./") ||
|
|
215
|
-
token.startsWith("../") ||
|
|
216
|
-
token.startsWith("/") ||
|
|
217
|
-
token.startsWith("~") ||
|
|
218
|
-
token.includes("/") ||
|
|
219
|
-
token.includes("\\"));
|
|
220
|
-
}
|
|
221
|
-
function knownTargets(registry) {
|
|
222
|
-
return Object.keys(registry.workspaces).sort();
|
|
223
|
-
}
|
|
224
|
-
async function resolveTarget(ctx, targetInput) {
|
|
225
|
-
const { repoRoot, registry } = await loadWorkspaceRegistry(ctx.cwd);
|
|
226
|
-
if (!targetInput) {
|
|
227
|
-
return { baseDir: ctx.cwd, repoRoot };
|
|
228
|
-
}
|
|
229
|
-
const aliasTarget = registry.workspaces[targetInput];
|
|
230
|
-
if (aliasTarget) {
|
|
231
|
-
const baseDir = resolve(repoRoot, aliasTarget);
|
|
232
|
-
const baseDirStat = await stat(baseDir).catch(() => undefined);
|
|
233
|
-
if (!baseDirStat?.isDirectory()) {
|
|
234
|
-
throw new Error(`SonarQube target "${targetInput}" points to a missing directory: ${baseDir}`);
|
|
235
|
-
}
|
|
236
|
-
return { baseDir, repoRoot, alias: targetInput };
|
|
237
|
-
}
|
|
238
|
-
if (looksLikePath(targetInput)) {
|
|
239
|
-
const baseDir = normalizePath(targetInput, ctx.cwd);
|
|
240
|
-
const baseDirStat = await stat(baseDir).catch(() => undefined);
|
|
241
|
-
if (!baseDirStat?.isDirectory()) {
|
|
242
|
-
throw new Error(`SonarQube target path not found: ${baseDir}`);
|
|
243
|
-
}
|
|
244
|
-
return { baseDir, repoRoot };
|
|
245
|
-
}
|
|
246
|
-
const known = knownTargets(registry);
|
|
247
|
-
if (known.length > 0) {
|
|
248
|
-
throw new Error(`Unknown SonarQube target "${targetInput}". Known targets: ${known.join(", ")}. Use /sonarqube init <alias> <path> to add one.`);
|
|
249
|
-
}
|
|
250
|
-
throw new Error(`Unknown SonarQube target "${targetInput}". Use /sonarqube init <alias> <path> to add one.`);
|
|
251
|
-
}
|
|
252
|
-
async function resolveInitTarget(ctx, alias, targetInput) {
|
|
253
|
-
if (targetInput) {
|
|
254
|
-
const resolved = await resolveTarget(ctx, targetInput);
|
|
255
|
-
return { ...resolved, alias };
|
|
256
|
-
}
|
|
257
|
-
const { repoRoot } = await loadWorkspaceRegistry(ctx.cwd);
|
|
258
|
-
return { baseDir: ctx.cwd, repoRoot, alias };
|
|
259
|
-
}
|
|
260
|
-
export { projectConfigPath, loadProjectConfig, saveProjectConfig, sonarqubeConfigDir };
|
|
261
|
-
// ── Config resolution (env > .pi/sonarqube.json > sonar-project.properties) ────
|
|
262
|
-
async function resolveConfig(ctx, inputPath) {
|
|
263
|
-
const { baseDir } = await resolveTarget(ctx, inputPath);
|
|
264
|
-
const propertiesPath = resolve(baseDir, "sonar-project.properties");
|
|
265
|
-
const propertiesText = await readOptionalText(propertiesPath);
|
|
266
|
-
const props = propertiesText ? parseProperties(propertiesText) : {};
|
|
267
|
-
const projectCfg = await loadProjectConfig(baseDir);
|
|
268
|
-
const serverUrl = normalizeServerUrl(process.env.SONARQUBE_URL ??
|
|
269
|
-
process.env.SONAR_HOST_URL ??
|
|
270
|
-
projectCfg?.serverUrl?.trim() ??
|
|
271
|
-
props["sonar.host.url"]);
|
|
272
|
-
const projectKey = process.env.SONAR_PROJECT_KEY?.trim() ||
|
|
273
|
-
projectCfg?.projectKey?.trim() ||
|
|
274
|
-
props["sonar.projectKey"]?.trim() ||
|
|
275
|
-
resolveProjectKey(baseDir, props);
|
|
276
|
-
const token = process.env.SONARQUBE_TOKEN?.trim() ||
|
|
277
|
-
process.env.SONAR_TOKEN?.trim() ||
|
|
278
|
-
projectCfg?.token?.trim() ||
|
|
279
|
-
props["sonar.token"]?.trim();
|
|
280
|
-
return {
|
|
281
|
-
baseDir,
|
|
282
|
-
serverUrl,
|
|
283
|
-
projectKey,
|
|
284
|
-
token: token || undefined,
|
|
285
|
-
hasProperties: Boolean(propertiesText),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
// ── Auth / API helpers ────────────────────────────────────────────────────────
|
|
289
|
-
function authHeader(token) {
|
|
290
|
-
if (!token)
|
|
291
|
-
return undefined;
|
|
292
|
-
return `Basic ${Buffer.from(`${token}:`).toString("base64")}`;
|
|
293
|
-
}
|
|
294
|
-
async function fetchJson(url, token, signal) {
|
|
295
|
-
const headers = { Accept: "application/json" };
|
|
296
|
-
const auth = authHeader(token);
|
|
297
|
-
if (auth)
|
|
298
|
-
headers.Authorization = auth;
|
|
299
|
-
const response = await fetch(url, { headers, signal });
|
|
300
|
-
if (!response.ok) {
|
|
301
|
-
const body = await response.text().catch(() => "");
|
|
302
|
-
const suffix = body.trim() ? `: ${body.trim().slice(0, 240)}` : "";
|
|
303
|
-
throw new Error(`SonarQube API request failed (${response.status} ${response.statusText}) for ${url}${suffix}`);
|
|
304
|
-
}
|
|
305
|
-
return (await response.json());
|
|
306
|
-
}
|
|
307
|
-
function toIssuePath(component, projectKey) {
|
|
308
|
-
const prefix = `${projectKey}:`;
|
|
309
|
-
if (component.startsWith(prefix))
|
|
310
|
-
return component.slice(prefix.length);
|
|
311
|
-
const colon = component.indexOf(":");
|
|
312
|
-
return colon >= 0 ? component.slice(colon + 1) : component;
|
|
313
|
-
}
|
|
314
|
-
function normalizeIssueList(values, uppercase = false) {
|
|
315
|
-
const cleaned = values
|
|
316
|
-
?.map((value) => value.trim())
|
|
317
|
-
.filter(Boolean)
|
|
318
|
-
.map((value) => (uppercase ? value.toUpperCase() : value));
|
|
319
|
-
if (!cleaned?.length)
|
|
320
|
-
return undefined;
|
|
321
|
-
return [...new Set(cleaned)];
|
|
322
|
-
}
|
|
323
|
-
function normalizeIssueFilters(filters) {
|
|
324
|
-
const normalized = {
|
|
325
|
-
severities: normalizeIssueList(filters?.severities, true),
|
|
326
|
-
statuses: normalizeIssueList(filters?.statuses, true),
|
|
327
|
-
types: normalizeIssueList(filters?.types, true),
|
|
328
|
-
rules: normalizeIssueList(filters?.rules),
|
|
329
|
-
};
|
|
330
|
-
return normalized.severities || normalized.statuses || normalized.types || normalized.rules ? normalized : undefined;
|
|
331
|
-
}
|
|
332
|
-
function mergeIssueFilters(...filters) {
|
|
333
|
-
const merged = {};
|
|
334
|
-
for (const filter of filters) {
|
|
335
|
-
if (!filter)
|
|
336
|
-
continue;
|
|
337
|
-
if (filter.severities?.length)
|
|
338
|
-
merged.severities = [...(merged.severities ?? []), ...filter.severities];
|
|
339
|
-
if (filter.statuses?.length)
|
|
340
|
-
merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
|
|
341
|
-
if (filter.types?.length)
|
|
342
|
-
merged.types = [...(merged.types ?? []), ...filter.types];
|
|
343
|
-
if (filter.rules?.length)
|
|
344
|
-
merged.rules = [...(merged.rules ?? []), ...filter.rules];
|
|
345
|
-
}
|
|
346
|
-
return normalizeIssueFilters(merged);
|
|
347
|
-
}
|
|
348
|
-
function splitFilterValues(value) {
|
|
349
|
-
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
350
|
-
}
|
|
351
|
-
function parseIssueFilterToken(token) {
|
|
352
|
-
const trimmed = token.trim();
|
|
353
|
-
if (!trimmed)
|
|
354
|
-
return undefined;
|
|
355
|
-
const explicit = trimmed.match(/^([^:=]+)[:=](.+)$/);
|
|
356
|
-
if (explicit) {
|
|
357
|
-
const key = explicit[1].trim().toLowerCase();
|
|
358
|
-
const values = splitFilterValues(explicit[2]);
|
|
359
|
-
if (values.length === 0)
|
|
360
|
-
return undefined;
|
|
361
|
-
if (key === "severity" || key === "severities")
|
|
362
|
-
return { severities: values.map((value) => value.toUpperCase()) };
|
|
363
|
-
if (key === "status" || key === "statuses")
|
|
364
|
-
return { statuses: values.map((value) => value.toUpperCase()) };
|
|
365
|
-
if (key === "type" || key === "types")
|
|
366
|
-
return { types: values.map((value) => value.toUpperCase()) };
|
|
367
|
-
if (key === "rule" || key === "rules")
|
|
368
|
-
return { rules: values };
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
371
|
-
const upper = trimmed.toUpperCase();
|
|
372
|
-
if (SONAR_SEVERITIES.includes(upper))
|
|
373
|
-
return { severities: [upper] };
|
|
374
|
-
if (SONAR_STATUSES.includes(upper))
|
|
375
|
-
return { statuses: [upper] };
|
|
376
|
-
if (SONAR_TYPES.includes(upper))
|
|
377
|
-
return { types: [upper] };
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
function parseSonarIssueArgs(tokens, allowIssueIndex = false) {
|
|
381
|
-
const filters = [];
|
|
382
|
-
let targetInput;
|
|
383
|
-
let issueIndex;
|
|
384
|
-
for (const token of tokens) {
|
|
385
|
-
if (!token)
|
|
386
|
-
continue;
|
|
387
|
-
if (allowIssueIndex && issueIndex === undefined && /^\d+$/.test(token)) {
|
|
388
|
-
issueIndex = Number(token);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const parsedFilter = parseIssueFilterToken(token);
|
|
392
|
-
if (parsedFilter) {
|
|
393
|
-
filters.push(parsedFilter);
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (!targetInput) {
|
|
397
|
-
targetInput = token;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return {
|
|
401
|
-
targetInput,
|
|
402
|
-
issueIndex,
|
|
403
|
-
filters: mergeIssueFilters(...filters),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
function issueFilterLabel(filters) {
|
|
407
|
-
if (!filters)
|
|
408
|
-
return "";
|
|
409
|
-
const parts = [
|
|
410
|
-
filters.severities?.length ? `severities=${filters.severities.join(",")}` : "",
|
|
411
|
-
filters.statuses?.length ? `statuses=${filters.statuses.join(",")}` : "",
|
|
412
|
-
filters.types?.length ? `types=${filters.types.join(",")}` : "",
|
|
413
|
-
filters.rules?.length ? `rules=${filters.rules.join(",")}` : "",
|
|
414
|
-
].filter(Boolean);
|
|
415
|
-
return parts.join(" • ");
|
|
416
|
-
}
|
|
417
|
-
function severitySortRank(severity) {
|
|
418
|
-
switch (severity.toUpperCase()) {
|
|
419
|
-
case "BLOCKER":
|
|
420
|
-
return 0;
|
|
421
|
-
case "CRITICAL":
|
|
422
|
-
return 1;
|
|
423
|
-
case "MAJOR":
|
|
424
|
-
return 2;
|
|
425
|
-
case "MINOR":
|
|
426
|
-
return 3;
|
|
427
|
-
case "INFO":
|
|
428
|
-
return 4;
|
|
429
|
-
default:
|
|
430
|
-
return 5;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
function compareIssuesForContext(left, right) {
|
|
434
|
-
const severityDiff = severitySortRank(left.severity) - severitySortRank(right.severity);
|
|
435
|
-
if (severityDiff !== 0)
|
|
436
|
-
return severityDiff;
|
|
437
|
-
const fileDiff = left.filePath.localeCompare(right.filePath);
|
|
438
|
-
if (fileDiff !== 0)
|
|
439
|
-
return fileDiff;
|
|
440
|
-
const leftLine = left.line ?? Number.POSITIVE_INFINITY;
|
|
441
|
-
const rightLine = right.line ?? Number.POSITIVE_INFINITY;
|
|
442
|
-
if (leftLine !== rightLine)
|
|
443
|
-
return leftLine - rightLine;
|
|
444
|
-
return left.message.localeCompare(right.message);
|
|
445
|
-
}
|
|
446
|
-
// ── Issue helpers ─────────────────────────────────────────────────────────────
|
|
447
|
-
function buildIssuePreview(baseDir, issue, radius = 3) {
|
|
448
|
-
return (async () => {
|
|
449
|
-
const filePath = resolve(baseDir, issue.filePath);
|
|
450
|
-
const content = await readFile(filePath, "utf8");
|
|
451
|
-
const lines = content.split(/\r?\n/);
|
|
452
|
-
const target = issue.line ?? 1;
|
|
453
|
-
const start = Math.max(1, target - radius);
|
|
454
|
-
const end = Math.min(lines.length, target + radius);
|
|
455
|
-
const width = String(end).length;
|
|
456
|
-
const rendered = [];
|
|
457
|
-
for (let line = start; line <= end; line++) {
|
|
458
|
-
const marker = line === target ? ">" : " ";
|
|
459
|
-
const num = String(line).padStart(width, " ");
|
|
460
|
-
rendered.push(`${marker} ${num} | ${lines[line - 1] ?? ""}`);
|
|
461
|
-
}
|
|
462
|
-
return rendered.join("\n");
|
|
463
|
-
})();
|
|
464
|
-
}
|
|
465
|
-
function formatIssue(issue, index) {
|
|
466
|
-
const loc = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
467
|
-
const prefix = index !== undefined ? `${String(index).padStart(2, "0")}. ` : "";
|
|
468
|
-
const rule = issue.ruleName ? `${issue.rule} (${issue.ruleName})` : issue.rule;
|
|
469
|
-
return `${prefix}${issue.severity} ${loc} — ${rule} — ${issue.message}`;
|
|
470
|
-
}
|
|
471
|
-
function formatSummary(state) {
|
|
472
|
-
const issueCount = state.totalIssues;
|
|
473
|
-
const noIssues = issueCount === 0;
|
|
474
|
-
const filterLabel = state.filters ? ` (${issueFilterLabel(state.filters)})` : "";
|
|
475
|
-
return noIssues
|
|
476
|
-
? `SonarQube: no issues found for ${state.projectKey}${filterLabel}`
|
|
477
|
-
: `SonarQube: ${issueCount} issue${issueCount === 1 ? "" : "s"} found for ${state.projectKey}${filterLabel}`;
|
|
478
|
-
}
|
|
479
|
-
function formatReport(state) {
|
|
480
|
-
const lines = [
|
|
481
|
-
`Project: ${state.projectKey}`,
|
|
482
|
-
`Server: ${state.serverUrl}`,
|
|
483
|
-
`Base dir: ${state.baseDir}`,
|
|
484
|
-
`Issues: ${state.totalIssues}`,
|
|
485
|
-
];
|
|
486
|
-
if (state.filters) {
|
|
487
|
-
lines.push(`Filters: ${issueFilterLabel(state.filters)}`);
|
|
488
|
-
}
|
|
489
|
-
if (state.issues.length === 0) {
|
|
490
|
-
lines.push("No open issues found.");
|
|
491
|
-
return lines.join("\n");
|
|
492
|
-
}
|
|
493
|
-
lines.push("");
|
|
494
|
-
for (const [index, issue] of state.issues.slice(0, 10).entries()) {
|
|
495
|
-
lines.push(formatIssue(issue, index + 1));
|
|
496
|
-
}
|
|
497
|
-
if (state.issues.length > 10) {
|
|
498
|
-
lines.push(`... ${state.issues.length - 10} more`);
|
|
499
|
-
}
|
|
500
|
-
return lines.join("\n");
|
|
501
|
-
}
|
|
502
|
-
function severityColor(theme, severity) {
|
|
503
|
-
switch (severity.toLowerCase()) {
|
|
504
|
-
case "blocker":
|
|
505
|
-
case "critical":
|
|
506
|
-
return theme.fg("error", severity);
|
|
507
|
-
case "major":
|
|
508
|
-
return theme.fg("warning", severity);
|
|
509
|
-
case "minor":
|
|
510
|
-
return theme.fg("accent", severity);
|
|
511
|
-
default:
|
|
512
|
-
return theme.fg("muted", severity);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const ANALYSIS_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
516
|
-
const ANALYSIS_WIDGET_KEY = "sonarqube-analysis";
|
|
517
|
-
function startAnalysisUi(ctx, projectKey) {
|
|
518
|
-
let frameIndex = 0;
|
|
519
|
-
let phase = "Starting...";
|
|
520
|
-
const render = () => {
|
|
521
|
-
const frame = ANALYSIS_SPINNER[frameIndex];
|
|
522
|
-
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [`${frame} SonarQube ${projectKey}`, phase]);
|
|
523
|
-
};
|
|
524
|
-
ctx.ui.setWorkingMessage(`Analyzing ${projectKey}...`);
|
|
525
|
-
ctx.ui.setWorkingIndicator({ frames: ANALYSIS_SPINNER, intervalMs: 80 });
|
|
526
|
-
render();
|
|
527
|
-
const timer = setInterval(() => {
|
|
528
|
-
frameIndex = (frameIndex + 1) % ANALYSIS_SPINNER.length;
|
|
529
|
-
render();
|
|
530
|
-
}, 80);
|
|
531
|
-
return {
|
|
532
|
-
setPhase(nextPhase) {
|
|
533
|
-
phase = nextPhase;
|
|
534
|
-
render();
|
|
535
|
-
},
|
|
536
|
-
stop() {
|
|
537
|
-
clearInterval(timer);
|
|
538
|
-
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, undefined);
|
|
539
|
-
ctx.ui.setWorkingMessage();
|
|
540
|
-
ctx.ui.setWorkingIndicator();
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
// ── Issue Browser UI component ────────────────────────────────────────────────
|
|
545
|
-
class IssueBrowser {
|
|
546
|
-
state;
|
|
547
|
-
theme;
|
|
548
|
-
done;
|
|
549
|
-
selected = 0;
|
|
550
|
-
cachedWidth;
|
|
551
|
-
cachedLines;
|
|
552
|
-
constructor(state, theme, done) {
|
|
553
|
-
this.state = state;
|
|
554
|
-
this.theme = theme;
|
|
555
|
-
this.done = done;
|
|
556
|
-
}
|
|
557
|
-
invalidate() {
|
|
558
|
-
this.cachedWidth = undefined;
|
|
559
|
-
this.cachedLines = undefined;
|
|
560
|
-
}
|
|
561
|
-
handleInput(data) {
|
|
562
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
563
|
-
this.done(null);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
if (matchesKey(data, Key.up)) {
|
|
567
|
-
this.selected = Math.max(0, this.selected - 1);
|
|
568
|
-
this.invalidate();
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
if (matchesKey(data, Key.down)) {
|
|
572
|
-
this.selected = Math.min(this.state.issues.length - 1, this.selected + 1);
|
|
573
|
-
this.invalidate();
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (matchesKey(data, Key.enter)) {
|
|
577
|
-
this.done(this.selected);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
render(width) {
|
|
581
|
-
if (this.cachedLines && this.cachedWidth === width) {
|
|
582
|
-
return this.cachedLines;
|
|
583
|
-
}
|
|
584
|
-
const lines = [];
|
|
585
|
-
const title = this.theme.fg("accent", this.theme.bold(" SonarQube Issues "));
|
|
586
|
-
const subtitleText = this.state.filters
|
|
587
|
-
? `${this.state.projectKey} • ${this.state.totalIssues} issue(s) • ${issueFilterLabel(this.state.filters)}`
|
|
588
|
-
: `${this.state.projectKey} • ${this.state.totalIssues} issue(s)`;
|
|
589
|
-
const subtitle = this.theme.fg("dim", subtitleText);
|
|
590
|
-
lines.push(truncateToWidth(title, width));
|
|
591
|
-
lines.push(truncateToWidth(subtitle, width));
|
|
592
|
-
lines.push("");
|
|
593
|
-
const pageSize = Math.min(20, this.state.issues.length);
|
|
594
|
-
const halfWindow = Math.floor(pageSize / 2);
|
|
595
|
-
const maxStart = Math.max(0, this.state.issues.length - pageSize);
|
|
596
|
-
const start = Math.max(0, Math.min(this.selected - halfWindow, maxStart));
|
|
597
|
-
const end = Math.min(this.state.issues.length, start + pageSize);
|
|
598
|
-
const visibleIssues = this.state.issues.slice(start, end);
|
|
599
|
-
if (visibleIssues.length === 0) {
|
|
600
|
-
lines.push(truncateToWidth(this.theme.fg("success", "No open issues found."), width));
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
if (start > 0) {
|
|
604
|
-
lines.push(truncateToWidth(this.theme.fg("dim", `... ${start} more above`), width));
|
|
605
|
-
}
|
|
606
|
-
visibleIssues.forEach((issue, index) => {
|
|
607
|
-
const issueIndex = start + index;
|
|
608
|
-
const selected = issueIndex === this.selected;
|
|
609
|
-
const marker = selected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
|
|
610
|
-
const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
611
|
-
const rule = issue.ruleName ? `${issue.rule} (${issue.ruleName})` : issue.rule;
|
|
612
|
-
const severity = severityColor(this.theme, issue.severity);
|
|
613
|
-
const summary = `${marker} ${String(issueIndex + 1).padStart(2, "0")}. ${severity} ${this.theme.fg("accent", location)} ${this.theme.fg("muted", rule)} ${this.theme.fg("text", `— ${issue.message}`)}`;
|
|
614
|
-
lines.push(truncateToWidth(summary, width));
|
|
615
|
-
});
|
|
616
|
-
if (end < this.state.issues.length) {
|
|
617
|
-
lines.push(truncateToWidth(this.theme.fg("dim", `... ${this.state.issues.length - end} more below`), width));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
lines.push("");
|
|
621
|
-
lines.push(truncateToWidth(this.theme.fg("dim", "Up/Down to move, Enter to preview, Esc to close"), width));
|
|
622
|
-
this.cachedWidth = width;
|
|
623
|
-
this.cachedLines = lines;
|
|
624
|
-
return lines;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
// ── State restoration ─────────────────────────────────────────────────────────
|
|
19
|
+
// ── State restoration ───────────────────────────────────────────────────────
|
|
628
20
|
async function restoreState(pi, ctx, statesByBaseDir) {
|
|
629
21
|
statesByBaseDir.clear();
|
|
630
22
|
let latest;
|
|
@@ -637,195 +29,7 @@ async function restoreState(pi, ctx, statesByBaseDir) {
|
|
|
637
29
|
ctx.ui.setStatus("sonarqube", undefined);
|
|
638
30
|
return latest;
|
|
639
31
|
}
|
|
640
|
-
// ──
|
|
641
|
-
function extractScannerHint(output, config) {
|
|
642
|
-
const lines = output.toLowerCase();
|
|
643
|
-
if (/status code 401|status 401|unauthorized/i.test(lines)) {
|
|
644
|
-
const defaultHint = config.serverUrl === "http://localhost:9000"
|
|
645
|
-
? " (default: http://localhost:9000)"
|
|
646
|
-
: "";
|
|
647
|
-
return [
|
|
648
|
-
`Authentication failed for ${config.serverUrl}${defaultHint}.`,
|
|
649
|
-
"Run `/sonarqube init` to set up your server URL and token, then retry.",
|
|
650
|
-
"Or set SONARQUBE_TOKEN in your environment.",
|
|
651
|
-
].join(" ");
|
|
652
|
-
}
|
|
653
|
-
if (/status code 403|status 403|forbidden/i.test(lines)) {
|
|
654
|
-
return [
|
|
655
|
-
`Access denied by ${config.serverUrl}.`,
|
|
656
|
-
'Your token may not have the required permissions ("Execute Analysis").',
|
|
657
|
-
"Run `/sonarqube init` to update the token or check server permissions.",
|
|
658
|
-
].join(" ");
|
|
659
|
-
}
|
|
660
|
-
if (/connect econnrefused|connect refused|connect timeout|enotfound|econnreset/i.test(lines)) {
|
|
661
|
-
return [
|
|
662
|
-
`Cannot reach SonarQube server at ${config.serverUrl}.`,
|
|
663
|
-
"Is the server running? Run `/sonarqube init` to check the server URL.",
|
|
664
|
-
"Default: http://localhost:9000",
|
|
665
|
-
].join(" ");
|
|
666
|
-
}
|
|
667
|
-
if (/not found|unknown url/i.test(lines) && (config.serverUrl === "http://localhost:9000" || /localhost/i.test(config.serverUrl))) {
|
|
668
|
-
return [
|
|
669
|
-
`SonarQube API at ${config.serverUrl} returned "not found".`,
|
|
670
|
-
"The default server URL is http://localhost:9000. Run `/sonarqube init` to configure.",
|
|
671
|
-
].join(" ");
|
|
672
|
-
}
|
|
673
|
-
return "";
|
|
674
|
-
}
|
|
675
|
-
// ── SonarScanner interaction ─────────────────────────────────────────────────
|
|
676
|
-
async function runScanner(pi, config, signal) {
|
|
677
|
-
const args = [
|
|
678
|
-
`-Dsonar.projectKey=${config.projectKey}`,
|
|
679
|
-
`-Dsonar.projectBaseDir=${config.baseDir}`,
|
|
680
|
-
`-Dsonar.host.url=${config.serverUrl}`,
|
|
681
|
-
...(config.hasProperties ? [] : [`-Dsonar.sources=.`]),
|
|
682
|
-
`-Dsonar.scm.disabled=true`,
|
|
683
|
-
];
|
|
684
|
-
if (config.token) {
|
|
685
|
-
args.push(`-Dsonar.token=${config.token}`);
|
|
686
|
-
}
|
|
687
|
-
const result = await pi.exec("sonar-scanner", args, {
|
|
688
|
-
cwd: config.baseDir,
|
|
689
|
-
signal,
|
|
690
|
-
timeout: 30 * 60 * 1000,
|
|
691
|
-
});
|
|
692
|
-
if (result.code !== 0) {
|
|
693
|
-
const output = [result.stdout, result.stderr]
|
|
694
|
-
.filter(Boolean)
|
|
695
|
-
.join("\n")
|
|
696
|
-
.trim();
|
|
697
|
-
const hint = extractScannerHint(output, config);
|
|
698
|
-
const detail = output ? `\n\n${output.slice(-4000)}` : "";
|
|
699
|
-
const message = hint
|
|
700
|
-
? `SonarQube scan failed for ${config.projectKey}. ${hint}`
|
|
701
|
-
: `SonarQube scan failed for ${config.projectKey}${detail}`;
|
|
702
|
-
throw new Error(message);
|
|
703
|
-
}
|
|
704
|
-
return result.stdout || result.stderr;
|
|
705
|
-
}
|
|
706
|
-
async function readReportTask(baseDir) {
|
|
707
|
-
const path = resolve(baseDir, ".scannerwork", "report-task.txt");
|
|
708
|
-
const text = await readFile(path, "utf8");
|
|
709
|
-
return parseProperties(text);
|
|
710
|
-
}
|
|
711
|
-
async function waitForAnalysis(serverUrl, token, ceTaskUrl, signal) {
|
|
712
|
-
for (let attempt = 0; attempt < 120; attempt++) {
|
|
713
|
-
const result = await fetchJson(ceTaskUrl, token, signal);
|
|
714
|
-
const status = result.task.status.toUpperCase();
|
|
715
|
-
if (status === "SUCCESS")
|
|
716
|
-
return result.task.analysisId;
|
|
717
|
-
if (status === "FAILED") {
|
|
718
|
-
throw new Error(`SonarQube analysis failed: ${result.task.errorMessage || "unknown error"}`);
|
|
719
|
-
}
|
|
720
|
-
if (status === "CANCELED") {
|
|
721
|
-
throw new Error("SonarQube analysis was canceled by the server");
|
|
722
|
-
}
|
|
723
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 2000));
|
|
724
|
-
}
|
|
725
|
-
throw new Error(`Timed out waiting for SonarQube analysis at ${serverUrl}`);
|
|
726
|
-
}
|
|
727
|
-
async function fetchRuleName(serverUrl, token, ruleKey, signal) {
|
|
728
|
-
try {
|
|
729
|
-
const result = await fetchJson(`${serverUrl}/api/rules/show?key=${encodeURIComponent(ruleKey)}`, token, signal);
|
|
730
|
-
return result.rule.name;
|
|
731
|
-
}
|
|
732
|
-
catch {
|
|
733
|
-
return undefined;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
async function fetchIssues(serverUrl, token, projectKey, signal, filters) {
|
|
737
|
-
const normalizedFilters = normalizeIssueFilters(filters);
|
|
738
|
-
const issues = [];
|
|
739
|
-
const ruleNames = new Map();
|
|
740
|
-
let page = 1;
|
|
741
|
-
let total = 0;
|
|
742
|
-
for (;;) {
|
|
743
|
-
const url = new URL("/api/issues/search", serverUrl);
|
|
744
|
-
url.searchParams.set("projects", projectKey);
|
|
745
|
-
url.searchParams.set("resolved", "false");
|
|
746
|
-
url.searchParams.set("ps", "100");
|
|
747
|
-
url.searchParams.set("p", String(page));
|
|
748
|
-
if (normalizedFilters?.severities?.length) {
|
|
749
|
-
url.searchParams.set("severities", normalizedFilters.severities.join(","));
|
|
750
|
-
}
|
|
751
|
-
if (normalizedFilters?.statuses?.length) {
|
|
752
|
-
url.searchParams.set("statuses", normalizedFilters.statuses.join(","));
|
|
753
|
-
}
|
|
754
|
-
if (normalizedFilters?.types?.length) {
|
|
755
|
-
url.searchParams.set("types", normalizedFilters.types.join(","));
|
|
756
|
-
}
|
|
757
|
-
if (normalizedFilters?.rules?.length) {
|
|
758
|
-
url.searchParams.set("rules", normalizedFilters.rules.join(","));
|
|
759
|
-
}
|
|
760
|
-
const result = await fetchJson(url.toString(), token, signal);
|
|
761
|
-
total = result.total;
|
|
762
|
-
for (const raw of result.issues) {
|
|
763
|
-
if (!ruleNames.has(raw.rule)) {
|
|
764
|
-
ruleNames.set(raw.rule, await fetchRuleName(serverUrl, token, raw.rule, signal));
|
|
765
|
-
}
|
|
766
|
-
issues.push({
|
|
767
|
-
key: raw.key,
|
|
768
|
-
rule: raw.rule,
|
|
769
|
-
ruleName: ruleNames.get(raw.rule),
|
|
770
|
-
severity: raw.severity,
|
|
771
|
-
message: raw.message,
|
|
772
|
-
filePath: toIssuePath(raw.component, projectKey),
|
|
773
|
-
line: typeof raw.line === "number" ? raw.line : null,
|
|
774
|
-
status: raw.status,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
if (issues.length >= total || result.issues.length === 0)
|
|
778
|
-
break;
|
|
779
|
-
page += 1;
|
|
780
|
-
}
|
|
781
|
-
if (normalizedFilters && issues.length > 1) {
|
|
782
|
-
issues.sort(compareIssuesForContext);
|
|
783
|
-
}
|
|
784
|
-
return issues;
|
|
785
|
-
}
|
|
786
|
-
function createAnalysisState(config, issues, extras = {}) {
|
|
787
|
-
return {
|
|
788
|
-
version: 1,
|
|
789
|
-
analyzedAt: nowIso(),
|
|
790
|
-
baseDir: config.baseDir,
|
|
791
|
-
serverUrl: config.serverUrl,
|
|
792
|
-
projectKey: config.projectKey,
|
|
793
|
-
dashboardUrl: extras.dashboardUrl,
|
|
794
|
-
ceTaskUrl: extras.ceTaskUrl,
|
|
795
|
-
analysisId: extras.analysisId,
|
|
796
|
-
filters: extras.filters,
|
|
797
|
-
totalIssues: issues.length,
|
|
798
|
-
issues,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
async function hasProjectAnalyses(serverUrl, token, projectKey, signal) {
|
|
802
|
-
try {
|
|
803
|
-
const result = await fetchJson(`${serverUrl}/api/project_analyses/search?project=${encodeURIComponent(projectKey)}&ps=1`, token, signal);
|
|
804
|
-
return (result.analyses?.length ?? 0) > 0;
|
|
805
|
-
}
|
|
806
|
-
catch {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
async function loadProjectIssuesFromApi(ctx, inputPath, filters) {
|
|
811
|
-
const config = await resolveConfig(ctx, inputPath);
|
|
812
|
-
const normalizedFilters = normalizeIssueFilters(filters);
|
|
813
|
-
const issues = await fetchIssues(config.serverUrl, config.token, config.projectKey, ctx.signal, normalizedFilters);
|
|
814
|
-
return createAnalysisState(config, issues, { filters: normalizedFilters });
|
|
815
|
-
}
|
|
816
|
-
async function resolveTargetState(ctx, statesByBaseDir, targetInput, filters) {
|
|
817
|
-
const resolvedTarget = await resolveTarget(ctx, targetInput);
|
|
818
|
-
try {
|
|
819
|
-
return await loadProjectIssuesFromApi(ctx, targetInput, filters);
|
|
820
|
-
}
|
|
821
|
-
catch (error) {
|
|
822
|
-
const cached = statesByBaseDir.get(resolvedTarget.baseDir);
|
|
823
|
-
if (cached)
|
|
824
|
-
return cached;
|
|
825
|
-
throw error;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// ── Analysis orchestration ────────────────────────────────────────────────────
|
|
32
|
+
// ── Analysis orchestration ──────────────────────────────────────────────────
|
|
829
33
|
async function analyzeProject(pi, ctx, inputPath, filters) {
|
|
830
34
|
const config = await resolveConfig(ctx, inputPath);
|
|
831
35
|
const baseDirStat = await stat(config.baseDir).catch(() => undefined);
|
|
@@ -841,7 +45,8 @@ async function analyzeProject(pi, ctx, inputPath, filters) {
|
|
|
841
45
|
const ceTaskUrl = report["ceTaskUrl"];
|
|
842
46
|
const dashboardUrl = report["dashboardUrl"];
|
|
843
47
|
const analysisId = ceTaskUrl
|
|
844
|
-
? (analysisUi.setPhase("Waiting for SonarQube analysis..."),
|
|
48
|
+
? (analysisUi.setPhase("Waiting for SonarQube analysis..."),
|
|
49
|
+
await waitForAnalysis(config.serverUrl, config.token, ceTaskUrl, ctx.signal))
|
|
845
50
|
: undefined;
|
|
846
51
|
analysisUi.setPhase("Fetching issues...");
|
|
847
52
|
const normalizedFilters = normalizeIssueFilters(filters);
|
|
@@ -862,114 +67,93 @@ async function analyzeProject(pi, ctx, inputPath, filters) {
|
|
|
862
67
|
analysisUi.stop();
|
|
863
68
|
}
|
|
864
69
|
}
|
|
865
|
-
// ──
|
|
866
|
-
async function
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (ctx.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
async function initCommandHandler(ctx, options = {}) {
|
|
883
|
-
const { baseDir, repoRoot } = await resolveInitTarget(ctx, options.alias, options.targetInput);
|
|
884
|
-
const existing = await loadProjectConfig(baseDir);
|
|
885
|
-
if (existing) {
|
|
886
|
-
const ok = await ctx.ui.confirm("Overwrite?", `SonarQube config already exists at ${baseDir}:\n Server: ${existing.serverUrl}\n Project: ${existing.projectKey}\n\nOverwrite?`);
|
|
887
|
-
if (!ok) {
|
|
888
|
-
if (ctx.hasUI)
|
|
889
|
-
ctx.ui.notify("Init cancelled.", "info");
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
70
|
+
// ── Init command helpers (extracted to reduce cognitive complexity) ─────────
|
|
71
|
+
async function confirmOverwrite(ctx, baseDir, existing) {
|
|
72
|
+
const ok = await ctx.ui.confirm("Overwrite?", [
|
|
73
|
+
`SonarQube config already exists at ${baseDir}:`,
|
|
74
|
+
` Server: ${existing.serverUrl}`,
|
|
75
|
+
` Project: ${existing.projectKey}`,
|
|
76
|
+
"",
|
|
77
|
+
"Overwrite?",
|
|
78
|
+
].join("\n"));
|
|
79
|
+
if (ok)
|
|
80
|
+
return true;
|
|
81
|
+
if (ctx.hasUI)
|
|
82
|
+
ctx.ui.notify("Init cancelled.", "info");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
async function collectInitInputs(ctx, existing, baseDir) {
|
|
893
86
|
const serverUrlInput = await ctx.ui.editor("SonarQube server URL", existing?.serverUrl ?? "http://localhost:9000");
|
|
894
87
|
if (serverUrlInput === undefined)
|
|
895
|
-
return;
|
|
88
|
+
return undefined;
|
|
896
89
|
const serverUrl = normalizeServerUrl(serverUrlInput || existing?.serverUrl || "http://localhost:9000");
|
|
897
90
|
const projectKeyInput = await ctx.ui.input("SonarQube project key", existing?.projectKey ?? basename(baseDir));
|
|
898
91
|
if (projectKeyInput === undefined)
|
|
899
|
-
return;
|
|
92
|
+
return undefined;
|
|
900
93
|
const projectKey = projectKeyInput || existing?.projectKey || slugify(basename(baseDir));
|
|
901
94
|
const tokenInput = await ctx.ui.input("SonarQube token (optional, press Enter to skip)", existing?.token ?? "");
|
|
902
95
|
if (tokenInput === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
return { serverUrl, projectKey, token: tokenInput.trim() || undefined };
|
|
98
|
+
}
|
|
99
|
+
async function initCommandHandler(ctx, options = {}) {
|
|
100
|
+
const { baseDir, repoRoot } = await resolveInitTarget(ctx, options.alias, options.targetInput);
|
|
101
|
+
const existing = await loadProjectConfig(baseDir);
|
|
102
|
+
const confirmed = existing ? await confirmOverwrite(ctx, baseDir, existing) : true;
|
|
103
|
+
if (!confirmed)
|
|
903
104
|
return;
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
});
|
|
105
|
+
const inputs = await collectInitInputs(ctx, existing, baseDir);
|
|
106
|
+
if (!inputs)
|
|
107
|
+
return;
|
|
108
|
+
await saveProjectConfig(baseDir, inputs);
|
|
109
|
+
const propertiesState = await ensureDefaultSonarProjectProperties(baseDir);
|
|
910
110
|
if (options.alias) {
|
|
911
111
|
await saveWorkspaceRegistry(repoRoot, options.alias, baseDir);
|
|
912
112
|
}
|
|
913
113
|
if (ctx.hasUI) {
|
|
914
|
-
|
|
915
|
-
ctx.ui.notify(`SonarQube config saved${targetLabel} to ${projectConfigPath(baseDir)}`, "info");
|
|
114
|
+
notifyInitResult(ctx, baseDir, propertiesState, options.alias);
|
|
916
115
|
}
|
|
917
116
|
}
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
"Filters:",
|
|
929
|
-
" /sonarqube issues be CRITICAL",
|
|
930
|
-
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
931
|
-
"",
|
|
932
|
-
"Autocomplete:",
|
|
933
|
-
" type /sonarqube and press Tab to complete the subcommand or filters",
|
|
934
|
-
"",
|
|
935
|
-
"Defaults:",
|
|
936
|
-
" config is stored in .pi/sonarqube.json",
|
|
937
|
-
" use project paths directly (no alias needed)",
|
|
938
|
-
].join("\n");
|
|
117
|
+
function notifyInitResult(ctx, baseDir, propertiesState, alias) {
|
|
118
|
+
const label = alias ? ` (${alias})` : "";
|
|
119
|
+
let note = "";
|
|
120
|
+
if (propertiesState === "created") {
|
|
121
|
+
note = ` and created ${sonarProjectPropertiesPath(baseDir)}`;
|
|
122
|
+
}
|
|
123
|
+
else if (propertiesState === "updated") {
|
|
124
|
+
note = ` and merged ${sonarProjectPropertiesPath(baseDir)}`;
|
|
125
|
+
}
|
|
126
|
+
ctx.ui.notify(`SonarQube config saved${label} to ${projectConfigPath(baseDir)}${note}`, "info");
|
|
939
127
|
}
|
|
940
|
-
|
|
941
|
-
|
|
128
|
+
// ── Tool result rendering helpers (reduces renderResult complexity) ─────────
|
|
129
|
+
function renderErrorResult(errorMsg, theme) {
|
|
130
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
942
131
|
}
|
|
943
|
-
function
|
|
944
|
-
return
|
|
132
|
+
function renderEmptyResult(summary, theme) {
|
|
133
|
+
return new Text(theme.fg("success", summary), 0, 0);
|
|
945
134
|
}
|
|
946
|
-
function
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
? { action: "init", targetInput: tokens[1] }
|
|
957
|
-
: { action: "init", alias: tokens[1] };
|
|
958
|
-
}
|
|
959
|
-
return { action: "init", alias: tokens[1], targetInput: tokens[2] };
|
|
960
|
-
}
|
|
961
|
-
if (head === "issues" || head === "view") {
|
|
962
|
-
return { action: "issues", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
135
|
+
function renderIssueResult(issue, contentText, expanded, theme) {
|
|
136
|
+
const text = contentText ?? formatIssue(issue);
|
|
137
|
+
const preview = expanded ? text.split("\n").slice(1).join("\n") : "";
|
|
138
|
+
const label = theme.fg("accent", formatIssue(issue));
|
|
139
|
+
return new Text(preview ? `${label}\n${preview}` : label, 0, 0);
|
|
140
|
+
}
|
|
141
|
+
function renderIssueListResult(state, expanded, theme) {
|
|
142
|
+
const summary = formatSummary(state);
|
|
143
|
+
if (state.issues.length === 0) {
|
|
144
|
+
return new Text(theme.fg("success", summary), 0, 0);
|
|
963
145
|
}
|
|
964
|
-
|
|
965
|
-
|
|
146
|
+
const visible = expanded ? state.issues : state.issues.slice(0, 5);
|
|
147
|
+
const lines = [theme.fg("accent", summary)];
|
|
148
|
+
for (const [i, issue] of visible.entries()) {
|
|
149
|
+
lines.push(theme.fg("muted", formatIssue(issue, i + 1)));
|
|
966
150
|
}
|
|
967
|
-
if (
|
|
968
|
-
|
|
151
|
+
if (!expanded && state.issues.length > visible.length) {
|
|
152
|
+
lines.push(theme.fg("dim", `... ${state.issues.length - visible.length} more`));
|
|
969
153
|
}
|
|
970
|
-
return
|
|
154
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
971
155
|
}
|
|
972
|
-
// ── Extension entrypoint
|
|
156
|
+
// ── Extension entrypoint ────────────────────────────────────────────────────
|
|
973
157
|
export default function sonarqube(pi) {
|
|
974
158
|
const statesByBaseDir = new Map();
|
|
975
159
|
let latestState;
|
|
@@ -987,10 +171,9 @@ export default function sonarqube(pi) {
|
|
|
987
171
|
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
988
172
|
const line = lines[cursorLine] ?? "";
|
|
989
173
|
const beforeCursor = line.slice(0, cursorCol);
|
|
990
|
-
const match =
|
|
991
|
-
if (!match)
|
|
174
|
+
const match = /^\/sonarqube(?:\s+(.*))?$/.exec(beforeCursor);
|
|
175
|
+
if (!match)
|
|
992
176
|
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
993
|
-
}
|
|
994
177
|
const argumentText = match[1] ?? "";
|
|
995
178
|
const { current: currentToken } = splitSonarArgumentContext(argumentText);
|
|
996
179
|
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues) ?? [];
|
|
@@ -1031,10 +214,7 @@ export default function sonarqube(pi) {
|
|
|
1031
214
|
if (params.action === "analyze") {
|
|
1032
215
|
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
1033
216
|
rememberState(state);
|
|
1034
|
-
return {
|
|
1035
|
-
content: [{ type: "text", text: formatReport(state) }],
|
|
1036
|
-
details: state,
|
|
1037
|
-
};
|
|
217
|
+
return { content: [{ type: "text", text: formatReport(state) }], details: state };
|
|
1038
218
|
}
|
|
1039
219
|
const targetState = await resolveTargetState(ctx, statesByBaseDir, params.path, filters);
|
|
1040
220
|
if (!targetState) {
|
|
@@ -1045,10 +225,7 @@ export default function sonarqube(pi) {
|
|
|
1045
225
|
}
|
|
1046
226
|
rememberState(targetState);
|
|
1047
227
|
if (params.action === "issues") {
|
|
1048
|
-
return {
|
|
1049
|
-
content: [{ type: "text", text: formatReport(targetState) }],
|
|
1050
|
-
details: targetState,
|
|
1051
|
-
};
|
|
228
|
+
return { content: [{ type: "text", text: formatReport(targetState) }], details: targetState };
|
|
1052
229
|
}
|
|
1053
230
|
const index = params.issueIndex ?? 1;
|
|
1054
231
|
const issue = targetState.issues[index - 1];
|
|
@@ -1065,8 +242,8 @@ export default function sonarqube(pi) {
|
|
|
1065
242
|
};
|
|
1066
243
|
}
|
|
1067
244
|
catch (error) {
|
|
1068
|
-
const
|
|
1069
|
-
return { content: [{ type: "text", text:
|
|
245
|
+
const msg = sonarErrorMessage(error);
|
|
246
|
+
return { content: [{ type: "text", text: msg }], details: { error: msg } };
|
|
1070
247
|
}
|
|
1071
248
|
},
|
|
1072
249
|
renderCall(args, theme, _context) {
|
|
@@ -1081,29 +258,20 @@ export default function sonarqube(pi) {
|
|
|
1081
258
|
},
|
|
1082
259
|
renderResult(result, { expanded }, theme, _context) {
|
|
1083
260
|
const state = result.details;
|
|
1084
|
-
if (state
|
|
1085
|
-
|
|
261
|
+
if (!state) {
|
|
262
|
+
const text = result.content[0];
|
|
263
|
+
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
1086
264
|
}
|
|
1087
|
-
if (
|
|
265
|
+
if ("error" in state && state.error) {
|
|
266
|
+
return renderErrorResult(state.error, theme);
|
|
267
|
+
}
|
|
268
|
+
if ("selectedIssue" in state && state.selectedIssue) {
|
|
1088
269
|
const issue = state.selectedIssue;
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
return new Text(`${theme.fg("accent", formatIssue(issue))}${preview ? `\n${preview}` : ""}`, 0, 0);
|
|
270
|
+
const contentText = result.content[0]?.type === "text" ? result.content[0].text : undefined;
|
|
271
|
+
return renderIssueResult(issue, contentText, expanded, theme);
|
|
1092
272
|
}
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1095
|
-
if (state.issues.length === 0) {
|
|
1096
|
-
return new Text(theme.fg("success", summary), 0, 0);
|
|
1097
|
-
}
|
|
1098
|
-
const visible = expanded ? state.issues : state.issues.slice(0, 5);
|
|
1099
|
-
const lines = [theme.fg("accent", summary)];
|
|
1100
|
-
visible.forEach((issue, index) => {
|
|
1101
|
-
lines.push(theme.fg("muted", formatIssue(issue, index + 1)));
|
|
1102
|
-
});
|
|
1103
|
-
if (!expanded && state.issues.length > visible.length) {
|
|
1104
|
-
lines.push(theme.fg("dim", `... ${state.issues.length - visible.length} more`));
|
|
1105
|
-
}
|
|
1106
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
273
|
+
if ("issues" in state) {
|
|
274
|
+
return renderIssueListResult(state, expanded, theme);
|
|
1107
275
|
}
|
|
1108
276
|
const text = result.content[0];
|
|
1109
277
|
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
@@ -1115,60 +283,21 @@ export default function sonarqube(pi) {
|
|
|
1115
283
|
handler: async (args, ctx) => {
|
|
1116
284
|
try {
|
|
1117
285
|
const parsed = parseCommandArgs(args);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (ctx.hasUI) {
|
|
1121
|
-
ctx.ui.notify(helpText(), "info");
|
|
1122
|
-
}
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
// --- Init ---
|
|
1126
|
-
if (parsed.action === "init") {
|
|
1127
|
-
await initCommandHandler(ctx, { alias: parsed.alias, targetInput: parsed.targetInput });
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
// --- Analyze ---
|
|
1131
|
-
if (parsed.action === "analyze") {
|
|
1132
|
-
const state = await analyzeProject(pi, ctx, parsed.targetInput, parsed.filters);
|
|
1133
|
-
rememberState(state);
|
|
1134
|
-
if (ctx.hasUI) {
|
|
1135
|
-
const scope = targetLabel(parsed.targetInput);
|
|
1136
|
-
ctx.ui.notify(`${formatSummary(state)}. Use /sonarqube issues${scope} to browse.`, state.issues.length === 0 ? "info" : "warning");
|
|
1137
|
-
}
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
const targetState = await resolveTargetState(ctx, statesByBaseDir, parsed.targetInput, parsed.filters);
|
|
1141
|
-
if (!targetState) {
|
|
1142
|
-
if (ctx.hasUI) {
|
|
1143
|
-
ctx.ui.notify(`No SonarQube analysis found for this target. Run /sonarqube analyze${targetLabel(parsed.targetInput)} first.`, "warning");
|
|
1144
|
-
}
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
rememberState(targetState);
|
|
1148
|
-
// --- Issues ---
|
|
1149
|
-
if (parsed.action === "issues") {
|
|
1150
|
-
if (targetState.issues.length === 0) {
|
|
286
|
+
switch (parsed.action) {
|
|
287
|
+
case "help":
|
|
1151
288
|
if (ctx.hasUI)
|
|
1152
|
-
ctx.ui.notify(
|
|
289
|
+
ctx.ui.notify(helpText(), "info");
|
|
290
|
+
return;
|
|
291
|
+
case "init":
|
|
292
|
+
await initCommandHandler(ctx, { alias: parsed.alias, targetInput: parsed.targetInput });
|
|
293
|
+
return;
|
|
294
|
+
case "analyze": {
|
|
295
|
+
await commandAnalyze(pi, ctx, parsed.targetInput, parsed.filters, rememberState);
|
|
1153
296
|
return;
|
|
1154
297
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const issue = targetState.issues[choice];
|
|
1158
|
-
if (issue)
|
|
1159
|
-
await openIssuePreview(ctx, targetState, issue);
|
|
1160
|
-
}
|
|
1161
|
-
return;
|
|
298
|
+
default:
|
|
299
|
+
await commandIssuesOrOpen(pi, ctx, statesByBaseDir, parsed, rememberState);
|
|
1162
300
|
}
|
|
1163
|
-
// --- Open ---
|
|
1164
|
-
const index = parsed.issueIndex ?? 1;
|
|
1165
|
-
const issue = targetState.issues[index - 1];
|
|
1166
|
-
if (!issue) {
|
|
1167
|
-
if (ctx.hasUI)
|
|
1168
|
-
ctx.ui.notify(`Issue #${index} was not found.`, "error");
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
await openIssuePreview(ctx, targetState, issue);
|
|
1172
301
|
}
|
|
1173
302
|
catch (error) {
|
|
1174
303
|
if (ctx.hasUI) {
|
|
@@ -1180,4 +309,49 @@ export default function sonarqube(pi) {
|
|
|
1180
309
|
},
|
|
1181
310
|
});
|
|
1182
311
|
}
|
|
312
|
+
async function commandAnalyze(pi, ctx, targetInput, filters, rememberState) {
|
|
313
|
+
const state = await analyzeProject(pi, ctx, targetInput, filters);
|
|
314
|
+
rememberState(state);
|
|
315
|
+
if (ctx.hasUI) {
|
|
316
|
+
const scope = targetLabel(targetInput);
|
|
317
|
+
ctx.ui.notify(`${formatSummary(state)}. Use /sonarqube issues${scope} to browse.`, state.issues.length === 0 ? "info" : "warning");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function commandIssuesOrOpen(pi, ctx, statesByBaseDir, parsed, rememberState) {
|
|
321
|
+
const targetState = await resolveTargetState(ctx, statesByBaseDir, parsed.targetInput, parsed.filters);
|
|
322
|
+
if (!targetState) {
|
|
323
|
+
if (ctx.hasUI) {
|
|
324
|
+
ctx.ui.notify(`No SonarQube analysis found for this target. Run /sonarqube analyze${targetLabel(parsed.targetInput)} first.`, "warning");
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
rememberState(targetState);
|
|
329
|
+
if (parsed.action === "issues") {
|
|
330
|
+
await commandShowIssues(ctx, targetState);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// "open"
|
|
334
|
+
const index = parsed.issueIndex ?? 1;
|
|
335
|
+
const issue = targetState.issues[index - 1];
|
|
336
|
+
if (!issue) {
|
|
337
|
+
if (ctx.hasUI)
|
|
338
|
+
ctx.ui.notify(`Issue #${index} was not found.`, "error");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await openIssuePreview(ctx, targetState, issue);
|
|
342
|
+
}
|
|
343
|
+
async function commandShowIssues(ctx, targetState) {
|
|
344
|
+
if (targetState.issues.length === 0) {
|
|
345
|
+
if (ctx.hasUI)
|
|
346
|
+
ctx.ui.notify(formatSummary(targetState), "info");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const choice = await showIssueBrowser(ctx, targetState);
|
|
350
|
+
if (choice != null) {
|
|
351
|
+
const issue = targetState.issues[choice];
|
|
352
|
+
if (issue)
|
|
353
|
+
await openIssuePreview(ctx, targetState, issue);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export { projectConfigPath, loadProjectConfig, saveProjectConfig } from "./config.js";
|
|
1183
357
|
//# sourceMappingURL=index.js.map
|