@aaronshaf/ger 1.2.10 → 2.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 (180) hide show
  1. package/.ast-grep/rules/no-as-casting.yml +13 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.github/workflows/ci-simple.yml +53 -0
  4. package/.github/workflows/ci.yml +171 -0
  5. package/.github/workflows/claude-code-review.yml +83 -0
  6. package/.github/workflows/claude.yml +50 -0
  7. package/.github/workflows/dependency-update.yml +84 -0
  8. package/.github/workflows/release.yml +166 -0
  9. package/.github/workflows/security-scan.yml +113 -0
  10. package/.github/workflows/security.yml +96 -0
  11. package/.husky/pre-commit +16 -0
  12. package/.husky/pre-push +25 -0
  13. package/.lintstagedrc.json +6 -0
  14. package/.tool-versions +1 -0
  15. package/CLAUDE.md +105 -0
  16. package/DEVELOPMENT.md +361 -0
  17. package/EXAMPLES.md +457 -0
  18. package/README.md +831 -16
  19. package/bin/ger +3 -18
  20. package/biome.json +36 -0
  21. package/bun.lock +678 -0
  22. package/bunfig.toml +8 -0
  23. package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
  24. package/docs/adr/0002-use-bun-runtime.md +64 -0
  25. package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
  26. package/docs/adr/0004-use-commander-for-cli.md +76 -0
  27. package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
  28. package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
  29. package/docs/adr/0007-git-hooks-for-quality.md +94 -0
  30. package/docs/adr/0008-no-as-typecasting.md +83 -0
  31. package/docs/adr/0009-file-size-limits.md +82 -0
  32. package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
  33. package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
  34. package/docs/adr/0012-build-status-message-parsing.md +94 -0
  35. package/docs/adr/0013-git-subprocess-integration.md +98 -0
  36. package/docs/adr/0014-group-management-support.md +95 -0
  37. package/docs/adr/0015-batch-comment-processing.md +111 -0
  38. package/docs/adr/0016-flexible-change-identifiers.md +94 -0
  39. package/docs/adr/0017-git-worktree-support.md +102 -0
  40. package/docs/adr/0018-auto-install-commit-hook.md +103 -0
  41. package/docs/adr/0019-sdk-package-exports.md +95 -0
  42. package/docs/adr/0020-code-coverage-enforcement.md +105 -0
  43. package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
  44. package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
  45. package/docs/adr/README.md +30 -0
  46. package/docs/prd/README.md +12 -0
  47. package/docs/prd/architecture.md +325 -0
  48. package/docs/prd/commands.md +425 -0
  49. package/docs/prd/data-model.md +349 -0
  50. package/docs/prd/overview.md +124 -0
  51. package/index.ts +219 -0
  52. package/oxlint.json +24 -0
  53. package/package.json +82 -15
  54. package/scripts/check-coverage.ts +69 -0
  55. package/scripts/check-file-size.ts +38 -0
  56. package/scripts/fix-test-mocks.ts +55 -0
  57. package/skills/gerrit-workflow/SKILL.md +247 -0
  58. package/skills/gerrit-workflow/examples.md +572 -0
  59. package/skills/gerrit-workflow/reference.md +728 -0
  60. package/src/api/gerrit.ts +696 -0
  61. package/src/cli/commands/abandon.ts +65 -0
  62. package/src/cli/commands/add-reviewer.ts +156 -0
  63. package/src/cli/commands/build-status.ts +282 -0
  64. package/src/cli/commands/checkout.ts +422 -0
  65. package/src/cli/commands/comment.ts +460 -0
  66. package/src/cli/commands/comments.ts +85 -0
  67. package/src/cli/commands/diff.ts +71 -0
  68. package/src/cli/commands/extract-url.ts +266 -0
  69. package/src/cli/commands/groups-members.ts +104 -0
  70. package/src/cli/commands/groups-show.ts +169 -0
  71. package/src/cli/commands/groups.ts +137 -0
  72. package/src/cli/commands/incoming.ts +226 -0
  73. package/src/cli/commands/init.ts +164 -0
  74. package/src/cli/commands/mine.ts +115 -0
  75. package/src/cli/commands/open.ts +57 -0
  76. package/src/cli/commands/projects.ts +68 -0
  77. package/src/cli/commands/push.ts +430 -0
  78. package/src/cli/commands/rebase.ts +52 -0
  79. package/src/cli/commands/remove-reviewer.ts +123 -0
  80. package/src/cli/commands/restore.ts +50 -0
  81. package/src/cli/commands/review.ts +486 -0
  82. package/src/cli/commands/search.ts +162 -0
  83. package/src/cli/commands/setup.ts +286 -0
  84. package/src/cli/commands/show.ts +491 -0
  85. package/src/cli/commands/status.ts +35 -0
  86. package/src/cli/commands/submit.ts +108 -0
  87. package/src/cli/commands/vote.ts +119 -0
  88. package/src/cli/commands/workspace.ts +200 -0
  89. package/src/cli/index.ts +53 -0
  90. package/src/cli/register-commands.ts +659 -0
  91. package/src/cli/register-group-commands.ts +88 -0
  92. package/src/cli/register-reviewer-commands.ts +97 -0
  93. package/src/prompts/default-review.md +86 -0
  94. package/src/prompts/system-inline-review.md +135 -0
  95. package/src/prompts/system-overall-review.md +206 -0
  96. package/src/schemas/config.test.ts +245 -0
  97. package/src/schemas/config.ts +84 -0
  98. package/src/schemas/gerrit.ts +681 -0
  99. package/src/services/commit-hook.ts +314 -0
  100. package/src/services/config.test.ts +150 -0
  101. package/src/services/config.ts +250 -0
  102. package/src/services/git-worktree.ts +342 -0
  103. package/src/services/review-strategy.ts +292 -0
  104. package/src/test-utils/mock-generator.ts +138 -0
  105. package/src/utils/change-id.test.ts +98 -0
  106. package/src/utils/change-id.ts +63 -0
  107. package/src/utils/comment-formatters.ts +153 -0
  108. package/src/utils/diff-context.ts +103 -0
  109. package/src/utils/diff-formatters.ts +141 -0
  110. package/src/utils/formatters.ts +85 -0
  111. package/src/utils/git-commit.test.ts +277 -0
  112. package/src/utils/git-commit.ts +122 -0
  113. package/src/utils/index.ts +55 -0
  114. package/src/utils/message-filters.ts +26 -0
  115. package/src/utils/review-formatters.ts +89 -0
  116. package/src/utils/review-prompt-builder.ts +110 -0
  117. package/src/utils/shell-safety.ts +117 -0
  118. package/src/utils/status-indicators.ts +100 -0
  119. package/src/utils/url-parser.test.ts +271 -0
  120. package/src/utils/url-parser.ts +118 -0
  121. package/tests/abandon.test.ts +230 -0
  122. package/tests/add-reviewer.test.ts +579 -0
  123. package/tests/build-status-watch.test.ts +344 -0
  124. package/tests/build-status.test.ts +789 -0
  125. package/tests/change-id-formats.test.ts +268 -0
  126. package/tests/checkout/integration.test.ts +653 -0
  127. package/tests/checkout/parse-input.test.ts +55 -0
  128. package/tests/checkout/validation.test.ts +178 -0
  129. package/tests/comment-batch-advanced.test.ts +431 -0
  130. package/tests/comment-gerrit-api-compliance.test.ts +414 -0
  131. package/tests/comment.test.ts +708 -0
  132. package/tests/comments.test.ts +323 -0
  133. package/tests/config-service-simple.test.ts +100 -0
  134. package/tests/diff.test.ts +419 -0
  135. package/tests/extract-url.test.ts +517 -0
  136. package/tests/groups-members.test.ts +256 -0
  137. package/tests/groups-show.test.ts +323 -0
  138. package/tests/groups.test.ts +334 -0
  139. package/tests/helpers/build-status-test-setup.ts +83 -0
  140. package/tests/helpers/config-mock.ts +27 -0
  141. package/tests/incoming.test.ts +357 -0
  142. package/tests/init.test.ts +70 -0
  143. package/tests/integration/commit-hook.test.ts +246 -0
  144. package/tests/interactive-incoming.test.ts +173 -0
  145. package/tests/mine.test.ts +285 -0
  146. package/tests/mocks/msw-handlers.ts +80 -0
  147. package/tests/open.test.ts +233 -0
  148. package/tests/projects.test.ts +259 -0
  149. package/tests/rebase.test.ts +271 -0
  150. package/tests/remove-reviewer.test.ts +357 -0
  151. package/tests/restore.test.ts +237 -0
  152. package/tests/review.test.ts +135 -0
  153. package/tests/search.test.ts +712 -0
  154. package/tests/setup.test.ts +63 -0
  155. package/tests/show-auto-detect.test.ts +324 -0
  156. package/tests/show.test.ts +813 -0
  157. package/tests/status.test.ts +145 -0
  158. package/tests/submit.test.ts +316 -0
  159. package/tests/unit/commands/push.test.ts +194 -0
  160. package/tests/unit/git-branch-detection.test.ts +82 -0
  161. package/tests/unit/git-worktree.test.ts +55 -0
  162. package/tests/unit/patterns/push-patterns.test.ts +148 -0
  163. package/tests/unit/schemas/gerrit.test.ts +85 -0
  164. package/tests/unit/services/commit-hook.test.ts +132 -0
  165. package/tests/unit/services/review-strategy.test.ts +349 -0
  166. package/tests/unit/test-utils/mock-generator.test.ts +154 -0
  167. package/tests/unit/utils/comment-formatters.test.ts +415 -0
  168. package/tests/unit/utils/diff-context.test.ts +171 -0
  169. package/tests/unit/utils/diff-formatters.test.ts +165 -0
  170. package/tests/unit/utils/formatters.test.ts +411 -0
  171. package/tests/unit/utils/message-filters.test.ts +227 -0
  172. package/tests/unit/utils/shell-safety.test.ts +230 -0
  173. package/tests/unit/utils/status-indicators.test.ts +137 -0
  174. package/tests/vote.test.ts +317 -0
  175. package/tests/workspace.test.ts +295 -0
  176. package/tsconfig.json +36 -5
  177. package/src/commands/branch.ts +0 -180
  178. package/src/ger.ts +0 -22
  179. package/src/types.d.ts +0 -35
  180. package/src/utils.ts +0 -130
@@ -0,0 +1,344 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test'
2
+ import { http, HttpResponse } from 'msw'
3
+ import { Effect } from 'effect'
4
+ import { buildStatusCommand } from '@/cli/commands/build-status'
5
+ import { GerritApiServiceLive } from '@/api/gerrit'
6
+ import type { MessageInfo } from '@/schemas/gerrit'
7
+ import {
8
+ server,
9
+ capturedStdout,
10
+ capturedErrors,
11
+ mockProcessExit,
12
+ setupBuildStatusTests,
13
+ teardownBuildStatusTests,
14
+ resetBuildStatusMocks,
15
+ createMockConfigLayer,
16
+ } from './helpers/build-status-test-setup'
17
+
18
+ beforeAll(() => {
19
+ setupBuildStatusTests()
20
+ })
21
+
22
+ afterAll(() => {
23
+ teardownBuildStatusTests()
24
+ })
25
+
26
+ afterEach(() => {
27
+ resetBuildStatusMocks()
28
+ })
29
+
30
+ describe('build-status command - watch mode', () => {
31
+ test('polls until success state is reached', async () => {
32
+ let callCount = 0
33
+
34
+ server.use(
35
+ http.get('*/a/changes/12345', ({ request }) => {
36
+ const url = new URL(request.url)
37
+ if (url.searchParams.get('o') === 'MESSAGES') {
38
+ callCount++
39
+
40
+ let messages: MessageInfo[]
41
+ if (callCount === 1) {
42
+ // First call: pending (no build started)
43
+ messages = []
44
+ } else if (callCount === 2) {
45
+ // Second call: running (build started, no verification)
46
+ messages = [
47
+ {
48
+ id: 'msg1',
49
+ message: 'Build Started',
50
+ date: '2024-01-15 10:00:00.000000000',
51
+ author: { _account_id: 9999, name: 'CI Bot' },
52
+ },
53
+ ]
54
+ } else {
55
+ // Third call: success (verified +1)
56
+ messages = [
57
+ {
58
+ id: 'msg1',
59
+ message: 'Build Started',
60
+ date: '2024-01-15 10:00:00.000000000',
61
+ author: { _account_id: 9999, name: 'CI Bot' },
62
+ },
63
+ {
64
+ id: 'msg2',
65
+ message: 'Patch Set 1: Verified+1',
66
+ date: '2024-01-15 10:05:00.000000000',
67
+ author: { _account_id: 9999, name: 'CI Bot' },
68
+ },
69
+ ]
70
+ }
71
+
72
+ return HttpResponse.json(
73
+ { messages },
74
+ { headers: { 'Content-Type': 'application/json' } },
75
+ )
76
+ }
77
+ return HttpResponse.text('Not Found', { status: 404 })
78
+ }),
79
+ )
80
+
81
+ const effect = buildStatusCommand('12345', {
82
+ watch: true,
83
+ interval: 0.1, // Fast polling for tests
84
+ timeout: 10,
85
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
86
+
87
+ await Effect.runPromise(effect)
88
+
89
+ // Should have multiple outputs (one per poll)
90
+ expect(capturedStdout.length).toBeGreaterThanOrEqual(3)
91
+ expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'pending' })
92
+ expect(JSON.parse(capturedStdout[1])).toEqual({ state: 'running' })
93
+ expect(JSON.parse(capturedStdout[2])).toEqual({ state: 'success' })
94
+
95
+ // Minimalistic output: no stderr messages except on timeout/error
96
+ expect(capturedErrors.length).toBe(0)
97
+ })
98
+
99
+ test('polls until failure state is reached', async () => {
100
+ let callCount = 0
101
+
102
+ server.use(
103
+ http.get('*/a/changes/12345', ({ request }) => {
104
+ const url = new URL(request.url)
105
+ if (url.searchParams.get('o') === 'MESSAGES') {
106
+ callCount++
107
+
108
+ let messages: MessageInfo[]
109
+ if (callCount === 1) {
110
+ messages = [
111
+ {
112
+ id: 'msg1',
113
+ message: 'Build Started',
114
+ date: '2024-01-15 10:00:00.000000000',
115
+ author: { _account_id: 9999, name: 'CI Bot' },
116
+ },
117
+ ]
118
+ } else {
119
+ messages = [
120
+ {
121
+ id: 'msg1',
122
+ message: 'Build Started',
123
+ date: '2024-01-15 10:00:00.000000000',
124
+ author: { _account_id: 9999, name: 'CI Bot' },
125
+ },
126
+ {
127
+ id: 'msg2',
128
+ message: 'Patch Set 1: Verified-1',
129
+ date: '2024-01-15 10:05:00.000000000',
130
+ author: { _account_id: 9999, name: 'CI Bot' },
131
+ },
132
+ ]
133
+ }
134
+
135
+ return HttpResponse.json(
136
+ { messages },
137
+ { headers: { 'Content-Type': 'application/json' } },
138
+ )
139
+ }
140
+ return HttpResponse.text('Not Found', { status: 404 })
141
+ }),
142
+ )
143
+
144
+ const effect = buildStatusCommand('12345', {
145
+ watch: true,
146
+ interval: 0.1,
147
+ timeout: 10,
148
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
149
+
150
+ await Effect.runPromise(effect)
151
+
152
+ expect(capturedStdout.length).toBeGreaterThanOrEqual(2)
153
+ expect(JSON.parse(capturedStdout[capturedStdout.length - 1])).toEqual({ state: 'failure' })
154
+
155
+ // Minimalistic output: no stderr messages except on timeout/error
156
+ expect(capturedErrors.length).toBe(0)
157
+ })
158
+
159
+ test('times out after specified duration', async () => {
160
+ server.use(
161
+ http.get('*/a/changes/12345', ({ request }) => {
162
+ const url = new URL(request.url)
163
+ if (url.searchParams.get('o') === 'MESSAGES') {
164
+ // Always return running state
165
+ return HttpResponse.json(
166
+ {
167
+ messages: [
168
+ {
169
+ id: 'msg1',
170
+ message: 'Build Started',
171
+ date: '2024-01-15 10:00:00.000000000',
172
+ author: { _account_id: 9999, name: 'CI Bot' },
173
+ },
174
+ ],
175
+ },
176
+ { headers: { 'Content-Type': 'application/json' } },
177
+ )
178
+ }
179
+ return HttpResponse.text('Not Found', { status: 404 })
180
+ }),
181
+ )
182
+
183
+ const effect = buildStatusCommand('12345', {
184
+ watch: true,
185
+ interval: 0.1,
186
+ timeout: 0.5, // Very short timeout
187
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
188
+
189
+ try {
190
+ await Effect.runPromise(effect)
191
+ } catch {
192
+ // Should exit with code 2 for timeout
193
+ expect(mockProcessExit).toHaveBeenCalledWith(2)
194
+ expect(capturedErrors.some((e: string) => e.includes('Timeout'))).toBe(true)
195
+ }
196
+ })
197
+
198
+ test('exit-status flag causes exit 1 on failure', async () => {
199
+ server.use(
200
+ http.get('*/a/changes/12345', ({ request }) => {
201
+ const url = new URL(request.url)
202
+ if (url.searchParams.get('o') === 'MESSAGES') {
203
+ return HttpResponse.json(
204
+ {
205
+ messages: [
206
+ {
207
+ id: 'msg1',
208
+ message: 'Build Started',
209
+ date: '2024-01-15 10:00:00.000000000',
210
+ author: { _account_id: 9999, name: 'CI Bot' },
211
+ },
212
+ {
213
+ id: 'msg2',
214
+ message: 'Patch Set 1: Verified-1',
215
+ date: '2024-01-15 10:05:00.000000000',
216
+ author: { _account_id: 9999, name: 'CI Bot' },
217
+ },
218
+ ],
219
+ },
220
+ { headers: { 'Content-Type': 'application/json' } },
221
+ )
222
+ }
223
+ return HttpResponse.text('Not Found', { status: 404 })
224
+ }),
225
+ )
226
+
227
+ const effect = buildStatusCommand('12345', {
228
+ watch: true,
229
+ interval: 0.1,
230
+ timeout: 10,
231
+ exitStatus: true,
232
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
233
+
234
+ try {
235
+ await Effect.runPromise(effect)
236
+ } catch {
237
+ // Should exit with code 1 for failure when --exit-status is used
238
+ expect(mockProcessExit).toHaveBeenCalledWith(1)
239
+ }
240
+ })
241
+
242
+ test('exit-status flag does not affect success state', async () => {
243
+ server.use(
244
+ http.get('*/a/changes/12345', ({ request }) => {
245
+ const url = new URL(request.url)
246
+ if (url.searchParams.get('o') === 'MESSAGES') {
247
+ return HttpResponse.json(
248
+ {
249
+ messages: [
250
+ {
251
+ id: 'msg1',
252
+ message: 'Build Started',
253
+ date: '2024-01-15 10:00:00.000000000',
254
+ author: { _account_id: 9999, name: 'CI Bot' },
255
+ },
256
+ {
257
+ id: 'msg2',
258
+ message: 'Patch Set 1: Verified+1',
259
+ date: '2024-01-15 10:05:00.000000000',
260
+ author: { _account_id: 9999, name: 'CI Bot' },
261
+ },
262
+ ],
263
+ },
264
+ { headers: { 'Content-Type': 'application/json' } },
265
+ )
266
+ }
267
+ return HttpResponse.text('Not Found', { status: 404 })
268
+ }),
269
+ )
270
+
271
+ const effect = buildStatusCommand('12345', {
272
+ watch: true,
273
+ interval: 0.1,
274
+ timeout: 10,
275
+ exitStatus: true,
276
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
277
+
278
+ await Effect.runPromise(effect)
279
+
280
+ // Should not call process.exit for success state
281
+ expect(mockProcessExit).not.toHaveBeenCalled()
282
+ expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'success' })
283
+ })
284
+
285
+ test('watch mode handles not_found state', async () => {
286
+ server.use(
287
+ http.get('*/a/changes/99999', () => {
288
+ return HttpResponse.text('Not Found', { status: 404 })
289
+ }),
290
+ )
291
+
292
+ const effect = buildStatusCommand('99999', {
293
+ watch: true,
294
+ interval: 0.1,
295
+ timeout: 10,
296
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
297
+
298
+ await Effect.runPromise(effect)
299
+
300
+ expect(capturedStdout.length).toBe(1)
301
+ expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'not_found' })
302
+
303
+ // 404 errors bypass pollBuildStatus and are handled in error handler
304
+ // Minimalistic output: no stderr messages for not_found state
305
+ expect(capturedErrors.length).toBe(0)
306
+ })
307
+
308
+ test('without watch flag, behaves as single check', async () => {
309
+ server.use(
310
+ http.get('*/a/changes/12345', ({ request }) => {
311
+ const url = new URL(request.url)
312
+ if (url.searchParams.get('o') === 'MESSAGES') {
313
+ return HttpResponse.json(
314
+ {
315
+ messages: [
316
+ {
317
+ id: 'msg1',
318
+ message: 'Build Started',
319
+ date: '2024-01-15 10:00:00.000000000',
320
+ author: { _account_id: 9999, name: 'CI Bot' },
321
+ },
322
+ ],
323
+ },
324
+ { headers: { 'Content-Type': 'application/json' } },
325
+ )
326
+ }
327
+ return HttpResponse.text('Not Found', { status: 404 })
328
+ }),
329
+ )
330
+
331
+ const effect = buildStatusCommand('12345', {
332
+ watch: false,
333
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
334
+
335
+ await Effect.runPromise(effect)
336
+
337
+ // Should only have one output (no polling)
338
+ expect(capturedStdout.length).toBe(1)
339
+ expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'running' })
340
+
341
+ // Should not have watch mode messages in stderr
342
+ expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(false)
343
+ })
344
+ })