@jonathan0823/pi-sonarqube 0.4.0 → 0.5.0
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.map +1 -1
- package/dist/api.js +48 -13
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +66 -19
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -29
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -4
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +41 -11
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +212 -58
- package/src/commands.ts +155 -53
- package/src/config.ts +84 -23
- package/src/index.ts +423 -86
- package/src/types.ts +27 -4
- package/src/ui.ts +121 -26
package/src/commands.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteItem,
|
|
3
|
+
AutocompleteSuggestions,
|
|
4
|
+
} from "@earendil-works/pi-tui";
|
|
5
|
+
import type {
|
|
6
|
+
SonarIssue,
|
|
7
|
+
SonarIssueFetchOptions,
|
|
8
|
+
SonarDuplicationMeasures,
|
|
9
|
+
IssueSeverityCounts,
|
|
10
|
+
IssueQualityCounts,
|
|
11
|
+
FileDuplication,
|
|
12
|
+
DuplicationBlockGroup,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import {
|
|
15
|
+
SONAR_SEVERITIES,
|
|
16
|
+
SONAR_STATUSES,
|
|
17
|
+
SONAR_TYPES,
|
|
18
|
+
SONAR_SOFTWARE_QUALITIES,
|
|
19
|
+
SONAR_IMPACT_SEVERITIES,
|
|
20
|
+
} from "./types.js";
|
|
4
21
|
import { looksLikePath } from "./config.js";
|
|
5
22
|
import { parseSonarIssueArgs, issueFilterLabel } from "./api.js";
|
|
6
23
|
|
|
@@ -9,12 +26,24 @@ import { parseSonarIssueArgs, issueFilterLabel } from "./api.js";
|
|
|
9
26
|
export const STATE_TYPE = "sonarqube-analysis-state";
|
|
10
27
|
|
|
11
28
|
const SONAR_COMMANDS = [
|
|
12
|
-
{
|
|
29
|
+
{
|
|
30
|
+
value: "analyze",
|
|
31
|
+
label: "analyze",
|
|
32
|
+
description: "run analysis and fetch issues",
|
|
33
|
+
},
|
|
13
34
|
{ value: "issues", label: "issues", description: "browse the latest issues" },
|
|
14
35
|
{ value: "open", label: "open", description: "preview a specific issue" },
|
|
15
36
|
{ value: "init", label: "init", description: "configure a project target" },
|
|
16
|
-
{
|
|
17
|
-
|
|
37
|
+
{
|
|
38
|
+
value: "metrics",
|
|
39
|
+
label: "metrics",
|
|
40
|
+
description: "show project metrics (duplication, issue counts)",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "duplications",
|
|
44
|
+
label: "duplications",
|
|
45
|
+
description: "browse duplicated files and blocks",
|
|
46
|
+
},
|
|
18
47
|
] as const;
|
|
19
48
|
|
|
20
49
|
// ── Autocomplete helpers ────────────────────────────────────────────────────
|
|
@@ -24,13 +53,23 @@ export function filterAutocompleteItems(
|
|
|
24
53
|
prefix: string,
|
|
25
54
|
): AutocompleteItem[] {
|
|
26
55
|
const query = prefix.trim().toLowerCase();
|
|
27
|
-
if (!query)
|
|
56
|
+
if (!query)
|
|
57
|
+
return items.map((item) => ({
|
|
58
|
+
value: item.value,
|
|
59
|
+
label: item.label,
|
|
60
|
+
description: item.description,
|
|
61
|
+
}));
|
|
28
62
|
return items
|
|
29
63
|
.filter(
|
|
30
64
|
(item) =>
|
|
31
|
-
item.value.toLowerCase().startsWith(query) ||
|
|
65
|
+
item.value.toLowerCase().startsWith(query) ||
|
|
66
|
+
item.label.toLowerCase().startsWith(query),
|
|
32
67
|
)
|
|
33
|
-
.map((item) => ({
|
|
68
|
+
.map((item) => ({
|
|
69
|
+
value: item.value,
|
|
70
|
+
label: item.label,
|
|
71
|
+
description: item.description,
|
|
72
|
+
}));
|
|
34
73
|
}
|
|
35
74
|
|
|
36
75
|
export function mergeAutocompleteSuggestions(
|
|
@@ -45,14 +84,16 @@ export function mergeAutocompleteSuggestions(
|
|
|
45
84
|
return items.length > 0 ? { prefix, items } : current;
|
|
46
85
|
}
|
|
47
86
|
|
|
48
|
-
export function splitSonarArgumentContext(
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
export function splitSonarArgumentContext(argumentText: string): {
|
|
88
|
+
command: string;
|
|
89
|
+
current: string;
|
|
90
|
+
tokens: string[];
|
|
91
|
+
} {
|
|
51
92
|
const trimmedLeft = argumentText.replace(/^\s+/, "");
|
|
52
93
|
if (!trimmedLeft) return { command: "", current: "", tokens: [] };
|
|
53
94
|
|
|
54
95
|
const tokens = trimmedLeft.split(/\s+/);
|
|
55
|
-
const current = /\s$/.test(argumentText) ? "" : tokens.pop() ?? "";
|
|
96
|
+
const current = /\s$/.test(argumentText) ? "" : (tokens.pop() ?? "");
|
|
56
97
|
const command = tokens[0] ?? current;
|
|
57
98
|
let tokensOut: string[];
|
|
58
99
|
if (tokens.length > 0) {
|
|
@@ -69,13 +110,21 @@ export function splitSonarArgumentContext(
|
|
|
69
110
|
};
|
|
70
111
|
}
|
|
71
112
|
|
|
72
|
-
function createFilterCompletionList(
|
|
113
|
+
function createFilterCompletionList(
|
|
114
|
+
mode?: "STANDARD" | "MQR",
|
|
115
|
+
): AutocompleteItem[] {
|
|
73
116
|
const buildItems = (
|
|
74
|
-
groups: ReadonlyArray<
|
|
117
|
+
groups: ReadonlyArray<
|
|
118
|
+
readonly [string, readonly string[], string, boolean]
|
|
119
|
+
>,
|
|
75
120
|
): AutocompleteItem[] =>
|
|
76
121
|
groups.flatMap(([prefix, values, description, includeBare]) =>
|
|
77
122
|
values.flatMap((value) => [
|
|
78
|
-
{
|
|
123
|
+
{
|
|
124
|
+
value: `${prefix}:${value}`,
|
|
125
|
+
label: `${prefix}:${value}`,
|
|
126
|
+
description,
|
|
127
|
+
},
|
|
79
128
|
...(includeBare ? [{ value, label: value, description }] : []),
|
|
80
129
|
]),
|
|
81
130
|
);
|
|
@@ -90,7 +139,9 @@ function createFilterCompletionList(mode?: "STANDARD" | "MQR"): AutocompleteItem
|
|
|
90
139
|
["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
|
|
91
140
|
] as const;
|
|
92
141
|
|
|
93
|
-
return mode === "MQR"
|
|
142
|
+
return mode === "MQR"
|
|
143
|
+
? [...buildItems(mqrGroups), ...buildItems(legacyGroups)]
|
|
144
|
+
: [...buildItems(legacyGroups), ...buildItems(mqrGroups)];
|
|
94
145
|
}
|
|
95
146
|
|
|
96
147
|
export function sonarArgumentCompletions(
|
|
@@ -100,18 +151,30 @@ export function sonarArgumentCompletions(
|
|
|
100
151
|
): AutocompleteItem[] | null {
|
|
101
152
|
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
102
153
|
const lowerCommand = command.toLowerCase();
|
|
103
|
-
const commandMatches = SONAR_COMMANDS.filter((item) =>
|
|
104
|
-
|
|
154
|
+
const commandMatches = SONAR_COMMANDS.filter((item) =>
|
|
155
|
+
item.value.startsWith(lowerCommand),
|
|
156
|
+
);
|
|
157
|
+
const isFullMatch = SONAR_COMMANDS.some(
|
|
158
|
+
(item) => item.value === lowerCommand,
|
|
159
|
+
);
|
|
105
160
|
|
|
106
161
|
if (!command || commandMatches.length > 1 || !isFullMatch) {
|
|
107
162
|
if (commandMatches.length === 0) return null;
|
|
108
|
-
return commandMatches.map((item) => ({
|
|
163
|
+
return commandMatches.map((item) => ({
|
|
164
|
+
value: item.value,
|
|
165
|
+
label: item.value,
|
|
166
|
+
description: item.description,
|
|
167
|
+
}));
|
|
109
168
|
}
|
|
110
169
|
|
|
111
170
|
if (lowerCommand === "open") {
|
|
112
171
|
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
|
|
113
172
|
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
114
|
-
return {
|
|
173
|
+
return {
|
|
174
|
+
value: String(index + 1),
|
|
175
|
+
label: String(index + 1),
|
|
176
|
+
description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
177
|
+
};
|
|
115
178
|
});
|
|
116
179
|
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
117
180
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
@@ -124,11 +187,17 @@ export function sonarArgumentCompletions(
|
|
|
124
187
|
return filtered.length > 0 ? filtered : null;
|
|
125
188
|
}
|
|
126
189
|
|
|
127
|
-
if (
|
|
190
|
+
if (
|
|
191
|
+
lowerCommand === "init" ||
|
|
192
|
+
lowerCommand === "metrics" ||
|
|
193
|
+
lowerCommand === "duplications"
|
|
194
|
+
) {
|
|
128
195
|
return null;
|
|
129
196
|
}
|
|
130
197
|
|
|
131
|
-
return tokens.length === 0
|
|
198
|
+
return tokens.length === 0
|
|
199
|
+
? filterAutocompleteItems(SONAR_COMMANDS, current)
|
|
200
|
+
: null;
|
|
132
201
|
}
|
|
133
202
|
|
|
134
203
|
// ── Command arg parsing ─────────────────────────────────────────────────────
|
|
@@ -136,13 +205,21 @@ export function sonarArgumentCompletions(
|
|
|
136
205
|
export type ParsedSonarCommand =
|
|
137
206
|
| { action: "help" }
|
|
138
207
|
| { action: "init"; alias?: string; targetInput?: string }
|
|
139
|
-
| {
|
|
208
|
+
| {
|
|
209
|
+
action: "analyze";
|
|
210
|
+
targetInput?: string;
|
|
211
|
+
filters?: SonarIssueFetchOptions;
|
|
212
|
+
}
|
|
140
213
|
| { action: "issues"; targetInput?: string; filters?: SonarIssueFetchOptions }
|
|
141
|
-
| {
|
|
214
|
+
| {
|
|
215
|
+
action: "open";
|
|
216
|
+
targetInput?: string;
|
|
217
|
+
issueIndex?: number;
|
|
218
|
+
filters?: SonarIssueFetchOptions;
|
|
219
|
+
}
|
|
142
220
|
| { action: "metrics"; targetInput?: string }
|
|
143
221
|
| { action: "duplications"; targetInput?: string; issueIndex?: number };
|
|
144
222
|
|
|
145
|
-
|
|
146
223
|
export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
147
224
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
148
225
|
if (tokens.length === 0) return { action: "help" };
|
|
@@ -170,7 +247,10 @@ export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
|
170
247
|
return { action: "metrics", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
171
248
|
}
|
|
172
249
|
if (head === "duplications") {
|
|
173
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
action: "duplications",
|
|
252
|
+
...parseSonarIssueArgs(tokens.slice(1), true),
|
|
253
|
+
};
|
|
174
254
|
}
|
|
175
255
|
|
|
176
256
|
return { action: "analyze", ...parseSonarIssueArgs(tokens) };
|
|
@@ -221,8 +301,11 @@ export function sonarErrorMessage(error: unknown): string {
|
|
|
221
301
|
|
|
222
302
|
export function formatIssue(issue: SonarIssue, index?: number): string {
|
|
223
303
|
const loc = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
224
|
-
const prefix =
|
|
225
|
-
|
|
304
|
+
const prefix =
|
|
305
|
+
typeof index === "number" ? `${String(index).padStart(2, "0")}. ` : "";
|
|
306
|
+
const rule = issue.ruleName
|
|
307
|
+
? `${issue.rule} (${issue.ruleName})`
|
|
308
|
+
: issue.rule;
|
|
226
309
|
return `${prefix}${issue.severity} ${loc} — ${rule} — ${issue.message}`;
|
|
227
310
|
}
|
|
228
311
|
|
|
@@ -240,10 +323,14 @@ export function formatMetricsOutput(metrics: {
|
|
|
240
323
|
lines.push("Coverage: n/a");
|
|
241
324
|
} else {
|
|
242
325
|
const pct = metrics.measures.coverage.toFixed(1);
|
|
243
|
-
const covered =
|
|
326
|
+
const covered =
|
|
327
|
+
(metrics.measures.linesToCover ?? 0) -
|
|
328
|
+
(metrics.measures.uncoveredLines ?? 0);
|
|
244
329
|
const uncovered = metrics.measures.uncoveredLines ?? 0;
|
|
245
330
|
const total = metrics.measures.linesToCover ?? 0;
|
|
246
|
-
lines.push(
|
|
331
|
+
lines.push(
|
|
332
|
+
`Coverage: ${pct}% covered=${covered} uncovered=${uncovered} lines=${total}`,
|
|
333
|
+
);
|
|
247
334
|
}
|
|
248
335
|
|
|
249
336
|
const density = metrics.measures.duplicatedLinesDensity.toFixed(1);
|
|
@@ -255,21 +342,22 @@ export function formatMetricsOutput(metrics: {
|
|
|
255
342
|
}
|
|
256
343
|
|
|
257
344
|
if (metrics.issueCounts) {
|
|
258
|
-
const counts =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
345
|
+
const counts =
|
|
346
|
+
metrics.cleanCodeMode === "MQR"
|
|
347
|
+
? [
|
|
348
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
349
|
+
`HIGH ${metrics.issueCounts.critical}`,
|
|
350
|
+
`MEDIUM ${metrics.issueCounts.major}`,
|
|
351
|
+
`LOW ${metrics.issueCounts.minor}`,
|
|
352
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
353
|
+
]
|
|
354
|
+
: [
|
|
355
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
356
|
+
`CRITICAL ${metrics.issueCounts.critical}`,
|
|
357
|
+
`MAJOR ${metrics.issueCounts.major}`,
|
|
358
|
+
`MINOR ${metrics.issueCounts.minor}`,
|
|
359
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
360
|
+
];
|
|
273
361
|
lines.push(`Issues: ${counts.join(" ")}`);
|
|
274
362
|
}
|
|
275
363
|
|
|
@@ -289,12 +377,17 @@ export function formatDuplicationsList(files: FileDuplication[]): string {
|
|
|
289
377
|
if (files.length === 0) return "No duplicated files found.";
|
|
290
378
|
const lines: string[] = [`Duplicated files (${files.length})`];
|
|
291
379
|
for (const [i, f] of files.entries()) {
|
|
292
|
-
lines.push(
|
|
380
|
+
lines.push(
|
|
381
|
+
` ${String(i + 1).padStart(2, " ")}. ${f.filePath} dup%=${f.duplicatedLinesDensity.toFixed(1)} blocks=${f.duplicatedBlocks} lines=${f.duplicatedLines}`,
|
|
382
|
+
);
|
|
293
383
|
}
|
|
294
384
|
return lines.join("\n");
|
|
295
385
|
}
|
|
296
386
|
|
|
297
|
-
export function formatDuplicationBlockDetail(
|
|
387
|
+
export function formatDuplicationBlockDetail(
|
|
388
|
+
filePath: string,
|
|
389
|
+
groups: DuplicationBlockGroup[],
|
|
390
|
+
): string {
|
|
298
391
|
if (groups.length === 0) return `No duplications found in ${filePath}.`;
|
|
299
392
|
const lines: string[] = [`Duplications in ${filePath}`, ""];
|
|
300
393
|
for (const [i, group] of groups.entries()) {
|
|
@@ -308,9 +401,15 @@ export function formatDuplicationBlockDetail(filePath: string, groups: Duplicati
|
|
|
308
401
|
return lines.join("\n");
|
|
309
402
|
}
|
|
310
403
|
|
|
311
|
-
export function formatSummary(state: {
|
|
404
|
+
export function formatSummary(state: {
|
|
405
|
+
totalIssues: number;
|
|
406
|
+
projectKey: string;
|
|
407
|
+
filters?: SonarIssueFetchOptions;
|
|
408
|
+
}): string {
|
|
312
409
|
const issueCount = state.totalIssues;
|
|
313
|
-
const filterLabel = state.filters
|
|
410
|
+
const filterLabel = state.filters
|
|
411
|
+
? ` (${issueFilterLabel(state.filters)})`
|
|
412
|
+
: "";
|
|
314
413
|
if (issueCount === 0) {
|
|
315
414
|
return `SonarQube: no issues found for ${state.projectKey}${filterLabel}`;
|
|
316
415
|
}
|
|
@@ -339,9 +438,12 @@ export function formatReport(state: {
|
|
|
339
438
|
}
|
|
340
439
|
|
|
341
440
|
if (state.measures && state.issues.length > 0) {
|
|
342
|
-
lines.push(
|
|
343
|
-
|
|
344
|
-
|
|
441
|
+
lines.push(
|
|
442
|
+
"",
|
|
443
|
+
state.measures.duplicatedBlocks > 0
|
|
444
|
+
? `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
|
|
445
|
+
: `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% (no duplications detected)`,
|
|
446
|
+
);
|
|
345
447
|
}
|
|
346
448
|
|
|
347
449
|
if (state.issues.length === 0) {
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
SonarInitConfig,
|
|
6
|
+
SonarProjectConfig,
|
|
7
|
+
SonarWorkspaceRegistry,
|
|
8
|
+
ResolvedTarget,
|
|
9
|
+
} from "./types.js";
|
|
5
10
|
|
|
6
11
|
// ── Generic helpers ─────────────────────────────────────────────────────────
|
|
7
12
|
|
|
@@ -40,7 +45,10 @@ export function normalizeServerUrl(url: string | undefined): string {
|
|
|
40
45
|
return url.trim().replace(/\/+$/, "");
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
export function resolveProjectKey(
|
|
48
|
+
export function resolveProjectKey(
|
|
49
|
+
baseDir: string,
|
|
50
|
+
props: Record<string, string>,
|
|
51
|
+
): string {
|
|
44
52
|
const fromProps = props["sonar.projectKey"]?.trim();
|
|
45
53
|
if (fromProps) return fromProps;
|
|
46
54
|
return slugify(basename(baseDir));
|
|
@@ -48,7 +56,9 @@ export function resolveProjectKey(baseDir: string, props: Record<string, string>
|
|
|
48
56
|
|
|
49
57
|
// ── File helpers ────────────────────────────────────────────────────────────
|
|
50
58
|
|
|
51
|
-
export async function readOptionalText(
|
|
59
|
+
export async function readOptionalText(
|
|
60
|
+
path: string,
|
|
61
|
+
): Promise<string | undefined> {
|
|
52
62
|
try {
|
|
53
63
|
return await readFile(path, "utf8");
|
|
54
64
|
} catch {
|
|
@@ -56,7 +66,9 @@ export async function readOptionalText(path: string): Promise<string | undefined
|
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
export async function readOptionalJson<T>(
|
|
69
|
+
export async function readOptionalJson<T>(
|
|
70
|
+
path: string,
|
|
71
|
+
): Promise<T | undefined> {
|
|
60
72
|
try {
|
|
61
73
|
const text = await readFile(path, "utf8");
|
|
62
74
|
return JSON.parse(text) as T;
|
|
@@ -87,14 +99,24 @@ export const DEFAULT_SONAR_EXCLUSIONS = [
|
|
|
87
99
|
"coverage/**",
|
|
88
100
|
".scannerwork/**",
|
|
89
101
|
"**/.env*",
|
|
102
|
+
".next/**",
|
|
103
|
+
".pi/**",
|
|
104
|
+
"build/**",
|
|
105
|
+
"out/**",
|
|
106
|
+
".turbo/**",
|
|
107
|
+
".vercel/**",
|
|
90
108
|
];
|
|
91
109
|
|
|
92
|
-
const DEFAULT_SONAR_PROJECT_PROPERTIES =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
const DEFAULT_SONAR_PROJECT_PROPERTIES =
|
|
111
|
+
[
|
|
112
|
+
"sonar.sources=.",
|
|
113
|
+
`sonar.exclusions=${DEFAULT_SONAR_EXCLUSIONS.join(",")}`,
|
|
114
|
+
].join("\n") + "\n";
|
|
96
115
|
|
|
97
|
-
export function mergeCommaSeparatedValues(
|
|
116
|
+
export function mergeCommaSeparatedValues(
|
|
117
|
+
existingValue: string | undefined,
|
|
118
|
+
additions: string[],
|
|
119
|
+
): string {
|
|
98
120
|
const merged = new Set(
|
|
99
121
|
(existingValue ?? "")
|
|
100
122
|
.split(",")
|
|
@@ -110,7 +132,9 @@ export function mergeCommaSeparatedValues(existingValue: string | undefined, add
|
|
|
110
132
|
|
|
111
133
|
const EXCLUSION_RE = /^(\s*)(sonar\.(?:sources|exclusions))\s*=\s*(.*)$/;
|
|
112
134
|
|
|
113
|
-
export async function ensureDefaultSonarProjectProperties(
|
|
135
|
+
export async function ensureDefaultSonarProjectProperties(
|
|
136
|
+
baseDir: string,
|
|
137
|
+
): Promise<"created" | "updated" | "unchanged"> {
|
|
114
138
|
const path = sonarProjectPropertiesPath(baseDir);
|
|
115
139
|
const existing = await readOptionalText(path);
|
|
116
140
|
if (!existing?.trim()) {
|
|
@@ -165,19 +189,31 @@ export async function ensureDefaultSonarProjectProperties(baseDir: string): Prom
|
|
|
165
189
|
|
|
166
190
|
const normalized = updatedLines.join("\n").replace(/\n*$/, "\n");
|
|
167
191
|
await writeFile(path, normalized, "utf8");
|
|
168
|
-
return existing.includes("sonar.sources") ||
|
|
192
|
+
return existing.includes("sonar.sources") ||
|
|
193
|
+
existing.includes("sonar.exclusions")
|
|
194
|
+
? "updated"
|
|
195
|
+
: "created";
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
// ── Config load/save ────────────────────────────────────────────────────────
|
|
172
199
|
|
|
173
|
-
export async function loadProjectConfig(
|
|
200
|
+
export async function loadProjectConfig(
|
|
201
|
+
baseDir: string,
|
|
202
|
+
): Promise<SonarInitConfig | undefined> {
|
|
174
203
|
return readOptionalJson<SonarInitConfig>(projectConfigPath(baseDir));
|
|
175
204
|
}
|
|
176
205
|
|
|
177
|
-
export async function saveProjectConfig(
|
|
206
|
+
export async function saveProjectConfig(
|
|
207
|
+
baseDir: string,
|
|
208
|
+
config: SonarInitConfig,
|
|
209
|
+
): Promise<void> {
|
|
178
210
|
const dir = sonarqubeConfigDir(baseDir);
|
|
179
211
|
await mkdir(dir, { recursive: true });
|
|
180
|
-
await writeFile(
|
|
212
|
+
await writeFile(
|
|
213
|
+
projectConfigPath(baseDir),
|
|
214
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
215
|
+
"utf8",
|
|
216
|
+
);
|
|
181
217
|
}
|
|
182
218
|
|
|
183
219
|
// ── Workspace registry ──────────────────────────────────────────────────────
|
|
@@ -211,16 +247,29 @@ export async function loadWorkspaceRegistry(
|
|
|
211
247
|
startDir: string,
|
|
212
248
|
): Promise<{ repoRoot: string; registry: SonarWorkspaceRegistry }> {
|
|
213
249
|
const repoRoot = await findRepoRoot(startDir);
|
|
214
|
-
const raw = await readOptionalJson<Partial<SonarWorkspaceRegistry>>(
|
|
215
|
-
|
|
250
|
+
const raw = await readOptionalJson<Partial<SonarWorkspaceRegistry>>(
|
|
251
|
+
workspaceRegistryPath(repoRoot),
|
|
252
|
+
);
|
|
253
|
+
const workspaces =
|
|
254
|
+
raw?.workspaces && typeof raw.workspaces === "object"
|
|
255
|
+
? { ...raw.workspaces }
|
|
256
|
+
: {};
|
|
216
257
|
return { repoRoot, registry: { version: 1, workspaces } };
|
|
217
258
|
}
|
|
218
259
|
|
|
219
|
-
export async function saveWorkspaceRegistry(
|
|
260
|
+
export async function saveWorkspaceRegistry(
|
|
261
|
+
startDir: string,
|
|
262
|
+
alias: string,
|
|
263
|
+
targetDir: string,
|
|
264
|
+
): Promise<void> {
|
|
220
265
|
const { repoRoot, registry } = await loadWorkspaceRegistry(startDir);
|
|
221
266
|
registry.workspaces[alias] = relative(repoRoot, targetDir) || ".";
|
|
222
267
|
await mkdir(sonarqubeConfigDir(repoRoot), { recursive: true });
|
|
223
|
-
await writeFile(
|
|
268
|
+
await writeFile(
|
|
269
|
+
workspaceRegistryPath(repoRoot),
|
|
270
|
+
JSON.stringify(registry, null, 2) + "\n",
|
|
271
|
+
"utf8",
|
|
272
|
+
);
|
|
224
273
|
}
|
|
225
274
|
|
|
226
275
|
export function looksLikePath(token: string): boolean {
|
|
@@ -237,10 +286,15 @@ export function looksLikePath(token: string): boolean {
|
|
|
237
286
|
}
|
|
238
287
|
|
|
239
288
|
export function knownTargets(registry: SonarWorkspaceRegistry): string[] {
|
|
240
|
-
return Object.keys(registry.workspaces).sort((left, right) =>
|
|
289
|
+
return Object.keys(registry.workspaces).sort((left, right) =>
|
|
290
|
+
left.localeCompare(right),
|
|
291
|
+
);
|
|
241
292
|
}
|
|
242
293
|
|
|
243
|
-
export async function resolveTarget(
|
|
294
|
+
export async function resolveTarget(
|
|
295
|
+
ctx: { cwd: string },
|
|
296
|
+
targetInput?: string,
|
|
297
|
+
): Promise<ResolvedTarget> {
|
|
244
298
|
const { repoRoot, registry } = await loadWorkspaceRegistry(ctx.cwd);
|
|
245
299
|
if (!targetInput) {
|
|
246
300
|
return { baseDir: ctx.cwd, repoRoot };
|
|
@@ -251,7 +305,9 @@ export async function resolveTarget(ctx: { cwd: string }, targetInput?: string):
|
|
|
251
305
|
const baseDir = resolve(repoRoot, aliasTarget);
|
|
252
306
|
const baseDirStat = await stat(baseDir).catch(() => undefined);
|
|
253
307
|
if (!baseDirStat?.isDirectory()) {
|
|
254
|
-
throw new Error(
|
|
308
|
+
throw new Error(
|
|
309
|
+
`SonarQube target "${targetInput}" points to a missing directory: ${baseDir}`,
|
|
310
|
+
);
|
|
255
311
|
}
|
|
256
312
|
return { baseDir, repoRoot, alias: targetInput };
|
|
257
313
|
}
|
|
@@ -271,7 +327,9 @@ export async function resolveTarget(ctx: { cwd: string }, targetInput?: string):
|
|
|
271
327
|
`Unknown SonarQube target "${targetInput}". Known targets: ${known.join(", ")}. Use /sonarqube init <alias> <path> to add one.`,
|
|
272
328
|
);
|
|
273
329
|
}
|
|
274
|
-
throw new Error(
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Unknown SonarQube target "${targetInput}". Use /sonarqube init <alias> <path> to add one.`,
|
|
332
|
+
);
|
|
275
333
|
}
|
|
276
334
|
|
|
277
335
|
export async function resolveInitTarget(
|
|
@@ -302,7 +360,10 @@ interface ResolveCtx {
|
|
|
302
360
|
signal?: AbortSignal;
|
|
303
361
|
}
|
|
304
362
|
|
|
305
|
-
export async function resolveConfig(
|
|
363
|
+
export async function resolveConfig(
|
|
364
|
+
ctx: ResolveCtx,
|
|
365
|
+
inputPath?: string,
|
|
366
|
+
): Promise<SonarProjectConfig> {
|
|
306
367
|
const { baseDir } = await resolveTarget(ctx, inputPath);
|
|
307
368
|
const propertiesPath = resolve(baseDir, "sonar-project.properties");
|
|
308
369
|
const propertiesText = await readOptionalText(propertiesPath);
|