diff --git a/pkgbuilds/hermes-desktop/PKGBUILD b/pkgbuilds/hermes-desktop/PKGBUILD index f00eee30..4f2f7954 100644 --- a/pkgbuilds/hermes-desktop/PKGBUILD +++ b/pkgbuilds/hermes-desktop/PKGBUILD @@ -2,7 +2,7 @@ pkgname=hermes-desktop pkgver=2026.8.31 -pkgrel=2 +pkgrel=1 pkgdesc='Installer and launcher for the self-updating Hermes desktop' arch=('x86_64') url='https://github.com/NousResearch/hermes-agent' @@ -30,7 +30,7 @@ source=("install-${_commit}.sh::https://raw.githubusercontent.com/NousResearch/h 'hermes-desktop.png') sha256sums=('5854b15670b51a8daae8f59ddfa917062de9f74be261eb73b4b8d719710f8968' '821556e6336796450ab852d375117b48a4887e71d255794fd6318d99982a5ab6' - '5113ba692fe00b0f4806f799ce16b85a44bb2bd901ca17577ba64c887b1fb33d' + 'b4c282ead8a5d1a631f702474ea36eb7362ab1f0f87ad2e71f60a8698de07035' 'c37d4cfa4801eccbd769fddaebb2115d54faa49a5b7bc0f305f563e3cefd9bd8' 'd60d164e24fdcf6532133b8ea43c77a201e4b9e9dbc396187b58d51d8590ef52') diff --git a/pkgbuilds/hermes-desktop/hermes-desktop.sh b/pkgbuilds/hermes-desktop/hermes-desktop.sh index e8a17166..4d9b8cd6 100644 --- a/pkgbuilds/hermes-desktop/hermes-desktop.sh +++ b/pkgbuilds/hermes-desktop/hermes-desktop.sh @@ -66,7 +66,7 @@ restore_launchers() { } stage() { - bash "$installer" --dir "$root" --branch main --non-interactive --stage "$1" + bash "$installer" --dir "${2:-$root}" --branch main --non-interactive --stage "$1" } install_desktop() { @@ -170,20 +170,29 @@ PY env -u PYTHONPATH -u PYTHONHOME "${cli[@]}" update --yes --branch main elif [[ ! -e $root ]]; then stage prerequisites - stage repository - printf '/.omarchy-hermes-desktop\n' >>"$root/.git/info/exclude" - printf 'pending\n' >"$marker" + # Publish only a complete clone with its ownership marker. An interrupted + # clone stays aside for inspection and cannot block the next installation. + local repository_dir + repository_dir=$(mktemp -d "$HERMES_HOME/.omarchy-hermes-repository.XXXXXX") + echo "Preparing the Hermes checkout in $repository_dir" + stage repository "$repository_dir/hermes-agent" + printf '/.omarchy-hermes-desktop\n' >>"$repository_dir/hermes-agent/.git/info/exclude" + printf 'pending\n' >"$repository_dir/hermes-agent/.omarchy-hermes-desktop" + mv -T --no-clobber "$repository_dir/hermes-agent" "$root" + [[ ! -e $repository_dir/hermes-agent ]] || die "Keeping the checkout that appeared at $root during installation." + rmdir "$repository_dir" fi if [[ $mise_predecessor == true ]]; then printf '%s\n' 'pipx:hermes-agent[extras=all]' >"$root/.git/omarchy-mise-predecessor" fi - if [[ $legacy == true ]]; then - env -u PYTHONPATH -u PYTHONHOME "${cli[@]}" desktop --build-only - else - for command in venv python-deps node-deps config desktop; do stage "$command"; done + if [[ $legacy == false ]]; then + for command in venv python-deps node-deps config; do stage "$command"; done fi + # The native builder also handles updates and supports the namespace + # sandbox. The shell installer's desktop stage still requires a sudo chown. + env -u PYTHONPATH -u PYTHONHOME "${cli[@]}" desktop --build-only # Build first: a failed download leaves the old terminal CLI usable. stage complete diff --git a/test/hermes-desktop-test.py b/test/hermes-desktop-test.py index 15f03357..78062b6a 100644 --- a/test/hermes-desktop-test.py +++ b/test/hermes-desktop-test.py @@ -23,16 +23,16 @@ case $stage in repository) mkdir -p "$root/venv/bin" git init -q "$root" + [[ ${FAIL_PARTIAL_CLONE:-} != 1 ]] || exit 44 git -C "$root" remote add origin https://github.com/NousResearch/hermes-agent.git touch "$root/hermes" cp "$TEST_PYTHON" "$root/venv/bin/python" git -C "$root" add hermes venv git -C "$root" -c user.email=test@example.com -c user.name=Test commit -qm initial - ;; -desktop) - mkdir -p "$root/apps/desktop/release/linux-unpacked" - cp "$TEST_GUI" "$root/apps/desktop/release/linux-unpacked/Hermes" - printf '/apps/\n/.hermes-bootstrap-complete\n' >> "$root/.git/info/exclude" + if [[ ${RACE_CLONE:-} == 1 ]]; then + mkdir -p "$HOME/.hermes/hermes-agent" + printf 'foreign checkout\n' > "$HOME/.hermes/hermes-agent/keep" + fi ;; complete) touch "$root/.hermes-bootstrap-complete";; path) @@ -69,7 +69,20 @@ class LauncherTests(unittest.TestCase): for k in ['FAIL_STAGE', 'FAIL_AFTER_PATH', 'PYTHONHOME', 'PYTHONPATH']: self.env.pop(k, None) self.write(self.base / 'installer', INSTALLER) - self.write(self.base / 'python', '#!/bin/bash\n[[ ${FAIL_READY:-} != 1 ]]\n') + self.write(self.base / 'python', '''#!/bin/bash +set -eu +if [[ ${2:-} == desktop ]]; then + [[ ${3:-} == --build-only ]] + printf 'desktop\\n' >> "$TEST_LOG" + [[ ${FAIL_STAGE:-} != desktop ]] || exit 42 + root=$(dirname "$1") + mkdir -p "$root/apps/desktop/release/linux-unpacked" + cp "$TEST_GUI" "$root/apps/desktop/release/linux-unpacked/Hermes" + printf '/apps/\\n/.hermes-bootstrap-complete\\n' >> "$root/.git/info/exclude" +else + [[ ${FAIL_READY:-} != 1 ]] +fi +''') self.write(self.base / 'gui', '''#!/usr/bin/env python3 import json,os,sys,time json.dump({'pid':os.getpid(),'args':sys.argv[1:],'root':os.environ['HERMES_DESKTOP_HERMES_ROOT'],'node':os.environ.get('ELECTRON_RUN_AS_NODE'),'password':os.environ['HERMES_DESKTOP_PASSWORD_STORE']},open(os.environ['TEST_OUTPUT'],'w')) @@ -103,6 +116,20 @@ if os.environ.get('TEST_GUI_WAIT'): time.sleep(30) self.assertEqual((self.root / '.omarchy-hermes-desktop').read_text(), 'ready\n') self.assertEqual(subprocess.check_output(['git','-C',str(self.root),'status','--porcelain'],env=self.env),b'') + def test_interrupted_clone_stays_aside_and_retry_succeeds(self): + self.run_launcher('--install', ok=False, FAIL_PARTIAL_CLONE='1') + self.assertFalse(self.root.exists()) + partials=list((self.home/'.hermes').glob('.omarchy-hermes-repository.*/hermes-agent/.git')) + self.assertEqual(len(partials),1) + self.install() + self.assertTrue(partials[0].is_dir()) + + def test_clone_publication_preserves_concurrent_checkout(self): + self.run_launcher('--install', ok=False, RACE_CLONE='1') + self.assertEqual((self.root/'keep').read_text(),'foreign checkout\n') + self.assertFalse((self.root/'.omarchy-hermes-desktop').exists()) + self.assertFalse((self.home/'.local/bin/hermes').exists()) + def test_warm_launch_and_package_reinstall_leave_runtime_untouched(self): self.install() before = self.log.read_bytes()