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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. 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
+