bonito-cli 0.5.2__tar.gz → 0.6.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 (42) hide show
  1. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/CHANGELOG.md +21 -0
  2. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/PKG-INFO +1 -1
  3. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/__init__.py +1 -1
  4. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/app.py +16 -0
  5. bonito_cli-0.6.1/bonito_cli/commands/approval.py +207 -0
  6. bonito_cli-0.6.1/bonito_cli/commands/audit.py +79 -0
  7. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/auth.py +35 -40
  8. bonito_cli-0.6.1/bonito_cli/commands/compliance.py +110 -0
  9. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/deploy.py +43 -23
  10. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/init.py +1 -1
  11. bonito_cli-0.6.1/bonito_cli/commands/mcp.py +221 -0
  12. bonito_cli-0.6.1/bonito_cli/commands/memory.py +170 -0
  13. bonito_cli-0.6.1/bonito_cli/commands/rbac.py +171 -0
  14. bonito_cli-0.6.1/bonito_cli/commands/routing.py +178 -0
  15. bonito_cli-0.6.1/bonito_cli/commands/scheduler.py +253 -0
  16. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/secrets.py +10 -11
  17. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/pyproject.toml +1 -1
  18. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/.gitignore +0 -0
  19. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/CLI_SPEC.md +0 -0
  20. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/README.md +0 -0
  21. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/__main__.py +0 -0
  22. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/api.py +0 -0
  23. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/__init__.py +0 -0
  24. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/admin.py +0 -0
  25. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/agents.py +0 -0
  26. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/analytics.py +0 -0
  27. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/chat.py +0 -0
  28. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/deployments.py +0 -0
  29. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/gateway.py +0 -0
  30. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/groups.py +0 -0
  31. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/kb.py +0 -0
  32. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/models.py +0 -0
  33. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/plan.py +0 -0
  34. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/policies.py +0 -0
  35. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/projects.py +0 -0
  36. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/providers.py +0 -0
  37. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/sso.py +0 -0
  38. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/config.py +0 -0
  39. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/__init__.py +0 -0
  40. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/auth.py +0 -0
  41. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/display.py +0 -0
  42. {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/feature_gate.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to the Bonito CLI will be documented in this file.
4
4
 
5
+ ## [0.6.0] - 2026-05-07
6
+
7
+ ### Added
8
+ - **8 new command modules** for full API parity:
9
+ - `bonito rbac` — role-based access control (roles, permissions, assignments)
10
+ - `bonito compliance` — SOC-2, HIPAA, GDPR, ISO27001 policy checks
11
+ - `bonito approval` — human-in-the-loop approval queue management
12
+ - `bonito scheduler` — cron-based autonomous agent execution
13
+ - `bonito audit` — audit log viewing and export
14
+ - `bonito memory` — persistent agent memory management
15
+ - `bonito mcp` — MCP server registration and management per-agent
16
+ - `bonito routing` — routing policy builder (cost/latency/balanced/failover/ab_test)
17
+
18
+ ### Fixed
19
+ - **`bonito deploy` now updates existing agents** instead of creating duplicates on re-deploy. Model, prompt, and config changes in `bonito.yaml` now propagate correctly.
20
+ - P0 bugs: signup flow, secrets API calls, stale URLs
21
+
22
+ ### Changed
23
+ - Deploy summary now shows `~` (yellow) for updated agents vs `v` (green) for newly created ones
24
+
25
+
5
26
  ## [0.5.1] - 2026-03-02
6
27
 
7
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bonito-cli
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: Bonito CLI — Unified multi-cloud AI management from your terminal
5
5
  Author-email: Bonito <hello@getbonito.com>
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """Bonito CLI — Unified multi-cloud AI management from your terminal."""
2
2
 
3
- __version__ = "0.5.2"
3
+ __version__ = "0.6.0"
@@ -26,6 +26,14 @@ from .commands.sso import app as sso_app
26
26
  from .commands.plan import app as plan_app
27
27
  from .commands.deploy import app as deploy_app
28
28
  from .commands.secrets import app as secrets_app
29
+ from .commands.rbac import app as rbac_app
30
+ from .commands.compliance import app as compliance_app
31
+ from .commands.approval import app as approval_app
32
+ from .commands.scheduler import app as scheduler_app
33
+ from .commands.audit import app as audit_app
34
+ from .commands.memory import app as memory_app
35
+ from .commands.mcp import app as mcp_app
36
+ from .commands.routing import app as routing_app
29
37
 
30
38
  console = Console()
31
39
 
@@ -88,6 +96,14 @@ app.add_typer(sso_app, name="sso", help="🔐 SAML Single Sign-O
88
96
  app.add_typer(plan_app, name="plan", help="💎 Subscription plans & usage")
89
97
  app.add_typer(deploy_app, name="deploy", help="📦 Deploy from bonito.yaml")
90
98
  app.add_typer(secrets_app, name="secrets", help="🔐 Org secrets management")
99
+ app.add_typer(rbac_app, name="rbac", help="🛡️ Role-based access control")
100
+ app.add_typer(compliance_app, name="compliance", help="📋 Compliance checks & reports")
101
+ app.add_typer(approval_app, name="approval", help="✅ Agent approval workflows")
102
+ app.add_typer(scheduler_app, name="scheduler", help="⏰ Agent scheduling")
103
+ app.add_typer(audit_app, name="audit", help="📝 Audit logs")
104
+ app.add_typer(memory_app, name="memory", help="🧠 Agent persistent memory")
105
+ app.add_typer(mcp_app, name="mcp", help="🔌 MCP server management")
106
+ app.add_typer(routing_app, name="routing", help="🔀 Routing rule management")
91
107
 
92
108
 
93
109
  # ── Callbacks ──────────────────────────────────────────────────
@@ -0,0 +1,207 @@
1
+ """Agent approval workflow commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as _json
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.prompt import Prompt
11
+
12
+ from ..api import api, APIError
13
+ from ..utils.auth import ensure_authenticated
14
+ from ..utils.display import (
15
+ get_output_format,
16
+ print_error,
17
+ print_info,
18
+ print_success,
19
+ print_table,
20
+ print_dict_as_table,
21
+ format_timestamp,
22
+ )
23
+
24
+ console = Console()
25
+ app = typer.Typer(help="Agent approval queue and human-in-the-loop workflows")
26
+
27
+
28
+ # -- list-pending ------------------------------------------------------
29
+
30
+
31
+ @app.command("list-pending")
32
+ def list_pending(
33
+ org_id: str = typer.Option(..., "--org", help="Organization ID"),
34
+ agent_id: Optional[str] = typer.Option(None, "--agent", help="Filter by agent ID"),
35
+ risk_level: Optional[str] = typer.Option(None, "--risk", help="Filter by risk level (low/medium/high/critical)"),
36
+ limit: int = typer.Option(50, "--limit", "-l", help="Max results"),
37
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
38
+ ):
39
+ """List pending approval actions."""
40
+ fmt = get_output_format(json_output)
41
+ ensure_authenticated()
42
+
43
+ params = {"limit": limit}
44
+ if agent_id:
45
+ params["agent_id"] = agent_id
46
+ if risk_level:
47
+ params["risk_level"] = risk_level
48
+
49
+ try:
50
+ with console.status("[cyan]Fetching pending approvals...[/cyan]"):
51
+ items = api.get(f"/organizations/{org_id}/approvals/queue", params=params)
52
+
53
+ if fmt == "json":
54
+ console.print_json(_json.dumps(items, default=str))
55
+ else:
56
+ if items:
57
+ data = [
58
+ {
59
+ "ID": str(i.get("id", ""))[:8] + "...",
60
+ "Agent": i.get("agent_name", ""),
61
+ "Action": i.get("action_type", ""),
62
+ "Risk": i.get("risk_level", ""),
63
+ "Requester": i.get("requester_name", "") or "",
64
+ "Created": format_timestamp(i.get("created_at")),
65
+ }
66
+ for i in items
67
+ ]
68
+ print_table(data, title="Pending Approvals")
69
+ print_info(f"{len(items)} pending action(s)")
70
+ else:
71
+ print_info("No pending approvals.")
72
+ except APIError as exc:
73
+ print_error(f"Failed to list pending approvals: {exc}")
74
+
75
+
76
+ # -- approve -----------------------------------------------------------
77
+
78
+
79
+ @app.command("approve")
80
+ def approve(
81
+ action_id: str = typer.Argument(..., help="Approval action ID"),
82
+ notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Review notes"),
83
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
84
+ ):
85
+ """Approve a pending action."""
86
+ fmt = get_output_format(json_output)
87
+ ensure_authenticated()
88
+
89
+ body = {"action": "approve"}
90
+ if notes:
91
+ body["review_notes"] = notes
92
+
93
+ try:
94
+ with console.status("[cyan]Approving action...[/cyan]"):
95
+ result = api.post(f"/approvals/{action_id}/review", body)
96
+
97
+ if fmt == "json":
98
+ console.print_json(_json.dumps(result, default=str))
99
+ else:
100
+ print_success(f"Action {action_id} approved")
101
+ except APIError as exc:
102
+ print_error(f"Failed to approve action: {exc}")
103
+
104
+
105
+ # -- reject ------------------------------------------------------------
106
+
107
+
108
+ @app.command("reject")
109
+ def reject(
110
+ action_id: str = typer.Argument(..., help="Approval action ID"),
111
+ notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Rejection reason"),
112
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
113
+ ):
114
+ """Reject a pending action."""
115
+ fmt = get_output_format(json_output)
116
+ ensure_authenticated()
117
+
118
+ body = {"action": "reject"}
119
+ if notes:
120
+ body["review_notes"] = notes
121
+
122
+ try:
123
+ with console.status("[cyan]Rejecting action...[/cyan]"):
124
+ result = api.post(f"/approvals/{action_id}/review", body)
125
+
126
+ if fmt == "json":
127
+ console.print_json(_json.dumps(result, default=str))
128
+ else:
129
+ print_success(f"Action {action_id} rejected")
130
+ except APIError as exc:
131
+ print_error(f"Failed to reject action: {exc}")
132
+
133
+
134
+ # -- get-config --------------------------------------------------------
135
+
136
+
137
+ @app.command("get-config")
138
+ def get_config(
139
+ agent_id: str = typer.Argument(..., help="Agent ID"),
140
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
141
+ ):
142
+ """List approval configs for an agent."""
143
+ fmt = get_output_format(json_output)
144
+ ensure_authenticated()
145
+
146
+ try:
147
+ with console.status("[cyan]Fetching approval configs...[/cyan]"):
148
+ configs = api.get(f"/agents/{agent_id}/approval-configs")
149
+
150
+ if fmt == "json":
151
+ console.print_json(_json.dumps(configs, default=str))
152
+ else:
153
+ if configs:
154
+ data = [
155
+ {
156
+ "ID": str(c.get("id", ""))[:8] + "...",
157
+ "Action Type": c.get("action_type", ""),
158
+ "Requires Approval": "Yes" if c.get("requires_approval") else "No",
159
+ "Timeout (hrs)": str(c.get("timeout_hours", "")),
160
+ "Required Approvers": str(c.get("required_approvers", 1)),
161
+ }
162
+ for c in configs
163
+ ]
164
+ print_table(data, title="Approval Configs")
165
+ else:
166
+ print_info("No approval configs found for this agent.")
167
+ except APIError as exc:
168
+ print_error(f"Failed to get approval configs: {exc}")
169
+
170
+
171
+ # -- update-config -----------------------------------------------------
172
+
173
+
174
+ @app.command("update-config")
175
+ def update_config(
176
+ config_id: str = typer.Argument(..., help="Approval config ID"),
177
+ requires_approval: Optional[bool] = typer.Option(None, "--requires-approval", help="Whether approval is required"),
178
+ timeout_hours: Optional[int] = typer.Option(None, "--timeout-hours", help="Approval timeout in hours"),
179
+ required_approvers: Optional[int] = typer.Option(None, "--required-approvers", help="Number of required approvers"),
180
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
181
+ ):
182
+ """Update an approval configuration."""
183
+ fmt = get_output_format(json_output)
184
+ ensure_authenticated()
185
+
186
+ body = {}
187
+ if requires_approval is not None:
188
+ body["requires_approval"] = requires_approval
189
+ if timeout_hours is not None:
190
+ body["timeout_hours"] = timeout_hours
191
+ if required_approvers is not None:
192
+ body["required_approvers"] = required_approvers
193
+
194
+ if not body:
195
+ print_error("No updates specified")
196
+ return
197
+
198
+ try:
199
+ with console.status("[cyan]Updating approval config...[/cyan]"):
200
+ result = api.put(f"/approval-configs/{config_id}", body)
201
+
202
+ if fmt == "json":
203
+ console.print_json(_json.dumps(result, default=str))
204
+ else:
205
+ print_success("Approval config updated successfully")
206
+ except APIError as exc:
207
+ print_error(f"Failed to update approval config: {exc}")
@@ -0,0 +1,79 @@
1
+ """Audit log commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as _json
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from ..api import api, APIError
12
+ from ..utils.auth import ensure_authenticated
13
+ from ..utils.display import (
14
+ get_output_format,
15
+ print_error,
16
+ print_info,
17
+ print_table,
18
+ format_timestamp,
19
+ )
20
+
21
+ console = Console()
22
+ app = typer.Typer(help="Audit log viewer")
23
+
24
+
25
+ @app.command("list")
26
+ def list_logs(
27
+ action: Optional[str] = typer.Option(None, "--action", "-a", help="Filter by action (e.g. create, update, delete)"),
28
+ resource_type: Optional[str] = typer.Option(None, "--resource", "-r", help="Filter by resource type"),
29
+ user_name: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user name"),
30
+ date_from: Optional[str] = typer.Option(None, "--from", help="Filter from date (ISO format: 2026-01-01)"),
31
+ date_to: Optional[str] = typer.Option(None, "--to", help="Filter to date (ISO format: 2026-12-31)"),
32
+ page: int = typer.Option(1, "--page", help="Page number"),
33
+ page_size: int = typer.Option(20, "--page-size", help="Page size (max 100)"),
34
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
35
+ ):
36
+ """List audit logs with optional filters."""
37
+ fmt = get_output_format(json_output)
38
+ ensure_authenticated()
39
+
40
+ params = {"page": page, "page_size": page_size}
41
+ if action:
42
+ params["action"] = action
43
+ if resource_type:
44
+ params["resource_type"] = resource_type
45
+ if user_name:
46
+ params["user_name"] = user_name
47
+ if date_from:
48
+ params["date_from"] = date_from
49
+ if date_to:
50
+ params["date_to"] = date_to
51
+
52
+ try:
53
+ with console.status("[cyan]Fetching audit logs...[/cyan]"):
54
+ result = api.get("/audit", params=params)
55
+
56
+ if fmt == "json":
57
+ console.print_json(_json.dumps(result, default=str))
58
+ else:
59
+ items = result.get("items", [])
60
+ total = result.get("total", 0)
61
+
62
+ if items:
63
+ data = [
64
+ {
65
+ "Timestamp": format_timestamp(i.get("created_at")),
66
+ "User": i.get("user_name", ""),
67
+ "Action": i.get("action", ""),
68
+ "Resource": i.get("resource_type", ""),
69
+ "Resource ID": str(i.get("resource_id", ""))[:8] + "..." if i.get("resource_id") else "",
70
+ "Details": (str(i.get("details", "")) or "")[:40],
71
+ }
72
+ for i in items
73
+ ]
74
+ print_table(data, title="Audit Logs")
75
+ print_info(f"Page {page} - Showing {len(items)} of {total} total entries")
76
+ else:
77
+ print_info("No audit log entries found.")
78
+ except APIError as exc:
79
+ print_error(f"Failed to list audit logs: {exc}")
@@ -33,21 +33,24 @@ def signup(
33
33
  password: Optional[str] = typer.Option(None, "--password", "-p", help="Account password"),
34
34
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Full name"),
35
35
  org: Optional[str] = typer.Option(None, "--org", "-o", help="Organization name"),
36
+ invite_code: Optional[str] = typer.Option(None, "--invite-code", "-i", help="Invite code (required for invite-only registration)"),
36
37
  non_interactive: bool = typer.Option(False, "--non-interactive", help="Non-interactive mode (requires all fields as flags)"),
37
38
  api_url: Optional[str] = typer.Option(None, "--api-url", help="Custom API URL"),
38
39
  ):
39
40
  """
40
41
  Sign up for a new Bonito account.
41
42
 
43
+ After signup you must verify your email before logging in.
44
+
42
45
  Examples:
43
46
  bonito auth signup
44
- bonito auth signup --email admin@company.com --name "John Doe" --org "ACME Corp"
45
- bonito auth signup --non-interactive --email admin@company.com --password secret --name "John Doe" --org "ACME Corp"
47
+ bonito auth signup --invite-code ABC123
48
+ bonito auth signup --email admin@company.com --name "John Doe" --org "ACME Corp" --invite-code ABC123
46
49
  """
47
50
  # In non-interactive mode, all fields must be provided as flags
48
51
  if non_interactive:
49
- if not all([email, password, name, org]):
50
- console.print("[red]✗ In non-interactive mode, all fields (--email, --password, --name, --org) are required[/red]")
52
+ if not all([email, password, name]):
53
+ console.print("[red]✗ In non-interactive mode, --email, --password, and --name are required[/red]")
51
54
  raise typer.Exit(1)
52
55
  else:
53
56
  # Interactive prompts
@@ -57,11 +60,13 @@ def signup(
57
60
  password = Prompt.ask("[cyan]Password[/cyan]", password=True)
58
61
  if not name:
59
62
  name = Prompt.ask("[cyan]Full name[/cyan]")
60
- if not org:
61
- org = Prompt.ask("[cyan]Organization name[/cyan]")
63
+ if not invite_code:
64
+ invite_code = Prompt.ask("[cyan]Invite code (leave blank if not required)[/cyan]", default="")
65
+ if not invite_code:
66
+ invite_code = None
62
67
 
63
- if not all([email, password, name, org]):
64
- console.print("[red]✗ All fields (email, password, name, organization) are required[/red]")
68
+ if not all([email, password, name]):
69
+ console.print("[red]✗ Email, password, and name are required[/red]")
65
70
  raise typer.Exit(1)
66
71
 
67
72
  # Allow overriding the API URL
@@ -74,53 +79,43 @@ def signup(
74
79
  api._client = None
75
80
 
76
81
  try:
82
+ body = {
83
+ "email": email,
84
+ "password": password,
85
+ "name": name,
86
+ }
87
+ if invite_code:
88
+ body["invite_code"] = invite_code
89
+
77
90
  with console.status("[cyan]Creating account…[/cyan]"):
78
91
  resp = httpx.post(
79
92
  f"{api.base_url}/api/auth/register",
80
- json={
81
- "email": email,
82
- "password": password,
83
- "name": name,
84
- },
93
+ json=body,
85
94
  timeout=15,
86
95
  )
87
96
 
88
- if resp.status_code != 200:
97
+ if resp.status_code not in (200, 201):
89
98
  detail = "Registration failed"
90
99
  try:
91
- detail = resp.json().get("detail", detail)
100
+ err = resp.json()
101
+ detail = err.get("detail") or err.get("message", detail)
92
102
  except Exception:
93
103
  pass
94
104
  console.print(f"[red]✗ {detail}[/red]")
95
105
  raise typer.Exit(1)
96
106
 
97
107
  data = resp.json()
98
-
99
- # Auto-login after successful signup
100
- creds = {
101
- "access_token": data["access_token"],
102
- "refresh_token": data.get("refresh_token"),
103
- "email": email,
104
- }
105
- save_credentials(creds)
106
-
107
- # Fetch user profile
108
- try:
109
- me = api.get("/auth/me")
110
- creds["user"] = me
111
- save_credentials(creds)
112
- org_name = me.get("org", {}).get("name", "—")
113
- console.print(
114
- Panel(
115
- f"[green]✓ Account created and logged in as [bold]{email}[/bold][/green]\n"
116
- f" Organization: {org_name}\n"
117
- f" Role: {me.get('role', 'member')}",
118
- title="🎉 Welcome to Bonito",
119
- border_style="green",
120
- )
108
+ message = data.get("message", "Account created")
109
+
110
+ console.print(
111
+ Panel(
112
+ f"[green]✓ {message}[/green]\n\n"
113
+ f" Please verify your email, then run:\n"
114
+ f" [cyan]bonito auth login --email {email}[/cyan]",
115
+ title="🎉 Welcome to Bonito",
116
+ border_style="green",
121
117
  )
122
- except Exception:
123
- console.print(f"[green]✓ Account created and logged in as {email}[/green]")
118
+ )
124
119
 
125
120
  except httpx.RequestError as exc:
126
121
  console.print(f"[red]✗ Connection failed: {exc}[/red]")
@@ -0,0 +1,110 @@
1
+ """Compliance checks and reporting commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as _json
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from ..api import api, APIError
11
+ from ..utils.auth import ensure_authenticated
12
+ from ..utils.display import (
13
+ get_output_format,
14
+ print_error,
15
+ print_info,
16
+ print_success,
17
+ print_table,
18
+ print_dict_as_table,
19
+ )
20
+
21
+ console = Console()
22
+ app = typer.Typer(help="Compliance checks and reporting")
23
+
24
+
25
+ # -- run-check ---------------------------------------------------------
26
+
27
+
28
+ @app.command("run-check")
29
+ def run_check(
30
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
31
+ ):
32
+ """Run a compliance scan across all providers."""
33
+ fmt = get_output_format(json_output)
34
+ ensure_authenticated()
35
+
36
+ try:
37
+ with console.status("[cyan]Running compliance scan...[/cyan]"):
38
+ result = api.post("/compliance/scan")
39
+
40
+ if fmt == "json":
41
+ console.print_json(_json.dumps(result, default=str))
42
+ else:
43
+ print_success("Compliance scan completed")
44
+ if isinstance(result, dict):
45
+ print_dict_as_table(result, title="Scan Results")
46
+ except APIError as exc:
47
+ print_error(f"Failed to run compliance scan: {exc}")
48
+
49
+
50
+ # -- list-checks -------------------------------------------------------
51
+
52
+
53
+ @app.command("list-checks")
54
+ def list_checks(
55
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
56
+ ):
57
+ """List all compliance checks and their status."""
58
+ fmt = get_output_format(json_output)
59
+ ensure_authenticated()
60
+
61
+ try:
62
+ with console.status("[cyan]Fetching compliance checks...[/cyan]"):
63
+ checks = api.get("/compliance/checks")
64
+
65
+ if fmt == "json":
66
+ console.print_json(_json.dumps(checks, default=str))
67
+ else:
68
+ if checks:
69
+ data = [
70
+ {
71
+ "Check": c.get("name", c.get("check_id", "")),
72
+ "Framework": c.get("framework", ""),
73
+ "Status": c.get("status", ""),
74
+ "Severity": c.get("severity", ""),
75
+ "Last Run": str(c.get("last_checked_at", ""))[:10] if c.get("last_checked_at") else "",
76
+ }
77
+ for c in checks
78
+ ]
79
+ print_table(data, title="Compliance Checks")
80
+ print_info(f"{len(checks)} check(s)")
81
+ else:
82
+ print_info("No compliance checks found.")
83
+ except APIError as exc:
84
+ print_error(f"Failed to list compliance checks: {exc}")
85
+
86
+
87
+ # -- get-report --------------------------------------------------------
88
+
89
+
90
+ @app.command("get-report")
91
+ def get_report(
92
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
93
+ ):
94
+ """Get the full compliance report."""
95
+ fmt = get_output_format(json_output)
96
+ ensure_authenticated()
97
+
98
+ try:
99
+ with console.status("[cyan]Generating compliance report...[/cyan]"):
100
+ report = api.get("/compliance/report")
101
+
102
+ if fmt == "json":
103
+ console.print_json(_json.dumps(report, default=str))
104
+ else:
105
+ if isinstance(report, dict):
106
+ print_dict_as_table(report, title="Compliance Report")
107
+ else:
108
+ console.print(report)
109
+ except APIError as exc:
110
+ print_error(f"Failed to get compliance report: {exc}")