94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { getSessionInfo } from '../db.js';
|
|
import {
|
|
sanitizeId,
|
|
tmuxSessionName,
|
|
ensureSession,
|
|
killSession,
|
|
hasSession,
|
|
} from '../pty/manager.js';
|
|
|
|
const ParamsSchema = z.object({ sid: z.string(), pid: z.string() });
|
|
// v1.10.8c: optional cols/rows on /start so the per-pane tmux session is
|
|
// born at the right dimensions. Bodyless POSTs remain valid (Fastify's
|
|
// tolerant parser).
|
|
const StartBodySchema = z
|
|
.object({
|
|
cols: z.coerce.number().int().min(1).max(2000).optional(),
|
|
rows: z.coerce.number().int().min(1).max(2000).optional(),
|
|
})
|
|
.partial()
|
|
.optional();
|
|
|
|
export function registerTerminalRoutes(app: FastifyInstance, tmuxConfPath: string): void {
|
|
// v1.10.8c: /start creates the per-pane tmux session. Idempotent — a second
|
|
// /start on the same paneId is a no-op (hasSession returns true). The WS
|
|
// attach handler also calls ensureSession as belt-and-suspenders, so /start
|
|
// is technically optional, but having it as a separate step surfaces tmux
|
|
// errors as HTTP responses (vs WS 1011 close codes).
|
|
app.post<{
|
|
Params: { sid: string; pid: string };
|
|
Body: { cols?: number; rows?: number } | undefined;
|
|
}>(
|
|
'/api/term/sessions/:sid/panes/:pid/start',
|
|
async (req, reply) => {
|
|
const p = ParamsSchema.safeParse(req.params);
|
|
if (!p.success) return reply.code(400).send({ error: 'bad_params' });
|
|
const sid = sanitizeId(p.data.sid);
|
|
const pid = sanitizeId(p.data.pid);
|
|
if (!sid || !pid) return reply.code(400).send({ error: 'bad_id_format' });
|
|
|
|
const b = StartBodySchema.safeParse(req.body ?? {});
|
|
const cols = b.success ? b.data?.cols : undefined;
|
|
const rows = b.success ? b.data?.rows : undefined;
|
|
|
|
const session = await getSessionInfo(sid);
|
|
if (!session) return reply.code(404).send({ error: 'unknown_session' });
|
|
|
|
const sessionName = tmuxSessionName(pid);
|
|
|
|
try {
|
|
await ensureSession(
|
|
tmuxConfPath,
|
|
sessionName,
|
|
session.project_path,
|
|
req.log,
|
|
cols,
|
|
rows,
|
|
);
|
|
} catch (err) {
|
|
req.log.error({ err }, 'ensureSession failed');
|
|
return reply.code(500).send({ error: 'tmux_failed' });
|
|
}
|
|
return reply.code(200).send({ tmux_session: sessionName });
|
|
},
|
|
);
|
|
|
|
// v1.10.8c: explicit pane teardown. Frontend calls this when the user
|
|
// intentionally closes a terminal pane (vs an implicit WS disconnect, which
|
|
// leaves the tmux session intact for refresh-driven resume).
|
|
app.post<{ Params: { sid: string; pid: string } }>(
|
|
'/api/term/sessions/:sid/panes/:pid/kill',
|
|
async (req, reply) => {
|
|
const p = ParamsSchema.safeParse(req.params);
|
|
if (!p.success) return reply.code(400).send({ error: 'bad_params' });
|
|
const sid = sanitizeId(p.data.sid);
|
|
const pid = sanitizeId(p.data.pid);
|
|
if (!sid || !pid) return reply.code(400).send({ error: 'bad_id_format' });
|
|
|
|
const sessionName = tmuxSessionName(pid);
|
|
if (!(await hasSession(tmuxConfPath, sessionName))) {
|
|
return reply.code(404).send({ error: 'unknown_pane' });
|
|
}
|
|
const killed = await killSession(tmuxConfPath, sessionName);
|
|
if (!killed) return reply.code(500).send({ error: 'tmux_kill_failed' });
|
|
return reply.code(200).send({ ok: true });
|
|
},
|
|
);
|
|
|
|
// Resize endpoint removed in v1.10.8c. Resize now flows in-band via the
|
|
// WebSocket as a `{type:"resize",cols,rows}` text frame — no more race
|
|
// between active-PTY-map registration and HTTP POST lookup. See ws/attach.ts.
|
|
}
|