patchi 0.7.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
patchi/cli/style.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
§2 CLI Theme System — semantic helpers.
|
|
3
|
+
|
|
4
|
+
Thin wrappers so migrating a raw `print(...)` to something themed is a
|
|
5
|
+
one-liner: `print(f"Error: {msg}")` -> `err(msg)`. Every helper uses semantic
|
|
6
|
+
tokens (never raw color) and pulls its symbol from the active palette via
|
|
7
|
+
`console.symbol()`, so `mono` gets ASCII (`[OK]`/`[!]`/`[X]`) automatically
|
|
8
|
+
instead of Unicode glyphs a dumb terminal or encoding can't render.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from patchi.cli.console import con, symbol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ok(msg: str) -> None:
|
|
17
|
+
con.print(f"[success]{symbol('ok')}[/] {msg}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def warn(msg: str) -> None:
|
|
21
|
+
con.print(f"[warn]{symbol('warn')}[/] {msg}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def err(msg: str) -> None:
|
|
25
|
+
con.print(f"[error]{symbol('err')}[/] {msg}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def info(msg: str) -> None:
|
|
29
|
+
con.print(f"[info]{msg}[/]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def muted(msg: str) -> None:
|
|
33
|
+
con.print(f"[muted]{msg}[/]")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def heading(msg: str) -> None:
|
|
37
|
+
con.print(f"\n[heading]{msg}[/]")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def accent(msg: str) -> str:
|
|
41
|
+
"""Returns markup (not printed) for embedding in a larger f-string."""
|
|
42
|
+
return f"[accent]{msg}[/]"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def path(p: str) -> str:
|
|
46
|
+
"""Returns markup (not printed) for embedding a file path in a larger line."""
|
|
47
|
+
return f"[path]{p}[/]"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def grade(score, letter: str) -> str:
|
|
51
|
+
"""Returns markup for a health-grade display, e.g. grade(87, 'B') -> '87 (B)' styled."""
|
|
52
|
+
key = f"grade.{letter.lower()}"
|
|
53
|
+
return f"[{key}]{score} ({letter})[/]"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def severity(label: str) -> str:
|
|
57
|
+
"""Returns markup for a severity label: severity('critical') -> styled 'CRITICAL'."""
|
|
58
|
+
key = f"sev.{label.lower()}"
|
|
59
|
+
return f"[{key}]{label.upper()}[/]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def kv(k: str, v) -> None:
|
|
63
|
+
con.print(f" [muted]{k}[/] {v}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def count(n) -> str:
|
|
67
|
+
"""Returns markup for a count/number that should stand out, e.g. in a summary line."""
|
|
68
|
+
return f"[count]{n}[/]"
|
patchi/cli/themes.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
§2 CLI Theme System — palette definitions.
|
|
3
|
+
|
|
4
|
+
Each palette maps the SAME semantic token set to different Rich styles, plus a
|
|
5
|
+
matching symbol set. Symbols are separate from the Rich Theme (which only maps
|
|
6
|
+
str -> Style, not str -> arbitrary text) so `mono` can swap Unicode glyphs for
|
|
7
|
+
plain ASCII on terminals/encodings that can't render them, and so status is
|
|
8
|
+
never conveyed by color alone (colorblind-safe: shape/symbol/label carries the
|
|
9
|
+
meaning, color is a bonus, not the only signal).
|
|
10
|
+
|
|
11
|
+
Token set (used identically across every palette — callers use meaning, never
|
|
12
|
+
raw color):
|
|
13
|
+
success, warn, error, info, muted -- status
|
|
14
|
+
heading, accent, path, count -- structure
|
|
15
|
+
grade.a .. grade.f -- health grades
|
|
16
|
+
sev.critical, sev.high, sev.medium, sev.low -- finding severity
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from rich.theme import Theme
|
|
22
|
+
|
|
23
|
+
_TOKEN_NAMES = (
|
|
24
|
+
"success",
|
|
25
|
+
"warn",
|
|
26
|
+
"error",
|
|
27
|
+
"info",
|
|
28
|
+
"muted",
|
|
29
|
+
"heading",
|
|
30
|
+
"accent",
|
|
31
|
+
"path",
|
|
32
|
+
"count",
|
|
33
|
+
"grade.a",
|
|
34
|
+
"grade.b",
|
|
35
|
+
"grade.c",
|
|
36
|
+
"grade.d",
|
|
37
|
+
"grade.f",
|
|
38
|
+
"sev.critical",
|
|
39
|
+
"sev.high",
|
|
40
|
+
"sev.medium",
|
|
41
|
+
"sev.low",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _make(styles: dict[str, str], symbols: dict[str, str]) -> dict:
|
|
46
|
+
missing = set(_TOKEN_NAMES) - set(styles)
|
|
47
|
+
assert not missing, f"palette missing tokens: {missing}"
|
|
48
|
+
return {"theme": Theme(styles), "symbols": symbols}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PALETTES: dict[str, dict] = {
|
|
52
|
+
# Matches config.py's existing default ("theme": "dark") -- not renamed to
|
|
53
|
+
# "default" to avoid yet another naming mismatch between config values and
|
|
54
|
+
# palette keys.
|
|
55
|
+
"dark": _make(
|
|
56
|
+
{
|
|
57
|
+
"success": "bold green",
|
|
58
|
+
"warn": "bold yellow",
|
|
59
|
+
"error": "bold red",
|
|
60
|
+
"info": "cyan",
|
|
61
|
+
"muted": "dim",
|
|
62
|
+
"heading": "bold white",
|
|
63
|
+
"accent": "bold magenta",
|
|
64
|
+
"path": "underline cyan",
|
|
65
|
+
"count": "bold",
|
|
66
|
+
"grade.a": "bold green",
|
|
67
|
+
"grade.b": "green",
|
|
68
|
+
"grade.c": "yellow",
|
|
69
|
+
"grade.d": "bold yellow",
|
|
70
|
+
"grade.f": "bold red",
|
|
71
|
+
"sev.critical": "bold white on red",
|
|
72
|
+
"sev.high": "bold red",
|
|
73
|
+
"sev.medium": "yellow",
|
|
74
|
+
"sev.low": "dim",
|
|
75
|
+
},
|
|
76
|
+
{"ok": "✓", "warn": "!", "err": "✗"},
|
|
77
|
+
),
|
|
78
|
+
"light": _make(
|
|
79
|
+
{
|
|
80
|
+
"success": "bold green3",
|
|
81
|
+
"warn": "bold dark_orange",
|
|
82
|
+
"error": "bold red3",
|
|
83
|
+
"info": "blue3",
|
|
84
|
+
"muted": "grey50",
|
|
85
|
+
"heading": "bold black",
|
|
86
|
+
"accent": "bold purple3",
|
|
87
|
+
"path": "underline blue3",
|
|
88
|
+
"count": "bold",
|
|
89
|
+
"grade.a": "bold green3",
|
|
90
|
+
"grade.b": "green3",
|
|
91
|
+
"grade.c": "dark_orange",
|
|
92
|
+
"grade.d": "bold dark_orange",
|
|
93
|
+
"grade.f": "bold red3",
|
|
94
|
+
"sev.critical": "bold white on red3",
|
|
95
|
+
"sev.high": "bold red3",
|
|
96
|
+
"sev.medium": "dark_orange",
|
|
97
|
+
"sev.low": "grey50",
|
|
98
|
+
},
|
|
99
|
+
{"ok": "✓", "warn": "!", "err": "✗"},
|
|
100
|
+
),
|
|
101
|
+
# Accessibility / NO_COLOR / non-TTY / --json. No color at all -- status is
|
|
102
|
+
# conveyed entirely by ASCII labels, since a dumb terminal or a screen
|
|
103
|
+
# reader gets nothing from color and may not even render Unicode glyphs.
|
|
104
|
+
"mono": _make(
|
|
105
|
+
{
|
|
106
|
+
"success": "none",
|
|
107
|
+
"warn": "none",
|
|
108
|
+
"error": "none",
|
|
109
|
+
"info": "none",
|
|
110
|
+
"muted": "none",
|
|
111
|
+
"heading": "bold",
|
|
112
|
+
"accent": "bold",
|
|
113
|
+
"path": "underline",
|
|
114
|
+
"count": "bold",
|
|
115
|
+
"grade.a": "none",
|
|
116
|
+
"grade.b": "none",
|
|
117
|
+
"grade.c": "none",
|
|
118
|
+
"grade.d": "none",
|
|
119
|
+
"grade.f": "none",
|
|
120
|
+
"sev.critical": "bold",
|
|
121
|
+
"sev.high": "bold",
|
|
122
|
+
"sev.medium": "none",
|
|
123
|
+
"sev.low": "none",
|
|
124
|
+
},
|
|
125
|
+
{"ok": "[OK]", "warn": "[!]", "err": "[X]"},
|
|
126
|
+
),
|
|
127
|
+
"highcontrast": _make(
|
|
128
|
+
{
|
|
129
|
+
"success": "bold bright_green",
|
|
130
|
+
"warn": "bold bright_yellow",
|
|
131
|
+
"error": "bold bright_red",
|
|
132
|
+
"info": "bold bright_cyan",
|
|
133
|
+
"muted": "white",
|
|
134
|
+
"heading": "bold bright_white",
|
|
135
|
+
"accent": "bold bright_magenta",
|
|
136
|
+
"path": "underline bright_cyan",
|
|
137
|
+
"count": "bold bright_white",
|
|
138
|
+
"grade.a": "bold bright_green",
|
|
139
|
+
"grade.b": "bright_green",
|
|
140
|
+
"grade.c": "bold bright_yellow",
|
|
141
|
+
"grade.d": "bold bright_yellow",
|
|
142
|
+
"grade.f": "bold bright_red",
|
|
143
|
+
"sev.critical": "bold bright_white on bright_red",
|
|
144
|
+
"sev.high": "bold bright_red",
|
|
145
|
+
"sev.medium": "bold bright_yellow",
|
|
146
|
+
"sev.low": "white",
|
|
147
|
+
},
|
|
148
|
+
{"ok": "✓", "warn": "!", "err": "✗"},
|
|
149
|
+
),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
DEFAULT_PALETTE = "dark"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_palette_name(
|
|
156
|
+
explicit: str | None,
|
|
157
|
+
config_theme: str | None,
|
|
158
|
+
no_color_env: bool,
|
|
159
|
+
is_tty: bool,
|
|
160
|
+
json_mode: bool,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Selection precedence, highest first:
|
|
164
|
+
1. --json -> force mono, UNCONDITIONALLY (deliberately ranked above even an
|
|
165
|
+
explicit --theme flag). This deviates from this feature's original
|
|
166
|
+
design doc, which put --theme above --json -- but --json's whole
|
|
167
|
+
purpose is a machine-parseable contract, and ANSI codes leaking into
|
|
168
|
+
JSON output from a theme flag (e.g. a shell alias someone forgot about)
|
|
169
|
+
would silently break downstream parsers. That's a correctness bug, not
|
|
170
|
+
a style preference, so it can't be overridden.
|
|
171
|
+
2. --theme flag (explicit)
|
|
172
|
+
3. NO_COLOR env var -> force mono (spec: https://no-color.org)
|
|
173
|
+
4. non-TTY -> force mono (piped output, no ANSI to confuse a non-terminal reader)
|
|
174
|
+
5. settings.json's theme value
|
|
175
|
+
6. DEFAULT_PALETTE
|
|
176
|
+
"""
|
|
177
|
+
if json_mode:
|
|
178
|
+
return "mono"
|
|
179
|
+
if explicit and explicit in PALETTES:
|
|
180
|
+
return explicit
|
|
181
|
+
if no_color_env:
|
|
182
|
+
return "mono"
|
|
183
|
+
if not is_tty:
|
|
184
|
+
return "mono"
|
|
185
|
+
if config_theme and config_theme in PALETTES:
|
|
186
|
+
return config_theme
|
|
187
|
+
return DEFAULT_PALETTE
|
patchi/cli/ux.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI UX — Shared UI components for consistent user experience.
|
|
3
|
+
|
|
4
|
+
Provides spinners, progress bars, status indicators, and formatting helpers
|
|
5
|
+
for a polished CLI experience.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from patchi.cli.ux import (
|
|
9
|
+
spinner, progress_bar, status_icon, format_header,
|
|
10
|
+
format_error, format_success, format_warning, format_step,
|
|
11
|
+
live_status, CountdownTimer,
|
|
12
|
+
)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
from typing import Any, Callable, Generator
|
|
20
|
+
|
|
21
|
+
from rich.console import Group
|
|
22
|
+
from rich.live import Live
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.progress import (
|
|
25
|
+
BarColumn,
|
|
26
|
+
MofNCompleteColumn,
|
|
27
|
+
Progress,
|
|
28
|
+
SpinnerColumn,
|
|
29
|
+
TextColumn,
|
|
30
|
+
TimeElapsedColumn,
|
|
31
|
+
)
|
|
32
|
+
from rich.table import Table
|
|
33
|
+
from rich.text import Text
|
|
34
|
+
|
|
35
|
+
from patchi.cli.console import con
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Status Icons ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
def status_icon(status: str) -> str:
|
|
41
|
+
"""Get a status icon for the given status."""
|
|
42
|
+
icons = {
|
|
43
|
+
"done": "✓",
|
|
44
|
+
"passed": "✓",
|
|
45
|
+
"success": "✓",
|
|
46
|
+
"running": "⟳",
|
|
47
|
+
"pending": "–",
|
|
48
|
+
"skipped": "–",
|
|
49
|
+
"failed": "✗",
|
|
50
|
+
"error": "✗",
|
|
51
|
+
"warning": "⚠",
|
|
52
|
+
}
|
|
53
|
+
return icons.get(status.lower(), "·")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def status_style(status: str) -> str:
|
|
57
|
+
"""Get Rich style for status."""
|
|
58
|
+
styles = {
|
|
59
|
+
"done": "green",
|
|
60
|
+
"passed": "green",
|
|
61
|
+
"success": "green",
|
|
62
|
+
"running": "cyan",
|
|
63
|
+
"pending": "dim",
|
|
64
|
+
"skipped": "yellow",
|
|
65
|
+
"failed": "red",
|
|
66
|
+
"error": "red",
|
|
67
|
+
"warning": "yellow",
|
|
68
|
+
}
|
|
69
|
+
return styles.get(status.lower(), "white")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def colored_status(status: str) -> str:
|
|
73
|
+
"""Get colored status string."""
|
|
74
|
+
icon = status_icon(status)
|
|
75
|
+
style = status_style(status)
|
|
76
|
+
return f"[{style}]{icon} {status}[/{style}]"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Formatting Helpers ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def format_header(title: str, subtitle: str | None = None) -> Panel:
|
|
82
|
+
"""Format a section header."""
|
|
83
|
+
if subtitle:
|
|
84
|
+
return Panel(
|
|
85
|
+
f"[bold]{title}[/bold]\n[dim]{subtitle}[/dim]",
|
|
86
|
+
border_style="blue",
|
|
87
|
+
padding=(0, 1),
|
|
88
|
+
)
|
|
89
|
+
return Panel(f"[bold]{title}[/bold]", border_style="blue", padding=(0, 1))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_step(step: int, total: int, description: str) -> Text:
|
|
93
|
+
"""Format a step indicator."""
|
|
94
|
+
return Text(f" [{step}/{total}] {description}", style="cyan")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_success(message: str) -> Panel:
|
|
98
|
+
"""Format a success message."""
|
|
99
|
+
return Panel(f"[green]✓ {message}[/green]", border_style="green", padding=(0, 1))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def format_error(message: str, details: str | None = None) -> Panel:
|
|
103
|
+
"""Format an error message."""
|
|
104
|
+
content = f"[red]✗ {message}[/red]"
|
|
105
|
+
if details:
|
|
106
|
+
content += f"\n\n[dim]{details}[/dim]"
|
|
107
|
+
return Panel(content, border_style="red", padding=(0, 1))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def format_warning(message: str) -> Panel:
|
|
111
|
+
"""Format a warning message."""
|
|
112
|
+
return Panel(f"[yellow]⚠ {message}[/yellow]", border_style="yellow", padding=(0, 1))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_info(message: str) -> Panel:
|
|
116
|
+
"""Format an info message."""
|
|
117
|
+
return Panel(f"[blue]ℹ {message}[/blue]", border_style="blue", padding=(0, 1))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Progress Bar ──────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
def create_progress() -> Progress:
|
|
123
|
+
"""Create a standard progress bar."""
|
|
124
|
+
return Progress(
|
|
125
|
+
SpinnerColumn(),
|
|
126
|
+
TextColumn("[progress.description]{task.description}"),
|
|
127
|
+
BarColumn(),
|
|
128
|
+
MofNCompleteColumn(),
|
|
129
|
+
TimeElapsedColumn(),
|
|
130
|
+
con=con,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def progress_bar(
|
|
135
|
+
items: list[Any],
|
|
136
|
+
description: str = "Processing",
|
|
137
|
+
process_fn: Callable[[Any], Any] | None = None,
|
|
138
|
+
) -> list[Any]:
|
|
139
|
+
"""Run items through a progress bar."""
|
|
140
|
+
results = []
|
|
141
|
+
with create_progress() as progress:
|
|
142
|
+
task = progress.add_task(description, total=len(items))
|
|
143
|
+
for item in items:
|
|
144
|
+
if process_fn:
|
|
145
|
+
result = process_fn(item)
|
|
146
|
+
results.append(result)
|
|
147
|
+
else:
|
|
148
|
+
results.append(item)
|
|
149
|
+
progress.advance(task)
|
|
150
|
+
return results
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── Spinner Context Manager ───────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
@contextmanager
|
|
156
|
+
def spinner(
|
|
157
|
+
message: str,
|
|
158
|
+
success_message: str | None = None,
|
|
159
|
+
error_message: str | None = None,
|
|
160
|
+
) -> Generator[Callable[[str], None], None, None]:
|
|
161
|
+
"""Context manager that shows a spinner while working.
|
|
162
|
+
|
|
163
|
+
Usage:
|
|
164
|
+
with spinner("Scanning...", "Done!", "Failed!") as update:
|
|
165
|
+
# do work
|
|
166
|
+
update("Processing files...")
|
|
167
|
+
# more work
|
|
168
|
+
"""
|
|
169
|
+
from rich.status import Status
|
|
170
|
+
|
|
171
|
+
status = Status(message, console=con, spinner="dots")
|
|
172
|
+
update_fn = lambda msg: status.update(msg)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
status.start()
|
|
176
|
+
yield update_fn
|
|
177
|
+
status.stop()
|
|
178
|
+
if success_message:
|
|
179
|
+
con.print(f"[green]✓ {success_message}[/green]")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
status.stop()
|
|
182
|
+
if error_message:
|
|
183
|
+
con.print(f"[red]✗ {error_message}: {e}[/red]")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ── Live Status Display ───────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
class LiveStatus:
|
|
190
|
+
"""Live status display for multi-step operations."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, title: str):
|
|
193
|
+
self.title = title
|
|
194
|
+
self.steps: list[dict] = []
|
|
195
|
+
self.current_step = 0
|
|
196
|
+
self._live: Live | None = None
|
|
197
|
+
|
|
198
|
+
def add_step(self, name: str, description: str) -> int:
|
|
199
|
+
"""Add a step and return its index."""
|
|
200
|
+
self.steps.append({
|
|
201
|
+
"name": name,
|
|
202
|
+
"description": description,
|
|
203
|
+
"status": "pending",
|
|
204
|
+
"result": None,
|
|
205
|
+
})
|
|
206
|
+
return len(self.steps) - 1
|
|
207
|
+
|
|
208
|
+
def start_step(self, index: int) -> None:
|
|
209
|
+
"""Mark a step as running."""
|
|
210
|
+
if 0 <= index < len(self.steps):
|
|
211
|
+
self.steps[index]["status"] = "running"
|
|
212
|
+
self.current_step = index
|
|
213
|
+
self._render()
|
|
214
|
+
|
|
215
|
+
def complete_step(self, index: int, status: str = "done", result: str | None = None) -> None:
|
|
216
|
+
"""Mark a step as complete."""
|
|
217
|
+
if 0 <= index < len(self.steps):
|
|
218
|
+
self.steps[index]["status"] = status
|
|
219
|
+
self.steps[index]["result"] = result
|
|
220
|
+
self._render()
|
|
221
|
+
|
|
222
|
+
def _render(self) -> None:
|
|
223
|
+
"""Render the current state."""
|
|
224
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
225
|
+
table.add_column("Status", width=3)
|
|
226
|
+
table.add_column("Step", ratio=3)
|
|
227
|
+
table.add_column("Result", ratio=2)
|
|
228
|
+
|
|
229
|
+
for step in self.steps:
|
|
230
|
+
icon = status_icon(step["status"])
|
|
231
|
+
style = status_style(step["status"])
|
|
232
|
+
result = step["result"] or ""
|
|
233
|
+
table.add_row(
|
|
234
|
+
f"[{style}]{icon}[/{style}]",
|
|
235
|
+
step["description"],
|
|
236
|
+
f"[dim]{result}[/dim]" if result else "",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if self._live:
|
|
240
|
+
self._live.update(table)
|
|
241
|
+
|
|
242
|
+
def __enter__(self) -> "LiveStatus":
|
|
243
|
+
self._live = Live(table=None, console=con, refresh_per_second=4)
|
|
244
|
+
self._live.start()
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
def __exit__(self, *args) -> None:
|
|
248
|
+
if self._live:
|
|
249
|
+
self._live.stop()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── Confirmation Prompt ────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
def confirm(message: str, default: bool = False) -> bool:
|
|
255
|
+
"""Prompt for confirmation with rich styling.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
message: The confirmation message
|
|
259
|
+
default: Default answer if user just presses Enter
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if confirmed, False otherwise
|
|
263
|
+
"""
|
|
264
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
265
|
+
while True:
|
|
266
|
+
try:
|
|
267
|
+
choice = input(f"{message} {suffix}: ").strip().lower()
|
|
268
|
+
if not choice:
|
|
269
|
+
return default
|
|
270
|
+
if choice in ("y", "yes"):
|
|
271
|
+
return True
|
|
272
|
+
if choice in ("n", "no"):
|
|
273
|
+
return False
|
|
274
|
+
con.print("[dim]Please enter y or n[/dim]")
|
|
275
|
+
except (EOFError, KeyboardInterrupt):
|
|
276
|
+
return default
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def select(
|
|
280
|
+
message: str,
|
|
281
|
+
options: list[str],
|
|
282
|
+
default: int = 0,
|
|
283
|
+
) -> int:
|
|
284
|
+
"""Prompt user to select from a list.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
message: The prompt message
|
|
288
|
+
options: List of options to choose from
|
|
289
|
+
default: Default selection index
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Selected index
|
|
293
|
+
"""
|
|
294
|
+
con.print(f"[bold]{message}[/bold]")
|
|
295
|
+
con.print()
|
|
296
|
+
|
|
297
|
+
for i, option in enumerate(options):
|
|
298
|
+
marker = "→" if i == default else " "
|
|
299
|
+
style = "cyan" if i == default else ""
|
|
300
|
+
con.print(f" {marker} [{style}]{i + 1}[/{style}]. {option}")
|
|
301
|
+
|
|
302
|
+
con.print()
|
|
303
|
+
|
|
304
|
+
while True:
|
|
305
|
+
try:
|
|
306
|
+
choice = input(f"Select (1-{len(options)}) [{default + 1}]: ").strip()
|
|
307
|
+
if not choice:
|
|
308
|
+
return default
|
|
309
|
+
idx = int(choice) - 1
|
|
310
|
+
if 0 <= idx < len(options):
|
|
311
|
+
return idx
|
|
312
|
+
con.print(f"[dim]Please enter a number between 1 and {len(options)}[/dim]")
|
|
313
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
314
|
+
return default
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ── Summary Panel ──────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
def summary_panel(
|
|
320
|
+
title: str,
|
|
321
|
+
items: dict[str, bool],
|
|
322
|
+
elapsed: float | None = None,
|
|
323
|
+
) -> Panel:
|
|
324
|
+
"""Create a summary panel showing pass/fail status.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
title: Panel title
|
|
328
|
+
items: Dict of item_name -> passed (bool)
|
|
329
|
+
elapsed: Optional elapsed time in seconds
|
|
330
|
+
"""
|
|
331
|
+
lines = []
|
|
332
|
+
passed = sum(1 for v in items.values() if v)
|
|
333
|
+
total = len(items)
|
|
334
|
+
|
|
335
|
+
for name, success in items.items():
|
|
336
|
+
icon = "✓" if success else "✗"
|
|
337
|
+
style = "green" if success else "red"
|
|
338
|
+
lines.append(f"[{style}]{icon} {name}[/{style}]")
|
|
339
|
+
|
|
340
|
+
content = "\n".join(lines)
|
|
341
|
+
|
|
342
|
+
if elapsed is not None:
|
|
343
|
+
content += f"\n\n[dim]Completed in {elapsed:.1f}s[/dim]"
|
|
344
|
+
|
|
345
|
+
border = "green" if passed == total else "yellow" if passed > 0 else "red"
|
|
346
|
+
|
|
347
|
+
return Panel(
|
|
348
|
+
f"[bold]{title}[/bold]\n\n{content}\n\n"
|
|
349
|
+
f"[dim]{passed}/{total} passed[/dim]",
|
|
350
|
+
border_style=border,
|
|
351
|
+
padding=(0, 1),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ── Status Style Helper ────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
def status_style(status: str) -> str:
|
|
358
|
+
"""Get Rich style for status."""
|
|
359
|
+
styles = {
|
|
360
|
+
"done": "green",
|
|
361
|
+
"passed": "green",
|
|
362
|
+
"success": "green",
|
|
363
|
+
"running": "cyan",
|
|
364
|
+
"pending": "dim",
|
|
365
|
+
"skipped": "yellow",
|
|
366
|
+
"failed": "red",
|
|
367
|
+
"error": "red",
|
|
368
|
+
"warning": "yellow",
|
|
369
|
+
}
|
|
370
|
+
return styles.get(status.lower(), "white")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── Result Table ──────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
def result_table(
|
|
376
|
+
title: str,
|
|
377
|
+
rows: list[dict],
|
|
378
|
+
columns: list[str] | None = None,
|
|
379
|
+
) -> Table:
|
|
380
|
+
"""Create a formatted result table."""
|
|
381
|
+
if columns is None:
|
|
382
|
+
columns = ["Check", "Status", "Findings", "Details"]
|
|
383
|
+
|
|
384
|
+
table = Table(title=title, show_header=True, header_style="bold")
|
|
385
|
+
for col in columns:
|
|
386
|
+
table.add_column(col)
|
|
387
|
+
|
|
388
|
+
for row in rows:
|
|
389
|
+
values = []
|
|
390
|
+
for col in columns:
|
|
391
|
+
key = col.lower()
|
|
392
|
+
if key in row:
|
|
393
|
+
val = row[key]
|
|
394
|
+
if key == "status":
|
|
395
|
+
values.append(colored_status(str(val)))
|
|
396
|
+
elif key == "findings":
|
|
397
|
+
count = int(val) if val else 0
|
|
398
|
+
if count > 0:
|
|
399
|
+
values.append(f"[red]{count}[/red]")
|
|
400
|
+
else:
|
|
401
|
+
values.append("[green]0[/green]")
|
|
402
|
+
else:
|
|
403
|
+
values.append(str(val))
|
|
404
|
+
else:
|
|
405
|
+
values.append("")
|
|
406
|
+
table.add_row(*values)
|
|
407
|
+
|
|
408
|
+
return table
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ── Countdown Timer ───────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
class CountdownTimer:
|
|
414
|
+
"""Visual countdown timer for timed operations."""
|
|
415
|
+
|
|
416
|
+
def __init__(self, seconds: int, description: str = "Time remaining"):
|
|
417
|
+
self.seconds = seconds
|
|
418
|
+
self.description = description
|
|
419
|
+
self._start = 0.0
|
|
420
|
+
|
|
421
|
+
def __enter__(self) -> "CountdownTimer":
|
|
422
|
+
self._start = time.monotonic()
|
|
423
|
+
return self
|
|
424
|
+
|
|
425
|
+
def __exit__(self, *args) -> None:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def elapsed(self) -> float:
|
|
430
|
+
return time.monotonic() - self._start
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def remaining(self) -> float:
|
|
434
|
+
return max(0, self.seconds - self.elapsed)
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def is_complete(self) -> bool:
|
|
438
|
+
return self.elapsed >= self.seconds
|
|
439
|
+
|
|
440
|
+
def format_remaining(self) -> str:
|
|
441
|
+
"""Format remaining time as MM:SS."""
|
|
442
|
+
r = int(self.remaining)
|
|
443
|
+
mins, secs = divmod(r, 60)
|
|
444
|
+
return f"{mins:02d}:{secs:02d}"
|
patchi/core/__init__.py
ADDED
|
File without changes
|
|
File without changes
|