@aaronshaf/ger 2.0.2 → 2.0.4

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.
@@ -8,6 +8,7 @@ interface VoteOptions {
8
8
  label?: string[]
9
9
  message?: string
10
10
  xml?: boolean
11
+ json?: boolean
11
12
  }
12
13
 
13
14
  /**
@@ -92,7 +93,20 @@ export const voteCommand = (
92
93
  yield* gerritApi.postReview(changeId, reviewInput)
93
94
 
94
95
  // Output success
95
- if (options.xml) {
96
+ if (options.json) {
97
+ console.log(
98
+ JSON.stringify(
99
+ {
100
+ status: 'success',
101
+ change_id: changeId,
102
+ labels,
103
+ ...(options.message ? { message: options.message } : {}),
104
+ },
105
+ null,
106
+ 2,
107
+ ),
108
+ )
109
+ } else if (options.xml) {
96
110
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
97
111
  console.log(`<vote_result>`)
98
112
  console.log(` <status>success</status>`)
@@ -7,6 +7,7 @@ import { type ConfigError, ConfigService } from '@/services/config'
7
7
 
8
8
  interface WorkspaceOptions {
9
9
  xml?: boolean
10
+ json?: boolean
10
11
  }
11
12
 
12
13
  const parseChangeSpec = (changeSpec: string): { changeId: string; patchset?: string } => {
@@ -129,7 +130,11 @@ export const workspaceCommand = (
129
130
 
130
131
  // Check if worktree already exists
131
132
  if (fs.existsSync(workspaceDir)) {
132
- if (options.xml) {
133
+ if (options.json) {
134
+ console.log(
135
+ JSON.stringify({ status: 'success', path: workspaceDir, exists: true }, null, 2),
136
+ )
137
+ } else if (options.xml) {
133
138
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
134
139
  console.log(`<workspace>`)
135
140
  console.log(` <path>${workspaceDir}</path>`)
@@ -150,7 +155,7 @@ export const workspaceCommand = (
150
155
 
151
156
  // Fetch the change ref
152
157
  const changeRef = revision.ref
153
- if (!options.xml) {
158
+ if (!options.xml && !options.json) {
154
159
  console.log(`Fetching change ${change._number}: ${change.subject}`)
155
160
  }
156
161
 
@@ -168,7 +173,7 @@ export const workspaceCommand = (
168
173
  }
169
174
 
170
175
  // Create worktree
171
- if (!options.xml) {
176
+ if (!options.xml && !options.json) {
172
177
  console.log(`Creating worktree at: ${workspaceDir}`)
173
178
  }
174
179
 
@@ -185,7 +190,21 @@ export const workspaceCommand = (
185
190
  throw new Error(`Failed to create worktree: ${error}`)
186
191
  }
187
192
 
188
- if (options.xml) {
193
+ if (options.json) {
194
+ console.log(
195
+ JSON.stringify(
196
+ {
197
+ status: 'success',
198
+ path: workspaceDir,
199
+ change_number: change._number,
200
+ subject: change.subject,
201
+ created: true,
202
+ },
203
+ null,
204
+ 2,
205
+ ),
206
+ )
207
+ } else if (options.xml) {
189
208
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
190
209
  console.log(`<workspace>`)
191
210
  console.log(` <path>${workspaceDir}</path>`)
@@ -33,10 +33,16 @@ import { sanitizeCDATA } from '@/utils/shell-safety'
33
33
  import { registerGroupCommands } from './register-group-commands'
34
34
  import { registerReviewerCommands } from './register-reviewer-commands'
35
35
 
36
- // Helper function to output error in plain text or XML format
37
- function outputError(error: unknown, options: { xml?: boolean }, resultTag: string): void {
36
+ // Helper function to output error in plain text, JSON, or XML format
37
+ function outputError(
38
+ error: unknown,
39
+ options: { xml?: boolean; json?: boolean },
40
+ resultTag: string,
41
+ ): void {
38
42
  const errorMessage = error instanceof Error ? error.message : String(error)
39
- if (options.xml) {
43
+ if (options.json) {
44
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
45
+ } else if (options.xml) {
40
46
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
41
47
  console.log(`<${resultTag}>`)
42
48
  console.log(` <status>error</status>`)
@@ -50,9 +56,13 @@ function outputError(error: unknown, options: { xml?: boolean }, resultTag: stri
50
56
  // Helper function to execute Effect with standard error handling
51
57
  async function executeEffect<E>(
52
58
  effect: Effect.Effect<void, E, never>,
53
- options: { xml?: boolean },
59
+ options: { xml?: boolean; json?: boolean },
54
60
  resultTag: string,
55
61
  ): Promise<void> {
62
+ if (options.xml && options.json) {
63
+ outputError(new Error('--xml and --json are mutually exclusive'), options, resultTag)
64
+ process.exit(1)
65
+ }
56
66
  try {
57
67
  await Effect.runPromise(effect)
58
68
  } catch (error) {
@@ -83,6 +93,7 @@ export function registerCommands(program: Command): void {
83
93
  .command('status')
84
94
  .description('Check connection status')
85
95
  .option('--xml', 'XML output for LLM consumption')
96
+ .option('--json', 'JSON output for programmatic consumption')
86
97
  .action(async (options) => {
87
98
  await executeEffect(
88
99
  statusCommand(options).pipe(
@@ -105,9 +116,14 @@ export function registerCommands(program: Command): void {
105
116
  'Line number in the NEW version of the file (not diff line numbers)',
106
117
  parseInt,
107
118
  )
119
+ .option(
120
+ '--reply-to <comment-id>',
121
+ 'Reply to a comment thread (requires --file and --line; resolves thread by default)',
122
+ )
108
123
  .option('--unresolved', 'Mark comment as unresolved (requires human attention)')
109
124
  .option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
110
125
  .option('--xml', 'XML output for LLM consumption')
126
+ .option('--json', 'JSON output for programmatic consumption')
111
127
  .addHelpText('after', COMMENT_HELP_TEXT)
112
128
  .action(async (changeId, options) => {
113
129
  await executeEffect(
@@ -125,6 +141,7 @@ export function registerCommands(program: Command): void {
125
141
  .command('diff <change-id>')
126
142
  .description('Get diff for a change (accepts change number or Change-ID)')
127
143
  .option('--xml', 'XML output for LLM consumption')
144
+ .option('--json', 'JSON output for programmatic consumption')
128
145
  .option('--file <file>', 'Specific file to diff')
129
146
  .option('--files-only', 'List changed files only')
130
147
  .option('--format <format>', 'Output format (unified, json, files)')
@@ -144,6 +161,7 @@ export function registerCommands(program: Command): void {
144
161
  .command('mine')
145
162
  .description('Show your open changes')
146
163
  .option('--xml', 'XML output for LLM consumption')
164
+ .option('--json', 'JSON output for programmatic consumption')
147
165
  .action(async (options) => {
148
166
  await executeEffect(
149
167
  mineCommand(options).pipe(
@@ -160,6 +178,7 @@ export function registerCommands(program: Command): void {
160
178
  .command('search [query]')
161
179
  .description('Search changes using Gerrit query syntax')
162
180
  .option('--xml', 'XML output for LLM consumption')
181
+ .option('--json', 'JSON output for programmatic consumption')
163
182
  .option('-n, --limit <number>', 'Limit results (default: 25)')
164
183
  .addHelpText('after', SEARCH_HELP_TEXT)
165
184
  .action(async (query, options) => {
@@ -168,16 +187,17 @@ export function registerCommands(program: Command): void {
168
187
  Effect.provide(ConfigServiceLive),
169
188
  )
170
189
  await Effect.runPromise(effect).catch((error: unknown) => {
171
- if (options.xml) {
190
+ const errorMessage = error instanceof Error ? error.message : String(error)
191
+ if (options.json) {
192
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
193
+ } else if (options.xml) {
172
194
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
173
195
  console.log(`<search_result>`)
174
196
  console.log(` <status>error</status>`)
175
- console.log(
176
- ` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
177
- )
197
+ console.log(` <error><![CDATA[${errorMessage}]]></error>`)
178
198
  console.log(`</search_result>`)
179
199
  } else {
180
- console.error('✗ Error:', error instanceof Error ? error.message : String(error))
200
+ console.error('✗ Error:', errorMessage)
181
201
  }
182
202
  process.exit(1)
183
203
  })
@@ -190,6 +210,7 @@ export function registerCommands(program: Command): void {
190
210
  'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
191
211
  )
192
212
  .option('--xml', 'XML output for LLM consumption')
213
+ .option('--json', 'JSON output for programmatic consumption')
193
214
  .action(async (changeId, options) => {
194
215
  await executeEffect(
195
216
  workspaceCommand(changeId, options).pipe(
@@ -206,6 +227,7 @@ export function registerCommands(program: Command): void {
206
227
  .command('incoming')
207
228
  .description('Show incoming changes for review (where you are a reviewer)')
208
229
  .option('--xml', 'XML output for LLM consumption')
230
+ .option('--json', 'JSON output for programmatic consumption')
209
231
  .option('-i, --interactive', 'Interactive mode with detailed view and diff')
210
232
  .action(async (options) => {
211
233
  await executeEffect(
@@ -226,6 +248,7 @@ export function registerCommands(program: Command): void {
226
248
  )
227
249
  .option('-m, --message <message>', 'Abandon message')
228
250
  .option('--xml', 'XML output for LLM consumption')
251
+ .option('--json', 'JSON output for programmatic consumption')
229
252
  .action(async (changeId, options) => {
230
253
  await executeEffect(
231
254
  abandonCommand(changeId, options).pipe(
@@ -243,6 +266,7 @@ export function registerCommands(program: Command): void {
243
266
  .description('Restore an abandoned change (accepts change number or Change-ID)')
244
267
  .option('-m, --message <message>', 'Restoration message')
245
268
  .option('--xml', 'XML output for LLM consumption')
269
+ .option('--json', 'JSON output for programmatic consumption')
246
270
  .action(async (changeId, options) => {
247
271
  await executeEffect(
248
272
  restoreCommand(changeId, options).pipe(
@@ -260,6 +284,7 @@ export function registerCommands(program: Command): void {
260
284
  .description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
261
285
  .option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
262
286
  .option('--xml', 'XML output for LLM consumption')
287
+ .option('--json', 'JSON output for programmatic consumption')
263
288
  .action(async (changeId, options) => {
264
289
  await executeEffect(
265
290
  rebaseCommand(changeId, options).pipe(
@@ -276,6 +301,7 @@ export function registerCommands(program: Command): void {
276
301
  .command('submit <change-id>')
277
302
  .description('Submit a change for merging (accepts change number or Change-ID)')
278
303
  .option('--xml', 'XML output for LLM consumption')
304
+ .option('--json', 'JSON output for programmatic consumption')
279
305
  .action(async (changeId, options) => {
280
306
  await executeEffect(
281
307
  submitCommand(changeId, options).pipe(
@@ -293,6 +319,7 @@ export function registerCommands(program: Command): void {
293
319
  .description('Get, set, or remove topic for a change (auto-detects from HEAD if not specified)')
294
320
  .option('--delete', 'Remove the topic from the change')
295
321
  .option('--xml', 'XML output for LLM consumption')
322
+ .option('--json', 'JSON output for programmatic consumption')
296
323
  .addHelpText('after', TOPIC_HELP_TEXT)
297
324
  .action(async (changeId, topic, options) => {
298
325
  await executeEffect(
@@ -314,6 +341,7 @@ export function registerCommands(program: Command): void {
314
341
  .option('--label <name> <value>', 'Custom label vote (can be used multiple times)')
315
342
  .option('-m, --message <message>', 'Comment with vote')
316
343
  .option('--xml', 'XML output for LLM consumption')
344
+ .option('--json', 'JSON output for programmatic consumption')
317
345
  .action(async (changeId, options) => {
318
346
  await executeEffect(
319
347
  voteCommand(changeId, options).pipe(
@@ -334,6 +362,7 @@ export function registerCommands(program: Command): void {
334
362
  .description('List Gerrit projects')
335
363
  .option('--pattern <regex>', 'Filter projects by name pattern')
336
364
  .option('--xml', 'XML output for LLM consumption')
365
+ .option('--json', 'JSON output for programmatic consumption')
337
366
  .action(async (options) => {
338
367
  await executeEffect(
339
368
  projectsCommand(options).pipe(
@@ -355,6 +384,7 @@ export function registerCommands(program: Command): void {
355
384
  'Show all comments on a change with diff context (accepts change number or Change-ID)',
356
385
  )
357
386
  .option('--xml', 'XML output for LLM consumption')
387
+ .option('--json', 'JSON output for programmatic consumption')
358
388
  .action(async (changeId, options) => {
359
389
  await executeEffect(
360
390
  commentsCommand(changeId, options).pipe(
@@ -523,6 +553,7 @@ Note:
523
553
  .description('Install the Gerrit commit-msg hook for automatic Change-Id generation')
524
554
  .option('--force', 'Overwrite existing hook')
525
555
  .option('--xml', 'XML output for LLM consumption')
556
+ .option('--json', 'JSON output for programmatic consumption')
526
557
  .addHelpText(
527
558
  'after',
528
559
  `
@@ -623,10 +654,8 @@ Note:
623
654
  `
624
655
  This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
625
656
  It performs a two-stage review process:
626
-
627
657
  1. Generates inline comments for specific code issues
628
658
  2. Generates an overall review comment
629
-
630
659
  By default, the review is only displayed in the terminal.
631
660
  Use --comment to post the review to Gerrit (with confirmation prompts).
632
661
  Use --comment --yes to post without confirmation.
@@ -636,22 +665,11 @@ Requirements:
636
665
  - Gerrit credentials must be configured (run 'ger setup' first)
637
666
 
638
667
  Examples:
639
- # Review a change using change number (display only)
640
668
  $ ger review 12345
641
-
642
- # Review using Change-ID
643
669
  $ ger review If5a3ae8cb5a107e187447802358417f311d0c4b1
644
-
645
- # Review and prompt to post comments
646
670
  $ ger review 12345 --comment
647
-
648
- # Review and auto-post comments without prompting
649
671
  $ ger review 12345 --comment --yes
650
-
651
- # Use specific AI tool
652
672
  $ ger review 12345 --tool gemini
653
-
654
- # Show debug output to troubleshoot issues
655
673
  $ ger review 12345 --debug
656
674
 
657
675
  Note: Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
@@ -9,14 +9,26 @@ import { groupsMembersCommand } from './commands/groups-members'
9
9
  // Helper function to execute Effect with standard error handling
10
10
  async function executeEffect<E>(
11
11
  effect: Effect.Effect<void, E, never>,
12
- options: { xml?: boolean },
12
+ options: { xml?: boolean; json?: boolean },
13
13
  resultTag: string,
14
14
  ): Promise<void> {
15
+ if (options.xml && options.json) {
16
+ console.log(
17
+ JSON.stringify(
18
+ { status: 'error', error: '--xml and --json are mutually exclusive' },
19
+ null,
20
+ 2,
21
+ ),
22
+ )
23
+ process.exit(1)
24
+ }
15
25
  try {
16
26
  await Effect.runPromise(effect)
17
27
  } catch (error) {
18
28
  const errorMessage = error instanceof Error ? error.message : String(error)
19
- if (options.xml) {
29
+ if (options.json) {
30
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
31
+ } else if (options.xml) {
20
32
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
21
33
  console.log(`<${resultTag}>`)
22
34
  console.log(` <status>error</status>`)
@@ -43,6 +55,7 @@ export function registerGroupCommands(program: Command): void {
43
55
  .option('--user <account>', 'Show groups a user belongs to')
44
56
  .option('--limit <n>', 'Maximum number of results (default: 25)')
45
57
  .option('--xml', 'XML output for LLM consumption')
58
+ .option('--json', 'JSON output for programmatic consumption')
46
59
  .action(async (options) => {
47
60
  await executeEffect(
48
61
  groupsCommand(options).pipe(
@@ -59,6 +72,7 @@ export function registerGroupCommands(program: Command): void {
59
72
  .command('groups-show <group-id>')
60
73
  .description('Show detailed information about a Gerrit group')
61
74
  .option('--xml', 'XML output for LLM consumption')
75
+ .option('--json', 'JSON output for programmatic consumption')
62
76
  .action(async (groupId, options) => {
63
77
  await executeEffect(
64
78
  groupsShowCommand(groupId, options).pipe(
@@ -75,6 +89,7 @@ export function registerGroupCommands(program: Command): void {
75
89
  .command('groups-members <group-id>')
76
90
  .description('List all members of a Gerrit group')
77
91
  .option('--xml', 'XML output for LLM consumption')
92
+ .option('--json', 'JSON output for programmatic consumption')
78
93
  .action(async (groupId, options) => {
79
94
  await executeEffect(
80
95
  groupsMembersCommand(groupId, options).pipe(
@@ -8,14 +8,26 @@ import { removeReviewerCommand } from './commands/remove-reviewer'
8
8
  // Helper function to execute Effect with standard error handling
9
9
  async function executeEffect<E>(
10
10
  effect: Effect.Effect<void, E, never>,
11
- options: { xml?: boolean },
11
+ options: { xml?: boolean; json?: boolean },
12
12
  resultTag: string,
13
13
  ): Promise<void> {
14
+ if (options.xml && options.json) {
15
+ console.log(
16
+ JSON.stringify(
17
+ { status: 'error', error: '--xml and --json are mutually exclusive' },
18
+ null,
19
+ 2,
20
+ ),
21
+ )
22
+ process.exit(1)
23
+ }
14
24
  try {
15
25
  await Effect.runPromise(effect)
16
26
  } catch (error) {
17
27
  const errorMessage = error instanceof Error ? error.message : String(error)
18
- if (options.xml) {
28
+ if (options.json) {
29
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
30
+ } else if (options.xml) {
19
31
  console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
20
32
  console.log(`<${resultTag}>`)
21
33
  console.log(` <status>error</status>`)
@@ -44,6 +56,7 @@ export function registerReviewerCommands(program: Command): void {
44
56
  'Notification level: none, owner, owner_reviewers, all (default: all)',
45
57
  )
46
58
  .option('--xml', 'XML output for LLM consumption')
59
+ .option('--json', 'JSON output for programmatic consumption')
47
60
  .addHelpText(
48
61
  'after',
49
62
  `
@@ -76,6 +89,7 @@ Examples:
76
89
  'Notification level: none, owner, owner_reviewers, all (default: all)',
77
90
  )
78
91
  .option('--xml', 'XML output for LLM consumption')
92
+ .option('--json', 'JSON output for programmatic consumption')
79
93
  .addHelpText(
80
94
  'after',
81
95
  `
@@ -1,6 +1,5 @@
1
1
  import { Schema } from '@effect/schema'
2
2
 
3
- // Authentication schemas
4
3
  export const GerritCredentials: Schema.Schema<{
5
4
  readonly host: string
6
5
  readonly username: string
@@ -21,7 +20,6 @@ export const GerritCredentials: Schema.Schema<{
21
20
  })
22
21
  export type GerritCredentials = Schema.Schema.Type<typeof GerritCredentials>
23
22
 
24
- // Forward declare RevisionInfo type for use in ChangeInfo
25
23
  export interface RevisionInfoType {
26
24
  readonly kind?: string
27
25
  readonly _number: number
@@ -65,7 +63,22 @@ export interface RevisionInfoType {
65
63
  >
66
64
  }
67
65
 
68
- // Change schemas
66
+ type ChangeReviewerAccount = {
67
+ readonly _account_id?: number
68
+ readonly name?: string
69
+ readonly email?: string
70
+ readonly username?: string
71
+ }
72
+ const ChangeReviewerAccountInfo: Schema.Schema<ChangeReviewerAccount> = Schema.Struct({
73
+ _account_id: Schema.optional(Schema.Number),
74
+ name: Schema.optional(Schema.String),
75
+ email: Schema.optional(Schema.String),
76
+ username: Schema.optional(Schema.String),
77
+ })
78
+ type ChangeReviewerMap = Partial<
79
+ Record<'REVIEWER' | 'CC' | 'REMOVED', ReadonlyArray<ChangeReviewerAccount>>
80
+ >
81
+
69
82
  export const ChangeInfo: Schema.Schema<{
70
83
  readonly id: string
71
84
  readonly project: string
@@ -119,6 +132,7 @@ export const ChangeInfo: Schema.Schema<{
119
132
  readonly current_revision?: string
120
133
  readonly revisions?: Record<string, RevisionInfoType>
121
134
  readonly topic?: string
135
+ readonly reviewers?: ChangeReviewerMap
122
136
  }> = Schema.Struct({
123
137
  id: Schema.String,
124
138
  project: Schema.String,
@@ -184,10 +198,16 @@ export const ChangeInfo: Schema.Schema<{
184
198
  current_revision: Schema.optional(Schema.String),
185
199
  revisions: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Any })),
186
200
  topic: Schema.optional(Schema.String),
201
+ reviewers: Schema.optional(
202
+ Schema.Struct({
203
+ REVIEWER: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
204
+ CC: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
205
+ REMOVED: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
206
+ }),
207
+ ),
187
208
  })
188
209
  export type ChangeInfo = Schema.Schema.Type<typeof ChangeInfo>
189
210
 
190
- // Comment schemas
191
211
  export const CommentInput: Schema.Schema<{
192
212
  readonly message: string
193
213
  readonly unresolved?: boolean
@@ -200,7 +220,6 @@ export const CommentInput: Schema.Schema<{
200
220
  })
201
221
  export type CommentInput = Schema.Schema.Type<typeof CommentInput>
202
222
 
203
- // Comment info returned from API
204
223
  export const CommentInfo: Schema.Schema<{
205
224
  readonly id: string
206
225
  readonly path?: string
@@ -246,7 +265,6 @@ export const CommentInfo: Schema.Schema<{
246
265
  })
247
266
  export type CommentInfo = Schema.Schema.Type<typeof CommentInfo>
248
267
 
249
- // Message info for review messages
250
268
  export const MessageInfo: Schema.Schema<{
251
269
  readonly id: string
252
270
  readonly message: string
@@ -290,6 +308,7 @@ export const ReviewInput: Schema.Schema<{
290
308
  readonly message: string
291
309
  readonly side?: 'PARENT' | 'REVISION'
292
310
  readonly unresolved?: boolean
311
+ readonly in_reply_to?: string
293
312
  }>
294
313
  >
295
314
  }> = Schema.Struct({
@@ -312,6 +331,7 @@ export const ReviewInput: Schema.Schema<{
312
331
  message: Schema.String,
313
332
  side: Schema.optional(Schema.Literal('PARENT', 'REVISION')),
314
333
  unresolved: Schema.optional(Schema.Boolean),
334
+ in_reply_to: Schema.optional(Schema.String),
315
335
  }),
316
336
  ),
317
337
  }),
@@ -319,7 +339,6 @@ export const ReviewInput: Schema.Schema<{
319
339
  })
320
340
  export type ReviewInput = Schema.Schema.Type<typeof ReviewInput>
321
341
 
322
- // Project schema
323
342
  export const ProjectInfo: Schema.Schema<{
324
343
  readonly id: string
325
344
  readonly name: string
@@ -333,7 +352,6 @@ export const ProjectInfo: Schema.Schema<{
333
352
  })
334
353
  export type ProjectInfo = Schema.Schema.Type<typeof ProjectInfo>
335
354
 
336
- // File and diff schemas
337
355
  export const FileInfo: Schema.Schema<{
338
356
  readonly status?: 'A' | 'D' | 'R' | 'C' | 'M'
339
357
  readonly lines_inserted?: number
@@ -503,17 +521,14 @@ export type DiffOptions = Schema.Schema.Type<typeof DiffOptions>
503
521
  // Command options schemas
504
522
  export const DiffCommandOptions: Schema.Schema<{
505
523
  readonly xml?: boolean
524
+ readonly json?: boolean
506
525
  readonly file?: string
507
526
  readonly filesOnly?: boolean
508
527
  readonly format?: 'unified' | 'json' | 'files'
509
528
  }> = Schema.Struct({
510
529
  xml: Schema.optional(Schema.Boolean),
511
- file: Schema.optional(
512
- Schema.String.pipe(
513
- Schema.minLength(1),
514
- Schema.annotations({ description: 'File path for diff (relative to repo root)' }),
515
- ),
516
- ),
530
+ json: Schema.optional(Schema.Boolean),
531
+ file: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
517
532
  filesOnly: Schema.optional(Schema.Boolean),
518
533
  format: Schema.optional(DiffFormat),
519
534
  })
@@ -135,11 +135,9 @@ export const getHooksDir = (): string => {
135
135
  export interface CommitHookServiceImpl {
136
136
  readonly hasHook: () => Effect.Effect<boolean, NotGitRepoError>
137
137
  readonly hasChangeId: (commit?: string) => Effect.Effect<boolean, NotGitRepoError>
138
- readonly installHook: () => Effect.Effect<
139
- void,
140
- HookInstallError | NotGitRepoError,
141
- ConfigServiceImpl
142
- >
138
+ readonly installHook: (
139
+ quiet?: boolean,
140
+ ) => Effect.Effect<void, HookInstallError | NotGitRepoError, ConfigServiceImpl>
143
141
  readonly ensureChangeId: () => Effect.Effect<
144
142
  void,
145
143
  HookInstallError | MissingChangeIdError | NotGitRepoError,
@@ -161,7 +159,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
161
159
  catch: () => new NotGitRepoError({ message: 'Not in a git repository' }),
162
160
  }),
163
161
 
164
- installHook: () =>
162
+ installHook: (quiet = false) =>
165
163
  Effect.gen(function* () {
166
164
  const configService = yield* ConfigService
167
165
 
@@ -177,7 +175,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
177
175
  const normalizedHost = config.host.replace(/\/$/, '')
178
176
  const hookUrl = `${normalizedHost}/tools/hooks/commit-msg`
179
177
 
180
- yield* Console.log(`Installing commit-msg hook from ${config.host}...`)
178
+ if (!quiet) yield* Console.log(`Installing commit-msg hook from ${config.host}...`)
181
179
 
182
180
  const hookContent = yield* Effect.tryPromise({
183
181
  try: async () => {
@@ -236,7 +234,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
236
234
  }),
237
235
  })
238
236
 
239
- yield* Console.log('commit-msg hook installed successfully')
237
+ if (!quiet) yield* Console.log('commit-msg hook installed successfully')
240
238
  }),
241
239
 
242
240
  ensureChangeId: () =>