mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
import { Hono } from "hono";
|
|
import type { ProcessManager } from "./manager.js";
|
|
import { MANAGER_HTML } from "./html.js";
|
|
|
|
function logReq(method: string, path: string, status?: number) {
|
|
const ts = new Date().toISOString();
|
|
const suffix = status != null ? ` -> ${status}` : "";
|
|
console.log(`[${ts}] [http] ${method} ${path}${suffix}`);
|
|
}
|
|
|
|
export function createApp(manager: ProcessManager): Hono {
|
|
const app = new Hono();
|
|
|
|
app.get("/", (c) => {
|
|
logReq("GET", "/", 200);
|
|
return c.html(MANAGER_HTML);
|
|
});
|
|
|
|
app.get("/api/instances", (c) => {
|
|
const list = manager.list();
|
|
logReq("GET", "/api/instances", 200);
|
|
return c.json(list);
|
|
});
|
|
|
|
app.post("/api/instances", async (c) => {
|
|
let body: { group?: string; command?: string };
|
|
try {
|
|
body = await c.req.json<{ group?: string; command?: string }>();
|
|
} catch {
|
|
logReq("POST", "/api/instances", 400);
|
|
return c.json({ error: "invalid JSON body" }, 400);
|
|
}
|
|
if (!body.group?.trim() || !body.command?.trim()) {
|
|
logReq("POST", "/api/instances", 400);
|
|
return c.json({ error: "group and command are required" }, 400);
|
|
}
|
|
const instance = manager.create(body.group.trim(), body.command.trim());
|
|
logReq("POST", `/api/instances group=${body.group}`, 201);
|
|
return c.json(
|
|
{
|
|
id: instance.id,
|
|
group: instance.group,
|
|
command: instance.command,
|
|
status: instance.status,
|
|
pid: instance.pid,
|
|
startTime: instance.startTime,
|
|
exitCode: instance.exitCode,
|
|
},
|
|
201,
|
|
);
|
|
});
|
|
|
|
app.post("/api/instances/:id/stop", (c) => {
|
|
const id = c.req.param("id");
|
|
const inst = manager.get(id);
|
|
if (!inst) {
|
|
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 404);
|
|
return c.json({ error: "not found" }, 404);
|
|
}
|
|
if (inst.status !== "running") {
|
|
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 400);
|
|
return c.json({ error: "not running" }, 400);
|
|
}
|
|
manager.stop(inst.id);
|
|
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 200);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.delete("/api/instances/:id", (c) => {
|
|
const id = c.req.param("id");
|
|
const inst = manager.get(id);
|
|
if (!inst) {
|
|
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 404);
|
|
return c.json({ error: "not found" }, 404);
|
|
}
|
|
if (inst.status === "running") {
|
|
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 400);
|
|
return c.json({ error: "still running" }, 400);
|
|
}
|
|
manager.remove(inst.id);
|
|
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 200);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.get("/api/instances/:id/logs", (c) => {
|
|
const id = c.req.param("id");
|
|
const inst = manager.get(id);
|
|
if (!inst) {
|
|
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs`, 404);
|
|
return c.json({ error: "not found" }, 404);
|
|
}
|
|
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs SSE`);
|
|
|
|
const stream = new ReadableStream({
|
|
start(controller) {
|
|
const encoder = new TextEncoder();
|
|
|
|
const send = (data: string) => {
|
|
try {
|
|
controller.enqueue(encoder.encode(data));
|
|
} catch {
|
|
// stream closed
|
|
}
|
|
};
|
|
|
|
// send historical logs
|
|
for (const log of inst.logs) {
|
|
send(`data: ${JSON.stringify(log)}\n\n`);
|
|
}
|
|
|
|
// subscribe to new logs
|
|
const unsub = manager.subscribe(inst.id, (entry) => {
|
|
send(`data: ${JSON.stringify(entry)}\n\n`);
|
|
});
|
|
|
|
// keepalive every 15s
|
|
const keepalive = setInterval(() => {
|
|
send(": keepalive\n\n");
|
|
}, 15000);
|
|
|
|
const cleanup = () => {
|
|
unsub();
|
|
clearInterval(keepalive);
|
|
logReq("SSE", `/api/instances/${id.slice(0, 8)}/logs closed`);
|
|
try {
|
|
controller.close();
|
|
} catch {
|
|
// already closed
|
|
}
|
|
};
|
|
|
|
c.req.raw.signal.addEventListener("abort", cleanup, { once: true });
|
|
},
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
});
|
|
});
|
|
|
|
// Catch-all: log unmatched routes for debugging
|
|
app.all("*", (c) => {
|
|
logReq(c.req.method, c.req.path, 404);
|
|
return c.json({ error: "not found", path: c.req.path }, 404);
|
|
});
|
|
|
|
return app;
|
|
}
|