feat: add integrated terminal with xterm.js and shell sessions
This commit is contained in:
@@ -3,10 +3,13 @@ import cors from "cors";
|
||||
import { createServer } from "http";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { initWebSocket, getClientCount } from "./services/ws.service.js";
|
||||
import workspaceRouter from "./routes/workspace.js";
|
||||
import dockerRouter from "./routes/docker.js";
|
||||
import editorRouter from "./routes/editor.js";
|
||||
import terminalRouter from "./routes/terminal.js";
|
||||
import { attachWebSocket, createTerminalSession } from "./services/terminal.service.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
@@ -28,6 +31,7 @@ app.get("/api/health", (_req, res) => {
|
||||
app.use("/api/workspace", workspaceRouter);
|
||||
app.use("/api/docker", dockerRouter);
|
||||
app.use("/api/editor", editorRouter);
|
||||
app.use("/api/terminal", terminalRouter);
|
||||
|
||||
const frontendDist = path.resolve(__dirname, "../../frontend/dist");
|
||||
app.use(express.static(frontendDist));
|
||||
@@ -35,6 +39,21 @@ app.get("*path", (_req, res) => {
|
||||
res.sendFile(path.join(frontendDist, "index.html"));
|
||||
});
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
const url = new URL(request.url || "", `http://${request.headers.host}`);
|
||||
const termMatch = url.pathname.match(/^\/ws\/terminal\/(.+)$/);
|
||||
if (termMatch) {
|
||||
const sessionId = termMatch[1];
|
||||
const termWss = new WebSocketServer({ noServer: true });
|
||||
termWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
if (!attachWebSocket(sessionId, ws)) {
|
||||
createTerminalSession(sessionId);
|
||||
attachWebSocket(sessionId, ws);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Layonara Forge listening on http://0.0.0.0:${PORT}`);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
createTerminalSession,
|
||||
listSessions,
|
||||
destroySession,
|
||||
} from "../services/terminal.service.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/sessions", (_req, res) => {
|
||||
res.json({ sessions: listSessions() });
|
||||
});
|
||||
|
||||
router.post("/sessions", (req, res) => {
|
||||
const id = (req.body.id as string) || `term-${Date.now()}`;
|
||||
createTerminalSession(id);
|
||||
res.json({ id });
|
||||
});
|
||||
|
||||
router.delete("/sessions/:id", (req, res) => {
|
||||
destroySession(req.params.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
interface TerminalSession {
|
||||
id: string;
|
||||
process: ChildProcess;
|
||||
ws: WebSocket | null;
|
||||
}
|
||||
|
||||
const sessions = new Map<string, TerminalSession>();
|
||||
|
||||
export function createTerminalSession(id: string): TerminalSession {
|
||||
const proc = spawn("/bin/bash", ["-l"], {
|
||||
env: { ...process.env, TERM: "xterm-256color" },
|
||||
cwd: process.env.WORKSPACE_PATH || "/workspace",
|
||||
});
|
||||
const session: TerminalSession = { id, process: proc, ws: null };
|
||||
sessions.set(id, session);
|
||||
proc.on("exit", () => sessions.delete(id));
|
||||
return session;
|
||||
}
|
||||
|
||||
export function attachWebSocket(sessionId: string, ws: WebSocket): boolean {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) return false;
|
||||
session.ws = ws;
|
||||
session.process.stdout?.on("data", (data: Buffer) => {
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send(data.toString());
|
||||
});
|
||||
session.process.stderr?.on("data", (data: Buffer) => {
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send(data.toString());
|
||||
});
|
||||
ws.on("message", (data) => {
|
||||
session.process.stdin?.write(data.toString());
|
||||
});
|
||||
ws.on("close", () => {
|
||||
session.ws = null;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function destroySession(id: string): void {
|
||||
const session = sessions.get(id);
|
||||
if (session) {
|
||||
session.process.kill();
|
||||
sessions.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export function listSessions(): string[] {
|
||||
return Array.from(sessions.keys());
|
||||
}
|
||||
Reference in New Issue
Block a user