Files
makelore/electron/api/routes/coding-files.ts

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;
}