bt-cli 0.4.13__py3-none-any.whl

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 (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,311 @@
1
+ """CLI commands for managing assets."""
2
+
3
+ from typing import Optional
4
+ import json
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...core.output import print_api_error
12
+ from ..client.base import get_client
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="Manage assets in BeyondInsight")
15
+ console = Console()
16
+
17
+
18
+ def print_assets_table(assets: list[dict], title: str = "Assets") -> None:
19
+ """Print assets in a formatted table."""
20
+ table = Table(title=title)
21
+ table.add_column("ID", style="cyan", no_wrap=True)
22
+ table.add_column("Name", style="green")
23
+ table.add_column("IP Address", style="yellow")
24
+ table.add_column("DNS Name", style="magenta")
25
+ table.add_column("Workgroup", style="blue")
26
+ table.add_column("OS", style="dim")
27
+
28
+ for asset in assets:
29
+ table.add_row(
30
+ str(asset.get("AssetID", "")),
31
+ asset.get("AssetName", "-"),
32
+ asset.get("IPAddress", "-"),
33
+ asset.get("DnsName", "-"),
34
+ asset.get("WorkgroupName", str(asset.get("WorkgroupID", "-"))),
35
+ asset.get("OperatingSystem", "-"),
36
+ )
37
+
38
+ console.print(table)
39
+
40
+
41
+ def print_asset_detail(asset: dict) -> None:
42
+ """Print detailed asset information."""
43
+ display_name = asset.get("AssetName") or asset.get("DnsName") or asset.get("IPAddress") or "Unknown"
44
+ console.print(f"\n[bold cyan]Asset: {display_name}[/bold cyan]\n")
45
+
46
+ info_table = Table(show_header=False, box=None)
47
+ info_table.add_column("Field", style="dim")
48
+ info_table.add_column("Value")
49
+
50
+ fields = [
51
+ ("ID", "AssetID"),
52
+ ("Name", "AssetName"),
53
+ ("IP Address", "IPAddress"),
54
+ ("DNS Name", "DnsName"),
55
+ ("Domain", "DomainName"),
56
+ ("MAC Address", "MACAddress"),
57
+ ("Asset Type", "AssetType"),
58
+ ("Operating System", "OperatingSystem"),
59
+ ("Workgroup", "WorkgroupName"),
60
+ ("Workgroup ID", "WorkgroupID"),
61
+ ("Created", "CreatedDate"),
62
+ ("Last Updated", "LastUpdatedDate"),
63
+ ]
64
+
65
+ for label, key in fields:
66
+ value = asset.get(key)
67
+ if value is not None:
68
+ info_table.add_row(label, str(value))
69
+
70
+ console.print(info_table)
71
+
72
+
73
+ @app.command("list")
74
+ def list_assets(
75
+ workgroup: Optional[int] = typer.Option(None, "--workgroup", "-w", help="Filter by workgroup ID"),
76
+ limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
77
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
78
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
79
+ ) -> None:
80
+ """List assets (by workgroup or all).
81
+
82
+ Examples:
83
+ bt pws assets list # First 50 assets
84
+ bt pws assets list --all # All assets
85
+ bt pws assets list -w 3 # Assets in workgroup 3
86
+ """
87
+ try:
88
+ with get_client() as client:
89
+ client.authenticate()
90
+ assets = client.list_assets(workgroup_id=workgroup, limit=None if fetch_all else limit)
91
+
92
+ if output == "json":
93
+ console.print_json(json.dumps(assets, default=str))
94
+ else:
95
+ if assets:
96
+ print_assets_table(assets)
97
+ if not fetch_all and len(assets) == limit:
98
+ console.print(f"[dim]Showing {len(assets)} results. Use --all to fetch all results.[/dim]")
99
+ else:
100
+ console.print("[yellow]No assets found.[/yellow]")
101
+
102
+ except httpx.HTTPStatusError as e:
103
+ print_api_error(e, "manage assets")
104
+ raise typer.Exit(1)
105
+ except httpx.RequestError as e:
106
+ print_api_error(e, "manage assets")
107
+ raise typer.Exit(1)
108
+ except Exception as e:
109
+ print_api_error(e, "manage assets")
110
+ raise typer.Exit(1)
111
+
112
+
113
+ @app.command("search")
114
+ def search_assets(
115
+ term: str = typer.Argument(..., help="Search term"),
116
+ limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
117
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
118
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
119
+ ) -> None:
120
+ """Search for assets."""
121
+ try:
122
+ with get_client() as client:
123
+ client.authenticate()
124
+ assets = client.search_assets(search_term=term, limit=None if fetch_all else limit)
125
+
126
+ if output == "json":
127
+ console.print_json(json.dumps(assets, default=str))
128
+ else:
129
+ if assets:
130
+ print_assets_table(assets)
131
+ if not fetch_all and len(assets) == limit:
132
+ console.print(f"[dim]Showing {len(assets)} results. Use --all to fetch all results.[/dim]")
133
+ else:
134
+ console.print("[yellow]No assets found.[/yellow]")
135
+
136
+ except httpx.HTTPStatusError as e:
137
+ print_api_error(e, "manage assets")
138
+ raise typer.Exit(1)
139
+ except httpx.RequestError as e:
140
+ print_api_error(e, "manage assets")
141
+ raise typer.Exit(1)
142
+ except Exception as e:
143
+ print_api_error(e, "manage assets")
144
+ raise typer.Exit(1)
145
+
146
+
147
+ @app.command("get")
148
+ def get_asset(
149
+ asset_id: int = typer.Argument(..., help="Asset ID"),
150
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
151
+ ) -> None:
152
+ """Get an asset by ID."""
153
+ try:
154
+ with get_client() as client:
155
+ client.authenticate()
156
+ asset = client.get_asset(asset_id)
157
+
158
+ if output == "json":
159
+ console.print_json(json.dumps(asset, default=str))
160
+ else:
161
+ print_asset_detail(asset)
162
+
163
+ except httpx.HTTPStatusError as e:
164
+ print_api_error(e, "manage assets")
165
+ raise typer.Exit(1)
166
+ except httpx.RequestError as e:
167
+ print_api_error(e, "manage assets")
168
+ raise typer.Exit(1)
169
+ except Exception as e:
170
+ print_api_error(e, "manage assets")
171
+ raise typer.Exit(1)
172
+
173
+
174
+ @app.command("create")
175
+ def create_asset(
176
+ workgroup_id: int = typer.Option(..., "--workgroup", "-w", help="Workgroup ID (required)"),
177
+ ip_address: str = typer.Option(..., "--ip", "-i", help="IP address"),
178
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Asset name"),
179
+ dns_name: Optional[str] = typer.Option(None, "--dns", help="DNS name"),
180
+ domain: Optional[str] = typer.Option(None, "--domain", "-d", help="Domain name"),
181
+ mac_address: Optional[str] = typer.Option(None, "--mac", help="MAC address"),
182
+ asset_type: Optional[str] = typer.Option(None, "--type", "-t", help="Asset type"),
183
+ description: Optional[str] = typer.Option(None, "--description", help="Description"),
184
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
185
+ ) -> None:
186
+ """Create a new asset in a workgroup."""
187
+ try:
188
+ with get_client() as client:
189
+ client.authenticate()
190
+ asset = client.create_asset(
191
+ workgroup_id=workgroup_id,
192
+ ip_address=ip_address,
193
+ asset_name=name,
194
+ dns_name=dns_name,
195
+ domain_name=domain,
196
+ mac_address=mac_address,
197
+ asset_type=asset_type,
198
+ description=description,
199
+ )
200
+
201
+ if output == "json":
202
+ console.print_json(json.dumps(asset, default=str))
203
+ else:
204
+ display_name = asset.get("AssetName") or asset.get("IPAddress") or "Unknown"
205
+ console.print(f"[green]Created asset:[/green] {display_name}")
206
+ console.print(f" ID: {asset.get('AssetID', 'N/A')}")
207
+
208
+ except httpx.HTTPStatusError as e:
209
+ print_api_error(e, "manage assets")
210
+ raise typer.Exit(1)
211
+ except httpx.RequestError as e:
212
+ print_api_error(e, "manage assets")
213
+ raise typer.Exit(1)
214
+ except Exception as e:
215
+ print_api_error(e, "manage assets")
216
+ raise typer.Exit(1)
217
+
218
+
219
+ @app.command("update")
220
+ def update_asset(
221
+ asset_id: int = typer.Argument(..., help="Asset ID to update"),
222
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="New asset name"),
223
+ ip_address: Optional[str] = typer.Option(None, "--ip", "-i", help="New IP address"),
224
+ dns_name: Optional[str] = typer.Option(None, "--dns", help="New DNS name"),
225
+ domain: Optional[str] = typer.Option(None, "--domain", "-d", help="New domain name"),
226
+ mac_address: Optional[str] = typer.Option(None, "--mac", help="New MAC address"),
227
+ asset_type: Optional[str] = typer.Option(None, "--type", "-t", help="New asset type"),
228
+ description: Optional[str] = typer.Option(None, "--description", help="New description"),
229
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
230
+ ) -> None:
231
+ """Update an existing asset.
232
+
233
+ Examples:
234
+ bt pws assets update 123 --dns "server.internal"
235
+ bt pws assets update 123 --name "new-name" --description "Updated"
236
+ """
237
+ try:
238
+ kwargs = {}
239
+ if name is not None:
240
+ kwargs["asset_name"] = name
241
+ if ip_address is not None:
242
+ kwargs["ip_address"] = ip_address
243
+ if dns_name is not None:
244
+ kwargs["dns_name"] = dns_name
245
+ if domain is not None:
246
+ kwargs["domain_name"] = domain
247
+ if mac_address is not None:
248
+ kwargs["mac_address"] = mac_address
249
+ if asset_type is not None:
250
+ kwargs["asset_type"] = asset_type
251
+ if description is not None:
252
+ kwargs["description"] = description
253
+
254
+ if not kwargs:
255
+ console.print("[yellow]No updates specified.[/yellow]")
256
+ raise typer.Exit(0)
257
+
258
+ with get_client() as client:
259
+ client.authenticate()
260
+ asset = client.update_asset(asset_id, **kwargs)
261
+
262
+ if output == "json":
263
+ console.print_json(json.dumps(asset, default=str))
264
+ else:
265
+ display_name = asset.get("AssetName") or asset.get("IPAddress") or "Unknown"
266
+ console.print(f"[green]Updated asset:[/green] {display_name}")
267
+ console.print(f" ID: {asset_id}")
268
+
269
+ except httpx.HTTPStatusError as e:
270
+ print_api_error(e, "update asset")
271
+ raise typer.Exit(1)
272
+ except httpx.RequestError as e:
273
+ print_api_error(e, "update asset")
274
+ raise typer.Exit(1)
275
+ except Exception as e:
276
+ print_api_error(e, "update asset")
277
+ raise typer.Exit(1)
278
+
279
+
280
+ @app.command("delete")
281
+ def delete_asset(
282
+ asset_id: int = typer.Argument(..., help="Asset ID to delete"),
283
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
284
+ ) -> None:
285
+ """Delete an asset."""
286
+ try:
287
+ with get_client() as client:
288
+ client.authenticate()
289
+
290
+ if not force:
291
+ asset = client.get_asset(asset_id)
292
+ name = asset.get("AssetName") or asset.get("IPAddress") or "Unknown"
293
+ confirm = typer.confirm(
294
+ f"Are you sure you want to delete asset '{name}' (ID: {asset_id})?"
295
+ )
296
+ if not confirm:
297
+ console.print("[yellow]Cancelled.[/yellow]")
298
+ raise typer.Exit(0)
299
+
300
+ client.delete_asset(asset_id)
301
+ console.print(f"[green]Deleted asset ID: {asset_id}[/green]")
302
+
303
+ except httpx.HTTPStatusError as e:
304
+ print_api_error(e, "manage assets")
305
+ raise typer.Exit(1)
306
+ except httpx.RequestError as e:
307
+ print_api_error(e, "manage assets")
308
+ raise typer.Exit(1)
309
+ except Exception as e:
310
+ print_api_error(e, "manage assets")
311
+ raise typer.Exit(1)
@@ -0,0 +1,166 @@
1
+ """CLI commands for authentication."""
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ from ...core.output import print_error, print_api_error
9
+ from ..client.base import get_client
10
+ from ..config import get_config
11
+
12
+ app = typer.Typer(no_args_is_help=True, help="Authentication commands")
13
+ console = Console()
14
+
15
+
16
+ @app.command("login")
17
+ def login() -> None:
18
+ """Authenticate and establish a session with Password Safe.
19
+
20
+ This tests your credentials and establishes a session.
21
+ The session is automatically managed per command, but this
22
+ command is useful for verifying your configuration.
23
+
24
+ Example:
25
+ pws auth login
26
+ """
27
+ try:
28
+ config = get_config()
29
+ console.print(f"[dim]API URL:[/dim] {config.api_url}")
30
+ console.print(f"[dim]Auth Method:[/dim] {config.auth_method}")
31
+
32
+ with get_client() as client:
33
+ response = client.authenticate()
34
+
35
+ user_name = response.get("UserName", "Unknown")
36
+ user_id = response.get("UserId", "N/A")
37
+
38
+ console.print(Panel(
39
+ f"[green]Authentication successful![/green]\n\n"
40
+ f"User: [bold]{user_name}[/bold]\n"
41
+ f"User ID: {user_id}",
42
+ title="Logged In",
43
+ ))
44
+
45
+ except ValueError as e:
46
+ print_error(f"Configuration error: {e}")
47
+ raise typer.Exit(1)
48
+ except httpx.HTTPStatusError as e:
49
+ print_api_error(e, "authenticate")
50
+ raise typer.Exit(1)
51
+ except httpx.RequestError as e:
52
+ print_api_error(e, "authenticate")
53
+ raise typer.Exit(1)
54
+ except Exception as e:
55
+ print_api_error(e, "authenticate")
56
+ raise typer.Exit(1)
57
+
58
+
59
+ @app.command("logout")
60
+ def logout() -> None:
61
+ """Sign out and end the current session.
62
+
63
+ Note: Sessions are automatically cleaned up, but this
64
+ command can be used to explicitly end a session.
65
+
66
+ Example:
67
+ pws auth logout
68
+ """
69
+ try:
70
+ with get_client() as client:
71
+ client.authenticate()
72
+ client.sign_out()
73
+ console.print("[green]Signed out successfully.[/green]")
74
+
75
+ except httpx.HTTPStatusError as e:
76
+ print_api_error(e, "sign out")
77
+ raise typer.Exit(1)
78
+ except httpx.RequestError as e:
79
+ print_api_error(e, "sign out")
80
+ raise typer.Exit(1)
81
+ except Exception as e:
82
+ print_api_error(e, "sign out")
83
+ raise typer.Exit(1)
84
+
85
+
86
+ @app.command("status")
87
+ def status() -> None:
88
+ """Show current authentication configuration and status.
89
+
90
+ Displays the configured API URL and authentication method
91
+ without actually authenticating.
92
+
93
+ Example:
94
+ pws auth status
95
+ """
96
+ try:
97
+ config = get_config()
98
+
99
+ console.print("\n[bold cyan]Password Safe Configuration[/bold cyan]\n")
100
+ console.print(f" API URL: [green]{config.api_url}[/green]")
101
+ console.print(f" Auth Method: [yellow]{config.auth_method}[/yellow]")
102
+
103
+ if config.auth_method == "api_key":
104
+ masked_key = config.api_key[:8] + "..." if config.api_key and len(config.api_key) > 8 else "***"
105
+ console.print(f" API Key: [dim]{masked_key}[/dim]")
106
+ if config.run_as:
107
+ console.print(f" Run As: [blue]{config.run_as}[/blue]")
108
+ else:
109
+ masked_id = config.client_id[:8] + "..." if config.client_id and len(config.client_id) > 8 else "***"
110
+ console.print(f" Client ID: [dim]{masked_id}[/dim]")
111
+
112
+ console.print(f" SSL Verify: {'Yes' if config.verify_ssl else 'No'}")
113
+ console.print(f" Timeout: {config.timeout}s")
114
+ console.print(f" API Version: {config.api_version}")
115
+ console.print()
116
+
117
+ except ValueError as e:
118
+ print_error(f"Configuration error: {e}")
119
+ console.print("\n[dim]Set environment variables:[/dim]")
120
+ console.print(" PWS_API_URL=https://your-server/BeyondTrust/api/public/v3")
121
+ console.print(" PWS_API_KEY=your-api-key")
122
+ console.print(" [dim]or[/dim]")
123
+ console.print(" PWS_CLIENT_ID=your-client-id")
124
+ console.print(" PWS_CLIENT_SECRET=your-client-secret")
125
+ raise typer.Exit(1)
126
+
127
+
128
+ @app.command("test")
129
+ def test_connection() -> None:
130
+ """Test the API connection and authentication.
131
+
132
+ Performs a full authentication and makes a simple API call
133
+ to verify connectivity.
134
+
135
+ Example:
136
+ pws auth test
137
+ """
138
+ try:
139
+ config = get_config()
140
+ console.print(f"[dim]Testing connection to:[/dim] {config.api_url}")
141
+
142
+ with get_client() as client:
143
+ # Authenticate
144
+ console.print("[dim]Authenticating...[/dim]")
145
+ response = client.authenticate()
146
+ console.print("[green]✓[/green] Authentication successful")
147
+
148
+ # Try a simple API call
149
+ console.print("[dim]Testing API call...[/dim]")
150
+ platforms = client.list_platforms()
151
+ console.print(f"[green]✓[/green] API call successful ({len(platforms)} platforms found)")
152
+
153
+ console.print("\n[bold green]All tests passed![/bold green]")
154
+
155
+ except ValueError as e:
156
+ print_error(f"Configuration error: {e}")
157
+ raise typer.Exit(1)
158
+ except httpx.HTTPStatusError as e:
159
+ print_api_error(e, "test connection")
160
+ raise typer.Exit(1)
161
+ except httpx.RequestError as e:
162
+ print_api_error(e, "test connection")
163
+ raise typer.Exit(1)
164
+ except Exception as e:
165
+ print_api_error(e, "test connection")
166
+ raise typer.Exit(1)