Files
ide/applications/electron-shell/ts/main.ts
T
2026-05-10 14:08:25 +00:00

187 lines
6.4 KiB
TypeScript

import type { IIdeSshTarget } from '@git.zone/ide-protocol';
import * as plugins from './plugins.js';
const defaultRemoteTheiaPort = 33990;
const defaultOpenCodePort = 4096;
class GitZoneIdeElectronShell {
private readonly tunnels: plugins.ideSsh.ISshTunnelHandle[] = [];
async start() {
await plugins.electron.app.whenReady();
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 plugins.ideSsh.listConnectableHosts(hosts).map((host) => ({
alias: host.alias,
hostName: host.hostName,
user: host.user,
port: host.port,
}));
});
plugins.electron.ipcMain.handle('gitzone:connect', async (_event, input: IConnectInput) => {
const localPort = await plugins.ideSsh.findFreePort();
const target: IIdeSshTarget = {
id: input.hostAlias,
hostAlias: input.hostAlias,
workspacePath: input.workspacePath,
};
const opencodePassword = plugins.crypto.randomBytes(24).toString('base64url');
const remoteTheiaPort = input.remoteTheiaPort ?? defaultRemoteTheiaPort;
const opencodePort = input.openCodePort ?? defaultOpenCodePort;
const bootstrapCommand = plugins.ideServerInstaller.createRemoteBootstrapCommand({
serverVersion: plugins.electron.app.getVersion(),
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}`);
}
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}`;
this.openWorkspaceWindow(url);
return { url, localPort, remoteTheiaPort, opencodePort };
});
}
private async openLauncherWindow() {
const window = new plugins.electron.BrowserWindow({
width: 960,
height: 720,
title: 'Git.Zone IDE',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
},
});
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 IConnectInput {
hostAlias: string;
workspacePath: string;
remoteTheiaPort?: number;
openCodePort?: number;
batchMode?: boolean;
}
const getArgValue = (name: string) => {
const index = process.argv.indexOf(name);
if (index === -1) {
return undefined;
}
return process.argv[index + 1];
};
const renderLauncherHtml = () => `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Git.Zone IDE</title>
<style>
body { margin: 0; font: 14px system-ui, sans-serif; background: #111827; color: #f9fafb; }
main { max-width: 720px; margin: 72px auto; padding: 32px; background: #1f2937; border-radius: 16px; }
h1 { margin-top: 0; font-size: 32px; }
label { display: block; margin-top: 18px; color: #d1d5db; }
input, select, button { width: 100%; box-sizing: border-box; margin-top: 8px; padding: 12px; border-radius: 10px; border: 1px solid #4b5563; background: #111827; color: #f9fafb; }
button { margin-top: 24px; background: #22c55e; border: 0; color: #052e16; font-weight: 700; cursor: pointer; }
pre { white-space: pre-wrap; color: #fca5a5; }
</style>
</head>
<body>
<main>
<h1>Git.Zone IDE</h1>
<p>Connect to an SSH host and open a remote Theia workspace powered by OpenCode server.</p>
<label>SSH Host</label>
<select id="host"></select>
<label>Remote Workspace Path</label>
<input id="workspace" value="$HOME" />
<label>Remote Theia Port</label>
<input id="theiaPort" value="${defaultRemoteTheiaPort}" />
<label>OpenCode Port</label>
<input id="opencodePort" value="${defaultOpenCodePort}" />
<button id="connect">Connect</button>
<pre id="output"></pre>
</main>
<script>
const { ipcRenderer } = require('electron');
const hostSelect = document.getElementById('host');
const output = document.getElementById('output');
ipcRenderer.invoke('gitzone:list-hosts').then((hosts) => {
for (const host of hosts) {
const option = document.createElement('option');
option.value = host.alias;
option.textContent = host.alias + (host.hostName ? ' (' + host.hostName + ')' : '');
hostSelect.appendChild(option);
}
}).catch((error) => output.textContent = error.stack || String(error));
document.getElementById('connect').addEventListener('click', async () => {
output.textContent = 'Connecting...';
try {
const result = await ipcRenderer.invoke('gitzone:connect', {
hostAlias: hostSelect.value,
workspacePath: document.getElementById('workspace').value,
remoteTheiaPort: Number(document.getElementById('theiaPort').value),
openCodePort: Number(document.getElementById('opencodePort').value),
});
output.textContent = 'Opened ' + result.url;
} catch (error) {
output.textContent = error.stack || String(error);
}
});
</script>
</body>
</html>`;
void new GitZoneIdeElectronShell().start();