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
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p watch` — Watch mode. Auto-scans on file saves.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
p watch — watch entire project
|
|
6
|
+
p watch src/auth — watch specific area only
|
|
7
|
+
p watch --dry-run — preview auto-fixes without applying
|
|
8
|
+
p watch --preview — show detailed before/after of proposed fixes
|
|
9
|
+
p watch --auto-fix — enable proactive fixing on file saves
|
|
10
|
+
|
|
11
|
+
Runs until Ctrl+C. Every time a source file is saved, Patchi
|
|
12
|
+
re-scans the changed area and updates the brain.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import signal
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from patchi.cli.console import con
|
|
23
|
+
from patchi.core.brain.freshness import BrainWatcher
|
|
24
|
+
from patchi.core.config import require_project_root
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.cli.watch_cmd")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _show_preview(root: Path, fixes: list, applied: list) -> None:
|
|
30
|
+
"""Show detailed before/after preview of proposed fixes."""
|
|
31
|
+
|
|
32
|
+
con.print()
|
|
33
|
+
con.print("[bold]Proposed Fixes — Preview[/bold]")
|
|
34
|
+
con.print("[dim]Showing what would change (not yet applied).[/dim]")
|
|
35
|
+
con.print()
|
|
36
|
+
|
|
37
|
+
for i, fix in enumerate(fixes, 1):
|
|
38
|
+
# Determine status
|
|
39
|
+
was_applied = fix in applied
|
|
40
|
+
status_color = "#22c55e" if was_applied else "#eab308"
|
|
41
|
+
status_text = "APPLIED" if was_applied else "PROPOSED"
|
|
42
|
+
|
|
43
|
+
con.print(f" [bold]{i}. {fix.fix_type}[/bold] → {fix.file}")
|
|
44
|
+
if fix.name:
|
|
45
|
+
con.print(f" [dim]Target: {fix.name}[/dim]")
|
|
46
|
+
con.print(f" [{status_color}]{status_text}[/{status_color}] [dim]{fix.description}[/dim]")
|
|
47
|
+
|
|
48
|
+
# Show before/after if the file exists and the fix has a suggested line
|
|
49
|
+
file_path = root / fix.file
|
|
50
|
+
if file_path.exists() and fix.suggested_line:
|
|
51
|
+
try:
|
|
52
|
+
lines = file_path.read_text(encoding="utf-8").splitlines()
|
|
53
|
+
line_num = fix.suggested_line
|
|
54
|
+
if 1 <= line_num <= len(lines):
|
|
55
|
+
# Show context (2 lines before and after)
|
|
56
|
+
start = max(0, line_num - 3)
|
|
57
|
+
end = min(len(lines), line_num + 2)
|
|
58
|
+
|
|
59
|
+
con.print(f" [dim]Before (line {line_num}):[/dim]")
|
|
60
|
+
for j in range(start, end):
|
|
61
|
+
prefix = ">>>" if j == line_num - 1 else " "
|
|
62
|
+
con.print(f" {prefix} {j+1:4d} │ {lines[j]}")
|
|
63
|
+
|
|
64
|
+
con.print(" [green]After:[/green]")
|
|
65
|
+
con.print(f" >>> {line_num:4d} │ {fix.suggested_line}")
|
|
66
|
+
except Exception as _exc:
|
|
67
|
+
_log.warning('_show_preview failed: %s', _exc)
|
|
68
|
+
|
|
69
|
+
con.print()
|
|
70
|
+
|
|
71
|
+
# Summary
|
|
72
|
+
con.print(f" [dim]Total: {len(fixes)} fixes ({len(applied)} applied, {len(fixes) - len(applied)} proposed)[/dim]")
|
|
73
|
+
con.print(" [dim]Run with --dry-run to preview without applying, or --auto-fix to enable auto-apply.[/dim]")
|
|
74
|
+
con.print()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run(
|
|
78
|
+
area: str | None = None,
|
|
79
|
+
root: Path | None = None,
|
|
80
|
+
dry_run: bool = False,
|
|
81
|
+
preview: bool = False,
|
|
82
|
+
auto_fix: bool = False,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Entry point for `p watch [area]`.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
area: Optional subpath to watch.
|
|
88
|
+
root: Override project root.
|
|
89
|
+
dry_run: If True, preview auto-fixes without applying them.
|
|
90
|
+
preview: If True, show detailed before/after of proposed fixes.
|
|
91
|
+
auto_fix: If True, enable proactive fixing on file saves (overrides config).
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
r = root or require_project_root()
|
|
95
|
+
except RuntimeError as e:
|
|
96
|
+
con.print(f"[red]{e}[/red]")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
area_label = f" [dim]→ {area}[/dim]" if area else " [dim](full project)[/dim]"
|
|
100
|
+
flags = []
|
|
101
|
+
if dry_run:
|
|
102
|
+
flags.append("dry-run")
|
|
103
|
+
if preview:
|
|
104
|
+
flags.append("preview")
|
|
105
|
+
if auto_fix:
|
|
106
|
+
flags.append("auto-fix")
|
|
107
|
+
flag_label = f" [yellow]({', '.join(flags)})[/yellow]" if flags else ""
|
|
108
|
+
con.print()
|
|
109
|
+
con.print(f"[bold #C8621A]Watch mode{area_label}{flag_label}[/bold #C8621A]")
|
|
110
|
+
con.print("[dim]Patchi is watching for file changes. Press Ctrl+C to stop.[/dim]")
|
|
111
|
+
con.print()
|
|
112
|
+
|
|
113
|
+
_running = [True]
|
|
114
|
+
_scan_count = [0]
|
|
115
|
+
|
|
116
|
+
def on_change(changed_paths: list[str]) -> None:
|
|
117
|
+
_scan_count[0] += 1
|
|
118
|
+
n = len(changed_paths)
|
|
119
|
+
short = changed_paths[0] if changed_paths else "?"
|
|
120
|
+
label = f"{short}" if n == 1 else f"{short} +{n - 1} more"
|
|
121
|
+
|
|
122
|
+
# Respect quiet hours — skip auto-scan during quiet window (WIRE-05)
|
|
123
|
+
try:
|
|
124
|
+
from patchi.core import config as cfg_mod
|
|
125
|
+
from patchi.core.notifications.quiet_hours import is_quiet_now
|
|
126
|
+
|
|
127
|
+
conf = cfg_mod.load(r)
|
|
128
|
+
qh = conf.get("quiet_hours", {})
|
|
129
|
+
if is_quiet_now(qh):
|
|
130
|
+
con.print(f"[dim]Quiet hours active — skipping auto-scan for {label}[/dim]")
|
|
131
|
+
return
|
|
132
|
+
except Exception as e:
|
|
133
|
+
_log.warning("on_change failed: %s", e)
|
|
134
|
+
|
|
135
|
+
con.print(f"[#C8621A]↺[/#C8621A] [dim]{label} changed — rescanning…[/dim]")
|
|
136
|
+
|
|
137
|
+
# Determine minimal area to rescan
|
|
138
|
+
rescan_area = area
|
|
139
|
+
if not rescan_area and changed_paths:
|
|
140
|
+
# Use the parent directory of the first changed file as the target
|
|
141
|
+
parent = str(Path(changed_paths[0]).parent)
|
|
142
|
+
if parent and parent != ".":
|
|
143
|
+
rescan_area = parent
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
from patchi.cli.commands.scan_cmd import run as run_scan
|
|
147
|
+
|
|
148
|
+
run_scan(area=rescan_area, quiet=True, root=r)
|
|
149
|
+
con.print("[#4ADE80]✓[/#4ADE80] [dim]Brain updated.[/dim]")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
con.print(f"[yellow]⚠[/yellow] [dim]Rescan failed: {e}[/dim]")
|
|
152
|
+
|
|
153
|
+
# ── Phase 4b: Proactive fixes on save (behind config flag) ─────────
|
|
154
|
+
try:
|
|
155
|
+
from patchi.core import config as cfg_mod
|
|
156
|
+
from patchi.core.brain.proactive import (
|
|
157
|
+
escalate_to_governor,
|
|
158
|
+
run_proactive,
|
|
159
|
+
)
|
|
160
|
+
from patchi.core.security.tool_verify import high_findings_on_file
|
|
161
|
+
|
|
162
|
+
conf = cfg_mod.load(r)
|
|
163
|
+
auto_fix_cfg = conf.get("auto_fix", {}) or {}
|
|
164
|
+
# --auto-fix flag overrides config.auto_fix.enabled
|
|
165
|
+
af_enabled = auto_fix or bool(auto_fix_cfg.get("enabled", False))
|
|
166
|
+
af_unsafe = bool(auto_fix_cfg.get("unsafe", False))
|
|
167
|
+
verify_sast = bool(auto_fix_cfg.get("verify_sast", False))
|
|
168
|
+
|
|
169
|
+
norm = []
|
|
170
|
+
for p in changed_paths:
|
|
171
|
+
pp = Path(p)
|
|
172
|
+
try:
|
|
173
|
+
norm.append(pp.relative_to(r).as_posix())
|
|
174
|
+
except ValueError:
|
|
175
|
+
norm.append(pp.as_posix())
|
|
176
|
+
if not norm:
|
|
177
|
+
norm = changed_paths
|
|
178
|
+
|
|
179
|
+
# Snapshot existing HIGH/CRITICAL findings BEFORE applying fixes so
|
|
180
|
+
# we only flag regressions the fix actually introduced (not
|
|
181
|
+
# pre-existing ones on every save).
|
|
182
|
+
pre_highs: dict[str, set] = {}
|
|
183
|
+
if af_enabled and not dry_run and verify_sast:
|
|
184
|
+
for cf in norm:
|
|
185
|
+
cp = Path(r) / cf
|
|
186
|
+
if cp.exists() and cp.suffix == ".py":
|
|
187
|
+
try:
|
|
188
|
+
pre_highs[cf] = {
|
|
189
|
+
(h.type, h.cwe) for h in high_findings_on_file(cp)
|
|
190
|
+
}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
_log.warning("SAST pre-snapshot failed for %s: %s", cf, e)
|
|
193
|
+
pre_highs[cf] = set()
|
|
194
|
+
|
|
195
|
+
# Apply only if auto_fix enabled AND not in dry-run mode
|
|
196
|
+
should_apply = af_enabled and not dry_run
|
|
197
|
+
result = run_proactive(r, norm, apply=should_apply, unsafe=af_unsafe)
|
|
198
|
+
|
|
199
|
+
if result["fixes"]:
|
|
200
|
+
mode = "preview" if (dry_run or preview) else "apply"
|
|
201
|
+
con.print(
|
|
202
|
+
f"[dim]Proactive ({mode}): {len(result['fixes'])} fix(es) proposed "
|
|
203
|
+
f"({len(result['applied'])} applied, "
|
|
204
|
+
f"{len(result['escalated'])} need review).[/dim]"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# --preview: show detailed before/after for each proposed fix
|
|
208
|
+
if preview and result["fixes"]:
|
|
209
|
+
_show_preview(r, result["fixes"], result["applied"])
|
|
210
|
+
|
|
211
|
+
for fix in result["escalated"]:
|
|
212
|
+
escalate_to_governor(r, fix)
|
|
213
|
+
con.print(
|
|
214
|
+
f"[#FACC15]⚠[/#FACC15] [yellow]Escalated to Governor:[/yellow] "
|
|
215
|
+
f"{fix.fix_type} → {fix.file}" + (f" ({fix.name})" if fix.name else "")
|
|
216
|
+
)
|
|
217
|
+
con.print(f" [dim]{fix.description}[/dim]")
|
|
218
|
+
|
|
219
|
+
# ── Phase 4b: verify applied fixes did not introduce a HIGH/CRITICAL SAST finding ──
|
|
220
|
+
# Opt-in (default off): Semgrep is slow per-file, so this is gated behind
|
|
221
|
+
# auto_fix.verify_sast to keep watch-mode latency acceptable. Only NEW highs
|
|
222
|
+
# (absent before the fix) are flagged, avoiding re-reporting pre-existing ones.
|
|
223
|
+
if should_apply and verify_sast:
|
|
224
|
+
try:
|
|
225
|
+
from patchi.core import memory as mem
|
|
226
|
+
from patchi.core.security.tool_verify import verify_proactive_fixes
|
|
227
|
+
|
|
228
|
+
issues = verify_proactive_fixes(
|
|
229
|
+
r,
|
|
230
|
+
[fx.file for fx in result.get("applied", [])],
|
|
231
|
+
pre_highs,
|
|
232
|
+
)
|
|
233
|
+
for iss in issues:
|
|
234
|
+
mem.save_issue(iss)
|
|
235
|
+
con.print(
|
|
236
|
+
f"[red]⚠ SAST regression after proactive fix:[/red] "
|
|
237
|
+
f"{iss['file']}:{iss.get('line', 0)} [{iss['name']}]"
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
con.print(f"[dim]SAST verify skipped: {e}[/dim]")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
con.print(f"[dim]Proactive check skipped: {e}[/dim]")
|
|
243
|
+
|
|
244
|
+
# ── Phase 4b: change-impact (blast radius) on save ────────────────
|
|
245
|
+
try:
|
|
246
|
+
from patchi.cli.commands.reason_cmd import run_impact
|
|
247
|
+
|
|
248
|
+
if norm:
|
|
249
|
+
run_impact(norm, root=r)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
con.print(f"[dim]Impact analysis skipped: {e}[/dim]")
|
|
252
|
+
|
|
253
|
+
# ── Phase 4b: secrets sweep on changed files only ─────────────────
|
|
254
|
+
try:
|
|
255
|
+
from patchi.core import memory as mem
|
|
256
|
+
from patchi.core.brain.secrets import scan_secrets
|
|
257
|
+
|
|
258
|
+
hits = scan_secrets(r, paths=norm) if norm else []
|
|
259
|
+
for h in hits:
|
|
260
|
+
mem.save_issue(
|
|
261
|
+
{
|
|
262
|
+
"source": "secrets",
|
|
263
|
+
"fix_type": "secret_leak",
|
|
264
|
+
"file": h.path,
|
|
265
|
+
"name": h.rule,
|
|
266
|
+
"description": f"Possible secret ({h.rule}) at line {h.line}: {h.snippet}",
|
|
267
|
+
"severity": "high",
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
con.print(
|
|
271
|
+
f"[#FACC15]🔒[/#FACC15] [yellow]Secret detected:[/yellow] "
|
|
272
|
+
f"{h.path}:{h.line} [{h.rule}]"
|
|
273
|
+
)
|
|
274
|
+
con.print(f" [dim]{h.snippet}[/dim]")
|
|
275
|
+
if hits:
|
|
276
|
+
con.print(f"[dim]Escalated {len(hits)} secret hit(s) to Governor for review.[/dim]")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
con.print(f"[dim]Secrets check skipped: {e}[/dim]")
|
|
279
|
+
|
|
280
|
+
con.print()
|
|
281
|
+
|
|
282
|
+
def handle_stop(sig, frame) -> None:
|
|
283
|
+
_running[0] = False
|
|
284
|
+
con.print()
|
|
285
|
+
con.print(f"[dim]Watch stopped. {_scan_count[0]} rescan(s) triggered this session.[/dim]")
|
|
286
|
+
con.print()
|
|
287
|
+
|
|
288
|
+
signal.signal(signal.SIGINT, handle_stop)
|
|
289
|
+
signal.signal(signal.SIGTERM, handle_stop)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
watcher = BrainWatcher(root=r, on_change=on_change, area=area, debounce_ms=600)
|
|
293
|
+
watcher.start()
|
|
294
|
+
|
|
295
|
+
# Keep main thread alive
|
|
296
|
+
while _running[0]:
|
|
297
|
+
time.sleep(0.2)
|
|
298
|
+
|
|
299
|
+
watcher.stop()
|
|
300
|
+
|
|
301
|
+
except RuntimeError as e:
|
|
302
|
+
con.print(f"[red]{e}[/red]")
|
|
303
|
+
except Exception as e:
|
|
304
|
+
con.print(f"[red]Watch error: {e}[/red]")
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p web` command — launch the unified Patchi web UI.
|
|
3
|
+
|
|
4
|
+
One server, one UI:
|
|
5
|
+
- Mission Control dashboard (/) — health, live agent feed, tools
|
|
6
|
+
- Brain map / Council / Attacks / Tests — intelligence views
|
|
7
|
+
- Findings / Review / Chat — workflow pages
|
|
8
|
+
- Hosted control plane (/hosted) — overview, compliance, webhooks
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
p web # serve on 127.0.0.1:1612
|
|
12
|
+
p web --port 8000 # custom port
|
|
13
|
+
p web --open # also open the browser
|
|
14
|
+
|
|
15
|
+
Requires the `web` extra: pip install patchi[web]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
27
|
+
DEFAULT_PORT = 1612
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_project(explicit: str | None) -> Path:
|
|
31
|
+
"""Pick the project to serve — EXACTLY like every other p <command>.
|
|
32
|
+
|
|
33
|
+
1:1 rule: the web UI opens the project the terminal is standing in.
|
|
34
|
+
Resolution is the same find_project_root() walk-up every other command
|
|
35
|
+
uses (nearest .patchi at or above cwd). The only extra is an explicit
|
|
36
|
+
--project override. Anything else refuses, same message, same exit.
|
|
37
|
+
|
|
38
|
+
The multi-project story lives IN the web UI (header switcher), not in
|
|
39
|
+
magic cwd guessing here.
|
|
40
|
+
"""
|
|
41
|
+
from patchi.core.config import find_project_root
|
|
42
|
+
|
|
43
|
+
if explicit:
|
|
44
|
+
p = Path(explicit).expanduser().resolve()
|
|
45
|
+
if not (p / ".patchi").is_dir():
|
|
46
|
+
console.print(
|
|
47
|
+
f"[red]{p} has no .patchi directory.[/red] Run [bold]p init[/bold] there first."
|
|
48
|
+
)
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
return p
|
|
51
|
+
|
|
52
|
+
found = find_project_root()
|
|
53
|
+
if found:
|
|
54
|
+
if found.resolve() == Path.home().resolve():
|
|
55
|
+
console.print(
|
|
56
|
+
"[yellow]Note:[/yellow] resolved to your HOME directory "
|
|
57
|
+
f"({found}) because it contains a .patchi folder. "
|
|
58
|
+
"If that's not intentional, cd into your real project or use "
|
|
59
|
+
"[bold]--project <path>[/bold]."
|
|
60
|
+
)
|
|
61
|
+
return found
|
|
62
|
+
|
|
63
|
+
# Same contract as require_project_root(); candidates listed as FYI only
|
|
64
|
+
from patchi.core.tenant import TenantManager
|
|
65
|
+
|
|
66
|
+
console.print(
|
|
67
|
+
"[red]No .patchi directory found.[/red] Run [bold]p init[/bold] in your project folder first."
|
|
68
|
+
)
|
|
69
|
+
nearby = TenantManager().discover_projects(near=Path.cwd())
|
|
70
|
+
if nearby:
|
|
71
|
+
console.print("[dim]Patchi projects near here:[/dim]")
|
|
72
|
+
for c in nearby[:5]:
|
|
73
|
+
console.print(f'[dim] - {c} -> p web --project "{c}"[/dim]')
|
|
74
|
+
if len(nearby) > 5:
|
|
75
|
+
console.print(f"[dim] … and {len(nearby) - 5} more[/dim]")
|
|
76
|
+
raise SystemExit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(
|
|
80
|
+
root: Path | None = None,
|
|
81
|
+
host: str = DEFAULT_HOST,
|
|
82
|
+
port: int = DEFAULT_PORT,
|
|
83
|
+
open_browser: bool = False,
|
|
84
|
+
project: str | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Start the unified Patchi web server (blocking)."""
|
|
87
|
+
try:
|
|
88
|
+
import uvicorn # noqa: F401
|
|
89
|
+
except ImportError as e:
|
|
90
|
+
console.print(
|
|
91
|
+
"[red]Missing dependencies.[/red] Install with: [bold]pip install patchi[web][/bold]"
|
|
92
|
+
)
|
|
93
|
+
raise SystemExit(1) from e
|
|
94
|
+
|
|
95
|
+
project_root = root or _resolve_project(project)
|
|
96
|
+
|
|
97
|
+
url = f"http://{host}:{port}"
|
|
98
|
+
console.print()
|
|
99
|
+
console.print(f"[bold #C8621A]Patchi Web UI[/bold #C8621A] -> [link={url}]{url}[/link]")
|
|
100
|
+
console.print("[dim]Mission Control · Council · Red Team · Live Tests · Hosted[/dim]")
|
|
101
|
+
console.print("[dim]Press Ctrl+C to stop.[/dim]")
|
|
102
|
+
console.print()
|
|
103
|
+
|
|
104
|
+
from patchi.web.app import create_app
|
|
105
|
+
|
|
106
|
+
app = create_app(project_root)
|
|
107
|
+
|
|
108
|
+
if open_browser:
|
|
109
|
+
import threading
|
|
110
|
+
import webbrowser
|
|
111
|
+
|
|
112
|
+
threading.Timer(1.5, webbrowser.open, args=(url,)).start()
|
|
113
|
+
|
|
114
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
patchi/cli/console.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Console factory — ensures consistent encoding handling across all commands.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from patchi.cli.console import con
|
|
6
|
+
con.print("...")
|
|
7
|
+
|
|
8
|
+
# Once, early in dispatch (main.py), after CLI flags are parsed:
|
|
9
|
+
from patchi.cli.console import configure_theme
|
|
10
|
+
configure_theme(explicit=getattr(args, "theme", None),
|
|
11
|
+
no_color_flag=getattr(args, "no_color", False),
|
|
12
|
+
json_mode=getattr(args, "json_output", False))
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
_encoding_fixed = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_log = logging.getLogger("patchi.cli.console")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ensure_utf8():
|
|
29
|
+
global _encoding_fixed
|
|
30
|
+
if _encoding_fixed:
|
|
31
|
+
return
|
|
32
|
+
_encoding_fixed = True
|
|
33
|
+
try:
|
|
34
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
_log.warning("_ensure_utf8 failed: %s", e)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_ensure_utf8()
|
|
41
|
+
con = Console(highlight=False)
|
|
42
|
+
|
|
43
|
+
# Active symbol set for the current palette (style.py reads this). Starts as
|
|
44
|
+
# the dark palette's symbols so con.print(...) with semantic tokens works
|
|
45
|
+
# correctly even if configure_theme() is never called (e.g. tests, scripts
|
|
46
|
+
# that import con directly without going through main.py's dispatch).
|
|
47
|
+
_active_symbols: dict[str, str] = {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def configure_theme(
|
|
51
|
+
explicit: str | None = None,
|
|
52
|
+
no_color_flag: bool = False,
|
|
53
|
+
json_mode: bool = False,
|
|
54
|
+
root=None,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Resolve the effective palette (see themes.resolve_palette_name for the
|
|
58
|
+
exact precedence) and apply it to the shared `con`. Returns the palette
|
|
59
|
+
name actually applied, mainly so callers/tests can assert on it.
|
|
60
|
+
|
|
61
|
+
Safe to call multiple times (e.g. once per command invocation) --
|
|
62
|
+
push_theme() replaces the active theme rather than stacking indefinitely
|
|
63
|
+
in practice, since this is only ever called once per process at
|
|
64
|
+
dispatch time, not nested.
|
|
65
|
+
"""
|
|
66
|
+
from patchi.cli.themes import PALETTES, resolve_palette_name
|
|
67
|
+
|
|
68
|
+
config_theme = None
|
|
69
|
+
try:
|
|
70
|
+
from patchi.core import config as cfg
|
|
71
|
+
|
|
72
|
+
config_theme = cfg.get("theme", root)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
_log.debug("configure_theme: could not read config theme, using default: %s", e)
|
|
75
|
+
|
|
76
|
+
no_color_env = no_color_flag or bool(os.environ.get("NO_COLOR"))
|
|
77
|
+
is_tty = con.is_terminal
|
|
78
|
+
|
|
79
|
+
name = resolve_palette_name(
|
|
80
|
+
explicit=explicit,
|
|
81
|
+
config_theme=config_theme,
|
|
82
|
+
no_color_env=no_color_env,
|
|
83
|
+
is_tty=is_tty,
|
|
84
|
+
json_mode=json_mode,
|
|
85
|
+
)
|
|
86
|
+
palette = PALETTES[name]
|
|
87
|
+
con.push_theme(palette["theme"])
|
|
88
|
+
con.no_color = name == "mono"
|
|
89
|
+
_active_symbols.clear()
|
|
90
|
+
_active_symbols.update(palette["symbols"])
|
|
91
|
+
return name
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def symbol(key: str) -> str:
|
|
95
|
+
"""Get the current palette's symbol for 'ok'/'warn'/'err', defaulting to
|
|
96
|
+
the dark palette's Unicode glyphs if configure_theme() was never called."""
|
|
97
|
+
if not _active_symbols:
|
|
98
|
+
from patchi.cli.themes import PALETTES
|
|
99
|
+
|
|
100
|
+
return PALETTES["dark"]["symbols"].get(key, "")
|
|
101
|
+
return _active_symbols.get(key, "")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|