Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 52 additions & 38 deletions src/cli/commands/integrate/claude/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { join } from 'node:path';

import { CLI_COMMAND } from '../../../../lib/config-constants';
import { getMcpConfig, getMcpConfigFilePath } from '../../../../lib/mcp/mcp-helper';
import { OBSOLETE_A3S_MARKER, removeObsoleteHookArtifacts } from '../../../../lib/migration';
import { createSonarSecretsBinaryFeature } from '../_common/features/sonar-secrets-binary-feature';
import { createSonarSecretsHooksFeature } from '../_common/features/sonar-secrets-hooks-feature';
import {
Expand All @@ -30,6 +31,7 @@ import {
upsertAgentHooks,
} from '../_common/hooks';
import {
type FeatureOperation,
type IntegrationContext,
type IntegrationDeclaration,
jsonPatch,
Expand Down Expand Up @@ -66,46 +68,49 @@ export const claudeIntegration: IntegrationDeclaration<ClaudeIntegrationOptions>
displayName: 'Claude Code',
features: [
createSonarSecretsBinaryFeature(),
createSonarSecretsHooksFeature({
agentDisplayName: 'Claude',
configDir: CLAUDE_CONFIG_DIR,
hooksConfigFileName: SETTINGS_FILE,
hooksPatchId: 'claude-settings-secrets-hooks',
scripts: [
{
id: 'pretool-secrets-script',
displayName: 'Claude PreToolUse hook script',
scriptPath: PRETOOL_SCRIPT_REL,
content: {
unix: getSecretPreToolTemplateUnix(),
windows: getSecretPreToolTemplateWindows(),
{
...createSonarSecretsHooksFeature({
agentDisplayName: 'Claude',
configDir: CLAUDE_CONFIG_DIR,
hooksConfigFileName: SETTINGS_FILE,
hooksPatchId: 'claude-settings-secrets-hooks',
scripts: [
{
id: 'pretool-secrets-script',
displayName: 'Claude PreToolUse hook script',
scriptPath: PRETOOL_SCRIPT_REL,
content: {
unix: getSecretPreToolTemplateUnix(),
windows: getSecretPreToolTemplateWindows(),
},
},
},
{
id: 'prompt-secrets-script',
displayName: 'Claude UserPromptSubmit hook script',
scriptPath: PROMPT_SCRIPT_REL,
content: {
unix: getSecretPromptTemplateUnix(),
windows: getSecretPromptTemplateWindows(),
{
id: 'prompt-secrets-script',
displayName: 'Claude UserPromptSubmit hook script',
scriptPath: PROMPT_SCRIPT_REL,
content: {
unix: getSecretPromptTemplateUnix(),
windows: getSecretPromptTemplateWindows(),
},
},
},
],
hookEntries: [
{
eventType: 'PreToolUse',
matcher: 'Read',
marker: 'sonar-secrets',
scriptPath: PRETOOL_SCRIPT_REL,
},
{
eventType: 'UserPromptSubmit',
matcher: '*',
marker: 'sonar-secrets',
scriptPath: PROMPT_SCRIPT_REL,
},
],
}),
],
hookEntries: [
{
eventType: 'PreToolUse',
matcher: 'Read',
marker: 'sonar-secrets',
scriptPath: PRETOOL_SCRIPT_REL,
},
{
eventType: 'UserPromptSubmit',
matcher: '*',
marker: 'sonar-secrets',
scriptPath: PROMPT_SCRIPT_REL,
},
],
}),
operations: [createRemoveObsoleteA3sArtifactsOperation()],
},
{
id: 'sonar-sqaa-hook',
displayName: 'SonarQube Agentic Analysis hook',
Expand Down Expand Up @@ -148,6 +153,7 @@ export const claudeIntegration: IntegrationDeclaration<ClaudeIntegrationOptions>
]),
}),
],
operations: [createRemoveObsoleteA3sArtifactsOperation()],
},
{
id: 'mcp-server',
Expand Down Expand Up @@ -242,3 +248,11 @@ function getRequiredStringAttr(context: IntegrationContext, key: string): string
}
return value;
}

function createRemoveObsoleteA3sArtifactsOperation(): FeatureOperation {
return {
id: 'remove-obsolete-a3s-artifacts',
displayName: 'Remove obsolete SQAA hook artifacts',
apply: ({ targetRoot }) => removeObsoleteHookArtifacts(targetRoot, OBSOLETE_A3S_MARKER),
};
}
3 changes: 3 additions & 0 deletions src/cli/commands/integrate/git/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const GIT_INTEGRATIONS = [nativeGitIntegration, huskyIntegration, preCommitInteg

export function registerGitIntegrations(registry = supportedIntegrations): void {
for (const integration of GIT_INTEGRATIONS) {
if (registry.get(integration.id)) {
continue;
}
registry.register(integration);
}
}
Expand Down
93 changes: 93 additions & 0 deletions src/lib/post-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@
resolveContextAugmentationAgent,
stopAllContextAugmentationTools,
} from '../cli/commands/integrate/_common/context-augmentation';
import {
integrationInstaller,
type IntegrationRegistry,
supportedIntegrations,
} from '../cli/commands/integrate/_common/registry';
import {
CLAUDE_INTEGRATION_ID,
registerClaudeIntegration,
} from '../cli/commands/integrate/claude/declaration';
import { installHooks } from '../cli/commands/integrate/claude/hooks.js';
import { registerCodexIntegration } from '../cli/commands/integrate/codex/declaration';
import { registerGitIntegrations } from '../cli/commands/integrate/git/tools';
import { CONTEXT_AUGMENTATION_BINARY_NAME, SECRETS_BINARY_NAME } from './install-types.js';
import logger from './logger';
import {
Expand Down Expand Up @@ -83,11 +94,84 @@
}

async function runActions(_previousVersion: string, _currentVersion: string): Promise<void> {
await migrateDeclarativeIntegrations();
await migrateClaudeCodeHooks();
await updateSecretsBinaryIfNeeded();
await updateContextAugmentationIfNeeded();
}

export async function migrateDeclarativeIntegrations(
registry: IntegrationRegistry = supportedIntegrations,
): Promise<void> {
ensureBuiltInIntegrationsRegistered(registry);

const state = loadState();
let stateChanged = false;

for (const integration of registry.list()) {
const installedIntegration = integrationInstaller.findInstalledIntegration(state, integration);
if (!installedIntegration) {
continue;
}

const featuresById = new Map(integration.features.map((feature) => [feature.id, feature]));
const knownFeatures = installedIntegration.features.filter((feature) =>
featuresById.has(feature.featureId),
);

if (knownFeatures.length !== installedIntegration.features.length) {
installedIntegration.features = knownFeatures;
stateChanged = true;
}

for (const installedFeature of knownFeatures) {
const feature = featuresById.get(installedFeature.featureId);
if (!feature) {
continue;
}

try {
const featureContext = {
targetRoot: installedFeature.targetRoot,
scope: installedFeature.scope,
attrs: installedFeature.attrs,
};
const applied = await integrationInstaller.applyFeature(
{ state, ...featureContext },
installedFeature,
feature,
);
integrationInstaller.recordInstalledFeature(
state,
featureContext,
integration,
feature,
applied,
);
stateChanged = true;
} catch (err) {
logger.debug(
`Declarative migration failed for ${integration.id}.${installedFeature.featureId}: ${(err as Error).message}`,
);
}
}
}

if (stateChanged) {
saveState(state);
}
}

function ensureBuiltInIntegrationsRegistered(registry: IntegrationRegistry): void {
if (registry !== supportedIntegrations) {
return;
}

registerClaudeIntegration();
registerCodexIntegration();
registerGitIntegrations();
}

/**
* Update the sonar-secrets binary if it is already installed but targets a different version
* than the one bundled with this CLI release.
Expand Down Expand Up @@ -303,9 +387,14 @@
*
* @param homedirFn - Injectable for tests; defaults to os.homedir()
*/
export async function migrateClaudeCodeHooks(homedirFn: () => string = homedir): Promise<void> {

Check failure on line 390 in src/lib/post-update.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=SonarSource_sonarqube-cli&issues=AZ5jA52RIOjC2UiM0sTU&open=AZ5jA52RIOjC2UiM0sTU&pullRequest=319
const state = loadState();

if (hasInstalledDeclarativeIntegration(state, CLAUDE_INTEGRATION_ID)) {
logger.debug('Declarative Claude integration detected — skipping legacy hook migration');
return;
}

type Location = { projectRoot: string; globalDir: string | undefined };
const locations: Location[] = [];

Expand Down Expand Up @@ -344,3 +433,7 @@
}
}
}

function hasInstalledDeclarativeIntegration(state: CliState, integrationId: string): boolean {
return state.integrations.installed.some((entry) => entry.integrationId === integrationId);
}
Loading
Loading