@aaronshaf/ger 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EXAMPLES.md ADDED
@@ -0,0 +1,409 @@
1
+ # Programmatic Usage Examples
2
+
3
+ This package can be used both as a CLI tool and as a library. Below are examples of using `@aaronshaf/ger` programmatically with Effect-TS.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @aaronshaf/ger
9
+ # or
10
+ npm install @aaronshaf/ger
11
+ ```
12
+
13
+ ## Basic Setup
14
+
15
+ All services in this package are built with Effect-TS, providing type-safe, composable operations.
16
+
17
+ ### Import the services
18
+
19
+ ```typescript
20
+ import { Effect, pipe } from 'effect'
21
+ import {
22
+ GerritApiService,
23
+ GerritApiServiceLive,
24
+ ConfigServiceLive,
25
+ type ChangeInfo,
26
+ } from '@aaronshaf/ger'
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ ### Using Environment Variables
32
+
33
+ Set these environment variables before running your program:
34
+
35
+ ```bash
36
+ export GERRIT_HOST="https://gerrit.example.com"
37
+ export GERRIT_USERNAME="your-username"
38
+ export GERRIT_PASSWORD="your-http-password"
39
+ ```
40
+
41
+ ### Using File-Based Config
42
+
43
+ Or run the CLI once to set up configuration:
44
+
45
+ ```bash
46
+ ger setup
47
+ ```
48
+
49
+ This stores credentials in `~/.ger/config.json`.
50
+
51
+ ## Examples
52
+
53
+ ### 1. Get Change Information
54
+
55
+ ```typescript
56
+ import { Effect, pipe } from 'effect'
57
+ import {
58
+ GerritApiService,
59
+ GerritApiServiceLive,
60
+ ConfigServiceLive,
61
+ } from '@aaronshaf/ger'
62
+
63
+ const getChangeDetails = (changeId: string) =>
64
+ Effect.gen(function* () {
65
+ const api = yield* GerritApiService
66
+ const change = yield* api.getChange(changeId)
67
+
68
+ console.log(`Change: ${change.subject}`)
69
+ console.log(`Status: ${change.status}`)
70
+ console.log(`Owner: ${change.owner?.name || 'Unknown'}`)
71
+
72
+ return change
73
+ })
74
+
75
+ // Run the program
76
+ const program = pipe(
77
+ getChangeDetails('12345'),
78
+ Effect.provide(GerritApiServiceLive),
79
+ Effect.provide(ConfigServiceLive)
80
+ )
81
+
82
+ Effect.runPromise(program)
83
+ .then(() => console.log('Done!'))
84
+ .catch(console.error)
85
+ ```
86
+
87
+ ### 2. List Open Changes
88
+
89
+ ```typescript
90
+ import { Effect, pipe } from 'effect'
91
+ import {
92
+ GerritApiService,
93
+ GerritApiServiceLive,
94
+ ConfigServiceLive,
95
+ } from '@aaronshaf/ger'
96
+
97
+ const listMyChanges = Effect.gen(function* () {
98
+ const api = yield* GerritApiService
99
+
100
+ // Query for your open changes
101
+ const changes = yield* api.listChanges('is:open owner:self')
102
+
103
+ console.log(`You have ${changes.length} open changes:`)
104
+ for (const change of changes) {
105
+ console.log(` - #${change._number}: ${change.subject}`)
106
+ }
107
+
108
+ return changes
109
+ })
110
+
111
+ const program = pipe(
112
+ listMyChanges,
113
+ Effect.provide(GerritApiServiceLive),
114
+ Effect.provide(ConfigServiceLive)
115
+ )
116
+
117
+ Effect.runPromise(program).catch(console.error)
118
+ ```
119
+
120
+ ### 3. Post a Comment
121
+
122
+ ```typescript
123
+ import { Effect, pipe } from 'effect'
124
+ import {
125
+ GerritApiService,
126
+ GerritApiServiceLive,
127
+ ConfigServiceLive,
128
+ type ReviewInput,
129
+ } from '@aaronshaf/ger'
130
+
131
+ const postComment = (changeId: string) =>
132
+ Effect.gen(function* () {
133
+ const api = yield* GerritApiService
134
+
135
+ const review: ReviewInput = {
136
+ message: 'Looks good to me!',
137
+ labels: {
138
+ 'Code-Review': 1,
139
+ },
140
+ }
141
+
142
+ yield* api.postReview(changeId, review)
143
+ console.log('Comment posted successfully!')
144
+ })
145
+
146
+ const program = pipe(
147
+ postComment('12345'),
148
+ Effect.provide(GerritApiServiceLive),
149
+ Effect.provide(ConfigServiceLive)
150
+ )
151
+
152
+ Effect.runPromise(program).catch(console.error)
153
+ ```
154
+
155
+ ### 4. Post Inline Comments
156
+
157
+ ```typescript
158
+ import { Effect, pipe } from 'effect'
159
+ import {
160
+ GerritApiService,
161
+ GerritApiServiceLive,
162
+ ConfigServiceLive,
163
+ type ReviewInput,
164
+ } from '@aaronshaf/ger'
165
+
166
+ const postInlineComments = (changeId: string) =>
167
+ Effect.gen(function* () {
168
+ const api = yield* GerritApiService
169
+
170
+ const review: ReviewInput = {
171
+ message: 'Review complete',
172
+ comments: {
173
+ 'src/api.ts': [
174
+ {
175
+ line: 42,
176
+ message: 'Consider using const here for immutability',
177
+ unresolved: false,
178
+ },
179
+ {
180
+ line: 55,
181
+ message: 'This could cause a security issue',
182
+ unresolved: true,
183
+ },
184
+ ],
185
+ 'src/utils.ts': [
186
+ {
187
+ line: 10,
188
+ message: 'Nice refactor!',
189
+ },
190
+ ],
191
+ },
192
+ }
193
+
194
+ yield* api.postReview(changeId, review)
195
+ console.log('Inline comments posted!')
196
+ })
197
+
198
+ const program = pipe(
199
+ postInlineComments('12345'),
200
+ Effect.provide(GerritApiServiceLive),
201
+ Effect.provide(ConfigServiceLive)
202
+ )
203
+
204
+ Effect.runPromise(program).catch(console.error)
205
+ ```
206
+
207
+ ### 5. Get Diff for a Change
208
+
209
+ ```typescript
210
+ import { Effect, pipe } from 'effect'
211
+ import {
212
+ GerritApiService,
213
+ GerritApiServiceLive,
214
+ ConfigServiceLive,
215
+ type DiffOptions,
216
+ } from '@aaronshaf/ger'
217
+
218
+ const getDiff = (changeId: string) =>
219
+ Effect.gen(function* () {
220
+ const api = yield* GerritApiService
221
+
222
+ // Get unified diff format (default)
223
+ const diff = yield* api.getDiff(changeId, { format: 'unified' })
224
+ console.log('Diff:', diff)
225
+
226
+ // Or get list of changed files
227
+ const files = yield* api.getDiff(changeId, { format: 'files' })
228
+ console.log('Changed files:', files)
229
+
230
+ return diff
231
+ })
232
+
233
+ const program = pipe(
234
+ getDiff('12345'),
235
+ Effect.provide(GerritApiServiceLive),
236
+ Effect.provide(ConfigServiceLive)
237
+ )
238
+
239
+ Effect.runPromise(program).catch(console.error)
240
+ ```
241
+
242
+ ### 6. Test Connection
243
+
244
+ ```typescript
245
+ import { Effect, pipe } from 'effect'
246
+ import {
247
+ GerritApiService,
248
+ GerritApiServiceLive,
249
+ ConfigServiceLive,
250
+ } from '@aaronshaf/ger'
251
+
252
+ const testConnection = Effect.gen(function* () {
253
+ const api = yield* GerritApiService
254
+ const isConnected = yield* api.testConnection
255
+
256
+ if (isConnected) {
257
+ console.log('✓ Connected to Gerrit!')
258
+ } else {
259
+ console.log('✗ Connection failed')
260
+ }
261
+
262
+ return isConnected
263
+ })
264
+
265
+ const program = pipe(
266
+ testConnection,
267
+ Effect.provide(GerritApiServiceLive),
268
+ Effect.provide(ConfigServiceLive)
269
+ )
270
+
271
+ Effect.runPromise(program).catch(console.error)
272
+ ```
273
+
274
+ ### 7. Error Handling with Effect
275
+
276
+ ```typescript
277
+ import { Effect, pipe, Console } from 'effect'
278
+ import {
279
+ GerritApiService,
280
+ GerritApiServiceLive,
281
+ ConfigServiceLive,
282
+ ApiError,
283
+ ConfigError,
284
+ } from '@aaronshaf/ger'
285
+
286
+ const safeGetChange = (changeId: string) =>
287
+ Effect.gen(function* () {
288
+ const api = yield* GerritApiService
289
+ const change = yield* api.getChange(changeId)
290
+ return change
291
+ }).pipe(
292
+ Effect.catchTag('ApiError', (error) =>
293
+ Console.error(`API Error: ${error.message}`).pipe(
294
+ Effect.map(() => null)
295
+ )
296
+ ),
297
+ Effect.catchTag('ConfigError', (error) =>
298
+ Console.error(`Config Error: ${error.message}`).pipe(
299
+ Effect.map(() => null)
300
+ )
301
+ )
302
+ )
303
+
304
+ const program = pipe(
305
+ safeGetChange('invalid-change'),
306
+ Effect.provide(GerritApiServiceLive),
307
+ Effect.provide(ConfigServiceLive)
308
+ )
309
+
310
+ Effect.runPromise(program)
311
+ ```
312
+
313
+ ### 8. Using Utilities
314
+
315
+ ```typescript
316
+ import {
317
+ normalizeChangeIdentifier,
318
+ extractChangeIdFromCommitMessage,
319
+ extractChangeNumber,
320
+ normalizeGerritHost,
321
+ } from '@aaronshaf/ger'
322
+
323
+ // Normalize change identifiers
324
+ const normalized = normalizeChangeIdentifier('12345')
325
+ // or
326
+ const normalizedId = normalizeChangeIdentifier('If5a3ae8cb5a107e187447802358417f311d0c4b1')
327
+
328
+ // Extract change ID from commit message
329
+ const commitMsg = `feat: add feature
330
+
331
+ Change-Id: If5a3ae8cb5a107e187447802358417f311d0c4b1`
332
+
333
+ const changeId = extractChangeIdFromCommitMessage(commitMsg)
334
+ console.log(changeId) // "If5a3ae8cb5a107e187447802358417f311d0c4b1"
335
+
336
+ // Extract change number from Gerrit URL
337
+ const url = 'https://gerrit.example.com/c/project/+/12345'
338
+ const changeNumber = extractChangeNumber(url)
339
+ console.log(changeNumber) // "12345"
340
+
341
+ // Normalize Gerrit host
342
+ const host = normalizeGerritHost('gerrit.example.com')
343
+ console.log(host) // "https://gerrit.example.com"
344
+ ```
345
+
346
+ ### 9. Working with Schemas
347
+
348
+ ```typescript
349
+ import { Schema } from '@effect/schema'
350
+ import { Effect } from 'effect'
351
+ import { ChangeInfo, ReviewInput } from '@aaronshaf/ger'
352
+
353
+ // Validate and decode API responses
354
+ const validateChange = (data: unknown) =>
355
+ Schema.decodeUnknown(ChangeInfo)(data)
356
+
357
+ // Validate review input before sending
358
+ const validateReview = (review: unknown) =>
359
+ Schema.decodeUnknown(ReviewInput)(review)
360
+
361
+ // Use in an Effect program
362
+ const safeReview = Effect.gen(function* () {
363
+ const review = {
364
+ message: 'LGTM',
365
+ labels: { 'Code-Review': 2 },
366
+ }
367
+
368
+ const validated = yield* validateReview(review)
369
+ console.log('Review is valid:', validated)
370
+
371
+ return validated
372
+ })
373
+ ```
374
+
375
+ ## Direct Module Access
376
+
377
+ You can also import directly from specific modules:
378
+
379
+ ```typescript
380
+ // Import from specific services
381
+ import { GerritApiService, GerritApiServiceLive } from '@aaronshaf/ger/api'
382
+ import { ConfigService, ConfigServiceLive } from '@aaronshaf/ger/services/config'
383
+
384
+ // Import from specific schemas
385
+ import { ChangeInfo, ReviewInput } from '@aaronshaf/ger/schemas/gerrit'
386
+
387
+ // Import utilities
388
+ import { normalizeChangeIdentifier, extractChangeNumber } from '@aaronshaf/ger/utils'
389
+ ```
390
+
391
+ ## TypeScript Configuration
392
+
393
+ Make sure your `tsconfig.json` includes:
394
+
395
+ ```json
396
+ {
397
+ "compilerOptions": {
398
+ "moduleResolution": "bundler",
399
+ "allowImportingTsExtensions": true,
400
+ "strict": true
401
+ }
402
+ }
403
+ ```
404
+
405
+ ## More Information
406
+
407
+ - See the [main README](./README.md) for CLI usage
408
+ - Check out the [Effect documentation](https://effect.website/) to learn more about Effect-TS
409
+ - View the type definitions in your IDE for detailed API documentation
package/README.md CHANGED
@@ -199,7 +199,7 @@ ger comments 12345 --pretty
199
199
  Extract URLs from change messages and comments for automation and scripting:
200
200
 
201
201
  ```bash
202
- # Extract all Jenkins build-summary-report URLs
202
+ # Extract URLs from current HEAD commit's change (auto-detect)
203
203
  ger extract-url "build-summary-report"
204
204
 
205
205
  # Get the latest build URL (using tail)
@@ -208,9 +208,15 @@ ger extract-url "build-summary-report" | tail -1
208
208
  # Get the first/oldest build URL (using head)
209
209
  ger extract-url "jenkins" | head -1
210
210
 
211
- # For a specific change
211
+ # For a specific change (using change number)
212
212
  ger extract-url "build-summary" 12345
213
213
 
214
+ # For a specific change (using Change-ID)
215
+ ger extract-url "build-summary" If5a3ae8cb5a107e187447802358417f311d0c4b1
216
+
217
+ # Chain with other tools for specific change
218
+ ger extract-url "build-summary-report" 12345 | tail -1 | jk failures --smart --xml
219
+
214
220
  # Use regex for precise matching
215
221
  ger extract-url "job/Canvas/job/main/\d+/" --regex
216
222
 
@@ -225,6 +231,7 @@ ger extract-url "jenkins" --xml
225
231
  ```
226
232
 
227
233
  #### How it works:
234
+ - **Change detection**: Auto-detects Change-ID from HEAD commit if not specified, or accepts explicit change number/Change-ID
228
235
  - **Pattern matching**: Substring match by default, regex with `--regex`
229
236
  - **Sources**: Searches messages by default, add `--include-comments` to include inline comments
230
237
  - **Ordering**: URLs are output in chronological order (oldest first)
package/index.ts ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @aaronshaf/ger - Gerrit CLI and SDK
3
+ *
4
+ * This package provides both a CLI tool and a programmatic API for interacting with Gerrit Code Review.
5
+ * Built with Effect-TS for type-safe, composable operations.
6
+ *
7
+ * @module
8
+ *
9
+ * @example Basic usage with Effect
10
+ * ```typescript
11
+ * import { Effect, pipe } from 'effect'
12
+ * import {
13
+ * GerritApiService,
14
+ * GerritApiServiceLive,
15
+ * ConfigServiceLive,
16
+ * } from '@aaronshaf/ger'
17
+ *
18
+ * const program = Effect.gen(function* () {
19
+ * const api = yield* GerritApiService
20
+ * const change = yield* api.getChange('12345')
21
+ * console.log(change.subject)
22
+ * })
23
+ *
24
+ * const runnable = pipe(
25
+ * program,
26
+ * Effect.provide(GerritApiServiceLive),
27
+ * Effect.provide(ConfigServiceLive)
28
+ * )
29
+ *
30
+ * Effect.runPromise(runnable)
31
+ * ```
32
+ */
33
+
34
+ // ============================================================================
35
+ // Core API Service
36
+ // ============================================================================
37
+
38
+ export {
39
+ // Service tag and implementation
40
+ GerritApiService,
41
+ GerritApiServiceLive,
42
+ // Types
43
+ type GerritApiServiceImpl,
44
+ // Errors
45
+ ApiError,
46
+ type ApiErrorFields,
47
+ } from './src/api/gerrit'
48
+
49
+ // ============================================================================
50
+ // Configuration Service
51
+ // ============================================================================
52
+
53
+ export {
54
+ // Service tag and implementation
55
+ ConfigService,
56
+ ConfigServiceLive,
57
+ // Types
58
+ type ConfigServiceImpl,
59
+ // Errors
60
+ ConfigError,
61
+ type ConfigErrorFields,
62
+ } from './src/services/config'
63
+
64
+ // ============================================================================
65
+ // Review Strategy Service
66
+ // ============================================================================
67
+
68
+ export {
69
+ // Strategy types
70
+ type ReviewStrategy,
71
+ // Built-in strategies
72
+ claudeCliStrategy,
73
+ geminiCliStrategy,
74
+ openCodeCliStrategy,
75
+ // Service
76
+ ReviewStrategyService,
77
+ ReviewStrategyServiceLive,
78
+ type ReviewStrategyServiceImpl,
79
+ // Errors
80
+ ReviewStrategyError,
81
+ type ReviewStrategyErrorFields,
82
+ } from './src/services/review-strategy'
83
+
84
+ // ============================================================================
85
+ // Git Worktree Service
86
+ // ============================================================================
87
+
88
+ export {
89
+ // Service tag and implementation
90
+ GitWorktreeService,
91
+ GitWorktreeServiceLive,
92
+ type GitWorktreeServiceImpl,
93
+ // Types
94
+ type WorktreeInfo,
95
+ // Errors
96
+ WorktreeCreationError,
97
+ type WorktreeCreationErrorFields,
98
+ PatchsetFetchError,
99
+ type PatchsetFetchErrorFields,
100
+ DirtyRepoError,
101
+ type DirtyRepoErrorFields,
102
+ NotGitRepoError,
103
+ type NotGitRepoErrorFields,
104
+ type GitWorktreeError,
105
+ } from './src/services/git-worktree'
106
+
107
+ // ============================================================================
108
+ // Schemas and Types
109
+ // ============================================================================
110
+
111
+ export {
112
+ // Authentication
113
+ GerritCredentials,
114
+ type GerritCredentials as GerritCredentialsType,
115
+ // Changes
116
+ ChangeInfo,
117
+ type ChangeInfo as ChangeInfoType,
118
+ // Comments
119
+ CommentInput,
120
+ type CommentInput as CommentInputType,
121
+ CommentInfo,
122
+ type CommentInfo as CommentInfoType,
123
+ // Messages
124
+ MessageInfo,
125
+ type MessageInfo as MessageInfoType,
126
+ // Reviews
127
+ ReviewInput,
128
+ type ReviewInput as ReviewInputType,
129
+ // Files and Diffs
130
+ FileInfo,
131
+ type FileInfo as FileInfoType,
132
+ FileDiffContent,
133
+ type FileDiffContent as FileDiffContentType,
134
+ RevisionInfo,
135
+ type RevisionInfo as RevisionInfoType,
136
+ // Diff Options
137
+ DiffFormat,
138
+ type DiffFormat as DiffFormatType,
139
+ DiffOptions,
140
+ type DiffOptions as DiffOptionsType,
141
+ DiffCommandOptions,
142
+ type DiffCommandOptions as DiffCommandOptionsType,
143
+ // Errors
144
+ GerritError,
145
+ type GerritError as GerritErrorType,
146
+ } from './src/schemas/gerrit'
147
+
148
+ export {
149
+ // Config schemas
150
+ AppConfig,
151
+ type AppConfig as AppConfigType,
152
+ AiConfig,
153
+ type AiConfig as AiConfigType,
154
+ // Utilities
155
+ aiConfigFromFlat,
156
+ migrateFromNestedConfig,
157
+ } from './src/schemas/config'
158
+
159
+ // ============================================================================
160
+ // Utilities
161
+ // ============================================================================
162
+
163
+ export {
164
+ // Change ID handling
165
+ normalizeChangeIdentifier,
166
+ isChangeId,
167
+ isChangeNumber,
168
+ isValidChangeIdentifier,
169
+ getIdentifierType,
170
+ } from './src/utils/change-id'
171
+
172
+ export {
173
+ // Git commit utilities
174
+ extractChangeIdFromCommitMessage,
175
+ getLastCommitMessage,
176
+ getChangeIdFromHead,
177
+ GitError,
178
+ NoChangeIdError,
179
+ } from './src/utils/git-commit'
180
+
181
+ export {
182
+ // URL parsing
183
+ extractChangeNumber,
184
+ normalizeGerritHost,
185
+ isValidChangeId,
186
+ } from './src/utils/url-parser'
187
+
188
+ export {
189
+ // Message filtering
190
+ filterMeaningfulMessages,
191
+ sortMessagesByDate,
192
+ } from './src/utils/message-filters'
193
+
194
+ export {
195
+ // Shell safety
196
+ sanitizeCDATA,
197
+ } from './src/utils/shell-safety'
198
+
199
+ export {
200
+ // Formatters
201
+ formatDate,
202
+ getStatusIndicator,
203
+ colors,
204
+ } from './src/utils/formatters'
205
+
206
+ export {
207
+ // Comment formatters
208
+ formatCommentsPretty,
209
+ formatCommentsXml,
210
+ type CommentWithContext,
211
+ } from './src/utils/comment-formatters'
212
+
213
+ export {
214
+ // Diff formatters
215
+ formatDiffPretty,
216
+ formatDiffSummary,
217
+ formatFilesList,
218
+ extractDiffStats,
219
+ } from './src/utils/diff-formatters'
package/package.json CHANGED
@@ -1,11 +1,56 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
+ "description": "Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS",
5
+ "keywords": [
6
+ "gerrit",
7
+ "code-review",
8
+ "cli",
9
+ "sdk",
10
+ "effect",
11
+ "effect-ts",
12
+ "typescript",
13
+ "api-client"
14
+ ],
4
15
  "module": "index.ts",
5
16
  "type": "module",
6
17
  "bin": {
7
18
  "ger": "./bin/ger"
8
19
  },
20
+ "exports": {
21
+ ".": {
22
+ "import": "./index.ts",
23
+ "types": "./index.ts"
24
+ },
25
+ "./api": {
26
+ "import": "./src/api/gerrit.ts",
27
+ "types": "./src/api/gerrit.ts"
28
+ },
29
+ "./services/config": {
30
+ "import": "./src/services/config.ts",
31
+ "types": "./src/services/config.ts"
32
+ },
33
+ "./services/review-strategy": {
34
+ "import": "./src/services/review-strategy.ts",
35
+ "types": "./src/services/review-strategy.ts"
36
+ },
37
+ "./services/git-worktree": {
38
+ "import": "./src/services/git-worktree.ts",
39
+ "types": "./src/services/git-worktree.ts"
40
+ },
41
+ "./schemas/gerrit": {
42
+ "import": "./src/schemas/gerrit.ts",
43
+ "types": "./src/schemas/gerrit.ts"
44
+ },
45
+ "./schemas/config": {
46
+ "import": "./src/schemas/config.ts",
47
+ "types": "./src/schemas/config.ts"
48
+ },
49
+ "./utils": {
50
+ "import": "./src/utils/index.ts",
51
+ "types": "./src/utils/index.ts"
52
+ }
53
+ },
9
54
  "repository": {
10
55
  "type": "git",
11
56
  "url": "git+https://github.com/aaronshaf/ger.git"
@@ -1,22 +1,8 @@
1
- import { Effect, pipe, Schema, Layer } from 'effect'
2
- import {
3
- ReviewStrategyService,
4
- type ReviewStrategy,
5
- ReviewStrategyError,
6
- } from '@/services/review-strategy'
1
+ import { Effect, pipe, Schema } from 'effect'
2
+ import { ReviewStrategyService, ReviewStrategyError } from '@/services/review-strategy'
7
3
  import { commentCommandWithInput } from './comment'
8
4
  import { Console } from 'effect'
9
- import { type ApiError, GerritApiService } from '@/api/gerrit'
10
- import type { CommentInfo } from '@/schemas/gerrit'
11
- import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
12
- import { formatDiffPretty } from '@/utils/diff-formatters'
13
- import { formatDate } from '@/utils/formatters'
14
- import {
15
- formatChangeAsXML,
16
- formatCommentsAsXML,
17
- formatMessagesAsXML,
18
- flattenComments,
19
- } from '@/utils/review-formatters'
5
+ import { GerritApiService } from '@/api/gerrit'
20
6
  import { buildEnhancedPrompt } from '@/utils/review-prompt-builder'
21
7
  import * as fs from 'node:fs/promises'
22
8
  import * as fsSync from 'node:fs'
@@ -25,7 +11,7 @@ import * as path from 'node:path'
25
11
  import { fileURLToPath } from 'node:url'
26
12
  import { dirname } from 'node:path'
27
13
  import * as readline from 'node:readline'
28
- import { GitWorktreeService, GitWorktreeServiceLive } from '@/services/git-worktree'
14
+ import { GitWorktreeService } from '@/services/git-worktree'
29
15
 
30
16
  // Get the directory of this module
31
17
  const __filename = fileURLToPath(import.meta.url)
@@ -126,7 +112,7 @@ const validateAndFixInlineComments = (
126
112
  for (const rawComment of rawComments) {
127
113
  // Validate comment structure using Effect Schema
128
114
  const parseResult = yield* Schema.decodeUnknown(InlineCommentSchema)(rawComment).pipe(
129
- Effect.catchTag('ParseError', (parseError) =>
115
+ Effect.catchTag('ParseError', (_parseError) =>
130
116
  Effect.gen(function* () {
131
117
  yield* Console.warn('Skipping comment with invalid structure')
132
118
  return yield* Effect.succeed(null)
@@ -192,118 +178,6 @@ const validateAndFixInlineComments = (
192
178
  return validComments
193
179
  })
194
180
 
195
- // Legacy helper for backward compatibility (will be removed)
196
- const getChangeDataAsXml = (changeId: string): Effect.Effect<string, ApiError, GerritApiService> =>
197
- Effect.gen(function* () {
198
- const gerritApi = yield* GerritApiService
199
-
200
- // Fetch all data
201
- const change = yield* gerritApi.getChange(changeId)
202
- const diffResult = yield* gerritApi.getDiff(changeId)
203
- const diff = typeof diffResult === 'string' ? diffResult : JSON.stringify(diffResult)
204
- const commentsMap = yield* gerritApi.getComments(changeId)
205
- const messages = yield* gerritApi.getMessages(changeId)
206
-
207
- const comments = flattenComments(commentsMap)
208
-
209
- // Build XML string using helper functions
210
- const xmlLines: string[] = []
211
- xmlLines.push(`<?xml version="1.0" encoding="UTF-8"?>`)
212
- xmlLines.push(`<show_result>`)
213
- xmlLines.push(` <status>success</status>`)
214
- xmlLines.push(...formatChangeAsXML(change))
215
- xmlLines.push(` <diff><![CDATA[${sanitizeCDATA(diff)}]]></diff>`)
216
- xmlLines.push(...formatCommentsAsXML(comments))
217
- xmlLines.push(...formatMessagesAsXML(messages))
218
- xmlLines.push(`</show_result>`)
219
-
220
- return xmlLines.join('\n')
221
- })
222
-
223
- // Helper to get change data and format as pretty string
224
- const getChangeDataAsPretty = (
225
- changeId: string,
226
- ): Effect.Effect<string, ApiError, GerritApiService> =>
227
- Effect.gen(function* () {
228
- const gerritApi = yield* GerritApiService
229
-
230
- // Fetch all data
231
- const change = yield* gerritApi.getChange(changeId)
232
- const diffResult = yield* gerritApi.getDiff(changeId)
233
- const diff = typeof diffResult === 'string' ? diffResult : JSON.stringify(diffResult)
234
- const commentsMap = yield* gerritApi.getComments(changeId)
235
- const messages = yield* gerritApi.getMessages(changeId)
236
-
237
- const comments = flattenComments(commentsMap)
238
-
239
- // Build pretty string
240
- const lines: string[] = []
241
-
242
- // Change details header
243
- lines.push('━'.repeat(80))
244
- lines.push(`📋 Change ${change._number}: ${change.subject}`)
245
- lines.push('━'.repeat(80))
246
- lines.push('')
247
-
248
- // Metadata
249
- lines.push('📝 Details:')
250
- lines.push(` Project: ${change.project}`)
251
- lines.push(` Branch: ${change.branch}`)
252
- lines.push(` Status: ${change.status}`)
253
- lines.push(` Owner: ${change.owner?.name || change.owner?.email || 'Unknown'}`)
254
- lines.push(` Created: ${change.created ? formatDate(change.created) : 'Unknown'}`)
255
- lines.push(` Updated: ${change.updated ? formatDate(change.updated) : 'Unknown'}`)
256
- lines.push(` Change-Id: ${change.change_id}`)
257
- lines.push('')
258
-
259
- // Diff section
260
- lines.push('🔍 Diff:')
261
- lines.push('─'.repeat(40))
262
- lines.push(formatDiffPretty(diff))
263
- lines.push('')
264
-
265
- // Comments section
266
- if (comments.length > 0) {
267
- lines.push('💬 Inline Comments:')
268
- lines.push('─'.repeat(40))
269
- for (const comment of comments) {
270
- const author = comment.author?.name || 'Unknown'
271
- const date = comment.updated ? formatDate(comment.updated) : 'Unknown'
272
- lines.push(`📅 ${date} - ${author}`)
273
- if (comment.path) lines.push(` File: ${comment.path}`)
274
- if (comment.line) lines.push(` Line: ${comment.line}`)
275
- lines.push(` ${comment.message}`)
276
- if (comment.unresolved) lines.push(` ⚠️ Unresolved`)
277
- lines.push('')
278
- }
279
- }
280
-
281
- // Messages section
282
- if (messages.length > 0) {
283
- lines.push('📝 Review Activity:')
284
- lines.push('─'.repeat(40))
285
- for (const message of messages) {
286
- const author = message.author?.name || 'Unknown'
287
- const date = formatDate(message.date)
288
- const cleanMessage = message.message.trim()
289
-
290
- // Skip very short automated messages
291
- if (
292
- cleanMessage.length < 10 &&
293
- (cleanMessage.includes('Build') || cleanMessage.includes('Patch'))
294
- ) {
295
- continue
296
- }
297
-
298
- lines.push(`📅 ${date} - ${author}`)
299
- lines.push(` ${cleanMessage}`)
300
- lines.push('')
301
- }
302
- }
303
-
304
- return lines.join('\n')
305
- })
306
-
307
181
  // Helper function to prompt user for confirmation
308
182
  const promptUser = (message: string): Effect.Effect<boolean, never> =>
309
183
  Effect.async<boolean, never>((resume) => {
@@ -11,6 +11,7 @@ import { AppConfig } from '@/schemas/config'
11
11
  import { Schema } from '@effect/schema'
12
12
  import { input, password } from '@inquirer/prompts'
13
13
  import { spawn } from 'node:child_process'
14
+ import { normalizeGerritHost } from '@/utils/url-parser'
14
15
 
15
16
  // Check if a command exists on the system
16
17
  const checkCommandExists = (command: string): Promise<boolean> =>
@@ -209,7 +210,7 @@ const setupEffect = (configService: ConfigServiceImpl) =>
209
210
 
210
211
  // Build flat config
211
212
  const configData = {
212
- host: host.trim().replace(/\/$/, ''), // Remove trailing slash
213
+ host: normalizeGerritHost(host),
213
214
  username: username.trim(),
214
215
  password: passwordValue,
215
216
  ...(aiToolCommand && {
package/src/cli/index.ts CHANGED
@@ -56,7 +56,7 @@ function getVersion(): string {
56
56
  const packageJsonPath = join(__dirname, '..', '..', 'package.json')
57
57
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
58
58
  return packageJson.version || '0.0.0'
59
- } catch (error) {
59
+ } catch {
60
60
  // Fallback version if package.json can't be read
61
61
  return '0.0.0'
62
62
  }
@@ -84,7 +84,11 @@ export class NotGitRepoError
84
84
  readonly name = 'NotGitRepoError'
85
85
  }
86
86
 
87
- export type GitError = WorktreeCreationError | PatchsetFetchError | DirtyRepoError | NotGitRepoError
87
+ export type GitWorktreeError =
88
+ | WorktreeCreationError
89
+ | PatchsetFetchError
90
+ | DirtyRepoError
91
+ | NotGitRepoError
88
92
 
89
93
  // Worktree info
90
94
  export interface WorktreeInfo {
@@ -99,8 +103,8 @@ export interface WorktreeInfo {
99
103
  const runGitCommand = (
100
104
  args: string[],
101
105
  options: { cwd?: string } = {},
102
- ): Effect.Effect<string, GitError, never> =>
103
- Effect.async<string, GitError, never>((resume) => {
106
+ ): Effect.Effect<string, GitWorktreeError, never> =>
107
+ Effect.async<string, GitWorktreeError, never>((resume) => {
104
108
  const child = spawn('git', args, {
105
109
  cwd: options.cwd || process.cwd(),
106
110
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -156,23 +160,6 @@ const validateGitRepo = (): Effect.Effect<void, NotGitRepoError, never> =>
156
160
  Effect.map(() => undefined),
157
161
  )
158
162
 
159
- // Check if working directory is clean
160
- const validateCleanRepo = (): Effect.Effect<void, DirtyRepoError, never> =>
161
- pipe(
162
- runGitCommand(['status', '--porcelain']),
163
- Effect.mapError(() => new DirtyRepoError({ message: 'Failed to check repository status' })),
164
- Effect.flatMap((output) =>
165
- output.trim() === ''
166
- ? Effect.succeed(undefined)
167
- : Effect.fail(
168
- new DirtyRepoError({
169
- message:
170
- 'Working directory has uncommitted changes. Please commit or stash changes before review.',
171
- }),
172
- ),
173
- ),
174
- )
175
-
176
163
  // Generate unique worktree path
177
164
  const generateWorktreePath = (changeId: string): string => {
178
165
  const timestamp = Date.now()
@@ -199,7 +186,7 @@ const buildRefspec = (changeNumber: string, patchsetNumber: number = 1): string
199
186
  }
200
187
 
201
188
  // Get the current HEAD commit hash to avoid branch conflicts
202
- const getCurrentCommit = (): Effect.Effect<string, GitError, never> =>
189
+ const getCurrentCommit = (): Effect.Effect<string, GitWorktreeError, never> =>
203
190
  pipe(
204
191
  runGitCommand(['rev-parse', 'HEAD']),
205
192
  Effect.map((output) => output.trim()),
@@ -243,11 +230,13 @@ const getLatestPatchsetNumber = (
243
230
 
244
231
  // GitWorktreeService implementation
245
232
  export interface GitWorktreeServiceImpl {
246
- validatePreconditions: () => Effect.Effect<void, GitError, never>
247
- createWorktree: (changeId: string) => Effect.Effect<WorktreeInfo, GitError, never>
248
- fetchAndCheckoutPatchset: (worktreeInfo: WorktreeInfo) => Effect.Effect<void, GitError, never>
233
+ validatePreconditions: () => Effect.Effect<void, GitWorktreeError, never>
234
+ createWorktree: (changeId: string) => Effect.Effect<WorktreeInfo, GitWorktreeError, never>
235
+ fetchAndCheckoutPatchset: (
236
+ worktreeInfo: WorktreeInfo,
237
+ ) => Effect.Effect<void, GitWorktreeError, never>
249
238
  cleanup: (worktreeInfo: WorktreeInfo) => Effect.Effect<void, never, never>
250
- getChangedFiles: () => Effect.Effect<string[], GitError, never>
239
+ getChangedFiles: () => Effect.Effect<string[], GitWorktreeError, never>
251
240
  }
252
241
 
253
242
  const GitWorktreeServiceImplLive: GitWorktreeServiceImpl = {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Utility functions for working with Gerrit
3
+ * @module utils
4
+ */
5
+
6
+ // Change ID utilities
7
+ export {
8
+ normalizeChangeIdentifier,
9
+ isChangeId,
10
+ isChangeNumber,
11
+ isValidChangeIdentifier,
12
+ getIdentifierType,
13
+ } from './change-id'
14
+
15
+ // Git commit utilities
16
+ export {
17
+ extractChangeIdFromCommitMessage,
18
+ getLastCommitMessage,
19
+ getChangeIdFromHead,
20
+ GitError,
21
+ NoChangeIdError,
22
+ } from './git-commit'
23
+
24
+ // URL parsing
25
+ export {
26
+ extractChangeNumber,
27
+ normalizeGerritHost,
28
+ isValidChangeId,
29
+ } from './url-parser'
30
+
31
+ // Message filtering
32
+ export { filterMeaningfulMessages, sortMessagesByDate } from './message-filters'
33
+
34
+ // Shell safety
35
+ export { sanitizeCDATA } from './shell-safety'
36
+
37
+ // Formatters
38
+ export {
39
+ formatDate,
40
+ getStatusIndicator,
41
+ colors,
42
+ } from './formatters'
43
+
44
+ export {
45
+ formatCommentsPretty,
46
+ formatCommentsXml,
47
+ type CommentWithContext,
48
+ } from './comment-formatters'
49
+
50
+ export {
51
+ formatDiffPretty,
52
+ formatDiffSummary,
53
+ formatFilesList,
54
+ extractDiffStats,
55
+ } from './diff-formatters'
@@ -1,6 +1,5 @@
1
1
  import { Effect } from 'effect'
2
2
  import { type ApiError, GerritApiService } from '@/api/gerrit'
3
- import type { CommentInfo, MessageInfo } from '@/schemas/gerrit'
4
3
  import { flattenComments } from '@/utils/review-formatters'
5
4
 
6
5
  export const buildEnhancedPrompt = (
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { extractChangeNumber, isValidChangeId } from './url-parser'
2
+ import { extractChangeNumber, isValidChangeId, normalizeGerritHost } from './url-parser'
3
3
 
4
4
  describe('extractChangeNumber', () => {
5
5
  test('extracts change number from standard Gerrit URL', () => {
@@ -121,3 +121,151 @@ describe('isValidChangeId', () => {
121
121
  expect(isValidChangeId('-abc')).toBe(false)
122
122
  })
123
123
  })
124
+
125
+ describe('normalizeGerritHost', () => {
126
+ describe('adding protocol', () => {
127
+ test('adds https:// when no protocol is provided', () => {
128
+ expect(normalizeGerritHost('gerrit.example.com')).toBe('https://gerrit.example.com')
129
+ })
130
+
131
+ test('adds https:// to hostname with port', () => {
132
+ expect(normalizeGerritHost('gerrit.example.com:8080')).toBe('https://gerrit.example.com:8080')
133
+ })
134
+
135
+ test('adds https:// to localhost', () => {
136
+ expect(normalizeGerritHost('localhost:8080')).toBe('https://localhost:8080')
137
+ })
138
+
139
+ test('adds https:// to IP address', () => {
140
+ expect(normalizeGerritHost('192.168.1.100')).toBe('https://192.168.1.100')
141
+ })
142
+
143
+ test('adds https:// to IP address with port', () => {
144
+ expect(normalizeGerritHost('192.168.1.100:8080')).toBe('https://192.168.1.100:8080')
145
+ })
146
+ })
147
+
148
+ describe('preserving existing protocol', () => {
149
+ test('preserves https:// when already present', () => {
150
+ expect(normalizeGerritHost('https://gerrit.example.com')).toBe('https://gerrit.example.com')
151
+ })
152
+
153
+ test('preserves http:// when explicitly provided', () => {
154
+ expect(normalizeGerritHost('http://gerrit.example.com')).toBe('http://gerrit.example.com')
155
+ })
156
+
157
+ test('preserves https:// with port', () => {
158
+ expect(normalizeGerritHost('https://gerrit.example.com:8080')).toBe(
159
+ 'https://gerrit.example.com:8080',
160
+ )
161
+ })
162
+
163
+ test('preserves http:// with port', () => {
164
+ expect(normalizeGerritHost('http://gerrit.example.com:8080')).toBe(
165
+ 'http://gerrit.example.com:8080',
166
+ )
167
+ })
168
+ })
169
+
170
+ describe('removing trailing slashes', () => {
171
+ test('removes single trailing slash', () => {
172
+ expect(normalizeGerritHost('https://gerrit.example.com/')).toBe('https://gerrit.example.com')
173
+ })
174
+
175
+ test('removes trailing slash from URL without protocol', () => {
176
+ expect(normalizeGerritHost('gerrit.example.com/')).toBe('https://gerrit.example.com')
177
+ })
178
+
179
+ test('removes trailing slash from URL with port', () => {
180
+ expect(normalizeGerritHost('https://gerrit.example.com:8080/')).toBe(
181
+ 'https://gerrit.example.com:8080',
182
+ )
183
+ })
184
+
185
+ test('handles URL without trailing slash', () => {
186
+ expect(normalizeGerritHost('https://gerrit.example.com')).toBe('https://gerrit.example.com')
187
+ })
188
+
189
+ test('does not remove slash from path', () => {
190
+ expect(normalizeGerritHost('https://gerrit.example.com/gerrit')).toBe(
191
+ 'https://gerrit.example.com/gerrit',
192
+ )
193
+ })
194
+
195
+ test('removes trailing slash from path', () => {
196
+ expect(normalizeGerritHost('https://gerrit.example.com/gerrit/')).toBe(
197
+ 'https://gerrit.example.com/gerrit',
198
+ )
199
+ })
200
+ })
201
+
202
+ describe('whitespace handling', () => {
203
+ test('trims leading whitespace', () => {
204
+ expect(normalizeGerritHost(' gerrit.example.com')).toBe('https://gerrit.example.com')
205
+ })
206
+
207
+ test('trims trailing whitespace', () => {
208
+ expect(normalizeGerritHost('gerrit.example.com ')).toBe('https://gerrit.example.com')
209
+ })
210
+
211
+ test('trims whitespace from URL with protocol', () => {
212
+ expect(normalizeGerritHost(' https://gerrit.example.com ')).toBe(
213
+ 'https://gerrit.example.com',
214
+ )
215
+ })
216
+
217
+ test('trims whitespace and removes trailing slash', () => {
218
+ expect(normalizeGerritHost(' gerrit.example.com/ ')).toBe('https://gerrit.example.com')
219
+ })
220
+ })
221
+
222
+ describe('combined scenarios', () => {
223
+ test('adds protocol and removes trailing slash', () => {
224
+ expect(normalizeGerritHost('gerrit.example.com/')).toBe('https://gerrit.example.com')
225
+ })
226
+
227
+ test('trims, adds protocol, and removes trailing slash', () => {
228
+ expect(normalizeGerritHost(' gerrit.example.com/ ')).toBe('https://gerrit.example.com')
229
+ })
230
+
231
+ test('handles subdomain with port', () => {
232
+ expect(normalizeGerritHost('review.git.example.com:8443')).toBe(
233
+ 'https://review.git.example.com:8443',
234
+ )
235
+ })
236
+
237
+ test('handles complex URL with path', () => {
238
+ expect(normalizeGerritHost('gerrit.example.com/gerrit')).toBe(
239
+ 'https://gerrit.example.com/gerrit',
240
+ )
241
+ })
242
+
243
+ test('normalizes complete real-world example', () => {
244
+ expect(normalizeGerritHost('gerrit-review.example.org')).toBe(
245
+ 'https://gerrit-review.example.org',
246
+ )
247
+ })
248
+ })
249
+
250
+ describe('edge cases', () => {
251
+ test('handles empty string', () => {
252
+ // Empty string becomes 'https:/' after normalization (protocol added, then trailing slash removed)
253
+ expect(normalizeGerritHost('')).toBe('https:/')
254
+ })
255
+
256
+ test('handles whitespace-only string', () => {
257
+ // Whitespace-only string becomes 'https:/' after normalization
258
+ expect(normalizeGerritHost(' ')).toBe('https:/')
259
+ })
260
+
261
+ test('handles just a slash', () => {
262
+ // Just a slash becomes 'https://' (protocol added to '/', then trailing slash removed leaving '//')
263
+ expect(normalizeGerritHost('/')).toBe('https://')
264
+ })
265
+
266
+ test('handles protocol only', () => {
267
+ // Protocol only becomes 'https:/' (trailing slash removed)
268
+ expect(normalizeGerritHost('https://')).toBe('https:/')
269
+ })
270
+ })
271
+ })
@@ -53,6 +53,33 @@ export const extractChangeNumber = (input: string): string => {
53
53
  }
54
54
  }
55
55
 
56
+ /**
57
+ * Normalizes a Gerrit host URL by adding https:// if no protocol is provided
58
+ * and removing trailing slashes
59
+ *
60
+ * @param host - The host URL to normalize (e.g., "gerrit.example.com" or "https://gerrit.example.com")
61
+ * @returns The normalized URL with protocol and without trailing slash
62
+ *
63
+ * @example
64
+ * normalizeGerritHost("gerrit.example.com") // returns "https://gerrit.example.com"
65
+ * normalizeGerritHost("gerrit.example.com:8080") // returns "https://gerrit.example.com:8080"
66
+ * normalizeGerritHost("http://gerrit.example.com") // returns "http://gerrit.example.com"
67
+ * normalizeGerritHost("https://gerrit.example.com/") // returns "https://gerrit.example.com"
68
+ */
69
+ export const normalizeGerritHost = (host: string): string => {
70
+ let normalized = host.trim()
71
+
72
+ // Add https:// if no protocol provided
73
+ if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
74
+ normalized = `https://${normalized}`
75
+ }
76
+
77
+ // Remove trailing slash
78
+ normalized = normalized.replace(/\/$/, '')
79
+
80
+ return normalized
81
+ }
82
+
56
83
  /**
57
84
  * Validates if a string is a valid Gerrit change identifier
58
85
  *
@@ -76,7 +76,7 @@ ${JSON.stringify(mockChange)}`)
76
76
  return HttpResponse.text('Not Found', { status: 404 })
77
77
  }),
78
78
 
79
- http.post('*/a/changes/:changeId/revisions/current/review', async ({ params, request }) => {
79
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ params }) => {
80
80
  const { changeId } = params
81
81
  if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
82
82
  return HttpResponse.text(`)]}'
@@ -1,17 +1,13 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
+ import { normalizeGerritHost } from '@/utils/url-parser'
2
3
 
3
4
  describe('Setup Command', () => {
4
- describe('URL processing', () => {
5
- test('should remove trailing slashes', () => {
6
- const url = 'https://gerrit.example.com/'
7
- const normalized = url.replace(/\/$/, '')
8
- expect(normalized).toBe('https://gerrit.example.com')
9
- })
10
-
11
- test('should handle URLs without trailing slashes', () => {
12
- const url = 'https://gerrit.example.com'
13
- const normalized = url.replace(/\/$/, '')
14
- expect(normalized).toBe('https://gerrit.example.com')
5
+ describe('URL normalization integration', () => {
6
+ test('should normalize host URL using normalizeGerritHost', () => {
7
+ // Test that the utility function is working as expected
8
+ expect(normalizeGerritHost('gerrit.example.com')).toBe('https://gerrit.example.com')
9
+ expect(normalizeGerritHost('https://gerrit.example.com/')).toBe('https://gerrit.example.com')
10
+ expect(normalizeGerritHost('gerrit.example.com:8080')).toBe('https://gerrit.example.com:8080')
15
11
  })
16
12
  })
17
13
 
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
2
  import { Effect, Layer } from 'effect'
3
- import { GitWorktreeService, GitWorktreeServiceLive } from '@/services/git-worktree'
3
+ import { GitWorktreeService } from '@/services/git-worktree'
4
4
 
5
5
  describe('Git Worktree Creation', () => {
6
6
  test('should handle commit-based worktree creation in service interface', async () => {
@@ -11,7 +11,6 @@ describe('Git Worktree Creation', () => {
11
11
  validatePreconditions: () => Effect.succeed(undefined),
12
12
  createWorktree: (changeId: string) => {
13
13
  // Simulate commit-based worktree creation (detached HEAD)
14
- const currentCommit = 'abc123def456' // Mock commit hash
15
14
  return Effect.succeed({
16
15
  path: `/tmp/test-worktree-${changeId}`,
17
16
  changeId,
@@ -100,7 +100,7 @@ describe('Review Strategy', () => {
100
100
  }
101
101
  })
102
102
 
103
- mockChildProcess.stderr.on.mockImplementation((event: string, callback: Function) => {
103
+ mockChildProcess.stderr.on.mockImplementation((_event: string, _callback: Function) => {
104
104
  // No stderr for success
105
105
  })
106
106
 
@@ -112,7 +112,7 @@ describe('Review Strategy', () => {
112
112
  }
113
113
 
114
114
  const setupFailedExecution = (exitCode = 1, stderr = 'Command failed') => {
115
- mockChildProcess.stdout.on.mockImplementation((event: string, callback: Function) => {
115
+ mockChildProcess.stdout.on.mockImplementation((_event: string, _callback: Function) => {
116
116
  // No stdout for failure
117
117
  })
118
118