bt-cli 0.4.28__tar.gz → 0.4.30__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 (174) hide show
  1. {bt_cli-0.4.28 → bt_cli-0.4.30}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.28 → bt_cli-0.4.30}/PKG-INFO +1 -1
  3. {bt_cli-0.4.28 → bt_cli-0.4.30}/pyproject.toml +1 -1
  4. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/commands/configure.py +218 -2
  6. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/rest_debug.py +5 -0
  7. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/client/passwordsafe.py +21 -6
  8. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/credentials.py +24 -10
  9. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/quick.py +37 -16
  10. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/models/common.py +16 -0
  11. {bt_cli-0.4.28 → bt_cli-0.4.30}/.claude/skills/bt/SKILL.md +0 -0
  12. {bt_cli-0.4.28 → bt_cli-0.4.30}/.claude/skills/entitle/SKILL.md +0 -0
  13. {bt_cli-0.4.28 → bt_cli-0.4.30}/.claude/skills/epmw/SKILL.md +0 -0
  14. {bt_cli-0.4.28 → bt_cli-0.4.30}/.claude/skills/pra/SKILL.md +0 -0
  15. {bt_cli-0.4.28 → bt_cli-0.4.30}/.claude/skills/pws/SKILL.md +0 -0
  16. {bt_cli-0.4.28 → bt_cli-0.4.30}/.env.example +0 -0
  17. {bt_cli-0.4.28 → bt_cli-0.4.30}/.github/workflows/ci.yml +0 -0
  18. {bt_cli-0.4.28 → bt_cli-0.4.30}/.github/workflows/release.yml +0 -0
  19. {bt_cli-0.4.28 → bt_cli-0.4.30}/.gitignore +0 -0
  20. {bt_cli-0.4.28 → bt_cli-0.4.30}/README.md +0 -0
  21. {bt_cli-0.4.28 → bt_cli-0.4.30}/assets/cli-help.png +0 -0
  22. {bt_cli-0.4.28 → bt_cli-0.4.30}/assets/cli-output.png +0 -0
  23. {bt_cli-0.4.28 → bt_cli-0.4.30}/bt-cli.spec +0 -0
  24. {bt_cli-0.4.28 → bt_cli-0.4.30}/bt_entry.py +0 -0
  25. {bt_cli-0.4.28 → bt_cli-0.4.30}/scripts/bt_entry.py +0 -0
  26. {bt_cli-0.4.28 → bt_cli-0.4.30}/scripts/sync-package-data.sh +0 -0
  27. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/cli.py +0 -0
  28. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/commands/__init__.py +0 -0
  29. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/commands/learn.py +0 -0
  30. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/commands/quick.py +0 -0
  31. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/__init__.py +0 -0
  32. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/auth.py +0 -0
  33. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/client.py +0 -0
  34. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/config.py +0 -0
  35. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/config_file.py +0 -0
  36. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/csv_utils.py +0 -0
  37. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/errors.py +0 -0
  38. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/output.py +0 -0
  39. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/core/prompts.py +0 -0
  40. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/CLAUDE.md +0 -0
  41. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/__init__.py +0 -0
  42. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  43. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  44. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  45. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  46. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  47. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/__init__.py +0 -0
  48. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/client/__init__.py +0 -0
  49. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/client/base.py +0 -0
  50. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/__init__.py +0 -0
  51. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/accounts.py +0 -0
  52. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/applications.py +0 -0
  53. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/auth.py +0 -0
  54. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/bundles.py +0 -0
  55. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/integrations.py +0 -0
  56. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/permissions.py +0 -0
  57. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/policies.py +0 -0
  58. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/resources.py +0 -0
  59. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/roles.py +0 -0
  60. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/users.py +0 -0
  61. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/commands/workflows.py +0 -0
  62. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/__init__.py +0 -0
  63. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/bundle.py +0 -0
  64. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/common.py +0 -0
  65. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/integration.py +0 -0
  66. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/permission.py +0 -0
  67. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/policy.py +0 -0
  68. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/resource.py +0 -0
  69. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/role.py +0 -0
  70. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/user.py +0 -0
  71. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/entitle/models/workflow.py +0 -0
  72. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/__init__.py +0 -0
  73. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/client/__init__.py +0 -0
  74. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/client/base.py +0 -0
  75. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/__init__.py +0 -0
  76. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/audits.py +0 -0
  77. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/auth.py +0 -0
  78. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/computers.py +0 -0
  79. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/events.py +0 -0
  80. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/groups.py +0 -0
  81. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/policies.py +0 -0
  82. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/quick.py +0 -0
  83. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/requests.py +0 -0
  84. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/roles.py +0 -0
  85. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/tasks.py +0 -0
  86. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/commands/users.py +0 -0
  87. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/epmw/models/__init__.py +0 -0
  88. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/__init__.py +0 -0
  89. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/client/__init__.py +0 -0
  90. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/client/base.py +0 -0
  91. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/__init__.py +0 -0
  92. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/auth.py +0 -0
  93. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/import_export.py +0 -0
  94. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  95. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  96. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/jump_items.py +0 -0
  97. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  98. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/policies.py +0 -0
  99. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/quick.py +0 -0
  100. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/teams.py +0 -0
  101. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/users.py +0 -0
  102. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/commands/vault.py +0 -0
  103. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/__init__.py +0 -0
  104. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/common.py +0 -0
  105. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/jump_client.py +0 -0
  106. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/jump_group.py +0 -0
  107. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/jump_item.py +0 -0
  108. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/jumpoint.py +0 -0
  109. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/team.py +0 -0
  110. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/user.py +0 -0
  111. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pra/models/vault.py +0 -0
  112. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/__init__.py +0 -0
  113. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/client/__init__.py +0 -0
  114. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/client/base.py +0 -0
  115. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  116. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/__init__.py +0 -0
  117. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/accounts.py +0 -0
  118. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/assets.py +0 -0
  119. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/attributes.py +0 -0
  120. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/auth.py +0 -0
  121. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/clouds.py +0 -0
  122. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/config.py +0 -0
  123. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/databases.py +0 -0
  124. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/directories.py +0 -0
  125. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/functional.py +0 -0
  126. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/import_export.py +0 -0
  127. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/platforms.py +0 -0
  128. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/search.py +0 -0
  129. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/secrets.py +0 -0
  130. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/systems.py +0 -0
  131. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/users.py +0 -0
  132. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/commands/workgroups.py +0 -0
  133. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/config.py +0 -0
  134. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/models/__init__.py +0 -0
  135. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/models/account.py +0 -0
  136. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/models/asset.py +0 -0
  137. {bt_cli-0.4.28 → bt_cli-0.4.30}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.28 → bt_cli-0.4.30}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.28**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.29**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.28
3
+ Version: 0.4.30
4
4
  Summary: BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM
5
5
  Author-email: Dave Grendysz <dgrendysz@beyondtrust.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.28"
7
+ version = "0.4.30"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.28"
3
+ __version__ = "0.4.30"
@@ -298,11 +298,21 @@ def show_config(
298
298
  profile: Optional[str] = typer.Option(None, "--profile", help="Profile to show"),
299
299
  show_secrets: bool = typer.Option(False, "--show-secrets", help="Show secret values"),
300
300
  ) -> None:
301
- """Show current configuration."""
301
+ """Show configuration from config file.
302
+
303
+ Note: This only shows profiles saved to ~/.bt-cli/config.yaml.
304
+ If you're using environment variables or .env file, use:
305
+
306
+ bt configure effective
307
+
308
+ to see the actual configuration in use.
309
+ """
302
310
  config = load_config_file()
303
311
 
304
312
  if not config.profiles:
305
- print_warning("No configuration found. Run 'bt configure' to set up.")
313
+ print_warning("No profiles in config file. Run 'bt configure' to set up.")
314
+ console.print("\n[dim]Tip: If using .env or environment variables, run:[/dim]")
315
+ console.print("[cyan] bt configure effective[/cyan]")
306
316
  raise typer.Exit(0)
307
317
 
308
318
  profiles_to_show = [profile] if profile else config.list_profiles()
@@ -413,3 +423,209 @@ def show_path() -> None:
413
423
  console.print("[green]Config file exists[/green]")
414
424
  else:
415
425
  console.print("[yellow]Config file does not exist yet[/yellow]")
426
+
427
+
428
+ @app.command("import-env")
429
+ def import_from_env(
430
+ profile: str = typer.Option("default", "--profile", "-p", help="Profile name to save as"),
431
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing profile"),
432
+ ) -> None:
433
+ """Import configuration from environment variables into a profile.
434
+
435
+ This saves the current BT_* environment variables (including .env file)
436
+ into a named profile in the config file. Useful for:
437
+
438
+ - Converting .env setup to profile-based config
439
+ - Creating profiles from existing environment
440
+ - Backing up current config to file
441
+
442
+ Example:
443
+ bt configure import-env --profile production
444
+ """
445
+ import os
446
+ from dotenv import load_dotenv, find_dotenv
447
+ from rich.prompt import Confirm
448
+
449
+ # Load .env if present
450
+ env_file = find_dotenv()
451
+ if env_file:
452
+ load_dotenv(env_file)
453
+ console.print(f"[dim]Loaded .env from: {env_file}[/dim]\n")
454
+
455
+ config = load_config_file()
456
+
457
+ # Check if profile exists
458
+ if profile in config.profiles and not force:
459
+ if not Confirm.ask(f"Profile '{profile}' exists. Overwrite?", default=False):
460
+ print_info("Cancelled")
461
+ raise typer.Exit(0)
462
+
463
+ # Product environment variable mappings
464
+ env_mappings = {
465
+ "pws": {
466
+ "api_url": "BT_PWS_API_URL",
467
+ "api_key": "BT_PWS_API_KEY",
468
+ "client_id": "BT_PWS_CLIENT_ID",
469
+ "client_secret": "BT_PWS_CLIENT_SECRET",
470
+ "run_as": "BT_PWS_RUN_AS",
471
+ "verify_ssl": "BT_PWS_VERIFY_SSL",
472
+ "timeout": "BT_PWS_TIMEOUT",
473
+ },
474
+ "pra": {
475
+ "api_url": "BT_PRA_API_URL",
476
+ "client_id": "BT_PRA_CLIENT_ID",
477
+ "client_secret": "BT_PRA_CLIENT_SECRET",
478
+ "verify_ssl": "BT_PRA_VERIFY_SSL",
479
+ "timeout": "BT_PRA_TIMEOUT",
480
+ },
481
+ "entitle": {
482
+ "api_url": "BT_ENTITLE_API_URL",
483
+ "api_key": "BT_ENTITLE_API_KEY",
484
+ "verify_ssl": "BT_ENTITLE_VERIFY_SSL",
485
+ "timeout": "BT_ENTITLE_TIMEOUT",
486
+ },
487
+ "epmw": {
488
+ "api_url": "BT_EPM_API_URL",
489
+ "client_id": "BT_EPM_CLIENT_ID",
490
+ "client_secret": "BT_EPM_CLIENT_SECRET",
491
+ "verify_ssl": "BT_EPM_VERIFY_SSL",
492
+ "timeout": "BT_EPM_TIMEOUT",
493
+ },
494
+ }
495
+
496
+ imported = []
497
+ for product, env_vars in env_mappings.items():
498
+ product_config = {}
499
+ for setting, env_var in env_vars.items():
500
+ value = os.getenv(env_var)
501
+ if value:
502
+ # Convert types
503
+ if setting == "verify_ssl":
504
+ product_config[setting] = value.lower() not in ("false", "0", "no")
505
+ elif setting == "timeout":
506
+ try:
507
+ product_config[setting] = float(value)
508
+ except ValueError:
509
+ product_config[setting] = 30.0
510
+ else:
511
+ product_config[setting] = value
512
+
513
+ if product_config.get("api_url"):
514
+ # Infer auth_method for PWS
515
+ if product == "pws":
516
+ if product_config.get("api_key"):
517
+ product_config["auth_method"] = "apikey"
518
+ elif product_config.get("client_id"):
519
+ product_config["auth_method"] = "oauth"
520
+
521
+ config.set_product_config(product, product_config, profile)
522
+ imported.append(product)
523
+
524
+ if imported:
525
+ if not config.default_profile or profile == "default":
526
+ config.default_profile = profile
527
+ save_config_file(config)
528
+ print_success(f"Imported {', '.join(imported)} into profile '{profile}'")
529
+ console.print(f"[dim]Config saved to: {CONFIG_FILE}[/dim]")
530
+ else:
531
+ print_warning("No configuration found in environment variables")
532
+
533
+
534
+ @app.command("effective")
535
+ def show_effective_config(
536
+ show_secrets: bool = typer.Option(False, "--show-secrets", help="Show secret values"),
537
+ ) -> None:
538
+ """Show effective configuration from all sources.
539
+
540
+ Shows what configuration is actually in use, combining:
541
+ - Environment variables (BT_PWS_*, BT_PRA_*, etc.)
542
+ - .env file (if present)
543
+ - Config file profiles (~/.bt-cli/config.yaml)
544
+
545
+ This is useful for debugging why the CLI connects successfully
546
+ but 'bt configure show' appears empty.
547
+ """
548
+ import os
549
+ from dotenv import load_dotenv, find_dotenv
550
+
551
+ # Load .env if present
552
+ env_file = find_dotenv()
553
+ if env_file:
554
+ load_dotenv(env_file)
555
+ console.print(f"[dim]Loaded .env from: {env_file}[/dim]\n")
556
+
557
+ def mask_secret(key: str, value: str) -> str:
558
+ if not value:
559
+ return "[dim]not set[/dim]"
560
+ if not show_secrets and any(s in key.lower() for s in ["secret", "key", "password"]):
561
+ return "****" + value[-4:] if len(value) > 4 else "****"
562
+ return value
563
+
564
+ # Product environment variable mappings
565
+ products = {
566
+ "Password Safe": {
567
+ "api_url": "BT_PWS_API_URL",
568
+ "api_key": "BT_PWS_API_KEY",
569
+ "client_id": "BT_PWS_CLIENT_ID",
570
+ "client_secret": "BT_PWS_CLIENT_SECRET",
571
+ "run_as": "BT_PWS_RUN_AS",
572
+ "verify_ssl": "BT_PWS_VERIFY_SSL",
573
+ "timeout": "BT_PWS_TIMEOUT",
574
+ },
575
+ "PRA": {
576
+ "api_url": "BT_PRA_API_URL",
577
+ "client_id": "BT_PRA_CLIENT_ID",
578
+ "client_secret": "BT_PRA_CLIENT_SECRET",
579
+ "verify_ssl": "BT_PRA_VERIFY_SSL",
580
+ "timeout": "BT_PRA_TIMEOUT",
581
+ },
582
+ "Entitle": {
583
+ "api_url": "BT_ENTITLE_API_URL",
584
+ "api_key": "BT_ENTITLE_API_KEY",
585
+ "verify_ssl": "BT_ENTITLE_VERIFY_SSL",
586
+ "timeout": "BT_ENTITLE_TIMEOUT",
587
+ },
588
+ "EPM Windows": {
589
+ "api_url": "BT_EPM_API_URL",
590
+ "client_id": "BT_EPM_CLIENT_ID",
591
+ "client_secret": "BT_EPM_CLIENT_SECRET",
592
+ "verify_ssl": "BT_EPM_VERIFY_SSL",
593
+ "timeout": "BT_EPM_TIMEOUT",
594
+ },
595
+ }
596
+
597
+ # Check active profile
598
+ active_profile = os.getenv("BT_PROFILE", "default")
599
+ console.print(f"[bold]Active Profile:[/bold] {active_profile}\n")
600
+
601
+ # Show effective config for each product
602
+ for product_name, env_vars in products.items():
603
+ table = Table(title=product_name, show_header=True, title_style="bold cyan")
604
+ table.add_column("Setting", style="green")
605
+ table.add_column("Env Var", style="dim")
606
+ table.add_column("Value")
607
+ table.add_column("Source", style="dim")
608
+
609
+ has_config = False
610
+ for setting, env_var in env_vars.items():
611
+ value = os.getenv(env_var, "")
612
+ if value:
613
+ has_config = True
614
+ source = ".env" if env_file else "environment"
615
+ table.add_row(setting, env_var, mask_secret(setting, value), source)
616
+
617
+ if has_config:
618
+ console.print(table)
619
+ console.print()
620
+ else:
621
+ console.print(f"[dim]{product_name}: not configured[/dim]\n")
622
+
623
+ # Also show config file info
624
+ config = load_config_file()
625
+ if config.profiles:
626
+ console.print("[bold]Config File Profiles:[/bold]")
627
+ for profile_name in config.list_profiles():
628
+ products_list = list(config.profiles[profile_name].keys())
629
+ is_default = " (default)" if profile_name == config.default_profile else ""
630
+ console.print(f" {profile_name}{is_default}: {', '.join(products_list)}")
631
+ console.print(f"\n[dim]Note: Environment variables override config file settings[/dim]")
@@ -74,6 +74,7 @@ def _sanitize_body(body: Any) -> Any:
74
74
  "client_secret", "client-secret", "clientsecret",
75
75
  "authorization", "bearer", "credential", "credentials",
76
76
  "access_token", "refresh_token", "id_token",
77
+ "private_key", "privatekey", "ssh_key", "sshkey", "passphrase",
77
78
  }
78
79
 
79
80
  if isinstance(body, dict):
@@ -118,6 +119,10 @@ def _truncate_body(body: Any, max_length: int = 500, sanitize: bool = True) -> s
118
119
  body = json.dumps(body, indent=2)
119
120
 
120
121
  body_str = str(body)
122
+
123
+ # Detect PEM private key material in string responses
124
+ if sanitize and "-----BEGIN" in body_str and "PRIVATE KEY" in body_str:
125
+ return "[REDACTED - private key material]"
121
126
  if len(body_str) > max_length:
122
127
  return body_str[:max_length] + f"\n... ({len(body_str) - max_length} more chars)"
123
128
  return body_str
@@ -2,6 +2,8 @@
2
2
 
3
3
  from typing import Any, Optional, TYPE_CHECKING
4
4
 
5
+ from ..models.common import CredentialType, CREDENTIAL_TYPE_META
6
+
5
7
  if TYPE_CHECKING:
6
8
  from .base import PasswordSafeClient
7
9
 
@@ -378,19 +380,32 @@ class PasswordSafeMixin:
378
380
  access_type=access_type,
379
381
  )
380
382
 
381
- def get_credential(self: "PasswordSafeClient", request_id: int) -> dict[str, Any]:
382
- """Get the credential (password) for an approved request.
383
+ def get_credential(
384
+ self: "PasswordSafeClient",
385
+ request_id: int,
386
+ credential_type: Optional[str] = None,
387
+ ) -> dict[str, Any]:
388
+ """Get the credential for an approved request.
383
389
 
384
390
  Args:
385
391
  request_id: Request ID
392
+ credential_type: Type of credential to retrieve:
393
+ password (default), dsskey (SSH private key), passphrase
386
394
 
387
395
  Returns:
388
- Credential object with Password field
396
+ Credential dict with key based on type (Password, PrivateKey, or Passphrase)
389
397
  """
390
- result = self.get(f"/Credentials/{request_id}")
391
- # API returns password as plain string, wrap in dict for consistency
398
+ # Only send type param for non-default types (preserves backward compat)
399
+ params = {"type": credential_type} if credential_type and credential_type != "password" else None
400
+ result = self.get(f"/Credentials/{request_id}", params=params)
401
+
402
+ # Determine the response dict key based on credential type
403
+ ctype = CredentialType(credential_type) if credential_type else CredentialType.PASSWORD
404
+ meta = CREDENTIAL_TYPE_META[ctype]
405
+
406
+ # API returns credential as plain string, wrap in dict for consistency
392
407
  if isinstance(result, str):
393
- return {"Password": result}
408
+ return {meta["key"]: result}
394
409
  return result
395
410
 
396
411
  def checkin_request(
@@ -6,11 +6,13 @@ import json
6
6
  import httpx
7
7
  import typer
8
8
  from rich.console import Console
9
+ from rich.markup import escape as rich_escape
9
10
  from rich.table import Table
10
11
  from rich.panel import Panel
11
12
 
12
13
  from ...core.output import print_error, print_api_error
13
14
  from ..client.base import get_client
15
+ from ..models.common import CredentialType, CREDENTIAL_TYPE_META
14
16
 
15
17
  app = typer.Typer(no_args_is_help=True, help="Checkout and manage credentials in Password Safe")
16
18
  console = Console()
@@ -77,7 +79,8 @@ def checkout_credential(
77
79
  f"System: {system}\n"
78
80
  f"Account: {account}\n"
79
81
  f"Duration: {duration} minutes\n\n"
80
- f"[dim]Use 'pws credentials show {request_id}' to get the password[/dim]",
82
+ f"[dim]Use 'pws credentials show {request_id}' to get the password[/dim]\n"
83
+ f"[dim]For SSH keys: 'pws credentials show {request_id} --credential-type dsskey'[/dim]",
81
84
  title="Checkout Request",
82
85
  ))
83
86
 
@@ -98,31 +101,42 @@ def checkout_credential(
98
101
  @app.command("show")
99
102
  def show_credential(
100
103
  request_id: int = typer.Argument(..., help="Request ID"),
104
+ credential_type: str = typer.Option(
105
+ "password", "--credential-type", help="Credential type: password, dsskey, passphrase"
106
+ ),
101
107
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
102
- raw: bool = typer.Option(False, "--raw", "-r", help="Output only the password (for scripts)"),
108
+ raw: bool = typer.Option(False, "--raw", "-r", help="Output only the credential value (for scripts)"),
103
109
  ) -> None:
104
- """Get the password for an approved credential request.
110
+ """Get the credential for an approved request.
105
111
 
106
112
  Example:
107
113
  pws credentials show 12345
108
- pws credentials show 12345 --raw # Just the password for scripts
109
- PASSWORD=$(bt pws credentials show 12345 --raw)
114
+ pws credentials show 12345 --raw
115
+ pws credentials show 12345 --credential-type dsskey --raw
116
+ KEY=$(bt pws credentials show 12345 --credential-type dsskey --raw)
110
117
  """
118
+ try:
119
+ ctype = CredentialType(credential_type)
120
+ except ValueError:
121
+ print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
122
+ raise typer.Exit(1)
123
+
124
+ meta = CREDENTIAL_TYPE_META[ctype]
125
+
111
126
  try:
112
127
  with get_client() as client:
113
128
  client.authenticate()
114
- credential = client.get_credential(request_id)
129
+ credential = client.get_credential(request_id, credential_type=credential_type)
115
130
 
116
131
  if raw:
117
- # Output just the password with no formatting (for scripts)
118
- print(credential.get("Password", ""), end="")
132
+ print(credential.get(meta["key"], ""), end="")
119
133
  elif output == "json":
120
134
  console.print_json(json.dumps(credential, default=str))
121
135
  else:
122
- password = credential.get("Password", "N/A")
136
+ value = rich_escape(credential.get(meta["key"], "N/A"))
123
137
  console.print(Panel(
124
138
  f"Request ID: [bold cyan]{request_id}[/bold cyan]\n"
125
- f"Password: [bold green]{password}[/bold green]",
139
+ f"{meta['label']}: [bold green]{value}[/bold green]",
126
140
  title="Credential",
127
141
  ))
128
142
 
@@ -6,12 +6,14 @@ import json
6
6
  import httpx
7
7
  import typer
8
8
  from rich.console import Console
9
+ from rich.markup import escape as rich_escape
9
10
  from rich.panel import Panel
10
11
  from rich.table import Table
11
12
 
12
13
  from ...core.output import print_api_error, print_error, print_success, print_warning
13
14
  from ...core.prompts import prompt_if_missing, prompt_from_list, prompt_choice
14
15
  from ..client.base import get_client
16
+ from ..models.common import CredentialType, CREDENTIAL_TYPE_META
15
17
 
16
18
  app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
17
19
  console = Console()
@@ -23,12 +25,13 @@ def quick_checkout(
23
25
  account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
24
26
  duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
25
27
  reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
26
- raw: bool = typer.Option(False, "--raw", help="Output only the password (for scripts)"),
28
+ credential_type: str = typer.Option("password", "--credential-type", help="Credential type: password, dsskey, passphrase"),
29
+ raw: bool = typer.Option(False, "--raw", help="Output only the credential value (for scripts)"),
27
30
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
28
31
  ) -> None:
29
- """Checkout credentials and show password in one step.
32
+ """Checkout credentials and show password/SSH key in one step.
30
33
 
31
- Combines: find system -> find account -> checkout -> show password
34
+ Combines: find system -> find account -> checkout -> show credential
32
35
 
33
36
  If system or account not provided, prompts interactively.
34
37
 
@@ -37,6 +40,7 @@ def quick_checkout(
37
40
  bt pws quick checkout -s "axion-finapp-01" -a "root"
38
41
  bt pws quick checkout -s axion -a root --duration 30
39
42
  PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
43
+ KEY=$(bt pws quick checkout -s server -a admin --credential-type dsskey --raw)
40
44
  """
41
45
  try:
42
46
  with get_client() as client:
@@ -115,12 +119,19 @@ def quick_checkout(
115
119
  request_id = request.get("RequestID")
116
120
 
117
121
  # Step 4: Get the credential
118
- credential = client.get_credential(request_id)
119
- password = credential.get("Password", "")
122
+ try:
123
+ ctype = CredentialType(credential_type)
124
+ except ValueError:
125
+ print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
126
+ raise typer.Exit(1)
127
+ meta = CREDENTIAL_TYPE_META[ctype]
128
+
129
+ credential = client.get_credential(request_id, credential_type=credential_type)
130
+ value = credential.get(meta["key"], "")
120
131
 
121
132
  # Output
122
133
  if raw:
123
- print(password, end="")
134
+ print(value, end="")
124
135
  elif output == "json":
125
136
  result = {
126
137
  "request_id": request_id,
@@ -128,7 +139,8 @@ def quick_checkout(
128
139
  "system_id": system_id,
129
140
  "account": account_name,
130
141
  "account_id": account_id,
131
- "password": password,
142
+ "credential_type": credential_type,
143
+ meta["key"]: value,
132
144
  "duration_minutes": duration,
133
145
  }
134
146
  console.print_json(json.dumps(result))
@@ -139,7 +151,7 @@ def quick_checkout(
139
151
  f"Account: [cyan]{account_name}[/cyan] (ID: {account_id})\n"
140
152
  f"Request ID: [bold yellow]{request_id}[/bold yellow]\n"
141
153
  f"Duration: {duration} minutes\n\n"
142
- f"Password: [bold green]{password}[/bold green]\n\n"
154
+ f"{meta['label']}: [bold green]{rich_escape(value)}[/bold green]\n\n"
143
155
  f"[dim]Checkin: bt pws credentials checkin {request_id}[/dim]",
144
156
  title="Quick Checkout",
145
157
  ))
@@ -288,16 +300,18 @@ def quick_password(
288
300
  system: Optional[str] = typer.Option(None, "--system", "-s", help="System name"),
289
301
  account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
290
302
  duration: int = typer.Option(5, "--duration", "-d", help="Duration in minutes (default: 5)"),
291
- auto_checkin: bool = typer.Option(True, "--auto-checkin/--no-auto-checkin", help="Auto checkin after showing password"),
303
+ credential_type: str = typer.Option("password", "--credential-type", help="Credential type: password, dsskey, passphrase"),
304
+ auto_checkin: bool = typer.Option(True, "--auto-checkin/--no-auto-checkin", help="Auto checkin after showing credential"),
292
305
  ) -> None:
293
- """Get a password quickly - checkout, show, and optionally auto-checkin.
306
+ """Get a credential quickly - checkout, show, and optionally auto-checkin.
294
307
 
295
- Ideal for quick lookups where you just need to see/copy the password.
308
+ Ideal for quick lookups where you just need to see/copy the password or SSH key.
296
309
  If system or account not provided, prompts interactively.
297
310
 
298
311
  Examples:
299
312
  bt pws quick password # Interactive mode
300
313
  bt pws quick password -s server -a root
314
+ bt pws quick password -s server -a root --credential-type dsskey
301
315
  bt pws quick password -s db-server -a admin --no-auto-checkin
302
316
  """
303
317
  try:
@@ -361,12 +375,19 @@ def quick_password(
361
375
  )
362
376
  request_id = request.get("RequestID")
363
377
 
364
- # Get password
365
- credential = client.get_credential(request_id)
366
- password = credential.get("Password", "")
378
+ # Get credential
379
+ try:
380
+ ctype = CredentialType(credential_type)
381
+ except ValueError:
382
+ print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
383
+ raise typer.Exit(1)
384
+ meta = CREDENTIAL_TYPE_META[ctype]
385
+
386
+ credential = client.get_credential(request_id, credential_type=credential_type)
387
+ value = credential.get(meta["key"], "")
367
388
 
368
- # Show password
369
- console.print(f"\n[bold green]{password}[/bold green]\n")
389
+ # Show credential
390
+ console.print(f"\n[bold green]{rich_escape(value)}[/bold green]\n")
370
391
  console.print(f"[dim]{account_name}@{system_name} (Request: {request_id})[/dim]")
371
392
 
372
393
  # Auto checkin
@@ -1,10 +1,26 @@
1
1
  """Common models and types shared across the API."""
2
2
 
3
+ from enum import Enum
3
4
  from typing import Any, Generic, Optional, TypeVar
4
5
  from datetime import datetime
5
6
  from pydantic import BaseModel, ConfigDict
6
7
 
7
8
 
9
+ class CredentialType(str, Enum):
10
+ """Type of credential to retrieve from Password Safe."""
11
+
12
+ PASSWORD = "password"
13
+ DSSKEY = "dsskey"
14
+ PASSPHRASE = "passphrase"
15
+
16
+
17
+ CREDENTIAL_TYPE_META = {
18
+ CredentialType.PASSWORD: {"key": "Password", "label": "Password"},
19
+ CredentialType.DSSKEY: {"key": "PrivateKey", "label": "Private Key"},
20
+ CredentialType.PASSPHRASE: {"key": "Passphrase", "label": "Passphrase"},
21
+ }
22
+
23
+
8
24
  # Type variable for generic paginated responses
9
25
  T = TypeVar("T", bound=BaseModel)
10
26
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes