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,649 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { workspaceManager } from '../config/workspaces.js';
|
|
3
|
+
import { GitHubDeviceAuth } from '../core/github-oauth.js';
|
|
4
|
+
import { GitLabDeviceAuth } from '../core/gitlab-oauth.js';
|
|
5
|
+
import { settingsManager } from '../config/settings.js';
|
|
6
|
+
import { repoRegistry } from '../config/repos.js';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
const router = Router();
|
|
11
|
+
// Store active device flow instances (workspaceId -> auth instance)
|
|
12
|
+
const activeDeviceFlows = new Map();
|
|
13
|
+
// Store active GitLab device flow instances (workspaceId -> auth instance)
|
|
14
|
+
const activeGitLabDeviceFlows = new Map();
|
|
15
|
+
// ============================================
|
|
16
|
+
// Workspace CRUD
|
|
17
|
+
// ============================================
|
|
18
|
+
/**
|
|
19
|
+
* GET /workspaces
|
|
20
|
+
* List all workspaces
|
|
21
|
+
*/
|
|
22
|
+
router.get('/', (_req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const workspaces = workspaceManager.getAll();
|
|
25
|
+
// Return workspaces with GitHub/GitLab connection status (but not the token)
|
|
26
|
+
const result = workspaces.map(ws => ({
|
|
27
|
+
id: ws.id,
|
|
28
|
+
name: ws.name,
|
|
29
|
+
scanPath: ws.scanPath,
|
|
30
|
+
github: ws.github ? {
|
|
31
|
+
username: ws.github.username,
|
|
32
|
+
tokenScope: ws.github.tokenScope,
|
|
33
|
+
connected: true,
|
|
34
|
+
} : null,
|
|
35
|
+
gitlab: ws.gitlab ? {
|
|
36
|
+
username: ws.gitlab.username,
|
|
37
|
+
tokenScope: ws.gitlab.tokenScope,
|
|
38
|
+
connected: true,
|
|
39
|
+
} : null,
|
|
40
|
+
repoConfigs: ws.repoConfigs || {},
|
|
41
|
+
createdAt: ws.createdAt,
|
|
42
|
+
updatedAt: ws.updatedAt,
|
|
43
|
+
}));
|
|
44
|
+
res.json({ success: true, data: result });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('Failed to list workspaces:', error);
|
|
48
|
+
res.status(500).json({ success: false, error: 'Failed to list workspaces' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* GET /workspaces/:id
|
|
53
|
+
* Get a single workspace
|
|
54
|
+
*/
|
|
55
|
+
router.get('/:id', (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const workspace = workspaceManager.get(req.params.id);
|
|
58
|
+
if (!workspace) {
|
|
59
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
60
|
+
}
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
data: {
|
|
64
|
+
id: workspace.id,
|
|
65
|
+
name: workspace.name,
|
|
66
|
+
scanPath: workspace.scanPath,
|
|
67
|
+
github: workspace.github ? {
|
|
68
|
+
username: workspace.github.username,
|
|
69
|
+
tokenScope: workspace.github.tokenScope,
|
|
70
|
+
connected: true,
|
|
71
|
+
} : null,
|
|
72
|
+
gitlab: workspace.gitlab ? {
|
|
73
|
+
username: workspace.gitlab.username,
|
|
74
|
+
tokenScope: workspace.gitlab.tokenScope,
|
|
75
|
+
connected: true,
|
|
76
|
+
} : null,
|
|
77
|
+
createdAt: workspace.createdAt,
|
|
78
|
+
updatedAt: workspace.updatedAt,
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('Failed to get workspace:', error);
|
|
84
|
+
res.status(500).json({ success: false, error: 'Failed to get workspace' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* POST /workspaces
|
|
89
|
+
* Create a new workspace
|
|
90
|
+
*/
|
|
91
|
+
router.post('/', (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const { name, scanPath } = req.body;
|
|
94
|
+
if (!name || !scanPath) {
|
|
95
|
+
return res.status(400).json({ success: false, error: 'Name and scanPath are required' });
|
|
96
|
+
}
|
|
97
|
+
const workspace = workspaceManager.create(name, scanPath);
|
|
98
|
+
// Reload repos to pick up the new workspace's scanPath
|
|
99
|
+
repoRegistry.reload();
|
|
100
|
+
res.status(201).json({
|
|
101
|
+
success: true,
|
|
102
|
+
data: {
|
|
103
|
+
id: workspace.id,
|
|
104
|
+
name: workspace.name,
|
|
105
|
+
scanPath: workspace.scanPath,
|
|
106
|
+
github: null,
|
|
107
|
+
gitlab: null,
|
|
108
|
+
createdAt: workspace.createdAt,
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : 'Failed to create workspace';
|
|
114
|
+
console.error('Failed to create workspace:', error);
|
|
115
|
+
res.status(400).json({ success: false, error: message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* PUT /workspaces/:id
|
|
120
|
+
* Update a workspace
|
|
121
|
+
*/
|
|
122
|
+
router.put('/:id', (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const { name, scanPath } = req.body;
|
|
125
|
+
const updates = {};
|
|
126
|
+
if (name !== undefined)
|
|
127
|
+
updates.name = name;
|
|
128
|
+
if (scanPath !== undefined)
|
|
129
|
+
updates.scanPath = scanPath;
|
|
130
|
+
const workspace = workspaceManager.update(req.params.id, updates);
|
|
131
|
+
// Reload repos if scanPath changed
|
|
132
|
+
if (scanPath !== undefined) {
|
|
133
|
+
repoRegistry.reload();
|
|
134
|
+
}
|
|
135
|
+
res.json({
|
|
136
|
+
success: true,
|
|
137
|
+
data: {
|
|
138
|
+
id: workspace.id,
|
|
139
|
+
name: workspace.name,
|
|
140
|
+
scanPath: workspace.scanPath,
|
|
141
|
+
github: workspace.github ? {
|
|
142
|
+
username: workspace.github.username,
|
|
143
|
+
tokenScope: workspace.github.tokenScope,
|
|
144
|
+
connected: true,
|
|
145
|
+
} : null,
|
|
146
|
+
gitlab: workspace.gitlab ? {
|
|
147
|
+
username: workspace.gitlab.username,
|
|
148
|
+
tokenScope: workspace.gitlab.tokenScope,
|
|
149
|
+
connected: true,
|
|
150
|
+
} : null,
|
|
151
|
+
createdAt: workspace.createdAt,
|
|
152
|
+
updatedAt: workspace.updatedAt,
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : 'Failed to update workspace';
|
|
158
|
+
console.error('Failed to update workspace:', error);
|
|
159
|
+
res.status(400).json({ success: false, error: message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* DELETE /workspaces/:id
|
|
164
|
+
* Delete a workspace
|
|
165
|
+
*/
|
|
166
|
+
router.delete('/:id', (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const deleted = workspaceManager.delete(req.params.id);
|
|
169
|
+
if (!deleted) {
|
|
170
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
171
|
+
}
|
|
172
|
+
// Clean up any active device flow for this workspace
|
|
173
|
+
activeDeviceFlows.delete(req.params.id);
|
|
174
|
+
activeGitLabDeviceFlows.delete(req.params.id);
|
|
175
|
+
// Reload repos to remove workspace associations
|
|
176
|
+
repoRegistry.reload();
|
|
177
|
+
res.json({ success: true, data: { deleted: true } });
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error('Failed to delete workspace:', error);
|
|
181
|
+
res.status(500).json({ success: false, error: 'Failed to delete workspace' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// ============================================
|
|
185
|
+
// GitHub OAuth (Device Flow)
|
|
186
|
+
// ============================================
|
|
187
|
+
/**
|
|
188
|
+
* POST /workspaces/:id/github/connect
|
|
189
|
+
* Start GitHub device flow for a workspace
|
|
190
|
+
* Returns user_code and verification_uri for user to authorize
|
|
191
|
+
*/
|
|
192
|
+
router.post('/:id/github/connect', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
const workspace = workspaceManager.get(req.params.id);
|
|
195
|
+
if (!workspace) {
|
|
196
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
197
|
+
}
|
|
198
|
+
// Check if GitLab is already connected
|
|
199
|
+
if (workspace.gitlab) {
|
|
200
|
+
return res.status(400).json({
|
|
201
|
+
success: false,
|
|
202
|
+
error: 'GitLab is already connected. Disconnect GitLab first to connect GitHub.',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Get GitHub client ID from settings
|
|
206
|
+
const settings = settingsManager.get();
|
|
207
|
+
const clientId = settings.github?.clientId;
|
|
208
|
+
if (!clientId) {
|
|
209
|
+
return res.status(400).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: 'GitHub OAuth not configured. Set github.clientId in settings.',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Create new device flow instance
|
|
215
|
+
const auth = new GitHubDeviceAuth(clientId);
|
|
216
|
+
const deviceCodeResponse = await auth.requestDeviceCode('repo read:user');
|
|
217
|
+
// Store the active flow
|
|
218
|
+
activeDeviceFlows.set(req.params.id, {
|
|
219
|
+
auth,
|
|
220
|
+
deviceCode: deviceCodeResponse.deviceCode,
|
|
221
|
+
userCode: deviceCodeResponse.userCode,
|
|
222
|
+
verificationUri: deviceCodeResponse.verificationUri,
|
|
223
|
+
expiresAt: Date.now() + (deviceCodeResponse.expiresIn * 1000),
|
|
224
|
+
});
|
|
225
|
+
res.json({
|
|
226
|
+
success: true,
|
|
227
|
+
data: {
|
|
228
|
+
userCode: deviceCodeResponse.userCode,
|
|
229
|
+
verificationUri: deviceCodeResponse.verificationUri,
|
|
230
|
+
expiresIn: deviceCodeResponse.expiresIn,
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : 'Failed to start GitHub auth';
|
|
236
|
+
console.error('Failed to start GitHub auth:', error);
|
|
237
|
+
res.status(500).json({ success: false, error: message });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
/**
|
|
241
|
+
* GET /workspaces/:id/github/status
|
|
242
|
+
* Poll for GitHub OAuth completion
|
|
243
|
+
* Returns status: 'pending' | 'success' | 'expired' | 'error'
|
|
244
|
+
*/
|
|
245
|
+
router.get('/:id/github/status', async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const flow = activeDeviceFlows.get(req.params.id);
|
|
248
|
+
if (!flow) {
|
|
249
|
+
return res.status(404).json({
|
|
250
|
+
success: false,
|
|
251
|
+
error: 'No active authentication flow for this workspace',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// Check if expired
|
|
255
|
+
if (Date.now() > flow.expiresAt) {
|
|
256
|
+
activeDeviceFlows.delete(req.params.id);
|
|
257
|
+
return res.json({ success: true, data: { status: 'expired', error: 'Device code has expired' } });
|
|
258
|
+
}
|
|
259
|
+
// Poll for token
|
|
260
|
+
const result = await flow.auth.pollForToken(flow.deviceCode);
|
|
261
|
+
if (result.status === 'success' && result.token) {
|
|
262
|
+
// Get user info
|
|
263
|
+
const user = await flow.auth.getUser(result.token.accessToken);
|
|
264
|
+
// Save to workspace
|
|
265
|
+
workspaceManager.setGitHubToken(req.params.id, result.token.accessToken, user.login, result.token.scope);
|
|
266
|
+
// Clean up
|
|
267
|
+
activeDeviceFlows.delete(req.params.id);
|
|
268
|
+
res.json({
|
|
269
|
+
success: true,
|
|
270
|
+
data: {
|
|
271
|
+
status: 'success',
|
|
272
|
+
user: {
|
|
273
|
+
username: user.login,
|
|
274
|
+
name: user.name,
|
|
275
|
+
avatarUrl: user.avatarUrl,
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else if (result.status === 'pending') {
|
|
281
|
+
res.json({ success: true, data: { status: 'pending' } });
|
|
282
|
+
}
|
|
283
|
+
else if (result.status === 'expired') {
|
|
284
|
+
activeDeviceFlows.delete(req.params.id);
|
|
285
|
+
res.json({ success: true, data: { status: 'expired', error: result.error } });
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
res.json({ success: true, data: { status: 'error', error: result.error } });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : 'Failed to check auth status';
|
|
293
|
+
console.error('Failed to check auth status:', error);
|
|
294
|
+
res.status(500).json({ success: false, error: message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* DELETE /workspaces/:id/github/disconnect
|
|
299
|
+
* Remove GitHub connection from a workspace
|
|
300
|
+
*/
|
|
301
|
+
router.delete('/:id/github/disconnect', (req, res) => {
|
|
302
|
+
try {
|
|
303
|
+
const workspace = workspaceManager.get(req.params.id);
|
|
304
|
+
if (!workspace) {
|
|
305
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
306
|
+
}
|
|
307
|
+
workspaceManager.clearGitHubToken(req.params.id);
|
|
308
|
+
// Clean up any active device flow
|
|
309
|
+
activeDeviceFlows.delete(req.params.id);
|
|
310
|
+
res.json({ success: true, data: { disconnected: true } });
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
console.error('Failed to disconnect GitHub:', error);
|
|
314
|
+
res.status(500).json({ success: false, error: 'Failed to disconnect GitHub' });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
// ============================================
|
|
318
|
+
// GitLab OAuth (Device Flow)
|
|
319
|
+
// ============================================
|
|
320
|
+
/**
|
|
321
|
+
* POST /workspaces/:id/gitlab/connect
|
|
322
|
+
* Start GitLab device flow for a workspace
|
|
323
|
+
* Returns user_code and verification_uri for user to authorize
|
|
324
|
+
*/
|
|
325
|
+
router.post('/:id/gitlab/connect', async (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const workspace = workspaceManager.get(req.params.id);
|
|
328
|
+
if (!workspace) {
|
|
329
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
330
|
+
}
|
|
331
|
+
// Check if GitHub is already connected
|
|
332
|
+
if (workspace.github) {
|
|
333
|
+
return res.status(400).json({
|
|
334
|
+
success: false,
|
|
335
|
+
error: 'GitHub is already connected. Disconnect GitHub first to connect GitLab.',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// Get GitLab client ID from settings
|
|
339
|
+
const settings = settingsManager.get();
|
|
340
|
+
const clientId = settings.gitlab?.clientId;
|
|
341
|
+
if (!clientId) {
|
|
342
|
+
return res.status(400).json({
|
|
343
|
+
success: false,
|
|
344
|
+
error: 'GitLab OAuth not configured. Set gitlab.clientId in settings.',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Create new device flow instance
|
|
348
|
+
const auth = new GitLabDeviceAuth(clientId);
|
|
349
|
+
const deviceCodeResponse = await auth.requestDeviceCode('api read_user');
|
|
350
|
+
// Store the active flow
|
|
351
|
+
activeGitLabDeviceFlows.set(req.params.id, {
|
|
352
|
+
auth,
|
|
353
|
+
deviceCode: deviceCodeResponse.deviceCode,
|
|
354
|
+
userCode: deviceCodeResponse.userCode,
|
|
355
|
+
verificationUri: deviceCodeResponse.verificationUri,
|
|
356
|
+
expiresAt: Date.now() + (deviceCodeResponse.expiresIn * 1000),
|
|
357
|
+
});
|
|
358
|
+
res.json({
|
|
359
|
+
success: true,
|
|
360
|
+
data: {
|
|
361
|
+
userCode: deviceCodeResponse.userCode,
|
|
362
|
+
verificationUri: deviceCodeResponse.verificationUri,
|
|
363
|
+
expiresIn: deviceCodeResponse.expiresIn,
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const message = error instanceof Error ? error.message : 'Failed to start GitLab auth';
|
|
369
|
+
console.error('Failed to start GitLab auth:', error);
|
|
370
|
+
res.status(500).json({ success: false, error: message });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
/**
|
|
374
|
+
* GET /workspaces/:id/gitlab/status
|
|
375
|
+
* Poll for GitLab OAuth completion
|
|
376
|
+
* Returns status: 'pending' | 'success' | 'expired' | 'error'
|
|
377
|
+
*/
|
|
378
|
+
router.get('/:id/gitlab/status', async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const flow = activeGitLabDeviceFlows.get(req.params.id);
|
|
381
|
+
if (!flow) {
|
|
382
|
+
return res.status(404).json({
|
|
383
|
+
success: false,
|
|
384
|
+
error: 'No active authentication flow for this workspace',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// Check if expired
|
|
388
|
+
if (Date.now() > flow.expiresAt) {
|
|
389
|
+
activeGitLabDeviceFlows.delete(req.params.id);
|
|
390
|
+
return res.json({ success: true, data: { status: 'expired', error: 'Device code has expired' } });
|
|
391
|
+
}
|
|
392
|
+
// Poll for token
|
|
393
|
+
const result = await flow.auth.pollForToken(flow.deviceCode);
|
|
394
|
+
if (result.status === 'success' && result.token) {
|
|
395
|
+
// Get user info
|
|
396
|
+
const user = await flow.auth.getUser(result.token.accessToken);
|
|
397
|
+
// Calculate expiration if provided
|
|
398
|
+
let expiresAt = null;
|
|
399
|
+
if (result.token.expiresIn) {
|
|
400
|
+
expiresAt = new Date(Date.now() + result.token.expiresIn * 1000).toISOString();
|
|
401
|
+
}
|
|
402
|
+
// Save to workspace
|
|
403
|
+
workspaceManager.setGitLabToken(req.params.id, result.token.accessToken, user.username, result.token.scope, result.token.refreshToken, expiresAt);
|
|
404
|
+
// Clean up
|
|
405
|
+
activeGitLabDeviceFlows.delete(req.params.id);
|
|
406
|
+
res.json({
|
|
407
|
+
success: true,
|
|
408
|
+
data: {
|
|
409
|
+
status: 'success',
|
|
410
|
+
user: {
|
|
411
|
+
username: user.username,
|
|
412
|
+
name: user.name,
|
|
413
|
+
avatarUrl: user.avatarUrl,
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else if (result.status === 'pending') {
|
|
419
|
+
res.json({ success: true, data: { status: 'pending' } });
|
|
420
|
+
}
|
|
421
|
+
else if (result.status === 'expired') {
|
|
422
|
+
activeGitLabDeviceFlows.delete(req.params.id);
|
|
423
|
+
res.json({ success: true, data: { status: 'expired', error: result.error } });
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
res.json({ success: true, data: { status: 'error', error: result.error } });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const message = error instanceof Error ? error.message : 'Failed to check auth status';
|
|
431
|
+
console.error('Failed to check auth status:', error);
|
|
432
|
+
res.status(500).json({ success: false, error: message });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
/**
|
|
436
|
+
* DELETE /workspaces/:id/gitlab/disconnect
|
|
437
|
+
* Remove GitLab connection from a workspace
|
|
438
|
+
*/
|
|
439
|
+
router.delete('/:id/gitlab/disconnect', (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const workspace = workspaceManager.get(req.params.id);
|
|
442
|
+
if (!workspace) {
|
|
443
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
444
|
+
}
|
|
445
|
+
workspaceManager.clearGitLabToken(req.params.id);
|
|
446
|
+
// Clean up any active device flow
|
|
447
|
+
activeGitLabDeviceFlows.delete(req.params.id);
|
|
448
|
+
res.json({ success: true, data: { disconnected: true } });
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.error('Failed to disconnect GitLab:', error);
|
|
452
|
+
res.status(500).json({ success: false, error: 'Failed to disconnect GitLab' });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
// ============================================
|
|
456
|
+
// Simple Repo Creation
|
|
457
|
+
// ============================================
|
|
458
|
+
/**
|
|
459
|
+
* POST /workspaces/:id/repos
|
|
460
|
+
* Create a new empty repository folder in workspace
|
|
461
|
+
*/
|
|
462
|
+
router.post('/:id/repos', async (req, res) => {
|
|
463
|
+
try {
|
|
464
|
+
const { id } = req.params;
|
|
465
|
+
const { repoName } = req.body;
|
|
466
|
+
// Validate repo name
|
|
467
|
+
if (!repoName || typeof repoName !== 'string') {
|
|
468
|
+
return res.status(400).json({ success: false, error: 'Repository name is required' });
|
|
469
|
+
}
|
|
470
|
+
// Validate format: lowercase alphanumeric + dashes
|
|
471
|
+
const validName = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
472
|
+
if (!validName.test(repoName)) {
|
|
473
|
+
return res.status(400).json({
|
|
474
|
+
success: false,
|
|
475
|
+
error: 'Repository name must be lowercase alphanumeric with dashes, and cannot start/end with a dash'
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
// Get workspace
|
|
479
|
+
const workspace = workspaceManager.get(id);
|
|
480
|
+
if (!workspace) {
|
|
481
|
+
return res.status(404).json({ success: false, error: 'Workspace not found' });
|
|
482
|
+
}
|
|
483
|
+
// Create folder path
|
|
484
|
+
const repoPath = path.join(workspace.scanPath, repoName);
|
|
485
|
+
// Check if folder already exists
|
|
486
|
+
if (fs.existsSync(repoPath)) {
|
|
487
|
+
return res.status(409).json({ success: false, error: 'A folder with this name already exists' });
|
|
488
|
+
}
|
|
489
|
+
// Create folder
|
|
490
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
491
|
+
// Initialize git
|
|
492
|
+
try {
|
|
493
|
+
execSync('git init', { cwd: repoPath, stdio: 'pipe' });
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Git init failed, but folder was created - continue
|
|
497
|
+
console.warn(`Git init failed for ${repoPath}, continuing without git`);
|
|
498
|
+
}
|
|
499
|
+
// Create a basic package.json
|
|
500
|
+
const packageJson = {
|
|
501
|
+
name: repoName,
|
|
502
|
+
version: '0.1.0',
|
|
503
|
+
private: true,
|
|
504
|
+
description: '',
|
|
505
|
+
scripts: {},
|
|
506
|
+
};
|
|
507
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
508
|
+
// Reload repo registry to pick up new folder
|
|
509
|
+
repoRegistry.reload();
|
|
510
|
+
// Find the new repo in registry
|
|
511
|
+
const repos = repoRegistry.getAll();
|
|
512
|
+
const newRepo = repos.find(r => r.path === repoPath);
|
|
513
|
+
res.status(201).json({
|
|
514
|
+
success: true,
|
|
515
|
+
data: {
|
|
516
|
+
repoId: newRepo?.id || repoName,
|
|
517
|
+
path: repoPath,
|
|
518
|
+
name: repoName,
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
const message = error instanceof Error ? error.message : 'Failed to create repository';
|
|
524
|
+
console.error('Failed to create repository:', error);
|
|
525
|
+
res.status(500).json({ success: false, error: message });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// ============================================
|
|
529
|
+
// Repo Config Overrides
|
|
530
|
+
// ============================================
|
|
531
|
+
/**
|
|
532
|
+
* GET /workspaces/:id/repos/:repoId/config
|
|
533
|
+
* Get repo config override for a workspace
|
|
534
|
+
*/
|
|
535
|
+
router.get('/:id/repos/:repoId/config', (req, res) => {
|
|
536
|
+
try {
|
|
537
|
+
const config = workspaceManager.getRepoConfig(req.params.id, req.params.repoId);
|
|
538
|
+
res.json({ success: true, data: config });
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
console.error('Failed to get repo config:', error);
|
|
542
|
+
res.status(500).json({ success: false, error: 'Failed to get repo config' });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
/**
|
|
546
|
+
* PUT /workspaces/:id/repos/:repoId/config
|
|
547
|
+
* Set repo config override for a workspace
|
|
548
|
+
*/
|
|
549
|
+
router.put('/:id/repos/:repoId/config', (req, res) => {
|
|
550
|
+
try {
|
|
551
|
+
const { proof, port, commands } = req.body;
|
|
552
|
+
workspaceManager.setRepoConfig(req.params.id, req.params.repoId, { proof, port, commands });
|
|
553
|
+
repoRegistry.reload(); // Apply changes immediately
|
|
554
|
+
res.json({ success: true });
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
const message = error instanceof Error ? error.message : 'Failed to save repo config';
|
|
558
|
+
console.error('Failed to save repo config:', error);
|
|
559
|
+
res.status(400).json({ success: false, error: message });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
/**
|
|
563
|
+
* DELETE /workspaces/:id/repos/:repoId/config
|
|
564
|
+
* Delete repo config override for a workspace
|
|
565
|
+
*/
|
|
566
|
+
router.delete('/:id/repos/:repoId/config', (req, res) => {
|
|
567
|
+
try {
|
|
568
|
+
const deleted = workspaceManager.deleteRepoConfig(req.params.id, req.params.repoId);
|
|
569
|
+
if (deleted) {
|
|
570
|
+
repoRegistry.reload(); // Apply changes immediately
|
|
571
|
+
}
|
|
572
|
+
res.json({ success: true, data: { deleted } });
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
console.error('Failed to delete repo config:', error);
|
|
576
|
+
res.status(500).json({ success: false, error: 'Failed to delete repo config' });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// ============================================
|
|
580
|
+
// Service Config Overrides (Monorepo)
|
|
581
|
+
// ============================================
|
|
582
|
+
/**
|
|
583
|
+
* GET /workspaces/:id/repos/:repoId/services/:serviceId/config
|
|
584
|
+
* Get service config override for a workspace repo
|
|
585
|
+
*/
|
|
586
|
+
router.get('/:id/repos/:repoId/services/:serviceId/config', (req, res) => {
|
|
587
|
+
try {
|
|
588
|
+
const config = workspaceManager.getServiceConfig(req.params.id, req.params.repoId, req.params.serviceId);
|
|
589
|
+
res.json({ success: true, data: config });
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
console.error('Failed to get service config:', error);
|
|
593
|
+
res.status(500).json({ success: false, error: 'Failed to get service config' });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
/**
|
|
597
|
+
* PUT /workspaces/:id/repos/:repoId/services/:serviceId/config
|
|
598
|
+
* Set service config override for a workspace repo
|
|
599
|
+
*/
|
|
600
|
+
router.put('/:id/repos/:repoId/services/:serviceId/config', (req, res) => {
|
|
601
|
+
try {
|
|
602
|
+
const { proof, port } = req.body;
|
|
603
|
+
workspaceManager.setServiceConfig(req.params.id, req.params.repoId, req.params.serviceId, { proof, port });
|
|
604
|
+
res.json({ success: true });
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
const message = error instanceof Error ? error.message : 'Failed to save service config';
|
|
608
|
+
console.error('Failed to save service config:', error);
|
|
609
|
+
res.status(400).json({ success: false, error: message });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
/**
|
|
613
|
+
* DELETE /workspaces/:id/repos/:repoId/services/:serviceId/config
|
|
614
|
+
* Delete service config override for a workspace repo
|
|
615
|
+
*/
|
|
616
|
+
router.delete('/:id/repos/:repoId/services/:serviceId/config', (req, res) => {
|
|
617
|
+
try {
|
|
618
|
+
const deleted = workspaceManager.deleteServiceConfig(req.params.id, req.params.repoId, req.params.serviceId);
|
|
619
|
+
res.json({ success: true, data: { deleted } });
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
console.error('Failed to delete service config:', error);
|
|
623
|
+
res.status(500).json({ success: false, error: 'Failed to delete service config' });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
// ============================================
|
|
627
|
+
// Migration
|
|
628
|
+
// ============================================
|
|
629
|
+
/**
|
|
630
|
+
* POST /workspaces/migrate
|
|
631
|
+
* Migrate existing scan paths to workspaces
|
|
632
|
+
*/
|
|
633
|
+
router.post('/migrate', (_req, res) => {
|
|
634
|
+
try {
|
|
635
|
+
const scanPaths = repoRegistry.getScanPaths();
|
|
636
|
+
const result = workspaceManager.migrateFromScanPaths(scanPaths);
|
|
637
|
+
res.json({
|
|
638
|
+
success: true,
|
|
639
|
+
data: result,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
const message = error instanceof Error ? error.message : 'Migration failed';
|
|
644
|
+
console.error('Migration failed:', error);
|
|
645
|
+
res.status(500).json({ success: false, error: message });
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
export default router;
|
|
649
|
+
//# sourceMappingURL=workspace-routes.js.map
|