@omercnet/paseo-pr-radar 0.3.4
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 +21 -0
- package/README.md +58 -0
- package/bun.lock +1213 -0
- package/client/pr-radar.tsx +860 -0
- package/client/radar.ts +601 -0
- package/docs/images/pr-radar-compact.png +0 -0
- package/docs/images/pr-radar-github-inbox-compact.png +0 -0
- package/docs/images/pr-radar-github-inbox-wide.png +0 -0
- package/docs/images/pr-radar-wide.png +0 -0
- package/index.client.tsx +23 -0
- package/index.server.ts +9 -0
- package/package.json +56 -0
- package/paseo-plugin.json +4 -0
- package/server/viewer-scope.ts +402 -0
- package/shared/viewer-scope.ts +53 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import type { acknowledgeViewerScope, GitHubInboxItem, viewerScope } from "../shared/viewer-scope";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const SEARCH_LIMIT = 100;
|
|
11
|
+
const ENRICHMENT_BATCH_SIZE = 20;
|
|
12
|
+
const COMMAND_TIMEOUT_MS = 15_000;
|
|
13
|
+
const MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const statePath = join(homedir(), ".paseo", "plugin-data", "pr-radar", "inbox-state.json");
|
|
15
|
+
const SCOPE_SEARCH_LIMIT = 1000;
|
|
16
|
+
|
|
17
|
+
interface SearchRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
number: number;
|
|
20
|
+
title: string;
|
|
21
|
+
url: string;
|
|
22
|
+
isDraft: boolean;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
author: { login: string; is_bot?: boolean; type?: string } | null;
|
|
26
|
+
repository: { nameWithOwner: string };
|
|
27
|
+
commentsCount: number;
|
|
28
|
+
labels: Array<{ name: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Enrichment {
|
|
32
|
+
id: string;
|
|
33
|
+
baseRefName: string;
|
|
34
|
+
headRefName: string;
|
|
35
|
+
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
|
|
36
|
+
mergeable: "UNKNOWN" | "MERGEABLE" | "CONFLICTING";
|
|
37
|
+
mergeStateStatus: string;
|
|
38
|
+
statusCheckRollup: { state: "SUCCESS" | "FAILURE" | "ERROR" | "PENDING" | "EXPECTED" } | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface StoredItem {
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
checksStatus: GitHubInboxItem["checksStatus"];
|
|
44
|
+
reviewDecision: GitHubInboxItem["reviewDecision"];
|
|
45
|
+
mergeable: GitHubInboxItem["mergeable"];
|
|
46
|
+
mergeStateStatus: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface StoredWindow {
|
|
50
|
+
items: Record<string, StoredItem>;
|
|
51
|
+
pendingChanges: Record<string, string[]>;
|
|
52
|
+
acknowledgedAt: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface StoredState {
|
|
56
|
+
version: 2;
|
|
57
|
+
windows: Record<string, StoredWindow>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runGh(args: string[], timeout = COMMAND_TIMEOUT_MS): Promise<string> {
|
|
61
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
62
|
+
timeout,
|
|
63
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
64
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
65
|
+
});
|
|
66
|
+
return stdout.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function delay(milliseconds: number): Promise<void> {
|
|
70
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
71
|
+
setTimeout(resolve, milliseconds);
|
|
72
|
+
return promise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function viewerLogin(): Promise<string> {
|
|
76
|
+
let lastError: unknown;
|
|
77
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
78
|
+
try {
|
|
79
|
+
const login = await runGh(["api", "user", "--jq", ".login"]);
|
|
80
|
+
if (login) return login;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
lastError = error;
|
|
83
|
+
if (attempt === 0) await delay(250);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastError instanceof Error
|
|
87
|
+
? lastError
|
|
88
|
+
: new Error("GitHub CLI is not authenticated on this Paseo host.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function searchPullRequests(
|
|
92
|
+
filter: "author" | "review-requested",
|
|
93
|
+
viewer: string,
|
|
94
|
+
since: string,
|
|
95
|
+
): Promise<SearchRecord[]> {
|
|
96
|
+
const fields = [
|
|
97
|
+
"id",
|
|
98
|
+
"number",
|
|
99
|
+
"title",
|
|
100
|
+
"url",
|
|
101
|
+
"isDraft",
|
|
102
|
+
"createdAt",
|
|
103
|
+
"updatedAt",
|
|
104
|
+
"author",
|
|
105
|
+
"repository",
|
|
106
|
+
"commentsCount",
|
|
107
|
+
"labels",
|
|
108
|
+
].join(",");
|
|
109
|
+
const output = await runGh([
|
|
110
|
+
"search",
|
|
111
|
+
"prs",
|
|
112
|
+
`--${filter}=${viewer}`,
|
|
113
|
+
"--state=open",
|
|
114
|
+
"--archived=false",
|
|
115
|
+
`--updated=>=${since}`,
|
|
116
|
+
`--limit=${SEARCH_LIMIT}`,
|
|
117
|
+
"--sort=updated",
|
|
118
|
+
"--order=desc",
|
|
119
|
+
`--json=${fields}`,
|
|
120
|
+
]);
|
|
121
|
+
return JSON.parse(output) as SearchRecord[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function searchScopeUrls(filter: "author" | "review-requested"): Promise<string[]> {
|
|
125
|
+
const output = await runGh([
|
|
126
|
+
"search",
|
|
127
|
+
"prs",
|
|
128
|
+
`--${filter}=@me`,
|
|
129
|
+
"--state=open",
|
|
130
|
+
"--archived=false",
|
|
131
|
+
`--limit=${SCOPE_SEARCH_LIMIT}`,
|
|
132
|
+
"--json=url",
|
|
133
|
+
]);
|
|
134
|
+
return (JSON.parse(output) as Array<{ url: string }>).map(({ url }) => url);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const enrichmentQuery = `
|
|
138
|
+
query($ids: [ID!]!) {
|
|
139
|
+
nodes(ids: $ids) {
|
|
140
|
+
... on PullRequest {
|
|
141
|
+
id
|
|
142
|
+
baseRefName
|
|
143
|
+
headRefName
|
|
144
|
+
reviewDecision
|
|
145
|
+
mergeable
|
|
146
|
+
mergeStateStatus
|
|
147
|
+
statusCheckRollup { state }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
function chunks<T>(items: T[], size: number): T[][] {
|
|
154
|
+
const result: T[][] = [];
|
|
155
|
+
for (let index = 0; index < items.length; index += size) {
|
|
156
|
+
result.push(items.slice(index, index + size));
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function enrich(
|
|
162
|
+
ids: string[],
|
|
163
|
+
): Promise<{ states: Map<string, Enrichment>; failures: number }> {
|
|
164
|
+
const results = await Promise.allSettled(
|
|
165
|
+
chunks(ids, ENRICHMENT_BATCH_SIZE).map(async (batch) => {
|
|
166
|
+
const args = ["api", "graphql", "-f", `query=${enrichmentQuery}`];
|
|
167
|
+
for (const id of batch) args.push("-F", `ids[]=${id}`);
|
|
168
|
+
const payload = JSON.parse(await runGh(args, 10_000)) as {
|
|
169
|
+
data?: { nodes?: Array<Enrichment | null> };
|
|
170
|
+
errors?: Array<{ message: string }>;
|
|
171
|
+
};
|
|
172
|
+
if (!payload.data?.nodes) {
|
|
173
|
+
throw new Error(payload.errors?.map(({ message }) => message).join("; ") || "No data");
|
|
174
|
+
}
|
|
175
|
+
return payload.data.nodes.filter((item): item is Enrichment => item !== null);
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
const states = new Map<string, Enrichment>();
|
|
179
|
+
let failures = 0;
|
|
180
|
+
for (const result of results) {
|
|
181
|
+
if (result.status === "rejected") {
|
|
182
|
+
failures += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
for (const item of result.value) states.set(item.id, item);
|
|
186
|
+
}
|
|
187
|
+
return { states, failures };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function checksStatus(state: Enrichment | undefined): GitHubInboxItem["checksStatus"] {
|
|
191
|
+
switch (state?.statusCheckRollup?.state) {
|
|
192
|
+
case "SUCCESS":
|
|
193
|
+
return "success";
|
|
194
|
+
case "FAILURE":
|
|
195
|
+
case "ERROR":
|
|
196
|
+
return "failure";
|
|
197
|
+
case "PENDING":
|
|
198
|
+
case "EXPECTED":
|
|
199
|
+
return "pending";
|
|
200
|
+
default:
|
|
201
|
+
return "none";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function reviewDecision(state: Enrichment | undefined): GitHubInboxItem["reviewDecision"] {
|
|
206
|
+
switch (state?.reviewDecision) {
|
|
207
|
+
case "APPROVED":
|
|
208
|
+
return "approved";
|
|
209
|
+
case "CHANGES_REQUESTED":
|
|
210
|
+
return "changes_requested";
|
|
211
|
+
case "REVIEW_REQUIRED":
|
|
212
|
+
return "pending";
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toInboxItem(
|
|
219
|
+
record: SearchRecord,
|
|
220
|
+
role: GitHubInboxItem["role"],
|
|
221
|
+
state: Enrichment | undefined,
|
|
222
|
+
): GitHubInboxItem {
|
|
223
|
+
const labels = record.labels.map(({ name }) => name);
|
|
224
|
+
const authorKind =
|
|
225
|
+
record.author?.is_bot || record.author?.type === "Bot" || record.author?.login.endsWith("[bot]")
|
|
226
|
+
? "bot"
|
|
227
|
+
: "human";
|
|
228
|
+
return {
|
|
229
|
+
id: record.id,
|
|
230
|
+
number: record.number,
|
|
231
|
+
url: record.url,
|
|
232
|
+
title: record.title,
|
|
233
|
+
repository: record.repository.nameWithOwner,
|
|
234
|
+
author: record.author?.login ?? null,
|
|
235
|
+
authorKind,
|
|
236
|
+
createdAt: record.createdAt,
|
|
237
|
+
updatedAt: record.updatedAt,
|
|
238
|
+
baseRefName: state?.baseRefName ?? "",
|
|
239
|
+
headRefName: state?.headRefName ?? "",
|
|
240
|
+
isDraft: record.isDraft,
|
|
241
|
+
isSecurity:
|
|
242
|
+
labels.some((label) => /security|vulnerability|cve/i.test(label)) ||
|
|
243
|
+
/security|vulnerabilit|\bcve\b/i.test(record.title),
|
|
244
|
+
comments: record.commentsCount,
|
|
245
|
+
labels,
|
|
246
|
+
mergeable: state?.mergeable ?? "UNKNOWN",
|
|
247
|
+
mergeStateStatus: state?.mergeStateStatus ?? null,
|
|
248
|
+
checksStatus: checksStatus(state),
|
|
249
|
+
reviewDecision: reviewDecision(state),
|
|
250
|
+
role,
|
|
251
|
+
changes: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function readState(): Promise<StoredState> {
|
|
256
|
+
try {
|
|
257
|
+
const state = JSON.parse(await readFile(statePath, "utf8")) as StoredState;
|
|
258
|
+
if (state.version === 2 && state.windows && typeof state.windows === "object") return state;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
261
|
+
console.error("PR Radar ignored unreadable inbox state", error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { version: 2, windows: {} };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function writeState(state: StoredState): Promise<void> {
|
|
268
|
+
await mkdir(dirname(statePath), { recursive: true });
|
|
269
|
+
const temporaryPath = `${statePath}.${process.pid}.tmp`;
|
|
270
|
+
await writeFile(temporaryPath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
271
|
+
await rename(temporaryPath, statePath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function storedItem(item: GitHubInboxItem): StoredItem {
|
|
275
|
+
return {
|
|
276
|
+
updatedAt: item.updatedAt,
|
|
277
|
+
checksStatus: item.checksStatus,
|
|
278
|
+
reviewDecision: item.reviewDecision,
|
|
279
|
+
mergeable: item.mergeable,
|
|
280
|
+
mergeStateStatus: item.mergeStateStatus,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function detectChanges(previous: StoredItem | undefined, item: GitHubInboxItem): string[] {
|
|
285
|
+
if (!previous) return ["New PR"];
|
|
286
|
+
const changes = new Set<string>();
|
|
287
|
+
if (previous.updatedAt !== item.updatedAt) changes.add("New activity");
|
|
288
|
+
if (previous.checksStatus !== item.checksStatus) {
|
|
289
|
+
changes.add(`Checks: ${previous.checksStatus} → ${item.checksStatus}`);
|
|
290
|
+
}
|
|
291
|
+
if (previous.reviewDecision !== item.reviewDecision) {
|
|
292
|
+
changes.add(`Review: ${previous.reviewDecision ?? "none"} → ${item.reviewDecision ?? "none"}`);
|
|
293
|
+
}
|
|
294
|
+
if (previous.mergeable !== item.mergeable) {
|
|
295
|
+
changes.add(`Mergeable: ${previous.mergeable.toLowerCase()} → ${item.mergeable.toLowerCase()}`);
|
|
296
|
+
}
|
|
297
|
+
if (previous.mergeStateStatus !== item.mergeStateStatus) {
|
|
298
|
+
changes.add(
|
|
299
|
+
`Merge state: ${previous.mergeStateStatus ?? "unknown"} → ${item.mergeStateStatus ?? "unknown"}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return [...changes];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function resolveViewerScope({
|
|
306
|
+
urls,
|
|
307
|
+
windowDays,
|
|
308
|
+
}: z.output<typeof viewerScope.input>): Promise<z.input<typeof viewerScope.output>> {
|
|
309
|
+
try {
|
|
310
|
+
const viewer = await viewerLogin();
|
|
311
|
+
const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1_000)
|
|
312
|
+
.toISOString()
|
|
313
|
+
.slice(0, 10);
|
|
314
|
+
const [authored, reviewRequested, authoredScope, reviewRequestedScope, stored] =
|
|
315
|
+
await Promise.all([
|
|
316
|
+
searchPullRequests("author", viewer, since),
|
|
317
|
+
searchPullRequests("review-requested", viewer, since),
|
|
318
|
+
searchScopeUrls("author"),
|
|
319
|
+
searchScopeUrls("review-requested"),
|
|
320
|
+
readState(),
|
|
321
|
+
]);
|
|
322
|
+
const records = new Map<string, { record: SearchRecord; role: GitHubInboxItem["role"] }>();
|
|
323
|
+
for (const record of reviewRequested) records.set(record.id, { record, role: "reviewer" });
|
|
324
|
+
for (const record of authored) records.set(record.id, { record, role: "author" });
|
|
325
|
+
const enrichment = await enrich([...records.keys()]);
|
|
326
|
+
const inboxItems = [...records.values()].map(({ record, role }) =>
|
|
327
|
+
toInboxItem(record, role, enrichment.states.get(record.id)),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const key = String(windowDays);
|
|
331
|
+
const previous = stored.windows[key] ?? { items: {}, pendingChanges: {}, acknowledgedAt: null };
|
|
332
|
+
const initialized = Object.keys(previous.items).length > 0;
|
|
333
|
+
const nextItems: Record<string, StoredItem> = {};
|
|
334
|
+
const nextPending: Record<string, string[]> = {};
|
|
335
|
+
for (const item of inboxItems) {
|
|
336
|
+
const prior = previous.items[item.id];
|
|
337
|
+
const detected =
|
|
338
|
+
initialized && (!prior || enrichment.states.has(item.id)) ? detectChanges(prior, item) : [];
|
|
339
|
+
item.changes = [...new Set([...(previous.pendingChanges[item.id] ?? []), ...detected])];
|
|
340
|
+
nextItems[item.id] = prior && !enrichment.states.has(item.id) ? prior : storedItem(item);
|
|
341
|
+
if (item.changes.length > 0) nextPending[item.id] = item.changes;
|
|
342
|
+
}
|
|
343
|
+
stored.windows[key] = {
|
|
344
|
+
items: nextItems,
|
|
345
|
+
pendingChanges: nextPending,
|
|
346
|
+
acknowledgedAt: previous.acknowledgedAt,
|
|
347
|
+
};
|
|
348
|
+
await writeState(stored);
|
|
349
|
+
|
|
350
|
+
const requestedUrls = new Set(urls.map((url) => url.toLowerCase()));
|
|
351
|
+
const authoredUrls = new Set([
|
|
352
|
+
...authored.map(({ url }) => url),
|
|
353
|
+
...authoredScope.filter((url) => requestedUrls.has(url.toLowerCase())),
|
|
354
|
+
]);
|
|
355
|
+
const reviewRequestedUrls = new Set([
|
|
356
|
+
...reviewRequested.map(({ url }) => url),
|
|
357
|
+
...reviewRequestedScope.filter((url) => requestedUrls.has(url.toLowerCase())),
|
|
358
|
+
]);
|
|
359
|
+
const coverageNote =
|
|
360
|
+
enrichment.failures > 0
|
|
361
|
+
? `${enrichment.failures} GitHub detail request${enrichment.failures === 1 ? "" : "s"} failed; affected PRs use conservative states.`
|
|
362
|
+
: `Open PRs visible to the GitHub CLI. Organization SSO restrictions may omit results.`;
|
|
363
|
+
return {
|
|
364
|
+
viewer,
|
|
365
|
+
authoredUrls: [...authoredUrls],
|
|
366
|
+
reviewRequestedUrls: [...reviewRequestedUrls],
|
|
367
|
+
inboxItems,
|
|
368
|
+
truncated: authored.length === SEARCH_LIMIT || reviewRequested.length === SEARCH_LIMIT,
|
|
369
|
+
coverageNote,
|
|
370
|
+
updates: inboxItems.filter(({ changes }) => changes.length > 0).length,
|
|
371
|
+
acknowledgedAt: previous.acknowledgedAt,
|
|
372
|
+
error: null,
|
|
373
|
+
};
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error("PR Radar GitHub inbox refresh failed", error);
|
|
376
|
+
return {
|
|
377
|
+
viewer: null,
|
|
378
|
+
authoredUrls: [],
|
|
379
|
+
reviewRequestedUrls: [],
|
|
380
|
+
inboxItems: [],
|
|
381
|
+
truncated: false,
|
|
382
|
+
coverageNote: "GitHub inbox data is unavailable.",
|
|
383
|
+
updates: 0,
|
|
384
|
+
acknowledgedAt: null,
|
|
385
|
+
error: error instanceof Error ? error.message : "GitHub viewer scope is unavailable.",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function acknowledgeViewerUpdates({
|
|
391
|
+
windowDays,
|
|
392
|
+
}: z.output<typeof acknowledgeViewerScope.input>): Promise<
|
|
393
|
+
z.input<typeof acknowledgeViewerScope.output>
|
|
394
|
+
> {
|
|
395
|
+
const state = await readState();
|
|
396
|
+
const key = String(windowDays);
|
|
397
|
+
const acknowledgedAt = new Date().toISOString();
|
|
398
|
+
const current = state.windows[key] ?? { items: {}, pendingChanges: {}, acknowledgedAt: null };
|
|
399
|
+
state.windows[key] = { ...current, pendingChanges: {}, acknowledgedAt };
|
|
400
|
+
await writeState(state);
|
|
401
|
+
return { acknowledgedAt };
|
|
402
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const GitHubInboxItemSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
number: z.number().int().positive(),
|
|
7
|
+
url: z.url(),
|
|
8
|
+
title: z.string(),
|
|
9
|
+
repository: z.string(),
|
|
10
|
+
author: z.string().nullable(),
|
|
11
|
+
authorKind: z.enum(["human", "bot"]),
|
|
12
|
+
createdAt: z.string(),
|
|
13
|
+
updatedAt: z.string(),
|
|
14
|
+
baseRefName: z.string(),
|
|
15
|
+
headRefName: z.string(),
|
|
16
|
+
isDraft: z.boolean(),
|
|
17
|
+
isSecurity: z.boolean(),
|
|
18
|
+
comments: z.number().int().nonnegative(),
|
|
19
|
+
labels: z.array(z.string()),
|
|
20
|
+
mergeable: z.enum(["UNKNOWN", "MERGEABLE", "CONFLICTING"]),
|
|
21
|
+
mergeStateStatus: z.string().nullable(),
|
|
22
|
+
checksStatus: z.enum(["success", "pending", "none", "failure"]),
|
|
23
|
+
reviewDecision: z.enum(["pending", "approved", "changes_requested"]).nullable(),
|
|
24
|
+
role: z.enum(["author", "reviewer"]),
|
|
25
|
+
changes: z.array(z.string()),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type GitHubInboxItem = z.infer<typeof GitHubInboxItemSchema>;
|
|
29
|
+
|
|
30
|
+
export const viewerScope = defineRpc({
|
|
31
|
+
name: "pr-radar.viewer-scope",
|
|
32
|
+
input: z.object({
|
|
33
|
+
urls: z.array(z.url()).max(200),
|
|
34
|
+
windowDays: z.number().int().min(1).max(365).default(30),
|
|
35
|
+
}),
|
|
36
|
+
output: z.object({
|
|
37
|
+
viewer: z.string().nullable(),
|
|
38
|
+
authoredUrls: z.array(z.url()),
|
|
39
|
+
reviewRequestedUrls: z.array(z.url()),
|
|
40
|
+
inboxItems: z.array(GitHubInboxItemSchema),
|
|
41
|
+
truncated: z.boolean(),
|
|
42
|
+
coverageNote: z.string(),
|
|
43
|
+
updates: z.number().int().nonnegative(),
|
|
44
|
+
acknowledgedAt: z.string().nullable(),
|
|
45
|
+
error: z.string().nullable(),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const acknowledgeViewerScope = defineRpc({
|
|
50
|
+
name: "pr-radar.acknowledge-updates",
|
|
51
|
+
input: z.object({ windowDays: z.number().int().min(1).max(365) }),
|
|
52
|
+
output: z.object({ acknowledgedAt: z.string() }),
|
|
53
|
+
});
|