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,452 @@
1
+ """Import/export commands for Password Safe bulk operations."""
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, print_error, print_success, print_warning
12
+ from ...core.csv_utils import read_csv, write_csv, validate_required_fields, parse_bool, parse_int
13
+ from ..client.base import get_client
14
+
15
+ import_app = typer.Typer(no_args_is_help=True, help="Import resources from CSV files")
16
+ export_app = typer.Typer(no_args_is_help=True, help="Export sample CSV templates")
17
+ console = Console()
18
+
19
+ # Column definitions for CSV formats
20
+ SYSTEMS_COLUMNS = [
21
+ "name", "ip_address", "workgroup_id", "platform_id", "port",
22
+ "functional_account_id", "elevation_command", "auto_manage",
23
+ "account_name", "account_password", "account_description"
24
+ ]
25
+
26
+ SECRETS_COLUMNS = [
27
+ "folder_path", "title", "username", "password", "description", "notes"
28
+ ]
29
+
30
+ # Sample data for templates
31
+ SYSTEMS_SAMPLE = [
32
+ {
33
+ "name": "web-server-01", "ip_address": "10.0.1.50", "workgroup_id": "3",
34
+ "platform_id": "2", "port": "22", "functional_account_id": "7",
35
+ "elevation_command": "sudo", "auto_manage": "true",
36
+ "account_name": "root", "account_password": "", "account_description": "Root account"
37
+ },
38
+ {
39
+ "name": "web-server-01", "ip_address": "10.0.1.50", "workgroup_id": "3",
40
+ "platform_id": "2", "port": "22", "functional_account_id": "7",
41
+ "elevation_command": "sudo", "auto_manage": "true",
42
+ "account_name": "svc-backup", "account_password": "Backup#2026!", "account_description": "Backup service"
43
+ },
44
+ {
45
+ "name": "db-server-01", "ip_address": "10.0.1.60", "workgroup_id": "3",
46
+ "platform_id": "2", "port": "22", "functional_account_id": "7",
47
+ "elevation_command": "sudo", "auto_manage": "true",
48
+ "account_name": "postgres", "account_password": "", "account_description": "Database admin"
49
+ },
50
+ {
51
+ "name": "win-server-01", "ip_address": "10.0.2.10", "workgroup_id": "2",
52
+ "platform_id": "1", "port": "5985", "functional_account_id": "",
53
+ "elevation_command": "", "auto_manage": "false",
54
+ "account_name": "Administrator", "account_password": "InitialPass123!", "account_description": "Local admin"
55
+ },
56
+ ]
57
+
58
+ SECRETS_SAMPLE = [
59
+ {
60
+ "folder_path": "Example Safe/Database", "title": "example-db-cred",
61
+ "username": "example_user", "password": "CHANGE_ME_123!",
62
+ "description": "Example database credential", "notes": ""
63
+ },
64
+ {
65
+ "folder_path": "Example Safe/API Keys", "title": "example-api-key",
66
+ "username": "api_service", "password": "example_api_key_here",
67
+ "description": "Example API key", "notes": '{"env":"example"}'
68
+ },
69
+ {
70
+ "folder_path": "Example Safe/Service Accounts", "title": "example-svc-account",
71
+ "username": "svc_example", "password": "CHANGE_ME_456!",
72
+ "description": "Example service account", "notes": ""
73
+ },
74
+ ]
75
+
76
+
77
+ @import_app.command("systems")
78
+ def import_systems(
79
+ file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
80
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
81
+ workgroup: Optional[int] = typer.Option(None, "--workgroup", "-w", help="Override workgroup ID for all rows"),
82
+ ) -> None:
83
+ """Import managed systems and accounts from CSV.
84
+
85
+ Each row creates a system + account. Multiple accounts per system use
86
+ multiple rows with the same system name (system is created only once).
87
+
88
+ Required columns: name, ip_address, workgroup_id, account_name
89
+ Optional: platform_id, port, functional_account_id, elevation_command,
90
+ auto_manage, account_password, account_description
91
+
92
+ Examples:
93
+ bt pws import systems --file systems.csv --dry-run
94
+ bt pws import systems --file systems.csv
95
+ bt pws import systems --file systems.csv --workgroup 3
96
+ """
97
+ try:
98
+ rows = read_csv(file)
99
+ if not rows:
100
+ print_error("CSV file is empty")
101
+ raise typer.Exit(1)
102
+
103
+ console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
104
+
105
+ # Validate all rows first
106
+ errors = []
107
+ required = ["name", "ip_address", "account_name"]
108
+ if not workgroup:
109
+ required.append("workgroup_id")
110
+
111
+ for i, row in enumerate(rows, 1):
112
+ row_errors = validate_required_fields(row, required, i)
113
+ errors.extend(row_errors)
114
+
115
+ if errors:
116
+ print_error("Validation errors:")
117
+ for err in errors[:20]:
118
+ console.print(f" [red]{err}[/red]")
119
+ if len(errors) > 20:
120
+ console.print(f" [red]... and {len(errors) - 20} more errors[/red]")
121
+ raise typer.Exit(1)
122
+
123
+ # Group rows by system name
124
+ systems_rows: dict[str, list[dict]] = {}
125
+ for row in rows:
126
+ name = row["name"].strip()
127
+ if name not in systems_rows:
128
+ systems_rows[name] = []
129
+ systems_rows[name].append(row)
130
+
131
+ console.print(f"[dim]Found {len(systems_rows)} unique systems with {len(rows)} total accounts[/dim]")
132
+
133
+ if dry_run:
134
+ console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
135
+ table = Table(title="Systems to Create")
136
+ table.add_column("System", style="green")
137
+ table.add_column("IP", style="cyan")
138
+ table.add_column("Workgroup", style="yellow")
139
+ table.add_column("Platform", style="magenta")
140
+ table.add_column("Accounts", style="blue")
141
+
142
+ for name, sys_rows in systems_rows.items():
143
+ first = sys_rows[0]
144
+ wg = workgroup or parse_int(first.get("workgroup_id", ""))
145
+ accounts = ", ".join(r["account_name"] for r in sys_rows)
146
+ table.add_row(
147
+ name,
148
+ first.get("ip_address", ""),
149
+ str(wg),
150
+ first.get("platform_id", "2"),
151
+ accounts
152
+ )
153
+
154
+ console.print(table)
155
+ console.print(f"\n[dim]Would create {len(systems_rows)} systems with {len(rows)} accounts[/dim]")
156
+ return
157
+
158
+ # Actually import
159
+ with get_client() as client:
160
+ client.authenticate()
161
+
162
+ created_systems = 0
163
+ created_accounts = 0
164
+ system_ids: dict[str, int] = {} # Map system name to ID
165
+
166
+ for name, sys_rows in systems_rows.items():
167
+ first = sys_rows[0]
168
+ wg_id = workgroup or parse_int(first.get("workgroup_id", ""))
169
+
170
+ if not wg_id:
171
+ print_warning(f"Skipping {name}: No workgroup ID")
172
+ continue
173
+
174
+ try:
175
+ # Create asset
176
+ console.print(f"[dim]Creating asset '{name}'...[/dim]")
177
+ asset = client.create_asset(
178
+ workgroup_id=wg_id,
179
+ ip_address=first.get("ip_address", "").strip(),
180
+ asset_name=name,
181
+ )
182
+ asset_id = asset.get("AssetID")
183
+
184
+ # Create managed system
185
+ console.print(f"[dim]Creating managed system '{name}'...[/dim]")
186
+ platform_id = parse_int(first.get("platform_id", ""), 2)
187
+ port = parse_int(first.get("port", ""), 22)
188
+ func_acct = parse_int(first.get("functional_account_id", ""))
189
+ auto_manage = parse_bool(first.get("auto_manage", ""))
190
+ elevation = first.get("elevation_command", "").strip() or None
191
+
192
+ system = client.create_managed_system(
193
+ system_name=name,
194
+ platform_id=platform_id,
195
+ asset_id=asset_id,
196
+ port=port,
197
+ functional_account_id=func_acct,
198
+ auto_management_flag=auto_manage if func_acct else False,
199
+ elevation_command=elevation,
200
+ )
201
+ system_id = system.get("ManagedSystemID")
202
+ system_ids[name] = system_id
203
+ created_systems += 1
204
+ console.print(f" [green]Created system: {name} (ID: {system_id})[/green]")
205
+
206
+ # Create accounts for this system
207
+ for row in sys_rows:
208
+ account_name = row.get("account_name", "").strip()
209
+ password = row.get("account_password", "").strip() or None
210
+ description = row.get("account_description", "").strip() or None
211
+
212
+ console.print(f"[dim]Creating account '{account_name}' on {name}...[/dim]")
213
+ account = client.create_managed_account(
214
+ system_id=system_id,
215
+ account_name=account_name,
216
+ password=password,
217
+ auto_management_flag=auto_manage if func_acct else False,
218
+ )
219
+ account_id = account.get("ManagedAccountID")
220
+ created_accounts += 1
221
+ console.print(f" [green]Created account: {account_name} (ID: {account_id})[/green]")
222
+
223
+ except httpx.HTTPStatusError as e:
224
+ print_warning(f"Error creating {name}: {e.response.text}")
225
+ except Exception as e:
226
+ print_warning(f"Error creating {name}: {e}")
227
+
228
+ print_success(f"Import complete: {created_systems} systems, {created_accounts} accounts created")
229
+
230
+ except FileNotFoundError as e:
231
+ print_error(str(e))
232
+ raise typer.Exit(1)
233
+ except typer.Exit:
234
+ raise
235
+ except Exception as e:
236
+ print_api_error(e, "import systems")
237
+ raise typer.Exit(1)
238
+
239
+
240
+ @import_app.command("secrets")
241
+ def import_secrets(
242
+ file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
243
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
244
+ folder: Optional[str] = typer.Option(None, "--folder", help="Override folder path for all rows"),
245
+ ) -> None:
246
+ """Import secrets from CSV.
247
+
248
+ Required columns: folder_path, title
249
+ Optional: username, password, description, notes
250
+
251
+ The folder_path should be the full path like "SafeName/FolderName/SubFolder".
252
+
253
+ Examples:
254
+ bt pws import secrets --file secrets.csv --dry-run
255
+ bt pws import secrets --file secrets.csv
256
+ bt pws import secrets --file secrets.csv --folder "PAM Demo/Database"
257
+ """
258
+ try:
259
+ rows = read_csv(file)
260
+ if not rows:
261
+ print_error("CSV file is empty")
262
+ raise typer.Exit(1)
263
+
264
+ console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
265
+
266
+ # Validate
267
+ errors = []
268
+ required = ["title"]
269
+ if not folder:
270
+ required.append("folder_path")
271
+
272
+ for i, row in enumerate(rows, 1):
273
+ row_errors = validate_required_fields(row, required, i)
274
+ errors.extend(row_errors)
275
+
276
+ if errors:
277
+ print_error("Validation errors:")
278
+ for err in errors[:20]:
279
+ console.print(f" [red]{err}[/red]")
280
+ raise typer.Exit(1)
281
+
282
+ if dry_run:
283
+ console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
284
+ table = Table(title="Secrets to Create")
285
+ table.add_column("Folder", style="cyan")
286
+ table.add_column("Title", style="green")
287
+ table.add_column("Username", style="yellow")
288
+
289
+ for row in rows:
290
+ table.add_row(
291
+ folder or row.get("folder_path", ""),
292
+ row.get("title", ""),
293
+ row.get("username", "") or "-"
294
+ )
295
+
296
+ console.print(table)
297
+ console.print(f"\n[dim]Would create {len(rows)} secrets[/dim]")
298
+ return
299
+
300
+ # Import
301
+ with get_client() as client:
302
+ client.authenticate()
303
+
304
+ # Get all folders to map paths to IDs
305
+ all_folders = client.list_folders()
306
+ folder_map: dict[str, int] = {}
307
+ for f in all_folders:
308
+ path = f.get("FolderPath", "") or f.get("Name", "")
309
+ if path:
310
+ folder_map[path.lower()] = f.get("Id")
311
+
312
+ created = 0
313
+ for row in rows:
314
+ folder_path = (folder or row.get("folder_path", "")).strip()
315
+ title = row.get("title", "").strip()
316
+
317
+ # Find folder ID
318
+ folder_id = folder_map.get(folder_path.lower())
319
+ if not folder_id:
320
+ print_warning(f"Folder not found: {folder_path}, skipping {title}")
321
+ continue
322
+
323
+ try:
324
+ console.print(f"[dim]Creating secret '{title}' in {folder_path}...[/dim]")
325
+ secret = client.create_secret(
326
+ folder_id=folder_id,
327
+ title=title,
328
+ username=row.get("username", "").strip() or None,
329
+ password=row.get("password", "").strip() or None,
330
+ description=row.get("description", "").strip() or None,
331
+ notes=row.get("notes", "").strip() or None,
332
+ )
333
+ secret_id = secret.get("Id")
334
+ created += 1
335
+ console.print(f" [green]Created secret: {title} (ID: {secret_id})[/green]")
336
+ except httpx.HTTPStatusError as e:
337
+ print_warning(f"Error creating {title}: {e.response.text}")
338
+ except Exception as e:
339
+ print_warning(f"Error creating {title}: {e}")
340
+
341
+ print_success(f"Import complete: {created} secrets created")
342
+
343
+ except FileNotFoundError as e:
344
+ print_error(str(e))
345
+ raise typer.Exit(1)
346
+ except typer.Exit:
347
+ raise
348
+ except Exception as e:
349
+ print_api_error(e, "import secrets")
350
+ raise typer.Exit(1)
351
+
352
+
353
+ @export_app.command("systems")
354
+ def export_systems(
355
+ file: str = typer.Option("pws-systems-template.csv", "--file", "-f", help="Output file path"),
356
+ sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
357
+ ) -> None:
358
+ """Export sample systems CSV template.
359
+
360
+ Examples:
361
+ bt pws export systems --file systems-template.csv
362
+ bt pws export systems --file systems-template.csv --sample
363
+ """
364
+ try:
365
+ if sample:
366
+ write_csv(file, SYSTEMS_SAMPLE, SYSTEMS_COLUMNS)
367
+ print_success(f"Sample systems template exported to: {file}")
368
+ console.print(f"[dim]Contains {len(SYSTEMS_SAMPLE)} example rows[/dim]")
369
+ else:
370
+ # Export actual data from API
371
+ with get_client() as client:
372
+ client.authenticate()
373
+
374
+ systems = client.list_managed_systems()
375
+ rows = []
376
+
377
+ for sys in systems:
378
+ system_id = sys.get("ManagedSystemID")
379
+ accounts = client.list_managed_accounts(system_id=system_id)
380
+
381
+ for acc in accounts:
382
+ rows.append({
383
+ "name": sys.get("SystemName", ""),
384
+ "ip_address": sys.get("IPAddress", ""),
385
+ "workgroup_id": str(sys.get("WorkgroupID", "")),
386
+ "platform_id": str(sys.get("PlatformID", "")),
387
+ "port": str(sys.get("Port", "")),
388
+ "functional_account_id": str(sys.get("FunctionalAccountID", "") or ""),
389
+ "elevation_command": sys.get("ElevationCommand", "") or "",
390
+ "auto_manage": "true" if sys.get("AutoManagementFlag") else "false",
391
+ "account_name": acc.get("AccountName", ""),
392
+ "account_password": "", # Don't export passwords
393
+ "account_description": acc.get("Description", "") or "",
394
+ })
395
+
396
+ write_csv(file, rows, SYSTEMS_COLUMNS)
397
+ print_success(f"Exported {len(rows)} system/account rows to: {file}")
398
+
399
+ except typer.Exit:
400
+ raise
401
+ except Exception as e:
402
+ print_api_error(e, "export systems")
403
+ raise typer.Exit(1)
404
+
405
+
406
+ @export_app.command("secrets")
407
+ def export_secrets(
408
+ file: str = typer.Option("pws-secrets-template.csv", "--file", "-f", help="Output file path"),
409
+ sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
410
+ ) -> None:
411
+ """Export sample secrets CSV template.
412
+
413
+ Examples:
414
+ bt pws export secrets --file secrets-template.csv
415
+ bt pws export secrets --file secrets-template.csv --sample
416
+ """
417
+ try:
418
+ if sample:
419
+ write_csv(file, SECRETS_SAMPLE, SECRETS_COLUMNS)
420
+ print_success(f"Sample secrets template exported to: {file}")
421
+ console.print(f"[dim]Contains {len(SECRETS_SAMPLE)} example rows[/dim]")
422
+ else:
423
+ # Export actual data from API
424
+ with get_client() as client:
425
+ client.authenticate()
426
+
427
+ secrets = client.list_secrets()
428
+ folders = client.list_folders()
429
+
430
+ # Map folder IDs to paths
431
+ folder_paths = {f.get("Id"): f.get("FolderPath", "") or f.get("Name", "") for f in folders}
432
+
433
+ rows = []
434
+ for s in secrets:
435
+ folder_id = s.get("FolderId")
436
+ rows.append({
437
+ "folder_path": folder_paths.get(folder_id, ""),
438
+ "title": s.get("Title", ""),
439
+ "username": s.get("Username", "") or "",
440
+ "password": "", # Don't export passwords
441
+ "description": s.get("Description", "") or "",
442
+ "notes": s.get("Notes", "") or "",
443
+ })
444
+
445
+ write_csv(file, rows, SECRETS_COLUMNS)
446
+ print_success(f"Exported {len(rows)} secrets to: {file}")
447
+
448
+ except typer.Exit:
449
+ raise
450
+ except Exception as e:
451
+ print_api_error(e, "export secrets")
452
+ raise typer.Exit(1)
@@ -0,0 +1,118 @@
1
+ """CLI commands for managing platforms (OS types)."""
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="View available platforms (OS types)")
15
+ console = Console()
16
+
17
+
18
+ def print_platforms_table(platforms: list[dict], title: str = "Platforms") -> None:
19
+ """Print platforms 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("Short Name", style="yellow")
24
+ table.add_column("Port", style="blue")
25
+ table.add_column("Session Type", style="magenta")
26
+
27
+ for platform in platforms:
28
+ port = platform.get("DefaultPort")
29
+ session_type = platform.get("DefaultSessionType")
30
+ table.add_row(
31
+ str(platform.get("PlatformID", "")),
32
+ platform.get("Name", ""),
33
+ platform.get("ShortName", "-"),
34
+ str(port) if port else "-",
35
+ session_type if session_type else "-",
36
+ )
37
+
38
+ console.print(table)
39
+
40
+
41
+ @app.command("list")
42
+ def list_platforms(
43
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
44
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
45
+ ) -> None:
46
+ """List all available platforms."""
47
+ try:
48
+ with get_client() as client:
49
+ client.authenticate()
50
+ platforms = client.list_platforms(search=search)
51
+
52
+ if output == "json":
53
+ console.print_json(json.dumps(platforms, default=str))
54
+ else:
55
+ if platforms:
56
+ print_platforms_table(platforms)
57
+ else:
58
+ console.print("[yellow]No platforms found.[/yellow]")
59
+
60
+ except httpx.HTTPStatusError as e:
61
+ print_api_error(e, "manage platforms")
62
+ raise typer.Exit(1)
63
+ except httpx.RequestError as e:
64
+ print_api_error(e, "manage platforms")
65
+ raise typer.Exit(1)
66
+ except Exception as e:
67
+ print_api_error(e, "manage platforms")
68
+ raise typer.Exit(1)
69
+
70
+
71
+ @app.command("get")
72
+ def get_platform(
73
+ platform_id: int = typer.Argument(..., help="Platform ID"),
74
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
75
+ ) -> None:
76
+ """Get a platform by ID."""
77
+ try:
78
+ with get_client() as client:
79
+ client.authenticate()
80
+ platform = client.get_platform(platform_id)
81
+
82
+ if output == "json":
83
+ console.print_json(json.dumps(platform, default=str))
84
+ else:
85
+ console.print(f"\n[bold cyan]Platform: {platform.get('Name', 'Unknown')}[/bold cyan]\n")
86
+
87
+ info_table = Table(show_header=False, box=None)
88
+ info_table.add_column("Field", style="dim")
89
+ info_table.add_column("Value")
90
+
91
+ fields = [
92
+ ("ID", "PlatformID"),
93
+ ("Name", "Name"),
94
+ ("Short Name", "ShortName"),
95
+ ("Default Port", "DefaultPort"),
96
+ ("Session Type", "DefaultSessionType"),
97
+ ("Auto Management", "AutoManagementFlag"),
98
+ ("Elevation Support", "SupportsElevationFlag"),
99
+ ]
100
+
101
+ for label, key in fields:
102
+ value = platform.get(key)
103
+ if value is not None:
104
+ if isinstance(value, bool):
105
+ value = "Yes" if value else "No"
106
+ info_table.add_row(label, str(value))
107
+
108
+ console.print(info_table)
109
+
110
+ except httpx.HTTPStatusError as e:
111
+ print_api_error(e, "manage platforms")
112
+ raise typer.Exit(1)
113
+ except httpx.RequestError as e:
114
+ print_api_error(e, "manage platforms")
115
+ raise typer.Exit(1)
116
+ except Exception as e:
117
+ print_api_error(e, "manage platforms")
118
+ raise typer.Exit(1)