@@ -249,6 +249,106 @@ def _start_status_bar_app(vault: str) -> None:
249249 "`compartment menubar` yourself" )
250250
251251
252+ def _install_kind () -> tuple [str , list [str ]]:
253+ """How this copy was installed, and the command that upgrades it."""
254+ exe = Path (sys .argv [0 ]).resolve ()
255+ if "uv/tools" in str (exe ) or shutil .which ("uv" ) and "uv/tools" in str (
256+ Path (shutil .which ("compartment" ) or "" ).resolve ()):
257+ return "uv" , ["uv" , "tool" , "upgrade" , "compartment" ]
258+ return "pip" , [sys .executable , "-m" , "pip" , "install" , "--upgrade" ,
259+ "compartment" ]
260+
261+
262+ def cmd_update (args ) -> None :
263+ """Upgrade Compartment in place, then put the app back up."""
264+ kind , cmd = _install_kind ()
265+ if args .source :
266+ target = "git+https://github.com/MaxFreedomPollard/Compartment@main"
267+ cmd = (["uv" , "tool" , "install" , "--force" , target ] if kind == "uv"
268+ else [sys .executable , "-m" , "pip" , "install" , "--upgrade" ,
269+ "--force-reinstall" , target ])
270+ print (f"Compartment { __version__ } - updating ({ kind } install)" )
271+ print (" " + " " .join (cmd ))
272+ try :
273+ res = subprocess .run (cmd , timeout = 1800 )
274+ except (OSError , subprocess .SubprocessError ) as exc :
275+ _die (f"update failed to run: { exc } " )
276+ if res .returncode != 0 :
277+ _die (f"update failed (exit { res .returncode } )" )
278+ ver = "?"
279+ try :
280+ out = subprocess .run ([shutil .which ("compartment" ) or "compartment" ,
281+ "--version" ], capture_output = True , text = True ,
282+ timeout = 120 )
283+ ver = (out .stdout or "" ).strip () or "?"
284+ except (OSError , subprocess .SubprocessError ):
285+ pass
286+ print (f" updated: now { ver } " )
287+ print (" your vault and settings are untouched" )
288+ if not args .no_app and sys .platform in ("darwin" , "win32" ):
289+ _restart_status_bar_app (args .vault )
290+
291+
292+ def _restart_status_bar_app (vault : str ) -> None :
293+ """Stop the running status bar app and start the new build."""
294+ try :
295+ app = _tray_app ()
296+ app .quit_running ()
297+ except Exception : # noqa: BLE001
298+ pass
299+ _start_status_bar_app (vault )
300+
301+
302+ def cmd_uninstall (args ) -> None :
303+ """Remove Compartment from this machine. The vault is KEPT unless asked."""
304+ print (f"Compartment { __version__ } - uninstalling" )
305+ if sys .platform in ("darwin" , "win32" ):
306+ try :
307+ app = _tray_app ()
308+ try :
309+ app .quit_running ()
310+ print (" status bar app stopped" )
311+ except Exception : # noqa: BLE001
312+ pass
313+ print (f" start at login: { app .set_login (False )} " )
314+ except Exception as exc : # noqa: BLE001
315+ print (f" status bar app: nothing to remove ({ exc } )" )
316+ try :
317+ if claude_hooks .is_installed ():
318+ claude_hooks .uninstall ()
319+ print (" Claude Code capture hook removed" )
320+ except Exception as exc : # noqa: BLE001
321+ print (f" capture hook: { exc } " )
322+ from .home import LEGACY_NAME , NAME
323+ for name in (NAME , LEGACY_NAME ):
324+ try :
325+ r = subprocess .run (["claude" , "mcp" , "remove" , name ],
326+ capture_output = True , text = True , timeout = 120 )
327+ if r .returncode == 0 :
328+ print (f" MCP registration removed: { name } " )
329+ except (OSError , subprocess .SubprocessError ):
330+ pass
331+ if args .purge :
332+ # only on an explicit --purge: this is the only copy of everything
333+ # the agent has ever learned, and it is not recoverable.
334+ for f in (args .vault , args .vault + ".config.json" ,
335+ args .vault + ".flock" ):
336+ try :
337+ os .remove (f )
338+ print (f" deleted { f } " )
339+ except OSError :
340+ pass
341+ print (" vault deleted (--purge)" )
342+ else :
343+ print (f" vault KEPT at { args .vault } " )
344+ print (" delete it yourself, or re-run with --purge, once you are sure" )
345+ kind , _ = _install_kind ()
346+ final = ("uv tool uninstall compartment" if kind == "uv"
347+ else f"{ sys .executable } -m pip uninstall compartment" )
348+ print ("\n One step left, which cannot remove itself while it is running:" )
349+ print (f" { final } " )
350+
351+
252352def _ask_yn (q : str ) -> bool :
253353 if not sys .stdin .isatty ():
254354 return False
@@ -1376,6 +1476,20 @@ def main(argv: list[str] | None = None) -> None:
13761476 help = "only packs signed by the project are trusted by default. Name an author's 64 hex character public key to trust it deliberately; repeatable" )
13771477 p .set_defaults (fn = cmd_pack_export )
13781478
1479+ p = sub .add_parser ("update" , help = "upgrade Compartment in place" )
1480+ p .add_argument ("--source" , action = "store_true" ,
1481+ help = "update from the GitHub main branch instead of PyPI" )
1482+ p .add_argument ("--no-app" , action = "store_true" ,
1483+ help = "do not restart the status bar app afterwards" )
1484+ p .set_defaults (fn = cmd_update )
1485+
1486+ p = sub .add_parser ("uninstall" ,
1487+ help = "remove Compartment from this machine "
1488+ "(the vault is kept unless --purge)" )
1489+ p .add_argument ("--purge" , action = "store_true" ,
1490+ help = "ALSO delete the vault and its config, permanently" )
1491+ p .set_defaults (fn = cmd_uninstall )
1492+
13791493 p = sub .add_parser ("integrate" ,
13801494 help = "one-command wiring into claude, hermes, or openclaw" )
13811495 p .add_argument ("target" , choices = ["claude" , "hermes" , "openclaw" ])
0 commit comments