@in-the-loop-labs/pair-review 1.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/LICENSE +674 -0
- package/README.md +371 -0
- package/bin/git-diff-lines +146 -0
- package/bin/pair-review.js +49 -0
- package/package.json +71 -0
- package/public/css/ai-summary-modal.css +183 -0
- package/public/css/pr.css +8698 -0
- package/public/css/repo-settings.css +891 -0
- package/public/css/styles.css +479 -0
- package/public/favicon.png +0 -0
- package/public/index.html +1104 -0
- package/public/js/components/AIPanel.js +1639 -0
- package/public/js/components/AISummaryModal.js +278 -0
- package/public/js/components/AnalysisConfigModal.js +684 -0
- package/public/js/components/ConfirmDialog.js +227 -0
- package/public/js/components/PreviewModal.js +344 -0
- package/public/js/components/ProgressModal.js +678 -0
- package/public/js/components/ReviewModal.js +531 -0
- package/public/js/components/SplitButton.js +382 -0
- package/public/js/components/StatusIndicator.js +265 -0
- package/public/js/components/SuggestionNavigator.js +489 -0
- package/public/js/components/Toast.js +166 -0
- package/public/js/local.js +1580 -0
- package/public/js/modules/analysis-history.js +940 -0
- package/public/js/modules/comment-manager.js +643 -0
- package/public/js/modules/diff-renderer.js +585 -0
- package/public/js/modules/file-comment-manager.js +1242 -0
- package/public/js/modules/gap-coordinates.js +190 -0
- package/public/js/modules/hunk-parser.js +358 -0
- package/public/js/modules/line-tracker.js +386 -0
- package/public/js/modules/panel-resizer.js +228 -0
- package/public/js/modules/storage-cleanup.js +36 -0
- package/public/js/modules/suggestion-manager.js +692 -0
- package/public/js/pr.js +3503 -0
- package/public/js/repo-settings.js +691 -0
- package/public/js/utils/file-order.js +87 -0
- package/public/js/utils/markdown.js +97 -0
- package/public/js/utils/suggestion-ui.js +55 -0
- package/public/js/utils/tier-icons.js +25 -0
- package/public/local.html +460 -0
- package/public/pr.html +329 -0
- package/public/repo-settings.html +243 -0
- package/src/ai/analyzer.js +2592 -0
- package/src/ai/claude-cli.js +153 -0
- package/src/ai/claude-provider.js +261 -0
- package/src/ai/codex-provider.js +361 -0
- package/src/ai/copilot-provider.js +345 -0
- package/src/ai/gemini-provider.js +375 -0
- package/src/ai/index.js +47 -0
- package/src/ai/prompts/baseline/_meta.json +14 -0
- package/src/ai/prompts/baseline/level1/balanced.js +239 -0
- package/src/ai/prompts/baseline/level1/fast.js +194 -0
- package/src/ai/prompts/baseline/level1/thorough.js +319 -0
- package/src/ai/prompts/baseline/level2/balanced.js +248 -0
- package/src/ai/prompts/baseline/level2/fast.js +201 -0
- package/src/ai/prompts/baseline/level2/thorough.js +367 -0
- package/src/ai/prompts/baseline/level3/balanced.js +280 -0
- package/src/ai/prompts/baseline/level3/fast.js +220 -0
- package/src/ai/prompts/baseline/level3/thorough.js +459 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
- package/src/ai/prompts/config.js +52 -0
- package/src/ai/prompts/index.js +267 -0
- package/src/ai/prompts/shared/diff-instructions.js +50 -0
- package/src/ai/prompts/shared/output-schema.js +179 -0
- package/src/ai/prompts/shared/valid-files.js +37 -0
- package/src/ai/provider.js +260 -0
- package/src/config.js +139 -0
- package/src/database.js +2284 -0
- package/src/git/gitattributes.js +207 -0
- package/src/git/worktree.js +688 -0
- package/src/github/client.js +893 -0
- package/src/github/parser.js +247 -0
- package/src/local-review.js +691 -0
- package/src/main.js +987 -0
- package/src/routes/analysis.js +897 -0
- package/src/routes/comments.js +534 -0
- package/src/routes/config.js +250 -0
- package/src/routes/local.js +1728 -0
- package/src/routes/pr.js +1164 -0
- package/src/routes/shared.js +218 -0
- package/src/routes/worktrees.js +500 -0
- package/src/server.js +295 -0
- package/src/utils/diff-annotator.js +414 -0
- package/src/utils/instructions.js +33 -0
- package/src/utils/json-extractor.js +107 -0
- package/src/utils/line-validation.js +183 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/paths.js +161 -0
- package/src/utils/stats-calculator.js +86 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
const simpleGit = require('simple-git');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse command line arguments to extract PR information
|
|
7
|
+
*/
|
|
8
|
+
class PRArgumentParser {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.git = simpleGit();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse PR arguments from command line
|
|
15
|
+
* @param {Array<string>} args - Command line arguments
|
|
16
|
+
* @returns {Promise<Object>} Parsed PR information { owner, repo, number }
|
|
17
|
+
*/
|
|
18
|
+
async parsePRArguments(args) {
|
|
19
|
+
if (args.length === 0) {
|
|
20
|
+
throw new Error('Pull request number or URL is required. Usage: npx pair-review <PR-number> or npx pair-review <GitHub-URL>');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const input = args[0];
|
|
24
|
+
const result = this.parsePRUrl(input);
|
|
25
|
+
|
|
26
|
+
if (result) {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if input is a PR number
|
|
31
|
+
const prNumber = parseInt(input);
|
|
32
|
+
if (isNaN(prNumber) || prNumber <= 0) {
|
|
33
|
+
throw new Error('Invalid input format. Expected: PR number, GitHub URL (https://github.com/owner/repo/pull/number), or Graphite URL (https://app.graphite.com/github/pr/owner/repo/number)');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse repository from current directory's git remote
|
|
37
|
+
const { owner, repo } = await this.parseRepositoryFromGitRemote();
|
|
38
|
+
return { owner, repo, number: prNumber };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a PR URL string and extract owner, repo, and PR number
|
|
43
|
+
* Handles both GitHub and Graphite URLs, with or without protocol
|
|
44
|
+
* @param {string} url - The PR URL to parse
|
|
45
|
+
* @returns {Object|null} { owner, repo, number } or null if not a valid PR URL
|
|
46
|
+
*/
|
|
47
|
+
parsePRUrl(url) {
|
|
48
|
+
if (!url || typeof url !== 'string') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Clean up the URL - trim whitespace
|
|
53
|
+
let normalizedUrl = url.trim();
|
|
54
|
+
|
|
55
|
+
// Add https:// if no protocol is present
|
|
56
|
+
if (normalizedUrl.startsWith('github.com')) {
|
|
57
|
+
normalizedUrl = 'https://' + normalizedUrl;
|
|
58
|
+
} else if (normalizedUrl.startsWith('app.graphite.dev') || normalizedUrl.startsWith('app.graphite.com')) {
|
|
59
|
+
normalizedUrl = 'https://' + normalizedUrl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if input is a GitHub URL
|
|
63
|
+
if (normalizedUrl.startsWith('https://github.com/')) {
|
|
64
|
+
try {
|
|
65
|
+
return this.parseGitHubURL(normalizedUrl);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if input is a Graphite URL
|
|
72
|
+
if (normalizedUrl.startsWith('https://app.graphite.dev/') || normalizedUrl.startsWith('https://app.graphite.com/')) {
|
|
73
|
+
try {
|
|
74
|
+
return this.parseGraphiteURL(normalizedUrl);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse GitHub URL to extract owner, repo, and PR number
|
|
85
|
+
* @param {string} url - GitHub pull request URL
|
|
86
|
+
* @returns {Object} Parsed information { owner, repo, number }
|
|
87
|
+
*/
|
|
88
|
+
parseGitHubURL(url) {
|
|
89
|
+
// Match GitHub PR URL pattern: https://github.com/owner/repo/pull/number
|
|
90
|
+
const match = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)(?:\/.*)?$/);
|
|
91
|
+
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new Error('Invalid GitHub URL format. Expected: https://github.com/owner/repo/pull/number');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const [, owner, repo, numberStr] = match;
|
|
97
|
+
return this._createPRInfo(owner, repo, numberStr, 'GitHub');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse Graphite URL to extract owner, repo, and PR number
|
|
102
|
+
* @param {string} url - Graphite pull request URL
|
|
103
|
+
* @returns {Object} Parsed information { owner, repo, number }
|
|
104
|
+
*/
|
|
105
|
+
parseGraphiteURL(url) {
|
|
106
|
+
// Match Graphite PR URL pattern: https://app.graphite.{dev|com}/github/pr/owner/repo/number[/optional-title]
|
|
107
|
+
const match = url.match(/^https:\/\/app\.graphite\.(?:dev|com)\/github\/pr\/([^\/]+)\/([^\/]+)\/(\d+)(?:\/.*)?$/);
|
|
108
|
+
|
|
109
|
+
if (!match) {
|
|
110
|
+
throw new Error('Invalid Graphite URL format. Expected: https://app.graphite.com/github/pr/owner/repo/number');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [, owner, repo, numberStr] = match;
|
|
114
|
+
return this._createPRInfo(owner, repo, numberStr, 'Graphite');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create and validate PR info object from parsed components
|
|
119
|
+
* @param {string} owner - Repository owner
|
|
120
|
+
* @param {string} repo - Repository name
|
|
121
|
+
* @param {string} numberStr - PR number as string
|
|
122
|
+
* @param {string} source - Source name for error messages ('GitHub' or 'Graphite')
|
|
123
|
+
* @returns {Object} Validated PR info { owner, repo, number }
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_createPRInfo(owner, repo, numberStr, source) {
|
|
127
|
+
const number = parseInt(numberStr);
|
|
128
|
+
|
|
129
|
+
if (isNaN(number) || number <= 0) {
|
|
130
|
+
const exampleUrl = source === 'GitHub'
|
|
131
|
+
? 'https://github.com/owner/repo/pull/number'
|
|
132
|
+
: 'https://app.graphite.com/github/pr/owner/repo/number';
|
|
133
|
+
throw new Error(`Invalid ${source} URL format. Expected: ${exampleUrl}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { owner, repo, number };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse repository owner and name from git remote origin URL
|
|
141
|
+
* @returns {Promise<Object>} Repository information { owner, repo }
|
|
142
|
+
*/
|
|
143
|
+
async parseRepositoryFromGitRemote() {
|
|
144
|
+
try {
|
|
145
|
+
// Check if we're in a git repository
|
|
146
|
+
const isRepo = await this.git.checkIsRepo();
|
|
147
|
+
if (!isRepo) {
|
|
148
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get remote origin URL
|
|
152
|
+
const remotes = await this.git.getRemotes(true);
|
|
153
|
+
const origin = remotes.find(remote => remote.name === 'origin');
|
|
154
|
+
|
|
155
|
+
if (!origin) {
|
|
156
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const remoteUrl = origin.refs.fetch || origin.refs.push;
|
|
160
|
+
if (!remoteUrl) {
|
|
161
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.parseRepositoryFromURL(remoteUrl);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error.message.includes('not a git repository') || error.message.includes('Not a git repository')) {
|
|
167
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse repository owner and name from various Git URL formats
|
|
175
|
+
* @param {string} url - Git remote URL (HTTPS or SSH)
|
|
176
|
+
* @returns {Object} Repository information { owner, repo }
|
|
177
|
+
*/
|
|
178
|
+
parseRepositoryFromURL(url) {
|
|
179
|
+
// Handle HTTPS URLs: https://github.com/owner/repo.git
|
|
180
|
+
let match = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
|
181
|
+
if (match) {
|
|
182
|
+
return { owner: match[1], repo: match[2] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle SSH URLs: git@github.com:owner/repo.git
|
|
186
|
+
match = url.match(/^git@github\.com:([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
|
187
|
+
if (match) {
|
|
188
|
+
return { owner: match[1], repo: match[2] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validate PR arguments
|
|
196
|
+
* @param {Object} prInfo - PR information { owner, repo, number }
|
|
197
|
+
* @throws {Error} If arguments are invalid
|
|
198
|
+
*/
|
|
199
|
+
validatePRArguments(prInfo) {
|
|
200
|
+
if (!prInfo.owner || typeof prInfo.owner !== 'string' || prInfo.owner.trim().length === 0) {
|
|
201
|
+
throw new Error('Invalid repository owner');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!prInfo.repo || typeof prInfo.repo !== 'string' || prInfo.repo.trim().length === 0) {
|
|
205
|
+
throw new Error('Invalid repository name');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!prInfo.number || typeof prInfo.number !== 'number' || prInfo.number <= 0) {
|
|
209
|
+
throw new Error('Invalid pull request number');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get current working directory path
|
|
215
|
+
* @returns {string} Current working directory
|
|
216
|
+
*/
|
|
217
|
+
getCurrentDirectory() {
|
|
218
|
+
return process.cwd();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if current directory is a git repository
|
|
223
|
+
* @returns {Promise<boolean>} Whether current directory is a git repo
|
|
224
|
+
*/
|
|
225
|
+
async isGitRepository() {
|
|
226
|
+
try {
|
|
227
|
+
return await this.git.checkIsRepo();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get git repository root directory
|
|
235
|
+
* @returns {Promise<string>} Git repository root path
|
|
236
|
+
*/
|
|
237
|
+
async getRepositoryRoot() {
|
|
238
|
+
try {
|
|
239
|
+
const revParseResult = await this.git.revparse(['--show-toplevel']);
|
|
240
|
+
return revParseResult.trim();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error('Current directory is not a git repository or has no GitHub remote origin');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { PRArgumentParser };
|