import { spawn, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const appRoot = path.resolve(__dirname, '..'); const toolRoot = path.join(appRoot, 'tools', 'epub-review-editor'); const reviewRoot = path.resolve(process.env.READEST_REVIEW_ROOT || path.join(appRoot, 'epub_review_sessions')); const defaultPort = Number(process.env.READEST_REVIEW_PORT || 5177); const venvRoot = path.join(toolRoot, '.venv'); const venvPython = process.platform === 'win32' ? path.join(venvRoot, 'Scripts', 'python.exe') : path.join(venvRoot, 'bin', 'python'); const run = (command, args, options = {}) => spawnSync(command, args, { cwd: options.cwd, encoding: 'utf8', timeout: options.timeout ?? 30_000, windowsHide: true, }); const pythonCandidates = process.platform === 'win32' ? [ ['py', ['-3']], ['python', []], ['python3', []], ] : [ ['python3', []], ['python', []], ]; const findPython = () => { for (const [command, args] of pythonCandidates) { const result = run(command, [...args, '--version'], { timeout: 10_000 }); if (result.status === 0) return { command, args }; } throw new Error('Python 3 was not found.'); }; const requestJson = (port, pathname) => new Promise((resolve) => { const req = http.get({ host: '127.0.0.1', port, path: pathname, timeout: 800 }, (res) => { let body = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { resolve(res.statusCode === 200 ? JSON.parse(body) : null); } catch { resolve(null); } }); }); req.on('timeout', () => { req.destroy(); resolve(null); }); req.on('error', () => resolve(null)); }); const findRunning = async () => { for (let port = defaultPort; port < defaultPort + 100; port++) { const payload = await requestJson(port, '/api/version'); if (payload?.version) return `http://localhost:${port}`; } return ''; }; if (!fs.existsSync(path.join(toolRoot, 'server.py'))) { throw new Error(`Missing review editor server: ${path.join(toolRoot, 'server.py')}`); } const existing = await findRunning(); if (existing) { console.log(`EPUB review editor is already running: ${existing}`); process.exit(0); } if (!fs.existsSync(venvPython)) { console.log('Creating review editor Python environment...'); const python = findPython(); const result = run(python.command, [...python.args, '-m', 'venv', venvRoot], { cwd: appRoot, timeout: 120_000, }); if (result.status !== 0) { throw new Error(result.stderr || result.stdout || 'Failed to create virtual environment.'); } } const flaskCheck = run( venvPython, ['-c', 'import importlib.util, sys; sys.exit(0 if importlib.util.find_spec("flask") else 1)'], { cwd: toolRoot, timeout: 30_000 }, ); if (flaskCheck.status !== 0) { console.log('Installing review editor dependencies...'); const install = run(venvPython, ['-m', 'pip', 'install', '-r', path.join(toolRoot, 'requirements.txt')], { cwd: toolRoot, timeout: 180_000, }); if (install.status !== 0) { throw new Error(install.stderr || install.stdout || 'Failed to install dependencies.'); } } fs.mkdirSync(reviewRoot, { recursive: true }); const child = spawn( venvPython, [ path.join(toolRoot, 'server.py'), '--review-root', reviewRoot, '--host', '127.0.0.1', '--port', String(defaultPort), '--no-browser', ], { cwd: toolRoot, stdio: 'inherit', windowsHide: true, }, ); child.on('exit', (code) => { process.exit(code ?? 0); });