v1.10: booterm container — xterm.js + tmux + node-pty
This commit is contained in:
88
apps/booterm/src/routes/terminals.ts
Normal file
88
apps/booterm/src/routes/terminals.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { getSessionInfo } from '../db.js';
|
||||
import {
|
||||
sanitizeId,
|
||||
tmuxSessionName,
|
||||
tmuxWindowName,
|
||||
ensureWindow,
|
||||
killWindow,
|
||||
hasSession,
|
||||
listWindows,
|
||||
} from '../pty/manager.js';
|
||||
import { resizePane } from '../ws/attach.js';
|
||||
|
||||
const ParamsSchema = z.object({ sid: z.string(), pid: z.string() });
|
||||
const ResizeBodySchema = z.object({
|
||||
cols: z.coerce.number().int().min(1).max(2000),
|
||||
rows: z.coerce.number().int().min(1).max(2000),
|
||||
});
|
||||
|
||||
export function registerTerminalRoutes(app: FastifyInstance, tmuxConfPath: string): void {
|
||||
app.post<{ Params: { sid: string; pid: string } }>(
|
||||
'/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 session = await getSessionInfo(sid);
|
||||
if (!session) return reply.code(404).send({ error: 'unknown_session' });
|
||||
|
||||
const sessionName = tmuxSessionName(sid);
|
||||
const windowName = tmuxWindowName(pid);
|
||||
|
||||
try {
|
||||
await ensureWindow(tmuxConfPath, sessionName, windowName, session.project_path, req.log);
|
||||
} catch (err) {
|
||||
req.log.error({ err }, 'ensureWindow failed');
|
||||
return reply.code(500).send({ error: 'tmux_failed' });
|
||||
}
|
||||
return reply.code(200).send({ tmux_window: windowName });
|
||||
},
|
||||
);
|
||||
|
||||
app.post<{ Params: { sid: string; pid: string }; Body: { cols: number; rows: number } }>(
|
||||
'/api/term/sessions/:sid/panes/:pid/resize',
|
||||
async (req, reply) => {
|
||||
const p = ParamsSchema.safeParse(req.params);
|
||||
if (!p.success) return reply.code(400).send({ error: 'bad_params' });
|
||||
const b = ResizeBodySchema.safeParse(req.body);
|
||||
if (!b.success) return reply.code(400).send({ error: 'bad_body' });
|
||||
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 ok = resizePane(pid, b.data.cols, b.data.rows);
|
||||
if (!ok) return reply.code(404).send({ error: 'no_active_pty' });
|
||||
return reply.code(200).send({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
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(sid);
|
||||
const windowName = tmuxWindowName(pid);
|
||||
|
||||
if (!(await hasSession(tmuxConfPath, sessionName))) {
|
||||
return reply.code(404).send({ error: 'unknown_session' });
|
||||
}
|
||||
const windows = await listWindows(tmuxConfPath, sessionName);
|
||||
if (!windows.includes(windowName)) {
|
||||
return reply.code(404).send({ error: 'unknown_pane' });
|
||||
}
|
||||
const killed = await killWindow(tmuxConfPath, sessionName, windowName);
|
||||
if (!killed) return reply.code(500).send({ error: 'tmux_kill_failed' });
|
||||
return reply.code(200).send({ ok: true });
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user