akashcli 4.5.6__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.6 → akashcli-4.5.8}/PKG-INFO +1 -1
  2. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/relay.py +336 -125
  3. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.5.6 → akashcli-4.5.8}/pyproject.toml +1 -1
  5. {akashcli-4.5.6 → akashcli-4.5.8}/README.md +0 -0
  6. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/__init__.py +0 -0
  7. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/_net.py +0 -0
  8. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/auth.py +0 -0
  9. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/cli.py +0 -0
  10. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/client.py +0 -0
  11. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/config.py +0 -0
  12. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/crypto.py +0 -0
  13. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/env_manager.py +0 -0
  14. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/exceptions.py +0 -0
  15. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/exec.py +0 -0
  16. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/integrity.py +0 -0
  17. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/logger.py +0 -0
  18. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/models.py +0 -0
  19. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/p2p.py +0 -0
  20. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/packages.py +0 -0
  21. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/restore.py +0 -0
  22. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/storage.py +0 -0
  24. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/terminal.py +0 -0
  25. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/uploader.py +0 -0
  26. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/utils.py +0 -0
  27. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/SOURCES.txt +0 -0
  28. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/dependency_links.txt +0 -0
  29. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/entry_points.txt +0 -0
  30. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/requires.txt +0 -0
  31. {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.5.6 → 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.6
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
@@ -221,8 +221,8 @@ def _handle_pty(ws, frame: dict) -> None:
221
221
 
222
222
  import subprocess as _sp
223
223
  shell = command or os.environ.get("SHELL", "/bin/bash")
224
- # Run the shell directly (not wrapped in bash -c) so it IS the login shell
225
- # and gets a proper controlling terminal from slave_fd.
224
+
225
+ # Run the shell directly so it gets a proper controlling terminal.
226
226
  try:
227
227
  proc = _sp.Popen(
228
228
  [shell],
@@ -243,6 +243,7 @@ def _handle_pty(ws, frame: dict) -> None:
243
243
  return
244
244
 
245
245
  os.close(slave_fd)
246
+
246
247
  stop = threading.Event()
247
248
 
248
249
  # Thread: master fd → WebSocket (drain as fast as possible)
@@ -688,164 +689,374 @@ def guest_serve(base_url: str) -> None:
688
689
  time.sleep(3)
689
690
 
690
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
+
691
805
  def guest_connect_shell(base_url: str, code: str) -> int:
692
806
  """
693
- Connect to a guest host by pairing code — no API key needed.
694
- Handles the approval handshake: shows a spinner while the host decides,
695
- then opens a full PTY shell if approved.
696
- 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.
697
812
  """
698
- import select as _sel, signal
813
+ import readline as _rl
814
+ import queue as _q
699
815
 
700
816
  code = code.strip().upper()
701
817
  ws_url = _guest_join_url(base_url, code)
702
818
 
703
- from .exec import _get_terminal_size
704
- 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))
705
825
 
706
- _raw_ok = False
707
- _old_settings = None
708
- _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)
709
863
  try:
710
- import tty, termios as _term
711
- _stdin_fd = sys.stdin.fileno()
712
- _old_settings = _term.tcgetattr(_stdin_fd)
713
- tty.setraw(_stdin_fd)
714
- _raw_ok = True
864
+ _rl.read_history_file(str(hist_file))
715
865
  except Exception:
716
866
  pass
867
+ _rl.set_history_length(500)
717
868
 
718
- def _restore():
719
- 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():
720
875
  try:
721
- import termios as _t
722
- _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
876
+ raw = ws.recv(timeout=0.1)
877
+ q.put(raw)
723
878
  except Exception:
724
- pass
879
+ if not stop_recv.is_set():
880
+ q.put(None) # signals disconnection
881
+ break
725
882
 
726
- exit_code = 1
727
- try:
728
- with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
729
- # First frame: could be error, waiting_approval, or connected
730
- first = json.loads(ws.recv())
731
- ftype = first.get("type")
883
+ recv_thread = threading.Thread(target=_recv_loop, daemon=True)
884
+ recv_thread.start()
732
885
 
733
- if ftype == "error":
734
- _restore()
735
- raise RuntimeError(first.get("message", "relay error"))
736
-
737
- if ftype == "waiting_approval":
738
- fp = first.get("fingerprint", "")
739
- _restore() # restore terminal so we can print cleanly
740
- print(f" {_Y}…{_RS} waiting for host to approve this connection…")
741
- if fp:
742
- print(f" {_DM}Your fingerprint: {_C}{fp}{_RS}")
743
- print(f" {_DM}(host has 30 seconds to respond){_RS}")
744
- print()
745
- # Wait for next frame: connected or denied
746
- try:
747
- second = json.loads(ws.recv(timeout=35))
748
- except Exception:
749
- raise RuntimeError("Timed out waiting for host approval.")
750
- if second.get("type") == "denied":
751
- raise RuntimeError("Host declined the connection.")
752
- if second.get("type") != "connected":
753
- raise RuntimeError(second.get("message", "unexpected response from host"))
754
- # 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":
755
924
  try:
756
- import tty as _tty2, termios as _term2
757
- _old_settings = _term2.tcgetattr(_stdin_fd)
758
- _tty2.setraw(_stdin_fd)
759
- _raw_ok = True
925
+ ws.send(json.dumps({"type": "pong"}))
760
926
  except Exception:
761
927
  pass
762
- elif ftype != "connected":
763
- _restore()
764
- raise RuntimeError(first.get("message", "unexpected server response"))
928
+ return lines, ec, new_cwd
765
929
 
766
- ws.send(json.dumps({
767
- "type": "pty_run",
768
- "command": os.environ.get("SHELL", "/bin/bash"),
769
- "rows": rows, "cols": cols,
770
- }))
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()
771
934
 
772
- stop = threading.Event()
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
773
945
 
774
- def _stdin_pump():
775
- try:
776
- while not stop.is_set():
777
- if _stdin_fd >= 0:
778
- r = _sel.select([_stdin_fd], [], [], 0.02)[0]
779
- if not r:
780
- continue
781
- # Read up to 4 KiB — handles paste bursts snappily
782
- chunk = os.read(_stdin_fd, 4096)
783
- else:
784
- chunk = sys.stdin.buffer.read(1)
785
- if not chunk:
786
- break
787
- ws.send(json.dumps({
788
- "type": "pty_in",
789
- "data": base64.b64encode(chunk).decode(),
790
- }))
791
- except Exception:
792
- pass
946
+ if not cmd:
947
+ continue
793
948
 
794
- def _on_resize(signum, _frame):
795
- r, c = _get_terminal_size()
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}")
796
974
  try:
797
- ws.send(json.dumps({"type": "resize", "rows": r, "cols": c}))
798
- except Exception:
799
- 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
989
+
990
+ # ── Remote command ────────────────────────────────────────────────
991
+ history.append(cmd)
992
+ last_cmd = cmd
800
993
  try:
801
- signal.signal(signal.SIGWINCH, _on_resize)
994
+ _rl.add_history(cmd)
802
995
  except Exception:
803
996
  pass
804
997
 
805
- t = threading.Thread(target=_stdin_pump, daemon=True)
806
- 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
+ )
807
1015
 
808
- while True:
809
- try:
810
- raw = ws.recv()
811
- except Exception:
812
- break
813
- try:
814
- frame = json.loads(raw)
815
- except Exception:
816
- continue
817
- ftype = frame.get("type")
818
- if ftype == "pty_out":
819
- data = base64.b64decode(frame["data"])
820
- if data:
821
- sys.stdout.buffer.write(data)
822
- sys.stdout.buffer.flush()
823
- elif ftype == "exit":
824
- exit_code = frame.get("code", 0)
825
- stop.set()
826
- break
827
- elif ftype == "error":
828
- stop.set()
829
- _restore()
830
- print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}", file=sys.stderr)
831
- exit_code = 1
832
- break
833
- elif ftype == "disconnected":
834
- stop.set()
835
- break
836
- 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__:"):
837
1031
  try:
838
- ws.send(json.dumps({"type": "pong"}))
1032
+ ec = int(ln[len("__AKASH_EXIT__:"):].strip())
839
1033
  except Exception:
840
- break
1034
+ pass
1035
+ else:
1036
+ clean_lines.append(ln)
841
1037
 
842
- 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:
843
1047
  pass
844
1048
  finally:
845
- stop.set() if 'stop' in dir() else None
846
- _restore()
1049
+ stop_recv.set()
847
1050
  try:
848
- 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))
849
1060
  except Exception:
850
1061
  pass
851
1062
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.6
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.6"
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