opencode-sonarqube 2.0.0 → 2.0.2

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 (2) hide show
  1. package/dist/index.js +86 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4234,7 +4234,8 @@ async function deriveProjectKey(directory) {
4234
4234
  return `project-${Date.now()}`;
4235
4235
  }
4236
4236
  function sanitizeProjectKey(input) {
4237
- return input.toLowerCase().replaceAll(/[^a-z0-9-_]/g, "-").replaceAll(/-+/g, "-").replaceAll(/(?:^-)|(?:-$)/g, "").slice(0, 400);
4237
+ const sanitized = input.toLowerCase().replaceAll(/[^a-z0-9-_]/g, "-").replaceAll(/-+/g, "-").replaceAll(/(?:^-)|(?:-$)/g, "").slice(0, 400);
4238
+ return sanitized || "project";
4238
4239
  }
4239
4240
  var configLogger, DEFAULT_CONFIG;
4240
4241
  var init_config = __esm(() => {
@@ -4244,8 +4245,7 @@ var init_config = __esm(() => {
4244
4245
  DEFAULT_CONFIG = {
4245
4246
  level: "enterprise",
4246
4247
  autoAnalyze: true,
4247
- newCodeDefinition: "previous_version",
4248
- sources: "src"
4248
+ newCodeDefinition: "previous_version"
4249
4249
  };
4250
4250
  });
4251
4251
 
@@ -4787,7 +4787,10 @@ class SonarQubeClient {
4787
4787
  return this.request("/api/authentication/validate");
4788
4788
  }
4789
4789
  async getVersion() {
4790
- const response = await fetch(`${this.baseUrl}/api/server/version`);
4790
+ const url2 = buildUrl(this.baseUrl, "/api/server/version");
4791
+ const response = await fetch(url2, {
4792
+ headers: { Authorization: buildAuthHeader(this.auth) }
4793
+ });
4791
4794
  return response.text();
4792
4795
  }
4793
4796
  async healthCheck() {
@@ -4987,10 +4990,8 @@ async function loadProjectState(directory) {
4987
4990
  async function saveProjectState(directory, state) {
4988
4991
  const stateDir = getStateDir(directory);
4989
4992
  const statePath = getStatePath(directory);
4990
- const dirExists = await Bun.file(stateDir).exists();
4991
- if (!dirExists) {
4992
- await Bun.write(`${stateDir}/.gitkeep`, "");
4993
- }
4993
+ const { mkdir } = await import("node:fs/promises");
4994
+ await mkdir(stateDir, { recursive: true });
4994
4995
  const content = JSON.stringify(state, null, 2);
4995
4996
  await Bun.write(statePath, content);
4996
4997
  logger5.info("Saved project state", { projectKey: state.projectKey });
@@ -19419,7 +19420,7 @@ function extractTaskId(output) {
19419
19420
  return altMatch?.[1];
19420
19421
  }
19421
19422
  function sanitizeArgValue(value) {
19422
- return value.replaceAll(/[;&|`$(){}[\]<>\\'"!\n\r]/g, "");
19423
+ return value.replaceAll(/[\n\r]/g, "");
19423
19424
  }
19424
19425
  async function runScanner(config3, state, options, directory) {
19425
19426
  const dir = directory ?? process.cwd();
@@ -19459,7 +19460,7 @@ async function runScanner(config3, state, options, directory) {
19459
19460
  stderr: "pipe",
19460
19461
  env: {
19461
19462
  ...process.env,
19462
- NODE_OPTIONS: "--max-old-space-size=4096"
19463
+ NODE_OPTIONS: `${process.env["NODE_OPTIONS"] ?? ""} --max-old-space-size=4096`.trim()
19463
19464
  }
19464
19465
  });
19465
19466
  const stdout = await new Response(proc.stdout).text();
@@ -19600,32 +19601,37 @@ function formatAnalysisResult(result) {
19600
19601
  init_bootstrap();
19601
19602
  init_logger();
19602
19603
  var logger7 = new Logger("sonarqube-hooks");
19603
- var editedFiles = new Set;
19604
- var lastAnalysisTime = 0;
19605
19604
  var ANALYSIS_COOLDOWN_MS = 5 * 60 * 1000;
19606
- var bootstrapInProgress = false;
19605
+ function createHookState() {
19606
+ return {
19607
+ editedFiles: new Set,
19608
+ lastAnalysisTime: 0,
19609
+ bootstrapInProgress: false
19610
+ };
19611
+ }
19612
+ var activeState = createHookState();
19607
19613
  function isAnalysisEnabled(config3) {
19608
19614
  return config3 !== null && config3.level !== "off" && config3.autoAnalyze;
19609
19615
  }
19610
- function isInCooldown() {
19616
+ function isInCooldown(state) {
19611
19617
  const now = Date.now();
19612
- return now - lastAnalysisTime < ANALYSIS_COOLDOWN_MS;
19618
+ return now - state.lastAnalysisTime < ANALYSIS_COOLDOWN_MS;
19613
19619
  }
19614
- async function handleBootstrap(config3, directory) {
19615
- if (bootstrapInProgress) {
19620
+ async function handleBootstrap(config3, directory, hookState) {
19621
+ if (hookState.bootstrapInProgress) {
19616
19622
  logger7.debug("Bootstrap already in progress");
19617
19623
  return;
19618
19624
  }
19619
19625
  logger7.info("First run detected - initializing SonarQube integration");
19620
- bootstrapInProgress = true;
19626
+ hookState.bootstrapInProgress = true;
19621
19627
  try {
19622
19628
  const result = await bootstrap({ config: config3, directory });
19623
- bootstrapInProgress = false;
19629
+ hookState.bootstrapInProgress = false;
19624
19630
  return result.success ? formatBootstrapMessage(result) : `**SonarQube Setup Failed**
19625
19631
 
19626
19632
  ${result.message}`;
19627
19633
  } catch (error45) {
19628
- bootstrapInProgress = false;
19634
+ hookState.bootstrapInProgress = false;
19629
19635
  logger7.error("Bootstrap failed", { error: String(error45) });
19630
19636
  const errorMsg = error45 instanceof Error ? error45.message : String(error45);
19631
19637
  return `**SonarQube Setup Error**
@@ -19633,7 +19639,7 @@ ${result.message}`;
19633
19639
  ${errorMsg}`;
19634
19640
  }
19635
19641
  }
19636
- async function performAnalysis(config3, state, directory) {
19642
+ async function performAnalysis(config3, state, directory, hookState) {
19637
19643
  const projectKey = state.projectKey;
19638
19644
  const projectName = config3.projectName ?? projectKey;
19639
19645
  const result = await runAnalysis(config3, state, {
@@ -19642,7 +19648,9 @@ async function performAnalysis(config3, state, directory) {
19642
19648
  sources: config3.sources,
19643
19649
  tests: config3.tests
19644
19650
  }, directory);
19645
- editedFiles.clear();
19651
+ if (result.success) {
19652
+ hookState.editedFiles.clear();
19653
+ }
19646
19654
  return formatAnalysisOutput(result, config3);
19647
19655
  }
19648
19656
  function formatAnalysisOutput(result, config3) {
@@ -19667,7 +19675,7 @@ function formatActionPrompt(result, _config) {
19667
19675
 
19668
19676
  **Action Required:** Found ${blockerCount} blocker(s) and ${criticalCount} critical issue(s). Please review and fix these issues before continuing.`;
19669
19677
  }
19670
- function createIdleHook(getConfig, getDirectory) {
19678
+ function createIdleHook(getConfig, getDirectory, hookState = activeState) {
19671
19679
  return async function handleSessionIdle() {
19672
19680
  const rawConfig = getConfig()?.["sonarqube"];
19673
19681
  const config3 = loadConfig(rawConfig);
@@ -19676,25 +19684,25 @@ function createIdleHook(getConfig, getDirectory) {
19676
19684
  }
19677
19685
  const directory = getDirectory();
19678
19686
  if (await needsBootstrap(directory)) {
19679
- return handleBootstrap(config3, directory);
19687
+ return handleBootstrap(config3, directory, hookState);
19680
19688
  }
19681
- if (isInCooldown()) {
19689
+ if (isInCooldown(hookState)) {
19682
19690
  logger7.debug("Skipping auto-analysis (cooldown)");
19683
19691
  return;
19684
19692
  }
19685
- if (editedFiles.size === 0) {
19693
+ if (hookState.editedFiles.size === 0) {
19686
19694
  logger7.debug("Skipping auto-analysis (no edited files)");
19687
19695
  return;
19688
19696
  }
19689
19697
  logger7.info("Session idle - triggering auto-analysis");
19690
- lastAnalysisTime = Date.now();
19698
+ hookState.lastAnalysisTime = Date.now();
19691
19699
  try {
19692
19700
  const state = await getProjectState(directory);
19693
19701
  if (!state) {
19694
19702
  logger7.warn("No project state found, cannot run analysis");
19695
19703
  return;
19696
19704
  }
19697
- return await performAnalysis(config3, state, directory);
19705
+ return await performAnalysis(config3, state, directory, hookState);
19698
19706
  } catch (error45) {
19699
19707
  logger7.error(`Auto-analysis failed: ${error45}`);
19700
19708
  return;
@@ -19732,22 +19740,26 @@ var IGNORED_FILE_PATTERNS = [
19732
19740
  function shouldIgnoreFile(filePath) {
19733
19741
  return IGNORED_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
19734
19742
  }
19735
- function createFileEditedHook() {
19743
+ function createFileEditedHook(hookState = activeState) {
19736
19744
  return function handleFileEdited(input) {
19737
19745
  const { filePath } = input;
19738
19746
  if (!shouldIgnoreFile(filePath)) {
19739
- editedFiles.add(filePath);
19747
+ hookState.editedFiles.add(filePath);
19748
+ activeState = hookState;
19740
19749
  logger7.debug(`Tracked edited file: ${filePath}`);
19741
19750
  }
19742
19751
  };
19743
19752
  }
19744
19753
  function getEditedFiles() {
19745
- return Array.from(editedFiles);
19754
+ return Array.from(activeState.editedFiles);
19746
19755
  }
19747
19756
  function createHooks(getConfig, getDirectory) {
19757
+ const hookState = createHookState();
19758
+ activeState = hookState;
19748
19759
  return {
19749
- sessionIdle: createIdleHook(getConfig, getDirectory),
19750
- fileEdited: createFileEditedHook()
19760
+ sessionIdle: createIdleHook(getConfig, getDirectory, hookState),
19761
+ fileEdited: createFileEditedHook(hookState),
19762
+ getEditedFiles: () => Array.from(hookState.editedFiles)
19751
19763
  };
19752
19764
  }
19753
19765
 
@@ -20495,21 +20507,19 @@ var SonarQubeToolArgsSchema = exports_external2.object({
20495
20507
  });
20496
20508
  async function executeSonarQubeTool(args, context) {
20497
20509
  const directory = context.directory ?? process.cwd();
20498
- const config3 = loadConfig(context.config);
20510
+ const rawConfig = context.config?.["sonarqube"] ?? context.config;
20511
+ const config3 = loadConfig(rawConfig);
20499
20512
  if (!config3) {
20500
20513
  return formatError2(ErrorMessages.configurationMissing("SonarQube configuration not found.", [
20501
- "Set environment variables: SONAR_HOST_URL and SONAR_TOKEN",
20514
+ "Set environment variables: SONAR_HOST_URL, SONAR_USER, and SONAR_PASSWORD",
20502
20515
  "Or create a .sonarqube/config.json file in your project",
20503
20516
  "Or add plugin configuration in opencode.json"
20504
20517
  ]) + `
20505
20518
 
20506
20519
  Required environment variables:
20507
20520
  - SONAR_HOST_URL (e.g., https://sonarqube.company.com)
20508
- - SONAR_TOKEN (analysis token)
20509
-
20510
- Optional:
20511
- - SONAR_USER
20512
- - SONAR_PASSWORD`);
20521
+ - SONAR_USER (e.g., admin)
20522
+ - SONAR_PASSWORD (password or token)`);
20513
20523
  }
20514
20524
  logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
20515
20525
  try {
@@ -20571,7 +20581,7 @@ ${setupResult.message}`);
20571
20581
  }
20572
20582
 
20573
20583
  // src/utils/shared-state.ts
20574
- import { readFileSync, writeFileSync } from "node:fs";
20584
+ import { readFileSync, writeFileSync, renameSync } from "node:fs";
20575
20585
  var SHARED_STATE_FILE = "/tmp/sonarqube-plugin-shared-state.json";
20576
20586
  var readSharedState = () => {
20577
20587
  try {
@@ -20584,7 +20594,9 @@ var readSharedState = () => {
20584
20594
  var writeSharedState = (state) => {
20585
20595
  try {
20586
20596
  state.lastUpdated = new Date().toISOString();
20587
- writeFileSync(SHARED_STATE_FILE, JSON.stringify(state, null, 2));
20597
+ const tmpFile = `${SHARED_STATE_FILE}.tmp.${process.pid}`;
20598
+ writeFileSync(tmpFile, JSON.stringify(state, null, 2));
20599
+ renameSync(tmpFile, SHARED_STATE_FILE);
20588
20600
  } catch {}
20589
20601
  };
20590
20602
  var mapSessionToDirectory = (sessionId, directory) => {
@@ -20846,6 +20858,10 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20846
20858
  safeLog("client.app.log failed (non-fatal)");
20847
20859
  }
20848
20860
  let pluginConfig;
20861
+ let pluginConfigLoadedAt = 0;
20862
+ const CONFIG_RELOAD_INTERVAL_MS = 60000;
20863
+ const transformCacheMap = new Map;
20864
+ const TRANSFORM_CACHE_TTL_MS = 60000;
20849
20865
  let lastAnalysisResult;
20850
20866
  const getConfig = () => pluginConfig;
20851
20867
  const getDirectory = () => {
@@ -20856,8 +20872,9 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20856
20872
  return effectiveDirectory;
20857
20873
  };
20858
20874
  const loadPluginConfig = async () => {
20859
- if (pluginConfig)
20875
+ if (pluginConfig && Date.now() - pluginConfigLoadedAt < CONFIG_RELOAD_INTERVAL_MS)
20860
20876
  return;
20877
+ pluginConfig = undefined;
20861
20878
  const dir = getDirectory();
20862
20879
  const sonarConfigPath = `${dir}/.sonarqube/config.json`;
20863
20880
  try {
@@ -20865,16 +20882,18 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20865
20882
  if (await configFile.exists()) {
20866
20883
  const config3 = await configFile.json();
20867
20884
  pluginConfig = { sonarqube: config3 };
20885
+ pluginConfigLoadedAt = Date.now();
20868
20886
  safeLog(`Config loaded from ${sonarConfigPath}`);
20869
20887
  return;
20870
20888
  }
20871
20889
  } catch {}
20872
20890
  pluginConfig = {};
20891
+ pluginConfigLoadedAt = Date.now();
20873
20892
  safeLog("Using environment variables for config");
20874
20893
  };
20875
20894
  const hooks = createHooks(getConfig, getDirectory);
20876
20895
  let currentSessionId;
20877
- let initialCheckDone = false;
20896
+ const initialCheckSessions = new Set;
20878
20897
  const buildQualityNotification = (qgStatus, counts, qgFailed) => {
20879
20898
  const statusEmoji = qgFailed ? "[FAIL]" : "[WARN]";
20880
20899
  const blockerNote = counts.blocker > 0 ? `**Action Required:** There are BLOCKER issues that should be fixed.
@@ -20887,9 +20906,9 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20887
20906
  ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
20888
20907
  };
20889
20908
  const performInitialQualityCheck = async (sessionId) => {
20890
- if (initialCheckDone)
20909
+ if (initialCheckSessions.has(sessionId))
20891
20910
  return;
20892
- initialCheckDone = true;
20911
+ initialCheckSessions.add(sessionId);
20893
20912
  try {
20894
20913
  await loadPluginConfig();
20895
20914
  const sonarConfig = pluginConfig?.["sonarqube"];
@@ -20936,7 +20955,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20936
20955
  });
20937
20956
  } catch {}
20938
20957
  };
20939
- const qualityGatePattern = /Quality Gate: \[(PASS|FAIL)\] (\w+)/;
20958
+ const qualityGatePattern = /Quality Gate[:\s*]*\[(?:PASS|FAIL)\]\s*(\w+)|\*\*Quality Gate:\s*(\w+)\*\*/;
20940
20959
  const issueCountPattern = /Blockers: (\d+), Critical: (\d+), Major: (\d+), Minor: (\d+), Info: (\d+)/;
20941
20960
  const parseAnalysisResult = (message) => {
20942
20961
  const resultMatch = qualityGatePattern.exec(message);
@@ -20945,7 +20964,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20945
20964
  return;
20946
20965
  }
20947
20966
  return {
20948
- qualityGate: resultMatch[2],
20967
+ qualityGate: resultMatch[1] || resultMatch[2],
20949
20968
  issues: {
20950
20969
  blocker: Number.parseInt(issueMatch[1], 10),
20951
20970
  critical: Number.parseInt(issueMatch[2], 10),
@@ -21004,8 +21023,8 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
21004
21023
  if (!config3 || config3.level === "off" || !config3.autoAnalyze) {
21005
21024
  return;
21006
21025
  }
21007
- const editedFiles2 = getEditedFiles();
21008
- if (editedFiles2.length === 0) {
21026
+ const editedFiles = hooks.getEditedFiles?.() ?? getEditedFiles();
21027
+ if (editedFiles.length === 0) {
21009
21028
  return;
21010
21029
  }
21011
21030
  const message = await hooks.sessionIdle();
@@ -21016,7 +21035,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
21016
21035
  if (parsedResult) {
21017
21036
  lastAnalysisResult = parsedResult;
21018
21037
  }
21019
- const passed = message.includes("[PASS]");
21038
+ const passed = message.includes("[PASS]") || message.includes("Quality Gate") && message.includes("OK");
21020
21039
  await showToast(passed ? "SonarQube: Quality Gate Passed" : "SonarQube: Issues Found", passed ? "success" : "error");
21021
21040
  await injectAnalysisResults(message, config3, currentSessionId);
21022
21041
  };
@@ -21115,7 +21134,7 @@ Git operation completed with changes. Consider running:
21115
21134
  const handleGitPush = async (command, outputData) => {
21116
21135
  if (!/git\s+push\b/.test(command))
21117
21136
  return;
21118
- if (outputData?.includes("error") || outputData?.includes("rejected"))
21137
+ if (outputData?.includes("error:") || outputData?.includes("fatal:") || outputData?.includes("rejected"))
21119
21138
  return;
21120
21139
  await showToast("Code pushed - SonarQube will analyze on server", "info");
21121
21140
  };
@@ -21174,16 +21193,27 @@ Git operation completed with changes. Consider running:
21174
21193
  safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
21175
21194
  const dir = sessionDir || effectiveDirectory;
21176
21195
  safeLog(` FINAL dir used: "${dir}"`);
21196
+ const now = Date.now();
21197
+ const cachedEntry = transformCacheMap.get(dir);
21198
+ if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
21199
+ if (cachedEntry.data) {
21200
+ output.system.push(cachedEntry.data);
21201
+ }
21202
+ return;
21203
+ }
21177
21204
  await loadPluginConfig();
21178
21205
  const sonarConfig = pluginConfig?.["sonarqube"];
21179
21206
  const config3 = loadConfig(sonarConfig);
21180
21207
  if (!config3 || config3.level === "off") {
21181
21208
  safeLog(` config level is off or null, returning early`);
21209
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21182
21210
  return;
21183
21211
  }
21184
21212
  const state = await getProjectState(dir);
21185
- if (!state?.projectKey)
21213
+ if (!state?.projectKey) {
21214
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21186
21215
  return;
21216
+ }
21187
21217
  const api2 = createSonarQubeAPI(config3, state);
21188
21218
  const [qgStatus, counts, newCodeResponse] = await Promise.all([
21189
21219
  api2.qualityGate.getStatus(state.projectKey),
@@ -21199,6 +21229,7 @@ Git operation completed with changes. Consider running:
21199
21229
  const qgFailed = qgStatus.projectStatus.status !== "OK";
21200
21230
  const newCodeIssues = newCodeResponse.paging.total;
21201
21231
  if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21232
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21202
21233
  return;
21203
21234
  }
21204
21235
  const systemContext = `## SonarQube Code Quality Status
@@ -21215,6 +21246,7 @@ ${config3.level === "enterprise" ? `This project follows enterprise-level qualit
21215
21246
  - \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
21216
21247
  - \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
21217
21248
  - \`sonarqube({ action: "issues" })\` - See all issues`;
21249
+ transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21218
21250
  output.system.push(systemContext);
21219
21251
  }, "experimental.chat.system.transform"),
21220
21252
  tool: {
@@ -21273,6 +21305,7 @@ Example usage:
21273
21305
  if (parsedResult) {
21274
21306
  lastAnalysisResult = parsedResult;
21275
21307
  }
21308
+ transformCacheMap.clear();
21276
21309
  }
21277
21310
  return result;
21278
21311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",