evo-cli 0.31.1__tar.gz → 0.31.2__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.
- {evo_cli-0.31.1 → evo_cli-0.31.2}/PKG-INFO +1 -1
- evo_cli-0.31.2/evo_cli/VERSION +1 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/update.py +162 -7
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_update.py +127 -2
- evo_cli-0.31.1/evo_cli/VERSION +0 -1
- {evo_cli-0.31.1 → evo_cli-0.31.2}/Containerfile +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/HISTORY.md +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/LICENSE +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/MANIFEST.in +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/README.md +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/__main__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/base.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/cli.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_hub.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_mirror.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_render.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/views.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/image.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/ngrok.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/openvpn.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/_dns.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/_dpi.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/_hosts.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/_probe.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/check.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/fix.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/probe.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/route/status.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/storage.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/console.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/align.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/core.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/creds.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/errors.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/gemini.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/imaging/ncnn.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/openvpn.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/gemini.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/SOURCES.txt +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/pyproject.toml +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/setup.cfg +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/__init__.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/fake_evo_agents.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/fixtures/hub/contract-0.2.0.json +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/fixtures/hub/mirror-demo.yaml +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_claude_code.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_cli.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_console.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_cred.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_download.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_gdrive.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_gh.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_harness.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_harness_hub.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_harness_show.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_image.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_mcp.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_ngrok.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_opencode.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_openvpn.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_plantuml.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_route.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_serp.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_storage.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_sysmon.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_tts.py +0 -0
- {evo_cli-0.31.1 → evo_cli-0.31.2}/tests/test_wifi.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.2
|
|
@@ -18,11 +18,16 @@ from evo_cli import __version__
|
|
|
18
18
|
from evo_cli.console import CommandError, console, info, run_command, step, success, warning
|
|
19
19
|
|
|
20
20
|
PACKAGE = "evo_cli"
|
|
21
|
+
TOOL_NAME = "evo-cli"
|
|
21
22
|
PYPI_JSON = "https://pypi.org/pypi/{package}/json"
|
|
22
23
|
|
|
23
24
|
VERSION_RE = re.compile(r"^v?(\d+(?:\.\d+)*)(?:[-_.]?(dev|alpha|a|beta|b|rc|c|pre)[-_.]?(\d*))?")
|
|
24
25
|
PRE_RANK = {"dev": -4, "alpha": -3, "a": -3, "beta": -2, "b": -2, "pre": -1, "rc": -1, "c": -1}
|
|
25
26
|
|
|
27
|
+
EXACT_PIN_RE = re.compile(r"^===?\s*([^\s,*]+)$")
|
|
28
|
+
PLAIN_RECEIPT_KEYS = {"requirements", "python", "entrypoints", "options"}
|
|
29
|
+
PLAIN_REQUIREMENT_KEYS = {"name", "extras", "specifier"}
|
|
30
|
+
|
|
26
31
|
EPILOG = Text.from_markup(
|
|
27
32
|
"[bold]Examples[/bold]\n\n"
|
|
28
33
|
" [cyan]evo update[/cyan] update to the newest release\n"
|
|
@@ -31,7 +36,9 @@ EPILOG = Text.from_markup(
|
|
|
31
36
|
" [cyan]evo update --dry-run[/cyan] print the commands without running them\n"
|
|
32
37
|
" [cyan]evo update --force[/cyan] reinstall even when already current\n\n"
|
|
33
38
|
"[dim]An editable checkout is updated with git (fast-forward only, refusing to touch a\n"
|
|
34
|
-
"dirty tree); a pipx, uv or pip install is upgraded with its own installer.
|
|
39
|
+
"dirty tree); a pipx, uv or pip install is upgraded with its own installer. A uv tool\n"
|
|
40
|
+
"pinned to an older ==version is reinstalled without the pin. The update succeeds only\n"
|
|
41
|
+
"when the installed version reaches the newest release afterwards.[/dim]"
|
|
35
42
|
)
|
|
36
43
|
|
|
37
44
|
|
|
@@ -138,7 +145,79 @@ def report(install, current, latest):
|
|
|
138
145
|
console.print()
|
|
139
146
|
|
|
140
147
|
|
|
141
|
-
def
|
|
148
|
+
def load_toml(path):
|
|
149
|
+
try:
|
|
150
|
+
import tomllib
|
|
151
|
+
except ImportError: # Python 3.10
|
|
152
|
+
try:
|
|
153
|
+
import tomli as tomllib
|
|
154
|
+
except ImportError:
|
|
155
|
+
return {}
|
|
156
|
+
try:
|
|
157
|
+
with open(path, "rb") as handle:
|
|
158
|
+
return tomllib.load(handle)
|
|
159
|
+
except (OSError, ValueError):
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def normalize_name(name):
|
|
164
|
+
return re.sub(r"[-_.]+", "-", str(name or "")).lower()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def uv_receipt(location):
|
|
168
|
+
"""The `[tool]` table of a uv tool receipt and its evo-cli requirement (None when absent)."""
|
|
169
|
+
tool = load_toml(Path(location) / "uv-receipt.toml").get("tool") or {}
|
|
170
|
+
for requirement in tool.get("requirements") or []:
|
|
171
|
+
if isinstance(requirement, dict) and normalize_name(requirement.get("name")) == TOOL_NAME:
|
|
172
|
+
return tool, requirement
|
|
173
|
+
return tool, None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def exact_pin(requirement):
|
|
177
|
+
match = EXACT_PIN_RE.match(str((requirement or {}).get("specifier") or "").strip())
|
|
178
|
+
return match.group(1) if match else None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def requirement_text(requirement, specifier):
|
|
182
|
+
extras = requirement.get("extras") or []
|
|
183
|
+
name = requirement["name"] + (f"[{','.join(extras)}]" if extras else "")
|
|
184
|
+
return f"{name}{specifier}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def uv_reinstall_command(location, latest, allow_pre):
|
|
188
|
+
"""Reinstall a uv tool whose receipt pins an older `==` version, since `uv tool upgrade` keeps the pin.
|
|
189
|
+
|
|
190
|
+
Only a plain receipt is rebuilt: one evo-cli requirement from the index plus an optional Python
|
|
191
|
+
request. Anything more (`--with`, a git source, index options) goes through `uv tool upgrade`,
|
|
192
|
+
and the version check afterwards says what to run.
|
|
193
|
+
"""
|
|
194
|
+
tool, requirement = uv_receipt(location)
|
|
195
|
+
pinned = exact_pin(requirement)
|
|
196
|
+
if latest is None or pinned is None or parse_version(pinned) >= parse_version(latest):
|
|
197
|
+
return None
|
|
198
|
+
if set(tool) - PLAIN_RECEIPT_KEYS or any((tool.get("options") or {}).values()):
|
|
199
|
+
return None
|
|
200
|
+
if len(tool.get("requirements") or []) != 1 or set(requirement) - PLAIN_REQUIREMENT_KEYS:
|
|
201
|
+
return None
|
|
202
|
+
info(f"The uv receipt pins [accent]{TOOL_NAME}=={pinned}[/accent], so the tool is reinstalled without the pin.")
|
|
203
|
+
cmd = [
|
|
204
|
+
"uv",
|
|
205
|
+
"tool",
|
|
206
|
+
"install",
|
|
207
|
+
"--force",
|
|
208
|
+
"--refresh-package",
|
|
209
|
+
requirement["name"],
|
|
210
|
+
requirement_text(requirement, f">={latest}"),
|
|
211
|
+
]
|
|
212
|
+
if tool.get("python"):
|
|
213
|
+
cmd += ["--python", str(tool["python"])]
|
|
214
|
+
if allow_pre:
|
|
215
|
+
cmd.append("--prerelease=allow")
|
|
216
|
+
return cmd
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_commands(install, latest, allow_pre):
|
|
220
|
+
mode = install["mode"]
|
|
142
221
|
target = f"{PACKAGE}"
|
|
143
222
|
if mode == "pipx":
|
|
144
223
|
if shutil.which("pipx") is None:
|
|
@@ -150,6 +229,9 @@ def build_commands(mode, allow_pre):
|
|
|
150
229
|
if mode == "uv":
|
|
151
230
|
if shutil.which("uv") is None:
|
|
152
231
|
raise click.ClickException("This looks like a uv tool install but uv is not on PATH.")
|
|
232
|
+
reinstall = uv_reinstall_command(install["location"], latest, allow_pre)
|
|
233
|
+
if reinstall:
|
|
234
|
+
return [reinstall]
|
|
153
235
|
cmd = ["uv", "tool", "upgrade", target]
|
|
154
236
|
if allow_pre:
|
|
155
237
|
cmd.append("--prerelease=allow")
|
|
@@ -163,6 +245,80 @@ def build_commands(mode, allow_pre):
|
|
|
163
245
|
return [cmd]
|
|
164
246
|
|
|
165
247
|
|
|
248
|
+
def installed_version():
|
|
249
|
+
"""The evo version the install holds now, read by a new interpreter: this process keeps the old one.
|
|
250
|
+
|
|
251
|
+
The probe drops the current directory ("") from sys.path so that a checkout's egg-info cannot answer.
|
|
252
|
+
"""
|
|
253
|
+
code = (
|
|
254
|
+
"import sys; sys.path = [p for p in sys.path if p]; "
|
|
255
|
+
f"from importlib.metadata import version; print(version({PACKAGE!r}))"
|
|
256
|
+
)
|
|
257
|
+
probe = subprocess.run(
|
|
258
|
+
[sys.executable, "-c", code],
|
|
259
|
+
capture_output=True,
|
|
260
|
+
text=True,
|
|
261
|
+
encoding="utf-8",
|
|
262
|
+
errors="replace",
|
|
263
|
+
check=False,
|
|
264
|
+
)
|
|
265
|
+
version = (probe.stdout or "").strip()
|
|
266
|
+
return version if probe.returncode == 0 and version else None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def pipx_spec(location):
|
|
270
|
+
try:
|
|
271
|
+
data = json.loads((Path(location) / "pipx_metadata.json").read_text(encoding="utf-8"))
|
|
272
|
+
except (OSError, ValueError):
|
|
273
|
+
return None
|
|
274
|
+
return (data.get("main_package") or {}).get("package_or_url")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def stale_hint(install, latest):
|
|
278
|
+
"""Why the installer stopped short of `latest`, and the command that gets there."""
|
|
279
|
+
location = install["location"]
|
|
280
|
+
if install["mode"] == "uv":
|
|
281
|
+
_, requirement = uv_receipt(location)
|
|
282
|
+
specifier = (requirement or {}).get("specifier")
|
|
283
|
+
fix = f"uv tool install --force --refresh-package {TOOL_NAME} '{TOOL_NAME}>={latest}'"
|
|
284
|
+
if specifier:
|
|
285
|
+
receipt = Path(location) / "uv-receipt.toml"
|
|
286
|
+
return (
|
|
287
|
+
f"{receipt} pins {TOOL_NAME}{specifier}, which `uv tool upgrade` keeps. "
|
|
288
|
+
f"Reinstall without the pin:\n {fix}"
|
|
289
|
+
)
|
|
290
|
+
return f"uv may have read a cached package index from before the release. Refresh it:\n {fix}"
|
|
291
|
+
if install["mode"] == "pipx":
|
|
292
|
+
fix = f"pipx install --force '{TOOL_NAME}>={latest}'"
|
|
293
|
+
spec = pipx_spec(location)
|
|
294
|
+
if spec and re.search(r"[=<>~!]", spec):
|
|
295
|
+
return f"pipx installed evo as {spec}, which `pipx upgrade` keeps. Reinstall without the pin:\n {fix}"
|
|
296
|
+
return f"Reinstall it:\n {fix}"
|
|
297
|
+
return (
|
|
298
|
+
"A pip constraint or a cached index may hold it back. Run:\n"
|
|
299
|
+
f" {sys.executable} -m pip install --upgrade '{TOOL_NAME}>={latest}'"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def confirm_update(install, current, latest):
|
|
304
|
+
"""Report success only for the version that is really installed now."""
|
|
305
|
+
installed = installed_version()
|
|
306
|
+
if installed is None:
|
|
307
|
+
raise click.ClickException("The installer finished, but the installed evo version cannot be read.")
|
|
308
|
+
if latest is None:
|
|
309
|
+
if installed == current:
|
|
310
|
+
warning(f"evo is still at [accent]{installed}[/accent], and PyPI did not say whether a newer one exists.")
|
|
311
|
+
else:
|
|
312
|
+
success(f"evo is now at [accent]{installed}[/accent].")
|
|
313
|
+
return
|
|
314
|
+
if parse_version(installed) < parse_version(latest):
|
|
315
|
+
raise click.ClickException(
|
|
316
|
+
f"evo is still at {installed}, not {latest}, after the update. {stale_hint(install, latest)}"
|
|
317
|
+
)
|
|
318
|
+
success(f"evo updated to [accent]{installed}[/accent].")
|
|
319
|
+
info("Open a new shell if the `evo` command still reports the old version.")
|
|
320
|
+
|
|
321
|
+
|
|
166
322
|
def execute(commands, dry_run):
|
|
167
323
|
for cmd in commands:
|
|
168
324
|
if dry_run:
|
|
@@ -238,7 +394,7 @@ def update_editable(path, check, force, dry_run):
|
|
|
238
394
|
success(f"evo is now at [accent]{version_file.read_text(encoding='utf-8').strip()}[/accent]")
|
|
239
395
|
|
|
240
396
|
|
|
241
|
-
def update_package(
|
|
397
|
+
def update_package(install, current, latest, check, allow_pre, force, dry_run):
|
|
242
398
|
if latest is None:
|
|
243
399
|
if check:
|
|
244
400
|
raise click.ClickException("Cannot determine the latest version from PyPI.")
|
|
@@ -253,10 +409,9 @@ def update_package(mode, current, latest, check, allow_pre, force, dry_run):
|
|
|
253
409
|
info("Run [accent]evo update[/accent] to install it.")
|
|
254
410
|
return
|
|
255
411
|
|
|
256
|
-
execute(build_commands(
|
|
412
|
+
execute(build_commands(install, latest, allow_pre), dry_run)
|
|
257
413
|
if not dry_run:
|
|
258
|
-
|
|
259
|
-
info("Open a new shell if the `evo` command still reports the old version.")
|
|
414
|
+
confirm_update(install, current, latest)
|
|
260
415
|
|
|
261
416
|
|
|
262
417
|
@click.command("update", epilog=EPILOG, context_settings={"help_option_names": ["-h", "--help"]})
|
|
@@ -288,4 +443,4 @@ def update(check, allow_pre, force, dry_run, timeout):
|
|
|
288
443
|
if install["mode"] == "editable":
|
|
289
444
|
update_editable(install["location"], check, force, dry_run)
|
|
290
445
|
return
|
|
291
|
-
update_package(install
|
|
446
|
+
update_package(install, __version__, latest, check, allow_pre, force, dry_run)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import subprocess
|
|
2
|
+
import sys
|
|
2
3
|
|
|
3
4
|
from click.testing import CliRunner
|
|
4
5
|
|
|
@@ -18,6 +19,29 @@ def _stub_pypi(monkeypatch, versions):
|
|
|
18
19
|
monkeypatch.setattr(update_command, "fetch_pypi", lambda package, timeout: _pypi(versions))
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def _stub_installer(monkeypatch, installed_after):
|
|
23
|
+
"""Record the installer commands; the version probe then reports `installed_after`."""
|
|
24
|
+
ran = []
|
|
25
|
+
monkeypatch.setattr(update_command, "run_command", lambda cmd: ran.append([str(part) for part in cmd]))
|
|
26
|
+
|
|
27
|
+
def fake_run(cmd, **kwargs):
|
|
28
|
+
assert cmd[0] == sys.executable
|
|
29
|
+
return _result(stdout=f"{installed_after}\n")
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(update_command.subprocess, "run", fake_run)
|
|
32
|
+
return ran
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _uv_install(monkeypatch, tmp_path, receipt):
|
|
36
|
+
(tmp_path / "uv-receipt.toml").write_text(receipt, encoding="utf-8")
|
|
37
|
+
monkeypatch.setattr(update_command, "detect_install", lambda: {"mode": "uv", "location": tmp_path})
|
|
38
|
+
monkeypatch.setattr(update_command.shutil, "which", lambda name: f"/usr/bin/{name}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _flat(output):
|
|
42
|
+
return " ".join(output.replace("│", " ").split())
|
|
43
|
+
|
|
44
|
+
|
|
21
45
|
def test_update_command_is_registered():
|
|
22
46
|
assert "update" in cli.commands
|
|
23
47
|
|
|
@@ -76,14 +100,113 @@ def test_update_runs_pip_upgrade_when_outdated(monkeypatch):
|
|
|
76
100
|
_stub_pypi(monkeypatch, ["0.11.3", "0.12.0"])
|
|
77
101
|
monkeypatch.setattr(update_command, "detect_install", lambda: {"mode": "pip", "location": "/venv"})
|
|
78
102
|
monkeypatch.setattr(update_command, "__version__", "0.11.3")
|
|
79
|
-
monkeypatch.setattr(
|
|
103
|
+
monkeypatch.setattr(
|
|
104
|
+
update_command, "build_commands", lambda install, latest, allow_pre: [["pip", "install", "-U", install["mode"]]]
|
|
105
|
+
)
|
|
80
106
|
calls = []
|
|
81
107
|
monkeypatch.setattr(update_command, "execute", lambda commands, dry_run: calls.append((commands, dry_run)))
|
|
108
|
+
_stub_installer(monkeypatch, "0.12.0")
|
|
82
109
|
|
|
83
110
|
result = CliRunner().invoke(cli, ["update"])
|
|
84
111
|
|
|
85
112
|
assert result.exit_code == 0
|
|
86
113
|
assert calls == [([["pip", "install", "-U", "pip"]], False)]
|
|
114
|
+
assert "evo updated to 0.12.0" in result.output
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_update_fails_when_the_upgrade_leaves_the_old_version(monkeypatch):
|
|
118
|
+
_stub_pypi(monkeypatch, ["0.11.3", "0.12.0"])
|
|
119
|
+
monkeypatch.setattr(update_command, "detect_install", lambda: {"mode": "pip", "location": "/venv"})
|
|
120
|
+
monkeypatch.setattr(update_command, "__version__", "0.11.3")
|
|
121
|
+
ran = _stub_installer(monkeypatch, "0.11.3")
|
|
122
|
+
|
|
123
|
+
result = CliRunner().invoke(cli, ["update"])
|
|
124
|
+
|
|
125
|
+
assert result.exit_code == 1
|
|
126
|
+
assert ran == [[sys.executable, "-m", "pip", "install", "--upgrade", "evo_cli"]]
|
|
127
|
+
assert "evo updated" not in result.output
|
|
128
|
+
assert "evo is still at 0.11.3, not 0.12.0" in _flat(result.output)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_uv_update_reinstalls_over_a_stale_exact_pin(monkeypatch, tmp_path):
|
|
132
|
+
_stub_pypi(monkeypatch, ["0.31.0", "0.31.1"])
|
|
133
|
+
monkeypatch.setattr(update_command, "__version__", "0.31.0")
|
|
134
|
+
_uv_install(
|
|
135
|
+
monkeypatch,
|
|
136
|
+
tmp_path,
|
|
137
|
+
'[tool]\nrequirements = [{ name = "evo-cli", specifier = "==0.31.0" }]\n'
|
|
138
|
+
'entrypoints = [{ name = "evo", install-path = "/home/me/.local/bin/evo" }]\n',
|
|
139
|
+
)
|
|
140
|
+
ran = _stub_installer(monkeypatch, "0.31.1")
|
|
141
|
+
|
|
142
|
+
result = CliRunner().invoke(cli, ["update"])
|
|
143
|
+
|
|
144
|
+
assert result.exit_code == 0, result.output
|
|
145
|
+
assert ran == [["uv", "tool", "install", "--force", "--refresh-package", "evo-cli", "evo-cli>=0.31.1"]]
|
|
146
|
+
assert "pins evo-cli==0.31.0" in _flat(result.output)
|
|
147
|
+
assert "evo updated to 0.31.1" in result.output
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_uv_reinstall_keeps_extras_and_python(monkeypatch, tmp_path):
|
|
151
|
+
_stub_pypi(monkeypatch, ["0.31.0", "0.31.1"])
|
|
152
|
+
monkeypatch.setattr(update_command, "__version__", "0.31.0")
|
|
153
|
+
_uv_install(
|
|
154
|
+
monkeypatch,
|
|
155
|
+
tmp_path,
|
|
156
|
+
'[tool]\nrequirements = [{ name = "evo-cli", extras = ["image"], specifier = "==0.31.0" }]\npython = "3.13"\n',
|
|
157
|
+
)
|
|
158
|
+
ran = _stub_installer(monkeypatch, "0.31.1")
|
|
159
|
+
|
|
160
|
+
result = CliRunner().invoke(cli, ["update"])
|
|
161
|
+
|
|
162
|
+
assert result.exit_code == 0, result.output
|
|
163
|
+
assert ran == [
|
|
164
|
+
[
|
|
165
|
+
"uv",
|
|
166
|
+
"tool",
|
|
167
|
+
"install",
|
|
168
|
+
"--force",
|
|
169
|
+
"--refresh-package",
|
|
170
|
+
"evo-cli",
|
|
171
|
+
"evo-cli[image]>=0.31.1",
|
|
172
|
+
"--python",
|
|
173
|
+
"3.13",
|
|
174
|
+
]
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_uv_upgrade_held_by_a_pin_it_cannot_rebuild_fails_with_the_fix(monkeypatch, tmp_path):
|
|
179
|
+
_stub_pypi(monkeypatch, ["0.31.0", "0.31.1"])
|
|
180
|
+
monkeypatch.setattr(update_command, "__version__", "0.31.0")
|
|
181
|
+
_uv_install(
|
|
182
|
+
monkeypatch,
|
|
183
|
+
tmp_path,
|
|
184
|
+
'[tool]\nrequirements = [{ name = "evo-cli", specifier = "==0.31.0" }, { name = "rich" }]\n',
|
|
185
|
+
)
|
|
186
|
+
ran = _stub_installer(monkeypatch, "0.31.0")
|
|
187
|
+
|
|
188
|
+
result = CliRunner().invoke(cli, ["update"])
|
|
189
|
+
output = _flat(result.output)
|
|
190
|
+
|
|
191
|
+
assert result.exit_code == 1
|
|
192
|
+
assert ran == [["uv", "tool", "upgrade", "evo_cli"]]
|
|
193
|
+
assert "evo updated" not in result.output
|
|
194
|
+
assert "evo is still at 0.31.0, not 0.31.1" in output
|
|
195
|
+
assert "pins evo-cli==0.31.0" in output
|
|
196
|
+
assert "uv tool install --force --refresh-package evo-cli 'evo-cli>=0.31.1'" in output
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_uv_upgrade_without_a_pin_skips_the_reinstall(monkeypatch, tmp_path):
|
|
200
|
+
_stub_pypi(monkeypatch, ["0.31.0", "0.31.1"])
|
|
201
|
+
monkeypatch.setattr(update_command, "__version__", "0.31.0")
|
|
202
|
+
_uv_install(monkeypatch, tmp_path, '[tool]\nrequirements = [{ name = "evo-cli" }]\n')
|
|
203
|
+
ran = _stub_installer(monkeypatch, "0.31.1")
|
|
204
|
+
|
|
205
|
+
result = CliRunner().invoke(cli, ["update"])
|
|
206
|
+
|
|
207
|
+
assert result.exit_code == 0, result.output
|
|
208
|
+
assert ran == [["uv", "tool", "upgrade", "evo_cli"]]
|
|
209
|
+
assert "evo updated to 0.31.1" in result.output
|
|
87
210
|
|
|
88
211
|
|
|
89
212
|
def test_editable_update_pulls_and_reports_head(monkeypatch, tmp_path):
|
|
@@ -167,7 +290,9 @@ def test_dry_run_prints_commands_without_running_them(monkeypatch):
|
|
|
167
290
|
_stub_pypi(monkeypatch, ["0.11.3", "0.12.0"])
|
|
168
291
|
monkeypatch.setattr(update_command, "detect_install", lambda: {"mode": "pip", "location": "/venv"})
|
|
169
292
|
monkeypatch.setattr(update_command, "__version__", "0.11.3")
|
|
170
|
-
monkeypatch.setattr(
|
|
293
|
+
monkeypatch.setattr(
|
|
294
|
+
update_command, "build_commands", lambda install, latest, allow_pre: [["pip", "install", "-U", "evo_cli"]]
|
|
295
|
+
)
|
|
171
296
|
monkeypatch.setattr(update_command, "run_command", lambda cmd: (_ for _ in ()).throw(AssertionError("ran")))
|
|
172
297
|
|
|
173
298
|
result = CliRunner().invoke(cli, ["update", "--dry-run"])
|
evo_cli-0.31.1/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|