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.
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/PKG-INFO +28 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/README.md +27 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/pyproject.toml +1 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_write.py +2 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run.py +2 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/run_logs.py +127 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret.py +168 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret_extract.py +149 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret_helpers.py +175 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret_lint.py +107 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret_manage.py +73 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/secret_value.py +106 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile.py +17 -0
- minitest_cli-0.40.0/src/minitest_cli/commands/test_profile_secret_refs.py +54 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_update.py +17 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story.py +10 -2
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_camera.py +11 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_create.py +7 -4
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_helpers.py +6 -1
- minitest_cli-0.40.0/src/minitest_cli/commands/user_story_iap.py +32 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_modify.py +8 -11
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_update.py +13 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/main.py +2 -0
- minitest_cli-0.40.0/src/minitest_cli/models/lane_logs.py +28 -0
- minitest_cli-0.40.0/tests/_secret_router.py +59 -0
- minitest_cli-0.40.0/tests/test_run_logs_command.py +151 -0
- minitest_cli-0.40.0/tests/test_secret_commands.py +247 -0
- minitest_cli-0.40.0/tests/test_secret_lint_extract.py +176 -0
- minitest_cli-0.40.0/tests/test_test_profile_secret_refs.py +72 -0
- minitest_cli-0.40.0/tests/test_user_story_in_app_purchases.py +100 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/uv.lock +1 -1
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.env.example +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/ci.yml +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/install-scripts.yml +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.github/workflows/release.yml +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.gitignore +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/.opencode/skill/release/SKILL.md +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/AGENTS.md +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/RELEASE.md +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/install.ps1 +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/install.sh +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/pyrightconfig.json +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/renovate.json +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/apps_manager_client.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/client.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/api/errors.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/assets/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/assets/callback.html +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/_abi_mismatch.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/_response_errors.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_knowledge.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_knowledge_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_network_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/app_skill_secret.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_dependencies.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_get.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/apps_table.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/auth.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/auth_api_key.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/batch_warnings.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_bundle.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_commit.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_conversion.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_fix_prompts.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_get.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/build_signing.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/capabilities.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/commit_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/deprecated_alias.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_asks.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_conversation.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_create.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_apply.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_conflicts.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_feature_show.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_legacy.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_lifecycle.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_pr.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_render.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_update.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/draft_watch.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/env.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/env_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/flow_types_write.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/init_playbook.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_fix.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_get.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_projection.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/issues_service.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/private_network.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/private_network_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/recording_timeline.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/residential_proxy.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_cancel.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_commit.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_commit_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_display.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_feedback.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_list.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_recording.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_recording_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_tagged.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/run_targets.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_detail.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_format.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/screens_tree.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/skill.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/tags.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/tags_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_file_list.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_card.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_default.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/test_profile_list.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/upgrade.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_app_skills.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_bindings.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_criteria.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_criterion_edits.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_delete.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_device_count.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_overrides.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_profiles.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/user_story_setup_commit.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts_helpers.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/commands/verdicts_projection.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/app_context.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/auth.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/config.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/credentials.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/oauth.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/tenants.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/core/token_exchange.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_env_vars.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_failure.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/app_screen.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/base.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/batch.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/build.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/commit_build.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/draft_feature.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/flow_type.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/issues.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/recording.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/story_run.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/tag.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/targets.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/models/user_story.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/confirm.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/dependency_sim.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/mermaid.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/output.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/update_check.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/src/minitest_cli/utils/video_frames.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/__init__.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/_commit_transport.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/_network_settings_server.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/conftest.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/draft_backend.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_api_client_auth.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_context.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_knowledge_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_app_skill_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_apps_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_apps_dependencies.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth_api_key.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_auth_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_batch_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_bundle.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_fix_prompts.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_build_get_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_capabilities_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_channel_header.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_code_quality.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_commit_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_deprecated_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_deprecated_flags.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_ask_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_conversation_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_feature_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_draft_pr_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_env.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_flow_types_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_init_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_issues_get_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_mermaid.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_native_device_types.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_network_errors.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_physical_devices.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_private_network_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_recording_timeline.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_residential_proxy_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_response_errors.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_cancel.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_run_frames_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_screens_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_skill_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_suite_change_provenance.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_tags_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_test_file_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_test_profile_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_upgrade_command.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_bindings_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_commands.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_user_story_setup_commit.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_verdicts.py +0 -0
- {minitest_cli-0.38.2 → minitest_cli-0.40.0}/tests/test_verdicts_actionable.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
+
)
|