import { spawn, ChildProcess } from "child_process"; import path from "path"; import fs from "fs"; import { ProjectStatus } from "../../types/dashboard"; interface RunningProject { pid: number; process: ChildProcess; port: number; status: ProjectStatus; startedAt: Date; restartCount: number; } interface StartOptions { projectId: string; command: string; port: number; cwd: string; autoRestart?: boolean; } class ProjectManager { private readonly processes: Map = new Map(); private readonly projectsDir: string; constructor() { this.projectsDir = path.join(process.cwd(), "projects"); } start(options: StartOptions): Promise { return new Promise((resolve, reject) => { const { projectId, command, port, cwd } = options; if (this.processes.has(projectId)) { const existing = this.processes.get(projectId)!; if (existing.status === "running") { return reject(new Error(`Project ${projectId} is already running`)); } this.cleanupProcess(projectId); } const [cmd, ...args] = command.split(" "); const child = spawn(cmd, args, { cwd, env: { ...process.env, PORT: String(port) }, stdio: "pipe", }); const runningProject: RunningProject = { pid: child.pid!, process: child, port, status: "running", startedAt: new Date(), restartCount: 0, }; this.processes.set(projectId, runningProject); child.on("exit", () => { const proj = this.processes.get(projectId); if (proj) proj.status = "stopped"; }); resolve(runningProject); }); } stop(projectId: string): boolean { const proj = this.processes.get(projectId); if (!proj) return false; proj.status = "stopped"; if (!proj.process.killed) { proj.process.kill("SIGTERM"); setTimeout(() => { if (!proj.process.killed) proj.process.kill("SIGKILL"); }, 5000); } this.cleanupProcess(projectId); return true; } async restart(options: StartOptions): Promise { this.stop(options.projectId); await new Promise((r) => setTimeout(r, 1000)); return this.start(options); } getStatus(projectId: string): RunningProject | null { return this.processes.get(projectId) ?? null; } listRunning(): Array<{ id: string; pid: number; port: number; status: ProjectStatus; startedAt: Date }> { return Array.from(this.processes.entries()).map(([id, proj]) => ({ id, pid: proj.pid, port: proj.port, status: proj.status, startedAt: proj.startedAt, })); } isRunning(projectId: string): boolean { const proj = this.processes.get(projectId); return proj?.status === "running"; } private cleanupProcess(projectId: string): void { this.processes.delete(projectId); } getExoConfigPath(projectId: string): string { return path.join(this.projectsDir, projectId, "system.exo"); } getLegacyConfigPath(projectId: string): string { return path.join(this.projectsDir, projectId, ".exocore.json"); } resolveProjectCwd(projectId: string): string { return path.join(this.projectsDir, projectId); } projectExists(projectId: string): boolean { return fs.existsSync(this.resolveProjectCwd(projectId)); } } export const projectManager = new ProjectManager();