|
1 | 1 | import type { Server, ServerWebSocket, BunRequest } from 'bun' |
2 | | -import { manager, onRawOutput, setOnSessionUpdate } from '../../plugin/pty/manager.ts' |
| 2 | +import { manager, onRawOutput, setOnSessionUpdate, PTYManager } from '../../plugin/pty/manager.ts' |
3 | 3 | import type { WSMessage, ServerConfig } from '../shared/types.ts' |
4 | 4 | import { get404Response } from './handlers/static.ts' |
5 | 5 | import { handleHealth } from './handlers/health.ts' |
@@ -50,116 +50,120 @@ function wrapWithSecurityHeaders( |
50 | 50 | } |
51 | 51 | } |
52 | 52 |
|
53 | | -function sendSessionList(ws: ServerWebSocket<any>): void { |
54 | | - const sessions = manager.list() |
55 | | - const sessionData = sessions.map((s) => ({ |
56 | | - id: s.id, |
57 | | - title: s.title, |
58 | | - description: s.description, |
59 | | - command: s.command, |
60 | | - status: s.status, |
61 | | - exitCode: s.exitCode, |
62 | | - pid: s.pid, |
63 | | - lineCount: s.lineCount, |
64 | | - createdAt: s.createdAt.toISOString(), |
65 | | - })) |
66 | | - const message: WSMessage = { type: 'session_list', sessions: sessionData } |
67 | | - ws.send(JSON.stringify(message)) |
| 53 | +async function handleRequest(req: Request): Promise<Response> { |
| 54 | + return get404Response({ url: req.url, method: req.method, note: 'No route matched' }) |
68 | 55 | } |
69 | 56 |
|
70 | | -function handleSubscribe(ws: ServerWebSocket<any>, message: WSMessage): void { |
71 | | - if (message.sessionId) { |
72 | | - const session = manager.get(message.sessionId) |
73 | | - if (!session) { |
74 | | - ws.send(JSON.stringify({ type: 'error', error: `Session ${message.sessionId} not found` })) |
75 | | - } else { |
76 | | - ws.subscribe(`session:${message.sessionId}`) |
77 | | - } |
| 57 | +export async function startWebServer( |
| 58 | + config: Partial<ServerConfig> = {}, |
| 59 | + testManager?: PTYManager |
| 60 | +): Promise<string> { |
| 61 | + const finalConfig = { ...defaultConfig, ...config } |
| 62 | + const ptyManager = testManager || manager |
| 63 | + |
| 64 | + function sendSessionList(ws: ServerWebSocket<any>): void { |
| 65 | + const sessions = ptyManager.list() |
| 66 | + const sessionData = sessions.map((s) => ({ |
| 67 | + id: s.id, |
| 68 | + title: s.title, |
| 69 | + description: s.description, |
| 70 | + command: s.command, |
| 71 | + status: s.status, |
| 72 | + exitCode: s.exitCode, |
| 73 | + pid: s.pid, |
| 74 | + lineCount: s.lineCount, |
| 75 | + createdAt: s.createdAt.toISOString(), |
| 76 | + })) |
| 77 | + const message: WSMessage = { type: 'session_list', sessions: sessionData } |
| 78 | + ws.send(JSON.stringify(message)) |
78 | 79 | } |
79 | | -} |
80 | 80 |
|
81 | | -function handleUnsubscribe(ws: ServerWebSocket<any>, message: WSMessage): void { |
82 | | - if (message.sessionId) { |
83 | | - ws.unsubscribe(`session:${message.sessionId}`) |
| 81 | + function handleSubscribe(ws: ServerWebSocket<any>, message: WSMessage): void { |
| 82 | + if (message.sessionId) { |
| 83 | + const session = ptyManager.get(message.sessionId) |
| 84 | + if (!session) { |
| 85 | + ws.send(JSON.stringify({ type: 'error', error: `Session ${message.sessionId} not found` })) |
| 86 | + } else { |
| 87 | + ws.subscribe(`session:${message.sessionId}`) |
| 88 | + } |
| 89 | + } |
84 | 90 | } |
85 | | -} |
86 | 91 |
|
87 | | -function handleSessionListRequest(ws: ServerWebSocket<any>, _message: WSMessage): void { |
88 | | - sendSessionList(ws) |
89 | | -} |
| 92 | + function handleUnsubscribe(ws: ServerWebSocket<any>, message: WSMessage): void { |
| 93 | + if (message.sessionId) { |
| 94 | + ws.unsubscribe(`session:${message.sessionId}`) |
| 95 | + } |
| 96 | + } |
90 | 97 |
|
91 | | -function handleUnknownMessage(ws: ServerWebSocket<any>, _message: WSMessage): void { |
92 | | - ws.send(JSON.stringify({ type: 'error', error: 'Unknown message type' })) |
93 | | -} |
| 98 | + function handleSessionListRequest(ws: ServerWebSocket<any>, _message: WSMessage): void { |
| 99 | + sendSessionList(ws) |
| 100 | + } |
94 | 101 |
|
95 | | -// Set callback for session updates |
96 | | -setOnSessionUpdate(() => { |
97 | | - const sessions = manager.list() |
98 | | - const sessionData = sessions.map((s) => ({ |
99 | | - id: s.id, |
100 | | - title: s.title, |
101 | | - description: s.description, |
102 | | - command: s.command, |
103 | | - status: s.status, |
104 | | - exitCode: s.exitCode, |
105 | | - pid: s.pid, |
106 | | - lineCount: s.lineCount, |
107 | | - createdAt: s.createdAt.toISOString(), |
108 | | - })) |
109 | | - const message = { type: 'session_list', sessions: sessionData } |
110 | | - for (const [ws] of wsClients) { |
111 | | - ws.send(JSON.stringify(message)) |
| 102 | + function handleUnknownMessage(ws: ServerWebSocket<any>, _message: WSMessage): void { |
| 103 | + ws.send(JSON.stringify({ type: 'error', error: 'Unknown message type' })) |
112 | 104 | } |
113 | | -}) |
114 | 105 |
|
115 | | -function handleWebSocketMessage(ws: ServerWebSocket<any>, data: string): void { |
116 | | - try { |
117 | | - const message: WSMessage = JSON.parse(data) |
| 106 | + // Set callback for session updates |
| 107 | + setOnSessionUpdate(() => { |
| 108 | + const sessions = ptyManager.list() |
| 109 | + const sessionData = sessions.map((s) => ({ |
| 110 | + id: s.id, |
| 111 | + title: s.title, |
| 112 | + description: s.description, |
| 113 | + command: s.command, |
| 114 | + status: s.status, |
| 115 | + exitCode: s.exitCode, |
| 116 | + pid: s.pid, |
| 117 | + lineCount: s.lineCount, |
| 118 | + createdAt: s.createdAt.toISOString(), |
| 119 | + })) |
| 120 | + const message = { type: 'session_list', sessions: sessionData } |
| 121 | + for (const [ws] of wsClients) { |
| 122 | + ws.send(JSON.stringify(message)) |
| 123 | + } |
| 124 | + }) |
118 | 125 |
|
119 | | - switch (message.type) { |
120 | | - case 'subscribe': |
121 | | - handleSubscribe(ws, message) |
122 | | - break |
| 126 | + function handleWebSocketMessage(ws: ServerWebSocket<any>, data: string): void { |
| 127 | + try { |
| 128 | + const message: WSMessage = JSON.parse(data) |
123 | 129 |
|
124 | | - case 'unsubscribe': |
125 | | - handleUnsubscribe(ws, message) |
126 | | - break |
| 130 | + switch (message.type) { |
| 131 | + case 'subscribe': |
| 132 | + handleSubscribe(ws, message) |
| 133 | + break |
127 | 134 |
|
128 | | - case 'session_list': |
129 | | - handleSessionListRequest(ws, message) |
130 | | - break |
| 135 | + case 'unsubscribe': |
| 136 | + handleUnsubscribe(ws, message) |
| 137 | + break |
131 | 138 |
|
132 | | - default: |
133 | | - handleUnknownMessage(ws, message) |
| 139 | + case 'session_list': |
| 140 | + handleSessionListRequest(ws, message) |
| 141 | + break |
| 142 | + |
| 143 | + default: |
| 144 | + handleUnknownMessage(ws, message) |
| 145 | + } |
| 146 | + } catch (err) { |
| 147 | + ws.send(JSON.stringify({ type: 'error', error: 'Invalid message format' })) |
134 | 148 | } |
135 | | - } catch (err) { |
136 | | - ws.send(JSON.stringify({ type: 'error', error: 'Invalid message format' })) |
137 | 149 | } |
138 | | -} |
139 | | - |
140 | | -const wsHandler = { |
141 | | - open(ws: ServerWebSocket<any>) { |
142 | | - wsConnectionCount++ |
143 | | - wsClients.set(ws, {}) |
144 | | - sendSessionList(ws) |
145 | | - }, |
146 | | - |
147 | | - message(ws: ServerWebSocket<any>, message: string) { |
148 | | - handleWebSocketMessage(ws, message) |
149 | | - }, |
150 | | - |
151 | | - close(_ws: ServerWebSocket<any>) { |
152 | | - wsConnectionCount-- |
153 | | - wsClients.delete(_ws) |
154 | | - }, |
155 | | -} |
156 | | - |
157 | | -async function handleRequest(req: Request): Promise<Response> { |
158 | | - return get404Response({ url: req.url, method: req.method, note: 'No route matched' }) |
159 | | -} |
160 | 150 |
|
161 | | -export async function startWebServer(config: Partial<ServerConfig> = {}): Promise<string> { |
162 | | - const finalConfig = { ...defaultConfig, ...config } |
| 151 | + const wsHandler = { |
| 152 | + open(ws: ServerWebSocket<any>) { |
| 153 | + wsConnectionCount++ |
| 154 | + wsClients.set(ws, {}) |
| 155 | + sendSessionList(ws) |
| 156 | + }, |
| 157 | + |
| 158 | + message(ws: ServerWebSocket<any>, message: string) { |
| 159 | + handleWebSocketMessage(ws, message) |
| 160 | + }, |
| 161 | + |
| 162 | + close(_ws: ServerWebSocket<any>) { |
| 163 | + wsConnectionCount-- |
| 164 | + wsClients.delete(_ws) |
| 165 | + }, |
| 166 | + } |
163 | 167 |
|
164 | 168 | if (server) { |
165 | 169 | return `http://${server.hostname}:${server.port}` |
@@ -199,34 +203,37 @@ export async function startWebServer(config: Partial<ServerConfig> = {}): Promis |
199 | 203 | }, |
200 | 204 | '/health': wrapWithSecurityHeaders(handleHealth), |
201 | 205 | '/api/sessions': wrapWithSecurityHeaders(async (req: Request) => { |
202 | | - if (req.method === 'GET') return getSessions() |
203 | | - if (req.method === 'POST') return createSession(req) |
| 206 | + if (req.method === 'GET') return getSessions(ptyManager) |
| 207 | + if (req.method === 'POST') return createSession(req, ptyManager) |
204 | 208 | return new Response('Method not allowed', { status: 405 }) |
205 | 209 | }), |
206 | 210 | '/api/sessions/clear': wrapWithSecurityHeaders(async (req: Request) => { |
207 | | - if (req.method === 'POST') return clearSessions() |
| 211 | + if (req.method === 'POST') return clearSessions(ptyManager) |
208 | 212 | return new Response('Method not allowed', { status: 405 }) |
209 | 213 | }), |
210 | 214 | '/api/sessions/:id': wrapWithSecurityHeaders(async (req: Request) => { |
211 | | - if (req.method === 'GET') return getSession(req as BunRequest<'/api/sessions/:id'>) |
| 215 | + if (req.method === 'GET') |
| 216 | + return getSession(req as BunRequest<'/api/sessions/:id'>, ptyManager) |
212 | 217 | return new Response('Method not allowed', { status: 405 }) |
213 | 218 | }), |
214 | 219 | '/api/sessions/:id/input': wrapWithSecurityHeaders(async (req: Request) => { |
215 | | - if (req.method === 'POST') return sendInput(req as BunRequest<'/api/sessions/:id/input'>) |
| 220 | + if (req.method === 'POST') |
| 221 | + return sendInput(req as BunRequest<'/api/sessions/:id/input'>, ptyManager) |
216 | 222 | return new Response('Method not allowed', { status: 405 }) |
217 | 223 | }), |
218 | 224 | '/api/sessions/:id/kill': wrapWithSecurityHeaders(async (req: Request) => { |
219 | | - if (req.method === 'POST') return killSession(req as BunRequest<'/api/sessions/:id/kill'>) |
| 225 | + if (req.method === 'POST') |
| 226 | + return killSession(req as BunRequest<'/api/sessions/:id/kill'>, ptyManager) |
220 | 227 | return new Response('Method not allowed', { status: 405 }) |
221 | 228 | }), |
222 | 229 | '/api/sessions/:id/buffer/raw': wrapWithSecurityHeaders(async (req: Request) => { |
223 | 230 | if (req.method === 'GET') |
224 | | - return getRawBuffer(req as BunRequest<'/api/sessions/:id/buffer/raw'>) |
| 231 | + return getRawBuffer(req as BunRequest<'/api/sessions/:id/buffer/raw'>, ptyManager) |
225 | 232 | return new Response('Method not allowed', { status: 405 }) |
226 | 233 | }), |
227 | 234 | '/api/sessions/:id/buffer/plain': wrapWithSecurityHeaders(async (req: Request) => { |
228 | 235 | if (req.method === 'GET') |
229 | | - return getPlainBuffer(req as BunRequest<'/api/sessions/:id/buffer/plain'>) |
| 236 | + return getPlainBuffer(req as BunRequest<'/api/sessions/:id/buffer/plain'>, ptyManager) |
230 | 237 | return new Response('Method not allowed', { status: 405 }) |
231 | 238 | }), |
232 | 239 | }, |
|
0 commit comments