webbee 0.6.4__tar.gz → 0.6.6__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.
- {webbee-0.6.4 → webbee-0.6.6}/CHANGELOG.md +11 -5
- {webbee-0.6.4 → webbee-0.6.6}/PKG-INFO +1 -1
- {webbee-0.6.4 → webbee-0.6.6}/pyproject.toml +1 -1
- webbee-0.6.6/src/webbee/__init__.py +1 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/autoupdate.py +98 -6
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_autoupdate.py +51 -1
- webbee-0.6.4/src/webbee/__init__.py +0 -1
- {webbee-0.6.4 → webbee-0.6.6}/.github/workflows/publish.yml +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/.gitignore +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/LICENSE +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/README.md +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/install.sh +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/__main__.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/_launch.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/account.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/active_sessions.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/banner_art.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/boot.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/browser.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/checkpoints.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/cli.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/clipboard.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/coding_context.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/commands.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/config.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/consent.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/details.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/events.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/frames.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/home.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/home_view.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/http.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/instance_lock.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/graph.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/outline.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/query.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/service.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/intel/watch.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/media.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/modals.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/mode_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/native_files.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/native_read.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/output_pane.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/pricing_constants.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/queue_panel.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/reflow.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/remote.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/render.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/repl.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/repo.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/selection.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/session.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/sessions.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/sizing.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/slots.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/steer.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/stream.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tab_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tabs.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/thread.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tier_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/todo_panel.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tokens.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tools.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/tui.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/update.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/urlopen.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/wallet.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/src/webbee/worktrees.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/__init__.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/conftest.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_account.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_active_sessions.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_checkpoints.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_cli.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_clipboard.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_clipboard_read.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_coding_context.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_commands.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_config.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_cpc_outline.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_cross_loop_token.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_details.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_events.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_freeze_fix.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_home.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_home_view.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_home_view_mouse.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_inspect_page.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_instance_lock.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_chunker.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_embed.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_graph.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_query.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_service.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_vectors.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_intel_watch.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_launch.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_marathon.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_modals_e2e.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_mode_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_native_files.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_newtab_mode.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_packaging.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_probe_cost.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_reflow.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_render.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_repl.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_repo.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_resilient_http.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_session.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_sessions.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_sizing.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_slots.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_steer.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_stream.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tab_bar_stability.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tabs.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_thread.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tier_invalidation.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tier_store.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_todo_panel.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tokens.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tools.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tui.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_tui_hardening.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_update.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_urlopen.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_version.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_wallet.py +0 -0
- {webbee-0.6.4 → webbee-0.6.6}/tests/test_worktrees.py +0 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.6.
|
|
3
|
+
## 0.6.6
|
|
4
4
|
|
|
5
|
-
* **
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* **The upgrade's lock is the user's own**: it lives in webbee's data folder (only this user can write it), never in
|
|
6
|
+
the shared temp folder where another user could hold every upgrade back; a lock another user made is ignored.
|
|
7
|
+
|
|
8
|
+
## 0.6.5
|
|
9
|
+
|
|
10
|
+
* **Two webbees started together never break the install**: the upgrade at the start runs one at a time (a lock per
|
|
11
|
+
user); the second start waits for the first and starts on the release it installed. A pipx install is upgraded in
|
|
12
|
+
place (`pipx runpip`), never deleted and built again; the new release must import before webbee starts on it, else
|
|
13
|
+
it is installed once more (live 2026-10-25: a start in the middle of another's reinstall failed on
|
|
14
|
+
`No module named 'pygments.styles'`).
|
|
9
15
|
|
|
10
16
|
## 0.6.4
|
|
11
17
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.6"
|
|
@@ -12,6 +12,7 @@ import os
|
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
|
+
import time
|
|
15
16
|
|
|
16
17
|
DIST = "webbee"
|
|
17
18
|
MCP = "imperal-mcp"
|
|
@@ -22,6 +23,10 @@ OFF_ENV = "WEBBEE_NO_AUTOUPDATE" # 1: never upgrade at the start (CI,
|
|
|
22
23
|
DONE_ENV = "WEBBEE_UPGRADED" # set on the restart after a self-upgrade: never twice in a row
|
|
23
24
|
LIGHT_ENV = "WEBBEE_LIGHT_PYTHON" # the old Python whose light install runs this release (webbee._launch)
|
|
24
25
|
STEP_TIMEOUT_S = 300
|
|
26
|
+
# one upgrade at a time per user: two webbees started together reinstalled one venv at once and the restart found it
|
|
27
|
+
# half-written (live 2026-10-25: "No module named 'pygments.styles'"); the second waits for the first
|
|
28
|
+
LOCK_WAIT_S = 2 * STEP_TIMEOUT_S + 60
|
|
29
|
+
LOCK_STALE_S = 3 * STEP_TIMEOUT_S
|
|
25
30
|
_WINDOWS = os.name == "nt"
|
|
26
31
|
# `python -c` puts the current folder first on sys.path: a webbee.py there would run in our place (security review).
|
|
27
32
|
# Dropped before any import -- `-I` is no way out, it also drops a `pip install --user` webbee.
|
|
@@ -104,14 +109,18 @@ def self_command(version: str, prefix: str = sys.prefix, executable: str = sys.e
|
|
|
104
109
|
light = env.get(LIGHT_ENV)
|
|
105
110
|
if light: # an old Python runs this release through its light install
|
|
106
111
|
return [light, "-m", "pip", "install", "--upgrade", "--no-cache-dir", spec]
|
|
107
|
-
return _install(tool_of(prefix), spec, uv, pipx, executable)
|
|
112
|
+
return _install(tool_of(prefix), spec, uv, pipx, executable, venv=DIST)
|
|
108
113
|
|
|
109
114
|
|
|
110
|
-
def _install(tool: str, spec: str, uv, pipx, python):
|
|
115
|
+
def _install(tool: str, spec: str, uv, pipx, python, venv: str = ""):
|
|
111
116
|
"""``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).
|
|
117
|
+
(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
|
|
118
|
+
in place (``runpip``): ``install --force`` deletes the venv and builds it again, and a start in that window found
|
|
119
|
+
it half-written (live 2026-10-25)."""
|
|
113
120
|
if tool == "uv" and uv:
|
|
114
121
|
return [uv, "tool", "install", "--force", "--refresh", spec]
|
|
122
|
+
if tool == "pipx" and pipx and venv:
|
|
123
|
+
return [pipx, "runpip", venv, "install", "--upgrade", "--no-cache-dir", spec]
|
|
115
124
|
if tool == "pipx" and pipx:
|
|
116
125
|
return [pipx, "install", "--force", "--pip-args=--no-cache-dir", spec]
|
|
117
126
|
if tool == "pip" and python:
|
|
@@ -154,7 +163,7 @@ def mcp_command(inst: dict, newest, uv, pipx, wheel=None):
|
|
|
154
163
|
return None # PyPI unread: what works stays
|
|
155
164
|
spec = _spec(MCP_SPEC, MCP, newest, wheel) if newest else (MCP_SPEC if inst else MCP_GIT_SPEC)
|
|
156
165
|
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)
|
|
166
|
+
return _install(tool, spec, uv, pipx, inst.get("python") if inst else None, venv=MCP if inst else "")
|
|
158
167
|
|
|
159
168
|
|
|
160
169
|
def restart_command(argv, env=os.environ, executable: str = sys.executable):
|
|
@@ -165,6 +174,65 @@ def restart_command(argv, env=os.environ, executable: str = sys.executable):
|
|
|
165
174
|
return [executable, "-c", _NO_CWD + "from webbee.cli import main; main()", *argv]
|
|
166
175
|
|
|
167
176
|
|
|
177
|
+
def _lock_path() -> str:
|
|
178
|
+
"""The lock in webbee's own data folder of this user (0700) -- never a shared temp folder, where anyone could make
|
|
179
|
+
it first and hold every upgrade back (security review 2026-10-25)."""
|
|
180
|
+
try:
|
|
181
|
+
from webbee._launch import _home
|
|
182
|
+
base = os.path.dirname(_home())
|
|
183
|
+
except Exception:
|
|
184
|
+
base = os.path.join(os.path.expanduser("~"), ".webbee")
|
|
185
|
+
os.makedirs(base, mode=0o700, exist_ok=True)
|
|
186
|
+
return os.path.join(base, "upgrade.lock")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _lock(path: str) -> bool:
|
|
190
|
+
"""This start's turn to upgrade: the lock file made by it alone (``O_EXCL``, any system); a lock older than any
|
|
191
|
+
upgrade takes is left over from a killed start and is taken over."""
|
|
192
|
+
for _ in range(2):
|
|
193
|
+
try:
|
|
194
|
+
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
195
|
+
os.write(fd, f"{os.getpid()} {time.time():.0f}".encode())
|
|
196
|
+
os.close(fd)
|
|
197
|
+
return True
|
|
198
|
+
except FileExistsError:
|
|
199
|
+
try:
|
|
200
|
+
st = os.stat(path)
|
|
201
|
+
if hasattr(os, "getuid") and st.st_uid != os.getuid():
|
|
202
|
+
return True # not this user's: no lock of ours, the upgrade goes on
|
|
203
|
+
if time.time() - st.st_mtime < LOCK_STALE_S:
|
|
204
|
+
return False
|
|
205
|
+
os.remove(path)
|
|
206
|
+
except OSError:
|
|
207
|
+
return False
|
|
208
|
+
except OSError:
|
|
209
|
+
return True # no place for a lock: upgrade as before rather than never
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _unlock(path: str) -> None:
|
|
214
|
+
try:
|
|
215
|
+
os.remove(path)
|
|
216
|
+
except OSError:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _wait_unlocked(path: str, sleep=time.sleep) -> None:
|
|
221
|
+
end = time.time() + LOCK_WAIT_S
|
|
222
|
+
while os.path.exists(path) and time.time() < end:
|
|
223
|
+
sleep(1)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def imports_ok(python: str, run=subprocess.run) -> bool:
|
|
227
|
+
"""The installed webbee starts: its modules import in its own Python (a half-written venv fails here, not at the
|
|
228
|
+
user's start)."""
|
|
229
|
+
try:
|
|
230
|
+
return run([python, "-c", _NO_CWD + "import webbee.cli, webbee.repl"], capture_output=True, text=True,
|
|
231
|
+
timeout=60).returncode == 0
|
|
232
|
+
except Exception:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
|
|
168
236
|
def _run(cmd, run=subprocess.run) -> bool:
|
|
169
237
|
try:
|
|
170
238
|
return run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
|
|
@@ -174,22 +242,43 @@ def _run(cmd, run=subprocess.run) -> bool:
|
|
|
174
242
|
|
|
175
243
|
|
|
176
244
|
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
|
|
245
|
+
execv=os.execv, env=os.environ, wheel=wheel_url, lock_path=_lock_path,
|
|
246
|
+
sleep=time.sleep) -> list:
|
|
178
247
|
"""Bring webbee and the shell's imperal-mcp to their newest releases, then let the start go on; a self-upgrade
|
|
179
248
|
starts webbee again on the new release (never returns then). Returns what it did, for the log. Never raises."""
|
|
180
249
|
did: list = []
|
|
250
|
+
lock = ""
|
|
181
251
|
try:
|
|
182
252
|
if env.get(OFF_ENV) == "1":
|
|
183
253
|
return did
|
|
254
|
+
lock = lock_path() if lock_path else ""
|
|
255
|
+
if lock and not _lock(lock):
|
|
256
|
+
# another webbee is upgrading this install: wait for it, then start on what it installed
|
|
257
|
+
say("🐝 another webbee is updating -- waiting for it ...")
|
|
258
|
+
_wait_unlocked(lock, sleep)
|
|
259
|
+
lock = ""
|
|
260
|
+
newest = fetch(DIST)
|
|
261
|
+
if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
|
|
262
|
+
env[DONE_ENV] = "1"
|
|
263
|
+
again = restart_command(list(argv), env=env)
|
|
264
|
+
if _WINDOWS:
|
|
265
|
+
sys.exit(subprocess.call(again))
|
|
266
|
+
execv(again[0], again)
|
|
267
|
+
return did
|
|
184
268
|
uv, pipx = find_uv(which), which("pipx")
|
|
185
269
|
newest = fetch(DIST)
|
|
186
270
|
if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
|
|
187
271
|
cmd = self_command(newest, env=env, uv=uv, pipx=pipx, wheel=wheel)
|
|
188
272
|
if cmd:
|
|
189
273
|
say(f"🐝 updating webbee {current} -> {newest} ...")
|
|
190
|
-
|
|
274
|
+
python = env.get(LIGHT_ENV) or ""
|
|
275
|
+
ok = _run(cmd, run) and (python or imports_ok(sys.executable, run) or
|
|
276
|
+
(_run(cmd, run) and imports_ok(sys.executable, run)))
|
|
277
|
+
if ok:
|
|
191
278
|
did.append(f"webbee {newest}")
|
|
192
279
|
env[DONE_ENV] = "1" # the restart inherits it: never a second self-upgrade
|
|
280
|
+
_unlock(lock) # exec never returns: the lock goes before it, not in finally
|
|
281
|
+
lock = ""
|
|
193
282
|
again = restart_command(list(argv), env=env)
|
|
194
283
|
if _WINDOWS: # no exec on Windows: run it and pass its exit code on
|
|
195
284
|
sys.exit(subprocess.call(again))
|
|
@@ -211,5 +300,8 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
|
|
|
211
300
|
raise
|
|
212
301
|
except Exception:
|
|
213
302
|
pass
|
|
303
|
+
finally:
|
|
304
|
+
if lock:
|
|
305
|
+
_unlock(lock)
|
|
214
306
|
return did
|
|
215
307
|
|
|
@@ -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", "--
|
|
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,53 @@ 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()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_the_lock_lives_in_webbee_s_own_folder(tmp_path, monkeypatch):
|
|
170
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
171
|
+
monkeypatch.delenv("WEBBEE_RUNTIME_HOME", raising=False)
|
|
172
|
+
path = au._lock_path()
|
|
173
|
+
assert path.startswith(str(tmp_path / "data")) and path.endswith("upgrade.lock")
|
|
174
|
+
import os
|
|
175
|
+
import stat
|
|
176
|
+
assert stat.S_IMODE(os.stat(os.path.dirname(path)).st_mode) & 0o077 == 0 # only this user
|
|
@@ -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
|
|
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
|