import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { detectLanguage } from './lib/detector.js'; import { startProject, stopProject, getProjectStatus, listRunningProjects, stopAllProjects } from './lib/projectManager.js'; import { initStore, createProject, getProject, getAllProjects, updateProject, deleteProject, deleteProjectFiles, generateId } from './lib/projectsStore.js'; import { writeProjectFiles, detectLanguageFromFiles, scaffoldNodeJS, scaffoldPython } from './lib/projectScaffolder.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const projectLogs = new Map(); const projectsBasePath = join(__dirname, 'projects'); initStore(); app.use(cors()); app.use(express.json()); app.get('/', (req, res) => { const projects = listRunningProjects(); res.json({ message: 'Project Runner Server', version: '2.1.0', runningProjects: projects, endpoints: { 'GET /': 'Server info', 'GET /health': 'Server health status', 'GET /projects': 'List all projects in ./projects folder', 'GET /projects/:id': 'Start/access project by ID', 'GET /projects/:id/status': 'Get project status', 'GET /projects/:id/logs': 'Get project startup logs', 'POST /projects/:id/stop': 'Stop a running project', 'POST /projects/:id/restart': 'Restart a project', 'POST /projects/stop-all': 'Stop all running projects', 'POST /api/deploy': 'Deploy a new project (upload files)', 'GET /api/deployed': 'List all deployed projects', 'GET /api/deployed/:id': 'Get deployed project details', 'POST /api/deployed/:id/start': 'Start a deployed project', 'PUT /api/deployed/:id': 'Update a deployed project', 'DELETE /api/deployed/:id': 'Delete a deployed project' } }); }); app.get('/health', (req, res) => { const projects = listRunningProjects(); res.json({ status: 'ok', uptime: process.uptime(), memory: process.memoryUsage(), projects: { count: projects.length, running: projects } }); }); app.get('/projects', async (req, res) => { try { const { readdir } = await import('node:fs/promises'); const projectsDir = join(__dirname, 'projects'); try { const entries = await readdir(projectsDir, { withFileTypes: true }); const projects = []; for (const entry of entries) { if (entry.isDirectory()) { const status = getProjectStatus(entry.name); try { const langInfo = await detectLanguage(join(projectsDir, entry.name)); projects.push({ id: entry.name, isDirectory: true, status: status.status, language: langInfo ? langInfo.language : 'unknown', isStreamlit: langInfo?.isStreamlit || false, tunnelUrl: status.tunnelUrl || null, localUrl: status.localUrl || null, tunnelProvider: status.tunnelProvider || null }); } catch { projects.push({ id: entry.name, isDirectory: true, status: status.status, language: 'unknown', isStreamlit: false, tunnelUrl: null, localUrl: null, tunnelProvider: null }); } } } res.json({ projects }); } catch { res.status(404).json({ error: 'Projects directory not found' }); } } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/projects/:id', async (req, res) => { const projectId = req.params.id; const projectsDir = join(__dirname, 'projects'); const projectPath = join(projectsDir, projectId); const existingStatus = getProjectStatus(projectId); if (existingStatus.running) { return res.json({ success: true, projectId, status: 'running', port: existingStatus.port, tunnelUrl: existingStatus.tunnelUrl, localUrl: existingStatus.localUrl, tunnelProvider: existingStatus.tunnelProvider || null, message: 'Project already running', iframeUrl: existingStatus.tunnelUrl || existingStatus.localUrl }); } try { const langInfo = await detectLanguage(projectPath); if (!langInfo) { return res.status(400).json({ success: false, error: 'Could not detect project language', message: 'Make sure your project has a valid entry file (package.json, main.py, index.html, etc.)' }); } console.log(`[${projectId}] Detected ${langInfo.language} project (via ${langInfo.detectedFile})`); const result = await startProject(projectId, projectPath, langInfo.config, langInfo.isStreamlit); if (result.success) { res.json({ success: true, projectId, language: langInfo.language, isStreamlit: langInfo.isStreamlit || false, status: result.status, port: result.port, tunnelUrl: result.tunnelUrl, localUrl: result.localUrl, pid: result.pid, message: result.message, tunnelProvider: result.tunnelUrl?.includes('trycloudflare') ? 'cloudflare' : result.tunnelUrl?.includes('ngrok') ? 'ngrok' : null, iframeUrl: result.tunnelUrl || result.localUrl }); } else { res.status(500).json({ success: false, projectId, error: result.error, status: result.status, message: result.message }); } } catch (error) { console.error(`[${projectId}] Error:`, error); res.status(500).json({ success: false, projectId, error: error.message, status: 'error' }); } }); app.get('/projects/:id/status', (req, res) => { const projectId = req.params.id; const status = getProjectStatus(projectId); res.json({ projectId, ...status }); }); app.get('/projects/:id/logs', (req, res) => { const projectId = req.params.id; const logs = projectLogs.get(projectId) || []; res.json({ projectId, logs: logs, hasLogs: logs.length > 0 }); }); app.post('/projects/:id/stop', async (req, res) => { const projectId = req.params.id; try { const result = await stopProject(projectId); res.json({ projectId, ...result }); } catch (error) { res.status(500).json({ success: false, projectId, error: error.message }); } }); app.post('/projects/:id/restart', async (req, res) => { const projectId = req.params.id; const projectsDir = join(__dirname, 'projects'); const projectPath = join(projectsDir, projectId); try { await stopProject(projectId); await new Promise(resolve => setTimeout(resolve, 1000)); const langInfo = await detectLanguage(projectPath); if (!langInfo) { return res.status(400).json({ success: false, error: 'Could not detect project language' }); } const result = await startProject(projectId, projectPath, langInfo.config, langInfo.isStreamlit); res.json({ ...result, projectId, message: result.success ? `Project restarted. ${result.message}` : result.message }); } catch (error) { res.status(500).json({ success: false, projectId, error: error.message }); } }); app.post('/projects/stop-all', async (req, res) => { try { await stopAllProjects(); res.json({ success: true, message: 'All projects stopped' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.post('/api/deploy', async (req, res) => { try { const { name, files, language, config } = req.body; if (!files || typeof files !== 'object') { return res.status(400).json({ success: false, error: 'files object is required' }); } const projectId = generateId(); const detected = detectLanguageFromFiles(files); const lang = language || detected.language; let processedFiles = { ...files }; if (lang === 'nodejs') processedFiles = scaffoldNodeJS(processedFiles); if (lang === 'python' || lang === 'streamlit') processedFiles = scaffoldPython(processedFiles); const project = createProject({ name: name || `project-${projectId}`, language: lang, files: processedFiles, config: config || {}, metadata: { detectedLanguage: detected.language, isStreamlit: detected.isStreamlit } }); await writeProjectFiles(projectId, processedFiles, projectsBasePath); res.json({ success: true, projectId: project.id, name: project.name, language: project.language, message: 'Project deployed successfully' }); } catch (error) { console.error('Deploy error:', error); res.status(500).json({ success: false, error: error.message }); } }); app.get('/api/deployed', (req, res) => { const projects = getAllProjects(); const result = projects.map(p => ({ id: p.id, name: p.name, language: p.language, isStreamlit: p.metadata?.isStreamlit || false, createdAt: p.createdAt, updatedAt: p.updatedAt })); res.json({ projects: result }); }); app.get('/api/deployed/:id', (req, res) => { const project = getProject(req.params.id); if (!project) { return res.status(404).json({ success: false, error: 'Project not found' }); } const status = getProjectStatus(req.params.id); res.json({ success: true, project: { id: project.id, name: project.name, language: project.language, isStreamlit: project.metadata?.isStreamlit || false, files: Object.keys(project.files), createdAt: project.createdAt, updatedAt: project.updatedAt, status: status.status, tunnelUrl: status.tunnelUrl || null, localUrl: status.localUrl || null } }); }); app.post('/api/deployed/:id/start', async (req, res) => { const projectId = req.params.id; const project = getProject(projectId); if (!project) { return res.status(404).json({ success: false, error: 'Project not found' }); } const projectPath = join(projectsBasePath, projectId); const langInfo = await detectLanguage(projectPath); if (!langInfo) { return res.status(400).json({ success: false, error: 'Could not detect project language' }); } const result = await startProject(projectId, projectPath, langInfo.config, langInfo.isStreamlit); res.json(result); }); app.put('/api/deployed/:id', async (req, res) => { try { const projectId = req.params.id; const project = getProject(projectId); if (!project) { return res.status(404).json({ success: false, error: 'Project not found' }); } const { files, name, config } = req.body; if (files) { let processedFiles = { ...files }; const lang = req.body.language || project.language; if (lang === 'nodejs') processedFiles = scaffoldNodeJS(processedFiles); if (lang === 'python' || lang === 'streamlit') processedFiles = scaffoldPython(processedFiles); await writeProjectFiles(projectId, processedFiles, projectsBasePath); project.files = processedFiles; } if (name) project.name = name; if (config) project.config = { ...project.config, ...config }; updateProject(projectId, project); res.json({ success: true, projectId, message: 'Project updated successfully' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.delete('/api/deployed/:id', async (req, res) => { const projectId = req.params.id; const project = getProject(projectId); if (!project) { return res.status(404).json({ success: false, error: 'Project not found' }); } await stopProject(projectId); await deleteProjectFiles(projectId); deleteProject(projectId); res.json({ success: true, projectId, message: 'Project deleted successfully' }); }); const PORT = process.env.PORT || 3000; const server = app.listen(PORT, () => { console.log(`Project Runner Server running on port ${PORT}`); console.log(`Access at: http://localhost:${PORT}`); console.log(`Projects directory: ${join(__dirname, 'projects')}`); }); process.on('SIGTERM', async () => { console.log('Shutting down... Stopping all projects...'); await stopAllProjects(); server.close(() => { console.log('Server closed'); process.exit(0); }); }); process.on('SIGINT', async () => { console.log('Shutting down... Stopping all projects...'); await stopAllProjects(); server.close(() => { console.log('Server closed'); process.exit(0); }); }); export default app;