Add environment variables configuration to OpenCode instances

- Add environment variables editor in advanced settings
- Store environment variables in global preferences
- Pass environment variables to OpenCode processes when spawning
- Display environment variables in instance information and server logs
- Fix system PATH binary handling for 'opencode' command
- Show detailed environment variable values in startup logs
- Save and restore last used binary across app restarts
- Add system PATH binary to recent binaries list when used

Features:
- Environment variables editor with add/remove functionality
- Persistent storage of environment variables across sessions
- Real-time display of active environment variables in logs
- Instance information shows configured environment variables
- Proper handling of system PATH vs custom binary paths
- Last used binary persistence and automatic restoration
This commit is contained in:
Shantur Rathore
2025-10-26 10:48:47 +00:00
parent f63a4b3754
commit 0b26ffd97d
8 changed files with 245 additions and 49 deletions

View File

@@ -37,47 +37,54 @@ export function setupInstanceIPC(mainWindow: BrowserWindow) {
return result.filePaths[0]
})
ipcMain.handle("instance:create", async (event, id: string, folder: string, binaryPath?: string) => {
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)
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 })
})
ipcMain.handle(
"instance:create",
async (event, id: string, folder: string, binaryPath?: string, environmentVariables?: Record<string, string>) => {
const instance: Instance = {
id,
folder,
port: 0,
pid: 0,
status: "starting",
}
return { id, port, pid, binaryPath: actualBinaryPath }
} catch (error) {
instance.status = "error"
instance.error = error instanceof Error ? error.message : String(error)
instances.set(id, instance)
mainWindow.webContents.send("instance:error", {
id,
error: instance.error,
})
try {
const {
pid,
port,
binaryPath: actualBinaryPath,
} = await processManager.spawn(folder, id, binaryPath, environmentVariables)
throw error
}
})
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)

View File

@@ -50,17 +50,29 @@ class ProcessManager {
}
}
async spawn(folder: string, instanceId: string, binaryPath?: string): Promise<ProcessInfo> {
async spawn(
folder: string,
instanceId: string,
binaryPath?: string,
environmentVariables?: Record<string, string>,
): Promise<ProcessInfo> {
this.validateFolder(folder)
const actualBinaryPath = binaryPath ? this.validateCustomBinary(binaryPath) : this.validateOpenCodeBinary()
this.sendLog(instanceId, "info", `Starting OpenCode server for ${folder} using ${actualBinaryPath}...`)
// Merge environment variables with process environment
const env = { ...process.env }
if (environmentVariables) {
Object.assign(env, environmentVariables)
this.sendLog(instanceId, "info", `Using ${Object.keys(environmentVariables).length} custom environment variables`)
}
return new Promise((resolve, reject) => {
const child = spawn(actualBinaryPath, ["serve", "--port", "0", "--print-logs", "--log-level", "DEBUG"], {
cwd: folder,
stdio: ["ignore", "pipe", "pipe"],
env: process.env,
env,
shell: false,
})

View File

@@ -6,6 +6,7 @@ export interface ElectronAPI {
id: string,
folder: string,
binaryPath?: string,
environmentVariables?: Record<string, string>,
) => Promise<{ id: string; port: number; pid: number; binaryPath: string }>
stopInstance: (pid: number) => Promise<void>
onInstanceStarted: (callback: (data: { id: string; port: number; pid: number; binaryPath: string }) => void) => void
@@ -35,8 +36,8 @@ export interface ElectronAPI {
const electronAPI: ElectronAPI = {
selectFolder: () => ipcRenderer.invoke("dialog:selectFolder"),
createInstance: (id: string, folder: string, binaryPath?: string) =>
ipcRenderer.invoke("instance:create", id, folder, binaryPath),
createInstance: (id: string, folder: string, binaryPath?: string, environmentVariables?: Record<string, string>) =>
ipcRenderer.invoke("instance:create", id, folder, binaryPath, environmentVariables),
stopInstance: (pid: number) => ipcRenderer.invoke("instance:stop", pid),
onInstanceStarted: (callback) => {
ipcRenderer.on("instance:started", (_, data) => callback(data))