akashcli 4.7.1__tar.gz → 4.7.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.
- {akashcli-4.7.1 → akashcli-4.7.2}/PKG-INFO +1 -1
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/__init__.py +1 -1
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/cli.py +65 -1
- akashcli-4.7.2/akashcli/updater.py +225 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/SOURCES.txt +1 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/pyproject.toml +1 -1
- {akashcli-4.7.1 → akashcli-4.7.2}/README.md +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/_net.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/auth.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/client.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/config.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/crypto.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/env_manager.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/exceptions.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/exec.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/integrity.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/logger.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/models.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/p2p.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/packages.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/relay.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/restore.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/storage.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/terminal.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/uploader.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/utils.py +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.7.1 → akashcli-4.7.2}/setup.cfg +0 -0
|
@@ -6,6 +6,7 @@ import subprocess
|
|
|
6
6
|
import re
|
|
7
7
|
import webbrowser
|
|
8
8
|
import tempfile
|
|
9
|
+
import atexit
|
|
9
10
|
from typing import Optional, List
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
@@ -18,6 +19,9 @@ from .config import ConfigManager, DEFAULT_STORAGE_URL
|
|
|
18
19
|
from .exceptions import AkashError
|
|
19
20
|
from .exec import _device_fingerprint
|
|
20
21
|
from .uploader import VaultUploader
|
|
22
|
+
from .updater import show_update_warning_if_available, check_for_updates, format_update_box, mark_update_printed
|
|
23
|
+
|
|
24
|
+
atexit.register(show_update_warning_if_available)
|
|
21
25
|
|
|
22
26
|
# ── ANSI palette (matches exec.py) ───────────────────────────────────────────
|
|
23
27
|
_C = "\033[1;36m" # cyan bold
|
|
@@ -184,6 +188,13 @@ def _version_callback(value: bool):
|
|
|
184
188
|
if value:
|
|
185
189
|
from . import __version__
|
|
186
190
|
print(f"akashcli v{__version__}")
|
|
191
|
+
try:
|
|
192
|
+
available, latest = check_for_updates(__version__, force_sync=False)
|
|
193
|
+
if available and latest:
|
|
194
|
+
print(format_update_box(__version__, latest))
|
|
195
|
+
mark_update_printed()
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
187
198
|
raise typer.Exit(0)
|
|
188
199
|
|
|
189
200
|
def raise_exit():
|
|
@@ -241,6 +252,7 @@ def _print_help():
|
|
|
241
252
|
f" {C}log{RS} {DM}[--stats]{RS} view operation history & bandwidth summary",
|
|
242
253
|
f" {C}check{RS} {DM}[--seal]{RS} verify source integrity & tamper detection",
|
|
243
254
|
f" {C}version{RS} check CLI and server status & versions",
|
|
255
|
+
f" {C}update{RS} check and install latest akashcli release",
|
|
244
256
|
"",
|
|
245
257
|
f" {DM}Run {RS}{C}akash <command> --help{RS}{DM} for full options on any command.{RS}",
|
|
246
258
|
"",
|
|
@@ -317,7 +329,50 @@ def version(url: Optional[str] = typer.Option(None, "--url", help="Server URL to
|
|
|
317
329
|
print(f" {_DM}server {_RS}{_cy(base)} {_dim(f'(HTTP {resp.status_code})')}")
|
|
318
330
|
except Exception:
|
|
319
331
|
print(f" {_DM}server {_RS}{_cy(base)} {_dim('(unreachable)')}")
|
|
320
|
-
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
available, latest = check_for_updates(__version__, force_sync=True)
|
|
335
|
+
if available and latest:
|
|
336
|
+
print(format_update_box(__version__, latest))
|
|
337
|
+
mark_update_printed()
|
|
338
|
+
else:
|
|
339
|
+
print(f" {_G}✓{_RS} {_DM}akashcli is up to date{_RS}\n")
|
|
340
|
+
except Exception:
|
|
341
|
+
print()
|
|
342
|
+
|
|
343
|
+
@app.command()
|
|
344
|
+
def update():
|
|
345
|
+
"""Check for updates and upgrade akashcli to the latest version."""
|
|
346
|
+
from . import __version__
|
|
347
|
+
_p()
|
|
348
|
+
print(f" {_C}◈{_RS} {_W}Checking for akashcli updates…{_RS}")
|
|
349
|
+
available, latest = check_for_updates(__version__, force_sync=True)
|
|
350
|
+
if not available or not latest:
|
|
351
|
+
_ok(f"akashcli is already at the latest version ({_cy('v' + __version__)})")
|
|
352
|
+
_p()
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
print(f" {_Y}⚡{_RS} New version available: {_DM}{__version__}{_RS} {_C}→{_RS} {_G}{latest}{_RS}")
|
|
356
|
+
with _Spin(f"updating akashcli to v{latest}…"):
|
|
357
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "akashcli", "--break-system-packages"]
|
|
358
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
359
|
+
if res.returncode != 0:
|
|
360
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "akashcli"]
|
|
361
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
362
|
+
|
|
363
|
+
if res.returncode == 0:
|
|
364
|
+
_ok(f"Successfully updated akashcli to {_G}v{latest}{_RS}!")
|
|
365
|
+
try:
|
|
366
|
+
from .integrity import seal
|
|
367
|
+
seal()
|
|
368
|
+
_ok("Integrity manifest re-sealed.")
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
else:
|
|
372
|
+
_err(f"Update failed: {res.stderr or res.stdout}")
|
|
373
|
+
_info(f"Please run manually: {_cy('pip install -U akashcli')}")
|
|
374
|
+
mark_update_printed()
|
|
375
|
+
_p()
|
|
321
376
|
|
|
322
377
|
@app.command()
|
|
323
378
|
def whoami():
|
|
@@ -2868,6 +2923,8 @@ def agent_update(
|
|
|
2868
2923
|
_err("Provide a Profile ID (e.g. 'akash agent update fbf6 --scopes +exec:package_install')")
|
|
2869
2924
|
raise typer.Exit(1)
|
|
2870
2925
|
|
|
2926
|
+
if role and not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
|
|
2927
|
+
role = "@" + role
|
|
2871
2928
|
effective_scopes = scopes if scopes else role
|
|
2872
2929
|
if not effective_scopes:
|
|
2873
2930
|
_err("Specify new scopes with --scopes or --role (e.g. --scopes +exec:package_install or --role @coder)")
|
|
@@ -2939,6 +2996,13 @@ def agent_create(
|
|
|
2939
2996
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
2940
2997
|
):
|
|
2941
2998
|
"""Generate new Client ID and Client Secret with hardware binding."""
|
|
2999
|
+
if role:
|
|
3000
|
+
if not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
|
|
3001
|
+
role = "@" + role
|
|
3002
|
+
effective_scopes = role
|
|
3003
|
+
else:
|
|
3004
|
+
effective_scopes = scopes
|
|
3005
|
+
|
|
2942
3006
|
api_key, base = _resolve_client(url, require_key=False)
|
|
2943
3007
|
hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
|
|
2944
3008
|
if api_key: hdrs["x-api-key"] = api_key
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
updater.py — Asynchronous, cached PyPI update checker for akashcli.
|
|
3
|
+
|
|
4
|
+
- Queries PyPI (https://pypi.org/pypi/akashcli/json) for the latest release.
|
|
5
|
+
- Caches response in ~/.akash/update_check.json with a 6-hour TTL.
|
|
6
|
+
- Background refresh runs detached so CLI commands have 0ms network latency.
|
|
7
|
+
- Renders an aligned, non-intrusive warning box when an update is available.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import subprocess
|
|
15
|
+
import unicodedata
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, Dict, Tuple
|
|
19
|
+
|
|
20
|
+
CACHE_FILE = Path.home() / ".akash" / "update_check.json"
|
|
21
|
+
CHECK_INTERVAL_SECONDS = 6 * 3600 # 6 hours
|
|
22
|
+
PYPI_URL = "https://pypi.org/pypi/akashcli/json"
|
|
23
|
+
|
|
24
|
+
_already_printed = False
|
|
25
|
+
|
|
26
|
+
# ANSI Color Palette
|
|
27
|
+
_C = "\033[1;36m" # cyan bold
|
|
28
|
+
_W = "\033[1;37m" # white bold
|
|
29
|
+
_G = "\033[1;32m" # green bold
|
|
30
|
+
_Y = "\033[1;33m" # yellow bold
|
|
31
|
+
_DM = "\033[2m" # dim
|
|
32
|
+
_GR = "\033[90m" # gray
|
|
33
|
+
_RS = "\033[0m" # reset
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cell_width(text: str) -> int:
|
|
37
|
+
"""Calculate terminal display width accounting for wide characters and ANSI codes."""
|
|
38
|
+
clean = re.sub(r'\x1b\[[0-9;]*m', '', text)
|
|
39
|
+
width = 0
|
|
40
|
+
for ch in clean:
|
|
41
|
+
if unicodedata.east_asian_width(ch) in ('W', 'F'):
|
|
42
|
+
width += 2
|
|
43
|
+
else:
|
|
44
|
+
width += 1
|
|
45
|
+
return width
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_newer_version(latest: str, current: str) -> bool:
|
|
49
|
+
"""Compare two semantic version strings safely."""
|
|
50
|
+
if not latest or not current:
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
from packaging.version import parse
|
|
54
|
+
return parse(latest) > parse(current)
|
|
55
|
+
except Exception:
|
|
56
|
+
try:
|
|
57
|
+
def to_tuple(v: str) -> Tuple[int, ...]:
|
|
58
|
+
nums = [int(x) for x in re.findall(r'\d+', v.split("+")[0])]
|
|
59
|
+
return tuple(nums)
|
|
60
|
+
return to_tuple(latest) > to_tuple(current)
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fetch_latest_version(timeout: float = 2.0) -> Optional[str]:
|
|
66
|
+
"""Synchronously fetch the latest version of akashcli from PyPI."""
|
|
67
|
+
try:
|
|
68
|
+
import httpx
|
|
69
|
+
with httpx.Client(timeout=timeout) as client:
|
|
70
|
+
resp = client.get(PYPI_URL)
|
|
71
|
+
if resp.status_code == 200:
|
|
72
|
+
data = resp.json()
|
|
73
|
+
return data.get("info", {}).get("version")
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_cache() -> Optional[Dict]:
|
|
80
|
+
"""Read the cached update check result."""
|
|
81
|
+
try:
|
|
82
|
+
if CACHE_FILE.exists():
|
|
83
|
+
return json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_cache(latest_version: str):
|
|
90
|
+
"""Write the latest version to cache."""
|
|
91
|
+
try:
|
|
92
|
+
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
data = {
|
|
94
|
+
"last_checked": time.time(),
|
|
95
|
+
"latest_version": latest_version
|
|
96
|
+
}
|
|
97
|
+
CACHE_FILE.write_text(json.dumps(data), encoding="utf-8")
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def trigger_background_check():
|
|
103
|
+
"""Trigger a detached background process to query PyPI and update the cache."""
|
|
104
|
+
try:
|
|
105
|
+
subprocess.Popen(
|
|
106
|
+
[sys.executable, "-m", "akashcli.updater"],
|
|
107
|
+
stdout=subprocess.DEVNULL,
|
|
108
|
+
stderr=subprocess.DEVNULL,
|
|
109
|
+
stdin=subprocess.DEVNULL,
|
|
110
|
+
start_new_session=True,
|
|
111
|
+
close_fds=True,
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_for_updates(current_version: str, force_sync: bool = False) -> Tuple[bool, Optional[str]]:
|
|
118
|
+
"""
|
|
119
|
+
Check if an update is available.
|
|
120
|
+
Returns (is_available, latest_version).
|
|
121
|
+
"""
|
|
122
|
+
if os.environ.get("AKASH_NO_UPDATE_CHECK", "").lower() in ("1", "true", "yes"):
|
|
123
|
+
return False, None
|
|
124
|
+
if os.environ.get("CI", "").lower() in ("1", "true", "yes"):
|
|
125
|
+
return False, None
|
|
126
|
+
|
|
127
|
+
now = time.time()
|
|
128
|
+
cache = read_cache()
|
|
129
|
+
|
|
130
|
+
if force_sync:
|
|
131
|
+
latest = fetch_latest_version(timeout=2.0)
|
|
132
|
+
if latest:
|
|
133
|
+
write_cache(latest)
|
|
134
|
+
return is_newer_version(latest, current_version), latest
|
|
135
|
+
# If fetch fails, fall back to cache
|
|
136
|
+
if cache and "latest_version" in cache:
|
|
137
|
+
latest = cache["latest_version"]
|
|
138
|
+
return is_newer_version(latest, current_version), latest
|
|
139
|
+
return False, None
|
|
140
|
+
|
|
141
|
+
# Check cache TTL
|
|
142
|
+
needs_refresh = True
|
|
143
|
+
if cache and isinstance(cache, dict):
|
|
144
|
+
last_checked = cache.get("last_checked", 0)
|
|
145
|
+
if (now - last_checked) < CHECK_INTERVAL_SECONDS:
|
|
146
|
+
needs_refresh = False
|
|
147
|
+
|
|
148
|
+
if needs_refresh:
|
|
149
|
+
trigger_background_check()
|
|
150
|
+
|
|
151
|
+
if cache and "latest_version" in cache:
|
|
152
|
+
latest = cache["latest_version"]
|
|
153
|
+
if is_newer_version(latest, current_version):
|
|
154
|
+
return True, latest
|
|
155
|
+
|
|
156
|
+
return False, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def format_update_box(current: str, latest: str) -> str:
|
|
160
|
+
"""Format a styled, pixel-aligned terminal banner for the update notification."""
|
|
161
|
+
line1 = f" {_Y}⚡{_RS} {_W}Update available:{_RS} {_DM}{current}{_RS} {_C}→{_RS} {_G}{latest}{_RS}"
|
|
162
|
+
cmd_str = "pip install -U akashcli"
|
|
163
|
+
line2 = f" {_DM}Run:{_RS} {_C}{cmd_str}{_RS} {_DM}to update{_RS}"
|
|
164
|
+
|
|
165
|
+
w1 = _cell_width(line1)
|
|
166
|
+
w2 = _cell_width(line2)
|
|
167
|
+
|
|
168
|
+
width = max(w1, w2) + 4
|
|
169
|
+
pad1 = width - w1
|
|
170
|
+
pad2 = width - w2
|
|
171
|
+
|
|
172
|
+
top = f" {_GR}╭{'─' * width}╮{_RS}"
|
|
173
|
+
mid1 = f" {_GR}│{_RS}{line1}{' ' * pad1}{_GR}│{_RS}"
|
|
174
|
+
mid2 = f" {_GR}│{_RS}{line2}{' ' * pad2}{_GR}│{_RS}"
|
|
175
|
+
bot = f" {_GR}╰{'─' * width}╯{_RS}"
|
|
176
|
+
|
|
177
|
+
return f"\n{top}\n{mid1}\n{mid2}\n{bot}\n"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def should_suppress_notice() -> bool:
|
|
181
|
+
"""Check if the notice should be suppressed (e.g. non-tty or json/pipe output)."""
|
|
182
|
+
if os.environ.get("AKASH_NO_UPDATE_CHECK", "").lower() in ("1", "true", "yes"):
|
|
183
|
+
return True
|
|
184
|
+
if not sys.stdout.isatty():
|
|
185
|
+
return True
|
|
186
|
+
# Suppress in quiet or raw data commands
|
|
187
|
+
for arg in sys.argv[1:]:
|
|
188
|
+
if arg in ("--json", "--raw", "--quiet", "-q", "pipe", "stream"):
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def show_update_warning_if_available():
|
|
194
|
+
"""Atexit / teardown hook: show update warning if available and not yet printed."""
|
|
195
|
+
global _already_printed
|
|
196
|
+
if _already_printed:
|
|
197
|
+
return
|
|
198
|
+
if should_suppress_notice():
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from . import __version__
|
|
203
|
+
available, latest = check_for_updates(__version__, force_sync=False)
|
|
204
|
+
if available and latest:
|
|
205
|
+
_already_printed = True
|
|
206
|
+
print(format_update_box(__version__, latest))
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def mark_update_printed():
|
|
212
|
+
"""Mark update warning as printed so it won't duplicate on exit."""
|
|
213
|
+
global _already_printed
|
|
214
|
+
_already_printed = True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main():
|
|
218
|
+
"""Background worker entry point: python3 -m akashcli.updater"""
|
|
219
|
+
latest = fetch_latest_version(timeout=4.0)
|
|
220
|
+
if latest:
|
|
221
|
+
write_cache(latest)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|