diff --git a/CLAUDE.md b/CLAUDE.md index d33358efe..174b8e17f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ The bare `sonar analyze` command accepts `-p, --project ` for the agent `sonar analyze dependency-risks` pre-scans discovered manifest files for secrets (via `sonar-secrets`) before the SCA scan and aborts if any are found. In the `analyze` path the secrets binary is a hard prerequisite (install failure aborts the run); in the git pre-commit hook path the manifest secrets *scan* fails open on a scan error (skips/warns). See `dependency-risk-helpers/manifest-secrets-guard.ts`. This scan-error fail-open is distinct from the git/agent secrets-hook auth+binary gate: an unauthenticated user or a missing `sonar-secrets` binary blocks the commit/push (git hooks throw `MissingDependenciesError` → exit 1). -`sonar import` (hidden while in development) imports a repository from a connected DevOps platform into SonarQube by provisioning a bound project. Accepts `--org ` and `--repo ` (prompted interactively when omitted) plus `--non-interactive`. Implementation lives in `src/commands/import/`. +`sonar import` (hidden while in development) imports a repository from a connected DevOps platform into SonarQube by provisioning a bound project. Accepts `--org ` and `--repo ` (prompted interactively when omitted) plus `--non-interactive`. On success it prints the organization's onboarding dashboard link (`{serverUrl}/organizations/{orgKey}/onboarding-dashboard`) before the final outro. Implementation lives in `src/commands/import/`. Declarative integration registry helpers live in `src/commands/integrate/_common/registry/index.ts`. New integration descriptors should use that public entrypoint for dependency/resource factories, operations, and registry validation. Command handlers should keep command-specific validation, prompts, and target resolution thin, then delegate feature selection, generic install messages, dependency/resource application, and state recording to `src/commands/integrate/_common/installer.ts`. diff --git a/src/commands/import/index.ts b/src/commands/import/index.ts index 34c58452d..755b41194 100644 --- a/src/commands/import/index.ts +++ b/src/commands/import/index.ts @@ -177,7 +177,17 @@ function reportSkipped(skipped: readonly SkippedRepo[]): void { } } -function reportOutcome(succeeded: number, failed: number, skippedCount: number): void { +/** Builds the SonarQube Cloud onboarding dashboard link for an organization. */ +function buildOnboardingDashboardUrl(serverUrl: string, orgKey: string): string { + return `${serverUrl.replace(/\/$/, '')}/organizations/${orgKey}/onboarding-dashboard`; +} + +function reportOutcome( + succeeded: number, + failed: number, + skippedCount: number, + dashboardUrl: string, +): void { const skippedSuffix = skippedCount > 0 ? ` (${skippedCount} skipped)` : ''; if (failed > 0) { @@ -192,7 +202,11 @@ function reportOutcome(succeeded: number, failed: number, skippedCount: number): } const succeededNoun = succeeded === 1 ? 'repository' : 'repositories'; - outro(`Imported ${succeeded} ${succeededNoun}${skippedSuffix}`, 'success'); + outro( + `Imported ${succeeded} ${succeededNoun}${skippedSuffix}`, + 'success', + `Dashboard: ${dashboardUrl}`, + ); } export async function importHandler(options: ImportOptions, auth: ResolvedAuth): Promise { @@ -210,7 +224,12 @@ export async function importHandler(options: ImportOptions, auth: ResolvedAuth): resolution.collection, ); reportSkipped(skipped); - reportOutcome(succeeded, failed, skipped.length); + reportOutcome( + succeeded, + failed, + skipped.length, + buildOnboardingDashboardUrl(auth.serverUrl, resolution.orgKey), + ); return; } @@ -231,5 +250,10 @@ export async function importHandler(options: ImportOptions, auth: ResolvedAuth): ); const { succeeded, failed } = progress.finish(); - reportOutcome(succeeded, failed, skipped.length); + reportOutcome( + succeeded, + failed, + skipped.length, + buildOnboardingDashboardUrl(auth.serverUrl, resolution.orgKey), + ); } diff --git a/src/ui/components/sections.ts b/src/ui/components/sections.ts index 8453d23cc..63cf8f231 100644 --- a/src/ui/components/sections.ts +++ b/src/ui/components/sections.ts @@ -20,7 +20,7 @@ // Structural markers — intro, outro -import { bold, green, isTTY, red } from '../colors.js'; +import { bold, cyan, green, isTTY, red } from '../colors.js'; import { isMockActive, recordCall } from '../mock.js'; const DIVIDER_BASE_WIDTH = 40; @@ -44,9 +44,13 @@ export function intro(title: string, subtitle?: string): void { } } -export function outro(message: string, status: 'success' | 'error' = 'success'): void { +export function outro( + message: string, + status: 'success' | 'error' = 'success', + detail?: string, +): void { if (isMockActive()) { - recordCall('outro', message, status); + recordCall('outro', message, status, detail); return; } @@ -56,8 +60,12 @@ export function outro(message: string, status: 'success' | 'error' = 'success'): if (isTTY) { process.stdout.write(`\n ${DIVIDER}\n`); process.stdout.write(` ${icon} ${bold(colorFn(message))}\n`); + // Aligns under the message text: 2 leading + 2-col emoji icon + 2 gap. + if (detail) process.stdout.write(` ${bold(cyan(detail))}\n`); process.stdout.write(` ${DIVIDER}\n\n`); } else { - process.stdout.write(`\n=== ${message} ===\n\n`); + process.stdout.write(`\n=== ${message} ===\n`); + if (detail) process.stdout.write(`${detail}\n`); + process.stdout.write('\n'); } } diff --git a/tests/integration/specs/import/import.test.ts b/tests/integration/specs/import/import.test.ts index e71a0e001..639377ae3 100644 --- a/tests/integration/specs/import/import.test.ts +++ b/tests/integration/specs/import/import.test.ts @@ -413,6 +413,9 @@ describe('sonar import', () => { ); expect(result.stdout).toContain('Manual — choose repositories yourself'); expect(result.stdout).toContain('Imported 2 repositories'); + expect(result.stdout).toContain( + `Dashboard: ${serverUrl}/organizations/my-org/onboarding-dashboard`, + ); const recorded = server.getRecordedRequests(); const provisionRequests = recorded.filter( (r) => r.path === '/api/alm_integration/provision_projects',