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,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P-Check Agent — sole job to get app into runnable, testable state.
|
|
3
|
+
|
|
4
|
+
Spec:
|
|
5
|
+
1. Install all dependencies
|
|
6
|
+
2. Run formatting/lint checks
|
|
7
|
+
3. Run other required setup (env files, codegen, config validation)
|
|
8
|
+
4. Attempt to build and start the app as local dev server (localhost)
|
|
9
|
+
|
|
10
|
+
When everything succeeds: output READY_TO_SERVE with exact local dev server URL and port. Stop.
|
|
11
|
+
When something fails: flag what failed + exact error + what is needed, escalate to Brain, wait for fix, re-run from step 1. Never READY_TO_SERVE while blocking error remains.
|
|
12
|
+
Hard rule: No other agent may treat app as usable until READY_TO_SERVE.
|
|
13
|
+
|
|
14
|
+
Gate file: .patchi/p_check_status.json {status, url, port, timestamp, error}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
|
|
26
|
+
from patchi.cli.console import con
|
|
27
|
+
from patchi.core.config import require_project_root
|
|
28
|
+
|
|
29
|
+
_STATUS_FILE = ".patchi/p_check_status.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write_status(root: Path, status: str, url: str | None = None, port: int | None = None, error: str | None = None, domain: str | None = None) -> None:
|
|
33
|
+
p = root / _STATUS_FILE
|
|
34
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
data = {"status": status, "url": url, "port": port, "error": error, "domain": domain, "timestamp": time.time(), "iso": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
|
36
|
+
tmp = p.with_suffix(".tmp")
|
|
37
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
38
|
+
tmp.replace(p)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_status(root: Path) -> dict | None:
|
|
42
|
+
p = root / _STATUS_FILE
|
|
43
|
+
if not p.exists():
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_ready(root: Path) -> tuple[bool, dict | None]:
|
|
52
|
+
"""Gate Rule helper: returns (ready, status_dict)."""
|
|
53
|
+
st = _read_status(root)
|
|
54
|
+
if not st:
|
|
55
|
+
return False, None
|
|
56
|
+
if st.get("status") != "READY_TO_SERVE":
|
|
57
|
+
return False, st
|
|
58
|
+
# optional TTL: 1 hour? For now no expiry, but check url still reachable?
|
|
59
|
+
if not st.get("url"):
|
|
60
|
+
return False, st
|
|
61
|
+
# check timestamp within 1 hour or still running?
|
|
62
|
+
return True, st
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Finding types p check --fix may auto-repair (its lane: type/deps/build/format/install).
|
|
66
|
+
# Everything else (incl. all security findings) is reported for the Brain, never touched.
|
|
67
|
+
_FIXABLE_TYPES = {"missing_env", "format_error"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _apply_domain_fixes(r: Path, blocking: list) -> list[str]:
|
|
71
|
+
"""Attempt safe auto-fixes for in-lane findings. Returns human-readable actions taken."""
|
|
72
|
+
import shutil
|
|
73
|
+
import subprocess
|
|
74
|
+
|
|
75
|
+
actions: list[str] = []
|
|
76
|
+
for f in blocking:
|
|
77
|
+
ftype = getattr(f, "type", "") or ""
|
|
78
|
+
if ftype == "missing_env":
|
|
79
|
+
src, dst = r / ".env.example", r / ".env"
|
|
80
|
+
try:
|
|
81
|
+
if src.exists() and not dst.exists():
|
|
82
|
+
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
83
|
+
actions.append("copied .env.example → .env (fill values before run)")
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
elif ftype == "format_error":
|
|
87
|
+
if shutil.which("ruff") and (r / "pyproject.toml").exists():
|
|
88
|
+
try:
|
|
89
|
+
proc = subprocess.run(
|
|
90
|
+
["ruff", "format", "."],
|
|
91
|
+
capture_output=True, text=True, timeout=60, cwd=str(r),
|
|
92
|
+
)
|
|
93
|
+
if proc.returncode == 0:
|
|
94
|
+
actions.append("ran ruff format .")
|
|
95
|
+
except Exception as _exc:
|
|
96
|
+
logging.getLogger("patchi").debug('suppressed: %s', _exc)
|
|
97
|
+
return actions
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_side_agents(r: Path, brain: dict, config: dict):
|
|
101
|
+
from patchi.core.agents.base import AgentInput
|
|
102
|
+
from patchi.core.agents.base import list_agents as la
|
|
103
|
+
|
|
104
|
+
agents = [a for a in la() if a.name in ("InstallAgent", "BuildAgent", "FormatAgent")]
|
|
105
|
+
findings: list = []
|
|
106
|
+
blocking: list = []
|
|
107
|
+
for cls in agents:
|
|
108
|
+
res = cls().run(AgentInput(root=r, scope=[], brain=brain, config=config, extra={}))
|
|
109
|
+
findings.extend(res.findings)
|
|
110
|
+
for f in res.findings:
|
|
111
|
+
sev = f.severity.value if hasattr(f.severity, "value") else str(f.severity)
|
|
112
|
+
if sev in ("high", "critical"):
|
|
113
|
+
blocking.append(f)
|
|
114
|
+
return findings, blocking
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run(fix: bool = False, json_output: bool = False, root: Path | None = None) -> None:
|
|
118
|
+
try:
|
|
119
|
+
r = root or require_project_root()
|
|
120
|
+
except RuntimeError as e:
|
|
121
|
+
con.print(f"[red]{e}[/red]")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
from patchi.core.agents.base import discover_agent_modules
|
|
125
|
+
|
|
126
|
+
discover_agent_modules()
|
|
127
|
+
import patchi.core.agents.side.build_agent # noqa: F401
|
|
128
|
+
import patchi.core.agents.side.format_agent # noqa: F401
|
|
129
|
+
import patchi.core.agents.side.install_agent # noqa: F401
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
from patchi.core import config as cfg
|
|
133
|
+
from patchi.core import memory as mem
|
|
134
|
+
|
|
135
|
+
brain = mem.get_brain(r)
|
|
136
|
+
config = cfg.load(r)
|
|
137
|
+
except Exception:
|
|
138
|
+
brain, config = {}, {}
|
|
139
|
+
|
|
140
|
+
# Step 1-3: run side agents
|
|
141
|
+
findings, blocking = _run_side_agents(r, brain, config)
|
|
142
|
+
|
|
143
|
+
# --fix: repair in-lane findings once, then re-run from step 1
|
|
144
|
+
if fix and blocking:
|
|
145
|
+
lane = [f for f in blocking if (getattr(f, "type", "") or "") in _FIXABLE_TYPES]
|
|
146
|
+
if lane:
|
|
147
|
+
actions = _apply_domain_fixes(r, lane)
|
|
148
|
+
if actions:
|
|
149
|
+
for a in actions:
|
|
150
|
+
con.print(f"[dim]--fix: {a}[/dim]")
|
|
151
|
+
findings, blocking = _run_side_agents(r, brain, config)
|
|
152
|
+
|
|
153
|
+
# Step 3 extra: env file check (missing .env but .env.example exists)
|
|
154
|
+
try:
|
|
155
|
+
if (r / ".env.example").exists() and not (r / ".env").exists():
|
|
156
|
+
from patchi.core.agents.base import Severity, make_finding
|
|
157
|
+
|
|
158
|
+
findings.append(
|
|
159
|
+
make_finding(
|
|
160
|
+
severity=Severity.HIGH,
|
|
161
|
+
file=".env",
|
|
162
|
+
line_start=0,
|
|
163
|
+
title="Missing .env (have .env.example)",
|
|
164
|
+
description="Copy .env.example to .env and fill values before app can run",
|
|
165
|
+
finding_type="missing_env",
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
blocking.append(findings[-1])
|
|
169
|
+
except Exception as _exc:
|
|
170
|
+
logging.getLogger("patchi").debug('suppressed: %s', _exc)
|
|
171
|
+
|
|
172
|
+
if blocking:
|
|
173
|
+
# Escalate to Brain
|
|
174
|
+
msg = f"P-Check BLOCKED: {len(blocking)} blocking error(s) in {', '.join({f.file or 'unknown' for f in blocking})}"
|
|
175
|
+
con.print()
|
|
176
|
+
con.print(Panel(f"[red]{msg}[/red]\n" + "\n".join(f" • {f.file}:{f.line} {f.message[:100]}" for f in blocking[:5]), title="P-Check — BLOCKED", border_style="#FF4D6D"))
|
|
177
|
+
con.print(f"[dim]Escalating to Brain (orchestrator) — domain: {[f.type for f in blocking[:3]]}[/dim]")
|
|
178
|
+
_write_status(r, "BLOCKED", error=msg, domain=",".join({f.type for f in blocking}))
|
|
179
|
+
try:
|
|
180
|
+
from patchi.core import memory as mem
|
|
181
|
+
|
|
182
|
+
mem.save_scan_result("PCheck", {"status": "BLOCKED", "findings": [f.to_dict() for f in findings]}, r)
|
|
183
|
+
# also save as issue for brain
|
|
184
|
+
for f in blocking[:5]:
|
|
185
|
+
mem.save_issue({"type": f.type, "file": f.file, "line": f.line, "message": f.message, "source": "PCheck"}, r)
|
|
186
|
+
except Exception as _exc:
|
|
187
|
+
logging.getLogger("patchi").debug('suppressed: %s', _exc)
|
|
188
|
+
con.print("[yellow]P-Check: fix blocking errors (or run p check --fix if in domain) then re-run `p check` from step 1. Never READY_TO_SERVE while blocked.[/yellow]")
|
|
189
|
+
con.print()
|
|
190
|
+
if json_output:
|
|
191
|
+
con.print(json.dumps({"status": "BLOCKED", "blocking": [f.to_dict() for f in blocking]}, indent=2))
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Step 4: attempt to build and start app as local dev server
|
|
195
|
+
url: str | None = None
|
|
196
|
+
port: int | None = None
|
|
197
|
+
try:
|
|
198
|
+
from patchi.core.testing.app_launcher import ensure_running
|
|
199
|
+
|
|
200
|
+
url = ensure_running(r, config, {})
|
|
201
|
+
if url:
|
|
202
|
+
# parse port
|
|
203
|
+
try:
|
|
204
|
+
port = int(url.rsplit(":", 1)[-1].split("/")[0])
|
|
205
|
+
except Exception:
|
|
206
|
+
port = None
|
|
207
|
+
except Exception as exc: # noqa: BLE001
|
|
208
|
+
con.print(f"[red]P-Check start failed: {exc}[/red]")
|
|
209
|
+
_write_status(r, "BLOCKED", error=str(exc), domain="app_start")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if not url:
|
|
213
|
+
msg = "P-Check could not start app — no start command detected or port health check failed"
|
|
214
|
+
con.print()
|
|
215
|
+
con.print(Panel(f"[yellow]{msg}[/yellow]\n[dim]Check app_launcher logs at .patchi/launcher/app.log[/dim]", title="P-Check — BLOCKED (no URL)", border_style="#FF8C42"))
|
|
216
|
+
con.print()
|
|
217
|
+
_write_status(r, "BLOCKED", error=msg, domain="app_start")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Success
|
|
221
|
+
con.print()
|
|
222
|
+
con.print(Panel(f"[bold #4ADE80]READY_TO_SERVE[/bold #4ADE80]\nLocal dev server URL: [bold]{url}[/bold] port {port}\n[dim]P-Check complete — app is runnable, testable. No other agent may proceed until this was emitted.[/dim]", border_style="#4ADE80"))
|
|
223
|
+
con.print()
|
|
224
|
+
_write_status(r, "READY_TO_SERVE", url=url, port=port)
|
|
225
|
+
try:
|
|
226
|
+
from patchi.core import memory as mem
|
|
227
|
+
|
|
228
|
+
mem.save_scan_result("PCheck", {"status": "READY_TO_SERVE", "url": url, "port": port, "findings": [f.to_dict() for f in findings]}, r)
|
|
229
|
+
except Exception as _exc:
|
|
230
|
+
logging.getLogger("patchi").debug('suppressed: %s', _exc)
|
|
231
|
+
if json_output:
|
|
232
|
+
con.print(json.dumps({"status": "READY_TO_SERVE", "url": url, "port": port}, indent=2))
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p cleanup` — Remove stale .patchi/ artifacts.
|
|
3
|
+
|
|
4
|
+
Cleans up generated/temporary files while preserving essential state:
|
|
5
|
+
- JUnit XML reports from dev checks
|
|
6
|
+
- Flake databases (patchi_history.db, *.db-journal, etc.)
|
|
7
|
+
- Stale caches (AST, file_info, brain freshness, CVE, etc.)
|
|
8
|
+
- Old log files
|
|
9
|
+
- Ephemeral snapshots
|
|
10
|
+
- Browser test screenshots/evidence (optional)
|
|
11
|
+
- Profiling/stats files (.stats, .prof, .lprof)
|
|
12
|
+
|
|
13
|
+
Preserves:
|
|
14
|
+
- config.json (project configuration)
|
|
15
|
+
- memory/ (charter, layers, known issues, brain state)
|
|
16
|
+
- api_key
|
|
17
|
+
- queue.json
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
p cleanup # Preview what would be removed (dry-run)
|
|
21
|
+
p cleanup --apply # Actually delete stale files
|
|
22
|
+
p cleanup --all # Also remove evidence/screenshots
|
|
23
|
+
p cleanup --older-than 7d # Only remove files older than 7 days
|
|
24
|
+
p cleanup --dry-run --json # JSON output for CI integration
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from patchi.cli.console import con
|
|
34
|
+
from patchi.core.config import find_project_root
|
|
35
|
+
|
|
36
|
+
_log = logging.getLogger("patchi.cli.commands.cleanup_cmd")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Artifact Patterns ────────────────────────────────────────────────────────
|
|
40
|
+
# (glob_pattern, description, keep_in_safe_mode)
|
|
41
|
+
|
|
42
|
+
CLEANUP_TARGETS = [
|
|
43
|
+
# JUnit XML reports
|
|
44
|
+
("**/*junit*.xml", "JUnit test reports", False),
|
|
45
|
+
("**/*test-result*.xml", "pytest result files", False),
|
|
46
|
+
("**/dev-check-*.xml", "dev-check XML reports", False),
|
|
47
|
+
|
|
48
|
+
# Flake/test databases
|
|
49
|
+
("**/*.db", "database files", False),
|
|
50
|
+
("**/*.db-journal", "database journals", False),
|
|
51
|
+
("**/*.db-wal", "database WAL files", False),
|
|
52
|
+
("**/*.db-shm", "database SHM files", False),
|
|
53
|
+
|
|
54
|
+
# Stale caches
|
|
55
|
+
("ast_cache.json", "AST parse cache", False),
|
|
56
|
+
("file_info_cache.json", "file info cache", False),
|
|
57
|
+
("brain_freshness.json", "brain freshness cache", False),
|
|
58
|
+
("diff_activation_cache.json", "diff activation cache", False),
|
|
59
|
+
("threat_model.json", "threat model cache", False),
|
|
60
|
+
("queue.json", "queue state", False),
|
|
61
|
+
("cache/cve_cache.json", "CVE lookup cache", False),
|
|
62
|
+
|
|
63
|
+
# Logs
|
|
64
|
+
("logs/*.log", "log files", False),
|
|
65
|
+
("logs/*.jsonl", "structured logs", False),
|
|
66
|
+
|
|
67
|
+
# Snapshots
|
|
68
|
+
("snapshots/**/*", "ephemeral snapshots", False),
|
|
69
|
+
|
|
70
|
+
# Profiling
|
|
71
|
+
("*.stats", "profiling stats", False),
|
|
72
|
+
("*.prof", "profiling data", False),
|
|
73
|
+
("*.lprof", "line profiling data", False),
|
|
74
|
+
|
|
75
|
+
# Scratch files
|
|
76
|
+
("b.json", "scratch data", False),
|
|
77
|
+
("command_list.md", "generated command list", False),
|
|
78
|
+
|
|
79
|
+
# Evidence (only with --all)
|
|
80
|
+
("evidence/dast/**/*", "DAST screenshots/evidence", True),
|
|
81
|
+
("evidence/browser_tests/**/*", "browser test evidence", True),
|
|
82
|
+
("evidence/test/**/*", "test evidence", True),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_age(age_str: str) -> float:
|
|
87
|
+
"""Parse age string like '7d', '24h', '30m' into seconds."""
|
|
88
|
+
age_str = age_str.strip().lower()
|
|
89
|
+
if age_str.endswith("d"):
|
|
90
|
+
return float(age_str[:-1]) * 86400
|
|
91
|
+
elif age_str.endswith("h"):
|
|
92
|
+
return float(age_str[:-1]) * 3600
|
|
93
|
+
elif age_str.endswith("m"):
|
|
94
|
+
return float(age_str[:-1]) * 60
|
|
95
|
+
else:
|
|
96
|
+
# Assume days
|
|
97
|
+
return float(age_str) * 86400
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_file_older_than(file_path: Path, max_age_seconds: float) -> bool:
|
|
101
|
+
"""Check if a file is older than the specified age."""
|
|
102
|
+
try:
|
|
103
|
+
mtime = file_path.stat().st_mtime
|
|
104
|
+
age = time.time() - mtime
|
|
105
|
+
return age > max_age_seconds
|
|
106
|
+
except Exception:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cleanup(
|
|
111
|
+
apply: bool = False,
|
|
112
|
+
include_evidence: bool = False,
|
|
113
|
+
older_than: str | None = None,
|
|
114
|
+
json_output: bool = False,
|
|
115
|
+
root: Path | None = None,
|
|
116
|
+
):
|
|
117
|
+
"""Remove stale .patchi/ artifacts.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
apply: If True, actually delete files. If False, just show what would be removed.
|
|
121
|
+
include_evidence: If True, also remove evidence/screenshots.
|
|
122
|
+
older_than: Only remove files older than this (e.g. '7d', '24h', '30m').
|
|
123
|
+
json_output: If True, output as JSON for CI integration.
|
|
124
|
+
root: Override project root detection.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
r = root or find_project_root()
|
|
128
|
+
except Exception:
|
|
129
|
+
try:
|
|
130
|
+
from patchi.core.config import require_project_root
|
|
131
|
+
r = require_project_root()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
if json_output:
|
|
134
|
+
pass
|
|
135
|
+
else:
|
|
136
|
+
con.print(f"[red]{e}[/red]")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
patchi_dir = r / ".patchi"
|
|
140
|
+
if not patchi_dir.is_dir():
|
|
141
|
+
if json_output:
|
|
142
|
+
pass
|
|
143
|
+
else:
|
|
144
|
+
con.print(f"[yellow]No .patchi directory found at {r}[/yellow]")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Parse age filter
|
|
148
|
+
max_age_seconds = _parse_age(older_than) if older_than else None
|
|
149
|
+
|
|
150
|
+
if not json_output:
|
|
151
|
+
con.print()
|
|
152
|
+
con.print("[bold #C8621A]Patchi Cleanup[/bold #C8621A]")
|
|
153
|
+
con.print(f"[dim]Directory: {patchi_dir}[/dim]")
|
|
154
|
+
mode_label = "DRY RUN (use --apply to delete)" if not apply else "[red]DELETING FILES[/red]"
|
|
155
|
+
age_label = f" | Age filter: > {older_than}" if older_than else ""
|
|
156
|
+
con.print(f"[dim]{mode_label}{age_label}[/dim]")
|
|
157
|
+
con.print()
|
|
158
|
+
|
|
159
|
+
total_files = 0
|
|
160
|
+
total_size = 0
|
|
161
|
+
deleted_files = 0
|
|
162
|
+
deleted_size = 0
|
|
163
|
+
skipped_files = 0
|
|
164
|
+
filtered_out = 0
|
|
165
|
+
cleanup_data = []
|
|
166
|
+
|
|
167
|
+
for pattern, description, evidence_only in CLEANUP_TARGETS:
|
|
168
|
+
if evidence_only and not include_evidence:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
matches = list(patchi_dir.glob(pattern))
|
|
172
|
+
|
|
173
|
+
if not matches:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Filter to actual files (not directories)
|
|
177
|
+
files = [f for f in matches if f.is_file()]
|
|
178
|
+
|
|
179
|
+
if not files:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Apply age filter
|
|
183
|
+
if max_age_seconds is not None:
|
|
184
|
+
old_files = [f for f in files if _is_file_older_than(f, max_age_seconds)]
|
|
185
|
+
filtered_out += len(files) - len(old_files)
|
|
186
|
+
files = old_files
|
|
187
|
+
|
|
188
|
+
if not files:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Calculate size
|
|
192
|
+
file_size = sum(f.stat().st_size for f in files if f.exists())
|
|
193
|
+
total_files += len(files)
|
|
194
|
+
total_size += file_size
|
|
195
|
+
|
|
196
|
+
size_str = _format_size(file_size)
|
|
197
|
+
count_str = f"{len(files)} file{'s' if len(files) != 1 else ''}"
|
|
198
|
+
|
|
199
|
+
if apply:
|
|
200
|
+
deleted_count = 0
|
|
201
|
+
for f in files:
|
|
202
|
+
try:
|
|
203
|
+
file_size_actual = f.stat().st_size if f.exists() else 0
|
|
204
|
+
f.unlink()
|
|
205
|
+
deleted_count += 1
|
|
206
|
+
deleted_files += 1
|
|
207
|
+
deleted_size += file_size_actual
|
|
208
|
+
except Exception as e:
|
|
209
|
+
skipped_files += 1
|
|
210
|
+
if not json_output:
|
|
211
|
+
con.print(f" [yellow]⚠ Could not delete {f.name}: {e}[/yellow]")
|
|
212
|
+
|
|
213
|
+
if deleted_count > 0 and not json_output:
|
|
214
|
+
con.print(f" [red]✗[/red] {description}: {count_str} ({size_str})")
|
|
215
|
+
else:
|
|
216
|
+
if not json_output:
|
|
217
|
+
con.print(f" [yellow]?[/yellow] {description}: {count_str} ({size_str})")
|
|
218
|
+
|
|
219
|
+
# Collect data for JSON output
|
|
220
|
+
cleanup_data.append({
|
|
221
|
+
"description": description,
|
|
222
|
+
"pattern": pattern,
|
|
223
|
+
"count": len(files),
|
|
224
|
+
"size_bytes": file_size,
|
|
225
|
+
"files": [str(f.relative_to(patchi_dir)) for f in files[:20]], # Limit to 20
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
# Also clean empty directories
|
|
229
|
+
empty_dirs = []
|
|
230
|
+
for d in sorted(patchi_dir.rglob("*"), reverse=True):
|
|
231
|
+
if d.is_dir() and not any(d.iterdir()):
|
|
232
|
+
empty_dirs.append(d)
|
|
233
|
+
|
|
234
|
+
if apply and empty_dirs:
|
|
235
|
+
for d in empty_dirs:
|
|
236
|
+
try:
|
|
237
|
+
d.rmdir()
|
|
238
|
+
if not json_output:
|
|
239
|
+
con.print(f" [red]✗[/red] Empty dir: {d.relative_to(patchi_dir)}")
|
|
240
|
+
except Exception as _exc:
|
|
241
|
+
_log.warning('cleanup failed: %s', _exc)
|
|
242
|
+
|
|
243
|
+
# JSON output for CI
|
|
244
|
+
if json_output:
|
|
245
|
+
{
|
|
246
|
+
"dry_run": not apply,
|
|
247
|
+
"older_than": older_than,
|
|
248
|
+
"total_files": total_files,
|
|
249
|
+
"total_size_bytes": total_size,
|
|
250
|
+
"deleted_files": deleted_files,
|
|
251
|
+
"deleted_size_bytes": deleted_size,
|
|
252
|
+
"skipped_files": skipped_files,
|
|
253
|
+
"filtered_out": filtered_out,
|
|
254
|
+
"categories": cleanup_data,
|
|
255
|
+
"preserved": _get_preserved(patchi_dir),
|
|
256
|
+
}
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Summary
|
|
260
|
+
con.print()
|
|
261
|
+
if apply:
|
|
262
|
+
if deleted_files > 0:
|
|
263
|
+
con.print(f"[green]✓ Cleaned {deleted_files} files ({_format_size(deleted_size)} freed)[/green]")
|
|
264
|
+
else:
|
|
265
|
+
con.print("[dim]No stale files to clean.[/dim]")
|
|
266
|
+
if skipped_files:
|
|
267
|
+
con.print(f"[yellow]⚠ {skipped_files} files could not be deleted[/yellow]")
|
|
268
|
+
else:
|
|
269
|
+
con.print(f"[yellow]Would remove {total_files} files ({_format_size(total_size)})[/yellow]")
|
|
270
|
+
if filtered_out > 0:
|
|
271
|
+
con.print(f"[dim]Filtered out {filtered_out} files (newer than {older_than})[/dim]")
|
|
272
|
+
con.print("[dim]Run with --apply to actually delete files.[/dim]")
|
|
273
|
+
|
|
274
|
+
# Show what's preserved
|
|
275
|
+
con.print()
|
|
276
|
+
con.print("[dim]Preserved:[/dim]")
|
|
277
|
+
preserved = _get_preserved(patchi_dir)
|
|
278
|
+
if preserved:
|
|
279
|
+
con.print(f"[dim] {', '.join(preserved)}[/dim]")
|
|
280
|
+
else:
|
|
281
|
+
con.print("[dim] (nothing to preserve)[/dim]")
|
|
282
|
+
con.print()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _get_preserved(patchi_dir: Path) -> list[str]:
|
|
286
|
+
"""Get list of preserved items."""
|
|
287
|
+
preserved = []
|
|
288
|
+
if (patchi_dir / "config.json").exists():
|
|
289
|
+
preserved.append("config.json")
|
|
290
|
+
if (patchi_dir / "memory").is_dir():
|
|
291
|
+
preserved.append("memory/")
|
|
292
|
+
if (patchi_dir / "api_key").exists():
|
|
293
|
+
preserved.append("api_key")
|
|
294
|
+
return preserved
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_patchi_size(root: Path | None = None) -> dict:
|
|
298
|
+
"""Get the total size of .patchi/ directory for doctor warnings."""
|
|
299
|
+
try:
|
|
300
|
+
r = root or find_project_root()
|
|
301
|
+
except Exception:
|
|
302
|
+
return {"total_bytes": 0, "total_files": 0}
|
|
303
|
+
|
|
304
|
+
patchi_dir = r / ".patchi"
|
|
305
|
+
if not patchi_dir.is_dir():
|
|
306
|
+
return {"total_bytes": 0, "total_files": 0}
|
|
307
|
+
|
|
308
|
+
total_bytes = 0
|
|
309
|
+
total_files = 0
|
|
310
|
+
for f in patchi_dir.rglob("*"):
|
|
311
|
+
if f.is_file():
|
|
312
|
+
try:
|
|
313
|
+
total_bytes += f.stat().st_size
|
|
314
|
+
total_files += 1
|
|
315
|
+
except Exception as _exc:
|
|
316
|
+
_log.warning('get_patchi_size failed: %s', _exc)
|
|
317
|
+
|
|
318
|
+
return {"total_bytes": total_bytes, "total_files": total_files}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _format_size(size: int) -> str:
|
|
322
|
+
"""Format bytes as human-readable string."""
|
|
323
|
+
if size < 1024:
|
|
324
|
+
return f"{size} B"
|
|
325
|
+
elif size < 1024 * 1024:
|
|
326
|
+
return f"{size / 1024:.1f} KB"
|
|
327
|
+
else:
|
|
328
|
+
return f"{size / (1024 * 1024):.1f} MB"
|