akashcli 4.5.9__tar.gz → 4.6.1__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.1}/PKG-INFO +1 -1
  2. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/relay.py +127 -193
  3. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.5.9 → akashcli-4.6.1}/pyproject.toml +1 -1
  5. {akashcli-4.5.9 → akashcli-4.6.1}/README.md +0 -0
  6. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/__init__.py +0 -0
  7. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/_net.py +0 -0
  8. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/auth.py +0 -0
  9. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/cli.py +0 -0
  10. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/client.py +0 -0
  11. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/config.py +0 -0
  12. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/crypto.py +0 -0
  13. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/env_manager.py +0 -0
  14. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/exceptions.py +0 -0
  15. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/exec.py +0 -0
  16. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/integrity.py +0 -0
  17. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/logger.py +0 -0
  18. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/models.py +0 -0
  19. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/p2p.py +0 -0
  20. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/packages.py +0 -0
  21. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/restore.py +0 -0
  22. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/storage.py +0 -0
  24. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/terminal.py +0 -0
  25. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/uploader.py +0 -0
  26. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli/utils.py +0 -0
  27. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/SOURCES.txt +0 -0
  28. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/dependency_links.txt +0 -0
  29. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/entry_points.txt +0 -0
  30. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/requires.txt +0 -0
  31. {akashcli-4.5.9 → akashcli-4.6.1}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.5.9 → akashcli-4.6.1}/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.1
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
@@ -210,10 +210,9 @@ def _handle_pty(ws, frame: dict) -> None:
210
210
  pass
211
211
 
212
212
  def _child_setup():
213
- """Run inside the child process before exec — creates a new session
214
- and attaches the slave PTY as the controlling terminal so bash gets
215
- full job control (no 'cannot set terminal process group' errors)."""
216
- os.setsid()
213
+ """Run inside the child process before exec.
214
+ start_new_session=True already calls setsid(); we just need to claim
215
+ the slave PTY as the controlling terminal via TIOCSCTTY."""
217
216
  try:
218
217
  fcntl.ioctl(0, termios.TIOCSCTTY, 0)
219
218
  except Exception:
@@ -222,12 +221,15 @@ def _handle_pty(ws, frame: dict) -> None:
222
221
  import subprocess as _sp
223
222
  shell = command or os.environ.get("SHELL", "/bin/bash")
224
223
 
225
- # Run the shell directly so it gets a proper controlling terminal.
224
+ # Use start_new_session=True so the child is a proper session leader
225
+ # (equivalent to setsid()), then TIOCSCTTY in preexec_fn attaches the
226
+ # slave PTY as the controlling terminal → bash gets full job control.
226
227
  try:
227
228
  proc = _sp.Popen(
228
229
  [shell],
229
230
  stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
230
231
  close_fds=True,
232
+ start_new_session=True,
231
233
  preexec_fn=_child_setup,
232
234
  cwd=os.path.expanduser("~"),
233
235
  env={**os.environ,
@@ -805,19 +807,19 @@ def _sh_history(history: list) -> None:
805
807
  def guest_connect_shell(base_url: str, code: str) -> int:
806
808
  """
807
809
  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.
810
+ Opens a real PTY tunnel to the host's shell — identical to connect_shell()
811
+ but uses the guest relay (no API key). Full interactive terminal: nano,
812
+ vim, htop, etc. all work. DevOS banner is printed before entering raw mode.
813
+ Returns the remote exit code.
812
814
  """
813
- import readline as _rl
814
- import queue as _q
815
+ import select as _sel
816
+ import signal
815
817
 
816
818
  code = code.strip().upper()
817
819
  ws_url = _guest_join_url(base_url, code)
818
820
 
819
- # ── Connect & handle approval ─────────────────────────────────────────────
820
- print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}")
821
+ # ── Connect & handle approval (before entering raw mode) ─────────────────
822
+ print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}", flush=True)
821
823
  try:
822
824
  ws = _ws_connect(ws_url, open_timeout=15, close_timeout=10)
823
825
  except Exception as e:
@@ -832,10 +834,10 @@ def guest_connect_shell(base_url: str, code: str) -> int:
832
834
 
833
835
  if ftype == "waiting_approval":
834
836
  fp = first.get("fingerprint", "")
835
- print(f" {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}")
837
+ print(f" {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}", flush=True)
836
838
  if fp:
837
- print(f" {_DM}fingerprint {_C}{fp}{_SH_RS}")
838
- print(f" {_DM}host has 30 s to respond{_RS}\n")
839
+ print(f" {_DM}fingerprint {_C}{fp}{_RS}", flush=True)
840
+ print(f" {_DM}host has 30 s to respond{_RS}\n", flush=True)
839
841
  try:
840
842
  second = json.loads(ws.recv(timeout=35))
841
843
  except Exception:
@@ -851,213 +853,145 @@ def guest_connect_shell(base_url: str, code: str) -> int:
851
853
  ws.close()
852
854
  raise RuntimeError(first.get("message", "unexpected server response"))
853
855
 
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"
856
+ # ── Print DevOS banner (before raw mode) ──────────────────────────────────
857
+ identity = os.environ.get("USER") or os.environ.get("LOGNAME") or "akash"
858
+ print(f"\n {_G}◉{_RS} {_C}akash shell{_RS} "
859
+ f"{_DM}code {code} · Ctrl-D or `exit` to quit{_RS}\n", flush=True)
859
860
 
860
- # readline history setup
861
- hist_file = _AKASH_DIR / "shell_history"
862
- _AKASH_DIR.mkdir(parents=True, exist_ok=True)
861
+ # ── Get terminal size ─────────────────────────────────────────────────────
863
862
  try:
864
- _rl.read_history_file(str(hist_file))
863
+ import shutil as _sh
864
+ cols, rows = _sh.get_terminal_size(fallback=(80, 24))
865
865
  except Exception:
866
- pass
867
- _rl.set_history_length(500)
866
+ rows, cols = 24, 80
868
867
 
869
- # ── Output queue — background thread feeds frames ─────────────────────────
870
- q: _q.Queue = _q.Queue()
871
- stop_recv = threading.Event()
868
+ # ── Enter raw mode ────────────────────────────────────────────────────────
869
+ _raw_ok = False
870
+ _old_settings = None
871
+ _stdin_fd = -1
872
+ try:
873
+ import tty as _tty, termios as _term
874
+ _stdin_fd = sys.stdin.fileno()
875
+ _old_settings = _term.tcgetattr(_stdin_fd)
876
+ _tty.setraw(_stdin_fd)
877
+ _raw_ok = True
878
+ except Exception:
879
+ pass
872
880
 
873
- def _recv_loop():
874
- while not stop_recv.is_set():
881
+ def _restore():
882
+ if _raw_ok and _old_settings is not None:
875
883
  try:
876
- raw = ws.recv(timeout=0.1)
877
- q.put(raw)
884
+ import termios as _t
885
+ _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
878
886
  except Exception:
879
- if not stop_recv.is_set():
880
- q.put(None) # signals disconnection
881
- break
887
+ pass
888
+
889
+ exit_code = 0
890
+ stop = threading.Event()
891
+
892
+ # ── Tell host to open a PTY shell ─────────────────────────────────────────
893
+ try:
894
+ ws.send(json.dumps({
895
+ "type": "pty_run",
896
+ "command": os.environ.get("SHELL", "/bin/bash"),
897
+ "rows": rows,
898
+ "cols": cols,
899
+ }))
900
+ except Exception as e:
901
+ _restore()
902
+ ws.close()
903
+ raise RuntimeError(f"Failed to start remote shell: {e}")
904
+
905
+ # ── Thread: local stdin → WS ──────────────────────────────────────────────
906
+ def _stdin_pump():
907
+ try:
908
+ while not stop.is_set():
909
+ if _stdin_fd >= 0:
910
+ r = _sel.select([_stdin_fd], [], [], 0.02)[0]
911
+ if not r:
912
+ continue
913
+ chunk = os.read(_stdin_fd, 4096)
914
+ else:
915
+ chunk = sys.stdin.buffer.read(1)
916
+ if not chunk:
917
+ break
918
+ ws.send(json.dumps({
919
+ "type": "pty_in",
920
+ "data": base64.b64encode(chunk).decode(),
921
+ }))
922
+ except Exception:
923
+ pass
924
+
925
+ # ── SIGWINCH → resize frame ───────────────────────────────────────────────
926
+ def _on_resize(signum, _frame):
927
+ try:
928
+ import shutil as _sh2
929
+ c2, r2 = _sh2.get_terminal_size(fallback=(80, 24))
930
+ ws.send(json.dumps({"type": "resize", "rows": r2, "cols": c2}))
931
+ except Exception:
932
+ pass
933
+ try:
934
+ signal.signal(signal.SIGWINCH, _on_resize)
935
+ except Exception:
936
+ pass
882
937
 
883
- recv_thread = threading.Thread(target=_recv_loop, daemon=True)
884
- recv_thread.start()
938
+ t = threading.Thread(target=_stdin_pump, daemon=True)
939
+ t.start()
885
940
 
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:
941
+ # ── Main loop: WS → local stdout (raw PTY output) ────────────────────────
942
+ try:
943
+ while True:
893
944
  try:
894
- raw = q.get(timeout=0.05)
895
- except _q.Empty:
896
- continue
897
- if raw is None:
945
+ raw = ws.recv()
946
+ except Exception:
898
947
  break
899
948
  try:
900
949
  frame = json.loads(raw)
901
950
  except Exception:
902
951
  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)
952
+ ftype = frame.get("type")
953
+ if ftype == "pty_out":
954
+ data = base64.b64decode(frame["data"])
955
+ if data:
956
+ sys.stdout.buffer.write(data)
957
+ sys.stdout.buffer.flush()
958
+ elif ftype == "exit":
959
+ exit_code = frame.get("code", 0)
960
+ stop.set()
961
+ break
962
+ elif ftype == "error":
963
+ stop.set()
964
+ _restore()
965
+ print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}\r\n",
966
+ file=sys.stderr, flush=True)
967
+ exit_code = 1
918
968
  break
919
- elif ft == "error":
920
- lines.append(f"{_R}error:{_RS} {frame.get('message','?')}")
921
- ec = 1
969
+ elif ftype == "disconnected":
970
+ stop.set()
922
971
  break
923
- elif ft == "ping":
972
+ elif ftype == "ping":
924
973
  try:
925
974
  ws.send(json.dumps({"type": "pong"}))
926
975
  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:
976
+ break
977
+ except KeyboardInterrupt:
1047
978
  pass
1048
979
  finally:
1049
- stop_recv.set()
980
+ stop.set()
981
+ _restore()
1050
982
  try:
1051
- ws.send(json.dumps({"type": "close"}))
983
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
1052
984
  except Exception:
1053
985
  pass
1054
986
  try:
1055
- ws.close()
987
+ ws.send(json.dumps({"type": "close"}))
1056
988
  except Exception:
1057
989
  pass
1058
990
  try:
1059
- _rl.write_history_file(str(hist_file))
991
+ ws.close()
1060
992
  except Exception:
1061
993
  pass
1062
994
 
995
+ # Print a clean newline after raw mode ends
996
+ print(f"\r\n {_DM}session ended code {exit_code}{_RS}\r\n", flush=True)
1063
997
  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.1
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.1"
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