pierre-review 0.1.23 → 0.1.25
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/dist/api/routes/insights.js +13 -1
- package/dist/api/routes/me.js +7 -2
- package/dist/api/routes/repos.js +96 -8
- package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
- package/dist/db/migrations/meta/_journal.json +7 -0
- package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
- package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
- package/dist/db/migrations-pg/meta/_journal.json +7 -0
- package/dist/db/queries.js +505 -7
- package/dist/db/schema.pg.js +10 -1
- package/dist/db/schema.sqlite.js +17 -6
- package/dist/github/queries.js +14 -3
- package/package.json +1 -1
- package/public/assets/index-5W0TZ6y7.css +10 -0
- package/public/assets/index-Ch3iNqCy.js +1371 -0
- package/public/index.html +2 -2
- package/public/assets/index-CbNTBha6.js +0 -1371
- package/public/assets/index-CjBmEO6s.css +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getInsights } from '../../db/queries.js';
|
|
1
|
+
import { getInsights, getRepoAnalytics } from '../../db/queries.js';
|
|
2
2
|
import { accountIdOf } from '../plugins/auth.js';
|
|
3
3
|
function parseIntList(raw) {
|
|
4
4
|
if (!raw)
|
|
@@ -19,5 +19,17 @@ export async function insightsRoutes(app) {
|
|
|
19
19
|
repoIds: parseIntList(q.repoIds),
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
+
// Heavier per-repo analytics for the drill-down chart panel — loaded on demand.
|
|
23
|
+
// Ownership-scoped: a repo not owned by the account 404s.
|
|
24
|
+
app.get('/api/insights/:repoId/analytics', async (req, reply) => {
|
|
25
|
+
const { repoId } = req.params;
|
|
26
|
+
const id = Number.parseInt(repoId, 10);
|
|
27
|
+
const data = Number.isFinite(id)
|
|
28
|
+
? await getRepoAnalytics(accountIdOf(req), id)
|
|
29
|
+
: null;
|
|
30
|
+
if (!data)
|
|
31
|
+
return reply.code(404).send({ error: 'repo not found' });
|
|
32
|
+
return data;
|
|
33
|
+
});
|
|
22
34
|
}
|
|
23
35
|
//# sourceMappingURL=insights.js.map
|
package/dist/api/routes/me.js
CHANGED
|
@@ -8,7 +8,10 @@ const dismissSchema = {
|
|
|
8
8
|
required: ['kind', 'refId'],
|
|
9
9
|
additionalProperties: false,
|
|
10
10
|
properties: {
|
|
11
|
-
kind: {
|
|
11
|
+
kind: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
enum: ['review_request', 'thread', 'watched_repo_pr', 'claude_review'],
|
|
14
|
+
},
|
|
12
15
|
refId: { type: 'integer' },
|
|
13
16
|
},
|
|
14
17
|
},
|
|
@@ -23,6 +26,7 @@ export async function meRoutes(app) {
|
|
|
23
26
|
awaitingReview: myTurn.awaitingReview.length,
|
|
24
27
|
yourPrsActivity: myTurn.yourPrs.length,
|
|
25
28
|
threadsAwaiting: myTurn.threadsAwaiting.length,
|
|
29
|
+
watchedRepoPrs: myTurn.watchedRepoPrs.length,
|
|
26
30
|
claudeReviewsToAction: myTurn.claudeReviewsToAction.length,
|
|
27
31
|
},
|
|
28
32
|
claudeReviewEnabled: config.claudeReviewEnabled,
|
|
@@ -35,7 +39,8 @@ export async function meRoutes(app) {
|
|
|
35
39
|
await dismissMyTurn(accountIdOf(req), kind, refId);
|
|
36
40
|
return { status: 'ok' };
|
|
37
41
|
});
|
|
38
|
-
// The "Done" tab: entries dismissed in the past 90 days (review_request + thread
|
|
42
|
+
// The "Done" tab: entries dismissed in the past 90 days (review_request + thread
|
|
43
|
+
// + claude_review).
|
|
39
44
|
app.get('/api/my-turn/done', async (req) => getCompletedDismissals(accountIdOf(req), 90));
|
|
40
45
|
// Un-dismiss: move a completed entry back to the inbox.
|
|
41
46
|
app.post('/api/my-turn/undismiss', { schema: dismissSchema }, async (req) => {
|
package/dist/api/routes/repos.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getGraphqlClientFor } from '../../github/client.js';
|
|
2
2
|
import { getAccessToken } from '../../auth/account.js';
|
|
3
|
-
import { REPO_ID_QUERY, REPO_SEARCH_QUERY, } from '../../github/queries.js';
|
|
3
|
+
import { OWNER_TYPE_QUERY, REPO_ID_QUERY, REPO_SEARCH_QUERY, } from '../../github/queries.js';
|
|
4
4
|
import { upsertRepo } from '../../sync/upsert.js';
|
|
5
5
|
import { getSyncStatus, isSyncRunning, requestSyncCancel, runSyncForRepo, waitForSyncToStop, } from '../../sync/sync-manager.js';
|
|
6
|
-
import { deleteRepo, getRepo, getWatchedRepoNodeIds, listRepos, } from '../../db/queries.js';
|
|
6
|
+
import { deleteRepo, getRepo, getWatchedRepoNodeIds, listRepos, setRepoInboxWatch, } from '../../db/queries.js';
|
|
7
7
|
import { accountIdOf } from '../plugins/auth.js';
|
|
8
8
|
// Local copy of the shared MAX_REPOS_PER_ACCOUNT value. `@pierre-review/shared` is
|
|
9
9
|
// a types-only package (not shipped in the published tarball), so the backend must
|
|
@@ -17,6 +17,9 @@ const createRepoSchema = {
|
|
|
17
17
|
properties: {
|
|
18
18
|
owner: { type: 'string', minLength: 1 },
|
|
19
19
|
name: { type: 'string', minLength: 1 },
|
|
20
|
+
// When true, also Watch the repo for the inbox on add (the picker passes
|
|
21
|
+
// true for "yours" repos). Optional; defaults to not-watched.
|
|
22
|
+
watch: { type: 'boolean' },
|
|
20
23
|
},
|
|
21
24
|
},
|
|
22
25
|
};
|
|
@@ -27,6 +30,15 @@ const idParamSchema = {
|
|
|
27
30
|
properties: { id: { type: 'integer' } },
|
|
28
31
|
},
|
|
29
32
|
};
|
|
33
|
+
const watchSchema = {
|
|
34
|
+
...idParamSchema,
|
|
35
|
+
body: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
required: ['inboxWatch'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: { inboxWatch: { type: 'boolean' } },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
30
42
|
const syncSchema = {
|
|
31
43
|
...idParamSchema,
|
|
32
44
|
querystring: {
|
|
@@ -60,13 +72,50 @@ export async function repoRoutes(app) {
|
|
|
60
72
|
return { error: 'BadRequest', message: 'Search query must not be empty' };
|
|
61
73
|
}
|
|
62
74
|
const accountId = accountIdOf(req);
|
|
75
|
+
// Translate the user term into a literal GitHub search query. An `owner/...`
|
|
76
|
+
// prefix scopes results to that owner (org:/user: qualifier — resolved by type);
|
|
77
|
+
// the remainder (or the whole term) is matched against the repo NAME only
|
|
78
|
+
// (`in:name`), and `needle` drives the literal re-rank below.
|
|
79
|
+
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
80
|
+
const slash = term.indexOf('/');
|
|
81
|
+
const owner = slash >= 0 ? term.slice(0, slash).trim() : '';
|
|
82
|
+
const rest = slash >= 0 ? term.slice(slash + 1).trim() : term;
|
|
83
|
+
let searchQuery;
|
|
84
|
+
let needle;
|
|
85
|
+
if (owner) {
|
|
86
|
+
// `owner/...` → scope to that owner. Note a stray leading slash ("/foo") leaves
|
|
87
|
+
// owner empty and falls through to the plain branch (no malformed `user:` query).
|
|
88
|
+
needle = rest;
|
|
89
|
+
let qualifier = `user:${owner}`;
|
|
90
|
+
try {
|
|
91
|
+
const ownerResp = await client(OWNER_TYPE_QUERY, {
|
|
92
|
+
login: owner,
|
|
93
|
+
});
|
|
94
|
+
const kind = ownerResp.repositoryOwner?.__typename ?? null;
|
|
95
|
+
if (kind == null) {
|
|
96
|
+
// Owner login doesn't exist → no possible matches.
|
|
97
|
+
return { results: [], hasNextPage: false, cursor: null };
|
|
98
|
+
}
|
|
99
|
+
qualifier = kind === 'Organization' ? `org:${owner}` : `user:${owner}`;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// Lookup failed (network / rate limit) — default to user: scoping and run the
|
|
103
|
+
// search anyway. Logged so a wrong-scope result is diagnosable.
|
|
104
|
+
req.log.warn({ err, owner }, 'repo search: owner-type lookup failed; defaulting to user: scope');
|
|
105
|
+
}
|
|
106
|
+
searchQuery = rest ? `${qualifier} ${rest} in:name` : qualifier;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Plain term (or a stray leading slash) → literal repo-name search.
|
|
110
|
+
needle = rest;
|
|
111
|
+
searchQuery = rest ? `${rest} in:name` : term;
|
|
112
|
+
}
|
|
63
113
|
let resp;
|
|
64
114
|
try {
|
|
65
|
-
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
66
115
|
// NB: the GraphQL variable is `searchQuery`, not `query` — @octokit/graphql
|
|
67
116
|
// reserves `query` for the document body and rejects it as a variable name.
|
|
68
117
|
resp = await client(REPO_SEARCH_QUERY, {
|
|
69
|
-
searchQuery
|
|
118
|
+
searchQuery,
|
|
70
119
|
first: limit,
|
|
71
120
|
cursor: cursor ?? null,
|
|
72
121
|
});
|
|
@@ -103,9 +152,31 @@ export async function repoRoutes(app) {
|
|
|
103
152
|
orgLogins.has(ownerLogin.toLowerCase()),
|
|
104
153
|
};
|
|
105
154
|
});
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
155
|
+
// Re-rank for literal matching: closest name match first (exact < prefix <
|
|
156
|
+
// substring < other), then your own/org repos, then stars. Array.prototype.sort
|
|
157
|
+
// is stable (Node ≥ 12), so GitHub's best-match order breaks remaining ties.
|
|
158
|
+
const lc = needle.toLowerCase();
|
|
159
|
+
const nameTier = (name) => {
|
|
160
|
+
if (!lc)
|
|
161
|
+
return 0;
|
|
162
|
+
const n = name.toLowerCase();
|
|
163
|
+
if (n === lc)
|
|
164
|
+
return 0;
|
|
165
|
+
if (n.startsWith(lc))
|
|
166
|
+
return 1;
|
|
167
|
+
if (n.includes(lc))
|
|
168
|
+
return 2;
|
|
169
|
+
return 3;
|
|
170
|
+
};
|
|
171
|
+
results.sort((a, b) => {
|
|
172
|
+
const t = nameTier(a.name) - nameTier(b.name);
|
|
173
|
+
if (t !== 0)
|
|
174
|
+
return t;
|
|
175
|
+
const own = Number(b.isOwnedOrMember) - Number(a.isOwnedOrMember);
|
|
176
|
+
if (own !== 0)
|
|
177
|
+
return own;
|
|
178
|
+
return b.stargazerCount - a.stargazerCount;
|
|
179
|
+
});
|
|
109
180
|
const body = {
|
|
110
181
|
results,
|
|
111
182
|
hasNextPage: resp.search.pageInfo.hasNextPage,
|
|
@@ -114,7 +185,7 @@ export async function repoRoutes(app) {
|
|
|
114
185
|
return body;
|
|
115
186
|
});
|
|
116
187
|
app.post('/api/repos', { schema: createRepoSchema }, async (req, reply) => {
|
|
117
|
-
const { owner, name } = req.body;
|
|
188
|
+
const { owner, name, watch } = req.body;
|
|
118
189
|
const accountId = accountIdOf(req);
|
|
119
190
|
let resp;
|
|
120
191
|
try {
|
|
@@ -150,11 +221,28 @@ export async function repoRoutes(app) {
|
|
|
150
221
|
const canonOwner = resp.repository.owner.login;
|
|
151
222
|
const canonName = resp.repository.name;
|
|
152
223
|
const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId);
|
|
224
|
+
// Auto-watch "yours" repos for the inbox. Idempotent on re-add and preserves an
|
|
225
|
+
// existing watch-start (setRepoInboxWatch only stamps the start when unset).
|
|
226
|
+
if (watch === true)
|
|
227
|
+
await setRepoInboxWatch(accountId, repoId, true);
|
|
153
228
|
// Kick off the initial backfill in the background.
|
|
154
229
|
runSyncForRepo(repoId, app.log, { background: true });
|
|
155
230
|
reply.status(201);
|
|
156
231
|
return getRepo(repoId, accountId);
|
|
157
232
|
});
|
|
233
|
+
// Toggle "Watch for inbox" on a repo. Inbox-only: it does not affect timeline
|
|
234
|
+
// visibility or syncing. Ownership-scoped → 404 for a repo this account doesn't own.
|
|
235
|
+
app.patch('/api/repos/:id', { schema: watchSchema }, async (req, reply) => {
|
|
236
|
+
const { id } = req.params;
|
|
237
|
+
const { inboxWatch } = req.body;
|
|
238
|
+
const accountId = accountIdOf(req);
|
|
239
|
+
const ok = await setRepoInboxWatch(accountId, id, inboxWatch);
|
|
240
|
+
if (!ok) {
|
|
241
|
+
reply.status(404);
|
|
242
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
243
|
+
}
|
|
244
|
+
return getRepo(id, accountId);
|
|
245
|
+
});
|
|
158
246
|
app.delete('/api/repos/:id', { schema: idParamSchema }, async (req, reply) => {
|
|
159
247
|
const { id } = req.params;
|
|
160
248
|
const accountId = accountIdOf(req);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- "Watch for inbox" per repo (additive). When `inbox_watch` is true, new open PRs by
|
|
2
|
+
-- others (opened on/after `inbox_watch_started_at`) surface in the My Turn inbox —
|
|
3
|
+
-- independent of timeline visibility and of removing the repo. `inbox_watch_started_at`
|
|
4
|
+
-- is set on the first watch and preserved across unwatch, so re-watching restores the
|
|
5
|
+
-- same window. Existing repos default to NOT watched (opt-in); new "yours" repos are
|
|
6
|
+
-- watched on add by the picker. Postgres baseline is regenerated separately via
|
|
7
|
+
-- db:generate:pg.
|
|
8
|
+
ALTER TABLE `repos` ADD `inbox_watch` integer DEFAULT false NOT NULL;--> statement-breakpoint
|
|
9
|
+
ALTER TABLE `repos` ADD `inbox_watch_started_at` integer;
|
|
@@ -106,6 +106,13 @@
|
|
|
106
106
|
"when": 1780800000005,
|
|
107
107
|
"tag": "0014_account_last_active",
|
|
108
108
|
"breakpoints": true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"idx": 15,
|
|
112
|
+
"version": "6",
|
|
113
|
+
"when": 1780800000006,
|
|
114
|
+
"tag": "0015_repos_inbox_watch",
|
|
115
|
+
"breakpoints": true
|
|
109
116
|
}
|
|
110
117
|
]
|
|
111
118
|
}
|