akashcli 4.6.1__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.1 → akashcli-4.6.3}/PKG-INFO +1 -1
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/relay.py +522 -129
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.6.1 → akashcli-4.6.3}/pyproject.toml +1 -1
- {akashcli-4.6.1 → akashcli-4.6.3}/README.md +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/__init__.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/_net.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/auth.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/cli.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/client.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/config.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/crypto.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/env_manager.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/exceptions.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/exec.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/integrity.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/logger.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/models.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/p2p.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/packages.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/restore.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/storage.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/terminal.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/uploader.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli/utils.py +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.6.1 → akashcli-4.6.3}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.6.1 → 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
|
|
|
@@ -211,30 +249,51 @@ def _handle_pty(ws, frame: dict) -> None:
|
|
|
211
249
|
|
|
212
250
|
def _child_setup():
|
|
213
251
|
"""Run inside the child process before exec.
|
|
214
|
-
start_new_session=True already
|
|
215
|
-
|
|
252
|
+
start_new_session=True already called setsid(); claim the slave PTY
|
|
253
|
+
as controlling terminal so bash gets full job control when possible."""
|
|
216
254
|
try:
|
|
217
255
|
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
|
218
256
|
except Exception:
|
|
219
257
|
pass
|
|
220
258
|
|
|
221
259
|
import subprocess as _sp
|
|
260
|
+
import tempfile as _tmp
|
|
261
|
+
|
|
222
262
|
shell = command or os.environ.get("SHELL", "/bin/bash")
|
|
223
263
|
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
264
|
+
# On Android non-root, TIOCSCTTY is blocked by SELinux so bash prints:
|
|
265
|
+
# "cannot set terminal process group … no job control"
|
|
266
|
+
# Fix: launch bash with --rcfile pointing to a temp file that sources
|
|
267
|
+
# ~/.bashrc then runs `set +m` (disable job-control monitoring).
|
|
268
|
+
# This silences the warning while keeping the shell fully interactive.
|
|
269
|
+
_env = {**os.environ,
|
|
270
|
+
"TERM": "xterm-256color",
|
|
271
|
+
"COLUMNS": str(cols), "LINES": str(rows)}
|
|
272
|
+
_bash_env_file = None
|
|
273
|
+
_shell_args = [shell]
|
|
274
|
+
if "bash" in shell.lower():
|
|
275
|
+
try:
|
|
276
|
+
_fd, _bash_env_file = _tmp.mkstemp(prefix=".akash_rc_", suffix=".sh")
|
|
277
|
+
# Source ~/.bashrc if it exists, then disable job-control monitoring
|
|
278
|
+
_rc = (
|
|
279
|
+
b'[ -f ~/.bashrc ] && . ~/.bashrc\n'
|
|
280
|
+
b'set +m\n'
|
|
281
|
+
)
|
|
282
|
+
os.write(_fd, _rc)
|
|
283
|
+
os.close(_fd)
|
|
284
|
+
_shell_args = [shell, "--rcfile", _bash_env_file]
|
|
285
|
+
except Exception:
|
|
286
|
+
_bash_env_file = None
|
|
287
|
+
|
|
227
288
|
try:
|
|
228
289
|
proc = _sp.Popen(
|
|
229
|
-
|
|
290
|
+
_shell_args,
|
|
230
291
|
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
|
|
231
292
|
close_fds=True,
|
|
232
293
|
start_new_session=True,
|
|
233
294
|
preexec_fn=_child_setup,
|
|
234
295
|
cwd=os.path.expanduser("~"),
|
|
235
|
-
env=
|
|
236
|
-
"TERM": "xterm-256color",
|
|
237
|
-
"COLUMNS": str(cols), "LINES": str(rows)},
|
|
296
|
+
env=_env,
|
|
238
297
|
)
|
|
239
298
|
except Exception as e:
|
|
240
299
|
try:
|
|
@@ -326,6 +385,12 @@ def _handle_pty(ws, frame: dict) -> None:
|
|
|
326
385
|
os.close(master_fd)
|
|
327
386
|
except Exception:
|
|
328
387
|
pass
|
|
388
|
+
# Clean up the BASH_ENV temp file if we created one
|
|
389
|
+
if _bash_env_file:
|
|
390
|
+
try:
|
|
391
|
+
os.unlink(_bash_env_file)
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
329
394
|
try:
|
|
330
395
|
ws.send(json.dumps({"type": "exit", "code": proc.returncode or 0}))
|
|
331
396
|
except Exception:
|
|
@@ -769,15 +834,19 @@ def _sh_help() -> None:
|
|
|
769
834
|
print()
|
|
770
835
|
print(f" {_SH_GR}╭─ AKASH SHELL COMMANDS{_SH_RS}")
|
|
771
836
|
cmds = [
|
|
772
|
-
(":h",
|
|
773
|
-
(":
|
|
774
|
-
(":clear",
|
|
775
|
-
(":status",
|
|
776
|
-
(":
|
|
777
|
-
(":
|
|
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"),
|
|
778
845
|
]
|
|
779
846
|
for cmd, desc in cmds:
|
|
780
|
-
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}")
|
|
781
850
|
print(f" {_SH_GR}╰─{_SH_RS}")
|
|
782
851
|
print()
|
|
783
852
|
|
|
@@ -804,21 +873,96 @@ def _sh_history(history: list) -> None:
|
|
|
804
873
|
print()
|
|
805
874
|
|
|
806
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
|
+
|
|
807
945
|
def guest_connect_shell(base_url: str, code: str) -> int:
|
|
808
946
|
"""
|
|
809
947
|
Connect to a guest host via pairing code.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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.
|
|
814
956
|
"""
|
|
957
|
+
import queue as _q
|
|
815
958
|
import select as _sel
|
|
816
959
|
import signal
|
|
960
|
+
import shutil as _sh
|
|
817
961
|
|
|
818
962
|
code = code.strip().upper()
|
|
819
963
|
ws_url = _guest_join_url(base_url, code)
|
|
820
964
|
|
|
821
|
-
# ── Connect
|
|
965
|
+
# ── 1. Connect ────────────────────────────────────────────────────────────
|
|
822
966
|
print(f"\n {_C}◈{_RS} {_DM}connecting…{_RS}", flush=True)
|
|
823
967
|
try:
|
|
824
968
|
ws = _ws_connect(ws_url, open_timeout=15, close_timeout=10)
|
|
@@ -853,145 +997,394 @@ def guest_connect_shell(base_url: str, code: str) -> int:
|
|
|
853
997
|
ws.close()
|
|
854
998
|
raise RuntimeError(first.get("message", "unexpected server response"))
|
|
855
999
|
|
|
856
|
-
# ──
|
|
857
|
-
identity
|
|
858
|
-
|
|
859
|
-
|
|
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
|
|
860
1005
|
|
|
861
|
-
#
|
|
1006
|
+
# readline history
|
|
1007
|
+
_AKASH_DIR.mkdir(parents=True, exist_ok=True)
|
|
1008
|
+
hist_file = _AKASH_DIR / "shell_history"
|
|
862
1009
|
try:
|
|
863
|
-
import
|
|
864
|
-
|
|
1010
|
+
import readline as _rl
|
|
1011
|
+
_rl.read_history_file(str(hist_file))
|
|
1012
|
+
_rl.set_history_length(500)
|
|
865
1013
|
except Exception:
|
|
866
|
-
|
|
1014
|
+
_rl = None # type: ignore
|
|
867
1015
|
|
|
868
|
-
# ──
|
|
869
|
-
|
|
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
|
|
870
1043
|
_old_settings = None
|
|
871
|
-
_stdin_fd = -1
|
|
872
|
-
try:
|
|
873
|
-
import tty as _tty, termios as _term
|
|
874
|
-
_stdin_fd = sys.stdin.fileno()
|
|
875
|
-
_old_settings = _term.tcgetattr(_stdin_fd)
|
|
876
|
-
_tty.setraw(_stdin_fd)
|
|
877
|
-
_raw_ok = True
|
|
878
|
-
except Exception:
|
|
879
|
-
pass
|
|
880
1044
|
|
|
881
|
-
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():
|
|
882
1057
|
if _raw_ok and _old_settings is not None:
|
|
883
1058
|
try:
|
|
884
|
-
import termios as
|
|
885
|
-
|
|
1059
|
+
import termios as _tm2
|
|
1060
|
+
_tm2.tcsetattr(_stdin_fd, _tm2.TCSADRAIN, _old_settings)
|
|
886
1061
|
except Exception:
|
|
887
1062
|
pass
|
|
888
1063
|
|
|
889
|
-
|
|
890
|
-
|
|
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
|
|
891
1085
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
904
1105
|
|
|
905
|
-
# ── Thread: local stdin → WS ──────────────────────────────────────────────
|
|
906
|
-
def _stdin_pump():
|
|
907
1106
|
try:
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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:
|
|
917
1129
|
break
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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:
|
|
923
1157
|
pass
|
|
1158
|
+
finally:
|
|
1159
|
+
_pty_stop.set()
|
|
1160
|
+
try:
|
|
1161
|
+
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
|
1162
|
+
except Exception:
|
|
1163
|
+
pass
|
|
924
1164
|
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
)
|
|
927
1182
|
try:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1183
|
+
ws.send(json.dumps({
|
|
1184
|
+
"type": "run",
|
|
1185
|
+
"command": actual,
|
|
1186
|
+
"cwd": cwd,
|
|
1187
|
+
}))
|
|
931
1188
|
except Exception:
|
|
932
|
-
|
|
933
|
-
try:
|
|
934
|
-
signal.signal(signal.SIGWINCH, _on_resize)
|
|
935
|
-
except Exception:
|
|
936
|
-
pass
|
|
1189
|
+
return [], 1, cwd
|
|
937
1190
|
|
|
938
|
-
|
|
939
|
-
|
|
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
|
|
940
1198
|
|
|
941
|
-
|
|
942
|
-
try:
|
|
943
|
-
while True:
|
|
1199
|
+
while time.time() < deadline:
|
|
944
1200
|
try:
|
|
945
|
-
raw =
|
|
946
|
-
except
|
|
1201
|
+
raw = _q_frames.get(timeout=0.05)
|
|
1202
|
+
except _q.Empty:
|
|
1203
|
+
continue
|
|
1204
|
+
if raw is None:
|
|
947
1205
|
break
|
|
948
1206
|
try:
|
|
949
1207
|
frame = json.loads(raw)
|
|
950
1208
|
except Exception:
|
|
951
1209
|
continue
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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)
|
|
968
1260
|
break
|
|
969
|
-
|
|
970
|
-
|
|
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
|
|
971
1271
|
break
|
|
972
|
-
|
|
1272
|
+
|
|
1273
|
+
elif ft == "ping":
|
|
973
1274
|
try:
|
|
974
1275
|
ws.send(json.dumps({"type": "pong"}))
|
|
975
1276
|
except Exception:
|
|
976
|
-
|
|
977
|
-
|
|
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:
|
|
978
1372
|
pass
|
|
979
1373
|
finally:
|
|
980
|
-
|
|
981
|
-
_restore()
|
|
1374
|
+
_stop_recv.set()
|
|
982
1375
|
try:
|
|
983
|
-
|
|
1376
|
+
ws.send(json.dumps({"type": "close"}))
|
|
984
1377
|
except Exception:
|
|
985
1378
|
pass
|
|
986
1379
|
try:
|
|
987
|
-
ws.
|
|
1380
|
+
ws.close()
|
|
988
1381
|
except Exception:
|
|
989
1382
|
pass
|
|
990
1383
|
try:
|
|
991
|
-
|
|
1384
|
+
if _rl:
|
|
1385
|
+
_rl.write_history_file(str(hist_file))
|
|
992
1386
|
except Exception:
|
|
993
1387
|
pass
|
|
994
1388
|
|
|
995
|
-
|
|
996
|
-
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)
|
|
997
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
|