@aaronshaf/ger 2.0.9 → 3.0.1
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/README.md +0 -26
- package/docs/prd/architecture.md +6 -48
- package/docs/prd/commands.md +26 -20
- package/index.ts +0 -19
- package/llms.txt +0 -14
- package/package.json +1 -5
- package/src/api/gerrit.ts +23 -33
- package/src/cli/commands/set-ready.ts +76 -0
- package/src/cli/commands/set-wip.ts +76 -0
- package/src/cli/commands/setup.ts +0 -1
- package/src/cli/index.ts +38 -0
- package/src/cli/register-commands.ts +3 -102
- package/src/cli/register-state-commands.ts +106 -0
- package/src/utils/diff-formatters.ts +32 -0
- package/src/cli/commands/review.ts +0 -486
- package/src/prompts/default-review.md +0 -86
- package/src/prompts/system-inline-review.md +0 -135
- package/src/prompts/system-overall-review.md +0 -206
- package/src/services/review-strategy.ts +0 -292
|
@@ -2,11 +2,8 @@ import type { Command } from 'commander'
|
|
|
2
2
|
import { Effect } from 'effect'
|
|
3
3
|
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
4
|
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
-
import { ReviewStrategyServiceLive } from '@/services/review-strategy'
|
|
6
|
-
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
7
5
|
import { CommitHookServiceLive } from '@/services/commit-hook'
|
|
8
|
-
import {
|
|
9
|
-
import { restoreCommand } from './commands/restore'
|
|
6
|
+
import { registerStateCommands } from './register-state-commands'
|
|
10
7
|
import { rebaseCommand } from './commands/rebase'
|
|
11
8
|
import { submitCommand } from './commands/submit'
|
|
12
9
|
import { topicCommand, TOPIC_HELP_TEXT } from './commands/topic'
|
|
@@ -23,7 +20,6 @@ import { installHookCommand } from './commands/install-hook'
|
|
|
23
20
|
import { mineCommand } from './commands/mine'
|
|
24
21
|
import { openCommand } from './commands/open'
|
|
25
22
|
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
|
|
26
|
-
import { reviewCommand } from './commands/review'
|
|
27
23
|
import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
|
|
28
24
|
import { setup } from './commands/setup'
|
|
29
25
|
import { showCommand, SHOW_HELP_TEXT } from './commands/show'
|
|
@@ -240,43 +236,8 @@ export function registerCommands(program: Command): void {
|
|
|
240
236
|
)
|
|
241
237
|
})
|
|
242
238
|
|
|
243
|
-
// abandon
|
|
244
|
-
program
|
|
245
|
-
.command('abandon [change-id]')
|
|
246
|
-
.description(
|
|
247
|
-
'Abandon a change (interactive mode if no change-id provided; accepts change number or Change-ID)',
|
|
248
|
-
)
|
|
249
|
-
.option('-m, --message <message>', 'Abandon message')
|
|
250
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
251
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
252
|
-
.action(async (changeId, options) => {
|
|
253
|
-
await executeEffect(
|
|
254
|
-
abandonCommand(changeId, options).pipe(
|
|
255
|
-
Effect.provide(GerritApiServiceLive),
|
|
256
|
-
Effect.provide(ConfigServiceLive),
|
|
257
|
-
),
|
|
258
|
-
options,
|
|
259
|
-
'abandon_result',
|
|
260
|
-
)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// restore command
|
|
264
|
-
program
|
|
265
|
-
.command('restore <change-id>')
|
|
266
|
-
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
267
|
-
.option('-m, --message <message>', 'Restoration message')
|
|
268
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
269
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
270
|
-
.action(async (changeId, options) => {
|
|
271
|
-
await executeEffect(
|
|
272
|
-
restoreCommand(changeId, options).pipe(
|
|
273
|
-
Effect.provide(GerritApiServiceLive),
|
|
274
|
-
Effect.provide(ConfigServiceLive),
|
|
275
|
-
),
|
|
276
|
-
options,
|
|
277
|
-
'restore_result',
|
|
278
|
-
)
|
|
279
|
-
})
|
|
239
|
+
// abandon / restore / set-ready / set-wip commands
|
|
240
|
+
registerStateCommands(program)
|
|
280
241
|
|
|
281
242
|
// rebase command
|
|
282
243
|
program
|
|
@@ -636,64 +597,4 @@ Note:
|
|
|
636
597
|
process.exit(1)
|
|
637
598
|
}
|
|
638
599
|
})
|
|
639
|
-
|
|
640
|
-
// review command
|
|
641
|
-
program
|
|
642
|
-
.command('review <change-id>')
|
|
643
|
-
.description(
|
|
644
|
-
'AI-powered code review that analyzes changes and optionally posts comments (accepts change number or Change-ID)',
|
|
645
|
-
)
|
|
646
|
-
.option('--comment', 'Post the review as comments (prompts for confirmation)')
|
|
647
|
-
.option('-y, --yes', 'Skip confirmation prompts when posting comments')
|
|
648
|
-
.option('--debug', 'Show debug output including AI responses')
|
|
649
|
-
.option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
|
|
650
|
-
.option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
|
|
651
|
-
.option('--system-prompt <prompt>', 'Custom system prompt for the AI')
|
|
652
|
-
.addHelpText(
|
|
653
|
-
'after',
|
|
654
|
-
`
|
|
655
|
-
This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
|
|
656
|
-
It performs a two-stage review process:
|
|
657
|
-
1. Generates inline comments for specific code issues
|
|
658
|
-
2. Generates an overall review comment
|
|
659
|
-
By default, the review is only displayed in the terminal.
|
|
660
|
-
Use --comment to post the review to Gerrit (with confirmation prompts).
|
|
661
|
-
Use --comment --yes to post without confirmation.
|
|
662
|
-
|
|
663
|
-
Requirements:
|
|
664
|
-
- One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
|
|
665
|
-
- Gerrit credentials must be configured (run 'ger setup' first)
|
|
666
|
-
|
|
667
|
-
Examples:
|
|
668
|
-
$ ger review 12345
|
|
669
|
-
$ ger review If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
670
|
-
$ ger review 12345 --comment
|
|
671
|
-
$ ger review 12345 --comment --yes
|
|
672
|
-
$ ger review 12345 --tool gemini
|
|
673
|
-
$ ger review 12345 --debug
|
|
674
|
-
|
|
675
|
-
Note: Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
676
|
-
`,
|
|
677
|
-
)
|
|
678
|
-
.action(async (changeId, options) => {
|
|
679
|
-
try {
|
|
680
|
-
const effect = reviewCommand(changeId, {
|
|
681
|
-
comment: options.comment,
|
|
682
|
-
yes: options.yes,
|
|
683
|
-
debug: options.debug,
|
|
684
|
-
prompt: options.prompt,
|
|
685
|
-
tool: options.tool,
|
|
686
|
-
systemPrompt: options.systemPrompt,
|
|
687
|
-
}).pipe(
|
|
688
|
-
Effect.provide(ReviewStrategyServiceLive),
|
|
689
|
-
Effect.provide(GerritApiServiceLive),
|
|
690
|
-
Effect.provide(ConfigServiceLive),
|
|
691
|
-
Effect.provide(GitWorktreeServiceLive),
|
|
692
|
-
)
|
|
693
|
-
await Effect.runPromise(effect)
|
|
694
|
-
} catch (error) {
|
|
695
|
-
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
696
|
-
process.exit(1)
|
|
697
|
-
}
|
|
698
|
-
})
|
|
699
600
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
|
+
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
+
import { abandonCommand } from './commands/abandon'
|
|
6
|
+
import { restoreCommand } from './commands/restore'
|
|
7
|
+
import { setReadyCommand } from './commands/set-ready'
|
|
8
|
+
import { setWipCommand } from './commands/set-wip'
|
|
9
|
+
|
|
10
|
+
type StateOptions = { message?: string; xml?: boolean; json?: boolean }
|
|
11
|
+
|
|
12
|
+
async function executeStateEffect(
|
|
13
|
+
effect: Effect.Effect<void, unknown, never>,
|
|
14
|
+
options: StateOptions,
|
|
15
|
+
resultTag: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
await Effect.runPromise(effect)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
21
|
+
if (options.xml) {
|
|
22
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
23
|
+
console.log(`<${resultTag}>`)
|
|
24
|
+
console.log(` <status>error</status>`)
|
|
25
|
+
console.log(` <error><![CDATA[${errorMessage}]]></error>`)
|
|
26
|
+
console.log(`</${resultTag}>`)
|
|
27
|
+
} else if (options.json) {
|
|
28
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
29
|
+
} else {
|
|
30
|
+
console.error('✗ Error:', errorMessage)
|
|
31
|
+
}
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerStateCommands(program: Command): void {
|
|
37
|
+
program
|
|
38
|
+
.command('abandon [change-id]')
|
|
39
|
+
.description(
|
|
40
|
+
'Abandon a change (interactive mode if no change-id provided; accepts change number or Change-ID)',
|
|
41
|
+
)
|
|
42
|
+
.option('-m, --message <message>', 'Abandon message')
|
|
43
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
44
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
45
|
+
.action(async (changeId, options) => {
|
|
46
|
+
await executeStateEffect(
|
|
47
|
+
abandonCommand(changeId, options).pipe(
|
|
48
|
+
Effect.provide(GerritApiServiceLive),
|
|
49
|
+
Effect.provide(ConfigServiceLive),
|
|
50
|
+
),
|
|
51
|
+
options,
|
|
52
|
+
'abandon_result',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('restore <change-id>')
|
|
58
|
+
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
59
|
+
.option('-m, --message <message>', 'Restoration message')
|
|
60
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
61
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
62
|
+
.action(async (changeId, options) => {
|
|
63
|
+
await executeStateEffect(
|
|
64
|
+
restoreCommand(changeId, options).pipe(
|
|
65
|
+
Effect.provide(GerritApiServiceLive),
|
|
66
|
+
Effect.provide(ConfigServiceLive),
|
|
67
|
+
),
|
|
68
|
+
options,
|
|
69
|
+
'restore_result',
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('set-ready <change-id>')
|
|
75
|
+
.description('Mark a WIP change as ready for review (accepts change number or Change-ID)')
|
|
76
|
+
.option('-m, --message <message>', 'Message to include with the status change')
|
|
77
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
78
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
79
|
+
.action(async (changeId, options) => {
|
|
80
|
+
await executeStateEffect(
|
|
81
|
+
setReadyCommand(changeId, options).pipe(
|
|
82
|
+
Effect.provide(GerritApiServiceLive),
|
|
83
|
+
Effect.provide(ConfigServiceLive),
|
|
84
|
+
),
|
|
85
|
+
options,
|
|
86
|
+
'set_ready_result',
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('set-wip <change-id>')
|
|
92
|
+
.description('Mark a change as work-in-progress (accepts change number or Change-ID)')
|
|
93
|
+
.option('-m, --message <message>', 'Message to include with the status change')
|
|
94
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
95
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
96
|
+
.action(async (changeId, options) => {
|
|
97
|
+
await executeStateEffect(
|
|
98
|
+
setWipCommand(changeId, options).pipe(
|
|
99
|
+
Effect.provide(GerritApiServiceLive),
|
|
100
|
+
Effect.provide(ConfigServiceLive),
|
|
101
|
+
),
|
|
102
|
+
options,
|
|
103
|
+
'set_wip_result',
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { colors } from './formatters'
|
|
2
|
+
import type { FileDiffContent } from '@/schemas/gerrit'
|
|
2
3
|
|
|
3
4
|
interface DiffStats {
|
|
4
5
|
additions: number
|
|
@@ -139,3 +140,34 @@ export const extractDiffStats = (diffContent: string): DiffStats => {
|
|
|
139
140
|
|
|
140
141
|
return { additions, deletions, files }
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
export const convertToUnifiedDiff = (diff: FileDiffContent, filePath: string): string => {
|
|
145
|
+
const lines: string[] = []
|
|
146
|
+
|
|
147
|
+
if (diff.diff_header) {
|
|
148
|
+
lines.push(...diff.diff_header)
|
|
149
|
+
} else {
|
|
150
|
+
lines.push(`--- a/${filePath}`)
|
|
151
|
+
lines.push(`+++ b/${filePath}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const section of diff.content) {
|
|
155
|
+
if (section.ab) {
|
|
156
|
+
for (const line of section.ab) {
|
|
157
|
+
lines.push(` ${line}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (section.a) {
|
|
161
|
+
for (const line of section.a) {
|
|
162
|
+
lines.push(`-${line}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (section.b) {
|
|
166
|
+
for (const line of section.b) {
|
|
167
|
+
lines.push(`+${line}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
}
|