v1.10: booterm container — xterm.js + tmux + node-pty

This commit is contained in:
2026-05-18 14:06:46 +00:00
parent d85b17081e
commit 7486e7d3e0
25 changed files with 1515 additions and 29 deletions

View 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 });
},
);
}