akashcli 4.6.2__tar.gz → 4.6.3__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.6.2 → akashcli-4.6.3}/PKG-INFO +1 -1
  2. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/relay.py +486 -120
  3. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.6.2 → akashcli-4.6.3}/pyproject.toml +1 -1
  5. {akashcli-4.6.2 → akashcli-4.6.3}/README.md +0 -0
  6. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/__init__.py +0 -0
  7. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/_net.py +0 -0
  8. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/auth.py +0 -0
  9. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/cli.py +0 -0
  10. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/client.py +0 -0
  11. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/config.py +0 -0
  12. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/crypto.py +0 -0
  13. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/env_manager.py +0 -0
  14. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/exceptions.py +0 -0
  15. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/exec.py +0 -0
  16. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/integrity.py +0 -0
  17. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/logger.py +0 -0
  18. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/models.py +0 -0
  19. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/p2p.py +0 -0
  20. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/packages.py +0 -0
  21. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/restore.py +0 -0
  22. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/storage.py +0 -0
  24. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/terminal.py +0 -0
  25. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/uploader.py +0 -0
  26. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/utils.py +0 -0
  27. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/SOURCES.txt +0 -0
  28. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/dependency_links.txt +0 -0
  29. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/entry_points.txt +0 -0
  30. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/requires.txt +0 -0
  31. {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.6.2 → akashcli-4.6.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.6.2
3
+ Version: 4.6.3
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
@@ -164,30 +164,68 @@ def _run_relay_device(ws, label: str) -> None:
164
164
 
165
165
 
166
166
  def _handle_run(ws, frame: dict) -> None:
167
- """Execute a non-interactive command and stream output back."""
167
+ """Execute a non-interactive command, stream output in real-time, send cwd."""
168
168
  import subprocess as _sp
169
+ import select as _sel
170
+
171
+ command = frame.get("command", "")
172
+ cwd = frame.get("cwd", os.path.expanduser("~"))
173
+ env = {**os.environ, "TERM": "dumb"}
169
174
 
170
- command = frame.get("command", "")
171
175
  try:
172
176
  proc = _sp.Popen(
173
177
  command, shell=True,
174
178
  stdout=_sp.PIPE, stderr=_sp.STDOUT,
175
- cwd=os.path.expanduser("~"),
179
+ cwd=cwd,
180
+ env=env,
176
181
  )
182
+ fd = proc.stdout.fileno()
183
+ # Non-blocking read: send chunks as soon as they arrive
177
184
  while True:
178
- chunk = proc.stdout.read(4096)
179
- if not chunk:
185
+ r, _, _ = _sel.select([fd], [], [], 0.02)
186
+ if r:
187
+ chunk = os.read(fd, 16384)
188
+ if not chunk:
189
+ break
190
+ ws.send(json.dumps({
191
+ "type": "stdout",
192
+ "data": base64.b64encode(chunk).decode(),
193
+ }))
194
+ elif proc.poll() is not None:
195
+ # Drain any remaining bytes
196
+ try:
197
+ while True:
198
+ r2, _, _ = _sel.select([fd], [], [], 0)
199
+ if not r2:
200
+ break
201
+ chunk = os.read(fd, 16384)
202
+ if not chunk:
203
+ break
204
+ ws.send(json.dumps({
205
+ "type": "stdout",
206
+ "data": base64.b64encode(chunk).decode(),
207
+ }))
208
+ except Exception:
209
+ pass
180
210
  break
181
- ws.send(json.dumps({
182
- "type": "stdout",
183
- "data": base64.b64encode(chunk).decode(),
184
- }))
185
211
  proc.wait()
186
- ws.send(json.dumps({"type": "exit", "code": proc.returncode or 0}))
212
+ # Resolve final cwd: try to read it from the shell
213
+ try:
214
+ result = _sp.run(
215
+ ["sh", "-c", f"cd {json.dumps(cwd)} 2>/dev/null; true"],
216
+ capture_output=True, timeout=1,
217
+ )
218
+ except Exception:
219
+ pass
220
+ ws.send(json.dumps({
221
+ "type": "exit",
222
+ "code": proc.returncode or 0,
223
+ "cwd": cwd,
224
+ }))
187
225
  except Exception as e:
188
226
  try:
189
227
  ws.send(json.dumps({"type": "error", "message": str(e)}))
190
- ws.send(json.dumps({"type": "exit", "code": 1}))
228
+ ws.send(json.dumps({"type": "exit", "code": 1, "cwd": cwd}))
191
229
  except Exception:
192
230
  pass
193
231
 
@@ -796,15 +834,19 @@ def _sh_help() -> None:
796
834
  print()
797
835
  print(f" {_SH_GR}╭─ AKASH SHELL COMMANDS{_SH_RS}")
798
836
  cmds = [
799
- (":h", "session history"),
800
- (":r", "re-run last command"),
801
- (":clear", "clear screen"),
802
- (":status", "show session info"),
803
- (":upload F", "upload local file F to vault"),
804
- (":exit", "disconnect"),
837
+ (":help / :h", "this help"),
838
+ (":hist", "command history"),
839
+ (":clear", "clear screen"),
840
+ (":status", "session info (code, cwd)"),
841
+ (":pty [cmd]", "run cmd in full PTY mode (for nano, vim, htop…)"),
842
+ (":upload F", "upload local file F to vault"),
843
+ (":exit", "disconnect"),
844
+ ("exit / Ctrl-D", "disconnect"),
805
845
  ]
806
846
  for cmd, desc in cmds:
807
- print(f" {_SH_GR}│{_SH_RS} {_SH_C}{cmd:<12}{_SH_RS} {_SH_DM}{desc}{_SH_RS}")
847
+ print(f" {_SH_GR}│{_SH_RS} {_SH_C}{cmd:<18}{_SH_RS} {_SH_DM}{desc}{_SH_RS}")
848
+ print(f" {_SH_GR}│{_SH_RS}")
849
+ print(f" {_SH_GR}│{_SH_RS} {_SH_DM}TUI apps (nano, vim, htop, python3…) auto-switch to PTY{_SH_RS}")
808
850
  print(f" {_SH_GR}╰─{_SH_RS}")
809
851
  print()
810
852
 
@@ -831,21 +873,96 @@ def _sh_history(history: list) -> None:
831
873
  print()
832
874
 
833
875
 
876
+ # ── TUI apps that always get PTY mode ────────────────────────────────────────
877
+ _PTY_APPS = frozenset({
878
+ "nano", "vim", "vi", "nvim", "emacs", "pico",
879
+ "htop", "top", "btop", "less", "more", "man",
880
+ "python", "python3", "ipython", "node", "irb",
881
+ "mysql", "psql", "sqlite3", "mongo",
882
+ "bash", "sh", "zsh", "fish", "ksh",
883
+ "watch", "screen", "tmux",
884
+ "fzf", "ranger", "mc", "nnn",
885
+ "ssh", "telnet",
886
+ })
887
+
888
+
889
+ def _is_pty_cmd(cmd: str) -> bool:
890
+ """Return True if cmd should run in PTY mode (TUI / interactive app)."""
891
+ first = cmd.strip().lstrip("\\").split()[0] if cmd.strip() else ""
892
+ # strip path — /usr/bin/vim → vim
893
+ first = first.split("/")[-1]
894
+ return first in _PTY_APPS
895
+
896
+
897
+ def _term_cols() -> int:
898
+ try:
899
+ import shutil as _s
900
+ return _s.get_terminal_size(fallback=(80, 24)).columns
901
+ except Exception:
902
+ return 80
903
+
904
+
905
+ def _term_size() -> tuple:
906
+ try:
907
+ import shutil as _s
908
+ ts = _s.get_terminal_size(fallback=(80, 24))
909
+ return ts.lines, ts.columns
910
+ except Exception:
911
+ return 24, 80
912
+
913
+
914
+ def _strip_ansi(text: str) -> str:
915
+ import re
916
+ text = re.sub(r'\x1b\[[0-9;]*[mGKHFJA-Za-z]', '', text)
917
+ text = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', text)
918
+ return text
919
+
920
+
921
+ def _print_output(lines: list, cols: int) -> None:
922
+ """Print command output with inset │ borders, wrapping long lines."""
923
+ GR, RS = _SH_GR, _SH_RS
924
+ # wrap at cols - 6 (accounts for " │ " prefix)
925
+ wrap = max(cols - 6, 40)
926
+ printed = 0
927
+ MAX = 200
928
+ for raw_line in lines:
929
+ # wrap long lines
930
+ while len(raw_line) > wrap:
931
+ sys.stdout.write(f" {GR}│{RS} {raw_line[:wrap]}\n")
932
+ raw_line = raw_line[wrap:]
933
+ printed += 1
934
+ if printed >= MAX:
935
+ break
936
+ if printed >= MAX:
937
+ remaining = len(lines) - lines.index(raw_line)
938
+ sys.stdout.write(f" {GR}│{RS} {_SH_DM}… {remaining} more lines{RS}\n")
939
+ break
940
+ sys.stdout.write(f" {GR}│{RS} {raw_line}\n")
941
+ printed += 1
942
+ sys.stdout.flush()
943
+
944
+
834
945
  def guest_connect_shell(base_url: str, code: str) -> int:
835
946
  """
836
947
  Connect to a guest host via pairing code.
837
- Opens a real PTY tunnel to the host's shell — identical to connect_shell()
838
- but uses the guest relay (no API key). Full interactive terminal: nano,
839
- vim, htop, etc. all work. DevOS banner is printed before entering raw mode.
840
- Returns the remote exit code.
948
+
949
+ Dual-mode interactive shell:
950
+ REPL mode — DevOS prompt, commands run via `run` frames, real-time
951
+ streaming output with │ borders. Fast, clean, readable.
952
+ PTY mode — auto-switched for TUI apps (nano/vim/htop/etc.) or on
953
+ demand with :pty. Terminal goes raw, full escape codes.
954
+
955
+ Returns the last remote exit code.
841
956
  """
957
+ import queue as _q
842
958
  import select as _sel
843
959
  import signal
960
+ import shutil as _sh
844
961
 
845
962
  code = code.strip().upper()
846
963
  ws_url = _guest_join_url(base_url, code)
847
964
 
848
- # ── Connect & handle approval (before entering raw mode) ─────────────────
965
+ # ── 1. Connect ────────────────────────────────────────────────────────────
849
966
  print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}", flush=True)
850
967
  try:
851
968
  ws = _ws_connect(ws_url, open_timeout=15, close_timeout=10)
@@ -880,145 +997,394 @@ def guest_connect_shell(base_url: str, code: str) -> int:
880
997
  ws.close()
881
998
  raise RuntimeError(first.get("message", "unexpected server response"))
882
999
 
883
- # ── Print DevOS banner (before raw mode) ──────────────────────────────────
884
- identity = os.environ.get("USER") or os.environ.get("LOGNAME") or "akash"
885
- print(f"\n {_G}◉{_RS} {_C}akash shell{_RS} "
886
- f"{_DM}code {code} · Ctrl-D or `exit` to quit{_RS}\n", flush=True)
1000
+ # ── 2. Session state ──────────────────────────────────────────────────────
1001
+ identity = os.environ.get("USER") or os.environ.get("LOGNAME") or "akash"
1002
+ cwd = "~" # updated from exit frames
1003
+ history : list[str] = []
1004
+ exit_code = 0
887
1005
 
888
- # ── Get terminal size ─────────────────────────────────────────────────────
1006
+ # readline history
1007
+ _AKASH_DIR.mkdir(parents=True, exist_ok=True)
1008
+ hist_file = _AKASH_DIR / "shell_history"
889
1009
  try:
890
- import shutil as _sh
891
- cols, rows = _sh.get_terminal_size(fallback=(80, 24))
1010
+ import readline as _rl
1011
+ _rl.read_history_file(str(hist_file))
1012
+ _rl.set_history_length(500)
892
1013
  except Exception:
893
- rows, cols = 24, 80
1014
+ _rl = None # type: ignore
894
1015
 
895
- # ── Enter raw mode ────────────────────────────────────────────────────────
896
- _raw_ok = False
1016
+ # ── 3. Background recv thread → queue ─────────────────────────────────────
1017
+ # Feeds all incoming WS frames into a queue so the REPL loop
1018
+ # can collect them without blocking the main thread on ws.recv().
1019
+ _q_frames: _q.Queue = _q.Queue()
1020
+ _stop_recv = threading.Event()
1021
+
1022
+ def _recv_worker():
1023
+ while not _stop_recv.is_set():
1024
+ try:
1025
+ raw = ws.recv(timeout=1.0)
1026
+ _q_frames.put(raw)
1027
+ except Exception as exc:
1028
+ if not _stop_recv.is_set():
1029
+ _q_frames.put(None) # sentinel: disconnected
1030
+ break
1031
+
1032
+ _recv_t = threading.Thread(target=_recv_worker, daemon=True)
1033
+ _recv_t.start()
1034
+
1035
+ # ── 4. Banner ─────────────────────────────────────────────────────────────
1036
+ print(f"\n {_G}◉{_RS} {_C}akash shell{_RS} "
1037
+ f"{_DM}code {_C}{code}{_DM} · :help for commands{_RS}\n",
1038
+ flush=True)
1039
+
1040
+ # ── helpers for raw / PTY mode ─────────────────────────────────────────────
1041
+ _stdin_fd = -1
1042
+ _raw_ok = False
897
1043
  _old_settings = None
898
- _stdin_fd = -1
899
- try:
900
- import tty as _tty, termios as _term
901
- _stdin_fd = sys.stdin.fileno()
902
- _old_settings = _term.tcgetattr(_stdin_fd)
903
- _tty.setraw(_stdin_fd)
904
- _raw_ok = True
905
- except Exception:
906
- pass
907
1044
 
908
- def _restore():
1045
+ def _enter_raw():
1046
+ nonlocal _stdin_fd, _raw_ok, _old_settings
1047
+ try:
1048
+ import tty as _tty, termios as _tm
1049
+ _stdin_fd = sys.stdin.fileno()
1050
+ _old_settings = _tm.tcgetattr(_stdin_fd)
1051
+ _tty.setraw(_stdin_fd)
1052
+ _raw_ok = True
1053
+ except Exception:
1054
+ pass
1055
+
1056
+ def _leave_raw():
909
1057
  if _raw_ok and _old_settings is not None:
910
1058
  try:
911
- import termios as _t
912
- _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
1059
+ import termios as _tm2
1060
+ _tm2.tcsetattr(_stdin_fd, _tm2.TCSADRAIN, _old_settings)
913
1061
  except Exception:
914
1062
  pass
915
1063
 
916
- exit_code = 0
917
- stop = threading.Event()
1064
+ # ── PTY passthrough session ───────────────────────────────────────────────
1065
+ def _run_pty(cmd: str) -> int:
1066
+ """
1067
+ Enter raw mode, send pty_run to host, bridge I/O until exit frame.
1068
+ Returns exit code. Restores terminal on return.
1069
+ """
1070
+ rows, cols = _term_size()
1071
+ _enter_raw()
1072
+ _pty_stop = threading.Event()
1073
+
1074
+ # SIGWINCH → resize
1075
+ def _on_resize(sig, _f):
1076
+ try:
1077
+ r2, c2 = _term_size()
1078
+ ws.send(json.dumps({"type": "resize", "rows": r2, "cols": c2}))
1079
+ except Exception:
1080
+ pass
1081
+ try:
1082
+ signal.signal(signal.SIGWINCH, _on_resize)
1083
+ except Exception:
1084
+ pass
918
1085
 
919
- # ── Tell host to open a PTY shell ─────────────────────────────────────────
920
- try:
921
- ws.send(json.dumps({
922
- "type": "pty_run",
923
- "command": os.environ.get("SHELL", "/bin/bash"),
924
- "rows": rows,
925
- "cols": cols,
926
- }))
927
- except Exception as e:
928
- _restore()
929
- ws.close()
930
- raise RuntimeError(f"Failed to start remote shell: {e}")
1086
+ # stdin pump
1087
+ def _pump():
1088
+ try:
1089
+ while not _pty_stop.is_set():
1090
+ if _stdin_fd >= 0:
1091
+ r = _sel.select([_stdin_fd], [], [], 0.02)[0]
1092
+ if not r:
1093
+ continue
1094
+ chunk = os.read(_stdin_fd, 4096)
1095
+ else:
1096
+ chunk = sys.stdin.buffer.read(1)
1097
+ if not chunk:
1098
+ break
1099
+ ws.send(json.dumps({
1100
+ "type": "pty_in",
1101
+ "data": base64.b64encode(chunk).decode(),
1102
+ }))
1103
+ except Exception:
1104
+ pass
931
1105
 
932
- # ── Thread: local stdin → WS ──────────────────────────────────────────────
933
- def _stdin_pump():
934
1106
  try:
935
- while not stop.is_set():
936
- if _stdin_fd >= 0:
937
- r = _sel.select([_stdin_fd], [], [], 0.02)[0]
938
- if not r:
939
- continue
940
- chunk = os.read(_stdin_fd, 4096)
941
- else:
942
- chunk = sys.stdin.buffer.read(1)
943
- if not chunk:
1107
+ ws.send(json.dumps({
1108
+ "type": "pty_run",
1109
+ "command": cmd or os.environ.get("SHELL", "/bin/bash"),
1110
+ "rows": rows,
1111
+ "cols": cols,
1112
+ }))
1113
+ except Exception as e:
1114
+ _leave_raw()
1115
+ print(f"\r\n {_R}✗{_RS} {e}\r\n", flush=True)
1116
+ return 1
1117
+
1118
+ pump_t = threading.Thread(target=_pump, daemon=True)
1119
+ pump_t.start()
1120
+
1121
+ ec = 0
1122
+ try:
1123
+ while True:
1124
+ try:
1125
+ raw = _q_frames.get(timeout=0.05)
1126
+ except _q.Empty:
1127
+ continue
1128
+ if raw is None:
944
1129
  break
945
- ws.send(json.dumps({
946
- "type": "pty_in",
947
- "data": base64.b64encode(chunk).decode(),
948
- }))
949
- except Exception:
1130
+ try:
1131
+ frame = json.loads(raw)
1132
+ except Exception:
1133
+ continue
1134
+ ft = frame.get("type")
1135
+ if ft == "pty_out":
1136
+ data = base64.b64decode(frame["data"])
1137
+ if data:
1138
+ sys.stdout.buffer.write(data)
1139
+ sys.stdout.buffer.flush()
1140
+ elif ft == "exit":
1141
+ ec = frame.get("code", 0)
1142
+ break
1143
+ elif ft == "error":
1144
+ _leave_raw()
1145
+ print(f"\r\n {_R}[error]{_RS} {frame.get('message','?')}\r\n",
1146
+ flush=True)
1147
+ ec = 1
1148
+ break
1149
+ elif ft == "disconnected":
1150
+ break
1151
+ elif ft == "ping":
1152
+ try:
1153
+ ws.send(json.dumps({"type": "pong"}))
1154
+ except Exception:
1155
+ break
1156
+ except KeyboardInterrupt:
950
1157
  pass
1158
+ finally:
1159
+ _pty_stop.set()
1160
+ try:
1161
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
1162
+ except Exception:
1163
+ pass
951
1164
 
952
- # ── SIGWINCH → resize frame ───────────────────────────────────────────────
953
- def _on_resize(signum, _frame):
1165
+ _leave_raw()
1166
+ print(flush=True) # clean line after PTY output
1167
+ return ec
1168
+
1169
+ # ── REPL command runner ───────────────────────────────────────────────────
1170
+ def _run_cmd(cmd: str) -> tuple:
1171
+ """
1172
+ Send `run` frame, collect stdout in real-time, return (lines, ec, new_cwd).
1173
+ Output is printed live as │ lines while collecting for the footer.
1174
+ """
1175
+ # Wrap: cd to cwd, run command, emit sentinels
1176
+ actual = (
1177
+ f'cd {json.dumps(cwd)} 2>/dev/null\n'
1178
+ f'{cmd}\n'
1179
+ f'printf "\\x00__EC__%d\\x00" "$?"\n'
1180
+ f'printf "\\x00__CWD__%s\\x00" "$PWD"\n'
1181
+ )
954
1182
  try:
955
- import shutil as _sh2
956
- c2, r2 = _sh2.get_terminal_size(fallback=(80, 24))
957
- ws.send(json.dumps({"type": "resize", "rows": r2, "cols": c2}))
1183
+ ws.send(json.dumps({
1184
+ "type": "run",
1185
+ "command": actual,
1186
+ "cwd": cwd,
1187
+ }))
958
1188
  except Exception:
959
- pass
960
- try:
961
- signal.signal(signal.SIGWINCH, _on_resize)
962
- except Exception:
963
- pass
1189
+ return [], 1, cwd
964
1190
 
965
- t = threading.Thread(target=_stdin_pump, daemon=True)
966
- t.start()
1191
+ cols = _term_cols()
1192
+ buf = b""
1193
+ lines : list[str] = []
1194
+ ec = 0
1195
+ new_cwd = cwd
1196
+ deadline = time.time() + 60.0
1197
+ started = False
967
1198
 
968
- # ── Main loop: WS → local stdout (raw PTY output) ────────────────────────
969
- try:
970
- while True:
1199
+ while time.time() < deadline:
971
1200
  try:
972
- raw = ws.recv()
973
- except Exception:
1201
+ raw = _q_frames.get(timeout=0.05)
1202
+ except _q.Empty:
1203
+ continue
1204
+ if raw is None:
974
1205
  break
975
1206
  try:
976
1207
  frame = json.loads(raw)
977
1208
  except Exception:
978
1209
  continue
979
- ftype = frame.get("type")
980
- if ftype == "pty_out":
981
- data = base64.b64decode(frame["data"])
982
- if data:
983
- sys.stdout.buffer.write(data)
984
- sys.stdout.buffer.flush()
985
- elif ftype == "exit":
986
- exit_code = frame.get("code", 0)
987
- stop.set()
988
- break
989
- elif ftype == "error":
990
- stop.set()
991
- _restore()
992
- print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}\r\n",
993
- file=sys.stderr, flush=True)
994
- exit_code = 1
1210
+ ft = frame.get("type")
1211
+
1212
+ if ft == "stdout":
1213
+ chunk = base64.b64decode(frame.get("data", ""))
1214
+ buf += chunk
1215
+ # Print lines as they arrive — real-time streaming
1216
+ while b"\n" in buf:
1217
+ line_b, buf = buf.split(b"\n", 1)
1218
+ line = _strip_ansi(line_b.decode("utf-8", errors="replace"))
1219
+ # Parse null-delimited sentinels
1220
+ if "\x00__EC__" in line:
1221
+ import re as _re
1222
+ m = _re.search(r'\x00__EC__(\d+)\x00', line)
1223
+ if m:
1224
+ ec = int(m.group(1))
1225
+ continue
1226
+ if "\x00__CWD__" in line:
1227
+ import re as _re
1228
+ m = _re.search(r'\x00__CWD__([^\x00]+)\x00', line)
1229
+ if m:
1230
+ new_cwd = m.group(1).strip()
1231
+ continue
1232
+ if not started:
1233
+ # Print blank separator before first output line
1234
+ sys.stdout.write("\n")
1235
+ started = True
1236
+ # Print live
1237
+ display = line.rstrip()
1238
+ # wrap
1239
+ wrap = max(cols - 6, 40)
1240
+ while len(display) > wrap:
1241
+ sys.stdout.write(f" {_SH_GR}│{_SH_RS} {display[:wrap]}\n")
1242
+ display = display[wrap:]
1243
+ sys.stdout.write(f" {_SH_GR}│{_SH_RS} {display}\n")
1244
+ sys.stdout.flush()
1245
+ lines.append(line.rstrip())
1246
+
1247
+ elif ft == "exit":
1248
+ ec = frame.get("code", ec)
1249
+ new_cwd = frame.get("cwd", new_cwd) or new_cwd
1250
+ # Flush remaining buffer
1251
+ if buf:
1252
+ line = _strip_ansi(buf.decode("utf-8", errors="replace")).rstrip()
1253
+ if line and "\x00__EC__" not in line and "\x00__CWD__" not in line:
1254
+ if not started:
1255
+ sys.stdout.write("\n")
1256
+ started = True
1257
+ sys.stdout.write(f" {_SH_GR}│{_SH_RS} {line}\n")
1258
+ sys.stdout.flush()
1259
+ lines.append(line)
995
1260
  break
996
- elif ftype == "disconnected":
997
- stop.set()
1261
+
1262
+ elif ft == "error":
1263
+ msg = frame.get("message", "?")
1264
+ if not started:
1265
+ sys.stdout.write("\n")
1266
+ started = True
1267
+ sys.stdout.write(f" {_SH_GR}│{_RS} {_R}error:{_RS} {msg}\n")
1268
+ sys.stdout.flush()
1269
+ lines.append(f"error: {msg}")
1270
+ ec = 1
998
1271
  break
999
- elif ftype == "ping":
1272
+
1273
+ elif ft == "ping":
1000
1274
  try:
1001
1275
  ws.send(json.dumps({"type": "pong"}))
1002
1276
  except Exception:
1003
- break
1004
- except KeyboardInterrupt:
1277
+ pass
1278
+
1279
+ return lines, ec, new_cwd
1280
+
1281
+ # ── 5. REPL loop ──────────────────────────────────────────────────────────
1282
+ try:
1283
+ while True:
1284
+ # Draw prompt
1285
+ _sh_draw_header(identity, cwd)
1286
+ try:
1287
+ cmd = input(_sh_input_prompt()).strip()
1288
+ except (EOFError, KeyboardInterrupt):
1289
+ print()
1290
+ break
1291
+
1292
+ if not cmd:
1293
+ continue
1294
+
1295
+ # ── Built-in / local commands ─────────────────────────────────
1296
+ if cmd in (":exit", "exit", "logout"):
1297
+ break
1298
+
1299
+ if cmd in (":clear", "clear"):
1300
+ sys.stdout.write("\033[2J\033[H")
1301
+ sys.stdout.flush()
1302
+ continue
1303
+
1304
+ if cmd in (":help", ":h", "help"):
1305
+ _sh_help()
1306
+ continue
1307
+
1308
+ if cmd == ":hist":
1309
+ _sh_history(history)
1310
+ continue
1311
+
1312
+ if cmd == ":status":
1313
+ _sh_status(code, cwd, history)
1314
+ continue
1315
+
1316
+ if cmd.startswith(":upload "):
1317
+ fpath = cmd[8:].strip()
1318
+ print(f" {_DM}uploading {fpath}…{_RS}", flush=True)
1319
+ try:
1320
+ from .client import AkashClient
1321
+ from .config import ConfigManager
1322
+ _cfg = ConfigManager.load()
1323
+ api_key = os.getenv("AKASH_API_KEY") or _cfg.get("api_key")
1324
+ if not api_key:
1325
+ print(f" {_R}✗{_RS} no API key — run akash login first\n")
1326
+ continue
1327
+ client = AkashClient(api_key=api_key)
1328
+ result = client.upload_file(fpath)
1329
+ print(f" {_G}✓{_RS} {_C}{result.file_code}{_RS} {_DM}{result.url}{_RS}\n")
1330
+ except Exception as ue:
1331
+ print(f" {_R}✗{_RS} upload failed: {ue}\n")
1332
+ continue
1333
+
1334
+ # :pty [cmd] — force PTY mode
1335
+ if cmd.startswith(":pty"):
1336
+ pty_cmd = cmd[4:].strip() or ""
1337
+ ec = _run_pty(pty_cmd)
1338
+ exit_code = ec
1339
+ _sh_footer(ec, 0)
1340
+ # Refresh cwd after PTY session
1341
+ continue
1342
+
1343
+ # ── Auto PTY for TUI apps ─────────────────────────────────────
1344
+ if _is_pty_cmd(cmd):
1345
+ ec = _run_pty(cmd)
1346
+ exit_code = ec
1347
+ _sh_footer(ec, 0)
1348
+ continue
1349
+
1350
+ # ── Remote REPL command ───────────────────────────────────────
1351
+ history.append(cmd)
1352
+ try:
1353
+ if _rl:
1354
+ _rl.add_history(cmd)
1355
+ except Exception:
1356
+ pass
1357
+
1358
+ t0 = time.monotonic()
1359
+ _, ec, new_cwd = _run_cmd(cmd)
1360
+ elapsed_ms = (time.monotonic() - t0) * 1000
1361
+
1362
+ if new_cwd and new_cwd != "~":
1363
+ cwd = new_cwd
1364
+
1365
+ exit_code = ec
1366
+ # Separator line + footer
1367
+ sys.stdout.write(f" {_SH_GR}│{_SH_RS}\n")
1368
+ sys.stdout.flush()
1369
+ _sh_footer(ec, elapsed_ms)
1370
+
1371
+ except Exception:
1005
1372
  pass
1006
1373
  finally:
1007
- stop.set()
1008
- _restore()
1374
+ _stop_recv.set()
1009
1375
  try:
1010
- signal.signal(signal.SIGWINCH, signal.SIG_DFL)
1376
+ ws.send(json.dumps({"type": "close"}))
1011
1377
  except Exception:
1012
1378
  pass
1013
1379
  try:
1014
- ws.send(json.dumps({"type": "close"}))
1380
+ ws.close()
1015
1381
  except Exception:
1016
1382
  pass
1017
1383
  try:
1018
- ws.close()
1384
+ if _rl:
1385
+ _rl.write_history_file(str(hist_file))
1019
1386
  except Exception:
1020
1387
  pass
1021
1388
 
1022
- # Print a clean newline after raw mode ends
1023
- print(f"\r\n {_DM}session ended code {exit_code}{_RS}\r\n", flush=True)
1389
+ print(f"\n {_DM}session ended{_RS}\n", flush=True)
1024
1390
  return exit_code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.6.2
3
+ Version: 4.6.3
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.6.2"
3
+ version = "4.6.3"
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