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:
49
apps/server/src/services/file_index.ts
Normal file
49
apps/server/src/services/file_index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
|
||||
interface MtimeSnap {
|
||||
root: number;
|
||||
gitHead: number | null;
|
||||
gitIndex: number | null;
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
files: string[];
|
||||
mtimes: MtimeSnap;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CacheEntry>(); // keyed by projectId
|
||||
|
||||
export async function getProjectFiles(projectId: string, projectRoot: string): Promise<string[]> {
|
||||
const current = await snapMtimes(projectRoot);
|
||||
const cached = cache.get(projectId);
|
||||
if (cached && eqMtimes(cached.mtimes, current)) {
|
||||
return cached.files;
|
||||
}
|
||||
const files = await runRgFiles(projectRoot);
|
||||
cache.set(projectId, { files, mtimes: current });
|
||||
return files;
|
||||
}
|
||||
|
||||
async function snapMtimes(root: string): Promise<MtimeSnap> {
|
||||
const rootStat = await fs.stat(root);
|
||||
let gitHead: number | null = null;
|
||||
let gitIndex: number | null = null;
|
||||
try { gitHead = (await fs.stat(path.join(root, '.git', 'HEAD'))).mtimeMs; } catch {}
|
||||
try { gitIndex = (await fs.stat(path.join(root, '.git', 'index'))).mtimeMs; } catch {}
|
||||
return { root: rootStat.mtimeMs, gitHead, gitIndex };
|
||||
}
|
||||
|
||||
function eqMtimes(a: MtimeSnap, b: MtimeSnap): boolean {
|
||||
return a.root === b.root && a.gitHead === b.gitHead && a.gitIndex === b.gitIndex;
|
||||
}
|
||||
|
||||
function runRgFiles(root: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('rg', ['--files'], { cwd: root, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout.split('\n').filter(Boolean));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user