@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,119 @@
1
+ import { Effect } from 'effect'
2
+ import { type ApiError, GerritApiService } from '@/api/gerrit'
3
+ import type { ReviewInput } from '@/schemas/gerrit'
4
+
5
+ interface VoteOptions {
6
+ codeReview?: number
7
+ verified?: number
8
+ label?: string[]
9
+ message?: string
10
+ xml?: boolean
11
+ }
12
+
13
+ /**
14
+ * Casts votes on a Gerrit change with optional comment message.
15
+ *
16
+ * Supports standard labels (Code-Review, Verified) and custom labels.
17
+ * At least one label must be provided.
18
+ *
19
+ * @param changeId - Change number or Change-ID to vote on
20
+ * @param options - Configuration options
21
+ * @param options.codeReview - Code-Review vote value (-2 to +2)
22
+ * @param options.verified - Verified vote value (-1 to +1)
23
+ * @param options.label - Custom label name-value pairs
24
+ * @param options.message - Optional comment message with the vote
25
+ * @param options.xml - Whether to output in XML format for LLM consumption
26
+ * @returns Effect that completes when votes are cast
27
+ */
28
+ export const voteCommand = (
29
+ changeId?: string,
30
+ options: VoteOptions = {},
31
+ ): Effect.Effect<void, ApiError, GerritApiService> =>
32
+ Effect.gen(function* () {
33
+ const gerritApi = yield* GerritApiService
34
+
35
+ if (!changeId || changeId.trim() === '') {
36
+ console.error('✗ Change ID is required')
37
+ console.error(' Usage: ger vote <change-id> --code-review <value> [--verified <value>]')
38
+ return
39
+ }
40
+
41
+ // Build labels object from options
42
+ const labels: Record<string, number> = {}
43
+
44
+ if (options.codeReview !== undefined) {
45
+ labels['Code-Review'] = options.codeReview
46
+ }
47
+
48
+ if (options.verified !== undefined) {
49
+ labels['Verified'] = options.verified
50
+ }
51
+
52
+ // Parse custom labels (format: --label <name> <value>)
53
+ if (options.label && options.label.length > 0) {
54
+ // Labels come in pairs: [name1, value1, name2, value2, ...]
55
+ if (options.label.length % 2 !== 0) {
56
+ console.error('✗ Invalid label format: labels must be provided as name-value pairs')
57
+ console.error(' Usage: --label <name> <value> [--label <name> <value> ...]')
58
+ return
59
+ }
60
+
61
+ for (let i = 0; i < options.label.length; i += 2) {
62
+ const labelName = options.label[i]
63
+ const labelValue = options.label[i + 1]
64
+ if (labelName && labelValue) {
65
+ const numValue = Number.parseInt(labelValue, 10)
66
+ if (Number.isNaN(numValue)) {
67
+ console.error(`✗ Invalid label value for ${labelName}: ${labelValue}`)
68
+ console.error(' Label values must be integers')
69
+ return
70
+ }
71
+ labels[labelName] = numValue
72
+ }
73
+ }
74
+ }
75
+
76
+ // Check if at least one label is provided
77
+ if (Object.keys(labels).length === 0) {
78
+ console.error('✗ At least one label is required')
79
+ console.error(
80
+ ' Usage: ger vote <change-id> --code-review <value> [--verified <value>] [--label <name> <value>]',
81
+ )
82
+ return
83
+ }
84
+
85
+ // Build ReviewInput
86
+ const reviewInput: ReviewInput = {
87
+ labels,
88
+ ...(options.message && { message: options.message }),
89
+ }
90
+
91
+ // Post the review
92
+ yield* gerritApi.postReview(changeId, reviewInput)
93
+
94
+ // Output success
95
+ if (options.xml) {
96
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
97
+ console.log(`<vote_result>`)
98
+ console.log(` <status>success</status>`)
99
+ console.log(` <change_id>${changeId}</change_id>`)
100
+ console.log(` <labels>`)
101
+ for (const [name, value] of Object.entries(labels)) {
102
+ console.log(` <label name="${name}">${value}</label>`)
103
+ }
104
+ console.log(` </labels>`)
105
+ if (options.message) {
106
+ console.log(` <message><![CDATA[${options.message}]]></message>`)
107
+ }
108
+ console.log(`</vote_result>`)
109
+ } else {
110
+ console.log(`✓ Voted on change ${changeId}`)
111
+ for (const [name, value] of Object.entries(labels)) {
112
+ const sign = value >= 0 ? '+' : ''
113
+ console.log(` ${name}: ${sign}${value}`)
114
+ }
115
+ if (options.message) {
116
+ console.log(` Message: ${options.message}`)
117
+ }
118
+ }
119
+ })
@@ -0,0 +1,200 @@
1
+ import { execSync, spawnSync } from 'node:child_process'
2
+ import * as fs from 'node:fs'
3
+ import * as path from 'node:path'
4
+ import { Effect } from 'effect'
5
+ import { type ApiError, GerritApiService } from '@/api/gerrit'
6
+ import { type ConfigError, ConfigService } from '@/services/config'
7
+
8
+ interface WorkspaceOptions {
9
+ xml?: boolean
10
+ }
11
+
12
+ const parseChangeSpec = (changeSpec: string): { changeId: string; patchset?: string } => {
13
+ const parts = changeSpec.split(':')
14
+ return {
15
+ changeId: parts[0],
16
+ patchset: parts[1],
17
+ }
18
+ }
19
+
20
+ const getGitRemotes = (): Record<string, string> => {
21
+ try {
22
+ const output = execSync('git remote -v', { encoding: 'utf8' })
23
+ const remotes: Record<string, string> = {}
24
+
25
+ for (const line of output.split('\n')) {
26
+ const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/)
27
+ if (match) {
28
+ remotes[match[1]] = match[2]
29
+ }
30
+ }
31
+
32
+ return remotes
33
+ } catch {
34
+ return {}
35
+ }
36
+ }
37
+
38
+ const findMatchingRemote = (gerritHost: string): string | null => {
39
+ const remotes = getGitRemotes()
40
+
41
+ // Parse gerrit host
42
+ const gerritUrl = new URL(gerritHost)
43
+ const gerritHostname = gerritUrl.hostname
44
+
45
+ // Check each remote
46
+ for (const [name, url] of Object.entries(remotes)) {
47
+ try {
48
+ // Handle both HTTP and SSH URLs
49
+ let remoteHostname: string
50
+
51
+ if (url.startsWith('git@') || url.includes('://')) {
52
+ if (url.startsWith('git@')) {
53
+ // SSH format: git@hostname:project
54
+ remoteHostname = url.split('@')[1].split(':')[0]
55
+ } else {
56
+ // HTTP format
57
+ const remoteUrl = new URL(url)
58
+ remoteHostname = remoteUrl.hostname
59
+ }
60
+
61
+ if (remoteHostname === gerritHostname) {
62
+ return name
63
+ }
64
+ }
65
+ } catch {
66
+ // Ignore malformed URLs
67
+ }
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ const isInGitRepo = (): boolean => {
74
+ try {
75
+ execSync('git rev-parse --git-dir', { encoding: 'utf8' })
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
80
+ }
81
+
82
+ const getRepoRoot = (): string => {
83
+ try {
84
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim()
85
+ } catch {
86
+ throw new Error('Not in a git repository')
87
+ }
88
+ }
89
+
90
+ export const workspaceCommand = (
91
+ changeSpec: string,
92
+ options: WorkspaceOptions,
93
+ ): Effect.Effect<void, ApiError | ConfigError | Error, GerritApiService | ConfigService> =>
94
+ Effect.gen(function* () {
95
+ // Check if we're in a git repo
96
+ if (!isInGitRepo()) {
97
+ throw new Error(
98
+ 'Not in a git repository. Please run this command from within a git repository.',
99
+ )
100
+ }
101
+
102
+ const repoRoot = getRepoRoot()
103
+ const { changeId, patchset } = parseChangeSpec(changeSpec)
104
+
105
+ // Get Gerrit credentials and find matching remote
106
+ const configService = yield* ConfigService
107
+ const credentials = yield* configService.getCredentials
108
+ const matchingRemote = findMatchingRemote(credentials.host)
109
+
110
+ if (!matchingRemote) {
111
+ throw new Error(`No git remote found matching Gerrit host: ${credentials.host}`)
112
+ }
113
+
114
+ // Get change details from Gerrit
115
+ const gerritApi = yield* GerritApiService
116
+ const change = yield* gerritApi.getChange(changeId)
117
+
118
+ // Determine patchset to use
119
+ const targetPatchset = patchset || 'current'
120
+ const revision = yield* gerritApi.getRevision(changeId, targetPatchset)
121
+
122
+ // Create workspace directory name - validate to prevent path traversal
123
+ const workspaceName = change._number.toString()
124
+ // Validate workspace name contains only digits
125
+ if (!/^\d+$/.test(workspaceName)) {
126
+ throw new Error(`Invalid change number: ${workspaceName}`)
127
+ }
128
+ const workspaceDir = path.join(repoRoot, '.ger', workspaceName)
129
+
130
+ // Check if worktree already exists
131
+ if (fs.existsSync(workspaceDir)) {
132
+ if (options.xml) {
133
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
134
+ console.log(`<workspace>`)
135
+ console.log(` <path>${workspaceDir}</path>`)
136
+ console.log(` <exists>true</exists>`)
137
+ console.log(`</workspace>`)
138
+ } else {
139
+ console.log(`✓ Workspace already exists at: ${workspaceDir}`)
140
+ console.log(` Run: cd ${workspaceDir}`)
141
+ }
142
+ return
143
+ }
144
+
145
+ // Ensure .ger directory exists
146
+ const gerDir = path.join(repoRoot, '.ger')
147
+ if (!fs.existsSync(gerDir)) {
148
+ fs.mkdirSync(gerDir, { recursive: true })
149
+ }
150
+
151
+ // Fetch the change ref
152
+ const changeRef = revision.ref
153
+ if (!options.xml) {
154
+ console.log(`Fetching change ${change._number}: ${change.subject}`)
155
+ }
156
+
157
+ try {
158
+ // Use spawnSync with array to prevent command injection
159
+ const fetchResult = spawnSync('git', ['fetch', matchingRemote, changeRef], {
160
+ encoding: 'utf8',
161
+ cwd: repoRoot,
162
+ })
163
+ if (fetchResult.status !== 0) {
164
+ throw new Error(fetchResult.stderr || 'Git fetch failed')
165
+ }
166
+ } catch (error) {
167
+ throw new Error(`Failed to fetch change: ${error}`)
168
+ }
169
+
170
+ // Create worktree
171
+ if (!options.xml) {
172
+ console.log(`Creating worktree at: ${workspaceDir}`)
173
+ }
174
+
175
+ try {
176
+ // Use spawnSync with array to prevent command injection
177
+ const worktreeResult = spawnSync('git', ['worktree', 'add', workspaceDir, 'FETCH_HEAD'], {
178
+ encoding: 'utf8',
179
+ cwd: repoRoot,
180
+ })
181
+ if (worktreeResult.status !== 0) {
182
+ throw new Error(worktreeResult.stderr || 'Git worktree add failed')
183
+ }
184
+ } catch (error) {
185
+ throw new Error(`Failed to create worktree: ${error}`)
186
+ }
187
+
188
+ if (options.xml) {
189
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
190
+ console.log(`<workspace>`)
191
+ console.log(` <path>${workspaceDir}</path>`)
192
+ console.log(` <change_number>${change._number}</change_number>`)
193
+ console.log(` <subject><![CDATA[${change.subject}]]></subject>`)
194
+ console.log(` <created>true</created>`)
195
+ console.log(`</workspace>`)
196
+ } else {
197
+ console.log(`✓ Workspace created successfully!`)
198
+ console.log(` Run: cd ${workspaceDir}`)
199
+ }
200
+ })
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Check Bun version requirement
4
+ const MIN_BUN_VERSION = '1.2.0'
5
+ const bunVersion = Bun.version
6
+
7
+ function compareSemver(a: string, b: string): number {
8
+ const parseVersion = (v: string) => v.split('.').map((n) => parseInt(n, 10))
9
+ const [aMajor, aMinor = 0, aPatch = 0] = parseVersion(a)
10
+ const [bMajor, bMinor = 0, bPatch = 0] = parseVersion(b)
11
+
12
+ if (aMajor !== bMajor) return aMajor - bMajor
13
+ if (aMinor !== bMinor) return aMinor - bMinor
14
+ return aPatch - bPatch
15
+ }
16
+
17
+ if (compareSemver(bunVersion, MIN_BUN_VERSION) < 0) {
18
+ console.error(`✗ Error: Bun version ${MIN_BUN_VERSION} or higher is required`)
19
+ console.error(` Current version: ${bunVersion}`)
20
+ console.error(` Please upgrade Bun: bun upgrade`)
21
+ process.exit(1)
22
+ }
23
+
24
+ import { Command } from 'commander'
25
+ import { readFileSync } from 'node:fs'
26
+ import { join, dirname } from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
28
+ import { registerCommands } from './register-commands'
29
+
30
+ // Read version from package.json
31
+ function getVersion(): string {
32
+ try {
33
+ // Get the directory of the current module
34
+ const __filename = fileURLToPath(import.meta.url)
35
+ const __dirname = dirname(__filename)
36
+
37
+ // Navigate up to the project root and read package.json
38
+ const packageJsonPath = join(__dirname, '..', '..', 'package.json')
39
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
40
+ return packageJson.version || '0.0.0'
41
+ } catch {
42
+ // Fallback version if package.json can't be read
43
+ return '0.0.0'
44
+ }
45
+ }
46
+
47
+ const program = new Command()
48
+
49
+ program.name('ger').description('LLM-centric Gerrit CLI tool').version(getVersion())
50
+
51
+ registerCommands(program)
52
+
53
+ program.parse(process.argv)