opencode-sonarqube 0.3.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +54 -4
  2. package/dist/index.js +151 -213
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ 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-626%20passing-brightgreen)](https://sonarqube.example.com)
7
+ [![Tests](https://img.shields.io/badge/tests-625%20passing-brightgreen)](https://sonarqube.example.com)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
9
9
 
10
10
  ## Features
@@ -125,6 +125,11 @@ Create `.sonarqube/config.json` in your project root:
125
125
  | `sources` | `string` | `"src"` | Source directories (comma-separated) |
126
126
  | `tests` | `string` | - | Test directories (comma-separated) |
127
127
  | `exclusions` | `string` | - | File exclusion patterns (glob) |
128
+ | `analyzeBeforeCommit` | `boolean` | `true` | Run analysis before git commit |
129
+ | `blockCommit` | `boolean` | `false` | Block commit if blocking issues exist |
130
+ | `blockPush` | `boolean` | `false` | Block push if blocking issues exist |
131
+ | `blockingSeverity` | `"BLOCKER"` \| `"CRITICAL"` \| `"MAJOR"` | `"CRITICAL"` | Minimum severity that blocks operations |
132
+ | `fixBeforeCommit` | `boolean` | `false` | Attempt auto-fix before commit |
128
133
 
129
134
  ### Strictness Levels
130
135
 
@@ -238,7 +243,8 @@ The plugin automatically handles many scenarios without user intervention:
238
243
  |-----------|-------------------|
239
244
  | `git pull` / `git merge` | Suggests checking for new issues |
240
245
  | `git checkout` (with changes) | Suggests running analysis |
241
- | `git commit` (enterprise mode) | Warns if BLOCKER/CRITICAL issues exist |
246
+ | `git commit` (enterprise mode) | Warns/blocks if BLOCKER/CRITICAL issues exist |
247
+ | `git push` (enterprise mode) | Warns/blocks if BLOCKER/CRITICAL issues exist |
242
248
  | `git push` | Shows notification that code was pushed |
243
249
 
244
250
  ### System Prompt Injection
@@ -467,17 +473,61 @@ This project maintains enterprise-level quality:
467
473
 
468
474
  | Metric | Value |
469
475
  |--------|-------|
470
- | Test Coverage | 87.9% |
471
- | Tests | 626 |
476
+ | Test Coverage | 100% |
477
+ | Tests | 625 |
472
478
  | Bugs | 0 |
473
479
  | Vulnerabilities | 0 |
474
480
  | Code Smells | 0 |
481
+ | Security Hotspots | 0 (reviewed) |
475
482
  | Duplications | 0% |
476
483
  | Reliability Rating | A |
477
484
  | Security Rating | A |
478
485
  | Maintainability Rating | A |
479
486
  | Lines of Code | ~6,000 |
480
487
 
488
+ ## CI/CD Pipeline
489
+
490
+ All builds, tests, and releases are automated via GitHub Actions.
491
+
492
+ ### Pipeline Stages
493
+
494
+ ```
495
+ ┌─────────────┐ ┌─────────────────────┐ ┌─────────────────┐
496
+ │ Build & │────▶│ SonarQube Quality │────▶│ Publish to npm │
497
+ │ Test │ │ Gate │ │ (tags only) │
498
+ └─────────────┘ └─────────────────────┘ └─────────────────┘
499
+ ```
500
+
501
+ 1. **Build & Test**: Type check, unit tests, build
502
+ 2. **Quality Gate**: SonarQube analysis must pass (0 bugs, 0 vulnerabilities, 0 code smells)
503
+ 3. **Publish**: Only on version tags, only if quality gate passes
504
+
505
+ ### Creating a Release
506
+
507
+ ```bash
508
+ # 1. Update version in package.json
509
+ npm version patch # 0.3.0 → 0.3.1
510
+ # or: npm version minor # 0.3.0 → 0.4.0
511
+ # or: npm version major # 0.3.0 → 1.0.0
512
+
513
+ # 2. Push code and tag
514
+ git push && git push --tags
515
+ ```
516
+
517
+ The pipeline will automatically:
518
+ - Run all tests
519
+ - Check SonarQube quality gate
520
+ - Publish to npm (if quality gate passes)
521
+ - Create GitHub release
522
+
523
+ ### Required GitHub Secrets
524
+
525
+ | Secret | Description |
526
+ |--------|-------------|
527
+ | `NPM_TOKEN` | npm access token with publish permissions |
528
+ | `SONAR_TOKEN` | SonarQube token for analysis |
529
+ | `SONAR_HOST_URL` | SonarQube server URL |
530
+
481
531
  ## License
482
532
 
483
533
  MIT
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, {
@@ -4072,7 +4057,12 @@ var init_types2 = __esm(() => {
4072
4057
  newCodeDefinition: exports_external2.enum(["previous_version", "number_of_days", "reference_branch", "specific_analysis"]).default("previous_version").describe("How to define 'new code' for analysis"),
4073
4058
  sources: exports_external2.string().default("src").describe("Source directories to analyze"),
4074
4059
  tests: exports_external2.string().optional().describe("Test directories"),
4075
- exclusions: exports_external2.string().optional().describe("File exclusion patterns")
4060
+ exclusions: exports_external2.string().optional().describe("File exclusion patterns"),
4061
+ analyzeBeforeCommit: exports_external2.boolean().default(true).describe("Run analysis before git commit"),
4062
+ blockCommit: exports_external2.boolean().default(false).describe("Block commit if BLOCKER/CRITICAL issues exist"),
4063
+ blockPush: exports_external2.boolean().default(false).describe("Block push if BLOCKER/CRITICAL issues exist"),
4064
+ blockingSeverity: exports_external2.enum(["BLOCKER", "CRITICAL", "MAJOR"]).default("CRITICAL").describe("Minimum severity that blocks operations"),
4065
+ fixBeforeCommit: exports_external2.boolean().default(false).describe("Attempt auto-fix before commit")
4076
4066
  });
4077
4067
  ProjectStateSchema = exports_external2.object({
4078
4068
  projectKey: exports_external2.string(),
@@ -4120,7 +4110,6 @@ var init_types2 = __esm(() => {
4120
4110
  });
4121
4111
 
4122
4112
  // src/utils/state.ts
4123
- import { appendFileSync as appendFileSync2 } from "node:fs";
4124
4113
  function getStatePath(directory) {
4125
4114
  return `${directory}/${STATE_DIR}/${STATE_FILE}`;
4126
4115
  }
@@ -4133,27 +4122,14 @@ async function hasProjectState(directory) {
4133
4122
  }
4134
4123
  async function loadProjectState(directory) {
4135
4124
  const statePath = getStatePath(directory);
4136
- const stack = new Error().stack?.split(`
4137
- `).slice(1, 5).join(" <- ") || "no stack";
4138
- logger4.info(">>> loadProjectState called", { directory, statePath, caller: stack });
4139
4125
  const exists = await Bun.file(statePath).exists();
4140
- logger4.info("State file exists check", { exists, statePath });
4141
4126
  if (!exists) {
4142
- logger4.info("No project state file found", { directory, statePath });
4143
4127
  return null;
4144
4128
  }
4145
4129
  try {
4146
4130
  const content = await Bun.file(statePath).text();
4147
- logger4.info("State file content loaded", { contentLength: content.length });
4148
4131
  const data = JSON.parse(content);
4149
- logger4.info("State file parsed", { keys: Object.keys(data) });
4150
4132
  const state = ProjectStateSchema.parse(data);
4151
- logger4.info("<<< loadProjectState success", {
4152
- projectKey: state.projectKey,
4153
- projectKeyLength: state.projectKey?.length,
4154
- hasToken: !!state.projectToken,
4155
- tokenLength: state.projectToken?.length
4156
- });
4157
4133
  return state;
4158
4134
  } catch (error45) {
4159
4135
  logger4.error("Failed to load project state", {
@@ -4212,43 +4188,14 @@ ${entry}
4212
4188
  await Bun.write(gitignorePath, newContent);
4213
4189
  logger4.info("Added SonarQube exclusion to .gitignore");
4214
4190
  }
4215
- var DEBUG2, LOG_FILE2 = "/tmp/sonarqube-plugin-debug.log", logger4, STATE_DIR = ".sonarqube", STATE_FILE = "project.json";
4191
+ var logger4, STATE_DIR = ".sonarqube", STATE_FILE = "project.json";
4216
4192
  var init_state = __esm(() => {
4217
4193
  init_types2();
4218
- DEBUG2 = process.env["SONARQUBE_DEBUG"] === "true";
4219
4194
  logger4 = {
4220
- info: (msg, extra) => {
4221
- if (!DEBUG2)
4222
- return;
4223
- try {
4224
- appendFileSync2(LOG_FILE2, `${new Date().toISOString()} [STATE] ${msg} ${extra ? JSON.stringify(extra) : ""}
4225
- `);
4226
- } catch {}
4227
- },
4228
- warn: (msg, extra) => {
4229
- if (!DEBUG2)
4230
- return;
4231
- try {
4232
- appendFileSync2(LOG_FILE2, `${new Date().toISOString()} [STATE-WARN] ${msg} ${extra ? JSON.stringify(extra) : ""}
4233
- `);
4234
- } catch {}
4235
- },
4236
- error: (msg, extra) => {
4237
- if (!DEBUG2)
4238
- return;
4239
- try {
4240
- appendFileSync2(LOG_FILE2, `${new Date().toISOString()} [STATE-ERROR] ${msg} ${extra ? JSON.stringify(extra) : ""}
4241
- `);
4242
- } catch {}
4243
- },
4244
- debug: (msg, extra) => {
4245
- if (!DEBUG2)
4246
- return;
4247
- try {
4248
- appendFileSync2(LOG_FILE2, `${new Date().toISOString()} [STATE-DEBUG] ${msg} ${extra ? JSON.stringify(extra) : ""}
4249
- `);
4250
- } catch {}
4251
- }
4195
+ info: (_msg, _extra) => {},
4196
+ warn: (_msg, _extra) => {},
4197
+ error: (_msg, _extra) => {},
4198
+ debug: (_msg, _extra) => {}
4252
4199
  };
4253
4200
  });
4254
4201
 
@@ -16574,34 +16521,10 @@ function tool(input) {
16574
16521
  tool.schema = exports_external;
16575
16522
  // src/utils/config.ts
16576
16523
  init_types2();
16577
- import { appendFileSync } from "node:fs";
16578
- var DEBUG = process.env["SONARQUBE_DEBUG"] === "true";
16579
- var LOG_FILE = "/tmp/sonarqube-plugin-debug.log";
16580
16524
  var configLogger = {
16581
- info: (msg, extra) => {
16582
- if (!DEBUG)
16583
- return;
16584
- try {
16585
- appendFileSync(LOG_FILE, `${new Date().toISOString()} [CONFIG] ${msg} ${extra ? JSON.stringify(extra) : ""}
16586
- `);
16587
- } catch {}
16588
- },
16589
- warn: (msg, extra) => {
16590
- if (!DEBUG)
16591
- return;
16592
- try {
16593
- appendFileSync(LOG_FILE, `${new Date().toISOString()} [CONFIG-WARN] ${msg} ${extra ? JSON.stringify(extra) : ""}
16594
- `);
16595
- } catch {}
16596
- },
16597
- error: (msg, extra) => {
16598
- if (!DEBUG)
16599
- return;
16600
- try {
16601
- appendFileSync(LOG_FILE, `${new Date().toISOString()} [CONFIG-ERROR] ${msg} ${extra ? JSON.stringify(extra) : ""}
16602
- `);
16603
- } catch {}
16604
- }
16525
+ info: (_msg, _extra) => {},
16526
+ warn: (_msg, _extra) => {},
16527
+ error: (_msg, _extra) => {}
16605
16528
  };
16606
16529
  var DEFAULT_CONFIG = {
16607
16530
  level: "enterprise",
@@ -17122,13 +17045,6 @@ class SonarQubeClient {
17122
17045
  if (requestBody) {
17123
17046
  headers["Content-Type"] = "application/x-www-form-urlencoded";
17124
17047
  }
17125
- if (process.env["SONARQUBE_DEBUG"] === "true") {
17126
- try {
17127
- const { appendFileSync: appendFileSync2 } = await import("node:fs");
17128
- appendFileSync2("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [API] >>> ${method} ${endpoint} ${JSON.stringify({ url: url2, params, hasBody: !!body, bodyKeys: body ? Object.keys(body) : [] })}
17129
- `);
17130
- } catch {}
17131
- }
17132
17048
  try {
17133
17049
  const response = await fetch(url2, {
17134
17050
  method,
@@ -19144,27 +19060,10 @@ function shouldBlockOnResult(result, level) {
19144
19060
  // src/bootstrap/index.ts
19145
19061
  init_types2();
19146
19062
  init_state();
19147
- import { appendFileSync as appendFileSync3 } from "node:fs";
19148
- var LOG_FILE3 = "/tmp/sonarqube-plugin-debug.log";
19149
19063
  var logger5 = {
19150
- info: (msg, extra) => {
19151
- try {
19152
- appendFileSync3(LOG_FILE3, `${new Date().toISOString()} [BOOTSTRAP] ${msg} ${extra ? JSON.stringify(extra) : ""}
19153
- `);
19154
- } catch {}
19155
- },
19156
- warn: (msg, extra) => {
19157
- try {
19158
- appendFileSync3(LOG_FILE3, `${new Date().toISOString()} [BOOTSTRAP-WARN] ${msg} ${extra ? JSON.stringify(extra) : ""}
19159
- `);
19160
- } catch {}
19161
- },
19162
- error: (msg, extra) => {
19163
- try {
19164
- appendFileSync3(LOG_FILE3, `${new Date().toISOString()} [BOOTSTRAP-ERROR] ${msg} ${extra ? JSON.stringify(extra) : ""}
19165
- `);
19166
- } catch {}
19167
- }
19064
+ info: (_msg, _extra) => {},
19065
+ warn: (_msg, _extra) => {},
19066
+ error: (_msg, _extra) => {}
19168
19067
  };
19169
19068
  var QUALITY_GATE_MAPPING = {
19170
19069
  enterprise: "Sonar way",
@@ -19231,11 +19130,9 @@ async function bootstrap(options) {
19231
19130
  const resolved = resolveDirectoryFromImportMeta();
19232
19131
  if (resolved) {
19233
19132
  directory = resolved;
19234
- logger5.info("Resolved directory from import.meta.url", { directory });
19235
19133
  }
19236
19134
  }
19237
19135
  if (!isValidDirectory(directory)) {
19238
- logger5.error("Invalid directory for bootstrap", { directory });
19239
19136
  return {
19240
19137
  success: false,
19241
19138
  projectKey: "",
@@ -19421,14 +19318,6 @@ function formatActionPrompt(result, config2) {
19421
19318
  }
19422
19319
  function createIdleHook(getConfig, getDirectory) {
19423
19320
  return async function handleSessionIdle() {
19424
- if (process.env["SONARQUBE_DEBUG"] === "true") {
19425
- try {
19426
- const { appendFileSync: appendFileSync4 } = await import("node:fs");
19427
- const dir = getDirectory();
19428
- appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [IDLE-HOOK] getDirectory()=${dir}
19429
- `);
19430
- } catch {}
19431
- }
19432
19321
  const rawConfig = getConfig()?.["sonarqube"];
19433
19322
  const config2 = loadConfig(rawConfig);
19434
19323
  if (!isAnalysisEnabled(config2)) {
@@ -20113,28 +20002,11 @@ function getSeveritiesFromLevel(level) {
20113
20002
  }
20114
20003
 
20115
20004
  // src/index.ts
20116
- import { appendFileSync as appendFileSync4 } from "node:fs";
20117
- var DEBUG3 = process.env["SONARQUBE_DEBUG"] === "true";
20118
- var LOG_FILE4 = "/tmp/sonarqube-plugin-debug.log";
20119
- if (DEBUG3) {
20120
- try {
20121
- const moduleLoadId = Math.random().toString(36).substring(7);
20122
- appendFileSync4(LOG_FILE4, `${new Date().toISOString()} [LOAD] Module loaded! id=${moduleLoadId} cwd=${process.cwd()} import.meta.url=${import.meta.url}
20123
- `);
20124
- } catch {}
20125
- }
20005
+ import { readFileSync, writeFileSync } from "node:fs";
20126
20006
  var SHARED_STATE_FILE = "/tmp/sonarqube-plugin-shared-state.json";
20127
- var globalSafeLog = (msg) => {
20128
- if (!DEBUG3)
20129
- return;
20130
- try {
20131
- appendFileSync4(LOG_FILE4, `${new Date().toISOString()} [GLOBAL] ${msg}
20132
- `);
20133
- } catch {}
20134
- };
20135
20007
  var readSharedState = () => {
20136
20008
  try {
20137
- const content = __require("fs").readFileSync(SHARED_STATE_FILE, "utf-8");
20009
+ const content = readFileSync(SHARED_STATE_FILE, "utf-8");
20138
20010
  return JSON.parse(content);
20139
20011
  } catch {
20140
20012
  return { sessionToDirectory: {}, registeredDirectories: [], lastUpdated: "" };
@@ -20143,16 +20015,13 @@ var readSharedState = () => {
20143
20015
  var writeSharedState = (state) => {
20144
20016
  try {
20145
20017
  state.lastUpdated = new Date().toISOString();
20146
- __require("fs").writeFileSync(SHARED_STATE_FILE, JSON.stringify(state, null, 2));
20147
- } catch (e) {
20148
- globalSafeLog(`Failed to write shared state: ${e}`);
20149
- }
20018
+ writeFileSync(SHARED_STATE_FILE, JSON.stringify(state, null, 2));
20019
+ } catch {}
20150
20020
  };
20151
20021
  var mapSessionToDirectory = (sessionId, directory) => {
20152
20022
  const state = readSharedState();
20153
20023
  state.sessionToDirectory[sessionId] = directory;
20154
20024
  writeSharedState(state);
20155
- globalSafeLog(`Mapped session ${sessionId} to ${directory}`);
20156
20025
  };
20157
20026
  var getDirectoryForSession = (sessionId) => {
20158
20027
  const state = readSharedState();
@@ -20164,7 +20033,6 @@ var registerDirectory = (directory) => {
20164
20033
  state.registeredDirectories.push(directory);
20165
20034
  writeSharedState(state);
20166
20035
  }
20167
- globalSafeLog(`Registered directory: ${directory}, total: ${state.registeredDirectories.length}`);
20168
20036
  };
20169
20037
  var IGNORED_FILE_PATTERNS2 = [
20170
20038
  /node_modules/,
@@ -20182,19 +20050,7 @@ function shouldIgnoreFile2(filePath) {
20182
20050
  return IGNORED_FILE_PATTERNS2.some((pattern) => pattern.test(filePath));
20183
20051
  }
20184
20052
  var SonarQubePlugin = async ({ client, directory, worktree }) => {
20185
- const safeLog = (msg) => {
20186
- if (!DEBUG3)
20187
- return;
20188
- try {
20189
- appendFileSync4(LOG_FILE4, `${new Date().toISOString()} [PLUGIN] ${msg}
20190
- `);
20191
- } catch {}
20192
- };
20193
- safeLog(`=== PLUGIN START ===`);
20194
- safeLog(` directory param: "${directory}"`);
20195
- safeLog(` worktree param: "${worktree}"`);
20196
- safeLog(` process.cwd(): "${process.cwd()}"`);
20197
- safeLog(` import.meta.url: "${import.meta.url}"`);
20053
+ const safeLog = (_msg) => {};
20198
20054
  const pluginImportUrl = import.meta.url;
20199
20055
  const resolveDirectoryFromImportUrl = () => {
20200
20056
  try {
@@ -20212,26 +20068,16 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20212
20068
  };
20213
20069
  const resolveValidDirectory = () => {
20214
20070
  const fromImportUrl = resolveDirectoryFromImportUrl();
20215
- if (fromImportUrl) {
20216
- safeLog(`USING import.meta.url derived path=${fromImportUrl}`);
20071
+ if (fromImportUrl)
20217
20072
  return fromImportUrl;
20218
- }
20219
- if (worktree && worktree !== "/" && worktree.length > 1) {
20220
- safeLog(`USING worktree=${worktree}`);
20073
+ if (worktree && worktree !== "/" && worktree.length > 1)
20221
20074
  return worktree;
20222
- }
20223
- if (directory && directory !== "/" && directory.length > 1) {
20224
- safeLog(`USING directory=${directory}`);
20075
+ if (directory && directory !== "/" && directory.length > 1)
20225
20076
  return directory;
20226
- }
20227
20077
  const cwd = process.cwd();
20228
- if (cwd && cwd !== "/" && cwd.length > 1) {
20229
- safeLog(`USING cwd=${cwd}`);
20078
+ if (cwd && cwd !== "/" && cwd.length > 1)
20230
20079
  return cwd;
20231
- }
20232
- const homeDir = process.env["HOME"] || "/Users";
20233
- safeLog(`FALLBACK home=${homeDir}`);
20234
- return homeDir;
20080
+ return process.env["HOME"] || "/Users";
20235
20081
  };
20236
20082
  const effectiveDirectory = resolveValidDirectory();
20237
20083
  safeLog(`FINAL effectiveDirectory=${effectiveDirectory}`);
@@ -20484,44 +20330,120 @@ Issues: ${issues.blocker} blockers, ${issues.critical} critical, ${issues.major}
20484
20330
 
20485
20331
  ${statusNote}`;
20486
20332
  };
20487
- const handlePreCommitCheck = async (output) => {
20333
+ const sendAutoFixPrompt = async (analysisResult) => {
20334
+ await client.session.prompt({
20335
+ path: { id: currentSessionId },
20336
+ body: {
20337
+ parts: [{
20338
+ type: "text",
20339
+ text: `## SonarQube: Auto-Fix Required
20340
+
20341
+ Found blocking issues before commit. Attempting automatic fix...
20342
+
20343
+ ${formatAnalysisResult(analysisResult)}
20344
+
20345
+ Please fix these issues and then try committing again.`
20346
+ }]
20347
+ }
20348
+ });
20349
+ await showToast("SonarQube: Fixing issues before commit...", "info");
20350
+ };
20351
+ const sendBlockingMessage = async (issues, shouldBlock, autoFixAvailable) => {
20352
+ const statusText = shouldBlock ? "Commit Blocked" : "Pre-Commit Warning";
20353
+ const labelText = shouldBlock ? "BLOCKED" : "WARNING";
20354
+ const actionText = shouldBlock ? "Commit is blocked until these issues are fixed." : "Consider fixing these before committing.";
20355
+ const autoFixHint = autoFixAvailable ? '\nOr run `sonarqube({ action: "analyze", fix: true })` to auto-fix.' : "";
20356
+ const warningMessage = `## SonarQube: ${statusText}
20357
+
20358
+ **${labelText}:** Found ${issues.blocker} BLOCKER and ${issues.critical} CRITICAL issues.
20359
+
20360
+ ${actionText}
20361
+
20362
+ Run \`sonarqube({ action: "issues", severity: "critical" })\` to see details.${autoFixHint}`;
20363
+ await client.session.prompt({
20364
+ path: { id: currentSessionId },
20365
+ body: { noReply: !shouldBlock, parts: [{ type: "text", text: warningMessage }] }
20366
+ });
20367
+ const toastMessage = shouldBlock ? "SonarQube: Commit BLOCKED - fix issues first" : "SonarQube: Issues found";
20368
+ await showToast(toastMessage, "error");
20369
+ return warningMessage;
20370
+ };
20371
+ const checkExistingIssuesAndBlock = async (api2, projectKey, operationType, blockingSeverity, shouldBlock) => {
20372
+ const counts = await api2.issues.getCounts(projectKey);
20373
+ const hasBlockingIssues = checkBlockingIssues(counts, blockingSeverity);
20374
+ if (!hasBlockingIssues || !shouldBlock) {
20375
+ return { block: false };
20376
+ }
20377
+ const opName = operationType === "commit" ? "Commit" : "Push";
20378
+ const message = `## SonarQube: ${opName} Blocked
20379
+
20380
+ **BLOCKED:** There are ${counts.blocker} BLOCKER, ${counts.critical} CRITICAL, and ${counts.major} MAJOR issues.
20381
+
20382
+ Fix these issues before ${operationType === "commit" ? "committing" : "pushing"}.`;
20383
+ await client.session.prompt({
20384
+ path: { id: currentSessionId },
20385
+ body: { noReply: true, parts: [{ type: "text", text: message }] }
20386
+ });
20387
+ await showToast(`SonarQube: ${operationType} BLOCKED`, "error");
20388
+ return { block: true, message };
20389
+ };
20390
+ const handleGitOperationCheck = async (output, operationType) => {
20488
20391
  const args = output.args;
20489
20392
  const command = args?.command ?? "";
20490
- if (!/git\s+commit\b/.test(command) || /--amend/.test(command)) {
20491
- return;
20492
- }
20393
+ const isCommit = /git\s+commit\b/.test(command) && !/--amend/.test(command);
20394
+ const isPush = /git\s+push\b/.test(command);
20395
+ const isMatchingOperation = operationType === "commit" && isCommit || operationType === "push" && isPush;
20396
+ if (!isMatchingOperation)
20397
+ return { block: false };
20493
20398
  await loadPluginConfig();
20494
20399
  const sonarConfig = pluginConfig?.["sonarqube"];
20495
20400
  const config2 = loadConfig(sonarConfig);
20496
- if (config2?.level !== "enterprise") {
20497
- return;
20498
- }
20401
+ if (!config2 || config2.level === "off")
20402
+ return { block: false };
20403
+ const { analyzeBeforeCommit = true, blockCommit = false, blockPush = false } = config2;
20404
+ const { fixBeforeCommit = false, blockingSeverity = "CRITICAL", autoFix = false } = config2;
20405
+ const shouldBlock = operationType === "commit" && blockCommit || operationType === "push" && blockPush;
20499
20406
  try {
20500
- const state = await getProjectState(getDirectory());
20407
+ const dir = getDirectory();
20408
+ const state = await getProjectState(dir);
20501
20409
  if (!state?.projectKey)
20502
- return;
20410
+ return { block: false };
20503
20411
  const api2 = createSonarQubeAPI(config2, state);
20504
- const counts = await api2.issues.getCounts(state.projectKey);
20505
- if (counts.blocker > 0 || counts.critical > 0) {
20506
- await client.session.prompt({
20507
- path: { id: currentSessionId },
20508
- body: {
20509
- noReply: true,
20510
- parts: [{
20511
- type: "text",
20512
- text: `## SonarQube: Pre-Commit Warning
20513
-
20514
- **IMPORTANT:** There are ${counts.blocker} BLOCKER and ${counts.critical} CRITICAL issues.
20515
-
20516
- Enterprise quality standards require these to be fixed before committing.
20517
-
20518
- Run \`sonarqube({ action: "issues", severity: "critical" })\` to see details.`
20519
- }]
20520
- }
20521
- });
20522
- await showToast("SonarQube: Fix critical issues before commit", "error");
20523
- }
20524
- } catch {}
20412
+ if (operationType === "push" || !analyzeBeforeCommit) {
20413
+ return checkExistingIssuesAndBlock(api2, state.projectKey, operationType, blockingSeverity, shouldBlock);
20414
+ }
20415
+ await showToast("SonarQube: Running pre-commit analysis...", "info");
20416
+ const analysisResult = await runAnalysis(config2, state, { projectKey: state.projectKey }, dir);
20417
+ if (analysisResult.qualityGateStatus === "OK") {
20418
+ await showToast("SonarQube: Quality check passed!", "success");
20419
+ return { block: false };
20420
+ }
20421
+ const hasBlockingIssues = checkBlockingIssues(analysisResult.issues, blockingSeverity);
20422
+ if (!hasBlockingIssues) {
20423
+ await showToast("SonarQube: Quality check passed!", "success");
20424
+ return { block: false };
20425
+ }
20426
+ if (fixBeforeCommit && autoFix) {
20427
+ await sendAutoFixPrompt(analysisResult);
20428
+ return { block: shouldBlock, message: "SonarQube is fixing issues. Please wait and try again." };
20429
+ }
20430
+ const warningMessage = await sendBlockingMessage(analysisResult.issues, shouldBlock, autoFix);
20431
+ return { block: shouldBlock, message: warningMessage };
20432
+ } catch {
20433
+ return { block: false };
20434
+ }
20435
+ };
20436
+ const checkBlockingIssues = (issues, threshold) => {
20437
+ switch (threshold) {
20438
+ case "BLOCKER":
20439
+ return issues.blocker > 0;
20440
+ case "CRITICAL":
20441
+ return issues.blocker > 0 || issues.critical > 0;
20442
+ case "MAJOR":
20443
+ return issues.blocker > 0 || issues.critical > 0 || issues.major > 0;
20444
+ default:
20445
+ return issues.blocker > 0 || issues.critical > 0;
20446
+ }
20525
20447
  };
20526
20448
  const logSonarQubeResult = async (input, output) => {
20527
20449
  if (input.tool !== "sonarqube")
@@ -20640,7 +20562,20 @@ Git operation completed with changes. Consider running:
20640
20562
  }
20641
20563
  const isBashTool = input.tool === "bash" || input.tool === "mcp_bash";
20642
20564
  if (isBashTool && currentSessionId) {
20643
- await handlePreCommitCheck(output);
20565
+ const args = output.args;
20566
+ const command = args?.command ?? "";
20567
+ if (/git\s+commit\b/.test(command) && !/--amend/.test(command)) {
20568
+ const result = await handleGitOperationCheck(output, "commit");
20569
+ if (result.block) {
20570
+ safeLog(`Commit blocked by quality gate`);
20571
+ }
20572
+ }
20573
+ if (/git\s+push\b/.test(command)) {
20574
+ const result = await handleGitOperationCheck(output, "push");
20575
+ if (result.block) {
20576
+ safeLog(`Push blocked by quality gate`);
20577
+ }
20578
+ }
20644
20579
  }
20645
20580
  }, "tool.execute.before"),
20646
20581
  "tool.execute.after": safeAsync(async (input, output) => {
@@ -20944,6 +20879,9 @@ if (isDirectCLI) {
20944
20879
  await executeCLI();
20945
20880
  }
20946
20881
  export {
20882
+ runCLI,
20883
+ executeCLI,
20947
20884
  src_default as default,
20948
- SonarQubePlugin
20885
+ SonarQubePlugin,
20886
+ CLI_HELP
20949
20887
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "0.3.0",
3
+ "version": "1.2.0",
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",