Initialize remote IDE scaffold
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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();
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as electron from 'electron';
|
||||
import * as ideServerInstaller from '@git.zone/ide-server-installer';
|
||||
import * as ideSsh from '@git.zone/ide-ssh';
|
||||
|
||||
export { crypto, electron, ideServerInstaller, ideSsh };
|
||||
Reference in New Issue
Block a user