akashcli 4.5.9__tar.gz → 4.6.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 (32) hide show
  1. {akashcli-4.5.9 → akashcli-4.6.0}/PKG-INFO +1 -1
  2. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/relay.py +120 -188
  3. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.5.9 → akashcli-4.6.0}/pyproject.toml +1 -1
  5. {akashcli-4.5.9 → akashcli-4.6.0}/README.md +0 -0
  6. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/__init__.py +0 -0
  7. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/_net.py +0 -0
  8. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/auth.py +0 -0
  9. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/cli.py +0 -0
  10. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/client.py +0 -0
  11. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/config.py +0 -0
  12. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/crypto.py +0 -0
  13. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/env_manager.py +0 -0
  14. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/exceptions.py +0 -0
  15. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/exec.py +0 -0
  16. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/integrity.py +0 -0
  17. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/logger.py +0 -0
  18. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/models.py +0 -0
  19. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/p2p.py +0 -0
  20. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/packages.py +0 -0
  21. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/restore.py +0 -0
  22. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/storage.py +0 -0
  24. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/terminal.py +0 -0
  25. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/uploader.py +0 -0
  26. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli/utils.py +0 -0
  27. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/SOURCES.txt +0 -0
  28. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/dependency_links.txt +0 -0
  29. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/entry_points.txt +0 -0
  30. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/requires.txt +0 -0
  31. {akashcli-4.5.9 → akashcli-4.6.0}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.5.9 → akashcli-4.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.9
3
+ Version: 4.6.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
@@ -805,19 +805,19 @@ def _sh_history(history: list) -> None:
805
805
  def guest_connect_shell(base_url: str, code: str) -> int:
806
806
  """
807
807
  Connect to a guest host via pairing code.
808
- Uses a Python REPL loop with DevOS-style UI — no raw-mode PTY needed on
809
- the client side. The remote side runs a persistent bash that we drive with
810
- `run` frames; output is shown with inset │ borders.
811
- Returns the last remote exit code.
808
+ Opens a real PTY tunnel to the host's shell — identical to connect_shell()
809
+ but uses the guest relay (no API key). Full interactive terminal: nano,
810
+ vim, htop, etc. all work. DevOS banner is printed before entering raw mode.
811
+ Returns the remote exit code.
812
812
  """
813
- import readline as _rl
814
- import queue as _q
813
+ import select as _sel
814
+ import signal
815
815
 
816
816
  code = code.strip().upper()
817
817
  ws_url = _guest_join_url(base_url, code)
818
818
 
819
- # ── Connect & handle approval ─────────────────────────────────────────────
820
- print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}")
819
+ # ── Connect & handle approval (before entering raw mode) ─────────────────
820
+ print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}", flush=True)
821
821
  try:
822
822
  ws = _ws_connect(ws_url, open_timeout=15, close_timeout=10)
823
823
  except Exception as e:
@@ -832,10 +832,10 @@ def guest_connect_shell(base_url: str, code: str) -> int:
832
832
 
833
833
  if ftype == "waiting_approval":
834
834
  fp = first.get("fingerprint", "")
835
- print(f" {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}")
835
+ print(f" {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}", flush=True)
836
836
  if fp:
837
- print(f" {_DM}fingerprint {_C}{fp}{_SH_RS}")
838
- print(f" {_DM}host has 30 s to respond{_RS}\n")
837
+ print(f" {_DM}fingerprint {_C}{fp}{_RS}", flush=True)
838
+ print(f" {_DM}host has 30 s to respond{_RS}\n", flush=True)
839
839
  try:
840
840
  second = json.loads(ws.recv(timeout=35))
841
841
  except Exception:
@@ -851,213 +851,145 @@ def guest_connect_shell(base_url: str, code: str) -> int:
851
851
  ws.close()
852
852
  raise RuntimeError(first.get("message", "unexpected server response"))
853
853
 
854
- # ── Session state ─────────────────────────────────────────────────────────
855
- cwd = os.path.expanduser("~")
856
- history : list[str] = []
857
- exit_code = 0
858
- identity = os.environ.get("USER") or os.environ.get("LOGNAME") or "akash"
854
+ # ── Print DevOS banner (before raw mode) ──────────────────────────────────
855
+ identity = os.environ.get("USER") or os.environ.get("LOGNAME") or "akash"
856
+ print(f"\n {_G}◉{_RS} {_C}akash shell{_RS} "
857
+ f"{_DM}code {code} · Ctrl-D or `exit` to quit{_RS}\n", flush=True)
859
858
 
860
- # readline history setup
861
- hist_file = _AKASH_DIR / "shell_history"
862
- _AKASH_DIR.mkdir(parents=True, exist_ok=True)
859
+ # ── Get terminal size ─────────────────────────────────────────────────────
863
860
  try:
864
- _rl.read_history_file(str(hist_file))
861
+ import shutil as _sh
862
+ cols, rows = _sh.get_terminal_size(fallback=(80, 24))
865
863
  except Exception:
866
- pass
867
- _rl.set_history_length(500)
864
+ rows, cols = 24, 80
868
865
 
869
- # ── Output queue — background thread feeds frames ─────────────────────────
870
- q: _q.Queue = _q.Queue()
871
- stop_recv = threading.Event()
866
+ # ── Enter raw mode ────────────────────────────────────────────────────────
867
+ _raw_ok = False
868
+ _old_settings = None
869
+ _stdin_fd = -1
870
+ try:
871
+ import tty as _tty, termios as _term
872
+ _stdin_fd = sys.stdin.fileno()
873
+ _old_settings = _term.tcgetattr(_stdin_fd)
874
+ _tty.setraw(_stdin_fd)
875
+ _raw_ok = True
876
+ except Exception:
877
+ pass
872
878
 
873
- def _recv_loop():
874
- while not stop_recv.is_set():
879
+ def _restore():
880
+ if _raw_ok and _old_settings is not None:
875
881
  try:
876
- raw = ws.recv(timeout=0.1)
877
- q.put(raw)
882
+ import termios as _t
883
+ _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
878
884
  except Exception:
879
- if not stop_recv.is_set():
880
- q.put(None) # signals disconnection
881
- break
885
+ pass
886
+
887
+ exit_code = 0
888
+ stop = threading.Event()
889
+
890
+ # ── Tell host to open a PTY shell ─────────────────────────────────────────
891
+ try:
892
+ ws.send(json.dumps({
893
+ "type": "pty_run",
894
+ "command": os.environ.get("SHELL", "/bin/bash"),
895
+ "rows": rows,
896
+ "cols": cols,
897
+ }))
898
+ except Exception as e:
899
+ _restore()
900
+ ws.close()
901
+ raise RuntimeError(f"Failed to start remote shell: {e}")
902
+
903
+ # ── Thread: local stdin → WS ──────────────────────────────────────────────
904
+ def _stdin_pump():
905
+ try:
906
+ while not stop.is_set():
907
+ if _stdin_fd >= 0:
908
+ r = _sel.select([_stdin_fd], [], [], 0.02)[0]
909
+ if not r:
910
+ continue
911
+ chunk = os.read(_stdin_fd, 4096)
912
+ else:
913
+ chunk = sys.stdin.buffer.read(1)
914
+ if not chunk:
915
+ break
916
+ ws.send(json.dumps({
917
+ "type": "pty_in",
918
+ "data": base64.b64encode(chunk).decode(),
919
+ }))
920
+ except Exception:
921
+ pass
922
+
923
+ # ── SIGWINCH → resize frame ───────────────────────────────────────────────
924
+ def _on_resize(signum, _frame):
925
+ try:
926
+ import shutil as _sh2
927
+ c2, r2 = _sh2.get_terminal_size(fallback=(80, 24))
928
+ ws.send(json.dumps({"type": "resize", "rows": r2, "cols": c2}))
929
+ except Exception:
930
+ pass
931
+ try:
932
+ signal.signal(signal.SIGWINCH, _on_resize)
933
+ except Exception:
934
+ pass
882
935
 
883
- recv_thread = threading.Thread(target=_recv_loop, daemon=True)
884
- recv_thread.start()
936
+ t = threading.Thread(target=_stdin_pump, daemon=True)
937
+ t.start()
885
938
 
886
- def _flush_queue(timeout: float = 30.0) -> tuple[list[str], int, str]:
887
- """Collect stdout/stderr until exit frame. Returns (lines, code, new_cwd)."""
888
- lines: list[str] = []
889
- ec = 0
890
- new_cwd = cwd
891
- deadline = time.time() + timeout
892
- while time.time() < deadline:
939
+ # ── Main loop: WS → local stdout (raw PTY output) ────────────────────────
940
+ try:
941
+ while True:
893
942
  try:
894
- raw = q.get(timeout=0.05)
895
- except _q.Empty:
896
- continue
897
- if raw is None:
943
+ raw = ws.recv()
944
+ except Exception:
898
945
  break
899
946
  try:
900
947
  frame = json.loads(raw)
901
948
  except Exception:
902
949
  continue
903
- ft = frame.get("type")
904
- if ft in ("stdout", "pty_out"):
905
- chunk = base64.b64decode(frame.get("data", ""))
906
- # Strip ANSI, split into lines for inset display
907
- text = chunk.decode("utf-8", errors="replace")
908
- # Remove ANSI escape codes for clean inset display
909
- import re as _re
910
- text = _re.sub(r'\x1b\[[0-9;]*[mGKHF]', '', text)
911
- text = _re.sub(r'\x1b\][^\x07]*\x07', '', text)
912
- for ln in text.splitlines():
913
- if ln.strip():
914
- lines.append(ln)
915
- elif ft == "exit":
916
- ec = frame.get("code", 0)
917
- new_cwd = frame.get("cwd", cwd)
950
+ ftype = frame.get("type")
951
+ if ftype == "pty_out":
952
+ data = base64.b64decode(frame["data"])
953
+ if data:
954
+ sys.stdout.buffer.write(data)
955
+ sys.stdout.buffer.flush()
956
+ elif ftype == "exit":
957
+ exit_code = frame.get("code", 0)
958
+ stop.set()
959
+ break
960
+ elif ftype == "error":
961
+ stop.set()
962
+ _restore()
963
+ print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}\r\n",
964
+ file=sys.stderr, flush=True)
965
+ exit_code = 1
918
966
  break
919
- elif ft == "error":
920
- lines.append(f"{_R}error:{_RS} {frame.get('message','?')}")
921
- ec = 1
967
+ elif ftype == "disconnected":
968
+ stop.set()
922
969
  break
923
- elif ft == "ping":
970
+ elif ftype == "ping":
924
971
  try:
925
972
  ws.send(json.dumps({"type": "pong"}))
926
973
  except Exception:
927
- pass
928
- return lines, ec, new_cwd
929
-
930
- # ── Session banner ────────────────────────────────────────────────────────
931
- print()
932
- print(f" {_G}◉{_RS} {_C}akash shell{_RS} {_DM}code {code} · :help for commands · :exit to quit{_RS}")
933
- print()
934
-
935
- # ── REPL loop ─────────────────────────────────────────────────────────────
936
- last_cmd = ""
937
- try:
938
- while True:
939
- _sh_draw_header(identity, cwd)
940
- try:
941
- cmd = input(_sh_input_prompt()).strip()
942
- except (EOFError, KeyboardInterrupt):
943
- print()
944
- break
945
-
946
- if not cmd:
947
- continue
948
-
949
- # ── Built-in commands ─────────────────────────────────────────────
950
- if cmd == ":exit" or cmd == "exit":
951
- break
952
- if cmd == ":clear" or cmd == "clear":
953
- print("\033[2J\033[H", end="", flush=True)
954
- continue
955
- if cmd == ":help":
956
- _sh_help()
957
- continue
958
- if cmd == ":h":
959
- _sh_history(history)
960
- continue
961
- if cmd == ":status":
962
- _sh_status(code, cwd, history)
963
- continue
964
- if cmd == ":r":
965
- if not last_cmd:
966
- print(f" {_Y}!{_RS} no previous command\n")
967
- continue
968
- cmd = last_cmd
969
- print(f" {_DM}re-running:{_RS} {cmd}")
970
- if cmd.startswith(":upload "):
971
- fpath = cmd[8:].strip()
972
- _p = print
973
- _p(f" {_DM}uploading {fpath}…{_RS}")
974
- try:
975
- from .client import AkashClient
976
- import os as _os
977
- cfg = __import__("akashcli.config", fromlist=["ConfigManager"]).ConfigManager
978
- _cfg = cfg.load()
979
- api_key = _os.getenv("AKASH_API_KEY") or _cfg.get("api_key")
980
- if not api_key:
981
- print(f" {_R}✗{_RS} no API key — run akash login first\n")
982
- continue
983
- client = AkashClient(api_key=api_key)
984
- result = client.upload_file(fpath)
985
- print(f" {_G}✓{_RS} uploaded {_C}{result.file_code}{_RS} {_DM}{result.url}{_RS}\n")
986
- except Exception as ue:
987
- print(f" {_R}✗{_RS} upload failed: {ue}\n")
988
- continue
989
-
990
- # ── Remote command ────────────────────────────────────────────────
991
- history.append(cmd)
992
- last_cmd = cmd
993
- try:
994
- _rl.add_history(cmd)
995
- except Exception:
996
- pass
997
-
998
- t0 = time.monotonic()
999
- # Send as `run` — host device executes via bash and streams stdout back.
1000
- # We always prefix with `cd <cwd>` so state persists across commands.
1001
- # For `cd` commands we also capture $PWD to update our local cwd tracker.
1002
- if cmd.startswith("cd ") or cmd == "cd":
1003
- # Run cd inside current cwd, then print new $PWD as sentinel
1004
- actual_cmd = (
1005
- f'cd {json.dumps(cwd)} 2>/dev/null; '
1006
- f'{cmd}; '
1007
- f'echo "__AKASH_CWD__:$PWD"'
1008
- )
1009
- else:
1010
- actual_cmd = (
1011
- f'cd {json.dumps(cwd)} 2>/dev/null && '
1012
- f'{{ {cmd}; }}; '
1013
- f'echo "__AKASH_EXIT__:$?"'
1014
- )
1015
-
1016
- try:
1017
- ws.send(json.dumps({"type": "run", "command": actual_cmd}))
1018
- except Exception:
1019
- print(f"\n {_R}✗{_RS} disconnected\n")
1020
- break
1021
-
1022
- lines, ec, _ = _flush_queue()
1023
- elapsed_ms = (time.monotonic() - t0) * 1000
1024
-
1025
- # Parse cwd tracking lines out of output
1026
- clean_lines = []
1027
- for ln in lines:
1028
- if ln.startswith("__AKASH_CWD__:"):
1029
- cwd = ln[len("__AKASH_CWD__:"):].strip() or cwd
1030
- elif ln.startswith("__AKASH_EXIT__:"):
1031
- try:
1032
- ec = int(ln[len("__AKASH_EXIT__:"):].strip())
1033
- except Exception:
1034
- pass
1035
- else:
1036
- clean_lines.append(ln)
1037
-
1038
- exit_code = ec
1039
-
1040
- if clean_lines:
1041
- _sh_inset(clean_lines)
1042
- else:
1043
- print()
1044
- _sh_footer(ec, elapsed_ms)
1045
-
1046
- except Exception:
974
+ break
975
+ except KeyboardInterrupt:
1047
976
  pass
1048
977
  finally:
1049
- stop_recv.set()
978
+ stop.set()
979
+ _restore()
1050
980
  try:
1051
- ws.send(json.dumps({"type": "close"}))
981
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
1052
982
  except Exception:
1053
983
  pass
1054
984
  try:
1055
- ws.close()
985
+ ws.send(json.dumps({"type": "close"}))
1056
986
  except Exception:
1057
987
  pass
1058
988
  try:
1059
- _rl.write_history_file(str(hist_file))
989
+ ws.close()
1060
990
  except Exception:
1061
991
  pass
1062
992
 
993
+ # Print a clean newline after raw mode ends
994
+ print(f"\r\n {_DM}session ended code {exit_code}{_RS}\r\n", flush=True)
1063
995
  return exit_code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.9
3
+ Version: 4.6.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.5.9"
3
+ version = "4.6.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
File without changes
File without changes