const fs = require('fs'); const express = require('express'); const path = require('path'); const { scanProjects } = require('./lib/scanner'); const pm = require('./lib/process-manager'); const { takeScreenshot } = require('./lib/screenshotter'); const centro = require('./lib/centrifugo'); const app = express(); const PORT = 3000; // Scan projects on startup let projects = scanProjects(); const projectsBySlug = new Map(projects.map(p => [p.slug, p])); console.log(`Found ${projects.length} projects:`); projects.forEach(p => { const fe = p.frontend ? ` + frontend(${p.frontend.dir} :${p.frontend.port} ${p.frontend.packageManager})` : ''; console.log(` ${p.slug} :${p.port} (${p.portSource || 'assigned'})${fe}`); }); // Publish status changes to Centrifugo pm.events.on('status', (slug, subtype, status) => { centro.publishStatus(slug, status, { subtype }); }); // Static files app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); // Serve project docs folders with directory listing app.use('/docs/:slug', (req, res, next) => { const project = projectsBySlug.get(req.params.slug); if (!project) return res.status(404).send('Project not found'); const docsRoot = path.join(project.path, 'docs'); const reqPath = decodeURIComponent(req.path).replace(/\.\./g, ''); const fullPath = path.join(docsRoot, reqPath); if (!fullPath.startsWith(docsRoot)) return res.status(403).send('Forbidden'); let stat; try { stat = fs.statSync(fullPath); } catch { return res.status(404).send('Not found'); } if (stat.isFile()) return res.sendFile(fullPath); // Directory listing const entries = fs.readdirSync(fullPath, { withFileTypes: true }); const basePath = `/docs/${project.slug}${reqPath}`.replace(/\/$/, ''); const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)); const files = entries.filter(e => e.isFile()).sort((a, b) => a.name.localeCompare(b.name)); const items = [ ...dirs.map(e => `
  • ${e.name}/
  • `), ...files.map(e => `
  • ${e.name}
  • `), ].join('\n'); res.send(` docs/${project.slug}${reqPath}

    docs/${project.slug}${reqPath}/

    ${reqPath !== '/' && reqPath !== '' ? '← back' : ''} `); }); // Centrifugo connection token app.get('/api/centrifugo/token', (req, res) => { const token = centro.createJWT('projects-browser'); res.json({ token }); }); // List all projects with live status app.get('/api/projects', (req, res) => { const statuses = pm.getAllStatuses(); const result = projects.map(p => { const s = statuses[p.slug]; const hasFrontend = !!p.frontend; return { ...p, status: s ? pm.combinedStatus(p.slug, hasFrontend) : 'stopped', apiStatus: s?.api?.status || 'stopped', frontendStatus: hasFrontend ? (s?.frontend?.status || 'stopped') : null, pid: s?.api?.pid || null, }; }); res.json(result); }); // Start a project app.post('/api/projects/:slug/start', (req, res) => { const project = projectsBySlug.get(req.params.slug); if (!project) return res.status(404).json({ error: 'Project not found' }); const result = pm.start(project.slug, project.path, project.port, project.frontend || null); res.json(result); }); // Stop a project app.post('/api/projects/:slug/stop', async (req, res) => { const project = projectsBySlug.get(req.params.slug); if (!project) return res.status(404).json({ error: 'Project not found' }); const result = await pm.stop(project.slug); res.json(result); }); // Screenshot a project app.post('/api/projects/:slug/screenshot', async (req, res) => { const project = projectsBySlug.get(req.params.slug); if (!project) return res.status(404).json({ error: 'Project not found' }); const s = pm.getStatus(project.slug); const hasFrontend = !!project.frontend; // For frontend projects, prefer frontend being up; otherwise check API if (hasFrontend) { if (s.frontend?.status !== 'running') { return res.status(400).json({ error: 'Frontend must be running to take screenshot' }); } } else { if (s.api.status !== 'running') { return res.status(400).json({ error: 'Project must be running to take screenshot' }); } } // Screenshot the frontend port if available, otherwise API port const screenshotPort = hasFrontend ? project.frontend.port : project.port; try { const imgPath = await takeScreenshot(project.slug, screenshotPort, hasFrontend); centro.publishScreenshot(project.slug); res.json({ screenshot: imgPath }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Screenshot all running projects app.post('/api/screenshot-all', async (req, res) => { const statuses = pm.getAllStatuses(); const results = {}; for (const p of projects) { const s = statuses[p.slug]; if (!s) continue; const hasFrontend = !!p.frontend; const combined = pm.combinedStatus(p.slug, hasFrontend); if (combined !== 'running') continue; const screenshotPort = hasFrontend ? p.frontend.port : p.port; try { results[p.slug] = await takeScreenshot(p.slug, screenshotPort, hasFrontend); centro.publishScreenshot(p.slug); } catch (err) { results[p.slug] = { error: err.message }; } } res.json(results); }); // Get logs for a project app.get('/api/projects/:slug/logs', (req, res) => { const project = projectsBySlug.get(req.params.slug); if (!project) return res.status(404).json({ error: 'Project not found' }); const s = pm.getStatus(project.slug); res.json({ api: s.api.logs, frontend: s.frontend?.logs || [], }); }); // Rescan projects app.post('/api/rescan', (req, res) => { projects = scanProjects(); projectsBySlug.clear(); projects.forEach(p => projectsBySlug.set(p.slug, p)); res.json({ count: projects.length }); }); // Graceful shutdown let shuttingDown = false; function shutdown() { if (shuttingDown) return; shuttingDown = true; console.log('\nShutting down, stopping all child processes...'); pm.killAllSync(); console.log('All processes killed.'); process.exit(0); } process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); app.listen(PORT, '0.0.0.0', () => { console.log(`Projects Browser running at http://0.0.0.0:${PORT}`); });