import { app, BrowserWindow, ipcMain } from 'electron'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; import http from 'http'; function parseArgs() { const args = { docs: null, headless: false, port: null }; const docsIndex = process.argv.indexOf('--docs'); if (docsIndex !== -1 && process.argv[docsIndex + 1]) { args.docs = process.argv[docsIndex + 1]; } if (process.argv.includes('--headless')) { args.headless = true; } const portIndex = process.argv.indexOf('--port'); if (portIndex !== -1 && process.argv[portIndex + 1]) { args.port = parseInt(process.argv[portIndex + 1], 10); } return args; } const argsGlobal = parseArgs(); if (argsGlobal.headless) { app.commandLine.appendSwitch('disable-gpu'); app.commandLine.appendSwitch('disable-software-rasterizer'); app.commandLine.appendSwitch('no-sandbox'); app.disableHardwareAcceleration(); } app.commandLine.appendSwitch('disable-gpu-shader-disk-cache'); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let mainWindow = null; let httpServer = null; const LOG_FILE = app.isPackaged ? path.join(app.getPath('userData'), 'electron.log') : path.join(__dirname, '..', 'electron.log'); function log(...args) { const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '); const ts = new Date().toISOString(); const line = `[${ts}] [main] ${msg}\n`; fs.appendFileSync(LOG_FILE, line); console.log(line.trim()); } process.stdout.write(`[${new Date().toISOString()}] [main] XCSDD starting in ${argsGlobal.headless ? 'HEADLESS' : 'normal'} mode\n`); process.stdout.write(`[${new Date().toISOString()}] [main] Args: docs=${argsGlobal.docs}, headless=${argsGlobal.headless}, port=${argsGlobal.port}\n`); function getIndexPath() { if (app.isPackaged) { return path.join(__dirname, '..', 'dist', 'index.html'); } return path.join(__dirname, '..', 'dist', 'index.html'); } function startHttpServer(port) { const distPath = app.isPackaged ? path.join(__dirname, '..', 'dist') : path.join(__dirname, '..', 'dist'); const mimeTypes = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', }; const server = http.createServer((req, res) => { let filePath = path.join(distPath, req.url === '/' ? 'index.html' : req.url); const ext = path.extname(filePath); const contentType = mimeTypes[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, content) => { if (err) { if (err.code === 'ENOENT') { fs.readFile(path.join(distPath, 'index.html'), (err2, content2) => { if (err2) { res.writeHead(404); res.end('Not Found'); } else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(content2); } }); } else { res.writeHead(500); res.end('Server Error'); } } else { res.writeHead(200, { 'Content-Type': contentType }); res.end(content); } }); }); server.listen(port, '0.0.0.0', () => { log(`HTTP server running at http://localhost:${port}/`); }); return server; } function createWindow() { const indexPath = getIndexPath(); log('Creating window, indexPath:', indexPath); log('__dirname:', __dirname); log('dist exists:', fs.existsSync(path.join(__dirname, '..', 'dist'))); log('index.html exists:', fs.existsSync(indexPath)); mainWindow = new BrowserWindow({ show: false, width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, devTools: !app.isPackaged, }, }); if (argsGlobal.port) { mainWindow.loadURL(`http://localhost:${argsGlobal.port}/`); } else { mainWindow.loadFile(indexPath); } mainWindow.on('ready-to-show', () => { if (!argsGlobal.headless) { mainWindow.show(); } }); mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDesc) => { log('FAIL LOAD:', errorCode, errorDesc); }); mainWindow.webContents.on('did-finish-load', () => { log('Page finished loading'); }); if (!app.isPackaged && !argsGlobal.headless) { mainWindow.webContents.openDevTools(); } mainWindow.on('closed', () => { mainWindow = null; }); } ipcMain.handle('list-docs-files', (_event, basePath) => { const docsPath = path.join(basePath, 'api'); if (!fs.existsSync(docsPath)) { return []; } const result = []; function walkDir(dir, baseDir) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkDir(fullPath, baseDir); } else if (entry.name.endsWith('.md')) { const relativePath = path.relative(baseDir, fullPath); result.push({ name: entry.name.replace('.md', ''), path: fullPath, relativePath: relativePath.replace(/\\/g, '/') }); } } } walkDir(docsPath, docsPath); return result; }); ipcMain.handle('read-doc-file', (_event, filePath) => { if (!fs.existsSync(filePath)) { return null; } return fs.readFileSync(filePath, 'utf-8'); }); ipcMain.on('renderer-log', (_event, level, ...args) => { log(`[renderer][${level}]`, ...args); }); app.whenReady().then(() => { log('App ready, parsing args:', argsGlobal); if (argsGlobal.port) { log('Starting HTTP server on port', argsGlobal.port); httpServer = startHttpServer(argsGlobal.port); } createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }).catch(err => { log('FATAL ERROR:', err); process.exit(1); }); app.on('window-all-closed', () => { if (httpServer) { httpServer.close(); } if (argsGlobal.headless) { log('Headless mode: window closed, keeping process alive for 5 seconds...'); setTimeout(() => { log('Headless mode: exiting now'); process.exit(0); }, 5000); } else if (process.platform !== 'darwin') { app.quit(); } }); process.on('uncaughtException', (err) => { log('Uncaught exception:', err); }); process.on('unhandledRejection', (reason) => { log('Unhandled rejection:', reason); });