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,673 @@
1
+ """EPMW policies commands - manage policies, revisions, and application groups."""
2
+
3
+ import json as json_lib
4
+ from typing import Optional
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from bt_cli.core.output import OutputFormat, print_api_error, print_error, print_json, print_success, print_warning
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="EPM Windows policies management")
15
+ revisions_app = typer.Typer(no_args_is_help=True, help="Policy revisions management")
16
+ appgroups_app = typer.Typer(no_args_is_help=True, help="Policy application groups (policy editor)")
17
+ app.add_typer(revisions_app, name="revisions")
18
+ app.add_typer(appgroups_app, name="appgroups")
19
+
20
+ console = Console()
21
+
22
+
23
+ # ============================================================================
24
+ # Policy Commands
25
+ # ============================================================================
26
+
27
+
28
+ @app.command("list")
29
+ def list_policies(
30
+ output: OutputFormat = typer.Option(
31
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
32
+ ),
33
+ ):
34
+ """List all policies."""
35
+ from bt_cli.epmw.client import get_client
36
+
37
+ try:
38
+ client = get_client()
39
+ policies = client.list_policies()
40
+
41
+ if output == OutputFormat.JSON:
42
+ print_json(policies)
43
+ else:
44
+ # Build mapping of policyId -> list of group names
45
+ groups = client.list_groups()
46
+ policy_groups: dict[str, list[str]] = {}
47
+ for group in groups:
48
+ policy_id = group.get("policyId")
49
+ if policy_id:
50
+ if policy_id not in policy_groups:
51
+ policy_groups[policy_id] = []
52
+ policy_groups[policy_id].append(group.get("name", "Unknown"))
53
+
54
+ table = Table(title="Policies")
55
+ table.add_column("ID", style="cyan", no_wrap=True, min_width=36)
56
+ table.add_column("Name", style="green")
57
+ table.add_column("Rev", justify="right")
58
+ table.add_column("Modified By", style="dim")
59
+ table.add_column("Assigned To", style="blue")
60
+ table.add_column("Checked Out By", style="yellow")
61
+
62
+ for policy in policies:
63
+ has_draft = policy.get("hasOpenDraft", False)
64
+ draft_user = policy.get("draftUser", "")
65
+ if has_draft and draft_user:
66
+ checkout_display = draft_user.split("@")[0] if "@" in draft_user else draft_user
67
+ elif has_draft:
68
+ checkout_display = "[dim]Unknown[/dim]"
69
+ else:
70
+ checkout_display = "-"
71
+
72
+ # Get assigned groups for this policy
73
+ policy_id = policy.get("id", "")
74
+ assigned_groups = policy_groups.get(policy_id, [])
75
+ if assigned_groups:
76
+ assigned_display = ", ".join(assigned_groups)
77
+ else:
78
+ assigned_display = "[dim]-[/dim]"
79
+
80
+ table.add_row(
81
+ str(policy_id), # Show full UUID
82
+ policy.get("name", ""),
83
+ str(policy.get("revision", "")),
84
+ (policy.get("lastModifiedUser", "") or "-").split("@")[0],
85
+ assigned_display,
86
+ checkout_display,
87
+ )
88
+
89
+ console.print(table)
90
+ except httpx.HTTPStatusError as e:
91
+ print_api_error(e, "list policies")
92
+ raise typer.Exit(1)
93
+ except httpx.RequestError as e:
94
+ print_api_error(e, "list policies")
95
+ raise typer.Exit(1)
96
+ except Exception as e:
97
+ print_api_error(e, "list policies")
98
+ raise typer.Exit(1)
99
+
100
+
101
+ @app.command("get")
102
+ def get_policy(
103
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
104
+ output: OutputFormat = typer.Option(
105
+ OutputFormat.JSON, "--output", "-o", help="Output format"
106
+ ),
107
+ ):
108
+ """Get policy details."""
109
+ from bt_cli.epmw.client import get_client
110
+
111
+ try:
112
+ client = get_client()
113
+ policy = client.get_policy(policy_id)
114
+
115
+ if output == OutputFormat.JSON:
116
+ print_json(policy)
117
+ else:
118
+ console.print(Panel(
119
+ f"[bold]Name:[/bold] {policy.get('name', '')}\n"
120
+ f"[bold]ID:[/bold] {policy.get('id', '')}\n"
121
+ f"[bold]Revision:[/bold] {policy.get('revision', '')}\n"
122
+ f"[bold]Description:[/bold] {policy.get('description', '-')}\n"
123
+ f"[bold]Assigned:[/bold] {'Yes' if policy.get('isAssignedToGroup') else 'No'}\n"
124
+ f"[bold]Has Draft:[/bold] {'Yes' if policy.get('hasOpenDraft') else 'No'}\n"
125
+ f"[bold]Draft User:[/bold] {policy.get('draftUser', '-')}\n"
126
+ f"[bold]Modified:[/bold] {policy.get('lastModifiedDate', '-')}\n"
127
+ f"[bold]Modified By:[/bold] {policy.get('lastModifiedUser', '-')}",
128
+ title=f"Policy: {policy.get('name', '')}",
129
+ ))
130
+ except httpx.HTTPStatusError as e:
131
+ print_api_error(e, "get policy")
132
+ raise typer.Exit(1)
133
+ except httpx.RequestError as e:
134
+ print_api_error(e, "get policy")
135
+ raise typer.Exit(1)
136
+ except Exception as e:
137
+ print_api_error(e, "get policy")
138
+ raise typer.Exit(1)
139
+
140
+
141
+ @app.command("create")
142
+ def create_policy(
143
+ name: str = typer.Option(..., "--name", "-n", help="Policy name"),
144
+ file: str = typer.Option(..., "--file", "-f", help="Path to policy XML file"),
145
+ description: str = typer.Option("", "--description", "-d", help="Policy description"),
146
+ output: OutputFormat = typer.Option(
147
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
148
+ ),
149
+ ):
150
+ """Create a new policy from an XML file.
151
+
152
+ The policy file should be in EPM Windows policy XML format. You can get a
153
+ template by downloading an existing policy:
154
+
155
+ bt epmw policies download <policy_id> > template.xml
156
+ """
157
+ from bt_cli.epmw.client import get_client
158
+
159
+ try:
160
+ # Read policy file
161
+ try:
162
+ with open(file, "r") as f:
163
+ policy_content = f.read()
164
+ except FileNotFoundError:
165
+ print_error(f"Policy file not found: {file}")
166
+ raise typer.Exit(1)
167
+
168
+ client = get_client()
169
+ policy_id = client.create_policy(name, description, policy_content)
170
+
171
+ if output == OutputFormat.JSON:
172
+ # Fetch full policy details for JSON output
173
+ policy = client.get_policy(policy_id)
174
+ print_json(policy)
175
+ else:
176
+ print_success(f"Created policy '{name}' (ID: {policy_id})")
177
+ except httpx.HTTPStatusError as e:
178
+ print_api_error(e, "create policy")
179
+ raise typer.Exit(1)
180
+ except httpx.RequestError as e:
181
+ print_api_error(e, "create policy")
182
+ raise typer.Exit(1)
183
+ except Exception as e:
184
+ print_api_error(e, "create policy")
185
+ raise typer.Exit(1)
186
+
187
+
188
+ @app.command("delete")
189
+ def delete_policy(
190
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
191
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
192
+ ):
193
+ """Delete a policy."""
194
+ from bt_cli.epmw.client import get_client
195
+
196
+ try:
197
+ client = get_client()
198
+
199
+ if not force:
200
+ policy = client.get_policy(policy_id)
201
+ policy_name = policy.get("name", policy_id)
202
+ if not typer.confirm(f"Delete policy '{policy_name}'?"):
203
+ print_warning("Cancelled.")
204
+ raise typer.Exit(0)
205
+
206
+ client.delete_policy(policy_id)
207
+ print_success(f"Deleted policy {policy_id[:8]}...")
208
+ except httpx.HTTPStatusError as e:
209
+ print_api_error(e, "delete policy")
210
+ raise typer.Exit(1)
211
+ except httpx.RequestError as e:
212
+ print_api_error(e, "delete policy")
213
+ raise typer.Exit(1)
214
+ except typer.Exit:
215
+ raise
216
+ except Exception as e:
217
+ print_api_error(e, "delete policy")
218
+ raise typer.Exit(1)
219
+
220
+
221
+ @app.command("update")
222
+ def update_policy(
223
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
224
+ name: str = typer.Option(..., "--name", "-n", help="New policy name"),
225
+ description: str = typer.Option("", "--description", "-d", help="New description"),
226
+ output: OutputFormat = typer.Option(
227
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
228
+ ),
229
+ ):
230
+ """Update a policy's name and description."""
231
+ from bt_cli.epmw.client import get_client
232
+
233
+ try:
234
+ client = get_client()
235
+ result = client.update_policy(policy_id, name, description)
236
+
237
+ if output == OutputFormat.JSON:
238
+ print_json(result)
239
+ else:
240
+ print_success(f"Updated policy '{name}' (ID: {policy_id[:8]}...)")
241
+ except httpx.HTTPStatusError as e:
242
+ print_api_error(e, "update policy")
243
+ raise typer.Exit(1)
244
+ except httpx.RequestError as e:
245
+ print_api_error(e, "update policy")
246
+ raise typer.Exit(1)
247
+ except Exception as e:
248
+ print_api_error(e, "update policy")
249
+ raise typer.Exit(1)
250
+
251
+
252
+ @app.command("revert")
253
+ def revert_policy(
254
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
255
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
256
+ ):
257
+ """Revert a policy to discard draft changes."""
258
+ from bt_cli.epmw.client import get_client
259
+
260
+ try:
261
+ client = get_client()
262
+
263
+ if not force:
264
+ policy = client.get_policy(policy_id)
265
+ policy_name = policy.get("name", policy_id)
266
+ if not policy.get("hasOpenDraft"):
267
+ print_warning(f"Policy '{policy_name}' has no draft to revert.")
268
+ raise typer.Exit(0)
269
+ if not typer.confirm(f"Revert draft changes for policy '{policy_name}'?"):
270
+ print_warning("Cancelled.")
271
+ raise typer.Exit(0)
272
+
273
+ client.revert_policy(policy_id)
274
+ print_success(f"Reverted policy {policy_id[:8]}... - draft changes discarded")
275
+ except httpx.HTTPStatusError as e:
276
+ print_api_error(e, "revert policy")
277
+ raise typer.Exit(1)
278
+ except httpx.RequestError as e:
279
+ print_api_error(e, "revert policy")
280
+ raise typer.Exit(1)
281
+ except typer.Exit:
282
+ raise
283
+ except Exception as e:
284
+ print_api_error(e, "revert policy")
285
+ raise typer.Exit(1)
286
+
287
+
288
+ @app.command("download")
289
+ def download_policy(
290
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
291
+ ):
292
+ """Download policy content (XML format)."""
293
+ from bt_cli.epmw.client import get_client
294
+
295
+ try:
296
+ client = get_client()
297
+ content = client.download_policy(policy_id)
298
+ # Policy content is XML
299
+ typer.echo(content)
300
+ except httpx.HTTPStatusError as e:
301
+ print_api_error(e, "download policy")
302
+ raise typer.Exit(1)
303
+ except httpx.RequestError as e:
304
+ print_api_error(e, "download policy")
305
+ raise typer.Exit(1)
306
+ except Exception as e:
307
+ print_api_error(e, "download policy")
308
+ raise typer.Exit(1)
309
+
310
+
311
+ @app.command("groups")
312
+ def list_policy_groups(
313
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
314
+ output: OutputFormat = typer.Option(
315
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
316
+ ),
317
+ ):
318
+ """List groups assigned to a policy."""
319
+ from bt_cli.epmw.client import get_client
320
+
321
+ try:
322
+ client = get_client()
323
+ groups = client.get_policy_groups(policy_id)
324
+
325
+ if output == OutputFormat.JSON:
326
+ print_json(groups)
327
+ else:
328
+ if not groups:
329
+ print_warning("No groups assigned to this policy.")
330
+ return
331
+
332
+ table = Table(title="Assigned Groups")
333
+ table.add_column("ID", style="cyan", no_wrap=True)
334
+ table.add_column("Name", style="green")
335
+ table.add_column("Description", style="dim")
336
+
337
+ for group in groups:
338
+ table.add_row(
339
+ str(group.get("id", ""))[:8] + "...",
340
+ group.get("name", ""),
341
+ group.get("description", "-"),
342
+ )
343
+
344
+ console.print(table)
345
+ except httpx.HTTPStatusError as e:
346
+ print_api_error(e, "list policy groups")
347
+ raise typer.Exit(1)
348
+ except httpx.RequestError as e:
349
+ print_api_error(e, "list policy groups")
350
+ raise typer.Exit(1)
351
+ except Exception as e:
352
+ print_api_error(e, "list policy groups")
353
+ raise typer.Exit(1)
354
+
355
+
356
+ # ============================================================================
357
+ # Revisions Subcommands
358
+ # ============================================================================
359
+
360
+
361
+ @revisions_app.command("list")
362
+ def list_revisions(
363
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
364
+ output: OutputFormat = typer.Option(
365
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
366
+ ),
367
+ ):
368
+ """List all revisions of a policy."""
369
+ from bt_cli.epmw.client import get_client
370
+
371
+ try:
372
+ client = get_client()
373
+ revisions = client.list_policy_revisions(policy_id)
374
+
375
+ if output == OutputFormat.JSON:
376
+ print_json(revisions)
377
+ else:
378
+ if not revisions:
379
+ print_warning("No revisions found.")
380
+ return
381
+
382
+ table = Table(title="Policy Revisions")
383
+ table.add_column("Revision ID", style="cyan", no_wrap=True)
384
+ table.add_column("Rev #", justify="right")
385
+ table.add_column("Comment", style="dim")
386
+ table.add_column("Modified", style="blue")
387
+ table.add_column("Modified By", style="green")
388
+
389
+ for rev in revisions:
390
+ table.add_row(
391
+ str(rev.get("id", ""))[:8] + "...",
392
+ str(rev.get("revision", "")),
393
+ rev.get("comment", "-") or "-",
394
+ rev.get("lastModifiedDate", "-") or "-",
395
+ (rev.get("lastModifiedUser", "") or "-").split("@")[0],
396
+ )
397
+
398
+ console.print(table)
399
+ except httpx.HTTPStatusError as e:
400
+ print_api_error(e, "list revisions")
401
+ raise typer.Exit(1)
402
+ except httpx.RequestError as e:
403
+ print_api_error(e, "list revisions")
404
+ raise typer.Exit(1)
405
+ except Exception as e:
406
+ print_api_error(e, "list revisions")
407
+ raise typer.Exit(1)
408
+
409
+
410
+ @revisions_app.command("get")
411
+ def get_revision(
412
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
413
+ revision_id: str = typer.Argument(..., help="Revision ID (UUID)"),
414
+ output: OutputFormat = typer.Option(
415
+ OutputFormat.JSON, "--output", "-o", help="Output format"
416
+ ),
417
+ ):
418
+ """Download a specific policy revision."""
419
+ from bt_cli.epmw.client import get_client
420
+
421
+ try:
422
+ client = get_client()
423
+ content = client.get_policy_revision(policy_id, revision_id)
424
+
425
+ if output == OutputFormat.JSON:
426
+ print_json(content)
427
+ else:
428
+ typer.echo(content)
429
+ except httpx.HTTPStatusError as e:
430
+ print_api_error(e, "get revision")
431
+ raise typer.Exit(1)
432
+ except httpx.RequestError as e:
433
+ print_api_error(e, "get revision")
434
+ raise typer.Exit(1)
435
+ except Exception as e:
436
+ print_api_error(e, "get revision")
437
+ raise typer.Exit(1)
438
+
439
+
440
+ @revisions_app.command("upload")
441
+ def upload_revision(
442
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
443
+ file: str = typer.Argument(..., help="Path to JSON file with policy content"),
444
+ comment: str = typer.Option("", "--comment", "-c", help="Revision comment"),
445
+ output: OutputFormat = typer.Option(
446
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
447
+ ),
448
+ ):
449
+ """Upload a new policy revision from a JSON file."""
450
+ from bt_cli.epmw.client import get_client
451
+
452
+ try:
453
+ # Read and parse the JSON file
454
+ with open(file, "r") as f:
455
+ content = json_lib.load(f)
456
+
457
+ client = get_client()
458
+ result = client.upload_policy_revision(policy_id, content, comment)
459
+
460
+ if output == OutputFormat.JSON:
461
+ print_json(result)
462
+ else:
463
+ rev_id = result.get("id", "") if result else ""
464
+ print_success(f"Uploaded new revision for policy {policy_id[:8]}...")
465
+ if rev_id:
466
+ console.print(f" Revision ID: {rev_id[:8]}...")
467
+ except FileNotFoundError:
468
+ print_api_error(FileNotFoundError(f"File not found: {file}"), "upload revision")
469
+ raise typer.Exit(1)
470
+ except json_lib.JSONDecodeError as e:
471
+ print_api_error(e, "upload revision (invalid JSON)")
472
+ raise typer.Exit(1)
473
+ except httpx.HTTPStatusError as e:
474
+ print_api_error(e, "upload revision")
475
+ raise typer.Exit(1)
476
+ except httpx.RequestError as e:
477
+ print_api_error(e, "upload revision")
478
+ raise typer.Exit(1)
479
+ except Exception as e:
480
+ print_api_error(e, "upload revision")
481
+ raise typer.Exit(1)
482
+
483
+
484
+ # ============================================================================
485
+ # Application Groups Subcommands (Policy Editor)
486
+ # ============================================================================
487
+
488
+
489
+ @appgroups_app.command("list")
490
+ def list_app_groups(
491
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
492
+ output: OutputFormat = typer.Option(
493
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
494
+ ),
495
+ ):
496
+ """List application groups in a policy."""
497
+ from bt_cli.epmw.client import get_client
498
+
499
+ try:
500
+ client = get_client()
501
+ groups = client.list_application_groups(policy_id)
502
+
503
+ if output == OutputFormat.JSON:
504
+ print_json(groups)
505
+ else:
506
+ if not groups:
507
+ print_warning("No application groups found in this policy.")
508
+ return
509
+
510
+ table = Table(title="Application Groups")
511
+ table.add_column("ID", style="cyan", no_wrap=True)
512
+ table.add_column("Name", style="green")
513
+ table.add_column("Description", style="dim")
514
+ table.add_column("Hidden", justify="center")
515
+
516
+ for group in groups:
517
+ table.add_row(
518
+ str(group.get("id", ""))[:8] + "...",
519
+ group.get("name", ""),
520
+ group.get("description", "-") or "-",
521
+ "[yellow]Yes[/yellow]" if group.get("hidden") else "[dim]No[/dim]",
522
+ )
523
+
524
+ console.print(table)
525
+ except httpx.HTTPStatusError as e:
526
+ print_api_error(e, "list application groups")
527
+ raise typer.Exit(1)
528
+ except httpx.RequestError as e:
529
+ print_api_error(e, "list application groups")
530
+ raise typer.Exit(1)
531
+ except Exception as e:
532
+ print_api_error(e, "list application groups")
533
+ raise typer.Exit(1)
534
+
535
+
536
+ @appgroups_app.command("get")
537
+ def get_app_group(
538
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
539
+ app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
540
+ output: OutputFormat = typer.Option(
541
+ OutputFormat.JSON, "--output", "-o", help="Output format"
542
+ ),
543
+ ):
544
+ """Get details of an application group."""
545
+ from bt_cli.epmw.client import get_client
546
+
547
+ try:
548
+ client = get_client()
549
+ group = client.get_application_group(policy_id, app_group_id)
550
+
551
+ if output == OutputFormat.JSON:
552
+ print_json(group)
553
+ else:
554
+ console.print(Panel(
555
+ f"[bold]Name:[/bold] {group.get('name', '')}\n"
556
+ f"[bold]ID:[/bold] {group.get('id', '')}\n"
557
+ f"[bold]Description:[/bold] {group.get('description', '-') or '-'}\n"
558
+ f"[bold]Hidden:[/bold] {'Yes' if group.get('hidden') else 'No'}",
559
+ title=f"Application Group: {group.get('name', '')}",
560
+ ))
561
+ except httpx.HTTPStatusError as e:
562
+ print_api_error(e, "get application group")
563
+ raise typer.Exit(1)
564
+ except httpx.RequestError as e:
565
+ print_api_error(e, "get application group")
566
+ raise typer.Exit(1)
567
+ except Exception as e:
568
+ print_api_error(e, "get application group")
569
+ raise typer.Exit(1)
570
+
571
+
572
+ @appgroups_app.command("create")
573
+ def create_app_group(
574
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
575
+ name: str = typer.Option(..., "--name", "-n", help="Application group name"),
576
+ description: str = typer.Option("", "--description", "-d", help="Description"),
577
+ hidden: bool = typer.Option(False, "--hidden", help="Mark group as hidden"),
578
+ output: OutputFormat = typer.Option(
579
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
580
+ ),
581
+ ):
582
+ """Create an application group in a policy."""
583
+ from bt_cli.epmw.client import get_client
584
+
585
+ try:
586
+ client = get_client()
587
+ result = client.create_application_group(policy_id, name, description, hidden)
588
+
589
+ if output == OutputFormat.JSON:
590
+ print_json(result)
591
+ else:
592
+ group_id = result.get("id", "") if result else ""
593
+ print_success(f"Created application group '{name}'")
594
+ if group_id:
595
+ console.print(f" ID: {group_id[:8]}...")
596
+ except httpx.HTTPStatusError as e:
597
+ print_api_error(e, "create application group")
598
+ raise typer.Exit(1)
599
+ except httpx.RequestError as e:
600
+ print_api_error(e, "create application group")
601
+ raise typer.Exit(1)
602
+ except Exception as e:
603
+ print_api_error(e, "create application group")
604
+ raise typer.Exit(1)
605
+
606
+
607
+ @appgroups_app.command("update")
608
+ def update_app_group(
609
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
610
+ app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
611
+ name: str = typer.Option(..., "--name", "-n", help="New name"),
612
+ description: str = typer.Option("", "--description", "-d", help="New description"),
613
+ hidden: bool = typer.Option(False, "--hidden", help="Mark group as hidden"),
614
+ output: OutputFormat = typer.Option(
615
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
616
+ ),
617
+ ):
618
+ """Update an application group."""
619
+ from bt_cli.epmw.client import get_client
620
+
621
+ try:
622
+ client = get_client()
623
+ result = client.update_application_group(
624
+ policy_id, app_group_id, name, description, hidden
625
+ )
626
+
627
+ if output == OutputFormat.JSON:
628
+ print_json(result)
629
+ else:
630
+ print_success(f"Updated application group '{name}'")
631
+ except httpx.HTTPStatusError as e:
632
+ print_api_error(e, "update application group")
633
+ raise typer.Exit(1)
634
+ except httpx.RequestError as e:
635
+ print_api_error(e, "update application group")
636
+ raise typer.Exit(1)
637
+ except Exception as e:
638
+ print_api_error(e, "update application group")
639
+ raise typer.Exit(1)
640
+
641
+
642
+ @appgroups_app.command("delete")
643
+ def delete_app_group(
644
+ policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
645
+ app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
646
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
647
+ ):
648
+ """Delete an application group."""
649
+ from bt_cli.epmw.client import get_client
650
+
651
+ try:
652
+ client = get_client()
653
+
654
+ if not force:
655
+ group = client.get_application_group(policy_id, app_group_id)
656
+ group_name = group.get("name", app_group_id)
657
+ if not typer.confirm(f"Delete application group '{group_name}'?"):
658
+ print_warning("Cancelled.")
659
+ raise typer.Exit(0)
660
+
661
+ client.delete_application_group(policy_id, app_group_id)
662
+ print_success(f"Deleted application group {app_group_id[:8]}...")
663
+ except httpx.HTTPStatusError as e:
664
+ print_api_error(e, "delete application group")
665
+ raise typer.Exit(1)
666
+ except httpx.RequestError as e:
667
+ print_api_error(e, "delete application group")
668
+ raise typer.Exit(1)
669
+ except typer.Exit:
670
+ raise
671
+ except Exception as e:
672
+ print_api_error(e, "delete application group")
673
+ raise typer.Exit(1)