glad-web 1.0.28 → 1.0.30

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.
@@ -43,6 +43,9 @@ const { JobStore } = require('../schedule/job-store');
43
43
  const JobRunner = require('../schedule/job-runner');
44
44
  const SchedulerService = require('../schedule/scheduler-service');
45
45
  const { getClaudeRuntimeConfig } = require('../claude/config');
46
+ const registerScheduleRoutes = require('../server/routes/schedules');
47
+ const registerWorkspaceRoutes = require('../server/routes/workspace');
48
+ const registerProviderRoutes = require('../server/routes/providers');
46
49
 
47
50
  async function webCommand(options) {
48
51
  const port = parseInt(options.port) || 3000;
@@ -124,65 +127,7 @@ async function webCommand(options) {
124
127
  res.json({ defaultWorkingDirectory: baseDir });
125
128
  });
126
129
 
127
- // API: Scheduled tasks
128
- app.get('/api/schedules', (req, res) => {
129
- res.json(jobStore.list());
130
- });
131
-
132
- app.post('/api/schedules', (req, res) => {
133
- try {
134
- const job = jobStore.create(req.body || {});
135
- res.json(job);
136
- } catch (e) {
137
- res.status(400).json({ error: e.message });
138
- }
139
- });
140
-
141
- app.get('/api/schedules/:id', (req, res) => {
142
- const job = jobStore.get(req.params.id);
143
- if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
144
- res.json(job);
145
- });
146
-
147
- app.patch('/api/schedules/:id', (req, res) => {
148
- const job = jobStore.update(req.params.id, req.body || {});
149
- if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
150
- res.json(job);
151
- });
152
-
153
- app.patch('/api/schedules/:id/enabled', (req, res) => {
154
- const job = jobStore.patchRuntime(req.params.id, { enabled: Boolean(req.body && req.body.enabled) });
155
- if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
156
- res.json(job);
157
- });
158
-
159
- app.delete('/api/schedules/:id', (req, res) => {
160
- res.json({ success: jobStore.delete(req.params.id) });
161
- });
162
-
163
- app.post('/api/schedules/:id/duplicate', (req, res) => {
164
- const job = jobStore.duplicate(req.params.id);
165
- if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
166
- res.json(job);
167
- });
168
-
169
- app.post('/api/schedules/:id/run', async (req, res) => {
170
- try {
171
- const result = await jobRunner.run(req.params.id, { manual: false });
172
- res.json(result);
173
- } catch (e) {
174
- res.status(400).json({ error: e.message });
175
- }
176
- });
177
-
178
- app.post('/api/schedules/:id/simulate', async (req, res) => {
179
- try {
180
- const result = await jobRunner.run(req.params.id, { manual: true, background: true });
181
- res.json(result);
182
- } catch (e) {
183
- res.status(400).json({ error: e.message });
184
- }
185
- });
130
+ registerScheduleRoutes(app, { jobStore, jobRunner });
186
131
 
187
132
  // API: List all active sessions
188
133
  app.get('/api/sessions', (req, res) => {
@@ -267,10 +212,10 @@ async function webCommand(options) {
267
212
  });
268
213
 
269
214
  // Browser images are stored only in a private, per-session temporary directory.
270
- // Codex receives the resulting local path through its app-server protocol.
215
+ // Structured providers receive either a local path or validated base64 content.
271
216
  app.post('/api/sessions/:id/attachments/images', express.raw({ type: () => true, limit: '50mb' }), async (req, res) => {
272
217
  try {
273
- const attachment = await sessionManager.storeCodexImageAttachment(req.params.id, req.body);
218
+ const attachment = await sessionManager.storeImageAttachment(req.params.id, req.body);
274
219
  res.status(201).json({ success: true, attachment });
275
220
  } catch (e) {
276
221
  res.status(e.statusCode || 500).json({ error: e.message });
@@ -281,7 +226,7 @@ async function webCommand(options) {
281
226
  // Small sequential chunks let the browser report progress from server receipts.
282
227
  app.post('/api/sessions/:id/attachments/images/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
283
228
  try {
284
- const result = await sessionManager.appendCodexImageChunk(req.params.id, {
229
+ const result = await sessionManager.appendImageChunk(req.params.id, {
285
230
  uploadId: req.get('X-Glad-Upload-Id'),
286
231
  chunkIndex: req.get('X-Glad-Chunk-Index'),
287
232
  chunkTotal: req.get('X-Glad-Chunk-Total')
@@ -294,7 +239,7 @@ async function webCommand(options) {
294
239
 
295
240
  app.delete('/api/sessions/:id/attachments/images/uploads/:uploadId', async (req, res) => {
296
241
  try {
297
- const removed = await sessionManager.discardCodexImageUpload(req.params.id, req.params.uploadId);
242
+ const removed = await sessionManager.discardImageUpload(req.params.id, req.params.uploadId);
298
243
  res.json({ success: true, removed });
299
244
  } catch (e) {
300
245
  res.status(e.statusCode || 500).json({ error: e.message });
@@ -303,7 +248,7 @@ async function webCommand(options) {
303
248
 
304
249
  app.delete('/api/sessions/:id/attachments/images/:attachmentId', async (req, res) => {
305
250
  try {
306
- const removed = await sessionManager.discardCodexImageAttachment(req.params.id, req.params.attachmentId);
251
+ const removed = await sessionManager.discardImageAttachment(req.params.id, req.params.attachmentId);
307
252
  if (!removed) return res.status(404).json({ error: 'Image attachment not found' });
308
253
  res.json({ success: true });
309
254
  } catch (e) {
@@ -323,173 +268,13 @@ async function webCommand(options) {
323
268
  res.json({ success: true, diagnostics });
324
269
  });
325
270
 
326
- app.get('/api/sessions/:id/claude-resume-sessions', (req, res) => {
327
- const items = sessionManager.listClaudeResumeSessions(req.params.id);
328
- if (!items) return res.status(404).json({ error: 'Claude session not found' });
329
- res.json({ success: true, items });
330
- });
331
-
332
- app.patch('/api/sessions/:id/claude-settings', (req, res) => {
333
- const state = sessionManager.updateClaudeSettings(req.params.id, req.body || {});
334
- if (!state) return res.status(404).json({ error: 'Claude session not found' });
335
- res.json({ success: true, state });
336
- });
337
-
338
- app.post('/api/sessions/:id/claude-abort', (req, res) => {
339
- const success = sessionManager.abortClaude(req.params.id);
340
- if (!success) return res.status(404).json({ error: 'Claude session not found or idle' });
341
- res.json({ success: true });
342
- });
343
-
344
- app.post('/api/sessions/:id/claude-resume', (req, res) => {
345
- const resumeSessionId = req.body && req.body.resumeSessionId;
346
- if (!resumeSessionId) return res.status(400).json({ error: 'Missing resumeSessionId' });
347
- const success = sessionManager.resumeClaude(req.params.id, resumeSessionId);
348
- if (!success) return res.status(404).json({ error: 'Claude session not found' });
349
- res.json({ success: true });
350
- });
351
-
352
- app.patch('/api/sessions/:id/codex-settings', async (req, res) => {
353
- try {
354
- const state = await sessionManager.updateCodexSettings(req.params.id, req.body || {});
355
- if (!state) return res.status(404).json({ error: 'Codex session not found' });
356
- res.json({ success: true, state });
357
- } catch (e) { res.status(400).json({ error: e.message }); }
358
- });
359
-
360
- app.get('/api/sessions/:id/codex-resume-threads', async (req, res) => {
361
- try {
362
- const items = await sessionManager.listCodexResumeThreads(req.params.id);
363
- if (!items) return res.status(404).json({ error: 'Codex session not found' });
364
- res.json({ success: true, items });
365
- } catch (e) { res.status(400).json({ error: e.message }); }
366
- });
367
-
368
- app.post('/api/sessions/:id/codex-abort', (req, res) => {
369
- const success = sessionManager.abortCodex(req.params.id);
370
- if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
371
- res.json({ success: true });
372
- });
373
-
374
- app.post('/api/sessions/:id/codex-resume', async (req, res) => {
375
- try {
376
- const success = await sessionManager.resumeCodex(req.params.id, req.body && req.body.threadId);
377
- if (!success) return res.status(409).json({ error: 'Codex session is busy or no thread is available' });
378
- res.json({ success: true });
379
- } catch (e) { res.status(400).json({ error: e.message }); }
380
- });
381
-
382
- app.post('/api/sessions/:id/codex-fork', async (req, res) => {
383
- try {
384
- const session = await sessionManager.forkCodex(req.params.id, req.body && req.body.threadId);
385
- if (!session) return res.status(404).json({ error: 'Codex session not found' });
386
- res.json({ success: true, id: session.id, name: session.name, threadId: session.threadId });
387
- } catch (e) { res.status(e.statusCode || 400).json({ error: e.message }); }
388
- });
389
-
390
- app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
391
- const presentation = req.body && req.body.presentation;
392
- if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
393
- try {
394
- const success = await sessionManager.switchCodexPresentation(req.params.id, presentation);
395
- if (!success) return res.status(409).json({ error: 'Codex session cannot switch presentation now' });
396
- res.json({ success: true });
397
- } catch (e) { res.status(409).json({ error: e.message }); }
398
- });
399
-
400
- app.post('/api/debug/client-log', (req, res) => {
401
- const { sessionId, event, payload } = req.body || {};
402
- sessionManager.logClientDebug(sessionId, event, payload);
403
- res.json({ success: true });
404
- });
405
-
406
- // API: Git Show
407
- app.get('/api/sessions/:id/git-show/:hash', async (req, res) => {
408
- const session = sessionManager.get(req.params.id);
409
- if (!session) return res.status(404).json({ error: 'Session not found' });
410
- const hash = req.params.hash;
411
- const result = await gitService.show(getSessionWorkingDirectory(session), hash);
412
- res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
413
- });
414
-
415
- // API: Git Branch Name for Commit
416
- app.get('/api/sessions/:id/git-branch/:hash', async (req, res) => {
417
- const session = sessionManager.get(req.params.id);
418
- if (!session) return res.status(404).json({ error: 'Session not found' });
419
- const hash = req.params.hash;
420
- const result = await gitService.nameRev(getSessionWorkingDirectory(session), hash);
421
- res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
422
- });
423
-
424
- // API: Git Log
425
- app.get('/api/sessions/:id/git-log', async (req, res) => {
426
- const session = sessionManager.get(req.params.id);
427
- if (!session) return res.status(404).json({ error: 'Session not found' });
428
- const result = await gitService.log(getSessionWorkingDirectory(session), req.query.maxCount);
429
- if (!result.success) {
430
- return res.status(500).json({ error: result.error, stderr: result.stderr });
431
- }
432
- res.json({ success: true, commits: result.commits });
433
- });
434
-
435
- // API: Git Status
436
- app.get('/api/sessions/:id/git-status', async (req, res) => {
437
- const session = sessionManager.get(req.params.id);
438
- if (!session) return res.status(404).json({ error: 'Session not found' });
439
- const result = await gitService.status(getSessionWorkingDirectory(session));
440
- if (!result.success) {
441
- return res.status(500).json({ error: result.error, stderr: result.stderr });
442
- }
443
- res.json({ success: true, files: result.files });
444
- });
445
-
446
- // API: Git Diff Numstat (unstaged and staged)
447
- app.get('/api/sessions/:id/git-diff-numstat', async (req, res) => {
448
- const session = sessionManager.get(req.params.id);
449
- if (!session) return res.status(404).json({ error: 'Session not found' });
450
- const isStaged = req.query.staged === 'true';
451
- const result = await gitService.diffNumstat(getSessionWorkingDirectory(session), isStaged);
452
- res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
453
- });
454
-
455
- // API: Git Diff File
456
- app.get('/api/sessions/:id/git-diff-file', async (req, res) => {
457
- const session = sessionManager.get(req.params.id);
458
- if (!session) return res.status(404).json({ error: 'Session not found' });
459
- const isStaged = req.query.staged === 'true';
460
- const filePath = req.query.path;
461
- if (!filePath) return res.status(400).json({ error: 'Missing file path' });
462
- const result = await gitService.diffFile(getSessionWorkingDirectory(session), filePath, isStaged);
463
- res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
464
- });
465
-
466
- // API: Get File Content
467
- app.get('/api/sessions/:id/file', async (req, res) => {
468
- const session = sessionManager.get(req.params.id);
469
- if (!session) return res.status(404).json({ error: 'Session not found' });
470
- const filePath = req.query.path || '';
471
- if (!filePath) return res.status(400).json({ error: 'Missing file path' });
472
- const cwd = getSessionWorkingDirectory(session);
473
- try {
474
- const content = workspaceService.readFile(cwd, filePath);
475
- res.json({ success: true, content });
476
- } catch (e) {
477
- res.status(e.statusCode || 200).json({ success: false, error: e.message });
478
- }
479
- });
271
+ registerProviderRoutes(app, { sessionManager });
480
272
 
481
- // API: Get Directory Contents
482
- app.get('/api/sessions/:id/fs/dir', async (req, res) => {
483
- const session = sessionManager.get(req.params.id);
484
- if (!session) return res.status(404).json({ error: 'Session not found' });
485
- const dirPath = req.query.path || '';
486
- const cwd = getSessionWorkingDirectory(session);
487
- try {
488
- const files = await workspaceService.listDirectory(cwd, dirPath);
489
- res.json({ success: true, files });
490
- } catch (e) {
491
- res.status(e.statusCode || 200).json({ success: false, error: e.message });
492
- }
273
+ registerWorkspaceRoutes(app, {
274
+ sessionManager,
275
+ gitService,
276
+ workspaceService,
277
+ getWorkingDirectory: getSessionWorkingDirectory
493
278
  });
494
279
 
495
280
 
@@ -555,7 +340,8 @@ async function webCommand(options) {
555
340
  session.write(payload.data);
556
341
  }
557
342
  if (payload.type === 'claude-input') {
558
- sessionManager.sendClaudeInput(sessionId, payload.text || '');
343
+ sessionManager.sendClaudeInput(sessionId, payload.text || '', payload.attachmentIds || [])
344
+ .catch(error => logger.error(`Claude input error: ${error.message}`));
559
345
  }
560
346
  if (payload.type === 'claude-permission') {
561
347
  sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
@@ -563,6 +349,12 @@ async function webCommand(options) {
563
349
  if (payload.type === 'claude-settings') {
564
350
  sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
565
351
  }
352
+ if (payload.type === 'claude-usage') {
353
+ sessionManager.showClaudeUsage(sessionId).catch(error => logger.error(`Claude usage error: ${error.message}`));
354
+ }
355
+ if (payload.type === 'claude-context') {
356
+ sessionManager.showClaudeContext(sessionId).catch(error => logger.error(`Claude context error: ${error.message}`));
357
+ }
566
358
  if (payload.type === 'claude-abort') {
567
359
  sessionManager.abortClaude(sessionId);
568
360
  }
@@ -618,6 +410,9 @@ async function webCommand(options) {
618
410
  // Frontend routes
619
411
  const assetsDir = path.resolve(__dirname, '../../assets');
620
412
  const webDir = path.join(__dirname, '../web');
413
+ const xtermScript = require.resolve('@xterm/xterm');
414
+ const xtermStyles = path.resolve(path.dirname(xtermScript), '../css/xterm.css');
415
+ const fitAddonScript = require.resolve('@xterm/addon-fit');
621
416
 
622
417
  const sendLogo = (req, res) => {
623
418
  res.sendFile('logo.svg', { root: assetsDir }, error => {
@@ -635,11 +430,34 @@ async function webCommand(options) {
635
430
  }
636
431
  });
637
432
 
638
- app.get(['/gitgraph.js', /.*\/gitgraph\.js$/], (req, res) => {
639
- res.sendFile('gitgraph.js', { root: webDir }, error => {
433
+ const sendWebAsset = assetName => (req, res) => {
434
+ res.sendFile(assetName, { root: webDir }, error => {
640
435
  if (error && !res.headersSent) res.status(404).send('Not found');
641
436
  });
642
- });
437
+ };
438
+
439
+ const webAssets = [
440
+ 'gitgraph.js',
441
+ 'styles.css',
442
+ 'core.js',
443
+ 'claude.js',
444
+ 'schedules.js',
445
+ 'shell.js',
446
+ 'codex.js',
447
+ 'session.js',
448
+ 'composer.js',
449
+ 'timed-inputs.js',
450
+ 'terminal-scroll.js',
451
+ 'git.js'
452
+ ];
453
+ for (const assetName of webAssets) {
454
+ const escapedName = assetName.replace('.', '\\.');
455
+ app.get([`/${assetName}`, new RegExp(`.*\\/${escapedName}$`)], sendWebAsset(assetName));
456
+ }
457
+
458
+ app.get('/vendor/xterm.js', (req, res) => res.sendFile(xtermScript));
459
+ app.get('/vendor/xterm.css', (req, res) => res.sendFile(xtermStyles));
460
+ app.get('/vendor/xterm-addon-fit.js', (req, res) => res.sendFile(fitAddonScript));
643
461
 
644
462
  app.get(['/logo.svg', /.*\/logo\.svg$/], sendLogo);
645
463
 
@@ -0,0 +1,103 @@
1
+ function registerProviderRoutes(app, { sessionManager }) {
2
+ app.get('/api/sessions/:id/claude-resume-sessions', (req, res) => {
3
+ const items = sessionManager.listClaudeResumeSessions(req.params.id);
4
+ if (!items) return res.status(404).json({ error: 'Claude session not found' });
5
+ res.json({ success: true, items });
6
+ });
7
+
8
+ app.patch('/api/sessions/:id/claude-settings', (req, res) => {
9
+ const state = sessionManager.updateClaudeSettings(req.params.id, req.body || {});
10
+ if (!state) return res.status(404).json({ error: 'Claude session not found' });
11
+ res.json({ success: true, state });
12
+ });
13
+
14
+ app.post('/api/sessions/:id/claude-abort', (req, res) => {
15
+ const success = sessionManager.abortClaude(req.params.id);
16
+ if (!success) return res.status(404).json({ error: 'Claude session not found or idle' });
17
+ res.json({ success: true });
18
+ });
19
+
20
+ app.post('/api/sessions/:id/claude-resume', (req, res) => {
21
+ const resumeSessionId = req.body?.resumeSessionId;
22
+ if (!resumeSessionId) return res.status(400).json({ error: 'Missing resumeSessionId' });
23
+ const success = sessionManager.resumeClaude(req.params.id, resumeSessionId);
24
+ if (!success) return res.status(404).json({ error: 'Claude session not found' });
25
+ res.json({ success: true });
26
+ });
27
+
28
+ app.post('/api/sessions/:id/claude-fork', async (req, res) => {
29
+ try {
30
+ const result = await sessionManager.forkClaude(req.params.id, req.body?.claudeSessionId);
31
+ if (!result) return res.status(404).json({ error: 'Claude session not found' });
32
+ res.json({ success: true, ...result });
33
+ } catch (error) {
34
+ res.status(error.statusCode || 400).json({ error: error.message });
35
+ }
36
+ });
37
+
38
+ app.patch('/api/sessions/:id/codex-settings', async (req, res) => {
39
+ try {
40
+ const state = await sessionManager.updateCodexSettings(req.params.id, req.body || {});
41
+ if (!state) return res.status(404).json({ error: 'Codex session not found' });
42
+ res.json({ success: true, state });
43
+ } catch (error) {
44
+ res.status(400).json({ error: error.message });
45
+ }
46
+ });
47
+
48
+ app.get('/api/sessions/:id/codex-resume-threads', async (req, res) => {
49
+ try {
50
+ const items = await sessionManager.listCodexResumeThreads(req.params.id);
51
+ if (!items) return res.status(404).json({ error: 'Codex session not found' });
52
+ res.json({ success: true, items });
53
+ } catch (error) {
54
+ res.status(400).json({ error: error.message });
55
+ }
56
+ });
57
+
58
+ app.post('/api/sessions/:id/codex-abort', (req, res) => {
59
+ const success = sessionManager.abortCodex(req.params.id);
60
+ if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
61
+ res.json({ success: true });
62
+ });
63
+
64
+ app.post('/api/sessions/:id/codex-resume', async (req, res) => {
65
+ try {
66
+ const success = await sessionManager.resumeCodex(req.params.id, req.body?.threadId);
67
+ if (!success) return res.status(409).json({ error: 'Codex session is busy or no thread is available' });
68
+ res.json({ success: true });
69
+ } catch (error) {
70
+ res.status(400).json({ error: error.message });
71
+ }
72
+ });
73
+
74
+ app.post('/api/sessions/:id/codex-fork', async (req, res) => {
75
+ try {
76
+ const session = await sessionManager.forkCodex(req.params.id, req.body?.threadId);
77
+ if (!session) return res.status(404).json({ error: 'Codex session not found' });
78
+ res.json({ success: true, id: session.id, name: session.name, threadId: session.threadId });
79
+ } catch (error) {
80
+ res.status(error.statusCode || 400).json({ error: error.message });
81
+ }
82
+ });
83
+
84
+ app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
85
+ const presentation = req.body?.presentation;
86
+ if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
87
+ try {
88
+ const success = await sessionManager.switchCodexPresentation(req.params.id, presentation);
89
+ if (!success) return res.status(409).json({ error: 'Codex session cannot switch presentation now' });
90
+ res.json({ success: true });
91
+ } catch (error) {
92
+ res.status(409).json({ error: error.message });
93
+ }
94
+ });
95
+
96
+ app.post('/api/debug/client-log', (req, res) => {
97
+ const { sessionId, event, payload } = req.body || {};
98
+ sessionManager.logClientDebug(sessionId, event, payload);
99
+ res.json({ success: true });
100
+ });
101
+ }
102
+
103
+ module.exports = registerProviderRoutes;
@@ -0,0 +1,54 @@
1
+ function registerScheduleRoutes(app, { jobStore, jobRunner }) {
2
+ app.get('/api/schedules', (req, res) => {
3
+ res.json(jobStore.list());
4
+ });
5
+
6
+ app.post('/api/schedules', (req, res) => {
7
+ try {
8
+ res.json(jobStore.create(req.body || {}));
9
+ } catch (error) {
10
+ res.status(400).json({ error: error.message });
11
+ }
12
+ });
13
+
14
+ app.get('/api/schedules/:id', (req, res) => {
15
+ const job = jobStore.get(req.params.id);
16
+ if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
17
+ res.json(job);
18
+ });
19
+
20
+ app.patch('/api/schedules/:id', (req, res) => {
21
+ const job = jobStore.update(req.params.id, req.body || {});
22
+ if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
23
+ res.json(job);
24
+ });
25
+
26
+ app.patch('/api/schedules/:id/enabled', (req, res) => {
27
+ const job = jobStore.patchRuntime(req.params.id, { enabled: Boolean(req.body?.enabled) });
28
+ if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
29
+ res.json(job);
30
+ });
31
+
32
+ app.delete('/api/schedules/:id', (req, res) => {
33
+ res.json({ success: jobStore.delete(req.params.id) });
34
+ });
35
+
36
+ app.post('/api/schedules/:id/duplicate', (req, res) => {
37
+ const job = jobStore.duplicate(req.params.id);
38
+ if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
39
+ res.json(job);
40
+ });
41
+
42
+ const runJob = options => async (req, res) => {
43
+ try {
44
+ res.json(await jobRunner.run(req.params.id, options));
45
+ } catch (error) {
46
+ res.status(400).json({ error: error.message });
47
+ }
48
+ };
49
+
50
+ app.post('/api/schedules/:id/run', runJob({ manual: false }));
51
+ app.post('/api/schedules/:id/simulate', runJob({ manual: true, background: true }));
52
+ }
53
+
54
+ module.exports = registerScheduleRoutes;
@@ -0,0 +1,77 @@
1
+ function registerWorkspaceRoutes(app, { sessionManager, gitService, workspaceService, getWorkingDirectory }) {
2
+ function getSession(req, res) {
3
+ const session = sessionManager.get(req.params.id);
4
+ if (!session) res.status(404).json({ error: 'Session not found' });
5
+ return session;
6
+ }
7
+
8
+ app.get('/api/sessions/:id/git-show/:hash', async (req, res) => {
9
+ const session = getSession(req, res);
10
+ if (!session) return;
11
+ const result = await gitService.show(getWorkingDirectory(session), req.params.hash);
12
+ res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
13
+ });
14
+
15
+ app.get('/api/sessions/:id/git-branch/:hash', async (req, res) => {
16
+ const session = getSession(req, res);
17
+ if (!session) return;
18
+ const result = await gitService.nameRev(getWorkingDirectory(session), req.params.hash);
19
+ res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
20
+ });
21
+
22
+ app.get('/api/sessions/:id/git-log', async (req, res) => {
23
+ const session = getSession(req, res);
24
+ if (!session) return;
25
+ const result = await gitService.log(getWorkingDirectory(session), req.query.maxCount);
26
+ if (!result.success) return res.status(500).json({ error: result.error, stderr: result.stderr });
27
+ res.json({ success: true, commits: result.commits });
28
+ });
29
+
30
+ app.get('/api/sessions/:id/git-status', async (req, res) => {
31
+ const session = getSession(req, res);
32
+ if (!session) return;
33
+ const result = await gitService.status(getWorkingDirectory(session));
34
+ if (!result.success) return res.status(500).json({ error: result.error, stderr: result.stderr });
35
+ res.json({ success: true, files: result.files });
36
+ });
37
+
38
+ app.get('/api/sessions/:id/git-diff-numstat', async (req, res) => {
39
+ const session = getSession(req, res);
40
+ if (!session) return;
41
+ const result = await gitService.diffNumstat(getWorkingDirectory(session), req.query.staged === 'true');
42
+ res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
43
+ });
44
+
45
+ app.get('/api/sessions/:id/git-diff-file', async (req, res) => {
46
+ const session = getSession(req, res);
47
+ if (!session) return;
48
+ if (!req.query.path) return res.status(400).json({ error: 'Missing file path' });
49
+ const result = await gitService.diffFile(getWorkingDirectory(session), req.query.path, req.query.staged === 'true');
50
+ res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
51
+ });
52
+
53
+ app.get('/api/sessions/:id/file', (req, res) => {
54
+ const session = getSession(req, res);
55
+ if (!session) return;
56
+ if (!req.query.path) return res.status(400).json({ error: 'Missing file path' });
57
+ try {
58
+ const content = workspaceService.readFile(getWorkingDirectory(session), req.query.path);
59
+ res.json({ success: true, content });
60
+ } catch (error) {
61
+ res.status(error.statusCode || 200).json({ success: false, error: error.message });
62
+ }
63
+ });
64
+
65
+ app.get('/api/sessions/:id/fs/dir', async (req, res) => {
66
+ const session = getSession(req, res);
67
+ if (!session) return;
68
+ try {
69
+ const files = await workspaceService.listDirectory(getWorkingDirectory(session), req.query.path || '');
70
+ res.json({ success: true, files });
71
+ } catch (error) {
72
+ res.status(error.statusCode || 200).json({ success: false, error: error.message });
73
+ }
74
+ });
75
+ }
76
+
77
+ module.exports = registerWorkspaceRoutes;