opencode-sonarqube 2.0.1 → 2.0.3

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 +84 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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 });
@@ -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,7 +20507,8 @@ 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
20514
  "Set environment variables: SONAR_HOST_URL, SONAR_USER, and SONAR_PASSWORD",
@@ -20568,7 +20581,7 @@ ${setupResult.message}`);
20568
20581
  }
20569
20582
 
20570
20583
  // src/utils/shared-state.ts
20571
- import { readFileSync, writeFileSync } from "node:fs";
20584
+ import { readFileSync, writeFileSync, renameSync } from "node:fs";
20572
20585
  var SHARED_STATE_FILE = "/tmp/sonarqube-plugin-shared-state.json";
20573
20586
  var readSharedState = () => {
20574
20587
  try {
@@ -20581,7 +20594,9 @@ var readSharedState = () => {
20581
20594
  var writeSharedState = (state) => {
20582
20595
  try {
20583
20596
  state.lastUpdated = new Date().toISOString();
20584
- 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);
20585
20600
  } catch {}
20586
20601
  };
20587
20602
  var mapSessionToDirectory = (sessionId, directory) => {
@@ -20843,6 +20858,10 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20843
20858
  safeLog("client.app.log failed (non-fatal)");
20844
20859
  }
20845
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;
20846
20865
  let lastAnalysisResult;
20847
20866
  const getConfig = () => pluginConfig;
20848
20867
  const getDirectory = () => {
@@ -20853,8 +20872,9 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20853
20872
  return effectiveDirectory;
20854
20873
  };
20855
20874
  const loadPluginConfig = async () => {
20856
- if (pluginConfig)
20875
+ if (pluginConfig && Date.now() - pluginConfigLoadedAt < CONFIG_RELOAD_INTERVAL_MS)
20857
20876
  return;
20877
+ pluginConfig = undefined;
20858
20878
  const dir = getDirectory();
20859
20879
  const sonarConfigPath = `${dir}/.sonarqube/config.json`;
20860
20880
  try {
@@ -20862,16 +20882,18 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20862
20882
  if (await configFile.exists()) {
20863
20883
  const config3 = await configFile.json();
20864
20884
  pluginConfig = { sonarqube: config3 };
20885
+ pluginConfigLoadedAt = Date.now();
20865
20886
  safeLog(`Config loaded from ${sonarConfigPath}`);
20866
20887
  return;
20867
20888
  }
20868
20889
  } catch {}
20869
20890
  pluginConfig = {};
20891
+ pluginConfigLoadedAt = Date.now();
20870
20892
  safeLog("Using environment variables for config");
20871
20893
  };
20872
20894
  const hooks = createHooks(getConfig, getDirectory);
20873
20895
  let currentSessionId;
20874
- let initialCheckDone = false;
20896
+ const initialCheckSessions = new Set;
20875
20897
  const buildQualityNotification = (qgStatus, counts, qgFailed) => {
20876
20898
  const statusEmoji = qgFailed ? "[FAIL]" : "[WARN]";
20877
20899
  const blockerNote = counts.blocker > 0 ? `**Action Required:** There are BLOCKER issues that should be fixed.
@@ -20884,9 +20906,9 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20884
20906
  ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
20885
20907
  };
20886
20908
  const performInitialQualityCheck = async (sessionId) => {
20887
- if (initialCheckDone)
20909
+ if (initialCheckSessions.has(sessionId))
20888
20910
  return;
20889
- initialCheckDone = true;
20911
+ initialCheckSessions.add(sessionId);
20890
20912
  try {
20891
20913
  await loadPluginConfig();
20892
20914
  const sonarConfig = pluginConfig?.["sonarqube"];
@@ -20933,7 +20955,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20933
20955
  });
20934
20956
  } catch {}
20935
20957
  };
20936
- const qualityGatePattern = /Quality Gate: \[(PASS|FAIL)\] (\w+)/;
20958
+ const qualityGatePattern = /Quality Gate[:\s*]*\[(?:PASS|FAIL)\]\s*(\w+)|\*\*Quality Gate:\s*(\w+)\*\*/;
20937
20959
  const issueCountPattern = /Blockers: (\d+), Critical: (\d+), Major: (\d+), Minor: (\d+), Info: (\d+)/;
20938
20960
  const parseAnalysisResult = (message) => {
20939
20961
  const resultMatch = qualityGatePattern.exec(message);
@@ -20942,7 +20964,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20942
20964
  return;
20943
20965
  }
20944
20966
  return {
20945
- qualityGate: resultMatch[2],
20967
+ qualityGate: resultMatch[1] || resultMatch[2],
20946
20968
  issues: {
20947
20969
  blocker: Number.parseInt(issueMatch[1], 10),
20948
20970
  critical: Number.parseInt(issueMatch[2], 10),
@@ -20958,10 +20980,11 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20958
20980
  return;
20959
20981
  }
20960
20982
  const payload = event.properties;
20961
- if (payload?.id) {
20962
- currentSessionId = payload.id;
20983
+ const sessionId = payload?.info?.id;
20984
+ if (sessionId) {
20985
+ currentSessionId = sessionId;
20963
20986
  if (event.type === "session.created") {
20964
- mapSessionToDirectory(payload.id, effectiveDirectory);
20987
+ mapSessionToDirectory(sessionId, effectiveDirectory);
20965
20988
  }
20966
20989
  }
20967
20990
  };
@@ -20970,8 +20993,8 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
20970
20993
  return;
20971
20994
  }
20972
20995
  const payload = event.properties;
20973
- if (payload?.path && !shouldIgnoreFile2(payload.path)) {
20974
- hooks.fileEdited({ filePath: payload.path });
20996
+ if (payload?.file && !shouldIgnoreFile2(payload.file)) {
20997
+ hooks.fileEdited({ filePath: payload.file });
20975
20998
  }
20976
20999
  };
20977
21000
  const injectAnalysisResults = async (message, _config, sessionId) => {
@@ -21001,8 +21024,8 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
21001
21024
  if (!config3 || config3.level === "off" || !config3.autoAnalyze) {
21002
21025
  return;
21003
21026
  }
21004
- const editedFiles2 = getEditedFiles();
21005
- if (editedFiles2.length === 0) {
21027
+ const editedFiles = hooks.getEditedFiles?.() ?? getEditedFiles();
21028
+ if (editedFiles.length === 0) {
21006
21029
  return;
21007
21030
  }
21008
21031
  const message = await hooks.sessionIdle();
@@ -21013,7 +21036,7 @@ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarq
21013
21036
  if (parsedResult) {
21014
21037
  lastAnalysisResult = parsedResult;
21015
21038
  }
21016
- const passed = message.includes("[PASS]");
21039
+ const passed = message.includes("[PASS]") || message.includes("Quality Gate") && message.includes("OK");
21017
21040
  await showToast(passed ? "SonarQube: Quality Gate Passed" : "SonarQube: Issues Found", passed ? "success" : "error");
21018
21041
  await injectAnalysisResults(message, config3, currentSessionId);
21019
21042
  };
@@ -21112,7 +21135,7 @@ Git operation completed with changes. Consider running:
21112
21135
  const handleGitPush = async (command, outputData) => {
21113
21136
  if (!/git\s+push\b/.test(command))
21114
21137
  return;
21115
- if (outputData?.includes("error") || outputData?.includes("rejected"))
21138
+ if (outputData?.includes("error:") || outputData?.includes("fatal:") || outputData?.includes("rejected"))
21116
21139
  return;
21117
21140
  await showToast("Code pushed - SonarQube will analyze on server", "info");
21118
21141
  };
@@ -21171,16 +21194,27 @@ Git operation completed with changes. Consider running:
21171
21194
  safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
21172
21195
  const dir = sessionDir || effectiveDirectory;
21173
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
+ }
21174
21205
  await loadPluginConfig();
21175
21206
  const sonarConfig = pluginConfig?.["sonarqube"];
21176
21207
  const config3 = loadConfig(sonarConfig);
21177
21208
  if (!config3 || config3.level === "off") {
21178
21209
  safeLog(` config level is off or null, returning early`);
21210
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21179
21211
  return;
21180
21212
  }
21181
21213
  const state = await getProjectState(dir);
21182
- if (!state?.projectKey)
21214
+ if (!state?.projectKey) {
21215
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21183
21216
  return;
21217
+ }
21184
21218
  const api2 = createSonarQubeAPI(config3, state);
21185
21219
  const [qgStatus, counts, newCodeResponse] = await Promise.all([
21186
21220
  api2.qualityGate.getStatus(state.projectKey),
@@ -21196,6 +21230,7 @@ Git operation completed with changes. Consider running:
21196
21230
  const qgFailed = qgStatus.projectStatus.status !== "OK";
21197
21231
  const newCodeIssues = newCodeResponse.paging.total;
21198
21232
  if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21233
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21199
21234
  return;
21200
21235
  }
21201
21236
  const systemContext = `## SonarQube Code Quality Status
@@ -21212,6 +21247,7 @@ ${config3.level === "enterprise" ? `This project follows enterprise-level qualit
21212
21247
  - \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
21213
21248
  - \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
21214
21249
  - \`sonarqube({ action: "issues" })\` - See all issues`;
21250
+ transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21215
21251
  output.system.push(systemContext);
21216
21252
  }, "experimental.chat.system.transform"),
21217
21253
  tool: {
@@ -21270,6 +21306,7 @@ Example usage:
21270
21306
  if (parsedResult) {
21271
21307
  lastAnalysisResult = parsedResult;
21272
21308
  }
21309
+ transformCacheMap.clear();
21273
21310
  }
21274
21311
  return result;
21275
21312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",