Exocore / exocore-web /server /services /projectManager.ts
ChoruYt's picture
Upload 216 files
a7f51f5 verified
Raw
History Blame Contribute Delete
3.49 kB
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<string, RunningProject> = new Map();
private readonly projectsDir: string;
constructor() {
this.projectsDir = path.join(process.cwd(), "projects");
}
start(options: StartOptions): Promise<RunningProject> {
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<RunningProject> {
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();