@jonathan0823/pi-sonarqube 0.3.2 → 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/src/commands.ts CHANGED
@@ -1,6 +1,23 @@
1
- import type { AutocompleteItem, AutocompleteSuggestions } from "@earendil-works/pi-tui";
2
- import type { SonarIssue, SonarIssueFetchOptions, SonarDuplicationMeasures, IssueSeverityCounts, IssueQualityCounts, FileDuplication, DuplicationBlockGroup } from "./types.js";
3
- import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES, SONAR_SOFTWARE_QUALITIES, SONAR_IMPACT_SEVERITIES } from "./types.js";
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
- { value: "analyze", label: "analyze", description: "run analysis and fetch issues" },
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
- { value: "metrics", label: "metrics", description: "show project metrics (duplication, issue counts)" },
17
- { value: "duplications", label: "duplications", description: "browse duplicated files and blocks" },
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) return items.map((item) => ({ value: item.value, label: item.label, description: item.description }));
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) || item.label.toLowerCase().startsWith(query),
65
+ item.value.toLowerCase().startsWith(query) ||
66
+ item.label.toLowerCase().startsWith(query),
32
67
  )
33
- .map((item) => ({ value: item.value, label: item.label, description: item.description }));
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
- argumentText: string,
50
- ): { command: string; current: string; tokens: string[] } {
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(mode?: "STANDARD" | "MQR"): AutocompleteItem[] {
113
+ function createFilterCompletionList(
114
+ mode?: "STANDARD" | "MQR",
115
+ ): AutocompleteItem[] {
73
116
  const buildItems = (
74
- groups: ReadonlyArray<readonly [string, readonly string[], string, boolean]>,
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
- { value: `${prefix}:${value}`, label: `${prefix}:${value}`, description },
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" ? [...buildItems(mqrGroups), ...buildItems(legacyGroups)] : [...buildItems(legacyGroups), ...buildItems(mqrGroups)];
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) => item.value.startsWith(lowerCommand));
104
- const isFullMatch = SONAR_COMMANDS.some((item) => item.value === lowerCommand);
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) => ({ value: item.value, label: item.value, description: item.description }));
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 { value: String(index + 1), label: String(index + 1), description: `${issue.severity} ${issue.filePath}${lineSuffix}` };
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 (lowerCommand === "init" || lowerCommand === "metrics" || lowerCommand === "duplications") {
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 ? filterAutocompleteItems(SONAR_COMMANDS, current) : null;
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
- | { action: "analyze"; targetInput?: string; filters?: SonarIssueFetchOptions }
208
+ | {
209
+ action: "analyze";
210
+ targetInput?: string;
211
+ filters?: SonarIssueFetchOptions;
212
+ }
140
213
  | { action: "issues"; targetInput?: string; filters?: SonarIssueFetchOptions }
141
- | { action: "open"; targetInput?: string; issueIndex?: number; filters?: SonarIssueFetchOptions }
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 { action: "duplications", ...parseSonarIssueArgs(tokens.slice(1), true) };
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 = typeof index === "number" ? `${String(index).padStart(2, "0")}. ` : "";
225
- const rule = issue.ruleName ? `${issue.rule} (${issue.ruleName})` : issue.rule;
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 = (metrics.measures.linesToCover ?? 0) - (metrics.measures.uncoveredLines ?? 0);
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(`Coverage: ${pct}% covered=${covered} uncovered=${uncovered} lines=${total}`);
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 = metrics.cleanCodeMode === "MQR"
259
- ? [
260
- `BLOCKER ${metrics.issueCounts.blocker}`,
261
- `HIGH ${metrics.issueCounts.critical}`,
262
- `MEDIUM ${metrics.issueCounts.major}`,
263
- `LOW ${metrics.issueCounts.minor}`,
264
- `INFO ${metrics.issueCounts.info}`,
265
- ]
266
- : [
267
- `BLOCKER ${metrics.issueCounts.blocker}`,
268
- `CRITICAL ${metrics.issueCounts.critical}`,
269
- `MAJOR ${metrics.issueCounts.major}`,
270
- `MINOR ${metrics.issueCounts.minor}`,
271
- `INFO ${metrics.issueCounts.info}`,
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(` ${String(i + 1).padStart(2, " ")}. ${f.filePath} blocks=${f.duplicatedBlocks} lines=${f.duplicatedLines}`);
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(filePath: string, groups: DuplicationBlockGroup[]): string {
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: { totalIssues: number; projectKey: string; filters?: SonarIssueFetchOptions }): string {
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 ? ` (${issueFilterLabel(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("", state.measures.duplicatedBlocks > 0
343
- ? `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
344
- : `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% (no duplications detected)`);
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 { SonarInitConfig, SonarProjectConfig, SonarWorkspaceRegistry, ResolvedTarget } from "./types.js";
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(baseDir: string, props: Record<string, string>): string {
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(path: string): Promise<string | undefined> {
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>(path: string): Promise<T | undefined> {
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
- "sonar.sources=.",
94
- `sonar.exclusions=${DEFAULT_SONAR_EXCLUSIONS.join(",")}`,
95
- ].join("\n") + "\n";
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(existingValue: string | undefined, additions: string[]): string {
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(baseDir: string): Promise<"created" | "updated" | "unchanged"> {
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") || existing.includes("sonar.exclusions") ? "updated" : "created";
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(baseDir: string): Promise<SonarInitConfig | undefined> {
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(baseDir: string, config: SonarInitConfig): Promise<void> {
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(projectConfigPath(baseDir), JSON.stringify(config, null, 2) + "\n", "utf8");
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>>(workspaceRegistryPath(repoRoot));
215
- const workspaces = raw?.workspaces && typeof raw.workspaces === "object" ? { ...raw.workspaces } : {};
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(startDir: string, alias: string, targetDir: string): Promise<void> {
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(workspaceRegistryPath(repoRoot), JSON.stringify(registry, null, 2) + "\n", "utf8");
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) => left.localeCompare(right));
289
+ return Object.keys(registry.workspaces).sort((left, right) =>
290
+ left.localeCompare(right),
291
+ );
241
292
  }
242
293
 
243
- export async function resolveTarget(ctx: { cwd: string }, targetInput?: string): Promise<ResolvedTarget> {
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(`SonarQube target "${targetInput}" points to a missing directory: ${baseDir}`);
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(`Unknown SonarQube target "${targetInput}". Use /sonarqube init <alias> <path> to add one.`);
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(ctx: ResolveCtx, inputPath?: string): Promise<SonarProjectConfig> {
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);