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