@aaronshaf/ger 0.1.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/.eslintrc.js +12 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude.yml +64 -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 +103 -0
- package/DEVELOPMENT.md +361 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/bin/ger +3 -0
- package/biome.json +36 -0
- package/bun.lock +688 -0
- package/bunfig.toml +8 -0
- package/oxlint.json +24 -0
- package/package.json +55 -0
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/src/api/gerrit.ts +466 -0
- package/src/cli/commands/abandon.ts +65 -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/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/review.ts +593 -0
- package/src/cli/commands/setup.ts +230 -0
- package/src/cli/commands/show.ts +303 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +420 -0
- package/src/prompts/default-review.md +80 -0
- package/src/prompts/system-inline-review.md +88 -0
- package/src/prompts/system-overall-review.md +152 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +75 -0
- package/src/schemas/gerrit.ts +455 -0
- package/src/services/ai-enhanced.ts +167 -0
- package/src/services/ai.ts +182 -0
- package/src/services/config.test.ts +414 -0
- package/src/services/config.ts +206 -0
- package/src/test-utils/mock-generator.ts +73 -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/message-filters.ts +26 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +123 -0
- package/src/utils/url-parser.ts +91 -0
- package/tests/abandon.test.ts +163 -0
- package/tests/ai-service.test.ts +489 -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 +707 -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/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +318 -0
- package/tests/mocks/fetch-mock.ts +139 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/review.test.ts +669 -0
- package/tests/setup.ts +13 -0
- package/tests/show.test.ts +439 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -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/prompt-helpers.test.ts +175 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tsconfig.json +40 -0
|
@@ -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,420 @@
|
|
|
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 { Effect } from 'effect'
|
|
26
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
27
|
+
import { ConfigServiceLive } from '@/services/config'
|
|
28
|
+
import { AiServiceLive } from '@/services/ai-enhanced'
|
|
29
|
+
import { abandonCommand } from './commands/abandon'
|
|
30
|
+
import { commentCommand } from './commands/comment'
|
|
31
|
+
import { commentsCommand } from './commands/comments'
|
|
32
|
+
import { diffCommand } from './commands/diff'
|
|
33
|
+
import { incomingCommand } from './commands/incoming'
|
|
34
|
+
import { mineCommand } from './commands/mine'
|
|
35
|
+
import { openCommand } from './commands/open'
|
|
36
|
+
import { reviewCommand } from './commands/review'
|
|
37
|
+
import { setup } from './commands/setup'
|
|
38
|
+
import { showCommand } from './commands/show'
|
|
39
|
+
import { statusCommand } from './commands/status'
|
|
40
|
+
import { workspaceCommand } from './commands/workspace'
|
|
41
|
+
|
|
42
|
+
const program = new Command()
|
|
43
|
+
|
|
44
|
+
program.name('gi').description('LLM-centric Gerrit CLI tool').version('0.1.0')
|
|
45
|
+
|
|
46
|
+
// setup command (new primary command)
|
|
47
|
+
program
|
|
48
|
+
.command('setup')
|
|
49
|
+
.description('Configure Gerrit credentials and AI tools')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
await setup()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// init command (kept for backward compatibility, redirects to setup)
|
|
55
|
+
program
|
|
56
|
+
.command('init')
|
|
57
|
+
.description('Initialize Gerrit credentials (alias for setup)')
|
|
58
|
+
.action(async () => {
|
|
59
|
+
await setup()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// status command
|
|
63
|
+
program
|
|
64
|
+
.command('status')
|
|
65
|
+
.description('Check connection status')
|
|
66
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
try {
|
|
69
|
+
const effect = statusCommand(options).pipe(
|
|
70
|
+
Effect.provide(GerritApiServiceLive),
|
|
71
|
+
Effect.provide(ConfigServiceLive),
|
|
72
|
+
)
|
|
73
|
+
await Effect.runPromise(effect)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Error:', error instanceof Error ? error.message : String(error))
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// comment command
|
|
81
|
+
program
|
|
82
|
+
.command('comment <change-id>')
|
|
83
|
+
.description('Post a comment on a change')
|
|
84
|
+
.option('-m, --message <message>', 'Comment message')
|
|
85
|
+
.option('--file <file>', 'File path for line-specific comment (relative to repo root)')
|
|
86
|
+
.option(
|
|
87
|
+
'--line <line>',
|
|
88
|
+
'Line number in the NEW version of the file (not diff line numbers)',
|
|
89
|
+
parseInt,
|
|
90
|
+
)
|
|
91
|
+
.option('--unresolved', 'Mark comment as unresolved (requires human attention)')
|
|
92
|
+
.option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
|
|
93
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
94
|
+
.addHelpText(
|
|
95
|
+
'after',
|
|
96
|
+
`
|
|
97
|
+
Examples:
|
|
98
|
+
# Post a general comment on a change
|
|
99
|
+
$ ger comment 12345 -m "Looks good to me!"
|
|
100
|
+
|
|
101
|
+
# Post a comment using piped input (useful for multi-line comments or scripts)
|
|
102
|
+
$ echo "This is a comment from stdin!" | ger comment 12345
|
|
103
|
+
$ cat review-notes.txt | ger comment 12345
|
|
104
|
+
|
|
105
|
+
# Post a line-specific comment (line number from NEW file version)
|
|
106
|
+
$ ger comment 12345 --file src/main.js --line 42 -m "Consider using const here"
|
|
107
|
+
|
|
108
|
+
# Post an unresolved comment requiring human attention
|
|
109
|
+
$ ger comment 12345 --file src/api.js --line 15 -m "Security concern" --unresolved
|
|
110
|
+
|
|
111
|
+
# Post multiple comments using batch mode
|
|
112
|
+
$ echo '{"message": "Review complete", "comments": [
|
|
113
|
+
{"file": "src/main.js", "line": 10, "message": "Good refactor"},
|
|
114
|
+
{"file": "src/api.js", "line": 25, "message": "Check error handling", "unresolved": true}
|
|
115
|
+
]}' | ger comment 12345 --batch
|
|
116
|
+
|
|
117
|
+
Note: Line numbers refer to the actual line numbers in the NEW version of the file,
|
|
118
|
+
NOT the line numbers shown in the diff view. To find the correct line number,
|
|
119
|
+
look at the file after all changes have been applied.`,
|
|
120
|
+
)
|
|
121
|
+
.action(async (changeId, options) => {
|
|
122
|
+
try {
|
|
123
|
+
const effect = commentCommand(changeId, options).pipe(
|
|
124
|
+
Effect.provide(GerritApiServiceLive),
|
|
125
|
+
Effect.provide(ConfigServiceLive),
|
|
126
|
+
)
|
|
127
|
+
await Effect.runPromise(effect)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (options.xml) {
|
|
130
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
131
|
+
console.log(`<comment_result>`)
|
|
132
|
+
console.log(` <status>error</status>`)
|
|
133
|
+
console.log(
|
|
134
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
135
|
+
)
|
|
136
|
+
console.log(`</comment_result>`)
|
|
137
|
+
} else {
|
|
138
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
139
|
+
}
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// diff command
|
|
145
|
+
program
|
|
146
|
+
.command('diff <change-id>')
|
|
147
|
+
.description('Get diff for a change')
|
|
148
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
149
|
+
.option('--file <file>', 'Specific file to diff')
|
|
150
|
+
.option('--files-only', 'List changed files only')
|
|
151
|
+
.option('--format <format>', 'Output format (unified, json, files)')
|
|
152
|
+
.action(async (changeId, options) => {
|
|
153
|
+
try {
|
|
154
|
+
const effect = diffCommand(changeId, options).pipe(
|
|
155
|
+
Effect.provide(GerritApiServiceLive),
|
|
156
|
+
Effect.provide(ConfigServiceLive),
|
|
157
|
+
)
|
|
158
|
+
await Effect.runPromise(effect)
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (options.xml) {
|
|
161
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
162
|
+
console.log(`<diff_result>`)
|
|
163
|
+
console.log(` <status>error</status>`)
|
|
164
|
+
console.log(
|
|
165
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
166
|
+
)
|
|
167
|
+
console.log(`</diff_result>`)
|
|
168
|
+
} else {
|
|
169
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
170
|
+
}
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// mine command
|
|
176
|
+
program
|
|
177
|
+
.command('mine')
|
|
178
|
+
.description('Show your open changes')
|
|
179
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
try {
|
|
182
|
+
const effect = mineCommand(options).pipe(
|
|
183
|
+
Effect.provide(GerritApiServiceLive),
|
|
184
|
+
Effect.provide(ConfigServiceLive),
|
|
185
|
+
)
|
|
186
|
+
await Effect.runPromise(effect)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (options.xml) {
|
|
189
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
190
|
+
console.log(`<mine_result>`)
|
|
191
|
+
console.log(` <status>error</status>`)
|
|
192
|
+
console.log(
|
|
193
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
194
|
+
)
|
|
195
|
+
console.log(`</mine_result>`)
|
|
196
|
+
} else {
|
|
197
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
198
|
+
}
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// workspace command
|
|
204
|
+
program
|
|
205
|
+
.command('workspace <change-id>')
|
|
206
|
+
.description('Create or switch to a git worktree for a Gerrit change')
|
|
207
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
208
|
+
.action(async (changeId, options) => {
|
|
209
|
+
try {
|
|
210
|
+
const effect = workspaceCommand(changeId, options).pipe(
|
|
211
|
+
Effect.provide(GerritApiServiceLive),
|
|
212
|
+
Effect.provide(ConfigServiceLive),
|
|
213
|
+
)
|
|
214
|
+
await Effect.runPromise(effect)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (options.xml) {
|
|
217
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
218
|
+
console.log(`<workspace_result>`)
|
|
219
|
+
console.log(` <status>error</status>`)
|
|
220
|
+
console.log(
|
|
221
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
222
|
+
)
|
|
223
|
+
console.log(`</workspace_result>`)
|
|
224
|
+
} else {
|
|
225
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
226
|
+
}
|
|
227
|
+
process.exit(1)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// incoming command
|
|
232
|
+
program
|
|
233
|
+
.command('incoming')
|
|
234
|
+
.description('Show incoming changes for review (where you are a reviewer)')
|
|
235
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
236
|
+
.option('-i, --interactive', 'Interactive mode with detailed view and diff')
|
|
237
|
+
.action(async (options) => {
|
|
238
|
+
try {
|
|
239
|
+
const effect = incomingCommand(options).pipe(
|
|
240
|
+
Effect.provide(GerritApiServiceLive),
|
|
241
|
+
Effect.provide(ConfigServiceLive),
|
|
242
|
+
)
|
|
243
|
+
await Effect.runPromise(effect)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (options.xml) {
|
|
246
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
247
|
+
console.log(`<incoming_result>`)
|
|
248
|
+
console.log(` <status>error</status>`)
|
|
249
|
+
console.log(
|
|
250
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
251
|
+
)
|
|
252
|
+
console.log(`</incoming_result>`)
|
|
253
|
+
} else {
|
|
254
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
255
|
+
}
|
|
256
|
+
process.exit(1)
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// abandon command
|
|
261
|
+
program
|
|
262
|
+
.command('abandon [change-id]')
|
|
263
|
+
.description('Abandon a change (interactive mode if no change-id provided)')
|
|
264
|
+
.option('-m, --message <message>', 'Abandon message')
|
|
265
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
266
|
+
.action(async (changeId, options) => {
|
|
267
|
+
try {
|
|
268
|
+
const effect = abandonCommand(changeId, options).pipe(
|
|
269
|
+
Effect.provide(GerritApiServiceLive),
|
|
270
|
+
Effect.provide(ConfigServiceLive),
|
|
271
|
+
)
|
|
272
|
+
await Effect.runPromise(effect)
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (options.xml) {
|
|
275
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
276
|
+
console.log(`<abandon_result>`)
|
|
277
|
+
console.log(` <status>error</status>`)
|
|
278
|
+
console.log(
|
|
279
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
280
|
+
)
|
|
281
|
+
console.log(`</abandon_result>`)
|
|
282
|
+
} else {
|
|
283
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
284
|
+
}
|
|
285
|
+
process.exit(1)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// comments command
|
|
290
|
+
program
|
|
291
|
+
.command('comments <change-id>')
|
|
292
|
+
.description('Show all comments on a change with diff context')
|
|
293
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
294
|
+
.action(async (changeId, options) => {
|
|
295
|
+
try {
|
|
296
|
+
const effect = commentsCommand(changeId, options).pipe(
|
|
297
|
+
Effect.provide(GerritApiServiceLive),
|
|
298
|
+
Effect.provide(ConfigServiceLive),
|
|
299
|
+
)
|
|
300
|
+
await Effect.runPromise(effect)
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (options.xml) {
|
|
303
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
304
|
+
console.log(`<comments_result>`)
|
|
305
|
+
console.log(` <status>error</status>`)
|
|
306
|
+
console.log(
|
|
307
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
308
|
+
)
|
|
309
|
+
console.log(`</comments_result>`)
|
|
310
|
+
} else {
|
|
311
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
312
|
+
}
|
|
313
|
+
process.exit(1)
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// open command
|
|
318
|
+
program
|
|
319
|
+
.command('open <change-id>')
|
|
320
|
+
.description('Open a change in the browser')
|
|
321
|
+
.action(async (changeId, options) => {
|
|
322
|
+
try {
|
|
323
|
+
const effect = openCommand(changeId, options).pipe(
|
|
324
|
+
Effect.provide(GerritApiServiceLive),
|
|
325
|
+
Effect.provide(ConfigServiceLive),
|
|
326
|
+
)
|
|
327
|
+
await Effect.runPromise(effect)
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// show command
|
|
335
|
+
program
|
|
336
|
+
.command('show <change-id>')
|
|
337
|
+
.description('Show comprehensive change information including metadata, diff, and all comments')
|
|
338
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
339
|
+
.action(async (changeId, options) => {
|
|
340
|
+
try {
|
|
341
|
+
const effect = showCommand(changeId, options).pipe(
|
|
342
|
+
Effect.provide(GerritApiServiceLive),
|
|
343
|
+
Effect.provide(ConfigServiceLive),
|
|
344
|
+
)
|
|
345
|
+
await Effect.runPromise(effect)
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (options.xml) {
|
|
348
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
349
|
+
console.log(`<show_result>`)
|
|
350
|
+
console.log(` <status>error</status>`)
|
|
351
|
+
console.log(
|
|
352
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
353
|
+
)
|
|
354
|
+
console.log(`</show_result>`)
|
|
355
|
+
} else {
|
|
356
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
357
|
+
}
|
|
358
|
+
process.exit(1)
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// review command
|
|
363
|
+
program
|
|
364
|
+
.command('review <change-id>')
|
|
365
|
+
.description('AI-powered code review that analyzes changes and optionally posts comments')
|
|
366
|
+
.option('--comment', 'Post the review as comments (prompts for confirmation)')
|
|
367
|
+
.option('-y, --yes', 'Skip confirmation prompts when posting comments')
|
|
368
|
+
.option('--debug', 'Show debug output including AI responses')
|
|
369
|
+
.option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
|
|
370
|
+
.addHelpText(
|
|
371
|
+
'after',
|
|
372
|
+
`
|
|
373
|
+
This command uses AI (claude, llm, or opencode CLI) to review a Gerrit change.
|
|
374
|
+
It performs a two-stage review process:
|
|
375
|
+
|
|
376
|
+
1. Generates inline comments for specific code issues
|
|
377
|
+
2. Generates an overall review comment
|
|
378
|
+
|
|
379
|
+
By default, the review is only displayed in the terminal.
|
|
380
|
+
Use --comment to post the review to Gerrit (with confirmation prompts).
|
|
381
|
+
Use --comment --yes to post without confirmation.
|
|
382
|
+
|
|
383
|
+
Requirements:
|
|
384
|
+
- One of these AI tools must be installed: claude, llm, or opencode
|
|
385
|
+
- Gerrit credentials must be configured (run 'ger setup' first)
|
|
386
|
+
|
|
387
|
+
Examples:
|
|
388
|
+
# Review a change (display only)
|
|
389
|
+
$ ger review 12345
|
|
390
|
+
|
|
391
|
+
# Review and prompt to post comments
|
|
392
|
+
$ ger review 12345 --comment
|
|
393
|
+
|
|
394
|
+
# Review and auto-post comments without prompting
|
|
395
|
+
$ ger review 12345 --comment --yes
|
|
396
|
+
|
|
397
|
+
# Show debug output to troubleshoot issues
|
|
398
|
+
$ ger review 12345 --debug
|
|
399
|
+
`,
|
|
400
|
+
)
|
|
401
|
+
.action(async (changeId, options) => {
|
|
402
|
+
try {
|
|
403
|
+
const effect = reviewCommand(changeId, {
|
|
404
|
+
comment: options.comment,
|
|
405
|
+
yes: options.yes,
|
|
406
|
+
debug: options.debug,
|
|
407
|
+
prompt: options.prompt,
|
|
408
|
+
}).pipe(
|
|
409
|
+
Effect.provide(AiServiceLive),
|
|
410
|
+
Effect.provide(GerritApiServiceLive),
|
|
411
|
+
Effect.provide(ConfigServiceLive),
|
|
412
|
+
)
|
|
413
|
+
await Effect.runPromise(effect)
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
416
|
+
process.exit(1)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
program.parse(process.argv)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Code Review Guidelines
|
|
2
|
+
|
|
3
|
+
You are reviewing a Gerrit change set. Provide thorough, constructive feedback focused on technical excellence and maintainability.
|
|
4
|
+
|
|
5
|
+
## Review Philosophy
|
|
6
|
+
|
|
7
|
+
1. **Understand First, Critique Second**
|
|
8
|
+
- Fully comprehend the author's intent before identifying issues
|
|
9
|
+
- Read COMPLETE files, not just diffs
|
|
10
|
+
- Check if apparent issues are handled elsewhere in the change
|
|
11
|
+
- Consider the broader architectural context
|
|
12
|
+
- Verify you're reviewing the LATEST patchset version
|
|
13
|
+
|
|
14
|
+
2. **Be Direct and Constructive**
|
|
15
|
+
- Focus on substantive technical concerns
|
|
16
|
+
- Explain WHY something is problematic, not just what
|
|
17
|
+
- Provide actionable suggestions when identifying issues
|
|
18
|
+
- Assume the author has domain expertise
|
|
19
|
+
- Ask clarifying questions when intent is unclear
|
|
20
|
+
|
|
21
|
+
## Review Categories (Priority Order)
|
|
22
|
+
|
|
23
|
+
### 1. CRITICAL ISSUES (Must Fix)
|
|
24
|
+
- **Correctness**: Logic errors, race conditions, data corruption risks
|
|
25
|
+
- **Security**: Authentication bypasses, injection vulnerabilities, data exposure
|
|
26
|
+
- **Data Loss**: Operations that could destroy or corrupt user data
|
|
27
|
+
- **Breaking Changes**: Incompatible API/schema changes without migration
|
|
28
|
+
- **Production Impact**: Issues that would cause outages or severe degradation
|
|
29
|
+
|
|
30
|
+
### 2. SIGNIFICANT CONCERNS (Should Fix)
|
|
31
|
+
- **Performance**: Memory leaks, N+1 queries, inefficient algorithms
|
|
32
|
+
- **Error Handling**: Missing error cases, silent failures, poor recovery
|
|
33
|
+
- **Resource Management**: Unclosed connections, file handles, cleanup issues
|
|
34
|
+
- **Type Safety**: Unsafe casts, missing validation, schema mismatches
|
|
35
|
+
- **Concurrency**: Deadlock risks, thread safety issues, synchronization problems
|
|
36
|
+
|
|
37
|
+
### 3. CODE QUALITY (Consider Fixing)
|
|
38
|
+
- **Architecture**: Design pattern violations, coupling issues, abstraction leaks
|
|
39
|
+
- **Maintainability**: Complex logic without justification, unclear naming
|
|
40
|
+
- **Testing**: Missing test coverage for critical paths, brittle test design
|
|
41
|
+
- **Documentation**: Misleading comments, missing API documentation
|
|
42
|
+
- **Best Practices**: Framework misuse, anti-patterns, deprecated APIs
|
|
43
|
+
|
|
44
|
+
### 4. MINOR IMPROVEMENTS (Optional)
|
|
45
|
+
- **Consistency**: Deviations from established patterns without reason
|
|
46
|
+
- **Efficiency**: Minor optimization opportunities
|
|
47
|
+
- **Clarity**: Code that works but could be more readable
|
|
48
|
+
- **Future-Proofing**: Anticipating likely future requirements
|
|
49
|
+
|
|
50
|
+
## What NOT to Review
|
|
51
|
+
|
|
52
|
+
- **Already Fixed**: Issues resolved in the current patchset
|
|
53
|
+
- **Style Preferences**: Formatting that doesn't impact readability
|
|
54
|
+
- **Micro-Optimizations**: Unless performance is a stated goal
|
|
55
|
+
- **Personal Preferences**: Unless they violate team standards
|
|
56
|
+
- **Out of Scope**: Issues in unchanged code (unless directly relevant)
|
|
57
|
+
|
|
58
|
+
## Context Requirements
|
|
59
|
+
|
|
60
|
+
Before commenting, verify:
|
|
61
|
+
1. The issue still exists in the current patchset
|
|
62
|
+
2. The fix wouldn't break other functionality
|
|
63
|
+
3. Your understanding of the code's purpose is correct
|
|
64
|
+
4. The issue isn't intentional or documented
|
|
65
|
+
5. The concern is worth the author's time to address
|
|
66
|
+
|
|
67
|
+
## Inline Comment Guidelines
|
|
68
|
+
|
|
69
|
+
- Start each comment with "🤖 " (robot emoji with space)
|
|
70
|
+
- Be specific about file paths and line numbers
|
|
71
|
+
- Group related issues when they share a root cause
|
|
72
|
+
- Provide concrete examples or corrections when helpful
|
|
73
|
+
- Use questions for clarification, statements for clear issues
|
|
74
|
+
|
|
75
|
+
## Remember
|
|
76
|
+
|
|
77
|
+
- The goal is to improve code quality while respecting the author's time
|
|
78
|
+
- Focus on issues that matter for correctness, security, and maintainability
|
|
79
|
+
- Your review should help ship better code, not perfect code
|
|
80
|
+
- When in doubt, phrase feedback as a question rather than a mandate
|