@jonathan0823/pi-sonarqube 0.6.0 → 0.8.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/README.md +8 -8
- package/dist/api.d.ts +3 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -6
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +3 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +34 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +122 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +25 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +77 -9
- package/dist/ui.js.map +1 -1
- package/package.json +4 -1
- package/src/api.ts +24 -24
- package/src/commands.ts +64 -9
- package/src/config.ts +151 -1
- package/src/index.ts +43 -6
- package/src/types.ts +3 -1
- package/src/ui.ts +114 -11
package/src/ui.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Input,
|
|
8
|
+
Key,
|
|
9
|
+
matchesKey,
|
|
10
|
+
truncateToWidth,
|
|
11
|
+
type Focusable,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
7
13
|
import { readFile } from "node:fs/promises";
|
|
8
14
|
import { resolve } from "node:path";
|
|
9
15
|
import type {
|
|
@@ -20,7 +26,7 @@ import {
|
|
|
20
26
|
normalizeIssueFilters,
|
|
21
27
|
fetchCleanCodeMode,
|
|
22
28
|
} from "./api.js";
|
|
23
|
-
import { resolveConfig, resolveTarget } from "./config.js";
|
|
29
|
+
import { resolveConfig, resolveTarget, gatherTargets } from "./config.js";
|
|
24
30
|
|
|
25
31
|
// ── Issue preview ───────────────────────────────────────────────────────────
|
|
26
32
|
|
|
@@ -162,11 +168,7 @@ export function startAnalysisUi(
|
|
|
162
168
|
// ── Searchable browser helpers ─────────────────────────────────────────────
|
|
163
169
|
|
|
164
170
|
function normalizeSearchQuery(query: string): string[] {
|
|
165
|
-
return query
|
|
166
|
-
.trim()
|
|
167
|
-
.toLowerCase()
|
|
168
|
-
.split(/\s+/)
|
|
169
|
-
.filter(Boolean);
|
|
171
|
+
return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
function matchesSearchQuery(haystack: string, query: string): boolean {
|
|
@@ -187,7 +189,11 @@ function filterSearchItems<T>(
|
|
|
187
189
|
|
|
188
190
|
interface SearchableBrowserConfig<T> {
|
|
189
191
|
title: string;
|
|
190
|
-
subtitle: (
|
|
192
|
+
subtitle: (
|
|
193
|
+
query: string,
|
|
194
|
+
totalCount: number,
|
|
195
|
+
filteredCount: number,
|
|
196
|
+
) => string;
|
|
191
197
|
searchHint: string;
|
|
192
198
|
emptyMessage: string;
|
|
193
199
|
footer: string;
|
|
@@ -295,7 +301,9 @@ export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
|
|
|
295
301
|
theme: Theme,
|
|
296
302
|
done: (result: number | null) => void,
|
|
297
303
|
) {
|
|
298
|
-
const filterSuffix = state.filters
|
|
304
|
+
const filterSuffix = state.filters
|
|
305
|
+
? ` • ${issueFilterLabel(state.filters)}`
|
|
306
|
+
: "";
|
|
299
307
|
super(state.issues, theme, done, {
|
|
300
308
|
title: "SonarQube Issues",
|
|
301
309
|
subtitle: (_query, totalCount, filteredCount) =>
|
|
@@ -317,7 +325,9 @@ export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
|
|
|
317
325
|
const marker = isSelected
|
|
318
326
|
? theme.fg("accent", ">")
|
|
319
327
|
: theme.fg("dim", " ");
|
|
320
|
-
const location = issue.line
|
|
328
|
+
const location = issue.line
|
|
329
|
+
? `${issue.filePath}:${issue.line}`
|
|
330
|
+
: issue.filePath;
|
|
321
331
|
const rule = issue.ruleName
|
|
322
332
|
? `${issue.rule} (${issue.ruleName})`
|
|
323
333
|
: issue.rule;
|
|
@@ -442,7 +452,8 @@ export class DuplicationBrowser extends SearchableListBrowser<FileDuplication> {
|
|
|
442
452
|
title: "SonarQube Duplications",
|
|
443
453
|
subtitle: (_query, totalCount, filteredCount) =>
|
|
444
454
|
`${totalCount} file(s) with duplicate code${scopeSuffix} • ${filteredCount} match(es)`,
|
|
445
|
-
searchHint:
|
|
455
|
+
searchHint:
|
|
456
|
+
"Search duplications by file path, duplicated lines, blocks, or density",
|
|
446
457
|
emptyMessage: "No matching duplicated files found.",
|
|
447
458
|
footer: "Up/Down to move, Enter for details, Esc to close",
|
|
448
459
|
pageSize: 15,
|
|
@@ -476,6 +487,98 @@ export async function showDuplicationBrowser(
|
|
|
476
487
|
);
|
|
477
488
|
}
|
|
478
489
|
|
|
490
|
+
// ── Workspace selector browser ────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
export class WorkspaceBrowser extends SearchableListBrowser<{
|
|
493
|
+
alias: string;
|
|
494
|
+
path: string;
|
|
495
|
+
}> {
|
|
496
|
+
constructor(
|
|
497
|
+
workspaces: { alias: string; path: string }[],
|
|
498
|
+
theme: Theme,
|
|
499
|
+
done: (result: number | null) => void,
|
|
500
|
+
) {
|
|
501
|
+
super(workspaces, theme, done, {
|
|
502
|
+
title: "SonarQube Workspaces",
|
|
503
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
504
|
+
`${totalCount} workspace(s) • ${filteredCount} match(es)`,
|
|
505
|
+
searchHint: "Search workspaces by alias or path",
|
|
506
|
+
emptyMessage: "No matching workspaces found.",
|
|
507
|
+
footer: "Up/Down to move, Enter to select, Esc to cancel",
|
|
508
|
+
pageSize: 15,
|
|
509
|
+
searchText: (ws) => `${ws.alias} ${ws.path}`,
|
|
510
|
+
renderItem: (ws, index, isSelected) => {
|
|
511
|
+
const marker = isSelected
|
|
512
|
+
? theme.fg("accent", ">")
|
|
513
|
+
: theme.fg("dim", " ");
|
|
514
|
+
return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", ws.alias.padEnd(20))} ${theme.fg("dim", ws.path)}`;
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function showWorkspaceBrowser(
|
|
521
|
+
ctx: ExtensionContext,
|
|
522
|
+
workspaces: { alias: string; path: string }[],
|
|
523
|
+
): Promise<string | null> {
|
|
524
|
+
if (ctx.mode !== "tui" || workspaces.length === 0) return null;
|
|
525
|
+
const choice = await ctx.ui.custom<number | null>(
|
|
526
|
+
(_tui, theme, _kb, done) => new WorkspaceBrowser(workspaces, theme, done),
|
|
527
|
+
);
|
|
528
|
+
if (choice == null) return null;
|
|
529
|
+
return workspaces[choice].path;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── Unified target picker (used by index.ts) ────────────────────────────────
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Helpers to make a relative path resolvable by `resolveTarget`.
|
|
536
|
+
* Paths that don't look like paths (no `/`, `.`, or leading `/`) get `./`
|
|
537
|
+
* prefixed so `looksLikePath` can detect them.
|
|
538
|
+
*/
|
|
539
|
+
function makeTargetResolvable(path: string): string {
|
|
540
|
+
if (path.startsWith("/") || path.startsWith(".") || path.includes("/"))
|
|
541
|
+
return path;
|
|
542
|
+
return "./" + path;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Resolve an optional target to a string usable by downstream commands.
|
|
547
|
+
*
|
|
548
|
+
* Returns:
|
|
549
|
+
* - the explicit target string (if given)
|
|
550
|
+
* - `undefined` when no targets exist → caller falls back to cwd
|
|
551
|
+
* - `null` when user cancels the picker → caller stops
|
|
552
|
+
* - the selected alias or path otherwise
|
|
553
|
+
*
|
|
554
|
+
* Headless ambiguity throws an error listing all candidates.
|
|
555
|
+
*/
|
|
556
|
+
export async function pickOrResolveTarget(
|
|
557
|
+
ctx: ExtensionContext | ExtensionCommandContext,
|
|
558
|
+
targetInput?: string,
|
|
559
|
+
): Promise<string | undefined | null> {
|
|
560
|
+
if (targetInput) return targetInput;
|
|
561
|
+
|
|
562
|
+
const { targets } = await gatherTargets(ctx.cwd);
|
|
563
|
+
|
|
564
|
+
if (targets.length === 0) return undefined;
|
|
565
|
+
|
|
566
|
+
if (targets.length === 1) {
|
|
567
|
+
return makeTargetResolvable(targets[0].path);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (ctx.mode !== "tui") {
|
|
571
|
+
const list = targets.map((t) => `"${t.alias}" (${t.path})`).join(", ");
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Multiple SonarQube targets found: ${list}. Pass a target explicitly, e.g. /sonarqube <command> ${targets[0].path}.`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const choice = await showWorkspaceBrowser(ctx, targets);
|
|
578
|
+
if (choice == null) return null;
|
|
579
|
+
return makeTargetResolvable(choice);
|
|
580
|
+
}
|
|
581
|
+
|
|
479
582
|
// ── Issue browser / preview helpers (used by index.ts) ──────────────────────
|
|
480
583
|
|
|
481
584
|
export async function showIssueBrowser(
|