@in-the-loop-labs/pair-review 4.0.0 → 4.1.1
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 +82 -7
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +3 -4
- package/plugin-pair-loop/.claude-plugin/plugin.json +19 -0
- package/plugin-pair-loop/skills/loop/SKILL.md +233 -0
- package/public/js/index.js +87 -10
- package/public/js/local.js +19 -9
- package/public/js/pr.js +64 -36
- package/public/js/repo-links.js +11 -3
- package/public/js/utils/analyze-params.js +69 -0
- package/public/js/utils/provider-model.js +66 -1
- package/public/js/vendor/pierre-diffs-worker.js +16158 -0
- package/public/js/vendor/pierre-diffs.js +1880 -0
- package/public/local.html +1 -0
- package/public/pr.html +1 -0
- package/public/setup.html +35 -16
- package/src/ai/codex-provider.js +59 -11
- package/src/config.js +150 -28
- package/src/database.js +127 -3
- package/src/external/github-adapter.js +18 -3
- package/src/github/client.js +37 -0
- package/src/github/parser.js +41 -7
- package/src/interactive-analysis-config.js +2 -2
- package/src/links/repo-links.js +66 -28
- package/src/local-review.js +134 -5
- package/src/local-scope.js +38 -0
- package/src/main.js +199 -33
- package/src/routes/config.js +47 -12
- package/src/routes/external-comments.js +13 -1
- package/src/routes/github-collections.js +175 -13
- package/src/routes/local.js +11 -20
- package/src/routes/pr.js +63 -36
- package/src/routes/setup.js +63 -8
- package/src/routes/shared.js +85 -0
- package/src/routes/stack-analysis.js +39 -3
- package/src/server.js +74 -3
- package/src/setup/local-setup.js +23 -7
- package/src/setup/pr-setup.js +237 -39
- package/src/setup/stack-setup.js +7 -2
- package/src/single-port.js +73 -18
- package/src/utils/host-resolution.js +157 -0
- package/plugin-code-critic/skills/loop/SKILL.md +0 -373
package/src/database.js
CHANGED
|
@@ -21,7 +21,7 @@ function getDbPath() {
|
|
|
21
21
|
/**
|
|
22
22
|
* Current schema version - increment this when adding new migrations
|
|
23
23
|
*/
|
|
24
|
-
const CURRENT_SCHEMA_VERSION =
|
|
24
|
+
const CURRENT_SCHEMA_VERSION = 51;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Database schema SQL statements
|
|
@@ -123,6 +123,7 @@ const SCHEMA_SQL = {
|
|
|
123
123
|
pr_data TEXT,
|
|
124
124
|
last_ai_run_id TEXT,
|
|
125
125
|
last_accessed_at TEXT,
|
|
126
|
+
host TEXT,
|
|
126
127
|
UNIQUE(pr_number, repository)
|
|
127
128
|
)
|
|
128
129
|
`,
|
|
@@ -312,6 +313,7 @@ const SCHEMA_SQL = {
|
|
|
312
313
|
html_url TEXT,
|
|
313
314
|
state TEXT DEFAULT 'open',
|
|
314
315
|
collection TEXT NOT NULL,
|
|
316
|
+
host TEXT,
|
|
315
317
|
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
316
318
|
)
|
|
317
319
|
`,
|
|
@@ -349,6 +351,7 @@ const SCHEMA_SQL = {
|
|
|
349
351
|
diff_position INTEGER,
|
|
350
352
|
commit_sha TEXT,
|
|
351
353
|
is_outdated INTEGER NOT NULL DEFAULT 0,
|
|
354
|
+
is_file_level INTEGER NOT NULL DEFAULT 0,
|
|
352
355
|
original_line_start INTEGER,
|
|
353
356
|
original_line_end INTEGER,
|
|
354
357
|
original_commit_sha TEXT,
|
|
@@ -397,8 +400,14 @@ const INDEX_SQL = [
|
|
|
397
400
|
'CREATE INDEX IF NOT EXISTS idx_context_files_review ON context_files(review_id)',
|
|
398
401
|
// Hunk summaries indexes
|
|
399
402
|
'CREATE INDEX IF NOT EXISTS idx_hunk_summaries_review ON hunk_summaries(review_id)',
|
|
400
|
-
// GitHub PR cache indexes
|
|
401
|
-
|
|
403
|
+
// GitHub PR cache indexes. `host` is part of the unique key so a dual-host
|
|
404
|
+
// repo can cache the same (collection, owner, repo, number) from github.com
|
|
405
|
+
// (host NULL) AND its alt host (host = api_host URL) without colliding —
|
|
406
|
+
// independently-numbered PRs across systems overlap on small numbers. SQLite
|
|
407
|
+
// treats NULLs as distinct in unique indexes, so two NULL-host rows would not
|
|
408
|
+
// collide, but the collections refresh DELETEs the collection before
|
|
409
|
+
// re-inserting, so duplicate NULL-host rows can't accumulate. See migration 51.
|
|
410
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_github_pr_cache_unique ON github_pr_cache(collection, owner, repo, number, host)',
|
|
402
411
|
// Worktree pool indexes
|
|
403
412
|
'CREATE INDEX IF NOT EXISTS idx_worktree_pool_repo ON worktree_pool(repository)',
|
|
404
413
|
'CREATE INDEX IF NOT EXISTS idx_worktree_pool_status ON worktree_pool(repository, status)',
|
|
@@ -1305,6 +1314,9 @@ const MIGRATIONS = {
|
|
|
1305
1314
|
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1306
1315
|
)
|
|
1307
1316
|
`);
|
|
1317
|
+
// Historical shape: no `host` column exists at v26 (added in migration
|
|
1318
|
+
// 50). Migration 51 widens this index to include `host`. Do NOT add
|
|
1319
|
+
// `host` here — it would reference a column that doesn't exist yet.
|
|
1308
1320
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_github_pr_cache_unique ON github_pr_cache(collection, owner, repo, number)');
|
|
1309
1321
|
console.log(' Created github_pr_cache table');
|
|
1310
1322
|
} else {
|
|
@@ -2159,6 +2171,72 @@ const MIGRATIONS = {
|
|
|
2159
2171
|
console.log(' Table tours already exists');
|
|
2160
2172
|
}
|
|
2161
2173
|
console.log('Migration to schema version 48 complete');
|
|
2174
|
+
},
|
|
2175
|
+
|
|
2176
|
+
// Migration to version 49: Add is_file_level flag to external_comments so
|
|
2177
|
+
// file-level review comments (GitHub subject_type='file') can be rendered in
|
|
2178
|
+
// the per-file comments zone instead of as a bogus line-1 annotation.
|
|
2179
|
+
// Idempotent single-column ALTER guarded by columnExists — safe to re-run.
|
|
2180
|
+
// A single DDL statement needs no transaction wrapper.
|
|
2181
|
+
49: (db) => {
|
|
2182
|
+
console.log('Running migration to schema version 49: Add is_file_level to external_comments...');
|
|
2183
|
+
if (!tableExists(db, 'external_comments')) {
|
|
2184
|
+
// Table not created yet (older instance below version 45). Migration 45
|
|
2185
|
+
// creates it with the current base schema; nothing to alter here.
|
|
2186
|
+
console.log(' external_comments table not present; nothing to alter');
|
|
2187
|
+
} else if (!columnExists(db, 'external_comments', 'is_file_level')) {
|
|
2188
|
+
db.exec('ALTER TABLE external_comments ADD COLUMN is_file_level INTEGER NOT NULL DEFAULT 0');
|
|
2189
|
+
console.log(' Added is_file_level column to external_comments');
|
|
2190
|
+
} else {
|
|
2191
|
+
console.log(' Column is_file_level already exists');
|
|
2192
|
+
}
|
|
2193
|
+
console.log('Migration to schema version 49 complete');
|
|
2194
|
+
},
|
|
2195
|
+
|
|
2196
|
+
// Migration to version 50: Add host column to pr_metadata and github_pr_cache
|
|
2197
|
+
// for per-PR host resolution (dual GitHub + alt-host repos). NULL means
|
|
2198
|
+
// github.com; otherwise the configured api_host URL string. No backfill — the
|
|
2199
|
+
// NULL-means-github fallback makes existing rows resolve identically, and rows
|
|
2200
|
+
// are re-stamped on next fetch. Each ALTER is guarded by columnExists so the
|
|
2201
|
+
// migration is idempotent; single DDL statements need no transaction wrapper.
|
|
2202
|
+
50: (db) => {
|
|
2203
|
+
console.log('Running migration to schema version 50: Add host to pr_metadata and github_pr_cache...');
|
|
2204
|
+
if (tableExists(db, 'pr_metadata') && !columnExists(db, 'pr_metadata', 'host')) {
|
|
2205
|
+
db.exec('ALTER TABLE pr_metadata ADD COLUMN host TEXT');
|
|
2206
|
+
console.log(' Added host column to pr_metadata');
|
|
2207
|
+
} else {
|
|
2208
|
+
console.log(' Column host already exists on pr_metadata (or table absent)');
|
|
2209
|
+
}
|
|
2210
|
+
if (tableExists(db, 'github_pr_cache') && !columnExists(db, 'github_pr_cache', 'host')) {
|
|
2211
|
+
db.exec('ALTER TABLE github_pr_cache ADD COLUMN host TEXT');
|
|
2212
|
+
console.log(' Added host column to github_pr_cache');
|
|
2213
|
+
} else {
|
|
2214
|
+
console.log(' Column host already exists on github_pr_cache (or table absent)');
|
|
2215
|
+
}
|
|
2216
|
+
console.log('Migration to schema version 50 complete');
|
|
2217
|
+
},
|
|
2218
|
+
|
|
2219
|
+
// Migration to version 51: widen idx_github_pr_cache_unique to include host.
|
|
2220
|
+
// The collections refresh inserts github rows (host NULL) and alt-host rows
|
|
2221
|
+
// (host = api_host URL) under the same collection in one transaction. For a
|
|
2222
|
+
// dual-host repo whose PRs are numbered independently per system, a shared
|
|
2223
|
+
// (collection, owner, repo, number) would violate the old 4-column unique
|
|
2224
|
+
// index — and the single throw rolls back the DELETE plus BOTH insert loops,
|
|
2225
|
+
// so the user sees zero PRs for that collection. Adding host to the key lets
|
|
2226
|
+
// the two systems' same-numbered PRs coexist. The new index is strictly wider
|
|
2227
|
+
// than the old one, so existing data can never violate it. Idempotent:
|
|
2228
|
+
// DROP IF EXISTS then CREATE IF NOT EXISTS yields the same shape on re-run and
|
|
2229
|
+
// is safe on both a v50 DB (old 4-column index) and a fresh DB.
|
|
2230
|
+
51: (db) => {
|
|
2231
|
+
console.log('Running migration to schema version 51: widen idx_github_pr_cache_unique to include host...');
|
|
2232
|
+
if (tableExists(db, 'github_pr_cache')) {
|
|
2233
|
+
db.exec('DROP INDEX IF EXISTS idx_github_pr_cache_unique');
|
|
2234
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_github_pr_cache_unique ON github_pr_cache(collection, owner, repo, number, host)');
|
|
2235
|
+
console.log(' Rebuilt idx_github_pr_cache_unique with host in the key');
|
|
2236
|
+
} else {
|
|
2237
|
+
console.log(' Table github_pr_cache absent; nothing to reindex');
|
|
2238
|
+
}
|
|
2239
|
+
console.log('Migration to schema version 51 complete');
|
|
2162
2240
|
}
|
|
2163
2241
|
};
|
|
2164
2242
|
|
|
@@ -4974,6 +5052,52 @@ class PRMetadataRepository {
|
|
|
4974
5052
|
};
|
|
4975
5053
|
}
|
|
4976
5054
|
|
|
5055
|
+
/**
|
|
5056
|
+
* Get the stored host binding for a PR.
|
|
5057
|
+
*
|
|
5058
|
+
* Distinguishes "no row" from "row says github". Used by per-PR host
|
|
5059
|
+
* resolution (dual GitHub + alt-host repos) to decide which system a PR
|
|
5060
|
+
* lives on before building a binding.
|
|
5061
|
+
*
|
|
5062
|
+
* @param {string} repository - Repository in owner/repo format
|
|
5063
|
+
* @param {number} prNumber - Pull request number
|
|
5064
|
+
* @returns {Promise<string|null|undefined>} The stored api_host URL string,
|
|
5065
|
+
* `null` when the row exists but host is unset (github.com), or `undefined`
|
|
5066
|
+
* when no pr_metadata row exists for the PR.
|
|
5067
|
+
*/
|
|
5068
|
+
async getPRHost(repository, prNumber) {
|
|
5069
|
+
const row = await queryOne(this.db, `
|
|
5070
|
+
SELECT host FROM pr_metadata
|
|
5071
|
+
WHERE pr_number = ? AND repository = ? COLLATE NOCASE
|
|
5072
|
+
`, [prNumber, repository]);
|
|
5073
|
+
|
|
5074
|
+
if (!row) return undefined;
|
|
5075
|
+
return row.host;
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
/**
|
|
5079
|
+
* Update ONLY the host binding for a PR's metadata row.
|
|
5080
|
+
*
|
|
5081
|
+
* Used to persist an explicit host correction (e.g. a `?host` query param on
|
|
5082
|
+
* the /pr fast path) without re-running full setup. Leaves every other column
|
|
5083
|
+
* untouched.
|
|
5084
|
+
*
|
|
5085
|
+
* @param {string} repository - Repository in owner/repo format
|
|
5086
|
+
* @param {number} prNumber - Pull request number
|
|
5087
|
+
* @param {string|null} host - api_host URL string, or null for github.com
|
|
5088
|
+
* @returns {Promise<boolean>} True if a matching row was updated, false when
|
|
5089
|
+
* no pr_metadata row exists for the PR.
|
|
5090
|
+
*/
|
|
5091
|
+
async updatePRHost(repository, prNumber, host) {
|
|
5092
|
+
const result = await run(this.db, `
|
|
5093
|
+
UPDATE pr_metadata
|
|
5094
|
+
SET host = ?, updated_at = CURRENT_TIMESTAMP
|
|
5095
|
+
WHERE pr_number = ? AND repository = ? COLLATE NOCASE
|
|
5096
|
+
`, [host, prNumber, repository]);
|
|
5097
|
+
|
|
5098
|
+
return result.changes > 0;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
4977
5101
|
/**
|
|
4978
5102
|
* Update the last_ai_run_id for a PR metadata record
|
|
4979
5103
|
* @param {number} id - PR metadata record ID
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
resolveHostBinding,
|
|
25
25
|
resolveBindingRepositoryFromPR
|
|
26
26
|
} = require('../config');
|
|
27
|
+
const { storedHostToOption } = require('../utils/host-resolution');
|
|
27
28
|
|
|
28
29
|
const name = 'github';
|
|
29
30
|
|
|
@@ -63,14 +64,26 @@ const credentialEnvVar = 'GITHUB_TOKEN';
|
|
|
63
64
|
* diff-relative `position` field, so `mapComment` must anchor by `line`
|
|
64
65
|
* instead — see its docstring.
|
|
65
66
|
*
|
|
67
|
+
* Per-PR host: for a DUAL repo (github + alt-host) the binding depends on which
|
|
68
|
+
* system the PR actually lives on. The caller passes the PR's stored
|
|
69
|
+
* `pr_metadata.host` via `options.storedHost` (undefined | null | api_host URL);
|
|
70
|
+
* this method applies the legacy-NULL convention (`storedHostToOption`) and
|
|
71
|
+
* threads the resulting `{ host }` into `resolveHostBinding`, so a dual repo's
|
|
72
|
+
* alt-hosted PR resolves to the alt binding (and the line-based anchoring path)
|
|
73
|
+
* instead of always hitting api.github.com two-arg. When `options.storedHost`
|
|
74
|
+
* is omitted the two-arg ambiguity rule applies, unchanged.
|
|
75
|
+
*
|
|
66
76
|
* @param {Object} config - Server config (see `loadConfig()`)
|
|
67
77
|
* @param {string} [repository] - "owner/repo" identifier for binding-aware resolution
|
|
68
78
|
* @param {Object} [_deps] - Test overrides for
|
|
69
79
|
* { GitHubClient, getGitHubToken, resolveHostBinding, resolveBindingRepositoryFromPR }
|
|
80
|
+
* @param {Object} [options] - Runtime resolution inputs
|
|
81
|
+
* @param {string|null|undefined} [options.storedHost] - The PR's stored
|
|
82
|
+
* pr_metadata.host (undefined = no row / unknown, null = github.com, URL = alt host)
|
|
70
83
|
* @returns {{ client: Object, isAltHost: boolean }}
|
|
71
84
|
* @throws {GitHubApiError} with status 401 when no token is configured
|
|
72
85
|
*/
|
|
73
|
-
function resolveCredentials(config, repository, _deps) {
|
|
86
|
+
function resolveCredentials(config, repository, _deps, options = {}) {
|
|
74
87
|
const deps = {
|
|
75
88
|
GitHubClient,
|
|
76
89
|
getGitHubToken,
|
|
@@ -82,10 +95,12 @@ function resolveCredentials(config, repository, _deps) {
|
|
|
82
95
|
|
|
83
96
|
if (repository) {
|
|
84
97
|
// Binding-aware path. Mirrors resolveBindingForRequest in routes/pr.js:
|
|
85
|
-
// resolve the PR identity to a binding key, then to a host binding
|
|
98
|
+
// resolve the PR identity to a binding key, then to a host binding —
|
|
99
|
+
// pinned to the PR's stored host for dual repos.
|
|
86
100
|
const [owner, repo] = String(repository).split('/');
|
|
87
101
|
const bindingRepository = deps.resolveBindingRepositoryFromPR(owner, repo, safeConfig);
|
|
88
|
-
const
|
|
102
|
+
const hostOption = storedHostToOption(safeConfig, bindingRepository, options.storedHost);
|
|
103
|
+
const binding = deps.resolveHostBinding(bindingRepository, safeConfig, hostOption || {});
|
|
89
104
|
if (!binding || !binding.token) {
|
|
90
105
|
throw new GitHubApiError(
|
|
91
106
|
`GitHub token not configured for ${repository}. Set ${credentialEnvVar}, add github_token to ~/.pair-review/config.json, or configure repos["${bindingRepository}"].token / token_command (required for alt-host repos)`,
|
package/src/github/client.js
CHANGED
|
@@ -1137,6 +1137,43 @@ class GitHubClient {
|
|
|
1137
1137
|
});
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* List open pull requests for a single repository via the REST API.
|
|
1142
|
+
*
|
|
1143
|
+
* Used by the dashboard collections sweep against alt-hosts, which speak a
|
|
1144
|
+
* REST subset and generally have no Search API. The returned rows carry the
|
|
1145
|
+
* classification fields (`author`, `requested_reviewers`, `requested_teams`)
|
|
1146
|
+
* so the caller can bucket each PR into a collection locally, plus the same
|
|
1147
|
+
* display fields `searchPullRequests` returns so both paths cache uniformly.
|
|
1148
|
+
*
|
|
1149
|
+
* @param {string} owner - Repository owner
|
|
1150
|
+
* @param {string} repo - Repository name
|
|
1151
|
+
* @returns {Promise<Array<{owner: string, repo: string, number: number, title: string, author: string|null, updated_at: string, html_url: string, state: string, requested_reviewers: string[], requested_teams: string[]}>>}
|
|
1152
|
+
*/
|
|
1153
|
+
async listOpenPullRequests(owner, repo) {
|
|
1154
|
+
const pulls = await this.octokit.paginate(
|
|
1155
|
+
this.octokit.rest.pulls.list,
|
|
1156
|
+
{ owner, repo, state: 'open', per_page: 100 }
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
return pulls.map(pr => ({
|
|
1160
|
+
owner,
|
|
1161
|
+
repo,
|
|
1162
|
+
number: pr.number,
|
|
1163
|
+
title: pr.title,
|
|
1164
|
+
author: pr.user?.login || null,
|
|
1165
|
+
updated_at: pr.updated_at,
|
|
1166
|
+
html_url: pr.html_url,
|
|
1167
|
+
state: pr.state,
|
|
1168
|
+
requested_reviewers: Array.isArray(pr.requested_reviewers)
|
|
1169
|
+
? pr.requested_reviewers.map(r => r && r.login).filter(Boolean)
|
|
1170
|
+
: [],
|
|
1171
|
+
requested_teams: Array.isArray(pr.requested_teams)
|
|
1172
|
+
? pr.requested_teams.map(t => t && t.slug).filter(Boolean)
|
|
1173
|
+
: []
|
|
1174
|
+
}));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1140
1177
|
/**
|
|
1141
1178
|
* Get the authenticated user's information.
|
|
1142
1179
|
* @returns {Promise<{login: string, name: string, avatar_url: string}>}
|
package/src/github/parser.js
CHANGED
|
@@ -90,11 +90,20 @@ class PRArgumentParser {
|
|
|
90
90
|
if (!owner || !repo || typeof number !== 'number' || isNaN(number) || number <= 0) {
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
|
+
// Carry the matched repo's `api_host` so downstream setup binds the PR to
|
|
94
|
+
// the alternate host without probing. A `url_pattern` match on a repo with
|
|
95
|
+
// no `api_host` (a plain github.com entry) resolves to `host: null`.
|
|
96
|
+
const matchedApiHost = (match.repoConfig
|
|
97
|
+
&& typeof match.repoConfig.api_host === 'string'
|
|
98
|
+
&& match.repoConfig.api_host)
|
|
99
|
+
? match.repoConfig.api_host
|
|
100
|
+
: null;
|
|
93
101
|
return {
|
|
94
102
|
owner,
|
|
95
103
|
repo,
|
|
96
104
|
number,
|
|
97
|
-
bindingRepository: match.bindingRepository
|
|
105
|
+
bindingRepository: match.bindingRepository,
|
|
106
|
+
host: matchedApiHost
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
|
|
@@ -114,16 +123,30 @@ class PRArgumentParser {
|
|
|
114
123
|
return null;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
// Clean up the URL - trim whitespace
|
|
127
|
+
const trimmedUrl = url.trim();
|
|
128
|
+
|
|
117
129
|
// Try config-driven URL pattern matching first. This handles
|
|
118
130
|
// alternate-host URLs and lets host-specific repos override the
|
|
119
131
|
// built-in github.com path if they choose.
|
|
120
|
-
const configMatch = this._matchUrlPatternFromConfig(
|
|
132
|
+
const configMatch = this._matchUrlPatternFromConfig(trimmedUrl);
|
|
121
133
|
if (configMatch) {
|
|
122
|
-
|
|
134
|
+
// INVARIANT: an `api_host`-bearing `url_pattern` must NEVER bind a
|
|
135
|
+
// canonical github.com / Graphite URL. An over-broad or unanchored pattern
|
|
136
|
+
// can match one and pre-pin it to the alt host (an explicit host bypasses
|
|
137
|
+
// the setup probe → a silent, durable wrong binding). When the config
|
|
138
|
+
// match carries an alt host but the URL is a canonical github/graphite URL,
|
|
139
|
+
// discard the match and fall through to the built-in parsers (host: null).
|
|
140
|
+
const sansProtocol = trimmedUrl.replace(/^https?:\/\//i, '');
|
|
141
|
+
const isCanonicalHostUrl = sansProtocol.startsWith('github.com/')
|
|
142
|
+
|| sansProtocol.startsWith('app.graphite.dev/')
|
|
143
|
+
|| sansProtocol.startsWith('app.graphite.com/');
|
|
144
|
+
if (!(configMatch.host && isCanonicalHostUrl)) {
|
|
145
|
+
return configMatch;
|
|
146
|
+
}
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
|
|
126
|
-
let normalizedUrl = url.trim();
|
|
149
|
+
let normalizedUrl = trimmedUrl;
|
|
127
150
|
|
|
128
151
|
// Add https:// if no protocol is present
|
|
129
152
|
if (normalizedUrl.startsWith('github.com')) {
|
|
@@ -222,7 +245,9 @@ class PRArgumentParser {
|
|
|
222
245
|
* @param {string} repo - Repository name
|
|
223
246
|
* @param {string} numberStr - PR number as string
|
|
224
247
|
* @param {string} source - Source name for error messages ('GitHub' or 'Graphite')
|
|
225
|
-
* @returns {Object} Validated PR info { owner, repo, number }
|
|
248
|
+
* @returns {Object} Validated PR info `{ owner, repo, number, host? }`. `host`
|
|
249
|
+
* is `null` for github.com/Graphite URLs (definitively github); it is OMITTED
|
|
250
|
+
* (undefined) for the pair-review:// scheme, which carries no host in its path.
|
|
226
251
|
* @private
|
|
227
252
|
*/
|
|
228
253
|
_createPRInfo(owner, repo, numberStr, source) {
|
|
@@ -237,7 +262,16 @@ class PRArgumentParser {
|
|
|
237
262
|
throw new Error(`Invalid ${source} URL format. Expected: ${exampleUrl}`);
|
|
238
263
|
}
|
|
239
264
|
|
|
240
|
-
|
|
265
|
+
// A github.com or Graphite URL unambiguously identifies a github.com PR, so
|
|
266
|
+
// the host is explicitly null (not "unknown"). The pair-review:// scheme
|
|
267
|
+
// carries NO host in its path and is not github-only, so leave `host`
|
|
268
|
+
// undefined — it flows into the probe/derive path like a bare number,
|
|
269
|
+
// instead of self-healing (dual repo) or throwing (exclusive alt) on github.
|
|
270
|
+
const result = { owner, repo, number };
|
|
271
|
+
if (source !== 'pair-review://') {
|
|
272
|
+
result.host = null;
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
/**
|
|
@@ -93,7 +93,7 @@ async function resolveCliInstructions(flags) {
|
|
|
93
93
|
* @param {Object} params
|
|
94
94
|
* @param {Object} params.db - Database instance
|
|
95
95
|
* @param {Object} params.config - Loaded global config
|
|
96
|
-
* @param {Object} params.flags - Parsed CLI flags (council/model/instructions)
|
|
96
|
+
* @param {Object} params.flags - Parsed CLI flags (council/provider/model/instructions)
|
|
97
97
|
* @param {string} params.repository - owner/repo (for repo-default resolution)
|
|
98
98
|
* @returns {Promise<Object|null>} The analysisConfig object, or null when no instructions.
|
|
99
99
|
*/
|
|
@@ -104,7 +104,7 @@ async function buildInteractiveAnalysisConfig({ db, config, flags, repository })
|
|
|
104
104
|
const reviewConfig = await resolveReviewConfig(
|
|
105
105
|
db,
|
|
106
106
|
repository,
|
|
107
|
-
{ council: flags.council, model: flags.model },
|
|
107
|
+
{ council: flags.council, provider: flags.provider, model: flags.model },
|
|
108
108
|
config
|
|
109
109
|
);
|
|
110
110
|
|
package/src/links/repo-links.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const { getRepoConfig } = require('../config');
|
|
34
|
+
const { isDualHostRepoConfig } = require('../utils/host-resolution');
|
|
34
35
|
const logger = require('../utils/logger');
|
|
35
36
|
|
|
36
37
|
// Whitelist of allowed `{placeholder}` names. Anything else in the template
|
|
@@ -158,13 +159,28 @@ function sanitizeSvgIcon(svg) {
|
|
|
158
159
|
* malformed), a warning is logged and `icon` becomes `null` — the link
|
|
159
160
|
* is still rendered, just without a custom icon.
|
|
160
161
|
*
|
|
162
|
+
* **Per-PR host awareness (dual-host repos only).** `host` is the PR's
|
|
163
|
+
* resolved host: `null` = github.com, an `api_host` URL string = the alt
|
|
164
|
+
* host, `undefined` = unknown / not applicable. It only affects a
|
|
165
|
+
* *dual-host* repo (`api_host` + `exclusive: false`):
|
|
166
|
+
* - `host === null` (github-hosted PR) → keep the GitHub/Graphite links
|
|
167
|
+
* (subject to any explicit `links.github/graphite: false`) and hide the
|
|
168
|
+
* alt-host external link.
|
|
169
|
+
* - `host === '<url>'` (alt-hosted PR) → keep the external link and hide
|
|
170
|
+
* the GitHub/Graphite links.
|
|
171
|
+
* Exclusive alt-host repos and plain github repos ignore `host` entirely, so
|
|
172
|
+
* existing two-arg callers (and `host === undefined`) render byte-identically
|
|
173
|
+
* to before this parameter existed.
|
|
174
|
+
*
|
|
161
175
|
* @param {Object} config
|
|
162
176
|
* @param {string} repository Canonical `owner/repo` identifier
|
|
177
|
+
* @param {string|null} [host] PR's resolved host: null=github, url=alt,
|
|
178
|
+
* undefined=unknown/not applicable
|
|
163
179
|
* @returns {{ external: { label: string, url_template: string, icon: string|null }|null,
|
|
164
180
|
* github: boolean,
|
|
165
181
|
* graphite: boolean }}
|
|
166
182
|
*/
|
|
167
|
-
function resolveRepoLinks(config, repository) {
|
|
183
|
+
function resolveRepoLinks(config, repository, host = undefined) {
|
|
168
184
|
const result = { external: null, github: true, graphite: true };
|
|
169
185
|
if (!config || !repository) return result;
|
|
170
186
|
|
|
@@ -172,33 +188,47 @@ function resolveRepoLinks(config, repository) {
|
|
|
172
188
|
if (!repoConfig || typeof repoConfig !== 'object') return result;
|
|
173
189
|
|
|
174
190
|
const links = repoConfig.links;
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
if (links && typeof links === 'object') {
|
|
192
|
+
if (links.github === false) result.github = false;
|
|
193
|
+
if (links.graphite === false) result.graphite = false;
|
|
194
|
+
|
|
195
|
+
const ext = links.external;
|
|
196
|
+
if (ext && typeof ext === 'object'
|
|
197
|
+
&& typeof ext.label === 'string' && ext.label
|
|
198
|
+
&& typeof ext.url_template === 'string'
|
|
199
|
+
&& ext.url_template.startsWith('https://')) {
|
|
200
|
+
let icon = null;
|
|
201
|
+
if (ext.icon !== undefined && ext.icon !== null && ext.icon !== '') {
|
|
202
|
+
icon = sanitizeSvgIcon(ext.icon);
|
|
203
|
+
if (icon === null) {
|
|
204
|
+
logger.warn(
|
|
205
|
+
`Dropping links.external.icon for "${repository}" — failed sanitisation.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
192
208
|
}
|
|
209
|
+
result.external = {
|
|
210
|
+
// Optional host display name (e.g. "Meteorite"). When absent, the
|
|
211
|
+
// field is null and consumers fall back to "GitHub" via resolveHostName.
|
|
212
|
+
name: (typeof ext.name === 'string' && ext.name) ? ext.name : null,
|
|
213
|
+
label: ext.label,
|
|
214
|
+
url_template: ext.url_template,
|
|
215
|
+
icon
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Per-PR host awareness for dual-host repos. A `host` of `undefined`
|
|
221
|
+
// (omitted arg, unknown host) leaves the repo-level result untouched, so
|
|
222
|
+
// non-dual repos and legacy two-arg callers are unaffected.
|
|
223
|
+
if (host !== undefined && isDualHostRepoConfig(repoConfig)) {
|
|
224
|
+
if (host === null) {
|
|
225
|
+
// github-hosted PR: the alt-host external link does not apply.
|
|
226
|
+
result.external = null;
|
|
227
|
+
} else {
|
|
228
|
+
// alt-hosted PR: hide the GitHub/Graphite links, keep the external one.
|
|
229
|
+
result.github = false;
|
|
230
|
+
result.graphite = false;
|
|
193
231
|
}
|
|
194
|
-
result.external = {
|
|
195
|
-
// Optional host display name (e.g. "Meteorite"). When absent, the
|
|
196
|
-
// field is null and consumers fall back to "GitHub" via resolveHostName.
|
|
197
|
-
name: (typeof ext.name === 'string' && ext.name) ? ext.name : null,
|
|
198
|
-
label: ext.label,
|
|
199
|
-
url_template: ext.url_template,
|
|
200
|
-
icon
|
|
201
|
-
};
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
return result;
|
|
@@ -212,12 +242,20 @@ function resolveRepoLinks(config, repository) {
|
|
|
212
242
|
* `"GitHub"`. This is the server-side counterpart to the frontend
|
|
213
243
|
* `window.RepoLinks.hostName()` accessor.
|
|
214
244
|
*
|
|
245
|
+
* Host-aware for dual-host repos: a github-hosted PR (`host === null`)
|
|
246
|
+
* reports "GitHub" even when an external name is configured, because
|
|
247
|
+
* `resolveRepoLinks` clears the external link for that host. See
|
|
248
|
+
* `resolveRepoLinks` for the `host` semantics. Non-dual repos and two-arg
|
|
249
|
+
* callers behave exactly as before.
|
|
250
|
+
*
|
|
215
251
|
* @param {Object} config
|
|
216
252
|
* @param {string} repository Canonical `owner/repo` identifier
|
|
253
|
+
* @param {string|null} [host] PR's resolved host: null=github, url=alt,
|
|
254
|
+
* undefined=unknown/not applicable
|
|
217
255
|
* @returns {string} The configured host name, or "GitHub" by default
|
|
218
256
|
*/
|
|
219
|
-
function resolveHostName(config, repository) {
|
|
220
|
-
const links = resolveRepoLinks(config, repository);
|
|
257
|
+
function resolveHostName(config, repository, host = undefined) {
|
|
258
|
+
const links = resolveRepoLinks(config, repository, host);
|
|
221
259
|
return (links.external && links.external.name) ? links.external.name : 'GitHub';
|
|
222
260
|
}
|
|
223
261
|
|