src / list-files.ts
import type { Options } from "globby";
import * as os from "os";
import * as path from "path";
function normalizePath(p: string): string {
let normalized = path.normalize(p);
if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
export function arePathsEqual(path1?: string, path2?: string): boolean {
if (!path1 && !path2) return true;
if (!path1 || !path2) return false;
path1 = normalizePath(path1);
path2 = normalizePath(path2);
return process.platform === "win32" ? path1.toLowerCase() === path2.toLowerCase() : path1 === path2;
}
const DEFAULT_IGNORE_DIRECTORIES = ["node_modules", "__pycache__", "dist", "build", ".git", ".idea", ".vscode"];
function isRestrictedPath(absolutePath: string): boolean {
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/";
if (arePathsEqual(absolutePath, root)) return true;
if (arePathsEqual(absolutePath, os.homedir())) return true;
return false;
}
function buildIgnorePatterns(absolutePath: string): string[] {
const patterns = [...DEFAULT_IGNORE_DIRECTORIES];
if (!path.basename(absolutePath).startsWith(".")) patterns.push(".*");
return patterns.map((dir) => `**/${dir}/**`);
}
export async function listFiles(
dirPath: string,
recursive: boolean,
limit: number
): Promise<[string[], boolean]> {
try {
const module = await import("globby");
const globby = module.globby;
const absolutePath = path.resolve(dirPath);
if (isRestrictedPath(absolutePath)) return [[], false];
const options: Options = {
cwd: dirPath,
dot: true,
absolute: true,
markDirectories: true,
gitignore: recursive,
ignore: recursive ? buildIgnorePatterns(absolutePath) : undefined,
onlyFiles: false,
suppressErrors: true,
};
const filePaths = recursive
? await globbyLevelByLevel(limit, options)
: (await globby("*", options)).slice(0, limit);
return [filePaths, filePaths.length >= limit];
} catch (e) {
throw new Error("Dependency 'globby' not found. Please run 'npm install globby' in the plugin directory.");
}
}
async function globbyLevelByLevel(limit: number, options?: Options) {
const { globby } = await import("globby");
const results: Set<string> = new Set();
const queue: string[] = ["*"];
const globbingProcess = async () => {
while (queue.length > 0 && results.size < limit) {
const pattern = queue.shift()!;
const filesAtLevel = await globby(pattern, options);
for (const file of filesAtLevel) {
if (results.size >= limit) break;
results.add(file);
if (file.endsWith("/")) {
const escapedFile = file.replace(/\(/g, "\\(").replace(/\)/g, "\\)");
queue.push(`${escapedFile}*`);
}
}
}
return Array.from(results).slice(0, limit);
};
const timeoutPromise = new Promise<string[]>((_, reject) => {
setTimeout(() => reject(new Error("Globbing timeout")), 10_000);
});
try {
return await Promise.race([globbingProcess(), timeoutPromise]);
} catch (_error) {
return Array.from(results);
}
}
export function formatFilesList(absolutePath: string, files: string[], didHitLimit: boolean): string {
const sorted = files.map((file) => {
const relativePath = path.relative(absolutePath, file).replace(/\\/g, "/");
return file.endsWith("/") ? relativePath + "/" : relativePath;
}).sort(); // Simplified sort
if (didHitLimit) {
return `${sorted.join("\n")}\n\n(File list truncated...)`;
} else if (sorted.length === 0) {
return "No files found.";
} else {
return sorted.join("\n");
}
}