A CLI tool (sonar) that integrates SonarQube Server and Cloud into developer workflows.
Release builds publish standalone executables for linux-x86-64, linux-arm64, macos-arm64, and windows-x86-64. The user-scripts/install.sh and user-scripts/install-prerelease.sh installers select the Linux artifact using uname -m (aarch64 / arm64 → linux-arm64, x86_64 / amd64 → linux-x86-64).
Use the package.json scripts for full test runs.
bun run lint # ESLint (TypeScript-aware, includes import sort)
bun run lint:fix # Auto-fix safe issues
bun run typecheck # tsc --noEmit
bun run test:unit # All unit tests
bun run test:integration # All integration tests, no coverage (local development)
bun run test:all # Unit + integration
bun run test:e2e # end-to-end tests- Unit:
bun test <file>— no setup needed. - Integration: run
bun run pretest:integrationonce first (builds binary, sets up resources), thenbun test <file>as many times as needed.
- Always fix TypeScript errors before considering a task done.
- Never attempt to fix linting issues until the implementation is correct.
- Use
import typefor type-only imports. - MANDATORY: After editing any
.tsfile, runbun run formatto format all source files at once, orbun x prettier --write <file>for a single file.
Each command lives in src/cli/commands/. The command tree is defined in src/cli/command-tree.ts and the entry point is src/index.ts.
To add a new command: add it to src/cli/command-tree.ts and implement the logic in a new folder under src/cli/commands/.
Please declare commands using the type defined in src/cli/commands/_common/sonar-command.ts.
By default, new commands should register a authenticatedAction(), only technical commands will use anonymousAction().
Please use the exception types defined in src/cli/commands/_common/error.ts for production code. If you need to throw an error from a mock in test code, it's fine to use the generic Error type.
Error subclasses extend the abstract CliError and carry their own exitCode, which SonarCommand.runCommand() forwards to process.exitCode:
InvalidOptionError→ exit code2(conflicting or invalid CLI options).CommandFailedError→ exit code1by default, or whatever is passed to the constructor.- Any other
Errorcaught byrunCommand→ exit code1.
- Persistent state (server URL, org, project) is managed via
src/lib/state-manager.ts. - Tokens are stored in the system keychain via
src/lib/keychain.ts— never store tokens in plain files. - All path and URL constants live in
src/lib/config-constants.ts— import from there instead of hardcoding. - Caller-agent hints (Cursor, Claude Code, or Copilot CLI) from the environment:
src/lib/agent-detector.ts(detectCallerAgent, etc.). sonar auth logoutrelies on state: if there is no active connection orisAuthenticatedis false, it only reports that you are already logged out (no keychain changes).- When
sonar auth loginruns the browser-based OAuth flow, the server-generated token name returned in the callback POST body is captured and persisted on the connection astokenName(seeAuthConnectioninsrc/lib/state.ts). The wire field isname(matching/api/user_tokens/revoke?name=); we keep it astokenNamein-memory to disambiguate from other "name" fields. Tokens supplied via--with-tokenare not assigned atokenName. - On
sonar auth logout, the CLI best-effort revokes the server-side token viaSonarQubeClient.revokeUserToken(...)(a one-line wrapper over the genericpostForm(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 notokenName(e.g. authenticated with--with-token, or upgraded from an older CLI), the CLI emits a manual-revocation hint on stderr instead.
Integration tests are the default. Unit tests are justified only when a situation is genuinely hard to recreate via integration tests due to test setup complexity. Before writing a unit test, first consider extending the harness or fake server infrastructure to handle the scenario. Unit tests are a last resort.
Follow the structure of existing tests for the command or feature area you are working in.
- Unit tests:
tests/unit/— usesrc/ui/mock.tsfor UI layer,tests/unit/keychain/keychain-test-handle.tsfor keychain. - Integration tests:
tests/integration/specs/<command>/— run the compiled binary against fake servers. UseTestHarnessfromtests/integration/harness/. - E2E tests:
tests/e2e/— real external dependencies that cannot be faked: OS keychain, install scripts with real network, real SonarQube server calls, and integration with external tools. Those tests are black-box tests and exercise the product from the outside.
Before writing a test, find an existing spec for the same command area and follow its structure.
Each test creates a fresh TestHarness and disposes it in afterEach. The harness runs the compiled binary in a fully isolated environment (temp dir, fake keychain, fake servers). For fine-grained state setup beyond withAuth, use harness.state() builder (see tests/integration/harness/environment-builder.ts). For git hook tests, use initGitRepo / stageFile from tests/integration/specs/hook/git-test-helpers.ts.
To run tests with coverage and produce the LCOV reports consumed by SonarQube, use:
bun run test:coverage # full pipeline: unit + integration + merge
bun run test:coverage:unit # unit only (faster, no binary build needed)Do not use bun test --coverage directly — Bun's native LCOV reporter emits spurious entries on non-executable lines (signatures, braces, blank lines) that cause false positives in SonarQube.
When adding, removing, or changing commands, scripts, or project structure, update CLAUDE.md, and AGENTS.md to reflect the change before finishing.
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.