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.
- {akashcli-4.6.2 → akashcli-4.6.3}/PKG-INFO +1 -1
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/relay.py +486 -120
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.6.2 → akashcli-4.6.3}/pyproject.toml +1 -1
- {akashcli-4.6.2 → akashcli-4.6.3}/README.md +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/__init__.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/_net.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/auth.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/cli.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/client.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/config.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/crypto.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/env_manager.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/exceptions.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/exec.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/integrity.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/logger.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/models.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/p2p.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/packages.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/restore.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/storage.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/terminal.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/uploader.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli/utils.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.3}/setup.cfg +0 -0
|
@@ -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
|
|
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=
|
|
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
|
-
|
|
179
|
-
if
|
|
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
|
-
|
|
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",
|
|
800
|
-
(":
|
|
801
|
-
(":clear",
|
|
802
|
-
(":status",
|
|
803
|
-
(":
|
|
804
|
-
(":
|
|
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:<
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
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
|
-
# ──
|
|
884
|
-
identity
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
#
|
|
1006
|
+
# readline history
|
|
1007
|
+
_AKASH_DIR.mkdir(parents=True, exist_ok=True)
|
|
1008
|
+
hist_file = _AKASH_DIR / "shell_history"
|
|
889
1009
|
try:
|
|
890
|
-
import
|
|
891
|
-
|
|
1010
|
+
import readline as _rl
|
|
1011
|
+
_rl.read_history_file(str(hist_file))
|
|
1012
|
+
_rl.set_history_length(500)
|
|
892
1013
|
except Exception:
|
|
893
|
-
|
|
1014
|
+
_rl = None # type: ignore
|
|
894
1015
|
|
|
895
|
-
# ──
|
|
896
|
-
|
|
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
|
|
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
|
|
912
|
-
|
|
1059
|
+
import termios as _tm2
|
|
1060
|
+
_tm2.tcsetattr(_stdin_fd, _tm2.TCSADRAIN, _old_settings)
|
|
913
1061
|
except Exception:
|
|
914
1062
|
pass
|
|
915
1063
|
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
953
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1183
|
+
ws.send(json.dumps({
|
|
1184
|
+
"type": "run",
|
|
1185
|
+
"command": actual,
|
|
1186
|
+
"cwd": cwd,
|
|
1187
|
+
}))
|
|
958
1188
|
except Exception:
|
|
959
|
-
|
|
960
|
-
try:
|
|
961
|
-
signal.signal(signal.SIGWINCH, _on_resize)
|
|
962
|
-
except Exception:
|
|
963
|
-
pass
|
|
1189
|
+
return [], 1, cwd
|
|
964
1190
|
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
969
|
-
try:
|
|
970
|
-
while True:
|
|
1199
|
+
while time.time() < deadline:
|
|
971
1200
|
try:
|
|
972
|
-
raw =
|
|
973
|
-
except
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1272
|
+
|
|
1273
|
+
elif ft == "ping":
|
|
1000
1274
|
try:
|
|
1001
1275
|
ws.send(json.dumps({"type": "pong"}))
|
|
1002
1276
|
except Exception:
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
1008
|
-
_restore()
|
|
1374
|
+
_stop_recv.set()
|
|
1009
1375
|
try:
|
|
1010
|
-
|
|
1376
|
+
ws.send(json.dumps({"type": "close"}))
|
|
1011
1377
|
except Exception:
|
|
1012
1378
|
pass
|
|
1013
1379
|
try:
|
|
1014
|
-
ws.
|
|
1380
|
+
ws.close()
|
|
1015
1381
|
except Exception:
|
|
1016
1382
|
pass
|
|
1017
1383
|
try:
|
|
1018
|
-
|
|
1384
|
+
if _rl:
|
|
1385
|
+
_rl.write_history_file(str(hist_file))
|
|
1019
1386
|
except Exception:
|
|
1020
1387
|
pass
|
|
1021
1388
|
|
|
1022
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|