Merge remote-tracking branch 'origin/antigravity/installers' into review/all

This commit is contained in:
Hermes Team 2026-08-23 22:24:29 +07:00
commit c730c769e0
11 changed files with 914 additions and 10 deletions

View file

@ -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 { }
}

View file

@ -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"

189
installer/install-linux.sh Normal file
View file

@ -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 <<EOF > "$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 <<EOF > "$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

View file

@ -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

Binary file not shown.

326
launcher/HermesHubWeb.cs Normal file
View file

@ -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;
}
}
}

BIN
launcher/HermesHubWeb.exe Normal file

Binary file not shown.

134
launcher/hermes-hub-web.sh Normal file
View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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