akashcli 4.4.0__tar.gz → 4.5.0__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 (31) hide show
  1. {akashcli-4.4.0 → akashcli-4.5.0}/PKG-INFO +1 -1
  2. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/cli.py +130 -50
  3. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/relay.py +192 -0
  4. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/PKG-INFO +1 -1
  5. {akashcli-4.4.0 → akashcli-4.5.0}/pyproject.toml +1 -1
  6. {akashcli-4.4.0 → akashcli-4.5.0}/README.md +0 -0
  7. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/__init__.py +0 -0
  8. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/auth.py +0 -0
  9. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/client.py +0 -0
  10. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/config.py +0 -0
  11. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/crypto.py +0 -0
  12. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/exec.py +0 -0
  15. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/integrity.py +0 -0
  16. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/logger.py +0 -0
  17. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/models.py +0 -0
  18. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/p2p.py +0 -0
  19. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/packages.py +0 -0
  20. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/restore.py +0 -0
  21. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/storage.py +0 -0
  23. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/terminal.py +0 -0
  24. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/uploader.py +0 -0
  25. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/utils.py +0 -0
  26. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.4.0 → akashcli-4.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.4.0
3
+ Version: 4.5.0
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -179,9 +179,11 @@ def _print_help():
179
179
  f" {C}pull{RS} {DM}<CODE>{RS} restore on any machine — no login needed",
180
180
  f"",
181
181
  f" {DM}REMOTE SHELL{RS}",
182
- f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, or on another device",
182
+ f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, on a device, or local",
183
+ f" {C}shell{RS} {DM}--code A3F7B2{RS} connect via one-time pairing code (no login)",
183
184
  f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run a command locally or on a device",
184
- f" {C}serve{RS} {DM}[--label NAME]{RS} make this device reachable for shell/run",
185
+ f" {C}serve{RS} {DM}[--share]{RS} share this shell (prints a pairing code)",
186
+ f" {C}serve{RS} {DM}[--label NAME]{RS} register device permanently (needs login)",
185
187
  f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or revoke enrolled devices",
186
188
  f"",
187
189
  f" {DM}FILES{RS}",
@@ -966,27 +968,37 @@ def run(
966
968
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
967
969
  save: bool = typer.Option(False, "--save", help="Upload command output to cloud"),
968
970
  save_name: Optional[str] = typer.Option(None, "--save-name", help="Filename for saved output"),
969
- device: Optional[str] = typer.Option(None, "--device","-d",help="Target device label (default: auto)"),
971
+ device: Optional[str] = typer.Option(None, "--device", "-d",
972
+ help="Run on a device (set up with: akash serve --label NAME)"),
970
973
  ):
971
- """Run a command — on the server or on a specific enrolled device.
974
+ """Run a command — locally, on the server, or on an enrolled device.
972
975
 
973
- Token cached 24 h — instant reconnect every time.
976
+ No login needed:
977
+ akash run "df -h" runs locally
978
+ akash run "ls" --save runs locally, uploads output
974
979
 
975
- Examples:
976
- akash run "ls -la ~" # run on server
977
- akash run "df -h" --device termux # run on your phone
978
- akash run "df -h" --device laptop # run on your laptop
979
- akash run "pip list" --save # save output to cloud
980
+ With login:
981
+ akash run "df -h" runs on server
982
+ akash run "df -h" --device termux runs on your phone
983
+ akash run "pip list" --save runs on server, saves output
984
+
985
+ To make a device reachable:
986
+ 1. On that device: akash login --label termux && akash serve
987
+ 2. From anywhere: akash run "cmd" --device termux
980
988
  """
981
989
  from .exec import acquire_exec_token, run_command, _clear_cached_token
982
990
 
983
991
  try:
984
992
  api_key, base = _resolve_client(url)
985
993
 
986
- # ── No key + device flag → relay needs auth ───────────────────────
994
+ # ── --device but no key ───────────────────────────────────────────
987
995
  if device is not None and not api_key:
988
- print(_GUEST_WARNING)
989
- _err("relay requires an API key. Run `akash login` first.")
996
+ _warn("relay requires an API key.")
997
+ _p()
998
+ _info(f"To run on {_cy(device.strip() or 'a device')} you need to log in first:")
999
+ _info(f" {_cy('akash login')} save your API key on this machine")
1000
+ _info(f" {_cy('akash serve')} run on the target device to make it reachable")
1001
+ _p()
990
1002
  raise typer.Exit(1)
991
1003
 
992
1004
  # ── Device relay path ─────────────────────────────────────────────
@@ -1130,39 +1142,68 @@ def _build_ws_url_for_run(token: str, base_url: str) -> str:
1130
1142
  def shell(
1131
1143
  url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
1132
1144
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
1133
- device: Optional[str] = typer.Option(None, "--device","-d", help="Target device label (default: auto)"),
1145
+ device: Optional[str] = typer.Option(None, "--device", "-d",
1146
+ help="Device label to connect to (set up with: akash serve --label NAME)"),
1147
+ code: Optional[str] = typer.Option(None, "--code", "-c",
1148
+ help="Pairing code from `akash serve` — no login needed"),
1134
1149
  ):
1135
- """Open an interactive shell — on the server or on an enrolled device.
1150
+ """Open a shell — local, on a device, or via a one-time pairing code.
1136
1151
 
1137
- Full PTY — colours, arrow keys, tab completion, Ctrl-C all work.
1138
- Token cached 24 h — instant reconnect.
1152
+ No login needed:
1153
+ akash shell open a local bash shell
1154
+ akash serve get a pairing code, share it
1155
+ akash shell --code A3F7B2 connect using that code
1139
1156
 
1140
- Examples:
1141
- akash shell # server shell
1142
- akash shell --device termux # shell on your phone
1143
- akash shell --device laptop # shell on your laptop
1144
- akash shell --device # auto (works if only 1 device enrolled)
1157
+ With login:
1158
+ akash shell full server shell
1159
+ akash shell --device termux relay shell on enrolled device
1160
+ akash shell --device auto-pick enrolled device
1145
1161
  """
1146
1162
  from .exec import acquire_exec_token, run_shell, _clear_cached_token
1163
+ import shutil as _sh, subprocess as _sp
1147
1164
 
1148
1165
  try:
1149
1166
  api_key, base = _resolve_client(url)
1150
1167
 
1151
- # ── No key + device flag → relay needs auth ───────────────────────
1168
+ # ── Pairing code path (no login needed) ───────────────────────────
1169
+ if code is not None:
1170
+ from .relay import guest_connect_shell
1171
+ c = code.strip().upper()
1172
+ _p()
1173
+ _info(f"connecting with code {_cy(c)}…")
1174
+ try:
1175
+ exit_code = guest_connect_shell(base, c)
1176
+ except RuntimeError as e:
1177
+ _err(str(e))
1178
+ raise typer.Exit(1)
1179
+ _p()
1180
+ _info("session closed")
1181
+ if exit_code != 0:
1182
+ raise typer.Exit(exit_code)
1183
+ return
1184
+
1185
+ # ── --device flag but no key ──────────────────────────────────────
1152
1186
  if device is not None and not api_key:
1153
- print(_GUEST_WARNING)
1154
- _err("relay requires an API key. Run `akash login` first.")
1187
+ _warn("relay requires an API key.")
1188
+ _p()
1189
+ _info(f"To connect to {_cy(device.strip() or 'a device')} you need to log in first:")
1190
+ _info(f" {_cy('akash login')} save your API key on this machine")
1191
+ _info(f" Or use a pairing code: {_cy('akash serve')} → {_cy('akash shell --code <CODE>')}")
1192
+ _p()
1155
1193
  raise typer.Exit(1)
1156
1194
 
1157
1195
  # ── Device relay path ─────────────────────────────────────────────
1158
1196
  if device is not None:
1159
1197
  from .relay import connect_shell
1160
1198
  label = device.strip() if device.strip() else None
1161
- _info(f"connecting to device {_cy(label or 'auto')}…")
1199
+ _p()
1200
+ _info(f"connecting to {_cy(label or 'auto')}…")
1162
1201
  try:
1163
1202
  exit_code = connect_shell(api_key, base, target=label)
1164
1203
  except RuntimeError as e:
1165
1204
  _err(str(e))
1205
+ _p()
1206
+ _info(f"Make sure the device is running: {_cy('akash serve')}")
1166
1207
  raise typer.Exit(1)
1167
1208
  _p()
1168
1209
  _info("session closed")
@@ -1170,20 +1211,17 @@ def shell(
1170
1211
  raise typer.Exit(exit_code)
1171
1212
  return
1172
1213
 
1173
- # ── No key → local PTY shell fallback ────────────────────────────
1214
+ # ── No key → silent local shell ───────────────────────────────────
1174
1215
  if not api_key:
1175
- import shutil as _sh
1176
- print(_GUEST_WARNING)
1177
- _p()
1178
1216
  shell_bin = os.environ.get("SHELL") or _sh.which("bash") or _sh.which("sh") or "/bin/sh"
1179
- _info(f"opening local shell {_dim(shell_bin)}")
1217
+ _info(f"local shell {_dim(shell_bin)} {_dim('· tip: run akash login to unlock remote shells')}")
1180
1218
  _p()
1181
- import subprocess as _sp
1182
1219
  result = _sp.run([shell_bin])
1183
1220
  _p()
1184
1221
  _info("session closed")
1185
1222
  raise typer.Exit(result.returncode)
1186
1223
 
1224
+ # ── Server shell ──────────────────────────────────────────────────
1187
1225
  if reauth:
1188
1226
  _clear_cached_token()
1189
1227
  _info("cleared cached token — re-authenticating…")
@@ -1215,23 +1253,43 @@ def devices(
1215
1253
  revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke (unenroll) a device by label"),
1216
1254
  raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
1217
1255
  ):
1218
- """List enrolled devices or revoke one.
1256
+ """List enrolled devices — or revoke one.
1219
1257
 
1220
- Each device you log in on with `akash login --label` appears here.
1221
- The server tracks when each device last authenticated.
1258
+ Each device you run `akash serve` on appears here.
1259
+ Connect from anywhere with: akash shell --device NAME
1222
1260
 
1223
1261
  Examples:
1224
1262
  akash devices list all enrolled devices
1225
1263
  akash devices --revoke laptop remove a device
1264
+
1265
+ Setup:
1266
+ akash login --label phone name this device
1267
+ akash serve register it as reachable
1226
1268
  """
1227
1269
  try:
1228
- api_key, base = _resolve_client(require_key=True)
1270
+ api_key, base = _resolve_client(require_key=False)
1229
1271
  except typer.Exit:
1230
1272
  raise
1231
1273
  except Exception as e:
1232
1274
  _err(str(e))
1233
1275
  raise typer.Exit(1)
1234
1276
 
1277
+ if not api_key:
1278
+ _p()
1279
+ _info("no devices yet — here's how to set up remote access:")
1280
+ _p()
1281
+ print(f" {_DM}Step 1 — on each device you want to reach:{_RS}")
1282
+ print(f" {_cy('akash login --label termux')} {_DM}(name this device){_RS}")
1283
+ print(f" {_cy('akash serve')} {_DM}(keep running — makes it reachable){_RS}")
1284
+ _p()
1285
+ print(f" {_DM}Step 2 — from anywhere:{_RS}")
1286
+ print(f" {_cy('akash shell --device termux')} {_DM}(open a shell on it){_RS}")
1287
+ print(f" {_cy('akash run --device termux \"cmd\"')} {_DM}(run a command on it){_RS}")
1288
+ _p()
1289
+ _info(f"Run {_cy('akash login')} first to save your API key.")
1290
+ _p()
1291
+ raise typer.Exit(0)
1292
+
1235
1293
  _hdrs = {"x-api-key": api_key, "Origin": "https://akash.cli"}
1236
1294
 
1237
1295
  if revoke:
@@ -1297,22 +1355,33 @@ def serve(
1297
1355
  daemon: bool = typer.Option(False, "--daemon", help="Run in background"),
1298
1356
  status: bool = typer.Option(False, "--status", help="Show serve daemon status"),
1299
1357
  stop: bool = typer.Option(False, "--stop", help="Stop background serve daemon"),
1358
+ share: bool = typer.Option(False, "--share", help="Guest mode: get a one-time code, no login needed"),
1300
1359
  ):
1301
- """Make this device reachable via `akash shell --device` and `akash run --device`.
1302
-
1303
- Registers this machine with the relay server and waits for incoming
1304
- shell/run requests. The connection is brokered through your cloud server —
1305
- no public IP required.
1306
-
1307
- Examples:
1308
- akash serve # foreground (Ctrl-C to stop)
1309
- akash serve --daemon # background daemon
1310
- akash serve --status # check if daemon is running
1311
- akash serve --stop # kill daemon
1312
- akash serve --label termux # override device label
1360
+ """Make this device reachable for remote shell — with or without login.
1361
+
1362
+ No login needed (one-time pairing code):
1363
+ akash serve --share prints a code, share it with anyone
1364
+ akash shell --code A3F7B2 they connect using that code
1365
+
1366
+ With login (permanent, device enrolled):
1367
+ akash serve register this device permanently
1368
+ akash serve --daemon run as background daemon
1369
+ akash serve --label myphone use a custom label
1370
+ akash serve --status check daemon status
1371
+ akash serve --stop stop daemon
1313
1372
  """
1314
1373
  from .relay import serve as _serve, serve_status, serve_stop
1315
1374
 
1375
+ # ── Guest / no-login mode ─────────────────────────────────────────────
1376
+ if share:
1377
+ from .relay import guest_serve
1378
+ _, base = _resolve_client(url)
1379
+ try:
1380
+ guest_serve(base)
1381
+ except KeyboardInterrupt:
1382
+ pass
1383
+ return
1384
+
1316
1385
  if status:
1317
1386
  st = serve_status()
1318
1387
  if st["running"]:
@@ -1320,7 +1389,8 @@ def serve(
1320
1389
  _ok(f"running {_dim('pid ' + pid_str)}")
1321
1390
  _info(f"logs {st['log']}")
1322
1391
  else:
1323
- _info("not running — start with: akash serve --daemon")
1392
+ _info("not running")
1393
+ _info(f"start with: {_cy('akash serve')} or {_cy('akash serve --daemon')}")
1324
1394
  return
1325
1395
 
1326
1396
  if stop:
@@ -1331,13 +1401,23 @@ def serve(
1331
1401
  return
1332
1402
 
1333
1403
  try:
1334
- api_key, base = _resolve_client(url, require_key=True)
1404
+ api_key, base = _resolve_client(url, require_key=False)
1335
1405
  except typer.Exit:
1336
1406
  raise
1337
1407
  except Exception as e:
1338
1408
  _err(str(e))
1339
1409
  raise typer.Exit(1)
1340
1410
 
1411
+ if not api_key:
1412
+ _p()
1413
+ _warn("not logged in.")
1414
+ _p()
1415
+ _info(f"For a quick one-time session (no login): {_cy('akash serve --share')}")
1416
+ _p()
1417
+ _info(f"To set up permanently: {_cy('akash login --label myphone')} then {_cy('akash serve')}")
1418
+ _p()
1419
+ raise typer.Exit(1)
1420
+
1341
1421
  # Resolve label: flag > config > "default"
1342
1422
  from .exec import _device_label as _cfg_label
1343
1423
  resolved_label = (label or _cfg_label() or "default").strip()
@@ -535,3 +535,195 @@ def serve_stop() -> bool:
535
535
  return True
536
536
  except Exception:
537
537
  return False
538
+
539
+
540
+ # ── Guest relay (no API key — pairing code) ───────────────────────────────────
541
+
542
+ def _guest_code_url(base: str) -> str:
543
+ return f"{base.rstrip('/')}/api/guest/relay/code"
544
+
545
+ def _guest_host_url(base: str, code: str) -> str:
546
+ return f"{_ws_base(base)}/api/guest/relay/host?code={code}"
547
+
548
+ def _guest_join_url(base: str, code: str) -> str:
549
+ return f"{_ws_base(base)}/api/guest/relay/join?code={code}"
550
+
551
+
552
+ def guest_serve(base_url: str) -> None:
553
+ """
554
+ Host a shell session with no API key.
555
+ Requests a pairing code from the server, prints it, then waits for
556
+ someone to connect with: akash shell --code <CODE>
557
+ """
558
+ import httpx
559
+ import websockets.sync.client as _ws
560
+
561
+ # 1. Get a pairing code
562
+ try:
563
+ r = httpx.post(_guest_code_url(base_url), timeout=10)
564
+ r.raise_for_status()
565
+ code = r.json()["code"]
566
+ ttl = r.json().get("expires_in", 600)
567
+ except Exception as e:
568
+ print(f" {_R}✗{_RS} could not get pairing code: {e}", file=sys.stderr)
569
+ return
570
+
571
+ print()
572
+ print(f" {_G}◉{_RS} guest relay ready")
573
+ print()
574
+ print(f" Your pairing code: {_C}{_Y}{code}{_RS}")
575
+ print()
576
+ print(f" {_DM}On another machine, run:{_RS}")
577
+ print(f" {_C}akash shell --code {code}{_RS}")
578
+ print()
579
+ print(f" {_DM}Code expires in {ttl // 60} min · Ctrl-C to stop{_RS}")
580
+ print()
581
+
582
+ # 2. Connect to host endpoint and serve shell frames
583
+ while True:
584
+ try:
585
+ ws_url = _guest_host_url(base_url, code)
586
+ with _ws.connect(ws_url, open_timeout=15, close_timeout=5) as ws:
587
+ ack = json.loads(ws.recv())
588
+ if ack.get("type") == "error":
589
+ print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
590
+ return
591
+ print(f" {_DM}waiting for connection…{_RS}")
592
+ _run_relay_device(ws, f"guest:{code}")
593
+ print(f"\n {_DM}session ended — waiting for next connection…{_RS}\n")
594
+ except KeyboardInterrupt:
595
+ print(f"\n {_DM}guest serve stopped{_RS}")
596
+ break
597
+ except Exception as exc:
598
+ print(f" {_Y}!{_RS} relay disconnected ({exc}) — reconnecting…")
599
+ time.sleep(3)
600
+
601
+
602
+ def guest_connect_shell(base_url: str, code: str) -> int:
603
+ """
604
+ Connect to a guest host by pairing code — no API key needed.
605
+ Returns the remote exit code.
606
+ """
607
+ import websockets.sync.client as _ws
608
+ import select as _sel, signal
609
+
610
+ code = code.strip().upper()
611
+ ws_url = _guest_join_url(base_url, code)
612
+
613
+ from .exec import _get_terminal_size
614
+ rows, cols = _get_terminal_size()
615
+
616
+ _raw_ok = False
617
+ _old_settings = None
618
+ _stdin_fd = -1
619
+ try:
620
+ import tty, termios as _term
621
+ _stdin_fd = sys.stdin.fileno()
622
+ _old_settings = _term.tcgetattr(_stdin_fd)
623
+ tty.setraw(_stdin_fd)
624
+ _raw_ok = True
625
+ except Exception:
626
+ pass
627
+
628
+ def _restore():
629
+ if _raw_ok and _old_settings is not None:
630
+ try:
631
+ import termios as _t
632
+ _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
633
+ except Exception:
634
+ pass
635
+
636
+ exit_code = 1
637
+ try:
638
+ with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
639
+ ack = json.loads(ws.recv())
640
+ if ack.get("type") == "error":
641
+ _restore()
642
+ raise RuntimeError(ack.get("message", "relay error"))
643
+
644
+ ws.send(json.dumps({
645
+ "type": "pty_run",
646
+ "command": os.environ.get("SHELL", "/bin/bash"),
647
+ "rows": rows, "cols": cols,
648
+ }))
649
+
650
+ stop = threading.Event()
651
+
652
+ def _stdin_pump():
653
+ try:
654
+ while not stop.is_set():
655
+ if _stdin_fd >= 0:
656
+ r = _sel.select([_stdin_fd], [], [], 0.05)[0]
657
+ if not r:
658
+ continue
659
+ chunk = os.read(_stdin_fd, 256)
660
+ else:
661
+ chunk = sys.stdin.buffer.read(1)
662
+ if not chunk:
663
+ break
664
+ ws.send(json.dumps({
665
+ "type": "pty_in",
666
+ "data": base64.b64encode(chunk).decode(),
667
+ }))
668
+ except Exception:
669
+ pass
670
+
671
+ def _on_resize(signum, _frame):
672
+ r, c = _get_terminal_size()
673
+ try:
674
+ ws.send(json.dumps({"type": "resize", "rows": r, "cols": c}))
675
+ except Exception:
676
+ pass
677
+ try:
678
+ signal.signal(signal.SIGWINCH, _on_resize)
679
+ except Exception:
680
+ pass
681
+
682
+ t = threading.Thread(target=_stdin_pump, daemon=True)
683
+ t.start()
684
+
685
+ while True:
686
+ try:
687
+ raw = ws.recv()
688
+ except Exception:
689
+ break
690
+ try:
691
+ frame = json.loads(raw)
692
+ except Exception:
693
+ continue
694
+ ftype = frame.get("type")
695
+ if ftype == "pty_out":
696
+ data = base64.b64decode(frame["data"])
697
+ if data:
698
+ sys.stdout.buffer.write(data)
699
+ sys.stdout.buffer.flush()
700
+ elif ftype == "exit":
701
+ exit_code = frame.get("code", 0)
702
+ stop.set()
703
+ break
704
+ elif ftype == "error":
705
+ stop.set()
706
+ _restore()
707
+ print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}", file=sys.stderr)
708
+ exit_code = 1
709
+ break
710
+ elif ftype == "disconnected":
711
+ stop.set()
712
+ break
713
+ elif ftype == "ping":
714
+ try:
715
+ ws.send(json.dumps({"type": "pong"}))
716
+ except Exception:
717
+ break
718
+
719
+ except KeyboardInterrupt:
720
+ pass
721
+ finally:
722
+ stop.set() if 'stop' in dir() else None
723
+ _restore()
724
+ try:
725
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
726
+ except Exception:
727
+ pass
728
+
729
+ return exit_code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.4.0
3
+ Version: 4.5.0
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.4.0"
3
+ version = "4.5.0"
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