@jonathan0823/pi-sonarqube 0.5.0 → 0.6.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/api.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readFile } from "node:fs/promises";
3
- import { resolve } from "node:path";
3
+ import { relative, resolve } from "node:path";
4
4
  import type {
5
5
  SonarIssue,
6
6
  SonarAnalysisState,
@@ -114,13 +114,17 @@ export function normalizeIssueFilters(
114
114
  rules: normalizeIssueList(filters?.rules),
115
115
  softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
116
116
  impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
117
+ pathScope: filters?.pathScope,
118
+ componentKeys: filters?.componentKeys,
117
119
  };
118
120
  return normalized.severities ||
119
121
  normalized.statuses ||
120
122
  normalized.types ||
121
123
  normalized.rules ||
122
124
  normalized.softwareQualities ||
123
- normalized.impactSeverities
125
+ normalized.impactSeverities ||
126
+ normalized.pathScope ||
127
+ normalized.componentKeys?.length
124
128
  ? normalized
125
129
  : undefined;
126
130
  }
@@ -178,6 +182,9 @@ export function parseIssueFilterToken(
178
182
  case "impactseverity":
179
183
  case "impactseverities":
180
184
  return { impactSeverities: values.map((v) => v.toUpperCase()) };
185
+ case "in":
186
+ case "path":
187
+ return { pathScope: values[0] };
181
188
  default:
182
189
  return undefined;
183
190
  }
@@ -238,30 +245,26 @@ export function parseSonarIssueArgs(
238
245
  return { targetInput, issueIndex, filters: mergeFilters(...filters) };
239
246
  }
240
247
 
248
+ function mergeFilterField<T>(
249
+ merged: T[] | undefined,
250
+ incoming?: T[],
251
+ ): T[] | undefined {
252
+ return incoming?.length ? [...(merged ?? []), ...incoming] : merged;
253
+ }
254
+
241
255
  function mergeFilters(
242
256
  ...filters: Array<Partial<SonarIssueFetchOptions> | undefined>
243
257
  ): SonarIssueFetchOptions | undefined {
244
258
  const merged: SonarIssueFetchOptions = {};
245
259
  for (const filter of filters) {
246
260
  if (!filter) continue;
247
- if (filter.severities?.length)
248
- merged.severities = [...(merged.severities ?? []), ...filter.severities];
249
- if (filter.statuses?.length)
250
- merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
251
- if (filter.types?.length)
252
- merged.types = [...(merged.types ?? []), ...filter.types];
253
- if (filter.rules?.length)
254
- merged.rules = [...(merged.rules ?? []), ...filter.rules];
255
- if (filter.softwareQualities?.length)
256
- merged.softwareQualities = [
257
- ...(merged.softwareQualities ?? []),
258
- ...filter.softwareQualities,
259
- ];
260
- if (filter.impactSeverities?.length)
261
- merged.impactSeverities = [
262
- ...(merged.impactSeverities ?? []),
263
- ...filter.impactSeverities,
264
- ];
261
+ merged.severities = mergeFilterField(merged.severities, filter.severities);
262
+ merged.statuses = mergeFilterField(merged.statuses, filter.statuses);
263
+ merged.types = mergeFilterField(merged.types, filter.types);
264
+ merged.rules = mergeFilterField(merged.rules, filter.rules);
265
+ merged.softwareQualities = mergeFilterField(merged.softwareQualities, filter.softwareQualities);
266
+ merged.impactSeverities = mergeFilterField(merged.impactSeverities, filter.impactSeverities);
267
+ if (filter.pathScope) merged.pathScope = filter.pathScope;
265
268
  }
266
269
  return normalizeIssueFilters(merged);
267
270
  }
@@ -279,9 +282,23 @@ export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
279
282
  parts.push(`qualities=${filters.softwareQualities.join(",")}`);
280
283
  if (filters.impactSeverities?.length)
281
284
  parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
285
+ if (filters.pathScope) parts.push(`in:${filters.pathScope}`);
282
286
  return parts.join(" • ");
283
287
  }
284
288
 
289
+ export function resolvePathScope(
290
+ baseDir: string,
291
+ projectKey: string,
292
+ pathScope: string,
293
+ ): string {
294
+ const raw = pathScope.replace(/^@/, "");
295
+ const absPath = resolve(baseDir, raw);
296
+ const relPath = relative(baseDir, absPath);
297
+ if (relPath === "" || relPath === ".") return projectKey;
298
+ const cleanRel = relPath.replace(/[\\/]+$/, "");
299
+ return `${projectKey}:${cleanRel}`;
300
+ }
301
+
285
302
  function severitySortRank(severity: string): number {
286
303
  switch (severity.toUpperCase()) {
287
304
  case "BLOCKER":
@@ -505,6 +522,11 @@ function buildIssueSearchUrl(
505
522
  "impactSeverities",
506
523
  filters.impactSeverities.join(","),
507
524
  );
525
+ if (filters?.componentKeys?.length)
526
+ url.searchParams.set(
527
+ "componentKeys",
528
+ filters.componentKeys.join(","),
529
+ );
508
530
  return url.toString();
509
531
  }
510
532
 
@@ -574,9 +596,19 @@ export async function fetchIssues(
574
596
  projectKey: string,
575
597
  signal?: AbortSignal,
576
598
  filters?: SonarIssueFetchOptions,
599
+ baseDir?: string,
577
600
  ): Promise<SonarIssue[]> {
578
601
  assertFiltersNotAmbiguous(filters);
579
602
  const normalizedFilters = normalizeIssueFilters(filters);
603
+
604
+ if (normalizedFilters?.pathScope && baseDir) {
605
+ const componentKey = resolvePathScope(
606
+ baseDir,
607
+ projectKey,
608
+ normalizedFilters.pathScope,
609
+ );
610
+ normalizedFilters.componentKeys = [componentKey];
611
+ }
580
612
  const issues: SonarIssue[] = [];
581
613
  const ruleNames = new Map<string, string | undefined>();
582
614
  let page = 1;
@@ -614,6 +646,62 @@ export async function fetchIssues(
614
646
  return issues;
615
647
  }
616
648
 
649
+ // ── Rule autocomplete helpers ───────────────────────────────────────────
650
+
651
+ export async function fetchProjectProfiles(
652
+ serverUrl: string,
653
+ token: string | undefined,
654
+ projectKey: string,
655
+ signal?: AbortSignal,
656
+ ): Promise<Array<{key: string; language: string; name: string}>> {
657
+ try {
658
+ const result = await fetchJson<{
659
+ profiles: Array<{key?: string; language?: string; name?: string}>;
660
+ }>(
661
+ `${serverUrl}/api/qualityprofiles/search?project=${encodeURIComponent(projectKey)}`,
662
+ token,
663
+ signal,
664
+ );
665
+ return (result.profiles ?? []).map(p => ({
666
+ key: p.key ?? '',
667
+ language: p.language ?? '',
668
+ name: p.name ?? '',
669
+ }));
670
+ } catch {
671
+ return [];
672
+ }
673
+ }
674
+
675
+ export async function fetchRuleSearch(
676
+ serverUrl: string,
677
+ token: string | undefined,
678
+ query: string,
679
+ languages?: string[],
680
+ signal?: AbortSignal,
681
+ ): Promise<Array<{key: string; name?: string}>> {
682
+ try {
683
+ const params = new URLSearchParams();
684
+ if (query) params.set("q", query);
685
+ params.set("ps", "20");
686
+ params.set("p", "1");
687
+ if (languages?.length) params.set("languages", languages.join(","));
688
+
689
+ const result = await fetchJson<{
690
+ rules: Array<{key?: string; name?: string}>;
691
+ }>(
692
+ `${serverUrl}/api/rules/search?${params.toString()}`,
693
+ token,
694
+ signal,
695
+ );
696
+ return (result.rules ?? []).map(r => ({
697
+ key: r.key ?? '',
698
+ name: r.name,
699
+ }));
700
+ } catch {
701
+ return [];
702
+ }
703
+ }
704
+
617
705
  export async function fetchRuleName(
618
706
  serverUrl: string,
619
707
  token: string | undefined,
@@ -785,6 +873,8 @@ export async function fetchFileDuplications(
785
873
  token: string | undefined,
786
874
  projectKey: string,
787
875
  signal?: AbortSignal,
876
+ pathScope?: string,
877
+ baseDir?: string,
788
878
  ): Promise<FileDuplication[]> {
789
879
  const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_lines_density,duplicated_blocks&ps=500`;
790
880
  const treeResult = await fetchJson<{
@@ -796,7 +886,7 @@ export async function fetchFileDuplications(
796
886
  }>;
797
887
  }>(treeUrl, token, signal);
798
888
 
799
- const results = (treeResult.components ?? [])
889
+ let results = (treeResult.components ?? [])
800
890
  .map((component) => {
801
891
  const measures = component.measures ?? [];
802
892
  const getValue = (metric: string): number => {
@@ -817,15 +907,22 @@ export async function fetchFileDuplications(
817
907
  duplicatedBlocks,
818
908
  };
819
909
  })
820
- .filter((f): f is FileDuplication => f !== null)
821
- .sort((left, right) => {
822
- const densityDiff =
823
- right.duplicatedLinesDensity - left.duplicatedLinesDensity;
824
- if (densityDiff !== 0) return densityDiff;
825
- const linesDiff = right.duplicatedLines - left.duplicatedLines;
826
- if (linesDiff !== 0) return linesDiff;
827
- return left.filePath.localeCompare(right.filePath);
828
- });
910
+ .filter((f): f is FileDuplication => f !== null);
911
+
912
+ if (pathScope && baseDir) {
913
+ const absPath = resolve(baseDir, pathScope);
914
+ const relPrefix = relative(baseDir, absPath);
915
+ results = results.filter((f) => f.filePath.startsWith(relPrefix));
916
+ }
917
+
918
+ results.sort((left, right) => {
919
+ const densityDiff =
920
+ right.duplicatedLinesDensity - left.duplicatedLinesDensity;
921
+ if (densityDiff !== 0) return densityDiff;
922
+ const linesDiff = right.duplicatedLines - left.duplicatedLines;
923
+ if (linesDiff !== 0) return linesDiff;
924
+ return left.filePath.localeCompare(right.filePath);
925
+ });
829
926
 
830
927
  return results;
831
928
  }
package/src/commands.ts CHANGED
@@ -18,8 +18,16 @@ import {
18
18
  SONAR_SOFTWARE_QUALITIES,
19
19
  SONAR_IMPACT_SEVERITIES,
20
20
  } from "./types.js";
21
+ import { execFileSync } from "node:child_process";
22
+ import { readdirSync } from "node:fs";
23
+ import { basename, dirname, relative, resolve } from "node:path";
21
24
  import { looksLikePath } from "./config.js";
22
- import { parseSonarIssueArgs, issueFilterLabel } from "./api.js";
25
+ import {
26
+ parseSonarIssueArgs,
27
+ issueFilterLabel,
28
+ fetchProjectProfiles,
29
+ fetchRuleSearch,
30
+ } from "./api.js";
23
31
 
24
32
  // ── Constants ───────────────────────────────────────────────────────────────
25
33
 
@@ -138,17 +146,291 @@ function createFilterCompletionList(
138
146
  ["quality", SONAR_SOFTWARE_QUALITIES, "software quality (MQR)", true],
139
147
  ["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
140
148
  ] as const;
149
+ const ruleItems = [
150
+ { value: "rule:", label: "rule:", description: "rule key" },
151
+ { value: "rules:", label: "rules:", description: "rule key" },
152
+ ];
153
+ const scopeItems = [
154
+ {
155
+ value: "in:",
156
+ label: "in:",
157
+ description: "path or dir scope (e.g. in:src/api.ts or in:src/)",
158
+ },
159
+ ];
141
160
 
142
161
  return mode === "MQR"
143
- ? [...buildItems(mqrGroups), ...buildItems(legacyGroups)]
144
- : [...buildItems(legacyGroups), ...buildItems(mqrGroups)];
162
+ ? [
163
+ ...scopeItems,
164
+ ...ruleItems,
165
+ ...buildItems(mqrGroups),
166
+ ...buildItems(legacyGroups),
167
+ ]
168
+ : [
169
+ ...scopeItems,
170
+ ...ruleItems,
171
+ ...buildItems(legacyGroups),
172
+ ...buildItems(mqrGroups),
173
+ ];
174
+ }
175
+
176
+ function listDirCompletions(
177
+ dirPath: string,
178
+ baseDir: string,
179
+ ): AutocompleteItem[] | null {
180
+ try {
181
+ const entries = readdirSync(dirPath, { withFileTypes: true });
182
+ const items: AutocompleteItem[] = [];
183
+ const relativePrefix =
184
+ dirPath === baseDir ? "" : relative(baseDir, dirPath) + "/";
185
+
186
+ for (const entry of entries) {
187
+ if (entry.name.startsWith(".")) continue;
188
+ const isDir = entry.isDirectory();
189
+ const name = isDir ? entry.name + "/" : entry.name;
190
+ const fullPath = relativePrefix + name;
191
+ items.push({
192
+ value: `in:${fullPath}`,
193
+ label: name,
194
+ description: relativePrefix.replace(/\/$/, ""),
195
+ });
196
+ }
197
+
198
+ items.sort((a, b) => {
199
+ const aDir = a.description === "" ? 0 : 1;
200
+ const bDir = b.description === "" ? 0 : 1;
201
+ if (aDir !== bDir) return aDir - bDir;
202
+ // Within same depth, dirs first, then by label
203
+ const aD = a.label.endsWith("/") ? 0 : 1;
204
+ const bD = b.label.endsWith("/") ? 0 : 1;
205
+ return aD - bD || a.label.localeCompare(b.label);
206
+ });
207
+
208
+ return items.length > 0 ? items.slice(0, 50) : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function fuzzyWithCmd(
215
+ cmd: string,
216
+ args: string[],
217
+ prefix: string,
218
+ baseDir: string,
219
+ ): AutocompleteItem[] | null {
220
+ const out = execFileSync(cmd, args, {
221
+ encoding: "utf8",
222
+ timeout: 3000,
223
+ maxBuffer: 5 * 1024 * 1024,
224
+ windowsHide: true,
225
+ });
226
+ const lines = out.trim().split("\n").filter(Boolean);
227
+ if (lines.length === 0) return null;
228
+
229
+ const lowerPrefix = prefix.toLowerCase();
230
+ const matched: AutocompleteItem[] = [];
231
+ const seen = new Set<string>();
232
+
233
+ for (const line of lines) {
234
+ if (matched.length >= 50) break;
235
+ // Normalize: fd outputs dirs with trailing /, rg doesn't
236
+ const isDir = line.endsWith("/");
237
+ const entry = isDir ? line.slice(0, -1) : line;
238
+ if (!entry.toLowerCase().includes(lowerPrefix) || seen.has(entry)) continue;
239
+ seen.add(entry);
240
+
241
+ const dir = dirname(entry);
242
+ const name = basename(entry) + (isDir ? "/" : "");
243
+ matched.push({
244
+ value: `in:${line}`,
245
+ label: name,
246
+ description: dir === "." ? "" : dir + "/",
247
+ });
248
+
249
+ // For rg (no dirs output), extract ancestor dirs that match
250
+ if (!isDir) {
251
+ addAncestorDirMatches(entry, lowerPrefix, matched, seen);
252
+ }
253
+ }
254
+
255
+ matched.sort((a, b) => {
256
+ const aDir = a.label.endsWith("/") ? 0 : 1;
257
+ const bDir = b.label.endsWith("/") ? 0 : 1;
258
+ if (aDir !== bDir) return aDir - bDir;
259
+ return a.label.localeCompare(b.label);
260
+ });
261
+
262
+ return matched;
263
+ }
264
+
265
+ /**
266
+ * For non-directory entries, extract ancestor directories that match the prefix
267
+ * and add them as autocomplete suggestions.
268
+ */
269
+ function addAncestorDirMatches(
270
+ entry: string,
271
+ lowerPrefix: string,
272
+ matched: AutocompleteItem[],
273
+ seen: Set<string>,
274
+ ): void {
275
+ const segs = entry.split("/");
276
+ for (let i = 0; i < segs.length - 1; i++) {
277
+ if (matched.length >= 50) break;
278
+ const dirPath = segs.slice(0, i + 1).join("/") + "/";
279
+ if (seen.has(dirPath)) continue;
280
+ seen.add(dirPath);
281
+ if (segs[i].toLowerCase().includes(lowerPrefix)) {
282
+ const parentDir = segs.slice(0, i).join("/");
283
+ matched.push({
284
+ value: `in:${dirPath}`,
285
+ label: segs[i] + "/",
286
+ description: parentDir || "",
287
+ });
288
+ }
289
+ }
145
290
  }
146
291
 
147
- export function sonarArgumentCompletions(
292
+ async function fetchRuleAutocompleteCompletions(
293
+ serverUrl: string,
294
+ token: string | undefined,
295
+ projectKey: string,
296
+ query: string,
297
+ ): Promise<AutocompleteItem[] | null> {
298
+ try {
299
+ const profiles = await fetchProjectProfiles(serverUrl, token, projectKey);
300
+ const languages = [
301
+ ...new Set(profiles.map((p) => p.language).filter(Boolean)),
302
+ ];
303
+ const rules = await fetchRuleSearch(serverUrl, token, query, languages);
304
+ if (rules.length === 0) return null;
305
+ return rules.map((r) => ({
306
+ value: `rule:${r.key}`,
307
+ label: r.key,
308
+ description: r.name ?? "",
309
+ }));
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+
315
+ function getInPathCompletions(
316
+ prefix: string,
317
+ baseDir: string,
318
+ ): AutocompleteItem[] | null {
319
+ // Directory browsing: show contents when prefix ends with /
320
+ if (prefix.endsWith("/")) {
321
+ return listDirCompletions(resolve(baseDir, prefix), baseDir);
322
+ }
323
+
324
+ // Empty prefix: show root directory contents
325
+ if (!prefix) {
326
+ return listDirCompletions(baseDir, baseDir);
327
+ }
328
+
329
+ // Try fd first (fast, respects .gitignore, matches Pi TUI behavior)
330
+ try {
331
+ return fuzzyWithCmd(
332
+ "fd",
333
+ [
334
+ "--base-directory",
335
+ baseDir,
336
+ "--type",
337
+ "f",
338
+ "--type",
339
+ "d",
340
+ "--hidden",
341
+ "--exclude",
342
+ ".git",
343
+ "--max-results",
344
+ "50",
345
+ "--full-path",
346
+ prefix,
347
+ ],
348
+ prefix,
349
+ baseDir,
350
+ );
351
+ } catch {
352
+ // fd not available
353
+ }
354
+
355
+ // Fallback: rg --files (slower but available on most systems)
356
+ try {
357
+ return fuzzyWithCmd(
358
+ "rg",
359
+ ["--files", "--color", "never", "--no-ignore-parent"],
360
+ prefix,
361
+ baseDir,
362
+ );
363
+ } catch {
364
+ // rg not available
365
+ }
366
+
367
+ // Last resort: list current directory
368
+ return listDirCompletions(baseDir, baseDir);
369
+ }
370
+
371
+ function getOpenCompletions(
372
+ issues: SonarIssue[] | undefined,
373
+ mode: "STANDARD" | "MQR" | undefined,
374
+ current: string,
375
+ ): AutocompleteItem[] | null {
376
+ const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
377
+ const lineSuffix = issue.line ? `:${issue.line}` : "";
378
+ return {
379
+ value: String(index + 1),
380
+ label: String(index + 1),
381
+ description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
382
+ };
383
+ });
384
+ const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
385
+ const filtered = filterAutocompleteItems(suggestions, current);
386
+ return filtered.length > 0 ? filtered : null;
387
+ }
388
+
389
+ async function getFilterCompletions(
390
+ current: string,
391
+ mode: "STANDARD" | "MQR" | undefined,
392
+ cwd: string | undefined,
393
+ serverUrl: string | undefined,
394
+ token: string | undefined,
395
+ projectKey: string | undefined,
396
+ ): Promise<AutocompleteItem[] | null> {
397
+ if (current.startsWith("in:") && cwd) {
398
+ return getInPathCompletions(current.slice(3), cwd);
399
+ }
400
+ if (
401
+ (current.startsWith("rule:") || current.startsWith("rules:")) &&
402
+ serverUrl &&
403
+ projectKey
404
+ ) {
405
+ const query = current.split(":").slice(1).join(":");
406
+ const ruleItems = await fetchRuleAutocompleteCompletions(
407
+ serverUrl,
408
+ token,
409
+ projectKey,
410
+ query,
411
+ );
412
+ const staticSuggestions = createFilterCompletionList(mode);
413
+ const filtered = filterAutocompleteItems(staticSuggestions, current);
414
+ const combined = [
415
+ ...(filtered.length > 0 ? filtered : []),
416
+ ...(ruleItems ?? []),
417
+ ];
418
+ return combined.length > 0 ? combined : null;
419
+ }
420
+ const suggestions = createFilterCompletionList(mode);
421
+ const filtered = filterAutocompleteItems(suggestions, current);
422
+ return filtered.length > 0 ? filtered : null;
423
+ }
424
+
425
+ export async function sonarArgumentCompletions(
148
426
  argumentText: string,
149
427
  issues?: SonarIssue[],
150
428
  mode?: "STANDARD" | "MQR",
151
- ): AutocompleteItem[] | null {
429
+ cwd?: string,
430
+ serverUrl?: string,
431
+ token?: string,
432
+ projectKey?: string,
433
+ ): Promise<AutocompleteItem[] | null> {
152
434
  const { command, current, tokens } = splitSonarArgumentContext(argumentText);
153
435
  const lowerCommand = command.toLowerCase();
154
436
  const commandMatches = SONAR_COMMANDS.filter((item) =>
@@ -168,23 +450,18 @@ export function sonarArgumentCompletions(
168
450
  }
169
451
 
170
452
  if (lowerCommand === "open") {
171
- const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
172
- const lineSuffix = issue.line ? `:${issue.line}` : "";
173
- return {
174
- value: String(index + 1),
175
- label: String(index + 1),
176
- description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
177
- };
178
- });
179
- const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
180
- const filtered = filterAutocompleteItems(suggestions, current);
181
- return filtered.length > 0 ? filtered : null;
453
+ return getOpenCompletions(issues, mode, current);
182
454
  }
183
455
 
184
456
  if (lowerCommand === "analyze" || lowerCommand === "issues") {
185
- const suggestions = createFilterCompletionList(mode);
186
- const filtered = filterAutocompleteItems(suggestions, current);
187
- return filtered.length > 0 ? filtered : null;
457
+ return getFilterCompletions(
458
+ current,
459
+ mode,
460
+ cwd,
461
+ serverUrl,
462
+ token,
463
+ projectKey,
464
+ );
188
465
  }
189
466
 
190
467
  if (
@@ -218,7 +495,12 @@ export type ParsedSonarCommand =
218
495
  filters?: SonarIssueFetchOptions;
219
496
  }
220
497
  | { action: "metrics"; targetInput?: string }
221
- | { action: "duplications"; targetInput?: string; issueIndex?: number };
498
+ | {
499
+ action: "duplications";
500
+ targetInput?: string;
501
+ issueIndex?: number;
502
+ filters?: SonarIssueFetchOptions;
503
+ };
222
504
 
223
505
  export function parseCommandArgs(args: string): ParsedSonarCommand {
224
506
  const tokens = args.trim().split(/\s+/).filter(Boolean);
@@ -273,6 +555,9 @@ export function helpText(): string {
273
555
  "Filters:",
274
556
  " /sonarqube issues be CRITICAL",
275
557
  " /sonarqube issues be severity:CRITICAL status:OPEN",
558
+ " /sonarqube issues be rule:S1192",
559
+ " /sonarqube issues in:src/api.ts",
560
+ " /sonarqube issues in:src/",
276
561
  " /sonarqube issues be quality:RELIABILITY",
277
562
  " /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
278
563
  "",
@@ -286,6 +571,7 @@ export function helpText(): string {
286
571
  "Defaults:",
287
572
  " config is stored in .pi/sonarqube.json",
288
573
  " use project paths directly (no alias needed)",
574
+ " issue and duplication browsers search file, rule, severity, status, message, and stats",
289
575
  ].join("\n");
290
576
  }
291
577