139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
import type { HostApiContext } from '../context';
|
|
import { CodingProductHostError } from '../coding-product-services';
|
|
import { sendJson } from '../route-utils';
|
|
|
|
function unavailable(res: ServerResponse): void {
|
|
sendJson(res, 503, {
|
|
success: false,
|
|
code: 'CODING_PRODUCT_TOOLS_UNAVAILABLE',
|
|
error: 'Coding product tools are unavailable',
|
|
});
|
|
}
|
|
|
|
function serviceError(res: ServerResponse, error: unknown): void {
|
|
if (error instanceof CodingProductHostError) {
|
|
sendJson(res, error.status, {
|
|
success: false,
|
|
code: error.code,
|
|
error: error.message,
|
|
});
|
|
return;
|
|
}
|
|
const code = error && typeof error === 'object' && 'code' in error
|
|
? String(error.code)
|
|
: '';
|
|
if (code === 'ENOENT') {
|
|
sendJson(res, 404, {
|
|
success: false,
|
|
code: 'CODING_FILE_NOT_FOUND',
|
|
error: 'Project file does not exist',
|
|
});
|
|
return;
|
|
}
|
|
const message = error instanceof Error ? error.message : '';
|
|
const knownInputError = new Set([
|
|
'File query is required',
|
|
'File query is too long',
|
|
'Search pattern is required',
|
|
'Search pattern is too long',
|
|
'Project file path must be relative',
|
|
'Project file path escapes the active project',
|
|
'Project file path is not a file',
|
|
'Binary project files cannot be previewed',
|
|
'Project file is not valid UTF-8 text',
|
|
]);
|
|
if (knownInputError.has(message)) {
|
|
sendJson(res, 400, {
|
|
success: false,
|
|
code: 'CODING_FILE_REQUEST_INVALID',
|
|
error: message,
|
|
});
|
|
return;
|
|
}
|
|
sendJson(res, 500, {
|
|
success: false,
|
|
code: 'CODING_PRODUCT_TOOL_FAILED',
|
|
error: 'Coding product request failed',
|
|
});
|
|
}
|
|
|
|
export async function handleCodingFileRoutes(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
url: URL,
|
|
ctx: HostApiContext,
|
|
): Promise<boolean> {
|
|
const host = ctx.codingProducts?.host;
|
|
const fixedGetRoutes = new Set([
|
|
'/api/coding/files/status',
|
|
'/api/coding/files/find',
|
|
'/api/coding/files/content',
|
|
'/api/coding/search',
|
|
'/api/coding/skills',
|
|
]);
|
|
const commandMatch = url.pathname.match(/^\/api\/coding\/conversations\/([^/]+)\/commands$/);
|
|
const changesMatch = url.pathname.match(/^\/api\/coding\/conversations\/([^/]+)\/changes$/);
|
|
if (!fixedGetRoutes.has(url.pathname) && !commandMatch && !changesMatch) return false;
|
|
if (req.method !== 'GET') return false;
|
|
if (!host) {
|
|
unavailable(res);
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
if (url.pathname === '/api/coding/files/status') {
|
|
sendJson(res, 200, { files: await host.fileStatus() });
|
|
return true;
|
|
}
|
|
if (url.pathname === '/api/coding/files/find') {
|
|
const query = url.searchParams.get('query')?.trim() ?? '';
|
|
const rawLimit = url.searchParams.get('limit');
|
|
const parsedLimit = rawLimit ? Number(rawLimit) : undefined;
|
|
sendJson(res, 200, {
|
|
files: await host.findFiles(
|
|
query,
|
|
parsedLimit !== undefined && Number.isSafeInteger(parsedLimit) && parsedLimit > 0
|
|
? parsedLimit
|
|
: undefined,
|
|
),
|
|
});
|
|
return true;
|
|
}
|
|
if (url.pathname === '/api/coding/files/content') {
|
|
sendJson(res, 200, {
|
|
file: await host.fileContent(url.searchParams.get('path')?.trim() ?? ''),
|
|
});
|
|
return true;
|
|
}
|
|
if (url.pathname === '/api/coding/search') {
|
|
sendJson(res, 200, {
|
|
matches: await host.searchText(url.searchParams.get('pattern')?.trim() ?? ''),
|
|
});
|
|
return true;
|
|
}
|
|
if (url.pathname === '/api/coding/skills') {
|
|
sendJson(res, 200, {
|
|
skills: await host.listSkills(url.searchParams.get('agentId')?.trim() || undefined),
|
|
});
|
|
return true;
|
|
}
|
|
if (commandMatch) {
|
|
sendJson(res, 200, {
|
|
commands: await host.listCommands(decodeURIComponent(commandMatch[1])),
|
|
});
|
|
return true;
|
|
}
|
|
if (changesMatch) {
|
|
sendJson(res, 200, {
|
|
changes: await host.getChanges(decodeURIComponent(changesMatch[1])),
|
|
});
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
serviceError(res, error);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|