import * as fs from 'fs/promises'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ─── Project Directory ───────────────────────────────────────── export function getTempProjectsDir(): string { return path.join(__dirname, '..', 'temp_projects'); } // ─── List all compiled projects with metadata ────────────────── export interface ProjectInfo { name: string; path: string; createdAt: string; hasPdf: boolean; pdfSizeBytes: number; sections: string[]; } export async function listProjects(): Promise { const baseDir = getTempProjectsDir(); const projects: ProjectInfo[] = []; try { const entries = await fs.readdir(baseDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const projectPath = path.join(baseDir, entry.name); const info: ProjectInfo = { name: entry.name, path: projectPath, createdAt: '', hasPdf: false, pdfSizeBytes: 0, sections: [], }; // Check for PDF in output/ const pdfPath = path.join(projectPath, 'output', 'main.pdf'); try { const pdfStat = await fs.stat(pdfPath); info.hasPdf = true; info.pdfSizeBytes = pdfStat.size; info.createdAt = pdfStat.mtime.toISOString(); } catch { // No PDF found try { const dirStat = await fs.stat(projectPath); info.createdAt = dirStat.mtime.toISOString(); } catch {} } // List sections const seccionesDir = path.join(projectPath, 'secciones'); try { const secFiles = await fs.readdir(seccionesDir); info.sections = secFiles.filter(f => f.endsWith('.tex')); } catch { // No secciones dir } projects.push(info); } // Sort by creation date, newest first projects.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); return projects; } catch { return []; } } // ─── Get latest version of a project by base name ────────────── export async function getLatestVersion(projectBaseName: string): Promise { const projects = await listProjects(); const matching = projects.filter(p => p.name.startsWith(projectBaseName)); if (matching.length === 0) return null; // Already sorted newest first return matching[0].path; } // ─── Create versioned project name ───────────────────────────── export function createVersionedName( baseName: string, version: number, operation: string, section?: string ): string { // Strip any existing version suffix starting from _v... to prevent recursive growth // If the user named their project "tesis_v2", it will be safely stripped to "tesis" const vMatch = baseName.match(/_v\d+_/); const rootName = vMatch ? baseName.substring(0, vMatch.index) : baseName; let sanitized = rootName.replace(/[^a-zA-Z0-9_-]/g, '_'); // Enforce a hard limit on the root name length to prevent Windows MAX_PATH issues if (sanitized.length > 50) sanitized = sanitized.substring(0, 50); // Shorten operation names (e.g., "patch_section" -> "patch", "restructure" -> "restructur") const op = operation.split('_')[0].substring(0, 10).replace(/[^a-zA-Z0-9_-]/g, '_'); // Shorten section filename const sec = section ? `_${section.replace(/\.tex$/, '').substring(0, 15).replace(/[^a-zA-Z0-9_-]/g, '_')}` : ''; // Use base36 for a shorter timestamp string (e.g. 'lwqxvz') const shortTime = Math.floor(Date.now() / 1000).toString(36); return `${sanitized}_v${version}_${op}${sec}_${shortTime}`; } // ─── Copy a project for creating a new version ───────────────── export async function copyProjectForVersion( sourceDir: string, newName: string ): Promise { const destDir = path.join(getTempProjectsDir(), newName); await fs.mkdir(destDir, { recursive: true }); // Copy main.tex try { await fs.copyFile( path.join(sourceDir, 'main.tex'), path.join(destDir, 'main.tex') ); } catch {} // Copy referencias.bib try { await fs.copyFile( path.join(sourceDir, 'referencias.bib'), path.join(destDir, 'referencias.bib') ); } catch {} // Copy secciones/ const srcSecciones = path.join(sourceDir, 'secciones'); const destSecciones = path.join(destDir, 'secciones'); try { const files = await fs.readdir(srcSecciones); await fs.mkdir(destSecciones, { recursive: true }); for (const file of files) { await fs.copyFile( path.join(srcSecciones, file), path.join(destSecciones, file) ); } } catch {} return destDir; } // ─── Read all files from an existing project ─────────────────── export interface ProjectContent { mainTex: string; referencesBib: string; sections: { filename: string; content: string }[]; projectPath: string; } export async function readProject(projectDir: string): Promise { const result: ProjectContent = { mainTex: '', referencesBib: '', sections: [], projectPath: projectDir, }; // Read main.tex try { result.mainTex = await fs.readFile(path.join(projectDir, 'main.tex'), 'utf8'); } catch { throw new Error(`main.tex not found in ${projectDir}`); } // Read referencias.bib try { result.referencesBib = await fs.readFile(path.join(projectDir, 'referencias.bib'), 'utf8'); } catch { // Optional — may not exist } // Read all sections const seccionesDir = path.join(projectDir, 'secciones'); try { const files = await fs.readdir(seccionesDir); for (const file of files) { if (file.endsWith('.tex')) { const content = await fs.readFile(path.join(seccionesDir, file), 'utf8'); result.sections.push({ filename: file, content }); } } // Sort alphabetically for consistency result.sections.sort((a, b) => a.filename.localeCompare(b.filename)); } catch { // No sections directory } return result; } // ─── Generate standalone .tex from section content ───────────── export function generateStandaloneTex(sectionContent: string, referencesBib?: string): string { const hasBib = referencesBib && referencesBib.trim().length > 0; const bibBlock = hasBib ? `\\usepackage[backend=biber,style=apa]{biblatex} \\addbibresource{referencias.bib}` : ''; const printBib = hasBib ? '\n\\printbibliography' : ''; return `\\documentclass[12pt]{article} \\usepackage[utf8]{inputenc} \\usepackage[T1]{fontenc} \\usepackage[english,spanish]{babel} \\usepackage{booktabs,adjustbox,threeparttable,multirow} \\usepackage{times} \\usepackage{geometry} \\geometry{a4paper, margin=2.5cm} \\usepackage{setspace} \\onehalfspacing ${bibBlock} \\begin{document} ${sectionContent} ${printBib} \\end{document} `; } // ─── Resolve a project directory (supports name or full path) ── export async function resolveProjectDir(projectDirOrName: string): Promise { // If it's already an absolute path and exists, use it directly if (path.isAbsolute(projectDirOrName)) { try { await fs.access(projectDirOrName); return projectDirOrName; } catch {} } // Try as a project name inside temp_projects const inTempProjects = path.join(getTempProjectsDir(), projectDirOrName); try { await fs.access(inTempProjects); return inTempProjects; } catch {} throw new Error( `Project not found: "${projectDirOrName}". Provide an absolute path or the name of a project in temp_projects/.` ); }