import { ipcMain, BrowserWindow, dialog } from "electron" import { processManager } from "./process-manager" import { randomBytes } from "crypto" import * as fs from "fs" import * as path from "path" import { execSync } from "child_process" import ignore from "ignore" interface Instance { id: string folder: string port: number pid: number status: "starting" | "ready" | "error" | "stopped" error?: string } const instances = new Map() function generateId(): string { return randomBytes(16).toString("hex") } export function setupInstanceIPC(mainWindow: BrowserWindow) { processManager.setMainWindow(mainWindow) ipcMain.handle("dialog:selectFolder", async () => { const result = await dialog.showOpenDialog(mainWindow!, { title: "Select Project Folder", properties: ["openDirectory"], }) if (result.canceled || !result.filePaths.length) { return null } return result.filePaths[0] }) ipcMain.handle( "instance:create", async (event, id: string, folder: string, binaryPath?: string, environmentVariables?: Record) => { const instance: Instance = { id, folder, port: 0, pid: 0, status: "starting", } instances.set(id, instance) try { const { pid, port, binaryPath: actualBinaryPath, } = await processManager.spawn(folder, id, binaryPath, environmentVariables) instance.port = port instance.pid = pid instance.status = "ready" mainWindow.webContents.send("instance:started", { id, port, pid, binaryPath: actualBinaryPath }) const meta = processManager.getAllProcesses().get(pid) if (meta) { meta.childProcess.on("exit", (code, signal) => { instance.status = "stopped" mainWindow.webContents.send("instance:stopped", { id }) }) } return { id, port, pid, binaryPath: actualBinaryPath } } catch (error) { instance.status = "error" instance.error = error instanceof Error ? error.message : String(error) mainWindow.webContents.send("instance:error", { id, error: instance.error, }) throw error } }, ) ipcMain.handle("instance:stop", async (event, pid: number) => { await processManager.kill(pid) for (const [id, instance] of instances.entries()) { if (instance.pid === pid) { instance.status = "stopped" break } } }) ipcMain.handle("instance:status", async (event, pid: number) => { return processManager.getStatus(pid) }) ipcMain.handle("instance:list", async () => { return Array.from(instances.values()) }) ipcMain.handle("fs:scanDirectory", async (event, workspaceFolder: string) => { const ig = ignore() ig.add([".git", "node_modules"]) const gitignorePath = path.join(workspaceFolder, ".gitignore") if (fs.existsSync(gitignorePath)) { const content = fs.readFileSync(gitignorePath, "utf-8") ig.add(content) } function scanDir(dirPath: string, baseDir: string): string[] { const results: string[] = [] try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dirPath, entry.name) const relativePath = path.relative(baseDir, fullPath) if (ig.ignores(relativePath)) { continue } if (entry.isDirectory()) { const dirWithSlash = relativePath + "/" if (!ig.ignores(dirWithSlash)) { results.push(dirWithSlash) const subFiles = scanDir(fullPath, baseDir) results.push(...subFiles) } } else { results.push(relativePath) } } } catch (error) { console.warn(`Error scanning ${dirPath}:`, error) } return results } return scanDir(workspaceFolder, workspaceFolder) }) // OpenCode binary operations ipcMain.handle("dialog:selectOpenCodeBinary", async () => { const result = await dialog.showOpenDialog(mainWindow!, { title: "Select OpenCode Binary", filters: [ { name: "Executable Files", extensions: ["exe", "cmd", "bat", "sh", "command", "app", ""] }, { name: "All Files", extensions: ["*"] }, ], properties: ["openFile"], }) if (result.canceled || !result.filePaths.length) { return null } return result.filePaths[0] }) ipcMain.handle("opencode:validateBinary", async (event, binaryPath: string) => { try { // Special handling for system PATH binary const isSystemPath = binaryPath === "opencode" if (!isSystemPath) { // Check if file exists and is executable for custom paths if (!fs.existsSync(binaryPath)) { return { valid: false, error: "File does not exist" } } const stats = fs.statSync(binaryPath) if (!stats.isFile()) { return { valid: false, error: "Path is not a file" } } } // Try to get version let version: string | undefined try { // Try -v flag first (opencode uses this) let versionOutput = execSync(`${binaryPath} -v`, { stdio: "pipe", encoding: "utf-8", timeout: 5000, }) version = versionOutput.trim() } catch (error) { // Version check failed, but binary might still be valid try { let versionOutput = execSync(`${binaryPath} --version`, { stdio: "pipe", encoding: "utf-8", timeout: 5000, }) version = versionOutput.trim() } catch (fallbackError) {} } // Try to run help command to verify it's actually opencode try { const helpOutput = execSync(`${binaryPath} --help`, { stdio: "pipe", encoding: "utf-8", timeout: 5000, }) if (!helpOutput.toLowerCase().includes("opencode")) { return { valid: false, error: "Not an OpenCode binary" } } } catch (error) { return { valid: false, error: "Binary failed to execute" } } return { valid: true, version } } catch (error) { return { valid: false, error: error instanceof Error ? error.message : String(error), } } }) }