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