feat(win): hermes wsl installer hooks
This commit is contained in:
@@ -2,9 +2,11 @@ import type { BrowserWindow } from 'electron';
|
||||
import type { GatewayManager } from '../gateway/manager';
|
||||
import type { ClawHubService } from '../gateway/clawhub';
|
||||
import type { HostEventBus } from './event-bus';
|
||||
import type { HermesManager } from '../hermes/manager';
|
||||
|
||||
export interface HostApiContext {
|
||||
gatewayManager: GatewayManager;
|
||||
hermesManager: HermesManager;
|
||||
clawHubService: ClawHubService;
|
||||
eventBus: HostEventBus;
|
||||
mainWindow: BrowserWindow | null;
|
||||
|
||||
31
electron/api/routes/hermes.ts
Normal file
31
electron/api/routes/hermes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { sendJson } from '../route-utils';
|
||||
|
||||
export async function handleHermesRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
if (url.pathname === '/api/hermes/status' && req.method === 'GET') {
|
||||
const status = await ctx.hermesManager.refreshStatus();
|
||||
sendJson(res, 200, { success: true, status });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/hermes/install' && req.method === 'POST') {
|
||||
const result = await ctx.hermesManager.install();
|
||||
sendJson(res, result.success ? 200 : 500, { ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/hermes/start' && req.method === 'POST') {
|
||||
const result = await ctx.hermesManager.startGateway();
|
||||
sendJson(res, result.success ? 200 : 500, { ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { extensionRegistry } from '../extensions/registry';
|
||||
import type { HostApiContext } from './context';
|
||||
import { handleAppRoutes } from './routes/app';
|
||||
import { handleGatewayRoutes } from './routes/gateway';
|
||||
import { handleHermesRoutes } from './routes/hermes';
|
||||
import { handleSettingsRoutes } from './routes/settings';
|
||||
import { handleProviderRoutes } from './routes/providers';
|
||||
import { handleAgentRoutes } from './routes/agents';
|
||||
@@ -29,6 +30,7 @@ type RouteHandler = (
|
||||
const coreRouteHandlers: RouteHandler[] = [
|
||||
handleAppRoutes,
|
||||
handleGatewayRoutes,
|
||||
handleHermesRoutes,
|
||||
handleSettingsRoutes,
|
||||
handleProviderRoutes,
|
||||
handleAgentRoutes,
|
||||
|
||||
97
electron/hermes/manager.ts
Normal file
97
electron/hermes/manager.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { getResourcesDir } from '../utils/paths';
|
||||
|
||||
export type HermesInstallState = 'unknown' | 'not_installed' | 'installed' | 'wsl_missing' | 'error';
|
||||
|
||||
export type HermesRunState = 'stopped' | 'running' | 'error';
|
||||
|
||||
export interface HermesStatus {
|
||||
installState: HermesInstallState;
|
||||
runState: HermesRunState;
|
||||
version?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function runPowerShell(scriptPath: string): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null }> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
scriptPath,
|
||||
], { windowsHide: true });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (d) => { stdout += String(d); });
|
||||
child.stderr.on('data', (d) => { stderr += String(d); });
|
||||
child.on('close', (code) => {
|
||||
resolve({ success: code === 0, stdout, stderr, exitCode: code });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runWsl(args: string[]): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null }> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('wsl.exe', args, { windowsHide: true });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (d) => { stdout += String(d); });
|
||||
child.stderr.on('data', (d) => { stderr += String(d); });
|
||||
child.on('close', (code) => resolve({ success: code === 0, stdout, stderr, exitCode: code }));
|
||||
});
|
||||
}
|
||||
|
||||
export class HermesManager {
|
||||
private status: HermesStatus = { installState: 'unknown', runState: 'stopped' };
|
||||
|
||||
getStatus(): HermesStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
async refreshStatus(): Promise<HermesStatus> {
|
||||
if (process.platform !== 'win32') {
|
||||
this.status = { installState: 'error', runState: 'stopped', error: 'Hermes Windows integration requires WSL2' };
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
const wslCheck = await runWsl(['-e', 'bash', '-lc', 'echo ok']);
|
||||
if (!wslCheck.success) {
|
||||
this.status = { installState: 'wsl_missing', runState: 'stopped', error: wslCheck.stderr.trim() || 'WSL2 not available' };
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
const version = await runWsl(['-e', 'bash', '-lc', 'command -v hermes >/dev/null 2>&1 && hermes --version || hermes version || true']);
|
||||
const raw = (version.stdout || version.stderr).trim();
|
||||
if (!raw) {
|
||||
this.status = { installState: 'not_installed', runState: 'stopped' };
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
this.status = { installState: 'installed', runState: this.status.runState, version: raw };
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
async install(): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null }> {
|
||||
if (process.platform !== 'win32') {
|
||||
return { success: false, stdout: '', stderr: 'Windows only', exitCode: 1 };
|
||||
}
|
||||
const scriptPath = join(getResourcesDir(), 'hermes', 'install-hermes.ps1');
|
||||
const result = await runPowerShell(scriptPath);
|
||||
await this.refreshStatus();
|
||||
return result;
|
||||
}
|
||||
|
||||
async startGateway(): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null }> {
|
||||
if (process.platform !== 'win32') {
|
||||
return { success: false, stdout: '', stderr: 'Windows only', exitCode: 1 };
|
||||
}
|
||||
const scriptPath = join(getResourcesDir(), 'hermes', 'start-hermes.ps1');
|
||||
const result = await runPowerShell(scriptPath);
|
||||
this.status = { ...this.status, runState: result.success ? 'running' : 'error', error: result.success ? undefined : result.stderr.trim() };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { app, BrowserWindow, nativeImage, session, shell } from 'electron';
|
||||
import type { Server } from 'node:http';
|
||||
import { join } from 'path';
|
||||
import { GatewayManager } from '../gateway/manager';
|
||||
import { HermesManager } from '../hermes/manager';
|
||||
import { registerIpcHandlers } from './ipc-handlers';
|
||||
import { createTray } from './tray';
|
||||
import { createMenu } from './menu';
|
||||
@@ -121,6 +122,7 @@ const gotTheLock = gotElectronLock && gotFileLock;
|
||||
// Global references
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let gatewayManager!: GatewayManager;
|
||||
let hermesManager!: HermesManager;
|
||||
let clawHubService!: ClawHubService;
|
||||
let hostEventBus!: HostEventBus;
|
||||
let hostApiServer: Server | null = null;
|
||||
@@ -339,6 +341,7 @@ async function initialize(): Promise<void> {
|
||||
|
||||
hostApiServer = startHostApiServer({
|
||||
gatewayManager,
|
||||
hermesManager,
|
||||
clawHubService,
|
||||
eventBus: hostEventBus,
|
||||
mainWindow: window,
|
||||
@@ -538,6 +541,7 @@ if (gotTheLock) {
|
||||
}
|
||||
|
||||
gatewayManager = new GatewayManager();
|
||||
hermesManager = new HermesManager();
|
||||
clawHubService = new ClawHubService();
|
||||
hostEventBus = new HostEventBus();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user