@aaronshaf/ger 3.0.2 → 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.
@@ -0,0 +1,73 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { Console, Effect } from 'effect'
3
+ import chalk from 'chalk'
4
+ export interface UpdateOptions {
5
+ skipPull?: boolean
6
+ }
7
+
8
+ const PACKAGE_NAME = '@aaronshaf/ger'
9
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
10
+
11
+ const readCurrentVersion = (): string => {
12
+ try {
13
+ // Bun.file is available at runtime; use dynamic require as fallback
14
+ const raw = require('../../package.json') as { version: string }
15
+ return raw.version
16
+ } catch {
17
+ return '0.0.0'
18
+ }
19
+ }
20
+
21
+ class UpdateError extends Error {
22
+ readonly _tag = 'UpdateError' as const
23
+ constructor(message: string) {
24
+ super(message)
25
+ this.name = 'UpdateError'
26
+ }
27
+ }
28
+
29
+ const fetchLatestVersion = (): Effect.Effect<string, UpdateError> =>
30
+ Effect.tryPromise({
31
+ try: async () => {
32
+ const res = await fetch(REGISTRY_URL)
33
+ if (!res.ok) throw new Error(`Registry returned ${res.status}`)
34
+ const data = (await res.json()) as { version: string }
35
+ return data.version
36
+ },
37
+ catch: (e) =>
38
+ new UpdateError(
39
+ `Failed to check latest version: ${e instanceof Error ? e.message : String(e)}`,
40
+ ),
41
+ })
42
+
43
+ export const updateCommand = (options: UpdateOptions): Effect.Effect<void, UpdateError, never> =>
44
+ Effect.gen(function* () {
45
+ if (!options.skipPull) {
46
+ yield* Console.log(chalk.dim('Checking for updates...'))
47
+
48
+ const latest = yield* fetchLatestVersion()
49
+ const current = readCurrentVersion()
50
+
51
+ yield* Console.log(` Current: ${chalk.cyan(current)}`)
52
+ yield* Console.log(` Latest: ${chalk.cyan(latest)}`)
53
+
54
+ if (current === latest) {
55
+ yield* Console.log(chalk.green(`✓ Already up to date (${current})`))
56
+ return
57
+ }
58
+
59
+ yield* Console.log('')
60
+ }
61
+
62
+ yield* Console.log(chalk.dim(`Installing ${PACKAGE_NAME}@latest...`))
63
+ yield* Effect.try({
64
+ try: () => {
65
+ execSync(`bun install -g ${PACKAGE_NAME}@latest`, { stdio: 'inherit', timeout: 60000 })
66
+ },
67
+ catch: (e) =>
68
+ new UpdateError(`Install failed: ${e instanceof Error ? e.message : String(e)}`),
69
+ })
70
+
71
+ yield* Console.log('')
72
+ yield* Console.log(chalk.green('✓ ger updated successfully'))
73
+ })
@@ -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,8 +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'
31
30
  import { filesCommand } from './commands/files'
32
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'
33
36
 
34
37
  // Helper function to output error in plain text, JSON, or XML format
35
38
  function outputError(
@@ -154,22 +157,7 @@ export function registerCommands(program: Command): void {
154
157
  )
155
158
  })
156
159
 
157
- // mine command
158
- program
159
- .command('mine')
160
- .description('Show your open changes')
161
- .option('--xml', 'XML output for LLM consumption')
162
- .option('--json', 'JSON output for programmatic consumption')
163
- .action(async (options) => {
164
- await executeEffect(
165
- mineCommand(options).pipe(
166
- Effect.provide(GerritApiServiceLive),
167
- Effect.provide(ConfigServiceLive),
168
- ),
169
- options,
170
- 'mine_result',
171
- )
172
- })
160
+ registerListCommands(program)
173
161
 
174
162
  // search command
175
163
  program
@@ -201,15 +189,16 @@ export function registerCommands(program: Command): void {
201
189
  })
202
190
  })
203
191
 
204
- // workspace command
192
+ // workspace command (deprecated — use 'ger tree setup' instead)
205
193
  program
206
194
  .command('workspace <change-id>')
207
- .description(
208
- 'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
209
- )
195
+ .description('[deprecated: use "ger tree setup"] Create a git worktree for a Gerrit change')
210
196
  .option('--xml', 'XML output for LLM consumption')
211
197
  .option('--json', 'JSON output for programmatic consumption')
212
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
+ }
213
202
  await executeEffect(
214
203
  workspaceCommand(changeId, options).pipe(
215
204
  Effect.provide(GerritApiServiceLive),
@@ -220,23 +209,7 @@ export function registerCommands(program: Command): void {
220
209
  )
221
210
  })
222
211
 
223
- // incoming command
224
- program
225
- .command('incoming')
226
- .description('Show incoming changes for review (where you are a reviewer)')
227
- .option('--xml', 'XML output for LLM consumption')
228
- .option('--json', 'JSON output for programmatic consumption')
229
- .option('-i, --interactive', 'Interactive mode with detailed view and diff')
230
- .action(async (options) => {
231
- await executeEffect(
232
- incomingCommand(options).pipe(
233
- Effect.provide(GerritApiServiceLive),
234
- Effect.provide(ConfigServiceLive),
235
- ),
236
- options,
237
- 'incoming_result',
238
- )
239
- })
212
+ registerTreeCommands(program)
240
213
 
241
214
  // abandon / restore / set-ready / set-wip commands
242
215
  registerStateCommands(program)
@@ -246,11 +219,12 @@ export function registerCommands(program: Command): void {
246
219
  .command('rebase [change-id]')
247
220
  .description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
248
221
  .option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
222
+ .option('--allow-conflicts', 'Allow rebasing even if conflicts exist')
249
223
  .option('--xml', 'XML output for LLM consumption')
250
224
  .option('--json', 'JSON output for programmatic consumption')
251
225
  .action(async (changeId, options) => {
252
226
  await executeEffect(
253
- rebaseCommand(changeId, options).pipe(
227
+ rebaseCommand(changeId, { ...options, allowConflicts: options.allowConflicts }).pipe(
254
228
  Effect.provide(GerritApiServiceLive),
255
229
  Effect.provide(ConfigServiceLive),
256
230
  ),
@@ -340,6 +314,26 @@ export function registerCommands(program: Command): void {
340
314
  // Register all group-related commands
341
315
  registerGroupCommands(program)
342
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
+
343
337
  // comments command
344
338
  program
345
339
  .command('comments <change-id>')
@@ -637,4 +631,26 @@ Note:
637
631
  process.exit(1)
638
632
  }
639
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
+ })
640
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>
@@ -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
  )