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

apowers313 / aiforge / 21002763057

14 Jan 2026 05:00PM UTC coverage: 82.93% (-1.8%) from 84.765%
21002763057

push

github

apowers313
chore: delint

993 of 1165 branches covered (85.24%)

Branch coverage included in aggregate %.

5206 of 6310 relevant lines covered (82.5%)

15.95 hits per line

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

56.52
/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

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

19
const router = Router();
1✔
20

21
// All routes require authentication
22
router.use(requireAuth);
1✔
23

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

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

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

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

43
type CreateShellBody = z.infer<typeof CreateShellSchema>;
44
type UpdateShellBody = z.infer<typeof UpdateShellSchema>;
45
type ProjectIdParams = z.infer<typeof ProjectIdParamsSchema>;
46
type ShellIdParams = z.infer<typeof ShellIdParamsSchema>;
47

48
/**
49
 * GET /api/projects/:projectId/shells
50
 * List all shells for a project
51
 */
52
router.get('/projects/:projectId/shells', validateParams(ProjectIdParamsSchema), async (req, res, next) => {
1✔
53
  try {
1✔
54
    if (!req.shellService) {
1!
55
      throw ApiError.internal('Shell service not configured');
×
56
    }
×
57

58
    const { projectId } = req.params as ProjectIdParams;
1✔
59
    const shells = await req.shellService.getByProjectId(projectId);
1✔
60
    res.json({ shells });
1✔
61
  } catch (err) {
1!
62
    next(err);
×
63
  }
×
64
});
1✔
65

66
/**
67
 * POST /api/projects/:projectId/shells
68
 * Create a new shell for a project
69
 */
70
router.post('/projects/:projectId/shells', validateParams(ProjectIdParamsSchema), validateBody(CreateShellSchema), async (req, res, next) => {
1✔
71
  try {
7✔
72
    if (!req.shellService) {
7!
73
      throw ApiError.internal('Shell service not configured');
×
74
    }
×
75

76
    const { projectId } = req.params as ProjectIdParams;
7✔
77
    const { name, type } = req.body as CreateShellBody;
7✔
78
    let shell = await req.shellService.create(projectId, name, type);
7✔
79

80
    // Auto-start the shell after creation (if PTY pool is configured)
81
    try {
6✔
82
      shell = await req.shellService.start(shell.id);
6!
83
    } catch (startErr) {
7✔
84
      // If PTY pool is not configured, just return the created shell without starting
85
      // The shell will have status 'inactive' until explicitly started
86
      if (!(startErr instanceof Error && startErr.message === 'PTY pool not configured')) {
6!
87
        throw startErr;
×
88
      }
×
89
    }
6✔
90
    res.status(201).json({ shell });
6✔
91
  } catch (err) {
7✔
92
    if (err instanceof Error && err.message === 'Project not found') {
1✔
93
      next(ApiError.notFound(err.message));
1✔
94
      return;
1✔
95
    }
1!
96
    next(err);
×
97
  }
×
98
});
7✔
99

100
/**
101
 * GET /api/shells/:id
102
 * Get a shell by ID
103
 */
104
router.get('/shells/:id', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
105
  try {
×
106
    if (!req.shellService) {
×
107
      throw ApiError.internal('Shell service not configured');
×
108
    }
×
109

110
    const { id } = req.params as ShellIdParams;
×
111
    const shell = await req.shellService.getById(id);
×
112
    if (!shell) {
×
113
      throw ApiError.notFound('Shell not found');
×
114
    }
×
115

116
    res.json({ shell });
×
117
  } catch (err) {
×
118
    next(err);
×
119
  }
×
120
});
×
121

122
/**
123
 * DELETE /api/shells/:id
124
 * Delete a shell
125
 */
126
router.delete('/shells/:id', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
127
  try {
2✔
128
    if (!req.shellService) {
2!
129
      throw ApiError.internal('Shell service not configured');
×
130
    }
×
131

132
    const { id } = req.params as ShellIdParams;
2✔
133
    const deleted = await req.shellService.delete(id);
2✔
134
    if (!deleted) {
2✔
135
      throw ApiError.notFound('Shell not found');
1✔
136
    }
1✔
137

138
    res.status(204).send();
1✔
139
  } catch (err) {
1✔
140
    next(err);
1✔
141
  }
1✔
142
});
2✔
143

144
/**
145
 * PATCH /api/shells/:id
146
 * Update a shell (rename)
147
 */
148
router.patch('/shells/:id', validateParams(ShellIdParamsSchema), validateBody(UpdateShellSchema), async (req, res, next) => {
1✔
149
  try {
2✔
150
    if (!req.shellService) {
2!
151
      throw ApiError.internal('Shell service not configured');
×
152
    }
×
153

154
    const { id } = req.params as ShellIdParams;
2✔
155
    const { name, done } = req.body as UpdateShellBody;
2✔
156

157
    // Build updates object with only defined values
158
    const updates: { name?: string; done?: boolean } = {};
2✔
159
    if (name !== undefined) {
2✔
160
      updates.name = name;
2✔
161
    }
2✔
162
    if (done !== undefined) {
2!
163
      updates.done = done;
×
164
    }
×
165

166
    const shell = await req.shellService.update(id, updates);
2✔
167
    if (!shell) {
2✔
168
      throw ApiError.notFound('Shell not found');
1✔
169
    }
1✔
170

171
    res.json({ shell });
1✔
172
  } catch (err) {
1✔
173
    next(err);
1✔
174
  }
1✔
175
});
2✔
176

177
/**
178
 * POST /api/shells/:id/start
179
 * Start a shell (spawn PTY process)
180
 */
181
router.post('/shells/:id/start', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
182
  try {
×
183
    if (!req.shellService) {
×
184
      throw ApiError.internal('Shell service not configured');
×
185
    }
×
186

187
    const { id } = req.params as ShellIdParams;
×
188
    const shell = await req.shellService.start(id);
×
189
    res.json({ shell });
×
190
  } catch (err) {
×
191
    if (err instanceof Error) {
×
192
      if (err.message === 'Shell not found') {
×
193
        next(ApiError.notFound(err.message));
×
194
        return;
×
195
      }
×
196
      if (err.message === 'PTY pool not configured') {
×
197
        next(ApiError.internal(err.message));
×
198
        return;
×
199
      }
×
200
    }
×
201
    next(err);
×
202
  }
×
203
});
×
204

205
/**
206
 * POST /api/shells/:id/stop
207
 * Stop a shell (kill PTY process)
208
 */
209
router.post('/shells/:id/stop', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
210
  try {
×
211
    if (!req.shellService) {
×
212
      throw ApiError.internal('Shell service not configured');
×
213
    }
×
214

215
    const { id } = req.params as ShellIdParams;
×
216
    const shell = await req.shellService.stop(id);
×
217
    if (!shell) {
×
218
      throw ApiError.notFound('Shell not found');
×
219
    }
×
220
    res.json({ shell });
×
221
  } catch (err) {
×
222
    if (err instanceof Error && err.message === 'PTY pool not configured') {
×
223
      next(ApiError.internal(err.message));
×
224
      return;
×
225
    }
×
226
    next(err);
×
227
  }
×
228
});
×
229

230
/**
231
 * POST /api/shells/:id/restart
232
 * Restart a shell (stop then start PTY process)
233
 */
234
router.post('/shells/:id/restart', validateParams(ShellIdParamsSchema), async (req, res, next) => {
1✔
235
  try {
1✔
236
    if (!req.shellService) {
1!
237
      throw ApiError.internal('Shell service not configured');
×
238
    }
×
239

240
    const { id } = req.params as ShellIdParams;
1✔
241

242
    // Stop the shell first (ignore if already stopped)
243
    await req.shellService.stop(id).catch(() => {
1✔
244
      // Shell might already be stopped, that's OK
245
    });
1✔
246

247
    // Start it again
248
    const shell = await req.shellService.start(id);
1!
249
    res.json({ shell });
×
250
  } catch (err) {
1✔
251
    if (err instanceof Error) {
1✔
252
      if (err.message === 'Shell not found') {
1!
253
        next(ApiError.notFound(err.message));
×
254
        return;
×
255
      }
×
256
      if (err.message === 'PTY pool not configured') {
1✔
257
        next(ApiError.internal(err.message));
1✔
258
        return;
1✔
259
      }
1✔
260
    }
1!
261
    next(err);
×
262
  }
×
263
});
1✔
264

265
/**
266
 * Middleware to attach shell service
267
 */
268
export function attachShellService(shellService: ShellService): RequestHandler {
1✔
269
  return (req: Request, _res: Response, next: NextFunction): void => {
8✔
270
    req.shellService = shellService;
116✔
271
    next();
116✔
272
  };
116✔
273
}
8✔
274

275
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