import type { IIdeSshTarget } from '@git.zone/ide-protocol'; import * as childProcess from 'node:child_process'; import { constants as fsConstants, createReadStream } from 'node:fs'; import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import * as plugins from './plugins.js'; const defaultRemoteTheiaPort = 33990; const defaultOpenCodePort = 4096; const electronDistDir = path.dirname(fileURLToPath(import.meta.url)); const electronPackageRoot = path.resolve(electronDistDir, '..'); const workspaceRoot = path.resolve(electronPackageRoot, '../..'); const runtimeMarkerFileName = plugins.ideServerInstaller.remoteEphemeralRuntimeMarkerFileName; class GitZoneIdeElectronShell { private readonly hostSessions = new Map(); private readonly tunnels: plugins.ideSsh.ISshTunnelHandle[] = []; private readonly localOpenCodeRuntimes = new Map(); private toolBridge: LocalOpenCodeToolBridge | undefined; async start() { await plugins.electron.app.whenReady(); console.log(`Git.Zone IDE SSH_AUTH_SOCK: ${process.env.SSH_AUTH_SOCK || 'not set'}`); this.registerIpcHandlers(); const remoteUrl = getArgValue('--remote-url'); if (remoteUrl) { this.openWorkspaceWindow(remoteUrl); } else { await this.openLauncherWindow(); } plugins.electron.app.on('activate', async () => { if (plugins.electron.BrowserWindow.getAllWindows().length === 0) { await this.openLauncherWindow(); } }); plugins.electron.app.on('before-quit', () => { for (const tunnel of this.tunnels) { void tunnel.dispose(); } for (const runtime of this.localOpenCodeRuntimes.values()) { disposeLocalOpenCodeRuntime(runtime); } void this.toolBridge?.dispose(); }); } private registerIpcHandlers() { plugins.electron.ipcMain.handle('gitzone:list-hosts', async () => { const hosts = await plugins.ideSsh.readSshConfig(); return { configPath: plugins.ideSsh.defaultSshConfigPath(), hosts: plugins.ideSsh.listConnectableHosts(hosts).map(toLauncherHost), }; }); plugins.electron.ipcMain.handle('gitzone:save-host', async (_event, input: ISaveHostInput) => { const host = await plugins.ideSsh.saveSshHostConfig({ alias: requireTrimmed(input.alias, 'Host alias'), hostName: requireTrimmed(input.hostName, 'HostName'), user: trimOptional(input.user), port: normalizeOptionalPort(input.port, 'Port'), identityFile: trimOptional(input.identityFile), proxyJump: trimOptional(input.proxyJump), forwardAgent: input.forwardAgent, }); return toLauncherHost(host); }); plugins.electron.ipcMain.handle('gitzone:connect', async (event, input: IConnectInput) => { const progress = createProgressEmitter(event.sender); const target = createSshTarget(input); progress(`Preparing SSH connection to ${target.hostAlias}.`); const serverVersion = plugins.electron.app.getVersion(); progress('Staging remote runtime payload.'); const runtime = await createLocalEphemeralRuntime(serverVersion); progress(`Runtime hash ${runtime.contentHash.slice(0, 12)} staged: ${runtime.fileCount} files, ${formatBytes(runtime.totalBytes)} unpacked.`); try { await uploadRuntimeIfNeeded(target, runtime, input.batchMode ?? true, progress); progress('Loading remote project registry.'); const projects = await loadRemoteProjects(target, runtime.remoteRoot, input.batchMode ?? true); const session: IRemoteHostSession = { id: plugins.crypto.randomBytes(12).toString('hex'), target, batchMode: input.batchMode ?? true, serverVersion, runtimeRoot: runtime.remoteRoot, runtimeHash: runtime.contentHash, projects, instances: new Map(), }; this.hostSessions.set(session.id, session); progress(`Connected to ${target.hostAlias}; ${projects.length} project${projects.length === 1 ? '' : 's'} registered.`); return toHostSessionDescriptor(session); } finally { await fs.rm(runtime.localRoot, { recursive: true, force: true }); } }); plugins.electron.ipcMain.handle('gitzone:add-project', async (event, input: IAddProjectInput) => { const progress = createProgressEmitter(event.sender); const session = requireHostSession(this.hostSessions, input.connectionId); const projectPath = requireTrimmed(input.path, 'Project path'); progress(`Adding remote project ${projectPath}.`); const upsertCommand = plugins.ideServerInstaller.createRemoteProjectUpsertCommand({ runtimeRoot: session.runtimeRoot, projectPath, title: trimOptional(input.title), }); const upsertResult = await plugins.ideSsh.runSshCommand(session.target, upsertCommand, { timeoutMs: 30000, batchMode: session.batchMode, }); if (upsertResult.exitCode !== 0) { throw new Error(upsertResult.stderr || `Remote project add failed with ${upsertResult.exitCode}`); } session.projects = parseRemoteProjectRegistry(upsertResult.stdout); progress(`Remote project registry now has ${session.projects.length} project${session.projects.length === 1 ? '' : 's'}.`); return { projects: session.projects }; }); plugins.electron.ipcMain.handle('gitzone:open-project', async (event, input: IOpenProjectInput) => { const progress = createProgressEmitter(event.sender); const session = requireHostSession(this.hostSessions, input.connectionId); const project = findProject(session, input); const existingInstance = session.instances.get(project.id); if (existingInstance) { progress(`Switching to running project ${project.title}.`); return existingInstance; } progress(`Allocating remote Theia port for ${project.title}.`); const portResult = await plugins.ideSsh.runSshCommand( session.target, plugins.ideServerInstaller.createRemoteEphemeralPortAllocationCommand({ runtimeRoot: session.runtimeRoot, count: 1, }), { timeoutMs: 30000, batchMode: session.batchMode, }, ); if (portResult.exitCode !== 0) { throw new Error(portResult.stderr || `Remote port allocation failed with ${portResult.exitCode}`); } const [remoteTheiaPort] = parseAllocatedPorts(portResult.stdout, 1); const localPort = await plugins.ideSsh.findFreePort(); progress(`Starting Theia for ${project.title} on remote port ${remoteTheiaPort}.`); const bootstrapResult = await plugins.ideSsh.runSshCommand( session.target, plugins.ideServerInstaller.createRemoteEphemeralBootstrapCommand({ serverVersion: session.serverVersion, runtimeRoot: session.runtimeRoot, workspacePath: project.path, theiaPort: remoteTheiaPort, opencodePort: defaultOpenCodePort, opencodeUsername: 'opencode', opencodePassword: '', theiaColorTheme: 'dark', }), { timeoutMs: 30000, batchMode: session.batchMode, }, ); if (bootstrapResult.exitCode !== 0) { throw new Error(bootstrapResult.stderr || `Remote bootstrap failed with ${bootstrapResult.exitCode}`); } progress(`Waiting for Theia readiness for ${project.title}.`); const readinessResult = await plugins.ideSsh.runSshCommand( session.target, plugins.ideServerInstaller.createRemoteEphemeralReadinessCommand({ runtimeRoot: session.runtimeRoot, theiaPort: remoteTheiaPort, }), { timeoutMs: 45000, batchMode: session.batchMode, }, ); if (readinessResult.exitCode !== 0) { throw new Error(readinessResult.stderr || `Remote Theia readiness check failed with ${readinessResult.exitCode}`); } progress(`Opening local tunnel for ${project.title}.`); const tunnel = plugins.ideSsh.startSshTunnel(session.target, { localPort, remotePort: remoteTheiaPort, batchMode: session.batchMode, }); this.tunnels.push(tunnel); const url = `http://127.0.0.1:${localPort}`; await waitForHttpUrl(url, 15000); const instanceId = `${session.id}:${project.id}`; const openCode = await this.startLocalOpenCodeForProject(session, project, instanceId, progress); const instance: IProjectInstance = { id: instanceId, projectId: project.id, title: project.title, path: project.path, url, localPort, remoteTheiaPort, openCode, }; session.instances.set(project.id, instance); progress(`Project ${project.title} is ready.`); return instance; }); } private async ensureToolBridge() { if (!this.toolBridge) { this.toolBridge = new LocalOpenCodeToolBridge(); await this.toolBridge.start(); } return this.toolBridge; } private async startLocalOpenCodeForProject( session: IRemoteHostSession, project: IRemoteProject, instanceId: string, progress: (message: string) => void, ): Promise { const bridge = await this.ensureToolBridge(); const proxyRoot = path.join(plugins.electron.app.getPath('userData'), 'opencode', sanitizeRuntimePart(instanceId)); const proxyWorkspacePath = path.join(proxyRoot, 'workspace'); const configDir = path.join(proxyRoot, 'config'); const bridgeToken = plugins.crypto.randomBytes(32).toString('base64url'); const port = await plugins.ideSsh.findFreePort(); const username = 'opencode'; const password = plugins.crypto.randomBytes(24).toString('base64url'); const baseUrl = `http://127.0.0.1:${port}`; try { progress(`Starting local OpenCode server for ${project.title}.`); await fs.rm(proxyRoot, { recursive: true, force: true }); await fs.mkdir(proxyWorkspacePath, { recursive: true }); await writeOpenCodeBridgeConfig(configDir); bridge.register(bridgeToken, { session, project, proxyWorkspacePath, }); const openCodeExecutable = await resolveOpenCodeExecutable(); const openCodeProcess = childProcess.spawn( openCodeExecutable, ['serve', '--hostname', '127.0.0.1', '--port', `${port}`], { cwd: proxyWorkspacePath, env: { ...process.env, OPENCODE_CONFIG_DIR: configDir, OPENCODE_SERVER_USERNAME: username, OPENCODE_SERVER_PASSWORD: password, GITZONE_IDE_TOOL_BRIDGE_URL: bridge.baseUrl, GITZONE_IDE_TOOL_BRIDGE_TOKEN: bridgeToken, }, shell: false, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }, ); const runtime: ILocalOpenCodeRuntime = { instanceId, process: openCodeProcess, bridge, bridgeToken, configDir, proxyWorkspacePath, baseUrl, username, password, }; this.localOpenCodeRuntimes.set(instanceId, runtime); openCodeProcess.stdout?.on('data', (chunk: Buffer) => console.log(`[opencode:${project.title}] ${chunk.toString('utf8').trim()}`)); openCodeProcess.stderr?.on('data', (chunk: Buffer) => console.warn(`[opencode:${project.title}] ${chunk.toString('utf8').trim()}`)); openCodeProcess.once('error', (error) => { console.warn(`Local OpenCode server failed for ${project.title}: ${error.message}`); }); openCodeProcess.once('exit', () => { bridge.unregister(bridgeToken); this.localOpenCodeRuntimes.delete(instanceId); }); await waitForOpenCodeHealth(baseUrl, username, password, 15000); progress(`Local OpenCode server ready for ${project.title}; remote tools are bridged over SSH.`); return { status: 'ready', message: 'Local OpenCode server is running with remote SSH tool overrides.', }; } catch (error) { bridge.unregister(bridgeToken); const message = error instanceof Error ? error.message : String(error); progress(`Local OpenCode server unavailable for ${project.title}: ${message}`); const runtime = this.localOpenCodeRuntimes.get(instanceId); if (runtime) { disposeLocalOpenCodeRuntime(runtime); this.localOpenCodeRuntimes.delete(instanceId); } return { status: 'unavailable', message, }; } } private async openLauncherWindow() { const window = new plugins.electron.BrowserWindow({ width: 960, height: 720, title: 'Git.Zone IDE', webPreferences: { contextIsolation: true, nodeIntegration: false, preload: fileURLToPath(new URL('../preload.cjs', import.meta.url)), webviewTag: true, }, }); window.webContents.on('console-message', (_event, _level, message) => { console.log(`[launcher] ${message}`); }); await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(renderLauncherHtml())}`); } private openWorkspaceWindow(url: string) { const window = new plugins.electron.BrowserWindow({ width: 1440, height: 960, title: 'Git.Zone IDE', webPreferences: { contextIsolation: true, nodeIntegration: false, }, }); void window.loadURL(url); } } interface ILauncherHost { alias: string; hostName?: string; user?: string; port?: number; identityFiles: string[]; proxyJump?: string; forwardAgent?: boolean; } interface ISaveHostInput { alias?: string; hostName?: string; user?: string; port?: number; identityFile?: string; proxyJump?: string; forwardAgent?: boolean; } interface IConnectInput extends ISaveHostInput { hostAlias?: string; workspacePath?: string; remoteTheiaPort?: number; openCodePort?: number; batchMode?: boolean; } interface IAddProjectInput { connectionId?: string; path?: string; title?: string; } interface IOpenProjectInput { connectionId?: string; projectId?: string; projectPath?: string; } interface IRemoteProject { id: string; path: string; title: string; createdAt?: string; updatedAt?: string; } interface IRemoteHostSession { id: string; target: IIdeSshTarget; batchMode: boolean; serverVersion: string; runtimeRoot: string; runtimeHash: string; projects: IRemoteProject[]; instances: Map; } interface IProjectInstance { id: string; projectId: string; title: string; path: string; url: string; localPort: number; remoteTheiaPort: number; openCode: IOpenCodeProjectStatus; } interface IOpenCodeProjectStatus { status: 'ready' | 'unavailable'; message: string; } interface ILocalOpenCodeRuntime { instanceId: string; process: childProcess.ChildProcess; bridge: LocalOpenCodeToolBridge; bridgeToken: string; configDir: string; proxyWorkspacePath: string; baseUrl: string; username: string; password: string; } interface IOpenCodeToolBridgeContext { session: IRemoteHostSession; project: IRemoteProject; proxyWorkspacePath: string; } class LocalOpenCodeToolBridge { private readonly contexts = new Map(); private server: ReturnType | undefined; baseUrl = ''; async start() { if (this.server) { return; } this.server = plugins.http.createServer((request, response) => { void this.handleRequest(request, response); }); await new Promise((resolve, reject) => { this.server!.once('error', reject); this.server!.listen(0, '127.0.0.1', () => resolve()); }); const address = this.server.address(); if (!address || typeof address !== 'object') { throw new Error('Unable to bind local OpenCode tool bridge.'); } this.baseUrl = `http://127.0.0.1:${address.port}`; } register(token: string, context: IOpenCodeToolBridgeContext) { this.contexts.set(token, context); } unregister(token: string) { this.contexts.delete(token); } async dispose() { this.contexts.clear(); if (!this.server) { return; } const server = this.server; this.server = undefined; await new Promise((resolve) => server.close(() => resolve())); } private async handleRequest(request: any, response: any) { try { if (request.method !== 'POST') { writeJsonResponse(response, 405, { error: 'Method not allowed' }); return; } const url = new URL(request.url ?? '/', this.baseUrl || 'http://127.0.0.1'); const toolName = decodeURIComponent(url.pathname.replace(/^\/tool\//, '')); if (!url.pathname.startsWith('/tool/') || !toolName) { writeJsonResponse(response, 404, { error: 'Tool route not found' }); return; } const authorization = request.headers.authorization ?? ''; const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''; const context = this.contexts.get(token); if (!context) { writeJsonResponse(response, 401, { error: 'Unauthorized' }); return; } const bodyText = await readRequestBody(request, 10 * 1024 * 1024); const body = bodyText ? JSON.parse(bodyText) as { args?: Record } : {}; const result = await executeRemoteOpenCodeTool(toolName, context, body.args ?? {}); writeJsonResponse(response, 200, result); } catch (error) { writeJsonResponse(response, 500, { error: error instanceof Error ? error.message : String(error), }); } } } const readRequestBody = async (request: NodeJS.ReadableStream, maxBytes: number) => { const chunks: Buffer[] = []; let totalBytes = 0; for await (const chunk of request) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); totalBytes += buffer.length; if (totalBytes > maxBytes) { throw new Error(`Request body exceeds ${maxBytes} bytes.`); } chunks.push(buffer); } return Buffer.concat(chunks).toString('utf8'); }; const writeJsonResponse = (response: { statusCode: number; setHeader(name: string, value: string): void; end(body: string): void }, statusCode: number, body: unknown) => { response.statusCode = statusCode; response.setHeader('content-type', 'application/json'); response.end(JSON.stringify(body)); }; const executeRemoteOpenCodeTool = async ( toolName: string, context: IOpenCodeToolBridgeContext, args: Record, ) => { const normalizedArgs = normalizeOpenCodeToolArgs(toolName, context, args); const timeoutMs = Math.max(30000, Number(args.timeout || 120000) + 5000); const result = await plugins.ideSsh.runSshCommand( context.session.target, plugins.ideServerInstaller.createRemoteOpenCodeToolCommand({ runtimeRoot: context.session.runtimeRoot, workspacePath: context.project.path, toolName, }), { timeoutMs, batchMode: context.session.batchMode, stdin: JSON.stringify({ args: normalizedArgs }), }, ); if (result.exitCode !== 0) { throw new Error(result.stderr || `Remote OpenCode tool ${toolName} failed with ${result.exitCode}`); } try { return JSON.parse(result.stdout || '""') as unknown; } catch (error) { throw new Error(`Remote OpenCode tool ${toolName} returned invalid JSON: ${error instanceof Error ? error.message : String(error)}\n${result.stdout}`); } }; const normalizeOpenCodeToolArgs = ( toolName: string, context: IOpenCodeToolBridgeContext, args: Record, ) => { const next: Record = { ...args }; if (toolName === 'bash') { if (typeof next.workdir === 'string') { next.workdir = mapOpenCodeRemotePath(next.workdir, context); } return next; } if (toolName === 'read' || toolName === 'write' || toolName === 'edit') { if (typeof next.filePath === 'string') { next.filePath = mapOpenCodeRemotePath(next.filePath, context); } return next; } if (toolName === 'grep' || toolName === 'glob') { if (typeof next.path === 'string') { next.path = mapOpenCodeRemotePath(next.path, context); } return next; } if (toolName === 'apply_patch' && typeof next.patchText === 'string') { next.patchText = normalizeOpenCodePatchText(next.patchText, context); } return next; }; const normalizeOpenCodePatchText = (patchText: string, context: IOpenCodeToolBridgeContext) => patchText .replace(/^(\*\*\* (?:Add|Update|Delete) File: )(.*)$/gm, (_match, prefix: string, filePath: string) => `${prefix}${mapOpenCodePatchPath(filePath, context)}`) .replace(/^(\*\*\* Move to: )(.*)$/gm, (_match, prefix: string, filePath: string) => `${prefix}${mapOpenCodePatchPath(filePath, context)}`); const mapOpenCodePatchPath = (filePath: string, context: IOpenCodeToolBridgeContext) => { const trimmedPath = filePath.trim(); if (!path.isAbsolute(trimmedPath)) { return trimmedPath; } const proxyWorkspacePath = path.resolve(context.proxyWorkspacePath); const normalizedPath = path.resolve(trimmedPath); if (normalizedPath === proxyWorkspacePath) { return '.'; } if (normalizedPath.startsWith(`${proxyWorkspacePath}${path.sep}`)) { return path.relative(proxyWorkspacePath, normalizedPath).split(path.sep).join('/'); } return trimmedPath; }; const mapOpenCodeRemotePath = (filePath: string, context: IOpenCodeToolBridgeContext) => { const trimmedPath = filePath.trim(); if (!trimmedPath) { return context.project.path; } if (trimmedPath === '~' || trimmedPath.startsWith('~/') || trimmedPath === '$HOME' || trimmedPath.startsWith('$HOME/')) { return trimmedPath; } const proxyWorkspacePath = path.resolve(context.proxyWorkspacePath); if (path.isAbsolute(trimmedPath)) { const normalizedPath = path.resolve(trimmedPath); if (normalizedPath === proxyWorkspacePath) { return context.project.path; } if (normalizedPath.startsWith(`${proxyWorkspacePath}${path.sep}`)) { const relativePath = path.relative(proxyWorkspacePath, normalizedPath).split(path.sep).join('/'); return plugins.ideServerInstaller.joinRemotePath(context.project.path, relativePath); } return trimmedPath; } return plugins.ideServerInstaller.joinRemotePath(context.project.path, trimmedPath); }; const writeOpenCodeBridgeConfig = async (configDir: string) => { await fs.mkdir(configDir, { recursive: true }); await fs.writeFile( path.join(configDir, 'opencode.json'), plugins.ideOpenCodeBridge.renderOpenCodeBridgeConfigContent(), ); const toolFiles = plugins.ideOpenCodeBridge.renderOpenCodeBridgeToolFiles(); for (const [relativePath, content] of Object.entries(toolFiles)) { const filePath = path.join(configDir, relativePath); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content); } }; const resolveOpenCodeExecutable = async () => { const candidates = [ process.env.GITZONE_IDE_OPENCODE_BINARY, process.env.OPENCODE_BINARY, path.join(os.homedir(), '.opencode', 'bin', 'opencode'), '/usr/local/bin/opencode', '/usr/bin/opencode', ].filter(Boolean) as string[]; for (const candidate of candidates) { try { await fs.access(candidate, fsConstants.X_OK); return candidate; } catch {} } return 'opencode'; }; const waitForOpenCodeHealth = async (baseUrl: string, username: string, password: string, timeoutMs: number) => { const authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; const startedAt = Date.now(); let lastError: unknown; while (Date.now() - startedAt < timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 1000); try { const response = await fetch(`${baseUrl}/global/health`, { headers: { authorization }, signal: controller.signal, }); if (response.ok) { return; } lastError = new Error(`HTTP ${response.status}`); } catch (error) { lastError = error; } finally { clearTimeout(timer); } await new Promise((resolve) => setTimeout(resolve, 250)); } throw new Error(`Local OpenCode server did not become ready at ${baseUrl}: ${lastError instanceof Error ? lastError.message : String(lastError)}`); }; const disposeLocalOpenCodeRuntime = (runtime: ILocalOpenCodeRuntime) => { runtime.bridge.unregister(runtime.bridgeToken); if (runtime.process.exitCode === null && !runtime.process.killed) { runtime.process.kill('SIGTERM'); } }; const toLauncherHost = (host: plugins.ideSsh.ISshHostConfig): ILauncherHost => ({ alias: host.alias, hostName: host.hostName, user: host.user, port: host.port, identityFiles: host.identityFiles, proxyJump: host.proxyJump, forwardAgent: host.forwardAgent, }); const createSshTarget = (input: IConnectInput): IIdeSshTarget => { const hostAlias = trimOptional(input.hostAlias) || trimOptional(input.hostName); if (!hostAlias) { throw new Error('Choose a saved host or enter a HostName.'); } return { id: hostAlias, hostAlias, hostName: trimOptional(input.hostName), user: trimOptional(input.user), port: normalizeOptionalPort(input.port, 'Port'), workspacePath: trimOptional(input.workspacePath), }; }; const requireTrimmed = (value: string | undefined, label: string) => { const trimmedValue = trimOptional(value); if (!trimmedValue) { throw new Error(`${label} is required.`); } return trimmedValue; }; const trimOptional = (value: string | undefined) => { const trimmedValue = value?.trim(); return trimmedValue || undefined; }; const normalizeOptionalPort = (value: number | undefined, label: string) => { if (value === undefined || value === null || value === 0) { return undefined; } const port = Number(value); if (!Number.isInteger(port) || port <= 0 || port > 65535) { throw new Error(`${label} must be a number from 1 to 65535.`); } return port; }; const toHostSessionDescriptor = (session: IRemoteHostSession) => ({ id: session.id, hostAlias: session.target.hostAlias, runtimeRoot: session.runtimeRoot, runtimeHash: session.runtimeHash, projects: session.projects, openProjects: [...session.instances.values()], }); const requireHostSession = (sessions: Map, connectionId: string | undefined) => { const sessionId = requireTrimmed(connectionId, 'Connection id'); const session = sessions.get(sessionId); if (!session) { throw new Error(`Remote host session not found: ${sessionId}`); } return session; }; const findProject = (session: IRemoteHostSession, input: IOpenProjectInput) => { const project = input.projectId ? session.projects.find((candidate) => candidate.id === input.projectId) : session.projects.find((candidate) => candidate.path === input.projectPath); if (!project) { throw new Error('Project not found in remote registry.'); } return project; }; const uploadRuntimeIfNeeded = async ( target: IIdeSshTarget, runtime: Awaited>, batchMode: boolean, progress: (message: string) => void, ) => { progress(`Checking remote /tmp cache at ${runtime.remoteRoot}.`); const cacheCheckCommand = plugins.ideServerInstaller.createRemoteEphemeralRuntimeCacheCheckCommand({ runtimeRoot: runtime.remoteRoot, runtimeSha256: runtime.contentHash, }); const cacheCheckResult = await plugins.ideSsh.runSshCommand(target, cacheCheckCommand, { timeoutMs: 30000, batchMode, }); if (cacheCheckResult.exitCode === 0) { progress(`Remote runtime hash matches; skipping copy for ${runtime.contentHash.slice(0, 12)}.`); return; } progress(`Remote runtime cache miss; copying ${formatBytes(runtime.totalBytes)} to ${runtime.remoteRoot}.`); const reportUploadProgress = createUploadProgressReporter(progress, runtime.totalBytes); const uploadResult = await plugins.ideSsh.uploadDirectoryToRemote(target, runtime.localRoot, runtime.remoteRoot, { timeoutMs: 300000, batchMode, onProgress: (uploadProgress) => reportUploadProgress(uploadProgress.bytesUploaded), }); if (uploadResult.exitCode !== 0) { throw new Error(uploadResult.stderr || `Remote runtime upload failed with ${uploadResult.exitCode}`); } progress('Remote runtime files copied; writing cache marker.'); const markCommand = plugins.ideServerInstaller.createRemoteEphemeralRuntimeMarkCommand({ runtimeRoot: runtime.remoteRoot, runtimeSha256: runtime.contentHash, }); const markResult = await plugins.ideSsh.runSshCommand(target, markCommand, { timeoutMs: 30000, batchMode, }); if (markResult.exitCode !== 0) { throw new Error(markResult.stderr || `Remote runtime marker write failed with ${markResult.exitCode}`); } progress(`Remote runtime upload complete; cache marker ${runtime.contentHash.slice(0, 12)} stored.`); }; const loadRemoteProjects = async (target: IIdeSshTarget, runtimeRoot: string, batchMode: boolean) => { const listResult = await plugins.ideSsh.runSshCommand( target, plugins.ideServerInstaller.createRemoteProjectListCommand({ runtimeRoot }), { timeoutMs: 30000, batchMode, }, ); if (listResult.exitCode !== 0) { throw new Error(listResult.stderr || `Remote project registry read failed with ${listResult.exitCode}`); } return parseRemoteProjectRegistry(listResult.stdout); }; const parseRemoteProjectRegistry = (text: string): IRemoteProject[] => { const parsed = JSON.parse(text || '{"projects":[]}') as { projects?: unknown }; if (!Array.isArray(parsed.projects)) { return []; } return parsed.projects .map((project) => normalizeRemoteProject(project)) .filter((project): project is IRemoteProject => !!project); }; const normalizeRemoteProject = (project: unknown): IRemoteProject | undefined => { if (!project || typeof project !== 'object') { return undefined; } const record = project as Record; const pathValue = typeof record.path === 'string' ? record.path : undefined; const id = typeof record.id === 'string' ? record.id : undefined; if (!pathValue || !id) { return undefined; } const title = typeof record.title === 'string' && record.title.trim() ? record.title : pathValue.split('/').filter(Boolean).pop() || pathValue; return { id, path: pathValue, title, createdAt: typeof record.createdAt === 'string' ? record.createdAt : undefined, updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : undefined, }; }; const parseAllocatedPorts = (stdout: string, expectedCount: number) => { const portsLine = stdout.split(/\r?\n/).find((line) => line.startsWith('ports=')); const ports = portsLine ?.slice('ports='.length) .split(',') .map((value) => Number(value)) .filter((port) => Number.isInteger(port) && port > 0 && port <= 65535) ?? []; if (ports.length < expectedCount) { throw new Error(`Expected ${expectedCount} remote ports, got: ${stdout}`); } return ports; }; const createProgressEmitter = (webContents: { isDestroyed(): boolean; send(channel: string, ...args: unknown[]): void }) => { return (message: string) => { console.log(`[connect] ${message}`); if (!webContents.isDestroyed()) { webContents.send('gitzone:connect-progress', message); } }; }; const createUploadProgressReporter = (progress: (message: string) => void, unpackedBytes: number) => { const startedAt = Date.now(); let lastReportedAt = 0; let lastReportedBytes = 0; return (bytesUploaded: number) => { const now = Date.now(); if (bytesUploaded - lastReportedBytes < 4 * 1024 * 1024 && now - lastReportedAt < 2000) { return; } lastReportedAt = now; lastReportedBytes = bytesUploaded; const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001); const uploadRate = bytesUploaded / elapsedSeconds; progress(`Copying runtime: ${formatBytes(bytesUploaded)} compressed sent at ${formatBytes(uploadRate)}/s (${formatBytes(unpackedBytes)} unpacked).`); }; }; const createLocalEphemeralRuntime = async (serverVersion: string) => { const stageId = `gitzone-ide-stage-${sanitizeRuntimePart(serverVersion)}-${Date.now()}-${plugins.crypto.randomBytes(4).toString('hex')}`; const localRoot = path.join(os.tmpdir(), stageId); const sourceLib = path.join(workspaceRoot, 'applications', 'remote-theia', 'lib'); const sourcePackageJson = path.join(workspaceRoot, 'applications', 'remote-theia', 'package.json'); const targetLib = path.join(localRoot, 'applications', 'remote-theia', 'lib'); const targetPackageJson = path.join(localRoot, 'applications', 'remote-theia', 'package.json'); const nodeBinary = await resolveLocalNodeBinary(); const targetNodeBinary = path.join(localRoot, 'node', 'bin', 'node'); await fs.rm(localRoot, { recursive: true, force: true }); await fs.access(path.join(sourceLib, 'backend', 'main.js')); await fs.mkdir(path.dirname(targetLib), { recursive: true }); await fs.cp(sourceLib, targetLib, { recursive: true }); await fs.copyFile(sourcePackageJson, targetPackageJson); await fs.mkdir(path.dirname(targetNodeBinary), { recursive: true }); await fs.copyFile(nodeBinary, targetNodeBinary); await fs.chmod(targetNodeBinary, 0o755); await copyNodeSharedLibraries(nodeBinary, path.join(localRoot, 'node', 'lib')); const runtimeHash = await hashLocalRuntimeDirectory(localRoot); const remoteRoot = `/tmp/gitzone-ide-${sanitizeRuntimePart(serverVersion)}-${runtimeHash.contentHash}`; return { localRoot, remoteRoot, ...runtimeHash }; }; const copyNodeSharedLibraries = async (nodeBinary: string, targetDirectory: string) => { const lddOutput = childProcess.execFileSync('ldd', [nodeBinary], { encoding: 'utf8' }); const requiredLibraries = new Set(['libatomic.so.1']); const libraryPaths = lddOutput .split(/\r?\n/) .map((line) => line.match(/=>\s+(\/\S+)/)?.[1]) .filter((libraryPath): libraryPath is string => !!libraryPath && requiredLibraries.has(path.basename(libraryPath))); if (libraryPaths.length === 0) { return; } await fs.mkdir(targetDirectory, { recursive: true }); for (const libraryPath of libraryPaths) { await fs.copyFile(libraryPath, path.join(targetDirectory, path.basename(libraryPath))); } }; const hashLocalRuntimeDirectory = async (rootDirectory: string) => { const hash = plugins.crypto.createHash('sha256'); const filePaths = await listLocalRuntimeFiles(rootDirectory); let fileCount = 0; let totalBytes = 0; for (const filePath of filePaths) { const relativePath = path.relative(rootDirectory, filePath).split(path.sep).join('/'); if (relativePath === runtimeMarkerFileName) { continue; } const stats = await fs.lstat(filePath); if (stats.isSymbolicLink()) { const linkTarget = await fs.readlink(filePath); fileCount++; totalBytes += Buffer.byteLength(linkTarget); hash.update(`link\0${relativePath}\0${linkTarget}\0`); continue; } if (!stats.isFile()) { continue; } fileCount++; totalBytes += stats.size; hash.update(`file\0${relativePath}\0${stats.mode & 0o111 ? 'x' : '-'}\0${stats.size}\0`); await updateHashFromFile(hash, filePath); hash.update('\0'); } return { contentHash: hash.digest('hex'), fileCount, totalBytes }; }; const listLocalRuntimeFiles = async (rootDirectory: string) => { const filePaths: string[] = []; const walk = async (directory: string): Promise => { const entries = await fs.readdir(directory, { withFileTypes: true }); for (const entry of entries) { const filePath = path.join(directory, entry.name); if (entry.isDirectory()) { await walk(filePath); } else if (entry.isFile() || entry.isSymbolicLink()) { filePaths.push(filePath); } } }; await walk(rootDirectory); return filePaths.sort((left, right) => path.relative(rootDirectory, left).localeCompare(path.relative(rootDirectory, right))); }; const updateHashFromFile = async (hash: ReturnType, filePath: string) => { await new Promise((resolve, reject) => { const stream = createReadStream(filePath); stream.on('data', (chunk) => { hash.update(chunk); }); stream.on('error', reject); stream.on('end', resolve); }); }; const resolveLocalNodeBinary = async () => { const candidates = [ process.env.GITZONE_IDE_NODE_BINARY, process.env.NODE_BINARY, '/usr/bin/node', ].filter(Boolean) as string[]; for (const candidate of candidates) { try { await fs.access(candidate, fsConstants.X_OK); return candidate; } catch {} } throw new Error('No local Node.js binary found to upload for the remote ephemeral runtime.'); }; const sanitizeRuntimePart = (value: string) => value.replace(/[^a-zA-Z0-9._-]/g, '-'); const formatBytes = (bytes: number) => { const units = ['B', 'KiB', 'MiB', 'GiB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex++; } return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; }; const waitForHttpUrl = async (url: string, timeoutMs: number) => { const startedAt = Date.now(); let lastError: unknown; while (Date.now() - startedAt < timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 1000); try { const response = await fetch(url, { signal: controller.signal }); if (response.ok) { return; } lastError = new Error(`HTTP ${response.status}`); } catch (error) { lastError = error; } finally { clearTimeout(timer); } await new Promise((resolve) => setTimeout(resolve, 250)); } throw new Error(`Local tunnel did not become ready at ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}`); }; const getArgValue = (name: string) => { const index = process.argv.indexOf(name); if (index === -1) { return undefined; } return process.argv[index + 1]; }; const renderLauncherHtml = () => ` Git.Zone IDE
Git.Zone IDENot connected
Connect to SSH Host

Connect to Remote Host

Connect first, then add remote project folders from the Git.Zone dashboard. Each opened project gets its own Theia backend and local SSH tunnel.

Start With SSH Hosts

Select or create a host in the Hosts sidebar. The shell uses your system OpenSSH config, SSH agent, ProxyJump, and hardware keys.

Output

      
ReadyOpenSSH
`; void new GitZoneIdeElectronShell().start();