Spaces:
Sleeping
Sleeping
| 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<ProjectInfo[]> { | |
| 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<string | null> { | |
| 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<string> { | |
| 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<ProjectContent> { | |
| 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<string> { | |
| // 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/.` | |
| ); | |
| } | |