akashcli 4.3.2__tar.gz → 4.4.1__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.2 → akashcli-4.4.1}/PKG-INFO +1 -1
  2. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/cli.py +216 -140
  3. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.3.2 → akashcli-4.4.1}/pyproject.toml +1 -1
  5. {akashcli-4.3.2 → akashcli-4.4.1}/README.md +0 -0
  6. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/__init__.py +0 -0
  7. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/auth.py +0 -0
  8. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/client.py +0 -0
  9. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/config.py +0 -0
  10. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/crypto.py +0 -0
  11. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/env_manager.py +0 -0
  12. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/exceptions.py +0 -0
  13. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/exec.py +0 -0
  14. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/integrity.py +0 -0
  15. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/logger.py +0 -0
  16. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/models.py +0 -0
  17. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/p2p.py +0 -0
  18. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/packages.py +0 -0
  19. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/relay.py +0 -0
  20. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/restore.py +0 -0
  21. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/storage.py +0 -0
  23. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/terminal.py +0 -0
  24. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/uploader.py +0 -0
  25. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli/utils.py +0 -0
  26. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.3.2 → akashcli-4.4.1}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.3.2 → akashcli-4.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.3.2
3
+ Version: 4.4.1
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
@@ -142,22 +142,72 @@ def _upload_with_progress(client: AkashClient, tmp_path: str, password=None) ->
142
142
  # `cloud_app` — file management (akash cloud upload/files/stream/delete/manage/bulk)
143
143
 
144
144
  app = typer.Typer(
145
- help=(
146
- "akash — encrypted cloud backup for your terminal.\n\n"
147
- "Quick start:\n"
148
- " akash login connect your API key\n"
149
- " akash backup save & sync your terminal\n"
150
- " akash pull <CODE> restore on a fresh machine\n\n"
151
- "Run akash cloud --help for file sync commands."
152
- ),
145
+ invoke_without_command=True,
153
146
  rich_markup_mode=None,
154
147
  add_completion=True,
148
+ no_args_is_help=False,
155
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
+
156
206
  cloud_app = typer.Typer(
157
- help="akash cloud — upload, stream, manage individual files.",
207
+ help="Manage individual cloud files.",
158
208
  rich_markup_mode=None,
159
209
  )
160
- 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")
161
211
 
162
212
 
163
213
  # ── Integrity check ───────────────────────────────────────────────────────────
@@ -865,44 +915,37 @@ def pull(
865
915
  _print_reload_hint(meta)
866
916
 
867
917
 
868
- # ── akash seal ────────────────────────────────────────────────────────────────
918
+ # ── akash check (seal + integrity merged) ────────────────────────────────────
869
919
 
870
920
  @app.command()
871
- def seal():
872
- """Hash all source files and write a signed integrity manifest."""
873
- from .integrity import seal as _seal, _manifest_path
874
- _info("sealing source integrity…")
875
- hashes = _seal()
876
- _ok(f"hashed {len(hashes)} source files")
877
- _ok(f"manifest → {_dim(str(_manifest_path()))}")
878
- _info("tamper detection active for backup, restore, and pull")
879
-
880
-
881
- # ── 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.
882
925
 
883
- @app.command()
884
- def integrity():
885
- """Check all installed source files match the sealed manifest."""
886
- from .integrity import status as _status
887
- s = _status()
888
- if not s["sealed"]:
889
- _warn(f"not sealed: {s['message']}")
890
- _info("run `akash seal` to enable tamper detection")
891
- return
892
- if s["ok"]:
893
- _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")
894
937
  else:
895
- _err("tamper detected!")
896
- print(s["message"], file=sys.stderr)
897
- raise typer.Exit(2)
898
-
899
-
900
- # ── akash docs ────────────────────────────────────────────────────────────────
901
-
902
- @app.command()
903
- def docs():
904
- """Open the documentation."""
905
- 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)
906
949
 
907
950
 
908
951
  # ── akash logout ─────────────────────────────────────────────────────────────
@@ -923,27 +966,37 @@ def run(
923
966
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
924
967
  save: bool = typer.Option(False, "--save", help="Upload command output to cloud"),
925
968
  save_name: Optional[str] = typer.Option(None, "--save-name", help="Filename for saved output"),
926
- device: Optional[str] = typer.Option(None, "--device","-d",help="Target device label (default: auto)"),
969
+ device: Optional[str] = typer.Option(None, "--device", "-d",
970
+ help="Run on a device (set up with: akash serve --label NAME)"),
927
971
  ):
928
- """Run a command — on the server or on a specific enrolled device.
972
+ """Run a command — locally, on the server, or on an enrolled device.
929
973
 
930
- Token cached 24 h — instant reconnect every time.
974
+ No login needed:
975
+ akash run "df -h" runs locally
976
+ akash run "ls" --save runs locally, uploads output
931
977
 
932
- Examples:
933
- akash run "ls -la ~" # run on server
934
- akash run "df -h" --device termux # run on your phone
935
- akash run "df -h" --device laptop # run on your laptop
936
- akash run "pip list" --save # save output to cloud
978
+ With login:
979
+ akash run "df -h" runs on server
980
+ akash run "df -h" --device termux runs on your phone
981
+ akash run "pip list" --save runs on server, saves output
982
+
983
+ To make a device reachable:
984
+ 1. On that device: akash login --label termux && akash serve
985
+ 2. From anywhere: akash run "cmd" --device termux
937
986
  """
938
987
  from .exec import acquire_exec_token, run_command, _clear_cached_token
939
988
 
940
989
  try:
941
990
  api_key, base = _resolve_client(url)
942
991
 
943
- # ── No key + device flag → relay needs auth ───────────────────────
992
+ # ── --device but no key ───────────────────────────────────────────
944
993
  if device is not None and not api_key:
945
- print(_GUEST_WARNING)
946
- _err("relay requires an API key. Run `akash login` first.")
994
+ _warn("relay requires an API key.")
995
+ _p()
996
+ _info(f"To run on {_cy(device.strip() or 'a device')} you need to log in first:")
997
+ _info(f" {_cy('akash login')} save your API key on this machine")
998
+ _info(f" {_cy('akash serve')} run on the target device to make it reachable")
999
+ _p()
947
1000
  raise typer.Exit(1)
948
1001
 
949
1002
  # ── Device relay path ─────────────────────────────────────────────
@@ -1087,39 +1140,52 @@ def _build_ws_url_for_run(token: str, base_url: str) -> str:
1087
1140
  def shell(
1088
1141
  url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
1089
1142
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
1090
- device: Optional[str] = typer.Option(None, "--device","-d", help="Target device label (default: auto)"),
1143
+ device: Optional[str] = typer.Option(None, "--device", "-d",
1144
+ help="Device label to connect to (set up with: akash serve --label NAME)"),
1091
1145
  ):
1092
- """Open an interactive shell — on the server or on an enrolled device.
1146
+ """Open a shell — local, server, or on any enrolled device.
1093
1147
 
1094
- Full PTY — colours, arrow keys, tab completion, Ctrl-C all work.
1095
- Token cached 24 h — instant reconnect.
1148
+ No login needed:
1149
+ akash shell open a local bash shell
1096
1150
 
1097
- Examples:
1098
- akash shell # server shell
1099
- akash shell --device termux # shell on your phone
1100
- akash shell --device laptop # shell on your laptop
1101
- akash shell --device # auto (works if only 1 device enrolled)
1151
+ With login (akash login):
1152
+ akash shell full server shell (persistent, coloured)
1153
+ akash shell --device termux relay shell on your phone
1154
+ akash shell --device laptop relay shell on your laptop
1155
+ akash shell --device auto-pick (works if only 1 device enrolled)
1156
+
1157
+ To make a device reachable:
1158
+ 1. On that device: akash login --label termux && akash serve
1159
+ 2. From anywhere: akash shell --device termux
1102
1160
  """
1103
1161
  from .exec import acquire_exec_token, run_shell, _clear_cached_token
1162
+ import shutil as _sh, subprocess as _sp
1104
1163
 
1105
1164
  try:
1106
1165
  api_key, base = _resolve_client(url)
1107
1166
 
1108
- # ── No key + device flag → relay needs auth ───────────────────────
1167
+ # ── --device flag but no key ──────────────────────────────────────
1109
1168
  if device is not None and not api_key:
1110
- print(_GUEST_WARNING)
1111
- _err("relay requires an API key. Run `akash login` first.")
1169
+ _warn("relay requires an API key.")
1170
+ _p()
1171
+ _info(f"To connect to {_cy(device.strip() or 'a device')} you need to log in first:")
1172
+ _info(f" {_cy('akash login')} save your API key on this machine")
1173
+ _info(f" {_cy('akash serve')} run on the target device to make it reachable")
1174
+ _p()
1112
1175
  raise typer.Exit(1)
1113
1176
 
1114
1177
  # ── Device relay path ─────────────────────────────────────────────
1115
1178
  if device is not None:
1116
1179
  from .relay import connect_shell
1117
1180
  label = device.strip() if device.strip() else None
1118
- _info(f"connecting to device {_cy(label or 'auto')}…")
1181
+ _p()
1182
+ _info(f"connecting to {_cy(label or 'auto')}…")
1119
1183
  try:
1120
1184
  exit_code = connect_shell(api_key, base, target=label)
1121
1185
  except RuntimeError as e:
1122
1186
  _err(str(e))
1187
+ _p()
1188
+ _info(f"Make sure the device is running: {_cy('akash serve')}")
1123
1189
  raise typer.Exit(1)
1124
1190
  _p()
1125
1191
  _info("session closed")
@@ -1127,20 +1193,17 @@ def shell(
1127
1193
  raise typer.Exit(exit_code)
1128
1194
  return
1129
1195
 
1130
- # ── No key → local PTY shell fallback ────────────────────────────
1196
+ # ── No key → silent local shell ───────────────────────────────────
1131
1197
  if not api_key:
1132
- import shutil as _sh
1133
- print(_GUEST_WARNING)
1134
- _p()
1135
1198
  shell_bin = os.environ.get("SHELL") or _sh.which("bash") or _sh.which("sh") or "/bin/sh"
1136
- _info(f"opening local shell {_dim(shell_bin)}")
1199
+ _info(f"local shell {_dim(shell_bin)} {_dim('· tip: run akash login to unlock remote shells')}")
1137
1200
  _p()
1138
- import subprocess as _sp
1139
1201
  result = _sp.run([shell_bin])
1140
1202
  _p()
1141
1203
  _info("session closed")
1142
1204
  raise typer.Exit(result.returncode)
1143
1205
 
1206
+ # ── Server shell ──────────────────────────────────────────────────
1144
1207
  if reauth:
1145
1208
  _clear_cached_token()
1146
1209
  _info("cleared cached token — re-authenticating…")
@@ -1172,23 +1235,43 @@ def devices(
1172
1235
  revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke (unenroll) a device by label"),
1173
1236
  raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
1174
1237
  ):
1175
- """List enrolled devices or revoke one.
1238
+ """List enrolled devices — or revoke one.
1176
1239
 
1177
- Each device you log in on with `akash login --label` appears here.
1178
- The server tracks when each device last authenticated.
1240
+ Each device you run `akash serve` on appears here.
1241
+ Connect from anywhere with: akash shell --device NAME
1179
1242
 
1180
1243
  Examples:
1181
1244
  akash devices list all enrolled devices
1182
1245
  akash devices --revoke laptop remove a device
1246
+
1247
+ Setup:
1248
+ akash login --label phone name this device
1249
+ akash serve register it as reachable
1183
1250
  """
1184
1251
  try:
1185
- api_key, base = _resolve_client(require_key=True)
1252
+ api_key, base = _resolve_client(require_key=False)
1186
1253
  except typer.Exit:
1187
1254
  raise
1188
1255
  except Exception as e:
1189
1256
  _err(str(e))
1190
1257
  raise typer.Exit(1)
1191
1258
 
1259
+ if not api_key:
1260
+ _p()
1261
+ _info("no devices yet — here's how to set up remote access:")
1262
+ _p()
1263
+ print(f" {_DM}Step 1 — on each device you want to reach:{_RS}")
1264
+ print(f" {_cy('akash login --label termux')} {_DM}(name this device){_RS}")
1265
+ print(f" {_cy('akash serve')} {_DM}(keep running — makes it reachable){_RS}")
1266
+ _p()
1267
+ print(f" {_DM}Step 2 — from anywhere:{_RS}")
1268
+ print(f" {_cy('akash shell --device termux')} {_DM}(open a shell on it){_RS}")
1269
+ print(f" {_cy('akash run --device termux \"cmd\"')} {_DM}(run a command on it){_RS}")
1270
+ _p()
1271
+ _info(f"Run {_cy('akash login')} first to save your API key.")
1272
+ _p()
1273
+ raise typer.Exit(0)
1274
+
1192
1275
  _hdrs = {"x-api-key": api_key, "Origin": "https://akash.cli"}
1193
1276
 
1194
1277
  if revoke:
@@ -1255,18 +1338,17 @@ def serve(
1255
1338
  status: bool = typer.Option(False, "--status", help="Show serve daemon status"),
1256
1339
  stop: bool = typer.Option(False, "--stop", help="Stop background serve daemon"),
1257
1340
  ):
1258
- """Make this device reachable via `akash shell --device` and `akash run --device`.
1341
+ """Make this device reachable for remote shell and run.
1259
1342
 
1260
- Registers this machine with the relay server and waits for incoming
1261
- shell/run requests. The connection is brokered through your cloud server —
1262
- no public IP required.
1343
+ Run this once on a device to register it. Then from any other
1344
+ machine: akash shell --device NAME or akash run --device NAME
1263
1345
 
1264
1346
  Examples:
1265
- akash serve # foreground (Ctrl-C to stop)
1266
- akash serve --daemon # background daemon
1267
- akash serve --status # check if daemon is running
1268
- akash serve --stop # kill daemon
1269
- akash serve --label termux # override device label
1347
+ akash serve start in foreground (Ctrl-C to stop)
1348
+ akash serve --daemon run as background daemon
1349
+ akash serve --status check if daemon is running
1350
+ akash serve --stop stop the daemon
1351
+ akash serve --label myphone use a custom label
1270
1352
  """
1271
1353
  from .relay import serve as _serve, serve_status, serve_stop
1272
1354
 
@@ -1277,7 +1359,8 @@ def serve(
1277
1359
  _ok(f"running {_dim('pid ' + pid_str)}")
1278
1360
  _info(f"logs {st['log']}")
1279
1361
  else:
1280
- _info("not running — start with: akash serve --daemon")
1362
+ _info("not running")
1363
+ _info(f"start with: {_cy('akash serve')} or {_cy('akash serve --daemon')}")
1281
1364
  return
1282
1365
 
1283
1366
  if stop:
@@ -1288,13 +1371,23 @@ def serve(
1288
1371
  return
1289
1372
 
1290
1373
  try:
1291
- api_key, base = _resolve_client(url, require_key=True)
1374
+ api_key, base = _resolve_client(url, require_key=False)
1292
1375
  except typer.Exit:
1293
1376
  raise
1294
1377
  except Exception as e:
1295
1378
  _err(str(e))
1296
1379
  raise typer.Exit(1)
1297
1380
 
1381
+ if not api_key:
1382
+ _p()
1383
+ _warn("not logged in — can't register with relay server.")
1384
+ _p()
1385
+ _info("Run this first:")
1386
+ print(f" {_cy('akash login --label myphone')} {_DM}# name this device{_RS}")
1387
+ print(f" {_cy('akash serve')} {_DM}# then run this{_RS}")
1388
+ _p()
1389
+ raise typer.Exit(1)
1390
+
1298
1391
  # Resolve label: flag > config > "default"
1299
1392
  from .exec import _device_label as _cfg_label
1300
1393
  resolved_label = (label or _cfg_label() or "default").strip()
@@ -1764,26 +1857,51 @@ def pipe(
1764
1857
  _p()
1765
1858
 
1766
1859
 
1767
- # ── akash log ─────────────────────────────────────────────────────────────────
1860
+ # ── akash log (+ --stats) ─────────────────────────────────────────────────────
1768
1861
 
1769
1862
  @app.command(name="log")
1770
1863
  def cmd_log(
1771
- last: int = typer.Option(20, "--last", "-n", help="Show last N entries"),
1772
- op: Optional[str] = typer.Option(None,"--op", "-o", help="Filter by operation type"),
1773
- raw: bool = typer.Option(False,"--json", help="Raw JSON output"),
1864
+ last: int = typer.Option(20, "--last", "-n", help="Show last N entries"),
1865
+ op: Optional[str] = typer.Option(None, "--op", "-o", help="Filter by operation type"),
1866
+ raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
1867
+ stats: bool = typer.Option(False, "--stats", "-s", help="Show summary instead of list"),
1774
1868
  ):
1775
- """Show operation history from ~/.akash/log.jsonl.
1869
+ """Show operation history — or a summary with --stats.
1776
1870
 
1777
1871
  Examples:
1778
1872
  akash log last 20 operations
1873
+ akash log --stats summary with counts per operation
1779
1874
  akash log --last 50 last 50 operations
1780
1875
  akash log --op backup only backup operations
1781
1876
  akash log --json raw JSON (pipe to jq)
1782
1877
  """
1783
- from .logger import read_log
1878
+ from .logger import read_log, log_summary
1784
1879
 
1785
- entries = read_log(last_n=last, op_filter=op)
1880
+ if stats:
1881
+ s = log_summary()
1882
+ if s["total"] == 0:
1883
+ _info("no log entries yet — run some commands first")
1884
+ return
1885
+ _p()
1886
+ print(f" {_C}◈{_RS} operation history")
1887
+ _p()
1888
+ _info(f"total {_ye(str(s['total']))} "
1889
+ f"({_G}{s['ok']} ok{_RS}" +
1890
+ (f" {_R}{s['fail']} failed{_RS}" if s["fail"] else "") + ")")
1891
+ _p()
1892
+ for oper, count in sorted(s["by_op"].items(), key=lambda x: -x[1]):
1893
+ bar = "█" * min(count * 2, 24)
1894
+ print(f" {_C}{oper:<14}{_RS} {_DM}{bar}{_RS} {count}")
1895
+ _p()
1896
+ uploads = read_log(last_n=100, op_filter=None)
1897
+ recent = [e for e in uploads if e.get("op") in ("backup", "pipe", "send", "cloud_upload") and e.get("code")]
1898
+ if recent:
1899
+ lu = recent[-1]
1900
+ print(f" {_DM}last upload {_RS}{_ye(str(lu.get('code','?')))} {_dim(str(lu.get('ts',''))[:16].replace('T',' '))}")
1901
+ _p()
1902
+ return
1786
1903
 
1904
+ entries = read_log(last_n=last, op_filter=op)
1787
1905
  if not entries:
1788
1906
  _info("no log entries yet")
1789
1907
  return
@@ -1798,11 +1916,7 @@ def cmd_log(
1798
1916
  oper = e.get("op", "?")
1799
1917
  ok = e.get("ok", True)
1800
1918
  icon = f"{_G}✓{_RS}" if ok else f"{_R}✗{_RS}"
1801
-
1802
- # Build a short detail string from the payload
1803
1919
  detail_parts = []
1804
- for skip in ("ts", "op", "ok", "error"):
1805
- pass
1806
1920
  for k, v in e.items():
1807
1921
  if k in ("ts", "op", "ok"):
1808
1922
  continue
@@ -1814,56 +1928,18 @@ def cmd_log(
1814
1928
  detail_parts.append(f"{k}={_dim(str(v))}")
1815
1929
  else:
1816
1930
  detail_parts.append(f"{k}={_dim(str(v)[:40])}")
1817
-
1818
1931
  detail = " ".join(detail_parts[:4])
1819
1932
  print(f" {icon} {_GR}{ts}{_RS} {_C}{oper:<12}{_RS} {detail}")
1820
1933
  _p()
1821
1934
 
1822
1935
 
1823
- # ── akash stats ───────────────────────────────────────────────────────────────
1824
-
1825
- @app.command()
1826
- def stats():
1827
- """Show a summary of your akash operation history."""
1828
- from .logger import log_summary, read_log
1829
-
1830
- s = log_summary()
1831
- if s["total"] == 0:
1832
- _info("no log entries yet — run some commands first")
1833
- return
1834
-
1835
- _p()
1836
- print(f" {_C}◈{_RS} operation history")
1837
- _p()
1838
- _info(f"total ops {_ye(str(s['total']))}")
1839
- _info(f"succeeded {_G}{s['ok']}{_RS}")
1840
- if s["fail"]:
1841
- _info(f"failed {_R}{s['fail']}{_RS}")
1842
- _p()
1843
- print(f" {_DM}by operation:{_RS}")
1844
- for op, count in sorted(s["by_op"].items(), key=lambda x: -x[1]):
1845
- bar_w = min(count * 2, 20)
1846
- bar = "█" * bar_w
1847
- print(f" {_C}{op:<14}{_RS} {_dim(bar)} {count}")
1848
- _p()
1849
-
1850
- # Last upload code
1851
- uploads = read_log(last_n=100, op_filter=None)
1852
- recent = [e for e in uploads if e.get("op") in ("backup", "pipe", "send", "cloud_upload") and e.get("code")]
1853
- if recent:
1854
- last_up = recent[-1]
1855
- print(f" {_DM}last upload {_RS}{_ye(str(last_up.get('code', '?')))} "
1856
- f"{_dim(str(last_up.get('ts', ''))[:16].replace('T',' '))}")
1857
- _p()
1858
-
1859
-
1860
1936
  # ── akash env ─────────────────────────────────────────────────────────────────
1861
1937
 
1862
1938
  env_app = typer.Typer(
1863
- help="akash env — encrypted local .env file manager.",
1939
+ help="Encrypted local .env store — set, get, push, pull.",
1864
1940
  rich_markup_mode=None,
1865
1941
  )
1866
- app.add_typer(env_app, name="env", help="Encrypted .env manager (set/get/list/push/pull)")
1942
+ app.add_typer(env_app, name="env", help="set · get · push · pull encrypted .env variables")
1867
1943
 
1868
1944
 
1869
1945
  @env_app.command("list")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.3.2
3
+ Version: 4.4.1
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.2"
3
+ version = "4.4.1"
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