praisonai-code 0.0.1__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 (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,848 @@
1
+ """
2
+ PraisonAI Batch CLI Command.
3
+
4
+ Run all Python files with PraisonAI imports in the current directory.
5
+ Designed for quick debugging - running multiple local scripts at once.
6
+
7
+ Usage:
8
+ praisonai batch # Run all PraisonAI scripts in current dir
9
+ praisonai batch --sub # Include subdirectories
10
+ praisonai batch --sub --depth 2 # Limit recursion depth
11
+ praisonai batch --parallel # Run in parallel with async reporting
12
+ praisonai batch list # List discovered files
13
+ praisonai batch stats # Show statistics
14
+ praisonai batch report # View latest report
15
+ """
16
+
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ import typer
22
+
23
+ app = typer.Typer(
24
+ help="Run all PraisonAI scripts in current folder for quick debugging",
25
+ no_args_is_help=False,
26
+ )
27
+
28
+
29
+ def _get_default_report_dir() -> Path:
30
+ """Get default report directory with timestamp."""
31
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
32
+ return Path.home() / "Downloads" / "reports" / "batch" / timestamp
33
+
34
+
35
+ def _get_latest_report_dir() -> Optional[Path]:
36
+ """Get the latest report directory."""
37
+ base_dir = Path.home() / "Downloads" / "reports" / "batch"
38
+ if not base_dir.exists():
39
+ return None
40
+
41
+ # Find most recent timestamp directory
42
+ dirs = [d for d in base_dir.iterdir() if d.is_dir()]
43
+ if not dirs:
44
+ return None
45
+
46
+ return max(dirs, key=lambda d: d.name)
47
+
48
+
49
+ @app.callback(invoke_without_command=True)
50
+ def batch_run(
51
+ ctx: typer.Context,
52
+ path: Optional[Path] = typer.Option(
53
+ None,
54
+ "--path", "-p",
55
+ help="Path to search (default: current directory)",
56
+ ),
57
+ recursive: bool = typer.Option(
58
+ False,
59
+ "--sub", "--recursive", "-r",
60
+ help="Include subdirectories",
61
+ ),
62
+ depth: Optional[int] = typer.Option(
63
+ None,
64
+ "--depth", "-d",
65
+ help="Maximum recursion depth (only with --sub)",
66
+ ),
67
+ include_tests: bool = typer.Option(
68
+ False,
69
+ "--include-tests",
70
+ help="Include test files (test_*.py, *_test.py)",
71
+ ),
72
+ timeout: int = typer.Option(
73
+ 60,
74
+ "--timeout", "-t",
75
+ help="Per-script timeout in seconds",
76
+ ),
77
+ max_items: Optional[int] = typer.Option(
78
+ None,
79
+ "--max-items",
80
+ help="Maximum scripts to run",
81
+ ),
82
+ fail_fast: bool = typer.Option(
83
+ False,
84
+ "--fail-fast", "-x",
85
+ help="Stop on first failure",
86
+ ),
87
+ parallel: bool = typer.Option(
88
+ True,
89
+ "--parallel/--sequential",
90
+ help="Run in parallel (default: parallel)",
91
+ ),
92
+ max_workers: int = typer.Option(
93
+ 4,
94
+ "--workers", "-w",
95
+ help="Max parallel workers (only with --parallel)",
96
+ ),
97
+ no_stream: bool = typer.Option(
98
+ False,
99
+ "--no-stream",
100
+ help="Don't stream output to terminal",
101
+ ),
102
+ report_dir: Optional[Path] = typer.Option(
103
+ None,
104
+ "--report-dir",
105
+ help="Directory for reports (default: ~/Downloads/reports/batch/<timestamp>)",
106
+ ),
107
+ no_report: bool = typer.Option(
108
+ False,
109
+ "--no-report",
110
+ help="Skip report generation",
111
+ ),
112
+ quiet: bool = typer.Option(
113
+ False,
114
+ "--quiet", "-q",
115
+ help="Minimal output",
116
+ ),
117
+ server: bool = typer.Option(
118
+ False,
119
+ "--server",
120
+ help="Run only server scripts (uvicorn, Flask, streamlit, etc.) with 10s timeout",
121
+ ),
122
+ filter_type: Optional[str] = typer.Option(
123
+ None,
124
+ "--filter", "-f",
125
+ help="Filter by type: 'agent', 'agents', or 'workflow'",
126
+ ),
127
+ ci: bool = typer.Option(
128
+ False,
129
+ "--ci",
130
+ help="CI-friendly output (no colors, proper exit codes)",
131
+ ),
132
+ ):
133
+ """
134
+ Run all PraisonAI scripts in the current folder.
135
+
136
+ Quick debugging tool for running multiple Python files that use
137
+ praisonaiagents or praisonai. Unlike 'praisonai run' (which runs
138
+ agent configs) or 'praisonai examples' (which runs repo examples),
139
+ this command finds and executes local scripts for rapid testing.
140
+
141
+ Only files containing 'from praisonaiagents' or 'from praisonai'
142
+ are executed. Test files (test_*.py) are excluded by default.
143
+
144
+ Examples:
145
+ praisonai batch # Run all in current dir
146
+ praisonai batch --sub # Include subdirectories
147
+ praisonai batch --sub --depth 2 # Limit depth
148
+ praisonai batch --parallel # Run in parallel
149
+ praisonai batch --timeout 120 # Custom timeout
150
+ """
151
+ # If a subcommand was invoked, skip the default run
152
+ if ctx.invoked_subcommand is not None:
153
+ return
154
+
155
+ # Lazy import to avoid loading at CLI startup
156
+ from praisonai.suite_runner import BatchSource, SuiteExecutor, RunResult
157
+
158
+ # Resolve paths
159
+ search_path = Path(path).resolve() if path else Path.cwd().resolve()
160
+ output_dir = report_dir or _get_default_report_dir()
161
+
162
+ if not search_path.exists():
163
+ if ci:
164
+ print(f"ERROR: Path not found: {search_path}")
165
+ else:
166
+ typer.echo(f"āŒ Path not found: {search_path}")
167
+ raise typer.Exit(2)
168
+
169
+ # Validate filter_type
170
+ if filter_type and filter_type not in ("agent", "agents", "workflow"):
171
+ if ci:
172
+ print(f"ERROR: Invalid filter type: {filter_type}. Use 'agent', 'agents', or 'workflow'")
173
+ else:
174
+ typer.echo(f"āŒ Invalid filter type: {filter_type}. Use 'agent', 'agents', or 'workflow'")
175
+ raise typer.Exit(2)
176
+
177
+ # Server mode uses 10s timeout by default
178
+ effective_timeout = 10 if server else timeout
179
+
180
+ # Create source
181
+ source = BatchSource(
182
+ root=search_path,
183
+ recursive=recursive,
184
+ depth=depth,
185
+ exclude_tests=not include_tests,
186
+ exclude_servers=not server, # Exclude servers by default, include when --server
187
+ server_only=server, # Only servers when --server flag
188
+ filter_type=filter_type,
189
+ )
190
+
191
+ # Discover items
192
+ items = source.discover()
193
+
194
+ if not items:
195
+ if ci:
196
+ print(f"INFO: No PraisonAI scripts found in {search_path}")
197
+ else:
198
+ typer.echo(f"ā„¹ļø No PraisonAI scripts found in {search_path}")
199
+ if not recursive:
200
+ typer.echo(" Tip: Use --sub to include subdirectories")
201
+ raise typer.Exit(0)
202
+
203
+ # Parallel execution
204
+ if parallel:
205
+ _run_parallel(
206
+ items=items,
207
+ source=source,
208
+ search_path=search_path,
209
+ output_dir=output_dir,
210
+ timeout=effective_timeout,
211
+ max_workers=max_workers,
212
+ quiet=quiet,
213
+ ci=ci,
214
+ )
215
+ return
216
+
217
+ # Sequential execution (default)
218
+ executor = SuiteExecutor(
219
+ suite="batch",
220
+ source_path=search_path,
221
+ timeout=effective_timeout,
222
+ fail_fast=fail_fast,
223
+ stream_output=not no_stream,
224
+ max_items=max_items,
225
+ report_dir=output_dir if not no_report else None,
226
+ generate_json=not no_report,
227
+ generate_md=not no_report,
228
+ generate_csv=not no_report,
229
+ pythonpath_additions=source.get_pythonpath(),
230
+ )
231
+
232
+ # Status icons (CI mode uses text)
233
+ if ci:
234
+ icons = {
235
+ "passed": "PASS",
236
+ "failed": "FAIL",
237
+ "skipped": "SKIP",
238
+ "timeout": "TIMEOUT",
239
+ "not_run": "NOT_RUN",
240
+ "xfail": "XFAIL",
241
+ }
242
+ else:
243
+ icons = {
244
+ "passed": "āœ…",
245
+ "failed": "āŒ",
246
+ "skipped": "ā­ļø",
247
+ "timeout": "ā±ļø",
248
+ "not_run": "šŸ“",
249
+ "xfail": "āš ļø",
250
+ }
251
+
252
+ def on_item_start(item, idx: int, total: int):
253
+ if not quiet:
254
+ if ci:
255
+ print(f"[{idx}/{total}] Running: {item.display_name}")
256
+ else:
257
+ typer.echo(f"\n[{idx}/{total}] Running: {item.display_name}")
258
+
259
+ def on_item_end(result: RunResult, idx: int, total: int):
260
+ icon = icons.get(result.status, "?")
261
+ duration = f"{result.duration_seconds:.2f}s" if result.duration_seconds else ""
262
+
263
+ if quiet:
264
+ if ci:
265
+ print(f"{icon} {result.display_name}")
266
+ else:
267
+ typer.echo(f"{icon} {result.display_name}")
268
+ else:
269
+ msg = f" {icon} {result.status.upper()}"
270
+ if duration:
271
+ msg += f" ({duration})"
272
+ if result.skip_reason:
273
+ msg += f" - {result.skip_reason}"
274
+ if result.error_message and not no_stream:
275
+ msg += f"\n Error: {result.error_message[:100]}"
276
+ if ci:
277
+ print(msg)
278
+ else:
279
+ typer.echo(msg)
280
+
281
+ def on_output(line: str, stream: str):
282
+ if not quiet and not no_stream:
283
+ prefix = " | " if ci else " │ "
284
+ if stream != "stdout":
285
+ prefix += "[err] "
286
+ if ci:
287
+ print(f"{prefix}{line.rstrip()}")
288
+ else:
289
+ typer.echo(f"{prefix}{line.rstrip()}")
290
+
291
+ # Print header
292
+ if not quiet:
293
+ if ci:
294
+ print("=" * 60)
295
+ print("PraisonAI Batch Runner")
296
+ print("=" * 60)
297
+ print(f"Path: {search_path}")
298
+ print(f"Recursive: {recursive}")
299
+ if depth:
300
+ print(f"Depth: {depth}")
301
+ print(f"Timeout: {effective_timeout}s")
302
+ print(f"Scripts: {len(items)}")
303
+ if server:
304
+ print("Mode: Server scripts only")
305
+ if filter_type:
306
+ print(f"Filter: {filter_type}")
307
+ if not no_report:
308
+ print(f"Reports: {output_dir}")
309
+ else:
310
+ typer.echo("=" * 60)
311
+ typer.echo("PraisonAI Batch Runner")
312
+ typer.echo("=" * 60)
313
+ typer.echo(f"Path: {search_path}")
314
+ typer.echo(f"Recursive: {recursive}")
315
+ if depth:
316
+ typer.echo(f"Depth: {depth}")
317
+ typer.echo(f"Timeout: {effective_timeout}s")
318
+ typer.echo(f"Scripts: {len(items)}")
319
+ if server:
320
+ typer.echo("Mode: Server scripts only")
321
+ if filter_type:
322
+ typer.echo(f"Filter: {filter_type}")
323
+ if not no_report:
324
+ typer.echo(f"Reports: {output_dir}")
325
+
326
+ # Run
327
+ report = executor.run(
328
+ items=items,
329
+ on_item_start=on_item_start,
330
+ on_item_end=on_item_end,
331
+ on_output=on_output if not no_stream else None,
332
+ )
333
+
334
+ # Print summary
335
+ totals = report.totals
336
+ total_count = sum(totals.values())
337
+
338
+ if not quiet:
339
+ if ci:
340
+ print("")
341
+ print("=" * 60)
342
+ print("SUMMARY")
343
+ print("=" * 60)
344
+ print(f" PASSED: {totals['passed']}")
345
+ print(f" FAILED: {totals['failed']}")
346
+ print(f" SKIPPED: {totals['skipped']}")
347
+ print(f" TIMEOUT: {totals['timeout']}")
348
+ print(" -----------------")
349
+ print(f" TOTAL: {total_count}")
350
+ print("=" * 60)
351
+ if not no_report and output_dir.exists():
352
+ print(f"Reports: {output_dir}")
353
+ else:
354
+ typer.echo("\n" + "=" * 60)
355
+ typer.echo("SUMMARY")
356
+ typer.echo("=" * 60)
357
+ typer.echo(f" āœ… Passed: {totals['passed']}")
358
+ typer.echo(f" āŒ Failed: {totals['failed']}")
359
+ typer.echo(f" ā­ļø Skipped: {totals['skipped']}")
360
+ typer.echo(f" ā±ļø Timeout: {totals['timeout']}")
361
+ typer.echo(" ─────────────────")
362
+ typer.echo(f" Total: {total_count}")
363
+ typer.echo("=" * 60)
364
+
365
+ if not no_report and output_dir.exists():
366
+ typer.echo(f"\nšŸ“ Reports saved to: {output_dir}")
367
+
368
+ # Exit code
369
+ if totals['failed'] > 0 or totals['timeout'] > 0:
370
+ raise typer.Exit(1)
371
+
372
+ raise typer.Exit(0)
373
+
374
+
375
+ def _run_parallel(
376
+ items,
377
+ source,
378
+ search_path: Path,
379
+ output_dir: Path,
380
+ timeout: int,
381
+ max_workers: int,
382
+ quiet: bool,
383
+ ci: bool = False,
384
+ ):
385
+ """Run items in parallel with async reporting."""
386
+ import subprocess
387
+ import sys
388
+ from concurrent.futures import ThreadPoolExecutor, as_completed
389
+ from praisonai.suite_runner import RunResult, RunReport, SuiteReporter
390
+ from datetime import timezone
391
+
392
+ if ci:
393
+ print("=" * 60)
394
+ print("PraisonAI Batch Runner (Parallel Mode)")
395
+ print("=" * 60)
396
+ print(f"Path: {search_path}")
397
+ print(f"Workers: {max_workers}")
398
+ print(f"Timeout: {timeout}s")
399
+ print(f"Scripts: {len(items)}")
400
+ print(f"Reports: {output_dir}")
401
+ print("")
402
+ print("Running... Progress:")
403
+ print("-" * 60)
404
+ else:
405
+ typer.echo("=" * 60)
406
+ typer.echo("PraisonAI Batch Runner (Parallel Mode)")
407
+ typer.echo("=" * 60)
408
+ typer.echo(f"Path: {search_path}")
409
+ typer.echo(f"Workers: {max_workers}")
410
+ typer.echo(f"Timeout: {timeout}s")
411
+ typer.echo(f"Scripts: {len(items)}")
412
+ typer.echo(f"Reports: {output_dir}")
413
+ typer.echo("")
414
+ typer.echo("Running in background... Progress:")
415
+ typer.echo("-" * 60)
416
+
417
+ results = []
418
+ completed = 0
419
+ total = len(items)
420
+
421
+ # Build environment
422
+ import os
423
+ env = os.environ.copy()
424
+ pythonpath = source.get_pythonpath()
425
+ if pythonpath:
426
+ existing = env.get('PYTHONPATH', '')
427
+ env['PYTHONPATH'] = os.pathsep.join(pythonpath + ([existing] if existing else []))
428
+
429
+ def run_item(item):
430
+ """Run a single item and return result."""
431
+ nonlocal completed
432
+
433
+ if not item.runnable:
434
+ completed += 1
435
+ return RunResult(
436
+ item_id=item.item_id,
437
+ suite="batch",
438
+ group=item.group,
439
+ source_path=item.source_path,
440
+ status="skipped" if item.skip else "not_run",
441
+ skip_reason=item.skip_reason,
442
+ code_hash=item.code_hash,
443
+ )
444
+
445
+ start_time = datetime.now(timezone.utc)
446
+
447
+ try:
448
+ result = subprocess.run(
449
+ [sys.executable, '-u', str(item.script_path)],
450
+ capture_output=True,
451
+ text=True,
452
+ timeout=timeout,
453
+ env=env,
454
+ cwd=item.script_path.parent,
455
+ )
456
+
457
+ end_time = datetime.now(timezone.utc)
458
+ duration = (end_time - start_time).total_seconds()
459
+
460
+ status = "passed" if result.returncode == 0 else "failed"
461
+ if item.xfail and result.returncode != 0:
462
+ status = "xfail"
463
+
464
+ error_message = None
465
+ if result.returncode != 0 and result.stderr:
466
+ lines = result.stderr.strip().split('\n')
467
+ error_message = lines[-1][:200] if lines else None
468
+
469
+ completed += 1
470
+
471
+ # Progress indicator
472
+ if ci:
473
+ icon = "PASS" if status == "passed" else "FAIL" if status == "failed" else "XFAIL"
474
+ else:
475
+ icon = "āœ…" if status == "passed" else "āŒ" if status == "failed" else "āš ļø"
476
+ if not quiet:
477
+ if ci:
478
+ print(f" [{completed}/{total}] {icon} {item.display_name} ({duration:.2f}s)")
479
+ else:
480
+ typer.echo(f" [{completed}/{total}] {icon} {item.display_name} ({duration:.2f}s)")
481
+
482
+ return RunResult(
483
+ item_id=item.item_id,
484
+ suite="batch",
485
+ group=item.group,
486
+ source_path=item.source_path,
487
+ status=status,
488
+ exit_code=result.returncode,
489
+ duration_seconds=duration,
490
+ start_time=start_time.isoformat(),
491
+ end_time=end_time.isoformat(),
492
+ error_message=error_message,
493
+ stdout=result.stdout,
494
+ stderr=result.stderr,
495
+ code_hash=item.code_hash,
496
+ )
497
+
498
+ except subprocess.TimeoutExpired:
499
+ completed += 1
500
+ end_time = datetime.now(timezone.utc)
501
+ duration = (end_time - start_time).total_seconds()
502
+
503
+ if not quiet:
504
+ if ci:
505
+ print(f" [{completed}/{total}] TIMEOUT {item.display_name} (timeout)")
506
+ else:
507
+ typer.echo(f" [{completed}/{total}] ā±ļø {item.display_name} (timeout)")
508
+
509
+ return RunResult(
510
+ item_id=item.item_id,
511
+ suite="batch",
512
+ group=item.group,
513
+ source_path=item.source_path,
514
+ status="timeout",
515
+ duration_seconds=duration,
516
+ error_message=f"Exceeded {timeout}s timeout",
517
+ code_hash=item.code_hash,
518
+ )
519
+ except Exception as e:
520
+ completed += 1
521
+ if not quiet:
522
+ if ci:
523
+ print(f" [{completed}/{total}] FAIL {item.display_name} (error: {e})")
524
+ else:
525
+ typer.echo(f" [{completed}/{total}] āŒ {item.display_name} (error: {e})")
526
+
527
+ return RunResult(
528
+ item_id=item.item_id,
529
+ suite="batch",
530
+ group=item.group,
531
+ source_path=item.source_path,
532
+ status="failed",
533
+ error_message=str(e),
534
+ code_hash=item.code_hash,
535
+ )
536
+
537
+ # Run in parallel
538
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
539
+ futures = {executor.submit(run_item, item): item for item in items}
540
+ for future in as_completed(futures):
541
+ result = future.result()
542
+ results.append(result)
543
+
544
+ # Create report
545
+ report = RunReport(
546
+ results=results,
547
+ suite="batch",
548
+ source_path=search_path,
549
+ groups_run=[],
550
+ )
551
+
552
+ # Generate reports
553
+ output_dir.mkdir(parents=True, exist_ok=True)
554
+ reporter = SuiteReporter(output_dir)
555
+ reporter.save_logs(results)
556
+ reporter.generate_json(report)
557
+ reporter.generate_markdown(report)
558
+ reporter.generate_csv(report)
559
+
560
+ # Print summary
561
+ totals = report.totals
562
+ total_count = sum(totals.values())
563
+
564
+ if ci:
565
+ print("-" * 60)
566
+ print("")
567
+ print("SUMMARY")
568
+ print("=" * 60)
569
+ print(f" PASSED: {totals['passed']}")
570
+ print(f" FAILED: {totals['failed']}")
571
+ print(f" SKIPPED: {totals['skipped']}")
572
+ print(f" TIMEOUT: {totals['timeout']}")
573
+ print(" -----------------")
574
+ print(f" TOTAL: {total_count}")
575
+ print("=" * 60)
576
+ print(f"Reports: {output_dir}")
577
+ else:
578
+ typer.echo("-" * 60)
579
+ typer.echo("\nSUMMARY")
580
+ typer.echo("=" * 60)
581
+ typer.echo(f" āœ… Passed: {totals['passed']}")
582
+ typer.echo(f" āŒ Failed: {totals['failed']}")
583
+ typer.echo(f" ā­ļø Skipped: {totals['skipped']}")
584
+ typer.echo(f" ā±ļø Timeout: {totals['timeout']}")
585
+ typer.echo(" ─────────────────")
586
+ typer.echo(f" Total: {total_count}")
587
+ typer.echo("=" * 60)
588
+ typer.echo(f"\nšŸ“ Reports saved to: {output_dir}")
589
+
590
+ # Exit code
591
+ if totals['failed'] > 0 or totals['timeout'] > 0:
592
+ raise typer.Exit(1)
593
+
594
+ raise typer.Exit(0)
595
+
596
+
597
+ @app.command("list")
598
+ def batch_list(
599
+ path: Optional[Path] = typer.Option(
600
+ None,
601
+ "--path", "-p",
602
+ help="Path to search",
603
+ ),
604
+ recursive: bool = typer.Option(
605
+ False,
606
+ "--sub", "--recursive", "-r",
607
+ help="Include subdirectories",
608
+ ),
609
+ depth: Optional[int] = typer.Option(
610
+ None,
611
+ "--depth", "-d",
612
+ help="Maximum recursion depth",
613
+ ),
614
+ include_tests: bool = typer.Option(
615
+ False,
616
+ "--include-tests",
617
+ help="Include test files",
618
+ ),
619
+ show_groups: bool = typer.Option(
620
+ False,
621
+ "--groups",
622
+ help="Show available groups only",
623
+ ),
624
+ ):
625
+ """
626
+ List discovered PraisonAI scripts without running them.
627
+
628
+ Examples:
629
+ praisonai batch list
630
+ praisonai batch list --sub
631
+ praisonai batch list --groups
632
+ """
633
+ from praisonai.suite_runner import BatchSource
634
+
635
+ search_path = Path(path).resolve() if path else Path.cwd().resolve()
636
+
637
+ if not search_path.exists():
638
+ typer.echo(f"āŒ Path not found: {search_path}")
639
+ raise typer.Exit(2)
640
+
641
+ source = BatchSource(
642
+ root=search_path,
643
+ recursive=recursive,
644
+ depth=depth,
645
+ exclude_tests=not include_tests,
646
+ )
647
+
648
+ # Show groups only
649
+ if show_groups:
650
+ groups = source.get_groups()
651
+ if not groups:
652
+ typer.echo(f"No PraisonAI scripts found in {search_path}")
653
+ return
654
+ typer.echo(f"Available groups in {search_path}:\n")
655
+ for g in groups:
656
+ typer.echo(f" - {g}")
657
+ return
658
+
659
+ items = source.discover()
660
+
661
+ if not items:
662
+ typer.echo(f"No PraisonAI scripts found in {search_path}")
663
+ if not recursive:
664
+ typer.echo("Tip: Use --sub to include subdirectories")
665
+ return
666
+
667
+ typer.echo(f"Found {len(items)} PraisonAI scripts in {search_path}\n")
668
+
669
+ for idx, item in enumerate(items, 1):
670
+ rel_path = item.source_path.relative_to(search_path)
671
+ flags = []
672
+ if item.skip:
673
+ flags.append("skip")
674
+ if item.uses_agent:
675
+ flags.append("Agent")
676
+ if item.uses_agents:
677
+ flags.append("Agents")
678
+ if item.uses_workflow:
679
+ flags.append("Workflow")
680
+
681
+ flag_str = f" [{', '.join(flags)}]" if flags else ""
682
+ typer.echo(f" {idx:3}. [{item.group}] {rel_path}{flag_str}")
683
+
684
+
685
+ @app.command("stats")
686
+ def batch_stats(
687
+ path: Optional[Path] = typer.Option(
688
+ None,
689
+ "--path", "-p",
690
+ help="Path to search",
691
+ ),
692
+ recursive: bool = typer.Option(
693
+ False,
694
+ "--sub", "--recursive", "-r",
695
+ help="Include subdirectories",
696
+ ),
697
+ depth: Optional[int] = typer.Option(
698
+ None,
699
+ "--depth", "-d",
700
+ help="Maximum recursion depth",
701
+ ),
702
+ include_tests: bool = typer.Option(
703
+ False,
704
+ "--include-tests",
705
+ help="Include test files",
706
+ ),
707
+ ):
708
+ """
709
+ Show statistics for PraisonAI scripts.
710
+
711
+ Displays counts by group, runnable status, and agent-centric usage.
712
+
713
+ Examples:
714
+ praisonai batch stats
715
+ praisonai batch stats --sub
716
+ """
717
+ from collections import Counter
718
+ from praisonai.suite_runner import BatchSource
719
+
720
+ search_path = Path(path).resolve() if path else Path.cwd().resolve()
721
+
722
+ if not search_path.exists():
723
+ typer.echo(f"āŒ Path not found: {search_path}")
724
+ raise typer.Exit(2)
725
+
726
+ source = BatchSource(
727
+ root=search_path,
728
+ recursive=recursive,
729
+ depth=depth,
730
+ exclude_tests=not include_tests,
731
+ )
732
+
733
+ items = source.discover()
734
+
735
+ if not items:
736
+ typer.echo(f"No PraisonAI scripts found in {search_path}")
737
+ return
738
+
739
+ # Calculate stats
740
+ group_counts = Counter(item.group for item in items)
741
+ runnable_by_group = Counter(item.group for item in items if item.runnable)
742
+
743
+ # Agent-centric stats
744
+ agent_count = sum(1 for item in items if item.uses_agent)
745
+ agents_count = sum(1 for item in items if item.uses_agents)
746
+ workflow_count = sum(1 for item in items if item.uses_workflow)
747
+
748
+ agent_by_group = Counter(item.group for item in items if item.uses_agent)
749
+ agents_by_group = Counter(item.group for item in items if item.uses_agents)
750
+ workflow_by_group = Counter(item.group for item in items if item.uses_workflow)
751
+
752
+ typer.echo("\nšŸ“Š Batch Execution Statistics")
753
+ typer.echo(f"Path: {search_path}")
754
+ typer.echo("=" * 80)
755
+
756
+ typer.echo(f"\n{'Group':<20} {'Total':>8} {'Runnable':>10} {'Agent':>8} {'Agents':>8} {'Workflow':>10}")
757
+ typer.echo("-" * 80)
758
+
759
+ for g in sorted(group_counts.keys()):
760
+ typer.echo(
761
+ f"{g:<20} {group_counts[g]:>8} {runnable_by_group[g]:>10} "
762
+ f"{agent_by_group[g]:>8} {agents_by_group[g]:>8} {workflow_by_group[g]:>10}"
763
+ )
764
+
765
+ typer.echo("-" * 80)
766
+ typer.echo(
767
+ f"{'TOTAL':<20} {len(items):>8} {sum(1 for i in items if i.runnable):>10} "
768
+ f"{agent_count:>8} {agents_count:>8} {workflow_count:>10}"
769
+ )
770
+ typer.echo()
771
+
772
+
773
+ @app.command("report")
774
+ def batch_report(
775
+ report_dir: Optional[Path] = typer.Option(
776
+ None,
777
+ "--dir", "-d",
778
+ help="Specific report directory (default: latest)",
779
+ ),
780
+ format: str = typer.Option(
781
+ "summary",
782
+ "--format", "-f",
783
+ help="Output format: summary, failures, full",
784
+ ),
785
+ ):
786
+ """
787
+ View the latest batch execution report.
788
+
789
+ Examples:
790
+ praisonai batch report
791
+ praisonai batch report --format failures
792
+ praisonai batch report --dir ~/Downloads/reports/batch/20240115_120000
793
+ """
794
+ import json
795
+
796
+ # Find report directory
797
+ if report_dir:
798
+ target_dir = Path(report_dir)
799
+ else:
800
+ target_dir = _get_latest_report_dir()
801
+
802
+ if not target_dir or not target_dir.exists():
803
+ typer.echo("āŒ No batch reports found.")
804
+ typer.echo(" Run 'praisonai batch' first to generate a report.")
805
+ raise typer.Exit(1)
806
+
807
+ # Load JSON report
808
+ json_path = target_dir / "report.json"
809
+ if not json_path.exists():
810
+ typer.echo(f"āŒ Report not found: {json_path}")
811
+ raise typer.Exit(1)
812
+
813
+ report_data = json.loads(json_path.read_text())
814
+
815
+ typer.echo(f"\nšŸ“Š Batch Report: {target_dir.name}")
816
+ typer.echo("=" * 60)
817
+
818
+ # Summary
819
+ totals = report_data.get("totals", {})
820
+ typer.echo(f" āœ… Passed: {totals.get('passed', 0)}")
821
+ typer.echo(f" āŒ Failed: {totals.get('failed', 0)}")
822
+ typer.echo(f" ā­ļø Skipped: {totals.get('skipped', 0)}")
823
+ typer.echo(f" ā±ļø Timeout: {totals.get('timeout', 0)}")
824
+ typer.echo("=" * 60)
825
+
826
+ if format in ("failures", "full"):
827
+ results = report_data.get("results", [])
828
+ failures = [r for r in results if r.get("status") == "failed"]
829
+
830
+ if failures:
831
+ typer.echo("\nāŒ FAILURES:")
832
+ typer.echo("-" * 60)
833
+ for r in failures:
834
+ typer.echo(f"\n {r.get('source_path', 'unknown')}")
835
+ if r.get("error_message"):
836
+ typer.echo(f" Error: {r['error_message'][:200]}")
837
+
838
+ if format == "full":
839
+ results = report_data.get("results", [])
840
+ typer.echo("\nšŸ“‹ ALL RESULTS:")
841
+ typer.echo("-" * 60)
842
+ for r in results:
843
+ status = r.get("status", "unknown")
844
+ icon = {"passed": "āœ…", "failed": "āŒ", "skipped": "ā­ļø", "timeout": "ā±ļø"}.get(status, "?")
845
+ duration = r.get("duration_seconds", 0)
846
+ typer.echo(f" {icon} {r.get('source_path', 'unknown')} ({duration:.2f}s)")
847
+
848
+ typer.echo(f"\nšŸ“ Full report: {target_dir}")