opencode-sonarqube 0.1.23 → 0.2.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 (2) hide show
  1. package/dist/index.js +89 -155
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -17989,7 +17989,7 @@ class SourcesAPI {
17989
17989
  this.formatFileLocation(filePath, issue2.line),
17990
17990
  `**Rule:** \`${issue2.rule}\``
17991
17991
  ];
17992
- const hasSourceContext = issue2.sourceContext && issue2.sourceContext.lines.length > 0;
17992
+ const hasSourceContext = (issue2.sourceContext?.lines.length ?? 0) > 0;
17993
17993
  if (hasSourceContext) {
17994
17994
  lines.push(...this.formatSourceBlock(issue2.sourceContext, issue2.line));
17995
17995
  }
@@ -18041,7 +18041,7 @@ class DuplicationsAPI {
18041
18041
  });
18042
18042
  const duplicatedComponents = components.components.filter((c) => {
18043
18043
  const measure = c.measures.find((m) => m.metric === "duplicated_lines_density");
18044
- return measure && Number.parseFloat(measure.value) > 0;
18044
+ return measure ? Number.parseFloat(measure.value) > 0 : false;
18045
18045
  }).slice(0, limit);
18046
18046
  const allDuplications = [];
18047
18047
  for (const component of duplicatedComponents) {
@@ -19178,33 +19178,43 @@ function generateTokenName(projectKey) {
19178
19178
  const uuid3 = crypto.randomUUID().split("-")[0];
19179
19179
  return `opencode-${projectKey}-${timestamp}-${uuid3}`;
19180
19180
  }
19181
- async function bootstrap(options) {
19182
- let { config: config2, directory, force = false } = options;
19181
+ function isValidDirectory(dir) {
19182
+ return Boolean(dir && dir !== "/" && dir !== "." && dir.length >= 2);
19183
+ }
19184
+ function resolveDirectoryFromImportMeta() {
19183
19185
  try {
19184
- const { appendFileSync: appendFileSync4 } = await import("node:fs");
19185
- appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [BOOTSTRAP] Starting bootstrap directory=${directory} config.projectKey="${config2.projectKey || "(empty)"}"
19186
- `);
19187
- } catch {}
19188
- if (!directory || directory === "/" || directory === "." || directory.length < 2) {
19189
- try {
19190
- const pluginUrl = import.meta.url;
19191
- const pluginPath = decodeURIComponent(pluginUrl.replace("file://", ""));
19192
- const pathParts = pluginPath.split("/");
19193
- const nodeModulesIndex = pathParts.findIndex((p) => p === "node_modules");
19194
- if (nodeModulesIndex > 0) {
19195
- const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
19196
- if (projectPath && projectPath !== "/" && projectPath.length > 1) {
19197
- directory = projectPath;
19198
- try {
19199
- const { appendFileSync: appendFileSync4 } = await import("node:fs");
19200
- appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [BOOTSTRAP] Fixed directory from import.meta.url: ${directory}
19201
- `);
19202
- } catch {}
19203
- }
19186
+ const pluginUrl = import.meta.url;
19187
+ const pluginPath = decodeURIComponent(pluginUrl.replace("file://", ""));
19188
+ const pathParts = pluginPath.split("/");
19189
+ const nodeModulesIndex = pathParts.indexOf("node_modules");
19190
+ if (nodeModulesIndex > 0) {
19191
+ const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
19192
+ if (isValidDirectory(projectPath)) {
19193
+ return projectPath;
19204
19194
  }
19205
- } catch {}
19195
+ }
19196
+ } catch {}
19197
+ return null;
19198
+ }
19199
+ async function generateAnalysisToken(client, tokenName, projectKey) {
19200
+ try {
19201
+ return await client.post("/api/user_tokens/generate", { name: tokenName, type: "PROJECT_ANALYSIS_TOKEN", projectKey });
19202
+ } catch {
19203
+ logger5.warn("PROJECT_ANALYSIS_TOKEN not available, using GLOBAL_ANALYSIS_TOKEN");
19204
+ return await client.post("/api/user_tokens/generate", { name: tokenName, type: "GLOBAL_ANALYSIS_TOKEN" });
19206
19205
  }
19207
- if (!directory || directory === "/" || directory === "." || directory.length < 2) {
19206
+ }
19207
+ async function bootstrap(options) {
19208
+ let { config: config2, directory, force = false } = options;
19209
+ logger5.info("Starting bootstrap", { directory, projectKey: config2.projectKey || "(auto)" });
19210
+ if (!isValidDirectory(directory)) {
19211
+ const resolved = resolveDirectoryFromImportMeta();
19212
+ if (resolved) {
19213
+ directory = resolved;
19214
+ logger5.info("Resolved directory from import.meta.url", { directory });
19215
+ }
19216
+ }
19217
+ if (!isValidDirectory(directory)) {
19208
19218
  logger5.error("Invalid directory for bootstrap", { directory });
19209
19219
  return {
19210
19220
  success: false,
@@ -19212,21 +19222,20 @@ async function bootstrap(options) {
19212
19222
  projectToken: "",
19213
19223
  qualityGate: "",
19214
19224
  languages: [],
19215
- message: `Invalid directory: ${directory}. OpenCode may have passed the wrong working directory.`,
19225
+ message: `Invalid directory: ${directory}`,
19216
19226
  isNewProject: false
19217
19227
  };
19218
19228
  }
19219
- logger5.info("Starting SonarQube bootstrap", { directory });
19220
19229
  if (!force) {
19221
- const state2 = await loadProjectState(directory);
19222
- if (state2?.setupComplete) {
19223
- logger5.info("Project already bootstrapped", { projectKey: state2.projectKey });
19230
+ const existingState = await loadProjectState(directory);
19231
+ if (existingState?.setupComplete) {
19232
+ logger5.info("Project already bootstrapped", { projectKey: existingState.projectKey });
19224
19233
  return {
19225
19234
  success: true,
19226
- projectKey: state2.projectKey,
19227
- projectToken: state2.projectToken,
19228
- qualityGate: state2.qualityGate,
19229
- languages: state2.languages,
19235
+ projectKey: existingState.projectKey,
19236
+ projectToken: existingState.projectToken,
19237
+ qualityGate: existingState.qualityGate,
19238
+ languages: existingState.languages,
19230
19239
  message: "Project already configured",
19231
19240
  isNewProject: false
19232
19241
  };
@@ -19239,49 +19248,24 @@ async function bootstrap(options) {
19239
19248
  }
19240
19249
  logger5.info("Connected to SonarQube", { version: health.version });
19241
19250
  const detection = await detectProjectType(directory);
19242
- logger5.info("Detected project type", { languages: detection.languages });
19243
19251
  const projectKey = config2.projectKey || sanitizeProjectKey(await deriveProjectKey(directory));
19244
19252
  const projectName = config2.projectName || projectKey;
19245
- logger5.info("Project identification", { projectKey, projectName });
19246
19253
  const projectsApi = new ProjectsAPI(adminClient);
19247
- let isNewProject = false;
19248
19254
  const exists = await projectsApi.exists(projectKey);
19249
- if (exists) {
19250
- logger5.info("Project already exists on SonarQube", { projectKey });
19251
- } else {
19252
- logger5.info("Creating new project on SonarQube", { projectKey });
19253
- await projectsApi.create({
19254
- projectKey,
19255
- name: projectName,
19256
- visibility: "private"
19257
- });
19258
- isNewProject = true;
19255
+ const isNewProject = !exists;
19256
+ if (isNewProject) {
19257
+ await projectsApi.create({ projectKey, name: projectName, visibility: "private" });
19258
+ logger5.info("Created new project", { projectKey });
19259
19259
  }
19260
19260
  const tokenName = generateTokenName(projectKey);
19261
- logger5.info("Generating project token", { tokenName });
19262
- let tokenResponse;
19263
- try {
19264
- tokenResponse = await adminClient.post("/api/user_tokens/generate", {
19265
- name: tokenName,
19266
- type: "PROJECT_ANALYSIS_TOKEN",
19267
- projectKey
19268
- });
19269
- } catch {
19270
- logger5.warn("PROJECT_ANALYSIS_TOKEN not available, using GLOBAL_ANALYSIS_TOKEN");
19271
- tokenResponse = await adminClient.post("/api/user_tokens/generate", {
19272
- name: tokenName,
19273
- type: "GLOBAL_ANALYSIS_TOKEN"
19274
- });
19275
- }
19261
+ const tokenResponse = await generateAnalysisToken(adminClient, tokenName, projectKey);
19276
19262
  const qualityGateName = config2.qualityGate ?? QUALITY_GATE_MAPPING[config2.level];
19277
19263
  try {
19278
19264
  await setProjectQualityGate(adminClient, projectKey, qualityGateName);
19279
- logger5.info("Quality gate configured", { qualityGate: qualityGateName });
19280
19265
  } catch (error45) {
19281
19266
  logger5.warn("Failed to set quality gate", { error: String(error45) });
19282
19267
  }
19283
19268
  await configureProjectSettings(adminClient, projectKey, detection.languages, config2);
19284
- logger5.info("Project settings configured");
19285
19269
  const state = createInitialState({
19286
19270
  projectKey,
19287
19271
  projectToken: tokenResponse.token,
@@ -19298,7 +19282,7 @@ async function bootstrap(options) {
19298
19282
  projectToken: tokenResponse.token,
19299
19283
  qualityGate: qualityGateName,
19300
19284
  languages: detection.languages,
19301
- message: isNewProject ? `Created new project '${projectKey}' on SonarQube` : `Configured existing project '${projectKey}'`,
19285
+ message: isNewProject ? `Created new project '${projectKey}'` : `Configured existing project '${projectKey}'`,
19302
19286
  isNewProject
19303
19287
  };
19304
19288
  }
@@ -20112,26 +20096,6 @@ try {
20112
20096
  appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [LOAD] Plugin module loaded! CWD=${process.cwd()}
20113
20097
  `);
20114
20098
  } catch {}
20115
- var LOG_FILE4 = "/tmp/sonarqube-plugin-debug.log";
20116
- var debugLog = {
20117
- _write: (level, msg, extra) => {
20118
- const timestamp = new Date().toISOString();
20119
- const logLine = `${timestamp} [${level}] ${msg} ${extra ? JSON.stringify(extra) : ""}
20120
- `;
20121
- try {
20122
- appendFileSync4(LOG_FILE4, logLine);
20123
- } catch {}
20124
- },
20125
- info: (msg, extra) => {
20126
- debugLog._write("INFO", msg, extra);
20127
- },
20128
- warn: (msg, extra) => {
20129
- debugLog._write("WARN", msg, extra);
20130
- },
20131
- error: (msg, extra) => {
20132
- debugLog._write("ERROR", msg, extra);
20133
- }
20134
- };
20135
20099
  var IGNORED_FILE_PATTERNS2 = [
20136
20100
  /node_modules/,
20137
20101
  /\.git/,
@@ -20173,7 +20137,7 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20173
20137
  const pluginUrl = import.meta.url;
20174
20138
  const pluginPath = decodeURIComponent(pluginUrl.replace("file://", ""));
20175
20139
  const pathParts = pluginPath.split("/");
20176
- const nodeModulesIndex = pathParts.findIndex((p) => p === "node_modules");
20140
+ const nodeModulesIndex = pathParts.indexOf("node_modules");
20177
20141
  if (nodeModulesIndex > 0) {
20178
20142
  const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
20179
20143
  if (projectPath && projectPath !== "/" && projectPath.length > 1) {
@@ -20205,98 +20169,69 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
20205
20169
  const getConfig = () => pluginConfig;
20206
20170
  const getDirectory = () => effectiveDirectory;
20207
20171
  const loadPluginConfig = async () => {
20208
- debugLog.info("loadPluginConfig called", { hasExistingConfig: !!pluginConfig });
20209
- if (pluginConfig) {
20210
- debugLog.info("Config already loaded, skipping");
20172
+ if (pluginConfig)
20211
20173
  return;
20212
- }
20174
+ const dir = getDirectory();
20175
+ const sonarConfigPath = `${dir}/.sonarqube/config.json`;
20213
20176
  try {
20214
- const configPath = `${getDirectory()}/opencode.json`;
20215
- debugLog.info("Loading config from", { configPath });
20216
- const configFile = Bun.file(configPath);
20177
+ const configFile = Bun.file(sonarConfigPath);
20217
20178
  if (await configFile.exists()) {
20218
- pluginConfig = await configFile.json();
20219
- debugLog.info("Config loaded", { keys: Object.keys(pluginConfig ?? {}) });
20220
- } else {
20221
- debugLog.info("No opencode.json found");
20179
+ const config2 = await configFile.json();
20180
+ pluginConfig = { sonarqube: config2 };
20181
+ safeLog(`Config loaded from ${sonarConfigPath}`);
20182
+ return;
20222
20183
  }
20223
- } catch (error45) {
20224
- debugLog.warn("Config load error", { error: String(error45) });
20225
- }
20184
+ } catch {}
20185
+ pluginConfig = {};
20186
+ safeLog("Using environment variables for config");
20226
20187
  };
20227
20188
  const hooks = createHooks(getConfig, getDirectory);
20228
20189
  let currentSessionId;
20229
20190
  let initialCheckDone = false;
20191
+ const buildQualityNotification = (qgStatus, counts, qgFailed) => {
20192
+ const statusEmoji = qgFailed ? "[FAIL]" : "[WARN]";
20193
+ const blockerNote = counts.blocker > 0 ? `**Action Required:** There are BLOCKER issues that should be fixed.
20194
+ ` : "";
20195
+ return `## SonarQube Status: ${statusEmoji}
20196
+
20197
+ **Quality Gate:** ${qgStatus}
20198
+ **Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
20199
+
20200
+ ${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
20201
+ };
20230
20202
  const performInitialQualityCheck = async (sessionId) => {
20231
- debugLog.info("=== performInitialQualityCheck START ===", { sessionId, initialCheckDone });
20232
- if (initialCheckDone) {
20233
- debugLog.info("Initial check already done, skipping");
20203
+ if (initialCheckDone)
20234
20204
  return;
20235
- }
20236
20205
  initialCheckDone = true;
20237
20206
  try {
20238
20207
  await loadPluginConfig();
20239
20208
  const sonarConfig = pluginConfig?.["sonarqube"];
20240
- debugLog.info("Loading SonarQube config", { hasSonarConfig: !!sonarConfig });
20241
20209
  const config2 = loadConfig(sonarConfig);
20242
- debugLog.info("Config loaded", { hasConfig: !!config2, level: config2?.level });
20243
- if (!config2 || config2.level === "off") {
20244
- debugLog.info("Config missing or level=off, skipping");
20210
+ if (!config2 || config2.level === "off")
20245
20211
  return;
20246
- }
20247
20212
  const dir = getDirectory();
20248
- debugLog.info("Checking needsBootstrap", { directory: dir });
20249
- const needsBoot = await needsBootstrap(dir);
20250
- debugLog.info("needsBootstrap result", { needsBoot });
20251
- if (needsBoot) {
20252
- debugLog.info("Bootstrap needed, skipping initial check");
20213
+ if (await needsBootstrap(dir))
20253
20214
  return;
20254
- }
20255
- debugLog.info("Loading project state");
20256
20215
  const state = await getProjectState(dir);
20257
- debugLog.info("Project state loaded", {
20258
- hasState: !!state,
20259
- projectKey: state?.projectKey,
20260
- hasToken: !!state?.projectToken
20261
- });
20262
- if (!state || !state.projectKey) {
20263
- debugLog.info("No state or projectKey, skipping");
20216
+ if (!state?.projectKey)
20264
20217
  return;
20265
- }
20266
- debugLog.info("Creating API and fetching quality status", { projectKey: state.projectKey });
20267
20218
  const api2 = createSonarQubeAPI(config2, state);
20268
20219
  const [qgStatus, counts] = await Promise.all([
20269
20220
  api2.qualityGate.getStatus(state.projectKey),
20270
20221
  api2.issues.getCounts(state.projectKey)
20271
20222
  ]);
20272
- debugLog.info("Quality status fetched", { qgStatus: qgStatus.projectStatus.status, counts });
20273
20223
  const hasIssues = counts.blocker > 0 || counts.critical > 0 || counts.major > 0;
20274
20224
  const qgFailed = qgStatus.projectStatus.status !== "OK";
20275
- if (hasIssues || qgFailed) {
20276
- const statusEmoji = qgFailed ? "[FAIL]" : "[WARN]";
20277
- const notification = `## SonarQube Status: ${statusEmoji}
20278
-
20279
- **Quality Gate:** ${qgStatus.projectStatus.status}
20280
- **Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
20281
-
20282
- ${counts.blocker > 0 ? `**Action Required:** There are BLOCKER issues that should be fixed.
20283
- ` : ""}
20284
- Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
20285
- await client.session.prompt({
20286
- path: { id: sessionId },
20287
- body: {
20288
- noReply: true,
20289
- parts: [{ type: "text", text: notification }]
20290
- }
20291
- });
20292
- await showToast(qgFailed ? "SonarQube: Quality Gate Failed" : "SonarQube: Issues Found", qgFailed ? "error" : "info");
20293
- }
20225
+ if (!hasIssues && !qgFailed)
20226
+ return;
20227
+ const notification = buildQualityNotification(qgStatus.projectStatus.status, counts, qgFailed);
20228
+ await client.session.prompt({
20229
+ path: { id: sessionId },
20230
+ body: { noReply: true, parts: [{ type: "text", text: notification }] }
20231
+ });
20232
+ await showToast(qgFailed ? "SonarQube: Quality Gate Failed" : "SonarQube: Issues Found", qgFailed ? "error" : "info");
20294
20233
  } catch (error45) {
20295
- try {
20296
- const { appendFileSync: appendFileSync5 } = await import("node:fs");
20297
- appendFileSync5("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [ERROR] performInitialQualityCheck FAILED: ${error45}
20298
- `);
20299
- } catch {}
20234
+ safeLog(`performInitialQualityCheck error: ${error45}`);
20300
20235
  }
20301
20236
  };
20302
20237
  const showToast = async (message, variant = "info") => {
@@ -20391,7 +20326,7 @@ After fixing, I will re-run the analysis to verify.`;
20391
20326
  });
20392
20327
  if (config2.autoFix && lastAnalysisResult) {
20393
20328
  const state = await getProjectState(getDirectory());
20394
- if (state && state.projectKey) {
20329
+ if (state?.projectKey) {
20395
20330
  const api2 = createSonarQubeAPI(config2, state);
20396
20331
  const issues = await api2.issues.getFormattedIssues({
20397
20332
  projectKey: state.projectKey,
@@ -20462,7 +20397,7 @@ ${statusNote}`;
20462
20397
  }
20463
20398
  try {
20464
20399
  const state = await getProjectState(getDirectory());
20465
- if (!state || !state.projectKey)
20400
+ if (!state?.projectKey)
20466
20401
  return;
20467
20402
  const api2 = createSonarQubeAPI(config2, state);
20468
20403
  const counts = await api2.issues.getCounts(state.projectKey);
@@ -20625,9 +20560,8 @@ Git operation completed with changes. Consider running:
20625
20560
  }
20626
20561
  const dir = getDirectory();
20627
20562
  const state = await getProjectState(dir);
20628
- if (!state || !state.projectKey) {
20563
+ if (!state?.projectKey)
20629
20564
  return;
20630
- }
20631
20565
  const api2 = createSonarQubeAPI(config2, state);
20632
20566
  const [qgStatus, counts, newCodeResponse] = await Promise.all([
20633
20567
  api2.qualityGate.getStatus(state.projectKey),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "0.1.23",
3
+ "version": "0.2.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": "0.1.23",
41
+ "opencode-sonarqube": "0.2.1",
42
42
  "zod": "^3.24.0"
43
43
  },
44
44
  "devDependencies": {