Skip to content

feat: uninstall the Chat GUI without removing the agent (CLI + desktop UI)#40355

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-3fd4df5d
Jun 7, 2026
Merged

feat: uninstall the Chat GUI without removing the agent (CLI + desktop UI)#40355
teknium1 merged 3 commits into
mainfrom
hermes/hermes-3fd4df5d

Conversation

@teknium1

@teknium1 teknium1 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

You can now uninstall the desktop Chat GUI without touching the Hermes agent — from the CLI (hermes uninstall --gui) and from the desktop app itself (Settings → About → Danger zone), with all three CLI uninstall modes mirrored in the GUI.

Changes

CLI

  • hermes_cli/gui_uninstall.py (new): cross-platform (Linux/macOS/Windows) discovery + removal of the GUI's artifacts — source-built apps/desktop/{dist,release,node_modules} + workspace-root node_modules + desktop-build-stamp.json, the packaged app bundle in its standard per-OS location, and the Electron userData dir. Never touches the agent source, venv, or $HERMES_HOME config/sessions/.env.
  • hermes uninstall --gui → GUI-only uninstall. --gui-summary → JSON install snapshot (powers the desktop UI's option gating + agent detection).
  • hermes uninstall --yes / --full --yes now run non-interactively (extracted _perform_uninstall() shared by the interactive + non-interactive paths). The keep-data and full flows also sweep the GUI artifacts.

Desktop

  • electron/desktop-uninstall.cjs (new, pure/testable): maps each mode (gui/lite/full) → CLI flags, resolves the running app bundle per OS, builds the detached cleanup script (waits for the app to exit, runs the Python uninstall, removes the bundle).
  • IPC hermes:uninstall:summary / :run + preload bridge + types.
  • Settings → About Danger zone with the three options (GUI only / GUI+agent keep-data / full). Agent-removing options auto-hide when no local agent is detected — ready for a future GUI-only "lite" client.

Docs: desktop.md "Uninstalling" section, cli-commands.md flag reference.

Validation

Surface Result
tests/hermes_cli/test_gui_uninstall.py (+ existing node-symlink tests) 22 passed
electron/desktop-uninstall.test.cjs (wired into test:desktop:platforms) 17 passed
E2E --gui-summary / --gui --yes (isolated HERMES_HOME) GUI removed, agent + config + venv preserved
E2E --yes (keep-data) / --full --yes (in-process) keep-data preserves $HERMES_HOME; full wipes it; neither prompts
py_compile + node --check on all touched files clean

Notes

  • The desktop runs the uninstall via a detached cleanup script because lite/full delete the venv hermes runs from, and the running app bundle is locked on macOS/Windows. Same shape as the existing self-update swap-and-relaunch flow.
  • Linux deb/rpm installs are surfaced as a "remove via apt/dnf" hint rather than force-removed (they're owned by the system package manager).

Infographic

uninstall-chat-gui-keep-agent

@teknium1 teknium1 requested a review from a team June 6, 2026 07:06
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-3fd4df5d vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9963 on HEAD, 9962 on base (🆕 +1)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/hermes_cli/test_gui_uninstall.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 5167 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels Jun 6, 2026
@teknium1 teknium1 force-pushed the hermes/hermes-3fd4df5d branch from 9a25992 to 8dc2024 Compare June 6, 2026 14:45
@teknium1

teknium1 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: Windows process-tree teardown before uninstall

Hardened the Windows path after a review caught that the cleanup script waited only on the desktop app's own PID. A backend grandchild (gateway / pty terminal / hermes REPL) can outlive the window and keep hermes.exe + venv files mandatory-locked, which would make the script's rmdir half-fail and leave a partial install — the same failure class the self-update path hit in #37532.

Changes

  • runDesktopUninstall now awaits releaseBackendLock() before spawning the cleanup script: tree-kills every backend PID the desktop owns (primary + pool) via taskkill /T /F and polls the venv shim until it's writable. Extracted the shared core out of releaseBackendLockForUpdate so the update hand-off and the uninstaller use the identical, incident-hardened teardown. No-op on macOS/Linux (no mandatory locks).
  • Windows cleanup script removes the bundle via a bounded rmdir retry loop (10×, 1s) instead of a single rmdir, since Windows releases directory handles lazily even after the holding process exits.
  • Dropped a fragile tasklist | findstr reap-by-path attempt — the Electron-side tree-kill-by-PID is the reliable mechanism.

On testing: the pure script-builders are covered by desktop-uninstall.test.cjs (17 pass, host-independent), and the Windows teardown reuses code already validated in production by #37532. A full headed-Windows-VM run of the packaged app wasn't used — it would require a Windows license + the electron-builder signing pipeline to package this branch, and still wouldn't add coverage beyond the unit tests + the reused hardened path. (KVM is available on the dev box if we ever want an end-to-end Windows harness, but it's out of scope here.)

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid, well-structured PR. Clean separation of pure/testable helpers (desktop-uninstall.cjs, gui_uninstall.py) from electron/CLI glue, good test coverage, sane safety posture (gui-only never touches agent source/venv/$HERMES_HOME; unknown mode throws instead of defaulting to a wipe). A few real issues before merge.

Findings

1. full mode bypasses the agent-detection gate (logic inconsistency)apps/desktop/src/app/settings/uninstall-section.tsx

{ mode: 'full', ... needsAgent: false }

The PR's contract is "agent-removing options auto-hide when no local agent is detected." full ("Remove the app, the agent, and all user data") is an agent-removing option, but needsAgent: false means it's always shown. On a future lite client (no agent) the menu offers "Uninstall everything → removes … the Hermes agent …" for an agent that isn't there. Either set needsAgent: true, or — if the intent is for full to still wipe leftover local user data on a lite client — make the title/description/consequence conditional so it stops claiming to remove a nonexistent agent. As-is it's inconsistent with lite (needsAgent: true), which removes the same agent.

2. Windows wait-loop is unbounded + substring PID matchdesktop-uninstall.cjs buildWindowsCleanupScript

:waitloop
tasklist /FI "PID eq %PID%" 2>nul | find "%PID%" >nul
if %ERRORLEVEL%==0 ( timeout /t 1 /nobreak >nul & goto waitloop )
  • find \"%PID%\" is a substring match — PID 99 matches 990, 1990, etc. The /FI \"PID eq %PID%\" filter already isolates the row, so the | find is redundant and can re-introduce false positives.
  • No iteration cap. The POSIX builder caps at 60 tries (~30s) then proceeds; this loops forever. If the PID never exits (or a false-positive match persists) the detached cleanup wedges silently and leaves a partial install. Add a bounded retry like the rmdir loop already has.

3. Windows lite/full deletes the venv that's running the uninstallmain.cjs runDesktopUninstall uses uninstallVenvPython() for all modes. For lite/full, _perform_uninstall rmtree's the agent tree (and on full, $HERMES_HOME), which contains venv\Scripts\python.exe — the live interpreter. On Windows that .exe is mandatory-locked, so the rmtree half-fails (POSIX is fine: open inode). releaseBackendLock kills backend PIDs, not this cleanup's own python. Run lite/full's python from outside the doomed venv, or let the batch script do the tree removal after python exits. At minimum, document this as a known Windows limitation (the PR notes no headed-Windows-VM run).

4. Misleading test namedesktop-uninstall.test.cjs

test('resolveRemovableAppPath returns null for a non-.app macOS exe (dev run)', () => {
  assert.equal(resolveRemovableAppPath('…/Electron.app/Contents/MacOS/Electron','darwin'),
    '/repo/node_modules/electron/dist/Electron.app')   // not null

The assertion returns a non-null .app; the dev-run safety actually comes from shouldRemoveAppBundle(isPackaged,…). Rename so the test documents the real contract.

Minor

  • set HERMES_HOME= / set PID= are unquoted in the Windows script while POSIX quotes via q(). Fine for standard paths, but &/^ in a path would break; the Windows q() strips embedded \" rather than escaping — worth a comment.
  • gui-only uninstall on a dev checkout sweeps workspace-root + apps/desktop/node_modules. Intended per the comment, but a developer running --gui will nuke their node_modules (recoverable, but surprising) — maybe note in the docs.
  • The bot's unresolved-import: pytest is test-only checker noise; ignore.

Strengths: the shared releaseBackendLock refactor (with the update path) is the right move, _perform_uninstall keeps the destructive sequence in one place, and the in-process --yes tests stub destructive externals instead of spawning the real CLI. Findings 1–3 are the ones I'd want resolved.

@ethernet8023

Copy link
Copy Markdown
Collaborator

We don't distribute rpm or deb packages, let's remove those code paths

teknium1 added 3 commits June 6, 2026 09:28
…p UI)

Adds a GUI-only uninstall path so people can remove the desktop Chat GUI
while keeping the Hermes agent + their config/sessions/.env, and surfaces
the three CLI uninstall modes inside the desktop app's Settings → About.

CLI:
- New hermes_cli/gui_uninstall.py: cross-platform discovery + removal of the
  desktop GUI's artifacts (source-built dist/release/node_modules + build
  stamp, the packaged app bundle, and the Electron userData dir) on Linux,
  macOS, and Windows. Never touches the agent source, venv, or user data.
- `hermes uninstall --gui` removes only the Chat GUI; `--gui-summary` prints a
  JSON install snapshot (used by the desktop UI to gate options + detect a
  missing agent for a future lite client).
- `hermes uninstall --yes` / `--full --yes` now run non-interactively, sharing
  the destructive sequence via a new _perform_uninstall() helper. The keep-data
  and full flows also sweep the GUI artifacts.

Desktop:
- electron/desktop-uninstall.cjs: pure helpers mapping each mode (gui/lite/full)
  to CLI flags, resolving the running app bundle per OS, and building the
  detached cleanup script that waits for the app to exit, runs the Python
  uninstall, and removes the bundle.
- IPC hermes:uninstall:summary / :run, preload bridge, and types.
- Settings → About "Danger zone" with the three options; agent-removing
  options hide when no local agent is detected.

Tests: tests/hermes_cli/test_gui_uninstall.py (22 pass with the existing
uninstall tests), electron/desktop-uninstall.test.cjs (17 pass, wired into
test:desktop:platforms). Docs: desktop.md "Uninstalling" + cli-commands.md.
…ndows lock safety)

The desktop uninstall cleanup script waited only on the desktop app's own
PID, but a backend grandchild (gateway / pty terminal / hermes REPL) can
outlive it and keep hermes.exe + venv files mandatory-locked on Windows —
making the script's rmdir half-fail and leaving a partial install, the same
failure class as the self-update path's #37532.

- main.cjs: runDesktopUninstall now awaits releaseBackendLock() before
  spawning the cleanup script — tree-kills every backend PID the desktop owns
  (primary + pool) via taskkill /T /F and polls the venv shim until unlocked.
  Extracted the shared core out of releaseBackendLockForUpdate so both the
  update hand-off and the uninstaller use the identical, incident-hardened
  teardown. No-op on macOS/Linux (no mandatory locks).
- desktop-uninstall.cjs: Windows cleanup script removes the bundle via a
  bounded rmdir retry loop (10x, 1s) instead of a single rmdir, since Windows
  releases directory handles lazily even after the holding process exits.
- Dropped a fragile tasklist|findstr reap-by-path attempt; the Electron-side
  tree-kill-by-PID is the reliable mechanism.

Tests: desktop-uninstall.test.cjs updated for the retry-loop output (17 pass).
…s, wait-loop)

Resolves @OutThisLife's review on #40355:

1. full mode now gated on agent presence (needsAgent: true). It removes the
   agent + user data, so on a lite client with no local agent it's hidden
   like lite — no more offering to remove an agent that isn't there.

2. (Finding 3, the real bug) lite/full no longer rmtree the venv from the
   venv's OWN python. On Windows a running python.exe is mandatory-locked, so
   that half-fails. New lightweight 'python -m hermes_cli.uninstall --mode X'
   entrypoint (stdlib-only imports) lets the desktop run agent-removing modes
   under the SYSTEM python (findSystemPython) with PYTHONPATH=<agentRoot>, so
   import hermes_cli resolves from source while the venv is torn down. Falls
   back to venv python + logs when no system python (gui-only unaffected).

3. Windows wait-loop is now bounded (60 tries, matching POSIX) and matches the
   PID as a whole space-delimited token via findstr (no substring 99->990
   trap, no redundant bare find). set HERMES_HOME/PID/PYTHONPATH now quoted.

4. Renamed the misleading 'returns null for dev run' test — the dev-run safety
   is shouldRemoveAppBundle(isPackaged=false), which the test now asserts.

Docs: note that --gui on a source checkout also sweeps node_modules/build
output. Tests: 18 python + 19 desktop pass.
@teknium1 teknium1 force-pushed the hermes/hermes-3fd4df5d branch from 8dc2024 to 029bfd6 Compare June 6, 2026 16:29
@teknium1

teknium1 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all four findings addressed in the latest push.

1. full bypassed the agent gate → Fixed. full is now needsAgent: true, so on a lite client with no local agent it's hidden alongside lite (a remote-backend lite client has no local agent or local user data the GUI installed, so gui-only is correct there). No more offering to remove a nonexistent agent.

2. Windows wait-loop unbounded + substring PID match → Fixed. Now bounded at 60 tries (matches the POSIX ~30s cap) and matches the PID as a whole space-delimited token (findstr /r /c:" %PID% " over tasklist /NH), so 99 can't match 990. Dropped the redundant | find.

3. lite/full deleted the venv running the uninstall → Fixed (this was the real bug). Added a lightweight python -m hermes_cli.uninstall --mode <gui|lite|full> entrypoint (stdlib-only imports — hermes_cli/__init__.py is just os/sys). The desktop now runs agent-removing modes under the system Python (findSystemPython) with PYTHONPATH=<agentRoot>, so import hermes_cli resolves from source while the venv (containing the locked python.exe) is torn down. Falls back to the venv python + logs a warning when no system Python exists; gui-only is unaffected (it doesn't touch the venv).

4. Misleading test name → Renamed. The test now documents that dev-run safety comes from shouldRemoveAppBundle(isPackaged=false, …) and asserts it, rather than implying a null return.

Minor: set HERMES_HOME/PID/PYTHONPATH are now quoted in the Windows script; added a comment on the q() strip behavior; added a docs note that --gui on a source checkout also sweeps node_modules/build output.

Tests: 18 Python + 19 desktop pass. The unresolved-import: pytest bot note is test-only checker noise as you said.

@teknium1 teknium1 merged commit 5b43bf7 into main Jun 7, 2026
24 checks passed
@teknium1 teknium1 deleted the hermes/hermes-3fd4df5d branch June 7, 2026 01:22
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
…p UI) (NousResearch#40355)

* feat: uninstall the Chat GUI without removing the agent (CLI + desktop UI)

Adds a GUI-only uninstall path so people can remove the desktop Chat GUI
while keeping the Hermes agent + their config/sessions/.env, and surfaces
the three CLI uninstall modes inside the desktop app's Settings → About.

CLI:
- New hermes_cli/gui_uninstall.py: cross-platform discovery + removal of the
  desktop GUI's artifacts (source-built dist/release/node_modules + build
  stamp, the packaged app bundle, and the Electron userData dir) on Linux,
  macOS, and Windows. Never touches the agent source, venv, or user data.
- `hermes uninstall --gui` removes only the Chat GUI; `--gui-summary` prints a
  JSON install snapshot (used by the desktop UI to gate options + detect a
  missing agent for a future lite client).
- `hermes uninstall --yes` / `--full --yes` now run non-interactively, sharing
  the destructive sequence via a new _perform_uninstall() helper. The keep-data
  and full flows also sweep the GUI artifacts.

Desktop:
- electron/desktop-uninstall.cjs: pure helpers mapping each mode (gui/lite/full)
  to CLI flags, resolving the running app bundle per OS, and building the
  detached cleanup script that waits for the app to exit, runs the Python
  uninstall, and removes the bundle.
- IPC hermes:uninstall:summary / :run, preload bridge, and types.
- Settings → About "Danger zone" with the three options; agent-removing
  options hide when no local agent is detected.

Tests: tests/hermes_cli/test_gui_uninstall.py (22 pass with the existing
uninstall tests), electron/desktop-uninstall.test.cjs (17 pass, wired into
test:desktop:platforms). Docs: desktop.md "Uninstalling" + cli-commands.md.

* fix(desktop): tear down backend process tree before GUI uninstall (Windows lock safety)

The desktop uninstall cleanup script waited only on the desktop app's own
PID, but a backend grandchild (gateway / pty terminal / hermes REPL) can
outlive it and keep hermes.exe + venv files mandatory-locked on Windows —
making the script's rmdir half-fail and leaving a partial install, the same
failure class as the self-update path's NousResearch#37532.

- main.cjs: runDesktopUninstall now awaits releaseBackendLock() before
  spawning the cleanup script — tree-kills every backend PID the desktop owns
  (primary + pool) via taskkill /T /F and polls the venv shim until unlocked.
  Extracted the shared core out of releaseBackendLockForUpdate so both the
  update hand-off and the uninstaller use the identical, incident-hardened
  teardown. No-op on macOS/Linux (no mandatory locks).
- desktop-uninstall.cjs: Windows cleanup script removes the bundle via a
  bounded rmdir retry loop (10x, 1s) instead of a single rmdir, since Windows
  releases directory handles lazily even after the holding process exits.
- Dropped a fragile tasklist|findstr reap-by-path attempt; the Electron-side
  tree-kill-by-PID is the reliable mechanism.

Tests: desktop-uninstall.test.cjs updated for the retry-loop output (17 pass).

* fix(desktop): address review on GUI uninstall (venv self-delete, gates, wait-loop)

Resolves @OutThisLife's review on NousResearch#40355:

1. full mode now gated on agent presence (needsAgent: true). It removes the
   agent + user data, so on a lite client with no local agent it's hidden
   like lite — no more offering to remove an agent that isn't there.

2. (Finding 3, the real bug) lite/full no longer rmtree the venv from the
   venv's OWN python. On Windows a running python.exe is mandatory-locked, so
   that half-fails. New lightweight 'python -m hermes_cli.uninstall --mode X'
   entrypoint (stdlib-only imports) lets the desktop run agent-removing modes
   under the SYSTEM python (findSystemPython) with PYTHONPATH=<agentRoot>, so
   import hermes_cli resolves from source while the venv is torn down. Falls
   back to venv python + logs when no system python (gui-only unaffected).

3. Windows wait-loop is now bounded (60 tries, matching POSIX) and matches the
   PID as a whole space-delimited token via findstr (no substring 99->990
   trap, no redundant bare find). set HERMES_HOME/PID/PYTHONPATH now quoted.

4. Renamed the misleading 'returns null for dev run' test — the dev-run safety
   is shouldRemoveAppBundle(isPackaged=false), which the test now asserts.

Docs: note that --gui on a source checkout also sweeps node_modules/build
output. Tests: 18 python + 19 desktop pass.
OmarB97 added a commit to OmarB97/hermes-agent that referenced this pull request Jun 10, 2026
… crash on launch) (#156)

electron/main.cjs:42 requires './desktop-uninstall.cjs' (modeRemovesUserData,
resolveRemovableAppPath, shouldRemoveAppBundle, uninstallArgsForMode), and
package.json's test:desktop:platforms references desktop-uninstall.test.cjs —
but both files were dropped during the #152 sync merge while their references
survived. The packaged app crashed on launch: "Cannot find module
'./desktop-uninstall.cjs'" at main.cjs:42. Restored both verbatim from
upstream/main (the uninstall feature, NousResearch#40355). node --test: 19/19 pass;
main.cjs resolves past the require.

Co-authored-by: Omar Baradei <omar@kostudios.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants