akashcli 4.3.1__tar.gz → 4.4.0__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 (31) hide show
  1. {akashcli-4.3.1 → akashcli-4.4.0}/PKG-INFO +1 -1
  2. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/cli.py +184 -112
  3. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.3.1 → akashcli-4.4.0}/pyproject.toml +1 -1
  5. {akashcli-4.3.1 → akashcli-4.4.0}/README.md +0 -0
  6. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/__init__.py +0 -0
  7. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/auth.py +0 -0
  8. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/client.py +0 -0
  9. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/config.py +0 -0
  10. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/crypto.py +0 -0
  11. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/env_manager.py +0 -0
  12. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/exceptions.py +0 -0
  13. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/exec.py +0 -0
  14. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/integrity.py +0 -0
  15. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/logger.py +0 -0
  16. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/models.py +0 -0
  17. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/p2p.py +0 -0
  18. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/packages.py +0 -0
  19. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/relay.py +0 -0
  20. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/restore.py +0 -0
  21. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/storage.py +0 -0
  23. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/terminal.py +0 -0
  24. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/uploader.py +0 -0
  25. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/utils.py +0 -0
  26. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.3.1 → akashcli-4.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.3.1
3
+ Version: 4.4.0
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
@@ -74,11 +74,14 @@ class _Spin:
74
74
  sys.stdout.flush()
75
75
 
76
76
 
77
- def _resolve_client(url_override: str = None):
77
+ def _resolve_client(url_override: str = None, require_key: bool = False):
78
78
  """
79
- Return (api_key, base_url).
80
- If no API key is configured, prompt the user interactively rather than
81
- crashing — so commands work on a fresh machine without `akash login`.
79
+ Return (api_key_or_None, base_url).
80
+
81
+ require_key=True — hard-fail with a clear message if no key found
82
+ (used by serve/devices which cannot work without server auth).
83
+ require_key=False — return None for api_key if missing; caller shows warning
84
+ and can offer a local/offline fallback.
82
85
  """
83
86
  import os as _os
84
87
  config = ConfigManager.load()
@@ -88,22 +91,23 @@ def _resolve_client(url_override: str = None):
88
91
  or config.get("api_url", "https://jstore.2bd.net")
89
92
  ).rstrip("/")
90
93
 
91
- api_key = _os.getenv("AKASH_API_KEY") or config.get("api_key")
92
- if not api_key:
94
+ api_key = _os.getenv("AKASH_API_KEY") or config.get("api_key") or None
95
+
96
+ if not api_key and require_key:
93
97
  print(f" {_Y}!{_RS} No API key found.")
94
- print(f" {_DM}Set AKASH_API_KEY env var, or run {_C}akash login{_RS}{_DM} to save credentials.{_RS}")
95
- try:
96
- api_key = typer.prompt(" API key", hide_input=True).strip()
97
- except (EOFError, KeyboardInterrupt):
98
- print()
99
- raise typer.Exit(1)
100
- if not api_key:
101
- _err("API key required.")
102
- raise typer.Exit(1)
98
+ print(f" {_DM}Run {_C}akash login{_RS}{_DM} to save your key, or set {_C}AKASH_API_KEY{_RS}{_DM}.{_RS}")
99
+ raise typer.Exit(1)
103
100
 
104
101
  return api_key, base
105
102
 
106
103
 
104
+ _GUEST_WARNING = (
105
+ f" {_Y}⚠{_RS} {_Y}Running as guest{_RS} — not logged in.\n"
106
+ f" {_DM}Features are limited and your data won't be saved to the cloud.\n"
107
+ f" Run {_RS}{_C}akash login{_RS}{_DM} to unlock full access.{_RS}"
108
+ )
109
+
110
+
107
111
  def _progress_bar(done: int, total: int, width: int = 28) -> str:
108
112
  """Return a plain ASCII progress bar string."""
109
113
  if not total:
@@ -138,22 +142,72 @@ def _upload_with_progress(client: AkashClient, tmp_path: str, password=None) ->
138
142
  # `cloud_app` — file management (akash cloud upload/files/stream/delete/manage/bulk)
139
143
 
140
144
  app = typer.Typer(
141
- help=(
142
- "akash — encrypted cloud backup for your terminal.\n\n"
143
- "Quick start:\n"
144
- " akash login connect your API key\n"
145
- " akash backup save & sync your terminal\n"
146
- " akash pull <CODE> restore on a fresh machine\n\n"
147
- "Run akash cloud --help for file sync commands."
148
- ),
145
+ invoke_without_command=True,
149
146
  rich_markup_mode=None,
150
147
  add_completion=True,
148
+ no_args_is_help=False,
151
149
  )
150
+
151
+ @app.callback(invoke_without_command=True)
152
+ def _main(ctx: typer.Context,
153
+ help: bool = typer.Option(False, "--help", "-h", is_eager=True, expose_value=False,
154
+ callback=lambda v: (_print_help(), raise_exit()) if v else None,
155
+ help="Show this message and exit.")):
156
+ """akash — your encrypted terminal cloud."""
157
+ if ctx.invoked_subcommand is None:
158
+ _print_help()
159
+ raise typer.Exit(0)
160
+
161
+ def raise_exit():
162
+ raise typer.Exit(0)
163
+
164
+ def _print_help():
165
+ C, G, Y, DM, RS = _C, _G, _Y, _DM, _RS
166
+ lines = [
167
+ f"",
168
+ f" {C}akash{RS} {DM}— encrypted cloud backup · remote shell · file vault{RS}",
169
+ f"",
170
+ f" {DM}ACCOUNT{RS}",
171
+ f" {C}login{RS} {DM}[--label NAME]{RS} save your API key + name this device",
172
+ f" {C}whoami{RS} show account info and storage quota",
173
+ f" {C}logout{RS} clear saved credentials",
174
+ f"",
175
+ f" {DM}TERMINAL BACKUP{RS}",
176
+ f" {C}backup{RS} save this terminal environment to the cloud",
177
+ f" {C}restore{RS} restore environment from the cloud",
178
+ f" {C}saves{RS} list your saved environments",
179
+ f" {C}pull{RS} {DM}<CODE>{RS} restore on any machine — no login needed",
180
+ f"",
181
+ f" {DM}REMOTE SHELL{RS}",
182
+ f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, or on another device",
183
+ f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run a command locally or on a device",
184
+ f" {C}serve{RS} {DM}[--label NAME]{RS} make this device reachable for shell/run",
185
+ f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or revoke enrolled devices",
186
+ f"",
187
+ f" {DM}FILES{RS}",
188
+ f" {C}pipe{RS} {DM}[--run CMD]{RS} upload stdin or a command's output",
189
+ f" {C}cloud{RS} {DM}<upload|files|stream…>{RS} manage individual cloud files",
190
+ f"",
191
+ f" {DM}SECURE TRANSFER{RS}",
192
+ f" {C}send{RS} {DM}<USER> <FILE>{RS} encrypt and send a file to another user",
193
+ f" {C}inbox{RS} receive and decrypt files sent to you",
194
+ f" {C}id{RS} show your P2P identity / manage contacts",
195
+ f"",
196
+ f" {DM}OTHER{RS}",
197
+ f" {C}env{RS} {DM}<set|get|list…>{RS} encrypted local .env manager",
198
+ f" {C}log{RS} {DM}[--stats]{RS} operation history (--stats for summary)",
199
+ f" {C}check{RS} {DM}[--seal]{RS} verify file integrity (--seal to re-seal)",
200
+ f"",
201
+ f" {DM}Run {RS}{C}akash <command> --help{RS}{DM} for full options on any command.{RS}",
202
+ f"",
203
+ ]
204
+ print("\n".join(lines))
205
+
152
206
  cloud_app = typer.Typer(
153
- help="akash cloud — upload, stream, manage individual files.",
207
+ help="Manage individual cloud files.",
154
208
  rich_markup_mode=None,
155
209
  )
156
- app.add_typer(cloud_app, name="cloud", help="File sync ops (upload, stream, delete…)")
210
+ app.add_typer(cloud_app, name="cloud", help="upload · stream · delete individual files")
157
211
 
158
212
 
159
213
  # ── Integrity check ───────────────────────────────────────────────────────────
@@ -861,44 +915,37 @@ def pull(
861
915
  _print_reload_hint(meta)
862
916
 
863
917
 
864
- # ── akash seal ────────────────────────────────────────────────────────────────
918
+ # ── akash check (seal + integrity merged) ────────────────────────────────────
865
919
 
866
920
  @app.command()
867
- def seal():
868
- """Hash all source files and write a signed integrity manifest."""
869
- from .integrity import seal as _seal, _manifest_path
870
- _info("sealing source integrity…")
871
- hashes = _seal()
872
- _ok(f"hashed {len(hashes)} source files")
873
- _ok(f"manifest → {_dim(str(_manifest_path()))}")
874
- _info("tamper detection active for backup, restore, and pull")
875
-
876
-
877
- # ── akash integrity ───────────────────────────────────────────────────────────
921
+ def check(
922
+ seal: bool = typer.Option(False, "--seal", help="Re-seal the integrity manifest"),
923
+ ):
924
+ """Verify file integrity — or re-seal with --seal.
878
925
 
879
- @app.command()
880
- def integrity():
881
- """Check all installed source files match the sealed manifest."""
882
- from .integrity import status as _status
883
- s = _status()
884
- if not s["sealed"]:
885
- _warn(f"not sealed: {s['message']}")
886
- _info("run `akash seal` to enable tamper detection")
887
- return
888
- if s["ok"]:
889
- _ok(f"integrity OK — {s['files']} files verified")
926
+ Examples:
927
+ akash check verify all source files are untampered
928
+ akash check --seal hash source files and write a new manifest
929
+ """
930
+ if seal:
931
+ from .integrity import seal as _seal, _manifest_path
932
+ _info("sealing source integrity…")
933
+ hashes = _seal()
934
+ _ok(f"hashed {len(hashes)} source files")
935
+ _ok(f"manifest → {_dim(str(_manifest_path()))}")
936
+ _info("tamper detection active for backup, restore, and pull")
890
937
  else:
891
- _err("tamper detected!")
892
- print(s["message"], file=sys.stderr)
893
- raise typer.Exit(2)
894
-
895
-
896
- # ── akash docs ────────────────────────────────────────────────────────────────
897
-
898
- @app.command()
899
- def docs():
900
- """Open the documentation."""
901
- webbrowser.open("https://github.com/juniorsir/akashcli")
938
+ from .integrity import status as _status
939
+ s = _status()
940
+ if not s["sealed"]:
941
+ _warn(f"not sealed yet — run {_cy('akash check --seal')} to enable tamper detection")
942
+ return
943
+ if s["ok"]:
944
+ _ok(f"integrity OK — {s['files']} files verified")
945
+ else:
946
+ _err("tamper detected!")
947
+ print(s["message"], file=sys.stderr)
948
+ raise typer.Exit(2)
902
949
 
903
950
 
904
951
  # ── akash logout ─────────────────────────────────────────────────────────────
@@ -936,6 +983,12 @@ def run(
936
983
  try:
937
984
  api_key, base = _resolve_client(url)
938
985
 
986
+ # ── No key + device flag → relay needs auth ───────────────────────
987
+ if device is not None and not api_key:
988
+ print(_GUEST_WARNING)
989
+ _err("relay requires an API key. Run `akash login` first.")
990
+ raise typer.Exit(1)
991
+
939
992
  # ── Device relay path ─────────────────────────────────────────────
940
993
  if device is not None:
941
994
  from .relay import connect_run
@@ -952,6 +1005,22 @@ def run(
952
1005
  raise typer.Exit(exit_code)
953
1006
  return
954
1007
 
1008
+ # ── No key → local execution fallback ────────────────────────────
1009
+ if not api_key:
1010
+ print(_GUEST_WARNING)
1011
+ _p()
1012
+ _info(f"running locally: {command}")
1013
+ _p()
1014
+ import subprocess as _sp
1015
+ result = _sp.run(command, shell=True)
1016
+ _p()
1017
+ if result.returncode == 0:
1018
+ _info("exited 0")
1019
+ else:
1020
+ _warn(f"exited {result.returncode}")
1021
+ raise typer.Exit(result.returncode)
1022
+ return
1023
+
955
1024
  if reauth:
956
1025
  _clear_cached_token()
957
1026
  _info("cleared cached token — re-authenticating…")
@@ -1079,6 +1148,12 @@ def shell(
1079
1148
  try:
1080
1149
  api_key, base = _resolve_client(url)
1081
1150
 
1151
+ # ── No key + device flag → relay needs auth ───────────────────────
1152
+ if device is not None and not api_key:
1153
+ print(_GUEST_WARNING)
1154
+ _err("relay requires an API key. Run `akash login` first.")
1155
+ raise typer.Exit(1)
1156
+
1082
1157
  # ── Device relay path ─────────────────────────────────────────────
1083
1158
  if device is not None:
1084
1159
  from .relay import connect_shell
@@ -1095,6 +1170,20 @@ def shell(
1095
1170
  raise typer.Exit(exit_code)
1096
1171
  return
1097
1172
 
1173
+ # ── No key → local PTY shell fallback ────────────────────────────
1174
+ if not api_key:
1175
+ import shutil as _sh
1176
+ print(_GUEST_WARNING)
1177
+ _p()
1178
+ shell_bin = os.environ.get("SHELL") or _sh.which("bash") or _sh.which("sh") or "/bin/sh"
1179
+ _info(f"opening local shell {_dim(shell_bin)}")
1180
+ _p()
1181
+ import subprocess as _sp
1182
+ result = _sp.run([shell_bin])
1183
+ _p()
1184
+ _info("session closed")
1185
+ raise typer.Exit(result.returncode)
1186
+
1098
1187
  if reauth:
1099
1188
  _clear_cached_token()
1100
1189
  _info("cleared cached token — re-authenticating…")
@@ -1136,7 +1225,7 @@ def devices(
1136
1225
  akash devices --revoke laptop remove a device
1137
1226
  """
1138
1227
  try:
1139
- api_key, base = _resolve_client()
1228
+ api_key, base = _resolve_client(require_key=True)
1140
1229
  except typer.Exit:
1141
1230
  raise
1142
1231
  except Exception as e:
@@ -1242,7 +1331,7 @@ def serve(
1242
1331
  return
1243
1332
 
1244
1333
  try:
1245
- api_key, base = _resolve_client(url)
1334
+ api_key, base = _resolve_client(url, require_key=True)
1246
1335
  except typer.Exit:
1247
1336
  raise
1248
1337
  except Exception as e:
@@ -1718,26 +1807,51 @@ def pipe(
1718
1807
  _p()
1719
1808
 
1720
1809
 
1721
- # ── akash log ─────────────────────────────────────────────────────────────────
1810
+ # ── akash log (+ --stats) ─────────────────────────────────────────────────────
1722
1811
 
1723
1812
  @app.command(name="log")
1724
1813
  def cmd_log(
1725
- last: int = typer.Option(20, "--last", "-n", help="Show last N entries"),
1726
- op: Optional[str] = typer.Option(None,"--op", "-o", help="Filter by operation type"),
1727
- raw: bool = typer.Option(False,"--json", help="Raw JSON output"),
1814
+ last: int = typer.Option(20, "--last", "-n", help="Show last N entries"),
1815
+ op: Optional[str] = typer.Option(None, "--op", "-o", help="Filter by operation type"),
1816
+ raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
1817
+ stats: bool = typer.Option(False, "--stats", "-s", help="Show summary instead of list"),
1728
1818
  ):
1729
- """Show operation history from ~/.akash/log.jsonl.
1819
+ """Show operation history — or a summary with --stats.
1730
1820
 
1731
1821
  Examples:
1732
1822
  akash log last 20 operations
1823
+ akash log --stats summary with counts per operation
1733
1824
  akash log --last 50 last 50 operations
1734
1825
  akash log --op backup only backup operations
1735
1826
  akash log --json raw JSON (pipe to jq)
1736
1827
  """
1737
- from .logger import read_log
1828
+ from .logger import read_log, log_summary
1738
1829
 
1739
- entries = read_log(last_n=last, op_filter=op)
1830
+ if stats:
1831
+ s = log_summary()
1832
+ if s["total"] == 0:
1833
+ _info("no log entries yet — run some commands first")
1834
+ return
1835
+ _p()
1836
+ print(f" {_C}◈{_RS} operation history")
1837
+ _p()
1838
+ _info(f"total {_ye(str(s['total']))} "
1839
+ f"({_G}{s['ok']} ok{_RS}" +
1840
+ (f" {_R}{s['fail']} failed{_RS}" if s["fail"] else "") + ")")
1841
+ _p()
1842
+ for oper, count in sorted(s["by_op"].items(), key=lambda x: -x[1]):
1843
+ bar = "█" * min(count * 2, 24)
1844
+ print(f" {_C}{oper:<14}{_RS} {_DM}{bar}{_RS} {count}")
1845
+ _p()
1846
+ uploads = read_log(last_n=100, op_filter=None)
1847
+ recent = [e for e in uploads if e.get("op") in ("backup", "pipe", "send", "cloud_upload") and e.get("code")]
1848
+ if recent:
1849
+ lu = recent[-1]
1850
+ print(f" {_DM}last upload {_RS}{_ye(str(lu.get('code','?')))} {_dim(str(lu.get('ts',''))[:16].replace('T',' '))}")
1851
+ _p()
1852
+ return
1740
1853
 
1854
+ entries = read_log(last_n=last, op_filter=op)
1741
1855
  if not entries:
1742
1856
  _info("no log entries yet")
1743
1857
  return
@@ -1752,11 +1866,7 @@ def cmd_log(
1752
1866
  oper = e.get("op", "?")
1753
1867
  ok = e.get("ok", True)
1754
1868
  icon = f"{_G}✓{_RS}" if ok else f"{_R}✗{_RS}"
1755
-
1756
- # Build a short detail string from the payload
1757
1869
  detail_parts = []
1758
- for skip in ("ts", "op", "ok", "error"):
1759
- pass
1760
1870
  for k, v in e.items():
1761
1871
  if k in ("ts", "op", "ok"):
1762
1872
  continue
@@ -1768,56 +1878,18 @@ def cmd_log(
1768
1878
  detail_parts.append(f"{k}={_dim(str(v))}")
1769
1879
  else:
1770
1880
  detail_parts.append(f"{k}={_dim(str(v)[:40])}")
1771
-
1772
1881
  detail = " ".join(detail_parts[:4])
1773
1882
  print(f" {icon} {_GR}{ts}{_RS} {_C}{oper:<12}{_RS} {detail}")
1774
1883
  _p()
1775
1884
 
1776
1885
 
1777
- # ── akash stats ───────────────────────────────────────────────────────────────
1778
-
1779
- @app.command()
1780
- def stats():
1781
- """Show a summary of your akash operation history."""
1782
- from .logger import log_summary, read_log
1783
-
1784
- s = log_summary()
1785
- if s["total"] == 0:
1786
- _info("no log entries yet — run some commands first")
1787
- return
1788
-
1789
- _p()
1790
- print(f" {_C}◈{_RS} operation history")
1791
- _p()
1792
- _info(f"total ops {_ye(str(s['total']))}")
1793
- _info(f"succeeded {_G}{s['ok']}{_RS}")
1794
- if s["fail"]:
1795
- _info(f"failed {_R}{s['fail']}{_RS}")
1796
- _p()
1797
- print(f" {_DM}by operation:{_RS}")
1798
- for op, count in sorted(s["by_op"].items(), key=lambda x: -x[1]):
1799
- bar_w = min(count * 2, 20)
1800
- bar = "█" * bar_w
1801
- print(f" {_C}{op:<14}{_RS} {_dim(bar)} {count}")
1802
- _p()
1803
-
1804
- # Last upload code
1805
- uploads = read_log(last_n=100, op_filter=None)
1806
- recent = [e for e in uploads if e.get("op") in ("backup", "pipe", "send", "cloud_upload") and e.get("code")]
1807
- if recent:
1808
- last_up = recent[-1]
1809
- print(f" {_DM}last upload {_RS}{_ye(str(last_up.get('code', '?')))} "
1810
- f"{_dim(str(last_up.get('ts', ''))[:16].replace('T',' '))}")
1811
- _p()
1812
-
1813
-
1814
1886
  # ── akash env ─────────────────────────────────────────────────────────────────
1815
1887
 
1816
1888
  env_app = typer.Typer(
1817
- help="akash env — encrypted local .env file manager.",
1889
+ help="Encrypted local .env store — set, get, push, pull.",
1818
1890
  rich_markup_mode=None,
1819
1891
  )
1820
- app.add_typer(env_app, name="env", help="Encrypted .env manager (set/get/list/push/pull)")
1892
+ app.add_typer(env_app, name="env", help="set · get · push · pull encrypted .env variables")
1821
1893
 
1822
1894
 
1823
1895
  @env_app.command("list")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.3.1
3
+ Version: 4.4.0
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.3.1"
3
+ version = "4.4.0"
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