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.
- {akashcli-4.3.1 → akashcli-4.4.0}/PKG-INFO +1 -1
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/cli.py +184 -112
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.3.1 → akashcli-4.4.0}/pyproject.toml +1 -1
- {akashcli-4.3.1 → akashcli-4.4.0}/README.md +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/__init__.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/auth.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/client.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/config.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/crypto.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/env_manager.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/exceptions.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/exec.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/integrity.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/logger.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/models.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/p2p.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/packages.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/relay.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/restore.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/storage.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/terminal.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/uploader.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli/utils.py +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.3.1 → akashcli-4.4.0}/setup.cfg +0 -0
|
@@ -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 (
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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}
|
|
95
|
-
|
|
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
|
-
|
|
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="
|
|
207
|
+
help="Manage individual cloud files.",
|
|
154
208
|
rich_markup_mode=None,
|
|
155
209
|
)
|
|
156
|
-
app.add_typer(cloud_app, name="cloud", help="
|
|
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
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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,
|
|
1726
|
-
op: Optional[str] = typer.Option(None,"--op", "-o", help="Filter by operation type"),
|
|
1727
|
-
raw: bool = typer.Option(False,"--json",
|
|
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
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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")
|
|
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
|