webbee 0.7.5__tar.gz → 0.7.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.
Files changed (161) hide show
  1. {webbee-0.7.5 → webbee-0.7.6}/.github/workflows/publish.yml +13 -0
  2. {webbee-0.7.5 → webbee-0.7.6}/CHANGELOG.md +14 -0
  3. {webbee-0.7.5 → webbee-0.7.6}/PKG-INFO +6 -1
  4. {webbee-0.7.5 → webbee-0.7.6}/README.md +5 -0
  5. webbee-0.7.6/install.sh +29 -0
  6. {webbee-0.7.5 → webbee-0.7.6}/pyproject.toml +1 -1
  7. webbee-0.7.6/src/webbee/__init__.py +1 -0
  8. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/autoupdate.py +166 -31
  9. {webbee-0.7.5 → webbee-0.7.6}/tests/test_autoupdate.py +166 -0
  10. {webbee-0.7.5 → webbee-0.7.6}/tests/test_packaging.py +6 -1
  11. webbee-0.7.5/install.sh +0 -20
  12. webbee-0.7.5/src/webbee/__init__.py +0 -1
  13. {webbee-0.7.5 → webbee-0.7.6}/.gitignore +0 -0
  14. {webbee-0.7.5 → webbee-0.7.6}/LICENSE +0 -0
  15. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/__main__.py +0 -0
  16. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/_launch.py +0 -0
  17. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/account.py +0 -0
  18. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/active_sessions.py +0 -0
  19. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/banner_art.py +0 -0
  20. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/boot.py +0 -0
  21. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/browser.py +0 -0
  22. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/checkpoints.py +0 -0
  23. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/cli.py +0 -0
  24. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/clipboard.py +0 -0
  25. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/clipboard_read.py +0 -0
  26. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/clipboard_session.py +0 -0
  27. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/coding_context.py +0 -0
  28. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/commands.py +0 -0
  29. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/config.py +0 -0
  30. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/consent.py +0 -0
  31. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/details.py +0 -0
  32. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/events.py +0 -0
  33. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/frames.py +0 -0
  34. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/home.py +0 -0
  35. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/home_view.py +0 -0
  36. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/http.py +0 -0
  37. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/instance_lock.py +0 -0
  38. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/__init__.py +0 -0
  39. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/graph.py +0 -0
  40. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/outline.py +0 -0
  41. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/query.py +0 -0
  42. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/service.py +0 -0
  43. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/store.py +0 -0
  44. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/tree.py +0 -0
  45. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/vectors.py +0 -0
  46. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/intel/watch.py +0 -0
  47. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/media.py +0 -0
  48. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/modals.py +0 -0
  49. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/mode_store.py +0 -0
  50. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/native_files.py +0 -0
  51. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/native_read.py +0 -0
  52. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/newtab_mode.py +0 -0
  53. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/output_pane.py +0 -0
  54. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/pricing_constants.py +0 -0
  55. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/queue_panel.py +0 -0
  56. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/reflow.py +0 -0
  57. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/remote.py +0 -0
  58. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/render.py +0 -0
  59. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/repl.py +0 -0
  60. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/repo.py +0 -0
  61. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/selection.py +0 -0
  62. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/session.py +0 -0
  63. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/sessions.py +0 -0
  64. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/sizing.py +0 -0
  65. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/slots.py +0 -0
  66. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/steer.py +0 -0
  67. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/stream.py +0 -0
  68. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tab_store.py +0 -0
  69. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tabs.py +0 -0
  70. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/thread.py +0 -0
  71. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tier_store.py +0 -0
  72. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/todo_panel.py +0 -0
  73. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tokens.py +0 -0
  74. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tools.py +0 -0
  75. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/tui.py +0 -0
  76. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/update.py +0 -0
  77. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/urlopen.py +0 -0
  78. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/wallet.py +0 -0
  79. {webbee-0.7.5 → webbee-0.7.6}/src/webbee/worktrees.py +0 -0
  80. {webbee-0.7.5 → webbee-0.7.6}/tests/__init__.py +0 -0
  81. {webbee-0.7.5 → webbee-0.7.6}/tests/conftest.py +0 -0
  82. {webbee-0.7.5 → webbee-0.7.6}/tests/test_account.py +0 -0
  83. {webbee-0.7.5 → webbee-0.7.6}/tests/test_active_sessions.py +0 -0
  84. {webbee-0.7.5 → webbee-0.7.6}/tests/test_audit_2026_07_25_findings.py +0 -0
  85. {webbee-0.7.5 → webbee-0.7.6}/tests/test_checkpoints.py +0 -0
  86. {webbee-0.7.5 → webbee-0.7.6}/tests/test_checkpoints_nested_1030.py +0 -0
  87. {webbee-0.7.5 → webbee-0.7.6}/tests/test_cli.py +0 -0
  88. {webbee-0.7.5 → webbee-0.7.6}/tests/test_clipboard.py +0 -0
  89. {webbee-0.7.5 → webbee-0.7.6}/tests/test_clipboard_read.py +0 -0
  90. {webbee-0.7.5 → webbee-0.7.6}/tests/test_code_map_fresh.py +0 -0
  91. {webbee-0.7.5 → webbee-0.7.6}/tests/test_coding_context.py +0 -0
  92. {webbee-0.7.5 → webbee-0.7.6}/tests/test_commands.py +0 -0
  93. {webbee-0.7.5 → webbee-0.7.6}/tests/test_config.py +0 -0
  94. {webbee-0.7.5 → webbee-0.7.6}/tests/test_cpc_contract_stable.py +0 -0
  95. {webbee-0.7.5 → webbee-0.7.6}/tests/test_cpc_outline.py +0 -0
  96. {webbee-0.7.5 → webbee-0.7.6}/tests/test_cross_loop_token.py +0 -0
  97. {webbee-0.7.5 → webbee-0.7.6}/tests/test_details.py +0 -0
  98. {webbee-0.7.5 → webbee-0.7.6}/tests/test_events.py +0 -0
  99. {webbee-0.7.5 → webbee-0.7.6}/tests/test_freeze_fix.py +0 -0
  100. {webbee-0.7.5 → webbee-0.7.6}/tests/test_home.py +0 -0
  101. {webbee-0.7.5 → webbee-0.7.6}/tests/test_home_view.py +0 -0
  102. {webbee-0.7.5 → webbee-0.7.6}/tests/test_home_view_mouse.py +0 -0
  103. {webbee-0.7.5 → webbee-0.7.6}/tests/test_inspect_page.py +0 -0
  104. {webbee-0.7.5 → webbee-0.7.6}/tests/test_instance_lock.py +0 -0
  105. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_any_folder.py +0 -0
  106. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_cache_and_sync_worker.py +0 -0
  107. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_checksum_tree.py +0 -0
  108. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_chunker.py +0 -0
  109. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_embed.py +0 -0
  110. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_folder_1106.py +0 -0
  111. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_graph.py +0 -0
  112. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_no_numpy.py +0 -0
  113. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_query.py +0 -0
  114. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_service.py +0 -0
  115. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_store.py +0 -0
  116. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_vectors.py +0 -0
  117. {webbee-0.7.5 → webbee-0.7.6}/tests/test_intel_watch.py +0 -0
  118. {webbee-0.7.5 → webbee-0.7.6}/tests/test_launch.py +0 -0
  119. {webbee-0.7.5 → webbee-0.7.6}/tests/test_marathon.py +0 -0
  120. {webbee-0.7.5 → webbee-0.7.6}/tests/test_modals_e2e.py +0 -0
  121. {webbee-0.7.5 → webbee-0.7.6}/tests/test_mode_and_remote_0_6_8.py +0 -0
  122. {webbee-0.7.5 → webbee-0.7.6}/tests/test_mode_store.py +0 -0
  123. {webbee-0.7.5 → webbee-0.7.6}/tests/test_native_files.py +0 -0
  124. {webbee-0.7.5 → webbee-0.7.6}/tests/test_newtab_mode.py +0 -0
  125. {webbee-0.7.5 → webbee-0.7.6}/tests/test_probe_cost.py +0 -0
  126. {webbee-0.7.5 → webbee-0.7.6}/tests/test_queue_manage_0337.py +0 -0
  127. {webbee-0.7.5 → webbee-0.7.6}/tests/test_reflow.py +0 -0
  128. {webbee-0.7.5 → webbee-0.7.6}/tests/test_remote_code_reads.py +0 -0
  129. {webbee-0.7.5 → webbee-0.7.6}/tests/test_render.py +0 -0
  130. {webbee-0.7.5 → webbee-0.7.6}/tests/test_repl.py +0 -0
  131. {webbee-0.7.5 → webbee-0.7.6}/tests/test_repo.py +0 -0
  132. {webbee-0.7.5 → webbee-0.7.6}/tests/test_resilient_http.py +0 -0
  133. {webbee-0.7.5 → webbee-0.7.6}/tests/test_review_fixes_1030.py +0 -0
  134. {webbee-0.7.5 → webbee-0.7.6}/tests/test_session.py +0 -0
  135. {webbee-0.7.5 → webbee-0.7.6}/tests/test_session_review_1030.py +0 -0
  136. {webbee-0.7.5 → webbee-0.7.6}/tests/test_sessions.py +0 -0
  137. {webbee-0.7.5 → webbee-0.7.6}/tests/test_sizing.py +0 -0
  138. {webbee-0.7.5 → webbee-0.7.6}/tests/test_slots.py +0 -0
  139. {webbee-0.7.5 → webbee-0.7.6}/tests/test_steer.py +0 -0
  140. {webbee-0.7.5 → webbee-0.7.6}/tests/test_stream.py +0 -0
  141. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tab_bar_stability.py +0 -0
  142. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tab_restore_0337.py +0 -0
  143. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tabs.py +0 -0
  144. {webbee-0.7.5 → webbee-0.7.6}/tests/test_terminal_ux_0336.py +0 -0
  145. {webbee-0.7.5 → webbee-0.7.6}/tests/test_thread.py +0 -0
  146. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tier_invalidation.py +0 -0
  147. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tier_store.py +0 -0
  148. {webbee-0.7.5 → webbee-0.7.6}/tests/test_todo_panel.py +0 -0
  149. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tokens.py +0 -0
  150. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tools.py +0 -0
  151. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tools_eol_cancel_1030.py +0 -0
  152. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tools_vision_websearch.py +0 -0
  153. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tui.py +0 -0
  154. {webbee-0.7.5 → webbee-0.7.6}/tests/test_tui_hardening.py +0 -0
  155. {webbee-0.7.5 → webbee-0.7.6}/tests/test_update.py +0 -0
  156. {webbee-0.7.5 → webbee-0.7.6}/tests/test_update_badge_0340.py +0 -0
  157. {webbee-0.7.5 → webbee-0.7.6}/tests/test_urlopen.py +0 -0
  158. {webbee-0.7.5 → webbee-0.7.6}/tests/test_version.py +0 -0
  159. {webbee-0.7.5 → webbee-0.7.6}/tests/test_version_badge_corner.py +0 -0
  160. {webbee-0.7.5 → webbee-0.7.6}/tests/test_wallet.py +0 -0
  161. {webbee-0.7.5 → webbee-0.7.6}/tests/test_worktrees.py +0 -0
@@ -17,6 +17,8 @@ jobs:
17
17
  contents: write # create the GitHub Release for the tag
18
18
  steps:
19
19
  - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0 # the stable branch moves only forward (an ancestor check)
20
22
  - uses: actions/setup-python@v5
21
23
  with:
22
24
  python-version: "3.12"
@@ -28,6 +30,17 @@ jobs:
28
30
  uses: pypa/gh-action-pypi-publish@release/v1
29
31
  with:
30
32
  password: ${{ secrets.PYPI_API_TOKEN }}
33
+ # The `stable` branch is what every install from git names (webbee autoupdate, `pipx reinstall`, `uv tool
34
+ # upgrade`): moved here to the release just published -- only forward, a re-run of an older tag leaves it.
35
+ - name: Move the stable branch to this release
36
+ run: |
37
+ if git ls-remote --exit-code origin refs/heads/stable >/dev/null; then
38
+ git fetch -q origin stable
39
+ if git merge-base --is-ancestor HEAD FETCH_HEAD; then
40
+ echo "stable is already at or past this release"; exit 0
41
+ fi
42
+ fi
43
+ git push origin "HEAD:refs/heads/stable"
31
44
  # Mirror the PyPI release as a GitHub Release (so the repo's Releases
32
45
  # sidebar tracks the latest version) with notes from CHANGELOG.md.
33
46
  - name: Create GitHub Release
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.6
4
+
5
+ * **Always the newest release, from git** (06.11: `pipx reinstall webbee` put back 0.6.7 while 0.7.5 was out).
6
+ * pipx and uv install again what they recorded. The upgrade at the start had recorded a release's own wheel
7
+ address, so a reinstall went back to that release.
8
+ * webbee, imperal-mcp and imperal-sdk now come from their repositories' `stable` branch, which each release's CI
9
+ moves to its tag. The newest release is read from git itself (the tag `stable` stands on), never from an index
10
+ that trails it.
11
+ * What pipx / uv record is the branch: `pipx reinstall`, `pipx upgrade --include-injected` and `uv tool upgrade`
12
+ bring the newest release. An install still recorded by a version is moved onto the branch once, even on the
13
+ newest release.
14
+ * `install.sh` installs from the branch too. No git, GitHub out of reach, or the light install of Python 3.7-3.9:
15
+ PyPI, as before.
16
+
3
17
  ## 0.7.5
4
18
 
5
19
  * Needs imperal-mcp 0.9.5:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.7.5
3
+ Version: 0.7.6
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
@@ -90,6 +90,11 @@ pipx install webbee
90
90
  # or: pip install webbee (inside a virtual environment)
91
91
  ```
92
92
 
93
+ Always the newest release: at its start webbee moves itself, imperal-mcp and imperal-sdk onto their git repositories'
94
+ `stable` branch (each release moves it to its tag) and records that branch, never a version. From then on
95
+ `pipx reinstall webbee`, `pipx upgrade --include-injected webbee` and `uv tool upgrade webbee` all bring the newest
96
+ release. Without git on the machine it stays on PyPI.
97
+
93
98
  No Python tools at hand? One line installs [uv](https://docs.astral.sh/uv/) (it brings its own Python) and webbee:
94
99
 
95
100
  ```sh
@@ -35,6 +35,11 @@ pipx install webbee
35
35
  # or: pip install webbee (inside a virtual environment)
36
36
  ```
37
37
 
38
+ Always the newest release: at its start webbee moves itself, imperal-mcp and imperal-sdk onto their git repositories'
39
+ `stable` branch (each release moves it to its tag) and records that branch, never a version. From then on
40
+ `pipx reinstall webbee`, `pipx upgrade --include-injected webbee` and `uv tool upgrade webbee` all bring the newest
41
+ release. Without git on the machine it stays on PyPI.
42
+
38
43
  No Python tools at hand? One line installs [uv](https://docs.astral.sh/uv/) (it brings its own Python) and webbee:
39
44
 
40
45
  ```sh
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # Webbee installer — bootstraps uv (which brings its own Python) then installs webbee.
3
+ # Always the newest release, straight from git: webbee, imperal-mcp and imperal-sdk from their repositories'
4
+ # `stable` branch, which every release moves to its tag. uv records the branch, never a version, so running this
5
+ # again, or `uv tool upgrade webbee`, brings the newest release. No git on the machine: the newest from PyPI.
6
+ set -e
7
+
8
+ if ! command -v uv >/dev/null 2>&1; then
9
+ echo "Installing uv (Python toolchain manager)…"
10
+ curl -LsSf https://astral.sh/uv/install.sh | sh
11
+ # uv installs to ~/.local/bin or ~/.cargo/bin; make it visible for this run.
12
+ export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
13
+ fi
14
+
15
+ GH="https://github.com/imperalcloud"
16
+ if command -v git >/dev/null 2>&1; then
17
+ echo "Installing webbee (newest release, from git)…"
18
+ uv tool install --force --refresh \
19
+ --with "imperal-mcp[code] @ git+$GH/imperal-mcp.git@stable" \
20
+ --with "imperal-sdk[universal-index] @ git+$GH/imperal-sdk.git@stable" \
21
+ "webbee @ git+$GH/webbee-code.git@stable"
22
+ else
23
+ echo "Installing webbee (newest release, from PyPI: no git here)…"
24
+ uv tool install --force --refresh "webbee"
25
+ fi
26
+
27
+ echo ""
28
+ echo "✅ webbee installed. Start it with: webbee"
29
+ echo " (if 'webbee' is not found, add uv's tool bin to your PATH: uv tool update-shell)"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.7.5"
3
+ version = "0.7.6"
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.7.6"
@@ -7,7 +7,16 @@ installed it (a uv tool, a pipx venv, plain pip, or the light install that runs
7
7
  the new release; then it brings the shell's imperal-mcp to its newest release WITH its code extra -- installed when
8
8
  missing, upgraded by the tool that installed it (live 2026-10-25: an install had lost its code extra and the code tools
9
9
  failed without a word). PyPI out of reach: nothing changes and webbee starts at once; nothing installed and PyPI out of
10
- reach: imperal-mcp from its source, git. Every step is bounded and never fails the start."""
10
+ reach: imperal-mcp from its source, git. Every step is bounded and never fails the start.
11
+
12
+ The source is git (owner 2026-11-06: "Скрипт ВСЕГДА только на САМЫЙ свежий пакет смотреть должен ... не на pypi, а на
13
+ сам git repo"): webbee, imperal-mcp and imperal-sdk come from their repositories' ``stable`` branch, which each
14
+ release's CI moves to its tag. pipx and uv record what they installed and ``pipx reinstall`` / ``pipx upgrade`` /
15
+ ``uv tool upgrade`` install that record again -- an install recorded as a release's own wheel address stayed on that
16
+ release forever (live 2026-11-06: ``pipx reinstall webbee`` put back 0.6.7 while 0.7.5 was out). The record is now the
17
+ branch, never a version: any of them brings the newest release, and an install still recorded by a version is moved
18
+ onto the branch once. No git (or GitHub out of reach), or the light install of an old Python (3.7 builds no package
19
+ from source): PyPI, as before."""
11
20
  import os
12
21
  import shutil
13
22
  import subprocess
@@ -16,8 +25,22 @@ import time
16
25
 
17
26
  DIST = "webbee"
18
27
  MCP = "imperal-mcp"
28
+ SDK = "imperal-sdk"
19
29
  MCP_SPEC = "imperal-mcp[code]"
20
- MCP_GIT_SPEC = "imperal-mcp[code] @ git+https://github.com/imperalcloud/imperal-mcp.git"
30
+ SDK_SPEC = "imperal-sdk[universal-index]"
31
+ STABLE = "stable" # the branch CI moves to each release tag (never a version: see above)
32
+ REPOS = {DIST: "https://github.com/imperalcloud/webbee-code.git",
33
+ MCP: "https://github.com/imperalcloud/imperal-mcp.git",
34
+ SDK: "https://github.com/imperalcloud/imperal-sdk.git"}
35
+ NAMES = {DIST: DIST, MCP: MCP_SPEC, SDK: SDK_SPEC} # each with the extras its install needs
36
+
37
+
38
+ def git_spec(dist: str, ref: str = STABLE) -> str:
39
+ """``dist`` (with its extras) from its repository at ``ref`` -- the ``stable`` branch unless a tag is named."""
40
+ return f"{NAMES[dist]} @ git+{REPOS[dist]}@{ref}"
41
+
42
+
43
+ MCP_GIT_SPEC = git_spec(MCP)
21
44
  PYPI = "https://pypi.org/pypi/{}/json"
22
45
  OFF_ENV = "WEBBEE_NO_AUTOUPDATE" # 1: never upgrade at the start (CI, a pinned setup)
23
46
  DONE_ENV = "WEBBEE_UPGRADED" # set on the restart after a self-upgrade: never twice in a row
@@ -74,6 +97,72 @@ def wheel_url(dist: str, version: str):
74
97
  return None
75
98
 
76
99
 
100
+ def stable_version(dist: str, git, run=subprocess.run):
101
+ """The release ``dist``'s ``stable`` branch stands on -- the version tag on its commit, read from the repository
102
+ itself (no index that trails a release) -- or None: no git, GitHub out of reach, no branch or no tag on it."""
103
+ if not git:
104
+ return None
105
+ try:
106
+ got = run([git, "ls-remote", REPOS[dist], f"refs/heads/{STABLE}", "refs/tags/v*"], capture_output=True,
107
+ text=True, timeout=30, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"})
108
+ if got.returncode != 0:
109
+ return None
110
+ head, direct, peeled = "", {}, {}
111
+ for line in str(got.stdout or "").splitlines():
112
+ sha, _, ref = line.strip().partition("\t")
113
+ if ref == f"refs/heads/{STABLE}":
114
+ head = sha
115
+ elif ref.startswith("refs/tags/v"):
116
+ name = ref[len("refs/tags/v"):]
117
+ if name.endswith("^{}"):
118
+ peeled[name[:-3]] = sha # the commit an annotated tag names
119
+ else:
120
+ direct[name] = sha
121
+ on_head = [v for v, sha in direct.items() if peeled.get(v, sha) == head and _ver(v)]
122
+ return max(on_head, key=_ver) if head and on_head else None
123
+ except Exception:
124
+ return None
125
+
126
+
127
+ def on_stable(tool: str, prefix: str, dists) -> bool:
128
+ """What pipx / uv will install again for the environment at ``prefix`` (``pipx reinstall``, ``pipx upgrade``,
129
+ ``uv tool upgrade``) names each of ``dists`` by its repository's stable branch. Plain pip keeps no such record."""
130
+ try:
131
+ if tool == "pipx":
132
+ import json
133
+ with open(os.path.join(prefix, "pipx_metadata.json"), encoding="utf-8") as f:
134
+ meta = json.load(f)
135
+ recorded = [str((meta.get("main_package") or {}).get("package_or_url") or "")]
136
+ recorded += [str((p or {}).get("package_or_url") or "") for p in (meta.get("injected_packages") or {}).values()]
137
+ return all(any(f"git+{REPOS[d]}@{STABLE}" in s for s in recorded) for d in dists)
138
+ if tool == "uv":
139
+ with open(os.path.join(prefix, "uv-receipt.toml"), encoding="utf-8") as f:
140
+ receipt = f.read()
141
+ return all(f'git = "{REPOS[d]}?rev={STABLE}"' in receipt for d in dists)
142
+ except Exception:
143
+ return False
144
+ return True
145
+
146
+
147
+ def _git_install(tool: str, main: str, withs, uv, pipx, python, venv: str, record: bool):
148
+ """``main`` and ``withs`` (the packages beside it) from git, by ``tool``, recorded as given. pipx: an environment
149
+ already recorded on the branch is upgraded by its own record (``upgrade --include-injected``, in place); otherwise
150
+ (``record``: a new one, or one recorded by a version) installed again and its packages injected -- the only way
151
+ pipx writes a new record. uv records ``--with`` beside the tool on every install."""
152
+ if tool == "uv" and uv:
153
+ cmd = [uv, "tool", "install", "--force", "--refresh"]
154
+ for w in withs:
155
+ cmd += ["--with", w]
156
+ return cmd + [main]
157
+ if tool == "pipx" and pipx:
158
+ if not record:
159
+ return [pipx, "upgrade", "--include-injected", venv]
160
+ return [[pipx, "install", "--force", main], [pipx, "inject", "--force", venv, *withs]]
161
+ if tool == "pip" and python:
162
+ return [python, "-m", "pip", "install", "--upgrade", "--no-cache-dir", main, *withs]
163
+ return None
164
+
165
+
77
166
  def _spec(name: str, dist: str, version: str, wheel) -> str:
78
167
  """``name`` (with its extras) at ``version``: by its wheel's address when known, else by the index."""
79
168
  url = wheel(dist, version) if wheel else None
@@ -106,10 +195,15 @@ def find_uv(which=safe_which):
106
195
 
107
196
 
108
197
  def self_command(version: str, prefix: str = sys.prefix, executable: str = sys.executable, env=os.environ,
109
- uv=None, pipx=None, wheel=None):
110
- """The command that installs webbee ``version`` where this webbee is installed -- or None."""
111
- spec = _spec(DIST, DIST, version, wheel)
198
+ uv=None, pipx=None, wheel=None, source: str = "pypi", record: bool = True, ref: str = STABLE):
199
+ """The command that installs webbee ``version`` where this webbee is installed -- or None. ``source`` "git": from
200
+ the repositories at ``ref`` (the stable branch; a tag only to put a release back), imperal-mcp and imperal-sdk
201
+ beside it; ``record``: pipx writes its record anew (see ``_git_install``)."""
112
202
  light = env.get(LIGHT_ENV)
203
+ if source == "git" and not light:
204
+ return _git_install(tool_of(prefix), git_spec(DIST, ref), [git_spec(MCP), git_spec(SDK)], uv, pipx,
205
+ executable, DIST, record)
206
+ spec = _spec(DIST, DIST, version, wheel)
113
207
  if light: # an old Python runs this release through its light install
114
208
  return [light, "-m", "pip", "install", "--upgrade", "--no-cache-dir", spec]
115
209
  return _install(tool_of(prefix), spec, uv, pipx, executable, venv=DIST)
@@ -132,13 +226,15 @@ def _install(tool: str, spec: str, uv, pipx, python, venv: str = ""):
132
226
 
133
227
 
134
228
  def mcp_installed(which=safe_which, run=subprocess.run) -> dict:
135
- """The shell's imperal-mcp (the command on PATH): ``{"tool", "python", "version", "code"}`` -- {} when none."""
229
+ """The shell's imperal-mcp (the command on PATH): ``{"tool", "python", "prefix", "version", "code"}`` -- {} when
230
+ none."""
136
231
  exe = which(MCP)
137
232
  if not exe:
138
233
  return {}
139
234
  real = os.path.realpath(exe)
140
235
  py = os.path.join(os.path.dirname(real), "python.exe" if _WINDOWS else "python")
141
- out = {"tool": tool_of(real), "python": py, "version": "", "code": False}
236
+ out = {"tool": tool_of(real), "python": py, "prefix": os.path.dirname(os.path.dirname(real)), "version": "",
237
+ "code": False}
142
238
  # asked twice when it says "no code extra": a reinstall is never made on one odd answer (live 2026-10-25: right after
143
239
  # another install the first answer said no webbee, the next ones yes)
144
240
  for _ in range(2):
@@ -155,18 +251,27 @@ def mcp_installed(which=safe_which, run=subprocess.run) -> dict:
155
251
  return out
156
252
 
157
253
 
158
- def mcp_command(inst: dict, newest, uv, pipx, wheel=None):
254
+ def mcp_command(inst: dict, newest, uv, pipx, wheel=None, source: str = "pypi"):
159
255
  """The one command that makes the shell's imperal-mcp the newest with its code extra -- by the tool that installed
160
- it (uv, else pipx, for a new install) -- or None when it already is, or nothing can do it."""
161
- if inst and inst.get("version") and inst.get("version") == newest and inst.get("code"):
256
+ it (uv, else pipx, for a new install) -- or None when it already is, or nothing can do it. ``source`` "git": from
257
+ the stable branches, imperal-sdk and webbee beside it, and an install recorded by a version is moved onto the
258
+ branch even when its version is the newest (what ``pipx reinstall`` / ``uv tool upgrade`` would put back)."""
259
+ git = source == "git"
260
+ recorded = bool(inst) and on_stable(inst.get("tool") or "", inst.get("prefix") or "", (MCP, SDK, DIST))
261
+ if inst and inst.get("version") and inst.get("version") == newest and inst.get("code") and (recorded or not git):
162
262
  return None
163
263
  if inst and not inst.get("version"):
164
264
  return None # it did not answer: a working install is never replaced blind
165
265
  if inst and not newest and inst.get("code"):
166
266
  return None # PyPI unread: what works stays
167
- spec = _spec(MCP_SPEC, MCP, newest, wheel) if newest else (MCP_SPEC if inst else MCP_GIT_SPEC)
267
+ if git and inst and _ver(inst.get("version")) > _ver(newest):
268
+ return None # newer than the last release (a build of its own): never taken back
168
269
  tool = inst.get("tool") if inst else ("uv" if uv else "pipx" if pipx else "")
169
- return _install(tool, spec, uv, pipx, inst.get("python") if inst else None, venv=MCP if inst else "")
270
+ if git:
271
+ return _git_install(tool or "", MCP_GIT_SPEC, [git_spec(SDK), git_spec(DIST)], uv, pipx,
272
+ inst.get("python") if inst else None, MCP, record=not recorded)
273
+ spec = _spec(MCP_SPEC, MCP, newest, wheel) if newest else (MCP_SPEC if inst else MCP_GIT_SPEC)
274
+ return _install(tool or "", spec, uv, pipx, inst.get("python") if inst else None, venv=MCP if inst else "")
170
275
 
171
276
 
172
277
  def restart_command(argv, env=os.environ, executable: str = sys.executable):
@@ -285,23 +390,46 @@ def imports_ok(python: str, run=subprocess.run, light: bool = False) -> bool:
285
390
  return False
286
391
 
287
392
 
393
+ def _steps(cmd) -> list:
394
+ """One command, or the commands of one install in their order (pipx: install, then inject)."""
395
+ return [list(c) for c in cmd] if cmd and isinstance(cmd[0], (list, tuple)) else [list(cmd or [])]
396
+
397
+
398
+ def _show(cmd) -> str:
399
+ """What a person runs by hand to do ``cmd``."""
400
+ return " && ".join(" ".join(f"'{a}'" if " " in a or "[" in a else a for a in c) for c in _steps(cmd))
401
+
402
+
288
403
  def _run(cmd, run=subprocess.run) -> bool:
404
+ """Every step of ``cmd`` in its order; the first that fails stops the rest."""
289
405
  try:
290
- return run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
291
- timeout=STEP_TIMEOUT_S).returncode == 0
406
+ for step in _steps(cmd):
407
+ if run(step, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
408
+ timeout=STEP_TIMEOUT_S).returncode != 0:
409
+ return False
410
+ return True
292
411
  except Exception:
293
412
  return False
294
413
 
295
414
 
296
- def _roll_back(current: str, newest: str, python: str, light: str, env, uv, pipx, wheel, run, say) -> bool:
415
+ def _newest(dist: str, git, fetch, stable) -> tuple:
416
+ """``(version, source)``: the release on ``dist``'s stable branch ("git") when git reads it, else PyPI's."""
417
+ v = stable(dist, git) if git else None
418
+ return (v, "git") if v else (fetch(dist), "pypi")
419
+
420
+
421
+ def _roll_back(current: str, newest: str, python: str, light: str, env, uv, pipx, wheel, run, say,
422
+ source: str = "pypi", prefix: str = sys.prefix) -> bool:
297
423
  """``newest`` installed but does not import: ``current`` installed again by the same tool and checked the same
298
- way -- never a release on disk that does not start. Says what happened either way."""
299
- back = self_command(current, env=env, uv=uv, pipx=pipx, wheel=wheel)
424
+ way -- never a release on disk that does not start. Says what happened either way. From git: ``current``'s own
425
+ tag (recorded so until the next release moves it onto the branch again)."""
426
+ back = self_command(current, prefix=prefix, env=env, uv=uv, pipx=pipx, wheel=wheel, source=source,
427
+ ref=f"v{current}")
300
428
  if back and _run(back, run) and imports_ok(python, run, light=bool(light)):
301
429
  say(f"🐝 webbee {newest} was installed but does not start (its modules do not import) -- "
302
430
  f"put {current} back; starting {current}")
303
431
  return True
304
- fix = " ".join(back) if back else f"pip install webbee=={current}"
432
+ fix = _show(back) if back else f"pip install webbee=={current}"
305
433
  say(f"🐝 webbee {newest} was installed but does not start, and {current} could not be put back -- "
306
434
  f"this session runs on, but the next start may fail: run {fix}")
307
435
  return False
@@ -309,7 +437,7 @@ def _roll_back(current: str, newest: str, python: str, light: str, env, uv, pipx
309
437
 
310
438
  def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe_which, run=subprocess.run,
311
439
  execv=os.execv, env=os.environ, wheel=wheel_url, lock_path=_lock_path,
312
- sleep=time.sleep) -> list:
440
+ sleep=time.sleep, stable=stable_version, prefix: str = sys.prefix) -> list:
313
441
  """Bring webbee and the shell's imperal-mcp to their newest releases, then let the start go on; a self-upgrade
314
442
  starts webbee again on the new release (never returns then). Returns what it did, for the log. Never raises."""
315
443
  did: list = []
@@ -330,7 +458,7 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
330
458
  return did
331
459
  if state == "free":
332
460
  lock = ""
333
- newest = fetch(DIST)
461
+ newest = _newest(DIST, None if env.get(LIGHT_ENV) else which("git"), fetch, stable)[0]
334
462
  if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
335
463
  env[DONE_ENV] = "1"
336
464
  again = restart_command(list(argv), env=env)
@@ -339,12 +467,19 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
339
467
  execv(again[0], again)
340
468
  return did
341
469
  uv, pipx = find_uv(which), which("pipx")
342
- newest = fetch(DIST)
343
- if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
344
- cmd = self_command(newest, env=env, uv=uv, pipx=pipx, wheel=wheel)
470
+ light = env.get(LIGHT_ENV) or ""
471
+ git = None if light else which("git") # 3.7's pip builds no package from source: the light install stays on PyPI
472
+ newest, source = _newest(DIST, git, fetch, stable)
473
+ tool = tool_of(prefix)
474
+ recorded = source != "git" or on_stable(tool, prefix, (DIST, MCP, SDK))
475
+ # the same release recorded by a version: moved onto the branch (what pipx/uv would put back on a reinstall)
476
+ if newest and (_ver(newest) > _ver(current) or (not recorded and _ver(newest) == _ver(current))) \
477
+ and not env.get(DONE_ENV):
478
+ cmd = self_command(newest, prefix=prefix, env=env, uv=uv, pipx=pipx, wheel=wheel, source=source,
479
+ record=not recorded)
345
480
  if cmd:
346
- say(f"🐝 updating webbee {current} -> {newest} ...")
347
- light = env.get(LIGHT_ENV) or ""
481
+ say(f"🐝 updating webbee {current} -> {newest} ..." if _ver(newest) > _ver(current) else
482
+ f"🐝 moving webbee {current} onto its release branch, so a reinstall brings the newest ...")
348
483
  python = light or sys.executable
349
484
  installed = _run(cmd, run)
350
485
  ok = installed and (imports_ok(python, run, light=bool(light)) or
@@ -353,7 +488,7 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
353
488
  # on disk a release that does not import (or an install that broke off halfway): the next start
354
489
  # would run it and stay stuck. The one running now (and any other open session) holds the old
355
490
  # modules: put the old release back
356
- _roll_back(current, newest, python, light, env, uv, pipx, wheel, run, say)
491
+ _roll_back(current, newest, python, light, env, uv, pipx, wheel, run, say, source, prefix)
357
492
  elif ok:
358
493
  did.append(f"webbee {newest}")
359
494
  env[DONE_ENV] = "1" # the restart inherits it: never a second self-upgrade
@@ -365,18 +500,18 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
365
500
  execv(again[0], again)
366
501
  return did
367
502
  else:
368
- say(f"🐝 webbee could not update itself -- run: {' '.join(cmd)}")
369
- mcp_newest = fetch(MCP)
503
+ say(f"🐝 webbee could not update itself -- run: {_show(cmd)}")
504
+ mcp_newest, mcp_source = _newest(MCP, git, fetch, stable)
370
505
  inst = mcp_installed(which, run)
371
- cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel)
506
+ cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel, source=mcp_source)
372
507
  if cmd:
373
508
  what = (f"imperal-mcp {inst.get('version') or '?'} -> {mcp_newest or 'source'}" if inst
374
- else f"imperal-mcp {mcp_newest or 'from source'}")
509
+ else f"imperal-mcp {mcp_newest or 'from source'}") + (" (git, stable)" if mcp_source == "git" else "")
375
510
  say(f"🐝 updating {what} ...")
376
511
  if _run(cmd, run):
377
512
  did.append(what)
378
513
  else:
379
- say(f"🐝 imperal-mcp could not be updated -- run: {' '.join(cmd)}")
514
+ say(f"🐝 imperal-mcp could not be updated -- run: {_show(cmd)}")
380
515
  except SystemExit:
381
516
  raise
382
517
  except Exception:
@@ -299,3 +299,169 @@ def test_an_upgrader_that_dies_during_the_wait_is_taken_over(tmp_path):
299
299
  which=_which({"pipx": "/bin/pipx"}), run=run, env={}, execv=lambda p, a: None,
300
300
  lock_path=lambda: str(lock), sleep=sleep)
301
301
  assert len(slept) == 1 and any("webbee==0.6.5" in c for c in run.calls)
302
+
303
+
304
+ # ── owner 2026-11-06: always the newest, from git. Live: `pipx reinstall webbee` put back 0.6.7 while 0.7.5 was out --
305
+ # pipx had recorded 0.6.7's wheel address and reinstalls its record; the record is now the stable branch ──
306
+ WB, MCP_G, SDK_G = au.git_spec(au.DIST), au.git_spec(au.MCP), au.git_spec(au.SDK)
307
+ H_OLD, H_NEW, H_TAG = "a" * 40, "b" * 40, "c" * 40
308
+
309
+
310
+ def _ls_remote(stable_sha):
311
+ lines = [f"{H_OLD}\trefs/tags/v0.7.4", # a lightweight tag
312
+ f"{H_TAG}\trefs/tags/v0.7.5", f"{H_NEW}\trefs/tags/v0.7.5^{{}}", # an annotated one: its commit
313
+ f"{stable_sha}\trefs/heads/stable"]
314
+ return lambda cmd, **kw: subprocess.CompletedProcess(cmd, 0, stdout="\n".join(lines) + "\n", stderr="")
315
+
316
+
317
+ def test_the_newest_release_is_the_tag_the_stable_branch_stands_on():
318
+ assert au.stable_version("webbee", "/bin/git", run=_ls_remote(H_NEW)) == "0.7.5"
319
+ assert au.stable_version("webbee", "/bin/git", run=_ls_remote(H_OLD)) == "0.7.4"
320
+ assert au.stable_version("webbee", "/bin/git", run=_ls_remote("d" * 40)) is None # between tags: not a release
321
+ assert au.stable_version("webbee", None) is None # no git
322
+ failed = lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, stdout="", stderr="") # noqa: E731
323
+ assert au.stable_version("webbee", "/bin/git", run=failed) is None # GitHub out of reach
324
+ seen = []
325
+ au.stable_version("imperal-sdk", "/bin/git", run=lambda cmd, **kw: seen.append((cmd, kw)) or 1 / 0)
326
+ assert seen[0][0][:3] == ["/bin/git", "ls-remote", au.REPOS["imperal-sdk"]]
327
+ assert seen[0][1]["env"]["GIT_TERMINAL_PROMPT"] == "0" # never waits on a prompt
328
+
329
+
330
+ def test_the_spec_is_the_branch_never_a_version():
331
+ assert WB == "webbee @ git+https://github.com/imperalcloud/webbee-code.git@stable"
332
+ assert MCP_G == "imperal-mcp[code] @ git+https://github.com/imperalcloud/imperal-mcp.git@stable"
333
+ assert SDK_G == "imperal-sdk[universal-index] @ git+https://github.com/imperalcloud/imperal-sdk.git@stable"
334
+ assert au.MCP_GIT_SPEC == MCP_G
335
+
336
+
337
+ def _pipx_venv(tmp_path, main, injected=()):
338
+ import json
339
+ venv = tmp_path / "pipx" / "venvs" / "webbee"
340
+ venv.mkdir(parents=True)
341
+ (venv / "pipx_metadata.json").write_text(json.dumps({
342
+ "main_package": {"package_or_url": main},
343
+ "injected_packages": {s.split()[0]: {"package_or_url": s} for s in injected}}))
344
+ return str(venv)
345
+
346
+
347
+ def test_what_pipx_and_uv_recorded_is_read(tmp_path):
348
+ pinned = _pipx_venv(tmp_path / "a", "webbee @ https://files.pythonhosted.org/x/webbee-0.6.7-py3-none-any.whl")
349
+ assert not au.on_stable("pipx", pinned, ("webbee",))
350
+ good = _pipx_venv(tmp_path / "b", WB, (MCP_G, SDK_G))
351
+ assert au.on_stable("pipx", good, ("webbee", "imperal-mcp", "imperal-sdk"))
352
+ half = _pipx_venv(tmp_path / "c", WB) # its packages still from PyPI
353
+ assert not au.on_stable("pipx", half, ("webbee", "imperal-mcp"))
354
+ uv = tmp_path / "uv" / "tools" / "imperal-mcp"
355
+ uv.mkdir(parents=True)
356
+ (uv / "uv-receipt.toml").write_text('[tool]\nrequirements = [{ name = "imperal-mcp", extras = ["code"], '
357
+ 'specifier = "==0.9.5" }]\n')
358
+ assert not au.on_stable("uv", str(uv), ("imperal-mcp",))
359
+ (uv / "uv-receipt.toml").write_text(
360
+ '[tool]\nrequirements = [\n'
361
+ ' { name = "imperal-mcp", extras = ["code"], git = "https://github.com/imperalcloud/imperal-mcp.git?rev=stable" },\n'
362
+ ' { name = "imperal-sdk", extras = ["universal-index"], git = "https://github.com/imperalcloud/imperal-sdk.git?rev=stable" },\n'
363
+ ' { name = "webbee", git = "https://github.com/imperalcloud/webbee-code.git?rev=stable" },\n]\n')
364
+ assert au.on_stable("uv", str(uv), ("imperal-mcp", "imperal-sdk", "webbee"))
365
+ assert not au.on_stable("pipx", str(tmp_path / "missing"), ("webbee",)) # unreadable: moved, never assumed
366
+ assert au.on_stable("pip", "/venv", ("webbee",)) # plain pip keeps no record
367
+
368
+
369
+ def test_from_git_each_tool_installs_and_records_the_branch():
370
+ uv = au.self_command("0.7.5", prefix="/u/.local/share/uv/tools/webbee", env={}, uv="/bin/uv", source="git")
371
+ assert uv == ["/bin/uv", "tool", "install", "--force", "--refresh", "--with", MCP_G, "--with", SDK_G, WB]
372
+ px = au.self_command("0.7.5", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx", source="git")
373
+ assert px == [["/bin/pipx", "install", "--force", WB], ["/bin/pipx", "inject", "--force", "webbee", MCP_G, SDK_G]]
374
+ kept = au.self_command("0.7.5", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx", source="git",
375
+ record=False) # already on the branch: its own record
376
+ assert kept == ["/bin/pipx", "upgrade", "--include-injected", "webbee"]
377
+ pip = au.self_command("0.7.5", prefix="/v", executable="/v/bin/python", env={}, source="git")
378
+ assert pip == ["/v/bin/python", "-m", "pip", "install", "--upgrade", "--no-cache-dir", WB, MCP_G, SDK_G]
379
+ light = au.self_command("0.7.5", env={au.LIGHT_ENV: "/usr/bin/python3.7"}, source="git", wheel=None)
380
+ assert light[-1] == "webbee==0.7.5" # 3.7 builds nothing from source: PyPI
381
+
382
+
383
+ def test_the_same_release_recorded_by_a_version_is_moved_onto_the_branch(tmp_path):
384
+ venv = _pipx_venv(tmp_path, "webbee @ https://files.pythonhosted.org/x/webbee-0.7.5-py3-none-any.whl")
385
+ run, execd, env, said = _Run(), [], {}, []
386
+ stable = {"webbee": "0.7.5", "imperal-mcp": "0.9.5"}.get
387
+ au.upgrade_before_start(["--once"], "0.7.5", say=said.append, fetch=lambda d: 1 / 0, wheel=None,
388
+ which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env=env,
389
+ execv=lambda p, a: execd.append(a), lock_path=lambda: str(tmp_path / "upgrade.lock"),
390
+ stable=lambda d, git: stable(d), prefix=venv)
391
+ assert ["/bin/pipx", "install", "--force", WB] in run.calls
392
+ assert ["/bin/pipx", "inject", "--force", "webbee", MCP_G, SDK_G] in run.calls
393
+ assert execd and any("onto its release branch" in s for s in said)
394
+
395
+
396
+ def test_on_the_branch_and_newest_nothing_is_installed(tmp_path, monkeypatch):
397
+ venv = _pipx_venv(tmp_path, WB, (MCP_G, SDK_G))
398
+ exe = tmp_path / "pipx" / "venvs" / "imperal-mcp" / "bin" / "imperal-mcp"
399
+ exe.parent.mkdir(parents=True)
400
+ exe.write_text("")
401
+ _pipx_venv(tmp_path / "m", MCP_G, (SDK_G, WB))
402
+ import shutil
403
+ shutil.copy(tmp_path / "m" / "pipx" / "venvs" / "webbee" / "pipx_metadata.json", exe.parent.parent)
404
+ monkeypatch.setattr(au, "find_uv", lambda which=None: None)
405
+ run = _Run(stdout="0.9.5 True")
406
+ au.upgrade_before_start([], "0.7.5", say=lambda m: None, fetch=lambda d: 1 / 0, wheel=None,
407
+ which=_which({"pipx": "/bin/pipx", "git": "/bin/git", "imperal-mcp": str(exe)}), run=run,
408
+ env={}, execv=lambda *a: 1 / 0, lock_path=lambda: str(tmp_path / "upgrade.lock"),
409
+ stable=lambda d, git: {"webbee": "0.7.5", "imperal-mcp": "0.9.5"}[d], prefix=venv)
410
+ assert not any("install" in c or "upgrade" in c or "inject" in c for c in run.calls)
411
+
412
+
413
+ def test_a_newer_release_on_the_branch_is_taken_by_the_install_s_own_record(tmp_path):
414
+ venv = _pipx_venv(tmp_path, WB, (MCP_G, SDK_G))
415
+ run, execd = _Run(), []
416
+ au.upgrade_before_start([], "0.7.4", say=lambda m: None, fetch=lambda d: 1 / 0, wheel=None,
417
+ which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
418
+ execv=lambda p, a: execd.append(a), lock_path=lambda: str(tmp_path / "upgrade.lock"),
419
+ stable=lambda d, git: "0.7.5", prefix=venv)
420
+ assert run.calls[0] == ["/bin/pipx", "upgrade", "--include-injected", "webbee"] and execd
421
+
422
+
423
+ def test_no_git_or_github_out_of_reach_falls_back_to_pypi(tmp_path):
424
+ run = _Run()
425
+ au.upgrade_before_start([], "0.7.4", say=lambda m: None, fetch={"webbee": "0.7.5"}.get, wheel=None,
426
+ which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
427
+ execv=lambda p, a: None, lock_path=lambda: str(tmp_path / "upgrade.lock"),
428
+ stable=lambda d, git: None, prefix="/h/.local/pipx/venvs/webbee")
429
+ assert run.calls[0][-1] == "webbee==0.7.5"
430
+
431
+
432
+ def test_the_shell_s_imperal_mcp_recorded_by_a_version_is_moved_onto_the_branch(tmp_path):
433
+ uv = tmp_path / "uv" / "tools" / "imperal-mcp"
434
+ uv.mkdir(parents=True)
435
+ (uv / "uv-receipt.toml").write_text('[tool]\nrequirements = [{ name = "imperal-mcp", extras = ["code"], '
436
+ 'specifier = "==0.9.5" }]\n')
437
+ inst = {"tool": "uv", "python": str(uv / "bin" / "python"), "prefix": str(uv), "version": "0.9.5", "code": True}
438
+ assert au.mcp_command(inst, "0.9.5", "/bin/uv", None) is None # PyPI: nothing to do
439
+ assert au.mcp_command(inst, "0.9.5", "/bin/uv", None, source="git") == [
440
+ "/bin/uv", "tool", "install", "--force", "--refresh", "--with", SDK_G, "--with", WB, MCP_G]
441
+ assert au.mcp_command(dict(inst, version="0.9.6"), "0.9.5", "/bin/uv", None, source="git") is None # own build
442
+ assert au.mcp_command({}, "0.9.5", None, "/bin/pipx", source="git") == [
443
+ ["/bin/pipx", "install", "--force", MCP_G], ["/bin/pipx", "inject", "--force", "imperal-mcp", SDK_G, WB]]
444
+
445
+
446
+ def test_the_steps_of_one_install_stop_at_the_first_that_fails():
447
+ calls = []
448
+
449
+ def run(cmd, **kw):
450
+ calls.append(cmd)
451
+ return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
452
+ assert au._run([["/bin/pipx", "install", "x"], ["/bin/pipx", "inject", "y"]], run) is False
453
+ assert len(calls) == 1
454
+ shown = au._show([["/bin/pipx", "install", "--force", WB], ["/bin/pipx", "inject", "--force", "webbee", MCP_G]])
455
+ assert shown == f"/bin/pipx install --force '{WB}' && /bin/pipx inject --force webbee '{MCP_G}'"
456
+
457
+
458
+ def test_a_git_release_that_does_not_import_is_put_back_by_its_own_tag(tmp_path, monkeypatch):
459
+ _checks(monkeypatch, [False, False, True])
460
+ venv = _pipx_venv(tmp_path, WB, (MCP_G, SDK_G))
461
+ run, said = _Run(), []
462
+ au.upgrade_before_start([], "0.7.4", say=said.append, fetch=lambda d: 1 / 0, wheel=None,
463
+ which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
464
+ execv=lambda *a: 1 / 0, lock_path=lambda: str(tmp_path / "upgrade.lock"),
465
+ stable=lambda d, git: "0.7.5", prefix=venv)
466
+ assert ["/bin/pipx", "install", "--force", au.git_spec("webbee", "v0.7.4")] in run.calls
467
+ assert any("put 0.7.4 back" in s for s in said)
@@ -33,5 +33,10 @@ def test_install_script_is_posix_and_uses_uv():
33
33
  # no longer needs extras. The [intel]/[intel-embed] names survive as empty
34
34
  # aliases, so an OLD copy of this script still works -- but the shipped one
35
35
  # must be the plain form.
36
- assert 'uv tool install "webbee"' in txt
36
+ # 2026-11-06: always the newest release, from git -- the three packages by their stable branch (never a version,
37
+ # so `uv tool upgrade` and a second run bring the newest); without git the plain form from PyPI
38
+ for spec in ('"webbee @ git+$GH/webbee-code.git@stable"', '"imperal-mcp[code] @ git+$GH/imperal-mcp.git@stable"',
39
+ '"imperal-sdk[universal-index] @ git+$GH/imperal-sdk.git@stable"'):
40
+ assert spec in txt
41
+ assert 'uv tool install --force --refresh "webbee"' in txt
37
42
  assert "webbee[intel" not in txt, "extras are redundant since 0.3.51"