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.
- {akashcli-4.5.6 → akashcli-4.5.8}/PKG-INFO +1 -1
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/relay.py +336 -125
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.5.6 → akashcli-4.5.8}/pyproject.toml +1 -1
- {akashcli-4.5.6 → akashcli-4.5.8}/README.md +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/__init__.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/_net.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/auth.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/cli.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/client.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/config.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/crypto.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/env_manager.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/exceptions.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/exec.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/integrity.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/logger.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/models.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/p2p.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/packages.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/restore.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/storage.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/terminal.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/uploader.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli/utils.py +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.5.6 → akashcli-4.5.8}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
225
|
-
#
|
|
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
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
722
|
-
|
|
876
|
+
raw = ws.recv(timeout=0.1)
|
|
877
|
+
q.put(raw)
|
|
723
878
|
except Exception:
|
|
724
|
-
|
|
879
|
+
if not stop_recv.is_set():
|
|
880
|
+
q.put(None) # signals disconnection
|
|
881
|
+
break
|
|
725
882
|
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
_restore()
|
|
764
|
-
raise RuntimeError(first.get("message", "unexpected server response"))
|
|
928
|
+
return lines, ec, new_cwd
|
|
765
929
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
994
|
+
_rl.add_history(cmd)
|
|
802
995
|
except Exception:
|
|
803
996
|
pass
|
|
804
997
|
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
1032
|
+
ec = int(ln[len("__AKASH_EXIT__:"):].strip())
|
|
839
1033
|
except Exception:
|
|
840
|
-
|
|
1034
|
+
pass
|
|
1035
|
+
else:
|
|
1036
|
+
clean_lines.append(ln)
|
|
841
1037
|
|
|
842
|
-
|
|
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
|
-
|
|
846
|
-
_restore()
|
|
1049
|
+
stop_recv.set()
|
|
847
1050
|
try:
|
|
848
|
-
|
|
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
|
|
|
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
|