@in-the-loop-labs/pair-review 2.1.1 → 2.2.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/README.md +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +10 -0
- package/public/js/components/ReviewModal.js +65 -2
- package/src/config.js +2 -1
- package/src/github/parser.js +29 -2
- package/src/main.js +35 -1
- package/src/protocol-handler.js +122 -0
- package/src/routes/config.js +2 -1
package/README.md
CHANGED
|
@@ -195,6 +195,9 @@ pair-review --local [path]
|
|
|
195
195
|
| `-h`, `--help` | Show help message with full CLI documentation |
|
|
196
196
|
| `-l`, `--local [path]` | Review local uncommitted changes. Optional path defaults to current directory |
|
|
197
197
|
| `--model <name>` | Override the AI model for any provider. Model availability depends on provider configuration. |
|
|
198
|
+
| `--register` | Register `pair-review://` URL scheme handler (macOS only) |
|
|
199
|
+
| `--unregister` | Unregister `pair-review://` URL scheme handler (macOS only) |
|
|
200
|
+
| `--command <cmd>` | Custom CLI command for `--register` (default: `npx @in-the-loop-labs/pair-review`) |
|
|
198
201
|
| `-v`, `--version` | Show version number |
|
|
199
202
|
|
|
200
203
|
### Examples
|
|
@@ -204,6 +207,8 @@ pair-review 123 # Review PR #123 in current repo
|
|
|
204
207
|
pair-review https://github.com/owner/repo/pull/456
|
|
205
208
|
pair-review --local # Review uncommitted local changes
|
|
206
209
|
pair-review 123 --ai # Auto-run AI analysis
|
|
210
|
+
pair-review --register # Register pair-review:// URL scheme (macOS)
|
|
211
|
+
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
207
212
|
```
|
|
208
213
|
|
|
209
214
|
## Configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
package/public/css/pr.css
CHANGED
|
@@ -4846,6 +4846,16 @@ tr.line-range-start .d2h-code-line-ctn {
|
|
|
4846
4846
|
opacity: 0.7;
|
|
4847
4847
|
}
|
|
4848
4848
|
|
|
4849
|
+
/* Assisted-by footer toggle (extends .remember-toggle) */
|
|
4850
|
+
.assisted-by-toggle {
|
|
4851
|
+
margin-top: 8px;
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4854
|
+
.assisted-by-toggle.disabled {
|
|
4855
|
+
opacity: 0.5;
|
|
4856
|
+
pointer-events: none;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4849
4859
|
.review-type-options {
|
|
4850
4860
|
display: flex;
|
|
4851
4861
|
flex-direction: column;
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
* Review Submission Modal Component
|
|
4
4
|
* Allows users to submit their review with comments to GitHub
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
const ASSISTED_BY_STORAGE_KEY = 'pair-review-assisted-by';
|
|
8
|
+
const DEFAULT_ASSISTED_BY_URL = 'https://github.com/in-the-loop-labs/pair-review';
|
|
9
|
+
|
|
6
10
|
class ReviewModal {
|
|
7
11
|
constructor() {
|
|
8
12
|
this.modal = null;
|
|
9
13
|
this.isVisible = false;
|
|
10
14
|
this.isSubmitting = false;
|
|
15
|
+
this.assistedByUrl = DEFAULT_ASSISTED_BY_URL;
|
|
16
|
+
fetch('/api/config')
|
|
17
|
+
.then(res => res.ok ? res.json() : null)
|
|
18
|
+
.then(data => {
|
|
19
|
+
if (data?.assisted_by_url) {
|
|
20
|
+
this.assistedByUrl = data.assisted_by_url;
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {}); // Use default on failure
|
|
11
24
|
this.createModal();
|
|
12
25
|
this.setupEventListeners();
|
|
13
26
|
}
|
|
@@ -69,6 +82,11 @@ class ReviewModal {
|
|
|
69
82
|
placeholder="Leave a comment about this pull request..."
|
|
70
83
|
rows="2"
|
|
71
84
|
></textarea>
|
|
85
|
+
<label class="remember-toggle assisted-by-toggle" id="assisted-by-toggle">
|
|
86
|
+
<input type="checkbox" id="assisted-by-checkbox" />
|
|
87
|
+
<span class="toggle-switch"></span>
|
|
88
|
+
<span class="toggle-label">Append pair-review footer</span>
|
|
89
|
+
</label>
|
|
72
90
|
</div>
|
|
73
91
|
|
|
74
92
|
<div class="review-type-section">
|
|
@@ -183,6 +201,9 @@ class ReviewModal {
|
|
|
183
201
|
if (e.target.matches('input[name="review-event"]')) {
|
|
184
202
|
window.reviewModal?.updateTextareaState();
|
|
185
203
|
}
|
|
204
|
+
if (e.target.matches('#assisted-by-checkbox')) {
|
|
205
|
+
window.reviewModal?.handleAssistedByToggle();
|
|
206
|
+
}
|
|
186
207
|
});
|
|
187
208
|
}
|
|
188
209
|
|
|
@@ -194,6 +215,7 @@ class ReviewModal {
|
|
|
194
215
|
updateTextareaState() {
|
|
195
216
|
const textarea = this.modal?.querySelector('#review-body-modal');
|
|
196
217
|
const selectedOption = this.modal?.querySelector('input[name="review-event"]:checked');
|
|
218
|
+
const toggle = this.modal?.querySelector('#assisted-by-toggle');
|
|
197
219
|
|
|
198
220
|
if (!textarea || !selectedOption) return;
|
|
199
221
|
|
|
@@ -204,9 +226,15 @@ class ReviewModal {
|
|
|
204
226
|
if (isDraft) {
|
|
205
227
|
textarea.title = 'Review summary is not included with draft reviews';
|
|
206
228
|
textarea.classList.add('disabled-textarea');
|
|
229
|
+
if (toggle) {
|
|
230
|
+
toggle.classList.add('disabled');
|
|
231
|
+
}
|
|
207
232
|
} else {
|
|
208
233
|
textarea.title = '';
|
|
209
234
|
textarea.classList.remove('disabled-textarea');
|
|
235
|
+
if (toggle) {
|
|
236
|
+
toggle.classList.remove('disabled');
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
239
|
}
|
|
212
240
|
|
|
@@ -235,6 +263,9 @@ class ReviewModal {
|
|
|
235
263
|
// Update textarea state (ensures it's enabled since COMMENT is selected by default)
|
|
236
264
|
this.updateTextareaState();
|
|
237
265
|
|
|
266
|
+
// Restore assisted-by toggle from localStorage
|
|
267
|
+
this.restoreAssistedByToggle();
|
|
268
|
+
|
|
238
269
|
// Clear any errors or warnings
|
|
239
270
|
this.hideError();
|
|
240
271
|
this.updateLargeReviewWarning(0);
|
|
@@ -431,6 +462,10 @@ class ReviewModal {
|
|
|
431
462
|
if (this.isSubmitting) return;
|
|
432
463
|
|
|
433
464
|
const reviewBody = this.modal.querySelector('#review-body-modal').value.trim();
|
|
465
|
+
const assistedByCheckbox = this.modal.querySelector('#assisted-by-checkbox');
|
|
466
|
+
const finalBody = assistedByCheckbox?.checked
|
|
467
|
+
? reviewBody + this.getAssistedByFooter()
|
|
468
|
+
: reviewBody;
|
|
434
469
|
const selectedOption = this.modal.querySelector('input[name="review-event"]:checked');
|
|
435
470
|
const reviewEvent = selectedOption ? selectedOption.value : 'COMMENT';
|
|
436
471
|
// Count BOTH line-level (.user-comment-row) and file-level (.file-comment-card.user-comment) comments
|
|
@@ -480,7 +515,7 @@ class ReviewModal {
|
|
|
480
515
|
},
|
|
481
516
|
body: JSON.stringify({
|
|
482
517
|
event: reviewEvent,
|
|
483
|
-
body:
|
|
518
|
+
body: finalBody
|
|
484
519
|
})
|
|
485
520
|
});
|
|
486
521
|
|
|
@@ -590,7 +625,6 @@ class ReviewModal {
|
|
|
590
625
|
const textarea = this.modal?.querySelector('#review-body-modal');
|
|
591
626
|
if (!textarea) return;
|
|
592
627
|
|
|
593
|
-
// Get AI summary from the AI panel
|
|
594
628
|
const summary = window.aiPanel?.getSummary?.();
|
|
595
629
|
if (!summary) {
|
|
596
630
|
if (window.toast) {
|
|
@@ -613,6 +647,35 @@ class ReviewModal {
|
|
|
613
647
|
}
|
|
614
648
|
}
|
|
615
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Get the "assisted by" footer string
|
|
652
|
+
*/
|
|
653
|
+
getAssistedByFooter() {
|
|
654
|
+
const url = this.assistedByUrl || DEFAULT_ASSISTED_BY_URL;
|
|
655
|
+
return `\n\n---\n_Review assisted by [pair-review](${url})_`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Restore the assisted-by toggle state from localStorage
|
|
660
|
+
*/
|
|
661
|
+
restoreAssistedByToggle() {
|
|
662
|
+
const checkbox = this.modal?.querySelector('#assisted-by-checkbox');
|
|
663
|
+
if (!checkbox) return;
|
|
664
|
+
|
|
665
|
+
const stored = localStorage.getItem(ASSISTED_BY_STORAGE_KEY);
|
|
666
|
+
checkbox.checked = stored !== 'false';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Handle the assisted-by checkbox toggle
|
|
671
|
+
*/
|
|
672
|
+
handleAssistedByToggle() {
|
|
673
|
+
const checkbox = this.modal?.querySelector('#assisted-by-checkbox');
|
|
674
|
+
if (!checkbox) return;
|
|
675
|
+
|
|
676
|
+
localStorage.setItem(ASSISTED_BY_STORAGE_KEY, String(checkbox.checked));
|
|
677
|
+
}
|
|
678
|
+
|
|
616
679
|
}
|
|
617
680
|
|
|
618
681
|
// Initialize when DOM is ready if not already initialized
|
package/src/config.js
CHANGED
|
@@ -24,7 +24,8 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
enable_chat: true, // When true, enables the chat panel feature (requires Pi AI provider)
|
|
25
25
|
chat: { enable_shortcuts: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons)
|
|
26
26
|
providers: {}, // Custom provider configurations (overrides built-in defaults)
|
|
27
|
-
monorepos: {} // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
27
|
+
monorepos: {}, // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
28
|
+
assisted_by_url: "https://github.com/in-the-loop-labs/pair-review" // URL for "Review assisted by" footer link
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/**
|
package/src/github/parser.js
CHANGED
|
@@ -30,7 +30,7 @@ class PRArgumentParser {
|
|
|
30
30
|
// Check if input is a PR number
|
|
31
31
|
const prNumber = parseInt(input);
|
|
32
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),
|
|
33
|
+
throw new Error('Invalid input format. Expected: PR number, GitHub URL (https://github.com/owner/repo/pull/number), Graphite URL (https://app.graphite.com/github/pr/owner/repo/number), or pair-review:// URL');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Parse repository from current directory's git remote
|
|
@@ -59,6 +59,15 @@ class PRArgumentParser {
|
|
|
59
59
|
normalizedUrl = 'https://' + normalizedUrl;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Check if input is a pair-review:// protocol URL
|
|
63
|
+
if (normalizedUrl.startsWith('pair-review://')) {
|
|
64
|
+
try {
|
|
65
|
+
return this.parseProtocolURL(normalizedUrl);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
// Check if input is a GitHub URL
|
|
63
72
|
if (normalizedUrl.startsWith('https://github.com/')) {
|
|
64
73
|
try {
|
|
@@ -114,6 +123,22 @@ class PRArgumentParser {
|
|
|
114
123
|
return this._createPRInfo(owner, repo, numberStr, 'Graphite');
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Parse pair-review:// protocol URL to extract owner, repo, and PR number
|
|
128
|
+
* @param {string} url - Protocol URL (e.g., pair-review://pr/owner/repo/123)
|
|
129
|
+
* @returns {Object} Parsed information { owner, repo, number }
|
|
130
|
+
*/
|
|
131
|
+
parseProtocolURL(url) {
|
|
132
|
+
const match = url.match(/^pair-review:\/\/pr\/([^\/]+)\/([^\/]+)\/(\d+)(?:\/.*)?$/);
|
|
133
|
+
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error('Invalid pair-review:// URL format. Expected: pair-review://pr/owner/repo/number');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [, owner, repo, numberStr] = match;
|
|
139
|
+
return this._createPRInfo(owner, repo, numberStr, 'pair-review://');
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/**
|
|
118
143
|
* Create and validate PR info object from parsed components
|
|
119
144
|
* @param {string} owner - Repository owner
|
|
@@ -129,7 +154,9 @@ class PRArgumentParser {
|
|
|
129
154
|
if (isNaN(number) || number <= 0) {
|
|
130
155
|
const exampleUrl = source === 'GitHub'
|
|
131
156
|
? 'https://github.com/owner/repo/pull/number'
|
|
132
|
-
: '
|
|
157
|
+
: source === 'pair-review://'
|
|
158
|
+
? 'pair-review://pr/owner/repo/number'
|
|
159
|
+
: 'https://app.graphite.com/github/pr/owner/repo/number';
|
|
133
160
|
throw new Error(`Invalid ${source} URL format. Expected: ${exampleUrl}`);
|
|
134
161
|
}
|
|
135
162
|
|
package/src/main.js
CHANGED
|
@@ -16,6 +16,7 @@ const simpleGit = require('simple-git');
|
|
|
16
16
|
const { getGeneratedFilePatterns } = require('./git/gitattributes');
|
|
17
17
|
const { getEmoji: getCategoryEmoji } = require('./utils/category-emoji');
|
|
18
18
|
const open = (...args) => import('open').then(({default: open}) => open(...args));
|
|
19
|
+
const { registerProtocolHandler, unregisterProtocolHandler } = require('./protocol-handler');
|
|
19
20
|
|
|
20
21
|
let db = null;
|
|
21
22
|
|
|
@@ -117,6 +118,9 @@ OPTIONS:
|
|
|
117
118
|
(automatic in GitHub Actions)
|
|
118
119
|
--yolo Allow AI providers full system access (skip read-only
|
|
119
120
|
restrictions). Analogous to --dangerously-skip-permissions
|
|
121
|
+
--register [--command <cmd>] Register pair-review:// URL scheme handler (macOS)
|
|
122
|
+
Default command: npx @in-the-loop-labs/pair-review
|
|
123
|
+
--unregister Unregister pair-review:// URL scheme handler (macOS)
|
|
120
124
|
-v, --version Show version number and exit
|
|
121
125
|
|
|
122
126
|
EXAMPLES:
|
|
@@ -125,6 +129,8 @@ EXAMPLES:
|
|
|
125
129
|
pair-review --local # Review uncommitted local changes
|
|
126
130
|
pair-review 123 --ai # Auto-run AI analysis
|
|
127
131
|
pair-review --ai-review # CI mode: auto-detect PR, submit review
|
|
132
|
+
pair-review --register # Register URL scheme handler
|
|
133
|
+
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
128
134
|
|
|
129
135
|
ENVIRONMENT VARIABLES:
|
|
130
136
|
GITHUB_TOKEN GitHub Personal Access Token (takes precedence over config file)
|
|
@@ -188,6 +194,9 @@ const KNOWN_FLAGS = new Set([
|
|
|
188
194
|
'-l', '--local',
|
|
189
195
|
'--mcp',
|
|
190
196
|
'--model',
|
|
197
|
+
'--register',
|
|
198
|
+
'--unregister',
|
|
199
|
+
'--command',
|
|
191
200
|
'--use-checkout',
|
|
192
201
|
'--yolo',
|
|
193
202
|
'-v', '--version'
|
|
@@ -243,9 +252,14 @@ function parseArgs(args) {
|
|
|
243
252
|
i++; // Skip next argument since we consumed it
|
|
244
253
|
}
|
|
245
254
|
// localPath will be resolved to cwd if not provided
|
|
246
|
-
} else if (arg === '--configure' || arg === '-h' || arg === '--help' || arg === '--mcp' || arg === '-v' || arg === '--version') {
|
|
255
|
+
} else if (arg === '--configure' || arg === '-h' || arg === '--help' || arg === '--mcp' || arg === '-v' || arg === '--version' || arg === '--register' || arg === '--unregister') {
|
|
247
256
|
// Skip flags that are handled earlier in main()
|
|
248
257
|
continue;
|
|
258
|
+
} else if (arg === '--command') {
|
|
259
|
+
// --command flag consumed by --register handler, skip it and its value
|
|
260
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
261
|
+
i++; // Skip the next argument (the command value)
|
|
262
|
+
}
|
|
249
263
|
} else if (arg.startsWith('-')) {
|
|
250
264
|
// Unknown flag - collect for error reporting
|
|
251
265
|
unknownFlags.push(arg);
|
|
@@ -346,6 +360,26 @@ AI PROVIDERS:
|
|
|
346
360
|
process.exit(0);
|
|
347
361
|
}
|
|
348
362
|
|
|
363
|
+
// Handle protocol handler registration
|
|
364
|
+
if (args.includes('--register')) {
|
|
365
|
+
const cmdIdx = args.indexOf('--command');
|
|
366
|
+
let command;
|
|
367
|
+
if (cmdIdx !== -1) {
|
|
368
|
+
if (cmdIdx + 1 < args.length && !args[cmdIdx + 1].startsWith('-')) {
|
|
369
|
+
command = args[cmdIdx + 1];
|
|
370
|
+
} else {
|
|
371
|
+
throw new Error('--command flag requires a command string');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await registerProtocolHandler({ command });
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (args.includes('--unregister')) {
|
|
379
|
+
await unregisterProtocolHandler();
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
|
|
349
383
|
// Load configuration
|
|
350
384
|
const { config, isFirstRun } = await loadConfig();
|
|
351
385
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { getConfigDir } = require('./config');
|
|
6
|
+
const logger = require('./utils/logger');
|
|
7
|
+
|
|
8
|
+
const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
|
|
9
|
+
|
|
10
|
+
// Default dependencies (overridable for testing)
|
|
11
|
+
const defaults = {
|
|
12
|
+
fs,
|
|
13
|
+
execSync,
|
|
14
|
+
getConfigDir,
|
|
15
|
+
logger,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a macOS custom URL scheme handler for pair-review://
|
|
20
|
+
* Creates an AppleScript .app that forwards URLs to the pair-review CLI.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {string} [options.command] - CLI command to invoke (default: npx @in-the-loop-labs/pair-review)
|
|
24
|
+
* @param {object} [options._deps] - Internal: dependency overrides for testing
|
|
25
|
+
*/
|
|
26
|
+
function registerProtocolHandler({ command, _deps } = {}) {
|
|
27
|
+
const deps = { ...defaults, ..._deps };
|
|
28
|
+
|
|
29
|
+
if (process.platform !== 'darwin') {
|
|
30
|
+
deps.logger.warn('Protocol handler registration is only supported on macOS');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const shell = process.env.SHELL || '/bin/zsh';
|
|
35
|
+
const resolvedCommand = command || 'npx @in-the-loop-labs/pair-review';
|
|
36
|
+
|
|
37
|
+
// Escape characters special inside AppleScript double-quoted strings
|
|
38
|
+
const escapeAS = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
39
|
+
const safeShell = escapeAS(shell);
|
|
40
|
+
const safeCommand = escapeAS(resolvedCommand);
|
|
41
|
+
|
|
42
|
+
const appleScriptSource = [
|
|
43
|
+
'on run',
|
|
44
|
+
'\t-- No-op: launched directly without a URL',
|
|
45
|
+
'end run',
|
|
46
|
+
'',
|
|
47
|
+
'on open location theURL',
|
|
48
|
+
`\tdo shell script "${safeShell} -l -c \\"${safeCommand} " & quoted form of theURL & " > /dev/null 2>&1 &\\""`,
|
|
49
|
+
'end open location',
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const appPath = path.join(deps.getConfigDir(), 'PairReview.app');
|
|
53
|
+
|
|
54
|
+
// Remove existing .app if present
|
|
55
|
+
deps.fs.rmSync(appPath, { recursive: true, force: true });
|
|
56
|
+
|
|
57
|
+
// Compile the AppleScript into an .app bundle
|
|
58
|
+
deps.execSync(`osacompile -o "${appPath}"`, { input: appleScriptSource });
|
|
59
|
+
|
|
60
|
+
// Mutate Info.plist to declare the URL scheme
|
|
61
|
+
const plistPath = path.join(appPath, 'Contents', 'Info.plist');
|
|
62
|
+
let plist = deps.fs.readFileSync(plistPath, 'utf-8');
|
|
63
|
+
|
|
64
|
+
const urlSchemeEntries = [
|
|
65
|
+
'\t<key>CFBundleIdentifier</key>',
|
|
66
|
+
'\t<string>com.pair-review.launcher</string>',
|
|
67
|
+
'\t<key>CFBundleURLTypes</key>',
|
|
68
|
+
'\t<array>',
|
|
69
|
+
'\t\t<dict>',
|
|
70
|
+
'\t\t\t<key>CFBundleURLName</key>',
|
|
71
|
+
'\t\t\t<string>pair-review URL</string>',
|
|
72
|
+
'\t\t\t<key>CFBundleURLSchemes</key>',
|
|
73
|
+
'\t\t\t<array>',
|
|
74
|
+
'\t\t\t\t<string>pair-review</string>',
|
|
75
|
+
'\t\t\t</array>',
|
|
76
|
+
'\t\t</dict>',
|
|
77
|
+
'\t</array>',
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
// Insert before the closing </dict> that precedes </plist>
|
|
81
|
+
plist = plist.replace('</dict>\n</plist>', `${urlSchemeEntries}\n</dict>\n</plist>`);
|
|
82
|
+
deps.fs.writeFileSync(plistPath, plist);
|
|
83
|
+
|
|
84
|
+
deps.execSync(`"${LSREGISTER}" -R -f "${appPath}"`);
|
|
85
|
+
|
|
86
|
+
// console.log intentional: CLI output, not server-side (see CLAUDE.md logging convention)
|
|
87
|
+
console.log('Registered pair-review:// URL scheme handler');
|
|
88
|
+
console.log(`Command: ${shell} -l -c '${resolvedCommand} <url>'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unregister the macOS custom URL scheme handler for pair-review://
|
|
93
|
+
* Removes the .app bundle and deregisters from Launch Services.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {object} [options._deps] - Internal: dependency overrides for testing
|
|
97
|
+
*/
|
|
98
|
+
function unregisterProtocolHandler({ _deps } = {}) {
|
|
99
|
+
const deps = { ...defaults, ..._deps };
|
|
100
|
+
|
|
101
|
+
if (process.platform !== 'darwin') {
|
|
102
|
+
deps.logger.warn('Protocol handler registration is only supported on macOS');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const appPath = path.join(deps.getConfigDir(), 'PairReview.app');
|
|
107
|
+
|
|
108
|
+
// Attempt to deregister from Launch Services (may fail if not registered)
|
|
109
|
+
try {
|
|
110
|
+
deps.execSync(`"${LSREGISTER}" -u "${appPath}"`);
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore — handler may not be registered
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove the .app bundle
|
|
116
|
+
deps.fs.rmSync(appPath, { recursive: true, force: true });
|
|
117
|
+
|
|
118
|
+
// console.log intentional: CLI output, not server-side (see CLAUDE.md logging convention)
|
|
119
|
+
console.log('Unregistered pair-review:// URL scheme handler');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { registerProtocolHandler, unregisterProtocolHandler };
|
package/src/routes/config.js
CHANGED
|
@@ -40,7 +40,8 @@ router.get('/api/config', (req, res) => {
|
|
|
40
40
|
is_running_via_npx: isRunningViaNpx(),
|
|
41
41
|
enable_chat: config.enable_chat !== false,
|
|
42
42
|
chat_enable_shortcuts: config.chat?.enable_shortcuts !== false,
|
|
43
|
-
pi_available: getCachedAvailability('pi')?.available || false
|
|
43
|
+
pi_available: getCachedAvailability('pi')?.available || false,
|
|
44
|
+
assisted_by_url: config.assisted_by_url || 'https://github.com/in-the-loop-labs/pair-review'
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
47
|
|