webbee 0.7.5__tar.gz → 0.7.7__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.7.5 → webbee-0.7.7}/.github/workflows/publish.yml +13 -0
- {webbee-0.7.5 → webbee-0.7.7}/CHANGELOG.md +22 -0
- {webbee-0.7.5 → webbee-0.7.7}/PKG-INFO +6 -1
- {webbee-0.7.5 → webbee-0.7.7}/README.md +5 -0
- webbee-0.7.7/install.sh +29 -0
- {webbee-0.7.5 → webbee-0.7.7}/pyproject.toml +1 -1
- webbee-0.7.7/src/webbee/__init__.py +1 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/autoupdate.py +175 -31
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_autoupdate.py +170 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_packaging.py +6 -1
- webbee-0.7.5/install.sh +0 -20
- webbee-0.7.5/src/webbee/__init__.py +0 -1
- {webbee-0.7.5 → webbee-0.7.7}/.gitignore +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/LICENSE +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/__main__.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/_launch.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/account.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/active_sessions.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/banner_art.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/boot.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/browser.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/checkpoints.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/cli.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/clipboard.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/coding_context.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/commands.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/config.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/consent.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/details.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/events.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/frames.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/home.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/home_view.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/http.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/instance_lock.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/graph.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/outline.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/query.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/service.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/tree.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/intel/watch.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/media.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/modals.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/mode_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/native_files.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/native_read.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/output_pane.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/pricing_constants.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/queue_panel.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/reflow.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/remote.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/render.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/repl.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/repo.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/selection.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/session.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/sessions.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/sizing.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/slots.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/steer.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/stream.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tab_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tabs.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/thread.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tier_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/todo_panel.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tokens.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tools.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/tui.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/update.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/urlopen.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/wallet.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/src/webbee/worktrees.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/__init__.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/conftest.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_account.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_active_sessions.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_checkpoints.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_checkpoints_nested_1030.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_cli.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_clipboard.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_clipboard_read.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_code_map_fresh.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_coding_context.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_commands.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_config.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_cpc_outline.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_cross_loop_token.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_details.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_events.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_freeze_fix.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_home.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_home_view.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_home_view_mouse.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_inspect_page.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_instance_lock.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_any_folder.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_cache_and_sync_worker.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_checksum_tree.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_chunker.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_embed.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_folder_1106.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_graph.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_query.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_service.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_vectors.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_intel_watch.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_launch.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_marathon.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_modals_e2e.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_mode_and_remote_0_6_8.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_mode_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_native_files.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_newtab_mode.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_probe_cost.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_reflow.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_remote_code_reads.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_render.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_repl.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_repo.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_resilient_http.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_review_fixes_1030.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_session.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_session_review_1030.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_sessions.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_sizing.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_slots.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_steer.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_stream.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tab_bar_stability.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tabs.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_thread.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tier_invalidation.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tier_store.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_todo_panel.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tokens.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tools.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tools_eol_cancel_1030.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tui.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_tui_hardening.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_update.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_urlopen.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_version.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/tests/test_wallet.py +0 -0
- {webbee-0.7.5 → webbee-0.7.7}/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,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.7
|
|
4
|
+
|
|
5
|
+
* `pipx reinstall webbee` brings the newest release for real (live check of 0.7.6: it still gave 0.7.5).
|
|
6
|
+
* imperal-mcp is installed beside webbee without its code extra. The extra requires webbee, and on a reinstall
|
|
7
|
+
pipx (uv backend) took that webbee from PyPI over the git one while PyPI's index still showed the release before.
|
|
8
|
+
webbee's own dependencies already bring what the extra holds.
|
|
9
|
+
* An install recorded with that extra is moved once onto the corrected record.
|
|
10
|
+
|
|
11
|
+
## 0.7.6
|
|
12
|
+
|
|
13
|
+
* **Always the newest release, from git** (06.11: `pipx reinstall webbee` put back 0.6.7 while 0.7.5 was out).
|
|
14
|
+
* pipx and uv install again what they recorded. The upgrade at the start had recorded a release's own wheel
|
|
15
|
+
address, so a reinstall went back to that release.
|
|
16
|
+
* webbee, imperal-mcp and imperal-sdk now come from their repositories' `stable` branch, which each release's CI
|
|
17
|
+
moves to its tag. The newest release is read from git itself (the tag `stable` stands on), never from an index
|
|
18
|
+
that trails it.
|
|
19
|
+
* What pipx / uv record is the branch: `pipx reinstall`, `pipx upgrade --include-injected` and `uv tool upgrade`
|
|
20
|
+
bring the newest release. An install still recorded by a version is moved onto the branch once, even on the
|
|
21
|
+
newest release.
|
|
22
|
+
* `install.sh` installs from the branch too. No git, GitHub out of reach, or the light install of Python 3.7-3.9:
|
|
23
|
+
PyPI, as before.
|
|
24
|
+
|
|
3
25
|
## 0.7.5
|
|
4
26
|
|
|
5
27
|
* Needs imperal-mcp 0.9.5:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webbee
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.7
|
|
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
|
webbee-0.7.7/install.sh
ADDED
|
@@ -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 @ 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)"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.7"
|
|
@@ -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,28 @@ 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
|
-
|
|
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, extras: bool = True) -> str:
|
|
39
|
+
"""``dist`` (with its extras) from its repository at ``ref`` -- the ``stable`` branch unless a tag is named."""
|
|
40
|
+
return f"{NAMES[dist] if extras else dist} @ git+{REPOS[dist]}@{ref}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MCP_GIT_SPEC = git_spec(MCP)
|
|
44
|
+
# imperal-mcp beside webbee goes WITHOUT its code extra: the extra requires webbee, and pipx (uv backend) installed
|
|
45
|
+
# that webbee from PyPI over the git one on a reinstall (live 2026-11-06: stable 0.7.6, `pipx reinstall` gave PyPI's
|
|
46
|
+
# 0.7.5). webbee's own dependencies already hold what the extra brings (tree-sitter, the sdk's index).
|
|
47
|
+
MCP_BESIDE_WEBBEE = git_spec(MCP, extras=False)
|
|
48
|
+
WEBBEE_ENV_SPECS = (git_spec(DIST), MCP_BESIDE_WEBBEE, git_spec(SDK)) # webbee's own environment
|
|
49
|
+
MCP_ENV_SPECS = (MCP_GIT_SPEC, git_spec(SDK), git_spec(DIST)) # the shell's imperal-mcp
|
|
21
50
|
PYPI = "https://pypi.org/pypi/{}/json"
|
|
22
51
|
OFF_ENV = "WEBBEE_NO_AUTOUPDATE" # 1: never upgrade at the start (CI, a pinned setup)
|
|
23
52
|
DONE_ENV = "WEBBEE_UPGRADED" # set on the restart after a self-upgrade: never twice in a row
|
|
@@ -74,6 +103,75 @@ def wheel_url(dist: str, version: str):
|
|
|
74
103
|
return None
|
|
75
104
|
|
|
76
105
|
|
|
106
|
+
def stable_version(dist: str, git, run=subprocess.run):
|
|
107
|
+
"""The release ``dist``'s ``stable`` branch stands on -- the version tag on its commit, read from the repository
|
|
108
|
+
itself (no index that trails a release) -- or None: no git, GitHub out of reach, no branch or no tag on it."""
|
|
109
|
+
if not git:
|
|
110
|
+
return None
|
|
111
|
+
try:
|
|
112
|
+
got = run([git, "ls-remote", REPOS[dist], f"refs/heads/{STABLE}", "refs/tags/v*"], capture_output=True,
|
|
113
|
+
text=True, timeout=30, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"})
|
|
114
|
+
if got.returncode != 0:
|
|
115
|
+
return None
|
|
116
|
+
head, direct, peeled = "", {}, {}
|
|
117
|
+
for line in str(got.stdout or "").splitlines():
|
|
118
|
+
sha, _, ref = line.strip().partition("\t")
|
|
119
|
+
if ref == f"refs/heads/{STABLE}":
|
|
120
|
+
head = sha
|
|
121
|
+
elif ref.startswith("refs/tags/v"):
|
|
122
|
+
name = ref[len("refs/tags/v"):]
|
|
123
|
+
if name.endswith("^{}"):
|
|
124
|
+
peeled[name[:-3]] = sha # the commit an annotated tag names
|
|
125
|
+
else:
|
|
126
|
+
direct[name] = sha
|
|
127
|
+
on_head = [v for v, sha in direct.items() if peeled.get(v, sha) == head and _ver(v)]
|
|
128
|
+
return max(on_head, key=_ver) if head and on_head else None
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def on_stable(tool: str, prefix: str, specs) -> bool:
|
|
134
|
+
"""What pipx / uv will install again for the environment at ``prefix`` (``pipx reinstall``, ``pipx upgrade``,
|
|
135
|
+
``uv tool upgrade``) is ``specs``, each by its repository's stable branch. pipx: each spec exactly as given (an
|
|
136
|
+
imperal-mcp recorded beside webbee WITH its code extra pulled webbee from PyPI on a reinstall); uv resolves the
|
|
137
|
+
record as one set, so its repository and branch suffice. Plain pip keeps no such record."""
|
|
138
|
+
try:
|
|
139
|
+
if tool == "pipx":
|
|
140
|
+
import json
|
|
141
|
+
with open(os.path.join(prefix, "pipx_metadata.json"), encoding="utf-8") as f:
|
|
142
|
+
meta = json.load(f)
|
|
143
|
+
recorded = [str((meta.get("main_package") or {}).get("package_or_url") or "")]
|
|
144
|
+
recorded += [str((p or {}).get("package_or_url") or "") for p in (meta.get("injected_packages") or {}).values()]
|
|
145
|
+
return all(s in recorded for s in specs)
|
|
146
|
+
if tool == "uv":
|
|
147
|
+
with open(os.path.join(prefix, "uv-receipt.toml"), encoding="utf-8") as f:
|
|
148
|
+
receipt = f.read()
|
|
149
|
+
urls = [s.split(" @ git+", 1)[-1].rsplit("@", 1)[0] for s in specs]
|
|
150
|
+
return all(f'git = "{u}?rev={STABLE}"' in receipt for u in urls)
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _git_install(tool: str, main: str, withs, uv, pipx, python, venv: str, record: bool):
|
|
157
|
+
"""``main`` and ``withs`` (the packages beside it) from git, by ``tool``, recorded as given. pipx: an environment
|
|
158
|
+
already recorded on the branch is upgraded by its own record (``upgrade --include-injected``, in place); otherwise
|
|
159
|
+
(``record``: a new one, or one recorded by a version) installed again and its packages injected -- the only way
|
|
160
|
+
pipx writes a new record. uv records ``--with`` beside the tool on every install."""
|
|
161
|
+
if tool == "uv" and uv:
|
|
162
|
+
cmd = [uv, "tool", "install", "--force", "--refresh"]
|
|
163
|
+
for w in withs:
|
|
164
|
+
cmd += ["--with", w]
|
|
165
|
+
return cmd + [main]
|
|
166
|
+
if tool == "pipx" and pipx:
|
|
167
|
+
if not record:
|
|
168
|
+
return [pipx, "upgrade", "--include-injected", venv]
|
|
169
|
+
return [[pipx, "install", "--force", main], [pipx, "inject", "--force", venv, *withs]]
|
|
170
|
+
if tool == "pip" and python:
|
|
171
|
+
return [python, "-m", "pip", "install", "--upgrade", "--no-cache-dir", main, *withs]
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
77
175
|
def _spec(name: str, dist: str, version: str, wheel) -> str:
|
|
78
176
|
"""``name`` (with its extras) at ``version``: by its wheel's address when known, else by the index."""
|
|
79
177
|
url = wheel(dist, version) if wheel else None
|
|
@@ -106,10 +204,15 @@ def find_uv(which=safe_which):
|
|
|
106
204
|
|
|
107
205
|
|
|
108
206
|
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
|
-
|
|
207
|
+
uv=None, pipx=None, wheel=None, source: str = "pypi", record: bool = True, ref: str = STABLE):
|
|
208
|
+
"""The command that installs webbee ``version`` where this webbee is installed -- or None. ``source`` "git": from
|
|
209
|
+
the repositories at ``ref`` (the stable branch; a tag only to put a release back), imperal-mcp and imperal-sdk
|
|
210
|
+
beside it; ``record``: pipx writes its record anew (see ``_git_install``)."""
|
|
112
211
|
light = env.get(LIGHT_ENV)
|
|
212
|
+
if source == "git" and not light:
|
|
213
|
+
return _git_install(tool_of(prefix), git_spec(DIST, ref), [MCP_BESIDE_WEBBEE, git_spec(SDK)], uv, pipx,
|
|
214
|
+
executable, DIST, record)
|
|
215
|
+
spec = _spec(DIST, DIST, version, wheel)
|
|
113
216
|
if light: # an old Python runs this release through its light install
|
|
114
217
|
return [light, "-m", "pip", "install", "--upgrade", "--no-cache-dir", spec]
|
|
115
218
|
return _install(tool_of(prefix), spec, uv, pipx, executable, venv=DIST)
|
|
@@ -132,13 +235,15 @@ def _install(tool: str, spec: str, uv, pipx, python, venv: str = ""):
|
|
|
132
235
|
|
|
133
236
|
|
|
134
237
|
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
|
|
238
|
+
"""The shell's imperal-mcp (the command on PATH): ``{"tool", "python", "prefix", "version", "code"}`` -- {} when
|
|
239
|
+
none."""
|
|
136
240
|
exe = which(MCP)
|
|
137
241
|
if not exe:
|
|
138
242
|
return {}
|
|
139
243
|
real = os.path.realpath(exe)
|
|
140
244
|
py = os.path.join(os.path.dirname(real), "python.exe" if _WINDOWS else "python")
|
|
141
|
-
out = {"tool": tool_of(real), "python": py, "
|
|
245
|
+
out = {"tool": tool_of(real), "python": py, "prefix": os.path.dirname(os.path.dirname(real)), "version": "",
|
|
246
|
+
"code": False}
|
|
142
247
|
# asked twice when it says "no code extra": a reinstall is never made on one odd answer (live 2026-10-25: right after
|
|
143
248
|
# another install the first answer said no webbee, the next ones yes)
|
|
144
249
|
for _ in range(2):
|
|
@@ -155,18 +260,27 @@ def mcp_installed(which=safe_which, run=subprocess.run) -> dict:
|
|
|
155
260
|
return out
|
|
156
261
|
|
|
157
262
|
|
|
158
|
-
def mcp_command(inst: dict, newest, uv, pipx, wheel=None):
|
|
263
|
+
def mcp_command(inst: dict, newest, uv, pipx, wheel=None, source: str = "pypi"):
|
|
159
264
|
"""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
|
-
|
|
265
|
+
it (uv, else pipx, for a new install) -- or None when it already is, or nothing can do it. ``source`` "git": from
|
|
266
|
+
the stable branches, imperal-sdk and webbee beside it, and an install recorded by a version is moved onto the
|
|
267
|
+
branch even when its version is the newest (what ``pipx reinstall`` / ``uv tool upgrade`` would put back)."""
|
|
268
|
+
git = source == "git"
|
|
269
|
+
recorded = bool(inst) and on_stable(inst.get("tool") or "", inst.get("prefix") or "", MCP_ENV_SPECS)
|
|
270
|
+
if inst and inst.get("version") and inst.get("version") == newest and inst.get("code") and (recorded or not git):
|
|
162
271
|
return None
|
|
163
272
|
if inst and not inst.get("version"):
|
|
164
273
|
return None # it did not answer: a working install is never replaced blind
|
|
165
274
|
if inst and not newest and inst.get("code"):
|
|
166
275
|
return None # PyPI unread: what works stays
|
|
167
|
-
|
|
276
|
+
if git and inst and _ver(inst.get("version")) > _ver(newest):
|
|
277
|
+
return None # newer than the last release (a build of its own): never taken back
|
|
168
278
|
tool = inst.get("tool") if inst else ("uv" if uv else "pipx" if pipx else "")
|
|
169
|
-
|
|
279
|
+
if git:
|
|
280
|
+
return _git_install(tool or "", MCP_GIT_SPEC, [git_spec(SDK), git_spec(DIST)], uv, pipx,
|
|
281
|
+
inst.get("python") if inst else None, MCP, record=not recorded)
|
|
282
|
+
spec = _spec(MCP_SPEC, MCP, newest, wheel) if newest else (MCP_SPEC if inst else MCP_GIT_SPEC)
|
|
283
|
+
return _install(tool or "", spec, uv, pipx, inst.get("python") if inst else None, venv=MCP if inst else "")
|
|
170
284
|
|
|
171
285
|
|
|
172
286
|
def restart_command(argv, env=os.environ, executable: str = sys.executable):
|
|
@@ -285,23 +399,46 @@ def imports_ok(python: str, run=subprocess.run, light: bool = False) -> bool:
|
|
|
285
399
|
return False
|
|
286
400
|
|
|
287
401
|
|
|
402
|
+
def _steps(cmd) -> list:
|
|
403
|
+
"""One command, or the commands of one install in their order (pipx: install, then inject)."""
|
|
404
|
+
return [list(c) for c in cmd] if cmd and isinstance(cmd[0], (list, tuple)) else [list(cmd or [])]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _show(cmd) -> str:
|
|
408
|
+
"""What a person runs by hand to do ``cmd``."""
|
|
409
|
+
return " && ".join(" ".join(f"'{a}'" if " " in a or "[" in a else a for a in c) for c in _steps(cmd))
|
|
410
|
+
|
|
411
|
+
|
|
288
412
|
def _run(cmd, run=subprocess.run) -> bool:
|
|
413
|
+
"""Every step of ``cmd`` in its order; the first that fails stops the rest."""
|
|
289
414
|
try:
|
|
290
|
-
|
|
291
|
-
|
|
415
|
+
for step in _steps(cmd):
|
|
416
|
+
if run(step, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
|
|
417
|
+
timeout=STEP_TIMEOUT_S).returncode != 0:
|
|
418
|
+
return False
|
|
419
|
+
return True
|
|
292
420
|
except Exception:
|
|
293
421
|
return False
|
|
294
422
|
|
|
295
423
|
|
|
296
|
-
def
|
|
424
|
+
def _newest(dist: str, git, fetch, stable) -> tuple:
|
|
425
|
+
"""``(version, source)``: the release on ``dist``'s stable branch ("git") when git reads it, else PyPI's."""
|
|
426
|
+
v = stable(dist, git) if git else None
|
|
427
|
+
return (v, "git") if v else (fetch(dist), "pypi")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _roll_back(current: str, newest: str, python: str, light: str, env, uv, pipx, wheel, run, say,
|
|
431
|
+
source: str = "pypi", prefix: str = sys.prefix) -> bool:
|
|
297
432
|
"""``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
|
-
|
|
433
|
+
way -- never a release on disk that does not start. Says what happened either way. From git: ``current``'s own
|
|
434
|
+
tag (recorded so until the next release moves it onto the branch again)."""
|
|
435
|
+
back = self_command(current, prefix=prefix, env=env, uv=uv, pipx=pipx, wheel=wheel, source=source,
|
|
436
|
+
ref=f"v{current}")
|
|
300
437
|
if back and _run(back, run) and imports_ok(python, run, light=bool(light)):
|
|
301
438
|
say(f"🐝 webbee {newest} was installed but does not start (its modules do not import) -- "
|
|
302
439
|
f"put {current} back; starting {current}")
|
|
303
440
|
return True
|
|
304
|
-
fix =
|
|
441
|
+
fix = _show(back) if back else f"pip install webbee=={current}"
|
|
305
442
|
say(f"🐝 webbee {newest} was installed but does not start, and {current} could not be put back -- "
|
|
306
443
|
f"this session runs on, but the next start may fail: run {fix}")
|
|
307
444
|
return False
|
|
@@ -309,7 +446,7 @@ def _roll_back(current: str, newest: str, python: str, light: str, env, uv, pipx
|
|
|
309
446
|
|
|
310
447
|
def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe_which, run=subprocess.run,
|
|
311
448
|
execv=os.execv, env=os.environ, wheel=wheel_url, lock_path=_lock_path,
|
|
312
|
-
sleep=time.sleep) -> list:
|
|
449
|
+
sleep=time.sleep, stable=stable_version, prefix: str = sys.prefix) -> list:
|
|
313
450
|
"""Bring webbee and the shell's imperal-mcp to their newest releases, then let the start go on; a self-upgrade
|
|
314
451
|
starts webbee again on the new release (never returns then). Returns what it did, for the log. Never raises."""
|
|
315
452
|
did: list = []
|
|
@@ -330,7 +467,7 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
|
|
|
330
467
|
return did
|
|
331
468
|
if state == "free":
|
|
332
469
|
lock = ""
|
|
333
|
-
newest =
|
|
470
|
+
newest = _newest(DIST, None if env.get(LIGHT_ENV) else which("git"), fetch, stable)[0]
|
|
334
471
|
if newest and _ver(newest) > _ver(current) and not env.get(DONE_ENV):
|
|
335
472
|
env[DONE_ENV] = "1"
|
|
336
473
|
again = restart_command(list(argv), env=env)
|
|
@@ -339,12 +476,19 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
|
|
|
339
476
|
execv(again[0], again)
|
|
340
477
|
return did
|
|
341
478
|
uv, pipx = find_uv(which), which("pipx")
|
|
342
|
-
|
|
343
|
-
if
|
|
344
|
-
|
|
479
|
+
light = env.get(LIGHT_ENV) or ""
|
|
480
|
+
git = None if light else which("git") # 3.7's pip builds no package from source: the light install stays on PyPI
|
|
481
|
+
newest, source = _newest(DIST, git, fetch, stable)
|
|
482
|
+
tool = tool_of(prefix)
|
|
483
|
+
recorded = source != "git" or on_stable(tool, prefix, WEBBEE_ENV_SPECS)
|
|
484
|
+
# the same release recorded by a version: moved onto the branch (what pipx/uv would put back on a reinstall)
|
|
485
|
+
if newest and (_ver(newest) > _ver(current) or (not recorded and _ver(newest) == _ver(current))) \
|
|
486
|
+
and not env.get(DONE_ENV):
|
|
487
|
+
cmd = self_command(newest, prefix=prefix, env=env, uv=uv, pipx=pipx, wheel=wheel, source=source,
|
|
488
|
+
record=not recorded)
|
|
345
489
|
if cmd:
|
|
346
|
-
say(f"🐝 updating webbee {current} -> {newest} ...")
|
|
347
|
-
|
|
490
|
+
say(f"🐝 updating webbee {current} -> {newest} ..." if _ver(newest) > _ver(current) else
|
|
491
|
+
f"🐝 moving webbee {current} onto its release branch, so a reinstall brings the newest ...")
|
|
348
492
|
python = light or sys.executable
|
|
349
493
|
installed = _run(cmd, run)
|
|
350
494
|
ok = installed and (imports_ok(python, run, light=bool(light)) or
|
|
@@ -353,7 +497,7 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
|
|
|
353
497
|
# on disk a release that does not import (or an install that broke off halfway): the next start
|
|
354
498
|
# would run it and stay stuck. The one running now (and any other open session) holds the old
|
|
355
499
|
# modules: put the old release back
|
|
356
|
-
_roll_back(current, newest, python, light, env, uv, pipx, wheel, run, say)
|
|
500
|
+
_roll_back(current, newest, python, light, env, uv, pipx, wheel, run, say, source, prefix)
|
|
357
501
|
elif ok:
|
|
358
502
|
did.append(f"webbee {newest}")
|
|
359
503
|
env[DONE_ENV] = "1" # the restart inherits it: never a second self-upgrade
|
|
@@ -365,18 +509,18 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
|
|
|
365
509
|
execv(again[0], again)
|
|
366
510
|
return did
|
|
367
511
|
else:
|
|
368
|
-
say(f"🐝 webbee could not update itself -- run: {
|
|
369
|
-
mcp_newest =
|
|
512
|
+
say(f"🐝 webbee could not update itself -- run: {_show(cmd)}")
|
|
513
|
+
mcp_newest, mcp_source = _newest(MCP, git, fetch, stable)
|
|
370
514
|
inst = mcp_installed(which, run)
|
|
371
|
-
cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel)
|
|
515
|
+
cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel, source=mcp_source)
|
|
372
516
|
if cmd:
|
|
373
517
|
what = (f"imperal-mcp {inst.get('version') or '?'} -> {mcp_newest or 'source'}" if inst
|
|
374
|
-
else f"imperal-mcp {mcp_newest or 'from source'}")
|
|
518
|
+
else f"imperal-mcp {mcp_newest or 'from source'}") + (" (git, stable)" if mcp_source == "git" else "")
|
|
375
519
|
say(f"🐝 updating {what} ...")
|
|
376
520
|
if _run(cmd, run):
|
|
377
521
|
did.append(what)
|
|
378
522
|
else:
|
|
379
|
-
say(f"🐝 imperal-mcp could not be updated -- run: {
|
|
523
|
+
say(f"🐝 imperal-mcp could not be updated -- run: {_show(cmd)}")
|
|
380
524
|
except SystemExit:
|
|
381
525
|
raise
|
|
382
526
|
except Exception:
|
|
@@ -299,3 +299,173 @@ 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
|
+
MCP_W = au.MCP_BESIDE_WEBBEE # beside webbee: no code extra (it would pull webbee from PyPI)
|
|
308
|
+
H_OLD, H_NEW, H_TAG = "a" * 40, "b" * 40, "c" * 40
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _ls_remote(stable_sha):
|
|
312
|
+
lines = [f"{H_OLD}\trefs/tags/v0.7.4", # a lightweight tag
|
|
313
|
+
f"{H_TAG}\trefs/tags/v0.7.5", f"{H_NEW}\trefs/tags/v0.7.5^{{}}", # an annotated one: its commit
|
|
314
|
+
f"{stable_sha}\trefs/heads/stable"]
|
|
315
|
+
return lambda cmd, **kw: subprocess.CompletedProcess(cmd, 0, stdout="\n".join(lines) + "\n", stderr="")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_the_newest_release_is_the_tag_the_stable_branch_stands_on():
|
|
319
|
+
assert au.stable_version("webbee", "/bin/git", run=_ls_remote(H_NEW)) == "0.7.5"
|
|
320
|
+
assert au.stable_version("webbee", "/bin/git", run=_ls_remote(H_OLD)) == "0.7.4"
|
|
321
|
+
assert au.stable_version("webbee", "/bin/git", run=_ls_remote("d" * 40)) is None # between tags: not a release
|
|
322
|
+
assert au.stable_version("webbee", None) is None # no git
|
|
323
|
+
failed = lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, stdout="", stderr="") # noqa: E731
|
|
324
|
+
assert au.stable_version("webbee", "/bin/git", run=failed) is None # GitHub out of reach
|
|
325
|
+
seen = []
|
|
326
|
+
au.stable_version("imperal-sdk", "/bin/git", run=lambda cmd, **kw: seen.append((cmd, kw)) or 1 / 0)
|
|
327
|
+
assert seen[0][0][:3] == ["/bin/git", "ls-remote", au.REPOS["imperal-sdk"]]
|
|
328
|
+
assert seen[0][1]["env"]["GIT_TERMINAL_PROMPT"] == "0" # never waits on a prompt
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_the_spec_is_the_branch_never_a_version():
|
|
332
|
+
assert WB == "webbee @ git+https://github.com/imperalcloud/webbee-code.git@stable"
|
|
333
|
+
assert MCP_G == "imperal-mcp[code] @ git+https://github.com/imperalcloud/imperal-mcp.git@stable"
|
|
334
|
+
assert SDK_G == "imperal-sdk[universal-index] @ git+https://github.com/imperalcloud/imperal-sdk.git@stable"
|
|
335
|
+
assert au.MCP_GIT_SPEC == MCP_G
|
|
336
|
+
assert MCP_W == "imperal-mcp @ git+https://github.com/imperalcloud/imperal-mcp.git@stable"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _pipx_venv(tmp_path, main, injected=()):
|
|
340
|
+
import json
|
|
341
|
+
venv = tmp_path / "pipx" / "venvs" / "webbee"
|
|
342
|
+
venv.mkdir(parents=True)
|
|
343
|
+
(venv / "pipx_metadata.json").write_text(json.dumps({
|
|
344
|
+
"main_package": {"package_or_url": main},
|
|
345
|
+
"injected_packages": {s.split()[0]: {"package_or_url": s} for s in injected}}))
|
|
346
|
+
return str(venv)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_what_pipx_and_uv_recorded_is_read(tmp_path):
|
|
350
|
+
pinned = _pipx_venv(tmp_path / "a", "webbee @ https://files.pythonhosted.org/x/webbee-0.6.7-py3-none-any.whl")
|
|
351
|
+
assert not au.on_stable("pipx", pinned, (WB,))
|
|
352
|
+
good = _pipx_venv(tmp_path / "b", WB, (MCP_W, SDK_G))
|
|
353
|
+
assert au.on_stable("pipx", good, au.WEBBEE_ENV_SPECS)
|
|
354
|
+
coded = _pipx_venv(tmp_path / "d", WB, (MCP_G, SDK_G)) # mcp WITH its code extra beside webbee
|
|
355
|
+
assert not au.on_stable("pipx", coded, au.WEBBEE_ENV_SPECS)
|
|
356
|
+
half = _pipx_venv(tmp_path / "c", WB) # its packages still from PyPI
|
|
357
|
+
assert not au.on_stable("pipx", half, au.WEBBEE_ENV_SPECS)
|
|
358
|
+
uv = tmp_path / "uv" / "tools" / "imperal-mcp"
|
|
359
|
+
uv.mkdir(parents=True)
|
|
360
|
+
(uv / "uv-receipt.toml").write_text('[tool]\nrequirements = [{ name = "imperal-mcp", extras = ["code"], '
|
|
361
|
+
'specifier = "==0.9.5" }]\n')
|
|
362
|
+
assert not au.on_stable("uv", str(uv), (MCP_G,))
|
|
363
|
+
(uv / "uv-receipt.toml").write_text(
|
|
364
|
+
'[tool]\nrequirements = [\n'
|
|
365
|
+
' { name = "imperal-mcp", extras = ["code"], git = "https://github.com/imperalcloud/imperal-mcp.git?rev=stable" },\n'
|
|
366
|
+
' { name = "imperal-sdk", extras = ["universal-index"], git = "https://github.com/imperalcloud/imperal-sdk.git?rev=stable" },\n'
|
|
367
|
+
' { name = "webbee", git = "https://github.com/imperalcloud/webbee-code.git?rev=stable" },\n]\n')
|
|
368
|
+
assert au.on_stable("uv", str(uv), au.MCP_ENV_SPECS)
|
|
369
|
+
assert not au.on_stable("pipx", str(tmp_path / "missing"), (WB,)) # unreadable: moved, never assumed
|
|
370
|
+
assert au.on_stable("pip", "/venv", (WB,)) # plain pip keeps no record
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_from_git_each_tool_installs_and_records_the_branch():
|
|
374
|
+
uv = au.self_command("0.7.5", prefix="/u/.local/share/uv/tools/webbee", env={}, uv="/bin/uv", source="git")
|
|
375
|
+
assert uv == ["/bin/uv", "tool", "install", "--force", "--refresh", "--with", MCP_W, "--with", SDK_G, WB]
|
|
376
|
+
px = au.self_command("0.7.5", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx", source="git")
|
|
377
|
+
assert px == [["/bin/pipx", "install", "--force", WB], ["/bin/pipx", "inject", "--force", "webbee", MCP_W, SDK_G]]
|
|
378
|
+
kept = au.self_command("0.7.5", prefix="/h/.local/pipx/venvs/webbee", env={}, pipx="/bin/pipx", source="git",
|
|
379
|
+
record=False) # already on the branch: its own record
|
|
380
|
+
assert kept == ["/bin/pipx", "upgrade", "--include-injected", "webbee"]
|
|
381
|
+
pip = au.self_command("0.7.5", prefix="/v", executable="/v/bin/python", env={}, source="git")
|
|
382
|
+
assert pip == ["/v/bin/python", "-m", "pip", "install", "--upgrade", "--no-cache-dir", WB, MCP_W, SDK_G]
|
|
383
|
+
light = au.self_command("0.7.5", env={au.LIGHT_ENV: "/usr/bin/python3.7"}, source="git", wheel=None)
|
|
384
|
+
assert light[-1] == "webbee==0.7.5" # 3.7 builds nothing from source: PyPI
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_the_same_release_recorded_by_a_version_is_moved_onto_the_branch(tmp_path):
|
|
388
|
+
venv = _pipx_venv(tmp_path, "webbee @ https://files.pythonhosted.org/x/webbee-0.7.5-py3-none-any.whl")
|
|
389
|
+
run, execd, env, said = _Run(), [], {}, []
|
|
390
|
+
stable = {"webbee": "0.7.5", "imperal-mcp": "0.9.5"}.get
|
|
391
|
+
au.upgrade_before_start(["--once"], "0.7.5", say=said.append, fetch=lambda d: 1 / 0, wheel=None,
|
|
392
|
+
which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env=env,
|
|
393
|
+
execv=lambda p, a: execd.append(a), lock_path=lambda: str(tmp_path / "upgrade.lock"),
|
|
394
|
+
stable=lambda d, git: stable(d), prefix=venv)
|
|
395
|
+
assert ["/bin/pipx", "install", "--force", WB] in run.calls
|
|
396
|
+
assert ["/bin/pipx", "inject", "--force", "webbee", MCP_W, SDK_G] in run.calls
|
|
397
|
+
assert execd and any("onto its release branch" in s for s in said)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_on_the_branch_and_newest_nothing_is_installed(tmp_path, monkeypatch):
|
|
401
|
+
venv = _pipx_venv(tmp_path, WB, (MCP_W, SDK_G))
|
|
402
|
+
exe = tmp_path / "pipx" / "venvs" / "imperal-mcp" / "bin" / "imperal-mcp"
|
|
403
|
+
exe.parent.mkdir(parents=True)
|
|
404
|
+
exe.write_text("")
|
|
405
|
+
_pipx_venv(tmp_path / "m", MCP_G, (SDK_G, WB))
|
|
406
|
+
import shutil
|
|
407
|
+
shutil.copy(tmp_path / "m" / "pipx" / "venvs" / "webbee" / "pipx_metadata.json", exe.parent.parent)
|
|
408
|
+
monkeypatch.setattr(au, "find_uv", lambda which=None: None)
|
|
409
|
+
run = _Run(stdout="0.9.5 True")
|
|
410
|
+
au.upgrade_before_start([], "0.7.5", say=lambda m: None, fetch=lambda d: 1 / 0, wheel=None,
|
|
411
|
+
which=_which({"pipx": "/bin/pipx", "git": "/bin/git", "imperal-mcp": str(exe)}), run=run,
|
|
412
|
+
env={}, execv=lambda *a: 1 / 0, lock_path=lambda: str(tmp_path / "upgrade.lock"),
|
|
413
|
+
stable=lambda d, git: {"webbee": "0.7.5", "imperal-mcp": "0.9.5"}[d], prefix=venv)
|
|
414
|
+
assert not any("install" in c or "upgrade" in c or "inject" in c for c in run.calls)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_a_newer_release_on_the_branch_is_taken_by_the_install_s_own_record(tmp_path):
|
|
418
|
+
venv = _pipx_venv(tmp_path, WB, (MCP_W, SDK_G))
|
|
419
|
+
run, execd = _Run(), []
|
|
420
|
+
au.upgrade_before_start([], "0.7.4", say=lambda m: None, fetch=lambda d: 1 / 0, wheel=None,
|
|
421
|
+
which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
|
|
422
|
+
execv=lambda p, a: execd.append(a), lock_path=lambda: str(tmp_path / "upgrade.lock"),
|
|
423
|
+
stable=lambda d, git: "0.7.5", prefix=venv)
|
|
424
|
+
assert run.calls[0] == ["/bin/pipx", "upgrade", "--include-injected", "webbee"] and execd
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_no_git_or_github_out_of_reach_falls_back_to_pypi(tmp_path):
|
|
428
|
+
run = _Run()
|
|
429
|
+
au.upgrade_before_start([], "0.7.4", say=lambda m: None, fetch={"webbee": "0.7.5"}.get, wheel=None,
|
|
430
|
+
which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
|
|
431
|
+
execv=lambda p, a: None, lock_path=lambda: str(tmp_path / "upgrade.lock"),
|
|
432
|
+
stable=lambda d, git: None, prefix="/h/.local/pipx/venvs/webbee")
|
|
433
|
+
assert run.calls[0][-1] == "webbee==0.7.5"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_the_shell_s_imperal_mcp_recorded_by_a_version_is_moved_onto_the_branch(tmp_path):
|
|
437
|
+
uv = tmp_path / "uv" / "tools" / "imperal-mcp"
|
|
438
|
+
uv.mkdir(parents=True)
|
|
439
|
+
(uv / "uv-receipt.toml").write_text('[tool]\nrequirements = [{ name = "imperal-mcp", extras = ["code"], '
|
|
440
|
+
'specifier = "==0.9.5" }]\n')
|
|
441
|
+
inst = {"tool": "uv", "python": str(uv / "bin" / "python"), "prefix": str(uv), "version": "0.9.5", "code": True}
|
|
442
|
+
assert au.mcp_command(inst, "0.9.5", "/bin/uv", None) is None # PyPI: nothing to do
|
|
443
|
+
assert au.mcp_command(inst, "0.9.5", "/bin/uv", None, source="git") == [
|
|
444
|
+
"/bin/uv", "tool", "install", "--force", "--refresh", "--with", SDK_G, "--with", WB, MCP_G]
|
|
445
|
+
assert au.mcp_command(dict(inst, version="0.9.6"), "0.9.5", "/bin/uv", None, source="git") is None # own build
|
|
446
|
+
assert au.mcp_command({}, "0.9.5", None, "/bin/pipx", source="git") == [
|
|
447
|
+
["/bin/pipx", "install", "--force", MCP_G], ["/bin/pipx", "inject", "--force", "imperal-mcp", SDK_G, WB]]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_the_steps_of_one_install_stop_at_the_first_that_fails():
|
|
451
|
+
calls = []
|
|
452
|
+
|
|
453
|
+
def run(cmd, **kw):
|
|
454
|
+
calls.append(cmd)
|
|
455
|
+
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
|
|
456
|
+
assert au._run([["/bin/pipx", "install", "x"], ["/bin/pipx", "inject", "y"]], run) is False
|
|
457
|
+
assert len(calls) == 1
|
|
458
|
+
shown = au._show([["/bin/pipx", "install", "--force", WB], ["/bin/pipx", "inject", "--force", "webbee", MCP_G]])
|
|
459
|
+
assert shown == f"/bin/pipx install --force '{WB}' && /bin/pipx inject --force webbee '{MCP_G}'"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_a_git_release_that_does_not_import_is_put_back_by_its_own_tag(tmp_path, monkeypatch):
|
|
463
|
+
_checks(monkeypatch, [False, False, True])
|
|
464
|
+
venv = _pipx_venv(tmp_path, WB, (MCP_W, SDK_G))
|
|
465
|
+
run, said = _Run(), []
|
|
466
|
+
au.upgrade_before_start([], "0.7.4", say=said.append, fetch=lambda d: 1 / 0, wheel=None,
|
|
467
|
+
which=_which({"pipx": "/bin/pipx", "git": "/bin/git"}), run=run, env={},
|
|
468
|
+
execv=lambda *a: 1 / 0, lock_path=lambda: str(tmp_path / "upgrade.lock"),
|
|
469
|
+
stable=lambda d, git: "0.7.5", prefix=venv)
|
|
470
|
+
assert ["/bin/pipx", "install", "--force", au.git_spec("webbee", "v0.7.4")] in run.calls
|
|
471
|
+
assert any("put 0.7.4 back" in s for s in said)
|