import fs from "node:fs/promises"; import { createReadStream } from "node:fs"; import path from "node:path"; import { glob } from "glob"; /** * Reads the first line of a file efficiently using Node.js readline. * * @param {string} filePath - The path to the file. * @return {Promise} - The first non-empty line of the file, or an empty string otherwise. */ export async function readFirstLine(filePath: string): Promise { const { createInterface } = await import("node:readline"); const fileStream = createReadStream(filePath); const rl = createInterface({ input: fileStream, crlfDelay: Infinity, // Handle \r\n properly }); try { for await (const line of rl) { const trimmedLine = line.trim(); if (trimmedLine.length > 0) { return trimmedLine; } } return ""; } catch { // If reading fails (e.g., file doesn't exist), return empty string return ""; } finally { rl.close(); fileStream.destroy(); } } /** * Reads up to N lines from a file using Node.js readline. * * @param {string} filePath - The path to the file. * @param {number} maxLines - Maximum number of non-empty lines to read. * @return {Promise} - Array of non-empty lines (up to maxLines). */ export async function readFirstNLines( filePath: string, maxLines: number, ): Promise { const { createInterface } = await import("node:readline"); const fileStream = createReadStream(filePath); const rl = createInterface({ input: fileStream, crlfDelay: Infinity, }); const lines: string[] = []; try { for await (const line of rl) { const trimmedLine = line.trim(); if (trimmedLine.length > 0) { lines.push(trimmedLine); if (lines.length >= maxLines) { break; } } } return lines; } catch { return lines; } finally { rl.close(); fileStream.destroy(); } } /** * Reads a file from the end and returns the last non-empty line. * * This version supports files that end with: * - "\n" (Unix-style, including modern macOS) * - "\r\n" (Windows-style) * - "\r" (older Mac-style, HL7, etc.) * * @param {string} filePath - The path to the file. * @param {number} [minLength=1] - Minimum length for the returned line. * @return {Promise} - The last non-empty line of the file, or an empty string if no non-empty lines found. */ export async function getLastLine( filePath: string, minLength = 1, ): Promise { let fileHandle; try { const stats = await fs.stat(filePath); const fileSize = stats.size; if (fileSize === 0) return ""; fileHandle = await fs.open(filePath, "r"); const bufferSize = 8 * 1024; // 8KB buffer is usually enough for the last line const buffer = Buffer.alloc(bufferSize); let lineEnd: number | null = null; let lineStart: number | null = null; let currentPosition = fileSize; while (currentPosition > 0 && lineStart === null) { const readSize = Math.min(bufferSize, currentPosition); currentPosition -= readSize; const { bytesRead } = await fileHandle.read( buffer, 0, readSize, currentPosition, ); for (let i = bytesRead - 1; i >= 0; i--) { const charCode = buffer[i]; if (lineEnd === null) { // Still looking for the end of the last non-empty line (skip trailing newlines and whitespace) if (charCode > 32) { lineEnd = currentPosition + i + 1; } } else { // Looking for the start of the line (the newline before it) if (charCode === 10 || charCode === 13) { lineStart = currentPosition + i + 1; break; } } } } if (lineEnd === null) return ""; if (lineStart === null) lineStart = 0; const length = lineEnd - lineStart; if (length < minLength) return ""; const resultBuffer = Buffer.alloc(length); await fileHandle.read(resultBuffer, 0, length, lineStart); const result = resultBuffer.toString("utf8").trim(); return result.length >= minLength ? result : ""; } catch { // If reading fails (e.g., file doesn't exist), return empty string return ""; } finally { if (fileHandle) { await fileHandle.close(); } } } /** * Simple Levenshtein distance implementation */ function levenshtein(a: string, b: string): number { const matrix = Array.from({ length: a.length + 1 }, () => Array.from({ length: b.length + 1 }, () => 0), ); for (let i = 0; i <= a.length; i++) matrix[i][0] = i; for (let j = 0; j <= b.length; j++) matrix[0][j] = j; for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; matrix[i][j] = Math.min( matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost, ); } } return matrix[a.length][b.length]; } /** * Suggests similar paths if a file is not found. */ export async function suggestPathUnderCwd( targetPath: string, workdir: string, ): Promise { try { const allFiles = await glob("**/*", { cwd: workdir, nodir: true, dot: true, ignore: ["**/.git/**", "**/node_modules/**"], }); const targetBasename = path.basename(targetPath); const suggestions = allFiles .map((file) => ({ path: file, distance: levenshtein(targetBasename, path.basename(file)), })) .filter((item) => item.distance <= 3) // Threshold for similarity .sort((a, b) => a.distance - b.distance) .slice(0, 5) .map((item) => item.path); return suggestions; } catch { return []; } } /** * Uses fuzzy matching to find the intended file. */ export async function findSimilarFile( targetPath: string, workdir: string, ): Promise { const suggestions = await suggestPathUnderCwd(targetPath, workdir); return suggestions.length > 0 ? suggestions[0] : null; }