alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import typer
8
+
9
+ from ...config import AppConfig, load_config
10
+ from ...custom_tools import (
11
+ CustomToolCatalogEntry,
12
+ build_custom_tool_session_state,
13
+ global_custom_tools_root,
14
+ project_custom_tools_root,
15
+ trust_project_tool,
16
+ untrust_project_tool,
17
+ )
18
+ from ...runtime_kind import RuntimeKind
19
+ from ...surface.styles import STYLE_CONTENT, STYLE_EMPHASIS
20
+ from ...tools.registry import iter_builtin_tool_metadata
21
+ from . import _patchable
22
+ from ._shared import _console, _resolve_tool_workspace_root, _Table
23
+
24
+ if TYPE_CHECKING:
25
+ from rich.table import Table
26
+
27
+
28
+ tool_app = typer.Typer(add_completion=False, help="Custom tool discovery and trust commands.")
29
+
30
+
31
+ def _discover_custom_tools_for_path(
32
+ *,
33
+ path: Path,
34
+ cfg: AppConfig | None = None,
35
+ ) -> tuple[Path, Any]:
36
+ workspace_root = _resolve_tool_workspace_root(path=path)
37
+ effective_cfg = cfg or _patchable("load_config", load_config)()
38
+ state = build_custom_tool_session_state(
39
+ workspace_root=workspace_root,
40
+ custom_tools_enabled=bool(getattr(effective_cfg, "custom_tools_enabled", True)),
41
+ mode="review",
42
+ runtime_kind=RuntimeKind.INTERACTIVE_CHAT,
43
+ built_in_tool_names={spec.name.casefold() for spec in iter_builtin_tool_metadata()},
44
+ catalog_view=True,
45
+ )
46
+ return workspace_root, state
47
+
48
+
49
+ def _resolve_custom_tool_entries_by_name(
50
+ *,
51
+ state: Any,
52
+ name: str,
53
+ ) -> list[CustomToolCatalogEntry]:
54
+ needle = str(name or "").strip().casefold()
55
+ if not needle:
56
+ return []
57
+ return [
58
+ entry
59
+ for entry in getattr(state, "catalog_entries", ())
60
+ if str(getattr(entry, "name", "")).casefold() == needle
61
+ ]
62
+
63
+
64
+ _CUSTOM_TOOLS_WIDE_TABLE_MIN_WIDTH = 140
65
+
66
+
67
+ def _custom_tool_source_filename(entry: CustomToolCatalogEntry) -> str:
68
+ return entry.source_path.name or entry.source_path.as_posix()
69
+
70
+
71
+ def _custom_tool_source_location(*, entry: CustomToolCatalogEntry, workspace_root: Path) -> str:
72
+ relative_path = ""
73
+ if entry.spec is not None and entry.spec.relative_tool_path:
74
+ relative_path = entry.spec.relative_tool_path
75
+ elif entry.issue is not None and entry.issue.relative_tool_path:
76
+ relative_path = entry.issue.relative_tool_path
77
+ elif entry.source_scope == "project":
78
+ try:
79
+ relative_path = (
80
+ entry.source_path.resolve().relative_to(workspace_root.resolve()).as_posix()
81
+ )
82
+ except ValueError:
83
+ relative_path = ""
84
+ else:
85
+ try:
86
+ relative_path = (
87
+ entry.source_path.resolve()
88
+ .relative_to(global_custom_tools_root().resolve())
89
+ .as_posix()
90
+ )
91
+ except ValueError:
92
+ relative_path = ""
93
+ relative_path = str(relative_path or "").strip().replace("\\", "/")
94
+ if entry.source_scope == "global" and relative_path:
95
+ global_prefix = global_custom_tools_root().name.strip().replace("\\", "/")
96
+ if (
97
+ global_prefix
98
+ and relative_path != global_prefix
99
+ and not relative_path.startswith(f"{global_prefix}/")
100
+ ):
101
+ return f"{global_prefix}/{relative_path}"
102
+ if relative_path:
103
+ return relative_path
104
+ if entry.source_scope == "global":
105
+ global_prefix = global_custom_tools_root().name.strip().replace("\\", "/")
106
+ if global_prefix:
107
+ return f"{global_prefix}/{_custom_tool_source_filename(entry)}"
108
+ return _custom_tool_source_filename(entry)
109
+ return _custom_tool_source_filename(entry)
110
+
111
+
112
+ def _custom_tools_table(
113
+ *,
114
+ entries: list[CustomToolCatalogEntry],
115
+ title: str,
116
+ workspace_root: Path,
117
+ ) -> Table:
118
+ table = _Table(title=title, expand=True)
119
+ table.add_column("name", no_wrap=True, ratio=2)
120
+ table.add_column("scope", no_wrap=True)
121
+ table.add_column("trust", no_wrap=True)
122
+ table.add_column("status", no_wrap=True)
123
+ table.add_column("file", no_wrap=True)
124
+ table.add_column("location", overflow="fold", ratio=4)
125
+ table.add_column("details", overflow="fold", ratio=3)
126
+ for entry in entries:
127
+ table.add_row(
128
+ entry.name,
129
+ entry.source_scope,
130
+ entry.trust,
131
+ entry.status,
132
+ _custom_tool_source_filename(entry),
133
+ _custom_tool_source_location(entry=entry, workspace_root=workspace_root),
134
+ entry.detail or "-",
135
+ )
136
+ return table
137
+
138
+
139
+ def _custom_tools_stacked_list(
140
+ *,
141
+ entries: list[CustomToolCatalogEntry],
142
+ title: str,
143
+ workspace_root: Path,
144
+ ) -> Any:
145
+ from rich.console import Group
146
+ from rich.text import Text
147
+
148
+ renderables: list[Any] = [Text(str(title), style=STYLE_EMPHASIS)]
149
+ for index, entry in enumerate(entries):
150
+ if index:
151
+ renderables.append(Text(""))
152
+ renderables.append(
153
+ Text.assemble(
154
+ (entry.name, STYLE_EMPHASIS),
155
+ (" · ", "bright_black"),
156
+ (entry.source_scope, "cyan"),
157
+ (" · ", "bright_black"),
158
+ (entry.trust, STYLE_CONTENT),
159
+ (" · ", "bright_black"),
160
+ (entry.status, STYLE_CONTENT),
161
+ )
162
+ )
163
+ renderables.append(
164
+ Text.assemble(
165
+ (" file: ", "dim"),
166
+ (_custom_tool_source_filename(entry), STYLE_CONTENT),
167
+ )
168
+ )
169
+ renderables.append(
170
+ Text.assemble(
171
+ (" source: ", "dim"),
172
+ (
173
+ _custom_tool_source_location(entry=entry, workspace_root=workspace_root),
174
+ STYLE_CONTENT,
175
+ ),
176
+ )
177
+ )
178
+ if entry.detail:
179
+ renderables.append(Text.assemble((" details: ", "dim"), (entry.detail, STYLE_CONTENT)))
180
+ return Group(*renderables)
181
+
182
+
183
+ def _custom_tools_list_renderable(
184
+ *,
185
+ entries: list[CustomToolCatalogEntry],
186
+ title: str,
187
+ workspace_root: Path,
188
+ width: int,
189
+ ) -> Any:
190
+ if width >= _CUSTOM_TOOLS_WIDE_TABLE_MIN_WIDTH:
191
+ return _custom_tools_table(entries=entries, title=title, workspace_root=workspace_root)
192
+ return _custom_tools_stacked_list(entries=entries, title=title, workspace_root=workspace_root)
193
+
194
+
195
+ @tool_app.command("list")
196
+ def tool_list(
197
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
198
+ ) -> None:
199
+ console = _console()
200
+ cfg = _patchable("load_config", load_config)()
201
+ workspace_root, state = _discover_custom_tools_for_path(path=path, cfg=cfg)
202
+ entries = list(state.catalog_entries)
203
+ if not entries:
204
+ console.print("No custom tools discovered.")
205
+ console.print(
206
+ f"[dim]Project root:[/dim] {project_custom_tools_root(workspace_root).as_posix()}"
207
+ )
208
+ console.print(f"[dim]Global root:[/dim] {global_custom_tools_root().as_posix()}")
209
+ return
210
+ console.print(
211
+ _custom_tools_list_renderable(
212
+ entries=entries,
213
+ title=f"Custom Tools ({len(entries)})",
214
+ workspace_root=workspace_root,
215
+ width=console.width,
216
+ )
217
+ )
218
+ console.print(
219
+ f"[dim]Project root:[/dim] {project_custom_tools_root(workspace_root).as_posix()}"
220
+ )
221
+ console.print(f"[dim]Global root:[/dim] {global_custom_tools_root().as_posix()}")
222
+
223
+
224
+ @tool_app.command("info")
225
+ def tool_info(
226
+ name: str = typer.Argument(..., help="Custom tool name."),
227
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
228
+ ) -> None:
229
+ console = _console()
230
+ cfg = _patchable("load_config", load_config)()
231
+ workspace_root, state = _discover_custom_tools_for_path(path=path, cfg=cfg)
232
+ matches = _resolve_custom_tool_entries_by_name(state=state, name=name)
233
+ if not matches:
234
+ console.print(f"[red]Custom tool not found:[/red] {name}")
235
+ raise typer.Exit(code=1)
236
+ for entry in matches:
237
+ table = _Table(title=f"Custom Tool: {entry.name}")
238
+ table.add_column("field")
239
+ table.add_column("value", overflow="fold")
240
+ table.add_row("scope", entry.source_scope)
241
+ table.add_row("trust", entry.trust)
242
+ table.add_row("status", entry.status)
243
+ table.add_row("source_path", entry.source_path.as_posix())
244
+ table.add_row("workspace_root", workspace_root.as_posix())
245
+ if entry.detail:
246
+ table.add_row("details", entry.detail)
247
+ if entry.spec is not None:
248
+ table.add_row("description", entry.spec.description)
249
+ table.add_row("manifest_version", str(entry.spec.manifest_version))
250
+ table.add_row("timeout_s", f"{entry.spec.timeout_s:g}")
251
+ table.add_row("isolation", entry.spec.isolation)
252
+ table.add_row(
253
+ "capabilities",
254
+ json.dumps(
255
+ entry.spec.capabilities.to_dict(),
256
+ ensure_ascii=True,
257
+ sort_keys=True,
258
+ ),
259
+ )
260
+ table.add_row("enabled_in", ", ".join(entry.spec.enabled_in))
261
+ table.add_row(
262
+ "required_env",
263
+ ", ".join(entry.spec.required_env) if entry.spec.required_env else "-",
264
+ )
265
+ table.add_row(
266
+ "missing_env",
267
+ ", ".join(entry.spec.missing_env) if entry.spec.missing_env else "-",
268
+ )
269
+ table.add_row(
270
+ "input_schema",
271
+ json.dumps(entry.spec.input_schema, ensure_ascii=True, sort_keys=True),
272
+ )
273
+ table.add_row(
274
+ "output_schema",
275
+ (
276
+ json.dumps(entry.spec.output_schema, ensure_ascii=True, sort_keys=True)
277
+ if entry.spec.output_schema is not None
278
+ else "-"
279
+ ),
280
+ )
281
+ console.print(table)
282
+ console.print(f"[dim]source_path:[/dim] {entry.source_path.as_posix()}")
283
+ console.print(f"[dim]workspace_root:[/dim] {workspace_root.as_posix()}")
284
+
285
+
286
+ def _select_project_tool_entry_or_exit(
287
+ *,
288
+ state: Any,
289
+ name: str,
290
+ console: Any,
291
+ ) -> CustomToolCatalogEntry:
292
+ matches = _resolve_custom_tool_entries_by_name(state=state, name=name)
293
+ if not matches:
294
+ console.print(f"[red]Custom tool not found:[/red] {name}")
295
+ raise typer.Exit(code=1)
296
+ for entry in matches:
297
+ if (
298
+ entry.source_scope == "project"
299
+ and entry.spec is not None
300
+ and entry.status != "shadowed"
301
+ ):
302
+ return entry
303
+ if any(entry.source_scope == "global" for entry in matches):
304
+ console.print("[red]Trust commands only apply to project custom tools.[/red]")
305
+ raise typer.Exit(code=2)
306
+ console.print("[red]No valid project custom tool matched that name.[/red]")
307
+ raise typer.Exit(code=2)
308
+
309
+
310
+ @tool_app.command("trust")
311
+ def tool_trust(
312
+ name: str = typer.Argument(..., help="Project custom tool name."),
313
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
314
+ ) -> None:
315
+ console = _console()
316
+ cfg = _patchable("load_config", load_config)()
317
+ _workspace_root, state = _discover_custom_tools_for_path(path=path, cfg=cfg)
318
+ entry = _select_project_tool_entry_or_exit(state=state, name=name, console=console)
319
+ assert entry.spec is not None
320
+ trust_project_tool(entry.spec)
321
+ console.print(f"Trusted project custom tool: {entry.spec.name}")
322
+
323
+
324
+ @tool_app.command("untrust")
325
+ def tool_untrust(
326
+ name: str = typer.Argument(..., help="Project custom tool name."),
327
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
328
+ ) -> None:
329
+ console = _console()
330
+ cfg = _patchable("load_config", load_config)()
331
+ _workspace_root, state = _discover_custom_tools_for_path(path=path, cfg=cfg)
332
+ entry = _select_project_tool_entry_or_exit(state=state, name=name, console=console)
333
+ assert entry.spec is not None
334
+ untrust_project_tool(entry.spec)
335
+ console.print(f"Untrusted project custom tool: {entry.spec.name}")
@@ -0,0 +1,364 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+ from ... import __version__
9
+ from ...config import AppConfig, ConfigError, load_config
10
+ from ...updates import (
11
+ InstallerPlan,
12
+ UpdateStatus,
13
+ check_for_updates,
14
+ detect_installer_plan,
15
+ maybe_refresh_update_cache_in_background,
16
+ passive_update_notice,
17
+ read_update_prompt_state,
18
+ record_update_prompt_shown,
19
+ record_update_skipped,
20
+ resolve_update_check_enabled,
21
+ run_installer_plan,
22
+ should_prompt_for_update,
23
+ status_from_cache,
24
+ )
25
+ from . import _patchable
26
+ from ._shared import _console
27
+
28
+ update_app = typer.Typer(add_completion=False, help="Check for and apply Alysis Code updates.")
29
+ _BACKGROUND_UPDATE_SUBCOMMANDS = {"chat", "run", "forge"}
30
+ _update_prompt_completed = False
31
+
32
+
33
+ def _cached_update_notice() -> str | None:
34
+ try:
35
+ return passive_update_notice(
36
+ current_version=__version__, cfg=_patchable("load_config", load_config)()
37
+ )
38
+ except Exception: # noqa: BLE001 - update notices must never block startup/status rendering
39
+ return None
40
+
41
+
42
+ def _start_background_update_check() -> None:
43
+ try:
44
+ maybe_refresh_update_cache_in_background(
45
+ current_version=__version__,
46
+ cfg=_patchable("load_config", load_config)(),
47
+ )
48
+ except Exception: # noqa: BLE001 - update checks must never block normal CLI startup
49
+ return
50
+
51
+
52
+ def _classic_update_prompt(console: Any, status: UpdateStatus, plan: InstallerPlan) -> str:
53
+ console.print(
54
+ f"[yellow]Alysis Code {status.latest_version} is available.[/yellow] "
55
+ f"You have {status.current_version}."
56
+ )
57
+ if status.url:
58
+ console.print(f"[dim]Release:[/dim] {status.url}")
59
+ if plan.supported and plan.display_command:
60
+ console.print(f"[dim]Update command:[/dim] {plan.display_command}")
61
+ elif plan.reason:
62
+ console.print(f"[dim]{plan.reason}[/dim]")
63
+ try:
64
+ answer = (
65
+ typer.prompt("Update now? [y=update / n=later / s=skip this version]", default="n")
66
+ .strip()
67
+ .lower()
68
+ )
69
+ except (EOFError, KeyboardInterrupt, typer.Abort):
70
+ # typer.prompt surfaces Ctrl-C/Ctrl-D as typer.Abort.
71
+ return "later"
72
+ if answer in {"y", "yes", "u", "update"}:
73
+ return "update"
74
+ if answer in {"s", "skip"}:
75
+ return "skip"
76
+ return "later"
77
+
78
+
79
+ def _tui_popup_enabled() -> bool:
80
+ try:
81
+ from ..tui.config import is_tui_enabled
82
+
83
+ return bool(is_tui_enabled())
84
+ except Exception: # noqa: BLE001 - TUI probing must not break the prompt
85
+ return False
86
+
87
+
88
+ def _pause_before_alt_screen(console: Any) -> None:
89
+ """Keep launch-time guidance readable before the chat TUI covers it."""
90
+ if not _tui_popup_enabled():
91
+ return
92
+ try:
93
+ typer.prompt("Press Enter to continue", default="", show_default=False)
94
+ except (EOFError, KeyboardInterrupt, typer.Abort):
95
+ pass
96
+
97
+
98
+ def _prompt_update_choice(console: Any, status: UpdateStatus, plan: InstallerPlan) -> str:
99
+ if _tui_popup_enabled():
100
+ try:
101
+ from ..tui.update_prompt import select_update_action
102
+
103
+ choice, interactive_available = select_update_action(
104
+ current_version=status.current_version,
105
+ latest_version=str(status.latest_version),
106
+ command=plan.display_command if plan.supported else None,
107
+ unsupported_reason=None if plan.supported else (plan.reason or None),
108
+ )
109
+ if interactive_available:
110
+ return choice or "later"
111
+ except Exception: # noqa: BLE001 - fall back to the classic inline prompt
112
+ pass
113
+ return _classic_update_prompt(console, status, plan)
114
+
115
+
116
+ def _apply_startup_update(console: Any, plan: InstallerPlan) -> str:
117
+ """Run the detected upgrade command; exits the process on success."""
118
+ _print_installer_plan(console, plan)
119
+ if not plan.supported:
120
+ console.print(
121
+ "[yellow]Automatic update is not available for this install.[/yellow] "
122
+ "Update manually using the installer that created this environment."
123
+ )
124
+ _pause_before_alt_screen(console)
125
+ return "manual"
126
+ try:
127
+ exit_code = _patchable("run_installer_plan", run_installer_plan)(plan)
128
+ except Exception as exc: # noqa: BLE001 - a failed upgrade must not block launch
129
+ console.print(f"[red]Update failed:[/red] {exc} Continuing with the current version.")
130
+ _pause_before_alt_screen(console)
131
+ return "update-failed"
132
+ if exit_code != 0:
133
+ console.print(
134
+ f"[red]Update command failed with exit code {exit_code}.[/red] "
135
+ "Continuing with the current version."
136
+ )
137
+ _pause_before_alt_screen(console)
138
+ return "update-failed"
139
+ console.print("[green]Update installed.[/green] Restart Alysis Code to use the new version.")
140
+ raise typer.Exit(code=0)
141
+
142
+
143
+ def maybe_prompt_update_at_startup(*, console: Any | None = None) -> str | None:
144
+ """Offer a newer release at launch, before the setup/workspace screens.
145
+
146
+ Runs at most once per process and decides from the cached check only, so
147
+ it never blocks startup on the network. Returns the action taken
148
+ ("update-failed" / "manual" / "skip" / "later"), or None when no prompt was
149
+ shown; a successful update exits the process via ``typer.Exit`` so the
150
+ user restarts into the new version.
151
+ """
152
+ global _update_prompt_completed
153
+ if _update_prompt_completed:
154
+ return None
155
+ _update_prompt_completed = True
156
+
157
+ try:
158
+ from .startup import _is_interactive_terminal
159
+
160
+ if not _is_interactive_terminal():
161
+ return None
162
+ cfg = _patchable("load_config", load_config)()
163
+ status = _patchable("status_from_cache", status_from_cache)(
164
+ current_version=__version__,
165
+ cfg=cfg,
166
+ )
167
+ state = read_update_prompt_state()
168
+ if not should_prompt_for_update(status=status, state=state, cfg=cfg):
169
+ return None
170
+ latest = status.latest_version
171
+ if not latest:
172
+ return None
173
+ plan = _patchable("detect_installer_plan", detect_installer_plan)()
174
+ if plan.method == "editable":
175
+ # Dev checkouts update via git; don't nag at every launch.
176
+ return None
177
+ record_update_prompt_shown(latest)
178
+ out = console if console is not None else _console()
179
+ choice = _prompt_update_choice(out, status, plan)
180
+ except Exception: # noqa: BLE001 - the update prompt must never break startup
181
+ return None
182
+
183
+ if choice == "skip":
184
+ try:
185
+ record_update_skipped(latest)
186
+ except Exception: # noqa: BLE001 - persisting the skip is best-effort
187
+ pass
188
+ out.print(f"[dim]Skipping version {latest}. Run `alysis update` any time.[/dim]")
189
+ return "skip"
190
+ if choice != "update":
191
+ return "later"
192
+ return _apply_startup_update(out, plan)
193
+
194
+
195
+ def _print_update_status(console: Any, status: UpdateStatus) -> None:
196
+ if status.source == "disabled":
197
+ console.print("Update checks are disabled in config.")
198
+ return
199
+ if status.update_available and status.latest_version:
200
+ console.print(
201
+ f"[yellow]Alysis Code {status.latest_version} is available.[/yellow] "
202
+ f"You have {status.current_version}."
203
+ )
204
+ if status.url:
205
+ console.print(f"[dim]Release:[/dim] {status.url}")
206
+ console.print("[dim]Run `alysis update` to apply it with confirmation.[/dim]")
207
+ return
208
+ if status.up_to_date:
209
+ console.print(f"Alysis Code is up to date ({status.current_version}).")
210
+ return
211
+ if status.error:
212
+ console.print(f"[red]Update check failed:[/red] {status.error}")
213
+ return
214
+ console.print("No update check has been recorded yet. Run `alysis update check`.")
215
+
216
+
217
+ def _update_status_or_exit(
218
+ *,
219
+ console: Any,
220
+ cfg: AppConfig,
221
+ cached: bool,
222
+ ) -> UpdateStatus:
223
+ if cached:
224
+ status = _patchable("status_from_cache", status_from_cache)(
225
+ current_version=__version__,
226
+ cfg=cfg,
227
+ )
228
+ else:
229
+ status = _patchable("check_for_updates", check_for_updates)(
230
+ current_version=__version__,
231
+ cfg=cfg,
232
+ force=True,
233
+ )
234
+ if status.error:
235
+ _print_update_status(console, status)
236
+ raise typer.Exit(code=1)
237
+ return status
238
+
239
+
240
+ def _print_installer_plan(console: Any, plan: InstallerPlan) -> None:
241
+ console.print(f"[dim]Installer:[/dim] {plan.method}")
242
+ if plan.reason:
243
+ console.print(f"[dim]Reason:[/dim] {plan.reason}")
244
+ if plan.display_command:
245
+ console.print(f"[dim]Command:[/dim] {plan.display_command}")
246
+
247
+
248
+ def _run_update_flow(*, yes: bool, dry_run: bool, cached: bool) -> None:
249
+ console = _console()
250
+ cfg = _patchable("load_config", load_config)()
251
+ try:
252
+ status = _update_status_or_exit(console=console, cfg=cfg, cached=cached)
253
+ except ConfigError as exc:
254
+ console.print(f"[red]Update config error:[/red] {exc}")
255
+ raise typer.Exit(code=2) from exc
256
+
257
+ _print_update_status(console, status)
258
+ if not status.update_available:
259
+ return
260
+
261
+ plan = _patchable("detect_installer_plan", detect_installer_plan)()
262
+ _print_installer_plan(console, plan)
263
+ if not plan.supported:
264
+ console.print(
265
+ "[yellow]Automatic command selection is not available for this install.[/yellow]"
266
+ )
267
+ console.print("Update manually using the installer that created this environment.")
268
+ raise typer.Exit(code=1)
269
+ if dry_run:
270
+ console.print("[dim]Dry run only; no command executed.[/dim]")
271
+ return
272
+ if not yes and not typer.confirm("Run this update command now?", default=False):
273
+ console.print("Update cancelled.")
274
+ return
275
+ exit_code = _patchable("run_installer_plan", run_installer_plan)(plan)
276
+ if exit_code != 0:
277
+ console.print(f"[red]Update command failed with exit code {exit_code}.[/red]")
278
+ raise typer.Exit(code=exit_code)
279
+ console.print("[green]Update command completed.[/green] Restart Alysis Code to use it.")
280
+
281
+
282
+ @update_app.callback(invoke_without_command=True)
283
+ def update_main(
284
+ ctx: typer.Context,
285
+ yes: bool = typer.Option(False, "--yes", "-y", help="Run the detected update command."),
286
+ dry_run: bool = typer.Option(
287
+ False,
288
+ "--dry-run",
289
+ help="Show the detected update command without running it.",
290
+ ),
291
+ cached: bool = typer.Option(
292
+ False,
293
+ "--cached",
294
+ help="Use cached version information instead of checking PyPI first.",
295
+ ),
296
+ ) -> None:
297
+ """Check for a newer Alysis Code release and apply it only after confirmation."""
298
+ if ctx.invoked_subcommand is not None:
299
+ return
300
+ _run_update_flow(yes=yes, dry_run=dry_run, cached=cached)
301
+
302
+
303
+ @update_app.command("check")
304
+ def update_check(
305
+ cached: bool = typer.Option(
306
+ False,
307
+ "--cached",
308
+ help="Use cached version information instead of checking PyPI.",
309
+ ),
310
+ as_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
311
+ ) -> None:
312
+ """Check whether a newer Alysis Code package is available."""
313
+ console = _console()
314
+ cfg = _patchable("load_config", load_config)()
315
+ try:
316
+ if cached:
317
+ status = _patchable("status_from_cache", status_from_cache)(
318
+ current_version=__version__,
319
+ cfg=cfg,
320
+ )
321
+ else:
322
+ status = _patchable("check_for_updates", check_for_updates)(
323
+ current_version=__version__,
324
+ cfg=cfg,
325
+ force=True,
326
+ )
327
+ except ConfigError as exc:
328
+ console.print(f"[red]Update config error:[/red] {exc}")
329
+ raise typer.Exit(code=2) from exc
330
+ if as_json:
331
+ console.print_json(json.dumps(status.to_json()))
332
+ if status.error:
333
+ raise typer.Exit(code=1)
334
+ return
335
+ _print_update_status(console, status)
336
+ if status.error:
337
+ raise typer.Exit(code=1)
338
+
339
+
340
+ def _update_status_summary(status: UpdateStatus, cfg: AppConfig | None) -> str:
341
+ if not resolve_update_check_enabled(cfg):
342
+ return "disabled"
343
+ if status.update_available and status.latest_version:
344
+ return f"available {status.latest_version} (run: alysis update)"
345
+ if status.up_to_date and status.latest_version:
346
+ return f"current ({status.current_version})"
347
+ if status.error:
348
+ return f"last check failed ({status.error})"
349
+ return "not checked (run: alysis update check)"
350
+
351
+
352
+ def _cached_update_status_summary(cfg: AppConfig | None) -> str:
353
+ try:
354
+ return _update_status_summary(
355
+ _patchable("status_from_cache", status_from_cache)(
356
+ current_version=__version__,
357
+ cfg=cfg,
358
+ ),
359
+ cfg,
360
+ )
361
+ except ConfigError as exc:
362
+ return f"config error ({exc})"
363
+ except Exception:
364
+ return "unknown"