akashcli 4.4.1__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.1 → akashcli-4.5.0}/PKG-INFO +1 -1
  2. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/cli.py +54 -24
  3. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/relay.py +192 -0
  4. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/PKG-INFO +1 -1
  5. {akashcli-4.4.1 → akashcli-4.5.0}/pyproject.toml +1 -1
  6. {akashcli-4.4.1 → akashcli-4.5.0}/README.md +0 -0
  7. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/__init__.py +0 -0
  8. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/auth.py +0 -0
  9. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/client.py +0 -0
  10. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/config.py +0 -0
  11. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/crypto.py +0 -0
  12. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/exec.py +0 -0
  15. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/integrity.py +0 -0
  16. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/logger.py +0 -0
  17. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/models.py +0 -0
  18. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/p2p.py +0 -0
  19. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/packages.py +0 -0
  20. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/restore.py +0 -0
  21. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/storage.py +0 -0
  23. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/terminal.py +0 -0
  24. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/uploader.py +0 -0
  25. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli/utils.py +0 -0
  26. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.4.1 → akashcli-4.5.0}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.4.1 → 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.1
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}",
@@ -1142,21 +1144,20 @@ def shell(
1142
1144
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
1143
1145
  device: Optional[str] = typer.Option(None, "--device", "-d",
1144
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"),
1145
1149
  ):
1146
- """Open a shell — local, server, or on any enrolled device.
1150
+ """Open a shell — local, on a device, or via a one-time pairing code.
1147
1151
 
1148
1152
  No login needed:
1149
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
1150
1156
 
1151
- With login (akash login):
1152
- akash shell full server shell (persistent, coloured)
1153
- akash shell --device termux relay shell on your phone
1154
- akash shell --device laptop relay shell on your laptop
1155
- akash shell --device auto-pick (works if only 1 device enrolled)
1156
-
1157
- To make a device reachable:
1158
- 1. On that device: akash login --label termux && akash serve
1159
- 2. From anywhere: akash shell --device termux
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
1160
1161
  """
1161
1162
  from .exec import acquire_exec_token, run_shell, _clear_cached_token
1162
1163
  import shutil as _sh, subprocess as _sp
@@ -1164,13 +1165,30 @@ def shell(
1164
1165
  try:
1165
1166
  api_key, base = _resolve_client(url)
1166
1167
 
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
+
1167
1185
  # ── --device flag but no key ──────────────────────────────────────
1168
1186
  if device is not None and not api_key:
1169
1187
  _warn("relay requires an API key.")
1170
1188
  _p()
1171
1189
  _info(f"To connect to {_cy(device.strip() or 'a device')} you need to log in first:")
1172
1190
  _info(f" {_cy('akash login')} save your API key on this machine")
1173
- _info(f" {_cy('akash serve')} run on the target device to make it reachable")
1191
+ _info(f" Or use a pairing code: {_cy('akash serve')} → {_cy('akash shell --code <CODE>')}")
1174
1192
  _p()
1175
1193
  raise typer.Exit(1)
1176
1194
 
@@ -1337,21 +1355,33 @@ def serve(
1337
1355
  daemon: bool = typer.Option(False, "--daemon", help="Run in background"),
1338
1356
  status: bool = typer.Option(False, "--status", help="Show serve daemon status"),
1339
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"),
1340
1359
  ):
1341
- """Make this device reachable for remote shell and run.
1360
+ """Make this device reachable for remote shell — with or without login.
1342
1361
 
1343
- Run this once on a device to register it. Then from any other
1344
- machine: akash shell --device NAME or akash run --device NAME
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
1345
1365
 
1346
- Examples:
1347
- akash serve start in foreground (Ctrl-C to stop)
1366
+ With login (permanent, device enrolled):
1367
+ akash serve register this device permanently
1348
1368
  akash serve --daemon run as background daemon
1349
- akash serve --status check if daemon is running
1350
- akash serve --stop stop the daemon
1351
1369
  akash serve --label myphone use a custom label
1370
+ akash serve --status check daemon status
1371
+ akash serve --stop stop daemon
1352
1372
  """
1353
1373
  from .relay import serve as _serve, serve_status, serve_stop
1354
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
+
1355
1385
  if status:
1356
1386
  st = serve_status()
1357
1387
  if st["running"]:
@@ -1380,11 +1410,11 @@ def serve(
1380
1410
 
1381
1411
  if not api_key:
1382
1412
  _p()
1383
- _warn("not logged in — can't register with relay server.")
1413
+ _warn("not logged in.")
1414
+ _p()
1415
+ _info(f"For a quick one-time session (no login): {_cy('akash serve --share')}")
1384
1416
  _p()
1385
- _info("Run this first:")
1386
- print(f" {_cy('akash login --label myphone')} {_DM}# name this device{_RS}")
1387
- print(f" {_cy('akash serve')} {_DM}# then run this{_RS}")
1417
+ _info(f"To set up permanently: {_cy('akash login --label myphone')} then {_cy('akash serve')}")
1388
1418
  _p()
1389
1419
  raise typer.Exit(1)
1390
1420
 
@@ -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.1
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.1"
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