Initialize remote IDE scaffold

This commit is contained in:
2026-05-10 14:08:25 +00:00
commit 138eea3231
97 changed files with 21129 additions and 0 deletions
@@ -0,0 +1,16 @@
appId: global.foss.git-zone.ide
productName: Git.Zone IDE
directories:
output: dist
files:
- dist_ts/**/*
- package.json
linux:
target:
- AppImage
mac:
target:
- dmg
win:
target:
- nsis
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@git.zone/ide-electron-shell",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist_ts/main.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "pnpm run build && electron dist_ts/main.js",
"package": "pnpm run build && electron-builder --config electron-builder.yml"
},
"dependencies": {
"@git.zone/ide-protocol": "workspace:*",
"@git.zone/ide-server-installer": "workspace:*",
"@git.zone/ide-ssh": "workspace:*",
"electron": "^42.0.1"
},
"devDependencies": {
"electron-builder": "^26.8.1"
},
"files": ["dist_ts/**/*", "electron-builder.yml"]
}
+186
View File
@@ -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 };
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist_ts",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["ts/**/*.ts"]
}