@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,286 @@
1
+ import chalk from 'chalk'
2
+ import { Effect, pipe, Console } from 'effect'
3
+ import {
4
+ ConfigService,
5
+ ConfigServiceLive,
6
+ ConfigError,
7
+ type ConfigServiceImpl,
8
+ } from '@/services/config'
9
+ import type { GerritCredentials } from '@/schemas/gerrit'
10
+ import { AppConfig } from '@/schemas/config'
11
+ import { Schema } from '@effect/schema'
12
+ import { input, password } from '@inquirer/prompts'
13
+ import { spawn } from 'node:child_process'
14
+ import { normalizeGerritHost } from '@/utils/url-parser'
15
+
16
+ // Check if a command exists on the system
17
+ const checkCommandExists = (command: string): Promise<boolean> =>
18
+ new Promise((resolve) => {
19
+ const child = spawn('which', [command], { stdio: 'ignore' })
20
+ child.on('close', (code) => {
21
+ resolve(code === 0)
22
+ })
23
+ child.on('error', () => {
24
+ resolve(false)
25
+ })
26
+ })
27
+
28
+ // AI tools to check for in order of preference
29
+ const AI_TOOLS = ['claude', 'llm', 'opencode', 'gemini'] as const
30
+
31
+ // Effect wrapper for detecting available AI tools
32
+ const detectAvailableAITools = () =>
33
+ Effect.tryPromise({
34
+ try: async () => {
35
+ const availableTools: string[] = []
36
+
37
+ for (const tool of AI_TOOLS) {
38
+ const exists = await checkCommandExists(tool)
39
+ if (exists) {
40
+ availableTools.push(tool)
41
+ }
42
+ }
43
+
44
+ return availableTools
45
+ },
46
+ catch: (error) => new ConfigError({ message: `Failed to detect AI tools: ${error}` }),
47
+ })
48
+
49
+ // Effect wrapper for getting existing config
50
+ const getExistingConfig = (configService: ConfigServiceImpl) =>
51
+ configService.getFullConfig.pipe(Effect.orElseSucceed(() => null))
52
+
53
+ // Test connection with credentials
54
+ const verifyCredentials = (credentials: GerritCredentials) =>
55
+ Effect.tryPromise({
56
+ try: async () => {
57
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')
58
+ const response = await fetch(`${credentials.host}/a/config/server/version`, {
59
+ headers: { Authorization: `Basic ${auth}` },
60
+ })
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`Authentication failed: ${response.status}`)
64
+ }
65
+
66
+ return response.ok
67
+ },
68
+ catch: (error) => {
69
+ if (error instanceof Error) {
70
+ // Authentication/permission errors
71
+ if (error.message.includes('401')) {
72
+ return new ConfigError({
73
+ message: 'Invalid credentials. Please check your username and password.',
74
+ })
75
+ }
76
+ if (error.message.includes('403')) {
77
+ return new ConfigError({
78
+ message: 'Access denied. Please verify your credentials and server permissions.',
79
+ })
80
+ }
81
+
82
+ // Network/hostname errors
83
+ if (error.message.includes('ENOTFOUND')) {
84
+ return new ConfigError({
85
+ message: `Hostname not found. Please check that the Gerrit URL is correct.\nExample: https://gerrit.example.com (without /a/ or paths)`,
86
+ })
87
+ }
88
+ if (error.message.includes('ECONNREFUSED')) {
89
+ return new ConfigError({
90
+ message: `Connection refused. The server may be down or the port may be incorrect.\nPlease verify the URL and try again.`,
91
+ })
92
+ }
93
+ if (error.message.includes('ETIMEDOUT')) {
94
+ return new ConfigError({
95
+ message: `Connection timed out. Please check:\n• Your internet connection\n• The Gerrit server URL\n• Any firewall or VPN settings`,
96
+ })
97
+ }
98
+ if (error.message.includes('certificate') || error.message.includes('SSL')) {
99
+ return new ConfigError({
100
+ message: `SSL/Certificate error. Please ensure the URL uses HTTPS and the certificate is valid.`,
101
+ })
102
+ }
103
+
104
+ // URL format errors
105
+ if (error.message.includes('Invalid URL') || error.message.includes('fetch failed')) {
106
+ return new ConfigError({
107
+ message: `Invalid URL format. Please use the full URL including https://\nExample: https://gerrit.example.com`,
108
+ })
109
+ }
110
+
111
+ // Generic network errors
112
+ if (error.message.includes('network') || error.message.includes('fetch')) {
113
+ return new ConfigError({
114
+ message: `Network error: ${error.message}\nPlease check your connection and the Gerrit server URL.`,
115
+ })
116
+ }
117
+
118
+ return new ConfigError({ message: error.message })
119
+ }
120
+ return new ConfigError({ message: 'Unknown error occurred' })
121
+ },
122
+ })
123
+
124
+ // Pure Effect-based setup implementation using inquirer
125
+ const setupEffect = (configService: ConfigServiceImpl) =>
126
+ pipe(
127
+ Effect.all([getExistingConfig(configService), detectAvailableAITools()]),
128
+ Effect.flatMap(([existingConfig, availableTools]) =>
129
+ pipe(
130
+ Console.log(chalk.bold('🔧 Gerrit CLI Setup')),
131
+ Effect.flatMap(() => Console.log('')),
132
+ Effect.flatMap(() => {
133
+ if (existingConfig) {
134
+ return Console.log(chalk.dim('(Press Enter to keep existing values)'))
135
+ } else {
136
+ return pipe(
137
+ Console.log(chalk.cyan('Please provide your Gerrit connection details:')),
138
+ Effect.flatMap(() =>
139
+ Console.log(chalk.dim('Example URL: https://gerrit.example.com')),
140
+ ),
141
+ Effect.flatMap(() =>
142
+ Console.log(
143
+ chalk.dim(
144
+ 'You can find your HTTP password in Gerrit Settings > HTTP Credentials',
145
+ ),
146
+ ),
147
+ ),
148
+ )
149
+ }
150
+ }),
151
+ Effect.flatMap(() =>
152
+ Effect.tryPromise({
153
+ try: async () => {
154
+ console.log('')
155
+
156
+ // Enable raw mode for proper password masking
157
+ const wasRawMode = process.stdin.isRaw
158
+ if (process.stdin.isTTY && !wasRawMode) {
159
+ process.stdin.setRawMode(true)
160
+ }
161
+
162
+ try {
163
+ // Gerrit Host URL
164
+ const host = await input({
165
+ message: 'Gerrit Host URL (e.g., https://gerrit.example.com)',
166
+ default: existingConfig?.host,
167
+ })
168
+
169
+ // Username
170
+ const username = await input({
171
+ message: 'Username (your Gerrit username)',
172
+ default: existingConfig?.username,
173
+ })
174
+
175
+ // Password - with proper masking and visual feedback
176
+ const passwordMessage = existingConfig?.password
177
+ ? `HTTP Password (generated from Gerrit settings) ${chalk.dim('(press Enter to keep existing)')}`
178
+ : 'HTTP Password (generated from Gerrit settings)'
179
+
180
+ const passwordValue =
181
+ (await password({
182
+ message: passwordMessage,
183
+ mask: true, // Show * characters as user types
184
+ })) ||
185
+ existingConfig?.password ||
186
+ ''
187
+
188
+ console.log('')
189
+ console.log(chalk.yellow('Optional: AI Configuration'))
190
+
191
+ // Show detected AI tools
192
+ if (availableTools.length > 0) {
193
+ console.log(chalk.dim(`Detected AI tools: ${availableTools.join(', ')}`))
194
+ }
195
+
196
+ // Get default suggestion
197
+ const defaultCommand =
198
+ existingConfig?.aiTool ||
199
+ (availableTools.includes('claude') ? 'claude' : availableTools[0]) ||
200
+ ''
201
+
202
+ // AI tool command with smart default
203
+ const aiToolCommand = await input({
204
+ message:
205
+ availableTools.length > 0
206
+ ? 'AI tool command (detected from system)'
207
+ : 'AI tool command (e.g., claude, llm, opencode, gemini)',
208
+ default: defaultCommand || 'claude',
209
+ })
210
+
211
+ // Build flat config
212
+ const configData = {
213
+ host: normalizeGerritHost(host),
214
+ username: username.trim(),
215
+ password: passwordValue,
216
+ ...(aiToolCommand && {
217
+ aiTool: aiToolCommand,
218
+ }),
219
+ aiAutoDetect: !aiToolCommand,
220
+ }
221
+
222
+ // Validate config using Schema instead of type assertion
223
+ const fullConfig = Schema.decodeUnknownSync(AppConfig)(configData)
224
+
225
+ return fullConfig
226
+ } finally {
227
+ // Restore raw mode state
228
+ if (process.stdin.isTTY && !wasRawMode) {
229
+ process.stdin.setRawMode(false)
230
+ }
231
+ }
232
+ },
233
+ catch: (error) => {
234
+ if (error instanceof Error && error.message.includes('User force closed')) {
235
+ console.log(`\n${chalk.yellow('Setup cancelled')}`)
236
+ process.exit(0)
237
+ }
238
+ return new ConfigError({
239
+ message: error instanceof Error ? error.message : 'Failed to get user input',
240
+ })
241
+ },
242
+ }),
243
+ ),
244
+ ),
245
+ ),
246
+ Effect.tap(() => Console.log('\nVerifying credentials...')),
247
+ Effect.flatMap((config) =>
248
+ pipe(
249
+ verifyCredentials({
250
+ host: config.host,
251
+ username: config.username,
252
+ password: config.password,
253
+ }),
254
+ Effect.map(() => config),
255
+ ),
256
+ ),
257
+ Effect.tap(() => Console.log(chalk.green('Successfully authenticated'))),
258
+ Effect.flatMap((config) => configService.saveFullConfig(config)),
259
+ Effect.tap(() => Console.log(chalk.green('\nConfiguration saved successfully!'))),
260
+ Effect.tap(() => Console.log('You can now use:')),
261
+ Effect.tap(() => Console.log(' • "ger mine" to view your changes')),
262
+ Effect.tap(() => Console.log(' • "ger show <change-id>" to view change details')),
263
+ Effect.tap(() => Console.log(' • "ger review <change-id>" to review with AI')),
264
+ Effect.catchAll((error) =>
265
+ pipe(
266
+ Console.error(
267
+ chalk.red(`\n${error instanceof ConfigError ? error.message : `Setup failed: ${error}`}`),
268
+ ),
269
+ Effect.flatMap(() => Effect.fail(error)),
270
+ ),
271
+ ),
272
+ )
273
+
274
+ export async function setup(): Promise<void> {
275
+ const program = pipe(
276
+ ConfigService,
277
+ Effect.flatMap((configService) => setupEffect(configService)),
278
+ ).pipe(Effect.provide(ConfigServiceLive))
279
+
280
+ try {
281
+ await Effect.runPromise(program)
282
+ } catch {
283
+ // Error already handled and displayed
284
+ process.exit(1)
285
+ }
286
+ }