@aaronshaf/ger 3.0.1 → 4.0.0

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.
Files changed (36) hide show
  1. package/docs/prd/commands.md +59 -1
  2. package/package.json +1 -1
  3. package/src/api/gerrit-types.ts +121 -0
  4. package/src/api/gerrit.ts +68 -102
  5. package/src/cli/commands/analyze.ts +284 -0
  6. package/src/cli/commands/cherry.ts +268 -0
  7. package/src/cli/commands/failures.ts +74 -0
  8. package/src/cli/commands/files.ts +86 -0
  9. package/src/cli/commands/list.ts +239 -0
  10. package/src/cli/commands/rebase.ts +5 -1
  11. package/src/cli/commands/retrigger.ts +91 -0
  12. package/src/cli/commands/reviewers.ts +95 -0
  13. package/src/cli/commands/setup.ts +19 -6
  14. package/src/cli/commands/tree-cleanup.ts +139 -0
  15. package/src/cli/commands/tree-rebase.ts +168 -0
  16. package/src/cli/commands/tree-setup.ts +202 -0
  17. package/src/cli/commands/trees.ts +107 -0
  18. package/src/cli/commands/update.ts +73 -0
  19. package/src/cli/register-analytics-commands.ts +90 -0
  20. package/src/cli/register-commands.ts +96 -40
  21. package/src/cli/register-list-commands.ts +105 -0
  22. package/src/cli/register-tree-commands.ts +128 -0
  23. package/src/schemas/config.ts +3 -0
  24. package/src/schemas/gerrit.ts +2 -0
  25. package/src/schemas/reviewer.ts +16 -0
  26. package/src/services/config.ts +15 -0
  27. package/tests/analyze.test.ts +197 -0
  28. package/tests/cherry.test.ts +208 -0
  29. package/tests/failures.test.ts +212 -0
  30. package/tests/files.test.ts +223 -0
  31. package/tests/helpers/config-mock.ts +4 -0
  32. package/tests/list.test.ts +220 -0
  33. package/tests/retrigger.test.ts +159 -0
  34. package/tests/reviewers.test.ts +259 -0
  35. package/tests/tree.test.ts +517 -0
  36. package/tests/update.test.ts +86 -0
@@ -0,0 +1,90 @@
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 { analyzeCommand } from './commands/analyze'
6
+ import { failuresCommand } from './commands/failures'
7
+ import { updateCommand } from './commands/update'
8
+
9
+ function executeEffect<E>(
10
+ effect: Effect.Effect<void, E, never>,
11
+ options: { xml?: boolean; json?: boolean },
12
+ resultTag: string,
13
+ ): Promise<void> {
14
+ if (options.xml && options.json) {
15
+ console.error('✗ Error: --xml and --json are mutually exclusive')
16
+ process.exit(1)
17
+ }
18
+ return Effect.runPromise(effect).catch((error: unknown) => {
19
+ const msg = error instanceof Error ? error.message : String(error)
20
+ if (options.json) {
21
+ console.log(JSON.stringify({ status: 'error', error: msg }, null, 2))
22
+ } else if (options.xml) {
23
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
24
+ console.log(`<${resultTag}>`)
25
+ console.log(` <status>error</status>`)
26
+ console.log(` <error><![CDATA[${msg}]]></error>`)
27
+ console.log(`</${resultTag}>`)
28
+ } else {
29
+ console.error('✗ Error:', msg)
30
+ }
31
+ process.exit(1)
32
+ })
33
+ }
34
+
35
+ export function registerAnalyticsCommands(program: Command): void {
36
+ // update command
37
+ program
38
+ .command('update')
39
+ .description('Update ger to the latest version')
40
+ .option('--skip-pull', 'Skip version check and install directly')
41
+ .action(async (options) => {
42
+ await executeEffect(updateCommand({ skipPull: options.skipPull }), options, 'update_result')
43
+ })
44
+
45
+ // failures command
46
+ program
47
+ .command('failures <change-id>')
48
+ .description('Get the most recent build failure link from Service Cloud Jenkins')
49
+ .option('--xml', 'XML output for LLM consumption')
50
+ .option('--json', 'JSON output for programmatic consumption')
51
+ .action(async (changeId, options) => {
52
+ await executeEffect(
53
+ failuresCommand(changeId, options).pipe(
54
+ Effect.provide(GerritApiServiceLive),
55
+ Effect.provide(ConfigServiceLive),
56
+ ),
57
+ options,
58
+ 'failures_result',
59
+ )
60
+ })
61
+
62
+ // analyze command
63
+ program
64
+ .command('analyze')
65
+ .description('Show contribution analytics for merged changes')
66
+ .option('--start-date <date>', 'Start date (YYYY-MM-DD, default: Jan 1 of current year)')
67
+ .option('--end-date <date>', 'End date (YYYY-MM-DD, default: today)')
68
+ .option('--repo <project>', 'Filter by Gerrit project name')
69
+ .option('--json', 'JSON output')
70
+ .option('--xml', 'XML output')
71
+ .option('--markdown', 'Markdown output')
72
+ .option('--csv', 'CSV output')
73
+ .option('--output <file>', 'Write output to file')
74
+ .action(async (options) => {
75
+ await executeEffect(
76
+ analyzeCommand({
77
+ startDate: options.startDate,
78
+ endDate: options.endDate,
79
+ repo: options.repo,
80
+ json: options.json,
81
+ xml: options.xml,
82
+ markdown: options.markdown,
83
+ csv: options.csv,
84
+ output: options.output,
85
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
86
+ options,
87
+ 'analyze_result',
88
+ )
89
+ })
90
+ }
@@ -15,9 +15,7 @@ import { commentCommand, COMMENT_HELP_TEXT } from './commands/comment'
15
15
  import { commentsCommand } from './commands/comments'
16
16
  import { diffCommand } from './commands/diff'
17
17
  import { extractUrlCommand } from './commands/extract-url'
18
- import { incomingCommand } from './commands/incoming'
19
18
  import { installHookCommand } from './commands/install-hook'
20
- import { mineCommand } from './commands/mine'
21
19
  import { openCommand } from './commands/open'
22
20
  import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
23
21
  import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
@@ -28,6 +26,13 @@ import { workspaceCommand } from './commands/workspace'
28
26
  import { sanitizeCDATA } from '@/utils/shell-safety'
29
27
  import { registerGroupCommands } from './register-group-commands'
30
28
  import { registerReviewerCommands } from './register-reviewer-commands'
29
+ import { registerTreeCommands } from './register-tree-commands'
30
+ import { filesCommand } from './commands/files'
31
+ import { reviewersCommand } from './commands/reviewers'
32
+ import { retriggerCommand, RETRIGGER_HELP_TEXT } from './commands/retrigger'
33
+ import { cherryCommand, CHERRY_HELP_TEXT } from './commands/cherry'
34
+ import { registerListCommands } from './register-list-commands'
35
+ import { registerAnalyticsCommands } from './register-analytics-commands'
31
36
 
32
37
  // Helper function to output error in plain text, JSON, or XML format
33
38
  function outputError(
@@ -152,22 +157,7 @@ export function registerCommands(program: Command): void {
152
157
  )
153
158
  })
154
159
 
155
- // mine command
156
- program
157
- .command('mine')
158
- .description('Show your open changes')
159
- .option('--xml', 'XML output for LLM consumption')
160
- .option('--json', 'JSON output for programmatic consumption')
161
- .action(async (options) => {
162
- await executeEffect(
163
- mineCommand(options).pipe(
164
- Effect.provide(GerritApiServiceLive),
165
- Effect.provide(ConfigServiceLive),
166
- ),
167
- options,
168
- 'mine_result',
169
- )
170
- })
160
+ registerListCommands(program)
171
161
 
172
162
  // search command
173
163
  program
@@ -199,15 +189,16 @@ export function registerCommands(program: Command): void {
199
189
  })
200
190
  })
201
191
 
202
- // workspace command
192
+ // workspace command (deprecated — use 'ger tree setup' instead)
203
193
  program
204
194
  .command('workspace <change-id>')
205
- .description(
206
- 'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
207
- )
195
+ .description('[deprecated: use "ger tree setup"] Create a git worktree for a Gerrit change')
208
196
  .option('--xml', 'XML output for LLM consumption')
209
197
  .option('--json', 'JSON output for programmatic consumption')
210
198
  .action(async (changeId, options) => {
199
+ if (!options.xml && !options.json) {
200
+ console.error('Note: "ger workspace" is deprecated. Use "ger tree setup" instead.')
201
+ }
211
202
  await executeEffect(
212
203
  workspaceCommand(changeId, options).pipe(
213
204
  Effect.provide(GerritApiServiceLive),
@@ -218,23 +209,7 @@ export function registerCommands(program: Command): void {
218
209
  )
219
210
  })
220
211
 
221
- // incoming command
222
- program
223
- .command('incoming')
224
- .description('Show incoming changes for review (where you are a reviewer)')
225
- .option('--xml', 'XML output for LLM consumption')
226
- .option('--json', 'JSON output for programmatic consumption')
227
- .option('-i, --interactive', 'Interactive mode with detailed view and diff')
228
- .action(async (options) => {
229
- await executeEffect(
230
- incomingCommand(options).pipe(
231
- Effect.provide(GerritApiServiceLive),
232
- Effect.provide(ConfigServiceLive),
233
- ),
234
- options,
235
- 'incoming_result',
236
- )
237
- })
212
+ registerTreeCommands(program)
238
213
 
239
214
  // abandon / restore / set-ready / set-wip commands
240
215
  registerStateCommands(program)
@@ -244,11 +219,12 @@ export function registerCommands(program: Command): void {
244
219
  .command('rebase [change-id]')
245
220
  .description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
246
221
  .option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
222
+ .option('--allow-conflicts', 'Allow rebasing even if conflicts exist')
247
223
  .option('--xml', 'XML output for LLM consumption')
248
224
  .option('--json', 'JSON output for programmatic consumption')
249
225
  .action(async (changeId, options) => {
250
226
  await executeEffect(
251
- rebaseCommand(changeId, options).pipe(
227
+ rebaseCommand(changeId, { ...options, allowConflicts: options.allowConflicts }).pipe(
252
228
  Effect.provide(GerritApiServiceLive),
253
229
  Effect.provide(ConfigServiceLive),
254
230
  ),
@@ -338,6 +314,26 @@ export function registerCommands(program: Command): void {
338
314
  // Register all group-related commands
339
315
  registerGroupCommands(program)
340
316
 
317
+ // retrigger command
318
+ program
319
+ .command('retrigger [change-id]')
320
+ .description(
321
+ 'Post the CI retrigger comment on a change (auto-detects from HEAD if no change-id given)',
322
+ )
323
+ .option('--xml', 'XML output for LLM consumption')
324
+ .option('--json', 'JSON output for programmatic consumption')
325
+ .addHelpText('after', RETRIGGER_HELP_TEXT)
326
+ .action(async (changeId, options) => {
327
+ await executeEffect(
328
+ retriggerCommand(changeId as string | undefined, options).pipe(
329
+ Effect.provide(GerritApiServiceLive),
330
+ Effect.provide(ConfigServiceLive),
331
+ ),
332
+ options,
333
+ 'retrigger_result',
334
+ )
335
+ })
336
+
341
337
  // comments command
342
338
  program
343
339
  .command('comments <change-id>')
@@ -578,6 +574,44 @@ Note:
578
574
  }
579
575
  })
580
576
 
577
+ // files command
578
+ program
579
+ .command('files [change-id]')
580
+ .description(
581
+ 'List files changed in a Gerrit change (auto-detects from HEAD commit if not specified)',
582
+ )
583
+ .option('--xml', 'XML output for LLM consumption')
584
+ .option('--json', 'JSON output for programmatic consumption')
585
+ .action(async (changeId, options) => {
586
+ await executeEffect(
587
+ filesCommand(changeId, options).pipe(
588
+ Effect.provide(GerritApiServiceLive),
589
+ Effect.provide(ConfigServiceLive),
590
+ ),
591
+ options,
592
+ 'files_result',
593
+ )
594
+ })
595
+
596
+ // reviewers command
597
+ program
598
+ .command('reviewers [change-id]')
599
+ .description(
600
+ 'List reviewers on a Gerrit change (auto-detects from HEAD commit if not specified)',
601
+ )
602
+ .option('--xml', 'XML output for LLM consumption')
603
+ .option('--json', 'JSON output for programmatic consumption')
604
+ .action(async (changeId, options) => {
605
+ await executeEffect(
606
+ reviewersCommand(changeId, options).pipe(
607
+ Effect.provide(GerritApiServiceLive),
608
+ Effect.provide(ConfigServiceLive),
609
+ ),
610
+ options,
611
+ 'reviewers_result',
612
+ )
613
+ })
614
+
581
615
  // checkout command
582
616
  program
583
617
  .command('checkout <change-id>')
@@ -597,4 +631,26 @@ Note:
597
631
  process.exit(1)
598
632
  }
599
633
  })
634
+
635
+ registerAnalyticsCommands(program)
636
+
637
+ // cherry command
638
+ program
639
+ .command('cherry <change-id>')
640
+ .description('Fetch and cherry-pick a Gerrit change onto the current branch')
641
+ .option('-n, --no-commit', 'Stage changes without committing')
642
+ .option('--no-verify', 'Bypass git commit hooks during cherry-pick')
643
+ .option('--remote <name>', 'Use specific git remote (default: auto-detect)')
644
+ .addHelpText('after', CHERRY_HELP_TEXT)
645
+ .action(async (changeId, options) => {
646
+ await executeEffect(
647
+ cherryCommand(changeId, {
648
+ noCommit: options.noCommit,
649
+ noVerify: options.noVerify,
650
+ remote: options.remote,
651
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
652
+ options,
653
+ 'cherry_result',
654
+ )
655
+ })
600
656
  }
@@ -0,0 +1,105 @@
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 { listCommand } from './commands/list'
6
+
7
+ function executeEffect<E>(
8
+ effect: Effect.Effect<void, E, never>,
9
+ options: { xml?: boolean; json?: boolean },
10
+ resultTag: string,
11
+ ): Promise<void> {
12
+ if (options.xml && options.json) {
13
+ console.error('✗ Error: --xml and --json are mutually exclusive')
14
+ process.exit(1)
15
+ }
16
+ return Effect.runPromise(effect).catch((error: unknown) => {
17
+ const msg = error instanceof Error ? error.message : String(error)
18
+ if (options.json) {
19
+ console.log(JSON.stringify({ status: 'error', error: msg }, null, 2))
20
+ } else if (options.xml) {
21
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
22
+ console.log(`<${resultTag}>`)
23
+ console.log(` <status>error</status>`)
24
+ console.log(` <error><![CDATA[${msg}]]></error>`)
25
+ console.log(`</${resultTag}>`)
26
+ } else {
27
+ console.error('✗ Error:', msg)
28
+ }
29
+ process.exit(1)
30
+ })
31
+ }
32
+
33
+ export function registerListCommands(program: Command): void {
34
+ // list command (primary)
35
+ program
36
+ .command('list')
37
+ .description('List your changes or changes needing your review')
38
+ .option('--status <status>', 'Filter by status: open, merged, abandoned (default: open)')
39
+ .option('-n, --limit <number>', 'Maximum number of changes to show (default: 25)', parseInt)
40
+ .option('--detailed', 'Show detailed information for each change')
41
+ .option('--reviewer', 'Show changes where you are a reviewer')
42
+ .option('--xml', 'XML output for LLM consumption')
43
+ .option('--json', 'JSON output for programmatic consumption')
44
+ .action(async (options) => {
45
+ await executeEffect(
46
+ listCommand(options).pipe(
47
+ Effect.provide(GerritApiServiceLive),
48
+ Effect.provide(ConfigServiceLive),
49
+ ),
50
+ options,
51
+ 'list_result',
52
+ )
53
+ })
54
+
55
+ // mine command (alias for list)
56
+ program
57
+ .command('mine')
58
+ .description('Show your open changes (alias for "ger list")')
59
+ .option('--xml', 'XML output for LLM consumption')
60
+ .option('--json', 'JSON output for programmatic consumption')
61
+ .action(async (options) => {
62
+ await executeEffect(
63
+ listCommand(options).pipe(
64
+ Effect.provide(GerritApiServiceLive),
65
+ Effect.provide(ConfigServiceLive),
66
+ ),
67
+ options,
68
+ 'list_result',
69
+ )
70
+ })
71
+
72
+ const registerReviewerListCommand = (name: string, description: string): void => {
73
+ program
74
+ .command(name)
75
+ .description(description)
76
+ .option('--status <status>', 'Filter by status: open, merged, abandoned (default: open)')
77
+ .option('-n, --limit <number>', 'Maximum number of changes to show (default: 25)', parseInt)
78
+ .option('--detailed', 'Show detailed information for each change')
79
+ .option('--all-verified', 'Include all verification states (default: open only)')
80
+ .option('-f, --filter <query>', 'Append custom Gerrit query syntax')
81
+ .option('--xml', 'XML output for LLM consumption')
82
+ .option('--json', 'JSON output for programmatic consumption')
83
+ .action(async (options) => {
84
+ await executeEffect(
85
+ listCommand({
86
+ ...options,
87
+ reviewer: true,
88
+ allVerified: options.allVerified,
89
+ filter: options.filter,
90
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
91
+ options,
92
+ 'list_result',
93
+ )
94
+ })
95
+ }
96
+
97
+ registerReviewerListCommand(
98
+ 'incoming',
99
+ 'Show changes where you are a reviewer or CC\'d (alias for "ger list --reviewer")',
100
+ )
101
+ registerReviewerListCommand(
102
+ 'team',
103
+ 'Show changes where you are a reviewer or CC\'d (alias for "ger list --reviewer")',
104
+ )
105
+ }
@@ -0,0 +1,128 @@
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 { treeSetupCommand, TREE_SETUP_HELP_TEXT } from './commands/tree-setup'
6
+ import { treesCommand } from './commands/trees'
7
+ import { treeCleanupCommand } from './commands/tree-cleanup'
8
+ import { treeRebaseCommand } from './commands/tree-rebase'
9
+
10
+ async function executeEffect<E>(
11
+ effect: Effect.Effect<void, E, never>,
12
+ options: { xml?: boolean; json?: boolean },
13
+ resultTag: string,
14
+ ): Promise<void> {
15
+ if (options.xml && options.json) {
16
+ console.error('--xml and --json are mutually exclusive')
17
+ process.exit(1)
18
+ }
19
+ try {
20
+ await Effect.runPromise(effect)
21
+ } catch (error) {
22
+ const errorMessage = error instanceof Error ? error.message : String(error)
23
+ if (options.json) {
24
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
25
+ } else if (options.xml) {
26
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
27
+ console.log(`<${resultTag}>`)
28
+ console.log(` <status>error</status>`)
29
+ console.log(` <error><![CDATA[${errorMessage}]]></error>`)
30
+ console.log(`</${resultTag}>`)
31
+ } else {
32
+ console.error('✗ Error:', errorMessage)
33
+ }
34
+ process.exit(1)
35
+ }
36
+ }
37
+
38
+ export function registerTreeCommands(program: Command): void {
39
+ const tree = program
40
+ .command('tree')
41
+ .description('Manage git worktrees for reviewing Gerrit changes')
42
+
43
+ tree
44
+ .command('setup <change-id>')
45
+ .description('Create a git worktree for reviewing a change')
46
+ .option('--xml', 'XML output for LLM consumption')
47
+ .option('--json', 'JSON output for programmatic consumption')
48
+ .addHelpText('after', TREE_SETUP_HELP_TEXT)
49
+ .action(async (changeId: string, options: { xml?: boolean; json?: boolean }) => {
50
+ await executeEffect(
51
+ treeSetupCommand(changeId, options).pipe(
52
+ Effect.provide(GerritApiServiceLive),
53
+ Effect.provide(ConfigServiceLive),
54
+ ),
55
+ options,
56
+ 'tree_setup_result',
57
+ )
58
+ })
59
+
60
+ tree
61
+ .command('cleanup [change-id]')
62
+ .description('Remove ger-managed worktrees (all, or a specific one by change number)')
63
+ .option('--force', 'Force removal even with uncommitted changes')
64
+ .option('--xml', 'XML output for LLM consumption')
65
+ .option('--json', 'JSON output for programmatic consumption')
66
+ .addHelpText(
67
+ 'after',
68
+ `
69
+ Examples:
70
+ # Remove all ger-managed worktrees
71
+ $ ger tree cleanup
72
+
73
+ # Remove worktree for a specific change
74
+ $ ger tree cleanup 12345
75
+
76
+ # Force removal (discards uncommitted changes)
77
+ $ ger tree cleanup 12345 --force`,
78
+ )
79
+ .action(
80
+ async (
81
+ changeId: string | undefined,
82
+ options: { force?: boolean; xml?: boolean; json?: boolean },
83
+ ) => {
84
+ await executeEffect(treeCleanupCommand(changeId, options), options, 'tree_cleanup_result')
85
+ },
86
+ )
87
+
88
+ tree
89
+ .command('rebase')
90
+ .description('Fetch origin and rebase the current worktree')
91
+ .option('--onto <branch>', 'Branch to rebase onto (default: auto-detect)')
92
+ .option('-i, --interactive', 'Interactive rebase')
93
+ .option('--xml', 'XML output for LLM consumption')
94
+ .option('--json', 'JSON output for programmatic consumption')
95
+ .addHelpText(
96
+ 'after',
97
+ `
98
+ Examples:
99
+ # Rebase onto auto-detected upstream
100
+ $ ger tree rebase
101
+
102
+ # Rebase onto a specific branch
103
+ $ ger tree rebase --onto origin/main
104
+
105
+ # Interactive rebase
106
+ $ ger tree rebase -i`,
107
+ )
108
+ .action(
109
+ async (options: { onto?: string; interactive?: boolean; xml?: boolean; json?: boolean }) => {
110
+ await executeEffect(
111
+ treeRebaseCommand(options).pipe(Effect.provide(ConfigServiceLive)),
112
+ options,
113
+ 'tree_rebase_result',
114
+ )
115
+ },
116
+ )
117
+
118
+ // 'trees' is a top-level command (matches gerry's naming)
119
+ program
120
+ .command('trees')
121
+ .description('List ger-managed git worktrees in the current repository')
122
+ .option('--all', 'Show all worktrees including the main checkout')
123
+ .option('--xml', 'XML output for LLM consumption')
124
+ .option('--json', 'JSON output for programmatic consumption')
125
+ .action(async (options: { all?: boolean; xml?: boolean; json?: boolean }) => {
126
+ await executeEffect(treesCommand(options), options, 'trees_result')
127
+ })
128
+ }
@@ -7,6 +7,7 @@ export const AppConfig: Schema.Struct<{
7
7
  password: typeof Schema.String
8
8
  aiTool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
9
9
  aiAutoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
10
+ retriggerComment: Schema.optional<typeof Schema.String>
10
11
  }> = Schema.Struct({
11
12
  // Gerrit credentials (flattened)
12
13
  host: Schema.String.pipe(
@@ -24,6 +25,8 @@ export const AppConfig: Schema.Struct<{
24
25
  // AI configuration (flattened)
25
26
  aiTool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
26
27
  aiAutoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
28
+ // CI retrigger comment
29
+ retriggerComment: Schema.optional(Schema.String),
27
30
  })
28
31
 
29
32
  export type AppConfig = Schema.Schema.Type<typeof AppConfig>
@@ -570,6 +570,8 @@ const ReviewerAccountInfo = Schema.Struct({
570
570
  username: Schema.optional(Schema.String),
571
571
  })
572
572
 
573
+ export type { ReviewerListItem } from './reviewer'
574
+
573
575
  export const ReviewerResult: Schema.Schema<{
574
576
  readonly input: string
575
577
  readonly reviewers?: ReadonlyArray<{
@@ -0,0 +1,16 @@
1
+ import { Schema } from '@effect/schema'
2
+
3
+ export const ReviewerListItem: Schema.Schema<{
4
+ readonly _account_id?: number
5
+ readonly name?: string
6
+ readonly email?: string
7
+ readonly username?: string
8
+ readonly approvals?: { readonly [x: string]: string }
9
+ }> = Schema.Struct({
10
+ _account_id: Schema.optional(Schema.Number),
11
+ name: Schema.optional(Schema.String),
12
+ email: Schema.optional(Schema.String),
13
+ username: Schema.optional(Schema.String),
14
+ approvals: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
15
+ })
16
+ export type ReviewerListItem = Schema.Schema.Type<typeof ReviewerListItem>
@@ -14,6 +14,8 @@ export interface ConfigServiceImpl {
14
14
  readonly saveAiConfig: (config: AiConfig) => Effect.Effect<void, ConfigError>
15
15
  readonly getFullConfig: Effect.Effect<AppConfig, ConfigError>
16
16
  readonly saveFullConfig: (config: AppConfig) => Effect.Effect<void, ConfigError>
17
+ readonly getRetriggerComment: Effect.Effect<string | undefined, ConfigError>
18
+ readonly saveRetriggerComment: (comment: string) => Effect.Effect<void, ConfigError>
17
19
  }
18
20
 
19
21
  // Export both the tag value and the type for use in Effect requirements
@@ -237,6 +239,17 @@ export const ConfigServiceLive: Layer.Layer<ConfigService, never, never> = Layer
237
239
  yield* saveFullConfig(updatedConfig)
238
240
  })
239
241
 
242
+ const getRetriggerComment = Effect.gen(function* () {
243
+ const config = yield* getFullConfig.pipe(Effect.orElseSucceed(() => null))
244
+ return config?.retriggerComment
245
+ })
246
+
247
+ const saveRetriggerComment = (comment: string) =>
248
+ Effect.gen(function* () {
249
+ const existingConfig = yield* getFullConfig
250
+ yield* saveFullConfig({ ...existingConfig, retriggerComment: comment })
251
+ })
252
+
240
253
  return {
241
254
  getCredentials,
242
255
  saveCredentials,
@@ -245,6 +258,8 @@ export const ConfigServiceLive: Layer.Layer<ConfigService, never, never> = Layer
245
258
  saveAiConfig,
246
259
  getFullConfig,
247
260
  saveFullConfig,
261
+ getRetriggerComment,
262
+ saveRetriggerComment,
248
263
  }
249
264
  }),
250
265
  )