deepdebug-local-agent 0.3.1
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/.dockerignore +24 -0
- package/.idea/deepdebug-local-agent.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/Dockerfile +46 -0
- package/cloudbuild.yaml +42 -0
- package/index.js +42 -0
- package/mcp-server.js +533 -0
- package/package.json +22 -0
- package/src/ai-engine.js +861 -0
- package/src/analyzers/config-analyzer.js +446 -0
- package/src/analyzers/controller-analyzer.js +429 -0
- package/src/analyzers/dto-analyzer.js +455 -0
- package/src/detectors/build-tool-detector.js +0 -0
- package/src/detectors/framework-detector.js +91 -0
- package/src/detectors/language-detector.js +89 -0
- package/src/detectors/multi-project-detector.js +191 -0
- package/src/detectors/service-detector.js +244 -0
- package/src/detectors.js +30 -0
- package/src/exec-utils.js +215 -0
- package/src/fs-utils.js +34 -0
- package/src/git/base-git-provider.js +384 -0
- package/src/git/git-provider-registry.js +110 -0
- package/src/git/github-provider.js +502 -0
- package/src/mcp-http-server.js +313 -0
- package/src/patch/patch-engine.js +339 -0
- package/src/patch-manager.js +816 -0
- package/src/patch.js +607 -0
- package/src/patch_bkp.js +154 -0
- package/src/ports.js +69 -0
- package/src/routes/workspace.route.js +528 -0
- package/src/runtimes/base-runtime.js +290 -0
- package/src/runtimes/java/gradle-runtime.js +378 -0
- package/src/runtimes/java/java-integrations.js +339 -0
- package/src/runtimes/java/maven-runtime.js +418 -0
- package/src/runtimes/node/node-integrations.js +247 -0
- package/src/runtimes/node/npm-runtime.js +466 -0
- package/src/runtimes/node/yarn-runtime.js +354 -0
- package/src/runtimes/runtime-registry.js +256 -0
- package/src/server-local.js +576 -0
- package/src/server.js +4565 -0
- package/src/utils/environment-diagnostics.js +666 -0
- package/src/utils/exec-utils.js +264 -0
- package/src/utils/fs-utils.js +218 -0
- package/src/workspace/detect-port.js +176 -0
- package/src/workspace/file-reader.js +54 -0
- package/src/workspace/git-client.js +0 -0
- package/src/workspace/process-manager.js +619 -0
- package/src/workspace/scanner.js +72 -0
- package/src/workspace-manager.js +172 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { BaseGitProvider } from './base-git-provider.js';
|
|
2
|
+
import { run } from '../utils/exec-utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHubProvider
|
|
6
|
+
*
|
|
7
|
+
* Implementação do Git provider para GitHub.
|
|
8
|
+
*/
|
|
9
|
+
export class GitHubProvider extends BaseGitProvider {
|
|
10
|
+
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
super(config);
|
|
13
|
+
this.clientId = config.clientId || process.env.GITHUB_CLIENT_ID;
|
|
14
|
+
this.clientSecret = config.clientSecret || process.env.GITHUB_CLIENT_SECRET;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ==========================================
|
|
18
|
+
// IDENTIFICATION
|
|
19
|
+
// ==========================================
|
|
20
|
+
|
|
21
|
+
getId() {
|
|
22
|
+
return 'github';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getName() {
|
|
26
|
+
return 'GitHub';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getApiBaseUrl() {
|
|
30
|
+
return 'https://api.github.com';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ==========================================
|
|
34
|
+
// AUTHENTICATION
|
|
35
|
+
// ==========================================
|
|
36
|
+
|
|
37
|
+
getAuthorizationUrl(redirectUri, state) {
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
client_id: this.clientId,
|
|
40
|
+
redirect_uri: redirectUri,
|
|
41
|
+
scope: 'repo user',
|
|
42
|
+
state
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return `https://github.com/login/oauth/authorize?${params}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exchangeCodeForToken(code, redirectUri) {
|
|
49
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Accept': 'application/json'
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
client_id: this.clientId,
|
|
57
|
+
client_secret: this.clientSecret,
|
|
58
|
+
code,
|
|
59
|
+
redirect_uri: redirectUri
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
|
|
65
|
+
if (data.error) {
|
|
66
|
+
throw new Error(data.error_description || data.error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.accessToken = data.access_token;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
accessToken: data.access_token,
|
|
73
|
+
tokenType: data.token_type,
|
|
74
|
+
scope: data.scope
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async validateToken() {
|
|
79
|
+
try {
|
|
80
|
+
await this.getCurrentUser();
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ==========================================
|
|
88
|
+
// API HELPERS
|
|
89
|
+
// ==========================================
|
|
90
|
+
|
|
91
|
+
async apiRequest(endpoint, options = {}) {
|
|
92
|
+
const url = endpoint.startsWith('http')
|
|
93
|
+
? endpoint
|
|
94
|
+
: `${this.getApiBaseUrl()}${endpoint}`;
|
|
95
|
+
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
...options,
|
|
98
|
+
headers: {
|
|
99
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
100
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
...options.headers
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const error = await response.json().catch(() => ({}));
|
|
108
|
+
throw new Error(error.message || `GitHub API error: ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return response.json();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ==========================================
|
|
115
|
+
// USER
|
|
116
|
+
// ==========================================
|
|
117
|
+
|
|
118
|
+
async getCurrentUser() {
|
|
119
|
+
const user = await this.apiRequest('/user');
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id: String(user.id),
|
|
123
|
+
username: user.login,
|
|
124
|
+
email: user.email,
|
|
125
|
+
name: user.name,
|
|
126
|
+
avatarUrl: user.avatar_url
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ==========================================
|
|
131
|
+
// REPOSITORIES
|
|
132
|
+
// ==========================================
|
|
133
|
+
|
|
134
|
+
async listRepositories(options = {}) {
|
|
135
|
+
const { page = 1, perPage = 30, sort = 'updated' } = options;
|
|
136
|
+
|
|
137
|
+
const repos = await this.apiRequest(
|
|
138
|
+
`/user/repos?page=${page}&per_page=${perPage}&sort=${sort}`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return repos.map(repo => ({
|
|
142
|
+
id: String(repo.id),
|
|
143
|
+
name: repo.name,
|
|
144
|
+
fullName: repo.full_name,
|
|
145
|
+
description: repo.description,
|
|
146
|
+
defaultBranch: repo.default_branch,
|
|
147
|
+
private: repo.private,
|
|
148
|
+
cloneUrl: repo.clone_url,
|
|
149
|
+
htmlUrl: repo.html_url
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getRepository(owner, repo) {
|
|
154
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}`);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id: String(data.id),
|
|
158
|
+
name: data.name,
|
|
159
|
+
fullName: data.full_name,
|
|
160
|
+
description: data.description,
|
|
161
|
+
defaultBranch: data.default_branch,
|
|
162
|
+
private: data.private,
|
|
163
|
+
cloneUrl: data.clone_url,
|
|
164
|
+
htmlUrl: data.html_url
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async cloneRepository(owner, repo, destPath, options = {}) {
|
|
169
|
+
const { branch, depth = 1 } = options;
|
|
170
|
+
|
|
171
|
+
const cloneUrl = this.getCloneUrl(owner, repo, true);
|
|
172
|
+
|
|
173
|
+
// Inserir token na URL para autenticação
|
|
174
|
+
const authedUrl = cloneUrl.replace(
|
|
175
|
+
'https://',
|
|
176
|
+
`https://${this.accessToken}@`
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const args = ['clone'];
|
|
180
|
+
|
|
181
|
+
if (depth) {
|
|
182
|
+
args.push('--depth', String(depth));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (branch) {
|
|
186
|
+
args.push('--branch', branch);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
args.push(authedUrl, destPath);
|
|
190
|
+
|
|
191
|
+
const result = await run('git', args);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: result.code === 0,
|
|
195
|
+
path: destPath,
|
|
196
|
+
output: result.stdout + result.stderr
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ==========================================
|
|
201
|
+
// BRANCHES
|
|
202
|
+
// ==========================================
|
|
203
|
+
|
|
204
|
+
async listBranches(owner, repo) {
|
|
205
|
+
const branches = await this.apiRequest(`/repos/${owner}/${repo}/branches`);
|
|
206
|
+
|
|
207
|
+
return branches.map(branch => ({
|
|
208
|
+
name: branch.name,
|
|
209
|
+
sha: branch.commit.sha,
|
|
210
|
+
protected: branch.protected
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async createBranch(owner, repo, branchName, fromRef) {
|
|
215
|
+
// Obter SHA do ref de origem
|
|
216
|
+
let sha = fromRef;
|
|
217
|
+
|
|
218
|
+
if (!fromRef.match(/^[a-f0-9]{40}$/)) {
|
|
219
|
+
// É um nome de branch, precisamos do SHA
|
|
220
|
+
const branch = await this.getBranch(owner, repo, fromRef);
|
|
221
|
+
sha = branch.sha;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Criar a referência
|
|
225
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/git/refs`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
ref: `refs/heads/${branchName}`,
|
|
229
|
+
sha
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
name: branchName,
|
|
235
|
+
sha: data.object.sha,
|
|
236
|
+
protected: false
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async getBranch(owner, repo, branch) {
|
|
241
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/branches/${branch}`);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
name: data.name,
|
|
245
|
+
sha: data.commit.sha,
|
|
246
|
+
protected: data.protected
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ==========================================
|
|
251
|
+
// COMMITS
|
|
252
|
+
// ==========================================
|
|
253
|
+
|
|
254
|
+
async listCommits(owner, repo, branch, options = {}) {
|
|
255
|
+
const { page = 1, perPage = 30 } = options;
|
|
256
|
+
|
|
257
|
+
const commits = await this.apiRequest(
|
|
258
|
+
`/repos/${owner}/${repo}/commits?sha=${branch}&page=${page}&per_page=${perPage}`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return commits.map(commit => ({
|
|
262
|
+
sha: commit.sha,
|
|
263
|
+
message: commit.commit.message,
|
|
264
|
+
authorName: commit.commit.author.name,
|
|
265
|
+
authorEmail: commit.commit.author.email,
|
|
266
|
+
date: new Date(commit.commit.author.date)
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async createCommit(owner, repo, branch, message, changes) {
|
|
271
|
+
// 1. Obter referência atual
|
|
272
|
+
const branchData = await this.getBranch(owner, repo, branch);
|
|
273
|
+
const baseSha = branchData.sha;
|
|
274
|
+
|
|
275
|
+
// 2. Obter tree atual
|
|
276
|
+
const baseCommit = await this.apiRequest(
|
|
277
|
+
`/repos/${owner}/${repo}/git/commits/${baseSha}`
|
|
278
|
+
);
|
|
279
|
+
const baseTreeSha = baseCommit.tree.sha;
|
|
280
|
+
|
|
281
|
+
// 3. Criar blobs para cada arquivo modificado
|
|
282
|
+
const treeItems = [];
|
|
283
|
+
|
|
284
|
+
for (const change of changes) {
|
|
285
|
+
if (change.action === 'delete') {
|
|
286
|
+
treeItems.push({
|
|
287
|
+
path: change.path,
|
|
288
|
+
mode: '100644',
|
|
289
|
+
type: 'blob',
|
|
290
|
+
sha: null
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
// Criar blob
|
|
294
|
+
const blob = await this.apiRequest(`/repos/${owner}/${repo}/git/blobs`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
content: change.content,
|
|
298
|
+
encoding: 'utf-8'
|
|
299
|
+
})
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
treeItems.push({
|
|
303
|
+
path: change.path,
|
|
304
|
+
mode: '100644',
|
|
305
|
+
type: 'blob',
|
|
306
|
+
sha: blob.sha
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 4. Criar nova tree
|
|
312
|
+
const newTree = await this.apiRequest(`/repos/${owner}/${repo}/git/trees`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
base_tree: baseTreeSha,
|
|
316
|
+
tree: treeItems
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 5. Criar commit
|
|
321
|
+
const newCommit = await this.apiRequest(`/repos/${owner}/${repo}/git/commits`, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
message,
|
|
325
|
+
tree: newTree.sha,
|
|
326
|
+
parents: [baseSha]
|
|
327
|
+
})
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 6. Atualizar referência da branch
|
|
331
|
+
await this.apiRequest(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
332
|
+
method: 'PATCH',
|
|
333
|
+
body: JSON.stringify({
|
|
334
|
+
sha: newCommit.sha
|
|
335
|
+
})
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
sha: newCommit.sha,
|
|
340
|
+
message: newCommit.message,
|
|
341
|
+
authorName: newCommit.author.name,
|
|
342
|
+
authorEmail: newCommit.author.email,
|
|
343
|
+
date: new Date(newCommit.author.date)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ==========================================
|
|
348
|
+
// FILES
|
|
349
|
+
// ==========================================
|
|
350
|
+
|
|
351
|
+
async getFileContent(owner, repo, path, ref = 'main') {
|
|
352
|
+
const data = await this.apiRequest(
|
|
353
|
+
`/repos/${owner}/${repo}/contents/${path}?ref=${ref}`
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
path: data.path,
|
|
358
|
+
content: Buffer.from(data.content, 'base64').toString('utf8'),
|
|
359
|
+
sha: data.sha,
|
|
360
|
+
encoding: data.encoding
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async createOrUpdateFile(owner, repo, path, content, message, branch, sha = null) {
|
|
365
|
+
const body = {
|
|
366
|
+
message,
|
|
367
|
+
content: Buffer.from(content).toString('base64'),
|
|
368
|
+
branch
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (sha) {
|
|
372
|
+
body.sha = sha;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/contents/${path}`, {
|
|
376
|
+
method: 'PUT',
|
|
377
|
+
body: JSON.stringify(body)
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
path: data.content.path,
|
|
382
|
+
sha: data.content.sha,
|
|
383
|
+
commit: data.commit
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ==========================================
|
|
388
|
+
// PULL REQUESTS
|
|
389
|
+
// ==========================================
|
|
390
|
+
|
|
391
|
+
async listPullRequests(owner, repo, options = {}) {
|
|
392
|
+
const { state = 'open', page = 1, perPage = 30 } = options;
|
|
393
|
+
|
|
394
|
+
const prs = await this.apiRequest(
|
|
395
|
+
`/repos/${owner}/${repo}/pulls?state=${state}&page=${page}&per_page=${perPage}`
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
return prs.map(pr => ({
|
|
399
|
+
number: pr.number,
|
|
400
|
+
title: pr.title,
|
|
401
|
+
body: pr.body,
|
|
402
|
+
state: pr.state,
|
|
403
|
+
head: pr.head.ref,
|
|
404
|
+
base: pr.base.ref,
|
|
405
|
+
htmlUrl: pr.html_url
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async createPullRequest(owner, repo, pr) {
|
|
410
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/pulls`, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
title: pr.title,
|
|
414
|
+
body: pr.body,
|
|
415
|
+
head: pr.head,
|
|
416
|
+
base: pr.base
|
|
417
|
+
})
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
number: data.number,
|
|
422
|
+
title: data.title,
|
|
423
|
+
body: data.body,
|
|
424
|
+
state: data.state,
|
|
425
|
+
head: data.head.ref,
|
|
426
|
+
base: data.base.ref,
|
|
427
|
+
htmlUrl: data.html_url
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async getPullRequest(owner, repo, prNumber) {
|
|
432
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/pulls/${prNumber}`);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
number: data.number,
|
|
436
|
+
title: data.title,
|
|
437
|
+
body: data.body,
|
|
438
|
+
state: data.state,
|
|
439
|
+
head: data.head.ref,
|
|
440
|
+
base: data.base.ref,
|
|
441
|
+
htmlUrl: data.html_url
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ==========================================
|
|
446
|
+
// WEBHOOKS
|
|
447
|
+
// ==========================================
|
|
448
|
+
|
|
449
|
+
async createWebhook(owner, repo, webhook) {
|
|
450
|
+
const data = await this.apiRequest(`/repos/${owner}/${repo}/hooks`, {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
name: 'web',
|
|
454
|
+
active: true,
|
|
455
|
+
events: webhook.events || ['push', 'pull_request'],
|
|
456
|
+
config: {
|
|
457
|
+
url: webhook.url,
|
|
458
|
+
content_type: 'json',
|
|
459
|
+
secret: webhook.secret
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
id: String(data.id),
|
|
466
|
+
url: data.config.url,
|
|
467
|
+
events: data.events,
|
|
468
|
+
active: data.active
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ==========================================
|
|
473
|
+
// HELPER METHODS
|
|
474
|
+
// ==========================================
|
|
475
|
+
|
|
476
|
+
getCloneUrl(owner, repo, useHttps = true) {
|
|
477
|
+
if (useHttps) {
|
|
478
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
479
|
+
}
|
|
480
|
+
return `git@github.com:${owner}/${repo}.git`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
parseRepositoryUrl(url) {
|
|
484
|
+
// Suporta:
|
|
485
|
+
// - https://github.com/owner/repo
|
|
486
|
+
// - https://github.com/owner/repo.git
|
|
487
|
+
// - git@github.com:owner/repo.git
|
|
488
|
+
|
|
489
|
+
let match = url.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
|
|
490
|
+
|
|
491
|
+
if (match) {
|
|
492
|
+
return {
|
|
493
|
+
owner: match[1],
|
|
494
|
+
repo: match[2]
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
throw new Error(`Invalid GitHub repository URL: ${url}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default GitHubProvider;
|