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 tunnels: plugins.ideSsh.ISshTunnelHandle[] = []; 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(); } }); } 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); progress('Allocating local tunnel port.'); const localPort = await plugins.ideSsh.findFreePort(); const target = createSshTarget(input); progress(`Preparing SSH connection to ${target.hostAlias}.`); const opencodePassword = plugins.crypto.randomBytes(24).toString('base64url'); const remoteTheiaPort = normalizeOptionalPort(input.remoteTheiaPort, 'Remote Theia Port') ?? defaultRemoteTheiaPort; const opencodePort = normalizeOptionalPort(input.openCodePort, 'OpenCode Port') ?? defaultOpenCodePort; 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 { 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: input.batchMode ?? true, }); if (cacheCheckResult.exitCode === 0) { progress(`Remote runtime hash matches; skipping copy for ${runtime.contentHash.slice(0, 12)}.`); } else { 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: input.batchMode ?? true, 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: input.batchMode ?? true, }); 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.`); } progress('Starting remote Theia runtime.'); const bootstrapCommand = plugins.ideServerInstaller.createRemoteEphemeralBootstrapCommand({ serverVersion, runtimeRoot: runtime.remoteRoot, workspacePath: input.workspacePath, theiaPort: remoteTheiaPort, opencodePort, opencodeUsername: 'opencode', opencodePassword, }); const bootstrapResult = await plugins.ideSsh.runSshCommand(target, bootstrapCommand, { timeoutMs: 30000, batchMode: input.batchMode ?? true, }); if (bootstrapResult.exitCode !== 0) { throw new Error(bootstrapResult.stderr || `Remote bootstrap failed with ${bootstrapResult.exitCode}`); } progress('Waiting for remote Theia readiness.'); const readinessCommand = plugins.ideServerInstaller.createRemoteEphemeralReadinessCommand({ runtimeRoot: runtime.remoteRoot, theiaPort: remoteTheiaPort, }); const readinessResult = await plugins.ideSsh.runSshCommand(target, readinessCommand, { timeoutMs: 45000, batchMode: input.batchMode ?? true, }); if (readinessResult.exitCode !== 0) { throw new Error(readinessResult.stderr || `Remote Theia readiness check failed with ${readinessResult.exitCode}`); } progress('Opening local SSH tunnel.'); const tunnel = plugins.ideSsh.startSshTunnel(target, { localPort, remotePort: remoteTheiaPort, batchMode: input.batchMode ?? true, }); this.tunnels.push(tunnel); const url = `http://127.0.0.1:${localPort}`; progress(`Waiting for local tunnel ${url}.`); await waitForHttpUrl(url, 15000); progress('Tunnel ready; opening workspace.'); return { url, localPort, remoteTheiaPort, opencodePort }; } finally { await fs.rm(runtime.localRoot, { recursive: true, force: true }); } }); } 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)), }, }); 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; } 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: requireTrimmed(input.workspacePath, 'Workspace path'), }; }; 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 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 targetLib = path.join(localRoot, 'applications', 'remote-theia', 'lib'); 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.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 IDE
Connect to SSH Host

Connect to Remote Workspace

Use system OpenSSH config, agent, keys, ProxyJump, and hardware-key behavior. Saved hosts are written to ~/.ssh/config.

Host
Workspace Runtime
Output

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