@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.
@@ -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 { abandonCommand } from './commands/abandon'
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 command
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
+ }