multi-projects-runner / lib /projectManager.js
Karan6933's picture
Upload 35 files
05d91af verified
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 };