@h-rig/github-provider-plugin 0.0.6-alpha.156

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.
@@ -0,0 +1,1247 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // packages/github-provider-plugin/src/credentials.ts
19
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20
+ import { resolve as resolve2 } from "path";
21
+ function selectedRepoTokenKey(input) {
22
+ return `user:${input.userId}|repo:${input.owner}/${input.repo}|workspace:${input.workspaceId}`;
23
+ }
24
+ function cleanToken(value) {
25
+ const trimmed = value?.trim() ?? "";
26
+ return trimmed.length > 0 ? trimmed : null;
27
+ }
28
+ function createGitHubCredentialProvider(options = {}) {
29
+ const sessionTokens = options.sessionTokens ?? {};
30
+ const hostToken = cleanToken(options.hostToken ?? process.env.GH_TOKEN ?? null);
31
+ return {
32
+ async resolveGitHubToken(input) {
33
+ const owner = input.owner.trim();
34
+ const repo = input.repo.trim();
35
+ const workspaceId = input.workspaceId.trim();
36
+ const userId = input.userId?.trim() ?? "";
37
+ if (input.purpose === "selected-repo") {
38
+ if (!owner || !repo || !workspaceId || !userId) {
39
+ throw new Error("No signed-in GitHub token is available for the selected repo; sign in to GitHub for this workspace.");
40
+ }
41
+ const token = cleanToken(sessionTokens[selectedRepoTokenKey({ owner, repo, workspaceId, userId })]);
42
+ if (!token) {
43
+ throw new Error("No signed-in GitHub token is available for the selected repo; sign in to GitHub for this workspace.");
44
+ }
45
+ return { token, source: "signed-in-user" };
46
+ }
47
+ if (hostToken) {
48
+ return { token: hostToken, source: "host-admin-fallback" };
49
+ }
50
+ throw new Error("No host GitHub token is configured for the explicit admin fallback operation.");
51
+ }
52
+ };
53
+ }
54
+ function createEnvGitHubCredentialProvider() {
55
+ return {
56
+ async resolveGitHubToken(input) {
57
+ if (input.purpose === "selected-repo") {
58
+ return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
59
+ }
60
+ const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
61
+ if (!token) {
62
+ throw new Error("No host GitHub token is configured for admin fallback.");
63
+ }
64
+ return { token, source: "host-admin-fallback" };
65
+ }
66
+ };
67
+ }
68
+ function createStateGitHubCredentialProvider(options = {}) {
69
+ const addCandidate = (candidates, path) => {
70
+ const trimmed = path?.trim();
71
+ if (!trimmed)
72
+ return;
73
+ const resolved = resolve2(trimmed);
74
+ if (!candidates.includes(resolved))
75
+ candidates.push(resolved);
76
+ };
77
+ const addStateDir = (candidates, dir) => {
78
+ const trimmed = dir?.trim();
79
+ if (!trimmed)
80
+ return;
81
+ addCandidate(candidates, resolve2(trimmed, "github-auth.json"));
82
+ };
83
+ const addProjectStateDir = (candidates, root) => {
84
+ const trimmed = root?.trim();
85
+ if (!trimmed)
86
+ return;
87
+ addStateDir(candidates, resolve2(trimmed, ".rig", "state"));
88
+ };
89
+ const stateFileCandidates = () => {
90
+ const candidates = [];
91
+ addCandidate(candidates, options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE);
92
+ addStateDir(candidates, options.stateDir);
93
+ addStateDir(candidates, process.env.RIG_STATE_DIR);
94
+ addProjectStateDir(candidates, process.env.PROJECT_RIG_ROOT);
95
+ addProjectStateDir(candidates, process.env.RIG_PROJECT_ROOT);
96
+ addProjectStateDir(candidates, process.env.RIG_HOST_PROJECT_ROOT);
97
+ addProjectStateDir(candidates, process.cwd());
98
+ return candidates;
99
+ };
100
+ const readToken = () => {
101
+ for (const stateFile of stateFileCandidates()) {
102
+ if (!existsSync2(stateFile))
103
+ continue;
104
+ try {
105
+ const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
106
+ const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
107
+ if (token)
108
+ return token;
109
+ } catch {}
110
+ }
111
+ return null;
112
+ };
113
+ return {
114
+ async resolveGitHubToken(input) {
115
+ const token = readToken();
116
+ if (input.purpose === "selected-repo") {
117
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
118
+ }
119
+ if (token) {
120
+ return { token, source: "signed-in-user" };
121
+ }
122
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
123
+ if (!fallback) {
124
+ throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
125
+ }
126
+ return { token: fallback, source: "host-admin-fallback" };
127
+ }
128
+ };
129
+ }
130
+ var init_credentials = () => {};
131
+
132
+ // packages/github-provider-plugin/src/service.ts
133
+ var exports_service = {};
134
+ __export(exports_service, {
135
+ githubProviderService: () => githubProviderService
136
+ });
137
+ var githubProviderService;
138
+ var init_service = __esm(() => {
139
+ init_credentials();
140
+ githubProviderService = {
141
+ createCredentialProvider: (options) => createGitHubCredentialProvider(options)
142
+ };
143
+ });
144
+
145
+ // packages/github-provider-plugin/src/auth-store.ts
146
+ import { randomBytes } from "crypto";
147
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
148
+ import { dirname, resolve } from "path";
149
+ import { resolveRigStatePaths } from "@rig/runtime/control-plane/server-paths";
150
+ function cleanString(value) {
151
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
152
+ }
153
+ function cleanScopes(value) {
154
+ if (!Array.isArray(value))
155
+ return [];
156
+ return value.flatMap((entry) => {
157
+ const clean = cleanString(entry);
158
+ return clean ? [clean] : [];
159
+ });
160
+ }
161
+ function parseApiSessions(value) {
162
+ if (!Array.isArray(value))
163
+ return [];
164
+ return value.flatMap((entry) => {
165
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
166
+ return [];
167
+ const record = entry;
168
+ const token = cleanString(record.token);
169
+ if (!token)
170
+ return [];
171
+ return [{
172
+ token,
173
+ login: cleanString(record.login),
174
+ userId: cleanString(record.userId),
175
+ createdAt: cleanString(record.createdAt) ?? undefined
176
+ }];
177
+ });
178
+ }
179
+ function parsePendingDevice(value) {
180
+ if (!value || typeof value !== "object")
181
+ return null;
182
+ const record = value;
183
+ const pollId = cleanString(record.pollId);
184
+ const deviceCode = cleanString(record.deviceCode);
185
+ const expiresAt = cleanString(record.expiresAt);
186
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
187
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
188
+ return null;
189
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
190
+ }
191
+ function parsePendingDevices(value) {
192
+ if (!Array.isArray(value))
193
+ return [];
194
+ return value.flatMap((entry) => {
195
+ const pending = parsePendingDevice(entry);
196
+ return pending ? [pending] : [];
197
+ });
198
+ }
199
+ function readStoredAuth(stateFile) {
200
+ if (!existsSync(stateFile))
201
+ return {};
202
+ try {
203
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
204
+ return {
205
+ ...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
206
+ login: cleanString(parsed.login),
207
+ userId: cleanString(parsed.userId),
208
+ scopes: cleanScopes(parsed.scopes),
209
+ selectedRepo: cleanString(parsed.selectedRepo),
210
+ tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
211
+ pendingDevice: parsePendingDevice(parsed.pendingDevice),
212
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
213
+ apiSessions: parseApiSessions(parsed.apiSessions),
214
+ updatedAt: cleanString(parsed.updatedAt) ?? undefined
215
+ };
216
+ } catch {
217
+ return {};
218
+ }
219
+ }
220
+ function newApiSessionToken() {
221
+ return `rig_${randomBytes(32).toString("base64url")}`;
222
+ }
223
+ function writeStoredAuth(stateFile, payload) {
224
+ mkdirSync(dirname(stateFile), { recursive: true });
225
+ writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}
226
+ `, { encoding: "utf8", mode: 384 });
227
+ try {
228
+ chmodSync(stateFile, 384);
229
+ } catch {}
230
+ }
231
+ function localProjectAuthStateFile(projectRoot) {
232
+ return resolve(projectRoot, ".rig", "state", "github-auth.json");
233
+ }
234
+ function resolveGitHubAuthStateFile(projectRoot) {
235
+ return resolve(resolveRigStatePaths(projectRoot).stateDir, "github-auth.json");
236
+ }
237
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
238
+ const targetFile = localProjectAuthStateFile(projectRoot);
239
+ mkdirSync(dirname(targetFile), { recursive: true });
240
+ if (existsSync(stateFile)) {
241
+ copyFileSync(stateFile, targetFile);
242
+ try {
243
+ chmodSync(targetFile, 384);
244
+ } catch {}
245
+ return;
246
+ }
247
+ writeStoredAuth(targetFile, {});
248
+ }
249
+ function createGitHubAuthStoreFromStateFile(stateFile) {
250
+ return {
251
+ stateFile,
252
+ status(options) {
253
+ const stored = readStoredAuth(stateFile);
254
+ const token = cleanString(stored.token);
255
+ return {
256
+ signedIn: Boolean(token),
257
+ login: cleanString(stored.login),
258
+ userId: cleanString(stored.userId),
259
+ scopes: cleanScopes(stored.scopes),
260
+ selectedRepo: cleanString(stored.selectedRepo),
261
+ oauthConfigured: options?.oauthConfigured === true,
262
+ tokenSource: token ? stored.tokenSource ?? "manual-token" : null
263
+ };
264
+ },
265
+ readToken() {
266
+ return cleanString(readStoredAuth(stateFile).token);
267
+ },
268
+ saveToken(input) {
269
+ const previous = readStoredAuth(stateFile);
270
+ writeStoredAuth(stateFile, {
271
+ ...previous,
272
+ token: input.token,
273
+ tokenSource: input.tokenSource,
274
+ login: input.login ?? null,
275
+ userId: input.userId ?? null,
276
+ scopes: input.scopes ?? [],
277
+ selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
278
+ pendingDevice: null,
279
+ pendingDevices: [],
280
+ apiSessions: previous.apiSessions ?? [],
281
+ updatedAt: new Date().toISOString()
282
+ });
283
+ },
284
+ createApiSession() {
285
+ const previous = readStoredAuth(stateFile);
286
+ const token = newApiSessionToken();
287
+ const session = {
288
+ token,
289
+ login: cleanString(previous.login),
290
+ userId: cleanString(previous.userId),
291
+ createdAt: new Date().toISOString()
292
+ };
293
+ writeStoredAuth(stateFile, {
294
+ ...previous,
295
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
296
+ updatedAt: new Date().toISOString()
297
+ });
298
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
299
+ },
300
+ readApiSession(token) {
301
+ const clean = cleanString(token);
302
+ if (!clean)
303
+ return null;
304
+ const previous = readStoredAuth(stateFile);
305
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
306
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
307
+ },
308
+ copyToProjectRoot(projectRoot) {
309
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
310
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
311
+ },
312
+ copyToLocalProjectRoot(projectRoot) {
313
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
314
+ },
315
+ savePendingDevice(input) {
316
+ const previous = readStoredAuth(stateFile);
317
+ const pendingDevices = [
318
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
319
+ ...previous.pendingDevices ?? [],
320
+ input
321
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
322
+ writeStoredAuth(stateFile, {
323
+ ...previous,
324
+ pendingDevice: null,
325
+ pendingDevices,
326
+ updatedAt: new Date().toISOString()
327
+ });
328
+ },
329
+ saveSelectedRepo(selectedRepo) {
330
+ const previous = readStoredAuth(stateFile);
331
+ writeStoredAuth(stateFile, {
332
+ ...previous,
333
+ selectedRepo: selectedRepo ?? null,
334
+ updatedAt: new Date().toISOString()
335
+ });
336
+ },
337
+ readPendingDevice(pollId) {
338
+ const previous = readStoredAuth(stateFile);
339
+ const pending = [
340
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
341
+ ...previous.pendingDevices ?? []
342
+ ].find((entry) => entry.pollId === pollId) ?? null;
343
+ if (!pending)
344
+ return null;
345
+ if (Date.parse(pending.expiresAt) <= Date.now())
346
+ return null;
347
+ return pending;
348
+ },
349
+ clearPendingDevice(pollId) {
350
+ const previous = readStoredAuth(stateFile);
351
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
352
+ writeStoredAuth(stateFile, {
353
+ ...previous,
354
+ pendingDevice: null,
355
+ pendingDevices: remaining,
356
+ updatedAt: new Date().toISOString()
357
+ });
358
+ }
359
+ };
360
+ }
361
+ function createGitHubAuthStore(projectRoot) {
362
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
363
+ }
364
+
365
+ // packages/github-provider-plugin/src/index.ts
366
+ init_credentials();
367
+
368
+ // packages/github-provider-plugin/src/projects.ts
369
+ function asRecord(value) {
370
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
371
+ }
372
+ function asString(value) {
373
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
374
+ }
375
+ function asNumber(value) {
376
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
377
+ }
378
+ async function defaultGraphQLFetch(query, variables, token) {
379
+ const response = await fetch("https://api.github.com/graphql", {
380
+ method: "POST",
381
+ headers: {
382
+ "content-type": "application/json",
383
+ authorization: `Bearer ${token}`,
384
+ accept: "application/vnd.github+json"
385
+ },
386
+ body: JSON.stringify({ query, variables })
387
+ });
388
+ const json = await response.json().catch(() => ({}));
389
+ if (!response.ok || json.errors) {
390
+ throw new Error(`GitHub Projects GraphQL request failed: ${JSON.stringify(json.errors ?? { status: response.status })}`);
391
+ }
392
+ return json.data;
393
+ }
394
+ function projectNodesFrom(data) {
395
+ const root = asRecord(data);
396
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
397
+ const projects = asRecord(owner?.projectsV2);
398
+ const nodes = projects?.nodes;
399
+ return Array.isArray(nodes) ? nodes : [];
400
+ }
401
+ async function listGitHubProjects(input) {
402
+ const query = `
403
+ query RigListProjects($owner: String!, $first: Int!) {
404
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
405
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
406
+ }
407
+ `;
408
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
409
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
410
+ return projectNodesFrom(data).flatMap((node) => {
411
+ const record = asRecord(node);
412
+ const id = asString(record?.id);
413
+ const number = asNumber(record?.number);
414
+ const title = asString(record?.title);
415
+ if (!id || number === undefined || !title)
416
+ return [];
417
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
418
+ });
419
+ }
420
+ async function resolveProjectStatusField(input) {
421
+ const query = `
422
+ query RigProjectStatusField($projectId: ID!) {
423
+ node(id: $projectId) {
424
+ ... on ProjectV2 {
425
+ fields(first: 50) {
426
+ nodes {
427
+ ... on ProjectV2FieldCommon { id name }
428
+ ... on ProjectV2SingleSelectField { id name options { id name } }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ `;
435
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
436
+ const data = await fetchGraphQL(query, { projectId: input.projectId }, input.token);
437
+ const fields = asRecord(asRecord(asRecord(data)?.node)?.fields)?.nodes;
438
+ for (const node of Array.isArray(fields) ? fields : []) {
439
+ const record = asRecord(node);
440
+ if (asString(record?.name)?.toLowerCase() !== "status")
441
+ continue;
442
+ const id = asString(record?.id);
443
+ if (!id)
444
+ continue;
445
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
446
+ const optionRecord = asRecord(option);
447
+ const optionId = asString(optionRecord?.id);
448
+ const name = asString(optionRecord?.name);
449
+ return optionId && name ? [{ id: optionId, name }] : [];
450
+ }) : [];
451
+ return { id, name: "Status", options };
452
+ }
453
+ throw new Error(`GitHub Project ${input.projectId} does not expose a Status single-select field.`);
454
+ }
455
+ async function ensureIssueProjectItem(input) {
456
+ const query = `
457
+ query RigFindProjectIssueItem($projectId: ID!, $issueNodeId: ID!) {
458
+ node(id: $projectId) {
459
+ ... on ProjectV2 {
460
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
461
+ }
462
+ }
463
+ }
464
+ `;
465
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
466
+ const data = await fetchGraphQL(query, { projectId: input.projectId, issueNodeId: input.issueNodeId }, input.token);
467
+ const nodes = asRecord(asRecord(asRecord(data)?.node)?.items)?.nodes;
468
+ for (const node of Array.isArray(nodes) ? nodes : []) {
469
+ const record = asRecord(node);
470
+ const content = asRecord(record?.content);
471
+ if (asString(content?.id) === input.issueNodeId) {
472
+ const id2 = asString(record?.id);
473
+ if (id2)
474
+ return { id: id2, created: false };
475
+ }
476
+ }
477
+ const mutation = `
478
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
479
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
480
+ }
481
+ `;
482
+ const created = await fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
483
+ const addResult = asRecord(asRecord(created)?.addProjectV2ItemById);
484
+ const id = asString(asRecord(addResult?.item)?.id);
485
+ if (!id)
486
+ throw new Error("GitHub Project item creation did not return an item id.");
487
+ return { id, created: true };
488
+ }
489
+ async function updateIssueProjectStatus(input) {
490
+ const mutation = `
491
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
492
+ updateProjectV2ItemFieldValue(input: {
493
+ projectId: $projectId,
494
+ itemId: $itemId,
495
+ fieldId: $fieldId,
496
+ value: { singleSelectOptionId: $optionId }
497
+ }) { projectV2Item { id } }
498
+ }
499
+ `;
500
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
501
+ await fetchGraphQL(mutation, {
502
+ projectId: input.projectId,
503
+ itemId: input.itemId,
504
+ fieldId: input.fieldId,
505
+ optionId: input.optionId
506
+ }, input.token);
507
+ }
508
+ // packages/github-provider-plugin/src/github-api.ts
509
+ import { randomUUID } from "crypto";
510
+ function normalizeString(value) {
511
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
512
+ }
513
+ function normalizeScopes(value) {
514
+ if (Array.isArray(value)) {
515
+ return value.flatMap((entry) => {
516
+ const normalized = normalizeString(entry);
517
+ return normalized ? [normalized] : [];
518
+ });
519
+ }
520
+ if (typeof value === "string") {
521
+ return value.split(/[ ,]+/).map((entry) => entry.trim()).filter(Boolean);
522
+ }
523
+ return [];
524
+ }
525
+ async function defaultPostGitHubForm(endpoint, body) {
526
+ const response = await fetch(endpoint, {
527
+ method: "POST",
528
+ headers: {
529
+ accept: "application/json",
530
+ "content-type": "application/x-www-form-urlencoded",
531
+ "user-agent": "rig"
532
+ },
533
+ body: new URLSearchParams(body)
534
+ });
535
+ const payload = await response.json().catch(() => ({}));
536
+ return { status: response.status, payload };
537
+ }
538
+ async function fetchGitHubUserInfo(token) {
539
+ const response = await fetch("https://api.github.com/user", {
540
+ headers: {
541
+ accept: "application/vnd.github+json",
542
+ authorization: `Bearer ${token}`,
543
+ "user-agent": "rig"
544
+ }
545
+ });
546
+ const payload = await response.json().catch(() => ({}));
547
+ if (!response.ok) {
548
+ throw new Error(typeof payload.message === "string" ? payload.message : `GitHub user lookup failed (${response.status}).`);
549
+ }
550
+ const login = normalizeString(payload.login);
551
+ const id = typeof payload.id === "number" ? String(payload.id) : normalizeString(payload.id);
552
+ if (!login || !id) {
553
+ throw new Error("GitHub user lookup did not return login/id.");
554
+ }
555
+ return { login, id, scopes: normalizeScopes(response.headers.get("x-oauth-scopes")) };
556
+ }
557
+ function resolveGitHubAuthStatus(input) {
558
+ return createGitHubAuthStore(input.projectRoot).status({ oauthConfigured: input.oauthConfigured });
559
+ }
560
+ async function saveGitHubTokenForProject(input) {
561
+ const token = normalizeString(input.token);
562
+ if (!token) {
563
+ throw new Error("token is required");
564
+ }
565
+ const user = await (input.fetchUser ?? fetchGitHubUserInfo)(token);
566
+ const store = createGitHubAuthStore(input.projectRoot);
567
+ store.saveToken({
568
+ token,
569
+ tokenSource: input.tokenSource ?? "manual-token",
570
+ login: user.login,
571
+ userId: user.id,
572
+ scopes: user.scopes ?? [],
573
+ selectedRepo: input.selectedRepo ?? undefined
574
+ });
575
+ return { ok: true, ...store.status({ oauthConfigured: true }) };
576
+ }
577
+ async function beginGitHubDeviceFlow(input) {
578
+ const clientId = normalizeString(input.clientId);
579
+ if (!clientId) {
580
+ throw new Error("clientId is required");
581
+ }
582
+ const postForm = input.postForm ?? defaultPostGitHubForm;
583
+ const result = await postForm("https://github.com/login/device/code", {
584
+ client_id: clientId,
585
+ scope: normalizeString(input.scope) ?? "repo read:project user:email"
586
+ });
587
+ const deviceCode = normalizeString(result.payload.device_code);
588
+ if (result.status < 200 || result.status >= 300 || !deviceCode) {
589
+ throw new Error(normalizeString(result.payload.error_description) ?? "GitHub device flow start failed.");
590
+ }
591
+ const expiresIn = typeof result.payload.expires_in === "number" ? result.payload.expires_in : 900;
592
+ const intervalSeconds = typeof result.payload.interval === "number" ? result.payload.interval : 5;
593
+ const pollId = randomUUID();
594
+ createGitHubAuthStore(input.projectRoot).savePendingDevice({
595
+ pollId,
596
+ deviceCode,
597
+ expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
598
+ intervalSeconds
599
+ });
600
+ if (input.selectedRepo) {
601
+ createGitHubAuthStore(input.projectRoot).saveSelectedRepo(input.selectedRepo);
602
+ }
603
+ return {
604
+ ok: true,
605
+ pollId,
606
+ userCode: normalizeString(result.payload.user_code),
607
+ verificationUri: normalizeString(result.payload.verification_uri),
608
+ expiresIn,
609
+ intervalSeconds
610
+ };
611
+ }
612
+ async function pollGitHubDeviceFlow(input) {
613
+ const clientId = normalizeString(input.clientId);
614
+ if (!clientId) {
615
+ throw new Error("clientId is required");
616
+ }
617
+ const store = createGitHubAuthStore(input.projectRoot);
618
+ const pending = store.readPendingDevice(input.pollId);
619
+ if (!pending) {
620
+ return { ok: false, status: "expired", error: "GitHub device flow expired or unknown." };
621
+ }
622
+ const result = await (input.postForm ?? defaultPostGitHubForm)("https://github.com/login/oauth/access_token", {
623
+ client_id: clientId,
624
+ device_code: pending.deviceCode,
625
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
626
+ });
627
+ const error = normalizeString(result.payload.error);
628
+ if (error === "authorization_pending" || error === "slow_down") {
629
+ return {
630
+ ok: false,
631
+ status: error === "slow_down" ? "slow-down" : "pending",
632
+ intervalSeconds: error === "slow_down" ? pending.intervalSeconds + 5 : pending.intervalSeconds
633
+ };
634
+ }
635
+ if (error || typeof result.payload.access_token !== "string") {
636
+ return {
637
+ ok: false,
638
+ status: "error",
639
+ error: normalizeString(result.payload.error_description) ?? "GitHub device authorization failed."
640
+ };
641
+ }
642
+ const token = result.payload.access_token;
643
+ const user = await (input.fetchUser ?? fetchGitHubUserInfo)(token);
644
+ store.saveToken({
645
+ token,
646
+ tokenSource: "oauth-device",
647
+ login: user.login,
648
+ userId: user.id,
649
+ scopes: user.scopes ?? normalizeScopes(result.payload.scope),
650
+ selectedRepo: input.selectedRepo ?? undefined
651
+ });
652
+ store.clearPendingDevice(input.pollId);
653
+ return { ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) };
654
+ }
655
+ function checkGitHubRepoPermissions(input) {
656
+ const store = createGitHubAuthStore(input.projectRoot);
657
+ const auth = store.status({ oauthConfigured: input.oauthConfigured });
658
+ if (!auth.signedIn) {
659
+ return { ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" };
660
+ }
661
+ const normalizedScopes = auth.scopes.map((scope) => scope.toLowerCase());
662
+ const broadEnough = normalizedScopes.includes("repo") || normalizedScopes.includes("public_repo");
663
+ return {
664
+ ok: true,
665
+ signedIn: true,
666
+ login: auth.login,
667
+ scopes: auth.scopes,
668
+ canOpenPullRequest: broadEnough,
669
+ pullRequests: broadEnough,
670
+ push: broadEnough,
671
+ reason: broadEnough ? "stored-token" : "token-scope-unverified"
672
+ };
673
+ }
674
+ async function probeGitHubRepository(input) {
675
+ const headers = {
676
+ accept: "application/vnd.github+json",
677
+ "user-agent": "rig"
678
+ };
679
+ if (input.token) {
680
+ headers.authorization = `Bearer ${input.token}`;
681
+ }
682
+ try {
683
+ const response = await (input.fetchRepository ?? fetch)(`https://api.github.com/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}`, { headers });
684
+ const payload = await response.json().catch(() => ({}));
685
+ if (response.ok) {
686
+ return {
687
+ ok: true,
688
+ owner: input.owner,
689
+ repo: input.repo,
690
+ status: response.status,
691
+ authenticated: Boolean(input.token),
692
+ authenticationRequired: payload.private === true && !input.token,
693
+ fullName: typeof payload.full_name === "string" ? payload.full_name : `${input.owner}/${input.repo}`,
694
+ private: typeof payload.private === "boolean" ? payload.private : null,
695
+ message: input.token ? "Repository access verified with signed-in GitHub credentials." : "Public repository access verified without credentials.",
696
+ scopes: input.scopes
697
+ };
698
+ }
699
+ const authenticationRequired = !input.token && (response.status === 401 || response.status === 403 || response.status === 404);
700
+ return {
701
+ ok: false,
702
+ owner: input.owner,
703
+ repo: input.repo,
704
+ status: response.status,
705
+ authenticated: Boolean(input.token),
706
+ authenticationRequired,
707
+ fullName: null,
708
+ private: null,
709
+ message: authenticationRequired ? "Repository is private, missing, or inaccessible without GitHub sign-in. Sign in before saving this config." : typeof payload.message === "string" ? payload.message : `GitHub repository probe failed (${response.status}).`,
710
+ scopes: input.scopes
711
+ };
712
+ } catch (error) {
713
+ return {
714
+ ok: false,
715
+ owner: input.owner,
716
+ repo: input.repo,
717
+ status: 0,
718
+ authenticated: Boolean(input.token),
719
+ authenticationRequired: !input.token,
720
+ fullName: null,
721
+ private: null,
722
+ message: error instanceof Error ? error.message : "GitHub repository probe failed.",
723
+ scopes: input.scopes
724
+ };
725
+ }
726
+ }
727
+ // packages/github-provider-plugin/src/issue-analysis.ts
728
+ import { createHash } from "crypto";
729
+ function stableIssueHash(issue) {
730
+ const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
731
+ const body = typeof issue.body === "string" ? issue.body : "";
732
+ const title = typeof issue.title === "string" ? issue.title : "";
733
+ return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
734
+ }
735
+ function renderIssueAnalysisPrompt(input) {
736
+ const issue = input.issue;
737
+ const neighbors = input.neighbors ?? [];
738
+ return [
739
+ "You are Rig issue analysis running inside Pi.",
740
+ "Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues; analyze backlog dependencies, children, readiness, size, risk, and planning.",
741
+ "Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
742
+ "Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
743
+ "",
744
+ "Issue:",
745
+ JSON.stringify({
746
+ id: issue.id,
747
+ title: issue.title,
748
+ body: issue.body,
749
+ labels: issue.labels,
750
+ deps: issue.deps,
751
+ status: issue.status
752
+ }, null, 2),
753
+ "",
754
+ "Neighbor tasks:",
755
+ JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
756
+ ].join(`
757
+ `);
758
+ }
759
+ function isRecord(value) {
760
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
761
+ }
762
+ function stringArray(value) {
763
+ if (!Array.isArray(value))
764
+ return;
765
+ return value.map(String).filter((entry) => entry.trim().length > 0);
766
+ }
767
+ function generatedIssues(value) {
768
+ if (!Array.isArray(value))
769
+ return;
770
+ return value.flatMap((entry) => {
771
+ if (!isRecord(entry) || typeof entry.title !== "string")
772
+ return [];
773
+ return [{
774
+ title: entry.title,
775
+ body: typeof entry.body === "string" ? entry.body : "",
776
+ labels: stringArray(entry.labels) ?? [],
777
+ ...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
778
+ }];
779
+ });
780
+ }
781
+ function findJsonLikeText(value) {
782
+ if (typeof value === "string") {
783
+ const trimmed = value.trim();
784
+ if (trimmed.startsWith("{") || trimmed.startsWith("```"))
785
+ return trimmed;
786
+ return null;
787
+ }
788
+ if (Array.isArray(value)) {
789
+ for (const entry of value) {
790
+ const found = findJsonLikeText(entry);
791
+ if (found)
792
+ return found;
793
+ }
794
+ return null;
795
+ }
796
+ if (!isRecord(value))
797
+ return null;
798
+ for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
799
+ const found = findJsonLikeText(value[key]);
800
+ if (found)
801
+ return found;
802
+ }
803
+ for (const entry of Object.values(value)) {
804
+ const found = findJsonLikeText(entry);
805
+ if (found)
806
+ return found;
807
+ }
808
+ return null;
809
+ }
810
+ function candidateAnalysisObject(value) {
811
+ if (!isRecord(value))
812
+ return null;
813
+ if (isRecord(value.result))
814
+ return candidateAnalysisObject(value.result) ?? value.result;
815
+ if (isRecord(value.analysis))
816
+ return candidateAnalysisObject(value.analysis) ?? value.analysis;
817
+ if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
818
+ return value;
819
+ }
820
+ const nested = findJsonLikeText(value);
821
+ if (nested && nested !== JSON.stringify(value)) {
822
+ try {
823
+ const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
824
+ return candidateAnalysisObject(parsedNested);
825
+ } catch {
826
+ return null;
827
+ }
828
+ }
829
+ return null;
830
+ }
831
+ function parseIssueAnalysisResult(raw) {
832
+ let parsed = raw;
833
+ if (typeof raw === "string") {
834
+ const trimmed = raw.trim();
835
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
836
+ try {
837
+ parsed = JSON.parse(fenced ?? trimmed);
838
+ } catch {
839
+ const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
840
+ parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
841
+ }
842
+ }
843
+ const candidate = candidateAnalysisObject(parsed);
844
+ if (!candidate)
845
+ return {};
846
+ const result = {};
847
+ if (isRecord(candidate.metadataPatch))
848
+ result.metadataPatch = candidate.metadataPatch;
849
+ const add = stringArray(candidate.labelsToAdd);
850
+ if (add?.length)
851
+ result.labelsToAdd = add;
852
+ const remove = stringArray(candidate.labelsToRemove);
853
+ if (remove?.length)
854
+ result.labelsToRemove = remove;
855
+ const generated = generatedIssues(candidate.generatedIssues);
856
+ if (generated?.length)
857
+ result.generatedIssues = generated;
858
+ return result;
859
+ }
860
+ function createDefaultPiIssueAnalysisCommandRunner() {
861
+ return async (command, args, options) => {
862
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
863
+ const proc = Bun.spawn([command, ...args], {
864
+ stdout: "pipe",
865
+ stderr: "pipe",
866
+ env
867
+ });
868
+ let timedOut = false;
869
+ const timer = setTimeout(() => {
870
+ timedOut = true;
871
+ proc.kill();
872
+ }, options.timeoutMs);
873
+ try {
874
+ const [stdout, stderr, exitCode] = await Promise.all([
875
+ new Response(proc.stdout).text(),
876
+ new Response(proc.stderr).text(),
877
+ proc.exited
878
+ ]);
879
+ return {
880
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
881
+ stdout,
882
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
883
+ };
884
+ } finally {
885
+ clearTimeout(timer);
886
+ }
887
+ };
888
+ }
889
+ function createPiIssueAnalyzer(input = {}) {
890
+ const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
891
+ const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
892
+ const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
893
+ return async ({ prompt }) => {
894
+ const args = ["--print", "--mode", "json", "--no-session"];
895
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
896
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
897
+ if (provider)
898
+ args.push("--provider", provider);
899
+ if (model)
900
+ args.push("--model", model);
901
+ args.push(prompt);
902
+ const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
903
+ if (result.exitCode !== 0) {
904
+ throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
905
+ }
906
+ return parseIssueAnalysisResult(result.stdout);
907
+ };
908
+ }
909
+ function defaultStatusComment(input) {
910
+ const changes = [
911
+ input.result.metadataPatch ? "metadata" : null,
912
+ input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
913
+ input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
914
+ input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
915
+ ].filter((entry) => Boolean(entry));
916
+ if (changes.length === 0)
917
+ return null;
918
+ return [
919
+ "<!-- rig:status-comment -->",
920
+ "### Rig issue analysis",
921
+ "",
922
+ `Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
923
+ "",
924
+ ...changes.map((change) => `- ${change}`)
925
+ ].join(`
926
+ `);
927
+ }
928
+ function uniqueLabels(labels, required = []) {
929
+ return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
930
+ }
931
+ function createIssueAnalysisWriteBack(input) {
932
+ return async ({ issue, result, reason }) => {
933
+ if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
934
+ if (!input.target.updateTask)
935
+ throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
936
+ await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
937
+ }
938
+ if (result.labelsToAdd?.length) {
939
+ if (!input.target.addLabels)
940
+ throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
941
+ await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
942
+ }
943
+ if (result.labelsToRemove?.length) {
944
+ if (!input.target.removeLabels)
945
+ throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
946
+ await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
947
+ }
948
+ const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
949
+ if (comment?.trim()) {
950
+ if (!input.target.updateTask)
951
+ throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
952
+ await input.target.updateTask(issue.id, { comment });
953
+ }
954
+ for (const generated of result.generatedIssues ?? []) {
955
+ if (!input.target.createIssue)
956
+ throw new Error("Issue analysis writeback requires createIssue for generated issues.");
957
+ await input.target.createIssue({
958
+ title: generated.title,
959
+ body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
960
+
961
+ depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
962
+ ` : generated.body,
963
+ labels: uniqueLabels(generated.labels, ["rig:generated"])
964
+ });
965
+ }
966
+ };
967
+ }
968
+ function sourceWithWriteBackCapabilities(source) {
969
+ const candidate = source;
970
+ if (typeof candidate.updateTask !== "function")
971
+ return null;
972
+ return {
973
+ get: candidate.get?.bind(candidate),
974
+ updateTask: candidate.updateTask.bind(candidate),
975
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
976
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
977
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
978
+ };
979
+ }
980
+ function issueAnalysisEnabled(config) {
981
+ const issueAnalysis = config.issueAnalysis;
982
+ if (!issueAnalysis)
983
+ return false;
984
+ if (issueAnalysis.enabled !== true)
985
+ return false;
986
+ if (issueAnalysis.mode === "off")
987
+ return false;
988
+ if (issueAnalysis.harness && issueAnalysis.harness !== "pi")
989
+ return false;
990
+ return true;
991
+ }
992
+ function createConfiguredIssueAnalysisRunner(input) {
993
+ if (!issueAnalysisEnabled(input.context.config))
994
+ return null;
995
+ const source = input.context.taskSourceRegistry.list()[0];
996
+ if (!source)
997
+ return null;
998
+ const target = sourceWithWriteBackCapabilities(source);
999
+ if (!target)
1000
+ return null;
1001
+ const analyzer = input.analyzer ?? createPiIssueAnalyzer({
1002
+ runCommand: input.runCommand,
1003
+ model: input.context.config.issueAnalysis?.model
1004
+ });
1005
+ const baseWriteBack = createIssueAnalysisWriteBack({ target });
1006
+ const service = createIssueAnalysisService({
1007
+ analyzer,
1008
+ writeBack: async (writeBackInput) => {
1009
+ await baseWriteBack(writeBackInput);
1010
+ await input.onWriteBack?.();
1011
+ }
1012
+ });
1013
+ return createContinuousIssueAnalysisRunner({
1014
+ loadIssues: async () => [...await source.list()],
1015
+ service,
1016
+ intervalMs: input.intervalMs,
1017
+ reason: "continuous-issue-analysis",
1018
+ ...input.setIntervalFn ? { setIntervalFn: input.setIntervalFn } : {},
1019
+ ...input.clearIntervalFn ? { clearIntervalFn: input.clearIntervalFn } : {},
1020
+ ...input.onError ? { onError: input.onError } : {}
1021
+ });
1022
+ }
1023
+ function createIssueAnalysisService(input) {
1024
+ const analyzedHashes = new Map;
1025
+ return {
1026
+ async analyze(issues, options = {}) {
1027
+ const results = [];
1028
+ const neighbors = options.neighbors ?? issues;
1029
+ for (const issue of issues) {
1030
+ const hash = stableIssueHash(issue);
1031
+ if (analyzedHashes.get(issue.id) === hash)
1032
+ continue;
1033
+ const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
1034
+ const result = await input.analyzer({ issue, neighbors, prompt });
1035
+ analyzedHashes.set(issue.id, hash);
1036
+ if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
1037
+ await input.writeBack?.({ issue, result, reason: options.reason });
1038
+ }
1039
+ results.push({ issue, result });
1040
+ }
1041
+ return results;
1042
+ },
1043
+ clearCache() {
1044
+ analyzedHashes.clear();
1045
+ }
1046
+ };
1047
+ }
1048
+ function createContinuousIssueAnalysisRunner(input) {
1049
+ const intervalMs = Math.max(1000, Math.trunc(input.intervalMs ?? 60000));
1050
+ const setIntervalFn = input.setIntervalFn ?? ((callback, ms) => setInterval(() => {
1051
+ callback();
1052
+ }, ms));
1053
+ const clearIntervalFn = input.clearIntervalFn ?? ((timer2) => clearInterval(timer2));
1054
+ let timer;
1055
+ let running = false;
1056
+ let inFlight = null;
1057
+ const tick = async (reason = input.reason ?? "continuous") => {
1058
+ if (inFlight)
1059
+ return inFlight;
1060
+ inFlight = (async () => {
1061
+ const issues = await input.loadIssues();
1062
+ return input.service.analyze(issues, { reason });
1063
+ })();
1064
+ try {
1065
+ return await inFlight;
1066
+ } finally {
1067
+ inFlight = null;
1068
+ }
1069
+ };
1070
+ return {
1071
+ start() {
1072
+ if (running)
1073
+ return;
1074
+ running = true;
1075
+ timer = setIntervalFn(async () => {
1076
+ try {
1077
+ await tick();
1078
+ } catch (error) {
1079
+ input.onError?.(error);
1080
+ }
1081
+ }, intervalMs);
1082
+ },
1083
+ stop() {
1084
+ if (!running)
1085
+ return;
1086
+ running = false;
1087
+ if (timer !== undefined)
1088
+ clearIntervalFn(timer);
1089
+ timer = undefined;
1090
+ },
1091
+ tick,
1092
+ isRunning() {
1093
+ return running;
1094
+ }
1095
+ };
1096
+ }
1097
+ // packages/github-provider-plugin/src/triage-run.ts
1098
+ import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
1099
+ function summarizeResults(results) {
1100
+ return results.reduce((summary, entry) => {
1101
+ if (entry.result.metadataPatch && Object.keys(entry.result.metadataPatch).length > 0) {
1102
+ summary.metadataPatches += 1;
1103
+ }
1104
+ summary.labelsAdded += entry.result.labelsToAdd?.length ?? 0;
1105
+ summary.labelsRemoved += entry.result.labelsToRemove?.length ?? 0;
1106
+ summary.generatedIssues += entry.result.generatedIssues?.length ?? 0;
1107
+ return summary;
1108
+ }, { metadataPatches: 0, labelsAdded: 0, labelsRemoved: 0, generatedIssues: 0 });
1109
+ }
1110
+ async function loadContext(projectRoot) {
1111
+ const context = await buildPluginHostContext(projectRoot);
1112
+ if (!context)
1113
+ return null;
1114
+ return {
1115
+ config: context.config,
1116
+ taskSourceRegistry: context.taskSourceRegistry
1117
+ };
1118
+ }
1119
+ async function runIssueAnalysisTriage(options) {
1120
+ const reason = options.reason?.trim() || "triage";
1121
+ const context = options.context ?? await loadContext(options.projectRoot);
1122
+ if (!context) {
1123
+ return {
1124
+ ok: true,
1125
+ enabled: false,
1126
+ reason,
1127
+ sourceId: null,
1128
+ sourceKind: null,
1129
+ analyzedIssues: 0,
1130
+ metadataPatches: 0,
1131
+ labelsAdded: 0,
1132
+ labelsRemoved: 0,
1133
+ generatedIssues: 0,
1134
+ writeBackRefreshes: 0,
1135
+ refreshedIssueCount: null,
1136
+ skippedReason: "no-config"
1137
+ };
1138
+ }
1139
+ const source = context.taskSourceRegistry.list()[0] ?? null;
1140
+ const sourceId = source?.id ?? null;
1141
+ const sourceKind = source?.kind ?? null;
1142
+ if (!issueAnalysisEnabled(context.config)) {
1143
+ return {
1144
+ ok: true,
1145
+ enabled: false,
1146
+ reason,
1147
+ sourceId,
1148
+ sourceKind,
1149
+ analyzedIssues: 0,
1150
+ metadataPatches: 0,
1151
+ labelsAdded: 0,
1152
+ labelsRemoved: 0,
1153
+ generatedIssues: 0,
1154
+ writeBackRefreshes: 0,
1155
+ refreshedIssueCount: null,
1156
+ skippedReason: "disabled"
1157
+ };
1158
+ }
1159
+ if (!source) {
1160
+ throw new Error("Issue analysis is enabled, but no configured task source is registered.");
1161
+ }
1162
+ let writeBackRefreshes = 0;
1163
+ let refreshedIssueCount = null;
1164
+ const refreshSnapshotAfterWriteBack = async () => {
1165
+ writeBackRefreshes += 1;
1166
+ const refreshed = await source.list();
1167
+ refreshedIssueCount = refreshed.length;
1168
+ await options.onWriteBack?.();
1169
+ };
1170
+ const runner = createConfiguredIssueAnalysisRunner({
1171
+ projectRoot: options.projectRoot,
1172
+ context,
1173
+ analyzer: options.analyzer,
1174
+ runCommand: options.runCommand,
1175
+ onWriteBack: refreshSnapshotAfterWriteBack
1176
+ });
1177
+ if (!runner) {
1178
+ throw new Error(`Issue analysis is enabled for ${sourceKind ?? "the configured source"}, but that task source does not expose Rig write-back capabilities.`);
1179
+ }
1180
+ const results = await runner.tick(reason);
1181
+ const summary = summarizeResults(results);
1182
+ return {
1183
+ ok: true,
1184
+ enabled: true,
1185
+ reason,
1186
+ sourceId,
1187
+ sourceKind,
1188
+ analyzedIssues: results.length,
1189
+ ...summary,
1190
+ writeBackRefreshes,
1191
+ refreshedIssueCount
1192
+ };
1193
+ }
1194
+ // packages/github-provider-plugin/src/plugin.ts
1195
+ import { definePlugin } from "@rig/core/config";
1196
+ import { GITHUB_PROVIDER_CAPABILITY_ID } from "@rig/contracts";
1197
+ var GITHUB_PROVIDER_PLUGIN_NAME = "@rig/github-provider-plugin";
1198
+ var githubProviderPlugin = definePlugin({
1199
+ name: GITHUB_PROVIDER_PLUGIN_NAME,
1200
+ version: "0.0.0-alpha.1",
1201
+ contributes: {
1202
+ capabilities: [
1203
+ {
1204
+ id: GITHUB_PROVIDER_CAPABILITY_ID,
1205
+ title: "GitHub SCM provider",
1206
+ description: "Resolve GitHub credentials for the configured SCM provider.",
1207
+ run: async () => (await Promise.resolve().then(() => (init_service(), exports_service))).githubProviderService
1208
+ }
1209
+ ]
1210
+ }
1211
+ });
1212
+ function createGitHubProviderPlugin() {
1213
+ return githubProviderPlugin;
1214
+ }
1215
+ export {
1216
+ updateIssueProjectStatus,
1217
+ saveGitHubTokenForProject,
1218
+ runIssueAnalysisTriage,
1219
+ resolveProjectStatusField,
1220
+ resolveGitHubAuthStatus,
1221
+ resolveGitHubAuthStateFile,
1222
+ renderIssueAnalysisPrompt,
1223
+ probeGitHubRepository,
1224
+ pollGitHubDeviceFlow,
1225
+ parseIssueAnalysisResult,
1226
+ listGitHubProjects,
1227
+ issueAnalysisEnabled,
1228
+ githubProviderPlugin,
1229
+ fetchGitHubUserInfo,
1230
+ ensureIssueProjectItem,
1231
+ createStateGitHubCredentialProvider,
1232
+ createPiIssueAnalyzer,
1233
+ createIssueAnalysisWriteBack,
1234
+ createIssueAnalysisService,
1235
+ createGitHubProviderPlugin,
1236
+ createGitHubCredentialProvider,
1237
+ createGitHubAuthStoreFromStateFile,
1238
+ createGitHubAuthStore,
1239
+ createEnvGitHubCredentialProvider,
1240
+ createDefaultPiIssueAnalysisCommandRunner,
1241
+ createContinuousIssueAnalysisRunner,
1242
+ createConfiguredIssueAnalysisRunner,
1243
+ copyGitHubAuthStateToLocalProjectRoot,
1244
+ checkGitHubRepoPermissions,
1245
+ beginGitHubDeviceFlow,
1246
+ GITHUB_PROVIDER_PLUGIN_NAME
1247
+ };