@aaronshaf/ger 0.1.0 → 0.1.3
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/bun.lock +29 -4
- package/package.json +2 -1
- package/src/cli/commands/review.ts +221 -213
- package/src/cli/index.ts +13 -4
- package/src/prompts/default-review.md +45 -39
- package/src/prompts/system-inline-review.md +50 -25
- package/src/prompts/system-overall-review.md +33 -8
- package/src/services/git-worktree.ts +297 -0
- package/src/services/review-strategy.ts +373 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +111 -0
- package/tests/review.test.ts +94 -628
- package/tests/unit/git-branch-detection.test.ts +83 -0
- package/tests/unit/git-worktree.test.ts +54 -0
- package/tests/unit/services/review-strategy.test.ts +494 -0
- package/src/services/ai-enhanced.ts +0 -167
- package/src/services/ai.ts +0 -182
- package/tests/ai-service.test.ts +0 -489
package/bun.lock
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"": {
|
|
5
5
|
"name": "ger",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"@anthropic-ai/claude-code": "^1.0.102",
|
|
7
8
|
"@effect/platform": "^0.90.6",
|
|
8
9
|
"@effect/platform-node": "^0.94.2",
|
|
9
10
|
"@effect/schema": "^0.75.5",
|
|
@@ -11,18 +12,18 @@
|
|
|
11
12
|
"chalk": "^5.6.0",
|
|
12
13
|
"cli-table3": "^0.6.5",
|
|
13
14
|
"commander": "^14.0.0",
|
|
14
|
-
"effect": "^3.17.
|
|
15
|
+
"effect": "^3.17.9",
|
|
15
16
|
"signal-exit": "3.0.7",
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@biomejs/biome": "^2.2.
|
|
19
|
+
"@biomejs/biome": "^2.2.2",
|
|
19
20
|
"@types/node": "^24.3.0",
|
|
20
21
|
"ast-grep": "^0.1.0",
|
|
21
|
-
"bun-types": "^1.2.
|
|
22
|
+
"bun-types": "^1.2.21",
|
|
22
23
|
"husky": "^9.1.7",
|
|
23
24
|
"lint-staged": "^16.1.5",
|
|
24
25
|
"msw": "^2.10.5",
|
|
25
|
-
"oxlint": "^1.
|
|
26
|
+
"oxlint": "^1.13.0",
|
|
26
27
|
"typescript": "^5.9.2",
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
"packages": {
|
|
35
|
+
"@anthropic-ai/claude-code": ["@anthropic-ai/claude-code@1.0.102", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "bin": { "claude": "cli.js" } }, "sha512-UIC6qNgKNZi1nLTf1bQvxNfd74xIAqJjIx6vggh3bJOMtuXBiFwrfPk1Pdf9CayYgwZYXgSmxYYaASt6i6ficQ=="],
|
|
36
|
+
|
|
34
37
|
"@babel/code-frame": ["@babel/code-frame@7.0.0-beta.37", "", { "dependencies": { "chalk": "^2.0.0", "esutils": "^2.0.2", "js-tokens": "^3.0.0" } }, "sha512-LIpcKm+2otOOvOvhCbD6wkNYi8aUwHk73uWR+hxBdW2EFht5D0QX89n4me8nyeNGWr5zC3Pvmjq+9MvUof+jkg=="],
|
|
35
38
|
|
|
36
39
|
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
|
@@ -93,6 +96,28 @@
|
|
|
93
96
|
|
|
94
97
|
"@effect/workflow": ["@effect/workflow@0.8.3", "", { "peerDependencies": { "@effect/platform": "^0.90.0", "@effect/rpc": "^0.68.3", "effect": "^3.17.6" } }, "sha512-8X5IOemCb6I66GMd84w6NSmaQ+Ya3oXwItCUMelQAEuRtGzwqsw8PNNunQgK/poSRkmpszlsKOb6kEVNjSdFiQ=="],
|
|
95
98
|
|
|
99
|
+
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
|
100
|
+
|
|
101
|
+
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
|
102
|
+
|
|
103
|
+
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
|
104
|
+
|
|
105
|
+
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
|
106
|
+
|
|
107
|
+
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
|
108
|
+
|
|
109
|
+
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
|
110
|
+
|
|
111
|
+
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
|
112
|
+
|
|
113
|
+
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
|
114
|
+
|
|
115
|
+
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
|
116
|
+
|
|
117
|
+
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
|
118
|
+
|
|
119
|
+
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
|
120
|
+
|
|
96
121
|
"@inquirer/checkbox": ["@inquirer/checkbox@4.2.2", "", { "dependencies": { "@inquirer/core": "^10.2.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g=="],
|
|
97
122
|
|
|
98
123
|
"@inquirer/confirm": ["@inquirer/confirm@5.1.16", "", { "dependencies": { "@inquirer/core": "^10.2.0", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaronshaf/ger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"typescript": "^5.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@anthropic-ai/claude-code": "^1.0.102",
|
|
32
33
|
"@effect/platform": "^0.90.6",
|
|
33
34
|
"@effect/platform-node": "^0.94.2",
|
|
34
35
|
"@effect/schema": "^0.75.5",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, pipe, Schema } from 'effect'
|
|
2
|
-
import {
|
|
1
|
+
import { Effect, pipe, Schema, Layer } from 'effect'
|
|
2
|
+
import { ReviewStrategyService, type ReviewStrategy } from '@/services/review-strategy'
|
|
3
3
|
import { commentCommandWithInput } from './comment'
|
|
4
4
|
import { Console } from 'effect'
|
|
5
5
|
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
@@ -7,6 +7,13 @@ import type { CommentInfo } from '@/schemas/gerrit'
|
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
9
9
|
import { formatDate } from '@/utils/formatters'
|
|
10
|
+
import {
|
|
11
|
+
formatChangeAsXML,
|
|
12
|
+
formatCommentsAsXML,
|
|
13
|
+
formatMessagesAsXML,
|
|
14
|
+
flattenComments,
|
|
15
|
+
} from '@/utils/review-formatters'
|
|
16
|
+
import { buildEnhancedPrompt } from '@/utils/review-prompt-builder'
|
|
10
17
|
import * as fs from 'node:fs/promises'
|
|
11
18
|
import * as fsSync from 'node:fs'
|
|
12
19
|
import * as os from 'node:os'
|
|
@@ -14,6 +21,7 @@ import * as path from 'node:path'
|
|
|
14
21
|
import { fileURLToPath } from 'node:url'
|
|
15
22
|
import { dirname } from 'node:path'
|
|
16
23
|
import * as readline from 'node:readline'
|
|
24
|
+
import { GitWorktreeService, GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
17
25
|
|
|
18
26
|
// Get the directory of this module
|
|
19
27
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -81,6 +89,8 @@ interface ReviewOptions {
|
|
|
81
89
|
comment?: boolean
|
|
82
90
|
yes?: boolean
|
|
83
91
|
prompt?: string
|
|
92
|
+
provider?: string
|
|
93
|
+
systemPrompt?: string
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
// Schema for validating AI-generated inline comments
|
|
@@ -178,7 +188,7 @@ const validateAndFixInlineComments = (
|
|
|
178
188
|
return validComments
|
|
179
189
|
})
|
|
180
190
|
|
|
181
|
-
//
|
|
191
|
+
// Legacy helper for backward compatibility (will be removed)
|
|
182
192
|
const getChangeDataAsXml = (changeId: string): Effect.Effect<string, ApiError, GerritApiService> =>
|
|
183
193
|
Effect.gen(function* () {
|
|
184
194
|
const gerritApi = yield* GerritApiService
|
|
@@ -190,83 +200,17 @@ const getChangeDataAsXml = (changeId: string): Effect.Effect<string, ApiError, G
|
|
|
190
200
|
const commentsMap = yield* gerritApi.getComments(changeId)
|
|
191
201
|
const messages = yield* gerritApi.getMessages(changeId)
|
|
192
202
|
|
|
193
|
-
|
|
194
|
-
const comments: CommentInfo[] = []
|
|
195
|
-
for (const [path, fileComments] of Object.entries(commentsMap)) {
|
|
196
|
-
for (const comment of fileComments) {
|
|
197
|
-
comments.push({ ...comment, path })
|
|
198
|
-
}
|
|
199
|
-
}
|
|
203
|
+
const comments = flattenComments(commentsMap)
|
|
200
204
|
|
|
201
|
-
// Build XML string
|
|
205
|
+
// Build XML string using helper functions
|
|
202
206
|
const xmlLines: string[] = []
|
|
203
207
|
xmlLines.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
204
208
|
xmlLines.push(`<show_result>`)
|
|
205
209
|
xmlLines.push(` <status>success</status>`)
|
|
206
|
-
xmlLines.push(
|
|
207
|
-
xmlLines.push(` <id>${escapeXML(change.change_id)}</id>`)
|
|
208
|
-
xmlLines.push(` <number>${change._number}</number>`)
|
|
209
|
-
xmlLines.push(` <subject><![CDATA[${sanitizeCDATA(change.subject)}]]></subject>`)
|
|
210
|
-
xmlLines.push(` <status>${escapeXML(change.status)}</status>`)
|
|
211
|
-
xmlLines.push(` <project>${escapeXML(change.project)}</project>`)
|
|
212
|
-
xmlLines.push(` <branch>${escapeXML(change.branch)}</branch>`)
|
|
213
|
-
xmlLines.push(` <owner>`)
|
|
214
|
-
if (change.owner?.name) {
|
|
215
|
-
xmlLines.push(` <name><![CDATA[${sanitizeCDATA(change.owner.name)}]]></name>`)
|
|
216
|
-
}
|
|
217
|
-
if (change.owner?.email) {
|
|
218
|
-
xmlLines.push(` <email>${escapeXML(change.owner.email)}</email>`)
|
|
219
|
-
}
|
|
220
|
-
xmlLines.push(` </owner>`)
|
|
221
|
-
xmlLines.push(` <created>${escapeXML(change.created || '')}</created>`)
|
|
222
|
-
xmlLines.push(` <updated>${escapeXML(change.updated || '')}</updated>`)
|
|
223
|
-
xmlLines.push(` </change>`)
|
|
210
|
+
xmlLines.push(...formatChangeAsXML(change))
|
|
224
211
|
xmlLines.push(` <diff><![CDATA[${sanitizeCDATA(diff)}]]></diff>`)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
xmlLines.push(` <comments>`)
|
|
228
|
-
xmlLines.push(` <count>${comments.length}</count>`)
|
|
229
|
-
for (const comment of comments) {
|
|
230
|
-
xmlLines.push(` <comment>`)
|
|
231
|
-
if (comment.id) xmlLines.push(` <id>${escapeXML(comment.id)}</id>`)
|
|
232
|
-
if (comment.path)
|
|
233
|
-
xmlLines.push(` <path><![CDATA[${sanitizeCDATA(comment.path)}]]></path>`)
|
|
234
|
-
if (comment.line) xmlLines.push(` <line>${comment.line}</line>`)
|
|
235
|
-
if (comment.author?.name) {
|
|
236
|
-
xmlLines.push(` <author><![CDATA[${sanitizeCDATA(comment.author.name)}]]></author>`)
|
|
237
|
-
}
|
|
238
|
-
if (comment.updated) xmlLines.push(` <updated>${escapeXML(comment.updated)}</updated>`)
|
|
239
|
-
if (comment.message) {
|
|
240
|
-
xmlLines.push(` <message><![CDATA[${sanitizeCDATA(comment.message)}]]></message>`)
|
|
241
|
-
}
|
|
242
|
-
if (comment.unresolved) xmlLines.push(` <unresolved>true</unresolved>`)
|
|
243
|
-
xmlLines.push(` </comment>`)
|
|
244
|
-
}
|
|
245
|
-
xmlLines.push(` </comments>`)
|
|
246
|
-
|
|
247
|
-
// Messages section
|
|
248
|
-
xmlLines.push(` <messages>`)
|
|
249
|
-
xmlLines.push(` <count>${messages.length}</count>`)
|
|
250
|
-
for (const message of messages) {
|
|
251
|
-
xmlLines.push(` <message>`)
|
|
252
|
-
xmlLines.push(` <id>${escapeXML(message.id)}</id>`)
|
|
253
|
-
if (message.author?.name) {
|
|
254
|
-
xmlLines.push(` <author><![CDATA[${sanitizeCDATA(message.author.name)}]]></author>`)
|
|
255
|
-
}
|
|
256
|
-
if (message.author?._account_id) {
|
|
257
|
-
xmlLines.push(` <author_id>${message.author._account_id}</author_id>`)
|
|
258
|
-
}
|
|
259
|
-
xmlLines.push(` <date>${escapeXML(message.date)}</date>`)
|
|
260
|
-
if (message._revision_number) {
|
|
261
|
-
xmlLines.push(` <revision>${message._revision_number}</revision>`)
|
|
262
|
-
}
|
|
263
|
-
if (message.tag) {
|
|
264
|
-
xmlLines.push(` <tag>${escapeXML(message.tag)}</tag>`)
|
|
265
|
-
}
|
|
266
|
-
xmlLines.push(` <message><![CDATA[${sanitizeCDATA(message.message)}]]></message>`)
|
|
267
|
-
xmlLines.push(` </message>`)
|
|
268
|
-
}
|
|
269
|
-
xmlLines.push(` </messages>`)
|
|
212
|
+
xmlLines.push(...formatCommentsAsXML(comments))
|
|
213
|
+
xmlLines.push(...formatMessagesAsXML(messages))
|
|
270
214
|
xmlLines.push(`</show_result>`)
|
|
271
215
|
|
|
272
216
|
return xmlLines.join('\n')
|
|
@@ -286,13 +230,7 @@ const getChangeDataAsPretty = (
|
|
|
286
230
|
const commentsMap = yield* gerritApi.getComments(changeId)
|
|
287
231
|
const messages = yield* gerritApi.getMessages(changeId)
|
|
288
232
|
|
|
289
|
-
|
|
290
|
-
const comments: CommentInfo[] = []
|
|
291
|
-
for (const [path, fileComments] of Object.entries(commentsMap)) {
|
|
292
|
-
for (const comment of fileComments) {
|
|
293
|
-
comments.push({ ...comment, path })
|
|
294
|
-
}
|
|
295
|
-
}
|
|
233
|
+
const comments = flattenComments(commentsMap)
|
|
296
234
|
|
|
297
235
|
// Build pretty string
|
|
298
236
|
const lines: string[] = []
|
|
@@ -378,19 +316,30 @@ const promptUser = (message: string): Effect.Effect<boolean, never> =>
|
|
|
378
316
|
|
|
379
317
|
export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
380
318
|
Effect.gen(function* () {
|
|
381
|
-
const
|
|
319
|
+
const reviewStrategy = yield* ReviewStrategyService
|
|
320
|
+
const gitService = yield* GitWorktreeService
|
|
382
321
|
|
|
383
|
-
// Load default prompts
|
|
322
|
+
// Load default prompts
|
|
384
323
|
const prompts = yield* loadDefaultPrompts
|
|
385
324
|
|
|
386
|
-
//
|
|
387
|
-
yield*
|
|
388
|
-
const aiTool = yield* aiService
|
|
389
|
-
.detectAiTool()
|
|
390
|
-
.pipe(Effect.catchTag('NoAiToolFoundError', (error) => Effect.fail(new Error(error.message))))
|
|
391
|
-
yield* Console.log(`✓ Found AI tool: ${aiTool}`)
|
|
325
|
+
// Validate preconditions
|
|
326
|
+
yield* gitService.validatePreconditions()
|
|
392
327
|
|
|
393
|
-
//
|
|
328
|
+
// Check for available AI strategies
|
|
329
|
+
yield* Console.log('→ Checking AI tool availability...')
|
|
330
|
+
const availableStrategies = yield* reviewStrategy.getAvailableStrategies()
|
|
331
|
+
|
|
332
|
+
if (availableStrategies.length === 0) {
|
|
333
|
+
return yield* Effect.fail(
|
|
334
|
+
new Error('No AI tools available. Please install claude, gemini, or codex CLI.'),
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Select strategy based on user preference
|
|
339
|
+
const selectedStrategy = yield* reviewStrategy.selectStrategy(options.provider)
|
|
340
|
+
yield* Console.log(`✓ Using AI tool: ${selectedStrategy.name}`)
|
|
341
|
+
|
|
342
|
+
// Load custom review prompt if provided
|
|
394
343
|
let userReviewPrompt = prompts.defaultReviewPrompt
|
|
395
344
|
|
|
396
345
|
if (options.prompt) {
|
|
@@ -404,81 +353,176 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
404
353
|
}
|
|
405
354
|
}
|
|
406
355
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
356
|
+
// Use Effect's resource management for worktree lifecycle
|
|
357
|
+
yield* Effect.acquireUseRelease(
|
|
358
|
+
// Acquire: Create worktree and setup
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
const worktreeInfo = yield* gitService.createWorktree(changeId)
|
|
361
|
+
yield* gitService.fetchAndCheckoutPatchset(worktreeInfo)
|
|
362
|
+
return worktreeInfo
|
|
363
|
+
}),
|
|
364
|
+
|
|
365
|
+
// Use: Run the enhanced review process
|
|
366
|
+
(worktreeInfo) =>
|
|
367
|
+
Effect.gen(function* () {
|
|
368
|
+
// Switch to worktree directory
|
|
369
|
+
const originalCwd = process.cwd()
|
|
370
|
+
process.chdir(worktreeInfo.path)
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Get changed files from git
|
|
374
|
+
const changedFiles = yield* gitService.getChangedFiles()
|
|
375
|
+
|
|
376
|
+
yield* Console.log(`→ Found ${changedFiles.length} changed files`)
|
|
377
|
+
if (options.debug) {
|
|
378
|
+
yield* Console.log(`[DEBUG] Changed files: ${changedFiles.join(', ')}`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Stage 1: Generate inline comments
|
|
382
|
+
yield* Console.log(`→ Generating inline comments for change ${changeId}...`)
|
|
383
|
+
|
|
384
|
+
const inlinePrompt = yield* buildEnhancedPrompt(
|
|
385
|
+
userReviewPrompt,
|
|
386
|
+
prompts.inlineReviewSystemPrompt,
|
|
387
|
+
changeId,
|
|
388
|
+
changedFiles,
|
|
389
|
+
)
|
|
410
390
|
|
|
411
|
-
|
|
391
|
+
// Run inline review using selected strategy
|
|
392
|
+
if (options.debug) {
|
|
393
|
+
yield* Console.log(`[DEBUG] Running inline review with ${selectedStrategy.name}`)
|
|
394
|
+
yield* Console.log(`[DEBUG] Working directory: ${worktreeInfo.path}`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const inlineResponse = yield* reviewStrategy
|
|
398
|
+
.executeWithStrategy(selectedStrategy, inlinePrompt, {
|
|
399
|
+
cwd: worktreeInfo.path,
|
|
400
|
+
systemPrompt: options.systemPrompt || prompts.inlineReviewSystemPrompt,
|
|
401
|
+
})
|
|
402
|
+
.pipe(
|
|
403
|
+
Effect.catchTag('ReviewStrategyError', (error) =>
|
|
404
|
+
Effect.gen(function* () {
|
|
405
|
+
yield* Console.error(`✗ Inline review failed: ${error.message}`)
|
|
406
|
+
return yield* Effect.fail(new Error(error.message))
|
|
407
|
+
}),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if (options.debug) {
|
|
412
|
+
yield* Console.log(`[DEBUG] Inline review completed`)
|
|
413
|
+
yield* Console.log(`[DEBUG] Response length: ${inlineResponse.length} chars`)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Response content is ready for parsing
|
|
417
|
+
const extractedInlineResponse = inlineResponse.trim()
|
|
418
|
+
|
|
419
|
+
if (options.debug) {
|
|
420
|
+
yield* Console.log(
|
|
421
|
+
`[DEBUG] Extracted response for parsing:\n${extractedInlineResponse}`,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Parse JSON array from response
|
|
426
|
+
const inlineCommentsArray = yield* Effect.tryPromise({
|
|
427
|
+
try: () => Promise.resolve(JSON.parse(extractedInlineResponse)),
|
|
428
|
+
catch: (error) => new Error(`Invalid JSON response: ${error}`),
|
|
429
|
+
}).pipe(
|
|
430
|
+
Effect.catchAll((error) =>
|
|
431
|
+
Effect.gen(function* () {
|
|
432
|
+
yield* Console.error(`✗ Failed to parse inline comments JSON: ${error}`)
|
|
433
|
+
yield* Console.error(`Raw extracted response: "${extractedInlineResponse}"`)
|
|
434
|
+
if (!options.debug) {
|
|
435
|
+
yield* Console.error('Run with --debug to see full AI output')
|
|
436
|
+
}
|
|
437
|
+
return yield* Effect.fail(error)
|
|
438
|
+
}),
|
|
439
|
+
),
|
|
440
|
+
)
|
|
412
441
|
|
|
413
|
-
|
|
414
|
-
|
|
442
|
+
// Validate that the response is an array
|
|
443
|
+
if (!Array.isArray(inlineCommentsArray)) {
|
|
444
|
+
yield* Console.error('✗ AI response is not an array of comments')
|
|
445
|
+
return yield* Effect.fail(new Error('Invalid inline comments format'))
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Validate and fix inline comments
|
|
449
|
+
const originalCount = inlineCommentsArray.length
|
|
450
|
+
const inlineComments = yield* validateAndFixInlineComments(
|
|
451
|
+
inlineCommentsArray,
|
|
452
|
+
changedFiles,
|
|
453
|
+
)
|
|
454
|
+
const validCount = inlineComments.length
|
|
415
455
|
|
|
416
|
-
|
|
417
|
-
|
|
456
|
+
if (originalCount > validCount) {
|
|
457
|
+
yield* Console.log(
|
|
458
|
+
`→ Filtered ${originalCount - validCount} invalid comments, ${validCount} remain`,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
418
461
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
462
|
+
// Handle inline comments output/posting
|
|
463
|
+
yield* handleInlineComments(inlineComments, changeId, options)
|
|
422
464
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
Effect.catchTag('AiResponseParseError', (error) =>
|
|
426
|
-
Effect.gen(function* () {
|
|
427
|
-
if (options.debug) {
|
|
428
|
-
yield* Console.error(`[DEBUG] AI output:\n${error.rawOutput}`)
|
|
429
|
-
}
|
|
430
|
-
return yield* Effect.fail(error)
|
|
431
|
-
}),
|
|
432
|
-
),
|
|
433
|
-
Effect.catchTag('AiServiceError', (error) =>
|
|
434
|
-
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
435
|
-
),
|
|
436
|
-
)
|
|
465
|
+
// Stage 2: Generate overall review comment
|
|
466
|
+
yield* Console.log(`→ Generating overall review comment for change ${changeId}...`)
|
|
437
467
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
468
|
+
const overallPrompt = yield* buildEnhancedPrompt(
|
|
469
|
+
userReviewPrompt,
|
|
470
|
+
prompts.overallReviewSystemPrompt,
|
|
471
|
+
changeId,
|
|
472
|
+
changedFiles,
|
|
473
|
+
)
|
|
441
474
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
475
|
+
// Run overall review using selected strategy
|
|
476
|
+
if (options.debug) {
|
|
477
|
+
yield* Console.log(`[DEBUG] Running overall review with ${selectedStrategy.name}`)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const overallResponse = yield* reviewStrategy
|
|
481
|
+
.executeWithStrategy(selectedStrategy, overallPrompt, {
|
|
482
|
+
cwd: worktreeInfo.path,
|
|
483
|
+
systemPrompt: options.systemPrompt || prompts.overallReviewSystemPrompt,
|
|
484
|
+
})
|
|
485
|
+
.pipe(
|
|
486
|
+
Effect.catchTag('ReviewStrategyError', (error) =>
|
|
487
|
+
Effect.gen(function* () {
|
|
488
|
+
yield* Console.error(`✗ Overall review failed: ${error.message}`)
|
|
489
|
+
return yield* Effect.fail(new Error(error.message))
|
|
490
|
+
}),
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if (options.debug) {
|
|
495
|
+
yield* Console.log(`[DEBUG] Overall review completed`)
|
|
496
|
+
yield* Console.log(`[DEBUG] Response length: ${overallResponse.length} chars`)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Response content is ready for use
|
|
500
|
+
const extractedOverallResponse = overallResponse.trim()
|
|
501
|
+
|
|
502
|
+
// Handle overall review output/posting
|
|
503
|
+
yield* handleOverallReview(extractedOverallResponse, changeId, options)
|
|
504
|
+
} finally {
|
|
505
|
+
// Always restore original working directory
|
|
506
|
+
process.chdir(originalCwd)
|
|
452
507
|
}
|
|
453
|
-
return yield* Effect.fail(error)
|
|
454
|
-
}),
|
|
455
|
-
),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
// Validate that the response is an array
|
|
459
|
-
if (!Array.isArray(inlineCommentsArray)) {
|
|
460
|
-
yield* Console.error('✗ AI response is not an array of comments')
|
|
461
|
-
return yield* Effect.fail(new Error('Invalid inline comments format'))
|
|
462
|
-
}
|
|
463
508
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const files = yield* gerritApi.getFiles(changeId)
|
|
467
|
-
const availableFiles = Object.keys(files)
|
|
468
|
-
|
|
469
|
-
// Validate and fix inline comments
|
|
470
|
-
const originalCount = inlineCommentsArray.length
|
|
471
|
-
const inlineComments = yield* validateAndFixInlineComments(inlineCommentsArray, availableFiles)
|
|
472
|
-
const validCount = inlineComments.length
|
|
509
|
+
yield* Console.log(`✓ Review complete for ${changeId}`)
|
|
510
|
+
}),
|
|
473
511
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
512
|
+
// Release: Always cleanup worktree
|
|
513
|
+
(worktreeInfo) => gitService.cleanup(worktreeInfo),
|
|
514
|
+
)
|
|
515
|
+
})
|
|
479
516
|
|
|
480
|
-
|
|
517
|
+
// Helper function to handle inline comments output/posting
|
|
518
|
+
const handleInlineComments = (
|
|
519
|
+
inlineComments: InlineComment[],
|
|
520
|
+
changeId: string,
|
|
521
|
+
options: ReviewOptions,
|
|
522
|
+
): Effect.Effect<void, Error, GerritApiService> =>
|
|
523
|
+
Effect.gen(function* () {
|
|
481
524
|
if (!options.comment) {
|
|
525
|
+
// Display mode
|
|
482
526
|
if (inlineComments.length > 0) {
|
|
483
527
|
yield* Console.log('\n━━━━━━ INLINE COMMENTS ━━━━━━')
|
|
484
528
|
for (const comment of inlineComments) {
|
|
@@ -489,7 +533,7 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
489
533
|
yield* Console.log('\n→ No inline comments')
|
|
490
534
|
}
|
|
491
535
|
} else {
|
|
492
|
-
//
|
|
536
|
+
// Comment posting mode
|
|
493
537
|
if (inlineComments.length > 0) {
|
|
494
538
|
yield* Console.log('\n━━━━━━ INLINE COMMENTS TO POST ━━━━━━')
|
|
495
539
|
for (const comment of inlineComments) {
|
|
@@ -498,82 +542,48 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
498
542
|
}
|
|
499
543
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
500
544
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
? true
|
|
504
|
-
: yield* promptUser('\nPost these inline comments to Gerrit?')
|
|
545
|
+
const shouldPost =
|
|
546
|
+
options.yes || (yield* promptUser('\nPost these inline comments to Gerrit?'))
|
|
505
547
|
|
|
506
548
|
if (shouldPost) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}),
|
|
518
|
-
),
|
|
519
|
-
)
|
|
520
|
-
yield* Console.log(`✓ Inline comments posted for ${changeId}`)
|
|
521
|
-
}
|
|
549
|
+
yield* pipe(
|
|
550
|
+
commentCommandWithInput(changeId, JSON.stringify(inlineComments), { batch: true }),
|
|
551
|
+
Effect.catchAll((error) =>
|
|
552
|
+
Effect.gen(function* () {
|
|
553
|
+
yield* Console.error(`✗ Failed to post inline comments: ${error}`)
|
|
554
|
+
return yield* Effect.fail(error)
|
|
555
|
+
}),
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
yield* Console.log(`✓ Inline comments posted for ${changeId}`)
|
|
522
559
|
} else {
|
|
523
560
|
yield* Console.log('→ Inline comments not posted')
|
|
524
561
|
}
|
|
525
|
-
} else {
|
|
526
|
-
yield* Console.log('\n→ No valid inline comments to post')
|
|
527
562
|
}
|
|
528
563
|
}
|
|
564
|
+
})
|
|
529
565
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
yield* Console.log('[DEBUG] Running AI for overall review...')
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Run overall review
|
|
541
|
-
const overallResponse = yield* aiService.runPrompt(overallPrompt, prettyData).pipe(
|
|
542
|
-
Effect.catchTag('AiResponseParseError', (error) =>
|
|
543
|
-
Effect.gen(function* () {
|
|
544
|
-
if (options.debug) {
|
|
545
|
-
yield* Console.error(`[DEBUG] AI output:\n${error.rawOutput}`)
|
|
546
|
-
}
|
|
547
|
-
return yield* Effect.fail(error)
|
|
548
|
-
}),
|
|
549
|
-
),
|
|
550
|
-
Effect.catchTag('AiServiceError', (error) =>
|
|
551
|
-
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
552
|
-
),
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
if (options.debug) {
|
|
556
|
-
yield* Console.log(`[DEBUG] Overall response:\n${overallResponse}`)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// If not in comment mode, just output the review
|
|
566
|
+
// Helper function to handle overall review output/posting
|
|
567
|
+
const handleOverallReview = (
|
|
568
|
+
overallResponse: string,
|
|
569
|
+
changeId: string,
|
|
570
|
+
options: ReviewOptions,
|
|
571
|
+
): Effect.Effect<void, Error, GerritApiService> =>
|
|
572
|
+
Effect.gen(function* () {
|
|
560
573
|
if (!options.comment) {
|
|
574
|
+
// Display mode
|
|
561
575
|
yield* Console.log('\n━━━━━━ OVERALL REVIEW ━━━━━━')
|
|
562
576
|
yield* Console.log(overallResponse)
|
|
563
577
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
564
578
|
} else {
|
|
565
|
-
//
|
|
579
|
+
// Comment posting mode
|
|
566
580
|
yield* Console.log('\n━━━━━━ OVERALL REVIEW TO POST ━━━━━━')
|
|
567
581
|
yield* Console.log(overallResponse)
|
|
568
582
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
569
583
|
|
|
570
|
-
|
|
571
|
-
const shouldPost = options.yes
|
|
572
|
-
? true
|
|
573
|
-
: yield* promptUser('\nPost this overall review to Gerrit?')
|
|
584
|
+
const shouldPost = options.yes || (yield* promptUser('\nPost this overall review to Gerrit?'))
|
|
574
585
|
|
|
575
586
|
if (shouldPost) {
|
|
576
|
-
// Post overall comment using the new direct input method
|
|
577
587
|
yield* pipe(
|
|
578
588
|
commentCommandWithInput(changeId, overallResponse, {}),
|
|
579
589
|
Effect.catchAll((error) =>
|
|
@@ -588,6 +598,4 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
588
598
|
yield* Console.log('→ Overall review not posted')
|
|
589
599
|
}
|
|
590
600
|
}
|
|
591
|
-
|
|
592
|
-
yield* Console.log(`✓ Review complete for ${changeId}`)
|
|
593
601
|
})
|