Adds CLI coverage for the new REST surface introduced in this PR: agnes store list / show / install / uninstall / upload / delete agnes my-stack show / toggle Covers 11 of the 15 new endpoints — listing, detail, install/uninstall, upload (multipart), delete, my-stack get + curated toggle. Photo / docs download endpoints intentionally skipped; analyst-side automation rarely needs raw bytes back, and the web UI already covers them. cli/v2_client.py: api_post_multipart + api_put_multipart helpers (httpx files= passthrough). api_delete + api_put_json fillers were already needed for non-multipart writes; added together. Tests: tests/test_cli_store.py — help-text smoke tests + happy-path mocked tests for list, install, upload, my-stack show, my-stack toggle. 12 new tests, all green.
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""`agnes my-stack {show,toggle}` — per-user marketplace composition view.
|
|
|
|
Reads ``GET /api/my-stack`` and writes
|
|
``PUT /api/my-stack/curated/{marketplace_id}/{plugin_name}`` opt-out flips.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Optional
|
|
|
|
import typer
|
|
|
|
from cli.v2_client import V2ClientError, api_get_json, api_put_json
|
|
|
|
my_stack_app = typer.Typer(help="Per-user marketplace composition (curated grants + Store installs)")
|
|
|
|
|
|
@my_stack_app.command("show")
|
|
def show_stack(
|
|
json_out: bool = typer.Option(False, "--json"),
|
|
):
|
|
"""Show admin-granted plugins (with opt-out state) and your Store installs."""
|
|
try:
|
|
body = api_get_json("/api/my-stack")
|
|
except V2ClientError as e:
|
|
typer.echo(str(e), err=True)
|
|
raise typer.Exit(1)
|
|
if json_out:
|
|
typer.echo(json.dumps(body, indent=2))
|
|
return
|
|
curated = body.get("curated", [])
|
|
store = body.get("store", [])
|
|
typer.echo(f"Curated (admin-granted): {len(curated)}")
|
|
for p in curated:
|
|
flag = "✓" if p["enabled"] else "✗"
|
|
typer.echo(
|
|
f" [{flag}] {p['marketplace_id']}/{p['plugin_name']:24s} "
|
|
f"manifest={p['manifest_name']} v{p.get('version') or '?'}"
|
|
)
|
|
typer.echo(f"\nFrom Store: {len(store)}")
|
|
for it in store:
|
|
typer.echo(
|
|
f" [{it['type']:6s}] {it['name']:24s} by {it['owner_username']:20s} "
|
|
f"invocation={it['invocation_name']} id={it['entity_id']}"
|
|
)
|
|
|
|
|
|
@my_stack_app.command("toggle")
|
|
def toggle(
|
|
marketplace_id: str = typer.Argument(...),
|
|
plugin_name: str = typer.Argument(...),
|
|
on: bool = typer.Option(False, "--on", help="Enable (drop opt-out)"),
|
|
off: bool = typer.Option(False, "--off", help="Disable (set opt-out)"),
|
|
):
|
|
"""Toggle a curated plugin on or off (writes a `user_plugin_optouts` row)."""
|
|
if on == off:
|
|
typer.echo("Pass exactly one of --on / --off.", err=True)
|
|
raise typer.Exit(2)
|
|
enabled = bool(on)
|
|
path = f"/api/my-stack/curated/{marketplace_id}/{plugin_name}"
|
|
try:
|
|
api_put_json(path, {"enabled": enabled})
|
|
except V2ClientError as e:
|
|
typer.echo(str(e), err=True)
|
|
raise typer.Exit(1)
|
|
state = "ENABLED" if enabled else "DISABLED"
|
|
typer.echo(f"{marketplace_id}/{plugin_name}: {state}")
|