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
patchi/cli/style.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ §2 CLI Theme System — semantic helpers.
3
+
4
+ Thin wrappers so migrating a raw `print(...)` to something themed is a
5
+ one-liner: `print(f"Error: {msg}")` -> `err(msg)`. Every helper uses semantic
6
+ tokens (never raw color) and pulls its symbol from the active palette via
7
+ `console.symbol()`, so `mono` gets ASCII (`[OK]`/`[!]`/`[X]`) automatically
8
+ instead of Unicode glyphs a dumb terminal or encoding can't render.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from patchi.cli.console import con, symbol
14
+
15
+
16
+ def ok(msg: str) -> None:
17
+ con.print(f"[success]{symbol('ok')}[/] {msg}")
18
+
19
+
20
+ def warn(msg: str) -> None:
21
+ con.print(f"[warn]{symbol('warn')}[/] {msg}")
22
+
23
+
24
+ def err(msg: str) -> None:
25
+ con.print(f"[error]{symbol('err')}[/] {msg}")
26
+
27
+
28
+ def info(msg: str) -> None:
29
+ con.print(f"[info]{msg}[/]")
30
+
31
+
32
+ def muted(msg: str) -> None:
33
+ con.print(f"[muted]{msg}[/]")
34
+
35
+
36
+ def heading(msg: str) -> None:
37
+ con.print(f"\n[heading]{msg}[/]")
38
+
39
+
40
+ def accent(msg: str) -> str:
41
+ """Returns markup (not printed) for embedding in a larger f-string."""
42
+ return f"[accent]{msg}[/]"
43
+
44
+
45
+ def path(p: str) -> str:
46
+ """Returns markup (not printed) for embedding a file path in a larger line."""
47
+ return f"[path]{p}[/]"
48
+
49
+
50
+ def grade(score, letter: str) -> str:
51
+ """Returns markup for a health-grade display, e.g. grade(87, 'B') -> '87 (B)' styled."""
52
+ key = f"grade.{letter.lower()}"
53
+ return f"[{key}]{score} ({letter})[/]"
54
+
55
+
56
+ def severity(label: str) -> str:
57
+ """Returns markup for a severity label: severity('critical') -> styled 'CRITICAL'."""
58
+ key = f"sev.{label.lower()}"
59
+ return f"[{key}]{label.upper()}[/]"
60
+
61
+
62
+ def kv(k: str, v) -> None:
63
+ con.print(f" [muted]{k}[/] {v}")
64
+
65
+
66
+ def count(n) -> str:
67
+ """Returns markup for a count/number that should stand out, e.g. in a summary line."""
68
+ return f"[count]{n}[/]"
patchi/cli/themes.py ADDED
@@ -0,0 +1,187 @@
1
+ """
2
+ §2 CLI Theme System — palette definitions.
3
+
4
+ Each palette maps the SAME semantic token set to different Rich styles, plus a
5
+ matching symbol set. Symbols are separate from the Rich Theme (which only maps
6
+ str -> Style, not str -> arbitrary text) so `mono` can swap Unicode glyphs for
7
+ plain ASCII on terminals/encodings that can't render them, and so status is
8
+ never conveyed by color alone (colorblind-safe: shape/symbol/label carries the
9
+ meaning, color is a bonus, not the only signal).
10
+
11
+ Token set (used identically across every palette — callers use meaning, never
12
+ raw color):
13
+ success, warn, error, info, muted -- status
14
+ heading, accent, path, count -- structure
15
+ grade.a .. grade.f -- health grades
16
+ sev.critical, sev.high, sev.medium, sev.low -- finding severity
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from rich.theme import Theme
22
+
23
+ _TOKEN_NAMES = (
24
+ "success",
25
+ "warn",
26
+ "error",
27
+ "info",
28
+ "muted",
29
+ "heading",
30
+ "accent",
31
+ "path",
32
+ "count",
33
+ "grade.a",
34
+ "grade.b",
35
+ "grade.c",
36
+ "grade.d",
37
+ "grade.f",
38
+ "sev.critical",
39
+ "sev.high",
40
+ "sev.medium",
41
+ "sev.low",
42
+ )
43
+
44
+
45
+ def _make(styles: dict[str, str], symbols: dict[str, str]) -> dict:
46
+ missing = set(_TOKEN_NAMES) - set(styles)
47
+ assert not missing, f"palette missing tokens: {missing}"
48
+ return {"theme": Theme(styles), "symbols": symbols}
49
+
50
+
51
+ PALETTES: dict[str, dict] = {
52
+ # Matches config.py's existing default ("theme": "dark") -- not renamed to
53
+ # "default" to avoid yet another naming mismatch between config values and
54
+ # palette keys.
55
+ "dark": _make(
56
+ {
57
+ "success": "bold green",
58
+ "warn": "bold yellow",
59
+ "error": "bold red",
60
+ "info": "cyan",
61
+ "muted": "dim",
62
+ "heading": "bold white",
63
+ "accent": "bold magenta",
64
+ "path": "underline cyan",
65
+ "count": "bold",
66
+ "grade.a": "bold green",
67
+ "grade.b": "green",
68
+ "grade.c": "yellow",
69
+ "grade.d": "bold yellow",
70
+ "grade.f": "bold red",
71
+ "sev.critical": "bold white on red",
72
+ "sev.high": "bold red",
73
+ "sev.medium": "yellow",
74
+ "sev.low": "dim",
75
+ },
76
+ {"ok": "✓", "warn": "!", "err": "✗"},
77
+ ),
78
+ "light": _make(
79
+ {
80
+ "success": "bold green3",
81
+ "warn": "bold dark_orange",
82
+ "error": "bold red3",
83
+ "info": "blue3",
84
+ "muted": "grey50",
85
+ "heading": "bold black",
86
+ "accent": "bold purple3",
87
+ "path": "underline blue3",
88
+ "count": "bold",
89
+ "grade.a": "bold green3",
90
+ "grade.b": "green3",
91
+ "grade.c": "dark_orange",
92
+ "grade.d": "bold dark_orange",
93
+ "grade.f": "bold red3",
94
+ "sev.critical": "bold white on red3",
95
+ "sev.high": "bold red3",
96
+ "sev.medium": "dark_orange",
97
+ "sev.low": "grey50",
98
+ },
99
+ {"ok": "✓", "warn": "!", "err": "✗"},
100
+ ),
101
+ # Accessibility / NO_COLOR / non-TTY / --json. No color at all -- status is
102
+ # conveyed entirely by ASCII labels, since a dumb terminal or a screen
103
+ # reader gets nothing from color and may not even render Unicode glyphs.
104
+ "mono": _make(
105
+ {
106
+ "success": "none",
107
+ "warn": "none",
108
+ "error": "none",
109
+ "info": "none",
110
+ "muted": "none",
111
+ "heading": "bold",
112
+ "accent": "bold",
113
+ "path": "underline",
114
+ "count": "bold",
115
+ "grade.a": "none",
116
+ "grade.b": "none",
117
+ "grade.c": "none",
118
+ "grade.d": "none",
119
+ "grade.f": "none",
120
+ "sev.critical": "bold",
121
+ "sev.high": "bold",
122
+ "sev.medium": "none",
123
+ "sev.low": "none",
124
+ },
125
+ {"ok": "[OK]", "warn": "[!]", "err": "[X]"},
126
+ ),
127
+ "highcontrast": _make(
128
+ {
129
+ "success": "bold bright_green",
130
+ "warn": "bold bright_yellow",
131
+ "error": "bold bright_red",
132
+ "info": "bold bright_cyan",
133
+ "muted": "white",
134
+ "heading": "bold bright_white",
135
+ "accent": "bold bright_magenta",
136
+ "path": "underline bright_cyan",
137
+ "count": "bold bright_white",
138
+ "grade.a": "bold bright_green",
139
+ "grade.b": "bright_green",
140
+ "grade.c": "bold bright_yellow",
141
+ "grade.d": "bold bright_yellow",
142
+ "grade.f": "bold bright_red",
143
+ "sev.critical": "bold bright_white on bright_red",
144
+ "sev.high": "bold bright_red",
145
+ "sev.medium": "bold bright_yellow",
146
+ "sev.low": "white",
147
+ },
148
+ {"ok": "✓", "warn": "!", "err": "✗"},
149
+ ),
150
+ }
151
+
152
+ DEFAULT_PALETTE = "dark"
153
+
154
+
155
+ def resolve_palette_name(
156
+ explicit: str | None,
157
+ config_theme: str | None,
158
+ no_color_env: bool,
159
+ is_tty: bool,
160
+ json_mode: bool,
161
+ ) -> str:
162
+ """
163
+ Selection precedence, highest first:
164
+ 1. --json -> force mono, UNCONDITIONALLY (deliberately ranked above even an
165
+ explicit --theme flag). This deviates from this feature's original
166
+ design doc, which put --theme above --json -- but --json's whole
167
+ purpose is a machine-parseable contract, and ANSI codes leaking into
168
+ JSON output from a theme flag (e.g. a shell alias someone forgot about)
169
+ would silently break downstream parsers. That's a correctness bug, not
170
+ a style preference, so it can't be overridden.
171
+ 2. --theme flag (explicit)
172
+ 3. NO_COLOR env var -> force mono (spec: https://no-color.org)
173
+ 4. non-TTY -> force mono (piped output, no ANSI to confuse a non-terminal reader)
174
+ 5. settings.json's theme value
175
+ 6. DEFAULT_PALETTE
176
+ """
177
+ if json_mode:
178
+ return "mono"
179
+ if explicit and explicit in PALETTES:
180
+ return explicit
181
+ if no_color_env:
182
+ return "mono"
183
+ if not is_tty:
184
+ return "mono"
185
+ if config_theme and config_theme in PALETTES:
186
+ return config_theme
187
+ return DEFAULT_PALETTE
patchi/cli/ux.py ADDED
@@ -0,0 +1,444 @@
1
+ """
2
+ CLI UX — Shared UI components for consistent user experience.
3
+
4
+ Provides spinners, progress bars, status indicators, and formatting helpers
5
+ for a polished CLI experience.
6
+
7
+ Usage:
8
+ from patchi.cli.ux import (
9
+ spinner, progress_bar, status_icon, format_header,
10
+ format_error, format_success, format_warning, format_step,
11
+ live_status, CountdownTimer,
12
+ )
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from contextlib import contextmanager
19
+ from typing import Any, Callable, Generator
20
+
21
+ from rich.console import Group
22
+ from rich.live import Live
23
+ from rich.panel import Panel
24
+ from rich.progress import (
25
+ BarColumn,
26
+ MofNCompleteColumn,
27
+ Progress,
28
+ SpinnerColumn,
29
+ TextColumn,
30
+ TimeElapsedColumn,
31
+ )
32
+ from rich.table import Table
33
+ from rich.text import Text
34
+
35
+ from patchi.cli.console import con
36
+
37
+
38
+ # ── Status Icons ──────────────────────────────────────────────────────────────
39
+
40
+ def status_icon(status: str) -> str:
41
+ """Get a status icon for the given status."""
42
+ icons = {
43
+ "done": "✓",
44
+ "passed": "✓",
45
+ "success": "✓",
46
+ "running": "⟳",
47
+ "pending": "–",
48
+ "skipped": "–",
49
+ "failed": "✗",
50
+ "error": "✗",
51
+ "warning": "⚠",
52
+ }
53
+ return icons.get(status.lower(), "·")
54
+
55
+
56
+ def status_style(status: str) -> str:
57
+ """Get Rich style for status."""
58
+ styles = {
59
+ "done": "green",
60
+ "passed": "green",
61
+ "success": "green",
62
+ "running": "cyan",
63
+ "pending": "dim",
64
+ "skipped": "yellow",
65
+ "failed": "red",
66
+ "error": "red",
67
+ "warning": "yellow",
68
+ }
69
+ return styles.get(status.lower(), "white")
70
+
71
+
72
+ def colored_status(status: str) -> str:
73
+ """Get colored status string."""
74
+ icon = status_icon(status)
75
+ style = status_style(status)
76
+ return f"[{style}]{icon} {status}[/{style}]"
77
+
78
+
79
+ # ── Formatting Helpers ────────────────────────────────────────────────────────
80
+
81
+ def format_header(title: str, subtitle: str | None = None) -> Panel:
82
+ """Format a section header."""
83
+ if subtitle:
84
+ return Panel(
85
+ f"[bold]{title}[/bold]\n[dim]{subtitle}[/dim]",
86
+ border_style="blue",
87
+ padding=(0, 1),
88
+ )
89
+ return Panel(f"[bold]{title}[/bold]", border_style="blue", padding=(0, 1))
90
+
91
+
92
+ def format_step(step: int, total: int, description: str) -> Text:
93
+ """Format a step indicator."""
94
+ return Text(f" [{step}/{total}] {description}", style="cyan")
95
+
96
+
97
+ def format_success(message: str) -> Panel:
98
+ """Format a success message."""
99
+ return Panel(f"[green]✓ {message}[/green]", border_style="green", padding=(0, 1))
100
+
101
+
102
+ def format_error(message: str, details: str | None = None) -> Panel:
103
+ """Format an error message."""
104
+ content = f"[red]✗ {message}[/red]"
105
+ if details:
106
+ content += f"\n\n[dim]{details}[/dim]"
107
+ return Panel(content, border_style="red", padding=(0, 1))
108
+
109
+
110
+ def format_warning(message: str) -> Panel:
111
+ """Format a warning message."""
112
+ return Panel(f"[yellow]⚠ {message}[/yellow]", border_style="yellow", padding=(0, 1))
113
+
114
+
115
+ def format_info(message: str) -> Panel:
116
+ """Format an info message."""
117
+ return Panel(f"[blue]ℹ {message}[/blue]", border_style="blue", padding=(0, 1))
118
+
119
+
120
+ # ── Progress Bar ──────────────────────────────────────────────────────────────
121
+
122
+ def create_progress() -> Progress:
123
+ """Create a standard progress bar."""
124
+ return Progress(
125
+ SpinnerColumn(),
126
+ TextColumn("[progress.description]{task.description}"),
127
+ BarColumn(),
128
+ MofNCompleteColumn(),
129
+ TimeElapsedColumn(),
130
+ con=con,
131
+ )
132
+
133
+
134
+ def progress_bar(
135
+ items: list[Any],
136
+ description: str = "Processing",
137
+ process_fn: Callable[[Any], Any] | None = None,
138
+ ) -> list[Any]:
139
+ """Run items through a progress bar."""
140
+ results = []
141
+ with create_progress() as progress:
142
+ task = progress.add_task(description, total=len(items))
143
+ for item in items:
144
+ if process_fn:
145
+ result = process_fn(item)
146
+ results.append(result)
147
+ else:
148
+ results.append(item)
149
+ progress.advance(task)
150
+ return results
151
+
152
+
153
+ # ── Spinner Context Manager ───────────────────────────────────────────────────
154
+
155
+ @contextmanager
156
+ def spinner(
157
+ message: str,
158
+ success_message: str | None = None,
159
+ error_message: str | None = None,
160
+ ) -> Generator[Callable[[str], None], None, None]:
161
+ """Context manager that shows a spinner while working.
162
+
163
+ Usage:
164
+ with spinner("Scanning...", "Done!", "Failed!") as update:
165
+ # do work
166
+ update("Processing files...")
167
+ # more work
168
+ """
169
+ from rich.status import Status
170
+
171
+ status = Status(message, console=con, spinner="dots")
172
+ update_fn = lambda msg: status.update(msg)
173
+
174
+ try:
175
+ status.start()
176
+ yield update_fn
177
+ status.stop()
178
+ if success_message:
179
+ con.print(f"[green]✓ {success_message}[/green]")
180
+ except Exception as e:
181
+ status.stop()
182
+ if error_message:
183
+ con.print(f"[red]✗ {error_message}: {e}[/red]")
184
+ raise
185
+
186
+
187
+ # ── Live Status Display ───────────────────────────────────────────────────────
188
+
189
+ class LiveStatus:
190
+ """Live status display for multi-step operations."""
191
+
192
+ def __init__(self, title: str):
193
+ self.title = title
194
+ self.steps: list[dict] = []
195
+ self.current_step = 0
196
+ self._live: Live | None = None
197
+
198
+ def add_step(self, name: str, description: str) -> int:
199
+ """Add a step and return its index."""
200
+ self.steps.append({
201
+ "name": name,
202
+ "description": description,
203
+ "status": "pending",
204
+ "result": None,
205
+ })
206
+ return len(self.steps) - 1
207
+
208
+ def start_step(self, index: int) -> None:
209
+ """Mark a step as running."""
210
+ if 0 <= index < len(self.steps):
211
+ self.steps[index]["status"] = "running"
212
+ self.current_step = index
213
+ self._render()
214
+
215
+ def complete_step(self, index: int, status: str = "done", result: str | None = None) -> None:
216
+ """Mark a step as complete."""
217
+ if 0 <= index < len(self.steps):
218
+ self.steps[index]["status"] = status
219
+ self.steps[index]["result"] = result
220
+ self._render()
221
+
222
+ def _render(self) -> None:
223
+ """Render the current state."""
224
+ table = Table(show_header=False, box=None, padding=(0, 1))
225
+ table.add_column("Status", width=3)
226
+ table.add_column("Step", ratio=3)
227
+ table.add_column("Result", ratio=2)
228
+
229
+ for step in self.steps:
230
+ icon = status_icon(step["status"])
231
+ style = status_style(step["status"])
232
+ result = step["result"] or ""
233
+ table.add_row(
234
+ f"[{style}]{icon}[/{style}]",
235
+ step["description"],
236
+ f"[dim]{result}[/dim]" if result else "",
237
+ )
238
+
239
+ if self._live:
240
+ self._live.update(table)
241
+
242
+ def __enter__(self) -> "LiveStatus":
243
+ self._live = Live(table=None, console=con, refresh_per_second=4)
244
+ self._live.start()
245
+ return self
246
+
247
+ def __exit__(self, *args) -> None:
248
+ if self._live:
249
+ self._live.stop()
250
+
251
+
252
+ # ── Confirmation Prompt ────────────────────────────────────────────────────────
253
+
254
+ def confirm(message: str, default: bool = False) -> bool:
255
+ """Prompt for confirmation with rich styling.
256
+
257
+ Args:
258
+ message: The confirmation message
259
+ default: Default answer if user just presses Enter
260
+
261
+ Returns:
262
+ True if confirmed, False otherwise
263
+ """
264
+ suffix = "[Y/n]" if default else "[y/N]"
265
+ while True:
266
+ try:
267
+ choice = input(f"{message} {suffix}: ").strip().lower()
268
+ if not choice:
269
+ return default
270
+ if choice in ("y", "yes"):
271
+ return True
272
+ if choice in ("n", "no"):
273
+ return False
274
+ con.print("[dim]Please enter y or n[/dim]")
275
+ except (EOFError, KeyboardInterrupt):
276
+ return default
277
+
278
+
279
+ def select(
280
+ message: str,
281
+ options: list[str],
282
+ default: int = 0,
283
+ ) -> int:
284
+ """Prompt user to select from a list.
285
+
286
+ Args:
287
+ message: The prompt message
288
+ options: List of options to choose from
289
+ default: Default selection index
290
+
291
+ Returns:
292
+ Selected index
293
+ """
294
+ con.print(f"[bold]{message}[/bold]")
295
+ con.print()
296
+
297
+ for i, option in enumerate(options):
298
+ marker = "→" if i == default else " "
299
+ style = "cyan" if i == default else ""
300
+ con.print(f" {marker} [{style}]{i + 1}[/{style}]. {option}")
301
+
302
+ con.print()
303
+
304
+ while True:
305
+ try:
306
+ choice = input(f"Select (1-{len(options)}) [{default + 1}]: ").strip()
307
+ if not choice:
308
+ return default
309
+ idx = int(choice) - 1
310
+ if 0 <= idx < len(options):
311
+ return idx
312
+ con.print(f"[dim]Please enter a number between 1 and {len(options)}[/dim]")
313
+ except (ValueError, EOFError, KeyboardInterrupt):
314
+ return default
315
+
316
+
317
+ # ── Summary Panel ──────────────────────────────────────────────────────────────
318
+
319
+ def summary_panel(
320
+ title: str,
321
+ items: dict[str, bool],
322
+ elapsed: float | None = None,
323
+ ) -> Panel:
324
+ """Create a summary panel showing pass/fail status.
325
+
326
+ Args:
327
+ title: Panel title
328
+ items: Dict of item_name -> passed (bool)
329
+ elapsed: Optional elapsed time in seconds
330
+ """
331
+ lines = []
332
+ passed = sum(1 for v in items.values() if v)
333
+ total = len(items)
334
+
335
+ for name, success in items.items():
336
+ icon = "✓" if success else "✗"
337
+ style = "green" if success else "red"
338
+ lines.append(f"[{style}]{icon} {name}[/{style}]")
339
+
340
+ content = "\n".join(lines)
341
+
342
+ if elapsed is not None:
343
+ content += f"\n\n[dim]Completed in {elapsed:.1f}s[/dim]"
344
+
345
+ border = "green" if passed == total else "yellow" if passed > 0 else "red"
346
+
347
+ return Panel(
348
+ f"[bold]{title}[/bold]\n\n{content}\n\n"
349
+ f"[dim]{passed}/{total} passed[/dim]",
350
+ border_style=border,
351
+ padding=(0, 1),
352
+ )
353
+
354
+
355
+ # ── Status Style Helper ────────────────────────────────────────────────────────
356
+
357
+ def status_style(status: str) -> str:
358
+ """Get Rich style for status."""
359
+ styles = {
360
+ "done": "green",
361
+ "passed": "green",
362
+ "success": "green",
363
+ "running": "cyan",
364
+ "pending": "dim",
365
+ "skipped": "yellow",
366
+ "failed": "red",
367
+ "error": "red",
368
+ "warning": "yellow",
369
+ }
370
+ return styles.get(status.lower(), "white")
371
+
372
+
373
+ # ── Result Table ──────────────────────────────────────────────────────────────
374
+
375
+ def result_table(
376
+ title: str,
377
+ rows: list[dict],
378
+ columns: list[str] | None = None,
379
+ ) -> Table:
380
+ """Create a formatted result table."""
381
+ if columns is None:
382
+ columns = ["Check", "Status", "Findings", "Details"]
383
+
384
+ table = Table(title=title, show_header=True, header_style="bold")
385
+ for col in columns:
386
+ table.add_column(col)
387
+
388
+ for row in rows:
389
+ values = []
390
+ for col in columns:
391
+ key = col.lower()
392
+ if key in row:
393
+ val = row[key]
394
+ if key == "status":
395
+ values.append(colored_status(str(val)))
396
+ elif key == "findings":
397
+ count = int(val) if val else 0
398
+ if count > 0:
399
+ values.append(f"[red]{count}[/red]")
400
+ else:
401
+ values.append("[green]0[/green]")
402
+ else:
403
+ values.append(str(val))
404
+ else:
405
+ values.append("")
406
+ table.add_row(*values)
407
+
408
+ return table
409
+
410
+
411
+ # ── Countdown Timer ───────────────────────────────────────────────────────────
412
+
413
+ class CountdownTimer:
414
+ """Visual countdown timer for timed operations."""
415
+
416
+ def __init__(self, seconds: int, description: str = "Time remaining"):
417
+ self.seconds = seconds
418
+ self.description = description
419
+ self._start = 0.0
420
+
421
+ def __enter__(self) -> "CountdownTimer":
422
+ self._start = time.monotonic()
423
+ return self
424
+
425
+ def __exit__(self, *args) -> None:
426
+ pass
427
+
428
+ @property
429
+ def elapsed(self) -> float:
430
+ return time.monotonic() - self._start
431
+
432
+ @property
433
+ def remaining(self) -> float:
434
+ return max(0, self.seconds - self.elapsed)
435
+
436
+ @property
437
+ def is_complete(self) -> bool:
438
+ return self.elapsed >= self.seconds
439
+
440
+ def format_remaining(self) -> str:
441
+ """Format remaining time as MM:SS."""
442
+ r = int(self.remaining)
443
+ mins, secs = divmod(r, 60)
444
+ return f"{mins:02d}:{secs:02d}"
File without changes
File without changes