Skip to content

Commit 55f4ff9

Browse files
committed
refactor(pty): streamline server URL handling and add port conflict resolution
Remove the separate pty_server_url tool and handle the 'background-pty-server-url' command directly in the plugin's command.execute.before hook. This simplifies the implementation and removes unnecessary files. Modify startWebServer to handle port conflicts by falling back to an OS-assigned port (port 0) if the default port is in use. Add getServerUrl utility function for retrieving the current server URL. These changes improve code maintainability and system reliability without altering the user-facing functionality.
1 parent e506a6e commit 55f4ff9

4 files changed

Lines changed: 94 additions & 92 deletions

File tree

src/plugin.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { ptyWrite } from './plugin/pty/tools/write.ts'
66
import { ptyRead } from './plugin/pty/tools/read.ts'
77
import { ptyList } from './plugin/pty/tools/list.ts'
88
import { ptyKill } from './plugin/pty/tools/kill.ts'
9-
import { ptyServerUrl } from './plugin/pty/tools/server-url.ts'
10-
import { startWebServer } from './web/server/server.ts'
9+
import { getServerUrl, startWebServer } from './web/server/server.ts'
1110

1211
interface SessionDeletedEvent {
1312
type: 'session.deleted'
@@ -17,30 +16,45 @@ interface SessionDeletedEvent {
1716
}
1817
}
1918
}
19+
const ptyServerUrlCommand = 'background-pty-server-url'
2020

2121
export const PTYPlugin = async ({ client, directory }: PluginContext): Promise<PluginResult> => {
2222
initPermissions(client, directory)
2323
initManager(client)
2424

2525
return {
26+
"command.execute.before": async (input) => {
27+
if (input.command === ptyServerUrlCommand) {
28+
const serverUrl = getServerUrl()
29+
client.session.prompt({
30+
path: { id: input.sessionID },
31+
body: {
32+
parts: [{
33+
type: 'text',
34+
text: serverUrl ? `PTY Web Server URL: ${serverUrl}` : 'PTY Web Server is not running.',
35+
}],
36+
noReply: true,
37+
}
38+
})
39+
throw new Error('Command handled by PTY plugin')
40+
}
41+
},
2642
tool: {
2743
pty_spawn: ptySpawn,
2844
pty_write: ptyWrite,
2945
pty_read: ptyRead,
3046
pty_list: ptyList,
3147
pty_kill: ptyKill,
32-
pty_server_url: ptyServerUrl,
3348
},
3449
config: async (input) => {
3550
if (!input.command) {
3651
input.command = {}
3752
}
38-
input.command['background-pty-server-url'] = {
39-
template:
40-
'Get the URL of the running PTY web server instance by calling the pty_server_url tool and display it.',
41-
description: 'Get the link to the running PTY web server',
53+
const serverUrl = await startWebServer()
54+
input.command[ptyServerUrlCommand] = {
55+
template: `${serverUrl}`,
56+
description: 'print link to PTY web server',
4257
}
43-
await startWebServer()
4458
},
4559
event: async ({ event }) => {
4660
if (!event) {

src/plugin/pty/tools/server-url.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/plugin/pty/tools/server-url.txt

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/web/server/server.ts

Lines changed: 72 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -176,69 +176,81 @@ export async function startWebServer(config: Partial<ServerConfig> = {}): Promis
176176

177177
const staticRoutes = await buildStaticRoutes()
178178

179-
server = Bun.serve({
180-
hostname: finalConfig.hostname,
181-
port: finalConfig.port,
182-
183-
routes: {
184-
...staticRoutes,
185-
'/': wrapWithSecurityHeaders(
186-
() => new Response(null, { status: 302, headers: { Location: '/index.html' } })
187-
),
188-
'/ws': (req: Request) => {
189-
if (req.headers.get('upgrade') === 'websocket') {
190-
const success = server!.upgrade(req)
191-
if (success) {
192-
return undefined // Upgrade succeeded, Bun sends 101 automatically
179+
const createServer = (port: number) => {
180+
return Bun.serve({
181+
hostname: finalConfig.hostname,
182+
port,
183+
184+
routes: {
185+
...staticRoutes,
186+
'/': wrapWithSecurityHeaders(
187+
() => new Response(null, { status: 302, headers: { Location: '/index.html' } })
188+
),
189+
'/ws': (req: Request) => {
190+
if (req.headers.get('upgrade') === 'websocket') {
191+
const success = server!.upgrade(req)
192+
if (success) {
193+
return undefined // Upgrade succeeded, Bun sends 101 automatically
194+
}
195+
return new Response('WebSocket upgrade failed', { status: 400 })
196+
} else {
197+
return new Response('WebSocket endpoint - use WebSocket upgrade', { status: 426 })
193198
}
194-
return new Response('WebSocket upgrade failed', { status: 400 })
195-
} else {
196-
return new Response('WebSocket endpoint - use WebSocket upgrade', { status: 426 })
197-
}
199+
},
200+
'/health': wrapWithSecurityHeaders(handleHealth),
201+
'/api/sessions': wrapWithSecurityHeaders(async (req: Request) => {
202+
if (req.method === 'GET') return getSessions()
203+
if (req.method === 'POST') return createSession(req)
204+
return new Response('Method not allowed', { status: 405 })
205+
}),
206+
'/api/sessions/clear': wrapWithSecurityHeaders(async (req: Request) => {
207+
if (req.method === 'POST') return clearSessions()
208+
return new Response('Method not allowed', { status: 405 })
209+
}),
210+
'/api/sessions/:id': wrapWithSecurityHeaders(async (req: Request) => {
211+
if (req.method === 'GET') return getSession(req as BunRequest<'/api/sessions/:id'>)
212+
return new Response('Method not allowed', { status: 405 })
213+
}),
214+
'/api/sessions/:id/input': wrapWithSecurityHeaders(async (req: Request) => {
215+
if (req.method === 'POST') return sendInput(req as BunRequest<'/api/sessions/:id/input'>)
216+
return new Response('Method not allowed', { status: 405 })
217+
}),
218+
'/api/sessions/:id/kill': wrapWithSecurityHeaders(async (req: Request) => {
219+
if (req.method === 'POST') return killSession(req as BunRequest<'/api/sessions/:id/kill'>)
220+
return new Response('Method not allowed', { status: 405 })
221+
}),
222+
'/api/sessions/:id/buffer/raw': wrapWithSecurityHeaders(async (req: Request) => {
223+
if (req.method === 'GET')
224+
return getRawBuffer(req as BunRequest<'/api/sessions/:id/buffer/raw'>)
225+
return new Response('Method not allowed', { status: 405 })
226+
}),
227+
'/api/sessions/:id/buffer/plain': wrapWithSecurityHeaders(async (req: Request) => {
228+
if (req.method === 'GET')
229+
return getPlainBuffer(req as BunRequest<'/api/sessions/:id/buffer/plain'>)
230+
return new Response('Method not allowed', { status: 405 })
231+
}),
198232
},
199-
'/health': wrapWithSecurityHeaders(handleHealth),
200-
'/api/sessions': wrapWithSecurityHeaders(async (req: Request) => {
201-
if (req.method === 'GET') return getSessions()
202-
if (req.method === 'POST') return createSession(req)
203-
return new Response('Method not allowed', { status: 405 })
204-
}),
205-
'/api/sessions/clear': wrapWithSecurityHeaders(async (req: Request) => {
206-
if (req.method === 'POST') return clearSessions()
207-
return new Response('Method not allowed', { status: 405 })
208-
}),
209-
'/api/sessions/:id': wrapWithSecurityHeaders(async (req: Request) => {
210-
if (req.method === 'GET') return getSession(req as BunRequest<'/api/sessions/:id'>)
211-
return new Response('Method not allowed', { status: 405 })
212-
}),
213-
'/api/sessions/:id/input': wrapWithSecurityHeaders(async (req: Request) => {
214-
if (req.method === 'POST') return sendInput(req as BunRequest<'/api/sessions/:id/input'>)
215-
return new Response('Method not allowed', { status: 405 })
216-
}),
217-
'/api/sessions/:id/kill': wrapWithSecurityHeaders(async (req: Request) => {
218-
if (req.method === 'POST') return killSession(req as BunRequest<'/api/sessions/:id/kill'>)
219-
return new Response('Method not allowed', { status: 405 })
220-
}),
221-
'/api/sessions/:id/buffer/raw': wrapWithSecurityHeaders(async (req: Request) => {
222-
if (req.method === 'GET')
223-
return getRawBuffer(req as BunRequest<'/api/sessions/:id/buffer/raw'>)
224-
return new Response('Method not allowed', { status: 405 })
225-
}),
226-
'/api/sessions/:id/buffer/plain': wrapWithSecurityHeaders(async (req: Request) => {
227-
if (req.method === 'GET')
228-
return getPlainBuffer(req as BunRequest<'/api/sessions/:id/buffer/plain'>)
229-
return new Response('Method not allowed', { status: 405 })
230-
}),
231-
},
232-
233-
websocket: {
234-
perMessageDeflate: true,
235-
...wsHandler,
236-
},
237-
238-
fetch: handleRequest,
239-
})
240233

241-
return `http://${finalConfig.hostname}:${finalConfig.port}`
234+
websocket: {
235+
perMessageDeflate: true,
236+
...wsHandler,
237+
},
238+
239+
fetch: handleRequest,
240+
})
241+
}
242+
243+
try {
244+
server = createServer(finalConfig.port)
245+
} catch (error: any) {
246+
if (error.code === 'EADDRINUSE' || error.message?.includes('EADDRINUSE')) {
247+
server = createServer(0)
248+
} else {
249+
throw error
250+
}
251+
}
252+
253+
return `http://${server.hostname}:${server.port}`
242254
}
243255

244256
export function stopWebServer(): void {

0 commit comments

Comments
 (0)