claudedesk 1.0.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/LICENSE +21 -0
- package/README.md +431 -0
- package/config/repos.example.json +128 -0
- package/config/settings.example.json +64 -0
- package/config/skills/code-review.md +76 -0
- package/config/skills/full-check.md +26 -0
- package/config/skills/lint-fix.md +23 -0
- package/dist/api/agent-routes.d.ts +2 -0
- package/dist/api/agent-routes.d.ts.map +1 -0
- package/dist/api/agent-routes.js +251 -0
- package/dist/api/agent-routes.js.map +1 -0
- package/dist/api/app-routes.d.ts +2 -0
- package/dist/api/app-routes.d.ts.map +1 -0
- package/dist/api/app-routes.js +150 -0
- package/dist/api/app-routes.js.map +1 -0
- package/dist/api/docker-routes.d.ts +2 -0
- package/dist/api/docker-routes.d.ts.map +1 -0
- package/dist/api/docker-routes.js +167 -0
- package/dist/api/docker-routes.js.map +1 -0
- package/dist/api/middleware.d.ts +6 -0
- package/dist/api/middleware.d.ts.map +1 -0
- package/dist/api/middleware.js +293 -0
- package/dist/api/middleware.js.map +1 -0
- package/dist/api/pin-auth.d.ts +65 -0
- package/dist/api/pin-auth.d.ts.map +1 -0
- package/dist/api/pin-auth.js +218 -0
- package/dist/api/pin-auth.js.map +1 -0
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +473 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/api/settings-routes.d.ts +2 -0
- package/dist/api/settings-routes.d.ts.map +1 -0
- package/dist/api/settings-routes.js +570 -0
- package/dist/api/settings-routes.js.map +1 -0
- package/dist/api/skill-routes.d.ts +2 -0
- package/dist/api/skill-routes.d.ts.map +1 -0
- package/dist/api/skill-routes.js +88 -0
- package/dist/api/skill-routes.js.map +1 -0
- package/dist/api/terminal-routes.d.ts +2 -0
- package/dist/api/terminal-routes.d.ts.map +1 -0
- package/dist/api/terminal-routes.js +3524 -0
- package/dist/api/terminal-routes.js.map +1 -0
- package/dist/api/tunnel-routes.d.ts +2 -0
- package/dist/api/tunnel-routes.d.ts.map +1 -0
- package/dist/api/tunnel-routes.js +196 -0
- package/dist/api/tunnel-routes.js.map +1 -0
- package/dist/api/workspace-routes.d.ts +3 -0
- package/dist/api/workspace-routes.d.ts.map +1 -0
- package/dist/api/workspace-routes.js +649 -0
- package/dist/api/workspace-routes.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +276 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/assets/index-B4r0njGe.js +780 -0
- package/dist/client/assets/index-CY_9MyE0.css +1 -0
- package/dist/client/favicon.svg +5 -0
- package/dist/client/icons/icon-192.svg +5 -0
- package/dist/client/icons/icon-512.svg +5 -0
- package/dist/client/icons/logo-with-message.png +0 -0
- package/dist/client/icons/logo.png +0 -0
- package/dist/client/index.html +25 -0
- package/dist/client/manifest.json +62 -0
- package/dist/client/sw.js +243 -0
- package/dist/config/agent-usage.d.ts +34 -0
- package/dist/config/agent-usage.d.ts.map +1 -0
- package/dist/config/agent-usage.js +87 -0
- package/dist/config/agent-usage.js.map +1 -0
- package/dist/config/repos.d.ts +34 -0
- package/dist/config/repos.d.ts.map +1 -0
- package/dist/config/repos.js +412 -0
- package/dist/config/repos.js.map +1 -0
- package/dist/config/settings.d.ts +634 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +459 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/config/skills.d.ts +18 -0
- package/dist/config/skills.d.ts.map +1 -0
- package/dist/config/skills.js +174 -0
- package/dist/config/skills.js.map +1 -0
- package/dist/config/workspaces.d.ts +961 -0
- package/dist/config/workspaces.d.ts.map +1 -0
- package/dist/config/workspaces.js +482 -0
- package/dist/config/workspaces.js.map +1 -0
- package/dist/core/app-manager.d.ts +85 -0
- package/dist/core/app-manager.d.ts.map +1 -0
- package/dist/core/app-manager.js +447 -0
- package/dist/core/app-manager.js.map +1 -0
- package/dist/core/claude-invoker.d.ts +49 -0
- package/dist/core/claude-invoker.d.ts.map +1 -0
- package/dist/core/claude-invoker.js +583 -0
- package/dist/core/claude-invoker.js.map +1 -0
- package/dist/core/claude-session-reader.d.ts +25 -0
- package/dist/core/claude-session-reader.d.ts.map +1 -0
- package/dist/core/claude-session-reader.js +184 -0
- package/dist/core/claude-session-reader.js.map +1 -0
- package/dist/core/claude-usage-query.d.ts +78 -0
- package/dist/core/claude-usage-query.d.ts.map +1 -0
- package/dist/core/claude-usage-query.js +294 -0
- package/dist/core/claude-usage-query.js.map +1 -0
- package/dist/core/git-credential-helper.d.ts +57 -0
- package/dist/core/git-credential-helper.d.ts.map +1 -0
- package/dist/core/git-credential-helper.js +176 -0
- package/dist/core/git-credential-helper.js.map +1 -0
- package/dist/core/git-sandbox.d.ts +135 -0
- package/dist/core/git-sandbox.d.ts.map +1 -0
- package/dist/core/git-sandbox.js +907 -0
- package/dist/core/git-sandbox.js.map +1 -0
- package/dist/core/github-integration.d.ts +66 -0
- package/dist/core/github-integration.d.ts.map +1 -0
- package/dist/core/github-integration.js +350 -0
- package/dist/core/github-integration.js.map +1 -0
- package/dist/core/github-oauth.d.ts +88 -0
- package/dist/core/github-oauth.d.ts.map +1 -0
- package/dist/core/github-oauth.js +244 -0
- package/dist/core/github-oauth.js.map +1 -0
- package/dist/core/gitlab-integration.d.ts +66 -0
- package/dist/core/gitlab-integration.d.ts.map +1 -0
- package/dist/core/gitlab-integration.js +353 -0
- package/dist/core/gitlab-integration.js.map +1 -0
- package/dist/core/gitlab-oauth.d.ts +100 -0
- package/dist/core/gitlab-oauth.d.ts.map +1 -0
- package/dist/core/gitlab-oauth.js +366 -0
- package/dist/core/gitlab-oauth.js.map +1 -0
- package/dist/core/insights-extractor.d.ts +68 -0
- package/dist/core/insights-extractor.d.ts.map +1 -0
- package/dist/core/insights-extractor.js +402 -0
- package/dist/core/insights-extractor.js.map +1 -0
- package/dist/core/logger.d.ts +27 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +70 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/process-runner.d.ts +27 -0
- package/dist/core/process-runner.d.ts.map +1 -0
- package/dist/core/process-runner.js +147 -0
- package/dist/core/process-runner.js.map +1 -0
- package/dist/core/project-detector.d.ts +30 -0
- package/dist/core/project-detector.d.ts.map +1 -0
- package/dist/core/project-detector.js +482 -0
- package/dist/core/project-detector.js.map +1 -0
- package/dist/core/qr-generator.d.ts +18 -0
- package/dist/core/qr-generator.d.ts.map +1 -0
- package/dist/core/qr-generator.js +61 -0
- package/dist/core/qr-generator.js.map +1 -0
- package/dist/core/remote-tunnel-manager.d.ts +59 -0
- package/dist/core/remote-tunnel-manager.d.ts.map +1 -0
- package/dist/core/remote-tunnel-manager.js +235 -0
- package/dist/core/remote-tunnel-manager.js.map +1 -0
- package/dist/core/shared-docker-manager.d.ts +41 -0
- package/dist/core/shared-docker-manager.d.ts.map +1 -0
- package/dist/core/shared-docker-manager.js +409 -0
- package/dist/core/shared-docker-manager.js.map +1 -0
- package/dist/core/skill-executor.d.ts +25 -0
- package/dist/core/skill-executor.d.ts.map +1 -0
- package/dist/core/skill-executor.js +171 -0
- package/dist/core/skill-executor.js.map +1 -0
- package/dist/core/terminal-session.d.ts +149 -0
- package/dist/core/terminal-session.d.ts.map +1 -0
- package/dist/core/terminal-session.js +2340 -0
- package/dist/core/terminal-session.js.map +1 -0
- package/dist/core/tunnel-manager.d.ts +35 -0
- package/dist/core/tunnel-manager.d.ts.map +1 -0
- package/dist/core/tunnel-manager.js +137 -0
- package/dist/core/tunnel-manager.js.map +1 -0
- package/dist/core/usage-manager.d.ts +57 -0
- package/dist/core/usage-manager.d.ts.map +1 -0
- package/dist/core/usage-manager.js +363 -0
- package/dist/core/usage-manager.js.map +1 -0
- package/dist/core/ws-manager.d.ts +39 -0
- package/dist/core/ws-manager.d.ts.map +1 -0
- package/dist/core/ws-manager.js +190 -0
- package/dist/core/ws-manager.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +229 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +868 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +119 -0
- package/dist/types.js.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Device Flow Implementation
|
|
3
|
+
*
|
|
4
|
+
* The device flow allows CLI/desktop apps to authenticate without a callback URL:
|
|
5
|
+
* 1. Request a device code from GitHub
|
|
6
|
+
* 2. User goes to github.com/login/device and enters the code
|
|
7
|
+
* 3. App polls GitHub until user completes authorization
|
|
8
|
+
* 4. GitHub returns access token
|
|
9
|
+
*
|
|
10
|
+
* See: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
|
|
11
|
+
*/
|
|
12
|
+
// Store active device flow sessions (device_code -> pending state)
|
|
13
|
+
const activeSessions = new Map();
|
|
14
|
+
export class GitHubDeviceAuth {
|
|
15
|
+
clientId;
|
|
16
|
+
constructor(clientId) {
|
|
17
|
+
this.clientId = clientId;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Step 1: Request a device code from GitHub
|
|
21
|
+
* User will need to visit the verification URI and enter the user code
|
|
22
|
+
*/
|
|
23
|
+
async requestDeviceCode(scope = 'repo') {
|
|
24
|
+
const response = await fetch('https://github.com/login/device/code', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Accept': 'application/json',
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
client_id: this.clientId,
|
|
32
|
+
scope,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const error = await response.text();
|
|
37
|
+
throw new Error(`Failed to request device code: ${error}`);
|
|
38
|
+
}
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const result = {
|
|
41
|
+
deviceCode: data.device_code,
|
|
42
|
+
userCode: data.user_code,
|
|
43
|
+
verificationUri: data.verification_uri,
|
|
44
|
+
expiresIn: data.expires_in,
|
|
45
|
+
interval: data.interval,
|
|
46
|
+
};
|
|
47
|
+
// Store session for polling
|
|
48
|
+
activeSessions.set(result.deviceCode, {
|
|
49
|
+
deviceCode: result.deviceCode,
|
|
50
|
+
interval: result.interval,
|
|
51
|
+
expiresAt: Date.now() + (result.expiresIn * 1000),
|
|
52
|
+
clientId: this.clientId,
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Step 2: Poll for access token
|
|
58
|
+
* Call this periodically (respecting the interval) until authorization completes
|
|
59
|
+
*/
|
|
60
|
+
async pollForToken(deviceCode) {
|
|
61
|
+
const session = activeSessions.get(deviceCode);
|
|
62
|
+
if (!session) {
|
|
63
|
+
return { status: 'error', error: 'Device code session not found' };
|
|
64
|
+
}
|
|
65
|
+
// Check if expired
|
|
66
|
+
if (Date.now() > session.expiresAt) {
|
|
67
|
+
activeSessions.delete(deviceCode);
|
|
68
|
+
return { status: 'expired', error: 'Device code has expired' };
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Accept': 'application/json',
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
client_id: session.clientId,
|
|
79
|
+
device_code: deviceCode,
|
|
80
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
// Check for errors
|
|
85
|
+
if (data.error) {
|
|
86
|
+
switch (data.error) {
|
|
87
|
+
case 'authorization_pending':
|
|
88
|
+
// User hasn't completed authorization yet - keep polling
|
|
89
|
+
return { status: 'pending' };
|
|
90
|
+
case 'slow_down':
|
|
91
|
+
// Too many requests - increase interval
|
|
92
|
+
session.interval += 5;
|
|
93
|
+
return { status: 'pending' };
|
|
94
|
+
case 'expired_token':
|
|
95
|
+
activeSessions.delete(deviceCode);
|
|
96
|
+
return { status: 'expired', error: 'Device code has expired' };
|
|
97
|
+
case 'access_denied':
|
|
98
|
+
activeSessions.delete(deviceCode);
|
|
99
|
+
return { status: 'error', error: 'User denied authorization' };
|
|
100
|
+
default:
|
|
101
|
+
return { status: 'error', error: data.error_description || data.error };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Success! We got the token
|
|
105
|
+
if (data.access_token) {
|
|
106
|
+
activeSessions.delete(deviceCode);
|
|
107
|
+
return {
|
|
108
|
+
status: 'success',
|
|
109
|
+
token: {
|
|
110
|
+
accessToken: data.access_token,
|
|
111
|
+
tokenType: data.token_type,
|
|
112
|
+
scope: data.scope,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { status: 'error', error: 'Unexpected response from GitHub' };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
120
|
+
return { status: 'error', error: errorMsg };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the polling interval for a device code session
|
|
125
|
+
*/
|
|
126
|
+
getPollingInterval(deviceCode) {
|
|
127
|
+
const session = activeSessions.get(deviceCode);
|
|
128
|
+
return session?.interval || 5;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Cancel an active device code session
|
|
132
|
+
*/
|
|
133
|
+
cancelSession(deviceCode) {
|
|
134
|
+
activeSessions.delete(deviceCode);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get user info using an access token
|
|
138
|
+
*/
|
|
139
|
+
async getUser(accessToken) {
|
|
140
|
+
const response = await fetch('https://api.github.com/user', {
|
|
141
|
+
headers: {
|
|
142
|
+
'Accept': 'application/json',
|
|
143
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
144
|
+
'User-Agent': 'ClaudeDesk-App',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const error = await response.text();
|
|
149
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
150
|
+
}
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
return {
|
|
153
|
+
login: data.login,
|
|
154
|
+
name: data.name,
|
|
155
|
+
avatarUrl: data.avatar_url,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify that an access token is still valid
|
|
160
|
+
*/
|
|
161
|
+
async verifyToken(accessToken) {
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch('https://api.github.com/user', {
|
|
164
|
+
headers: {
|
|
165
|
+
'Accept': 'application/json',
|
|
166
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
167
|
+
'User-Agent': 'ClaudeDesk-App',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return response.ok;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a new GitHub API client with an access token
|
|
179
|
+
* For making authenticated GitHub API calls
|
|
180
|
+
*/
|
|
181
|
+
export class GitHubAPI {
|
|
182
|
+
accessToken;
|
|
183
|
+
constructor(accessToken) {
|
|
184
|
+
this.accessToken = accessToken;
|
|
185
|
+
}
|
|
186
|
+
async request(endpoint, options = {}) {
|
|
187
|
+
const url = endpoint.startsWith('https://')
|
|
188
|
+
? endpoint
|
|
189
|
+
: `https://api.github.com${endpoint}`;
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
...options,
|
|
192
|
+
headers: {
|
|
193
|
+
'Accept': 'application/json',
|
|
194
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
195
|
+
'User-Agent': 'ClaudeDesk-App',
|
|
196
|
+
...options.headers,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const error = await response.text();
|
|
201
|
+
throw new Error(`GitHub API error: ${response.status} - ${error}`);
|
|
202
|
+
}
|
|
203
|
+
return response.json();
|
|
204
|
+
}
|
|
205
|
+
async getUser() {
|
|
206
|
+
const data = await this.request('/user');
|
|
207
|
+
return {
|
|
208
|
+
login: data.login,
|
|
209
|
+
name: data.name,
|
|
210
|
+
avatarUrl: data.avatar_url,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async createRepo(name, isPrivate = true, description) {
|
|
214
|
+
const data = await this.request('/user/repos', {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
'Content-Type': 'application/json',
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
name,
|
|
221
|
+
private: isPrivate,
|
|
222
|
+
description,
|
|
223
|
+
auto_init: false,
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
name: data.name,
|
|
228
|
+
fullName: data.full_name,
|
|
229
|
+
htmlUrl: data.html_url,
|
|
230
|
+
cloneUrl: data.clone_url,
|
|
231
|
+
sshUrl: data.ssh_url,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async listRepos(page = 1, perPage = 30) {
|
|
235
|
+
const data = await this.request(`/user/repos?page=${page}&per_page=${perPage}&sort=updated`);
|
|
236
|
+
return data.map((repo) => ({
|
|
237
|
+
name: repo.name,
|
|
238
|
+
fullName: repo.full_name,
|
|
239
|
+
htmlUrl: repo.html_url,
|
|
240
|
+
private: repo.private,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=github-oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-oauth.js","sourceRoot":"","sources":["../../src/core/github-oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface MRResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
mrUrl?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class GitLabIntegration {
|
|
7
|
+
/**
|
|
8
|
+
* Check if GitLab CLI (glab) is installed and authenticated
|
|
9
|
+
*/
|
|
10
|
+
isAvailable(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Create a merge request using GitLab API with OAuth token.
|
|
13
|
+
* This method is preferred over CLI as it doesn't require `glab` to be installed.
|
|
14
|
+
*
|
|
15
|
+
* @param repoPath - Path to the repository (to get remote URL)
|
|
16
|
+
* @param branch - Branch to create MR from (source branch)
|
|
17
|
+
* @param title - MR title
|
|
18
|
+
* @param description - MR description
|
|
19
|
+
* @param token - OAuth access token
|
|
20
|
+
* @param targetBranch - Target branch for the MR (optional, defaults to main/master)
|
|
21
|
+
* @returns MRResult with success status and MR URL
|
|
22
|
+
*/
|
|
23
|
+
createMRWithToken(repoPath: string, branch: string, title: string, description: string, token: string, targetBranch?: string): Promise<MRResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Create a merge request.
|
|
26
|
+
* First tries OAuth API if workspace has a token, then falls back to glab CLI.
|
|
27
|
+
*
|
|
28
|
+
* @param repoPath - Path to the repository (working directory for git commands)
|
|
29
|
+
* @param branch - Branch to create MR from
|
|
30
|
+
* @param title - MR title
|
|
31
|
+
* @param body - MR body/description
|
|
32
|
+
* @param workspaceLookupPath - Optional path used for workspace token lookup (for worktree support)
|
|
33
|
+
* @returns MRResult with success status and MR URL
|
|
34
|
+
*/
|
|
35
|
+
createMR(repoPath: string, branch: string, title: string, body: string, workspaceLookupPath?: string, targetBranch?: string): Promise<MRResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Create a merge request using GitLab CLI (glab).
|
|
38
|
+
* Fallback method when OAuth is not available.
|
|
39
|
+
*/
|
|
40
|
+
private createMRWithCLI;
|
|
41
|
+
/**
|
|
42
|
+
* Create a new project on GitLab and push local code
|
|
43
|
+
* First checks if workspace has a GitLab token, otherwise falls back to glab CLI
|
|
44
|
+
* @param repoPath - Path to the local repository
|
|
45
|
+
* @param projectName - Name for the GitLab project
|
|
46
|
+
* @param visibility - Project visibility (default: 'private')
|
|
47
|
+
* @returns Result with success status and remote URL
|
|
48
|
+
*/
|
|
49
|
+
createProject(repoPath: string, projectName: string, visibility?: 'private' | 'public'): Promise<MRResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Create project using GitLab API with OAuth token
|
|
52
|
+
*/
|
|
53
|
+
private createProjectWithToken;
|
|
54
|
+
/**
|
|
55
|
+
* Create project using GitLab CLI (glab)
|
|
56
|
+
*/
|
|
57
|
+
private createProjectWithCLI;
|
|
58
|
+
/**
|
|
59
|
+
* Get the URL to create an MR manually in the browser
|
|
60
|
+
* @param repoPath - Path to the repository
|
|
61
|
+
* @param branch - Branch to create MR from
|
|
62
|
+
*/
|
|
63
|
+
getMRCreateUrl(repoPath: string, branch: string): string | null;
|
|
64
|
+
}
|
|
65
|
+
export declare const gitlabIntegration: GitLabIntegration;
|
|
66
|
+
//# sourceMappingURL=gitlab-integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-integration.d.ts","sourceRoot":"","sources":["../../src/core/gitlab-integration.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqBD,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,WAAW,IAAI,OAAO;IAStB;;;;;;;;;;;OAWG;IACG,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,QAAQ,CAAC;IAiFpB;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBrJ;;;OAGG;IACH,OAAO,CAAC,eAAe;IA0CvB;;;;;;;OAOG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAE,SAAS,GAAG,QAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAc3H;;OAEG;YACW,sBAAsB;IAiFpC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiC5B;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAoBhE;AAGD,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { workspaceManager } from '../config/workspaces.js';
|
|
4
|
+
/**
|
|
5
|
+
* Helper to make HTTPS requests using native Node.js https module.
|
|
6
|
+
* Avoids embedding tokens in shell commands (SEC-01 fix).
|
|
7
|
+
*/
|
|
8
|
+
function httpsRequest(options, data) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = https.request(options, (res) => {
|
|
11
|
+
let body = '';
|
|
12
|
+
res.on('data', (chunk) => (body += chunk));
|
|
13
|
+
res.on('end', () => resolve({ statusCode: res.statusCode || 0, body }));
|
|
14
|
+
});
|
|
15
|
+
req.on('error', reject);
|
|
16
|
+
if (data)
|
|
17
|
+
req.write(data);
|
|
18
|
+
req.end();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse project path from a GitLab remote URL.
|
|
23
|
+
* Supports both SSH and HTTPS formats.
|
|
24
|
+
* Returns URL-encoded project path for API calls.
|
|
25
|
+
*/
|
|
26
|
+
function parseGitLabRemote(remoteUrl) {
|
|
27
|
+
// SSH format: git@gitlab.com:owner/repo.git or git@gitlab.com:group/subgroup/repo.git
|
|
28
|
+
// HTTPS format: https://gitlab.com/owner/repo.git
|
|
29
|
+
const match = remoteUrl.match(/gitlab\.com[:/](.+?)(?:\.git)?$/);
|
|
30
|
+
if (match) {
|
|
31
|
+
const projectPath = match[1].replace(/\.git$/, '');
|
|
32
|
+
return {
|
|
33
|
+
projectPath,
|
|
34
|
+
projectPathEncoded: encodeURIComponent(projectPath),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export class GitLabIntegration {
|
|
40
|
+
/**
|
|
41
|
+
* Check if GitLab CLI (glab) is installed and authenticated
|
|
42
|
+
*/
|
|
43
|
+
isAvailable() {
|
|
44
|
+
try {
|
|
45
|
+
execSync('glab auth status', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a merge request using GitLab API with OAuth token.
|
|
54
|
+
* This method is preferred over CLI as it doesn't require `glab` to be installed.
|
|
55
|
+
*
|
|
56
|
+
* @param repoPath - Path to the repository (to get remote URL)
|
|
57
|
+
* @param branch - Branch to create MR from (source branch)
|
|
58
|
+
* @param title - MR title
|
|
59
|
+
* @param description - MR description
|
|
60
|
+
* @param token - OAuth access token
|
|
61
|
+
* @param targetBranch - Target branch for the MR (optional, defaults to main/master)
|
|
62
|
+
* @returns MRResult with success status and MR URL
|
|
63
|
+
*/
|
|
64
|
+
async createMRWithToken(repoPath, branch, title, description, token, targetBranch) {
|
|
65
|
+
try {
|
|
66
|
+
// Get the remote URL to determine project path
|
|
67
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
68
|
+
cwd: repoPath,
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
+
}).trim();
|
|
72
|
+
const parsed = parseGitLabRemote(remoteUrl);
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
return { success: false, error: 'Could not parse GitLab remote URL' };
|
|
75
|
+
}
|
|
76
|
+
const { projectPathEncoded } = parsed;
|
|
77
|
+
// Determine target branch if not provided
|
|
78
|
+
if (!targetBranch) {
|
|
79
|
+
try {
|
|
80
|
+
// Try to detect main branch
|
|
81
|
+
const branches = execSync('git branch -a', {
|
|
82
|
+
cwd: repoPath,
|
|
83
|
+
encoding: 'utf-8',
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
targetBranch = branches.includes('remotes/origin/main') ? 'main' : 'master';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
targetBranch = 'main';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Create MR via GitLab API using native https module (SEC-01 fix: no token in shell/temp files)
|
|
93
|
+
const requestBody = JSON.stringify({
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
source_branch: branch,
|
|
97
|
+
target_branch: targetBranch,
|
|
98
|
+
});
|
|
99
|
+
console.log(`[GitLabIntegration] Calling GitLab API: POST /api/v4/projects/${projectPathEncoded}/merge_requests`);
|
|
100
|
+
const response = await httpsRequest({
|
|
101
|
+
hostname: 'gitlab.com',
|
|
102
|
+
path: `/api/v4/projects/${projectPathEncoded}/merge_requests`,
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
Authorization: `Bearer ${token}`,
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
'Content-Length': Buffer.byteLength(requestBody),
|
|
109
|
+
},
|
|
110
|
+
}, requestBody);
|
|
111
|
+
console.log(`[GitLabIntegration] API response status: ${response.statusCode}`);
|
|
112
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
113
|
+
const mr = JSON.parse(response.body);
|
|
114
|
+
console.log(`[GitLabIntegration] MR created via API: ${mr.web_url}`);
|
|
115
|
+
return { success: true, mrUrl: mr.web_url };
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
let errorMsg = response.body;
|
|
119
|
+
try {
|
|
120
|
+
const err = JSON.parse(response.body);
|
|
121
|
+
errorMsg = err.message || err.error || JSON.stringify(err);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
errorMsg = `HTTP ${response.statusCode}: ${response.body.substring(0, 200)}`;
|
|
125
|
+
}
|
|
126
|
+
console.error(`[GitLabIntegration] API error creating MR: ${errorMsg}`);
|
|
127
|
+
return { success: false, error: errorMsg };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const err = error;
|
|
132
|
+
const errorMsg = err.stderr?.toString() || err.message || 'Unknown error';
|
|
133
|
+
console.error(`[GitLabIntegration] Failed to create MR via API: ${errorMsg}`);
|
|
134
|
+
return { success: false, error: errorMsg };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a merge request.
|
|
139
|
+
* First tries OAuth API if workspace has a token, then falls back to glab CLI.
|
|
140
|
+
*
|
|
141
|
+
* @param repoPath - Path to the repository (working directory for git commands)
|
|
142
|
+
* @param branch - Branch to create MR from
|
|
143
|
+
* @param title - MR title
|
|
144
|
+
* @param body - MR body/description
|
|
145
|
+
* @param workspaceLookupPath - Optional path used for workspace token lookup (for worktree support)
|
|
146
|
+
* @returns MRResult with success status and MR URL
|
|
147
|
+
*/
|
|
148
|
+
async createMR(repoPath, branch, title, body, workspaceLookupPath, targetBranch) {
|
|
149
|
+
// First, check if this repo belongs to a workspace with GitLab token
|
|
150
|
+
// Use workspaceLookupPath if provided (for worktree mode), otherwise use repoPath
|
|
151
|
+
const lookupPath = workspaceLookupPath || repoPath;
|
|
152
|
+
console.log(`[GitLabIntegration] Looking up workspace for path: ${lookupPath}`);
|
|
153
|
+
const workspace = workspaceManager.getWorkspaceForRepo(lookupPath);
|
|
154
|
+
console.log(`[GitLabIntegration] Found workspace: ${workspace ? workspace.name + ' (id: ' + workspace.id + ', scanPath: ' + workspace.scanPath + ')' : 'null'}`);
|
|
155
|
+
const tokenData = workspace ? workspaceManager.getGitLabToken(workspace.id) : null;
|
|
156
|
+
console.log(`[GitLabIntegration] Token data exists: ${!!tokenData}`);
|
|
157
|
+
if (tokenData) {
|
|
158
|
+
console.log(`[GitLabIntegration] Using OAuth token for MR creation`);
|
|
159
|
+
const result = await this.createMRWithToken(repoPath, branch, title, body, tokenData.accessToken, targetBranch);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
// If API fails, fall through to CLI
|
|
164
|
+
console.log(`[GitLabIntegration] OAuth MR creation failed, trying CLI fallback: ${result.error}`);
|
|
165
|
+
}
|
|
166
|
+
// Fall back to glab CLI
|
|
167
|
+
return this.createMRWithCLI(repoPath, branch, title, body, targetBranch);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a merge request using GitLab CLI (glab).
|
|
171
|
+
* Fallback method when OAuth is not available.
|
|
172
|
+
*/
|
|
173
|
+
createMRWithCLI(repoPath, branch, title, body, targetBranch) {
|
|
174
|
+
if (!this.isAvailable()) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: 'GitLab CLI (glab) is not installed or not authenticated. Install from https://gitlab.com/gitlab-org/cli, or connect GitLab OAuth in workspace settings.',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
// Escape special characters in title and body for shell
|
|
182
|
+
const escapedTitle = title.replace(/"/g, '\\"').replace(/`/g, '\\`');
|
|
183
|
+
const escapedBody = body.replace(/"/g, '\\"').replace(/`/g, '\\`');
|
|
184
|
+
// Build command with optional target branch
|
|
185
|
+
let cmd = `glab mr create --title "${escapedTitle}" --description "${escapedBody}" --source-branch "${branch}"`;
|
|
186
|
+
if (targetBranch) {
|
|
187
|
+
cmd += ` --target-branch "${targetBranch}"`;
|
|
188
|
+
}
|
|
189
|
+
const result = execSync(cmd, {
|
|
190
|
+
cwd: repoPath,
|
|
191
|
+
encoding: 'utf-8',
|
|
192
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
193
|
+
timeout: 60000, // 60s timeout
|
|
194
|
+
}).trim();
|
|
195
|
+
// glab mr create outputs the MR URL on success
|
|
196
|
+
const mrUrl = result.match(/https:\/\/gitlab\.com\/.+\/merge_requests\/\d+/)?.[0] || result;
|
|
197
|
+
console.log(`[GitLabIntegration] MR created via CLI: ${mrUrl}`);
|
|
198
|
+
return { success: true, mrUrl };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const err = error;
|
|
202
|
+
const errorMsg = err.stderr?.toString() || err.message || 'Unknown error';
|
|
203
|
+
console.error(`[GitLabIntegration] Failed to create MR via CLI: ${errorMsg}`);
|
|
204
|
+
return { success: false, error: errorMsg };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create a new project on GitLab and push local code
|
|
209
|
+
* First checks if workspace has a GitLab token, otherwise falls back to glab CLI
|
|
210
|
+
* @param repoPath - Path to the local repository
|
|
211
|
+
* @param projectName - Name for the GitLab project
|
|
212
|
+
* @param visibility - Project visibility (default: 'private')
|
|
213
|
+
* @returns Result with success status and remote URL
|
|
214
|
+
*/
|
|
215
|
+
async createProject(repoPath, projectName, visibility = 'private') {
|
|
216
|
+
// First, check if this repo belongs to a workspace with GitLab token
|
|
217
|
+
const workspace = workspaceManager.getWorkspaceForRepo(repoPath);
|
|
218
|
+
const tokenData = workspace ? workspaceManager.getGitLabToken(workspace.id) : null;
|
|
219
|
+
if (tokenData) {
|
|
220
|
+
// Use GitLab API with workspace token
|
|
221
|
+
return this.createProjectWithToken(repoPath, projectName, visibility, tokenData.accessToken);
|
|
222
|
+
}
|
|
223
|
+
// Fall back to glab CLI
|
|
224
|
+
return this.createProjectWithCLI(repoPath, projectName, visibility);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Create project using GitLab API with OAuth token
|
|
228
|
+
*/
|
|
229
|
+
async createProjectWithToken(repoPath, projectName, visibility, token) {
|
|
230
|
+
try {
|
|
231
|
+
// Create project via GitLab API using native https module (SEC-01 fix: no token in shell)
|
|
232
|
+
const projectData = JSON.stringify({
|
|
233
|
+
name: projectName,
|
|
234
|
+
visibility,
|
|
235
|
+
initialize_with_readme: false,
|
|
236
|
+
});
|
|
237
|
+
const response = await httpsRequest({
|
|
238
|
+
hostname: 'gitlab.com',
|
|
239
|
+
path: '/api/v4/projects',
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: {
|
|
242
|
+
Accept: 'application/json',
|
|
243
|
+
Authorization: `Bearer ${token}`,
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
'Content-Length': Buffer.byteLength(projectData),
|
|
246
|
+
},
|
|
247
|
+
}, projectData);
|
|
248
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
249
|
+
console.error(`[GitLabIntegration] API error: ${response.body}`);
|
|
250
|
+
return { success: false, error: response.body };
|
|
251
|
+
}
|
|
252
|
+
const project = JSON.parse(response.body);
|
|
253
|
+
const webUrl = project.web_url;
|
|
254
|
+
const httpUrlToRepo = project.http_url_to_repo;
|
|
255
|
+
// Now add remote and push using git commands with credential helper
|
|
256
|
+
const { createCredentialHelperScript, removeCredentialHelperScript } = await import('./git-credential-helper.js');
|
|
257
|
+
const scriptPath = createCredentialHelperScript(token, 'gitlab');
|
|
258
|
+
try {
|
|
259
|
+
// Add remote (without token in URL)
|
|
260
|
+
execSync(`git remote add origin "${httpUrlToRepo}"`, {
|
|
261
|
+
cwd: repoPath,
|
|
262
|
+
encoding: 'utf-8',
|
|
263
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
264
|
+
});
|
|
265
|
+
// Push with credential helper
|
|
266
|
+
const pushCmd = process.platform === 'win32'
|
|
267
|
+
? 'cmd /c "git push -u origin main || git push -u origin master"'
|
|
268
|
+
: 'git push -u origin main || git push -u origin master';
|
|
269
|
+
execSync(pushCmd, {
|
|
270
|
+
cwd: repoPath,
|
|
271
|
+
encoding: 'utf-8',
|
|
272
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
273
|
+
timeout: 120000,
|
|
274
|
+
env: {
|
|
275
|
+
...process.env,
|
|
276
|
+
GIT_ASKPASS: scriptPath,
|
|
277
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
// Always clean up credential helper script
|
|
283
|
+
removeCredentialHelperScript(scriptPath);
|
|
284
|
+
}
|
|
285
|
+
console.log(`[GitLabIntegration] Project created via API: ${webUrl}`);
|
|
286
|
+
return { success: true, mrUrl: webUrl };
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
const err = error;
|
|
290
|
+
const errorMsg = err.stderr?.toString() || err.message || 'Unknown error';
|
|
291
|
+
console.error(`[GitLabIntegration] Failed to create project via API: ${errorMsg}`);
|
|
292
|
+
return { success: false, error: errorMsg };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Create project using GitLab CLI (glab)
|
|
297
|
+
*/
|
|
298
|
+
createProjectWithCLI(repoPath, projectName, visibility) {
|
|
299
|
+
if (!this.isAvailable()) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: 'GitLab CLI (glab) is not installed or not authenticated. Install from https://gitlab.com/gitlab-org/cli',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
// glab repo create will create the project and set up remote origin
|
|
307
|
+
const result = execSync(`glab repo create ${projectName} --${visibility} --source=. --remote=origin --push`, {
|
|
308
|
+
cwd: repoPath,
|
|
309
|
+
encoding: 'utf-8',
|
|
310
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
311
|
+
timeout: 120000, // 2 min timeout for push
|
|
312
|
+
}).trim();
|
|
313
|
+
// Extract the project URL from output
|
|
314
|
+
const projectUrl = result.match(/https:\/\/gitlab\.com\/[^\s]+/)?.[0] || result;
|
|
315
|
+
console.log(`[GitLabIntegration] Project created via CLI: ${projectUrl}`);
|
|
316
|
+
return { success: true, mrUrl: projectUrl };
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
const err = error;
|
|
320
|
+
const errorMsg = err.stderr?.toString() || err.message || 'Unknown error';
|
|
321
|
+
console.error(`[GitLabIntegration] Failed to create project via CLI: ${errorMsg}`);
|
|
322
|
+
return { success: false, error: errorMsg };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get the URL to create an MR manually in the browser
|
|
327
|
+
* @param repoPath - Path to the repository
|
|
328
|
+
* @param branch - Branch to create MR from
|
|
329
|
+
*/
|
|
330
|
+
getMRCreateUrl(repoPath, branch) {
|
|
331
|
+
try {
|
|
332
|
+
// Get the remote URL
|
|
333
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
334
|
+
cwd: repoPath,
|
|
335
|
+
encoding: 'utf-8',
|
|
336
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
337
|
+
}).trim();
|
|
338
|
+
// Parse GitLab URL from remote
|
|
339
|
+
// Supports: git@gitlab.com:user/repo.git, https://gitlab.com/user/repo.git
|
|
340
|
+
let match = remoteUrl.match(/gitlab\.com[:/]([^/]+\/[^/.]+)/);
|
|
341
|
+
if (!match)
|
|
342
|
+
return null;
|
|
343
|
+
const pathWithNamespace = match[1].replace(/\.git$/, '');
|
|
344
|
+
return `https://gitlab.com/${pathWithNamespace}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Singleton instance
|
|
352
|
+
export const gitlabIntegration = new GitLabIntegration();
|
|
353
|
+
//# sourceMappingURL=gitlab-integration.js.map
|