batch3 T4: file_ops + file_index services; UI endpoints; tools refactor
- services/file_ops.ts: shared listDir/viewFile/grep/findFiles core - services/file_index.ts: per-project flat file list cached on mtime of project root + .git/HEAD + .git/index (rg --files honors .gitignore) - services/tools.ts: tools delegate to file_ops, output format unchanged - routes/projects.ts: GET /list_dir, /view_file, /files endpoints - web client: api.projects.listDir/viewFile/files + mirrored types Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@ import type { Config } from '../config.js';
|
||||
import type { Broker } from '../services/broker.js';
|
||||
import type { Project, AvailableProject } from '../types/api.js';
|
||||
import { requireUser } from '../auth.js';
|
||||
import { resolveProjectRoot, PathScopeError } from '../services/path_guard.js';
|
||||
import { listDir, viewFile } from '../services/file_ops.js';
|
||||
import { getProjectFiles } from '../services/file_index.js';
|
||||
|
||||
const AddProjectBody = z.object({
|
||||
path: z.string().min(1),
|
||||
@@ -132,4 +135,125 @@ export function registerProjectRoutes(
|
||||
out.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return out;
|
||||
});
|
||||
|
||||
// GET /api/projects/:id/list_dir?path=<relpath>
|
||||
app.get<{ Params: { id: string }; Querystring: { path?: string } }>(
|
||||
'/api/projects/:id/list_dir',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params;
|
||||
const relPath = req.query.path ?? '.';
|
||||
|
||||
const rows = await sql<Project[]>`
|
||||
SELECT id, name, path, added_at, last_session_id
|
||||
FROM projects WHERE id = ${id}
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'not found' };
|
||||
}
|
||||
const project = rows[0]!;
|
||||
let projectRoot: string;
|
||||
try {
|
||||
projectRoot = await resolveProjectRoot(project.path);
|
||||
} catch (err) {
|
||||
if (err instanceof PathScopeError) {
|
||||
reply.code(404);
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listDir(projectRoot, relPath);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof PathScopeError) {
|
||||
reply.code(400);
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/projects/:id/view_file?path=<relpath>
|
||||
app.get<{ Params: { id: string }; Querystring: { path?: string } }>(
|
||||
'/api/projects/:id/view_file',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params;
|
||||
const relPath = req.query.path;
|
||||
|
||||
if (!relPath) {
|
||||
reply.code(400);
|
||||
return { error: 'path is required' };
|
||||
}
|
||||
|
||||
const rows = await sql<Project[]>`
|
||||
SELECT id, name, path, added_at, last_session_id
|
||||
FROM projects WHERE id = ${id}
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'not found' };
|
||||
}
|
||||
const project = rows[0]!;
|
||||
let projectRoot: string;
|
||||
try {
|
||||
projectRoot = await resolveProjectRoot(project.path);
|
||||
} catch (err) {
|
||||
if (err instanceof PathScopeError) {
|
||||
reply.code(404);
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await viewFile(projectRoot, relPath);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof PathScopeError) {
|
||||
reply.code(400);
|
||||
return { error: err.message };
|
||||
}
|
||||
// File not found (pathGuard throws PathScopeError for non-existent paths)
|
||||
if (err instanceof Error && err.message.includes('does not exist')) {
|
||||
reply.code(404);
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/projects/:id/files
|
||||
app.get<{ Params: { id: string } }>(
|
||||
'/api/projects/:id/files',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const rows = await sql<Project[]>`
|
||||
SELECT id, name, path, added_at, last_session_id
|
||||
FROM projects WHERE id = ${id}
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'not found' };
|
||||
}
|
||||
const project = rows[0]!;
|
||||
let projectRoot: string;
|
||||
try {
|
||||
projectRoot = await resolveProjectRoot(project.path);
|
||||
} catch (err) {
|
||||
if (err instanceof PathScopeError) {
|
||||
reply.code(404);
|
||||
return { error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const files = await getProjectFiles(id, projectRoot);
|
||||
return { files };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user