opencode-sonarqube 2.0.3 → 2.1.1

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.
Files changed (3) hide show
  1. package/README.md +59 -7
  2. package/dist/index.js +244 -87
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -4,16 +4,17 @@ OpenCode Plugin for SonarQube integration - Enterprise-level code quality from t
4
4
 
5
5
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://sonarqube.example.com)
6
6
  [![Quality Gate](https://img.shields.io/badge/quality%20gate-passed-brightgreen)](https://sonarqube.example.com)
7
- [![Tests](https://img.shields.io/badge/tests-612%20passing-brightgreen)](https://sonarqube.example.com)
7
+ [![Tests](https://img.shields.io/badge/tests-722%20passing-brightgreen)](https://sonarqube.example.com)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
9
+ [![npm version](https://img.shields.io/npm/v/opencode-sonarqube)](https://www.npmjs.com/package/opencode-sonarqube)
9
10
 
10
11
  ## Features
11
12
 
12
13
  - **Automatic Analysis**: Triggers SonarQube analysis when the AI agent becomes idle
13
- - **15 Tool Actions**: Comprehensive SonarQube integration for AI agents
14
+ - **16 Tool Actions**: Comprehensive SonarQube integration for AI agents
14
15
  - **Clean as You Code**: Focus on new code issues with `newissues` action
15
16
  - **Custom Command**: Use `/sonarqube` command for quick analysis
16
- - **Security Hotspots**: Review and track security hotspots requiring manual review
17
+ - **Security Hotspots**: Review, resolve, and bulk-dismiss security hotspots directly via API
17
18
  - **Quality Gate Integration**: Shows pass/fail status with detailed metrics
18
19
  - **Git Integration**: Detects git operations and suggests quality checks
19
20
  - **System Prompt Injection**: AI always knows current quality status
@@ -100,6 +101,44 @@ Add these to your `~/.zshrc` or `~/.bashrc` to make them permanent.
100
101
  - Tell the AI: "Setup SonarQube for this project"
101
102
  - Or use: `sonarqube({ action: "setup" })`
102
103
 
104
+ ## Updating
105
+
106
+ The plugin is installed per project via `package.json`. To update to the latest version:
107
+
108
+ ```bash
109
+ # Update in the current project
110
+ bun add opencode-sonarqube@latest
111
+
112
+ # Or with npm
113
+ npm install opencode-sonarqube@latest
114
+ ```
115
+
116
+ Then restart OpenCode to load the new version.
117
+
118
+ **Tip — Always get the latest version automatically:**
119
+
120
+ By default, `bun add` pins a specific version (e.g., `"^2.1.0"`). To always pull the newest release on every OpenCode restart, change your `package.json` dependency to:
121
+
122
+ ```json
123
+ {
124
+ "dependencies": {
125
+ "opencode-sonarqube": "latest"
126
+ }
127
+ }
128
+ ```
129
+
130
+ This ensures you always run the most recent version without manual updates.
131
+
132
+ **Update all projects at once:**
133
+
134
+ ```bash
135
+ # Run this from any directory to update all projects that use the plugin
136
+ for dir in $(grep -rl '"opencode-sonarqube"' ~/Projekte/*/package.json 2>/dev/null | xargs -I{} dirname {}); do
137
+ echo "Updating: $dir"
138
+ (cd "$dir" && bun add opencode-sonarqube@latest)
139
+ done
140
+ ```
141
+
103
142
  ## Configuration
104
143
 
105
144
  ### Environment Variables (Required)
@@ -192,6 +231,7 @@ The plugin adds a `sonarqube` tool with these actions:
192
231
  | `newissues` | Get only issues in NEW code (Clean as You Code) |
193
232
  | `worstfiles` | Show files with most issues (prioritize refactoring) |
194
233
  | `hotspots` | Get security hotspots that need manual review |
234
+ | `reviewhotspot` | Review/resolve hotspots as SAFE, FIXED, or ACKNOWLEDGED |
195
235
  | `duplications` | Find code duplications across the project |
196
236
 
197
237
  ### Status & Validation
@@ -216,15 +256,18 @@ The plugin adds a `sonarqube` tool with these actions:
216
256
  ```typescript
217
257
  sonarqube({
218
258
  action: "analyze" | "issues" | "newissues" | "worstfiles" | "status" |
219
- "validate" | "hotspots" | "duplications" | "rule" | "history" |
220
- "profile" | "branches" | "metrics" | "setup",
259
+ "validate" | "hotspots" | "reviewhotspot" | "duplications" | "rule" |
260
+ "history" | "profile" | "branches" | "metrics" | "setup",
221
261
  scope: "all" | "new" | "changed", // What to analyze
222
262
  severity: "blocker" | "critical" | "major" | "minor" | "info" | "all",
223
263
  fix: true | false, // Include fix suggestions
224
264
  projectKey: "override-key", // Optional override
225
265
  force: true | false, // Force re-initialization
226
266
  ruleKey: "typescript:S1234", // For "rule" action
227
- branch: "feature-branch" // For multi-branch analysis
267
+ branch: "feature-branch", // For multi-branch analysis
268
+ hotspotKey: "uuid-of-hotspot", // For "reviewhotspot" action (omit for bulk review)
269
+ resolution: "SAFE" | "FIXED" | "ACKNOWLEDGED", // Hotspot review resolution
270
+ comment: "Reason for the decision" // Optional review comment
228
271
  })
229
272
  ```
230
273
 
@@ -316,6 +359,15 @@ sonarqube({ action: "rule", ruleKey: "typescript:S3776" })
316
359
 
317
360
  // Enterprise validation before release
318
361
  sonarqube({ action: "validate" })
362
+
363
+ // List security hotspots
364
+ sonarqube({ action: "hotspots" })
365
+
366
+ // Review a single hotspot as safe
367
+ sonarqube({ action: "reviewhotspot", hotspotKey: "abc-123", resolution: "SAFE", comment: "Form field name, not a password" })
368
+
369
+ // Bulk-review ALL pending hotspots as safe
370
+ sonarqube({ action: "reviewhotspot", resolution: "SAFE" })
319
371
  ```
320
372
 
321
373
  ## CLI Usage
@@ -477,7 +529,7 @@ This project maintains enterprise-level quality:
477
529
  | Metric | Value |
478
530
  |--------|-------|
479
531
  | Test Coverage | 96% |
480
- | Tests | 612 |
532
+ | Tests | 722 |
481
533
  | Bugs | 0 |
482
534
  | Vulnerabilities | 0 |
483
535
  | Code Smells | 0 |
package/dist/index.js CHANGED
@@ -17808,6 +17808,36 @@ class IssuesAPI extends BaseAPI {
17808
17808
  const hotspots = await this.getSecurityHotspots(projectKey);
17809
17809
  return hotspots.filter((h) => h.status === "TO_REVIEW");
17810
17810
  }
17811
+ async reviewHotspot(hotspotKey, status, resolution, comment) {
17812
+ this.logger.info(`Reviewing hotspot ${hotspotKey}: status=${status}, resolution=${resolution}`);
17813
+ const body = {
17814
+ hotspot: hotspotKey,
17815
+ status
17816
+ };
17817
+ if (resolution && status === "REVIEWED") {
17818
+ body.resolution = resolution;
17819
+ }
17820
+ if (comment) {
17821
+ body.comment = comment;
17822
+ }
17823
+ await this.client.post("/api/hotspots/change_status", body);
17824
+ this.logger.info(`Hotspot ${hotspotKey} reviewed successfully`);
17825
+ }
17826
+ async bulkReviewHotspots(hotspotKeys, resolution, comment) {
17827
+ let success2 = 0;
17828
+ let failed = 0;
17829
+ const errors4 = [];
17830
+ for (const key of hotspotKeys) {
17831
+ try {
17832
+ await this.reviewHotspot(key, "REVIEWED", resolution, comment);
17833
+ success2++;
17834
+ } catch (error45) {
17835
+ failed++;
17836
+ errors4.push(`${key}: ${error45 instanceof Error ? error45.message : String(error45)}`);
17837
+ }
17838
+ }
17839
+ return { success: success2, failed, errors: errors4 };
17840
+ }
17811
17841
  }
17812
17842
  // src/api/quality-gate/api.ts
17813
17843
  init_types2();
@@ -19349,6 +19379,20 @@ function groupIssuesBySeverity(issues) {
19349
19379
  }
19350
19380
  function formatIssuesForAgent(issues, qualityGateStatus) {
19351
19381
  if (issues.length === 0) {
19382
+ if (qualityGateStatus !== "OK") {
19383
+ return `## SonarQube Analysis Results
19384
+
19385
+ **Quality Gate: ${qualityGateStatus}**
19386
+
19387
+ No code issues found, but the Quality Gate is not passing.
19388
+ This is typically caused by **unreviewed security hotspots** or **code duplication thresholds**.
19389
+
19390
+ **To fix:**
19391
+ - \`sonarqube({ action: "hotspots" })\` — List security hotspots
19392
+ - \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` — Bulk-review all hotspots as safe
19393
+ - \`sonarqube({ action: "status" })\` — See which conditions are failing
19394
+ - \`sonarqube({ action: "duplications" })\` — Check code duplication`;
19395
+ }
19352
19396
  return `## SonarQube Analysis Results
19353
19397
 
19354
19398
  **Quality Gate: ${qualityGateStatus}**
@@ -20288,8 +20332,20 @@ async function handleStatus(ctx) {
20288
20332
  output += `
20289
20333
  ### Failed Conditions
20290
20334
  `;
20335
+ const hasHotspotCondition = failedConditions.some((c) => c.metricKey.includes("security_hotspots") || c.metricKey.includes("hotspot"));
20291
20336
  for (const condition of failedConditions) {
20292
20337
  output += `- **${condition.metricKey}**: ${condition.actualValue} (threshold: ${condition.errorThreshold})
20338
+ `;
20339
+ }
20340
+ if (hasHotspotCondition) {
20341
+ output += `
20342
+ ### How to Fix Security Hotspot Condition
20343
+ The Quality Gate is failing because unreviewed security hotspots exist.
20344
+ You can review them directly:
20345
+
20346
+ 1. **List hotspots:** \`sonarqube({ action: "hotspots" })\`
20347
+ 2. **Bulk-review all as safe:** \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\`
20348
+ 3. **Review individually:** \`sonarqube({ action: "reviewhotspot", hotspotKey: "<key>", resolution: "SAFE", comment: "reason" })\`
20293
20349
  `;
20294
20350
  }
20295
20351
  }
@@ -20338,38 +20394,103 @@ async function handleHotspots(ctx) {
20338
20394
  output += `### Hotspots Requiring Review
20339
20395
 
20340
20396
  `;
20341
- output += `| Risk | File | Line | Issue |
20397
+ output += `| # | Risk | Key | File | Line | Issue |
20342
20398
  `;
20343
- output += `|------|------|------|-------|
20399
+ output += `|---|------|-----|------|------|-------|
20344
20400
  `;
20345
- for (const hotspot of toReview.slice(0, 20)) {
20401
+ for (let i = 0;i < Math.min(toReview.length, 50); i++) {
20402
+ const hotspot = toReview[i];
20346
20403
  const risk = hotspot.vulnerabilityProbability;
20347
20404
  const file2 = hotspot.component.split(":").pop() ?? hotspot.component;
20348
20405
  const line = hotspot.line ?? "?";
20349
- const msg = hotspot.message.length > 60 ? hotspot.message.substring(0, 57) + "..." : hotspot.message;
20350
- output += `| ${risk} | ${file2} | ${line} | ${msg} |
20406
+ const msg = hotspot.message.length > 50 ? hotspot.message.substring(0, 47) + "..." : hotspot.message;
20407
+ output += `| ${i + 1} | ${risk} | \`${hotspot.key.substring(0, 8)}\` | ${file2} | ${line} | ${msg} |
20351
20408
  `;
20352
20409
  }
20353
- if (toReview.length > 20) {
20410
+ if (toReview.length > 50) {
20354
20411
  output += `
20355
- *... and ${toReview.length - 20} more hotspots*
20412
+ *... and ${toReview.length - 50} more hotspots*
20356
20413
  `;
20357
20414
  }
20358
20415
  output += `
20359
- ### Review Guidelines
20416
+ ### How to Review Hotspots
20417
+
20418
+ You can review hotspots directly using this tool:
20360
20419
 
20361
- 1. **HIGH risk**: Review immediately - potential security vulnerability
20362
- 2. **MEDIUM risk**: Review soon - possible security concern
20363
- 3. **LOW risk**: Review when convenient - minor security consideration
20420
+ **Review a single hotspot:**
20421
+ \`\`\`
20422
+ sonarqube({ action: "reviewhotspot", hotspotKey: "<key>", resolution: "SAFE", comment: "Reviewed: no security risk because..." })
20423
+ \`\`\`
20364
20424
 
20365
- For each hotspot, determine if it's:
20366
- - **Safe**: The code is secure despite the warning
20367
- - **Fixed**: The vulnerability has been remediated
20368
- - **At Risk**: Requires code changes to fix
20425
+ **Review ALL pending hotspots as SAFE:**
20426
+ \`\`\`
20427
+ sonarqube({ action: "reviewhotspot", resolution: "SAFE", comment: "Bulk review: all hotspots verified safe" })
20428
+ \`\`\`
20429
+
20430
+ **Resolutions:**
20431
+ - \`SAFE\` — The code is secure despite the warning (most common)
20432
+ - \`FIXED\` — The vulnerability has been remediated
20433
+ - \`ACKNOWLEDGED\` — Known risk, accepted
20434
+
20435
+ ### Review Guidelines
20436
+
20437
+ 1. **HIGH risk**: Review the code carefully — potential security vulnerability
20438
+ 2. **MEDIUM risk**: Check regex patterns, randomness, protocol usage
20439
+ 3. **LOW risk**: Usually informational — hardcoded IPs, HTTP URLs in non-production code
20369
20440
  `;
20370
20441
  }
20371
20442
  return output;
20372
20443
  }
20444
+ async function handleReviewHotspot(ctx, hotspotKey, resolution, comment) {
20445
+ const { api: api2, projectKey } = ctx;
20446
+ const validResolutions = ["SAFE", "FIXED", "ACKNOWLEDGED"];
20447
+ const res = resolution?.toUpperCase() ?? "SAFE";
20448
+ if (!validResolutions.includes(res)) {
20449
+ return `**Error:** Invalid resolution "${resolution}". Must be one of: SAFE, FIXED, ACKNOWLEDGED`;
20450
+ }
20451
+ if (!hotspotKey) {
20452
+ const toReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
20453
+ if (toReview.length === 0) {
20454
+ return formatSuccess("Review Hotspots", "No pending hotspots to review. All hotspots have already been reviewed.");
20455
+ }
20456
+ const result = await api2.issues.bulkReviewHotspots(toReview.map((h) => h.key), res, comment ?? `Bulk reviewed as ${res} via opencode-sonarqube plugin`);
20457
+ let output = `## Hotspot Bulk Review Complete
20458
+
20459
+ **Project:** \`${projectKey}\`
20460
+ **Resolution:** ${res}
20461
+ **Reviewed:** ${result.success} hotspots
20462
+ **Failed:** ${result.failed} hotspots
20463
+ `;
20464
+ if (result.errors.length > 0) {
20465
+ output += `
20466
+ ### Errors
20467
+ `;
20468
+ for (const err of result.errors.slice(0, 10)) {
20469
+ output += `- ${err}
20470
+ `;
20471
+ }
20472
+ }
20473
+ output += `
20474
+ > Run \`sonarqube({ action: "status" })\` to check the updated Quality Gate.`;
20475
+ return output;
20476
+ }
20477
+ try {
20478
+ await api2.issues.reviewHotspot(hotspotKey, "REVIEWED", res, comment ?? `Reviewed as ${res} via opencode-sonarqube plugin`);
20479
+ return `## Hotspot Reviewed
20480
+
20481
+ **Hotspot:** \`${hotspotKey}\`
20482
+ **Status:** REVIEWED
20483
+ **Resolution:** ${res}
20484
+ ${comment ? `**Comment:** ${comment}` : ""}
20485
+
20486
+ > Run \`sonarqube({ action: "hotspots" })\` to see remaining hotspots.`;
20487
+ } catch (error45) {
20488
+ const msg = error45 instanceof Error ? error45.message : String(error45);
20489
+ return `**Error reviewing hotspot:** ${msg}
20490
+
20491
+ Make sure the hotspot key is correct. Use \`sonarqube({ action: "hotspots" })\` to list hotspot keys.`;
20492
+ }
20493
+ }
20373
20494
  // src/tools/handlers/duplications.ts
20374
20495
  async function handleDuplications(ctx) {
20375
20496
  const { api: api2, projectKey } = ctx;
@@ -20496,14 +20617,17 @@ Run \`sonarqube({ action: "analyze" })\` to generate metrics.`));
20496
20617
  // src/tools/sonarqube.ts
20497
20618
  var logger10 = new Logger("sonarqube-tool");
20498
20619
  var SonarQubeToolArgsSchema = exports_external2.object({
20499
- action: exports_external2.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform: analyze (run scanner), issues (all issues), newissues (only new code issues), status (quality gate), init/setup (initialize), validate (enterprise check), hotspots (security review), duplications (code duplicates), rule (explain rule), history (past analyses), profile (quality profile), branches (branch status), metrics (detailed metrics), worstfiles (files with most issues)"),
20620
+ action: exports_external2.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "reviewhotspot", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform: analyze (run scanner), issues (all issues), newissues (only new code issues), status (quality gate), init/setup (initialize), validate (enterprise check), hotspots (security review), duplications (code duplicates), rule (explain rule), history (past analyses), profile (quality profile), branches (branch status), metrics (detailed metrics), worstfiles (files with most issues)"),
20500
20621
  scope: exports_external2.enum(["all", "new", "changed"]).optional().default("all").describe("Scope of analysis: all files, only new code, or changed files"),
20501
20622
  severity: exports_external2.enum(["blocker", "critical", "major", "minor", "info", "all"]).optional().default("all").describe("Filter issues by minimum severity level"),
20502
20623
  fix: exports_external2.boolean().optional().default(false).describe("If true, include fix suggestions in the response"),
20503
20624
  projectKey: exports_external2.string().optional().describe("Override the project key (usually auto-detected)"),
20504
20625
  force: exports_external2.boolean().optional().default(false).describe("Force re-initialization even if already set up"),
20505
20626
  ruleKey: exports_external2.string().optional().describe("Rule key to explain (e.g., 'typescript:S1234') - required for 'rule' action"),
20506
- branch: exports_external2.string().optional().describe("Branch name for multi-branch analysis (default: main branch)")
20627
+ branch: exports_external2.string().optional().describe("Branch name for multi-branch analysis (default: main branch)"),
20628
+ hotspotKey: exports_external2.string().optional().describe("Hotspot key to review (UUID). If omitted with 'reviewhotspot' action, all TO_REVIEW hotspots are bulk-reviewed."),
20629
+ resolution: exports_external2.enum(["SAFE", "FIXED", "ACKNOWLEDGED"]).optional().describe("Hotspot review resolution: SAFE (no risk), FIXED (remediated), ACKNOWLEDGED (accepted risk)"),
20630
+ comment: exports_external2.string().optional().describe("Review comment explaining the decision")
20507
20631
  });
20508
20632
  async function executeSonarQubeTool(args, context) {
20509
20633
  const directory = context.directory ?? process.cwd();
@@ -20556,6 +20680,8 @@ ${setupResult.message}`);
20556
20680
  return await handleValidate(ctx);
20557
20681
  case "hotspots":
20558
20682
  return await handleHotspots(ctx);
20683
+ case "reviewhotspot":
20684
+ return await handleReviewHotspot(ctx, args.hotspotKey, args.resolution, args.comment);
20559
20685
  case "duplications":
20560
20686
  return await handleDuplications(ctx);
20561
20687
  case "rule":
@@ -21148,6 +21274,90 @@ Git operation completed with changes. Consider running:
21148
21274
  }
21149
21275
  };
21150
21276
  };
21277
+ const buildSystemTransformContext = async (_input) => {
21278
+ safeLog(`=== system.transform ENTERED ===`);
21279
+ const inputAny = _input;
21280
+ const sessionID = inputAny?.sessionID;
21281
+ const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
21282
+ const dir = sessionDir || effectiveDirectory;
21283
+ safeLog(` dir used: "${dir}"`);
21284
+ const now = Date.now();
21285
+ const cachedEntry = transformCacheMap.get(dir);
21286
+ if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
21287
+ return cachedEntry.data;
21288
+ }
21289
+ await loadPluginConfig();
21290
+ const sonarConfig = pluginConfig?.["sonarqube"];
21291
+ const config3 = loadConfig(sonarConfig);
21292
+ if (!config3 || config3.level === "off") {
21293
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21294
+ return;
21295
+ }
21296
+ const state = await getProjectState(dir);
21297
+ if (!state?.projectKey) {
21298
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21299
+ return;
21300
+ }
21301
+ const api2 = createSonarQubeAPI(config3, state);
21302
+ const [qgStatus, counts, newCodeResponse] = await Promise.all([
21303
+ api2.qualityGate.getStatus(state.projectKey),
21304
+ api2.issues.getCounts(state.projectKey),
21305
+ api2.issues.search({
21306
+ projectKey: state.projectKey,
21307
+ inNewCode: true,
21308
+ resolved: false,
21309
+ pageSize: 1
21310
+ }).catch(() => ({ paging: { total: 0 } }))
21311
+ ]);
21312
+ const qgFailed = qgStatus.projectStatus.status !== "OK";
21313
+ const newCodeIssues = newCodeResponse.paging.total;
21314
+ const hasIssues = counts.blocker > 0 || counts.critical > 0;
21315
+ if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21316
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21317
+ return;
21318
+ }
21319
+ const hotspotInfo = await buildHotspotInfo(api2, state.projectKey);
21320
+ const systemContext = formatSystemContext(qgStatus.projectStatus.status, qgFailed, counts, newCodeIssues, hotspotInfo, config3.level);
21321
+ transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21322
+ return systemContext;
21323
+ };
21324
+ const buildHotspotInfo = async (api2, projectKey) => {
21325
+ try {
21326
+ const hotspotsToReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
21327
+ if (hotspotsToReview.length > 0) {
21328
+ return `**Security Hotspots:** ${hotspotsToReview.length} unreviewed hotspots (may cause Quality Gate to fail)
21329
+ **To fix:** \`sonarqube({ action: "hotspots" })\` to list, then \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` to bulk-review
21330
+ `;
21331
+ }
21332
+ } catch {}
21333
+ return "";
21334
+ };
21335
+ const formatSystemContext = (qgStatusText, qgFailed, counts, newCodeIssues, hotspotInfo, level) => {
21336
+ const lines = [
21337
+ "## SonarQube Code Quality Status",
21338
+ "",
21339
+ `**Quality Gate:** ${qgStatusText}${qgFailed ? " (FAILED)" : ""}`,
21340
+ `**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major`
21341
+ ];
21342
+ if (newCodeIssues > 0) {
21343
+ lines.push(`**New Code Issues:** ${newCodeIssues} issues in recent changes`);
21344
+ }
21345
+ if (hotspotInfo) {
21346
+ lines.push(hotspotInfo);
21347
+ }
21348
+ if (counts.blocker > 0) {
21349
+ lines.push("**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.");
21350
+ }
21351
+ if (level === "enterprise") {
21352
+ lines.push("This project follows enterprise-level quality standards (zero tolerance for issues).");
21353
+ }
21354
+ lines.push("", "**Recommended Actions:**", '- `sonarqube({ action: "newissues" })` - See issues in your recent changes (Clean as You Code)', '- `sonarqube({ action: "worstfiles" })` - Find files needing most attention', '- `sonarqube({ action: "issues" })` - See all issues');
21355
+ if (hotspotInfo) {
21356
+ lines.push('- `sonarqube({ action: "reviewhotspot", resolution: "SAFE" })` - Bulk-review all security hotspots as safe');
21357
+ }
21358
+ return lines.join(`
21359
+ `);
21360
+ };
21151
21361
  safeLog(`=== PLUGIN INIT COMPLETE, returning hooks ===`);
21152
21362
  safeLog(` This instance's pluginImportUrl: "${pluginImportUrl}"`);
21153
21363
  safeLog(` This instance's effectiveDirectory: "${effectiveDirectory}"`);
@@ -21182,73 +21392,10 @@ Git operation completed with changes. Consider running:
21182
21392
  }
21183
21393
  }, "experimental.session.compacting"),
21184
21394
  "experimental.chat.system.transform": safeAsync(async (_input, output) => {
21185
- safeLog(`=== system.transform ENTERED ===`);
21186
- const inputAny = _input;
21187
- const sessionID = inputAny?.sessionID;
21188
- safeLog(` sessionID: "${sessionID}"`);
21189
- const sharedState = readSharedState();
21190
- safeLog(` sharedState sessions: ${JSON.stringify(sharedState.sessionToDirectory)}`);
21191
- safeLog(` sharedState directories: ${JSON.stringify(sharedState.registeredDirectories)}`);
21192
- const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
21193
- safeLog(` sessionDir from shared state: "${sessionDir}"`);
21194
- safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
21195
- const dir = sessionDir || effectiveDirectory;
21196
- safeLog(` FINAL dir used: "${dir}"`);
21197
- const now = Date.now();
21198
- const cachedEntry = transformCacheMap.get(dir);
21199
- if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
21200
- if (cachedEntry.data) {
21201
- output.system.push(cachedEntry.data);
21202
- }
21203
- return;
21204
- }
21205
- await loadPluginConfig();
21206
- const sonarConfig = pluginConfig?.["sonarqube"];
21207
- const config3 = loadConfig(sonarConfig);
21208
- if (!config3 || config3.level === "off") {
21209
- safeLog(` config level is off or null, returning early`);
21210
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21211
- return;
21395
+ const systemContext = await buildSystemTransformContext(_input);
21396
+ if (systemContext) {
21397
+ output.system.push(systemContext);
21212
21398
  }
21213
- const state = await getProjectState(dir);
21214
- if (!state?.projectKey) {
21215
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21216
- return;
21217
- }
21218
- const api2 = createSonarQubeAPI(config3, state);
21219
- const [qgStatus, counts, newCodeResponse] = await Promise.all([
21220
- api2.qualityGate.getStatus(state.projectKey),
21221
- api2.issues.getCounts(state.projectKey),
21222
- api2.issues.search({
21223
- projectKey: state.projectKey,
21224
- inNewCode: true,
21225
- resolved: false,
21226
- pageSize: 1
21227
- }).catch(() => ({ paging: { total: 0 } }))
21228
- ]);
21229
- const hasIssues = counts.blocker > 0 || counts.critical > 0;
21230
- const qgFailed = qgStatus.projectStatus.status !== "OK";
21231
- const newCodeIssues = newCodeResponse.paging.total;
21232
- if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21233
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21234
- return;
21235
- }
21236
- const systemContext = `## SonarQube Code Quality Status
21237
-
21238
- **Quality Gate:** ${qgStatus.projectStatus.status}${qgFailed ? " (FAILED)" : ""}
21239
- **Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
21240
- ${newCodeIssues > 0 ? `**New Code Issues:** ${newCodeIssues} issues in recent changes
21241
- ` : ""}
21242
- ${counts.blocker > 0 ? `**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.
21243
- ` : ""}
21244
- ${config3.level === "enterprise" ? `This project follows enterprise-level quality standards (zero tolerance for issues).
21245
- ` : ""}
21246
- **Recommended Actions:**
21247
- - \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
21248
- - \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
21249
- - \`sonarqube({ action: "issues" })\` - See all issues`;
21250
- transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21251
- output.system.push(systemContext);
21252
21399
  }, "experimental.chat.system.transform"),
21253
21400
  tool: {
21254
21401
  sonarqube: tool({
@@ -21263,6 +21410,7 @@ Actions:
21263
21410
  - status: Get quality gate status and metrics
21264
21411
  - validate: Check if project meets enterprise quality standards
21265
21412
  - hotspots: Get security hotspots that need review
21413
+ - reviewhotspot: Review/resolve security hotspots (mark as SAFE, FIXED, or ACKNOWLEDGED)
21266
21414
  - duplications: Find code duplications across the project
21267
21415
  - rule: Explain a specific SonarQube rule
21268
21416
  - history: Show past analysis history
@@ -21276,16 +21424,22 @@ Example usage:
21276
21424
  - sonarqube({ action: "newissues" }) - Issues only in your recent changes
21277
21425
  - sonarqube({ action: "worstfiles" }) - Files needing most attention
21278
21426
  - sonarqube({ action: "issues", severity: "critical" }) - Get critical+ issues
21279
- - sonarqube({ action: "status" }) - Check quality gate status`,
21427
+ - sonarqube({ action: "status" }) - Check quality gate status
21428
+ - sonarqube({ action: "hotspots" }) - List security hotspots
21429
+ - sonarqube({ action: "reviewhotspot", resolution: "SAFE" }) - Bulk-review all hotspots as SAFE
21430
+ - sonarqube({ action: "reviewhotspot", hotspotKey: "abc123", resolution: "SAFE", comment: "No risk" }) - Review single hotspot`,
21280
21431
  args: {
21281
- action: tool.schema.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform"),
21432
+ action: tool.schema.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "reviewhotspot", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform"),
21282
21433
  scope: tool.schema.enum(["all", "new", "changed"]).optional().describe("Scope of analysis"),
21283
21434
  severity: tool.schema.enum(["blocker", "critical", "major", "minor", "info", "all"]).optional().describe("Filter issues by minimum severity"),
21284
21435
  fix: tool.schema.boolean().optional().describe("Include fix suggestions in response"),
21285
21436
  projectKey: tool.schema.string().optional().describe("Override project key"),
21286
21437
  force: tool.schema.boolean().optional().describe("Force re-initialization"),
21287
21438
  ruleKey: tool.schema.string().optional().describe("Rule key to explain (e.g., 'typescript:S1234')"),
21288
- branch: tool.schema.string().optional().describe("Branch name for multi-branch projects")
21439
+ branch: tool.schema.string().optional().describe("Branch name for multi-branch projects"),
21440
+ hotspotKey: tool.schema.string().optional().describe("Hotspot key (UUID) to review. If omitted with reviewhotspot action, all pending hotspots are bulk-reviewed."),
21441
+ resolution: tool.schema.string().optional().describe("Hotspot review resolution: SAFE, FIXED, or ACKNOWLEDGED"),
21442
+ comment: tool.schema.string().optional().describe("Review comment explaining the decision")
21289
21443
  },
21290
21444
  async execute(args, _ctx) {
21291
21445
  const result = await executeSonarQubeTool({
@@ -21296,7 +21450,10 @@ Example usage:
21296
21450
  projectKey: args.projectKey,
21297
21451
  force: args.force ?? false,
21298
21452
  ruleKey: args.ruleKey,
21299
- branch: args.branch
21453
+ branch: args.branch,
21454
+ hotspotKey: args.hotspotKey,
21455
+ resolution: args.resolution,
21456
+ comment: args.comment
21300
21457
  }, {
21301
21458
  directory: getDirectory(),
21302
21459
  config: pluginConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "OpenCode Plugin for SonarQube integration - Enterprise-level code quality from the start",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "homepage": "https://github.com/mguttmann/opencode-sonarqube#readme",
39
39
  "dependencies": {
40
40
  "@opencode-ai/plugin": "^1.1.34",
41
- "opencode-sonarqube": "^1.2.45",
41
+ "opencode-sonarqube": "latest",
42
42
  "zod": "^3.24.0"
43
43
  },
44
44
  "devDependencies": {