@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.
Files changed (91) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +371 -0
  3. package/bin/git-diff-lines +146 -0
  4. package/bin/pair-review.js +49 -0
  5. package/package.json +71 -0
  6. package/public/css/ai-summary-modal.css +183 -0
  7. package/public/css/pr.css +8698 -0
  8. package/public/css/repo-settings.css +891 -0
  9. package/public/css/styles.css +479 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +1104 -0
  12. package/public/js/components/AIPanel.js +1639 -0
  13. package/public/js/components/AISummaryModal.js +278 -0
  14. package/public/js/components/AnalysisConfigModal.js +684 -0
  15. package/public/js/components/ConfirmDialog.js +227 -0
  16. package/public/js/components/PreviewModal.js +344 -0
  17. package/public/js/components/ProgressModal.js +678 -0
  18. package/public/js/components/ReviewModal.js +531 -0
  19. package/public/js/components/SplitButton.js +382 -0
  20. package/public/js/components/StatusIndicator.js +265 -0
  21. package/public/js/components/SuggestionNavigator.js +489 -0
  22. package/public/js/components/Toast.js +166 -0
  23. package/public/js/local.js +1580 -0
  24. package/public/js/modules/analysis-history.js +940 -0
  25. package/public/js/modules/comment-manager.js +643 -0
  26. package/public/js/modules/diff-renderer.js +585 -0
  27. package/public/js/modules/file-comment-manager.js +1242 -0
  28. package/public/js/modules/gap-coordinates.js +190 -0
  29. package/public/js/modules/hunk-parser.js +358 -0
  30. package/public/js/modules/line-tracker.js +386 -0
  31. package/public/js/modules/panel-resizer.js +228 -0
  32. package/public/js/modules/storage-cleanup.js +36 -0
  33. package/public/js/modules/suggestion-manager.js +692 -0
  34. package/public/js/pr.js +3503 -0
  35. package/public/js/repo-settings.js +691 -0
  36. package/public/js/utils/file-order.js +87 -0
  37. package/public/js/utils/markdown.js +97 -0
  38. package/public/js/utils/suggestion-ui.js +55 -0
  39. package/public/js/utils/tier-icons.js +25 -0
  40. package/public/local.html +460 -0
  41. package/public/pr.html +329 -0
  42. package/public/repo-settings.html +243 -0
  43. package/src/ai/analyzer.js +2592 -0
  44. package/src/ai/claude-cli.js +153 -0
  45. package/src/ai/claude-provider.js +261 -0
  46. package/src/ai/codex-provider.js +361 -0
  47. package/src/ai/copilot-provider.js +345 -0
  48. package/src/ai/gemini-provider.js +375 -0
  49. package/src/ai/index.js +47 -0
  50. package/src/ai/prompts/baseline/_meta.json +14 -0
  51. package/src/ai/prompts/baseline/level1/balanced.js +239 -0
  52. package/src/ai/prompts/baseline/level1/fast.js +194 -0
  53. package/src/ai/prompts/baseline/level1/thorough.js +319 -0
  54. package/src/ai/prompts/baseline/level2/balanced.js +248 -0
  55. package/src/ai/prompts/baseline/level2/fast.js +201 -0
  56. package/src/ai/prompts/baseline/level2/thorough.js +367 -0
  57. package/src/ai/prompts/baseline/level3/balanced.js +280 -0
  58. package/src/ai/prompts/baseline/level3/fast.js +220 -0
  59. package/src/ai/prompts/baseline/level3/thorough.js +459 -0
  60. package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
  61. package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
  62. package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
  63. package/src/ai/prompts/config.js +52 -0
  64. package/src/ai/prompts/index.js +267 -0
  65. package/src/ai/prompts/shared/diff-instructions.js +50 -0
  66. package/src/ai/prompts/shared/output-schema.js +179 -0
  67. package/src/ai/prompts/shared/valid-files.js +37 -0
  68. package/src/ai/provider.js +260 -0
  69. package/src/config.js +139 -0
  70. package/src/database.js +2284 -0
  71. package/src/git/gitattributes.js +207 -0
  72. package/src/git/worktree.js +688 -0
  73. package/src/github/client.js +893 -0
  74. package/src/github/parser.js +247 -0
  75. package/src/local-review.js +691 -0
  76. package/src/main.js +987 -0
  77. package/src/routes/analysis.js +897 -0
  78. package/src/routes/comments.js +534 -0
  79. package/src/routes/config.js +250 -0
  80. package/src/routes/local.js +1728 -0
  81. package/src/routes/pr.js +1164 -0
  82. package/src/routes/shared.js +218 -0
  83. package/src/routes/worktrees.js +500 -0
  84. package/src/server.js +295 -0
  85. package/src/utils/diff-annotator.js +414 -0
  86. package/src/utils/instructions.js +33 -0
  87. package/src/utils/json-extractor.js +107 -0
  88. package/src/utils/line-validation.js +183 -0
  89. package/src/utils/logger.js +142 -0
  90. package/src/utils/paths.js +161 -0
  91. 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 };