src / utils / fs.ts
import fs from "node:fs/promises";
import path from "node:path";
import { ensureDirExists, isHidden, resolveInside } from "./path";
const IGNORE_DIRS = new Set(["node_modules", "dist", ".git", ".lmstudio", "temp", "tmp"]);
export async function readTextFileSafe(baseDir: string, filePath: string, maxBytes: number): Promise<string> {
const full = resolveInside(baseDir, filePath);
const stat = await fs.stat(full);
if (!stat.isFile()) {
throw new Error("Not a file");
}
if (stat.size > maxBytes) {
throw new Error(`File too large (${stat.size} bytes > ${maxBytes})`);
}
return fs.readFile(full, "utf8");
}
export async function writeTextFileSafe(baseDir: string, filePath: string, content: string, maxBytes: number): Promise<string> {
const full = resolveInside(baseDir, filePath);
const bytes = Buffer.byteLength(content, "utf8");
if (bytes > maxBytes) {
throw new Error(`Content too large (${bytes} bytes > ${maxBytes})`);
}
ensureDirExists(path.dirname(full));
await fs.writeFile(full, content, "utf8");
return full;
}
export async function deletePathSafe(baseDir: string, filePath: string): Promise<string> {
const full = resolveInside(baseDir, filePath);
await fs.rm(full, { recursive: true, force: true });
return full;
}
export async function listTree(baseDir: string, subPath = ".", depth = 3): Promise<string[]> {
const start = resolveInside(baseDir, subPath);
const out: string[] = [];
async function walk(dir: string, prefix = "", currentDepth = 0): Promise<void> {
if (currentDepth > depth) return;
const entries = await fs.readdir(dir, { withFileTypes: true });
entries.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of entries) {
if (IGNORE_DIRS.has(entry.name)) continue;
if (entry.name !== "." && isHidden(entry.name)) continue;
const rel = path.relative(baseDir, path.join(dir, entry.name));
out.push(`${prefix}${entry.isDirectory() ? "📁" : "📄"} ${rel}`);
if (entry.isDirectory()) {
await walk(path.join(dir, entry.name), `${prefix} `, currentDepth + 1);
}
}
}
const stat = await fs.stat(start);
if (stat.isDirectory()) {
await walk(start);
} else {
out.push(`📄 ${path.relative(baseDir, start)}`);
}
return out;
}
export async function exists(baseDir: string, filePath: string): Promise<boolean> {
try {
await fs.stat(resolveInside(baseDir, filePath));
return true;
} catch {
return false;
}
}
export async function ensureProjectRoot(baseDir: string): Promise<void> {
ensureDirExists(baseDir);
}