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.
Files changed (33) hide show
  1. {akashcli-4.7.1 → akashcli-4.7.2}/PKG-INFO +1 -1
  2. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/__init__.py +1 -1
  3. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/cli.py +65 -1
  4. akashcli-4.7.2/akashcli/updater.py +225 -0
  5. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/PKG-INFO +1 -1
  6. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/SOURCES.txt +1 -0
  7. {akashcli-4.7.1 → akashcli-4.7.2}/pyproject.toml +1 -1
  8. {akashcli-4.7.1 → akashcli-4.7.2}/README.md +0 -0
  9. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/_net.py +0 -0
  10. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/auth.py +0 -0
  11. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/client.py +0 -0
  12. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/config.py +0 -0
  13. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/crypto.py +0 -0
  14. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/env_manager.py +0 -0
  15. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/exceptions.py +0 -0
  16. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/exec.py +0 -0
  17. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/integrity.py +0 -0
  18. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/logger.py +0 -0
  19. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/models.py +0 -0
  20. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/p2p.py +0 -0
  21. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/packages.py +0 -0
  22. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/relay.py +0 -0
  23. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/restore.py +0 -0
  24. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/snapshot_crypto.py +0 -0
  25. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/storage.py +0 -0
  26. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/terminal.py +0 -0
  27. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/uploader.py +0 -0
  28. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli/utils.py +0 -0
  29. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.7.1 → akashcli-4.7.2}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.7.1 → akashcli-4.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.1
3
+ Version: 4.7.2
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License-Expression: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -2,7 +2,7 @@ try:
2
2
  from importlib.metadata import version
3
3
  __version__ = version("akashcli")
4
4
  except Exception:
5
- __version__ = "4.7.1"
5
+ __version__ = "4.7.2"
6
6
 
7
7
  AKASH_VERSION = __version__
8
8
 
@@ -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
- print()
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.1
3
+ Version: 4.7.2
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License-Expression: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -20,6 +20,7 @@ akashcli/restore.py
20
20
  akashcli/snapshot_crypto.py
21
21
  akashcli/storage.py
22
22
  akashcli/terminal.py
23
+ akashcli/updater.py
23
24
  akashcli/uploader.py
24
25
  akashcli/utils.py
25
26
  akashcli.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.7.1"
3
+ version = "4.7.2"
4
4
  description = "Akash CLI — secure remote shell relay, file vault, and device management"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes