/workspaces refactor
This commit is contained in:
@@ -15,6 +15,11 @@ const ohMyZshSource = isDocker ? '/opt/oh-my-zsh' : null;
|
||||
const port = Number(process.env.TERMINAL_PTY_PORT ?? '5337');
|
||||
const host = process.env.TERMINAL_PTY_HOST ?? '127.0.0.1';
|
||||
|
||||
const BUFFER_MAX = 50 * 1024;
|
||||
|
||||
/** @type {Map<string, { term: import('node-pty').IPty, buffer: string, cols: number, rows: number, ws: import('ws').WebSocket | null, initConfig: object }>} */
|
||||
const sessions = new Map();
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('terminal-sidecar');
|
||||
@@ -50,7 +55,6 @@ const ensureUserFiles = async (homeDir) => {
|
||||
if (ohMyZshSource && existsSync(ohMyZshSource)) {
|
||||
await cp(ohMyZshSource, ohMyZshPath, { recursive: true });
|
||||
} else {
|
||||
// Clone oh-my-zsh on first run (host mode)
|
||||
const proc = Bun.spawn({
|
||||
cmd: ['git', 'clone', '--depth=1', 'https://github.com/ohmyzsh/ohmyzsh.git', ohMyZshPath],
|
||||
stdout: 'ignore',
|
||||
@@ -60,7 +64,6 @@ const ensureUserFiles = async (homeDir) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Install starship on host if missing (not needed in Docker)
|
||||
if (!isDocker) {
|
||||
const starshipBin = join(homeDir, '.local', 'bin', 'starship');
|
||||
if (!existsSync(starshipBin)) {
|
||||
@@ -75,20 +78,15 @@ const ensureUserFiles = async (homeDir) => {
|
||||
}
|
||||
};
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
let term = null;
|
||||
let initialized = false;
|
||||
const appendBuffer = (session, data) => {
|
||||
session.buffer += data;
|
||||
if (session.buffer.length > BUFFER_MAX) {
|
||||
session.buffer = session.buffer.slice(-BUFFER_MAX);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (term) {
|
||||
try {
|
||||
term.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
term = null;
|
||||
};
|
||||
wss.on('connection', (ws) => {
|
||||
let currentSessionId = null;
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
let msg;
|
||||
@@ -98,13 +96,58 @@ wss.on('connection', (ws) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'init' && !initialized) {
|
||||
if (msg.type === 'init') {
|
||||
const sessionId = msg.sessionId;
|
||||
if (!sessionId) return;
|
||||
|
||||
currentSessionId = sessionId;
|
||||
const existing = sessions.get(sessionId);
|
||||
|
||||
console.log(`[sidecar] init sessionId=${sessionId} existing=${!!existing} total=${sessions.size}`);
|
||||
|
||||
if (existing) {
|
||||
// Evict old WS if still attached
|
||||
if (existing.ws && existing.ws !== ws) {
|
||||
sendJson(existing.ws, { type: 'detached' });
|
||||
try {
|
||||
existing.ws.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
existing.ws = ws;
|
||||
|
||||
// Replay buffer
|
||||
if (existing.buffer.length > 0) {
|
||||
sendJson(ws, { type: 'output', data: existing.buffer });
|
||||
}
|
||||
|
||||
// Resize PTY to new client dimensions
|
||||
const cols = msg.cols ?? existing.cols;
|
||||
const rows = msg.rows ?? existing.rows;
|
||||
if (cols > 0 && rows > 0 && (cols !== existing.cols || rows !== existing.rows)) {
|
||||
existing.cols = cols;
|
||||
existing.rows = rows;
|
||||
try {
|
||||
existing.term.resize(cols, rows);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// New session — spawn PTY
|
||||
const shell = msg.shell ?? { command: '/bin/bash', args: ['-i'] };
|
||||
const cwd = msg.cwd ?? process.cwd();
|
||||
const homeDir = msg.homeDir ?? process.cwd();
|
||||
const userLabel = msg.userLabel ?? 'officer';
|
||||
const prompt = `${userLabel} in %~ %# `;
|
||||
const bashPrompt = `${userLabel} \w \$ `;
|
||||
const bashPrompt = `${userLabel} \\w \\$ `;
|
||||
const cols = msg.cols ?? 80;
|
||||
const rows = msg.rows ?? 24;
|
||||
|
||||
try {
|
||||
await ensureUserFiles(homeDir);
|
||||
@@ -112,11 +155,12 @@ wss.on('connection', (ws) => {
|
||||
// ignore
|
||||
}
|
||||
|
||||
let term;
|
||||
try {
|
||||
term = pty.spawn(shell.command, shell.args ?? [], {
|
||||
name: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cols,
|
||||
rows,
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -139,37 +183,63 @@ wss.on('connection', (ws) => {
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
const session = {
|
||||
term,
|
||||
buffer: '',
|
||||
cols,
|
||||
rows,
|
||||
ws,
|
||||
initConfig: { shell, cwd, homeDir, userLabel },
|
||||
};
|
||||
sessions.set(sessionId, session);
|
||||
|
||||
term.onData((output) => {
|
||||
sendJson(ws, { type: 'output', data: output });
|
||||
appendBuffer(session, output);
|
||||
if (session.ws) {
|
||||
sendJson(session.ws, { type: 'output', data: output });
|
||||
}
|
||||
});
|
||||
|
||||
term.onExit(() => {
|
||||
sendJson(ws, { type: 'exit' });
|
||||
cleanup();
|
||||
if (session.ws) {
|
||||
sendJson(session.ws, { type: 'exit' });
|
||||
}
|
||||
sessions.delete(sessionId);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!term) return;
|
||||
// Route other messages to current session
|
||||
if (!currentSessionId) return;
|
||||
const session = sessions.get(currentSessionId);
|
||||
if (!session) return;
|
||||
|
||||
switch (msg.type) {
|
||||
case 'input':
|
||||
term.write(msg.data ?? '');
|
||||
session.term.write(msg.data ?? '');
|
||||
break;
|
||||
case 'resize':
|
||||
if (msg.cols > 0 && msg.rows > 0) term.resize(msg.cols, msg.rows);
|
||||
if (msg.cols > 0 && msg.rows > 0) {
|
||||
session.cols = msg.cols;
|
||||
session.rows = msg.rows;
|
||||
session.term.resize(msg.cols, msg.rows);
|
||||
}
|
||||
break;
|
||||
case 'cwd':
|
||||
if (msg.path) term.write(`cd ${JSON.stringify(msg.path)}\r`);
|
||||
if (msg.path) session.term.write(`cd ${JSON.stringify(msg.path)}\r`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
cleanup();
|
||||
// Detach WS from session — do NOT kill PTY
|
||||
if (currentSessionId) {
|
||||
const session = sessions.get(currentSessionId);
|
||||
if (session && session.ws === ws) {
|
||||
session.ws = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user