ravmem-cli 1.1.1__tar.gz → 1.3.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.
- {ravmem_cli-1.1.1/ravmem_cli.egg-info → ravmem_cli-1.3.0}/PKG-INFO +5 -3
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/README.md +4 -2
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/pyproject.toml +1 -1
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/license.py +181 -52
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/main.py +10 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0/ravmem_cli.egg-info}/PKG-INFO +5 -3
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/LICENSE +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/__init__.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/_common.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/_errors.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/analyze.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/audit.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/auth.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/browse.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/config_cmd.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/diff.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/doctor.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/git_local.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/impact.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/info.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/kg.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/remote.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/search.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/cli/serve.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/client/__init__.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/client/api.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/client/config.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/config.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/config_loader.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/kg_config.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem/kg_init.py +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/SOURCES.txt +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/dependency_links.txt +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/entry_points.txt +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/requires.txt +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/top_level.txt +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/setup.cfg +0 -0
- {ravmem_cli-1.1.1 → ravmem_cli-1.3.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ravmem-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
|
|
5
5
|
Author-email: RavMem <harpreet.singh@ravmem.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,8 +44,10 @@ pip install ravmem-cli
|
|
|
44
44
|
# Point the CLI at your RavMem server
|
|
45
45
|
ravmem remote config --url http://localhost:7432 --api-key <ADMIN_TOKEN>
|
|
46
46
|
|
|
47
|
-
# Activate the license (one command: seals the key to
|
|
48
|
-
# brings the server live — the key is
|
|
47
|
+
# Activate the license (one command: seals the key to the server's machine,
|
|
48
|
+
# then brings the server live — the key is saved on the server's own data
|
|
49
|
+
# volume so it survives restarts; it's never stored on the machine you run
|
|
50
|
+
# this CLI command from)
|
|
49
51
|
ravmem license activate RAVMEM-XXXX-XXXX-XXXX-XXXX
|
|
50
52
|
|
|
51
53
|
# Register a repository
|
|
@@ -14,8 +14,10 @@ pip install ravmem-cli
|
|
|
14
14
|
# Point the CLI at your RavMem server
|
|
15
15
|
ravmem remote config --url http://localhost:7432 --api-key <ADMIN_TOKEN>
|
|
16
16
|
|
|
17
|
-
# Activate the license (one command: seals the key to
|
|
18
|
-
# brings the server live — the key is
|
|
17
|
+
# Activate the license (one command: seals the key to the server's machine,
|
|
18
|
+
# then brings the server live — the key is saved on the server's own data
|
|
19
|
+
# volume so it survives restarts; it's never stored on the machine you run
|
|
20
|
+
# this CLI command from)
|
|
19
21
|
ravmem license activate RAVMEM-XXXX-XXXX-XXXX-XXXX
|
|
20
22
|
|
|
21
23
|
# Register a repository
|
|
@@ -39,6 +39,55 @@ def _get_client():
|
|
|
39
39
|
return RavMemClient(base_url=cfg["server_url"], api_key=cfg.get("api_key") or None)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _post_with_key_fallback(client, con: Console, path: str, body: Optional[dict] = None) -> dict:
|
|
43
|
+
"""POST to `path`; if the server has no license key on file, prompt for it
|
|
44
|
+
once and retry with it included in the body (the server persists it there,
|
|
45
|
+
so this shouldn't come up again on subsequent calls)."""
|
|
46
|
+
body = dict(body or {})
|
|
47
|
+
try:
|
|
48
|
+
return client.post(path, json=body)
|
|
49
|
+
except RavMemAPIError as exc:
|
|
50
|
+
if exc.status == 400 and "RAVMEM_LICENSE_KEY is not set" in (exc.detail or "") and not body.get("license_key"):
|
|
51
|
+
con.print("[yellow]Server has no license key on file — enter it to proceed.[/yellow]")
|
|
52
|
+
key = typer.prompt("License key", hide_input=True).strip()
|
|
53
|
+
if not key:
|
|
54
|
+
raise
|
|
55
|
+
body["license_key"] = key
|
|
56
|
+
return client.post(path, json=body)
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _license_hint(detail: str) -> Optional[str]:
|
|
61
|
+
"""Map a license-server refusal to the concrete next step for the operator."""
|
|
62
|
+
d = detail or ""
|
|
63
|
+
if "LICENSE_REVOKED" in d:
|
|
64
|
+
return ("This license was revoked. Rotate the key in the console (that un-revokes it and "
|
|
65
|
+
"issues a new key), then run: ravmem license activate <new-key>")
|
|
66
|
+
if "LICENSE_KEY_NOT_FOUND" in d or "License key not found" in d or "No account found" in d:
|
|
67
|
+
return ("The license server doesn't recognise this key. If it was rotated in the console, "
|
|
68
|
+
"run: ravmem license activate <new-key>")
|
|
69
|
+
if "ACTIVATION_LIMIT_REACHED" in d:
|
|
70
|
+
return ("Every seat on this license is in use — possibly by this same machine under a "
|
|
71
|
+
"previous key. Deactivate the old machine in the console, then retry.")
|
|
72
|
+
if "SUBSCRIPTION_SUSPENDED" in d:
|
|
73
|
+
return "The subscription is suspended. Resolve billing in the console, then run: ravmem license renew"
|
|
74
|
+
if "SUBSCRIPTION_EXPIRED" in d:
|
|
75
|
+
return "The subscription has expired. Renew it in the console, then run: ravmem license renew"
|
|
76
|
+
if "No license record found" in d:
|
|
77
|
+
return "Run: ravmem license activate <key>"
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _fail(con: Console, prefix: str, exc: Exception, note: Optional[str] = None) -> None:
|
|
82
|
+
con.print(f"[red]{prefix}: {exc}[/red]")
|
|
83
|
+
if note:
|
|
84
|
+
con.print(f"[dim]{note}[/dim]")
|
|
85
|
+
hint = _license_hint(getattr(exc, "detail", "") or str(exc))
|
|
86
|
+
if hint:
|
|
87
|
+
con.print(f"[yellow]-> {hint}[/yellow]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
42
91
|
# ---------------------------------------------------------------------------
|
|
43
92
|
# ravmem license init
|
|
44
93
|
# ---------------------------------------------------------------------------
|
|
@@ -106,9 +155,11 @@ def activate_cmd(
|
|
|
106
155
|
) -> None:
|
|
107
156
|
"""One-step activation: seal the license to this machine and bring the server live.
|
|
108
157
|
|
|
109
|
-
The key is sent once
|
|
110
|
-
|
|
111
|
-
|
|
158
|
+
The key is sent once and sealed to this machine (machine-bound). It's also
|
|
159
|
+
saved to the server's own data volume (not to its .env) so the license
|
|
160
|
+
survives restarts and container recreates without needing the key re-entered.
|
|
161
|
+
It is never stored on the machine you run this CLI command from. Works
|
|
162
|
+
whether the server is in activation-only mode (first run) or already running.
|
|
112
163
|
"""
|
|
113
164
|
con = _console()
|
|
114
165
|
if not key:
|
|
@@ -126,19 +177,25 @@ def activate_cmd(
|
|
|
126
177
|
# that is fine — we just continue to the entitlement step.
|
|
127
178
|
con.print("Sealing license to this machine…")
|
|
128
179
|
sealed_now = False
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
for attempt in range(3):
|
|
181
|
+
try:
|
|
182
|
+
client.post("/activate", json={"license_key": key})
|
|
183
|
+
sealed_now = True
|
|
184
|
+
con.print("[green]K3 sealed. Waiting for the server to load modules…[/green]")
|
|
185
|
+
break
|
|
186
|
+
except RavMemAPIError as exc:
|
|
187
|
+
if exc.status == 404:
|
|
188
|
+
con.print("[dim]Server already activated — proceeding to entitlement init.[/dim]")
|
|
189
|
+
break
|
|
190
|
+
_fail(con, "Activation failed", exc)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
# Transient: server still booting, or a network blip to it.
|
|
193
|
+
if attempt < 2:
|
|
194
|
+
time.sleep(3)
|
|
195
|
+
continue
|
|
196
|
+
con.print(f"[red]Could not reach the server: {exc}[/red]")
|
|
197
|
+
con.print("[yellow]-> Check it's running ('docker logs ravmem'), then retry.[/yellow]")
|
|
138
198
|
raise typer.Exit(1)
|
|
139
|
-
except Exception as exc:
|
|
140
|
-
con.print(f"[red]Could not reach the server: {exc}[/red]")
|
|
141
|
-
raise typer.Exit(1)
|
|
142
199
|
|
|
143
200
|
# Phase 2 — wait for the re-exec'd full server to finish loading modules.
|
|
144
201
|
if sealed_now:
|
|
@@ -161,17 +218,57 @@ def activate_cmd(
|
|
|
161
218
|
|
|
162
219
|
# Phase 3 — store the signed entitlement JWT (idempotent: 409 = already done).
|
|
163
220
|
con.print("Activating entitlements…")
|
|
164
|
-
init_payload: dict = {"key_hash": key_hash}
|
|
221
|
+
init_payload: dict = {"key_hash": key_hash, "license_key": key}
|
|
165
222
|
if server_url:
|
|
166
223
|
init_payload["server_url"] = server_url
|
|
167
224
|
try:
|
|
168
225
|
result = client.post("/api/license/init", json=init_payload)
|
|
169
226
|
except RavMemAPIError as exc:
|
|
170
|
-
if exc.status
|
|
227
|
+
if exc.status != 409:
|
|
228
|
+
_fail(con, "Entitlement init failed", exc)
|
|
229
|
+
|
|
230
|
+
# Already initialized. That alone doesn't mean everything is fine: the
|
|
231
|
+
# stored token may be broken (recreate), the license may be
|
|
232
|
+
# deactivated/revoked, or the key typed here may differ from the one
|
|
233
|
+
# this server holds (rotated key, or a typo). Only report "ready" when
|
|
234
|
+
# all three check out; otherwise re-fetch with the key given here —
|
|
235
|
+
# the license server validates it either way, and on refusal nothing
|
|
236
|
+
# stored locally is changed.
|
|
237
|
+
con.print("[dim]License already initialized — verifying…[/dim]")
|
|
238
|
+
try:
|
|
239
|
+
status_data = client.get("/api/license/status")
|
|
240
|
+
except RavMemAPIError as exc2:
|
|
241
|
+
_fail(con, "Could not verify license status", exc2)
|
|
242
|
+
|
|
243
|
+
integrity = status_data.get("token_integrity")
|
|
244
|
+
status = status_data.get("status")
|
|
245
|
+
stored_prefix = status_data.get("key_hash_prefix")
|
|
246
|
+
key_differs = bool(stored_prefix) and not key_hash.startswith(stored_prefix)
|
|
247
|
+
|
|
248
|
+
if integrity == "ok" and status in ("ACTIVE", "GRACE_PERIOD") and not key_differs:
|
|
171
249
|
con.print("[bold green]License already initialized — ravmem is ready.[/bold green]")
|
|
172
250
|
return
|
|
173
|
-
|
|
174
|
-
|
|
251
|
+
|
|
252
|
+
if key_differs:
|
|
253
|
+
con.print("[yellow]This key differs from the one this server is activated with — "
|
|
254
|
+
"switching to it…[/yellow]")
|
|
255
|
+
elif status not in ("ACTIVE", "GRACE_PERIOD"):
|
|
256
|
+
con.print(f"[yellow]License status is {status} — re-activating…[/yellow]")
|
|
257
|
+
else:
|
|
258
|
+
con.print(f"[yellow]Stored token integrity is '{integrity}' — repairing…[/yellow]")
|
|
259
|
+
try:
|
|
260
|
+
repair_result = client.post("/api/license/repair", json={"license_key": key})
|
|
261
|
+
except RavMemAPIError as exc3:
|
|
262
|
+
_fail(con, "Activation failed", exc3,
|
|
263
|
+
note="Your existing license on this server was left unchanged." if key_differs else None)
|
|
264
|
+
|
|
265
|
+
if repair_result.get("status") == "rebind_pending":
|
|
266
|
+
con.print(f"[yellow]{repair_result.get('message')}[/yellow]")
|
|
267
|
+
raise typer.Exit(1)
|
|
268
|
+
con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
|
|
269
|
+
con.print(f" Status: {repair_result.get('status')}")
|
|
270
|
+
con.print(f" Expires: {repair_result.get('expiry_ts')}")
|
|
271
|
+
return
|
|
175
272
|
|
|
176
273
|
con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
|
|
177
274
|
con.print(f" Status: {result.get('status')}")
|
|
@@ -335,6 +432,8 @@ def status_cmd(
|
|
|
335
432
|
expiry_ts = data.get("expiry_ts")
|
|
336
433
|
table.add_row("Expires", _fmt_ts(expiry_ts) + _days_until(expiry_ts))
|
|
337
434
|
table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
|
|
435
|
+
if data.get("heartbeat_interval"):
|
|
436
|
+
table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
|
|
338
437
|
table.add_row("Last server time", _fmt_ts(data.get("last_server_time")))
|
|
339
438
|
|
|
340
439
|
grace_deadline = data.get("grace_deadline")
|
|
@@ -358,6 +457,8 @@ def status_cmd(
|
|
|
358
457
|
)
|
|
359
458
|
elif status == "GRACE_PERIOD":
|
|
360
459
|
con.print("\n[yellow]⚠ License in grace period. Renew before the grace window closes.[/yellow]")
|
|
460
|
+
else:
|
|
461
|
+
_status_hint(con, status)
|
|
361
462
|
|
|
362
463
|
if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED"):
|
|
363
464
|
raise typer.Exit(1)
|
|
@@ -367,16 +468,31 @@ def status_cmd(
|
|
|
367
468
|
# ravmem license renew
|
|
368
469
|
# ---------------------------------------------------------------------------
|
|
369
470
|
|
|
471
|
+
_KEY_OPTION_HELP = (
|
|
472
|
+
"License key to use. Only needed if the key was rotated in the console "
|
|
473
|
+
"(this server is then switched onto it), or if the server has no key on file."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _run_recovery(con: Console, path: str, key: Optional[str], failed: str) -> dict:
|
|
478
|
+
client = _get_client()
|
|
479
|
+
body = {"license_key": key.strip()} if key and key.strip() else {}
|
|
480
|
+
try:
|
|
481
|
+
return _post_with_key_fallback(client, con, path, body)
|
|
482
|
+
except RavMemAPIError as exc:
|
|
483
|
+
_fail(con, failed, exc)
|
|
484
|
+
|
|
485
|
+
|
|
370
486
|
@app.command("renew")
|
|
371
|
-
def renew_cmd(
|
|
487
|
+
def renew_cmd(
|
|
488
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
489
|
+
) -> None:
|
|
372
490
|
"""Re-activate an expired license against the live license server."""
|
|
373
491
|
con = _console()
|
|
374
|
-
client = _get_client()
|
|
375
492
|
con.print("Renewing license…")
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
con.print(f"[red]Renewal failed: {exc}[/red]")
|
|
493
|
+
result = _run_recovery(con, "/api/license/renew", key, "Renewal failed")
|
|
494
|
+
if result.get("status") == "rebind_pending":
|
|
495
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
380
496
|
raise typer.Exit(1)
|
|
381
497
|
con.print(f"[green]Renewed. Status: {result.get('status')} | Expiry: {result.get('expiry_ts')}[/green]")
|
|
382
498
|
|
|
@@ -386,20 +502,19 @@ def renew_cmd() -> None:
|
|
|
386
502
|
# ---------------------------------------------------------------------------
|
|
387
503
|
|
|
388
504
|
@app.command("repair")
|
|
389
|
-
def repair_cmd(
|
|
390
|
-
|
|
505
|
+
def repair_cmd(
|
|
506
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
507
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
508
|
+
) -> None:
|
|
509
|
+
"""Re-fetch a valid license token for this server (broken token, recreated
|
|
510
|
+
container, deactivated machine, or a key rotated in the console)."""
|
|
391
511
|
con = _console()
|
|
392
|
-
|
|
393
|
-
"[yellow]This
|
|
394
|
-
"
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
client = _get_client()
|
|
399
|
-
try:
|
|
400
|
-
result = client.post("/api/license/repair", json={})
|
|
401
|
-
except RavMemAPIError as exc:
|
|
402
|
-
con.print(f"[red]Repair failed: {exc}[/red]")
|
|
512
|
+
if not yes:
|
|
513
|
+
con.print("[yellow]This re-contacts the license server and replaces this server's stored token.[/yellow]")
|
|
514
|
+
typer.confirm("Continue?", abort=True)
|
|
515
|
+
result = _run_recovery(con, "/api/license/repair", key, "Repair failed")
|
|
516
|
+
if result.get("status") == "rebind_pending":
|
|
517
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
403
518
|
raise typer.Exit(1)
|
|
404
519
|
con.print(f"[green]Repair complete. Status: {result.get('status')}[/green]")
|
|
405
520
|
|
|
@@ -442,29 +557,43 @@ def check_cmd() -> None:
|
|
|
442
557
|
table.add_row("Revision", str(data.get("revision", "—")))
|
|
443
558
|
table.add_row("Expires", _fmt_ts(data.get("expiry_ts")))
|
|
444
559
|
table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
|
|
560
|
+
if data.get("heartbeat_interval"):
|
|
561
|
+
table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
|
|
445
562
|
grace_deadline = data.get("grace_deadline")
|
|
446
563
|
if grace_deadline:
|
|
447
564
|
table.add_row("[yellow]Grace deadline[/yellow]", f"[yellow]{_fmt_ts(grace_deadline)}[/yellow]")
|
|
448
565
|
con.print(table)
|
|
566
|
+
_status_hint(con, status)
|
|
449
567
|
|
|
450
568
|
if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED", "SUSPENDED"):
|
|
451
569
|
raise typer.Exit(1)
|
|
452
570
|
|
|
453
571
|
|
|
572
|
+
def _status_hint(con: Console, status: str) -> None:
|
|
573
|
+
hints = {
|
|
574
|
+
"REVOKED": _license_hint("LICENSE_REVOKED"),
|
|
575
|
+
"REQUIRES_REACTIVATION": "Run: ravmem license repair (or: ravmem license activate <key>)",
|
|
576
|
+
"TAMPERED": "Run: ravmem license repair",
|
|
577
|
+
"DEACTIVATED": "This machine was deactivated. To keep it, run: ravmem license activate <key>",
|
|
578
|
+
"SUSPENDED": _license_hint("SUBSCRIPTION_SUSPENDED"),
|
|
579
|
+
"UNINITIALIZED": "Run: ravmem license activate <key>",
|
|
580
|
+
}
|
|
581
|
+
if hints.get(status):
|
|
582
|
+
con.print(f"[yellow]-> {hints[status]}[/yellow]")
|
|
583
|
+
|
|
584
|
+
|
|
454
585
|
@app.command("rebind")
|
|
455
|
-
def rebind_cmd(
|
|
456
|
-
|
|
586
|
+
def rebind_cmd(
|
|
587
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
588
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Move this license's seat onto this machine (after migration/recreate)."""
|
|
457
591
|
con = _console()
|
|
458
|
-
|
|
459
|
-
"[yellow]This
|
|
460
|
-
"
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
client = _get_client()
|
|
465
|
-
try:
|
|
466
|
-
result = client.post("/api/license/rebind", json={})
|
|
467
|
-
except RavMemAPIError as exc:
|
|
468
|
-
con.print(f"[red]Rebind request failed: {exc}[/red]")
|
|
592
|
+
if not yes:
|
|
593
|
+
con.print("[yellow]This moves the license seat onto this machine's current identity.[/yellow]")
|
|
594
|
+
typer.confirm("Continue?", abort=True)
|
|
595
|
+
result = _run_recovery(con, "/api/license/rebind", key, "Rebind failed")
|
|
596
|
+
if result.get("status") == "rebind_pending":
|
|
597
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
469
598
|
raise typer.Exit(1)
|
|
470
|
-
con.print(f"[green]{result.get('message', '
|
|
599
|
+
con.print(f"[green]{result.get('message', 'Rebound.')}[/green]")
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"""CLI entry point — registers all subcommands."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import sys
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import typer
|
|
7
8
|
|
|
9
|
+
# Legacy Windows consoles (cp1252) can't encode the ⚠/…/— glyphs used in
|
|
10
|
+
# output; without this, e.g. `ravmem license status` crashed with a
|
|
11
|
+
# UnicodeEncodeError *after* printing, exiting non-zero on a healthy license.
|
|
12
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
13
|
+
try:
|
|
14
|
+
_stream.reconfigure(errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
8
18
|
|
|
9
19
|
def setup_logging(verbose: bool = False) -> None:
|
|
10
20
|
"""Configure root logger based on verbosity or RAVMEM_LOG_LEVEL env var.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ravmem-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
|
|
5
5
|
Author-email: RavMem <harpreet.singh@ravmem.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,8 +44,10 @@ pip install ravmem-cli
|
|
|
44
44
|
# Point the CLI at your RavMem server
|
|
45
45
|
ravmem remote config --url http://localhost:7432 --api-key <ADMIN_TOKEN>
|
|
46
46
|
|
|
47
|
-
# Activate the license (one command: seals the key to
|
|
48
|
-
# brings the server live — the key is
|
|
47
|
+
# Activate the license (one command: seals the key to the server's machine,
|
|
48
|
+
# then brings the server live — the key is saved on the server's own data
|
|
49
|
+
# volume so it survives restarts; it's never stored on the machine you run
|
|
50
|
+
# this CLI command from)
|
|
49
51
|
ravmem license activate RAVMEM-XXXX-XXXX-XXXX-XXXX
|
|
50
52
|
|
|
51
53
|
# Register a repository
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|