| import { spawn } from 'node:child_process'; |
| import portfinder from 'portfinder'; |
| import { join, dirname } from 'node:path'; |
| import { fileURLToPath } from 'node:url'; |
| import { writeFile, readFile } from 'node:fs/promises'; |
| import { createTunnel, destroyTunnel, getTunnelInfo } from './tunnelManager.js'; |
|
|
| const __filename = fileURLToPath(import.meta.url); |
| const __dirname = dirname(__filename); |
|
|
| const runningProjects = new Map(); |
|
|
| async function execAsync(command, options = {}) { |
| return new Promise((resolve, reject) => { |
| exec(command, options, (error, stdout, stderr) => { |
| if (error) reject(new Error(stderr || error.message)); |
| else resolve(stdout); |
| }); |
| }); |
| } |
|
|
| function exec(command, options = {}, callback) { |
| const child = spawn(command, { |
| shell: options.shell || '/bin/bash', |
| cwd: options.cwd || process.cwd(), |
| env: { ...process.env, ...options.env }, |
| stdio: ['ignore', 'pipe', 'pipe'] |
| }); |
| |
| let stdout = ''; |
| let stderr = ''; |
| |
| child.stdout.on('data', (data) => { |
| stdout += data.toString(); |
| }); |
| |
| child.stderr.on('data', (data) => { |
| stderr += data.toString(); |
| }); |
| |
| child.on('close', (code) => { |
| callback(null, stdout, stderr); |
| }); |
| |
| child.on('error', (error) => { |
| callback(error, stdout, stderr); |
| }); |
| |
| return child; |
| } |
|
|
| async function getAvailablePort(startPort = 3001) { |
| return new Promise((resolve, reject) => { |
| portfinder.getPort({ port: startPort, maxPort: 9999 }, (err, port) => { |
| if (err) reject(err); |
| else resolve(port); |
| }); |
| }); |
| } |
|
|
| async function getLockPath(projectPath) { |
| return join(projectPath, '.project-lock'); |
| } |
|
|
| async function hasInstallLock(projectPath, type = 'npm') { |
| const lockPath = await getLockPath(projectPath); |
| try { |
| const content = await readFile(lockPath, 'utf-8'); |
| const locks = JSON.parse(content); |
| return locks[type] === true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| async function setInstallLock(projectPath, type = 'npm') { |
| const lockPath = await getLockPath(projectPath); |
| let locks = {}; |
| try { |
| const content = await readFile(lockPath, 'utf-8'); |
| locks = JSON.parse(content); |
| } catch {} |
| locks[type] = true; |
| await writeFile(lockPath, JSON.stringify(locks, null, 2)); |
| } |
|
|
| export async function startProject(projectId, projectPath, languageConfig, isStreamlit = false) { |
| if (runningProjects.has(projectId)) { |
| const existing = runningProjects.get(projectId); |
| return { |
| success: true, |
| projectId, |
| port: existing.port, |
| tunnelUrl: existing.tunnelUrl, |
| status: 'running', |
| pid: existing.pid, |
| localUrl: `http://localhost:${existing.port}`, |
| message: 'Project already running' |
| }; |
| } |
| |
| const port = await getAvailablePort(); |
| |
| console.log(`[${projectId}] Setting up project environment...`); |
| |
| try { |
| await languageConfig.setup(projectPath, execAsync, isStreamlit); |
| console.log(`[${projectId}] Setup complete, starting project...`); |
| } catch (error) { |
| console.error(`[${projectId}] Setup failed:`, error.message); |
| return { |
| success: false, |
| projectId, |
| error: `Setup failed: ${error.message}`, |
| status: 'error', |
| message: 'Failed to setup project dependencies' |
| }; |
| } |
| |
| const startConfig = languageConfig.start(projectPath, port, isStreamlit); |
| |
| return new Promise((resolve) => { |
| let resolved = false; |
| let portDetected = false; |
| let startupOutput = ''; |
| let detectedPort = null; |
| |
| const child = spawn(startConfig.command, startConfig.args, { |
| cwd: startConfig.cwd, |
| env: startConfig.env, |
| shell: startConfig.shell || false |
| }); |
| |
| const cleanup = async () => { |
| if (!resolved) return; |
| await destroyTunnel(projectId); |
| }; |
| |
| child.stdout.on('data', async (data) => { |
| const output = data.toString(); |
| startupOutput += output; |
| console.log(`[${projectId}] ${output.trim()}`); |
| |
| if (!portDetected) { |
| const detected = languageConfig.detectPort(output); |
| if (detected) { |
| detectedPort = parseInt(detected); |
| portDetected = true; |
| await handlePortDetected(detectedPort); |
| } |
| } |
| }); |
| |
| child.stderr.on('data', async (data) => { |
| const output = data.toString(); |
| startupOutput += output; |
| console.error(`[${projectId}] ${output.trim()}`); |
| |
| if (!portDetected && !resolved) { |
| const detected = languageConfig.detectPort(output); |
| if (detected) { |
| detectedPort = parseInt(detected); |
| portDetected = true; |
| await handlePortDetected(detectedPort); |
| } |
| } |
| }); |
| |
| const handlePortDetected = async (finalPort) => { |
| if (resolved) return; |
| |
| const tunnelUrl = await createTunnel(finalPort, projectId); |
| |
| runningProjects.set(projectId, { |
| pid: child.pid, |
| port: finalPort, |
| tunnelUrl: tunnelUrl, |
| child: child, |
| projectPath: projectPath, |
| startupOutput: startupOutput |
| }); |
| |
| resolved = true; |
| resolve({ |
| success: true, |
| projectId, |
| port: finalPort, |
| tunnelUrl: tunnelUrl, |
| localUrl: `http://localhost:${finalPort}`, |
| status: 'running', |
| pid: child.pid, |
| message: tunnelUrl ? `Project started with public URL: ${tunnelUrl}` : 'Project started (tunnel unavailable)' |
| }); |
| }; |
| |
| child.on('error', (error) => { |
| console.error(`[${projectId}] Process error:`, error.message); |
| if (!resolved) { |
| resolved = true; |
| resolve({ |
| success: false, |
| projectId, |
| error: error.message, |
| status: 'error', |
| message: 'Failed to start project' |
| }); |
| } |
| }); |
| |
| child.on('close', async (code) => { |
| console.log(`[${projectId}] Process exited with code ${code}`); |
| |
| await destroyTunnel(projectId); |
| runningProjects.delete(projectId); |
| |
| if (!resolved) { |
| resolved = true; |
| resolve({ |
| success: false, |
| projectId, |
| error: `Process exited with code ${code}`, |
| status: 'stopped', |
| message: 'Project stopped (user can restart anytime)' |
| }); |
| } |
| }); |
| |
| setTimeout(async () => { |
| if (!resolved) { |
| const finalPort = detectedPort || port; |
| console.log(`[${projectId}] Port detection timeout, using port ${finalPort}`); |
| |
| const tunnelUrl = await createTunnel(finalPort, projectId); |
| |
| runningProjects.set(projectId, { |
| pid: child.pid, |
| port: finalPort, |
| tunnelUrl: tunnelUrl, |
| child: child, |
| projectPath: projectPath, |
| startupOutput: startupOutput |
| }); |
| |
| resolved = true; |
| resolve({ |
| success: true, |
| projectId, |
| port: finalPort, |
| tunnelUrl: tunnelUrl, |
| localUrl: `http://localhost:${finalPort}`, |
| status: 'running', |
| pid: child.pid, |
| message: tunnelUrl ? `Project started with public URL: ${tunnelUrl}` : 'Project started (tunnel unavailable)', |
| note: 'Port detected from default config' |
| }); |
| } |
| }, 20000); |
| }); |
| } |
|
|
| export async function stopProject(projectId) { |
| const project = runningProjects.get(projectId); |
| if (!project) { |
| return { success: true, message: 'Project not running' }; |
| } |
| |
| try { |
| await destroyTunnel(projectId); |
| |
| if (project.child) { |
| project.child.kill('SIGTERM'); |
| setTimeout(() => { |
| if (project.child && !project.child.killed) { |
| project.child.kill('SIGKILL'); |
| } |
| }, 5000); |
| } |
| |
| runningProjects.delete(projectId); |
| return { success: true, message: 'Project stopped (you can restart anytime)' }; |
| } catch (error) { |
| return { success: false, error: error.message }; |
| } |
| } |
|
|
| export function getProjectStatus(projectId) { |
| const project = runningProjects.get(projectId); |
| if (!project) { |
| return { status: 'stopped', running: false }; |
| } |
| |
| const tunnelInfo = getTunnelInfo(projectId); |
| |
| return { |
| status: 'running', |
| running: true, |
| port: project.port, |
| tunnelUrl: tunnelInfo?.url || project.tunnelUrl, |
| localUrl: `http://localhost:${project.port}`, |
| pid: project.pid, |
| tunnelProvider: tunnelInfo?.provider || null |
| }; |
| } |
|
|
| export function listRunningProjects() { |
| const projects = []; |
| for (const [id, project] of runningProjects) { |
| const tunnelInfo = getTunnelInfo(id); |
| projects.push({ |
| id, |
| port: project.port, |
| tunnelUrl: tunnelInfo?.url || project.tunnelUrl, |
| tunnelProvider: tunnelInfo?.provider || null, |
| pid: project.pid |
| }); |
| } |
| return projects; |
| } |
|
|
| export async function stopAllProjects() { |
| const promises = []; |
| for (const projectId of runningProjects.keys()) { |
| promises.push(stopProject(projectId)); |
| } |
| await Promise.all(promises); |
| } |
|
|
| export { execAsync, exec, getAvailablePort, runningProjects }; |
|
|