minitest-cli 0.38.2__tar.gz → 0.40.0__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 (246) hide show
  1. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/PKG-INFO +28 -1
  2. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/README.md +27 -0
  3. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/pyproject.toml +1 -1
  4. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_write.py +2 -1
  5. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run.py +2 -0
  6. minitest_cli-0.40.0/src/minitest_cli/commands/run_logs.py +127 -0
  7. minitest_cli-0.40.0/src/minitest_cli/commands/secret.py +168 -0
  8. minitest_cli-0.40.0/src/minitest_cli/commands/secret_extract.py +149 -0
  9. minitest_cli-0.40.0/src/minitest_cli/commands/secret_helpers.py +175 -0
  10. minitest_cli-0.40.0/src/minitest_cli/commands/secret_lint.py +107 -0
  11. minitest_cli-0.40.0/src/minitest_cli/commands/secret_manage.py +73 -0
  12. minitest_cli-0.40.0/src/minitest_cli/commands/secret_value.py +106 -0
  13. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile.py +17 -0
  14. minitest_cli-0.40.0/src/minitest_cli/commands/test_profile_secret_refs.py +54 -0
  15. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_update.py +17 -0
  16. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story.py +10 -2
  17. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_camera.py +11 -1
  18. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_create.py +7 -4
  19. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_helpers.py +6 -1
  20. minitest_cli-0.40.0/src/minitest_cli/commands/user_story_iap.py +32 -0
  21. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_modify.py +8 -11
  22. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_update.py +13 -1
  23. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/main.py +2 -0
  24. minitest_cli-0.40.0/src/minitest_cli/models/lane_logs.py +28 -0
  25. minitest_cli-0.40.0/tests/_secret_router.py +59 -0
  26. minitest_cli-0.40.0/tests/test_run_logs_command.py +151 -0
  27. minitest_cli-0.40.0/tests/test_secret_commands.py +247 -0
  28. minitest_cli-0.40.0/tests/test_secret_lint_extract.py +176 -0
  29. minitest_cli-0.40.0/tests/test_test_profile_secret_refs.py +72 -0
  30. minitest_cli-0.40.0/tests/test_user_story_in_app_purchases.py +100 -0
  31. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/uv.lock +1 -1
  32. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.env.example +0 -0
  33. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/ci.yml +0 -0
  34. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/install-scripts.yml +0 -0
  35. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/release.yml +0 -0
  36. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.gitignore +0 -0
  37. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.opencode/skill/release/SKILL.md +0 -0
  38. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/AGENTS.md +0 -0
  39. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/RELEASE.md +0 -0
  40. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/install.ps1 +0 -0
  41. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/install.sh +0 -0
  42. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/pyrightconfig.json +0 -0
  43. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/renovate.json +0 -0
  44. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/__init__.py +0 -0
  45. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/__init__.py +0 -0
  46. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/apps_manager_client.py +0 -0
  47. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/client.py +0 -0
  48. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/errors.py +0 -0
  49. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/assets/__init__.py +0 -0
  50. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/assets/callback.html +0 -0
  51. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/__init__.py +0 -0
  52. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/_abi_mismatch.py +0 -0
  53. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/_response_errors.py +0 -0
  54. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_knowledge.py +0 -0
  55. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_knowledge_helpers.py +0 -0
  56. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_network_helpers.py +0 -0
  57. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill.py +0 -0
  58. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_helpers.py +0 -0
  59. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_secret.py +0 -0
  60. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps.py +0 -0
  61. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_dependencies.py +0 -0
  62. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_get.py +0 -0
  63. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_helpers.py +0 -0
  64. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_table.py +0 -0
  65. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/auth.py +0 -0
  66. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/auth_api_key.py +0 -0
  67. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch.py +0 -0
  68. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch_helpers.py +0 -0
  69. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch_warnings.py +0 -0
  70. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build.py +0 -0
  71. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_bundle.py +0 -0
  72. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_commit.py +0 -0
  73. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_conversion.py +0 -0
  74. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_fix_prompts.py +0 -0
  75. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_get.py +0 -0
  76. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_helpers.py +0 -0
  77. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_signing.py +0 -0
  78. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/capabilities.py +0 -0
  79. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/commit_helpers.py +0 -0
  80. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/deprecated_alias.py +0 -0
  81. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft.py +0 -0
  82. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_asks.py +0 -0
  83. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_conversation.py +0 -0
  84. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_create.py +0 -0
  85. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature.py +0 -0
  86. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_apply.py +0 -0
  87. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_conflicts.py +0 -0
  88. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_helpers.py +0 -0
  89. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_show.py +0 -0
  90. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_helpers.py +0 -0
  91. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_legacy.py +0 -0
  92. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_lifecycle.py +0 -0
  93. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_pr.py +0 -0
  94. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_render.py +0 -0
  95. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_update.py +0 -0
  96. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_watch.py +0 -0
  97. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/env.py +0 -0
  98. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/env_helpers.py +0 -0
  99. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types.py +0 -0
  100. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types_helpers.py +0 -0
  101. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types_write.py +0 -0
  102. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init.py +0 -0
  103. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init_helpers.py +0 -0
  104. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init_playbook.py +0 -0
  105. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues.py +0 -0
  106. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_fix.py +0 -0
  107. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_get.py +0 -0
  108. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_helpers.py +0 -0
  109. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_projection.py +0 -0
  110. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_service.py +0 -0
  111. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/private_network.py +0 -0
  112. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/private_network_helpers.py +0 -0
  113. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/recording_timeline.py +0 -0
  114. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/residential_proxy.py +0 -0
  115. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_cancel.py +0 -0
  116. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_commit.py +0 -0
  117. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_commit_helpers.py +0 -0
  118. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_display.py +0 -0
  119. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_feedback.py +0 -0
  120. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_helpers.py +0 -0
  121. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_list.py +0 -0
  122. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_recording.py +0 -0
  123. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_recording_helpers.py +0 -0
  124. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_tagged.py +0 -0
  125. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_targets.py +0 -0
  126. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens.py +0 -0
  127. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_detail.py +0 -0
  128. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_format.py +0 -0
  129. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_helpers.py +0 -0
  130. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_tree.py +0 -0
  131. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/skill.py +0 -0
  132. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/tags.py +0 -0
  133. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/tags_helpers.py +0 -0
  134. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file.py +0 -0
  135. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file_helpers.py +0 -0
  136. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file_list.py +0 -0
  137. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_card.py +0 -0
  138. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_default.py +0 -0
  139. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_helpers.py +0 -0
  140. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_list.py +0 -0
  141. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/upgrade.py +0 -0
  142. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_app_skills.py +0 -0
  143. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_bindings.py +0 -0
  144. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_criteria.py +0 -0
  145. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_criterion_edits.py +0 -0
  146. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_delete.py +0 -0
  147. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_device_count.py +0 -0
  148. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_overrides.py +0 -0
  149. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_profiles.py +0 -0
  150. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_setup_commit.py +0 -0
  151. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts.py +0 -0
  152. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts_helpers.py +0 -0
  153. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts_projection.py +0 -0
  154. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/__init__.py +0 -0
  155. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/app_context.py +0 -0
  156. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/auth.py +0 -0
  157. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/config.py +0 -0
  158. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/credentials.py +0 -0
  159. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/oauth.py +0 -0
  160. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/tenants.py +0 -0
  161. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/token_exchange.py +0 -0
  162. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/__init__.py +0 -0
  163. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app.py +0 -0
  164. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_env_vars.py +0 -0
  165. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_failure.py +0 -0
  166. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_screen.py +0 -0
  167. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/base.py +0 -0
  168. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/batch.py +0 -0
  169. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/build.py +0 -0
  170. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/commit_build.py +0 -0
  171. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/draft_feature.py +0 -0
  172. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/flow_type.py +0 -0
  173. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/issues.py +0 -0
  174. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/recording.py +0 -0
  175. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/story_run.py +0 -0
  176. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/tag.py +0 -0
  177. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/targets.py +0 -0
  178. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/user_story.py +0 -0
  179. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/__init__.py +0 -0
  180. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/confirm.py +0 -0
  181. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/dependency_sim.py +0 -0
  182. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/mermaid.py +0 -0
  183. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/output.py +0 -0
  184. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
  185. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/update_check.py +0 -0
  186. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/video_frames.py +0 -0
  187. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/__init__.py +0 -0
  188. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/_commit_transport.py +0 -0
  189. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/_network_settings_server.py +0 -0
  190. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/conftest.py +0 -0
  191. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/draft_backend.py +0 -0
  192. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_api_client_auth.py +0 -0
  193. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_context.py +0 -0
  194. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_knowledge_commands.py +0 -0
  195. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_skill_commands.py +0 -0
  196. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_apps_commands.py +0 -0
  197. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_apps_dependencies.py +0 -0
  198. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth.py +0 -0
  199. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth_api_key.py +0 -0
  200. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth_commands.py +0 -0
  201. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_batch_commands.py +0 -0
  202. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_bundle.py +0 -0
  203. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_commands.py +0 -0
  204. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_fix_prompts.py +0 -0
  205. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_get_command.py +0 -0
  206. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_capabilities_command.py +0 -0
  207. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_channel_header.py +0 -0
  208. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_code_quality.py +0 -0
  209. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_commit_commands.py +0 -0
  210. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_deprecated_commands.py +0 -0
  211. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_deprecated_flags.py +0 -0
  212. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_ask_commands.py +0 -0
  213. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_commands.py +0 -0
  214. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_conversation_commands.py +0 -0
  215. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_feature_commands.py +0 -0
  216. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_pr_commands.py +0 -0
  217. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_env.py +0 -0
  218. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_flow_types_commands.py +0 -0
  219. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_init_command.py +0 -0
  220. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues.py +0 -0
  221. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues_commands.py +0 -0
  222. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues_get_command.py +0 -0
  223. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_mermaid.py +0 -0
  224. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_native_device_types.py +0 -0
  225. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_network_errors.py +0 -0
  226. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_physical_devices.py +0 -0
  227. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_private_network_commands.py +0 -0
  228. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_recording_timeline.py +0 -0
  229. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_residential_proxy_commands.py +0 -0
  230. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_response_errors.py +0 -0
  231. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_cancel.py +0 -0
  232. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_commands.py +0 -0
  233. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_frames_command.py +0 -0
  234. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_screens_commands.py +0 -0
  235. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_skill_command.py +0 -0
  236. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_suite_change_provenance.py +0 -0
  237. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_tags_commands.py +0 -0
  238. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_test_file_commands.py +0 -0
  239. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_test_profile_commands.py +0 -0
  240. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_upgrade_command.py +0 -0
  241. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_bindings_commands.py +0 -0
  242. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_commands.py +0 -0
  243. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_setup_commit.py +0 -0
  244. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_verdicts.py +0 -0
  245. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_verdicts_actionable.py +0 -0
  246. {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: minitest-cli
3
- Version: 0.38.2
3
+ Version: 0.40.0
4
4
  Summary: Minitest CLI – command-line interface for the Minitest testing platform
5
5
  Project-URL: Homepage, https://minitap.ai/
6
6
  Project-URL: Source, https://github.com/minitap-ai/minitest-cli
@@ -215,6 +215,7 @@ app defaults.
215
215
  | `minitest test-profile` | Test profiles (Test accounts in the webapp) |
216
216
  | `minitest tags` | List, create, update and delete scenario tags |
217
217
  | `minitest app-skill` | App skills (Skills in the webapp): procedures Mini loads to arrange test state |
218
+ | `minitest secret` | App secrets vault: passwords, OTP seeds, cards, API tokens referenced as `{{secret:NAME}}` (write-only values) |
218
219
  | `minitest screens` | Inspect the screens exploration mapped for an app |
219
220
  | `minitest build` | Native iOS/Android build management |
220
221
  | `minitest run` | Start runs and inspect their scenario runs |
@@ -239,6 +240,32 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
239
240
  | `run cancel --srp`, `run recording --srp` | `--target-id` |
240
241
  | `--type` on `scenario create/update/list` | `--tag` |
241
242
 
243
+ ## Secrets
244
+
245
+ Credentials live in the app's vault, never in scenario text. Write
246
+ `{{secret:NAME}}` (card fields: `{{secret:NAME.number}}`, `.expiry`, `.cvc`,
247
+ `.holder_name`, `.postal_code`) where Mini must type one; `$NAME` still works
248
+ but `secret lint` warns. Values are write-only: no command prints one back.
249
+ Workspaces without the feature get `Secrets are not enabled for this workspace yet` (exit 4).
250
+
251
+ ```bash
252
+ printf '%s' "$ADMIN_PW" | minitest secret set ADMIN_PASSWORD --kind password --note "Admin login"
253
+ echo '{"number":"4242424242424242","expiry":"12/34","cvc":"123"}' \
254
+ | minitest secret set STRIPE_CARD --kind card # optional holder_name, postal_code
255
+ minitest secret list [--missing] # names, kinds, usages, requests
256
+ minitest secret describe ADMIN_PASSWORD # usages + reference to copy
257
+ minitest secret lint --scenario <id> # exit 1 on leaks or missing secrets
258
+ minitest secret extract <id> --as ADMIN_PASSWORD --kind password [--dry-run]
259
+ minitest test-profile update <id> --password-secret ADMIN_PASSWORD
260
+ ```
261
+
262
+ `set` reads stdin only and refuses a terminal (`--interactive` opens a hidden
263
+ prompt). Inside a test run (`MINITAP_SECRETS_SOCKET` or `STORY_RUN_ID` set) it
264
+ refuses: use `minitest secret request NAME --kind <kind> --reason "…"` instead.
265
+ `extract` reads the leaked literal from the scenario on your machine, creates
266
+ the secret with it, then rewrites every occurrence to `{{secret:NAME}}`. Older
267
+ scenario versions and past run logs may still hold the literal: rotate it.
268
+
242
269
  ## App network settings
243
270
 
244
271
  Two per-app settings decide how test devices reach the network. Anyone on the
@@ -182,6 +182,7 @@ app defaults.
182
182
  | `minitest test-profile` | Test profiles (Test accounts in the webapp) |
183
183
  | `minitest tags` | List, create, update and delete scenario tags |
184
184
  | `minitest app-skill` | App skills (Skills in the webapp): procedures Mini loads to arrange test state |
185
+ | `minitest secret` | App secrets vault: passwords, OTP seeds, cards, API tokens referenced as `{{secret:NAME}}` (write-only values) |
185
186
  | `minitest screens` | Inspect the screens exploration mapped for an app |
186
187
  | `minitest build` | Native iOS/Android build management |
187
188
  | `minitest run` | Start runs and inspect their scenario runs |
@@ -206,6 +207,32 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
206
207
  | `run cancel --srp`, `run recording --srp` | `--target-id` |
207
208
  | `--type` on `scenario create/update/list` | `--tag` |
208
209
 
210
+ ## Secrets
211
+
212
+ Credentials live in the app's vault, never in scenario text. Write
213
+ `{{secret:NAME}}` (card fields: `{{secret:NAME.number}}`, `.expiry`, `.cvc`,
214
+ `.holder_name`, `.postal_code`) where Mini must type one; `$NAME` still works
215
+ but `secret lint` warns. Values are write-only: no command prints one back.
216
+ Workspaces without the feature get `Secrets are not enabled for this workspace yet` (exit 4).
217
+
218
+ ```bash
219
+ printf '%s' "$ADMIN_PW" | minitest secret set ADMIN_PASSWORD --kind password --note "Admin login"
220
+ echo '{"number":"4242424242424242","expiry":"12/34","cvc":"123"}' \
221
+ | minitest secret set STRIPE_CARD --kind card # optional holder_name, postal_code
222
+ minitest secret list [--missing] # names, kinds, usages, requests
223
+ minitest secret describe ADMIN_PASSWORD # usages + reference to copy
224
+ minitest secret lint --scenario <id> # exit 1 on leaks or missing secrets
225
+ minitest secret extract <id> --as ADMIN_PASSWORD --kind password [--dry-run]
226
+ minitest test-profile update <id> --password-secret ADMIN_PASSWORD
227
+ ```
228
+
229
+ `set` reads stdin only and refuses a terminal (`--interactive` opens a hidden
230
+ prompt). Inside a test run (`MINITAP_SECRETS_SOCKET` or `STORY_RUN_ID` set) it
231
+ refuses: use `minitest secret request NAME --kind <kind> --reason "…"` instead.
232
+ `extract` reads the leaked literal from the scenario on your machine, creates
233
+ the secret with it, then rewrites every occurrence to `{{secret:NAME}}`. Older
234
+ scenario versions and past run logs may still hold the literal: rotate it.
235
+
209
236
  ## App network settings
210
237
 
211
238
  Two per-app settings decide how test devices reach the network. Anyone on the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "minitest-cli"
3
- version = "0.38.2"
3
+ version = "0.40.0"
4
4
  description = "Minitest CLI – command-line interface for the Minitest testing platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -95,7 +95,8 @@ def register(app: typer.Typer) -> None:
95
95
  instructions: Instructions = None,
96
96
  instructions_file: InstructionsFile = None,
97
97
  ) -> None:
98
- """Create a skill. Reference secrets as $NAME and set them with `secret set`."""
98
+ """Create a skill. Reference secrets as $NAME (or {{secret:NAME}} once Secrets are enabled:
99
+ see `minitest secret`) and set them with `secret set`."""
99
100
  settings, json_mode, app_id = command_context()
100
101
  body = _patch(description, platforms, False, instructions, instructions_file)
101
102
  if "description" not in body or "instructions" not in body:
@@ -32,6 +32,7 @@ from minitest_cli.commands.run_targets import (
32
32
  )
33
33
  from minitest_cli.commands.run_feedback import feedback
34
34
  from minitest_cli.commands.run_list import list_runs
35
+ from minitest_cli.commands.run_logs import register_logs_command
35
36
  from minitest_cli.commands.run_recording import register_recording_commands
36
37
  from minitest_cli.commands.run_tagged import print_batch_started, start_tagged
37
38
  from minitest_cli.commands.verdicts import verdicts
@@ -173,3 +174,4 @@ app.command(name="from-commit")(from_commit)
173
174
  app.command(name="verdicts")(verdicts)
174
175
  app.command(name="feedback")(feedback)
175
176
  register_recording_commands(app)
177
+ register_logs_command(app)
@@ -0,0 +1,127 @@
1
+ """`minitest run logs`: download the log files a scenario run's target captured."""
2
+
3
+ import gzip
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from minitest_cli.api.client import ApiClient
11
+ from minitest_cli.commands.run_helpers import (
12
+ EXIT_NOT_FOUND,
13
+ base_path,
14
+ ensure_uuid,
15
+ handle_response_error,
16
+ resolve_app,
17
+ run_api_call,
18
+ )
19
+ from minitest_cli.commands.run_recording_helpers import download, select_platform_run
20
+ from minitest_cli.models.lane_logs import LaneLogFile, LaneLogs
21
+ from minitest_cli.models.story_run import PlatformRun, StoryRunResponse
22
+ from minitest_cli.utils.output import print_error, print_info, print_json, print_table
23
+
24
+ _FILE_LABELS = {
25
+ "device_log": "Device log",
26
+ "crash_report": "Crash report",
27
+ "browser_console": "Browser console",
28
+ "network_har": "Network (HAR)",
29
+ }
30
+
31
+
32
+ def logs(
33
+ run_id: Annotated[str, typer.Argument(help="Scenario-run ID.")],
34
+ platform: Annotated[str | None, typer.Option(help="Target platform (ios/android/web).")] = None,
35
+ target_id: Annotated[
36
+ str | None, typer.Option("--target-id", help="Exact target (srpId) when ambiguous.")
37
+ ] = None,
38
+ out: Annotated[Path | None, typer.Option(help="Output directory.")] = None,
39
+ ) -> None:
40
+ """Download a run's log files: device log, crash report, browser console, network (HAR).
41
+
42
+ Files are written decompressed. Crashes found in the device log are listed with
43
+ their time on the recording.
44
+ """
45
+ settings, app_id, json_mode = resolve_app()
46
+ ensure_uuid(run_id, kind="run id")
47
+
48
+ async def _fetch() -> tuple[PlatformRun, LaneLogs]:
49
+ async with ApiClient(settings) as client:
50
+ resp = await client.get(f"{base_path(app_id)}/{run_id}")
51
+ handle_response_error(resp, resource="Run")
52
+ target = select_platform_run(
53
+ StoryRunResponse.model_validate(resp.json()), platform, target_id
54
+ )
55
+ resp = await client.get(
56
+ f"{base_path(app_id)}/{run_id}/lane-logs", params={"srp_id": target.srp_id}
57
+ )
58
+ if resp.status_code == 404:
59
+ print_error("No logs for this run: logs aren't enabled for your workspace.")
60
+ raise typer.Exit(code=EXIT_NOT_FOUND)
61
+ handle_response_error(resp, resource="Run")
62
+ return target, LaneLogs.model_validate(resp.json())
63
+
64
+ target, lane_logs = run_api_call(_fetch())
65
+ _exit_without_files(lane_logs, target)
66
+ out_dir = out or Path("minitest-logs") / f"{run_id}-{target.platform}"
67
+ written = [(file, run_api_call(_save(file, out_dir))) for file in lane_logs.files]
68
+
69
+ if json_mode:
70
+ print_json(
71
+ {
72
+ "files": [
73
+ {"kind": f.kind, "path": str(path), "bytes": path.stat().st_size}
74
+ for f, path in written
75
+ ],
76
+ "crashes": [c.model_dump(by_alias=True) for c in lane_logs.crashes],
77
+ }
78
+ )
79
+ return
80
+ rows = [[_FILE_LABELS.get(f.kind, f.kind), _size(path), str(path)] for f, path in written]
81
+ print_table(["File", "Size", "Path"], rows, title=str(out_dir))
82
+ for crash in lane_logs.crashes:
83
+ print_info(f"App crash {_clock(crash.offset_ms)} into the run: {crash.summary}")
84
+
85
+
86
+ def _exit_without_files(lane_logs: LaneLogs, target: PlatformRun) -> None:
87
+ if lane_logs.files:
88
+ return
89
+ if lane_logs.status == "pending":
90
+ print_error(f"The {target.platform} target is still running; logs arrive when it ends.")
91
+ raise typer.Exit(code=1)
92
+ if lane_logs.unavailable_reason == "browser_unsupported":
93
+ print_error("No logs for this browser: only Chrome and Edge record the console and HAR.")
94
+ elif target.platform == "ios":
95
+ print_error("No logs were captured for the ios target (physical iPhones aren't captured).")
96
+ else:
97
+ print_error(f"No logs were captured for the {target.platform} target.")
98
+ raise typer.Exit(code=EXIT_NOT_FOUND)
99
+
100
+
101
+ async def _save(file: LaneLogFile, out_dir: Path) -> Path:
102
+ dest = out_dir / file.file_name
103
+ if not file.compressed:
104
+ await download(file.url, dest)
105
+ return dest
106
+ packed = dest.with_name(f"{dest.name}.gz")
107
+ await download(file.url, packed)
108
+ with gzip.open(packed, "rb") as source, dest.open("wb") as target:
109
+ shutil.copyfileobj(source, target)
110
+ packed.unlink()
111
+ return dest
112
+
113
+
114
+ def _size(path: Path) -> str:
115
+ size = path.stat().st_size
116
+ if size >= 1024**2:
117
+ return f"{size / 1024**2:.1f} MB"
118
+ return f"{size / 1024:.0f} KB" if size >= 1024 else f"{size} B"
119
+
120
+
121
+ def _clock(offset_ms: int) -> str:
122
+ minutes, seconds = divmod(offset_ms / 1000, 60)
123
+ return f"{int(minutes)}:{seconds:04.1f}"
124
+
125
+
126
+ def register_logs_command(app: typer.Typer) -> None:
127
+ app.command(name="logs")(logs)
@@ -0,0 +1,168 @@
1
+ """``minitest secret``: the app's Secrets vault. Values are write-only and never printed."""
2
+
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+
7
+ from minitest_cli.commands import secret_extract, secret_lint, secret_manage
8
+ from minitest_cli.commands.secret_helpers import (
9
+ CARD_FIELDS,
10
+ EXIT_NOT_FOUND,
11
+ JsonOption,
12
+ KindOption,
13
+ call,
14
+ command_context,
15
+ format_usage,
16
+ reference,
17
+ refuse_agent_context,
18
+ secrets_path,
19
+ validate_name,
20
+ )
21
+ from minitest_cli.commands.secret_value import read_secret_body
22
+ from minitest_cli.utils.output import (
23
+ print_error,
24
+ print_info,
25
+ print_json,
26
+ print_success,
27
+ print_table,
28
+ )
29
+
30
+ app = typer.Typer(
31
+ name="secret",
32
+ help=(
33
+ "Manage the app's secrets (passwords, OTP seeds, cards, API tokens). Reference them in "
34
+ "scenarios and skills as {{secret:NAME}} (card fields: {{secret:NAME.number}}); the "
35
+ "legacy $NAME still works but lint warns. Values are write-only and never printed."
36
+ ),
37
+ no_args_is_help=True,
38
+ )
39
+
40
+ NameArg = Annotated[str, typer.Argument(help="UPPER_SNAKE secret name, e.g. ADMIN_PASSWORD.")]
41
+
42
+
43
+ def fetch_vault(settings: Any, app_id: str) -> dict[str, Any]:
44
+ return call(settings, "GET", secrets_path(app_id), collection=True) or {}
45
+
46
+
47
+ def _request_rows(requests: list[dict[str, Any]]) -> list[list[str]]:
48
+ return [
49
+ [
50
+ r.get("name", ""),
51
+ r.get("kind", ""),
52
+ r.get("storyName") or r.get("storyId") or "",
53
+ r.get("requestedBy", ""),
54
+ r.get("reason", ""),
55
+ ]
56
+ for r in requests
57
+ ]
58
+
59
+
60
+ @app.command(name="list")
61
+ def list_secrets(
62
+ missing: Annotated[
63
+ bool, typer.Option("--missing", help="Only show requested secrets that do not exist yet.")
64
+ ] = False,
65
+ json_flag: JsonOption = False,
66
+ ) -> None:
67
+ """List secrets (name, kind, note, version, usages) and open secret requests."""
68
+ settings, json_mode, app_id = command_context(json_flag)
69
+ vault = fetch_vault(settings, app_id)
70
+ secrets: list[dict[str, Any]] = vault.get("secrets") or []
71
+ requests: list[dict[str, Any]] = vault.get("requests") or []
72
+ if json_mode:
73
+ print_json(
74
+ {"requests": requests} if missing else {"secrets": secrets, "requests": requests}
75
+ )
76
+ return
77
+ if not missing:
78
+ if secrets:
79
+ rows = [
80
+ [
81
+ s.get("name", ""),
82
+ s.get("kind", ""),
83
+ str(s.get("version", "")),
84
+ str(len(s.get("usages") or [])),
85
+ s.get("note") or "",
86
+ ]
87
+ for s in secrets
88
+ ]
89
+ print_table(["Name", "Kind", "Version", "Usages", "Note"], rows, title="Secrets")
90
+ else:
91
+ print_info("No secrets yet. Add one with: minitest secret set NAME --kind <kind>")
92
+ if requests:
93
+ headers = ["Name", "Kind", "Scenario", "Requested By", "Reason"]
94
+ print_table(headers, _request_rows(requests), title="Requested (missing) secrets")
95
+ elif missing:
96
+ print_info("No missing secrets.")
97
+
98
+
99
+ @app.command(name="describe")
100
+ def describe_secret(name: NameArg, json_flag: JsonOption = False) -> None:
101
+ """Show a secret's kind, note, usages and the reference to copy (never its value)."""
102
+ settings, json_mode, app_id = command_context(json_flag)
103
+ vault = fetch_vault(settings, app_id)
104
+ found = next((s for s in vault.get("secrets") or [] if s.get("name") == name), None)
105
+ requests = [r for r in vault.get("requests") or [] if r.get("name") == name]
106
+ if found is None and not requests:
107
+ print_error(f"No secret named {name}.")
108
+ raise typer.Exit(code=EXIT_NOT_FOUND)
109
+ fields = list(CARD_FIELDS) if found and found.get("kind") == "card" else []
110
+ refs = [reference(name, f) for f in fields] or [reference(name)]
111
+ if json_mode:
112
+ print_json({"secret": found, "references": refs, "requests": requests})
113
+ return
114
+ if found is None:
115
+ print(f"{name}: requested, not set yet") # noqa: T201
116
+ print_table(["Name", "Kind", "Scenario", "Requested By", "Reason"], _request_rows(requests))
117
+ print_info(f"Set it with: minitest secret set {name} --kind {requests[0].get('kind')}")
118
+ return
119
+ for label, value in (
120
+ ("Name", name),
121
+ ("Kind", found.get("kind")),
122
+ ("Note", found.get("note") or "-"),
123
+ ("Version", found.get("version")),
124
+ ("Updated", f"{found.get('updatedAt', '-')} by {found.get('updatedBy') or '-'}"),
125
+ ("Reference", ", ".join(refs)),
126
+ ):
127
+ print(f"{label}: {value}") # noqa: T201
128
+ usages = found.get("usages") or []
129
+ print(f"Usages: {len(usages)}") # noqa: T201
130
+ for usage in usages:
131
+ print(f" - {format_usage(usage)}") # noqa: T201
132
+
133
+
134
+ @app.command(name="set")
135
+ def set_secret(
136
+ name: NameArg,
137
+ kind: KindOption,
138
+ note: Annotated[str | None, typer.Option("--note", help="What this secret is for.")] = None,
139
+ interactive: Annotated[bool, typer.Option("--interactive", hidden=True)] = False,
140
+ json_flag: JsonOption = False,
141
+ ) -> None:
142
+ """Create or replace a secret. The value is read from stdin only.
143
+
144
+ password/totp/token/text: the raw value, e.g. ``printf '%s' "$V" | minitest secret set X
145
+ --kind token``. card: a JSON object, e.g. ``echo '{"number":"4242…","expiry":"12/34",
146
+ "cvc":"123"}' | minitest secret set CARD --kind card`` (optional holder_name, postal_code).
147
+ """
148
+ refuse_agent_context()
149
+ validate_name(name)
150
+ settings, json_mode, app_id = command_context(json_flag)
151
+ body: dict[str, Any] = {
152
+ "kind": kind.value,
153
+ **read_secret_body(name, kind, interactive=interactive),
154
+ }
155
+ if note is not None:
156
+ body["note"] = note
157
+ saved = call(settings, "PUT", f"{secrets_path(app_id)}/{name}", json=body) or {}
158
+ if json_mode:
159
+ print_json(saved)
160
+ return
161
+ print_success(
162
+ f"Secret {name} saved (v{saved.get('version', '?')}). Reference: {reference(name)}"
163
+ )
164
+
165
+
166
+ secret_manage.register(app)
167
+ secret_lint.register(app)
168
+ secret_extract.register(app)
@@ -0,0 +1,149 @@
1
+ """``secret extract``: move a literal credential out of a scenario into the vault.
2
+
3
+ The server never reads a value out of a scenario, so the CLI does it: it fetches the
4
+ scenario (which the caller can already read), lints it, takes the leaked literal locally,
5
+ creates the secret with it, then PATCHes the scenario with every occurrence replaced by
6
+ ``{{secret:NAME}}``. The literal is never printed, only its position and length.
7
+ """
8
+
9
+ import re
10
+ from typing import Annotated, Any, NoReturn
11
+
12
+ import typer
13
+
14
+ from minitest_cli.api.client import ApiClient
15
+ from minitest_cli.commands.secret_helpers import (
16
+ JsonOption,
17
+ SecretKind,
18
+ command_context,
19
+ reference,
20
+ refuse_agent_context,
21
+ request,
22
+ secrets_path,
23
+ validate_name,
24
+ )
25
+ from minitest_cli.commands.secret_lint import fetch_story, lint_text, position, story_sources
26
+ from minitest_cli.commands.user_story_criteria import extract_criteria_items
27
+ from minitest_cli.commands.user_story_helpers import base_path, handle_response_error, run_api_call
28
+ from minitest_cli.utils.output import err_console, print_error, print_json, print_success
29
+ from minitest_cli.utils.output import print_info, print_warning
30
+
31
+ _LABELLED = re.compile(
32
+ r"^\s*(?:password|passwd|pwd|pass|passcode|pin|otp|code|token|secret|api[ _-]?key)"
33
+ r"(?:\s*[:=]\s*|\s+is\s+|\s+)(?P<value>\S.*?)\s*$",
34
+ re.IGNORECASE | re.DOTALL,
35
+ )
36
+ MIN_LITERAL_LENGTH = 4
37
+
38
+
39
+ def literal_of(text: str, leak: dict[str, Any]) -> str:
40
+ """The leaked literal; a ``password: xxx`` heuristic span keeps only the value."""
41
+ raw = text[int(leak["start"]) : int(leak["end"])].strip().strip("\"'`")
42
+ match = _LABELLED.match(raw)
43
+ return match.group("value").strip("\"'`") if match else raw
44
+
45
+
46
+ def _fail(message: str) -> NoReturn:
47
+ print_error(message)
48
+ raise typer.Exit(code=1)
49
+
50
+
51
+ def _candidates(results: list[tuple[str, str, dict[str, Any]]]) -> list[tuple[str, str, str]]:
52
+ seen: dict[str, tuple[str, str, str]] = {}
53
+ for label, text, result in results:
54
+ for leak in result.get("leaks") or []:
55
+ literal = literal_of(text, leak) if not leak.get("secret") else ""
56
+ if len(literal) >= MIN_LITERAL_LENGTH and literal not in seen:
57
+ seen[literal] = (label, position(text, int(leak["start"])), literal)
58
+ return list(seen.values())
59
+
60
+
61
+ def _patch_body(story: dict[str, Any], literal: str, ref: str) -> tuple[dict[str, Any], int]:
62
+ body: dict[str, Any] = {}
63
+ count = (story.get("description") or "").count(literal)
64
+ if count:
65
+ body["description"] = story["description"].replace(literal, ref)
66
+ items = extract_criteria_items(story)
67
+ hits = sum(item["content"].count(literal) for item in items)
68
+ if hits:
69
+ body["acceptanceCriteria"] = [
70
+ {**item, "content": item["content"].replace(literal, ref)} for item in items
71
+ ]
72
+ return body, count + hits
73
+
74
+
75
+ def register(app: typer.Typer) -> None:
76
+
77
+ @app.command(name="extract")
78
+ def extract(
79
+ scenario_id: Annotated[str, typer.Argument(help="Scenario ID holding the literal.")],
80
+ name: Annotated[str, typer.Option("--as", help="Name of the secret to create.")],
81
+ kind: Annotated[SecretKind, typer.Option("--kind", help="password, totp, token or text.")],
82
+ note: Annotated[str | None, typer.Option("--note", help="What it is for.")] = None,
83
+ leak: Annotated[
84
+ int | None, typer.Option("--leak", help="Which leak to extract (1-based), if several.")
85
+ ] = None,
86
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show the plan only.")] = False,
87
+ json_flag: JsonOption = False,
88
+ ) -> None:
89
+ """Create a secret from a literal leaked in a scenario and replace it by its reference."""
90
+ refuse_agent_context()
91
+ validate_name(name)
92
+ if kind is SecretKind.card:
93
+ _fail("A card has several fields: create it with `secret set --kind card` instead.")
94
+ settings, json_mode, app_id = command_context(json_flag)
95
+ ref = reference(name)
96
+ story_path = f"{base_path(app_id)}/{scenario_id}"
97
+
98
+ async def _run() -> dict[str, Any]:
99
+ async with ApiClient(settings) as client:
100
+ vault = await request(client, "GET", secrets_path(app_id), collection=True) or {}
101
+ if any(s.get("name") == name for s in vault.get("secrets") or []):
102
+ _fail(f"A secret named {name} already exists; pick another --as name.")
103
+ story = await fetch_story(client, app_id, scenario_id)
104
+ results = [
105
+ (lb, tx, await lint_text(client, app_id, tx)) for lb, tx in story_sources(story)
106
+ ]
107
+ found = _candidates(results)
108
+ if not found:
109
+ _fail("No literal secret found in this scenario (see `minitest secret lint`).")
110
+ if (leak is None and len(found) > 1) or (
111
+ leak is not None and not 0 < leak <= len(found)
112
+ ):
113
+ print_error(f"{len(found)} literals found; choose one with --leak N:")
114
+ for i, (label, at, literal) in enumerate(found, start=1):
115
+ err_console.print(f" {i}. {label} at {at} ({len(literal)} characters)")
116
+ raise typer.Exit(code=1)
117
+ label, at, literal = found[(leak or 1) - 1]
118
+ body, count = _patch_body(story, literal, ref)
119
+ plan = {"secret": name, "source": label, "at": at, "replacements": count}
120
+ if dry_run:
121
+ return {**plan, "dryRun": True}
122
+ secret_body: dict[str, Any] = {"kind": kind.value, "value": literal}
123
+ if note is not None:
124
+ secret_body["note"] = note
125
+ await request(client, "PUT", f"{secrets_path(app_id)}/{name}", json=secret_body)
126
+ resp = await client.patch(story_path, json=body)
127
+ if resp.status_code >= 400:
128
+ print_error(f"Secret {name} was created but the scenario was not updated.")
129
+ handle_response_error(resp)
130
+ return plan
131
+
132
+ plan = run_api_call(_run())
133
+ if json_mode:
134
+ print_json(plan)
135
+ return
136
+ where = f"{plan['source']} at {plan['at']}"
137
+ if plan.get("dryRun"):
138
+ print_info(
139
+ f"Would create {name} from the literal in {where} and replace "
140
+ f"{plan['replacements']} occurrence(s) with {ref}."
141
+ )
142
+ return
143
+ print_success(
144
+ f"Secret {name} created; {plan['replacements']} occurrence(s) now read {ref}."
145
+ )
146
+ print_warning(
147
+ "Older scenario versions and past run logs may still hold the literal: "
148
+ "rotate the credential if it is sensitive."
149
+ )