opencode-sonarqube 2.1.0 → 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.
- package/README.md +59 -7
- package/dist/index.js +113 -81
- 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
|
[](https://sonarqube.example.com)
|
|
6
6
|
[](https://sonarqube.example.com)
|
|
7
|
-
[](https://sonarqube.example.com)
|
|
8
8
|
[](./LICENSE)
|
|
9
|
+
[](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
|
-
- **
|
|
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
|
|
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" | "
|
|
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"
|
|
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 |
|
|
532
|
+
| Tests | 722 |
|
|
481
533
|
| Bugs | 0 |
|
|
482
534
|
| Vulnerabilities | 0 |
|
|
483
535
|
| Code Smells | 0 |
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
3
|
var __export = (target, all) => {
|
|
19
4
|
for (var name in all)
|
|
20
5
|
__defProp(target, name, {
|
|
@@ -19394,6 +19379,20 @@ function groupIssuesBySeverity(issues) {
|
|
|
19394
19379
|
}
|
|
19395
19380
|
function formatIssuesForAgent(issues, qualityGateStatus) {
|
|
19396
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
|
+
}
|
|
19397
19396
|
return `## SonarQube Analysis Results
|
|
19398
19397
|
|
|
19399
19398
|
**Quality Gate: ${qualityGateStatus}**
|
|
@@ -20333,8 +20332,20 @@ async function handleStatus(ctx) {
|
|
|
20333
20332
|
output += `
|
|
20334
20333
|
### Failed Conditions
|
|
20335
20334
|
`;
|
|
20335
|
+
const hasHotspotCondition = failedConditions.some((c) => c.metricKey.includes("security_hotspots") || c.metricKey.includes("hotspot"));
|
|
20336
20336
|
for (const condition of failedConditions) {
|
|
20337
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" })\`
|
|
20338
20349
|
`;
|
|
20339
20350
|
}
|
|
20340
20351
|
}
|
|
@@ -21263,6 +21274,90 @@ Git operation completed with changes. Consider running:
|
|
|
21263
21274
|
}
|
|
21264
21275
|
};
|
|
21265
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
|
+
};
|
|
21266
21361
|
safeLog(`=== PLUGIN INIT COMPLETE, returning hooks ===`);
|
|
21267
21362
|
safeLog(` This instance's pluginImportUrl: "${pluginImportUrl}"`);
|
|
21268
21363
|
safeLog(` This instance's effectiveDirectory: "${effectiveDirectory}"`);
|
|
@@ -21297,73 +21392,10 @@ Git operation completed with changes. Consider running:
|
|
|
21297
21392
|
}
|
|
21298
21393
|
}, "experimental.session.compacting"),
|
|
21299
21394
|
"experimental.chat.system.transform": safeAsync(async (_input, output) => {
|
|
21300
|
-
|
|
21301
|
-
|
|
21302
|
-
|
|
21303
|
-
safeLog(` sessionID: "${sessionID}"`);
|
|
21304
|
-
const sharedState = readSharedState();
|
|
21305
|
-
safeLog(` sharedState sessions: ${JSON.stringify(sharedState.sessionToDirectory)}`);
|
|
21306
|
-
safeLog(` sharedState directories: ${JSON.stringify(sharedState.registeredDirectories)}`);
|
|
21307
|
-
const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
|
|
21308
|
-
safeLog(` sessionDir from shared state: "${sessionDir}"`);
|
|
21309
|
-
safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
|
|
21310
|
-
const dir = sessionDir || effectiveDirectory;
|
|
21311
|
-
safeLog(` FINAL dir used: "${dir}"`);
|
|
21312
|
-
const now = Date.now();
|
|
21313
|
-
const cachedEntry = transformCacheMap.get(dir);
|
|
21314
|
-
if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
|
|
21315
|
-
if (cachedEntry.data) {
|
|
21316
|
-
output.system.push(cachedEntry.data);
|
|
21317
|
-
}
|
|
21318
|
-
return;
|
|
21395
|
+
const systemContext = await buildSystemTransformContext(_input);
|
|
21396
|
+
if (systemContext) {
|
|
21397
|
+
output.system.push(systemContext);
|
|
21319
21398
|
}
|
|
21320
|
-
await loadPluginConfig();
|
|
21321
|
-
const sonarConfig = pluginConfig?.["sonarqube"];
|
|
21322
|
-
const config3 = loadConfig(sonarConfig);
|
|
21323
|
-
if (!config3 || config3.level === "off") {
|
|
21324
|
-
safeLog(` config level is off or null, returning early`);
|
|
21325
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21326
|
-
return;
|
|
21327
|
-
}
|
|
21328
|
-
const state = await getProjectState(dir);
|
|
21329
|
-
if (!state?.projectKey) {
|
|
21330
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21331
|
-
return;
|
|
21332
|
-
}
|
|
21333
|
-
const api2 = createSonarQubeAPI(config3, state);
|
|
21334
|
-
const [qgStatus, counts, newCodeResponse] = await Promise.all([
|
|
21335
|
-
api2.qualityGate.getStatus(state.projectKey),
|
|
21336
|
-
api2.issues.getCounts(state.projectKey),
|
|
21337
|
-
api2.issues.search({
|
|
21338
|
-
projectKey: state.projectKey,
|
|
21339
|
-
inNewCode: true,
|
|
21340
|
-
resolved: false,
|
|
21341
|
-
pageSize: 1
|
|
21342
|
-
}).catch(() => ({ paging: { total: 0 } }))
|
|
21343
|
-
]);
|
|
21344
|
-
const hasIssues = counts.blocker > 0 || counts.critical > 0;
|
|
21345
|
-
const qgFailed = qgStatus.projectStatus.status !== "OK";
|
|
21346
|
-
const newCodeIssues = newCodeResponse.paging.total;
|
|
21347
|
-
if (!hasIssues && !qgFailed && newCodeIssues === 0) {
|
|
21348
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21349
|
-
return;
|
|
21350
|
-
}
|
|
21351
|
-
const systemContext = `## SonarQube Code Quality Status
|
|
21352
|
-
|
|
21353
|
-
**Quality Gate:** ${qgStatus.projectStatus.status}${qgFailed ? " (FAILED)" : ""}
|
|
21354
|
-
**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
|
|
21355
|
-
${newCodeIssues > 0 ? `**New Code Issues:** ${newCodeIssues} issues in recent changes
|
|
21356
|
-
` : ""}
|
|
21357
|
-
${counts.blocker > 0 ? `**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.
|
|
21358
|
-
` : ""}
|
|
21359
|
-
${config3.level === "enterprise" ? `This project follows enterprise-level quality standards (zero tolerance for issues).
|
|
21360
|
-
` : ""}
|
|
21361
|
-
**Recommended Actions:**
|
|
21362
|
-
- \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
|
|
21363
|
-
- \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
|
|
21364
|
-
- \`sonarqube({ action: "issues" })\` - See all issues`;
|
|
21365
|
-
transformCacheMap.set(dir, { data: systemContext, timestamp: now });
|
|
21366
|
-
output.system.push(systemContext);
|
|
21367
21399
|
}, "experimental.chat.system.transform"),
|
|
21368
21400
|
tool: {
|
|
21369
21401
|
sonarqube: tool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sonarqube",
|
|
3
|
-
"version": "2.1.
|
|
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": "
|
|
41
|
+
"opencode-sonarqube": "latest",
|
|
42
42
|
"zod": "^3.24.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|