launch: check for min version for hermes desktop (#16912)

This commit is contained in:
Bruce MacDonald 2026-06-29 11:50:11 -07:00 committed by GitHub
parent 1c5ebbf5f4
commit ada1eb5163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 184 additions and 1 deletions

View file

@ -14,6 +14,7 @@ import (
"strconv"
"strings"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v3"
"github.com/ollama/ollama/api"
@ -23,6 +24,8 @@ import (
)
const (
// https://github.com/NousResearch/hermes-agent/releases/tag/v2026.6.5
hermesDesktopMinVersion = "v0.16.0"
hermesInstallScript = "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-setup"
hermesWindowsInstallURL = "https://hermes-agent.nousresearch.com/install.ps1"
hermesWindowsInstallCmd = "& ([scriptblock]::Create((irm " + hermesWindowsInstallURL + "))) -SkipSetup"
@ -94,9 +97,48 @@ func (h *HermesDesktop) Run(_ string, _ []LaunchModel, args []string) error {
if err != nil {
return err
}
if err := h.ensureHermesDesktopMinVersion(bin); err != nil {
return err
}
return hermesAttachedCommand(bin, h.launchArgs(args)...).Run()
}
func (h *HermesDesktop) ensureHermesDesktopMinVersion(bin string) error {
if hermesGOOS == "windows" {
return nil
}
version := hermesVersionOf(bin)
if version == "" {
return nil
}
if semver.Compare(version, hermesDesktopMinVersion) >= 0 {
return nil
}
fmt.Fprintf(os.Stderr, "%sHermes %s is older than the minimum version (%s) for `hermes desktop`; updating...%s\n", ansiGray, version, hermesDesktopMinVersion, ansiReset)
if err := hermesAttachedCommand(bin, "update").Run(); err != nil {
return fmt.Errorf("failed to update hermes to %s or newer: %w", hermesDesktopMinVersion, err)
}
return nil
}
func hermesVersionOf(bin string) string {
out, err := hermesCommand(bin, "--version").Output()
if err != nil {
return ""
}
firstLine := strings.SplitN(strings.TrimSpace(string(out)), "\n", 2)[0]
return parseHermesVersion(firstLine)
}
func parseHermesVersion(firstLine string) string {
for _, field := range strings.Fields(firstLine) {
if semver.IsValid(field) {
return field
}
}
return ""
}
func (h *HermesDesktop) Onboard() error {
return config.MarkIntegrationOnboarded("hermes-desktop")
}

View file

@ -630,7 +630,7 @@ func hermesDesktopTestExecutableRelativePath(goos string) string {
func writeHermesDesktopTestBinary(t *testing.T, dir string) {
t.Helper()
bin := filepath.Join(dir, "hermes")
if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '[%s]\\n' \"$*\" >> \"$HOME/hermes-invocations.log\"\n"), 0o755); err != nil {
if err := os.WriteFile(bin, []byte("#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n printf 'Hermes Agent v0.16.0 (2026.6.5)\\n'\n exit 0\nfi\nprintf '[%s]\\n' \"$*\" >> \"$HOME/hermes-invocations.log\"\n"), 0o755); err != nil {
t.Fatal(err)
}
}
@ -732,6 +732,147 @@ func TestHermesDesktopRun(t *testing.T) {
})
}
}
func writeHermesVersionedTestBinary(t *testing.T, dir, version string) {
t.Helper()
script := "#!/bin/sh\n" +
"case \"$1\" in\n" +
" --version)\n" +
" printf 'Hermes Agent " + version + " (test)\\n'\n" +
" ;;\n" +
" update)\n" +
" printf 'update\\n' >> \"$HOME/hermes-update.log\"\n" +
" ;;\n" +
" *)\n" +
" printf '[%s]\\n' \"$*\" >> \"$HOME/hermes-invocations.log\"\n" +
" ;;\n" +
"esac\n"
bin := filepath.Join(dir, "hermes")
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
}
func TestParseHermesVersion(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"standard release", "Hermes Agent v0.16.0 (2026.6.5)", "v0.16.0"},
{"newer release", "Hermes Agent v0.17.0 (2026.6.19)", "v0.17.0"},
{"older release", "Hermes Agent v0.15.1 (2026.5.29)", "v0.15.1"},
{"prerelease", "Hermes Agent v0.16.0-rc1 (2026.6.5)", "v0.16.0-rc1"},
{"no version token", "Hermes Agent", ""},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseHermesVersion(tt.input); got != tt.want {
t.Fatalf("parseHermesVersion(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestHermesDesktopRun_UpdatesCliOlderThanMinVersion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a POSIX shell test binary")
}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
withLauncherHooks(t)
withInteractiveSession(t, true)
withHermesPlatform(t, runtime.GOOS)
clearHermesMessagingEnvVars(t)
clearHermesDesktopPackageEnvVars(t)
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH"))
writeHermesVersionedTestBinary(t, tmpDir, "v0.15.1")
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
t.Fatalf("did not expect messaging prompt during desktop launch: %s", prompt)
return false, nil
}
if err := (&HermesDesktop{}).Run("", nil, []string{"--foreground"}); err != nil {
t.Fatalf("Run returned error: %v", err)
}
updateLog, err := os.ReadFile(filepath.Join(tmpDir, "hermes-update.log"))
if err != nil {
t.Fatalf("expected hermes update to run for an older CLI: %v", err)
}
if strings.TrimSpace(string(updateLog)) != "update" {
t.Fatalf("expected update log 'update', got %q", updateLog)
}
if got := readHermesDesktopInvocations(t, tmpDir); got != "[desktop --foreground]" {
t.Fatalf("expected desktop launch after update, got %q", got)
}
}
func TestHermesDesktopRun_SkipsMinVersionCheckOnWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a POSIX shell test binary")
}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
withLauncherHooks(t)
withInteractiveSession(t, true)
withHermesPlatform(t, "windows")
clearHermesMessagingEnvVars(t)
clearHermesDesktopPackageEnvVars(t)
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH"))
writeHermesVersionedTestBinary(t, tmpDir, "v0.15.1")
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
t.Fatalf("did not expect messaging prompt during desktop launch: %s", prompt)
return false, nil
}
if err := (&HermesDesktop{}).Run("", nil, []string{"--foreground"}); err != nil {
t.Fatalf("Run returned error: %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "hermes-update.log")); err == nil {
t.Fatal("expected hermes update NOT to run on Windows, but hermes-update.log exists")
}
if got := readHermesDesktopInvocations(t, tmpDir); got != "[desktop --foreground]" {
t.Fatalf("expected desktop launch without update, got %q", got)
}
}
func TestHermesDesktopRun_DoesNotUpdateCliAtOrAboveMinVersion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a POSIX shell test binary")
}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
withLauncherHooks(t)
withInteractiveSession(t, true)
withHermesPlatform(t, runtime.GOOS)
clearHermesMessagingEnvVars(t)
clearHermesDesktopPackageEnvVars(t)
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH"))
writeHermesVersionedTestBinary(t, tmpDir, "v0.17.0")
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
t.Fatalf("did not expect messaging prompt during desktop launch: %s", prompt)
return false, nil
}
if err := (&HermesDesktop{}).Run("", nil, []string{"--foreground"}); err != nil {
t.Fatalf("Run returned error: %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "hermes-update.log")); err == nil {
t.Fatal("expected hermes update NOT to run for a current CLI, but hermes-update.log exists")
}
if got := readHermesDesktopInvocations(t, tmpDir); got != "[desktop --foreground]" {
t.Fatalf("expected desktop launch without update, got %q", got)
}
}
func TestHermesDesktopRunUsesWindowsLocalAppDataPackage(t *testing.T) {
tmpDir := t.TempDir()