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.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/config/repos.example.json +128 -0
  4. package/config/settings.example.json +64 -0
  5. package/config/skills/code-review.md +76 -0
  6. package/config/skills/full-check.md +26 -0
  7. package/config/skills/lint-fix.md +23 -0
  8. package/dist/api/agent-routes.d.ts +2 -0
  9. package/dist/api/agent-routes.d.ts.map +1 -0
  10. package/dist/api/agent-routes.js +251 -0
  11. package/dist/api/agent-routes.js.map +1 -0
  12. package/dist/api/app-routes.d.ts +2 -0
  13. package/dist/api/app-routes.d.ts.map +1 -0
  14. package/dist/api/app-routes.js +150 -0
  15. package/dist/api/app-routes.js.map +1 -0
  16. package/dist/api/docker-routes.d.ts +2 -0
  17. package/dist/api/docker-routes.d.ts.map +1 -0
  18. package/dist/api/docker-routes.js +167 -0
  19. package/dist/api/docker-routes.js.map +1 -0
  20. package/dist/api/middleware.d.ts +6 -0
  21. package/dist/api/middleware.d.ts.map +1 -0
  22. package/dist/api/middleware.js +293 -0
  23. package/dist/api/middleware.js.map +1 -0
  24. package/dist/api/pin-auth.d.ts +65 -0
  25. package/dist/api/pin-auth.d.ts.map +1 -0
  26. package/dist/api/pin-auth.js +218 -0
  27. package/dist/api/pin-auth.js.map +1 -0
  28. package/dist/api/routes.d.ts +2 -0
  29. package/dist/api/routes.d.ts.map +1 -0
  30. package/dist/api/routes.js +473 -0
  31. package/dist/api/routes.js.map +1 -0
  32. package/dist/api/settings-routes.d.ts +2 -0
  33. package/dist/api/settings-routes.d.ts.map +1 -0
  34. package/dist/api/settings-routes.js +570 -0
  35. package/dist/api/settings-routes.js.map +1 -0
  36. package/dist/api/skill-routes.d.ts +2 -0
  37. package/dist/api/skill-routes.d.ts.map +1 -0
  38. package/dist/api/skill-routes.js +88 -0
  39. package/dist/api/skill-routes.js.map +1 -0
  40. package/dist/api/terminal-routes.d.ts +2 -0
  41. package/dist/api/terminal-routes.d.ts.map +1 -0
  42. package/dist/api/terminal-routes.js +3524 -0
  43. package/dist/api/terminal-routes.js.map +1 -0
  44. package/dist/api/tunnel-routes.d.ts +2 -0
  45. package/dist/api/tunnel-routes.d.ts.map +1 -0
  46. package/dist/api/tunnel-routes.js +196 -0
  47. package/dist/api/tunnel-routes.js.map +1 -0
  48. package/dist/api/workspace-routes.d.ts +3 -0
  49. package/dist/api/workspace-routes.d.ts.map +1 -0
  50. package/dist/api/workspace-routes.js +649 -0
  51. package/dist/api/workspace-routes.js.map +1 -0
  52. package/dist/cli.d.ts +3 -0
  53. package/dist/cli.d.ts.map +1 -0
  54. package/dist/cli.js +276 -0
  55. package/dist/cli.js.map +1 -0
  56. package/dist/client/assets/index-B4r0njGe.js +780 -0
  57. package/dist/client/assets/index-CY_9MyE0.css +1 -0
  58. package/dist/client/favicon.svg +5 -0
  59. package/dist/client/icons/icon-192.svg +5 -0
  60. package/dist/client/icons/icon-512.svg +5 -0
  61. package/dist/client/icons/logo-with-message.png +0 -0
  62. package/dist/client/icons/logo.png +0 -0
  63. package/dist/client/index.html +25 -0
  64. package/dist/client/manifest.json +62 -0
  65. package/dist/client/sw.js +243 -0
  66. package/dist/config/agent-usage.d.ts +34 -0
  67. package/dist/config/agent-usage.d.ts.map +1 -0
  68. package/dist/config/agent-usage.js +87 -0
  69. package/dist/config/agent-usage.js.map +1 -0
  70. package/dist/config/repos.d.ts +34 -0
  71. package/dist/config/repos.d.ts.map +1 -0
  72. package/dist/config/repos.js +412 -0
  73. package/dist/config/repos.js.map +1 -0
  74. package/dist/config/settings.d.ts +634 -0
  75. package/dist/config/settings.d.ts.map +1 -0
  76. package/dist/config/settings.js +459 -0
  77. package/dist/config/settings.js.map +1 -0
  78. package/dist/config/skills.d.ts +18 -0
  79. package/dist/config/skills.d.ts.map +1 -0
  80. package/dist/config/skills.js +174 -0
  81. package/dist/config/skills.js.map +1 -0
  82. package/dist/config/workspaces.d.ts +961 -0
  83. package/dist/config/workspaces.d.ts.map +1 -0
  84. package/dist/config/workspaces.js +482 -0
  85. package/dist/config/workspaces.js.map +1 -0
  86. package/dist/core/app-manager.d.ts +85 -0
  87. package/dist/core/app-manager.d.ts.map +1 -0
  88. package/dist/core/app-manager.js +447 -0
  89. package/dist/core/app-manager.js.map +1 -0
  90. package/dist/core/claude-invoker.d.ts +49 -0
  91. package/dist/core/claude-invoker.d.ts.map +1 -0
  92. package/dist/core/claude-invoker.js +583 -0
  93. package/dist/core/claude-invoker.js.map +1 -0
  94. package/dist/core/claude-session-reader.d.ts +25 -0
  95. package/dist/core/claude-session-reader.d.ts.map +1 -0
  96. package/dist/core/claude-session-reader.js +184 -0
  97. package/dist/core/claude-session-reader.js.map +1 -0
  98. package/dist/core/claude-usage-query.d.ts +78 -0
  99. package/dist/core/claude-usage-query.d.ts.map +1 -0
  100. package/dist/core/claude-usage-query.js +294 -0
  101. package/dist/core/claude-usage-query.js.map +1 -0
  102. package/dist/core/git-credential-helper.d.ts +57 -0
  103. package/dist/core/git-credential-helper.d.ts.map +1 -0
  104. package/dist/core/git-credential-helper.js +176 -0
  105. package/dist/core/git-credential-helper.js.map +1 -0
  106. package/dist/core/git-sandbox.d.ts +135 -0
  107. package/dist/core/git-sandbox.d.ts.map +1 -0
  108. package/dist/core/git-sandbox.js +907 -0
  109. package/dist/core/git-sandbox.js.map +1 -0
  110. package/dist/core/github-integration.d.ts +66 -0
  111. package/dist/core/github-integration.d.ts.map +1 -0
  112. package/dist/core/github-integration.js +350 -0
  113. package/dist/core/github-integration.js.map +1 -0
  114. package/dist/core/github-oauth.d.ts +88 -0
  115. package/dist/core/github-oauth.d.ts.map +1 -0
  116. package/dist/core/github-oauth.js +244 -0
  117. package/dist/core/github-oauth.js.map +1 -0
  118. package/dist/core/gitlab-integration.d.ts +66 -0
  119. package/dist/core/gitlab-integration.d.ts.map +1 -0
  120. package/dist/core/gitlab-integration.js +353 -0
  121. package/dist/core/gitlab-integration.js.map +1 -0
  122. package/dist/core/gitlab-oauth.d.ts +100 -0
  123. package/dist/core/gitlab-oauth.d.ts.map +1 -0
  124. package/dist/core/gitlab-oauth.js +366 -0
  125. package/dist/core/gitlab-oauth.js.map +1 -0
  126. package/dist/core/insights-extractor.d.ts +68 -0
  127. package/dist/core/insights-extractor.d.ts.map +1 -0
  128. package/dist/core/insights-extractor.js +402 -0
  129. package/dist/core/insights-extractor.js.map +1 -0
  130. package/dist/core/logger.d.ts +27 -0
  131. package/dist/core/logger.d.ts.map +1 -0
  132. package/dist/core/logger.js +70 -0
  133. package/dist/core/logger.js.map +1 -0
  134. package/dist/core/process-runner.d.ts +27 -0
  135. package/dist/core/process-runner.d.ts.map +1 -0
  136. package/dist/core/process-runner.js +147 -0
  137. package/dist/core/process-runner.js.map +1 -0
  138. package/dist/core/project-detector.d.ts +30 -0
  139. package/dist/core/project-detector.d.ts.map +1 -0
  140. package/dist/core/project-detector.js +482 -0
  141. package/dist/core/project-detector.js.map +1 -0
  142. package/dist/core/qr-generator.d.ts +18 -0
  143. package/dist/core/qr-generator.d.ts.map +1 -0
  144. package/dist/core/qr-generator.js +61 -0
  145. package/dist/core/qr-generator.js.map +1 -0
  146. package/dist/core/remote-tunnel-manager.d.ts +59 -0
  147. package/dist/core/remote-tunnel-manager.d.ts.map +1 -0
  148. package/dist/core/remote-tunnel-manager.js +235 -0
  149. package/dist/core/remote-tunnel-manager.js.map +1 -0
  150. package/dist/core/shared-docker-manager.d.ts +41 -0
  151. package/dist/core/shared-docker-manager.d.ts.map +1 -0
  152. package/dist/core/shared-docker-manager.js +409 -0
  153. package/dist/core/shared-docker-manager.js.map +1 -0
  154. package/dist/core/skill-executor.d.ts +25 -0
  155. package/dist/core/skill-executor.d.ts.map +1 -0
  156. package/dist/core/skill-executor.js +171 -0
  157. package/dist/core/skill-executor.js.map +1 -0
  158. package/dist/core/terminal-session.d.ts +149 -0
  159. package/dist/core/terminal-session.d.ts.map +1 -0
  160. package/dist/core/terminal-session.js +2340 -0
  161. package/dist/core/terminal-session.js.map +1 -0
  162. package/dist/core/tunnel-manager.d.ts +35 -0
  163. package/dist/core/tunnel-manager.d.ts.map +1 -0
  164. package/dist/core/tunnel-manager.js +137 -0
  165. package/dist/core/tunnel-manager.js.map +1 -0
  166. package/dist/core/usage-manager.d.ts +57 -0
  167. package/dist/core/usage-manager.d.ts.map +1 -0
  168. package/dist/core/usage-manager.js +363 -0
  169. package/dist/core/usage-manager.js.map +1 -0
  170. package/dist/core/ws-manager.d.ts +39 -0
  171. package/dist/core/ws-manager.d.ts.map +1 -0
  172. package/dist/core/ws-manager.js +190 -0
  173. package/dist/core/ws-manager.js.map +1 -0
  174. package/dist/index.d.ts +7 -0
  175. package/dist/index.d.ts.map +1 -0
  176. package/dist/index.js +229 -0
  177. package/dist/index.js.map +1 -0
  178. package/dist/types.d.ts +868 -0
  179. package/dist/types.d.ts.map +1 -0
  180. package/dist/types.js +119 -0
  181. package/dist/types.js.map +1 -0
  182. 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