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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- 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"
|