git-workspace-service 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2377 @@
1
+ import * as crypto from 'crypto';
2
+ import { randomUUID } from 'crypto';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import * as fs3 from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as fs from 'fs';
8
+ import * as os from 'os';
9
+
10
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
11
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
12
+ }) : x)(function(x) {
13
+ if (typeof require !== "undefined") return require.apply(this, arguments);
14
+ throw Error('Dynamic require of "' + x + '" is not supported');
15
+ });
16
+
17
+ // src/utils/branch-naming.ts
18
+ var DEFAULT_BRANCH_PREFIX = "parallax";
19
+ var DEFAULT_OPTIONS = {
20
+ maxSlugLength: 30,
21
+ prefix: DEFAULT_BRANCH_PREFIX
22
+ };
23
+ function generateBranchName(config, options) {
24
+ const opts = { ...DEFAULT_OPTIONS, ...options };
25
+ const role = sanitizeForBranch(config.role);
26
+ const slug = config.slug ? sanitizeForBranch(config.slug, opts.maxSlugLength) : "";
27
+ const parts = [opts.prefix, config.executionId, role];
28
+ if (slug) {
29
+ parts[2] = `${role}-${slug}`;
30
+ }
31
+ return parts.join("/");
32
+ }
33
+ function parseBranchName(branchName, options) {
34
+ const opts = { ...DEFAULT_OPTIONS, ...options };
35
+ if (!branchName.startsWith(`${opts.prefix}/`)) {
36
+ return null;
37
+ }
38
+ const parts = branchName.split("/");
39
+ if (parts.length < 3) {
40
+ return null;
41
+ }
42
+ const [, executionId, roleAndSlug] = parts;
43
+ const dashIndex = roleAndSlug.indexOf("-");
44
+ if (dashIndex === -1) {
45
+ return { executionId, role: roleAndSlug };
46
+ }
47
+ return {
48
+ executionId,
49
+ role: roleAndSlug.substring(0, dashIndex),
50
+ slug: roleAndSlug.substring(dashIndex + 1)
51
+ };
52
+ }
53
+ function isManagedBranch(branchName, options) {
54
+ const opts = { ...DEFAULT_OPTIONS, ...options };
55
+ return branchName.startsWith(`${opts.prefix}/`);
56
+ }
57
+ function filterBranchesByExecution(branches, executionId, options) {
58
+ const opts = { ...DEFAULT_OPTIONS, ...options };
59
+ const prefix = `${opts.prefix}/${executionId}/`;
60
+ return branches.filter((b) => b.startsWith(prefix));
61
+ }
62
+ function createBranchInfo(config, options) {
63
+ return {
64
+ name: generateBranchName(config, options),
65
+ executionId: config.executionId,
66
+ baseBranch: config.baseBranch,
67
+ createdAt: /* @__PURE__ */ new Date()
68
+ };
69
+ }
70
+ function sanitizeForBranch(input, maxLength) {
71
+ let result = input.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
72
+ if (maxLength && result.length > maxLength) {
73
+ result = result.substring(0, maxLength).replace(/-$/, "");
74
+ }
75
+ return result;
76
+ }
77
+ function generateSlug(description, maxLength = 30) {
78
+ const words = description.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2).filter((w) => !STOP_WORDS.has(w)).slice(0, 4);
79
+ return sanitizeForBranch(words.join("-"), maxLength);
80
+ }
81
+ var STOP_WORDS = /* @__PURE__ */ new Set([
82
+ "the",
83
+ "and",
84
+ "for",
85
+ "with",
86
+ "this",
87
+ "that",
88
+ "from",
89
+ "have",
90
+ "has",
91
+ "will",
92
+ "would",
93
+ "could",
94
+ "should",
95
+ "been",
96
+ "being",
97
+ "into",
98
+ "than",
99
+ "then",
100
+ "when",
101
+ "where",
102
+ "which",
103
+ "while",
104
+ "about",
105
+ "after",
106
+ "before"
107
+ ]);
108
+ function createNodeCredentialHelperScript(contextFilePath) {
109
+ return `#!/usr/bin/env node
110
+ const fs = require('fs');
111
+ const readline = require('readline');
112
+
113
+ const contextFile = '${contextFilePath}';
114
+
115
+ async function main() {
116
+ // Parse input from git
117
+ const rl = readline.createInterface({ input: process.stdin });
118
+ const request = {};
119
+
120
+ for await (const line of rl) {
121
+ if (!line.trim()) break;
122
+ const [key, value] = line.split('=');
123
+ if (key && value !== undefined) {
124
+ request[key.trim()] = value.trim();
125
+ }
126
+ }
127
+
128
+ // Read context file
129
+ try {
130
+ const context = JSON.parse(fs.readFileSync(contextFile, 'utf8'));
131
+
132
+ // Check expiration
133
+ if (new Date(context.expiresAt) < new Date()) {
134
+ console.error('Credential expired');
135
+ process.exit(1);
136
+ }
137
+
138
+ // Output credentials
139
+ console.log('username=x-access-token');
140
+ console.log('password=' + context.token);
141
+ console.log('');
142
+ } catch (error) {
143
+ console.error('Failed to read credentials:', error.message);
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ main();
149
+ `;
150
+ }
151
+ function createShellCredentialHelperScript(contextFilePath) {
152
+ return `#!/bin/sh
153
+ # Git Credential Helper
154
+ # Reads credentials from: ${contextFilePath}
155
+
156
+ # Read the context file
157
+ if [ ! -f "${contextFilePath}" ]; then
158
+ echo "Credential context file not found" >&2
159
+ exit 1
160
+ fi
161
+
162
+ # Parse JSON and extract token (using basic shell tools)
163
+ TOKEN=$(cat "${contextFilePath}" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
164
+ EXPIRES=$(cat "${contextFilePath}" | grep -o '"expiresAt":"[^"]*"' | cut -d'"' -f4)
165
+
166
+ # Check if we got a token
167
+ if [ -z "$TOKEN" ]; then
168
+ echo "No token found in credential context" >&2
169
+ exit 1
170
+ fi
171
+
172
+ # Output credentials in git credential format
173
+ echo "username=x-access-token"
174
+ echo "password=$TOKEN"
175
+ echo ""
176
+ `;
177
+ }
178
+ async function configureCredentialHelper(workspacePath, context) {
179
+ const helperDir = path.join(workspacePath, ".git-workspace");
180
+ await fs.promises.mkdir(helperDir, { recursive: true });
181
+ const contextFilePath = path.join(helperDir, "credential-context.json");
182
+ await fs.promises.writeFile(
183
+ contextFilePath,
184
+ JSON.stringify(context, null, 2),
185
+ { mode: 384 }
186
+ // Read/write only for owner
187
+ );
188
+ const helperScriptPath = path.join(helperDir, "git-credential-helper");
189
+ const helperScript = createShellCredentialHelperScript(contextFilePath);
190
+ await fs.promises.writeFile(helperScriptPath, helperScript, { mode: 448 });
191
+ return helperScriptPath;
192
+ }
193
+ async function updateCredentials(workspacePath, newToken, newExpiresAt) {
194
+ const contextFilePath = path.join(workspacePath, ".git-workspace", "credential-context.json");
195
+ const existingContent = await fs.promises.readFile(contextFilePath, "utf8");
196
+ const context = JSON.parse(existingContent);
197
+ context.token = newToken;
198
+ context.expiresAt = newExpiresAt;
199
+ await fs.promises.writeFile(
200
+ contextFilePath,
201
+ JSON.stringify(context, null, 2),
202
+ { mode: 384 }
203
+ );
204
+ }
205
+ async function cleanupCredentialFiles(workspacePath) {
206
+ const helperDir = path.join(workspacePath, ".git-workspace");
207
+ try {
208
+ await fs.promises.rm(helperDir, { recursive: true, force: true });
209
+ } catch {
210
+ }
211
+ }
212
+ function getGitCredentialConfig(helperScriptPath) {
213
+ return [
214
+ // Clear any existing helpers
215
+ `git config credential.helper ''`,
216
+ // Use our custom helper script
217
+ `git config --add credential.helper '!${helperScriptPath}'`,
218
+ // Disable interactive prompts
219
+ `git config credential.interactive false`
220
+ ];
221
+ }
222
+ function outputCredentials(username, password) {
223
+ console.log(`username=${username}`);
224
+ console.log(`password=${password}`);
225
+ console.log("");
226
+ }
227
+
228
+ // src/workspace-service.ts
229
+ var execAsync = promisify(exec);
230
+ var WorkspaceService = class {
231
+ workspaces = /* @__PURE__ */ new Map();
232
+ baseDir;
233
+ branchPrefix;
234
+ credentialService;
235
+ logger;
236
+ eventHandlers = /* @__PURE__ */ new Set();
237
+ constructor(options) {
238
+ this.baseDir = options.config.baseDir;
239
+ this.branchPrefix = options.config.branchPrefix || "parallax";
240
+ this.credentialService = options.credentialService;
241
+ this.logger = options.logger;
242
+ }
243
+ /**
244
+ * Initialize the workspace service
245
+ */
246
+ async initialize() {
247
+ await fs3.mkdir(this.baseDir, { recursive: true });
248
+ this.log("info", { baseDir: this.baseDir }, "Workspace service initialized");
249
+ }
250
+ /**
251
+ * Register an event handler
252
+ */
253
+ onEvent(handler) {
254
+ this.eventHandlers.add(handler);
255
+ return () => this.eventHandlers.delete(handler);
256
+ }
257
+ /**
258
+ * Provision a new workspace for a task
259
+ */
260
+ async provision(config) {
261
+ const workspaceId = randomUUID();
262
+ this.log(
263
+ "info",
264
+ {
265
+ workspaceId,
266
+ repo: config.repo,
267
+ executionId: config.execution.id,
268
+ role: config.task.role
269
+ },
270
+ "Provisioning workspace"
271
+ );
272
+ await this.emitEvent({
273
+ type: "workspace:provisioning",
274
+ workspaceId,
275
+ executionId: config.execution.id,
276
+ timestamp: /* @__PURE__ */ new Date()
277
+ });
278
+ const workspacePath = path.join(this.baseDir, workspaceId);
279
+ await fs3.mkdir(workspacePath, { recursive: true });
280
+ const credential = await this.credentialService.getCredentials({
281
+ repo: config.repo,
282
+ access: "write",
283
+ context: {
284
+ executionId: config.execution.id,
285
+ taskId: config.task.id,
286
+ userId: config.user?.id,
287
+ reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
288
+ },
289
+ // Pass user-provided credentials if available
290
+ userProvided: config.userCredentials
291
+ });
292
+ await this.emitEvent({
293
+ type: "credential:granted",
294
+ workspaceId,
295
+ credentialId: credential.id,
296
+ executionId: config.execution.id,
297
+ timestamp: /* @__PURE__ */ new Date()
298
+ });
299
+ const branchInfo = createBranchInfo(
300
+ {
301
+ executionId: config.execution.id,
302
+ role: config.task.role,
303
+ slug: config.task.slug,
304
+ baseBranch: config.baseBranch
305
+ },
306
+ { prefix: this.branchPrefix }
307
+ );
308
+ const workspace = {
309
+ id: workspaceId,
310
+ path: workspacePath,
311
+ repo: config.repo,
312
+ branch: branchInfo,
313
+ credential,
314
+ provisionedAt: /* @__PURE__ */ new Date(),
315
+ status: "provisioning"
316
+ };
317
+ this.workspaces.set(workspaceId, workspace);
318
+ try {
319
+ await this.cloneRepo(workspace, credential.token);
320
+ await this.createBranch(workspace);
321
+ await this.configureGit(workspace);
322
+ workspace.status = "ready";
323
+ this.workspaces.set(workspaceId, workspace);
324
+ this.log(
325
+ "info",
326
+ {
327
+ workspaceId,
328
+ path: workspacePath,
329
+ branch: branchInfo.name
330
+ },
331
+ "Workspace provisioned"
332
+ );
333
+ await this.emitEvent({
334
+ type: "workspace:ready",
335
+ workspaceId,
336
+ executionId: config.execution.id,
337
+ timestamp: /* @__PURE__ */ new Date()
338
+ });
339
+ return workspace;
340
+ } catch (error) {
341
+ workspace.status = "error";
342
+ this.workspaces.set(workspaceId, workspace);
343
+ const errorMessage = error instanceof Error ? error.message : String(error);
344
+ this.log("error", { workspaceId, error: errorMessage }, "Failed to provision workspace");
345
+ await this.emitEvent({
346
+ type: "workspace:error",
347
+ workspaceId,
348
+ executionId: config.execution.id,
349
+ timestamp: /* @__PURE__ */ new Date(),
350
+ error: errorMessage
351
+ });
352
+ throw error;
353
+ }
354
+ }
355
+ /**
356
+ * Finalize a workspace (push, create PR, cleanup)
357
+ */
358
+ async finalize(workspaceId, options) {
359
+ const workspace = this.workspaces.get(workspaceId);
360
+ if (!workspace) {
361
+ throw new Error(`Workspace not found: ${workspaceId}`);
362
+ }
363
+ workspace.status = "finalizing";
364
+ this.workspaces.set(workspaceId, workspace);
365
+ this.log(
366
+ "info",
367
+ {
368
+ workspaceId,
369
+ push: options.push,
370
+ createPr: options.createPr
371
+ },
372
+ "Finalizing workspace"
373
+ );
374
+ await this.emitEvent({
375
+ type: "workspace:finalizing",
376
+ workspaceId,
377
+ executionId: workspace.branch.executionId,
378
+ timestamp: /* @__PURE__ */ new Date()
379
+ });
380
+ let pr;
381
+ try {
382
+ if (options.push) {
383
+ await this.pushBranch(workspace);
384
+ }
385
+ if (options.createPr && options.pr) {
386
+ pr = await this.createPullRequest(workspace, options.pr);
387
+ workspace.branch.pullRequest = pr;
388
+ await this.emitEvent({
389
+ type: "pr:created",
390
+ workspaceId,
391
+ executionId: workspace.branch.executionId,
392
+ timestamp: /* @__PURE__ */ new Date(),
393
+ data: {
394
+ prNumber: pr.number,
395
+ prUrl: pr.url
396
+ }
397
+ });
398
+ }
399
+ if (options.cleanup) {
400
+ await this.cleanup(workspaceId);
401
+ } else {
402
+ workspace.status = "ready";
403
+ this.workspaces.set(workspaceId, workspace);
404
+ }
405
+ return pr;
406
+ } catch (error) {
407
+ const errorMessage = error instanceof Error ? error.message : String(error);
408
+ this.log("error", { workspaceId, error: errorMessage }, "Failed to finalize workspace");
409
+ throw error;
410
+ }
411
+ }
412
+ /**
413
+ * Get a workspace by ID
414
+ */
415
+ get(workspaceId) {
416
+ return this.workspaces.get(workspaceId) || null;
417
+ }
418
+ /**
419
+ * Get all workspaces for an execution
420
+ */
421
+ getForExecution(executionId) {
422
+ return Array.from(this.workspaces.values()).filter(
423
+ (w) => w.branch.executionId === executionId
424
+ );
425
+ }
426
+ /**
427
+ * Clean up a workspace
428
+ */
429
+ async cleanup(workspaceId) {
430
+ const workspace = this.workspaces.get(workspaceId);
431
+ if (!workspace) {
432
+ return;
433
+ }
434
+ this.log("info", { workspaceId }, "Cleaning up workspace");
435
+ try {
436
+ await cleanupCredentialFiles(workspace.path);
437
+ } catch (error) {
438
+ const errorMessage = error instanceof Error ? error.message : String(error);
439
+ this.log("warn", { workspaceId, error: errorMessage }, "Failed to clean up credential files");
440
+ }
441
+ await this.credentialService.revokeCredential(workspace.credential.id);
442
+ await this.emitEvent({
443
+ type: "credential:revoked",
444
+ workspaceId,
445
+ credentialId: workspace.credential.id,
446
+ executionId: workspace.branch.executionId,
447
+ timestamp: /* @__PURE__ */ new Date()
448
+ });
449
+ try {
450
+ await fs3.rm(workspace.path, { recursive: true, force: true });
451
+ } catch (error) {
452
+ const errorMessage = error instanceof Error ? error.message : String(error);
453
+ this.log("warn", { workspaceId, error: errorMessage }, "Failed to remove workspace directory");
454
+ }
455
+ workspace.status = "cleaned_up";
456
+ this.workspaces.set(workspaceId, workspace);
457
+ await this.emitEvent({
458
+ type: "workspace:cleaned_up",
459
+ workspaceId,
460
+ executionId: workspace.branch.executionId,
461
+ timestamp: /* @__PURE__ */ new Date()
462
+ });
463
+ }
464
+ /**
465
+ * Clean up all workspaces for an execution
466
+ */
467
+ async cleanupForExecution(executionId) {
468
+ const workspaces = this.getForExecution(executionId);
469
+ await Promise.all(workspaces.map((w) => this.cleanup(w.id)));
470
+ await this.credentialService.revokeForExecution(executionId);
471
+ }
472
+ // ─────────────────────────────────────────────────────────────
473
+ // Private Methods
474
+ // ─────────────────────────────────────────────────────────────
475
+ async cloneRepo(workspace, token) {
476
+ const cloneUrl = token ? this.buildAuthenticatedUrl(workspace.repo, token) : workspace.repo;
477
+ await this.execInDir(
478
+ workspace.path,
479
+ `git clone --depth 1 --branch ${workspace.branch.baseBranch} ${cloneUrl} .`
480
+ );
481
+ }
482
+ async createBranch(workspace) {
483
+ await this.execInDir(workspace.path, `git checkout -b ${workspace.branch.name}`);
484
+ }
485
+ async configureGit(workspace) {
486
+ await this.execInDir(workspace.path, 'git config user.name "Workspace Agent"');
487
+ await this.execInDir(workspace.path, 'git config user.email "agent@workspace.local"');
488
+ if (!workspace.credential.token) {
489
+ this.log(
490
+ "debug",
491
+ { workspaceId: workspace.id },
492
+ "Using SSH authentication, skipping credential helper"
493
+ );
494
+ return;
495
+ }
496
+ const credentialContext = {
497
+ workspaceId: workspace.id,
498
+ executionId: workspace.branch.executionId,
499
+ repo: workspace.repo,
500
+ token: workspace.credential.token,
501
+ expiresAt: workspace.credential.expiresAt.toISOString()
502
+ };
503
+ const helperScriptPath = await configureCredentialHelper(
504
+ workspace.path,
505
+ credentialContext
506
+ );
507
+ const configCommands = getGitCredentialConfig(helperScriptPath);
508
+ for (const cmd of configCommands) {
509
+ await this.execInDir(workspace.path, cmd);
510
+ }
511
+ this.log(
512
+ "debug",
513
+ { workspaceId: workspace.id, helperPath: helperScriptPath },
514
+ "Git credential helper configured"
515
+ );
516
+ }
517
+ async pushBranch(workspace) {
518
+ await this.execInDir(workspace.path, `git push -u origin ${workspace.branch.name}`);
519
+ }
520
+ async createPullRequest(workspace, config) {
521
+ const repoInfo = this.parseRepo(workspace.repo);
522
+ if (!repoInfo) {
523
+ throw new Error(`Invalid repository format: ${workspace.repo}`);
524
+ }
525
+ const provider = this.credentialService.getProvider(workspace.credential.provider);
526
+ if (!provider) {
527
+ throw new Error(`Provider not configured: ${workspace.credential.provider}`);
528
+ }
529
+ const pr = await provider.createPullRequest({
530
+ repo: workspace.repo,
531
+ sourceBranch: workspace.branch.name,
532
+ targetBranch: config.targetBranch,
533
+ title: config.title,
534
+ body: config.body,
535
+ draft: config.draft,
536
+ labels: config.labels,
537
+ reviewers: config.reviewers,
538
+ credential: workspace.credential
539
+ });
540
+ pr.executionId = workspace.branch.executionId;
541
+ this.log(
542
+ "info",
543
+ {
544
+ workspaceId: workspace.id,
545
+ prNumber: pr.number,
546
+ prUrl: pr.url
547
+ },
548
+ "Pull request created"
549
+ );
550
+ return pr;
551
+ }
552
+ parseRepo(repo) {
553
+ const patterns = [/github\.com[/:]([^/]+)\/([^/.]+)/, /^([^/]+)\/([^/]+)$/];
554
+ for (const pattern of patterns) {
555
+ const match = repo.match(pattern);
556
+ if (match) {
557
+ return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
558
+ }
559
+ }
560
+ return null;
561
+ }
562
+ buildAuthenticatedUrl(repo, token) {
563
+ let url = repo;
564
+ if (url.startsWith("git@github.com:")) {
565
+ url = url.replace("git@github.com:", "https://github.com/");
566
+ }
567
+ if (!url.endsWith(".git")) {
568
+ url = `${url}.git`;
569
+ }
570
+ if (!url.startsWith("https://")) {
571
+ url = `https://${url}`;
572
+ }
573
+ url = url.replace("https://", `https://x-access-token:${token}@`);
574
+ return url;
575
+ }
576
+ async execInDir(dir, command) {
577
+ const safeCommand = command.replace(/x-access-token:[^@]+@/g, "x-access-token:***@");
578
+ this.log("debug", { dir, command: safeCommand }, "Executing git command");
579
+ const { stdout, stderr } = await execAsync(command, { cwd: dir });
580
+ if (stderr && !stderr.includes("Cloning into")) {
581
+ this.log("debug", { stderr: stderr.substring(0, 200) }, "Git stderr");
582
+ }
583
+ return stdout;
584
+ }
585
+ log(level, data, message) {
586
+ if (this.logger) {
587
+ this.logger[level](data, message);
588
+ }
589
+ }
590
+ async emitEvent(event) {
591
+ for (const handler of this.eventHandlers) {
592
+ try {
593
+ await handler(event);
594
+ } catch (error) {
595
+ const errorMessage = error instanceof Error ? error.message : String(error);
596
+ this.log("warn", { event: event.type, error: errorMessage }, "Event handler error");
597
+ }
598
+ }
599
+ }
600
+ };
601
+
602
+ // src/oauth/device-flow.ts
603
+ var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
604
+ var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
605
+ var ERROR_AUTHORIZATION_PENDING = "authorization_pending";
606
+ var ERROR_SLOW_DOWN = "slow_down";
607
+ var ERROR_EXPIRED_TOKEN = "expired_token";
608
+ var ERROR_ACCESS_DENIED = "access_denied";
609
+ var OAuthDeviceFlow = class {
610
+ clientId;
611
+ clientSecret;
612
+ provider;
613
+ permissions;
614
+ promptEmitter;
615
+ timeout;
616
+ logger;
617
+ constructor(config, logger) {
618
+ this.clientId = config.clientId;
619
+ this.clientSecret = config.clientSecret;
620
+ this.provider = config.provider || "github";
621
+ this.permissions = config.permissions || {
622
+ repositories: { type: "all" },
623
+ contents: "write",
624
+ pullRequests: "write",
625
+ issues: "write",
626
+ metadata: "read",
627
+ canDeleteBranch: true,
628
+ canForcePush: false,
629
+ canDeleteRepository: false,
630
+ canAdminister: false
631
+ };
632
+ this.promptEmitter = config.promptEmitter;
633
+ this.timeout = config.timeout || 900;
634
+ this.logger = logger;
635
+ }
636
+ /**
637
+ * Start the device code flow and wait for authorization
638
+ */
639
+ async authorize() {
640
+ this.log("info", {}, "Starting OAuth device code flow");
641
+ const deviceCode = await this.requestDeviceCode();
642
+ const prompt = {
643
+ provider: this.provider,
644
+ verificationUri: deviceCode.verificationUri,
645
+ userCode: deviceCode.userCode,
646
+ expiresIn: deviceCode.expiresIn,
647
+ requestedPermissions: this.permissions
648
+ };
649
+ if (this.promptEmitter) {
650
+ this.promptEmitter.onAuthRequired(prompt);
651
+ } else {
652
+ this.printAuthPrompt(prompt);
653
+ }
654
+ try {
655
+ const token = await this.pollForToken(deviceCode);
656
+ const result = {
657
+ success: true,
658
+ provider: this.provider
659
+ };
660
+ if (this.promptEmitter) {
661
+ this.promptEmitter.onAuthComplete(result);
662
+ }
663
+ this.log("info", {}, "OAuth authorization successful");
664
+ return token;
665
+ } catch (error) {
666
+ const result = {
667
+ success: false,
668
+ provider: this.provider,
669
+ error: error instanceof Error ? error.message : String(error)
670
+ };
671
+ if (this.promptEmitter) {
672
+ this.promptEmitter.onAuthComplete(result);
673
+ }
674
+ throw error;
675
+ }
676
+ }
677
+ /**
678
+ * Request a device code from GitHub
679
+ */
680
+ async requestDeviceCode() {
681
+ const scope = this.permissionsToScopes(this.permissions);
682
+ this.log("debug", { scope }, "Requesting device code");
683
+ const response = await fetch(GITHUB_DEVICE_CODE_URL, {
684
+ method: "POST",
685
+ headers: {
686
+ Accept: "application/json",
687
+ "Content-Type": "application/json"
688
+ },
689
+ body: JSON.stringify({
690
+ client_id: this.clientId,
691
+ scope
692
+ })
693
+ });
694
+ if (!response.ok) {
695
+ const text = await response.text();
696
+ throw new Error(`Failed to request device code: ${response.status} ${text}`);
697
+ }
698
+ const data = await response.json();
699
+ return {
700
+ deviceCode: data.device_code,
701
+ userCode: data.user_code,
702
+ verificationUri: data.verification_uri,
703
+ verificationUriComplete: data.verification_uri_complete,
704
+ expiresIn: data.expires_in,
705
+ interval: data.interval || 5
706
+ };
707
+ }
708
+ /**
709
+ * Poll for the access token until authorized or timeout
710
+ */
711
+ async pollForToken(deviceCode) {
712
+ const startTime = Date.now();
713
+ let interval = deviceCode.interval * 1e3;
714
+ const expiresAt = startTime + deviceCode.expiresIn * 1e3;
715
+ while (Date.now() < expiresAt) {
716
+ if (Date.now() - startTime > this.timeout * 1e3) {
717
+ throw new Error("OAuth flow timed out");
718
+ }
719
+ await this.sleep(interval);
720
+ const secondsRemaining = Math.ceil((expiresAt - Date.now()) / 1e3);
721
+ if (this.promptEmitter?.onAuthPending) {
722
+ this.promptEmitter.onAuthPending(secondsRemaining);
723
+ }
724
+ try {
725
+ const token = await this.exchangeDeviceCode(deviceCode.deviceCode);
726
+ return token;
727
+ } catch (error) {
728
+ if (error instanceof DeviceFlowError) {
729
+ switch (error.code) {
730
+ case ERROR_AUTHORIZATION_PENDING:
731
+ this.log("debug", {}, "Authorization pending, continuing to poll");
732
+ continue;
733
+ case ERROR_SLOW_DOWN:
734
+ interval += 5e3;
735
+ this.log("debug", { interval }, "Slowing down polling");
736
+ continue;
737
+ case ERROR_EXPIRED_TOKEN:
738
+ throw new Error("Device code expired. Please try again.");
739
+ case ERROR_ACCESS_DENIED:
740
+ throw new Error("User denied authorization");
741
+ default:
742
+ throw error;
743
+ }
744
+ }
745
+ throw error;
746
+ }
747
+ }
748
+ throw new Error("Device code expired");
749
+ }
750
+ /**
751
+ * Exchange device code for access token
752
+ */
753
+ async exchangeDeviceCode(deviceCode) {
754
+ const body = {
755
+ client_id: this.clientId,
756
+ device_code: deviceCode,
757
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
758
+ };
759
+ if (this.clientSecret) {
760
+ body.client_secret = this.clientSecret;
761
+ }
762
+ const response = await fetch(GITHUB_TOKEN_URL, {
763
+ method: "POST",
764
+ headers: {
765
+ Accept: "application/json",
766
+ "Content-Type": "application/json"
767
+ },
768
+ body: JSON.stringify(body)
769
+ });
770
+ const data = await response.json();
771
+ if (data.error) {
772
+ throw new DeviceFlowError(data.error, data.error_description);
773
+ }
774
+ const scopes = (data.scope || "").split(/[,\s]+/).filter(Boolean);
775
+ return {
776
+ accessToken: data.access_token,
777
+ tokenType: data.token_type || "bearer",
778
+ scopes,
779
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
780
+ refreshToken: data.refresh_token,
781
+ provider: this.provider,
782
+ permissions: this.permissions,
783
+ createdAt: /* @__PURE__ */ new Date()
784
+ };
785
+ }
786
+ /**
787
+ * Refresh an expired token
788
+ */
789
+ async refreshToken(refreshTokenValue) {
790
+ this.log("info", {}, "Refreshing OAuth token");
791
+ const body = {
792
+ client_id: this.clientId,
793
+ grant_type: "refresh_token",
794
+ refresh_token: refreshTokenValue
795
+ };
796
+ if (this.clientSecret) {
797
+ body.client_secret = this.clientSecret;
798
+ }
799
+ const response = await fetch(GITHUB_TOKEN_URL, {
800
+ method: "POST",
801
+ headers: {
802
+ Accept: "application/json",
803
+ "Content-Type": "application/json"
804
+ },
805
+ body: JSON.stringify(body)
806
+ });
807
+ if (!response.ok) {
808
+ const text = await response.text();
809
+ throw new Error(`Failed to refresh token: ${response.status} ${text}`);
810
+ }
811
+ const data = await response.json();
812
+ if (data.error) {
813
+ throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
814
+ }
815
+ const scopes = (data.scope || "").split(/[,\s]+/).filter(Boolean);
816
+ return {
817
+ accessToken: data.access_token,
818
+ tokenType: data.token_type || "bearer",
819
+ scopes,
820
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
821
+ refreshToken: data.refresh_token || refreshTokenValue,
822
+ provider: this.provider,
823
+ permissions: this.permissions,
824
+ createdAt: /* @__PURE__ */ new Date()
825
+ };
826
+ }
827
+ /**
828
+ * Convert AgentPermissions to GitHub OAuth scopes
829
+ */
830
+ permissionsToScopes(permissions) {
831
+ const scopes = [];
832
+ if (permissions.contents === "write") {
833
+ scopes.push("repo");
834
+ } else if (permissions.contents === "read") {
835
+ scopes.push("public_repo");
836
+ }
837
+ if (permissions.pullRequests !== "none" && !scopes.includes("repo")) {
838
+ scopes.push("public_repo");
839
+ }
840
+ scopes.push("read:user");
841
+ return scopes.join(" ");
842
+ }
843
+ /**
844
+ * Print auth prompt to console (default behavior)
845
+ */
846
+ printAuthPrompt(prompt) {
847
+ console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
848
+ console.log("\u2502 GitHub Authorization Required \u2502");
849
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
850
+ console.log(`\u2502 1. Go to: ${prompt.verificationUri.padEnd(35)}\u2502`);
851
+ console.log(`\u2502 2. Enter code: ${prompt.userCode.padEnd(29)}\u2502`);
852
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
853
+ console.log(`\u2502 \u23F3 Waiting for authorization... \u2502`);
854
+ console.log(`\u2502 Code expires in ${prompt.expiresIn} seconds${" ".repeat(18)}\u2502`);
855
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
856
+ }
857
+ sleep(ms) {
858
+ return new Promise((resolve) => setTimeout(resolve, ms));
859
+ }
860
+ log(level, data, message) {
861
+ if (this.logger) {
862
+ this.logger[level](data, message);
863
+ }
864
+ }
865
+ };
866
+ var DeviceFlowError = class extends Error {
867
+ constructor(code, description) {
868
+ super(description || code);
869
+ this.code = code;
870
+ this.description = description;
871
+ this.name = "DeviceFlowError";
872
+ }
873
+ };
874
+ var TokenStore = class {
875
+ /**
876
+ * Check if a token is expired
877
+ */
878
+ isExpired(token) {
879
+ if (!token.expiresAt) {
880
+ return false;
881
+ }
882
+ const buffer = 5 * 60 * 1e3;
883
+ return Date.now() > token.expiresAt.getTime() - buffer;
884
+ }
885
+ /**
886
+ * Check if a token needs refresh (expired or close to expiry)
887
+ */
888
+ needsRefresh(token) {
889
+ if (!token.expiresAt) {
890
+ return false;
891
+ }
892
+ const buffer = 10 * 60 * 1e3;
893
+ return Date.now() > token.expiresAt.getTime() - buffer;
894
+ }
895
+ };
896
+ var MemoryTokenStore = class extends TokenStore {
897
+ tokens = /* @__PURE__ */ new Map();
898
+ async save(provider, token) {
899
+ this.tokens.set(provider, token);
900
+ }
901
+ async get(provider) {
902
+ return this.tokens.get(provider) || null;
903
+ }
904
+ async clear(provider) {
905
+ if (provider) {
906
+ this.tokens.delete(provider);
907
+ } else {
908
+ this.tokens.clear();
909
+ }
910
+ }
911
+ async list() {
912
+ return Array.from(this.tokens.keys());
913
+ }
914
+ };
915
+ var FileTokenStore = class extends TokenStore {
916
+ directory;
917
+ encryptionKey;
918
+ constructor(options = {}) {
919
+ super();
920
+ this.directory = options.directory || path.join(os.homedir(), ".parallax", "tokens");
921
+ if (options.encryptionKey) {
922
+ this.encryptionKey = crypto.createHash("sha256").update(options.encryptionKey).digest();
923
+ }
924
+ }
925
+ async save(provider, token) {
926
+ await this.ensureDirectory();
927
+ const filePath = this.getTokenPath(provider);
928
+ const serialized = JSON.stringify({
929
+ ...token,
930
+ expiresAt: token.expiresAt?.toISOString(),
931
+ createdAt: token.createdAt.toISOString()
932
+ });
933
+ const data = this.encryptionKey ? this.encrypt(serialized) : serialized;
934
+ await fs3.writeFile(filePath, data, "utf-8");
935
+ }
936
+ async get(provider) {
937
+ const filePath = this.getTokenPath(provider);
938
+ try {
939
+ const data = await fs3.readFile(filePath, "utf-8");
940
+ const serialized = this.encryptionKey ? this.decrypt(data) : data;
941
+ const parsed = JSON.parse(serialized);
942
+ return {
943
+ ...parsed,
944
+ expiresAt: parsed.expiresAt ? new Date(parsed.expiresAt) : void 0,
945
+ createdAt: new Date(parsed.createdAt)
946
+ };
947
+ } catch (error) {
948
+ if (error.code === "ENOENT") {
949
+ return null;
950
+ }
951
+ throw error;
952
+ }
953
+ }
954
+ async clear(provider) {
955
+ if (provider) {
956
+ const filePath = this.getTokenPath(provider);
957
+ try {
958
+ await fs3.unlink(filePath);
959
+ } catch (error) {
960
+ if (error.code !== "ENOENT") {
961
+ throw error;
962
+ }
963
+ }
964
+ } else {
965
+ try {
966
+ const files = await fs3.readdir(this.directory);
967
+ await Promise.all(
968
+ files.filter((f) => f.endsWith(".token")).map((f) => fs3.unlink(path.join(this.directory, f)))
969
+ );
970
+ } catch (error) {
971
+ if (error.code !== "ENOENT") {
972
+ throw error;
973
+ }
974
+ }
975
+ }
976
+ }
977
+ async list() {
978
+ try {
979
+ const files = await fs3.readdir(this.directory);
980
+ return files.filter((f) => f.endsWith(".token")).map((f) => f.replace(".token", ""));
981
+ } catch {
982
+ return [];
983
+ }
984
+ }
985
+ getTokenPath(provider) {
986
+ return path.join(this.directory, `${provider}.token`);
987
+ }
988
+ async ensureDirectory() {
989
+ await fs3.mkdir(this.directory, { recursive: true, mode: 448 });
990
+ }
991
+ encrypt(data) {
992
+ if (!this.encryptionKey) {
993
+ return data;
994
+ }
995
+ const iv = crypto.randomBytes(16);
996
+ const cipher = crypto.createCipheriv("aes-256-cbc", this.encryptionKey, iv);
997
+ let encrypted = cipher.update(data, "utf8", "hex");
998
+ encrypted += cipher.final("hex");
999
+ return iv.toString("hex") + ":" + encrypted;
1000
+ }
1001
+ decrypt(data) {
1002
+ if (!this.encryptionKey) {
1003
+ return data;
1004
+ }
1005
+ const [ivHex, encrypted] = data.split(":");
1006
+ const iv = Buffer.from(ivHex, "hex");
1007
+ const decipher = crypto.createDecipheriv("aes-256-cbc", this.encryptionKey, iv);
1008
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
1009
+ decrypted += decipher.final("utf8");
1010
+ return decrypted;
1011
+ }
1012
+ };
1013
+
1014
+ // src/credential-service.ts
1015
+ var CredentialService = class {
1016
+ grants = /* @__PURE__ */ new Map();
1017
+ defaultTtl;
1018
+ maxTtl;
1019
+ providers;
1020
+ grantStore;
1021
+ tokenStore;
1022
+ oauthConfig;
1023
+ logger;
1024
+ constructor(options = {}) {
1025
+ this.defaultTtl = options.defaultTtlSeconds || 3600;
1026
+ this.maxTtl = options.maxTtlSeconds || 3600;
1027
+ this.providers = options.providers || /* @__PURE__ */ new Map();
1028
+ this.grantStore = options.grantStore;
1029
+ this.tokenStore = options.tokenStore || new MemoryTokenStore();
1030
+ this.oauthConfig = options.oauth;
1031
+ this.logger = options.logger;
1032
+ }
1033
+ /**
1034
+ * Get the token store (for external access to cached tokens)
1035
+ */
1036
+ getTokenStore() {
1037
+ return this.tokenStore;
1038
+ }
1039
+ /**
1040
+ * Register a provider adapter
1041
+ */
1042
+ registerProvider(provider) {
1043
+ this.providers.set(provider.name, provider);
1044
+ this.log("info", { provider: provider.name }, "Provider registered");
1045
+ }
1046
+ /**
1047
+ * Get a provider adapter
1048
+ */
1049
+ getProvider(name) {
1050
+ return this.providers.get(name);
1051
+ }
1052
+ /**
1053
+ * Request credentials for a repository
1054
+ */
1055
+ async getCredentials(request) {
1056
+ const provider = this.detectProvider(request.repo);
1057
+ this.log(
1058
+ "info",
1059
+ {
1060
+ repo: request.repo,
1061
+ provider,
1062
+ access: request.access,
1063
+ executionId: request.context.executionId
1064
+ },
1065
+ "Credential request"
1066
+ );
1067
+ const ttlSeconds = Math.min(
1068
+ request.ttlSeconds || this.defaultTtl,
1069
+ this.maxTtl
1070
+ );
1071
+ let credential = null;
1072
+ if (request.userProvided) {
1073
+ credential = this.createUserProvidedCredential(request, ttlSeconds, provider);
1074
+ this.log(
1075
+ "info",
1076
+ { repo: request.repo, type: request.userProvided.type },
1077
+ "Using user-provided credentials"
1078
+ );
1079
+ }
1080
+ if (!credential) {
1081
+ credential = await this.getCachedOAuthCredential(provider, request, ttlSeconds);
1082
+ }
1083
+ if (!credential) {
1084
+ const providerAdapter = this.providers.get(provider);
1085
+ if (providerAdapter) {
1086
+ try {
1087
+ credential = await providerAdapter.getCredentials(request);
1088
+ } catch (error) {
1089
+ this.log(
1090
+ "warn",
1091
+ { repo: request.repo, provider, error },
1092
+ "Failed to get provider credentials"
1093
+ );
1094
+ }
1095
+ }
1096
+ }
1097
+ if (!credential && this.oauthConfig) {
1098
+ credential = await this.getOAuthCredentialViaDeviceFlow(provider, request, ttlSeconds);
1099
+ }
1100
+ if (!credential) {
1101
+ throw new Error(
1102
+ `No credentials available for repository: ${request.repo}. ` + (this.oauthConfig ? "OAuth device flow failed or was cancelled." : "Configure OAuth to enable interactive authentication.")
1103
+ );
1104
+ }
1105
+ const grant = {
1106
+ id: credential.id,
1107
+ type: credential.type,
1108
+ repo: request.repo,
1109
+ provider,
1110
+ grantedTo: {
1111
+ executionId: request.context.executionId,
1112
+ taskId: request.context.taskId,
1113
+ agentId: request.context.agentId
1114
+ },
1115
+ permissions: credential.permissions,
1116
+ createdAt: /* @__PURE__ */ new Date(),
1117
+ expiresAt: credential.expiresAt
1118
+ };
1119
+ this.grants.set(grant.id, grant);
1120
+ if (this.grantStore) {
1121
+ try {
1122
+ await this.grantStore.create({
1123
+ ...grant,
1124
+ reason: request.context.reason
1125
+ });
1126
+ } catch (error) {
1127
+ this.log(
1128
+ "warn",
1129
+ { grantId: grant.id, error },
1130
+ "Failed to persist credential grant"
1131
+ );
1132
+ }
1133
+ }
1134
+ this.log(
1135
+ "info",
1136
+ {
1137
+ grantId: grant.id,
1138
+ repo: request.repo,
1139
+ expiresAt: credential.expiresAt,
1140
+ persisted: !!this.grantStore
1141
+ },
1142
+ "Credential granted"
1143
+ );
1144
+ return credential;
1145
+ }
1146
+ /**
1147
+ * Revoke a credential grant
1148
+ */
1149
+ async revokeCredential(grantId) {
1150
+ const grant = this.grants.get(grantId);
1151
+ if (!grant) {
1152
+ if (this.grantStore) {
1153
+ try {
1154
+ await this.grantStore.revoke(grantId);
1155
+ } catch (error) {
1156
+ this.log("warn", { grantId, error }, "Failed to revoke credential in store");
1157
+ }
1158
+ }
1159
+ return;
1160
+ }
1161
+ grant.revokedAt = /* @__PURE__ */ new Date();
1162
+ this.grants.set(grantId, grant);
1163
+ const provider = this.providers.get(grant.provider);
1164
+ if (provider) {
1165
+ try {
1166
+ await provider.revokeCredential(grantId);
1167
+ } catch (error) {
1168
+ this.log("warn", { grantId, error }, "Failed to revoke credential via provider");
1169
+ }
1170
+ }
1171
+ if (this.grantStore) {
1172
+ try {
1173
+ await this.grantStore.revoke(grantId);
1174
+ } catch (error) {
1175
+ this.log("warn", { grantId, error }, "Failed to revoke credential in store");
1176
+ }
1177
+ }
1178
+ this.log("info", { grantId }, "Credential revoked");
1179
+ }
1180
+ /**
1181
+ * Revoke all credentials for an execution
1182
+ */
1183
+ async revokeForExecution(executionId) {
1184
+ let count = 0;
1185
+ for (const [id, grant] of this.grants) {
1186
+ if (grant.grantedTo.executionId === executionId && !grant.revokedAt) {
1187
+ grant.revokedAt = /* @__PURE__ */ new Date();
1188
+ this.grants.set(id, grant);
1189
+ count++;
1190
+ }
1191
+ }
1192
+ if (this.grantStore) {
1193
+ try {
1194
+ const storeCount = await this.grantStore.revokeForExecution(executionId);
1195
+ count = Math.max(count, storeCount);
1196
+ } catch (error) {
1197
+ this.log("warn", { executionId, error }, "Failed to revoke credentials in store");
1198
+ }
1199
+ }
1200
+ this.log("info", { executionId, revokedCount: count }, "Credentials revoked for execution");
1201
+ return count;
1202
+ }
1203
+ /**
1204
+ * Check if a credential is valid
1205
+ */
1206
+ isValid(grantId) {
1207
+ const grant = this.grants.get(grantId);
1208
+ if (!grant) return false;
1209
+ if (grant.revokedAt) return false;
1210
+ if (/* @__PURE__ */ new Date() > grant.expiresAt) return false;
1211
+ return true;
1212
+ }
1213
+ /**
1214
+ * Get grant info for audit
1215
+ */
1216
+ async getGrant(grantId) {
1217
+ const memoryGrant = this.grants.get(grantId);
1218
+ if (memoryGrant) {
1219
+ return memoryGrant;
1220
+ }
1221
+ if (this.grantStore) {
1222
+ try {
1223
+ return await this.grantStore.findById(grantId);
1224
+ } catch (error) {
1225
+ this.log("warn", { grantId, error }, "Failed to get grant from store");
1226
+ }
1227
+ }
1228
+ return null;
1229
+ }
1230
+ /**
1231
+ * List all grants for an execution
1232
+ */
1233
+ async getGrantsForExecution(executionId) {
1234
+ if (this.grantStore) {
1235
+ try {
1236
+ return await this.grantStore.findByExecutionId(executionId);
1237
+ } catch (error) {
1238
+ this.log("warn", { executionId, error }, "Failed to get grants from store");
1239
+ }
1240
+ }
1241
+ return Array.from(this.grants.values()).filter(
1242
+ (g) => g.grantedTo.executionId === executionId
1243
+ );
1244
+ }
1245
+ // ─────────────────────────────────────────────────────────────
1246
+ // Private Methods
1247
+ // ─────────────────────────────────────────────────────────────
1248
+ detectProvider(repo) {
1249
+ const lowerRepo = repo.toLowerCase();
1250
+ if (lowerRepo.includes("github.com") || lowerRepo.startsWith("github:")) {
1251
+ return "github";
1252
+ }
1253
+ if (lowerRepo.includes("gitlab.com") || lowerRepo.startsWith("gitlab:")) {
1254
+ return "gitlab";
1255
+ }
1256
+ if (lowerRepo.includes("bitbucket.org") || lowerRepo.startsWith("bitbucket:")) {
1257
+ return "bitbucket";
1258
+ }
1259
+ if (lowerRepo.includes("dev.azure.com") || lowerRepo.includes("visualstudio.com")) {
1260
+ return "azure_devops";
1261
+ }
1262
+ return "self_hosted";
1263
+ }
1264
+ /**
1265
+ * Create a GitCredential from user-provided credentials (PAT or OAuth token)
1266
+ */
1267
+ createUserProvidedCredential(request, ttlSeconds, provider) {
1268
+ const userCreds = request.userProvided;
1269
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
1270
+ let credentialType;
1271
+ let token;
1272
+ if (userCreds.type === "ssh") {
1273
+ credentialType = "ssh_key";
1274
+ token = "";
1275
+ } else {
1276
+ credentialType = userCreds.type === "pat" ? "pat" : "oauth";
1277
+ token = userCreds.token;
1278
+ }
1279
+ const permissions = request.access === "write" ? ["contents:read", "contents:write", "pull_requests:write"] : ["contents:read"];
1280
+ return {
1281
+ id: randomUUID(),
1282
+ type: credentialType,
1283
+ token,
1284
+ repo: request.repo,
1285
+ permissions,
1286
+ expiresAt,
1287
+ provider: userCreds.provider || provider
1288
+ };
1289
+ }
1290
+ /**
1291
+ * Check for a cached OAuth token and create credential from it
1292
+ */
1293
+ async getCachedOAuthCredential(provider, request, ttlSeconds) {
1294
+ try {
1295
+ const cachedToken = await this.tokenStore.get(provider);
1296
+ if (!cachedToken) {
1297
+ return null;
1298
+ }
1299
+ if (this.tokenStore.isExpired(cachedToken)) {
1300
+ if (cachedToken.refreshToken && this.oauthConfig) {
1301
+ const refreshedToken = await this.refreshOAuthToken(provider, cachedToken.refreshToken);
1302
+ if (refreshedToken) {
1303
+ return this.createOAuthCredential(refreshedToken, request, ttlSeconds);
1304
+ }
1305
+ }
1306
+ this.log("info", { provider }, "Cached OAuth token expired");
1307
+ return null;
1308
+ }
1309
+ if (this.tokenStore.needsRefresh(cachedToken) && cachedToken.refreshToken && this.oauthConfig) {
1310
+ const refreshedToken = await this.refreshOAuthToken(provider, cachedToken.refreshToken);
1311
+ if (refreshedToken) {
1312
+ return this.createOAuthCredential(refreshedToken, request, ttlSeconds);
1313
+ }
1314
+ }
1315
+ this.log("info", { provider }, "Using cached OAuth token");
1316
+ return this.createOAuthCredential(cachedToken, request, ttlSeconds);
1317
+ } catch (error) {
1318
+ this.log("warn", { provider, error }, "Failed to get cached OAuth token");
1319
+ return null;
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Initiate interactive OAuth device flow
1324
+ */
1325
+ async getOAuthCredentialViaDeviceFlow(provider, request, ttlSeconds) {
1326
+ if (!this.oauthConfig) {
1327
+ return null;
1328
+ }
1329
+ if (provider !== "github") {
1330
+ this.log("warn", { provider }, "OAuth device flow only supported for GitHub");
1331
+ return null;
1332
+ }
1333
+ this.log("info", { repo: request.repo }, "Starting OAuth device flow for authentication");
1334
+ try {
1335
+ const deviceFlow = new OAuthDeviceFlow({
1336
+ clientId: this.oauthConfig.clientId,
1337
+ clientSecret: this.oauthConfig.clientSecret,
1338
+ provider,
1339
+ permissions: this.oauthConfig.permissions,
1340
+ promptEmitter: this.oauthConfig.promptEmitter
1341
+ });
1342
+ const token = await deviceFlow.authorize();
1343
+ await this.tokenStore.save(provider, token);
1344
+ this.log("info", { provider }, "OAuth device flow completed successfully");
1345
+ return this.createOAuthCredential(token, request, ttlSeconds);
1346
+ } catch (error) {
1347
+ this.log("error", { provider, error }, "OAuth device flow failed");
1348
+ return null;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Refresh an OAuth token
1353
+ */
1354
+ async refreshOAuthToken(provider, refreshToken) {
1355
+ if (!this.oauthConfig) {
1356
+ return null;
1357
+ }
1358
+ try {
1359
+ const deviceFlow = new OAuthDeviceFlow({
1360
+ clientId: this.oauthConfig.clientId,
1361
+ clientSecret: this.oauthConfig.clientSecret,
1362
+ provider,
1363
+ permissions: this.oauthConfig.permissions
1364
+ });
1365
+ const newToken = await deviceFlow.refreshToken(refreshToken);
1366
+ await this.tokenStore.save(provider, newToken);
1367
+ this.log("info", { provider }, "OAuth token refreshed successfully");
1368
+ return newToken;
1369
+ } catch (error) {
1370
+ this.log("warn", { provider, error }, "Failed to refresh OAuth token");
1371
+ return null;
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Create a GitCredential from an OAuthToken
1376
+ */
1377
+ createOAuthCredential(token, request, ttlSeconds) {
1378
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
1379
+ const permissions = [];
1380
+ if (token.permissions.contents === "read" || token.permissions.contents === "write") {
1381
+ permissions.push("contents:read");
1382
+ }
1383
+ if (token.permissions.contents === "write") {
1384
+ permissions.push("contents:write");
1385
+ }
1386
+ if (token.permissions.pullRequests === "read" || token.permissions.pullRequests === "write") {
1387
+ permissions.push("pull_requests:read");
1388
+ }
1389
+ if (token.permissions.pullRequests === "write") {
1390
+ permissions.push("pull_requests:write");
1391
+ }
1392
+ if (token.permissions.issues === "read" || token.permissions.issues === "write") {
1393
+ permissions.push("issues:read");
1394
+ }
1395
+ if (token.permissions.issues === "write") {
1396
+ permissions.push("issues:write");
1397
+ }
1398
+ return {
1399
+ id: randomUUID(),
1400
+ type: "oauth",
1401
+ token: token.accessToken,
1402
+ repo: request.repo,
1403
+ permissions,
1404
+ expiresAt,
1405
+ provider: token.provider
1406
+ };
1407
+ }
1408
+ log(level, data, message) {
1409
+ if (this.logger) {
1410
+ this.logger[level](data, message);
1411
+ }
1412
+ }
1413
+ };
1414
+ var octokitCache = null;
1415
+ function loadOctokit() {
1416
+ if (!octokitCache) {
1417
+ try {
1418
+ const { Octokit } = __require("@octokit/rest");
1419
+ const { createAppAuth } = __require("@octokit/auth-app");
1420
+ octokitCache = { Octokit, createAppAuth };
1421
+ } catch {
1422
+ throw new Error(
1423
+ "@octokit/rest and @octokit/auth-app are required for GitHub provider. Install them with: npm install @octokit/rest @octokit/auth-app"
1424
+ );
1425
+ }
1426
+ }
1427
+ return octokitCache;
1428
+ }
1429
+ var GitHubProvider = class {
1430
+ constructor(config, logger) {
1431
+ this.config = config;
1432
+ this.logger = logger;
1433
+ }
1434
+ name = "github";
1435
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1436
+ appOctokit;
1437
+ installations = /* @__PURE__ */ new Map();
1438
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1439
+ installationOctokits = /* @__PURE__ */ new Map();
1440
+ initialized = false;
1441
+ /**
1442
+ * Initialize provider - fetch all installations
1443
+ */
1444
+ async initialize() {
1445
+ if (this.initialized) {
1446
+ return;
1447
+ }
1448
+ const { Octokit, createAppAuth } = loadOctokit();
1449
+ this.log("info", {}, "Initializing GitHub provider");
1450
+ this.appOctokit = new Octokit({
1451
+ authStrategy: createAppAuth,
1452
+ auth: {
1453
+ appId: this.config.appId,
1454
+ privateKey: this.config.privateKey
1455
+ },
1456
+ baseUrl: this.config.baseUrl
1457
+ });
1458
+ try {
1459
+ const { data: installations } = await this.appOctokit.apps.listInstallations();
1460
+ for (const installation of installations) {
1461
+ await this.registerInstallation(installation.id);
1462
+ }
1463
+ this.initialized = true;
1464
+ this.log(
1465
+ "info",
1466
+ { installationCount: installations.length },
1467
+ "GitHub provider initialized"
1468
+ );
1469
+ } catch (error) {
1470
+ this.log("error", { error }, "Failed to initialize GitHub provider");
1471
+ throw error;
1472
+ }
1473
+ }
1474
+ /**
1475
+ * Register an installation (called on webhook or init)
1476
+ */
1477
+ async registerInstallation(installationId) {
1478
+ if (!this.appOctokit) {
1479
+ await this.initialize();
1480
+ }
1481
+ try {
1482
+ const { data: installation } = await this.appOctokit.apps.getInstallation({
1483
+ installation_id: installationId
1484
+ });
1485
+ const octokit = await this.getInstallationOctokit(installationId);
1486
+ const { data: repos } = await octokit.apps.listReposAccessibleToInstallation({
1487
+ per_page: 100
1488
+ });
1489
+ const account = installation.account;
1490
+ const accountLogin = account?.login || account?.slug || "unknown";
1491
+ const accountType = account?.type === "Organization" ? "Organization" : "User";
1492
+ const appInstallation = {
1493
+ installationId,
1494
+ accountLogin,
1495
+ accountType,
1496
+ repositories: repos.repositories?.map((r) => r.full_name) || [],
1497
+ permissions: installation.permissions
1498
+ };
1499
+ this.installations.set(appInstallation.accountLogin, appInstallation);
1500
+ this.log(
1501
+ "info",
1502
+ {
1503
+ installationId,
1504
+ account: appInstallation.accountLogin,
1505
+ repoCount: appInstallation.repositories.length
1506
+ },
1507
+ "Installation registered"
1508
+ );
1509
+ return appInstallation;
1510
+ } catch (error) {
1511
+ this.log("error", { installationId, error }, "Failed to register installation");
1512
+ throw error;
1513
+ }
1514
+ }
1515
+ /**
1516
+ * Get installation for a repository
1517
+ */
1518
+ getInstallationForRepo(owner, repo) {
1519
+ const installation = this.installations.get(owner);
1520
+ if (!installation) {
1521
+ return null;
1522
+ }
1523
+ const fullName = `${owner}/${repo}`;
1524
+ if (!installation.repositories.includes(fullName)) {
1525
+ return null;
1526
+ }
1527
+ return installation;
1528
+ }
1529
+ /**
1530
+ * Get credentials for a repository (implements GitProviderAdapter)
1531
+ */
1532
+ async getCredentials(request) {
1533
+ const repoInfo = this.parseRepo(request.repo);
1534
+ if (!repoInfo) {
1535
+ throw new Error(`Invalid repository format: ${request.repo}`);
1536
+ }
1537
+ return this.getCredentialsForRepo(
1538
+ repoInfo.owner,
1539
+ repoInfo.repo,
1540
+ request.access,
1541
+ request.ttlSeconds
1542
+ );
1543
+ }
1544
+ /**
1545
+ * Get credentials for a repository by owner/repo
1546
+ */
1547
+ async getCredentialsForRepo(owner, repo, access, ttlSeconds = 3600) {
1548
+ if (!this.initialized) {
1549
+ await this.initialize();
1550
+ }
1551
+ const installation = this.getInstallationForRepo(owner, repo);
1552
+ if (!installation) {
1553
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1554
+ }
1555
+ const { createAppAuth } = loadOctokit();
1556
+ const auth = createAppAuth({
1557
+ appId: this.config.appId,
1558
+ privateKey: this.config.privateKey
1559
+ });
1560
+ const { token, expiresAt } = await auth({
1561
+ type: "installation",
1562
+ installationId: installation.installationId,
1563
+ repositoryNames: [repo],
1564
+ permissions: access === "write" ? { contents: "write", pull_requests: "write", metadata: "read" } : { contents: "read", metadata: "read" }
1565
+ });
1566
+ return {
1567
+ id: randomUUID(),
1568
+ type: "github_app",
1569
+ token,
1570
+ repo: `${owner}/${repo}`,
1571
+ permissions: access === "write" ? ["contents:write", "pull_requests:write", "metadata:read"] : ["contents:read", "metadata:read"],
1572
+ expiresAt: expiresAt ? new Date(expiresAt) : new Date(Date.now() + ttlSeconds * 1e3),
1573
+ provider: "github"
1574
+ };
1575
+ }
1576
+ /**
1577
+ * Revoke a credential (no-op for GitHub App tokens - they expire automatically)
1578
+ */
1579
+ async revokeCredential(_credentialId) {
1580
+ }
1581
+ /**
1582
+ * Create a pull request (implements GitProviderAdapter)
1583
+ */
1584
+ async createPullRequest(options) {
1585
+ const repoInfo = this.parseRepo(options.repo);
1586
+ if (!repoInfo) {
1587
+ throw new Error(`Invalid repository format: ${options.repo}`);
1588
+ }
1589
+ return this.createPullRequestForRepo(repoInfo.owner, repoInfo.repo, {
1590
+ title: options.title,
1591
+ body: options.body,
1592
+ head: options.sourceBranch,
1593
+ base: options.targetBranch,
1594
+ draft: options.draft,
1595
+ labels: options.labels,
1596
+ reviewers: options.reviewers
1597
+ });
1598
+ }
1599
+ /**
1600
+ * Create a pull request by owner/repo
1601
+ */
1602
+ async createPullRequestForRepo(owner, repo, options) {
1603
+ if (!this.initialized) {
1604
+ await this.initialize();
1605
+ }
1606
+ const installation = this.getInstallationForRepo(owner, repo);
1607
+ if (!installation) {
1608
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1609
+ }
1610
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1611
+ const { data: pr } = await octokit.pulls.create({
1612
+ owner,
1613
+ repo,
1614
+ title: options.title,
1615
+ body: options.body,
1616
+ head: options.head,
1617
+ base: options.base,
1618
+ draft: options.draft
1619
+ });
1620
+ if (options.labels && options.labels.length > 0) {
1621
+ await octokit.issues.addLabels({
1622
+ owner,
1623
+ repo,
1624
+ issue_number: pr.number,
1625
+ labels: options.labels
1626
+ });
1627
+ }
1628
+ if (options.reviewers && options.reviewers.length > 0) {
1629
+ await octokit.pulls.requestReviewers({
1630
+ owner,
1631
+ repo,
1632
+ pull_number: pr.number,
1633
+ reviewers: options.reviewers
1634
+ });
1635
+ }
1636
+ this.log(
1637
+ "info",
1638
+ { owner, repo, prNumber: pr.number, title: options.title },
1639
+ "Pull request created"
1640
+ );
1641
+ return {
1642
+ number: pr.number,
1643
+ url: pr.html_url,
1644
+ state: pr.state,
1645
+ sourceBranch: options.head,
1646
+ targetBranch: options.base,
1647
+ title: options.title,
1648
+ executionId: "",
1649
+ // Set by caller
1650
+ createdAt: new Date(pr.created_at)
1651
+ };
1652
+ }
1653
+ /**
1654
+ * Check if a branch exists (implements GitProviderAdapter)
1655
+ */
1656
+ async branchExists(repo, branch, _credential) {
1657
+ const repoInfo = this.parseRepo(repo);
1658
+ if (!repoInfo) {
1659
+ return false;
1660
+ }
1661
+ if (!this.initialized) {
1662
+ await this.initialize();
1663
+ }
1664
+ const installation = this.getInstallationForRepo(repoInfo.owner, repoInfo.repo);
1665
+ if (!installation) {
1666
+ return false;
1667
+ }
1668
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1669
+ try {
1670
+ await octokit.repos.getBranch({
1671
+ owner: repoInfo.owner,
1672
+ repo: repoInfo.repo,
1673
+ branch
1674
+ });
1675
+ return true;
1676
+ } catch {
1677
+ return false;
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Get the default branch for a repository (implements GitProviderAdapter)
1682
+ */
1683
+ async getDefaultBranch(repo, _credential) {
1684
+ const repoInfo = this.parseRepo(repo);
1685
+ if (!repoInfo) {
1686
+ throw new Error(`Invalid repository format: ${repo}`);
1687
+ }
1688
+ if (!this.initialized) {
1689
+ await this.initialize();
1690
+ }
1691
+ const installation = this.getInstallationForRepo(repoInfo.owner, repoInfo.repo);
1692
+ if (!installation) {
1693
+ throw new Error(`No GitHub App installation found for ${repo}`);
1694
+ }
1695
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1696
+ const { data: repoData } = await octokit.repos.get({
1697
+ owner: repoInfo.owner,
1698
+ repo: repoInfo.repo
1699
+ });
1700
+ return repoData.default_branch;
1701
+ }
1702
+ /**
1703
+ * Get pull request status
1704
+ */
1705
+ async getPullRequest(owner, repo, prNumber) {
1706
+ if (!this.initialized) {
1707
+ await this.initialize();
1708
+ }
1709
+ const installation = this.getInstallationForRepo(owner, repo);
1710
+ if (!installation) {
1711
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1712
+ }
1713
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1714
+ const { data: pr } = await octokit.pulls.get({
1715
+ owner,
1716
+ repo,
1717
+ pull_number: prNumber
1718
+ });
1719
+ return {
1720
+ number: pr.number,
1721
+ url: pr.html_url,
1722
+ state: pr.merged ? "merged" : pr.state,
1723
+ sourceBranch: pr.head.ref,
1724
+ targetBranch: pr.base.ref,
1725
+ title: pr.title,
1726
+ executionId: "",
1727
+ // Would need to parse from branch name
1728
+ createdAt: new Date(pr.created_at),
1729
+ mergedAt: pr.merged_at ? new Date(pr.merged_at) : void 0
1730
+ };
1731
+ }
1732
+ /**
1733
+ * Delete a branch
1734
+ */
1735
+ async deleteBranch(owner, repo, branch) {
1736
+ if (!this.initialized) {
1737
+ await this.initialize();
1738
+ }
1739
+ const installation = this.getInstallationForRepo(owner, repo);
1740
+ if (!installation) {
1741
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1742
+ }
1743
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1744
+ await octokit.git.deleteRef({
1745
+ owner,
1746
+ repo,
1747
+ ref: `heads/${branch}`
1748
+ });
1749
+ this.log("info", { owner, repo, branch }, "Branch deleted");
1750
+ }
1751
+ /**
1752
+ * List all managed branches for a repo
1753
+ */
1754
+ async listManagedBranches(owner, repo, prefix = "parallax/") {
1755
+ if (!this.initialized) {
1756
+ await this.initialize();
1757
+ }
1758
+ const installation = this.getInstallationForRepo(owner, repo);
1759
+ if (!installation) {
1760
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1761
+ }
1762
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1763
+ const branches = [];
1764
+ let page = 1;
1765
+ while (true) {
1766
+ const { data } = await octokit.repos.listBranches({
1767
+ owner,
1768
+ repo,
1769
+ per_page: 100,
1770
+ page
1771
+ });
1772
+ if (data.length === 0) break;
1773
+ for (const branch of data) {
1774
+ if (branch.name.startsWith(prefix)) {
1775
+ branches.push(branch.name);
1776
+ }
1777
+ }
1778
+ page++;
1779
+ }
1780
+ return branches;
1781
+ }
1782
+ // ─────────────────────────────────────────────────────────────
1783
+ // Issue Management
1784
+ // ─────────────────────────────────────────────────────────────
1785
+ /**
1786
+ * Create an issue
1787
+ */
1788
+ async createIssue(owner, repo, options) {
1789
+ if (!this.initialized) {
1790
+ await this.initialize();
1791
+ }
1792
+ const installation = this.getInstallationForRepo(owner, repo);
1793
+ if (!installation) {
1794
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1795
+ }
1796
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1797
+ const { data: issue } = await octokit.issues.create({
1798
+ owner,
1799
+ repo,
1800
+ title: options.title,
1801
+ body: options.body,
1802
+ labels: options.labels,
1803
+ assignees: options.assignees,
1804
+ milestone: options.milestone
1805
+ });
1806
+ this.log(
1807
+ "info",
1808
+ { owner, repo, issueNumber: issue.number, title: options.title },
1809
+ "Issue created"
1810
+ );
1811
+ return {
1812
+ number: issue.number,
1813
+ url: issue.html_url,
1814
+ state: issue.state,
1815
+ title: issue.title,
1816
+ body: issue.body || "",
1817
+ labels: issue.labels.map(
1818
+ (l) => typeof l === "string" ? l : l.name || ""
1819
+ ),
1820
+ assignees: issue.assignees?.map((a) => a.login) || [],
1821
+ createdAt: new Date(issue.created_at),
1822
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1823
+ };
1824
+ }
1825
+ /**
1826
+ * Get an issue by number
1827
+ */
1828
+ async getIssue(owner, repo, issueNumber) {
1829
+ if (!this.initialized) {
1830
+ await this.initialize();
1831
+ }
1832
+ const installation = this.getInstallationForRepo(owner, repo);
1833
+ if (!installation) {
1834
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1835
+ }
1836
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1837
+ const { data: issue } = await octokit.issues.get({
1838
+ owner,
1839
+ repo,
1840
+ issue_number: issueNumber
1841
+ });
1842
+ return {
1843
+ number: issue.number,
1844
+ url: issue.html_url,
1845
+ state: issue.state,
1846
+ title: issue.title,
1847
+ body: issue.body || "",
1848
+ labels: issue.labels.map(
1849
+ (l) => typeof l === "string" ? l : l.name || ""
1850
+ ),
1851
+ assignees: issue.assignees?.map((a) => a.login) || [],
1852
+ createdAt: new Date(issue.created_at),
1853
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1854
+ };
1855
+ }
1856
+ /**
1857
+ * List issues with optional filters
1858
+ */
1859
+ async listIssues(owner, repo, options) {
1860
+ if (!this.initialized) {
1861
+ await this.initialize();
1862
+ }
1863
+ const installation = this.getInstallationForRepo(owner, repo);
1864
+ if (!installation) {
1865
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1866
+ }
1867
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1868
+ const { data: issues } = await octokit.issues.listForRepo({
1869
+ owner,
1870
+ repo,
1871
+ state: options?.state || "open",
1872
+ labels: options?.labels?.join(","),
1873
+ assignee: options?.assignee,
1874
+ since: options?.since?.toISOString(),
1875
+ per_page: 100
1876
+ });
1877
+ return issues.filter((issue) => !issue.pull_request).map((issue) => ({
1878
+ number: issue.number,
1879
+ url: issue.html_url,
1880
+ state: issue.state,
1881
+ title: issue.title,
1882
+ body: issue.body || "",
1883
+ labels: issue.labels.map(
1884
+ (l) => typeof l === "string" ? l : l.name || ""
1885
+ ),
1886
+ assignees: issue.assignees?.map((a) => a.login) || [],
1887
+ createdAt: new Date(issue.created_at),
1888
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1889
+ }));
1890
+ }
1891
+ /**
1892
+ * Update an issue (labels, state, assignees, etc.)
1893
+ */
1894
+ async updateIssue(owner, repo, issueNumber, options) {
1895
+ if (!this.initialized) {
1896
+ await this.initialize();
1897
+ }
1898
+ const installation = this.getInstallationForRepo(owner, repo);
1899
+ if (!installation) {
1900
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1901
+ }
1902
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1903
+ const { data: issue } = await octokit.issues.update({
1904
+ owner,
1905
+ repo,
1906
+ issue_number: issueNumber,
1907
+ title: options.title,
1908
+ body: options.body,
1909
+ state: options.state,
1910
+ labels: options.labels,
1911
+ assignees: options.assignees
1912
+ });
1913
+ this.log(
1914
+ "info",
1915
+ { owner, repo, issueNumber, updates: Object.keys(options) },
1916
+ "Issue updated"
1917
+ );
1918
+ return {
1919
+ number: issue.number,
1920
+ url: issue.html_url,
1921
+ state: issue.state,
1922
+ title: issue.title,
1923
+ body: issue.body || "",
1924
+ labels: issue.labels.map(
1925
+ (l) => typeof l === "string" ? l : l.name || ""
1926
+ ),
1927
+ assignees: issue.assignees?.map((a) => a.login) || [],
1928
+ createdAt: new Date(issue.created_at),
1929
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1930
+ };
1931
+ }
1932
+ /**
1933
+ * Add labels to an issue
1934
+ */
1935
+ async addLabels(owner, repo, issueNumber, labels) {
1936
+ if (!this.initialized) {
1937
+ await this.initialize();
1938
+ }
1939
+ const installation = this.getInstallationForRepo(owner, repo);
1940
+ if (!installation) {
1941
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1942
+ }
1943
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1944
+ await octokit.issues.addLabels({
1945
+ owner,
1946
+ repo,
1947
+ issue_number: issueNumber,
1948
+ labels
1949
+ });
1950
+ this.log("info", { owner, repo, issueNumber, labels }, "Labels added to issue");
1951
+ }
1952
+ /**
1953
+ * Remove a label from an issue
1954
+ */
1955
+ async removeLabel(owner, repo, issueNumber, label) {
1956
+ if (!this.initialized) {
1957
+ await this.initialize();
1958
+ }
1959
+ const installation = this.getInstallationForRepo(owner, repo);
1960
+ if (!installation) {
1961
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1962
+ }
1963
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1964
+ await octokit.issues.removeLabel({
1965
+ owner,
1966
+ repo,
1967
+ issue_number: issueNumber,
1968
+ name: label
1969
+ });
1970
+ this.log("info", { owner, repo, issueNumber, label }, "Label removed from issue");
1971
+ }
1972
+ /**
1973
+ * Add a comment to an issue
1974
+ */
1975
+ async addComment(owner, repo, issueNumber, options) {
1976
+ if (!this.initialized) {
1977
+ await this.initialize();
1978
+ }
1979
+ const installation = this.getInstallationForRepo(owner, repo);
1980
+ if (!installation) {
1981
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1982
+ }
1983
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1984
+ const { data: comment } = await octokit.issues.createComment({
1985
+ owner,
1986
+ repo,
1987
+ issue_number: issueNumber,
1988
+ body: options.body
1989
+ });
1990
+ this.log("info", { owner, repo, issueNumber, commentId: comment.id }, "Comment added to issue");
1991
+ return {
1992
+ id: comment.id,
1993
+ url: comment.html_url,
1994
+ body: comment.body || "",
1995
+ author: comment.user?.login || "unknown",
1996
+ createdAt: new Date(comment.created_at)
1997
+ };
1998
+ }
1999
+ /**
2000
+ * List comments on an issue
2001
+ */
2002
+ async listComments(owner, repo, issueNumber) {
2003
+ if (!this.initialized) {
2004
+ await this.initialize();
2005
+ }
2006
+ const installation = this.getInstallationForRepo(owner, repo);
2007
+ if (!installation) {
2008
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
2009
+ }
2010
+ const octokit = await this.getInstallationOctokit(installation.installationId);
2011
+ const { data: comments } = await octokit.issues.listComments({
2012
+ owner,
2013
+ repo,
2014
+ issue_number: issueNumber,
2015
+ per_page: 100
2016
+ });
2017
+ return comments.map((comment) => ({
2018
+ id: comment.id,
2019
+ url: comment.html_url,
2020
+ body: comment.body || "",
2021
+ author: comment.user?.login || "unknown",
2022
+ createdAt: new Date(comment.created_at)
2023
+ }));
2024
+ }
2025
+ /**
2026
+ * Close an issue
2027
+ */
2028
+ async closeIssue(owner, repo, issueNumber) {
2029
+ return this.updateIssue(owner, repo, issueNumber, { state: "closed" });
2030
+ }
2031
+ /**
2032
+ * Reopen an issue
2033
+ */
2034
+ async reopenIssue(owner, repo, issueNumber) {
2035
+ return this.updateIssue(owner, repo, issueNumber, { state: "open" });
2036
+ }
2037
+ // ─────────────────────────────────────────────────────────────
2038
+ // Private Methods
2039
+ // ─────────────────────────────────────────────────────────────
2040
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2041
+ async getInstallationOctokit(installationId) {
2042
+ if (!this.installationOctokits.has(installationId)) {
2043
+ const { Octokit, createAppAuth } = loadOctokit();
2044
+ const octokit = new Octokit({
2045
+ authStrategy: createAppAuth,
2046
+ auth: {
2047
+ appId: this.config.appId,
2048
+ privateKey: this.config.privateKey,
2049
+ installationId
2050
+ },
2051
+ baseUrl: this.config.baseUrl
2052
+ });
2053
+ this.installationOctokits.set(installationId, octokit);
2054
+ }
2055
+ return this.installationOctokits.get(installationId);
2056
+ }
2057
+ parseRepo(repo) {
2058
+ const patterns = [
2059
+ /github\.com[/:]([^/]+)\/([^/.]+)/,
2060
+ /^([^/]+)\/([^/]+)$/
2061
+ ];
2062
+ for (const pattern of patterns) {
2063
+ const match = repo.match(pattern);
2064
+ if (match) {
2065
+ return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
2066
+ }
2067
+ }
2068
+ return null;
2069
+ }
2070
+ log(level, data, message) {
2071
+ if (this.logger) {
2072
+ this.logger[level](data, message);
2073
+ }
2074
+ }
2075
+ };
2076
+
2077
+ // src/providers/github-pat-client.ts
2078
+ var OctokitClass = null;
2079
+ function loadOctokit2() {
2080
+ if (!OctokitClass) {
2081
+ try {
2082
+ const { Octokit } = __require("@octokit/rest");
2083
+ OctokitClass = Octokit;
2084
+ } catch {
2085
+ throw new Error(
2086
+ "@octokit/rest is required for GitHubPatClient. Install it with: npm install @octokit/rest"
2087
+ );
2088
+ }
2089
+ }
2090
+ return OctokitClass;
2091
+ }
2092
+ var GitHubPatClient = class {
2093
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2094
+ octokit;
2095
+ logger;
2096
+ constructor(options, logger) {
2097
+ this.logger = logger;
2098
+ const Octokit = loadOctokit2();
2099
+ this.octokit = new Octokit({
2100
+ auth: options.token,
2101
+ baseUrl: options.baseUrl
2102
+ });
2103
+ }
2104
+ // ─────────────────────────────────────────────────────────────
2105
+ // Pull Requests
2106
+ // ─────────────────────────────────────────────────────────────
2107
+ /**
2108
+ * Create a pull request
2109
+ */
2110
+ async createPullRequest(owner, repo, options) {
2111
+ const { data: pr } = await this.octokit.pulls.create({
2112
+ owner,
2113
+ repo,
2114
+ title: options.title,
2115
+ body: options.body,
2116
+ head: options.head,
2117
+ base: options.base,
2118
+ draft: options.draft
2119
+ });
2120
+ if (options.labels && options.labels.length > 0) {
2121
+ await this.octokit.issues.addLabels({
2122
+ owner,
2123
+ repo,
2124
+ issue_number: pr.number,
2125
+ labels: options.labels
2126
+ });
2127
+ }
2128
+ if (options.reviewers && options.reviewers.length > 0) {
2129
+ await this.octokit.pulls.requestReviewers({
2130
+ owner,
2131
+ repo,
2132
+ pull_number: pr.number,
2133
+ reviewers: options.reviewers
2134
+ });
2135
+ }
2136
+ this.log("info", { owner, repo, prNumber: pr.number }, "Pull request created");
2137
+ return {
2138
+ number: pr.number,
2139
+ url: pr.html_url,
2140
+ state: pr.state,
2141
+ sourceBranch: options.head,
2142
+ targetBranch: options.base,
2143
+ title: options.title,
2144
+ executionId: "",
2145
+ createdAt: new Date(pr.created_at)
2146
+ };
2147
+ }
2148
+ /**
2149
+ * Get a pull request
2150
+ */
2151
+ async getPullRequest(owner, repo, prNumber) {
2152
+ const { data: pr } = await this.octokit.pulls.get({
2153
+ owner,
2154
+ repo,
2155
+ pull_number: prNumber
2156
+ });
2157
+ return {
2158
+ number: pr.number,
2159
+ url: pr.html_url,
2160
+ state: pr.merged ? "merged" : pr.state,
2161
+ sourceBranch: pr.head.ref,
2162
+ targetBranch: pr.base.ref,
2163
+ title: pr.title,
2164
+ executionId: "",
2165
+ createdAt: new Date(pr.created_at),
2166
+ mergedAt: pr.merged_at ? new Date(pr.merged_at) : void 0
2167
+ };
2168
+ }
2169
+ // ─────────────────────────────────────────────────────────────
2170
+ // Issues
2171
+ // ─────────────────────────────────────────────────────────────
2172
+ /**
2173
+ * Create an issue
2174
+ */
2175
+ async createIssue(owner, repo, options) {
2176
+ const { data: issue } = await this.octokit.issues.create({
2177
+ owner,
2178
+ repo,
2179
+ title: options.title,
2180
+ body: options.body,
2181
+ labels: options.labels,
2182
+ assignees: options.assignees,
2183
+ milestone: options.milestone
2184
+ });
2185
+ this.log("info", { owner, repo, issueNumber: issue.number }, "Issue created");
2186
+ return this.mapIssue(issue);
2187
+ }
2188
+ /**
2189
+ * Get an issue
2190
+ */
2191
+ async getIssue(owner, repo, issueNumber) {
2192
+ const { data: issue } = await this.octokit.issues.get({
2193
+ owner,
2194
+ repo,
2195
+ issue_number: issueNumber
2196
+ });
2197
+ return this.mapIssue(issue);
2198
+ }
2199
+ /**
2200
+ * List issues
2201
+ */
2202
+ async listIssues(owner, repo, options) {
2203
+ const { data: issues } = await this.octokit.issues.listForRepo({
2204
+ owner,
2205
+ repo,
2206
+ state: options?.state || "open",
2207
+ labels: options?.labels?.join(","),
2208
+ assignee: options?.assignee,
2209
+ per_page: 100
2210
+ });
2211
+ return issues.filter((issue) => !issue.pull_request).map((issue) => this.mapIssue(issue));
2212
+ }
2213
+ /**
2214
+ * Update an issue
2215
+ */
2216
+ async updateIssue(owner, repo, issueNumber, options) {
2217
+ const { data: issue } = await this.octokit.issues.update({
2218
+ owner,
2219
+ repo,
2220
+ issue_number: issueNumber,
2221
+ ...options
2222
+ });
2223
+ this.log("info", { owner, repo, issueNumber }, "Issue updated");
2224
+ return this.mapIssue(issue);
2225
+ }
2226
+ /**
2227
+ * Add labels to an issue
2228
+ */
2229
+ async addLabels(owner, repo, issueNumber, labels) {
2230
+ await this.octokit.issues.addLabels({
2231
+ owner,
2232
+ repo,
2233
+ issue_number: issueNumber,
2234
+ labels
2235
+ });
2236
+ this.log("info", { owner, repo, issueNumber, labels }, "Labels added");
2237
+ }
2238
+ /**
2239
+ * Remove a label from an issue
2240
+ */
2241
+ async removeLabel(owner, repo, issueNumber, label) {
2242
+ await this.octokit.issues.removeLabel({
2243
+ owner,
2244
+ repo,
2245
+ issue_number: issueNumber,
2246
+ name: label
2247
+ });
2248
+ this.log("info", { owner, repo, issueNumber, label }, "Label removed");
2249
+ }
2250
+ /**
2251
+ * Add a comment to an issue or PR
2252
+ */
2253
+ async addComment(owner, repo, issueNumber, options) {
2254
+ const { data: comment } = await this.octokit.issues.createComment({
2255
+ owner,
2256
+ repo,
2257
+ issue_number: issueNumber,
2258
+ body: options.body
2259
+ });
2260
+ this.log("info", { owner, repo, issueNumber, commentId: comment.id }, "Comment added");
2261
+ return {
2262
+ id: comment.id,
2263
+ url: comment.html_url,
2264
+ body: comment.body || "",
2265
+ author: comment.user?.login || "unknown",
2266
+ createdAt: new Date(comment.created_at)
2267
+ };
2268
+ }
2269
+ /**
2270
+ * List comments on an issue or PR
2271
+ */
2272
+ async listComments(owner, repo, issueNumber) {
2273
+ const { data: comments } = await this.octokit.issues.listComments({
2274
+ owner,
2275
+ repo,
2276
+ issue_number: issueNumber,
2277
+ per_page: 100
2278
+ });
2279
+ return comments.map((comment) => ({
2280
+ id: comment.id,
2281
+ url: comment.html_url,
2282
+ body: comment.body || "",
2283
+ author: comment.user?.login || "unknown",
2284
+ createdAt: new Date(comment.created_at)
2285
+ }));
2286
+ }
2287
+ /**
2288
+ * Close an issue
2289
+ */
2290
+ async closeIssue(owner, repo, issueNumber) {
2291
+ return this.updateIssue(owner, repo, issueNumber, { state: "closed" });
2292
+ }
2293
+ /**
2294
+ * Reopen an issue
2295
+ */
2296
+ async reopenIssue(owner, repo, issueNumber) {
2297
+ return this.updateIssue(owner, repo, issueNumber, { state: "open" });
2298
+ }
2299
+ // ─────────────────────────────────────────────────────────────
2300
+ // Branches
2301
+ // ─────────────────────────────────────────────────────────────
2302
+ /**
2303
+ * Delete a branch
2304
+ */
2305
+ async deleteBranch(owner, repo, branch) {
2306
+ await this.octokit.git.deleteRef({
2307
+ owner,
2308
+ repo,
2309
+ ref: `heads/${branch}`
2310
+ });
2311
+ this.log("info", { owner, repo, branch }, "Branch deleted");
2312
+ }
2313
+ /**
2314
+ * Check if a branch exists
2315
+ */
2316
+ async branchExists(owner, repo, branch) {
2317
+ try {
2318
+ await this.octokit.repos.getBranch({ owner, repo, branch });
2319
+ return true;
2320
+ } catch {
2321
+ return false;
2322
+ }
2323
+ }
2324
+ // ─────────────────────────────────────────────────────────────
2325
+ // Private Methods
2326
+ // ─────────────────────────────────────────────────────────────
2327
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2328
+ mapIssue(issue) {
2329
+ return {
2330
+ number: issue.number,
2331
+ url: issue.html_url,
2332
+ state: issue.state,
2333
+ title: issue.title,
2334
+ body: issue.body || "",
2335
+ labels: issue.labels.map(
2336
+ (l) => typeof l === "string" ? l : l.name || ""
2337
+ ),
2338
+ assignees: issue.assignees?.map((a) => a.login) || [],
2339
+ createdAt: new Date(issue.created_at),
2340
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
2341
+ };
2342
+ }
2343
+ log(level, data, message) {
2344
+ if (this.logger) {
2345
+ this.logger[level](data, message);
2346
+ }
2347
+ }
2348
+ };
2349
+
2350
+ // src/types.ts
2351
+ var DEFAULT_AGENT_PERMISSIONS = {
2352
+ repositories: { type: "selected", repos: [] },
2353
+ contents: "write",
2354
+ pullRequests: "write",
2355
+ issues: "write",
2356
+ metadata: "read",
2357
+ canDeleteBranch: true,
2358
+ // Needed for cleanup
2359
+ canForcePush: false,
2360
+ canDeleteRepository: false,
2361
+ canAdminister: false
2362
+ };
2363
+ var READONLY_AGENT_PERMISSIONS = {
2364
+ repositories: { type: "selected", repos: [] },
2365
+ contents: "read",
2366
+ pullRequests: "read",
2367
+ issues: "read",
2368
+ metadata: "read",
2369
+ canDeleteBranch: false,
2370
+ canForcePush: false,
2371
+ canDeleteRepository: false,
2372
+ canAdminister: false
2373
+ };
2374
+
2375
+ export { CredentialService, DEFAULT_AGENT_PERMISSIONS, DEFAULT_BRANCH_PREFIX, FileTokenStore, GitHubPatClient, GitHubProvider, MemoryTokenStore, OAuthDeviceFlow, READONLY_AGENT_PERMISSIONS, TokenStore, WorkspaceService, cleanupCredentialFiles, configureCredentialHelper, createBranchInfo, createNodeCredentialHelperScript, createShellCredentialHelperScript, filterBranchesByExecution, generateBranchName, generateSlug, getGitCredentialConfig, isManagedBranch, outputCredentials, parseBranchName, updateCredentials };
2376
+ //# sourceMappingURL=index.js.map
2377
+ //# sourceMappingURL=index.js.map