import * as childProcess from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const workspaceDir = path.resolve(packageDir, '../..'); const electronBin = path.join(workspaceDir, 'node_modules', '.bin', 'electron'); const electronEntry = path.join(packageDir, 'dist_ts', 'main.js'); const getUid = () => process.getuid?.() ?? 1000; const isSocket = (filePath) => { if (!filePath) { return false; } try { return fs.statSync(filePath).isSocket(); } catch { return false; } }; const parseAgentEnv = () => { const agentEnvPath = path.join(os.homedir(), '.ssh', 'agent.env'); try { const agentEnvText = fs.readFileSync(agentEnvPath, 'utf8'); const socketMatch = agentEnvText.match(/(?:^|\n)SSH_AUTH_SOCK=([^;\n]+)/); return socketMatch?.[1]; } catch { return undefined; } }; const findTmpAgentSocket = () => { let candidates = []; try { const tmpEntries = fs.readdirSync('/tmp', { withFileTypes: true }); for (const entry of tmpEntries) { if (!entry.isDirectory() || !entry.name.startsWith('ssh-')) { continue; } const directory = path.join('/tmp', entry.name); try { for (const socketEntry of fs.readdirSync(directory, { withFileTypes: true })) { if (!socketEntry.name.startsWith('agent.')) { continue; } const socketPath = path.join(directory, socketEntry.name); if (isSocket(socketPath)) { candidates.push({ path: socketPath, mtimeMs: fs.statSync(socketPath).mtimeMs }); } } } catch {} } } catch {} candidates = candidates.sort((a, b) => b.mtimeMs - a.mtimeMs); return candidates[0]?.path; }; const resolveSshAuthSock = (env) => { if (isSocket(env.SSH_AUTH_SOCK)) { return env.SSH_AUTH_SOCK; } const agentEnvSocket = parseAgentEnv(); if (isSocket(agentEnvSocket)) { return agentEnvSocket; } return findTmpAgentSocket(); }; const commandExists = (command) => { try { childProcess.execFileSync('which', [command], { stdio: 'ignore' }); return true; } catch { return false; } }; const isSwayRunning = () => { try { childProcess.execFileSync('pgrep', ['-x', 'sway'], { stdio: 'ignore' }); return true; } catch { return false; } }; const getWaylandSocket = () => { const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`; try { return fs.readdirSync(runtimeDir).find((file) => file.startsWith('wayland-')); } catch { return undefined; } }; const removeStaleWaylandSocket = (socket) => { if (!socket || isSwayRunning()) { return; } const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`; for (const file of [socket, `${socket}.lock`]) { try { fs.unlinkSync(path.join(runtimeDir, file)); } catch {} } }; const detectLinuxDisplay = () => { if (process.env.WAYLAND_DISPLAY) { return { type: 'wayland', socket: process.env.WAYLAND_DISPLAY }; } if (process.env.DISPLAY) { return { type: 'x11' }; } const existingSocket = getWaylandSocket(); if (existingSocket) { if (isSwayRunning()) { return { type: 'wayland', socket: existingSocket }; } removeStaleWaylandSocket(existingSocket); } try { const driDevices = fs.readdirSync('/dev/dri'); if (driDevices.some((device) => device.startsWith('card'))) { return { type: 'drm' }; } } catch {} return { type: 'none' }; }; const startSway = async () => { if (!commandExists('sway')) { throw new Error('No display is available and sway is not installed.'); } const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`; const configDir = path.join(runtimeDir, 'gitzone-ide-sway'); const configPath = path.join(configDir, 'config'); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( configPath, ['default_border none', 'default_floating_border none', 'hide_edge_borders both', ''].join('\n'), ); console.log('Starting Sway for Git.Zone IDE...'); const swayProcess = childProcess.spawn('sway', ['--unsupported-gpu', '-c', configPath], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir, XDG_SESSION_TYPE: 'wayland', LIBSEAT_BACKEND: process.env.LIBSEAT_BACKEND || 'seatd', WLR_LIBINPUT_NO_DEVICES: '1', }, }); let stderr = ''; swayProcess.stderr?.on('data', (chunk) => { stderr += chunk.toString(); }); const startedAt = Date.now(); while (Date.now() - startedAt < 10000) { const socket = getWaylandSocket(); if (socket) { console.log(`Sway started on ${socket}.`); return { process: swayProcess, socket }; } await new Promise((resolve) => setTimeout(resolve, 250)); } try { swayProcess.kill('SIGTERM'); } catch {} throw new Error(`Sway did not start within 10s.${stderr ? `\n${stderr}` : ''}`); }; const stopChildProcess = (child) => { if (!child || child.killed) { return; } try { child.kill('SIGTERM'); } catch {} }; const launchElectron = (args, env, swayProcess) => { const electronProcess = childProcess.spawn(electronBin, args, { cwd: packageDir, stdio: 'inherit', env, }); const cleanup = () => stopChildProcess(swayProcess); process.once('SIGINT', () => { cleanup(); electronProcess.kill('SIGINT'); process.exit(0); }); process.once('SIGTERM', () => { cleanup(); electronProcess.kill('SIGTERM'); process.exit(0); }); electronProcess.on('exit', (code, signal) => { cleanup(); process.exit(code ?? (signal ? 1 : 0)); }); }; const main = async () => { const electronArgs = [electronEntry, '--no-sandbox', ...process.argv.slice(2)]; const electronEnv = { ...process.env }; let swayProcess; electronEnv.GITZONE_IDE_NODE_BINARY = process.execPath; const sshAuthSock = resolveSshAuthSock(electronEnv); if (sshAuthSock) { electronEnv.SSH_AUTH_SOCK = sshAuthSock; console.log(`Using SSH agent socket: ${sshAuthSock}`); } else { console.warn('No SSH agent socket found. System ssh will only use identity files and default keys.'); } if (os.platform() === 'linux') { const display = detectLinuxDisplay(); console.log(`Electron display mode: ${display.type}`); if (display.type === 'wayland') { electronEnv.WAYLAND_DISPLAY = display.socket; electronEnv.XDG_RUNTIME_DIR = electronEnv.XDG_RUNTIME_DIR || `/run/user/${getUid()}`; electronArgs.push('--ozone-platform=wayland'); } else if (display.type === 'drm') { const sway = await startSway(); swayProcess = sway.process; electronEnv.WAYLAND_DISPLAY = sway.socket; electronEnv.XDG_RUNTIME_DIR = electronEnv.XDG_RUNTIME_DIR || `/run/user/${getUid()}`; electronEnv.RUNTIME_ENV = 'sway'; electronArgs.push('--ozone-platform=wayland'); console.log('Git.Zone IDE is available on the local Sway display.'); } else if (display.type === 'none') { throw new Error('No display is available. Start X11/Wayland, install sway, or run under xvfb-run.'); } } launchElectron(electronArgs, electronEnv, swayProcess); }; main().catch((error) => { console.error(error.stack || error.message || String(error)); process.exit(1); });