• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 21152598554

19 Jan 2026 10:00PM UTC coverage: 83.846% (+0.9%) from 82.966%
21152598554

push

github

apowers313
Merge branch 'master' of https://github.com/apowers313/aiforge

1604 of 1876 branches covered (85.5%)

Branch coverage included in aggregate %.

8242 of 9867 relevant lines covered (83.53%)

20.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.06
/src/server/api/routes/shells.ts
1
/**
2
 * Shells API routes
3
 */
4
import { Router } from 'express';
1✔
5
import type { Request, Response, NextFunction, RequestHandler } from 'express';
6
import { z } from 'zod';
1✔
7
import { validateBody, validateParams } from '../middleware/validation.js';
1✔
8
import { requireAuth } from '../middleware/auth.js';
1✔
9
import { ApiError } from '../middleware/error.js';
1✔
10
import type { ShellService } from '../../services/shell/ShellService.js';
11
import type { ShellContextService } from '../../services/shell/ShellContextService.js';
12

13
// Extend Express Request to include shell service and shell context service
14
declare module 'express-serve-static-core' {
15
  interface Request {
16
    shellService?: ShellService;
17
    shellContextService?: ShellContextService;
18
  }
19
}
20

21
const router = Router();
1✔
22

23
// All routes require authentication
24
router.use(requireAuth);
1✔
25

26
// Request schemas
27
const CreateShellSchema = z.object({
1✔
28
  name: z.string().min(1).max(255).optional(),
1✔
29
  type: z.enum(['bash', 'ai']).optional().default('bash'),
1✔
30
});
1✔
31

32
const UpdateShellSchema = z.object({
1✔
33
  name: z.string().min(1).max(255).optional(),
1✔
34
  done: z.boolean().optional(),
1✔
35
});
1✔
36

37
const ProjectIdParamsSchema = z.object({
1✔
38
  projectId: z.string().uuid('Invalid project ID'),
1✔
39
});
1✔
40

41
const ShellIdParamsSchema = z.object({
1✔
42
  id: z.string().uuid('Invalid shell ID'),
1✔
43
});
1✔
44

45
const TodoIdParamsSchema = z.object({
1✔
46
  id: z.string().uuid('Invalid shell ID'),
1✔
47
  todoId: z.string().uuid('Invalid todo ID'),
1✔
48
});
1✔
49

50
const CreateTodoSchema = z.object({
1✔
51
  text: z.string().min(1, 'TODO text is required').max(500),
1✔
52
});
1✔
53

54
const UpdateTodoSchema = z.object({
1✔
55
  text: z.string().min(1).max(500).optional(),
1✔
56
  completed: z.boolean().optional(),
1✔
57
});
1✔
58

59
const UpdateNotesSchema = z.object({
1✔
60
  notes: z.string().max(50000), // Allow empty notes
1✔
61
});
1✔
62

63
const ReorderTodosSchema = z.object({
1✔
64
  todoIds: z.array(z.string()),
1✔
65
});
1✔
66

67
type CreateShellBody = z.infer<typeof CreateShellSchema>;
68
type UpdateShellBody = z.infer<typeof UpdateShellSchema>;
69
type ProjectIdParams = z.infer<typeof ProjectIdParamsSchema>;
70
type ShellIdParams = z.infer<typeof ShellIdParamsSchema>;
71
type TodoIdParams = z.infer<typeof TodoIdParamsSchema>;
72
type CreateTodoBody = z.infer<typeof CreateTodoSchema>;
73
type UpdateTodoBody = z.infer<typeof UpdateTodoSchema>;
74
type UpdateNotesBody = z.infer<typeof UpdateNotesSchema>;
75
type ReorderTodosBody = z.infer<typeof ReorderTodosSchema>;
76

77
/**
78
 * GET /api/projects/:projectId/shells
79
 * List all shells for a project
80
 */
81
router.get('/projects/:projectId/shells', validateParams(ProjectIdParamsSchema), async (req, res, next) => {
1✔
82
  try {
1✔
83
    if (!req.shellService) {
1!
84
      throw ApiError.internal('Shell service not configured');
×
85
    }
×
86

87
    const { projectId } = req.params as ProjectIdParams;
1✔
88
    const shells = await req.shellService.getByProjectId(projectId);
1✔
89
    res.json({ shells });
1✔
90
  } catch (err) {
1!
91
    next(err);
×
92
  }
×
93
});
1✔
94

95
/**
96
 * POST /api/projects/:projectId/shells
97
 * Create a new shell for a project
98
 */
99
router.post('/projects/:projectId/shells', validateParams(ProjectIdParamsSchema), validateBody(CreateShellSchema), async (req, res, next) => {
1✔
100
  try {
30✔
101
    if (!req.shellService) {
30!
102
      throw ApiError.internal('Shell service not configured');
×
103
    }
×
104

105
    const { projectId } = req.params as ProjectIdParams;
30✔
106
    const { name, type } = req.body as CreateShellBody;
30✔
107
    // Create shell in inactive state - client opens via WebSocket session.open
108
    const shell = await req.shellService.create(projectId, name, type);
30✔
109
    res.status(201).json({ shell });
29✔
110
  } catch (err) {
30✔
111
    if (err instanceof Error && err.message === 'Project not found') {
1✔
112
      next(ApiError.notFound(err.message));
1✔
113
      return;
1✔
114
    }
1!
115
    next(err);
×
116
  }
×
117
});
30✔
118

119
/**
120
 * GET /api/shells/:id
121
 * Get a shell by ID
122
 */
123
router.get('/shells/:id', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
124
  try {
×
125
    if (!req.shellService) {
×
126
      throw ApiError.internal('Shell service not configured');
×
127
    }
×
128

129
    const { id } = req.params as ShellIdParams;
×
130
    const shell = await req.shellService.getById(id);
×
131
    if (!shell) {
×
132
      throw ApiError.notFound('Shell not found');
×
133
    }
×
134

135
    res.json({ shell });
×
136
  } catch (err) {
×
137
    next(err);
×
138
  }
×
139
});
×
140

141
/**
142
 * DELETE /api/shells/:id
143
 * Delete a shell
144
 */
145
router.delete('/shells/:id', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
146
  try {
2✔
147
    if (!req.shellService) {
2!
148
      throw ApiError.internal('Shell service not configured');
×
149
    }
×
150

151
    const { id } = req.params as ShellIdParams;
2✔
152
    const deleted = await req.shellService.delete(id);
2✔
153
    if (!deleted) {
2✔
154
      throw ApiError.notFound('Shell not found');
1✔
155
    }
1✔
156

157
    res.status(204).send();
1✔
158
  } catch (err) {
1✔
159
    next(err);
1✔
160
  }
1✔
161
});
2✔
162

163
/**
164
 * PATCH /api/shells/:id
165
 * Update a shell (rename)
166
 */
167
router.patch('/shells/:id', validateParams(ShellIdParamsSchema), validateBody(UpdateShellSchema), async (req, res, next) => {
1✔
168
  try {
2✔
169
    if (!req.shellService) {
2!
170
      throw ApiError.internal('Shell service not configured');
×
171
    }
×
172

173
    const { id } = req.params as ShellIdParams;
2✔
174
    const { name, done } = req.body as UpdateShellBody;
2✔
175

176
    // Build updates object with only defined values
177
    const updates: { name?: string; done?: boolean } = {};
2✔
178
    if (name !== undefined) {
2✔
179
      updates.name = name;
2✔
180
    }
2✔
181
    if (done !== undefined) {
2!
182
      updates.done = done;
×
183
    }
×
184

185
    const shell = await req.shellService.update(id, updates);
2✔
186
    if (!shell) {
2✔
187
      throw ApiError.notFound('Shell not found');
1✔
188
    }
1✔
189

190
    res.json({ shell });
1✔
191
  } catch (err) {
1✔
192
    next(err);
1✔
193
  }
1✔
194
});
2✔
195

196
/**
197
 * POST /api/shells/:id/stop
198
 * Stop a shell (kill PTY process)
199
 */
200
router.post('/shells/:id/stop', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
201
  try {
×
202
    if (!req.shellService) {
×
203
      throw ApiError.internal('Shell service not configured');
×
204
    }
×
205

206
    const { id } = req.params as ShellIdParams;
×
207
    const shell = await req.shellService.stop(id);
×
208
    if (!shell) {
×
209
      throw ApiError.notFound('Shell not found');
×
210
    }
×
211
    res.json({ shell });
×
212
  } catch (err) {
×
213
    if (err instanceof Error && err.message === 'PTY pool not configured') {
×
214
      next(ApiError.internal(err.message));
×
215
      return;
×
216
    }
×
217
    next(err);
×
218
  }
×
219
});
×
220

221
/**
222
 * POST /api/shells/:id/restart
223
 * Restart a shell (stops PTY process - client re-opens via WebSocket session.open)
224
 */
225
router.post('/shells/:id/restart', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
226
  try {
1✔
227
    if (!req.shellService) {
1!
228
      throw ApiError.internal('Shell service not configured');
×
229
    }
×
230

231
    const { id } = req.params as ShellIdParams;
1✔
232

233
    // Stop the shell (client re-opens via WebSocket session.open message)
234
    const shell = await req.shellService.stop(id);
1!
235
    if (!shell) {
×
236
      throw ApiError.notFound('Shell not found');
×
237
    }
×
238
    res.json({ shell });
×
239
  } catch (err) {
1✔
240
    if (err instanceof Error && err.message === 'PTY pool not configured') {
1✔
241
      next(ApiError.internal(err.message));
1✔
242
      return;
1✔
243
    }
1!
244
    next(err);
×
245
  }
×
246
});
1✔
247

248
// =============================================================================
249
// Shell Context Routes (TODOs and Notes)
250
// =============================================================================
251

252
/**
253
 * GET /api/shells/:id/context
254
 * Get shell context (todos and notes)
255
 */
256
router.get('/shells/:id/context', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
257
  try {
5✔
258
    if (!req.shellService) {
5!
259
      throw ApiError.internal('Shell service not configured');
×
260
    }
×
261
    if (!req.shellContextService) {
5!
262
      throw ApiError.internal('Shell context service not configured');
×
263
    }
×
264

265
    const { id } = req.params as ShellIdParams;
5✔
266

267
    // Verify shell exists
268
    const shell = await req.shellService.getById(id);
5✔
269
    if (!shell) {
5✔
270
      throw ApiError.notFound('Shell not found');
1✔
271
    }
1✔
272

273
    const context = await req.shellContextService.getContext(id);
4✔
274
    res.json(context);
4✔
275
  } catch (err) {
5✔
276
    next(err);
1✔
277
  }
1✔
278
});
5✔
279

280
/**
281
 * POST /api/shells/:id/todos
282
 * Add a TODO to a shell
283
 */
284
router.post('/shells/:id/todos', validateParams(ShellIdParamsSchema), validateBody(CreateTodoSchema), async (req, res, next) => {
1✔
285
  try {
15✔
286
    if (!req.shellService) {
15!
287
      throw ApiError.internal('Shell service not configured');
×
288
    }
×
289
    if (!req.shellContextService) {
15!
290
      throw ApiError.internal('Shell context service not configured');
×
291
    }
×
292

293
    const { id } = req.params as ShellIdParams;
15✔
294
    const { text } = req.body as CreateTodoBody;
15✔
295

296
    // Verify shell exists
297
    const shell = await req.shellService.getById(id);
15✔
298
    if (!shell) {
15✔
299
      throw ApiError.notFound('Shell not found');
1✔
300
    }
1✔
301

302
    const todo = await req.shellContextService.addTodo(id, text);
14✔
303
    res.status(201).json(todo);
14✔
304
  } catch (err) {
15✔
305
    next(err);
1✔
306
  }
1✔
307
});
15✔
308

309
/**
310
 * POST /api/shells/:id/todos/clear-completed
311
 * Clear all completed TODOs
312
 * NOTE: Must be before :todoId routes to avoid matching
313
 */
314
router.post('/shells/:id/todos/clear-completed', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
315
  try {
3✔
316
    if (!req.shellService) {
3!
317
      throw ApiError.internal('Shell service not configured');
×
318
    }
×
319
    if (!req.shellContextService) {
3!
320
      throw ApiError.internal('Shell context service not configured');
×
321
    }
×
322

323
    const { id } = req.params as ShellIdParams;
3✔
324

325
    // Verify shell exists
326
    const shell = await req.shellService.getById(id);
3✔
327
    if (!shell) {
3✔
328
      throw ApiError.notFound('Shell not found');
1✔
329
    }
1✔
330

331
    const cleared = await req.shellContextService.clearCompleted(id);
2✔
332
    res.json({ cleared });
2✔
333
  } catch (err) {
3✔
334
    next(err);
1✔
335
  }
1✔
336
});
3✔
337

338
/**
339
 * PUT /api/shells/:id/todos/reorder
340
 * Reorder TODOs
341
 * NOTE: Must be before :todoId routes to avoid matching
342
 */
343
router.put('/shells/:id/todos/reorder', validateParams(ShellIdParamsSchema), validateBody(ReorderTodosSchema), async (req, res, next) => {
1✔
344
  try {
2✔
345
    if (!req.shellService) {
2!
346
      throw ApiError.internal('Shell service not configured');
×
347
    }
×
348
    if (!req.shellContextService) {
2!
349
      throw ApiError.internal('Shell context service not configured');
×
350
    }
×
351

352
    const { id } = req.params as ShellIdParams;
2✔
353
    const { todoIds } = req.body as ReorderTodosBody;
2✔
354

355
    // Verify shell exists
356
    const shell = await req.shellService.getById(id);
2✔
357
    if (!shell) {
2✔
358
      throw ApiError.notFound('Shell not found');
1✔
359
    }
1✔
360

361
    await req.shellContextService.reorderTodos(id, todoIds);
1✔
362
    const context = await req.shellContextService.getContext(id);
1✔
363
    res.json(context);
1✔
364
  } catch (err) {
1✔
365
    next(err);
1✔
366
  }
1✔
367
});
2✔
368

369
/**
370
 * PUT /api/shells/:id/todos/:todoId
371
 * Update a TODO
372
 */
373
router.put('/shells/:id/todos/:todoId', validateParams(TodoIdParamsSchema), validateBody(UpdateTodoSchema), async (req, res, next) => {
1✔
374
  try {
5✔
375
    if (!req.shellService) {
5!
376
      throw ApiError.internal('Shell service not configured');
×
377
    }
×
378
    if (!req.shellContextService) {
5!
379
      throw ApiError.internal('Shell context service not configured');
×
380
    }
×
381

382
    const { id, todoId } = req.params as TodoIdParams;
5✔
383
    const body = req.body as UpdateTodoBody;
5✔
384

385
    // Verify shell exists
386
    const shell = await req.shellService.getById(id);
5✔
387
    if (!shell) {
5!
388
      throw ApiError.notFound('Shell not found');
×
389
    }
×
390

391
    // Build updates object with only defined values (filter out undefined)
392
    const updates: { text?: string; completed?: boolean } = {};
5✔
393
    if (body.text !== undefined) {
5✔
394
      updates.text = body.text;
2✔
395
    }
2✔
396
    if (body.completed !== undefined) {
5✔
397
      updates.completed = body.completed;
3✔
398
    }
3✔
399

400
    const todo = await req.shellContextService.updateTodo(id, todoId, updates);
5✔
401
    if (!todo) {
5✔
402
      throw ApiError.notFound('TODO not found');
1✔
403
    }
1✔
404

405
    res.json(todo);
4✔
406
  } catch (err) {
5✔
407
    next(err);
1✔
408
  }
1✔
409
});
5✔
410

411
/**
412
 * DELETE /api/shells/:id/todos/:todoId
413
 * Delete a TODO
414
 */
415
router.delete('/shells/:id/todos/:todoId', validateParams(TodoIdParamsSchema), async (req, res, next) => {
1✔
416
  try {
2✔
417
    if (!req.shellService) {
2!
418
      throw ApiError.internal('Shell service not configured');
×
419
    }
×
420
    if (!req.shellContextService) {
2!
421
      throw ApiError.internal('Shell context service not configured');
×
422
    }
×
423

424
    const { id, todoId } = req.params as TodoIdParams;
2✔
425

426
    // Verify shell exists
427
    const shell = await req.shellService.getById(id);
2✔
428
    if (!shell) {
2!
429
      throw ApiError.notFound('Shell not found');
×
430
    }
×
431

432
    const deleted = await req.shellContextService.deleteTodo(id, todoId);
2✔
433
    if (!deleted) {
2✔
434
      throw ApiError.notFound('TODO not found');
1✔
435
    }
1✔
436

437
    res.status(204).send();
1✔
438
  } catch (err) {
1✔
439
    next(err);
1✔
440
  }
1✔
441
});
2✔
442

443
/**
444
 * PATCH /api/shells/:id/notes
445
 * Update shell notes
446
 */
447
router.patch('/shells/:id/notes', validateParams(ShellIdParamsSchema), validateBody(UpdateNotesSchema), async (req, res, next) => {
1✔
448
  try {
3✔
449
    if (!req.shellService) {
3!
450
      throw ApiError.internal('Shell service not configured');
×
451
    }
×
452
    if (!req.shellContextService) {
3!
453
      throw ApiError.internal('Shell context service not configured');
×
454
    }
×
455

456
    const { id } = req.params as ShellIdParams;
3✔
457
    const { notes } = req.body as UpdateNotesBody;
3✔
458

459
    // Verify shell exists
460
    const shell = await req.shellService.getById(id);
3✔
461
    if (!shell) {
3✔
462
      throw ApiError.notFound('Shell not found');
1✔
463
    }
1✔
464

465
    await req.shellContextService.updateNotes(id, notes);
2✔
466
    const context = await req.shellContextService.getContext(id);
2✔
467
    res.json(context);
2✔
468
  } catch (err) {
3✔
469
    next(err);
1✔
470
  }
1✔
471
});
3✔
472

473
/**
474
 * Middleware to attach shell service
475
 */
476
export function attachShellService(shellService: ShellService): RequestHandler {
1✔
477
  return (req: Request, _res: Response, next: NextFunction): void => {
12✔
478
    req.shellService = shellService;
285✔
479
    next();
285✔
480
  };
285✔
481
}
12✔
482

483
/**
484
 * Middleware to attach shell context service
485
 */
486
export function attachShellContextService(shellContextService: ShellContextService): RequestHandler {
1✔
487
  return (req: Request, _res: Response, next: NextFunction): void => {
12✔
488
    req.shellContextService = shellContextService;
285✔
489
    next();
285✔
490
  };
285✔
491
}
12✔
492

493
export { router as shellsRouter };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc