diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index 47892f3..f9a3fd5 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -279,6 +279,18 @@ namespace HermesHubSetup File.Copy(launcherSrc, Path.Combine(HermesHome, "HermesHub.exe"), true); } + string webLauncherSrc = Path.Combine(sourceRoot, @"launcher\HermesHubWeb.exe"); + if (!File.Exists(webLauncherSrc)) + { + webLauncherSrc = Path.Combine(sourceRoot, "HermesHubWeb.exe"); + } + + if (File.Exists(webLauncherSrc)) + { + File.Copy(webLauncherSrc, Path.Combine(TargetInstallDir, "HermesHubWeb.exe"), true); + File.Copy(webLauncherSrc, Path.Combine(HermesHome, "HermesHubWeb.exe"), true); + } + // Copy Setup.exe itself to target dir for uninstaller/repair string setupSrc = Process.GetCurrentProcess().MainModule.FileName; if (File.Exists(setupSrc)) @@ -419,6 +431,12 @@ namespace HermesHubSetup try { File.Delete(homeExe); } catch { } } + string homeWebExe = Path.Combine(HermesHome, "HermesHubWeb.exe"); + if (File.Exists(homeWebExe)) + { + try { File.Delete(homeWebExe); } catch { } + } + // Remove plugin string pluginDir = Path.Combine(HermesHome, @"plugins\antigravity-provider"); if (Directory.Exists(pluginDir)) @@ -504,19 +522,53 @@ namespace HermesHubSetup try { string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs); - string shortcutPath = Path.Combine(startMenu, "Hermes Hub.lnk"); - string targetExe = Path.Combine(TargetInstallDir, "HermesHub.exe"); + string targetWebExe = Path.Combine(TargetInstallDir, "HermesHubWeb.exe"); + if (!File.Exists(targetWebExe)) targetWebExe = Path.Combine(HermesHome, "HermesHubWeb.exe"); + + string targetDesktopExe = Path.Combine(TargetInstallDir, "HermesHub.exe"); + if (!File.Exists(targetDesktopExe)) targetDesktopExe = Path.Combine(HermesHome, "HermesHub.exe"); Type shellType = Type.GetTypeFromProgID("WScript.Shell"); if (shellType != null) { dynamic shell = Activator.CreateInstance(shellType); - dynamic shortcut = shell.CreateShortcut(shortcutPath); - shortcut.TargetPath = targetExe; - shortcut.WorkingDirectory = TargetInstallDir; - shortcut.Description = "Multi-Agent & Multi-Provider Control Hub for Hermes Agent"; - shortcut.IconLocation = targetExe + ",0"; - shortcut.Save(); + + // 1. Web Application Window Shortcut (Primary Web App mode) + if (File.Exists(targetWebExe)) + { + string webShortcutPath = Path.Combine(startMenu, "Hermes Hub (Web).lnk"); + dynamic webShortcut = shell.CreateShortcut(webShortcutPath); + webShortcut.TargetPath = targetWebExe; + webShortcut.WorkingDirectory = TargetInstallDir; + webShortcut.Description = "Hermes Hub — Web Application Window"; + webShortcut.IconLocation = targetWebExe + ",0"; + webShortcut.Save(); + } + + // 2. Native Desktop Shortcut (CustomTkinter GUI) + if (File.Exists(targetDesktopExe)) + { + string desktopShortcutPath = Path.Combine(startMenu, "Hermes Hub (Desktop).lnk"); + dynamic desktopShortcut = shell.CreateShortcut(desktopShortcutPath); + desktopShortcut.TargetPath = targetDesktopExe; + desktopShortcut.WorkingDirectory = TargetInstallDir; + desktopShortcut.Description = "Hermes Hub — Native Desktop App"; + desktopShortcut.IconLocation = targetDesktopExe + ",0"; + desktopShortcut.Save(); + } + + // 3. Standard "Hermes Hub.lnk" shortcut + string standardTarget = File.Exists(targetWebExe) ? targetWebExe : targetDesktopExe; + if (File.Exists(standardTarget)) + { + string standardShortcutPath = Path.Combine(startMenu, "Hermes Hub.lnk"); + dynamic standardShortcut = shell.CreateShortcut(standardShortcutPath); + standardShortcut.TargetPath = standardTarget; + standardShortcut.WorkingDirectory = TargetInstallDir; + standardShortcut.Description = "Multi-Agent & Multi-Provider Control Hub for Hermes Agent"; + standardShortcut.IconLocation = standardTarget + ",0"; + standardShortcut.Save(); + } } } catch { } @@ -527,8 +579,18 @@ namespace HermesHubSetup try { string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs); - string shortcutPath = Path.Combine(startMenu, "Hermes Hub.lnk"); - if (File.Exists(shortcutPath)) File.Delete(shortcutPath); + string[] shortcuts = new string[] + { + "Hermes Hub.lnk", + "Hermes Hub (Web).lnk", + "Hermes Hub (Desktop).lnk", + "Hermes Hub Web.lnk" + }; + foreach (string sc in shortcuts) + { + string p = Path.Combine(startMenu, sc); + if (File.Exists(p)) try { File.Delete(p); } catch { } + } } catch { } } diff --git a/installer/build_installer.ps1 b/installer/build_installer.ps1 index 81b7cdb..623135a 100644 --- a/installer/build_installer.ps1 +++ b/installer/build_installer.ps1 @@ -12,6 +12,20 @@ $DistDir = Join-Path $RepoRoot "dist" New-Item -ItemType Directory -Path $DistDir -Force | Out-Null $OutFile = Join-Path $DistDir "HermesHubSetup.exe" +$LauncherDir = Join-Path $RepoRoot "launcher" +$HubCs = Join-Path $LauncherDir "HermesHub.cs" +$HubWebCs = Join-Path $LauncherDir "HermesHubWeb.cs" + +Write-Host "Compiling native launchers..." -ForegroundColor Cyan +if (Test-Path $HubCs) { + $HubExe = Join-Path $LauncherDir "HermesHub.exe" + & $CscPath /target:winexe /out:"$HubExe" /r:System.Windows.Forms.dll /r:System.Drawing.dll "$HubCs" +} +if (Test-Path $HubWebCs) { + $HubWebExe = Join-Path $LauncherDir "HermesHubWeb.exe" + & $CscPath /target:winexe /out:"$HubWebExe" /r:System.Windows.Forms.dll /r:System.Drawing.dll "$HubWebCs" +} + Write-Host "Compiling HermesHubSetup.exe..." -ForegroundColor Cyan & $CscPath /target:winexe /out:"$OutFile" /r:System.Windows.Forms.dll /r:System.Drawing.dll "$SourceFile" diff --git a/installer/install-linux.sh b/installer/install-linux.sh new file mode 100644 index 0000000..e65acd5 --- /dev/null +++ b/installer/install-linux.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# ============================================================================== +# Hermes Hub — Linux Installation Script +# Mirrors plugin files, registers .desktop entry, creates application launcher, +# and supports both desktop and headless server environments. +# ============================================================================== + +set -e + +HUB_VERSION="0.1.1" +DEFAULT_HERMES_HOME="$HOME/.hermes" +HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "======================================================================" +echo " HERMES HUB INSTALLER (Linux / POSIX) " +echo "======================================================================" +echo "Version : $HUB_VERSION" +echo "Hermes Home : $HERMES_HOME" +echo "Source Root : $REPO_ROOT" +echo "" + +# 1. Check / Discover Python Runtime and Hermes Environment +echo "[1/6] Checking Python and Hermes environment..." +PYTHON_BIN="" + +if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then + PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" +elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then + PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif command -v python3 >/dev/null 2>&1; then + PYTHON_BIN="$(command -v python3)" +elif command -v python >/dev/null 2>&1; then + PYTHON_BIN="$(command -v python)" +fi + +if [ -z "$PYTHON_BIN" ]; then + echo "❌ Error: Python 3 not found on system. Please install Python 3.9+." >&2 + exit 10 +fi + +PY_VER="$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" +echo " Using Python: $PYTHON_BIN (v$PY_VER)" + +# Ensure Hermes Home Directory Structure +mkdir -p "$HERMES_HOME" +mkdir -p "$HERMES_HOME/config" +mkdir -p "$HERMES_HOME/plugins/antigravity-provider/src" +mkdir -p "$HERMES_HOME/plugins/antigravity-provider/assets" +mkdir -p "$HOME/.local/bin" +mkdir -p "$HOME/.local/share/applications" + +# 2. Check and Install Dependencies +echo "[2/6] Verifying Python dependencies..." +DEPS_OK=true +"$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml; print('DEPS_OK')" >/dev/null 2>&1 || DEPS_OK=false + +if [ "$DEPS_OK" != "true" ]; then + echo " Installing required packages (fastapi, uvicorn, pydantic, psutil, pyyaml)..." + "$PYTHON_BIN" -m pip install --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || { + echo "⚠️ Warning: pip install returned non-zero code. Trying with --user..." + "$PYTHON_BIN" -m pip install --user --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || true + } +fi + +# 3. Mirror Plugin Files to ~/.hermes/plugins/antigravity-provider (with cleanup of stale files) +echo "[3/6] Deploying plugin source files (mirrored)..." +PLUGIN_SRC="$REPO_ROOT/src/antigravity_provider" +PLUGIN_DST="$HERMES_HOME/plugins/antigravity-provider/src/antigravity_provider" + +if [ -d "$PLUGIN_SRC" ]; then + mkdir -p "$PLUGIN_DST" + # Use rsync with --delete if available, or python mirroring + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete --exclude='__pycache__' --exclude='*.pyc' "$PLUGIN_SRC/" "$PLUGIN_DST/" + else + "$PYTHON_BIN" -c " +import os, shutil +src = r'$PLUGIN_SRC' +dst = r'$PLUGIN_DST' +if os.path.exists(dst): + shutil.rmtree(dst) +shutil.copytree(src, dst, ignore=shutil.ignore_patterns('__pycache__', '*.pyc')) +" + fi +else + echo "❌ Error: Source directory $PLUGIN_SRC not found!" >&2 + exit 12 +fi + +# Deploy Assets +if [ -d "$REPO_ROOT/assets" ]; then + mkdir -p "$HERMES_HOME/plugins/antigravity-provider/assets" + cp -r "$REPO_ROOT/assets/"* "$HERMES_HOME/plugins/antigravity-provider/assets/" 2>/dev/null || true +fi + +# 4. Write Deployment Manifest +echo "[4/6] Writing deployment manifest..." +MANIFEST_FILE="$HERMES_HOME/plugins/antigravity-provider/deployment_manifest.json" +GIT_COMMIT="$(cd "$REPO_ROOT" 2>/dev/null && git rev-parse --short HEAD 2>/dev/null || echo 'fb23bff')" +TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u)" + +cat < "$MANIFEST_FILE" +{ + "version": "$HUB_VERSION", + "deployed_at": "$TIMESTAMP", + "git_commit": "$GIT_COMMIT", + "platform": "linux" +} +EOF + +# Deploy Template Config only if not existing +if [ ! -f "$HERMES_HOME/config/router_profiles.yaml" ] && [ -f "$REPO_ROOT/config/router_profiles.example.yaml" ]; then + echo " Installing default router_profiles.yaml from template..." + cp "$REPO_ROOT/config/router_profiles.example.yaml" "$HERMES_HOME/config/router_profiles.yaml" +else + echo " Preserving existing user router_profiles.yaml." +fi + +# 5. Deploy Launcher Executable and .desktop Entry +echo "[5/6] Creating application launcher and .desktop entry..." +LAUNCHER_SRC="$REPO_ROOT/launcher/hermes-hub-web.sh" +if [ ! -f "$LAUNCHER_SRC" ]; then + LAUNCHER_SRC="$SCRIPT_DIR/hermes-hub-web.sh" +fi + +LAUNCHER_BIN="$HOME/.local/bin/hermes-hub-web" +cp "$LAUNCHER_SRC" "$LAUNCHER_BIN" +chmod +x "$LAUNCHER_BIN" + +# Also place in ~/.hermes/bin for convenience +mkdir -p "$HERMES_HOME/bin" +cp "$LAUNCHER_SRC" "$HERMES_HOME/bin/hermes-hub-web" +chmod +x "$HERMES_HOME/bin/hermes-hub-web" + +# Create .desktop file +DESKTOP_FILE="$HOME/.local/share/applications/hermes-hub-web.desktop" +ICON_PATH="$HERMES_HOME/plugins/antigravity-provider/assets/branding/app/HermesHub.ico" +if [ ! -f "$ICON_PATH" ]; then + ICON_PATH="utilities-terminal" +fi + +cat < "$DESKTOP_FILE" +[Desktop Entry] +Version=1.0 +Type=Application +Name=Hermes Hub Web +GenericName=Multi-Agent & Multi-Provider Control Hub +Comment=Multi-Agent & Multi-Provider Control Hub for Hermes Agent +Exec=$LAUNCHER_BIN +Icon=$ICON_PATH +Terminal=false +Categories=Development;Utility; +StartupNotify=true +StartupWMClass=hermes-hub-web +EOF + +chmod +x "$DESKTOP_FILE" +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true +fi + +# 6. Post-install Smoke Test Verification +echo "[6/6] Running post-install verification smoke test..." +"$PYTHON_BIN" -c " +import sys +sys.path.insert(0, '$HERMES_HOME/plugins/antigravity-provider/src') +import antigravity_provider.router.web.server +print('HERMES_HUB_LINUX_VERIFY_OK') +" || { + echo "❌ Error: Post-install verification failed!" >&2 + exit 14 +} + +echo "" +echo "======================================================================" +echo " HERMES HUB SUCCESSFULLY INSTALLED ON LINUX! " +echo "======================================================================" +echo "Application Launcher : $LAUNCHER_BIN" +echo "Desktop Shortcut : $DESKTOP_FILE" +echo "" +echo "To launch the Web Application window:" +echo " $LAUNCHER_BIN" +echo "" +echo "Or open 'Hermes Hub Web' from your Applications menu." +echo "======================================================================" +exit 0 diff --git a/installer/uninstall-linux.sh b/installer/uninstall-linux.sh new file mode 100644 index 0000000..e7e8fa3 --- /dev/null +++ b/installer/uninstall-linux.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# ============================================================================== +# Hermes Hub — Linux Uninstaller +# Removes application files, plugin integration, launcher and .desktop shortcuts. +# Preserves user data and credentials by default unless --purge-user-data is passed. +# ============================================================================== + +set -e + +DEFAULT_HERMES_HOME="$HOME/.hermes" +HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}" + +PURGE_USER_DATA=false +for arg in "$@"; do + if [ "$arg" = "--purge-user-data" ] || [ "$arg" = "-p" ]; then + PURGE_USER_DATA=true + fi +done + +echo "======================================================================" +echo " HERMES HUB UNINSTALLER (Linux / POSIX) " +echo "======================================================================" +echo "Hermes Home : $HERMES_HOME" +echo "" + +# 1. Remove Plugin Integration +echo "[1/3] Removing plugin integration..." +if [ -d "$HERMES_HOME/plugins/antigravity-provider" ]; then + rm -rf "$HERMES_HOME/plugins/antigravity-provider" + echo " Removed $HERMES_HOME/plugins/antigravity-provider" +fi + +# 2. Remove Launchers and Shortcuts +echo "[2/3] Removing application launchers and desktop entries..." +rm -f "$HOME/.local/bin/hermes-hub-web" +rm -f "$HERMES_HOME/bin/hermes-hub-web" +rm -f "$HOME/.local/share/applications/hermes-hub-web.desktop" + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true +fi + +# 3. User Data Handling +if [ "$PURGE_USER_DATA" = "true" ]; then + echo "[3/3] Purging user data (--purge-user-data specified)..." + rm -f "$HERMES_HOME/config/router_profiles.yaml" + rm -rf "$HERMES_HOME/agy_profiles" + rm -rf "$HERMES_HOME/codex_profiles" + rm -rf "$HERMES_HOME/opencode_profiles" + echo " User configuration and profiles purged." +else + echo "[3/3] Preserving user data and credentials." + echo " Your router profiles, auth keys, and settings in $HERMES_HOME remain intact." +fi + +echo "" +echo "======================================================================" +echo " HERMES HUB UNINSTALLED SUCCESSFULLY FROM LINUX " +echo "======================================================================" +exit 0 diff --git a/launcher/HermesHub.exe b/launcher/HermesHub.exe index c869c42..7e84bcd 100644 Binary files a/launcher/HermesHub.exe and b/launcher/HermesHub.exe differ diff --git a/launcher/HermesHubWeb.cs b/launcher/HermesHubWeb.cs new file mode 100644 index 0000000..8c80783 --- /dev/null +++ b/launcher/HermesHubWeb.cs @@ -0,0 +1,326 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace HermesHub +{ + public static class WebLauncher + { + [STAThread] + public static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string hermesHome = Environment.GetEnvironmentVariable("HERMES_HOME"); + if (string.IsNullOrEmpty(hermesHome)) + { + hermesHome = Path.Combine(localAppData, "hermes"); + } + + // 1. Determine Web API Host and Port from hub_settings.json or default + string host = "127.0.0.1"; + int port = 5800; + string settingsFile = Path.Combine(hermesHome, "hub_settings.json"); + if (File.Exists(settingsFile)) + { + try + { + string json = File.ReadAllText(settingsFile, Encoding.UTF8); + int pIdx = json.IndexOf("\"web_api_port\":", StringComparison.OrdinalIgnoreCase); + if (pIdx >= 0) + { + int colon = json.IndexOf(':', pIdx); + int comma = json.IndexOfAny(new char[] { ',', '}', '\r', '\n' }, colon + 1); + if (colon >= 0 && comma > colon) + { + string pStr = json.Substring(colon + 1, comma - colon - 1).Trim(); + int parsedPort; + if (int.TryParse(pStr, out parsedPort) && parsedPort > 0 && parsedPort < 65536) + { + port = parsedPort; + } + } + } + int hIdx = json.IndexOf("\"web_api_host\":", StringComparison.OrdinalIgnoreCase); + if (hIdx >= 0) + { + int q1 = json.IndexOf('"', hIdx + 15); + int q2 = json.IndexOf('"', q1 + 1); + if (q1 >= 0 && q2 > q1) + { + string hStr = json.Substring(q1 + 1, q2 - q1 - 1).Trim(); + if (!string.IsNullOrEmpty(hStr) && hStr != "0.0.0.0") + { + host = hStr; + } + } + } + } + catch { } + } + + string targetUrl = string.Format("http://{0}:{1}/", host, port); + string healthUrl = string.Format("http://{0}:{1}/api/health", host, port); + + // 2. Check if server is already running and healthy + bool serverWasAlreadyRunning = IsServerHealthy(healthUrl); + Process serverProcess = null; + + if (!serverWasAlreadyRunning) + { + // Find Python + string hermesPythonW = Path.Combine(hermesHome, @"hermes-agent\venv\Scripts\pythonw.exe"); + string hermesPython = Path.Combine(hermesHome, @"hermes-agent\venv\Scripts\python.exe"); + string exe = File.Exists(hermesPythonW) ? hermesPythonW : hermesPython; + + if (!File.Exists(exe)) + { + MessageBox.Show( + "Hermes Python not found:\n" + exe + "\n\nPlease install Hermes Agent first.", + "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + string pluginSrc = Path.Combine(hermesHome, @"plugins\antigravity-provider\src"); + string agentDir = Path.Combine(hermesHome, "hermes-agent"); + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + string hubSrc = Path.GetFullPath(Path.Combine(baseDir, @"..\src")); + + StringBuilder script = new StringBuilder(); + script.AppendLine("import sys"); + if (Directory.Exists(pluginSrc)) + script.AppendLine("sys.path.insert(0, r'" + pluginSrc.Replace('\\', '/') + "')"); + if (Directory.Exists(agentDir)) + script.AppendLine("sys.path.insert(0, r'" + agentDir.Replace('\\', '/') + "')"); + if (Directory.Exists(hubSrc)) + script.AppendLine("sys.path.insert(0, r'" + hubSrc.Replace('\\', '/') + "')"); + script.AppendLine("from antigravity_provider.router.web.server import run_server"); + script.AppendLine("run_server()"); + + string entryScript = Path.Combine(hermesHome, "hermes_hub_web_entry.py"); + try + { + File.WriteAllText(entryScript, script.ToString(), Encoding.UTF8); + } + catch (Exception ex) + { + MessageBox.Show("Cannot write entry script:\n" + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + ProcessStartInfo serverPsi = new ProcessStartInfo(); + serverPsi.FileName = exe; + serverPsi.Arguments = "\"" + entryScript + "\""; + serverPsi.WorkingDirectory = hermesHome; + serverPsi.UseShellExecute = false; + serverPsi.CreateNoWindow = true; + serverPsi.WindowStyle = ProcessWindowStyle.Hidden; + + try + { + serverProcess = Process.Start(serverPsi); + } + catch (Exception ex) + { + MessageBox.Show("Failed to start Hermes Hub web server:\n" + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // 3. Poll /api/health with timeout (up to 15s) + bool ready = false; + for (int i = 0; i < 75; i++) + { + if (IsServerHealthy(healthUrl)) + { + ready = true; + break; + } + if (serverProcess.HasExited) + { + MessageBox.Show("Hermes Hub web server process terminated unexpectedly.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + Thread.Sleep(200); + } + + if (!ready) + { + MessageBox.Show("Hermes Hub web server failed to respond within 15 seconds.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + if (serverProcess != null && !serverProcess.HasExited) + { + try { serverProcess.Kill(); } catch { } + } + return; + } + } + + // 4. Locate browser in strict priority: Edge -> Chrome -> Chromium registry -> Fallback + string browserPath = FindChromiumBrowser(); + Process browserProc = null; + + if (!string.IsNullOrEmpty(browserPath)) + { + ProcessStartInfo browserPsi = new ProcessStartInfo(); + browserPsi.FileName = browserPath; + browserPsi.Arguments = string.Format("--app=\"{0}\" --window-size=1400,900", targetUrl); + browserPsi.UseShellExecute = false; + + try + { + browserProc = Process.Start(browserPsi); + } + catch (Exception ex) + { + MessageBox.Show( + "Не удалось запустить браузер в режиме приложения:\n" + ex.Message + "\n\nОткрываю стандартный браузер.", + "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Warning); + Process.Start(targetUrl); + } + } + else + { + // No Chromium browser found + MessageBox.Show( + "Браузер с поддержкой режима приложения (Microsoft Edge или Google Chrome) не найден.\n\n" + + "Интерфейс будет открыт в стандартном браузере с адресной строкой.", + "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Information); + try + { + Process.Start(targetUrl); + } + catch (Exception ex) + { + MessageBox.Show("Не удалось открыть браузер: " + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // 5. Server Lifecycle: + // If the server was started by this launcher session and browser is tracked, + // wait for browser window to close, then gracefully terminate server process. + if (!serverWasAlreadyRunning && serverProcess != null && !serverProcess.HasExited && browserProc != null) + { + try + { + browserProc.WaitForExit(); + } + catch { } + + try + { + if (!serverProcess.HasExited) + { + serverProcess.Kill(); + } + } + catch { } + } + } + + public static bool IsServerHealthy(string url) + { + try + { + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); + req.Timeout = 800; + req.Method = "GET"; + using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) + { + if (resp.StatusCode == HttpStatusCode.OK) + { + using (StreamReader r = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) + { + string txt = r.ReadToEnd(); + return txt.Contains("\"ok\":true") || txt.Contains("\"ok\": true"); + } + } + } + } + catch { } + return false; + } + + public static string FindChromiumBrowser() + { + // 1. Microsoft Edge + string[] edgePaths = new string[] + { + @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", + @"C:\Program Files\Microsoft\Edge\Application\msedge.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Edge\Application\msedge.exe") + }; + foreach (string p in edgePaths) + { + if (File.Exists(p)) return p; + } + + string edgeReg = GetAppPathFromRegistry("msedge.exe"); + if (!string.IsNullOrEmpty(edgeReg) && File.Exists(edgeReg)) return edgeReg; + + // 2. Google Chrome + string[] chromePaths = new string[] + { + @"C:\Program Files\Google\Chrome\Application\chrome.exe", + @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\Application\chrome.exe") + }; + foreach (string p in chromePaths) + { + if (File.Exists(p)) return p; + } + + string chromeReg = GetAppPathFromRegistry("chrome.exe"); + if (!string.IsNullOrEmpty(chromeReg) && File.Exists(chromeReg)) return chromeReg; + + // 3. Brave / Vivaldi / Chromium + string[] otherPaths = new string[] + { + @"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"BraveSoftware\Brave-Browser\Application\brave.exe"), + @"C:\Program Files\Vivaldi\Application\vivaldi.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Vivaldi\Application\vivaldi.exe") + }; + foreach (string p in otherPaths) + { + if (File.Exists(p)) return p; + } + + string braveReg = GetAppPathFromRegistry("brave.exe"); + if (!string.IsNullOrEmpty(braveReg) && File.Exists(braveReg)) return braveReg; + + return null; + } + + private static string GetAppPathFromRegistry(string exeName) + { + try + { + string key = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\" + exeName; + using (RegistryKey rk = Registry.LocalMachine.OpenSubKey(key)) + { + if (rk != null) + { + object val = rk.GetValue(null); + if (val != null) return val.ToString(); + } + } + using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(key)) + { + if (rk != null) + { + object val = rk.GetValue(null); + if (val != null) return val.ToString(); + } + } + } + catch { } + return null; + } + } +} diff --git a/launcher/HermesHubWeb.exe b/launcher/HermesHubWeb.exe new file mode 100644 index 0000000..d0434a2 Binary files /dev/null and b/launcher/HermesHubWeb.exe differ diff --git a/launcher/hermes-hub-web.sh b/launcher/hermes-hub-web.sh new file mode 100644 index 0000000..7333d76 --- /dev/null +++ b/launcher/hermes-hub-web.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# ============================================================================== +# Hermes Hub — Linux Web Application Launcher +# Starts the background web server if not already active, waits for /api/health, +# and opens the UI in application window mode (--app=URL) or handles headless mode. +# ============================================================================== + +set -e + +DEFAULT_HERMES_HOME="$HOME/.hermes" +HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}" + +# 1. Discover Python Runtime +PYTHON_BIN="" +if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then + PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" +elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then + PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif command -v python3 >/dev/null 2>&1; then + PYTHON_BIN="$(command -v python3)" +elif command -v python >/dev/null 2>&1; then + PYTHON_BIN="$(command -v python)" +else + echo "❌ Error: Python 3 not found on system." >&2 + exit 1 +fi + +# 2. Configure Environment and Read Settings (Host/Port) +export PYTHONPATH="$HERMES_HOME/plugins/antigravity-provider/src:$HERMES_HOME/hermes-agent:$PYTHONPATH" + +HOST="127.0.0.1" +PORT=5800 + +SETTINGS_FILE="$HERMES_HOME/hub_settings.json" +if [ -f "$SETTINGS_FILE" ]; then + PARSED_PORT="$("$PYTHON_BIN" -c "import json; print(json.load(open('$SETTINGS_FILE')).get('web_api_port', 5800))" 2>/dev/null || echo 5800)" + if [ -n "$PARSED_PORT" ] && [ "$PARSED_PORT" -gt 0 ] 2>/dev/null; then + PORT="$PARSED_PORT" + fi + PARSED_HOST="$("$PYTHON_BIN" -c "import json; h = json.load(open('$SETTINGS_FILE')).get('web_api_host', '127.0.0.1'); print('127.0.0.1' if h in ('0.0.0.0', '') else h)" 2>/dev/null || echo '127.0.0.1')" + if [ -n "$PARSED_HOST" ]; then + HOST="$PARSED_HOST" + fi +fi + +TARGET_URL="http://$HOST:$PORT/" +HEALTH_URL="http://$HOST:$PORT/api/health" + +# Function to check server health +check_health() { + "$PYTHON_BIN" -c " +import urllib.request, json +try: + req = urllib.request.urlopen('$HEALTH_URL', timeout=0.8) + data = json.loads(req.read().decode('utf-8')) + sys.exit(0 if data.get('ok') is True else 1) +except Exception: + import sys + sys.exit(1) +" >/dev/null 2>&1 +} + +# 3. Start Web Server if not already active +SERVER_STARTED_BY_US=false +if ! check_health; then + echo "Starting Hermes Hub Web Server in background..." + LOG_FILE="$HERMES_HOME/hermes_web_server.log" + nohup "$PYTHON_BIN" -m antigravity_provider.router.web > "$LOG_FILE" 2>&1 & + SERVER_PID=$! + SERVER_STARTED_BY_US=true + + # Wait for server readiness (polling /api/health) + READY=false + for i in $(seq 1 75); do + if check_health; then + READY=true + break + fi + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "❌ Error: Web server process terminated unexpectedly. Check $LOG_FILE" >&2 + exit 1 + fi + sleep 0.2 + done + + if [ "$READY" != "true" ]; then + echo "❌ Error: Web server failed to respond at $HEALTH_URL within 15 seconds." >&2 + exit 1 + fi +fi + +# 4. Headless Server Check (SSH / No Graphical Display) +if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then + echo "======================================================================" + echo " Hermes Hub Web Server is running (Headless Mode)" + echo " Web Interface URL: $TARGET_URL" + echo "" + echo " To access the interface from your local computer, forward the port:" + echo " ssh -L $PORT:127.0.0.1:$PORT user@server" + echo "" + echo " Then open in your browser:" + echo " $TARGET_URL" + echo "======================================================================" + exit 0 +fi + +# 5. Graphical Environment: Search for Chromium-based browser in priority order +CHROMIUM_BIN="" + +for b in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge microsoft-edge-stable brave-browser; do + if command -v "$b" >/dev/null 2>&1; then + CHROMIUM_BIN="$(command -v "$b")" + break + fi +done + +if [ -n "$CHROMIUM_BIN" ]; then + # Launch in Application Window mode without address bar, tabs, or menus + "$CHROMIUM_BIN" --app="$TARGET_URL" --window-size=1400,900 "$@" +else + # Fallback to standard default browser + echo "Запуск в обычном браузере: режим приложения (без адресной строки) требует Google Chrome, Chromium или Microsoft Edge." + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$TARGET_URL" + elif command -v sensible-browser >/dev/null 2>&1; then + sensible-browser "$TARGET_URL" + elif command -v firefox >/dev/null 2>&1; then + firefox "$TARGET_URL" + else + echo "Please open $TARGET_URL in your web browser." + fi +fi + +exit 0 diff --git a/scripts/install.ps1 b/scripts/install.ps1 index d7d2884..0e1de1e 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -58,6 +58,11 @@ if (Test-Path $LauncherExe) { Copy-Item -Path $LauncherExe -Destination (Join-Path $TargetDir "HermesHub.exe") -Force Copy-Item -Path $LauncherExe -Destination (Join-Path $HermesHome "HermesHub.exe") -Force } +$WebLauncherExe = Join-Path $RepoRoot "launcher\HermesHubWeb.exe" +if (Test-Path $WebLauncherExe) { + Copy-Item -Path $WebLauncherExe -Destination (Join-Path $TargetDir "HermesHubWeb.exe") -Force + Copy-Item -Path $WebLauncherExe -Destination (Join-Path $HermesHome "HermesHubWeb.exe") -Force +} # 5. Deploy Plugin Integration into Hermes Write-Host "[4/6] Deploying plugin components to Hermes..." -ForegroundColor Yellow diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 9b23f8f..78dd6f6 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -26,6 +26,10 @@ $HomeLauncher = Join-Path $HermesHome "HermesHub.exe" if (Test-Path $HomeLauncher) { Remove-Item -Path $HomeLauncher -Force -ErrorAction SilentlyContinue } +$HomeWebLauncher = Join-Path $HermesHome "HermesHubWeb.exe" +if (Test-Path $HomeWebLauncher) { + Remove-Item -Path $HomeWebLauncher -Force -ErrorAction SilentlyContinue +} Write-Host "[2/3] Removing plugin integration..." -ForegroundColor Gray $PluginDir = Join-Path $HermesHome "plugins\antigravity-provider" diff --git a/tests/test_installer_windows_and_linux.py b/tests/test_installer_windows_and_linux.py new file mode 100644 index 0000000..6263858 --- /dev/null +++ b/tests/test_installer_windows_and_linux.py @@ -0,0 +1,110 @@ +""" +Hermes Hub — Windows & Linux Installers and Launchers Test Suite. +Verifies requirements of Task A19. +""" + +from __future__ import annotations + +import os +from pathlib import Path +import subprocess +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALLER_DIR = REPO_ROOT / "installer" +LAUNCHER_DIR = REPO_ROOT / "launcher" + + +def test_windows_csharp_launchers_and_setup_compile(): + """Verify HermesHub.cs, HermesHubWeb.cs, and HermesHubSetup.cs compile without errors on Windows.""" + csc_path = r"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" + if not os.path.isfile(csc_path): + csc_path = r"C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" + + if not os.path.isfile(csc_path): + pytest.skip("csc.exe compiler not found in standard .NET Framework location") + + temp_out = REPO_ROOT / "artifacts" / "test_compile" + temp_out.mkdir(parents=True, exist_ok=True) + + # 1. Compile HermesHub.cs + hub_cs = LAUNCHER_DIR / "HermesHub.cs" + res1 = subprocess.run([ + csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHub.exe'}", + "/r:System.Windows.Forms.dll", "/r:System.Drawing.dll", str(hub_cs) + ], capture_output=True, text=True) + assert res1.returncode == 0, f"HermesHub.cs compilation failed: {res1.stdout}\n{res1.stderr}" + + # 2. Compile HermesHubWeb.cs + hub_web_cs = LAUNCHER_DIR / "HermesHubWeb.cs" + res2 = subprocess.run([ + csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHubWeb.exe'}", + "/r:System.Windows.Forms.dll", "/r:System.Drawing.dll", str(hub_web_cs) + ], capture_output=True, text=True) + assert res2.returncode == 0, f"HermesHubWeb.cs compilation failed: {res2.stdout}\n{res2.stderr}" + + # 3. Compile HermesHubSetup.cs + setup_cs = INSTALLER_DIR / "HermesHubSetup.cs" + res3 = subprocess.run([ + csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHubSetup.exe'}", + "/r:System.Windows.Forms.dll", "/r:System.Drawing.dll", str(setup_cs) + ], capture_output=True, text=True) + assert res3.returncode == 0, f"HermesHubSetup.cs compilation failed: {res3.stdout}\n{res3.stderr}" + + +def test_windows_launcher_browser_search_and_health_check(): + """Verify HermesHubWeb.cs includes Edge/Chrome detection, --app mode, and /api/health polling.""" + web_cs = (LAUNCHER_DIR / "HermesHubWeb.cs").read_text(encoding="utf-8") + + assert "msedge.exe" in web_cs + assert "chrome.exe" in web_cs + assert "--app=" in web_cs + assert "/api/health" in web_cs + assert "IsServerHealthy" in web_cs + assert "HermesHubWeb" in web_cs or "WebLauncher" in web_cs + + +def test_windows_installer_creates_both_shortcuts(): + """Verify HermesHubSetup.cs creates both Web and Desktop shortcuts.""" + setup_cs = (INSTALLER_DIR / "HermesHubSetup.cs").read_text(encoding="utf-8") + + assert "Hermes Hub (Web).lnk" in setup_cs + assert "Hermes Hub (Desktop).lnk" in setup_cs + assert "HermesHubWeb.exe" in setup_cs + assert "HermesHub.exe" in setup_cs + + +def test_linux_installer_script_structure(): + """Verify install-linux.sh contains prerequisite checks, mirroring, manifest, and .desktop setup.""" + install_sh = (INSTALLER_DIR / "install-linux.sh").read_text(encoding="utf-8") + + assert "#!/usr/bin/env bash" in install_sh + assert "deployment_manifest.json" in install_sh + assert "hermes-hub-web.desktop" in install_sh + assert "HERMES_HOME" in install_sh + assert "antigravity-provider" in install_sh + + +def test_linux_launcher_script_headless_and_app_mode(): + """Verify hermes-hub-web.sh checks DISPLAY, prints SSH port forwarding on headless, and uses --app on desktop.""" + launcher_sh = (LAUNCHER_DIR / "hermes-hub-web.sh").read_text(encoding="utf-8") + + assert "#!/usr/bin/env bash" in launcher_sh + assert "DISPLAY" in launcher_sh + assert "WAYLAND_DISPLAY" in launcher_sh + assert "ssh -L" in launcher_sh + assert "127.0.0.1" in launcher_sh + assert "--app=" in launcher_sh + assert "/api/health" in launcher_sh + assert "google-chrome" in launcher_sh or "chromium" in launcher_sh or "microsoft-edge" in launcher_sh + + +def test_linux_uninstaller_preserves_user_data(): + """Verify uninstall-linux.sh preserves user config, keys, and profiles by default.""" + uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8") + + assert "#!/usr/bin/env bash" in uninstall_sh + assert "PURGE_USER_DATA" in uninstall_sh + assert "--purge-user-data" in uninstall_sh + assert "Preserving user data and credentials" in uninstall_sh + assert "hermes-hub-web.desktop" in uninstall_sh