refactor: extract shared route handlers and modularize plan server (#214)

* refactor: extract shared route handlers and modularize plan server

Pull duplicated /api/image, /api/upload, /api/agents handlers and
handleServerReady into shared-handlers.ts. Move /api/doc and Obsidian
reference endpoints into reference-handlers.ts. Eliminates ~120 lines
of verbatim duplication across index.ts, review.ts, and annotate.ts.

Closes #210

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: deduplicate OpencodeClient type and fix stale module comment

Export OpencodeClient interface from shared-handlers.ts and import it
in index.ts and review.ts instead of redefining inline. Also corrects
the module doc comment about /api/agents usage scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Ramos
2026-03-03 23:17:16 -08:00
committed by GitHub
parent e5f3cfffbb
commit d9c2d46113
5 changed files with 340 additions and 394 deletions
+6 -73
View File
@@ -9,17 +9,16 @@
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/
import { mkdirSync } from "fs";
import { isRemoteSession, getServerPort } from "./remote";
import { openBrowser } from "./browser";
import { type DiffType, type GitContext, runGitDiff } from "./git";
import { getRepoInfo } from "./repo";
import { validateImagePath, validateUploadExtension, UPLOAD_DIR } from "./image";
import { handleImage, handleUpload, handleAgents, handleServerReady, type OpencodeClient } from "./shared-handlers";
// Re-export utilities
export { isRemoteSession, getServerPort } from "./remote";
export { openBrowser } from "./browser";
export { type DiffType, type DiffOption, type GitContext } from "./git";
export { handleServerReady as handleReviewServerReady } from "./shared-handlers";
// --- Types ---
@@ -45,11 +44,7 @@ export interface ReviewServerOptions {
/** Called when server starts with the URL, remote status, and port */
onReady?: (url: string, isRemote: boolean, port: number) => void;
/** OpenCode client for querying available agents (OpenCode only) */
opencodeClient?: {
app: {
agents: (options?: object) => Promise<{ data?: Array<{ name: string; description?: string; mode: string; hidden?: boolean }> }>;
};
};
opencodeClient?: OpencodeClient;
}
export interface ReviewServerResult {
@@ -177,66 +172,17 @@ export async function startReviewServer(
// API: Serve images (local paths or temp uploads)
if (url.pathname === "/api/image") {
const imagePath = url.searchParams.get("path");
if (!imagePath) {
return new Response("Missing path parameter", { status: 400 });
}
const validation = validateImagePath(imagePath);
if (!validation.valid) {
return new Response(validation.error!, { status: 403 });
}
try {
const file = Bun.file(validation.resolved);
if (!(await file.exists())) {
return new Response("File not found", { status: 404 });
}
return new Response(file);
} catch {
return new Response("Failed to read file", { status: 500 });
}
return handleImage(req);
}
// API: Upload image -> save to temp -> return path
if (url.pathname === "/api/upload" && req.method === "POST") {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return new Response("No file provided", { status: 400 });
}
const extResult = validateUploadExtension(file.name);
if (!extResult.valid) {
return Response.json({ error: extResult.error }, { status: 400 });
}
mkdirSync(UPLOAD_DIR, { recursive: true });
const tempPath = `${UPLOAD_DIR}/${crypto.randomUUID()}.${extResult.ext}`;
await Bun.write(tempPath, file);
return Response.json({ path: tempPath, originalName: file.name });
} catch (err) {
const message =
err instanceof Error ? err.message : "Upload failed";
return Response.json({ error: message }, { status: 500 });
}
return handleUpload(req);
}
// API: Get available agents (OpenCode only)
if (url.pathname === "/api/agents") {
if (!options.opencodeClient) {
return Response.json({ agents: [] });
}
try {
const result = await options.opencodeClient.app.agents({});
const agents = (result.data ?? [])
.filter((a) => a.mode === "primary" && !a.hidden)
.map((a) => ({ id: a.name, name: a.name, description: a.description }));
return Response.json({ agents });
} catch {
return Response.json({ agents: [], error: "Failed to fetch agents" });
}
return handleAgents(options.opencodeClient);
}
// API: Submit review feedback
@@ -307,16 +253,3 @@ export async function startReviewServer(
stop: () => server.stop(),
};
}
/**
* Default behavior: open browser for local sessions
*/
export async function handleReviewServerReady(
url: string,
isRemote: boolean,
_port: number
): Promise<void> {
if (!isRemote) {
await openBrowser(url);
}
}