mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 13:54:01 +00:00
201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
import { Hono } from "hono"
|
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
|
import { upgradeWebSocket } from "hono/bun"
|
|
import z from "zod"
|
|
import { Pty } from "@/pty"
|
|
import { NotFoundError } from "../../storage/db"
|
|
import { errors } from "../error"
|
|
import { lazy } from "../../util/lazy"
|
|
|
|
export const PtyRoutes = lazy(() =>
|
|
new Hono()
|
|
.get(
|
|
"/",
|
|
describeRoute({
|
|
summary: "List PTY sessions",
|
|
description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
|
|
operationId: "pty.list",
|
|
responses: {
|
|
200: {
|
|
description: "List of sessions",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(Pty.Info.array()),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
async (c) => {
|
|
return c.json(Pty.list())
|
|
},
|
|
)
|
|
.post(
|
|
"/",
|
|
describeRoute({
|
|
summary: "Create PTY session",
|
|
description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
|
|
operationId: "pty.create",
|
|
responses: {
|
|
200: {
|
|
description: "Created session",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(Pty.Info),
|
|
},
|
|
},
|
|
},
|
|
...errors(400),
|
|
},
|
|
}),
|
|
validator("json", Pty.CreateInput),
|
|
async (c) => {
|
|
const info = await Pty.create(c.req.valid("json"))
|
|
return c.json(info)
|
|
},
|
|
)
|
|
.get(
|
|
"/:ptyID",
|
|
describeRoute({
|
|
summary: "Get PTY session",
|
|
description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
|
|
operationId: "pty.get",
|
|
responses: {
|
|
200: {
|
|
description: "Session info",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(Pty.Info),
|
|
},
|
|
},
|
|
},
|
|
...errors(404),
|
|
},
|
|
}),
|
|
validator("param", z.object({ ptyID: z.string() })),
|
|
async (c) => {
|
|
const info = Pty.get(c.req.valid("param").ptyID)
|
|
if (!info) {
|
|
throw new NotFoundError({ message: "Session not found" })
|
|
}
|
|
return c.json(info)
|
|
},
|
|
)
|
|
.put(
|
|
"/:ptyID",
|
|
describeRoute({
|
|
summary: "Update PTY session",
|
|
description: "Update properties of an existing pseudo-terminal (PTY) session.",
|
|
operationId: "pty.update",
|
|
responses: {
|
|
200: {
|
|
description: "Updated session",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(Pty.Info),
|
|
},
|
|
},
|
|
},
|
|
...errors(400),
|
|
},
|
|
}),
|
|
validator("param", z.object({ ptyID: z.string() })),
|
|
validator("json", Pty.UpdateInput),
|
|
async (c) => {
|
|
const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
|
return c.json(info)
|
|
},
|
|
)
|
|
.delete(
|
|
"/:ptyID",
|
|
describeRoute({
|
|
summary: "Remove PTY session",
|
|
description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
|
|
operationId: "pty.remove",
|
|
responses: {
|
|
200: {
|
|
description: "Session removed",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(z.boolean()),
|
|
},
|
|
},
|
|
},
|
|
...errors(404),
|
|
},
|
|
}),
|
|
validator("param", z.object({ ptyID: z.string() })),
|
|
async (c) => {
|
|
await Pty.remove(c.req.valid("param").ptyID)
|
|
return c.json(true)
|
|
},
|
|
)
|
|
.get(
|
|
"/:ptyID/connect",
|
|
describeRoute({
|
|
summary: "Connect to PTY session",
|
|
description: "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
|
|
operationId: "pty.connect",
|
|
responses: {
|
|
200: {
|
|
description: "Connected session",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(z.boolean()),
|
|
},
|
|
},
|
|
},
|
|
...errors(404),
|
|
},
|
|
}),
|
|
validator("param", z.object({ ptyID: z.string() })),
|
|
upgradeWebSocket((c) => {
|
|
const id = c.req.param("ptyID")
|
|
const cursor = (() => {
|
|
const value = c.req.query("cursor")
|
|
if (!value) return
|
|
const parsed = Number(value)
|
|
if (!Number.isSafeInteger(parsed) || parsed < -1) return
|
|
return parsed
|
|
})()
|
|
let handler: ReturnType<typeof Pty.connect>
|
|
if (!Pty.get(id)) throw new Error("Session not found")
|
|
|
|
type Socket = {
|
|
readyState: number
|
|
send: (data: string | Uint8Array | ArrayBuffer) => void
|
|
close: (code?: number, reason?: string) => void
|
|
}
|
|
|
|
const isSocket = (value: unknown): value is Socket => {
|
|
if (!value || typeof value !== "object") return false
|
|
if (!("readyState" in value)) return false
|
|
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
|
|
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
|
|
return typeof (value as { readyState?: unknown }).readyState === "number"
|
|
}
|
|
|
|
return {
|
|
onOpen(_event, ws) {
|
|
const socket = ws.raw
|
|
if (!isSocket(socket)) {
|
|
ws.close()
|
|
return
|
|
}
|
|
handler = Pty.connect(id, socket, cursor, ws)
|
|
},
|
|
onMessage(event) {
|
|
if (typeof event.data !== "string") return
|
|
handler?.onMessage(event.data)
|
|
},
|
|
onClose() {
|
|
handler?.onClose()
|
|
},
|
|
onError() {
|
|
handler?.onClose()
|
|
},
|
|
}
|
|
}),
|
|
),
|
|
)
|