← Files SonarQubeARCHIVED FILE

scripts/setup.js

4.34 KB · Oct 2, 2026 · 00:28 UTC

↓ Download file

#!/usr/bin/env node
"use strict";

const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");

const CLAUDE_INTEGRATION_ID = "claude-code";

const HOOK_DISPLAY_NAMES = {
  "sonar-secrets": "Secrets Detection",
  "sonar-sqaa": "Vortex analysis",
  "sonar-secrets-hooks": "Secrets Detection",
  "sonar-sqaa-hook": "Vortex analysis",
};

const DECLARATIVE_HOOK_FEATURE_IDS = new Set([
  "sonar-secrets-hooks",
  "sonar-sqaa-hook",
]);

// sonarqube-cli merged the standalone `sonar-sqaa-hook` container into the
// shared Vortex container for Claude; the analysis hook now shows up as this
// subfeature nested under `vortex-claude` instead of its own top-level id.
const CLAUDE_VORTEX_FEATURE_ID = "vortex-claude";
const VORTEX_ANALYSIS_SUBFEATURE_ID = "sqaa-posttooluse";

function hasMergedVortexAnalysisHook(feature) {
  return (
    feature?.featureId === CLAUDE_VORTEX_FEATURE_ID &&
    Array.isArray(feature.subfeatures) &&
    feature.subfeatures.some(
      (subfeature) => subfeature?.featureId === VORTEX_ANALYSIS_SUBFEATURE_ID
    )
  );
}

function hasSonarCli() {
  const envPath = process.env.PATH || "";
  const dirs = envPath.split(path.delimiter);
  const exts =
    process.platform === "win32"
      ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
      : [""];

  for (const dir of dirs) {
    for (const ext of exts) {
      try {
        fs.accessSync(path.join(dir, "sonar" + ext), fs.constants.F_OK);
        return true;
      } catch {
        // not in this directory
      }
    }
  }
  return false;
}

function readState() {
  const statePath = path.join(
    os.homedir(),
    ".sonar",
    "sonarqube-cli",
    "state.json"
  );
  try {
    return JSON.parse(fs.readFileSync(statePath, "utf8"));
  } catch {
    return null;
  }
}

function canonicalPath(p) {
  try {
    return fs.realpathSync.native(p);
  } catch {
    return path.resolve(p);
  }
}

function samePath(a, b) {
  if (!a || !b) return false;
  const caseInsensitive =
    process.platform === "darwin" || process.platform === "win32";
  const norm = (p) => {
    const c = canonicalPath(p);
    return caseInsensitive ? c.toLowerCase() : c;
  };
  return norm(a) === norm(b);
}

function featureAppliesToCwd(feature, cwd) {
  if (feature?.scope === "global") {
    return true;
  }
  if (feature?.scope === "project") {
    return samePath(cwd, feature.targetRoot);
  }
  return false;
}

function collectLegacyHookNames(state, cwd) {
  const claudeHooks = (state?.agentExtensions ?? []).filter(
    (entry) =>
      entry?.agentId === CLAUDE_INTEGRATION_ID &&
      entry.kind === "hook" &&
      typeof entry.name === "string"
  );
  const globalHooks = claudeHooks.filter((entry) => entry.global === true);
  const localHooks = claudeHooks.filter((entry) =>
    samePath(cwd, entry.projectRoot)
  );
  return [...globalHooks, ...localHooks].map((entry) => entry.name);
}

function collectDeclarativeHookNames(state, cwd) {
  const claudeIntegration = (state?.integrations?.installed ?? []).find(
    (entry) => entry?.integrationId === CLAUDE_INTEGRATION_ID
  );
  if (!claudeIntegration) {
    return [];
  }

  return (claudeIntegration.features ?? [])
    .filter((feature) => featureAppliesToCwd(feature, cwd))
    .filter(
      (feature) =>
        (typeof feature?.featureId === "string" &&
          DECLARATIVE_HOOK_FEATURE_IDS.has(feature.featureId)) ||
        hasMergedVortexAnalysisHook(feature)
    )
    .map((feature) =>
      hasMergedVortexAnalysisHook(feature) ? "sonar-sqaa-hook" : feature.featureId
    );
}

function collectInstalledHookNames(state, cwd) {
  const names = [
    ...collectLegacyHookNames(state, cwd),
    ...collectDeclarativeHookNames(state, cwd),
  ];
  return new Set(names);
}

const cwd = process.cwd();
const state = readState();
const sonarOk = hasSonarCli();
const installed = collectInstalledHookNames(state, cwd);

const hookList = Array.from(
  new Set(Array.from(installed).map((name) => HOOK_DISPLAY_NAMES[name] || name))
).join(", ");

const lines = [
  "SonarQube plugin initialised.",
  "  sonarqube-cli: " +
    (sonarOk ? "✓ found" : "✗ not found — run /sonarqube:sonar-integrate"),
  "  SonarQube hooks: " +
    (installed.size > 0
      ? "✓ " + hookList
      : "✗ no hooks installed — run /sonarqube:sonar-integrate"),
];

process.stdout.write(JSON.stringify({ systemMessage: lines.join("\n") }) + "\n");
process.exit(0);

SHA-256: b96ca90e1ee30db9b629224418c9345abe5a56f1496f026dcca321a641bd49d0