diff --git a/CLAUDE.md b/CLAUDE.md
index 9c0c5c314..17477d2f5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -137,7 +137,8 @@ Error subclasses extend the abstract `CliError` and carry their own `exitCode`,
- `sonar auth logout` relies on state: if there is no active connection or `isAuthenticated` is false, it only reports that you are already logged out (no keychain changes).
- When `sonar auth login` runs the browser-based OAuth flow, the server-generated token name returned in the callback POST body is captured and persisted on the connection as `tokenName` (see `AuthConnection` in `src/lib/state.ts`). The wire field is `name` (matching `/api/user_tokens/revoke?name=`); we keep it as `tokenName` in-memory to disambiguate from other "name" fields.
- On `sonar auth logout`, the CLI best-effort revokes the server-side token via `SonarQubeClient.revokeUserToken(...)` (a one-line wrapper over the generic `postForm(endpoint, params)` helper) before clearing the keychain entry. Failures (network error, non-2xx response) are reported via a warning on stderr; local cleanup still proceeds. When the connection has no `tokenName` (e.g. upgraded from an older CLI version), the CLI emits a manual-revocation hint on stderr instead.
-- `sonar system status` displays a diagnostic overview: authentication (token verified live via `/api/authentication/validate`, four states: Not Set / Active / Invalid / Set Unverified), installed binaries with update availability, configured integrations with paths, and MCP Server configured/running status. Supports `--json` for machine consumption. Implementation in `src/cli/commands/system/status.ts`. Token verification uses `checkTokenStatus` from `_common/token.ts`; CLI update availability comes from `Distribution/sonarqube-cli/stable.version`, while `sonar self-update` downloads the platform installer script from the GitHub `user-scripts/` directory when it actually performs the update. MCP config validation is JSON-based for Claude/Copilot (checks `mcpServers.sonarqube` structure) and trusts file existence for Codex TOML configs.
+- `sonar system status` displays a diagnostic overview: authentication (token verified live via `/api/authentication/validate`, four states: Not Set / Active / Invalid / Set Unverified), installed binaries with update availability, configured integrations with paths, and MCP Server configured/running status. Supports `--json` for machine consumption. Implementation in `src/cli/commands/system/status.ts`. Token verification uses `checkTokenStatus` from `_common/token.ts`; CLI update availability comes from `Distribution/sonarqube-cli/stable.version`, while `sonar update` downloads the platform installer script from the GitHub `user-scripts/` directory when it actually performs the update. MCP config validation is JSON-based for Claude/Copilot (checks `mcpServers.sonarqube` structure) and trusts file existence for Codex TOML configs.
+- `sonar self-update` is a deprecated, hidden alias for `sonar update` (standalone distribution only, see `IS_STANDALONE_DISTRIBUTION`): calling it prints a deprecation warning pointing to `sonar update`, then runs the same `updateVersion()` logic. Both are registered in `src/cli/command-tree.ts`.
## Tests
@@ -176,3 +177,35 @@ When adding, removing, or changing commands, scripts, or project structure, upda
The docs site is generated from the CLI source — do not edit `commands.json`, `llms.txt`, or `sitemap.xml` by hand. This is done by automation post-release. The docs generator also stamps the current package version into the nav badge in `index.html` and `commands.html`, plus the JSON-LD `softwareVersion` and `meta-version` in `index.html`.
Keep a fixed `` in the docs HTML so production resolves relative assets and internal links under the public mount point. This is intentionally production-oriented: Netlify deploy previews served from `/` will not work with that fixed base.
+
+
+# SonarQube Agentic Analysis protocol
+
+SonarQube Agentic Analysis is the final confirmation layer at the end of every turn in which you wrote to one or more files in the workspace (create, edit, patch, format — any tool call that changed file contents on disk).
+
+**Trigger:** the moment before you send your final reply for the turn.
+
+When the trigger fires, you **MUST** run SonarQube Agentic Analysis on the full set of files you modified this turn — **one command, not a per-file loop**.
+
+**Preferred:** pass every path you modified this turn in a single invocation (repeat `--file` for each path). Use paths **relative to the project root** (e.g. `src/foo.ts`). For end-of-turn analysis, **always** pass `--depth DEEP` — even when you changed only one file — so cross-file findings are included.
+
+```bash
+sonar analyze agentic --project SonarSource_sonarqube-cli --depth DEEP --file --file
+```
+
+**Fallback:** when you cannot reliably list every modified path (e.g. you did not track them, or git state is the only source of truth), use the git change set with DEEP analysis:
+
+```bash
+sonar analyze agentic --project SonarSource_sonarqube-cli --depth DEEP
+```
+
+Per-edit hooks run faster STANDARD analysis. End-of-turn analysis must always use `--depth DEEP` (including a single `--file`). Use `--depth STANDARD` only for optional faster passes — never for the mandatory end-of-turn run.
+
+Non-negotiable rules:
+
+1. Mid-turn edits do **not** need SonarQube Agentic Analysis — only the final on-disk state matters. Skipping SonarQube Agentic Analysis at end-of-turn is a protocol violation.
+2. Run SonarQube Agentic Analysis **before** sending your final reply, marking the task done, or handing control back to the user.
+3. If SonarQube Agentic Analysis reports issues on lines you touched in this turn, fix them, then re-run SonarQube Agentic Analysis on the same scope (change set or explicit file list). Repeat until clean (or only pre-existing findings on lines you did not touch remain). Pre-existing findings on untouched lines are out of scope — do not "fix" them unless the user asked.
+4. If SonarQube Agentic Analysis is skipped (no SonarQube Cloud connection, or no project configured), state the skip reason to the user once and continue — do not retry.
+5. Do not suppress, summarize away, or omit SonarQube Agentic Analysis findings from your reply. Surface them verbatim.
+
diff --git a/src/cli/command-tree.ts b/src/cli/command-tree.ts
index d7c9c722e..dc62d6567 100644
--- a/src/cli/command-tree.ts
+++ b/src/cli/command-tree.ts
@@ -102,9 +102,9 @@ import {
import { listProjects, type ListProjectsOptions } from './commands/list/projects';
import { remediate, type RemediateOptions } from './commands/remediate';
import { runMcp } from './commands/run/mcp.js';
-import { selfUpdate, type SelfUpdateOptions } from './commands/self-update';
import { systemReset, type SystemResetOptions } from './commands/system/reset';
import { systemStatus, type SystemStatusOptions } from './commands/system/status';
+import { type SelfUpdateOptions, updateVersion } from './commands/update';
import { getBanner, getCustomRootHelp } from './root-help.js';
const DEFAULT_PAGE_SIZE = MAX_PAGE_SIZE;
@@ -575,14 +575,28 @@ system
// Update the CLI to the latest version
if (IS_STANDALONE_DISTRIBUTION) {
- COMMAND_TREE.command('self-update')
+ COMMAND_TREE.command('update')
.description('Update SonarQube CLI to the latest version')
.rootHelp({
category: 'cli-management',
})
.option('--status', 'Check for a newer version without installing')
.option('--force', 'Install the latest version even if already up to date')
- .anonymousAction((options: SelfUpdateOptions) => selfUpdate(options));
+ .anonymousAction((options: SelfUpdateOptions) => updateVersion(options));
+
+ // `self-update` is deprecated in favour of `sonar update`.
+ const selfUpdateCmd = COMMAND_TREE.command('self-update', { hidden: true })
+ .description(
+ "Update SonarQube CLI to the latest version (deprecated — use 'sonar update' instead)",
+ )
+ .option('--status', 'Check for a newer version without installing')
+ .option('--force', 'Install the latest version even if already up to date')
+ .anonymousAction((options: SelfUpdateOptions) => updateVersion(options));
+ selfUpdateCmd.hook('preAction', () => {
+ warn(
+ "sonar self-update is deprecated and will be removed in one of the upcoming versions. Use 'sonar update' instead.",
+ );
+ });
}
const runCommand = COMMAND_TREE.command('run', { hidden: true }).description(
diff --git a/src/cli/commands/system/status.ts b/src/cli/commands/system/status.ts
index 4dd514965..b4e713173 100644
--- a/src/cli/commands/system/status.ts
+++ b/src/cli/commands/system/status.ts
@@ -57,7 +57,7 @@ import { checkTokenStatus } from '../_common/token';
import { supportedIntegrations } from '../integrate';
import { checkAntigravitySecretsHookFile } from '../integrate/antigravity/health';
import { resolveAntigravityHooksJsonPathForScope } from '../integrate/antigravity/hooks';
-import { checkForUpdate, type UpdateCheckResult } from '../self-update/update-check';
+import { checkForUpdate, type UpdateCheckResult } from '../update/update-check';
const SCA_SCANNER_CACHE_DIR = join(CLI_DIR, 'sca-scanner-cache');
@@ -495,7 +495,7 @@ function buildRecommendations(
recommendations.push(`Fix client certificate configuration: ${network.error}`);
if (updateResult && !updateResult.upToDate) {
recommendations.push(
- `Run 'sonar self-update' to update to v${updateResult.latest.version.noBuild.text}`,
+ `Run 'sonar update' to update to v${updateResult.latest.version.noBuild.text}`,
);
}
diff --git a/src/cli/commands/self-update/index.ts b/src/cli/commands/update/index.ts
similarity index 89%
rename from src/cli/commands/self-update/index.ts
rename to src/cli/commands/update/index.ts
index 5dadd3c0a..2d1006720 100644
--- a/src/cli/commands/self-update/index.ts
+++ b/src/cli/commands/update/index.ts
@@ -27,7 +27,7 @@ export interface SelfUpdateOptions {
force?: boolean;
}
-async function selfUpdateStatus(): Promise {
+async function updateVersionStatus(): Promise {
info('Checking for updates...');
const { currentVersion, latest, upToDate } = await checkForUpdate();
@@ -40,13 +40,13 @@ async function selfUpdateStatus(): Promise {
success('Already up to date');
} else {
warn(`Update available: v${latest.version.noBuild.text}`);
- text(' Run: sonar self-update');
+ text(' Run: sonar update');
}
}
-export async function selfUpdate(options: SelfUpdateOptions = {}): Promise {
+export async function updateVersion(options: SelfUpdateOptions = {}): Promise {
if (options.status) {
- await selfUpdateStatus();
+ await updateVersionStatus();
return;
}
@@ -78,7 +78,7 @@ export async function selfUpdate(options: SelfUpdateOptions = {}): Promise
: `Update script exited with code ${String(installResult.scriptExitStatus ?? 'unknown')}`;
throw new CommandFailedError(message, {
remediationHint:
- "Rerun 'sonar self-update --force' or update manually using the installer script.",
+ "Rerun 'sonar update --force' or update manually using the installer script.",
});
}
case 'installed':
diff --git a/src/cli/commands/self-update/update-check.ts b/src/cli/commands/update/update-check.ts
similarity index 100%
rename from src/cli/commands/self-update/update-check.ts
rename to src/cli/commands/update/update-check.ts
diff --git a/src/lib/update-notification.ts b/src/lib/update-notification.ts
index 6dd3d530c..3297cd881 100644
--- a/src/lib/update-notification.ts
+++ b/src/lib/update-notification.ts
@@ -29,7 +29,7 @@ import { Version } from '../cli/commands/_common/version.js';
import {
BACKGROUND_UPDATE_CHECK_TIMEOUT_MS,
fetchLatestVersion,
-} from '../cli/commands/self-update/update-check.js';
+} from '../cli/commands/update/update-check.js';
import { TELEMETRY_FLUSH_MODE_ENV } from '../telemetry/index.js';
import { cyan } from '../ui/colors.js';
import { isFormattedOutputMode, text } from '../ui/index.js';
@@ -141,7 +141,7 @@ function renderUpdateNotification(currentNoBuild: string, latestNoBuild: string)
undefined,
'stderr',
);
- text(' → Run `sonar self-update` to upgrade.', undefined, 'stderr');
+ text(' → Run `sonar update` to get the latest version', undefined, 'stderr');
}
/**
diff --git a/tests/integration/specs/help/root-help.test.ts b/tests/integration/specs/help/root-help.test.ts
index d35e1512d..2a288069c 100644
--- a/tests/integration/specs/help/root-help.test.ts
+++ b/tests/integration/specs/help/root-help.test.ts
@@ -61,7 +61,7 @@ function getExpectedRootHelp(): string {
' auth Manage authentication tokens and credentials',
' config Configure CLI settings',
' system System diagnostics and maintenance commands for the SonarQube CLI installation.',
- ' self-update Update SonarQube CLI to the latest version',
+ ' update Update SonarQube CLI to the latest version',
'',
' OPTIONS',
' -h, --help Display help for a specific command',
diff --git a/tests/integration/specs/system/system-status.test.ts b/tests/integration/specs/system/system-status.test.ts
index d3f1d0545..a47159472 100644
--- a/tests/integration/specs/system/system-status.test.ts
+++ b/tests/integration/specs/system/system-status.test.ts
@@ -579,7 +579,7 @@ describe('system status', () => {
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('RECOMMENDATIONS');
- expect(result.stdout).toContain('sonar self-update');
+ expect(result.stdout).toContain('sonar update');
expect(result.stdout).toContain(newerVersion);
},
{ timeout: 15000 },
@@ -752,7 +752,7 @@ describe('system status', () => {
expect(result.exitCode).toBe(0);
// No update recommendation when the update check fails
- expect(result.stdout).not.toContain('sonar self-update');
+ expect(result.stdout).not.toContain('sonar update');
},
{ timeout: 15000 },
);
diff --git a/tests/integration/specs/update/command.test.ts b/tests/integration/specs/update/command.test.ts
new file mode 100644
index 000000000..747aa254e
--- /dev/null
+++ b/tests/integration/specs/update/command.test.ts
@@ -0,0 +1,68 @@
+/*
+ * SonarQube CLI
+ * Copyright (C) SonarSource Sàrl
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+// Integration tests for the `sonar update` command and the deprecated `sonar self-update` alias
+
+import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
+
+import { TestHarness } from '../../harness';
+
+describe('update command', () => {
+ let harness: TestHarness;
+
+ beforeEach(async () => {
+ harness = await TestHarness.create();
+ });
+
+ afterEach(async () => {
+ await harness.dispose();
+ });
+
+ it(
+ 'sonar update --status reports an available update without a deprecation warning',
+ async () => {
+ const newerVersion = '99.0.0';
+ await harness.newFakeBinariesServer().withStableVersion(newerVersion).start();
+
+ const result = await harness.run('update --status');
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout + result.stderr).toContain(`Update available: v${newerVersion}`);
+ expect(result.stderr).not.toContain('deprecated');
+ },
+ { timeout: 15000 },
+ );
+
+ it(
+ 'sonar self-update --status still works but warns that it is deprecated',
+ async () => {
+ const newerVersion = '99.0.0';
+ await harness.newFakeBinariesServer().withStableVersion(newerVersion).start();
+
+ const result = await harness.run('self-update --status');
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout + result.stderr).toContain(`Update available: v${newerVersion}`);
+ expect(result.stderr).toContain('deprecated');
+ expect(result.stderr).toContain('sonar update');
+ },
+ { timeout: 15000 },
+ );
+});
diff --git a/tests/integration/specs/self-update/post-update.test.ts b/tests/integration/specs/update/post-update.test.ts
similarity index 100%
rename from tests/integration/specs/self-update/post-update.test.ts
rename to tests/integration/specs/update/post-update.test.ts
diff --git a/tests/unit/cli/commands/self-update/migration.test.ts b/tests/unit/cli/commands/update/migration.test.ts
similarity index 100%
rename from tests/unit/cli/commands/self-update/migration.test.ts
rename to tests/unit/cli/commands/update/migration.test.ts
diff --git a/tests/unit/cli/commands/self-update/pgp-verification.test.ts b/tests/unit/cli/commands/update/pgp-verification.test.ts
similarity index 100%
rename from tests/unit/cli/commands/self-update/pgp-verification.test.ts
rename to tests/unit/cli/commands/update/pgp-verification.test.ts
diff --git a/tests/unit/cli/commands/self-update/post-update.test.ts b/tests/unit/cli/commands/update/post-update.test.ts
similarity index 100%
rename from tests/unit/cli/commands/self-update/post-update.test.ts
rename to tests/unit/cli/commands/update/post-update.test.ts
diff --git a/tests/unit/cli/commands/self-update/sonarsource-releases.test.ts b/tests/unit/cli/commands/update/sonarsource-releases.test.ts
similarity index 100%
rename from tests/unit/cli/commands/self-update/sonarsource-releases.test.ts
rename to tests/unit/cli/commands/update/sonarsource-releases.test.ts
diff --git a/tests/unit/cli/commands/self-update/self-update.test.ts b/tests/unit/cli/commands/update/update-version.test.ts
similarity index 89%
rename from tests/unit/cli/commands/self-update/self-update.test.ts
rename to tests/unit/cli/commands/update/update-version.test.ts
index 689c6eac8..27a8d3571 100644
--- a/tests/unit/cli/commands/self-update/self-update.test.ts
+++ b/tests/unit/cli/commands/update/update-version.test.ts
@@ -72,8 +72,8 @@ void mock.module('../../../../../src/lib/version', () => ({
}));
const { checkForUpdate, fetchLatestVersion } =
- await import('../../../../../src/cli/commands/self-update/update-check');
-const { selfUpdate } = await import('../../../../../src/cli/commands/self-update');
+ await import('../../../../../src/cli/commands/update/update-check');
+const { updateVersion } = await import('../../../../../src/cli/commands/update');
function stableVersionResponse(version: string) {
return {
@@ -193,7 +193,7 @@ describe('checkForUpdate', () => {
});
});
-describe('selfUpdate --status', () => {
+describe('updateVersion --status', () => {
let fetchSpy: ReturnType;
beforeEach(() => {
@@ -210,7 +210,7 @@ describe('selfUpdate --status', () => {
it('reports an available update without installing', async () => {
fetchSpy.mockResolvedValue(stableVersionResponse('99.0.0.241'));
- await selfUpdate({ status: true });
+ await updateVersion({ status: true });
const messages = getMockUiCalls().map((c) => c.args.join(' '));
// Build number must be stripped from displayed versions
@@ -221,14 +221,41 @@ describe('selfUpdate --status', () => {
it('reports already up to date', async () => {
fetchSpy.mockResolvedValue(stableVersionResponse('0.0.1'));
- await selfUpdate({ status: true });
+ await updateVersion({ status: true });
const messages = getMockUiCalls().map((c) => c.args.join(' '));
expect(messages.some((m) => /up to date/i.test(m))).toBe(true);
});
});
-describe('selfUpdate --force', () => {
+describe('updateVersion (no options)', () => {
+ let fetchSpy: ReturnType;
+
+ beforeEach(() => {
+ setMockUi(true);
+ clearMockUiCalls();
+ fetchSpy = spyOn(globalThis, 'fetch');
+ });
+
+ afterEach(() => {
+ fetchSpy.mockRestore();
+ setMockUi(false);
+ });
+
+ it('reports already up to date and does not attempt to install', async () => {
+ const [major, minor, patch] = (await import('../../../../../package.json')).version.split('.');
+ fetchSpy.mockResolvedValue(stableVersionResponse(`${major}.${minor}.${patch}.999`));
+
+ await updateVersion();
+
+ const messages = getMockUiCalls().map((c) => c.args.join(' '));
+ expect(messages.some((m) => /already up to date/i.test(m))).toBe(true);
+ // Only the stable.version check should run; the install script must not be fetched.
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('updateVersion --force', () => {
let fetchSpy: ReturnType;
beforeEach(() => {
@@ -262,7 +289,7 @@ describe('selfUpdate --force', () => {
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
});
- await selfUpdate({ force: true });
+ await updateVersion({ force: true });
}
it('installs even when already up to date', async () => {
@@ -295,7 +322,7 @@ describe('selfUpdate --force', () => {
try {
await runForce('2.0.0');
- throw new Error('Expected selfUpdate to fail');
+ throw new Error('Expected updateVersion to fail');
} catch (error) {
expect((error as Error).message).toContain('Update script exited with code 12');
}
@@ -309,7 +336,7 @@ describe('selfUpdate --force', () => {
try {
await runForce('2.0.0');
- throw new Error('Expected selfUpdate to fail');
+ throw new Error('Expected updateVersion to fail');
} catch (error) {
expect((error as Error).message).toContain(
'Failed to start update script: spawnSync /bin/bash ENOENT',
@@ -340,7 +367,7 @@ describe('selfUpdate --force', () => {
try {
await runForce('2.0.0', 'Write-Host hi\n');
- throw new Error('Expected selfUpdate to fail');
+ throw new Error('Expected updateVersion to fail');
} catch (error) {
expect((error as Error).message).toContain(
'Failed to start update script: spawn cmd.exe ENOENT',
diff --git a/tests/unit/lib/update-notification.test.ts b/tests/unit/lib/update-notification.test.ts
index d3dafea77..e9a97ab16 100644
--- a/tests/unit/lib/update-notification.test.ts
+++ b/tests/unit/lib/update-notification.test.ts
@@ -189,7 +189,7 @@ describe('maybeNotifyUpdateAvailable', () => {
expect(output).toContain(
`A new version of SonarQube CLI is available: ${major}.${minor}.${patch} → 99.0.0`,
);
- expect(output).toContain('Run `sonar self-update` to upgrade.');
+ expect(output).toContain('Run `sonar update` to get the latest version');
});
it('persists fetch metadata in state', async () => {
@@ -219,7 +219,7 @@ describe('maybeNotifyUpdateAvailable', () => {
const output = notificationOutput();
expect(output).toContain('A new version of SonarQube CLI is available');
- expect(output).toContain('Run `sonar self-update` to upgrade.');
+ expect(output).toContain('Run `sonar update` to get the latest version');
});
it('does not re-fetch within 24h after a failed check', async () => {