From b419b8365d7402d81632f6c984425922a39f2427 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 23 May 2026 06:19:03 +0000 Subject: [PATCH 1/2] feat(plugin): config-driven launcher; refresh docs; NOTICE third-party licenses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on the marketplace restructure (#1). Changes: 1. Externalize launcher knobs into plugin/skills/sonar-predictor/config.env - SONAR_MAVEN_REPO_URL Maven proxy where the analyzer bundle is fetched - SONAR_BUNDLE_VERSION Bundle version pin (default: 0.1.1) - SONAR_MIN_JAVA_VERSION Minimum Java major required (default: 17) - SONAR_JRE_URL_TEMPLATE JRE source for auto-download; tokens {os} {arch} {version} are substituted (default: Adoptium API) - SONAR_JRE_VERSION JRE version to fetch (default: 17) - SONAR_DISABLE_JRE_AUTODOWNLOAD Set to 1 to refuse the JRE auto-download Plain KEY=VALUE format parsed by both bash and Windows launchers. Env vars of the same name take precedence over the file (one-off override without editing). Forking workflow: clone, edit config.env, push, install. 2. Bash launcher gains a 'no Java anywhere -> download a JRE' step that runs after the bundle is cached. Searches $JAVA_HOME, PATH, and the same common install dirs the bundle's own launcher probes; if nothing meets the minimum, substitutes {os}/{arch}/{version} into SONAR_JRE_URL_TEMPLATE, downloads, extracts to ~/.cache/sonar-predictor/jre//, and exports JAVA_HOME for the bundle launcher to pick up. Windows launcher reads the same config.env but still requires Java 17+ on the system (or in JAVA_HOME). Auto-install on Windows is deferred. 3. Refresh README.md and dist/README.md for the marketplace install path: - Quick-start is now the two-command marketplace install (Claude + Copilot) - Add a 'Corporate / air-gapped setup' section documenting config.env and SONAR_PREDICTOR_HOME for fully pre-staged installs - Expand the License section into 'License & third-party components' with the SonarSource analyzer license situation called out honestly 4. Add NOTICE listing third-party components and their licenses: - sonarlint-analysis-engine — LGPL v3 - SonarSource language analyzers — SONAR Source-Available License v1.0 (SSALv1) since SonarSource's 2024 relicense - Apache 2.0 build / runtime dependencies (picocli, jackson, maven plugins) - Adoptium Temurin JRE — GPL-2.0 WITH Classpath-exception-2.0 NOTICE also documents the future direction: have the bootstrap fetch each analyzer JAR individually from Maven Central instead of bundling them into the dist artifact. That eliminates any SSALv1 redistribution question (SonarSource's own Maven Central distribution becomes the sole channel). The plugin's config.env already isolates the Maven URL, so the transition is transparent to corporate proxies. Smoke-tested end to end: - default config: bundle from Maven Central, system Java used. - SONAR_MAVEN_REPO_URL=bogus env override correctly steered the download (failed at DNS as expected) — env-var precedence confirmed. - SONAR_MIN_JAVA_VERSION=99 forced the JRE auto-install path; Adoptium Temurin 17.0.19 downloaded and ran. - SONAR_DISABLE_JRE_AUTODOWNLOAD=1 blocked the install with the right error. Co-Authored-By: Claude Opus 4.7 --- NOTICE | 99 +++++++++ README.md | 50 +++-- dist/README.md | 76 +++---- plugin/skills/sonar-predictor/SKILL.md | 2 +- plugin/skills/sonar-predictor/bin/sonar | 211 +++++++++++++++++--- plugin/skills/sonar-predictor/bin/sonar.bat | 38 ++-- plugin/skills/sonar-predictor/config.env | 46 +++++ 7 files changed, 415 insertions(+), 107 deletions(-) create mode 100644 NOTICE create mode 100644 plugin/skills/sonar-predictor/config.env diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..bd90c07 --- /dev/null +++ b/NOTICE @@ -0,0 +1,99 @@ +sonar-predictor +Copyright 2025–2026 Amit Kumar and the sonar-predict contributors + +This product is licensed under the Apache License, Version 2.0 (see LICENSE). + +================================================================================ +THIRD-PARTY COMPONENTS +================================================================================ + +`sonar-predictor` invokes third-party analyzers and libraries at runtime. The +following components are NOT redistributed inside this repository (the +installable plugin) — the launcher downloads them on first invocation from +Maven Central. They are listed here for transparency about what your machine +runs after you install this plugin. + +------------------------------------------------------------------------------ +SonarSource analysis engine +------------------------------------------------------------------------------ + + org.sonarsource.sonarlint.core:sonarlint-analysis-engine + License: GNU Lesser General Public License v3.0 (LGPL-3.0) + Source : https://github.com/SonarSource/sonarlint-core + + The embedded analysis runtime — the same engine that powers SonarLint in + IDEs. The CLI and daemon link to it as a normal library dependency, which + is the use LGPL-3.0 is specifically designed to allow under any combining + license (including Apache 2.0). + +------------------------------------------------------------------------------ +SonarSource language analyzers +------------------------------------------------------------------------------ + + org.sonarsource.java :sonar-java-plugin + org.sonarsource.python :sonar-python-plugin + org.sonarsource.javascript:sonar-javascript-plugin + org.sonarsource.php :sonar-php-plugin + org.sonarsource.kotlin :sonar-kotlin-plugin + org.sonarsource.slang :sonar-go-plugin + org.sonarsource.slang :sonar-ruby-plugin + org.sonarsource.slang :sonar-scala-plugin + org.sonarsource.html :sonar-html-plugin + org.sonarsource.xml :sonar-xml-plugin + + License: SONAR Source-Available License v1.0 (SSALv1) + Text : https://www.sonarsource.com/license/ssal/ + + These are the per-language rule packs the analysis engine loads at runtime. + SonarSource relicensed them from LGPL v3 to SSALv1 in 2024. SSALv1 is + "source-available": it permits free internal use and free local execution + (what this tool does), and restricts using the analyzers to operate a + competing "Service Offering". Read the full license text at the link above + before using `sonar-predictor` to build a hosted service offering. + + No source code from these analyzers is included in this repository. + `sonar-predictor` invokes them only through the public SonarSource APIs + exposed by sonarlint-analysis-engine. + +------------------------------------------------------------------------------ +Build-time / direct-dependency Apache 2.0 components +------------------------------------------------------------------------------ + + info.picocli:picocli (CLI framework) + com.fasterxml.jackson.core:jackson-* (JSON serialization) + org.apache.maven.plugins:maven-* and the embedded + build-helper / central-publishing-maven-plugin (build tooling) + + All Apache License 2.0. + +------------------------------------------------------------------------------ +Runtime JRE auto-download (optional) +------------------------------------------------------------------------------ + + When no Java 17+ runtime is found on the user's machine, the plugin's + bootstrap launcher fetches a JRE from the URL configured in + `plugin/skills/sonar-predictor/config.env`. The public default uses the + Adoptium Temurin API (https://api.adoptium.net), which serves Eclipse + Temurin OpenJDK builds under the GNU General Public License v2 with the + Classpath Exception (GPL-2.0 WITH Classpath-exception-2.0). + + An air-gapped or corporate setup can replace this URL with a private JRE + mirror; the bootstrap does not require Adoptium specifically. + +================================================================================ +NOTES ON THE BUNDLED MAVEN CENTRAL ARTIFACT +================================================================================ + +The launcher currently downloads a single archive, +`sonar-predictor-dist-.zip`, from Maven Central. That archive (built +and published by this project's release workflow) contains the SonarSource +analyzer JARs alongside this project's own CLI and daemon jars, repackaged +unmodified. + +A future release will change the launcher to fetch each analyzer JAR +individually from Maven Central using its published coordinates, so that +SonarSource's own Maven Central distribution is the sole redistribution +channel and `sonar-predict-dist-*.zip` no longer carries third-party JARs. +This eliminates any redistribution question about the SSALv1-licensed +analyzers. The plugin's `config.env` already isolates the Maven repository +URL, so this transition is transparent to corporate Maven proxy setups. diff --git a/README.md b/README.md index ddac2bf..22ceb01 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,16 @@ It is **scan-only** — it reads your source and reports findings, and never mod ## Quick start -Grab the self-contained bundle from the [latest release](https://github.com/RandomCodeSpace/sonar-predict/releases/latest) — it carries the CLI, the daemon and all 12 analyzers: +`sonar-predictor` installs as a Claude Code / GitHub Copilot CLI plugin from this repo's built-in marketplace. The plugin is tiny (kilobytes); the analyzer bundle is fetched lazily from Maven Central on first scan and cached locally for every subsequent run. -```sh -curl -L -o sonar-predictor.zip \ - https://github.com/RandomCodeSpace/sonar-predict/releases/download/v0.1.0/sonar-predict-skill-0.1.0.zip -unzip sonar-predictor.zip -d sonar-predictor - -./sonar-predictor/bin/sonar check src/Main.java +``` +/plugin marketplace add RandomCodeSpace/sonar-predict +/plugin install sonar-predictor@sonar-predict ``` -The launcher auto-discovers a Java 17+ runtime and starts the daemon on first use — no configuration. +The same two commands work on Claude Code and Copilot CLI — they share the `.claude-plugin` format. Two named scanner subagents come with the plugin: `sonar-scanner-claude` (model: haiku) and `sonar-scanner-copilot` (model: gpt-5-mini); selection is by agent name. + +Prefer the raw CLI? Grab the skill bundle directly from the [latest release](https://github.com/RandomCodeSpace/sonar-predict/releases/latest) and run `./bin/sonar` from the unpacked directory. The first invocation auto-discovers Java 17+ (or downloads one if none is found); every subsequent call is offline. ## Usage @@ -77,18 +76,26 @@ sonar --format json check --diff sonar --coverage target/site/jacoco/jacoco.xml --coverage-min 80 analyze . ``` -## Use it as an AI-agent skill +## Corporate / air-gapped setup + +The plugin's launcher reads a single configuration file at `plugin/skills/sonar-predictor/config.env`. Edit it (or set the same-named environment variable, which takes precedence) to point at a corporate Maven proxy, a private JRE mirror, or a vendored bundle version — no code changes. -`sonar-predictor` is packaged as a [Claude Code](https://claude.com/claude-code) skill — an -agent discovers it and runs it as a quality gate automatically. Install the bundle into your -skills directory: +| Key | What it controls | +|-----|------------------| +| `SONAR_MAVEN_REPO_URL` | Where the analyzer bundle is downloaded from. Default: Maven Central. | +| `SONAR_BUNDLE_VERSION` | Bundle version pin. Bump in lockstep with each plugin release. | +| `SONAR_MIN_JAVA_VERSION` | Minimum Java major version required. Default: `17`. | +| `SONAR_JRE_URL_TEMPLATE` | JRE source for auto-download (when none is on the system). Tokens `{os}` `{arch}` `{version}` are substituted at runtime. Default: Adoptium Temurin API. | +| `SONAR_JRE_VERSION` | JRE version to fetch. Default: `17`. | +| `SONAR_DISABLE_JRE_AUTODOWNLOAD` | Set to `1` to refuse the JRE auto-download (corp-policy escape hatch). | + +For a fully pre-staged install — no network at all on the developer machine — extract a skill bundle by hand into a known location and point the launcher at it: ```sh -unzip sonar-predict-skill-0.1.0.zip -d ~/.claude/skills/ +export SONAR_PREDICTOR_HOME=/opt/sonar-predictor ``` -The bundled `SKILL.md` tells the agent when to run it and how to read its output; the CLI's -own `--help` is the single source of truth for commands. +The forking workflow: clone this repo, edit `plugin/skills/sonar-predictor/config.env`, push to your fork, `/plugin marketplace add /sonar-predict`. One file. ## How it works @@ -134,6 +141,15 @@ smells, vulnerabilities and security hotspots — and imports coverage. It is a first-pass, **not** a replacement for release-time gates: the SonarQube server's deep cross-file taint analysis, Fortify, and dependency/CVE scanning remain their own tools. -## License +## License & third-party components + +This project (the CLI, daemon, protocol, dist module, plugin scaffolding) is licensed under the [Apache License 2.0](LICENSE). + +The SonarSource analyzers and engine that `sonar-predictor` invokes at runtime are **third-party components with their own licenses**, listed in [NOTICE](NOTICE). Headline points: + +- **`sonarlint-analysis-engine`** (the embedded analysis runtime, by SonarSource) — **LGPL v3**. +- **SonarSource language analyzers** (`sonar-java`, `sonar-python`, `sonar-javascript`, `sonar-php`, `sonar-kotlin`, `sonar-go`, `sonar-ruby`, `sonar-scala`, `sonar-html`, `sonar-xml`) — **SONAR Source-Available License v1.0 (SSALv1)** since SonarSource's 2024 license change. Source-available, free for internal use, with restrictions on operating a competing "Service Offering". + +The plugin **does not redistribute** these JARs — its launcher fetches them from Maven Central on first invocation (the same channel SonarSource themselves publish to). Editing `SONAR_MAVEN_REPO_URL` to a corporate mirror that proxies Maven Central is equivalent. None of this project's source code is derived from SonarSource code. -[Apache License 2.0](LICENSE). +For a deeper inventory and the legal text of each license, see [NOTICE](NOTICE). If your environment's policy needs the analyzer JARs to come from somewhere other than Maven Central, the corporate / air-gapped setup above is your knob. diff --git a/dist/README.md b/dist/README.md index c8d8f58..66ba386 100644 --- a/dist/README.md +++ b/dist/README.md @@ -1,71 +1,61 @@ # sonar-predictor :: dist -The distribution module. It builds **sonar-predictor** as a self-contained -Claude Code agent skill — a directory an AI agent can use with zero setup. +The distribution module. It assembles the **skill bundle** that `sonar-predictor`'s plugin launcher downloads at first run. -## Build - -From the repository root: +## What this module builds ```sh mvn package ``` -This produces, under `dist/target/`: +produces, under `dist/target/`: - `skill/sonar-predictor/` — the exploded skill bundle: - - `SKILL.md` — the agent-facing skill definition (the only file loaded into context) + - `SKILL.md` — the agent-facing skill definition - `bin/sonar`, `bin/sonar.bat` — launchers that auto-discover a Java 17+ runtime - - `lib/` — the CLI and daemon fat jars + - `lib/` — the CLI and daemon shaded fat jars - `plugins/` — the 10 SonarSource analyzer plugins -- `sonar-predict-skill-.zip` — the same tree zipped, for non-skill use - (manual install, or transfer to an air-gapped host) +- `sonar-predict-skill-.zip` — the same tree zipped, attached to the GitHub Release and **published to Maven Central as `io.github.randomcodespace.sonarpredict:sonar-predictor-dist::zip`**. The plugin's bootstrap launcher pulls this artifact (from Maven Central, or a corporate proxy of it) on first invocation. -The analyzer plugins are fetched through Maven, so any mirror, Nexus, or -corporate proxy configured in your `settings.xml` is honored — there are no -hardcoded download URLs. +The analyzer plugins are resolved through Maven, so any mirror, Nexus, or corporate proxy configured in your `settings.xml` is honored — there are no hardcoded download URLs in the build. -## Install as a Claude Code skill +## How users install `sonar-predictor` -```sh -cp -r dist/target/skill/sonar-predictor ~/.claude/skills/ +End users do not interact with this module directly. They install the plugin from the in-repo `/plugin/` directory through their AI tool's marketplace: + +``` +/plugin marketplace add RandomCodeSpace/sonar-predict +/plugin install sonar-predictor@sonar-predict ``` -The skill is then available to agents in any project. +The plugin's launcher (`/plugin/skills/sonar-predictor/bin/sonar`) downloads the bundle this module builds from Maven Central, verifies its SHA-1, caches it under `~/.cache/sonar-predictor//`, and execs the cached `bin/sonar` on every subsequent call. -## Prerequisites +For raw / non-plugin use (manual or air-gapped install), the same skill bundle zip is attached to each GitHub Release. -The skill runs on a system Java; nothing is downloaded at runtime. +## Prerequisites at runtime **Required:** -- **Java 17+** (JDK or JRE) — the CLI and daemon are JVM processes. `bin/sonar` auto-discovers it (`JAVA_HOME` → `PATH` → common install locations); one must exist somewhere on the machine. +- **Java 17+** (JDK or JRE) — the CLI and daemon are JVM processes. `bin/sonar` auto-discovers it (`JAVA_HOME` → `PATH` → common install locations); the plugin's bootstrap also auto-downloads one from `SONAR_JRE_URL_TEMPLATE` (defaulting to Adoptium Temurin) if none is found. - **Linux or macOS** — the daemon uses Unix domain sockets. Windows is not yet supported (the TCP-socket fallback is a TODO). - A writable temp directory and ~1 GB free RAM (the daemon embeds the analysis engine; idle ~150–350 MB, up to ~1 GB during JS/TS analysis). - ~165 MB disk for the bundle. **For specific features:** -- **`git`** on `PATH` — required for `sonar check --diff` (the primary agent workflow). `sonar check ` and `sonar analyze ` work without it. -- **Node.js 18.17+** on `PATH` — required for JavaScript/TypeScript/CSS analysis. Without it those 3 languages are skipped (the other 9 still analyze). +- **`git`** on `PATH` — required for `sonar check --diff` (the primary agent workflow). +- **Node.js 18.17+** on `PATH` — required for JavaScript/TypeScript/CSS analysis. Without it those 3 languages are skipped. -**Not required:** no network at runtime (fully offline after install), no per-language SDK or compiler, no Maven (build-time only). - -If no Java 17+ is found, the `bin/sonar` launcher exits with a clear message. +**Not required after first invocation:** no network, no per-language SDK or compiler, no Maven. ## Releasing -A release is cut by pushing a version tag. The -`.github/workflows/publish.yml` GitHub Actions pipeline then: +A release is cut by pushing a version tag. The `.github/workflows/publish.yml` GitHub Actions pipeline then: -1. derives the release version from the tag (`v0.1.0` → `0.1.0`) and strips - `-SNAPSHOT` — Maven Central rejects snapshot versions, -2. builds, tests, GPG-signs and deploys the library modules - `protocol`, `daemon` and `cli` to Maven Central via the Sonatype - Central Portal (this `dist` module sets `maven.deploy.skip=true`, so it - is built but never staged to Central), -3. creates a GitHub Release carrying two bundles: - - **`sonar-predict--src.zip`** — the whole repository as a - `git archive` of `HEAD`, - - **`sonar-predict-skill-.zip`** — the assembled skill bundle. +1. derives the release version from the tag (`v0.1.0` → `0.1.0`) and strips `-SNAPSHOT`, +2. builds, tests, GPG-signs and deploys the library modules `protocol`, `daemon` and `cli` to Maven Central via the Sonatype Central Portal, +3. publishes this `dist` module's skill bundle zip to Maven Central as `sonar-predictor-dist::zip` (this is the artifact the plugin's bootstrap downloads), +4. creates a GitHub Release carrying two bundles: + - `sonar-predict--src.zip` — the whole repository as a `git archive` of `HEAD`, + - `sonar-predict-skill-.zip` — the assembled skill bundle. To cut release `X.Y.Z`: @@ -74,13 +64,11 @@ git tag vX.Y.Z git push origin vX.Y.Z ``` -The workflow can also be run manually from the **Actions** tab -(`workflow_dispatch`), supplying the version explicitly. +When the release is cut, bump the plugin launcher's pinned bundle version in `plugin/skills/sonar-predictor/config.env` (`SONAR_BUNDLE_VERSION`) in the same commit, so a freshly installed plugin downloads the bundle that matches it. -### Required repository secrets +The workflow can also be run manually from the **Actions** tab (`workflow_dispatch`), supplying the version explicitly. -The pipeline needs these secrets on the `RandomCodeSpace/sonar-predict` -repository (Settings → Secrets and variables → Actions): +### Required repository secrets | Secret | Purpose | |--------|---------| @@ -89,6 +77,4 @@ repository (Settings → Secrets and variables → Actions): | `MAVEN_GPG_PRIVATE_KEY` | ASCII-armored GPG private key used to sign artifacts | | `MAVEN_GPG_PASSPHRASE` | Passphrase for that GPG key (omit if the key has none) | -`GITHUB_TOKEN` is provided automatically and needs no setup. The -`io.github.randomcodespace` namespace must also be registered and verified -on the Sonatype Central Portal. +`GITHUB_TOKEN` is provided automatically. The `io.github.randomcodespace` namespace must be registered and verified on the Sonatype Central Portal. diff --git a/plugin/skills/sonar-predictor/SKILL.md b/plugin/skills/sonar-predictor/SKILL.md index ecfd2ea..773be2d 100644 --- a/plugin/skills/sonar-predictor/SKILL.md +++ b/plugin/skills/sonar-predictor/SKILL.md @@ -15,6 +15,6 @@ Exit codes: `0` clean, `1` issues found, `2` tool error. Acting on findings: fix `BUG`/`VULNERABILITY`/`SECURITY_HOTSPOT` and `CRITICAL`/`MAJOR` first. This is a fast first-pass gate, not the release gate — fix the real issues and move on. -**Air-gapped / pre-staged installs.** Set `SONAR_PREDICTOR_HOME=/path/to/extracted/sonar-predictor` to point the launcher at a pre-downloaded bundle and skip the first-run download. +**Configuration / corporate environments.** The launcher reads `config.env` next to this `SKILL.md` — Maven proxy URL, JRE download URL, bundle and Java version pins. Edit it (or override with same-named env vars) to point at a corporate Nexus / Artifactory mirror and a private JRE source. The defaults use Maven Central and Adoptium Temurin's public API. `SONAR_PREDICTOR_HOME=/path/to/extracted/sonar-predictor` skips the bundle download entirely for fully air-gapped / pre-staged installs. **Plugin-bundled agent variants.** Two named scanner subagents ship with this plugin: invoke `sonar-scanner-claude` on Claude Code (model: haiku) or `sonar-scanner-copilot` on GitHub Copilot CLI (model: gpt-5-mini). Selection is by agent name — pick the one matching your platform. diff --git a/plugin/skills/sonar-predictor/bin/sonar b/plugin/skills/sonar-predictor/bin/sonar index 077e7b7..342febc 100755 --- a/plugin/skills/sonar-predictor/bin/sonar +++ b/plugin/skills/sonar-predictor/bin/sonar @@ -1,37 +1,74 @@ #!/usr/bin/env bash # Bootstrap wrapper for the sonar-predictor skill bundle. # -# On first invocation, downloads the analyzer bundle (~150 MB) from Maven -# Central into the user cache, verifies its SHA-1, and unpacks it. Every -# subsequent call exec's the cached launcher directly — no network. +# On first invocation: +# 1. loads ../config.env (corporate Maven proxy, JRE mirror, version pin), +# 2. downloads sonar-predictor-dist-.zip from SONAR_MAVEN_REPO_URL, +# verifies its SHA-1 sidecar, and unpacks it into the user cache, +# 3. ensures a Java >= SONAR_MIN_JAVA_VERSION is available — downloading +# one from SONAR_JRE_URL_TEMPLATE into the user cache if not found +# (the bundle's own launcher already searches JAVA_HOME / PATH / common +# install dirs; this only kicks in when that comes up empty), +# 4. exec's the cached launcher. +# Every subsequent call exec's the cached launcher directly with no network. # -# Override the bundle location for air-gapped or pre-staged installs: +# Override the bundle location for air-gapped / pre-staged installs: # SONAR_PREDICTOR_HOME=/path/to/extracted/sonar-predictor +# +# All other knobs (Maven proxy, JRE source, version pin, min Java) live in +# ../config.env; each one is also an env-var override. set -euo pipefail -# Pinned to the bundle version published to Maven Central. Bumped in lockstep -# with the plugin's release: each tagged plugin release publishes a matching -# sonar-predictor-dist-.zip and updates this pin. -# -# Note: v0.1.2's dist artifact on Central is the (now-removed) plugin-bundle -# shape, not the skill-bundle this wrapper expects. v0.1.1 is the last good -# skill bundle; v0.1.3 onwards will be skill-shaped again. -VERSION="0.1.1" +# -------- 1. load config ----------------------------------------------------- + +HERE="$(cd "$(dirname "$0")" && pwd)" +CONFIG="$HERE/../config.env" + +if [ -f "$CONFIG" ]; then + # Parse KEY=VALUE lines; env vars already in scope take precedence. + while IFS='=' read -r key value; do + case "$key" in + ''|'#'*) continue ;; + esac + key="${key%%[[:space:]]*}" + if [ -z "${!key:-}" ]; then + printf -v "$key" '%s' "$value" + export "$key" + fi + done < "$CONFIG" +fi +# Defaults if config.env is missing or empty. +: "${SONAR_MAVEN_REPO_URL:=https://repo1.maven.org/maven2}" +: "${SONAR_BUNDLE_VERSION:=0.1.1}" +: "${SONAR_MIN_JAVA_VERSION:=17}" +: "${SONAR_JRE_URL_TEMPLATE:=https://api.adoptium.net/v3/binary/latest/{version}/ga/{os}/{arch}/jre/hotspot/normal/eclipse}" +: "${SONAR_JRE_VERSION:=17}" +: "${SONAR_DISABLE_JRE_AUTODOWNLOAD:=}" + +VERSION="$SONAR_BUNDLE_VERSION" GROUP_PATH="io/github/randomcodespace/sonarpredict" ARTIFACT="sonar-predictor-dist" -BASE_URL="https://repo1.maven.org/maven2/${GROUP_PATH}/${ARTIFACT}/${VERSION}" +BUNDLE_URL_BASE="${SONAR_MAVEN_REPO_URL%/}/${GROUP_PATH}/${ARTIFACT}/${VERSION}" +CACHE_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/sonar-predictor" if [ -n "${SONAR_PREDICTOR_HOME:-}" ]; then SKILL_DIR="$SONAR_PREDICTOR_HOME" else - CACHE_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/sonar-predictor" SKILL_DIR="$CACHE_ROOT/$VERSION" fi - REAL_SONAR="$SKILL_DIR/bin/sonar" +# -------- helpers ------------------------------------------------------------ + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "sonar-predictor: required command '$1' not found on PATH" >&2 + exit 2 + fi +} + sha1_of() { if command -v sha1sum >/dev/null 2>&1; then sha1sum "$1" | awk '{print $1}' @@ -43,41 +80,138 @@ sha1_of() { fi } -if [ ! -x "$REAL_SONAR" ]; then - for cmd in curl unzip; do - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "sonar-predictor: required command '$cmd' not found on PATH" >&2 - exit 2 +java_major_of() { + local v raw maj + v="$("$1" -version 2>&1 | head -1)" || return 1 + raw="$(printf '%s' "$v" | sed -E 's/^[^"]*"([^"]+)".*/\1/')" + [ -n "$raw" ] || return 1 + maj="$(printf '%s' "$raw" | cut -d. -f1)" + if [ "$maj" = "1" ]; then + maj="$(printf '%s' "$raw" | cut -d. -f2)" + fi + printf '%s' "$maj" +} + +found_java_meets_min() { + # The bundle's own launcher searches JAVA_HOME, PATH and common dirs. We + # only need to know whether *some* working Java >= the minimum exists; if + # so, we hand off to it. Otherwise we install one and set JAVA_HOME so the + # bundle's launcher picks it up. + local cand major + for cand in \ + "${JAVA_HOME:-}/bin/java" \ + "$(command -v java 2>/dev/null || true)" \ + /usr/lib/jvm/*/bin/java \ + /usr/java/*/bin/java \ + /Library/Java/JavaVirtualMachines/*/Contents/Home/bin/java \ + "${HOME:-/nonexistent}"/.sdkman/candidates/java/*/bin/java \ + /opt/java/*/bin/java \ + /opt/*/bin/java + do + [ -x "$cand" ] || continue + major="$(java_major_of "$cand" 2>/dev/null || true)" + [ -n "$major" ] || continue + if [ "$major" -ge "$SONAR_MIN_JAVA_VERSION" ] 2>/dev/null; then + return 0 fi done + return 1 +} + +detect_os_arch() { + case "$(uname -s)" in + Linux*) OS=linux ;; + Darwin*) OS=mac ;; + *) echo "sonar-predictor: unsupported OS '$(uname -s)' for JRE auto-download" >&2; exit 2 ;; + esac + case "$(uname -m)" in + x86_64|amd64) ARCH=x64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "sonar-predictor: unsupported arch '$(uname -m)' for JRE auto-download" >&2; exit 2 ;; + esac +} + +install_jre() { + local jre_dir="$CACHE_ROOT/jre/$SONAR_JRE_VERSION" + if [ -x "$jre_dir/bin/java" ]; then + export JAVA_HOME="$jre_dir" + return 0 + fi - echo "sonar-predictor: first run — downloading $VERSION bundle from Maven Central..." >&2 + case "${SONAR_DISABLE_JRE_AUTODOWNLOAD:-}" in + 1|true|yes|TRUE|YES) + echo "sonar-predictor: no Java >= $SONAR_MIN_JAVA_VERSION found, and SONAR_DISABLE_JRE_AUTODOWNLOAD is set" >&2 + exit 2 + ;; + esac + + require_cmd curl + require_cmd tar + detect_os_arch + + local url="$SONAR_JRE_URL_TEMPLATE" + url="${url//\{os\}/$OS}" + url="${url//\{arch\}/$ARCH}" + url="${url//\{version\}/$SONAR_JRE_VERSION}" + + echo "sonar-predictor: no Java $SONAR_MIN_JAVA_VERSION+ found — downloading JRE $SONAR_JRE_VERSION ($OS/$ARCH) from $url" >&2 + mkdir -p "$(dirname "$jre_dir")" + + local tmp; tmp="$(mktemp -d)" + curl -fsSL --retry 3 -o "$tmp/jre.tgz" "$url" + ( cd "$tmp" && tar -xzf jre.tgz ) + + # Find the JRE root (the dir whose bin/java is executable). Handles both + # Linux layout (.../jdk-17.../bin/java) and macOS layout + # (.../jdk-17.../Contents/Home/bin/java). + local java_path home + java_path="$(find "$tmp" -mindepth 2 -path '*/bin/java' -type f -executable 2>/dev/null | head -1 || true)" + if [ -z "$java_path" ]; then + echo "sonar-predictor: downloaded JRE has no bin/java; archive layout unrecognised" >&2 + rm -rf "$tmp" + exit 2 + fi + home="${java_path%/bin/java}" + mv "$home" "$jre_dir" + rm -rf "$tmp" + + if [ ! -x "$jre_dir/bin/java" ]; then + echo "sonar-predictor: JRE install completed but $jre_dir/bin/java is missing" >&2 + exit 2 + fi + export JAVA_HOME="$jre_dir" +} + +# -------- 2. ensure bundle is cached ---------------------------------------- + +if [ ! -x "$REAL_SONAR" ]; then + require_cmd curl + require_cmd unzip + + echo "sonar-predictor: first run — downloading $VERSION bundle from $SONAR_MAVEN_REPO_URL..." >&2 mkdir -p "$(dirname "$SKILL_DIR")" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT - ZIP="$TMP/bundle.zip" - SHA="$TMP/bundle.zip.sha1" - - curl -fsSL --retry 3 -o "$ZIP" "$BASE_URL/$ARTIFACT-$VERSION.zip" - curl -fsSL --retry 3 -o "$SHA" "$BASE_URL/$ARTIFACT-$VERSION.zip.sha1" + curl -fsSL --retry 3 -o "$TMP/bundle.zip" "$BUNDLE_URL_BASE/$ARTIFACT-$VERSION.zip" + curl -fsSL --retry 3 -o "$TMP/bundle.zip.sha1" "$BUNDLE_URL_BASE/$ARTIFACT-$VERSION.zip.sha1" - EXPECTED="$(awk '{print $1}' "$SHA")" - ACTUAL="$(sha1_of "$ZIP")" + EXPECTED="$(awk '{print $1}' "$TMP/bundle.zip.sha1")" + ACTUAL="$(sha1_of "$TMP/bundle.zip")" if [ "$EXPECTED" != "$ACTUAL" ]; then echo "sonar-predictor: SHA-1 verification failed (expected $EXPECTED, got $ACTUAL)" >&2 exit 2 fi - unzip -q "$ZIP" -d "$TMP/extracted" + unzip -q "$TMP/bundle.zip" -d "$TMP/extracted" if [ ! -d "$TMP/extracted/sonar-predictor" ]; then - echo "sonar-predictor: unexpected bundle layout (no sonar-predictor/ dir at zip root)" >&2 + echo "sonar-predictor: unexpected bundle layout (no sonar-predictor/ at zip root)" >&2 exit 2 fi - # If another invocation populated the cache while we were downloading, - # respect its work — atomically rename, ignoring failure on race. mv "$TMP/extracted/sonar-predictor" "$SKILL_DIR" 2>/dev/null || true + trap - EXIT + rm -rf "$TMP" if [ ! -x "$REAL_SONAR" ]; then echo "sonar-predictor: bootstrap completed but $REAL_SONAR is not executable" >&2 @@ -85,4 +219,17 @@ if [ ! -x "$REAL_SONAR" ]; then fi fi +# -------- 3. ensure a usable Java is available ------------------------------ + +if ! found_java_meets_min; then + install_jre +fi + +# -------- 4. hand off ------------------------------------------------------- + +if [ -n "${JAVA_HOME:-}" ]; then + export JAVA_HOME + export PATH="$JAVA_HOME/bin:$PATH" +fi + exec "$REAL_SONAR" "$@" diff --git a/plugin/skills/sonar-predictor/bin/sonar.bat b/plugin/skills/sonar-predictor/bin/sonar.bat index c60592d..003ef76 100644 --- a/plugin/skills/sonar-predictor/bin/sonar.bat +++ b/plugin/skills/sonar-predictor/bin/sonar.bat @@ -1,20 +1,34 @@ @echo off rem Bootstrap wrapper for the sonar-predictor skill bundle (Windows). rem -rem On first invocation, downloads the analyzer bundle (~150 MB) from Maven -rem Central into the user cache, verifies its SHA-1, and unpacks it. Every -rem subsequent call dispatches to the cached launcher directly — no network. +rem Loads ../config.env (KEY=VALUE lines, # for comments) and uses the +rem configured Maven proxy + bundle version to download the analyzer bundle +rem on first invocation. Env vars of the same name take precedence over the +rem file. rem -rem Override the bundle location for air-gapped or pre-staged installs: +rem Java auto-install is NOT yet supported on Windows — install Java 17+ +rem manually (or set JAVA_HOME). The bundle's own launcher will discover it. +rem +rem Override the bundle location for air-gapped / pre-staged installs: rem set SONAR_PREDICTOR_HOME=C:\path\to\extracted\sonar-predictor setlocal enabledelayedexpansion -rem v0.1.2's dist artifact on Central is the (now-removed) plugin-bundle shape, -rem not the skill-bundle this wrapper expects. v0.1.1 is the last good skill -rem bundle; v0.1.3 onwards will be skill-shaped again. -set "VERSION=0.1.1" -set "BASE_URL=https://repo1.maven.org/maven2/io/github/randomcodespace/sonarpredict/sonar-predictor-dist/%VERSION%" +rem ---- 1. load config.env (env vars already set win) ---- +set "CONFIG=%~dp0..\config.env" +if exist "%CONFIG%" ( + for /f "usebackq eol=# tokens=1,* delims==" %%a in ("%CONFIG%") do ( + if not "%%a"=="" if not defined %%a set "%%a=%%b" + ) +) + +rem ---- defaults ---- +if not defined SONAR_MAVEN_REPO_URL set "SONAR_MAVEN_REPO_URL=https://repo1.maven.org/maven2" +if not defined SONAR_BUNDLE_VERSION set "SONAR_BUNDLE_VERSION=0.1.1" + +set "VERSION=%SONAR_BUNDLE_VERSION%" +set "ARTIFACT=sonar-predictor-dist" +set "BUNDLE_URL_BASE=%SONAR_MAVEN_REPO_URL%/io/github/randomcodespace/sonarpredict/sonar-predictor-dist/%VERSION%" if defined SONAR_PREDICTOR_HOME ( set "SKILL_DIR=%SONAR_PREDICTOR_HOME%" @@ -30,7 +44,7 @@ set "REAL_SONAR=%SKILL_DIR%\bin\sonar.bat" if exist "%REAL_SONAR%" goto :exec -echo sonar-predictor: first run -- downloading %VERSION% bundle from Maven Central... 1>&2 +echo sonar-predictor: first run -- downloading %VERSION% bundle from %SONAR_MAVEN_REPO_URL%... 1>&2 set "TMP=%TEMP%\sonar-predictor-bootstrap-%RANDOM%-%RANDOM%" mkdir "%TMP%" || (echo sonar-predictor: failed to create temp dir 1>&2 & exit /b 2) @@ -39,11 +53,11 @@ set "ZIP=%TMP%\bundle.zip" set "SHA=%TMP%\bundle.zip.sha1" powershell -NoProfile -ExecutionPolicy Bypass -Command ^ - "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BASE_URL%/sonar-predictor-dist-%VERSION%.zip' -OutFile '%ZIP%' } catch { exit 2 }" + "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BUNDLE_URL_BASE%/%ARTIFACT%-%VERSION%.zip' -OutFile '%ZIP%' } catch { exit 2 }" if errorlevel 1 (echo sonar-predictor: bundle download failed 1>&2 & rmdir /S /Q "%TMP%" & exit /b 2) powershell -NoProfile -ExecutionPolicy Bypass -Command ^ - "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BASE_URL%/sonar-predictor-dist-%VERSION%.zip.sha1' -OutFile '%SHA%' } catch { exit 2 }" + "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BUNDLE_URL_BASE%/%ARTIFACT%-%VERSION%.zip.sha1' -OutFile '%SHA%' } catch { exit 2 }" if errorlevel 1 (echo sonar-predictor: SHA-1 download failed 1>&2 & rmdir /S /Q "%TMP%" & exit /b 2) for /f "tokens=1" %%i in (%SHA%) do set "EXPECTED=%%i" diff --git a/plugin/skills/sonar-predictor/config.env b/plugin/skills/sonar-predictor/config.env new file mode 100644 index 0000000..edc2e2d --- /dev/null +++ b/plugin/skills/sonar-predictor/config.env @@ -0,0 +1,46 @@ +# sonar-predictor — bootstrap configuration +# +# Fork-and-go: this is the only file you need to edit to point the launcher +# at a corporate Maven proxy, a private JRE mirror, or a vendored bundle +# version. The launcher reads this file before doing anything. +# +# Format: KEY=VALUE per line, # for comments, no quotes, no shell expansion. +# Both the bash launcher (bin/sonar) and the Windows launcher (bin/sonar.bat) +# parse this file the same way. +# +# An environment variable of the same name takes precedence over the value +# set here — set one to override without editing this file. + +# --- Analyzer bundle source ---------------------------------------------------- + +# Maven proxy / repository where sonar-predictor-dist-.zip lives. +# Public default: Maven Central. Corporate Nexus / Artifactory example: +# SONAR_MAVEN_REPO_URL=https://nexus.corp.example.com/repository/maven-public +SONAR_MAVEN_REPO_URL=https://repo1.maven.org/maven2 + +# Bundle version. Bumped in lockstep with each plugin release. A fork can pin +# to a vendored / mirrored version without touching the launcher. +SONAR_BUNDLE_VERSION=0.1.1 + +# --- Java runtime -------------------------------------------------------------- + +# Minimum Java major version the analyzer requires. +SONAR_MIN_JAVA_VERSION=17 + +# If no Java >= SONAR_MIN_JAVA_VERSION is found on PATH, in JAVA_HOME, or in +# the common install locations the bundle's launcher already searches, the +# bootstrap downloads a JRE from this URL. Tokens are substituted at download: +# {os} linux | mac (Windows JRE auto-install is not yet supported) +# {arch} x64 | aarch64 +# {version} the value of SONAR_JRE_VERSION +# +# Public default: Adoptium Temurin API (302-redirects to a GitHub release). +# Corporate mirror example (predictable archive layout): +# SONAR_JRE_URL_TEMPLATE=https://artifacts.corp.example.com/temurin/{version}/jre-{os}-{arch}.tar.gz +SONAR_JRE_URL_TEMPLATE=https://api.adoptium.net/v3/binary/latest/{version}/ga/{os}/{arch}/jre/hotspot/normal/eclipse +SONAR_JRE_VERSION=17 + +# Set to 1 (or true, yes) to skip the JRE auto-download even if no Java is +# found — for environments with a policy against the bootstrap fetching +# binaries. The launcher will then print an error and exit instead. +SONAR_DISABLE_JRE_AUTODOWNLOAD= From 7ddd82dea7831cd82207c6b6e2a245b92520ad15 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 23 May 2026 06:35:26 +0000 Subject: [PATCH 2/2] feat(plugin): add agent-scan subcommand that writes JSON to a file and returns a pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new `./bin/sonar agent-scan [scope]` wrapper in the plugin's bootstrap that bakes the out-of-context discipline into the tool, replacing the "agent must remember to redirect to a temp file + jq" pattern. ./bin/sonar agent-scan -> git changeset (default) ./bin/sonar agent-scan check src/Main.java -> specific files ./bin/sonar agent-scan analyze src/ -> whole directory On invocation: 1. Ensures .sonar-predictor/ exists at the project root. 2. If inside a git repo, appends .sonar-predictor/ to .gitignore on first use (with a comment header). Idempotent — re-runs do nothing. 3. Runs `sonar --format json `, redirecting stdout+stderr to .sonar-predictor/scan.json. 4. Prints a compact summary on stdout: total issue count, severity breakdown, and the file path. Example: sonar-predictor: 47 issues written to .sonar-predictor/scan.json severity: BLOCKER=3 CRITICAL=12 MAJOR=22 MINOR=8 INFO=2 query: jq '...' .sonar-predictor/scan.json 5. Propagates the underlying CLI's exit code (0 clean, 1 issues, 2 error). The summary uses jq when available; if not, falls back to a plain pointer. Both agent variants (sonar-scanner-claude, sonar-scanner-copilot) now invoke `agent-scan` instead of the manual redirect+jq dance. Their prompts retain the jq drill-down recipe for when the orchestrator asks for specifics — the file is right there, the recipe stays cheap. SKILL.md declares the agent-scan contract as the canonical agent invocation pattern. The repo's own .gitignore gains a .sonar-predictor/ entry so dogfooding scans against this repo never get committed. Windows .bat gets parity for the subcommand: writes the file, gitignores the path, prints a pointer. No jq-based summary on Windows (jq isn't typically on PATH); just the pointer. Smoke-tested: agent-scan in this git repo wrote a 0-issue scan.json (116 bytes), exit 0, stdout summary correct, .gitignore unchanged because the line was already present (the idempotent guard works). Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 ++ plugin/agents/sonar-scanner-claude.md | 53 ++++++++++++--------- plugin/agents/sonar-scanner-copilot.md | 53 ++++++++++++--------- plugin/skills/sonar-predictor/SKILL.md | 2 + plugin/skills/sonar-predictor/bin/sonar | 46 ++++++++++++++++++ plugin/skills/sonar-predictor/bin/sonar.bat | 31 ++++++++++++ 6 files changed, 142 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 57e239f..2316f52 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ dependency-reduced-pom.xml # module. Not committed to keep the repo lean. The distribution build (dist # module) fetches its own copies through Maven and does not use this directory. daemon/plugins/*.jar + +# sonar-predictor scan output, written by `bin/sonar agent-scan`. Never commit. +.sonar-predictor/ diff --git a/plugin/agents/sonar-scanner-claude.md b/plugin/agents/sonar-scanner-claude.md index 2c81e56..15dc2ee 100644 --- a/plugin/agents/sonar-scanner-claude.md +++ b/plugin/agents/sonar-scanner-claude.md @@ -15,40 +15,47 @@ and safe. You are given a scope: a directory, a repository, a file list, or "the git changeset". Scan exactly that — no more, no less. -## Running the scanner — keep its output OUT of your context +## Running the scanner — `bin/sonar agent-scan` -Invoke the `sonar-predictor` skill (via the `Skill` tool). It exposes the -analyzer CLI and tells you how to invoke it — read its `SKILL.md` and the -tool's own `--help`. Do not assume command syntax. +Invoke the `sonar-predictor` skill (via the `Skill` tool). The skill's +`bin/sonar agent-scan [scope]` subcommand writes the full JSON output to +`.sonar-predictor/scan.json` at the project root and adds that path to +`.gitignore` on first use, so the analyzer's large output never enters your +context. It prints a compact summary on stdout — issue count, severity +breakdown, file path. -**Critical:** a full scan emits a large JSON document — bigger than a context -window. Never let that output land in your context, and never `cat` or read -the raw JSON. Always: +Default scope is the current git changeset (no arg). Pass explicit scope +after `agent-scan`: -1. Redirect JSON output to a temp file (e.g. `... --format json ... > "$J" 2>&1`). -2. Extract a small summary from that file with `jq`. -3. Report only those small extracts, then delete the temp file. +- `agent-scan` — git changeset (default) +- `agent-scan check src/Main.java` — specific files +- `agent-scan analyze src/` — whole directory + +Read `bin/sonar --help` for the underlying command vocabulary if you need +something unusual; don't guess flag names. Exit codes: `0` clean, `1` issues found (a normal result, not a failure), -`2` tool error (report it verbatim). +`2` tool error. + +## What to report -A `jq` recipe — issues are nested under `.files[].issues[]`: +The stdout summary from `agent-scan` is your top-line: issue count, severity +counts, and the `.sonar-predictor/scan.json` path. Report that verbatim plus +a one-line verdict. + +If your caller wants drill-down (specific files, specific rules, criticals +only), query the JSON file with `jq` — never read it raw. Issues are nested +under `.files[].issues[]`: ```sh -jq -r '.issueCount' "$J" -jq -rc '[.files[].issues[].severity]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J" -jq -rc '[.files[].issues[].type]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J" +jq -rc '[.files[]?.issues[]?.type] | group_by(.) | map("\(.[0])=\(length)") | join(" ")' .sonar-predictor/scan.json jq -r '["BLOCKER","CRITICAL","MAJOR","MINOR","INFO"] as $r | [.files[] as $f | $f.issues[] | {s:.severity,k:.ruleKey,f:($f.file|split("/")|last),l:.startLine,m:.message}] | sort_by(.s as $x | $r|index($x)) | .[0:8][] - | " \(.s) \(.k) \(.f):\(.l) \(.m[0:80])"' "$J" -jq -rc '.warnings' "$J" + | " \(.s) \(.k) \(.f):\(.l) \(.m[0:80])"' .sonar-predictor/scan.json +jq -rc '.warnings' .sonar-predictor/scan.json ``` -## What to report - -A concise summary — never the raw JSON, never a file-by-file dump: the total -issue count, the severity and type breakdown, the ~8 highest-severity findings -as `ruleKey file:line message`, and any analyzer warnings (a skipped language -or a crashed sensor means files went unanalyzed). State the verdict plainly. +Never dump raw JSON, never do a file-by-file walk. Concise, actionable summary +only. diff --git a/plugin/agents/sonar-scanner-copilot.md b/plugin/agents/sonar-scanner-copilot.md index a81c9c0..3c55a61 100644 --- a/plugin/agents/sonar-scanner-copilot.md +++ b/plugin/agents/sonar-scanner-copilot.md @@ -15,40 +15,47 @@ and safe. You are given a scope: a directory, a repository, a file list, or "the git changeset". Scan exactly that — no more, no less. -## Running the scanner — keep its output OUT of your context +## Running the scanner — `bin/sonar agent-scan` -Invoke the `sonar-predictor` skill (via the `skill` tool). It exposes the -analyzer CLI and tells you how to invoke it — read its `SKILL.md` and the -tool's own `--help`. Do not assume command syntax. +Invoke the `sonar-predictor` skill (via the `skill` tool). The skill's +`bin/sonar agent-scan [scope]` subcommand writes the full JSON output to +`.sonar-predictor/scan.json` at the project root and adds that path to +`.gitignore` on first use, so the analyzer's large output never enters your +context. It prints a compact summary on stdout — issue count, severity +breakdown, file path. -**Critical:** a full scan emits a large JSON document — bigger than a context -window. Never let that output land in your context, and never `view` or read -the raw JSON. Always: +Default scope is the current git changeset (no arg). Pass explicit scope +after `agent-scan`: -1. Redirect JSON output to a temp file (e.g. `... --format json ... > "$J" 2>&1`). -2. Extract a small summary from that file with `jq`. -3. Report only those small extracts, then delete the temp file. +- `agent-scan` — git changeset (default) +- `agent-scan check src/Main.java` — specific files +- `agent-scan analyze src/` — whole directory + +Read `bin/sonar --help` for the underlying command vocabulary if you need +something unusual; don't guess flag names. Exit codes: `0` clean, `1` issues found (a normal result, not a failure), -`2` tool error (report it verbatim). +`2` tool error. + +## What to report -A `jq` recipe — issues are nested under `.files[].issues[]`: +The stdout summary from `agent-scan` is your top-line: issue count, severity +counts, and the `.sonar-predictor/scan.json` path. Report that verbatim plus +a one-line verdict. + +If your caller wants drill-down (specific files, specific rules, criticals +only), query the JSON file with `jq` — never `view` it raw. Issues are nested +under `.files[].issues[]`: ```sh -jq -r '.issueCount' "$J" -jq -rc '[.files[].issues[].severity]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J" -jq -rc '[.files[].issues[].type]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J" +jq -rc '[.files[]?.issues[]?.type] | group_by(.) | map("\(.[0])=\(length)") | join(" ")' .sonar-predictor/scan.json jq -r '["BLOCKER","CRITICAL","MAJOR","MINOR","INFO"] as $r | [.files[] as $f | $f.issues[] | {s:.severity,k:.ruleKey,f:($f.file|split("/")|last),l:.startLine,m:.message}] | sort_by(.s as $x | $r|index($x)) | .[0:8][] - | " \(.s) \(.k) \(.f):\(.l) \(.m[0:80])"' "$J" -jq -rc '.warnings' "$J" + | " \(.s) \(.k) \(.f):\(.l) \(.m[0:80])"' .sonar-predictor/scan.json +jq -rc '.warnings' .sonar-predictor/scan.json ``` -## What to report - -A concise summary — never the raw JSON, never a file-by-file dump: the total -issue count, the severity and type breakdown, the ~8 highest-severity findings -as `ruleKey file:line message`, and any analyzer warnings (a skipped language -or a crashed sensor means files went unanalyzed). State the verdict plainly. +Never dump raw JSON, never do a file-by-file walk. Concise, actionable summary +only. diff --git a/plugin/skills/sonar-predictor/SKILL.md b/plugin/skills/sonar-predictor/SKILL.md index 773be2d..abeef0d 100644 --- a/plugin/skills/sonar-predictor/SKILL.md +++ b/plugin/skills/sonar-predictor/SKILL.md @@ -11,6 +11,8 @@ An offline SonarSource pre-push quality gate — runs the genuine analyzers loca Run `./bin/sonar` from this skill's base directory (the folder with this `SKILL.md`), or by its absolute path — it is not on `PATH`. The first invocation downloads the analyzer bundle (~150 MB) from Maven Central into a user cache; every subsequent call runs from that cache with no network. **Read the tool's own help before invoking it:** `./bin/sonar --help` lists the commands and the global options, and `./bin/sonar --help` gives a command's own options and exact argument order. The skill scans a git changeset or explicit files and directories and reports in a chosen format — the help states the precise flags and where each one goes. That generated help is the single source of truth; this `SKILL.md` deliberately does not restate command syntax, which would drift. Do not guess flag names or their placement — read the help. +**Agent invocation pattern — `./bin/sonar agent-scan [scope]`.** A wrapper subcommand that bakes the out-of-context discipline into the tool: it runs the scan with `--format json` redirected to `.sonar-predictor/scan.json` at the project root, adds `.sonar-predictor/` to `.gitignore` on first use (when inside a git repo), and prints a compact summary to stdout — issue count, severity breakdown, file path. The calling agent reports that summary and points its caller at the file; deeper drill-down happens with `jq` on the file, on demand. With no scope argument, `agent-scan` defaults to `check --diff` (the git changeset); pass an explicit scope (`agent-scan analyze src/`, `agent-scan check src/Main.java`, etc.) to scan something else. All flags forward to the underlying CLI. + Exit codes: `0` clean, `1` issues found, `2` tool error. Acting on findings: fix `BUG`/`VULNERABILITY`/`SECURITY_HOTSPOT` and `CRITICAL`/`MAJOR` first. This is a fast first-pass gate, not the release gate — fix the real issues and move on. diff --git a/plugin/skills/sonar-predictor/bin/sonar b/plugin/skills/sonar-predictor/bin/sonar index 342febc..150a49e 100755 --- a/plugin/skills/sonar-predictor/bin/sonar +++ b/plugin/skills/sonar-predictor/bin/sonar @@ -232,4 +232,50 @@ if [ -n "${JAVA_HOME:-}" ]; then export PATH="$JAVA_HOME/bin:$PATH" fi +# The `agent-scan` subcommand bakes the out-of-context discipline into the +# tool: run the scan with JSON output redirected to .sonar-predictor/scan.json +# at the project root, add that path to .gitignore on first use, and print a +# compact summary on stdout. The calling agent reports the summary and the +# file path; deeper drill-down happens with jq on the file, on demand. +agent_scan() { + local scan_file=".sonar-predictor/scan.json" + mkdir -p "$(dirname "$scan_file")" + + # Add the scan dir to .gitignore on first use, only inside a git repo. + if git rev-parse --git-dir >/dev/null 2>&1; then + if [ ! -f .gitignore ] || ! grep -qF '.sonar-predictor/' .gitignore; then + printf '\n# sonar-predictor scan output (do not commit)\n.sonar-predictor/\n' >> .gitignore + fi + fi + + # No args -> default to `check --diff` (the git changeset). + if [ $# -eq 0 ]; then + set -- check --diff + fi + + local rc=0 + set +e + "$REAL_SONAR" --format json "$@" > "$scan_file" 2>&1 + rc=$? + set -e + + if command -v jq >/dev/null 2>&1; then + local total sev + total="$(jq -r '(.issueCount // ([.files[]?.issues[]?] | length))' "$scan_file" 2>/dev/null)" + sev="$(jq -rc '[.files[]?.issues[]?.severity] | group_by(.) | map("\(.[0])=\(length)") | join(" ")' "$scan_file" 2>/dev/null)" + echo "sonar-predictor: ${total:-?} issues written to $scan_file" + [ -n "$sev" ] && echo " severity: $sev" + echo " query: jq '...' $scan_file" + else + echo "sonar-predictor: scan complete -> $scan_file (install jq for an inline summary)" + fi + return "$rc" +} + +if [ "${1:-}" = "agent-scan" ]; then + shift + agent_scan "$@" + exit "$?" +fi + exec "$REAL_SONAR" "$@" diff --git a/plugin/skills/sonar-predictor/bin/sonar.bat b/plugin/skills/sonar-predictor/bin/sonar.bat index 003ef76..98b4430 100644 --- a/plugin/skills/sonar-predictor/bin/sonar.bat +++ b/plugin/skills/sonar-predictor/bin/sonar.bat @@ -89,5 +89,36 @@ if not exist "%REAL_SONAR%" ( ) :exec +if /i "%~1"=="agent-scan" ( + shift /1 + call :agent_scan %* + exit /b %ERRORLEVEL% +) call "%REAL_SONAR%" %* exit /b %ERRORLEVEL% + +rem agent-scan: write JSON to .sonar-predictor\scan.json, gitignore that +rem dir on first use, print a short pointer on stdout. +:agent_scan +set "SCAN_FILE=.sonar-predictor\scan.json" +if not exist ".sonar-predictor" mkdir ".sonar-predictor" + +git rev-parse --git-dir >nul 2>&1 +if not errorlevel 1 ( + if not exist .gitignore ( + >>.gitignore echo .sonar-predictor/ + ) else ( + findstr /C:".sonar-predictor/" .gitignore >nul 2>&1 || >>.gitignore echo .sonar-predictor/ + ) +) + +if "%~1"=="" ( + call "%REAL_SONAR%" --format json check --diff > "%SCAN_FILE%" 2>&1 +) else ( + call "%REAL_SONAR%" --format json %* > "%SCAN_FILE%" 2>&1 +) +set RC=%ERRORLEVEL% + +echo sonar-predictor: scan complete -^> %SCAN_FILE% +echo query: jq ^"...^" %SCAN_FILE% +exit /b %RC%