UNPKG

@aaronshaf/ger

Version:

Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS

284 lines (251 loc) 13.2 kB
# Architecture ## System Overview ``` ┌─────────────────────────────────────────────────────────────┐ CLI Layer ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ show comment push vote ... └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └───────┼───────────┼───────────┼───────────┼─────────────────┘ ┌───────┴───────────┴───────────┴───────────┴─────────────────┐ Service Layer ┌──────────────┐ ┌──────────────┐ GerritApi ConfigService│ Service └──────────────┘ └──────────────┘ └───────┬───────────────────────────────────────────────────────┘ ┌───────┴─────────────────────────────────────────────────────┐ External Systems ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ Gerrit API Git (spawn) AI Tools (claude/llm) └──────────────┘ └──────────────┘ └──────────────┘ └─────────────────────────────────────────────────────────────┘ ``` ## Directory Structure ``` src/ ├── cli/ # CLI interface layer ├── index.ts # Entry point, command registration ├── register-commands.ts # Main command setup ├── register-group-commands.ts # Group commands └── commands/ # Individual commands ├── show.ts ├── comment.ts ├── push.ts └── ... (27 commands) ├── api/ # External API clients └── gerrit.ts # Gerrit REST API service ├── services/ # Business logic services ├── config.ts # Configuration management ├── git-worktree.ts # Git worktree operations └── commit-hook.ts # Gerrit hook installation ├── schemas/ # Data validation schemas ├── gerrit.ts # Gerrit API types └── config.ts # Config file schema ├── utils/ # Shared utilities ├── change-id.ts # Change identifier parsing ├── git-commit.ts # Git operations ├── formatters.ts # Output formatting ├── shell-safety.ts # XML/CDATA handling └── ... (diff, comment utils) └── i18n/ # Internationalization (planned) tests/ ├── unit/ # Pure function tests ├── integration/ # API + command tests ├── mocks/ # MSW handlers └── helpers/ # Test utilities ``` ## Dependency Injection Effect Layers provide dependency injection: ```typescript // Define service interface interface GerritApiService { readonly getChange: (id: string) => Effect.Effect<ChangeInfo, ApiError> readonly listChanges: (query?: string) => Effect.Effect<ChangeInfo[], ApiError> // ... } // Create service tag const GerritApiService = Context.GenericTag<GerritApiService>('GerritApiService') // Implement live service const GerritApiServiceLive = Layer.succeed(GerritApiService, { getChange: (id) => Effect.gen(function* () { const config = yield* ConfigService const response = yield* fetchJson(`${config.host}/a/changes/${id}`) return yield* Schema.decodeUnknown(ChangeInfo)(response) }), // ... }) // Use in commands const showCommand = Effect.gen(function* () { const api = yield* GerritApiService const change = yield* api.getChange(changeId) console.log(formatChange(change)) }) // Provide layers at runtime Effect.runPromise( showCommand.pipe( Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive) ) ) ``` ## Error Handling Tagged errors with Effect Schema: ```typescript // Define error types export class ApiError extends Schema.TaggedError<ApiError>()('ApiError', { message: Schema.String, statusCode: Schema.Number, url: Schema.String, }) {} export class ConfigError extends Schema.TaggedError<ConfigError>()('ConfigError', { message: Schema.String, }) {} // Handle by tag Effect.catchTag('ApiError', (error) => { console.error(`API Error: ${error.message} (${error.statusCode})`) return Effect.succeed(null) }) // Or let errors propagate Effect.runPromise(effect).catch((error) => { if (error._tag === 'ApiError') { console.error(`API failed: ${error.message}`) } }) ``` ## Data Flow ### Read Operation (show command) ``` User: ger show 12345 ┌─────────────────────┐ Parse arguments Normalize change ID └─────────┬───────────┘ ┌─────────────────────┐ Load config (~/.ger/config.json)│ └─────────┬───────────┘ ┌─────────────────────┐ API: GET /changes Validate response └─────────┬───────────┘ ┌─────────────────────┐ Format output (text/xml/json) └─────────┬───────────┘ Console ``` ### Write Operation (comment command) ``` User: echo '...' | ger comment 12345 ┌─────────────────────┐ Parse stdin (JSON) Validate schema └─────────┬───────────┘ ┌─────────────────────┐ Load config └─────────┬───────────┘ ┌─────────────────────┐ API: POST /review (batch comments) └─────────┬───────────┘ Success/Error ``` ## Configuration Architecture ``` Priority: File > Environment > Error ┌─────────────────────────────────────┐ ~/.ger/config.json { "host": "https://gerrit.com", "username": "user", "password": "token", "aiTool": "claude", "aiAutoDetect": true } └─────────────────────────────────────┘ (file not found) ┌─────────────────────────────────────┐ Environment Variables GERRIT_HOST GERRIT_USERNAME GERRIT_PASSWORD └─────────────────────────────────────┘ (not set) ┌─────────────────────────────────────┐ ConfigError: No credentials found └─────────────────────────────────────┘ ``` ## Testing Architecture ``` ┌─────────────────────────────────────────────────────────────┐ Test Runner (Bun) └─────────────────────────────────────────────────────────────┘ ┌─────────────────────┼─────────────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ Unit Tests Integration E2E Tests Tests (manual) - Schemas - Commands - Utilities - API flows - Full CLI - Formatters - Services - Real Gerrit └───────────────┘ └───────┬───────┘ └───────────────┘ ┌───────────────────┐ MSW Handlers Mock HTTP at network level └───────────────────┘ ``` ## Security Model ``` ┌─────────────────────────────────────────────────────────────┐ Credential Storage - File: ~/.ger/config.json (mode 0600) - Never logged or printed in errors - Basic auth over HTTPS only └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ Input Validation - Schema validation on all external data - Git subprocess (no shell=true, no string interpolation) - CDATA wrapping for XML output └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ Output Sanitization - Credentials never in output - Error messages sanitized - XML special chars handled via CDATA └─────────────────────────────────────────────────────────────┘ ```