akashcli 4.5.7__tar.gz → 4.5.8__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.7 → akashcli-4.5.8}/PKG-INFO +1 -1
  2. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/relay.py +333 -156
  3. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.5.7 → akashcli-4.5.8}/pyproject.toml +1 -1
  5. {akashcli-4.5.7 → akashcli-4.5.8}/README.md +0 -0
  6. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/__init__.py +0 -0
  7. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/_net.py +0 -0
  8. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/auth.py +0 -0
  9. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/cli.py +0 -0
  10. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/client.py +0 -0
  11. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/config.py +0 -0
  12. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/crypto.py +0 -0
  13. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/env_manager.py +0 -0
  14. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/exceptions.py +0 -0
  15. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/exec.py +0 -0
  16. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/integrity.py +0 -0
  17. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/logger.py +0 -0
  18. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/models.py +0 -0
  19. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/p2p.py +0 -0
  20. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/packages.py +0 -0
  21. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/restore.py +0 -0
  22. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/storage.py +0 -0
  24. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/terminal.py +0 -0
  25. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/uploader.py +0 -0
  26. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli/utils.py +0 -0
  27. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/SOURCES.txt +0 -0
  28. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/dependency_links.txt +0 -0
  29. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/entry_points.txt +0 -0
  30. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/requires.txt +0 -0
  31. {akashcli-4.5.7 → akashcli-4.5.8}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.5.7 → akashcli-4.5.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.7
3
+ Version: 4.5.8
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
@@ -222,21 +222,7 @@ def _handle_pty(ws, frame: dict) -> None:
222
222
  import subprocess as _sp
223
223
  shell = command or os.environ.get("SHELL", "/bin/bash")
224
224
 
225
- # DevOS-style PS1 injected into every relay shell session.
226
- # Uses only bash builtins + git (optional). No external deps.
227
- _AKASH_PS1 = (
228
- r'__akash_ps1() { '
229
- r' local b; b=$(git branch --show-current 2>/dev/null); '
230
- r' [ -n "$b" ] && echo -e " \033[90m(\033[1;35m$b\033[90m)\033[0m"; '
231
- r'}; '
232
- r'PS1="\n\033[1;36m╭─\033[0m \033[1;32m\u\033[90m@\033[1;32m\h\033[0m'
233
- r' \033[1;34m\w\033[0m\$(__akash_ps1)\n'
234
- r'\033[1;36m╰─❯\033[0m "; '
235
- r'export PS1'
236
- )
237
-
238
- # Run the shell directly (not wrapped in bash -c) so it IS the login shell
239
- # and gets a proper controlling terminal from slave_fd.
225
+ # Run the shell directly so it gets a proper controlling terminal.
240
226
  try:
241
227
  proc = _sp.Popen(
242
228
  [shell],
@@ -258,14 +244,6 @@ def _handle_pty(ws, frame: dict) -> None:
258
244
 
259
245
  os.close(slave_fd)
260
246
 
261
- # Give bash ~120 ms to finish sourcing .bashrc, then inject our PS1.
262
- import time as _time
263
- _time.sleep(0.12)
264
- try:
265
- os.write(master_fd, (_AKASH_PS1 + "\n").encode())
266
- except Exception:
267
- pass
268
-
269
247
  stop = threading.Event()
270
248
 
271
249
  # Thread: master fd → WebSocket (drain as fast as possible)
@@ -711,175 +689,374 @@ def guest_serve(base_url: str) -> None:
711
689
  time.sleep(3)
712
690
 
713
691
 
692
+ # ── DevOS-style shell UI ──────────────────────────────────────────────────────
693
+
694
+ _SH_C = "\033[1;36m" # cyan bold (border, ╭╰)
695
+ _SH_W = "\033[1;37m" # white bold (identity)
696
+ _SH_G = "\033[1;32m" # green bold (ok exit)
697
+ _SH_R = "\033[1;31m" # red bold (error exit)
698
+ _SH_Y = "\033[1;33m" # yellow bold (git branch)
699
+ _SH_DM = "\033[2m" # dim (timing, path)
700
+ _SH_GR = "\033[90m" # gray (│ border, separators)
701
+ _SH_RS = "\033[0m" # reset
702
+
703
+
704
+ def _sh_sanitize_path(path: str, maxlen: int = 34) -> str:
705
+ home = os.path.expanduser("~")
706
+ if path == home:
707
+ return "~"
708
+ if path.startswith(home + "/"):
709
+ path = "~" + path[len(home):]
710
+ if len(path) <= maxlen:
711
+ return path
712
+ parts = path.split("/")
713
+ tail = parts[-1]
714
+ mid = parts[-2] if len(parts) >= 2 else ""
715
+ return f".../{mid}/{tail}" if mid else f".../{tail}"
716
+
717
+
718
+ def _sh_git_branch(cwd: str) -> str:
719
+ """Read git branch from .git/HEAD — no subprocess, pure Python."""
720
+ d = cwd
721
+ while d and d != "/":
722
+ head = os.path.join(d, ".git", "HEAD")
723
+ if os.path.isfile(head):
724
+ try:
725
+ line = open(head).readline().strip()
726
+ if line.startswith("ref: refs/heads/"):
727
+ return line[len("ref: refs/heads/"):]
728
+ return line[:7]
729
+ except Exception:
730
+ return ""
731
+ d = os.path.dirname(d)
732
+ return ""
733
+
734
+
735
+ def _sh_draw_header(identity: str, cwd: str) -> None:
736
+ path = _sh_sanitize_path(cwd)
737
+ branch = _sh_git_branch(cwd)
738
+ branch_part = (
739
+ f" {_SH_GR}({_SH_Y}{branch}{_SH_GR}){_SH_RS}" if branch else ""
740
+ )
741
+ print(
742
+ f"{_SH_C}╭─{_SH_RS} {_SH_W}{identity}{_SH_RS} "
743
+ f"{_SH_GR}•{_SH_RS} {_SH_DM}{path}{_SH_RS}"
744
+ f"{branch_part}"
745
+ )
746
+
747
+
748
+ def _sh_input_prompt() -> str:
749
+ return f"{_SH_C}╰─{_SH_RS} {_SH_W}${_SH_RS} "
750
+
751
+
752
+ def _sh_footer(ec: int, elapsed_ms: float) -> None:
753
+ color = _SH_G if ec == 0 else _SH_R
754
+ t = f" {_SH_GR}{elapsed_ms/1000:.2f}s{_SH_RS}" if elapsed_ms >= 100 else ""
755
+ print(f" {_SH_GR}•{_SH_RS} {color}[{ec}]{_SH_RS}{t}")
756
+ print()
757
+
758
+
759
+ def _sh_inset(lines: list[str]) -> None:
760
+ print()
761
+ for line in lines:
762
+ print(f" {_SH_GR}│{_SH_RS} {line}")
763
+ print(f" {_SH_GR}│{_SH_RS}")
764
+
765
+
766
+ def _sh_help() -> None:
767
+ print()
768
+ print(f" {_SH_GR}╭─ AKASH SHELL COMMANDS{_SH_RS}")
769
+ cmds = [
770
+ (":h", "session history"),
771
+ (":r", "re-run last command"),
772
+ (":clear", "clear screen"),
773
+ (":status", "show session info"),
774
+ (":upload F", "upload local file F to vault"),
775
+ (":exit", "disconnect"),
776
+ ]
777
+ for cmd, desc in cmds:
778
+ print(f" {_SH_GR}│{_SH_RS} {_SH_C}{cmd:<12}{_SH_RS} {_SH_DM}{desc}{_SH_RS}")
779
+ print(f" {_SH_GR}╰─{_SH_RS}")
780
+ print()
781
+
782
+
783
+ def _sh_status(code: str, cwd: str, history: list) -> None:
784
+ print()
785
+ print(f" {_SH_GR}╭─ SESSION{_SH_RS}")
786
+ print(f" {_SH_GR}│{_SH_RS} code {_SH_C}{code}{_SH_RS}")
787
+ print(f" {_SH_GR}│{_SH_RS} cwd {_SH_DM}{cwd}{_SH_RS}")
788
+ print(f" {_SH_GR}│{_SH_RS} history {len(history)} commands")
789
+ print(f" {_SH_GR}╰─{_SH_RS}")
790
+ print()
791
+
792
+
793
+ def _sh_history(history: list) -> None:
794
+ print()
795
+ if not history:
796
+ print(f" {_SH_GR}│{_SH_RS} (no commands yet)")
797
+ else:
798
+ start = max(0, len(history) - 15)
799
+ for i, cmd in enumerate(history[start:], start + 1):
800
+ print(f" {_SH_GR}│{_SH_RS} {_SH_DM}{i:>3}{_SH_RS} {cmd}")
801
+ print(f" {_SH_GR}│{_SH_RS}")
802
+ print()
803
+
804
+
714
805
  def guest_connect_shell(base_url: str, code: str) -> int:
715
806
  """
716
- Connect to a guest host by pairing code — no API key needed.
717
- Handles the approval handshake: shows a spinner while the host decides,
718
- then opens a full PTY shell if approved.
719
- Returns the remote exit code.
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.
720
812
  """
721
- import select as _sel, signal
813
+ import readline as _rl
814
+ import queue as _q
722
815
 
723
816
  code = code.strip().upper()
724
817
  ws_url = _guest_join_url(base_url, code)
725
818
 
726
- from .exec import _get_terminal_size
727
- rows, cols = _get_terminal_size()
819
+ # ── Connect & handle approval ─────────────────────────────────────────────
820
+ print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}")
821
+ try:
822
+ ws = _ws_connect(ws_url, open_timeout=15, close_timeout=10)
823
+ except Exception as e:
824
+ raise RuntimeError(str(e))
728
825
 
729
- _raw_ok = False
730
- _old_settings = None
731
- _stdin_fd = -1
826
+ first = json.loads(ws.recv())
827
+ ftype = first.get("type")
828
+
829
+ if ftype == "error":
830
+ ws.close()
831
+ raise RuntimeError(first.get("message", "relay error"))
832
+
833
+ if ftype == "waiting_approval":
834
+ fp = first.get("fingerprint", "")
835
+ print(f" {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}")
836
+ if fp:
837
+ print(f" {_DM}fingerprint {_C}{fp}{_SH_RS}")
838
+ print(f" {_DM}host has 30 s to respond{_RS}\n")
839
+ try:
840
+ second = json.loads(ws.recv(timeout=35))
841
+ except Exception:
842
+ ws.close()
843
+ raise RuntimeError("Timed out waiting for host approval.")
844
+ if second.get("type") == "denied":
845
+ ws.close()
846
+ raise RuntimeError("Host declined the connection.")
847
+ if second.get("type") != "connected":
848
+ ws.close()
849
+ raise RuntimeError(second.get("message", "unexpected response from host"))
850
+ elif ftype != "connected":
851
+ ws.close()
852
+ raise RuntimeError(first.get("message", "unexpected server response"))
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"
859
+
860
+ # readline history setup
861
+ hist_file = _AKASH_DIR / "shell_history"
862
+ _AKASH_DIR.mkdir(parents=True, exist_ok=True)
732
863
  try:
733
- import tty, termios as _term
734
- _stdin_fd = sys.stdin.fileno()
735
- _old_settings = _term.tcgetattr(_stdin_fd)
736
- tty.setraw(_stdin_fd)
737
- _raw_ok = True
864
+ _rl.read_history_file(str(hist_file))
738
865
  except Exception:
739
866
  pass
867
+ _rl.set_history_length(500)
740
868
 
741
- def _restore():
742
- if _raw_ok and _old_settings is not None:
869
+ # ── Output queue — background thread feeds frames ─────────────────────────
870
+ q: _q.Queue = _q.Queue()
871
+ stop_recv = threading.Event()
872
+
873
+ def _recv_loop():
874
+ while not stop_recv.is_set():
743
875
  try:
744
- import termios as _t
745
- _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
876
+ raw = ws.recv(timeout=0.1)
877
+ q.put(raw)
746
878
  except Exception:
747
- pass
879
+ if not stop_recv.is_set():
880
+ q.put(None) # signals disconnection
881
+ break
748
882
 
749
- exit_code = 1
750
- try:
751
- with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
752
- # First frame: could be error, waiting_approval, or connected
753
- first = json.loads(ws.recv())
754
- ftype = first.get("type")
883
+ recv_thread = threading.Thread(target=_recv_loop, daemon=True)
884
+ recv_thread.start()
755
885
 
756
- if ftype == "error":
757
- _restore()
758
- raise RuntimeError(first.get("message", "relay error"))
759
-
760
- if ftype == "waiting_approval":
761
- fp = first.get("fingerprint", "")
762
- _restore() # restore terminal so we can print cleanly
763
- print(f"\n {_C}◈{_RS} {_DM}waiting for host to approve…{_RS}")
764
- if fp:
765
- print(f" {_DM}fingerprint {_C}{fp}{_RS}")
766
- print(f" {_DM}host has 30 s to respond{_RS}\n")
767
- # Wait for next frame: connected or denied
768
- try:
769
- second = json.loads(ws.recv(timeout=35))
770
- except Exception:
771
- raise RuntimeError("Timed out waiting for host approval.")
772
- if second.get("type") == "denied":
773
- raise RuntimeError("Host declined the connection.")
774
- if second.get("type") != "connected":
775
- raise RuntimeError(second.get("message", "unexpected response from host"))
776
- # Re-enter raw mode for the PTY session
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:
893
+ try:
894
+ raw = q.get(timeout=0.05)
895
+ except _q.Empty:
896
+ continue
897
+ if raw is None:
898
+ break
899
+ try:
900
+ frame = json.loads(raw)
901
+ except Exception:
902
+ 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)
918
+ break
919
+ elif ft == "error":
920
+ lines.append(f"{_R}error:{_RS} {frame.get('message','?')}")
921
+ ec = 1
922
+ break
923
+ elif ft == "ping":
777
924
  try:
778
- import tty as _tty2, termios as _term2
779
- _old_settings = _term2.tcgetattr(_stdin_fd)
780
- _tty2.setraw(_stdin_fd)
781
- _raw_ok = True
925
+ ws.send(json.dumps({"type": "pong"}))
782
926
  except Exception:
783
927
  pass
784
- elif ftype != "connected":
785
- _restore()
786
- raise RuntimeError(first.get("message", "unexpected server response"))
928
+ return lines, ec, new_cwd
787
929
 
788
- # Print a clean DevOS-style session header before entering raw mode
789
- _restore()
790
- print(f"\n {_G}◉{_RS} {_C}akash shell{_RS} {_DM}· code {code} · Ctrl-C to disconnect{_RS}\n")
791
- # Re-enter raw mode
792
- try:
793
- import tty as _tty3, termios as _term3
794
- _old_settings = _term3.tcgetattr(_stdin_fd)
795
- _tty3.setraw(_stdin_fd)
796
- _raw_ok = True
797
- except Exception:
798
- pass
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()
799
934
 
800
- ws.send(json.dumps({
801
- "type": "pty_run",
802
- "command": os.environ.get("SHELL", "/bin/bash"),
803
- "rows": rows, "cols": cols,
804
- }))
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
805
945
 
806
- stop = threading.Event()
946
+ if not cmd:
947
+ continue
807
948
 
808
- def _stdin_pump():
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}")
809
974
  try:
810
- while not stop.is_set():
811
- if _stdin_fd >= 0:
812
- r = _sel.select([_stdin_fd], [], [], 0.02)[0]
813
- if not r:
814
- continue
815
- # Read up to 4 KiB — handles paste bursts snappily
816
- chunk = os.read(_stdin_fd, 4096)
817
- else:
818
- chunk = sys.stdin.buffer.read(1)
819
- if not chunk:
820
- break
821
- ws.send(json.dumps({
822
- "type": "pty_in",
823
- "data": base64.b64encode(chunk).decode(),
824
- }))
825
- except Exception:
826
- pass
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
827
989
 
828
- def _on_resize(signum, _frame):
829
- r, c = _get_terminal_size()
830
- try:
831
- ws.send(json.dumps({"type": "resize", "rows": r, "cols": c}))
832
- except Exception:
833
- pass
990
+ # ── Remote command ────────────────────────────────────────────────
991
+ history.append(cmd)
992
+ last_cmd = cmd
834
993
  try:
835
- signal.signal(signal.SIGWINCH, _on_resize)
994
+ _rl.add_history(cmd)
836
995
  except Exception:
837
996
  pass
838
997
 
839
- t = threading.Thread(target=_stdin_pump, daemon=True)
840
- t.start()
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
+ )
841
1015
 
842
- while True:
843
- try:
844
- raw = ws.recv()
845
- except Exception:
846
- break
847
- try:
848
- frame = json.loads(raw)
849
- except Exception:
850
- continue
851
- ftype = frame.get("type")
852
- if ftype == "pty_out":
853
- data = base64.b64decode(frame["data"])
854
- if data:
855
- sys.stdout.buffer.write(data)
856
- sys.stdout.buffer.flush()
857
- elif ftype == "exit":
858
- exit_code = frame.get("code", 0)
859
- stop.set()
860
- break
861
- elif ftype == "error":
862
- stop.set()
863
- _restore()
864
- print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}", file=sys.stderr)
865
- exit_code = 1
866
- break
867
- elif ftype == "disconnected":
868
- stop.set()
869
- break
870
- elif ftype == "ping":
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__:"):
871
1031
  try:
872
- ws.send(json.dumps({"type": "pong"}))
1032
+ ec = int(ln[len("__AKASH_EXIT__:"):].strip())
873
1033
  except Exception:
874
- break
1034
+ pass
1035
+ else:
1036
+ clean_lines.append(ln)
875
1037
 
876
- except KeyboardInterrupt:
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:
877
1047
  pass
878
1048
  finally:
879
- stop.set() if 'stop' in dir() else None
880
- _restore()
1049
+ stop_recv.set()
881
1050
  try:
882
- signal.signal(signal.SIGWINCH, signal.SIG_DFL)
1051
+ ws.send(json.dumps({"type": "close"}))
1052
+ except Exception:
1053
+ pass
1054
+ try:
1055
+ ws.close()
1056
+ except Exception:
1057
+ pass
1058
+ try:
1059
+ _rl.write_history_file(str(hist_file))
883
1060
  except Exception:
884
1061
  pass
885
1062
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.7
3
+ Version: 4.5.8
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.7"
3
+ version = "4.5.8"
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