comate-cli 0.7.5__tar.gz → 0.7.7__tar.gz
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.
- {comate_cli-0.7.5 → comate_cli-0.7.7}/PKG-INFO +1 -1
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/main.py +10 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/app.py +38 -16
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup_profile.py +38 -11
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/update_check.py +210 -49
- {comate_cli-0.7.5 → comate_cli-0.7.7}/pyproject.toml +1 -1
- comate_cli-0.7.7/tests/conftest.py +33 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_startup_latency.py +65 -38
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_startup_profile.py +112 -1
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_update_check.py +270 -15
- {comate_cli-0.7.5 → comate_cli-0.7.7}/uv.lock +3 -415
- comate_cli-0.7.5/tests/conftest.py +0 -14
- {comate_cli-0.7.5 → comate_cli-0.7.7}/.gitignore +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/CHANGELOG.md +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/README.md +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_picker_ui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_roundtrip.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_store_load.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/config/test_store_save.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/statusline/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/statusline/test_model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/statusline/test_picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/statusline/test_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_token_cost_config.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_goal_resume_tui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_goal_resume_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_goal_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_printer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_logo.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_startup_latency.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_thinking_display.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.7}/tests/test_usage_command.py +0 -0
|
@@ -7,7 +7,12 @@ import os
|
|
|
7
7
|
import signal
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
|
+
import time
|
|
10
11
|
import warnings
|
|
12
|
+
|
|
13
|
+
# 进程启动锚点:在任何重三方库 import(anthropic/prompt_toolkit/rich)之前捕获,
|
|
14
|
+
# 交给 StartupProfiler 测量「run() 之前的 import」这段以往不可见的启动成本。
|
|
15
|
+
_PROCESS_START_PERF = time.perf_counter()
|
|
11
16
|
try:
|
|
12
17
|
import termios
|
|
13
18
|
except ImportError:
|
|
@@ -268,7 +273,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
268
273
|
atexit.register(term_guard.restore, reason="atexit")
|
|
269
274
|
atexit.register(noise_guard.begin_shutdown)
|
|
270
275
|
|
|
276
|
+
_app_import_start_perf = time.perf_counter()
|
|
271
277
|
from comate_cli.terminal_agent.app import run
|
|
278
|
+
_app_import_done_perf = time.perf_counter()
|
|
272
279
|
|
|
273
280
|
try:
|
|
274
281
|
rpc_stdio, resume_session_id, resume_select, print_prompt = _parse_args(run_argv)
|
|
@@ -295,6 +302,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
295
302
|
resume_session_id=resume_session_id,
|
|
296
303
|
resume_select=resume_select,
|
|
297
304
|
print_message=print_message,
|
|
305
|
+
process_start_perf=_PROCESS_START_PERF,
|
|
306
|
+
app_import_start_perf=_app_import_start_perf,
|
|
307
|
+
app_import_done_perf=_app_import_done_perf,
|
|
298
308
|
)
|
|
299
309
|
)
|
|
300
310
|
except KeyboardInterrupt:
|
|
@@ -36,12 +36,15 @@ from comate_cli.terminal_agent.update_check import (
|
|
|
36
36
|
get_pending_update_prompt_info,
|
|
37
37
|
mark_update_seen,
|
|
38
38
|
record_skip_until_next_version,
|
|
39
|
+
record_update_check_attempt,
|
|
39
40
|
run_update_command,
|
|
41
|
+
should_check_for_update,
|
|
40
42
|
show_update_prompt,
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
console = Console()
|
|
44
46
|
logger = logging.getLogger(__name__)
|
|
47
|
+
_UPDATE_CHECK_DELAY_S = 1.0
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def _resolve_cli_project_root() -> Path:
|
|
@@ -69,7 +72,7 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
|
|
|
69
72
|
if decision == UpdatePromptDecision.SKIP:
|
|
70
73
|
return False
|
|
71
74
|
if decision == UpdatePromptDecision.SHOW_HINT:
|
|
72
|
-
console.print(format_update_hint(info))
|
|
75
|
+
console.print(f"[dim]{format_update_hint(info)}[/]")
|
|
73
76
|
mark_update_seen(info)
|
|
74
77
|
return False
|
|
75
78
|
|
|
@@ -86,7 +89,7 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
|
|
|
86
89
|
async def _handle_background_update_on_launch(
|
|
87
90
|
info: UpdateInfo,
|
|
88
91
|
*,
|
|
89
|
-
|
|
92
|
+
renderer: EventRenderer,
|
|
90
93
|
profiler: StartupProfiler,
|
|
91
94
|
) -> None:
|
|
92
95
|
try:
|
|
@@ -95,12 +98,12 @@ async def _handle_background_update_on_launch(
|
|
|
95
98
|
profiler.mark("update_check.skip")
|
|
96
99
|
return
|
|
97
100
|
if decision == UpdatePromptDecision.SHOW_HINT:
|
|
98
|
-
|
|
101
|
+
renderer.append_system_message(format_update_hint(info))
|
|
99
102
|
mark_update_seen(info)
|
|
100
103
|
profiler.mark("update_check.hint")
|
|
101
104
|
return
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
renderer.append_system_message(format_update_hint(info))
|
|
104
107
|
profiler.mark("update_check.prompt_deferred")
|
|
105
108
|
except Exception:
|
|
106
109
|
logger.debug("startup background update handling failed", exc_info=True)
|
|
@@ -109,18 +112,21 @@ async def _handle_background_update_on_launch(
|
|
|
109
112
|
|
|
110
113
|
def _schedule_update_check_on_launch(
|
|
111
114
|
*,
|
|
112
|
-
|
|
115
|
+
renderer: EventRenderer,
|
|
113
116
|
profiler: StartupProfiler,
|
|
114
117
|
) -> asyncio.Task[None]:
|
|
115
118
|
async def _run() -> None:
|
|
116
119
|
try:
|
|
117
120
|
profiler.mark("update_check.start")
|
|
121
|
+
profiler.mark("update_check.delay.start")
|
|
122
|
+
await asyncio.sleep(_UPDATE_CHECK_DELAY_S)
|
|
123
|
+
profiler.mark("update_check.delay.done")
|
|
118
124
|
update_info = await _check_update(profiler=profiler.child("update_check"))
|
|
119
125
|
profiler.mark("update_check.done")
|
|
120
126
|
if update_info is not None:
|
|
121
127
|
await _handle_background_update_on_launch(
|
|
122
128
|
update_info,
|
|
123
|
-
|
|
129
|
+
renderer=renderer,
|
|
124
130
|
profiler=profiler,
|
|
125
131
|
)
|
|
126
132
|
except asyncio.CancelledError:
|
|
@@ -436,9 +442,18 @@ async def run(
|
|
|
436
442
|
resume_session_id: str | None = None,
|
|
437
443
|
resume_select: bool = False,
|
|
438
444
|
print_message: str | None = None,
|
|
445
|
+
process_start_perf: float | None = None,
|
|
446
|
+
app_import_start_perf: float | None = None,
|
|
447
|
+
app_import_done_perf: float | None = None,
|
|
439
448
|
) -> None:
|
|
440
449
|
_install_event_loop_exception_handler()
|
|
441
|
-
|
|
450
|
+
# process_start_perf 由入口 main() 在模块 import 之前捕获,使 profiler 能测到
|
|
451
|
+
# 「run() 之前的重三方库 import」这段以往不可见的启动成本(柱子 A)。
|
|
452
|
+
profiler = StartupProfiler.from_env(logger=logger, started_at=process_start_perf)
|
|
453
|
+
if app_import_start_perf is not None:
|
|
454
|
+
profiler.mark_at("main.app_import.start", app_import_start_perf)
|
|
455
|
+
if app_import_done_perf is not None:
|
|
456
|
+
profiler.mark_at("main.app_import.done", app_import_done_perf)
|
|
442
457
|
profiler.mark("cli_project_root.start")
|
|
443
458
|
project_root = _resolve_cli_project_root()
|
|
444
459
|
profiler.mark("cli_project_root.done")
|
|
@@ -534,10 +549,16 @@ async def run(
|
|
|
534
549
|
tui = TerminalAgentTUI(session, status_bar, renderer)
|
|
535
550
|
profiler.mark("tui.init.done")
|
|
536
551
|
tui.add_resume_history(mode)
|
|
537
|
-
update_check_task =
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
552
|
+
update_check_task: asyncio.Task[None] | None = None
|
|
553
|
+
if should_check_for_update():
|
|
554
|
+
# attempt 语义:先记录尝试时间,确保 interval 内最多发起一次网络检查
|
|
555
|
+
record_update_check_attempt()
|
|
556
|
+
update_check_task = _schedule_update_check_on_launch(
|
|
557
|
+
renderer=renderer,
|
|
558
|
+
profiler=profiler,
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
profiler.mark("update_check.throttled")
|
|
541
562
|
|
|
542
563
|
async def _mcp_loader() -> None:
|
|
543
564
|
await _preload_mcp_in_tui(session, profiler=profiler.child("mcp"))
|
|
@@ -565,11 +586,12 @@ async def run(
|
|
|
565
586
|
)
|
|
566
587
|
finally:
|
|
567
588
|
try:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
589
|
+
if update_check_task is not None:
|
|
590
|
+
await _cancel_background_task(
|
|
591
|
+
update_check_task,
|
|
592
|
+
timeout_s=0.1,
|
|
593
|
+
task_name="terminal-update-check",
|
|
594
|
+
)
|
|
573
595
|
if active_session is session:
|
|
574
596
|
await _graceful_shutdown(active_session)
|
|
575
597
|
else:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
|
|
7
8
|
|
|
@@ -17,40 +18,65 @@ class StartupProfiler:
|
|
|
17
18
|
started_at: float | None = None,
|
|
18
19
|
prefix: str = "",
|
|
19
20
|
pending_messages: list[str] | None = None,
|
|
21
|
+
lock: threading.Lock | None = None,
|
|
20
22
|
) -> None:
|
|
21
23
|
self._logger = logger
|
|
22
24
|
self._enabled = enabled
|
|
23
25
|
self._started_at = started_at if started_at is not None else time.perf_counter()
|
|
24
26
|
self._prefix = prefix.strip(".")
|
|
25
27
|
self._pending_messages = pending_messages if pending_messages is not None else []
|
|
28
|
+
self._lock = lock if lock is not None else threading.Lock()
|
|
26
29
|
|
|
27
30
|
@classmethod
|
|
28
|
-
def from_env(
|
|
31
|
+
def from_env(
|
|
32
|
+
cls,
|
|
33
|
+
*,
|
|
34
|
+
logger: logging.Logger,
|
|
35
|
+
started_at: float | None = None,
|
|
36
|
+
) -> "StartupProfiler":
|
|
29
37
|
raw_value = os.environ.get("COMATE_STARTUP_PROFILE", "")
|
|
30
38
|
enabled = raw_value.strip().lower() in _TRUTHY_VALUES
|
|
31
|
-
return cls(logger=logger, enabled=enabled)
|
|
39
|
+
return cls(logger=logger, enabled=enabled, started_at=started_at)
|
|
32
40
|
|
|
33
41
|
def mark(self, phase: str) -> None:
|
|
42
|
+
self.mark_at(phase, time.perf_counter())
|
|
43
|
+
|
|
44
|
+
def mark_at(self, phase: str, perf_value: float) -> None:
|
|
45
|
+
"""记录一个发生在指定 perf_counter() 时刻的阶段。
|
|
46
|
+
|
|
47
|
+
用于补记 profiler 创建之前(如 main 入口的模块 import)已经测好的时间点,
|
|
48
|
+
elapsed_ms 仍以 started_at 为锚点,保持与 mark() 同一坐标系。
|
|
49
|
+
"""
|
|
34
50
|
if not self._enabled:
|
|
35
51
|
return
|
|
36
52
|
normalized_phase = phase.strip(".")
|
|
37
53
|
if self._prefix:
|
|
38
54
|
normalized_phase = f"{self._prefix}.{normalized_phase}"
|
|
39
|
-
elapsed_ms = (
|
|
40
|
-
self.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
elapsed_ms = (perf_value - self._started_at) * 1000
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._pending_messages.append(
|
|
58
|
+
f"startup_profile phase={normalized_phase} elapsed_ms={elapsed_ms:.3f}"
|
|
59
|
+
)
|
|
60
|
+
pending = self._drain_pending_locked()
|
|
61
|
+
for message in pending:
|
|
62
|
+
self._logger.info(message)
|
|
44
63
|
|
|
45
64
|
def flush(self) -> None:
|
|
46
|
-
if not self._enabled
|
|
65
|
+
if not self._enabled:
|
|
47
66
|
return
|
|
67
|
+
with self._lock:
|
|
68
|
+
pending = self._drain_pending_locked()
|
|
69
|
+
for message in pending:
|
|
70
|
+
self._logger.info(message)
|
|
71
|
+
|
|
72
|
+
def _drain_pending_locked(self) -> list[str]:
|
|
73
|
+
if not self._pending_messages:
|
|
74
|
+
return []
|
|
48
75
|
if not self._logger.isEnabledFor(logging.INFO):
|
|
49
|
-
return
|
|
76
|
+
return []
|
|
50
77
|
pending = list(self._pending_messages)
|
|
51
78
|
self._pending_messages.clear()
|
|
52
|
-
|
|
53
|
-
self._logger.info(message)
|
|
79
|
+
return pending
|
|
54
80
|
|
|
55
81
|
def child(self, prefix: str) -> "StartupProfiler":
|
|
56
82
|
normalized_prefix = prefix.strip(".")
|
|
@@ -64,4 +90,5 @@ class StartupProfiler:
|
|
|
64
90
|
started_at=self._started_at,
|
|
65
91
|
prefix=normalized_prefix,
|
|
66
92
|
pending_messages=self._pending_messages,
|
|
93
|
+
lock=self._lock,
|
|
67
94
|
)
|
|
@@ -5,6 +5,9 @@ import importlib.metadata
|
|
|
5
5
|
import locale
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
+
import ssl
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
8
11
|
from dataclasses import dataclass
|
|
9
12
|
from enum import Enum
|
|
10
13
|
from pathlib import Path
|
|
@@ -24,6 +27,93 @@ PACKAGE_NAME = "comate-cli"
|
|
|
24
27
|
RELEASE_NOTES_URL = "https://github.com/AndyLee1024/agent-sdk/releases/latest"
|
|
25
28
|
UPDATE_COMMAND = ("uv", "tool", "update", PACKAGE_NAME)
|
|
26
29
|
_SETTINGS_SECTION = "updates"
|
|
30
|
+
_UPDATE_CHECK_TRUST_ENV = "COMATE_CLI_UPDATE_CHECK_TRUST_ENV"
|
|
31
|
+
_TRUTHY_ENV_VALUES = {"1", "true", "yes", "on"}
|
|
32
|
+
|
|
33
|
+
# 启动后台更新检查限频:默认 24h 内只查一次,避免每次启动都付网络 + httpx 构造成本。
|
|
34
|
+
# 设为 0(或负数)表示禁用限频,每次启动都检查。
|
|
35
|
+
_LAST_CHECK_AT_KEY = "last_check_at"
|
|
36
|
+
_UPDATE_CHECK_INTERVAL_ENV = "COMATE_CLI_UPDATE_CHECK_INTERVAL_S"
|
|
37
|
+
_DEFAULT_UPDATE_CHECK_INTERVAL_S = 24 * 60 * 60
|
|
38
|
+
|
|
39
|
+
# certifi 预建的 SSL context(进程级复用)。默认 verify=True 会让 httpx 走
|
|
40
|
+
# ssl.create_default_context() → 在 Windows 上枚举系统证书库,实测可达数秒;
|
|
41
|
+
# 显式传 cafile 则不触发 load_default_certs(),构造降到几十毫秒。
|
|
42
|
+
_update_check_ssl_context: ssl.SSLContext | None = None
|
|
43
|
+
_ssl_context_lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_update_check_ssl_context() -> ssl.SSLContext | None:
|
|
47
|
+
"""惰性构建并复用 certifi SSL context;失败返回 None(回退 httpx 默认行为)。"""
|
|
48
|
+
global _update_check_ssl_context
|
|
49
|
+
if _update_check_ssl_context is not None:
|
|
50
|
+
return _update_check_ssl_context
|
|
51
|
+
with _ssl_context_lock:
|
|
52
|
+
if _update_check_ssl_context is None:
|
|
53
|
+
try:
|
|
54
|
+
import certifi
|
|
55
|
+
|
|
56
|
+
_update_check_ssl_context = ssl.create_default_context(
|
|
57
|
+
cafile=certifi.where()
|
|
58
|
+
)
|
|
59
|
+
except Exception:
|
|
60
|
+
logger.debug(
|
|
61
|
+
"failed to build certifi SSL context for update check",
|
|
62
|
+
exc_info=True,
|
|
63
|
+
)
|
|
64
|
+
return None
|
|
65
|
+
return _update_check_ssl_context
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_update_check_interval_s() -> float:
|
|
69
|
+
raw = os.environ.get(_UPDATE_CHECK_INTERVAL_ENV, "").strip()
|
|
70
|
+
if not raw:
|
|
71
|
+
return float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
|
|
72
|
+
try:
|
|
73
|
+
value = float(raw)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
|
|
76
|
+
return value if value >= 0 else float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def should_check_for_update(
|
|
80
|
+
*,
|
|
81
|
+
package: str = PACKAGE_NAME,
|
|
82
|
+
settings_path: Path | None = None,
|
|
83
|
+
now: float | None = None,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""启动时是否应执行网络版本检查(限频判断,纯本地 IO)。"""
|
|
86
|
+
interval_s = _resolve_update_check_interval_s()
|
|
87
|
+
if interval_s <= 0:
|
|
88
|
+
return True
|
|
89
|
+
state = _load_package_update_state(package, settings_path=settings_path)
|
|
90
|
+
try:
|
|
91
|
+
last_check_at = float(state.get(_LAST_CHECK_AT_KEY)) # type: ignore[arg-type]
|
|
92
|
+
except (TypeError, ValueError):
|
|
93
|
+
return True
|
|
94
|
+
current = now if now is not None else time.time()
|
|
95
|
+
if current < last_check_at:
|
|
96
|
+
# 时钟回拨:视为需要重新检查
|
|
97
|
+
return True
|
|
98
|
+
return (current - last_check_at) >= interval_s
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def record_update_check_attempt(
|
|
102
|
+
*,
|
|
103
|
+
package: str = PACKAGE_NAME,
|
|
104
|
+
settings_path: Path | None = None,
|
|
105
|
+
now: float | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""记录一次更新检查尝试时间(attempt 语义:每个 interval 内最多一次网络请求)。"""
|
|
108
|
+
current = now if now is not None else time.time()
|
|
109
|
+
try:
|
|
110
|
+
_write_package_update_state(
|
|
111
|
+
package,
|
|
112
|
+
{_LAST_CHECK_AT_KEY: current},
|
|
113
|
+
settings_path=settings_path,
|
|
114
|
+
)
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.debug("failed to record update check attempt", exc_info=True)
|
|
27
117
|
|
|
28
118
|
|
|
29
119
|
class UpdatePromptDecision(Enum):
|
|
@@ -65,6 +155,11 @@ def _is_chinese_locale() -> bool:
|
|
|
65
155
|
return False
|
|
66
156
|
|
|
67
157
|
|
|
158
|
+
def _update_check_trust_env() -> bool:
|
|
159
|
+
raw = os.environ.get(_UPDATE_CHECK_TRUST_ENV, "")
|
|
160
|
+
return raw.strip().lower() in _TRUTHY_ENV_VALUES
|
|
161
|
+
|
|
162
|
+
|
|
68
163
|
async def check_update(
|
|
69
164
|
*,
|
|
70
165
|
package: str = PACKAGE_NAME,
|
|
@@ -73,77 +168,143 @@ async def check_update(
|
|
|
73
168
|
profiler: Any | None = None,
|
|
74
169
|
) -> UpdateInfo | None:
|
|
75
170
|
"""异步检查 comate-cli 是否有新版本,返回结构化版本信息或 None。"""
|
|
76
|
-
|
|
171
|
+
if profiler is not None:
|
|
172
|
+
profiler.mark("thread.await.start")
|
|
77
173
|
try:
|
|
174
|
+
return await asyncio.to_thread(
|
|
175
|
+
check_update_blocking,
|
|
176
|
+
package=package,
|
|
177
|
+
release_notes_url=release_notes_url,
|
|
178
|
+
log=log,
|
|
179
|
+
profiler=profiler,
|
|
180
|
+
)
|
|
181
|
+
except asyncio.CancelledError:
|
|
78
182
|
if profiler is not None:
|
|
79
|
-
profiler.mark("
|
|
80
|
-
|
|
81
|
-
except
|
|
183
|
+
profiler.mark("thread.await.cancelled")
|
|
184
|
+
raise
|
|
185
|
+
except Exception:
|
|
186
|
+
if profiler is not None:
|
|
187
|
+
profiler.mark("thread.await.failed")
|
|
188
|
+
active_logger = log or logger
|
|
189
|
+
active_logger.debug("update check worker failed", exc_info=True)
|
|
82
190
|
return None
|
|
83
191
|
finally:
|
|
84
192
|
if profiler is not None:
|
|
85
|
-
profiler.mark("
|
|
193
|
+
profiler.mark("thread.await.done")
|
|
86
194
|
|
|
87
|
-
if profiler is not None:
|
|
88
|
-
profiler.mark("locale.start")
|
|
89
|
-
if _is_chinese_locale():
|
|
90
|
-
url = f"https://mirrors.tuna.tsinghua.edu.cn/pypi/{package}/json"
|
|
91
|
-
source_label = "tuna"
|
|
92
|
-
else:
|
|
93
|
-
url = f"https://pypi.org/pypi/{package}/json"
|
|
94
|
-
source_label = "pypi"
|
|
95
|
-
if profiler is not None:
|
|
96
|
-
profiler.mark("locale.done")
|
|
97
195
|
|
|
196
|
+
def check_update_blocking(
|
|
197
|
+
*,
|
|
198
|
+
package: str = PACKAGE_NAME,
|
|
199
|
+
release_notes_url: str = RELEASE_NOTES_URL,
|
|
200
|
+
log: logging.Logger | None = None,
|
|
201
|
+
profiler: Any | None = None,
|
|
202
|
+
) -> UpdateInfo | None:
|
|
203
|
+
"""同步执行版本检查;由 async wrapper 放入 worker thread,避免阻塞 TUI event loop。"""
|
|
204
|
+
active_logger = log or logger
|
|
98
205
|
try:
|
|
99
206
|
if profiler is not None:
|
|
100
|
-
profiler.mark("
|
|
101
|
-
import httpx
|
|
102
|
-
if profiler is not None:
|
|
103
|
-
profiler.mark("httpx_import.done")
|
|
207
|
+
profiler.mark("thread.run.start")
|
|
104
208
|
|
|
105
|
-
|
|
209
|
+
try:
|
|
106
210
|
if profiler is not None:
|
|
107
|
-
profiler.mark("
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
211
|
+
profiler.mark("current_version.start")
|
|
212
|
+
current = importlib.metadata.version(package)
|
|
213
|
+
except importlib.metadata.PackageNotFoundError:
|
|
214
|
+
return None
|
|
215
|
+
finally:
|
|
111
216
|
if profiler is not None:
|
|
112
|
-
profiler.mark("
|
|
113
|
-
except Exception:
|
|
114
|
-
active_logger.debug(f"update check failed (source={source_label})", exc_info=True)
|
|
115
|
-
return None
|
|
217
|
+
profiler.mark("current_version.done")
|
|
116
218
|
|
|
117
|
-
try:
|
|
118
219
|
if profiler is not None:
|
|
119
|
-
profiler.mark("
|
|
120
|
-
|
|
220
|
+
profiler.mark("locale.start")
|
|
221
|
+
if _is_chinese_locale():
|
|
222
|
+
url = f"https://mirrors.tuna.tsinghua.edu.cn/pypi/{package}/json"
|
|
223
|
+
source_label = "tuna"
|
|
224
|
+
else:
|
|
225
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
226
|
+
source_label = "pypi"
|
|
227
|
+
if profiler is not None:
|
|
228
|
+
profiler.mark("locale.done")
|
|
121
229
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
230
|
+
try:
|
|
231
|
+
if profiler is not None:
|
|
232
|
+
profiler.mark("httpx_import.start")
|
|
233
|
+
import httpx
|
|
234
|
+
if profiler is not None:
|
|
235
|
+
profiler.mark("httpx_import.done")
|
|
236
|
+
|
|
237
|
+
trust_env = _update_check_trust_env()
|
|
238
|
+
if profiler is not None:
|
|
239
|
+
profiler.mark(
|
|
240
|
+
"http_client.trust_env.enabled"
|
|
241
|
+
if trust_env
|
|
242
|
+
else "http_client.trust_env.disabled"
|
|
243
|
+
)
|
|
244
|
+
profiler.mark("http_client.construct.start")
|
|
245
|
+
client_kwargs: dict[str, Any] = {"timeout": 3.0, "trust_env": trust_env}
|
|
246
|
+
if not trust_env:
|
|
247
|
+
# 默认路径用 certifi context 绕开 Windows 证书库枚举(见 _get_update_check_ssl_context)。
|
|
248
|
+
# trust_env 逃生通道(企业代理/自签根证书)保持 httpx 默认 verify=True 行为不变。
|
|
249
|
+
ssl_context = _get_update_check_ssl_context()
|
|
250
|
+
if ssl_context is not None:
|
|
251
|
+
client_kwargs["verify"] = ssl_context
|
|
252
|
+
client = httpx.Client(**client_kwargs)
|
|
253
|
+
if profiler is not None:
|
|
254
|
+
profiler.mark("http_client.construct.done")
|
|
255
|
+
|
|
256
|
+
if profiler is not None:
|
|
257
|
+
profiler.mark("http_client.enter.start")
|
|
258
|
+
with client:
|
|
259
|
+
if profiler is not None:
|
|
260
|
+
profiler.mark("http_client.enter.done")
|
|
261
|
+
profiler.mark("http_get.start")
|
|
262
|
+
resp = client.get(url)
|
|
263
|
+
resp.raise_for_status()
|
|
264
|
+
latest = resp.json()["info"]["version"]
|
|
265
|
+
if profiler is not None:
|
|
266
|
+
profiler.mark("http_get.done")
|
|
267
|
+
except Exception:
|
|
268
|
+
if profiler is not None:
|
|
269
|
+
profiler.mark("http_client.failed")
|
|
270
|
+
active_logger.debug(f"update check failed (source={source_label})", exc_info=True)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
if profiler is not None:
|
|
275
|
+
profiler.mark("version_compare.start")
|
|
276
|
+
from packaging.version import Version
|
|
277
|
+
|
|
278
|
+
if Version(latest) > Version(current):
|
|
279
|
+
return UpdateInfo(
|
|
280
|
+
package=package,
|
|
281
|
+
current_version=current,
|
|
282
|
+
latest_version=latest,
|
|
283
|
+
release_notes_url=release_notes_url,
|
|
284
|
+
)
|
|
285
|
+
except Exception:
|
|
286
|
+
active_logger.debug(
|
|
287
|
+
"update comparison failed (current=%s, latest=%s)",
|
|
288
|
+
current,
|
|
289
|
+
latest,
|
|
290
|
+
exc_info=True,
|
|
128
291
|
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
latest,
|
|
134
|
-
exc_info=True,
|
|
135
|
-
)
|
|
292
|
+
finally:
|
|
293
|
+
if profiler is not None:
|
|
294
|
+
profiler.mark("version_compare.done")
|
|
295
|
+
return None
|
|
136
296
|
finally:
|
|
137
297
|
if profiler is not None:
|
|
138
|
-
profiler.mark("
|
|
139
|
-
return None
|
|
298
|
+
profiler.mark("thread.run.done")
|
|
140
299
|
|
|
141
300
|
|
|
142
301
|
def format_update_hint(info: UpdateInfo) -> str:
|
|
302
|
+
# Returns plain text only. Callers are responsible for adding markup
|
|
303
|
+
# appropriate to their render target (Rich Console vs prompt_toolkit).
|
|
143
304
|
return (
|
|
144
|
-
f"
|
|
305
|
+
f"✨ New version available: {info.latest_version} "
|
|
145
306
|
f"(current: {info.current_version}) "
|
|
146
|
-
f"Run
|
|
307
|
+
f"Run `uv tool update {info.package}` to update."
|
|
147
308
|
)
|
|
148
309
|
|
|
149
310
|
|
|
@@ -367,7 +528,7 @@ def _load_package_update_state(
|
|
|
367
528
|
|
|
368
529
|
def _write_package_update_state(
|
|
369
530
|
package: str,
|
|
370
|
-
values: dict[str,
|
|
531
|
+
values: dict[str, Any],
|
|
371
532
|
*,
|
|
372
533
|
settings_path: Path | None,
|
|
373
534
|
) -> None:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _ensure_cli_package_on_path() -> None:
|
|
10
|
+
cli_project_root = Path(__file__).resolve().parents[1]
|
|
11
|
+
cli_project_root_str = str(cli_project_root)
|
|
12
|
+
if cli_project_root_str not in sys.path:
|
|
13
|
+
sys.path.insert(0, cli_project_root_str)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_ensure_cli_package_on_path()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(autouse=True)
|
|
20
|
+
def _isolate_update_check_settings(tmp_path, monkeypatch):
|
|
21
|
+
"""每个测试用独立临时 settings.json,隔离启动更新检查的限频状态。
|
|
22
|
+
|
|
23
|
+
update_check 的限频(last_check_at)与 last_seen_version 都落在 USER_SETTINGS_PATH。
|
|
24
|
+
驱动 run()/scheduler 的集成测试若写到真实用户配置,会造成跨用例顺序相关的脏状态。
|
|
25
|
+
在这里把模块级 USER_SETTINGS_PATH 指向临时目录,统一隔离。
|
|
26
|
+
显式传 settings_path 的单测不受影响。
|
|
27
|
+
"""
|
|
28
|
+
from comate_cli.terminal_agent import update_check
|
|
29
|
+
|
|
30
|
+
monkeypatch.setattr(
|
|
31
|
+
update_check, "USER_SETTINGS_PATH", tmp_path / "settings.json"
|
|
32
|
+
)
|
|
33
|
+
yield
|