webbee 0.6.4__tar.gz → 0.6.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {webbee-0.6.4 → webbee-0.6.5}/PKG-INFO +1 -1
  2. {webbee-0.6.4 → webbee-0.6.5}/pyproject.toml +1 -1
  3. webbee-0.6.5/src/webbee/__init__.py +1 -0
  4. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/autoupdate.py +89 -6
  5. {webbee-0.6.4 → webbee-0.6.5}/tests/test_autoupdate.py +41 -1
  6. webbee-0.6.4/src/webbee/__init__.py +0 -1
  7. {webbee-0.6.4 → webbee-0.6.5}/.github/workflows/publish.yml +0 -0
  8. {webbee-0.6.4 → webbee-0.6.5}/.gitignore +0 -0
  9. {webbee-0.6.4 → webbee-0.6.5}/CHANGELOG.md +0 -0
  10. {webbee-0.6.4 → webbee-0.6.5}/LICENSE +0 -0
  11. {webbee-0.6.4 → webbee-0.6.5}/README.md +0 -0
  12. {webbee-0.6.4 → webbee-0.6.5}/install.sh +0 -0
  13. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/__main__.py +0 -0
  14. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/_launch.py +0 -0
  15. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/account.py +0 -0
  16. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/active_sessions.py +0 -0
  17. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/banner_art.py +0 -0
  18. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/boot.py +0 -0
  19. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/browser.py +0 -0
  20. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/checkpoints.py +0 -0
  21. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/cli.py +0 -0
  22. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/clipboard.py +0 -0
  23. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/clipboard_read.py +0 -0
  24. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/clipboard_session.py +0 -0
  25. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/coding_context.py +0 -0
  26. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/commands.py +0 -0
  27. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/config.py +0 -0
  28. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/consent.py +0 -0
  29. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/details.py +0 -0
  30. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/events.py +0 -0
  31. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/frames.py +0 -0
  32. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/home.py +0 -0
  33. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/home_view.py +0 -0
  34. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/http.py +0 -0
  35. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/instance_lock.py +0 -0
  36. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/__init__.py +0 -0
  37. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/graph.py +0 -0
  38. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/outline.py +0 -0
  39. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/query.py +0 -0
  40. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/service.py +0 -0
  41. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/store.py +0 -0
  42. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/vectors.py +0 -0
  43. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/intel/watch.py +0 -0
  44. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/media.py +0 -0
  45. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/modals.py +0 -0
  46. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/mode_store.py +0 -0
  47. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/native_files.py +0 -0
  48. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/native_read.py +0 -0
  49. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/newtab_mode.py +0 -0
  50. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/output_pane.py +0 -0
  51. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/pricing_constants.py +0 -0
  52. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/queue_panel.py +0 -0
  53. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/reflow.py +0 -0
  54. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/remote.py +0 -0
  55. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/render.py +0 -0
  56. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/repl.py +0 -0
  57. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/repo.py +0 -0
  58. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/selection.py +0 -0
  59. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/session.py +0 -0
  60. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/sessions.py +0 -0
  61. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/sizing.py +0 -0
  62. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/slots.py +0 -0
  63. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/steer.py +0 -0
  64. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/stream.py +0 -0
  65. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tab_store.py +0 -0
  66. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tabs.py +0 -0
  67. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/thread.py +0 -0
  68. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tier_store.py +0 -0
  69. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/todo_panel.py +0 -0
  70. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tokens.py +0 -0
  71. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tools.py +0 -0
  72. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/tui.py +0 -0
  73. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/update.py +0 -0
  74. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/urlopen.py +0 -0
  75. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/wallet.py +0 -0
  76. {webbee-0.6.4 → webbee-0.6.5}/src/webbee/worktrees.py +0 -0
  77. {webbee-0.6.4 → webbee-0.6.5}/tests/__init__.py +0 -0
  78. {webbee-0.6.4 → webbee-0.6.5}/tests/conftest.py +0 -0
  79. {webbee-0.6.4 → webbee-0.6.5}/tests/test_account.py +0 -0
  80. {webbee-0.6.4 → webbee-0.6.5}/tests/test_active_sessions.py +0 -0
  81. {webbee-0.6.4 → webbee-0.6.5}/tests/test_audit_2026_07_25_findings.py +0 -0
  82. {webbee-0.6.4 → webbee-0.6.5}/tests/test_checkpoints.py +0 -0
  83. {webbee-0.6.4 → webbee-0.6.5}/tests/test_cli.py +0 -0
  84. {webbee-0.6.4 → webbee-0.6.5}/tests/test_clipboard.py +0 -0
  85. {webbee-0.6.4 → webbee-0.6.5}/tests/test_clipboard_read.py +0 -0
  86. {webbee-0.6.4 → webbee-0.6.5}/tests/test_coding_context.py +0 -0
  87. {webbee-0.6.4 → webbee-0.6.5}/tests/test_commands.py +0 -0
  88. {webbee-0.6.4 → webbee-0.6.5}/tests/test_config.py +0 -0
  89. {webbee-0.6.4 → webbee-0.6.5}/tests/test_cpc_contract_stable.py +0 -0
  90. {webbee-0.6.4 → webbee-0.6.5}/tests/test_cpc_outline.py +0 -0
  91. {webbee-0.6.4 → webbee-0.6.5}/tests/test_cross_loop_token.py +0 -0
  92. {webbee-0.6.4 → webbee-0.6.5}/tests/test_details.py +0 -0
  93. {webbee-0.6.4 → webbee-0.6.5}/tests/test_events.py +0 -0
  94. {webbee-0.6.4 → webbee-0.6.5}/tests/test_freeze_fix.py +0 -0
  95. {webbee-0.6.4 → webbee-0.6.5}/tests/test_home.py +0 -0
  96. {webbee-0.6.4 → webbee-0.6.5}/tests/test_home_view.py +0 -0
  97. {webbee-0.6.4 → webbee-0.6.5}/tests/test_home_view_mouse.py +0 -0
  98. {webbee-0.6.4 → webbee-0.6.5}/tests/test_inspect_page.py +0 -0
  99. {webbee-0.6.4 → webbee-0.6.5}/tests/test_instance_lock.py +0 -0
  100. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_chunker.py +0 -0
  101. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_embed.py +0 -0
  102. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_graph.py +0 -0
  103. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_no_numpy.py +0 -0
  104. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_query.py +0 -0
  105. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_service.py +0 -0
  106. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_store.py +0 -0
  107. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_vectors.py +0 -0
  108. {webbee-0.6.4 → webbee-0.6.5}/tests/test_intel_watch.py +0 -0
  109. {webbee-0.6.4 → webbee-0.6.5}/tests/test_launch.py +0 -0
  110. {webbee-0.6.4 → webbee-0.6.5}/tests/test_marathon.py +0 -0
  111. {webbee-0.6.4 → webbee-0.6.5}/tests/test_modals_e2e.py +0 -0
  112. {webbee-0.6.4 → webbee-0.6.5}/tests/test_mode_store.py +0 -0
  113. {webbee-0.6.4 → webbee-0.6.5}/tests/test_native_files.py +0 -0
  114. {webbee-0.6.4 → webbee-0.6.5}/tests/test_newtab_mode.py +0 -0
  115. {webbee-0.6.4 → webbee-0.6.5}/tests/test_packaging.py +0 -0
  116. {webbee-0.6.4 → webbee-0.6.5}/tests/test_probe_cost.py +0 -0
  117. {webbee-0.6.4 → webbee-0.6.5}/tests/test_queue_manage_0337.py +0 -0
  118. {webbee-0.6.4 → webbee-0.6.5}/tests/test_reflow.py +0 -0
  119. {webbee-0.6.4 → webbee-0.6.5}/tests/test_render.py +0 -0
  120. {webbee-0.6.4 → webbee-0.6.5}/tests/test_repl.py +0 -0
  121. {webbee-0.6.4 → webbee-0.6.5}/tests/test_repo.py +0 -0
  122. {webbee-0.6.4 → webbee-0.6.5}/tests/test_resilient_http.py +0 -0
  123. {webbee-0.6.4 → webbee-0.6.5}/tests/test_session.py +0 -0
  124. {webbee-0.6.4 → webbee-0.6.5}/tests/test_sessions.py +0 -0
  125. {webbee-0.6.4 → webbee-0.6.5}/tests/test_sizing.py +0 -0
  126. {webbee-0.6.4 → webbee-0.6.5}/tests/test_slots.py +0 -0
  127. {webbee-0.6.4 → webbee-0.6.5}/tests/test_steer.py +0 -0
  128. {webbee-0.6.4 → webbee-0.6.5}/tests/test_stream.py +0 -0
  129. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tab_bar_stability.py +0 -0
  130. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tab_restore_0337.py +0 -0
  131. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tabs.py +0 -0
  132. {webbee-0.6.4 → webbee-0.6.5}/tests/test_terminal_ux_0336.py +0 -0
  133. {webbee-0.6.4 → webbee-0.6.5}/tests/test_thread.py +0 -0
  134. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tier_invalidation.py +0 -0
  135. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tier_store.py +0 -0
  136. {webbee-0.6.4 → webbee-0.6.5}/tests/test_todo_panel.py +0 -0
  137. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tokens.py +0 -0
  138. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tools.py +0 -0
  139. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tools_vision_websearch.py +0 -0
  140. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tui.py +0 -0
  141. {webbee-0.6.4 → webbee-0.6.5}/tests/test_tui_hardening.py +0 -0
  142. {webbee-0.6.4 → webbee-0.6.5}/tests/test_update.py +0 -0
  143. {webbee-0.6.4 → webbee-0.6.5}/tests/test_update_badge_0340.py +0 -0
  144. {webbee-0.6.4 → webbee-0.6.5}/tests/test_urlopen.py +0 -0
  145. {webbee-0.6.4 → webbee-0.6.5}/tests/test_version.py +0 -0
  146. {webbee-0.6.4 → webbee-0.6.5}/tests/test_version_badge_corner.py +0 -0
  147. {webbee-0.6.4 → webbee-0.6.5}/tests/test_wallet.py +0 -0
  148. {webbee-0.6.4 → webbee-0.6.5}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.6.4
3
+ Version: 0.6.5
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.6.4"
3
+ version = "0.6.5"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-or-later"
@@ -0,0 +1 @@
1
+ __version__ = "0.6.5"
@@ -12,6 +12,8 @@ import os
12
12
  import shutil
13
13
  import subprocess
14
14
  import sys
15
+ import tempfile
16
+ import time
15
17
 
16
18
  DIST = "webbee"
17
19
  MCP = "imperal-mcp"
@@ -22,6 +24,10 @@ OFF_ENV = "WEBBEE_NO_AUTOUPDATE" # 1: never upgrade at the start (CI,
22
24
  DONE_ENV = "WEBBEE_UPGRADED" # set on the restart after a self-upgrade: never twice in a row
23
25
  LIGHT_ENV = "WEBBEE_LIGHT_PYTHON" # the old Python whose light install runs this release (webbee._launch)
24
26
  STEP_TIMEOUT_S = 300
27
+ # one upgrade at a time per user: two webbees started together reinstalled one venv at once and the restart found it
28
+ # half-written (live 2026-10-25: "No module named 'pygments.styles'"); the second waits for the first
29
+ LOCK_WAIT_S = 2 * STEP_TIMEOUT_S + 60
30
+ LOCK_STALE_S = 3 * STEP_TIMEOUT_S
25
31
  _WINDOWS = os.name == "nt"
26
32
  # `python -c` puts the current folder first on sys.path: a webbee.py there would run in our place (security review).
27
33
  # Dropped before any import -- `-I` is no way out, it also drops a `pip install --user` webbee.
@@ -104,14 +110,18 @@ def self_command(version: str, prefix: str = sys.prefix, executable: str = sys.e
104
110
  light = env.get(LIGHT_ENV)
105
111
  if light: # an old Python runs this release through its light install
106
112
  return [light, "-m", "pip", "install", "--upgrade", "--no-cache-dir", spec]
107
- return _install(tool_of(prefix), spec, uv, pipx, executable)
113
+ return _install(tool_of(prefix), spec, uv, pipx, executable, venv=DIST)
108
114
 
109
115
 
110
- def _install(tool: str, spec: str, uv, pipx, python):
116
+ def _install(tool: str, spec: str, uv, pipx, python, venv: str = ""):
111
117
  """``spec`` installed by ``tool`` -- past its index cache: a release minutes old is missing from a cached index page
112
- (live 2026-10-25: pip listed nothing newer than 0.5.13 while PyPI served 0.6.0)."""
118
+ (live 2026-10-25: pip listed nothing newer than 0.5.13 while PyPI served 0.6.0). A pipx venv that exists is upgraded
119
+ in place (``runpip``): ``install --force`` deletes the venv and builds it again, and a start in that window found
120
+ it half-written (live 2026-10-25)."""
113
121
  if tool == "uv" and uv:
114
122
  return [uv, "tool", "install", "--force", "--refresh", spec]
123
+ if tool == "pipx" and pipx and venv:
124
+ return [pipx, "runpip", venv, "install", "--upgrade", "--no-cache-dir", spec]
115
125
  if tool == "pipx" and pipx:
116
126
  return [pipx, "install", "--force", "--pip-args=--no-cache-dir", spec]
117
127
  if tool == "pip" and python:
@@ -154,7 +164,7 @@ def mcp_command(inst: dict, newest, uv, pipx, wheel=None):
154
164
  return None # PyPI unread: what works stays
155
165
  spec = _spec(MCP_SPEC, MCP, newest, wheel) if newest else (MCP_SPEC if inst else MCP_GIT_SPEC)
156
166
  tool = inst.get("tool") if inst else ("uv" if uv else "pipx" if pipx else "")
157
- return _install(tool, spec, uv, pipx, inst.get("python") if inst else None)
167
+ return _install(tool, spec, uv, pipx, inst.get("python") if inst else None, venv=MCP if inst else "")
158
168
 
159
169
 
160
170
  def restart_command(argv, env=os.environ, executable: str = sys.executable):
@@ -165,6 +175,55 @@ def restart_command(argv, env=os.environ, executable: str = sys.executable):
165
175
  return [executable, "-c", _NO_CWD + "from webbee.cli import main; main()", *argv]
166
176
 
167
177
 
178
+ def _lock_path() -> str:
179
+ who = str(os.getuid()) if hasattr(os, "getuid") else (os.environ.get("USERNAME") or "user")
180
+ return os.path.join(tempfile.gettempdir(), f"webbee-upgrade-{who}.lock")
181
+
182
+
183
+ def _lock(path: str) -> bool:
184
+ """This start's turn to upgrade: the lock file made by it alone (``O_EXCL``, any system); a lock older than any
185
+ upgrade takes is left over from a killed start and is taken over."""
186
+ for _ in range(2):
187
+ try:
188
+ fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
189
+ os.write(fd, f"{os.getpid()} {time.time():.0f}".encode())
190
+ os.close(fd)
191
+ return True
192
+ except FileExistsError:
193
+ try:
194
+ if time.time() - os.path.getmtime(path) < LOCK_STALE_S:
195
+ return False
196
+ os.remove(path)
197
+ except OSError:
198
+ return False
199
+ except OSError:
200
+ return True # no place for a lock: upgrade as before rather than never
201
+ return False
202
+
203
+
204
+ def _unlock(path: str) -> None:
205
+ try:
206
+ os.remove(path)
207
+ except OSError:
208
+ pass
209
+
210
+
211
+ def _wait_unlocked(path: str, sleep=time.sleep) -> None:
212
+ end = time.time() + LOCK_WAIT_S
213
+ while os.path.exists(path) and time.time() < end:
214
+ sleep(1)
215
+
216
+
217
+ def imports_ok(python: str, run=subprocess.run) -> bool:
218
+ """The installed webbee starts: its modules import in its own Python (a half-written venv fails here, not at the
219
+ user's start)."""
220
+ try:
221
+ return run([python, "-c", _NO_CWD + "import webbee.cli, webbee.repl"], capture_output=True, text=True,
222
+ timeout=60).returncode == 0
223
+ except Exception:
224
+ return False
225
+
226
+
168
227
  def _run(cmd, run=subprocess.run) -> bool:
169
228
  try:
170
229
  return run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
@@ -174,22 +233,43 @@ def _run(cmd, run=subprocess.run) -> bool:
174
233
 
175
234
 
176
235
  def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe_which, run=subprocess.run,
177
- execv=os.execv, env=os.environ, wheel=wheel_url) -> list:
236
+ execv=os.execv, env=os.environ, wheel=wheel_url, lock_path=_lock_path,
237
+ sleep=time.sleep) -> list:
178
238
  """Bring webbee and the shell's imperal-mcp to their newest releases, then let the start go on; a self-upgrade
179
239
  starts webbee again on the new release (never returns then). Returns what it did, for the log. Never raises."""
180
240
  did: list = []
241
+ lock = ""
181
242
  try:
182
243
  if env.get(OFF_ENV) == "1":
183
244
  return did
245
+ lock = lock_path() if lock_path else ""
246
+ if lock and not _lock(lock):
247
+ # another webbee is upgrading this install: wait for it, then start on what it installed
248
+ say("🐝 another webbee is updating -- waiting for it ...")
249
+ _wait_unlocked(lock, sleep)
250
+ lock = ""
251
+ newest = fetch(DIST)
252
+ if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
253
+ env[DONE_ENV] = "1"
254
+ again = restart_command(list(argv), env=env)
255
+ if _WINDOWS:
256
+ sys.exit(subprocess.call(again))
257
+ execv(again[0], again)
258
+ return did
184
259
  uv, pipx = find_uv(which), which("pipx")
185
260
  newest = fetch(DIST)
186
261
  if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
187
262
  cmd = self_command(newest, env=env, uv=uv, pipx=pipx, wheel=wheel)
188
263
  if cmd:
189
264
  say(f"🐝 updating webbee {current} -> {newest} ...")
190
- if _run(cmd, run):
265
+ python = env.get(LIGHT_ENV) or ""
266
+ ok = _run(cmd, run) and (python or imports_ok(sys.executable, run) or
267
+ (_run(cmd, run) and imports_ok(sys.executable, run)))
268
+ if ok:
191
269
  did.append(f"webbee {newest}")
192
270
  env[DONE_ENV] = "1" # the restart inherits it: never a second self-upgrade
271
+ _unlock(lock) # exec never returns: the lock goes before it, not in finally
272
+ lock = ""
193
273
  again = restart_command(list(argv), env=env)
194
274
  if _WINDOWS: # no exec on Windows: run it and pass its exit code on
195
275
  sys.exit(subprocess.call(again))
@@ -211,5 +291,8 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
211
291
  raise
212
292
  except Exception:
213
293
  pass
294
+ finally:
295
+ if lock:
296
+ _unlock(lock)
214
297
  return did
215
298
 
@@ -30,7 +30,7 @@ def test_webbee_upgrades_itself_by_its_own_tool():
30
30
  uv = au.self_command("0.6.0", prefix="/u/.local/share/uv/tools/webbee", env={}, uv="/bin/uv")
31
31
  assert uv == ["/bin/uv", "tool", "install", "--force", "--refresh", "webbee==0.6.0"]
32
32
  px = au.self_command("0.6.0", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx")
33
- assert px == ["/bin/pipx", "install", "--force", "--pip-args=--no-cache-dir", "webbee==0.6.0"]
33
+ assert px == ["/bin/pipx", "runpip", "webbee", "install", "--upgrade", "--no-cache-dir", "webbee==0.6.0"]
34
34
  pip = au.self_command("0.6.0", prefix="/venv", executable="/venv/bin/python", env={})
35
35
  assert pip == ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "--no-cache-dir", "webbee==0.6.0"]
36
36
  light = au.self_command("0.6.0", env={au.LIGHT_ENV: "/usr/bin/python3.8"}) # an old Python's light install
@@ -124,3 +124,43 @@ def test_a_release_is_installed_by_its_wheel_s_address_when_pypi_names_it():
124
124
  assert got[-1] == f"imperal-mcp[code] @ {mcp_url}"
125
125
  assert au.self_command("0.6.1", prefix="/v", executable="/v/bin/python", env={}, wheel=lambda d, v: None)[-1] \
126
126
  == "webbee==0.6.1"
127
+
128
+
129
+ # ── live 2026-10-25: two starts reinstalled one venv at once; the restart found it half-written ──
130
+ def test_pipx_upgrades_its_venv_in_place():
131
+ px = au.self_command("0.6.5", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx")
132
+ assert px == ["/bin/pipx", "runpip", "webbee", "install", "--upgrade", "--no-cache-dir", "webbee==0.6.5"]
133
+ inst = {"tool": "pipx", "python": "/p/bin/python", "version": "0.8.2", "code": True}
134
+ assert au.mcp_command(inst, "0.8.3", None, "/bin/pipx") == [
135
+ "/bin/pipx", "runpip", "imperal-mcp", "install", "--upgrade", "--no-cache-dir", "imperal-mcp[code]==0.8.3"]
136
+ assert au.mcp_command({}, "0.8.3", None, "/bin/pipx")[:3] == ["/bin/pipx", "install", "--force"] # a new one
137
+
138
+
139
+ def test_a_second_start_waits_for_the_first_upgrade_and_starts_on_it(tmp_path):
140
+ lock = tmp_path / "webbee-upgrade.lock"
141
+ lock.write_text("1 0") # another webbee is upgrading right now
142
+ run, execd, env, said = _Run(), [], {}, []
143
+ slept = []
144
+
145
+ def sleep(s):
146
+ slept.append(s)
147
+ lock.unlink() # ... and it finishes
148
+ au.upgrade_before_start(["--once"], "0.6.4", say=said.append, fetch={"webbee": "0.6.5"}.get, wheel=None,
149
+ which=_which({"pipx": "/bin/pipx"}), run=run, execv=lambda p, a: execd.append(a),
150
+ env=env, lock_path=lambda: str(lock), sleep=sleep)
151
+ assert run.calls == [] and slept and execd and env[au.DONE_ENV] == "1" # no second install, the new release
152
+ assert "another webbee is updating" in said[0]
153
+
154
+
155
+ def test_the_lock_is_released_before_the_restart_and_a_broken_install_is_done_again(tmp_path, monkeypatch):
156
+ lock = tmp_path / "webbee-upgrade.lock"
157
+ seen = []
158
+ checks = iter([False, True]) # the first install left it broken, the second is whole
159
+ monkeypatch.setattr(au, "imports_ok", lambda python, run=None: next(checks))
160
+ run = _Run()
161
+ au.upgrade_before_start([], "0.6.4", say=lambda m: None, fetch={"webbee": "0.6.5"}.get, wheel=None,
162
+ which=_which({"pipx": "/bin/pipx"}), run=run, env={}, lock_path=lambda: str(lock),
163
+ execv=lambda p, a: seen.append(lock.exists()))
164
+ assert seen == [False] # no lock left behind by exec
165
+ assert len([c for c in run.calls if "install" in c]) == 2
166
+ assert not lock.exists()
@@ -1 +0,0 @@
1
- __version__ = "0.6.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes