@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.
- package/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +708 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -196
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- 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
|
+
})
|
package/src/cli/index.ts
ADDED
|
@@ -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)
|