railmux 0.4.0.dev27__tar.gz → 0.4.0.dev28__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.
Files changed (181) hide show
  1. {railmux-0.4.0.dev27/src/railmux.egg-info → railmux-0.4.0.dev28}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev28/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_install_log.py +12 -0
  4. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_msys2.py +155 -23
  5. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_install_log.py +16 -0
  7. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_msys2.py +110 -1
  8. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tools/windows_archive_smoke.py +18 -0
  9. railmux-0.4.0.dev27/src/railmux/__init__.py +0 -1
  10. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/LICENSE +0 -0
  11. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/MANIFEST.in +0 -0
  12. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/README.md +0 -0
  13. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/pyproject.toml +0 -0
  14. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/setup.cfg +0 -0
  15. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/__main__.py +0 -0
  16. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/atomic_file.py +0 -0
  17. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/background_index.py +0 -0
  18. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/cli.py +0 -0
  19. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/codex_index.py +0 -0
  20. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/config.py +0 -0
  21. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/config_cli.py +0 -0
  22. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fast_display_client.py +0 -0
  27. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fast_display_server.py +0 -0
  31. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/favorites.py +0 -0
  32. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/fuzzy.py +0 -0
  33. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/help_workspace.py +0 -0
  34. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/launcher.py +0 -0
  35. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/local_open.py +0 -0
  38. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/models.py +0 -0
  39. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/modes.py +0 -0
  40. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/provider_paths.py +0 -0
  45. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/remote_config.py +0 -0
  46. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/renames.py +0 -0
  47. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/restart_state.py +0 -0
  48. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/runtime_config.py +0 -0
  49. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/scroll_agent.py +0 -0
  50. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/scroll_manager.py +0 -0
  51. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/selection_isolation.py +0 -0
  52. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/self_update.py +0 -0
  53. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/session_cache.py +0 -0
  54. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/session_index.py +0 -0
  55. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/session_lease.py +0 -0
  56. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/setting_contracts.py +0 -0
  57. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/settings.py +0 -0
  58. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ssh_args.py +0 -0
  59. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ssh_compat.py +0 -0
  60. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ssh_display_diagnostics.py +0 -0
  61. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ssh_doctor.py +0 -0
  62. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/system_deps.py +0 -0
  63. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/terminal_status.py +0 -0
  64. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/tmux_binding_manager.py +0 -0
  65. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/tmux_ctl.py +0 -0
  66. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/tmux_health.py +0 -0
  67. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/tmux_server.py +0 -0
  68. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/tool_panes.py +0 -0
  69. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/transcript.py +0 -0
  70. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/__init__.py +0 -0
  71. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/_widgets.py +0 -0
  72. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/app.py +0 -0
  73. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/keymap.py +0 -0
  74. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/modals.py +0 -0
  75. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_attach_relay.py +0 -0
  82. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_bootstrap.py +0 -0
  83. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_pacman.py +0 -0
  84. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_tmux_lifecycle.py +0 -0
  85. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux/windows_ui_transition.py +0 -0
  86. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux.egg-info/SOURCES.txt +0 -0
  87. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux.egg-info/dependency_links.txt +0 -0
  88. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux.egg-info/entry_points.txt +0 -0
  89. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux.egg-info/requires.txt +0 -0
  90. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/src/railmux.egg-info/top_level.txt +0 -0
  91. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_agent_workspace.py +0 -0
  92. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_atomic_file.py +0 -0
  93. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_background_index.py +0 -0
  94. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_cli.py +0 -0
  95. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_codex_delete.py +0 -0
  96. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_codex_index.py +0 -0
  97. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_codex_lineage.py +0 -0
  98. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_codex_transcript.py +0 -0
  99. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_config.py +0 -0
  100. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_config_cli.py +0 -0
  101. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_denested_transport_benchmark.py +0 -0
  102. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_diagnostics.py +0 -0
  103. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_discovery.py +0 -0
  104. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_display_transport.py +0 -0
  105. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_fast_full_window.py +0 -0
  106. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_focus_highlight.py +0 -0
  107. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_fullscreen_binding.py +0 -0
  108. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_fuzzy.py +0 -0
  109. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_help_agent.py +0 -0
  110. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_help_workspace.py +0 -0
  111. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_history_cleanup.py +0 -0
  112. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_keymap.py +0 -0
  113. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_launcher.py +0 -0
  114. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_layout_profile.py +0 -0
  115. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_legacy_sessions.py +0 -0
  116. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_local_clipboard.py +0 -0
  117. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_local_open.py +0 -0
  118. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_modals.py +0 -0
  119. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_mode_selection.py +0 -0
  120. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_modes.py +0 -0
  121. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_mouse_manager.py +0 -0
  122. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_new_project.py +0 -0
  123. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_options.py +0 -0
  124. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_orphan_recovery.py +0 -0
  125. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_pane_surface.py +0 -0
  126. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_path_codec.py +0 -0
  127. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_projects_pane.py +0 -0
  128. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_provider_paths.py +0 -0
  129. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_release_notes.py +0 -0
  130. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_release_workflow.py +0 -0
  131. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_remote_config.py +0 -0
  132. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_renames.py +0 -0
  133. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_restart_state.py +0 -0
  134. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_running_pane.py +0 -0
  135. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_running_sort.py +0 -0
  136. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_runtime_config.py +0 -0
  137. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_scroll_agent.py +0 -0
  138. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_scroll_manager.py +0 -0
  139. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_selection_isolation.py +0 -0
  140. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_self_update.py +0 -0
  141. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_session_cache.py +0 -0
  142. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_session_index.py +0 -0
  143. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_session_lease.py +0 -0
  144. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_sessions_pane.py +0 -0
  145. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_setting_contracts.py +0 -0
  146. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_settings.py +0 -0
  147. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_sidebar.py +0 -0
  148. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_soft_quit.py +0 -0
  149. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_ssh_args.py +0 -0
  150. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_ssh_compat.py +0 -0
  151. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_ssh_display_benchmark.py +0 -0
  152. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_ssh_display_diagnostics.py +0 -0
  153. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_ssh_doctor.py +0 -0
  154. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_status.py +0 -0
  155. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_statusbar.py +0 -0
  156. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_system_deps.py +0 -0
  157. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_terminal_size.py +0 -0
  158. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_terminal_status.py +0 -0
  159. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_termux_local_touch.py +0 -0
  160. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_binding_manager.py +0 -0
  161. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_ctl.py +0 -0
  162. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_health.py +0 -0
  163. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_integration.py +0 -0
  164. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_server.py +0 -0
  165. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tmux_status.py +0 -0
  166. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_tool_panes.py +0 -0
  167. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_transcript.py +0 -0
  168. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_utf8_state.py +0 -0
  169. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_web_demo_recorder.py +0 -0
  170. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_wheel_smoke.py +0 -0
  171. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_widgets.py +0 -0
  172. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_attach_relay.py +0 -0
  173. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_bootstrap.py +0 -0
  174. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_pacman.py +0 -0
  175. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_tmux_lifecycle.py +0 -0
  176. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_ui_transition.py +0 -0
  177. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_windows_wrapper_contract.py +0 -0
  178. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tests/test_yolo_prompt.py +0 -0
  179. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tools/denested_transport_benchmark.py +0 -0
  180. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tools/release_notes.py +0 -0
  181. {railmux-0.4.0.dev27 → railmux-0.4.0.dev28}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev27
3
+ Version: 0.4.0.dev28
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev28"
@@ -56,6 +56,10 @@ _PIP_NETWORK_ERROR_MARKERS = (
56
56
  "connection aborted",
57
57
  "temporary failure in name resolution",
58
58
  )
59
+ _MSYS2_RESTART_MARKER = (
60
+ "to complete this update all msys2 processes including this terminal "
61
+ "will be closed"
62
+ )
59
63
  _EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
60
64
  _PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
61
65
  _PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
@@ -142,6 +146,7 @@ class InstallReporter:
142
146
  self._network_failure = False
143
147
  self._pip_network_failure = False
144
148
  self._hard_failed_hosts: set[str] = set()
149
+ self._msys2_restart_requested = False
145
150
 
146
151
  def __enter__(self) -> InstallReporter:
147
152
  self.path.parent.mkdir(parents=True, exist_ok=True)
@@ -209,6 +214,7 @@ class InstallReporter:
209
214
  self._network_failure = False
210
215
  self._pip_network_failure = False
211
216
  self._hard_failed_hosts.clear()
217
+ self._msys2_restart_requested = False
212
218
  self._write_log(f"\n--- {label} ---\n")
213
219
 
214
220
  def command_output(self, output: bytes | str | None) -> None:
@@ -259,6 +265,8 @@ class InstallReporter:
259
265
  if line.strip():
260
266
  self._tail.append(line)
261
267
  lowered = line.lower()
268
+ if _MSYS2_RESTART_MARKER in lowered:
269
+ self._msys2_restart_requested = True
262
270
  if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
263
271
  self._network_failure = True
264
272
  if any(marker in lowered for marker in _PIP_NETWORK_ERROR_MARKERS):
@@ -364,6 +372,10 @@ class InstallReporter:
364
372
  def hard_failed_mirror_hosts(self) -> frozenset[str]:
365
373
  return frozenset(self._hard_failed_hosts)
366
374
 
375
+ @property
376
+ def command_requested_msys2_restart(self) -> bool:
377
+ return self._msys2_restart_requested
378
+
367
379
  def heartbeat(self) -> None:
368
380
  now = self._clock()
369
381
  if now - self._last_heartbeat < _HEARTBEAT_SECONDS:
@@ -119,6 +119,10 @@ _PACKAGE_URL_COMMAND = (
119
119
  _PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
120
120
  _PACKAGE_INVENTORY_LIMIT = 1024 * 1024
121
121
  _CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
122
+ _MAX_BASE_UPDATE_PASSES = 3
123
+ _PRIVATE_GPG_SHUTDOWN_COMMAND = (
124
+ "gpgconf --homedir /etc/pacman.d/gnupg --kill all"
125
+ )
122
126
  _IN_USE_APPS_COMMAND = (
123
127
  "for f in /proc/[0-9]*/cmdline; do "
124
128
  "[ -r \"$f\" ] || continue; tr '\\0' '\\n' <\"$f\"; done"
@@ -1624,18 +1628,23 @@ def _run_checked(
1624
1628
  if returncode:
1625
1629
  if allow_failure:
1626
1630
  return returncode
1627
- if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
1628
- reporter.note(
1629
- "Windows reported STATUS_DLL_NOT_FOUND. Check whether security "
1630
- "software quarantined a file in the private MSYS2 staging tree.",
1631
- level="error",
1632
- )
1631
+ _explain_windows_status(returncode, reporter)
1633
1632
  reporter.command_failed(label, returncode)
1634
1633
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1635
1634
  reporter.command_succeeded()
1636
1635
  return 0
1637
1636
 
1638
1637
 
1638
+ def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
1639
+ if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
1640
+ reporter.note(
1641
+ "Windows reported STATUS_DLL_NOT_FOUND outside a confirmed MSYS2 "
1642
+ "core-update restart. Check whether security software quarantined "
1643
+ "a file in the private MSYS2 staging tree.",
1644
+ level="error",
1645
+ )
1646
+
1647
+
1639
1648
  def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1640
1649
  timeout = " --disable-download-timeout" if relaxed else ""
1641
1650
  needed = f" --needed {_PACMAN_PACKAGES}" if packages else ""
@@ -1644,10 +1653,11 @@ def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1644
1653
  if not packages
1645
1654
  else ""
1646
1655
  )
1656
+ operation = "-Syu" if packages else "-Syuu"
1647
1657
  return (
1648
1658
  'cache=$(cygpath -u "$1") && mkdir -p "$cache" && '
1649
1659
  f"{keyring}pacman --config {_PACMAN_CONFIG} --cachedir \"$cache\" "
1650
- f"-Syu --noconfirm{timeout}{needed}"
1660
+ f"{operation} --noconfirm{timeout}{needed}"
1651
1661
  )
1652
1662
 
1653
1663
 
@@ -1693,7 +1703,8 @@ def _run_pacman_with_recovery(
1693
1703
  runner: Runner | None,
1694
1704
  label: str,
1695
1705
  mirror_optimizer: MirrorOptimizer,
1696
- ) -> None:
1706
+ allow_core_restart: bool = False,
1707
+ ) -> bool:
1697
1708
  argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
1698
1709
  returncode = _run_checked(
1699
1710
  argv,
@@ -1704,11 +1715,20 @@ def _run_pacman_with_recovery(
1704
1715
  progress="pacman",
1705
1716
  allow_failure=True,
1706
1717
  )
1718
+ restart_requested = reporter.command_requested_msys2_restart
1707
1719
  if returncode == 0:
1708
- return
1720
+ return restart_requested
1721
+ if allow_core_restart and not packages and restart_requested:
1722
+ reporter.note(
1723
+ "MSYS2 replaced core runtime files and closed its own process as "
1724
+ "expected; continuing from a fresh Windows-launched shell.",
1725
+ level="warning",
1726
+ )
1727
+ return True
1709
1728
  network_failure = reporter.command_had_network_failure
1710
1729
  hard_failed_hosts = reporter.hard_failed_mirror_hosts
1711
1730
  if not network_failure:
1731
+ _explain_windows_status(returncode, reporter)
1712
1732
  reporter.command_failed(label, returncode)
1713
1733
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1714
1734
  reporter.note(
@@ -1739,13 +1759,112 @@ def _run_pacman_with_recovery(
1739
1759
  _pacman_command(packages=packages, relaxed=True),
1740
1760
  str(cache),
1741
1761
  )
1742
- _run_checked(
1762
+ retry_returncode = _run_checked(
1743
1763
  retry_argv,
1744
1764
  env=env,
1745
1765
  reporter=reporter,
1746
1766
  runner=runner,
1747
1767
  label=f"{label} resilient retry",
1748
1768
  progress="pacman",
1769
+ allow_failure=True,
1770
+ )
1771
+ restart_requested = reporter.command_requested_msys2_restart
1772
+ if retry_returncode == 0:
1773
+ return restart_requested
1774
+ if allow_core_restart and not packages and restart_requested:
1775
+ reporter.note(
1776
+ "MSYS2 replaced core runtime files and closed its own process as "
1777
+ "expected; continuing from a fresh Windows-launched shell.",
1778
+ level="warning",
1779
+ )
1780
+ return True
1781
+ _explain_windows_status(retry_returncode, reporter)
1782
+ reporter.command_failed(f"{label} resilient retry", retry_returncode)
1783
+ raise RuntimeInstallError(
1784
+ f"{label} resilient retry failed with exit code {retry_returncode}"
1785
+ )
1786
+
1787
+
1788
+ def _run_base_update_with_restarts(
1789
+ root: Path,
1790
+ *,
1791
+ cache: Path,
1792
+ env: Mapping[str, str],
1793
+ reporter: InstallReporter,
1794
+ runner: Runner | None,
1795
+ mirror_optimizer: MirrorOptimizer,
1796
+ ) -> None:
1797
+ """Run MSYS2's full upgrade in fresh processes until restart-safe."""
1798
+ try:
1799
+ for pass_number in range(1, _MAX_BASE_UPDATE_PASSES + 1):
1800
+ restart_requested = _run_pacman_with_recovery(
1801
+ root,
1802
+ packages=False,
1803
+ cache=cache,
1804
+ env=env,
1805
+ reporter=reporter,
1806
+ runner=runner,
1807
+ label=f"MSYS2 base update pass {pass_number}",
1808
+ mirror_optimizer=mirror_optimizer,
1809
+ allow_core_restart=pass_number < _MAX_BASE_UPDATE_PASSES,
1810
+ )
1811
+ if restart_requested:
1812
+ reporter.note(
1813
+ "Starting a new MSYS2 process for the remaining update…"
1814
+ )
1815
+ continue
1816
+ if pass_number == 1:
1817
+ # MSYS2's supported unattended update procedure invokes Syuu
1818
+ # twice, even when the first pass did not request a restart.
1819
+ reporter.note(
1820
+ "Starting the required second MSYS2 update pass in a new "
1821
+ "process…"
1822
+ )
1823
+ continue
1824
+ return
1825
+ raise RuntimeInstallError(
1826
+ "MSYS2 repeatedly requested a core-runtime restart; the temporary "
1827
+ "base was not published"
1828
+ )
1829
+ finally:
1830
+ _stop_private_gpg_agents(
1831
+ root,
1832
+ env=env,
1833
+ reporter=reporter,
1834
+ runner=runner,
1835
+ strict=sys.exc_info()[0] is None,
1836
+ )
1837
+
1838
+
1839
+ def _stop_private_gpg_agents(
1840
+ root: Path,
1841
+ *,
1842
+ env: Mapping[str, str],
1843
+ reporter: InstallReporter,
1844
+ runner: Runner | None,
1845
+ strict: bool,
1846
+ ) -> None:
1847
+ returncode = _run_checked(
1848
+ _bash_command(root, _PRIVATE_GPG_SHUTDOWN_COMMAND),
1849
+ env=env,
1850
+ reporter=reporter,
1851
+ runner=runner,
1852
+ label="Stopping private MSYS2 keyring agents",
1853
+ allow_failure=True,
1854
+ )
1855
+ if returncode == 0:
1856
+ return
1857
+ if not strict:
1858
+ reporter.note(
1859
+ "Could not stop a private MSYS2 keyring agent while recovering "
1860
+ "from another installation failure.",
1861
+ level="warning",
1862
+ )
1863
+ return
1864
+ reporter.command_failed("Stopping private MSYS2 keyring agents", returncode)
1865
+ raise RuntimeInstallError(
1866
+ "stopping private MSYS2 keyring agents failed with exit code "
1867
+ f"{returncode}"
1749
1868
  )
1750
1869
 
1751
1870
 
@@ -2399,16 +2518,20 @@ def install_managed_runtime(
2399
2518
  "pacman is noninteractive; displayed [Y/n] prompts do "
2400
2519
  "not require input."
2401
2520
  )
2521
+ reporter.note(
2522
+ "Core updates may close the first MSYS2 process; "
2523
+ "Railmux will start the required fresh process "
2524
+ "automatically.",
2525
+ level="muted",
2526
+ )
2402
2527
  package_cache = cache_base / f"pacman-{MSYS2_RUNTIME_ID}"
2403
2528
  package_cache.mkdir(parents=True, exist_ok=True)
2404
- _run_pacman_with_recovery(
2529
+ _run_base_update_with_restarts(
2405
2530
  root,
2406
- packages=False,
2407
2531
  cache=package_cache,
2408
2532
  env=child_env,
2409
2533
  reporter=reporter,
2410
2534
  runner=runner,
2411
- label="MSYS2 base update",
2412
2535
  mirror_optimizer=mirror_optimizer,
2413
2536
  )
2414
2537
  reporter.done()
@@ -2445,16 +2568,25 @@ def install_managed_runtime(
2445
2568
  reporter=reporter,
2446
2569
  runner=runner,
2447
2570
  )
2448
- _run_pacman_with_recovery(
2449
- root,
2450
- packages=True,
2451
- cache=package_cache,
2452
- env=child_env,
2453
- reporter=reporter,
2454
- runner=runner,
2455
- label="MSYS2 package installation",
2456
- mirror_optimizer=mirror_optimizer,
2457
- )
2571
+ try:
2572
+ _run_pacman_with_recovery(
2573
+ root,
2574
+ packages=True,
2575
+ cache=package_cache,
2576
+ env=child_env,
2577
+ reporter=reporter,
2578
+ runner=runner,
2579
+ label="MSYS2 package installation",
2580
+ mirror_optimizer=mirror_optimizer,
2581
+ )
2582
+ finally:
2583
+ _stop_private_gpg_agents(
2584
+ root,
2585
+ env=child_env,
2586
+ reporter=reporter,
2587
+ runner=runner,
2588
+ strict=sys.exc_info()[0] is None,
2589
+ )
2458
2590
  reporter.done()
2459
2591
 
2460
2592
  identity = _ensure_base_content_identity(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev27
3
+ Version: 0.4.0.dev28
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -64,6 +64,22 @@ def test_verbose_reporter_streams_the_same_sanitized_output(tmp_path):
64
64
  assert "package 中文\nprogress\n" in path.read_text(encoding="utf-8")
65
65
 
66
66
 
67
+ def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
68
+ path = tmp_path / "install.log"
69
+ with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
70
+ reporter.command_started("first update")
71
+ reporter.command_output(
72
+ ":: To complete this update all MSYS2 processes including this "
73
+ "terminal will be closed. Confirm to proceed [Y/n]"
74
+ )
75
+ reporter.command_output_finished()
76
+ assert reporter.command_requested_msys2_restart
77
+
78
+ reporter.command_started("second update")
79
+ reporter.command_output("warning: terminate other MSYS2 programs\n")
80
+ assert not reporter.command_requested_msys2_restart
81
+
82
+
67
83
  def test_verbose_reporter_never_fails_on_a_legacy_windows_console_codec(tmp_path):
68
84
  stream = Cp1252Stream()
69
85
  path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-5.log"
@@ -797,6 +797,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
797
797
  ).read_text(encoding="utf-8")
798
798
  )["railmux"] == VERSION
799
799
  joined = [" ".join(command) for command in commands]
800
+ base_updates = [command for command in joined if "-Syuu --noconfirm" in command]
801
+ assert len(base_updates) == 2
800
802
  assert any("-Syu --noconfirm" in command for command in joined)
801
803
  assert any("--needed tmux python python-pip" in command for command in joined)
802
804
  assert any('python -m venv "$1/venv"' in command for command in joined)
@@ -806,7 +808,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
806
808
  assert len(logs) == 1
807
809
  log = logs[0].read_text(encoding="utf-8")
808
810
  assert "[1/7] Preparing verified MSYS2" in log
809
- assert "--- MSYS2 base update ---" in log
811
+ assert "--- MSYS2 base update pass 1 ---" in log
812
+ assert "--- MSYS2 base update pass 2 ---" in log
810
813
  assert "Installation completed successfully" in log
811
814
  config = runtime.root / "etc" / "railmux-pacman.conf"
812
815
  assert "[msys]" in config.read_text(encoding="utf-8")
@@ -1472,6 +1475,112 @@ def test_pacman_network_failure_retries_with_cache_and_relaxed_timeout(tmp_path)
1472
1475
  assert f"Server = {repo}" in rendered
1473
1476
 
1474
1477
 
1478
+ def test_base_update_restarts_after_confirmed_core_shutdown(tmp_path):
1479
+ attempts = []
1480
+ restart = (
1481
+ b"upgrading msys2-runtime...\n"
1482
+ b":: To complete this update all MSYS2 processes including this "
1483
+ b"terminal will be closed. Confirm to proceed [Y/n]\n"
1484
+ )
1485
+
1486
+ def runner(argv, *, env, check):
1487
+ attempts.append(argv)
1488
+ update_attempts = [item for item in attempts if "-Syuu" in item[4]]
1489
+ if len(update_attempts) == 1 and "-Syuu" in argv[4]:
1490
+ return completed(argv, returncode=0xC0000135, stdout=restart)
1491
+ return completed(argv, stdout=b"there is nothing to do\n")
1492
+
1493
+ stream = io.StringIO()
1494
+ with InstallReporter(
1495
+ tmp_path / "install.log", verbose=False, stream=stream
1496
+ ) as reporter:
1497
+ windows_msys2._run_base_update_with_restarts(
1498
+ tmp_path / "msys64",
1499
+ cache=tmp_path / "cache",
1500
+ env={},
1501
+ reporter=reporter,
1502
+ runner=runner,
1503
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1504
+ None, None, False, (), ()
1505
+ ),
1506
+ )
1507
+
1508
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1509
+ assert len(updates) == 2
1510
+ assert updates[0][0] == updates[1][0]
1511
+ assert windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in attempts[-1][4]
1512
+ rendered = stream.getvalue()
1513
+ assert "closed its own process as expected" in rendered
1514
+ assert "failed with exit code" not in rendered
1515
+
1516
+
1517
+ def test_base_update_does_not_mask_unconfirmed_missing_dll(tmp_path, monkeypatch):
1518
+ monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
1519
+ attempts = []
1520
+
1521
+ def runner(argv, *, env, check):
1522
+ attempts.append(argv)
1523
+ if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
1524
+ return completed(argv)
1525
+ return completed(
1526
+ argv,
1527
+ returncode=0xC0000135,
1528
+ stdout=b"upgrading msys2-runtime...\n",
1529
+ )
1530
+
1531
+ stream = io.StringIO()
1532
+ with InstallReporter(
1533
+ tmp_path / "install.log", verbose=False, stream=stream
1534
+ ) as reporter:
1535
+ with pytest.raises(RuntimeInstallError, match="3221225781"):
1536
+ windows_msys2._run_base_update_with_restarts(
1537
+ tmp_path / "msys64",
1538
+ cache=tmp_path / "cache",
1539
+ env={},
1540
+ reporter=reporter,
1541
+ runner=runner,
1542
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1543
+ None, None, False, (), ()
1544
+ ),
1545
+ )
1546
+
1547
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1548
+ assert len(updates) == 1
1549
+ assert "outside a confirmed MSYS2 core-update restart" in stream.getvalue()
1550
+
1551
+
1552
+ def test_base_update_restart_loop_is_bounded(tmp_path):
1553
+ attempts = []
1554
+ restart = (
1555
+ b":: To complete this update all MSYS2 processes including this "
1556
+ b"terminal will be closed. Confirm to proceed [Y/n]\n"
1557
+ )
1558
+
1559
+ def runner(argv, *, env, check):
1560
+ attempts.append(argv)
1561
+ if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
1562
+ return completed(argv)
1563
+ return completed(argv, stdout=restart)
1564
+
1565
+ with InstallReporter(
1566
+ tmp_path / "install.log", verbose=False, stream=io.StringIO()
1567
+ ) as reporter:
1568
+ with pytest.raises(RuntimeInstallError, match="repeatedly requested"):
1569
+ windows_msys2._run_base_update_with_restarts(
1570
+ tmp_path / "msys64",
1571
+ cache=tmp_path / "cache",
1572
+ env={},
1573
+ reporter=reporter,
1574
+ runner=runner,
1575
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1576
+ None, None, False, (), ()
1577
+ ),
1578
+ )
1579
+
1580
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1581
+ assert len(updates) == windows_msys2._MAX_BASE_UPDATE_PASSES
1582
+
1583
+
1475
1584
  def test_completed_package_cache_count_ignores_signatures_partials_and_links(tmp_path):
1476
1585
  (tmp_path / "python.pkg.tar.zst").write_bytes(b"package")
1477
1586
  (tmp_path / "python.pkg.tar.zst.sig").write_bytes(b"signature")
@@ -5,6 +5,7 @@ import io
5
5
  import os
6
6
  import stat
7
7
  import subprocess
8
+ import sys
8
9
  import tempfile
9
10
  from pathlib import Path
10
11
 
@@ -13,8 +14,13 @@ from railmux.windows_msys2 import (
13
14
  MSYS2_ARCHIVE_SHA256,
14
15
  Msys2Runtime,
15
16
  _extract_msys2_archive,
17
+ _run_base_update_with_restarts,
16
18
  download_from_sources,
17
19
  )
20
+ from railmux.windows_pacman import (
21
+ optimize_pacman_mirror,
22
+ write_msys_only_pacman_config,
23
+ )
18
24
 
19
25
 
20
26
  def main() -> int:
@@ -39,6 +45,18 @@ def main() -> int:
39
45
  first = stream.read(1)
40
46
  stream.seek(0)
41
47
  stream.write(first)
48
+ write_msys_only_pacman_config(runtime.root)
49
+ with InstallReporter(
50
+ stage / "update.log", verbose=True, stream=sys.stdout
51
+ ) as reporter:
52
+ _run_base_update_with_restarts(
53
+ runtime.root,
54
+ cache=stage / "pacman-cache",
55
+ env=runtime.environment(os.environ),
56
+ reporter=reporter,
57
+ runner=None,
58
+ mirror_optimizer=optimize_pacman_mirror,
59
+ )
42
60
  result = subprocess.run(
43
61
  [
44
62
  str(runtime.bash),
@@ -1 +0,0 @@
1
- __version__ = "0.4.0.dev27"
File without changes
File without changes
File without changes
File without changes