@aaronshaf/ger 1.2.11 → 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 -196
  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,245 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { Schema } from '@effect/schema'
3
+ import { Effect } from 'effect'
4
+ import { AppConfig, AiConfig, aiConfigFromFlat, migrateFromNestedConfig } from './config'
5
+
6
+ describe('Config Schemas', () => {
7
+ describe('AppConfig (Flat Structure)', () => {
8
+ test('validates complete flat config', () => {
9
+ const validConfig = {
10
+ host: 'https://gerrit.example.com',
11
+ username: 'testuser',
12
+ password: 'testpass123',
13
+ aiTool: 'claude' as const,
14
+ aiAutoDetect: true,
15
+ }
16
+
17
+ const result = Schema.decodeUnknownSync(AppConfig)(validConfig)
18
+ expect(result).toEqual(validConfig)
19
+ })
20
+
21
+ test('validates minimal flat config with defaults', () => {
22
+ const minimalConfig = {
23
+ host: 'https://gerrit.example.com',
24
+ username: 'testuser',
25
+ password: 'testpass123',
26
+ }
27
+
28
+ const result = Schema.decodeUnknownSync(AppConfig)(minimalConfig)
29
+ expect(result).toEqual({
30
+ ...minimalConfig,
31
+ aiAutoDetect: true, // default value
32
+ aiTool: undefined,
33
+ })
34
+ })
35
+
36
+ test('rejects invalid host URL', () => {
37
+ const invalidConfig = {
38
+ host: 'not-a-url',
39
+ username: 'testuser',
40
+ password: 'testpass123',
41
+ }
42
+
43
+ expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
44
+ })
45
+
46
+ test('rejects empty username', () => {
47
+ const invalidConfig = {
48
+ host: 'https://gerrit.example.com',
49
+ username: '',
50
+ password: 'testpass123',
51
+ }
52
+
53
+ expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
54
+ })
55
+
56
+ test('rejects empty password', () => {
57
+ const invalidConfig = {
58
+ host: 'https://gerrit.example.com',
59
+ username: 'testuser',
60
+ password: '',
61
+ }
62
+
63
+ expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
64
+ })
65
+
66
+ test('validates all AI tool options', () => {
67
+ const tools = ['claude', 'llm', 'opencode', 'gemini'] as const
68
+
69
+ tools.forEach((tool) => {
70
+ const config = {
71
+ host: 'https://gerrit.example.com',
72
+ username: 'testuser',
73
+ password: 'testpass123',
74
+ aiTool: tool,
75
+ }
76
+
77
+ const result = Schema.decodeUnknownSync(AppConfig)(config)
78
+ expect(result.aiTool).toBe(tool)
79
+ })
80
+ })
81
+
82
+ test('rejects invalid AI tool', () => {
83
+ const invalidConfig = {
84
+ host: 'https://gerrit.example.com',
85
+ username: 'testuser',
86
+ password: 'testpass123',
87
+ aiTool: 'invalid-tool',
88
+ }
89
+
90
+ expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
91
+ })
92
+ })
93
+
94
+ describe('Legacy AiConfig (Backward Compatibility)', () => {
95
+ test('validates legacy AI config structure', () => {
96
+ const validAiConfig = {
97
+ tool: 'claude' as const,
98
+ autoDetect: false,
99
+ }
100
+
101
+ const result = Schema.decodeUnknownSync(AiConfig)(validAiConfig)
102
+ expect(result).toEqual(validAiConfig)
103
+ })
104
+
105
+ test('validates minimal legacy AI config with defaults', () => {
106
+ const minimalAiConfig = {}
107
+
108
+ const result = Schema.decodeUnknownSync(AiConfig)(minimalAiConfig)
109
+ expect(result).toEqual({
110
+ autoDetect: true, // default value
111
+ tool: undefined,
112
+ })
113
+ })
114
+ })
115
+
116
+ describe('Helper Functions', () => {
117
+ test('aiConfigFromFlat converts flat config to legacy AI config', () => {
118
+ const flatConfig = {
119
+ host: 'https://gerrit.example.com',
120
+ username: 'testuser',
121
+ password: 'testpass123',
122
+ aiTool: 'claude' as const,
123
+ aiAutoDetect: false,
124
+ }
125
+
126
+ const aiConfig = aiConfigFromFlat(flatConfig)
127
+ expect(aiConfig).toEqual({
128
+ tool: 'claude',
129
+ autoDetect: false,
130
+ })
131
+ })
132
+
133
+ test('aiConfigFromFlat handles undefined AI options', () => {
134
+ const flatConfig = {
135
+ host: 'https://gerrit.example.com',
136
+ username: 'testuser',
137
+ password: 'testpass123',
138
+ aiAutoDetect: true,
139
+ }
140
+
141
+ const aiConfig = aiConfigFromFlat(flatConfig)
142
+ expect(aiConfig).toEqual({
143
+ tool: undefined,
144
+ autoDetect: true,
145
+ })
146
+ })
147
+
148
+ test('migrateFromNestedConfig converts old nested format', () => {
149
+ const nestedConfig = {
150
+ credentials: {
151
+ host: 'https://gerrit.example.com',
152
+ username: 'testuser',
153
+ password: 'testpass123',
154
+ },
155
+ ai: {
156
+ tool: 'claude' as const,
157
+ autoDetect: false,
158
+ },
159
+ }
160
+
161
+ const flatConfig = migrateFromNestedConfig(nestedConfig)
162
+ expect(flatConfig).toEqual({
163
+ host: 'https://gerrit.example.com',
164
+ username: 'testuser',
165
+ password: 'testpass123',
166
+ aiTool: 'claude',
167
+ aiAutoDetect: false,
168
+ })
169
+ })
170
+
171
+ test('migrateFromNestedConfig handles missing AI config', () => {
172
+ const nestedConfig = {
173
+ credentials: {
174
+ host: 'https://gerrit.example.com',
175
+ username: 'testuser',
176
+ password: 'testpass123',
177
+ },
178
+ }
179
+
180
+ const flatConfig = migrateFromNestedConfig(nestedConfig)
181
+ expect(flatConfig).toEqual({
182
+ host: 'https://gerrit.example.com',
183
+ username: 'testuser',
184
+ password: 'testpass123',
185
+ aiTool: undefined,
186
+ aiAutoDetect: true, // default
187
+ })
188
+ })
189
+
190
+ test('migrateFromNestedConfig handles partial AI config', () => {
191
+ const nestedConfig = {
192
+ credentials: {
193
+ host: 'https://gerrit.example.com',
194
+ username: 'testuser',
195
+ password: 'testpass123',
196
+ },
197
+ ai: {
198
+ tool: 'llm' as const,
199
+ // autoDetect missing
200
+ },
201
+ }
202
+
203
+ const flatConfig = migrateFromNestedConfig(nestedConfig)
204
+ expect(flatConfig).toEqual({
205
+ host: 'https://gerrit.example.com',
206
+ username: 'testuser',
207
+ password: 'testpass123',
208
+ aiTool: 'llm',
209
+ aiAutoDetect: true, // default when missing
210
+ })
211
+ })
212
+ })
213
+
214
+ describe('Effect Schema Integration', () => {
215
+ test('Effect.gen with valid flat config', async () => {
216
+ const config = {
217
+ host: 'https://gerrit.example.com',
218
+ username: 'testuser',
219
+ password: 'testpass123',
220
+ aiTool: 'claude' as const,
221
+ aiAutoDetect: true,
222
+ }
223
+
224
+ const result = await Effect.gen(function* () {
225
+ return yield* Schema.decodeUnknown(AppConfig)(config)
226
+ }).pipe(Effect.runPromise)
227
+
228
+ expect(result).toEqual(config)
229
+ })
230
+
231
+ test('Effect.gen with validation error', async () => {
232
+ const invalidConfig = {
233
+ host: 'not-a-url',
234
+ username: 'testuser',
235
+ password: 'testpass123',
236
+ }
237
+
238
+ await expect(
239
+ Effect.gen(function* () {
240
+ return yield* Schema.decodeUnknown(AppConfig)(invalidConfig)
241
+ }).pipe(Effect.runPromise),
242
+ ).rejects.toThrow()
243
+ })
244
+ })
245
+ })
@@ -0,0 +1,84 @@
1
+ import { Schema } from '@effect/schema'
2
+
3
+ // Flat Application Configuration (similar to ji structure)
4
+ export const AppConfig: Schema.Struct<{
5
+ host: typeof Schema.String
6
+ username: typeof Schema.String
7
+ password: typeof Schema.String
8
+ aiTool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
9
+ aiAutoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
10
+ }> = Schema.Struct({
11
+ // Gerrit credentials (flattened)
12
+ host: Schema.String.pipe(
13
+ Schema.pattern(/^https?:\/\/.+$/),
14
+ Schema.annotations({ description: 'Gerrit server URL' }),
15
+ ),
16
+ username: Schema.String.pipe(
17
+ Schema.minLength(1),
18
+ Schema.annotations({ description: 'Gerrit username' }),
19
+ ),
20
+ password: Schema.String.pipe(
21
+ Schema.minLength(1),
22
+ Schema.annotations({ description: 'HTTP password or API token' }),
23
+ ),
24
+ // AI configuration (flattened)
25
+ aiTool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
26
+ aiAutoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
27
+ })
28
+
29
+ export type AppConfig = Schema.Schema.Type<typeof AppConfig>
30
+
31
+ // Legacy schemas for backward compatibility (deprecated)
32
+ export const AiConfig: Schema.Struct<{
33
+ tool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
34
+ autoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
35
+ }> = Schema.Struct({
36
+ tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
37
+ autoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
38
+ })
39
+
40
+ export type AiConfig = Schema.Schema.Type<typeof AiConfig>
41
+
42
+ // Helper to convert from flat config to legacy AI config
43
+ export const aiConfigFromFlat = (config: AppConfig): AiConfig => ({
44
+ tool: config.aiTool,
45
+ autoDetect: config.aiAutoDetect,
46
+ })
47
+
48
+ // Schema for validating legacy nested config structure
49
+ const LegacyNestedConfig = Schema.Struct({
50
+ credentials: Schema.Struct({
51
+ host: Schema.String.pipe(
52
+ Schema.pattern(/^https?:\/\/.+$/),
53
+ Schema.annotations({ description: 'Gerrit server URL' }),
54
+ ),
55
+ username: Schema.String.pipe(Schema.minLength(1)),
56
+ password: Schema.String.pipe(Schema.minLength(1)),
57
+ }),
58
+ ai: Schema.optional(
59
+ Schema.Struct({
60
+ tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
61
+ autoDetect: Schema.optional(Schema.Boolean),
62
+ }),
63
+ ),
64
+ })
65
+
66
+ type LegacyNestedConfig = Schema.Schema.Type<typeof LegacyNestedConfig>
67
+
68
+ // Helper to convert from legacy nested format to flat format with validation
69
+ export const migrateFromNestedConfig = (nested: unknown): AppConfig => {
70
+ // Validate input structure using Schema
71
+ const validatedNested = Schema.decodeUnknownSync(LegacyNestedConfig)(nested)
72
+
73
+ // Convert to flat structure
74
+ const flatConfig = {
75
+ host: validatedNested.credentials.host,
76
+ username: validatedNested.credentials.username,
77
+ password: validatedNested.credentials.password,
78
+ aiTool: validatedNested.ai?.tool,
79
+ aiAutoDetect: validatedNested.ai?.autoDetect ?? true,
80
+ }
81
+
82
+ // Validate the resulting flat config
83
+ return Schema.decodeUnknownSync(AppConfig)(flatConfig)
84
+ }