railmux 0.4.0.dev6__tar.gz → 0.4.0.dev7__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 (172) hide show
  1. {railmux-0.4.0.dev6/src/railmux.egg-info → railmux-0.4.0.dev7}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev7/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/windows_bootstrap.py +30 -7
  4. railmux-0.4.0.dev7/src/railmux/windows_install_log.py +189 -0
  5. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/windows_msys2.py +191 -91
  6. railmux-0.4.0.dev7/src/railmux/windows_pacman.py +259 -0
  7. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7/src/railmux.egg-info}/PKG-INFO +1 -1
  8. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux.egg-info/SOURCES.txt +4 -0
  9. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_windows_bootstrap.py +52 -0
  10. railmux-0.4.0.dev7/tests/test_windows_install_log.py +160 -0
  11. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_windows_msys2.py +15 -9
  12. railmux-0.4.0.dev7/tests/test_windows_pacman.py +223 -0
  13. railmux-0.4.0.dev6/src/railmux/__init__.py +0 -1
  14. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/LICENSE +0 -0
  15. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/MANIFEST.in +0 -0
  16. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/README.md +0 -0
  17. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/pyproject.toml +0 -0
  18. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/setup.cfg +0 -0
  19. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/__main__.py +0 -0
  20. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/atomic_file.py +0 -0
  21. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/background_index.py +0 -0
  22. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/cli.py +0 -0
  23. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/codex_index.py +0 -0
  24. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/config.py +0 -0
  25. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/config_cli.py +0 -0
  26. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/diagnostics.py +0 -0
  27. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/discovery.py +0 -0
  28. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/display_transport.py +0 -0
  29. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/entrypoint.py +0 -0
  30. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fast_display_client.py +0 -0
  31. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fast_display_history.py +0 -0
  32. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fast_display_input.py +0 -0
  33. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fast_display_protocol.py +0 -0
  34. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fast_display_server.py +0 -0
  35. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/favorites.py +0 -0
  36. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/fuzzy.py +0 -0
  37. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/help_workspace.py +0 -0
  38. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/launcher.py +0 -0
  39. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/legacy_sessions.py +0 -0
  40. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/local_clipboard.py +0 -0
  41. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/local_open.py +0 -0
  42. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/models.py +0 -0
  43. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/modes.py +0 -0
  44. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/mouse_manager.py +0 -0
  45. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/orphan_marker.py +0 -0
  46. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/pane_surface.py +0 -0
  47. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/path_codec.py +0 -0
  48. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/provider_paths.py +0 -0
  49. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/remote_config.py +0 -0
  50. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/renames.py +0 -0
  51. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/restart_state.py +0 -0
  52. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/runtime_config.py +0 -0
  53. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/scroll_agent.py +0 -0
  54. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/scroll_manager.py +0 -0
  55. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/selection_isolation.py +0 -0
  56. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/self_update.py +0 -0
  57. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/session_cache.py +0 -0
  58. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/session_index.py +0 -0
  59. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/setting_contracts.py +0 -0
  60. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/settings.py +0 -0
  61. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ssh_args.py +0 -0
  62. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ssh_compat.py +0 -0
  63. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ssh_display_diagnostics.py +0 -0
  64. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ssh_doctor.py +0 -0
  65. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/system_deps.py +0 -0
  66. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/terminal_status.py +0 -0
  67. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/tmux_binding_manager.py +0 -0
  68. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/tmux_ctl.py +0 -0
  69. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/tmux_health.py +0 -0
  70. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/tmux_server.py +0 -0
  71. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/tool_panes.py +0 -0
  72. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/transcript.py +0 -0
  73. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/__init__.py +0 -0
  74. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/_widgets.py +0 -0
  75. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/app.py +0 -0
  76. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/keymap.py +0 -0
  77. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/modals.py +0 -0
  78. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/projects_pane.py +0 -0
  79. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/running_pane.py +0 -0
  80. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/sessions_pane.py +0 -0
  81. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/sidebar.py +0 -0
  82. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/statusbar.py +0 -0
  83. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux/ui/workspace.py +0 -0
  84. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux.egg-info/dependency_links.txt +0 -0
  85. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux.egg-info/entry_points.txt +0 -0
  86. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux.egg-info/requires.txt +0 -0
  87. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/src/railmux.egg-info/top_level.txt +0 -0
  88. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_agent_workspace.py +0 -0
  89. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_atomic_file.py +0 -0
  90. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_background_index.py +0 -0
  91. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_cli.py +0 -0
  92. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_codex_delete.py +0 -0
  93. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_codex_index.py +0 -0
  94. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_codex_lineage.py +0 -0
  95. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_codex_transcript.py +0 -0
  96. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_config.py +0 -0
  97. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_config_cli.py +0 -0
  98. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_denested_transport_benchmark.py +0 -0
  99. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_diagnostics.py +0 -0
  100. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_discovery.py +0 -0
  101. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_display_transport.py +0 -0
  102. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_fast_full_window.py +0 -0
  103. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_focus_highlight.py +0 -0
  104. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_fullscreen_binding.py +0 -0
  105. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_fuzzy.py +0 -0
  106. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_help_agent.py +0 -0
  107. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_help_workspace.py +0 -0
  108. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_history_cleanup.py +0 -0
  109. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_keymap.py +0 -0
  110. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_launcher.py +0 -0
  111. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_layout_profile.py +0 -0
  112. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_legacy_sessions.py +0 -0
  113. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_local_clipboard.py +0 -0
  114. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_local_open.py +0 -0
  115. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_modals.py +0 -0
  116. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_mode_selection.py +0 -0
  117. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_modes.py +0 -0
  118. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_mouse_manager.py +0 -0
  119. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_new_project.py +0 -0
  120. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_options.py +0 -0
  121. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_orphan_recovery.py +0 -0
  122. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_pane_surface.py +0 -0
  123. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_path_codec.py +0 -0
  124. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_projects_pane.py +0 -0
  125. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_provider_paths.py +0 -0
  126. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_release_notes.py +0 -0
  127. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_release_workflow.py +0 -0
  128. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_remote_config.py +0 -0
  129. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_renames.py +0 -0
  130. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_restart_state.py +0 -0
  131. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_running_pane.py +0 -0
  132. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_running_sort.py +0 -0
  133. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_runtime_config.py +0 -0
  134. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_scroll_agent.py +0 -0
  135. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_scroll_manager.py +0 -0
  136. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_selection_isolation.py +0 -0
  137. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_self_update.py +0 -0
  138. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_session_cache.py +0 -0
  139. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_session_index.py +0 -0
  140. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_sessions_pane.py +0 -0
  141. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_setting_contracts.py +0 -0
  142. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_settings.py +0 -0
  143. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_sidebar.py +0 -0
  144. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_soft_quit.py +0 -0
  145. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_ssh_args.py +0 -0
  146. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_ssh_compat.py +0 -0
  147. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_ssh_display_benchmark.py +0 -0
  148. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_ssh_display_diagnostics.py +0 -0
  149. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_ssh_doctor.py +0 -0
  150. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_status.py +0 -0
  151. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_statusbar.py +0 -0
  152. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_system_deps.py +0 -0
  153. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_terminal_size.py +0 -0
  154. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_terminal_status.py +0 -0
  155. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_termux_local_touch.py +0 -0
  156. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_binding_manager.py +0 -0
  157. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_ctl.py +0 -0
  158. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_health.py +0 -0
  159. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_integration.py +0 -0
  160. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_server.py +0 -0
  161. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tmux_status.py +0 -0
  162. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_windows_wrapper_contract.py +0 -0
  169. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tests/test_yolo_prompt.py +0 -0
  170. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tools/denested_transport_benchmark.py +0 -0
  171. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/tools/release_notes.py +0 -0
  172. {railmux-0.4.0.dev6 → railmux-0.4.0.dev7}/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.dev6
3
+ Version: 0.4.0.dev7
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.dev7"
@@ -26,7 +26,7 @@ def _print_help() -> None:
26
26
  " railmux ssh HOST [OPTIONS]\n"
27
27
  " railmux config [--remote HOST] [OPTIONS]\n"
28
28
  " railmux doctor [--remote HOST] [OPTIONS]\n"
29
- " railmux runtime {status,install} [--yes]\n\n"
29
+ " railmux runtime {status,install} [--yes] [--verbose]\n\n"
30
30
  "Windows preview: Railmux runs in a private managed MSYS2/tmux "
31
31
  "runtime while Codex and Claude Code remain Windows-native and use "
32
32
  "the existing Windows user session directories."
@@ -56,7 +56,8 @@ def _confirm_install(*, input_fn: Callable[[str], str] = input) -> bool:
56
56
  try:
57
57
  answer = input_fn(
58
58
  "Install the private MSYS2/tmux runtime now? "
59
- "This downloads about 50 MB and uses about 300 MB [y/N] "
59
+ "This downloads a 50 MB base plus required updates and packages, "
60
+ "and uses about 300 MB or more of private disk space [y/N] "
60
61
  )
61
62
  except (EOFError, KeyboardInterrupt):
62
63
  print()
@@ -68,13 +69,18 @@ def _install(
68
69
  *,
69
70
  environ: Mapping[str, str],
70
71
  assume_yes: bool,
72
+ verbose: bool = False,
71
73
  input_fn: Callable[[str], str] = input,
72
74
  ) -> Msys2Runtime | None:
73
75
  if not assume_yes and not _confirm_install(input_fn=input_fn):
74
76
  print("MSYS2 runtime installation cancelled.", file=sys.stderr)
75
77
  return None
76
78
  try:
77
- runtime = install_managed_runtime(version=__version__, environ=environ)
79
+ runtime = install_managed_runtime(
80
+ version=__version__,
81
+ environ=environ,
82
+ verbose=verbose,
83
+ )
78
84
  except RuntimeInstallError as exc:
79
85
  print(f"error: {exc}", file=sys.stderr)
80
86
  return None
@@ -135,12 +141,24 @@ def main(
135
141
  if arguments[:2] == ["runtime", "status"] and len(arguments) == 2:
136
142
  return _runtime_status(runtime, environ=environ)
137
143
  if arguments and arguments[0] == "runtime":
138
- if arguments[1:] not in (["install"], ["install", "--yes"]):
139
- print("error: usage: railmux runtime {status,install} [--yes]", file=sys.stderr)
144
+ install_arguments = arguments[1:]
145
+ install_flags = install_arguments[1:] if install_arguments[:1] == ["install"] else []
146
+ if (
147
+ not install_arguments
148
+ or install_arguments[0] != "install"
149
+ or len(install_flags) != len(set(install_flags))
150
+ or any(flag not in {"--yes", "--verbose"} for flag in install_flags)
151
+ ):
152
+ print(
153
+ "error: usage: railmux runtime {status,install} "
154
+ "[--yes] [--verbose]",
155
+ file=sys.stderr,
156
+ )
140
157
  return 2
141
158
  runtime = runtime or _install(
142
159
  environ=environ,
143
- assume_yes=arguments[-1:] == ["--yes"],
160
+ assume_yes="--yes" in install_flags,
161
+ verbose="--verbose" in install_flags,
144
162
  input_fn=input_fn,
145
163
  )
146
164
  return 0 if runtime is not None else 2
@@ -162,7 +180,12 @@ def main(
162
180
  file=sys.stderr,
163
181
  )
164
182
  return 2
165
- runtime = _install(environ=environ, assume_yes=False, input_fn=input_fn)
183
+ runtime = _install(
184
+ environ=environ,
185
+ assume_yes=False,
186
+ verbose=False,
187
+ input_fn=input_fn,
188
+ )
166
189
  if runtime is None:
167
190
  return 2
168
191
 
@@ -0,0 +1,189 @@
1
+ """Bounded user output and complete UTF-8 logs for Windows runtime setup."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import sys
7
+ import time
8
+ from collections import deque
9
+ from collections.abc import Callable, Mapping
10
+ from pathlib import Path
11
+ from typing import BinaryIO, TextIO
12
+
13
+
14
+ _LOG_KEEP_COUNT = 5
15
+ _FAILURE_LINE_LIMIT = 500
16
+ _LOG_NAME_RE = re.compile(
17
+ r"install-[0-9A-Za-z.+-]+-[0-9]{8}T[0-9]{6}Z-[0-9]+\.log\Z"
18
+ )
19
+ _URL_CREDENTIAL_RE = re.compile(r"(https?://)[^/@\s]+@", re.IGNORECASE)
20
+ _SECRET_QUERY_RE = re.compile(
21
+ r"(?i)([?&](?:access_token|password|token)=)[^&\s]+"
22
+ )
23
+ _MIRROR_WARNING_MARKERS = (
24
+ "too many errors from ",
25
+ "failed retrieving file ",
26
+ )
27
+
28
+
29
+ def _redact_line(line: str) -> str:
30
+ line = _URL_CREDENTIAL_RE.sub(r"\1<redacted>@", line)
31
+ return _SECRET_QUERY_RE.sub(r"\1<redacted>", line)
32
+
33
+
34
+ def install_log_path(
35
+ environ: Mapping[str, str],
36
+ *,
37
+ version: str,
38
+ clock: Callable[[], float] = time.time,
39
+ ) -> Path:
40
+ local_app_data = environ.get("LOCALAPPDATA", "").strip()
41
+ if not local_app_data:
42
+ raise OSError("LOCALAPPDATA is unavailable")
43
+ stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(clock()))
44
+ return (
45
+ Path(local_app_data)
46
+ / "Railmux"
47
+ / "logs"
48
+ / f"install-{version}-{stamp}-{os.getpid()}.log"
49
+ )
50
+
51
+
52
+ def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None:
53
+ candidates: list[Path] = []
54
+ try:
55
+ entries = list(directory.iterdir())
56
+ except OSError:
57
+ return
58
+ for entry in entries:
59
+ if entry.is_file() and _LOG_NAME_RE.fullmatch(entry.name):
60
+ candidates.append(entry)
61
+ try:
62
+ candidates.sort(
63
+ key=lambda path: (path.stat().st_mtime_ns, path.name),
64
+ reverse=True,
65
+ )
66
+ except OSError:
67
+ return
68
+ for stale in candidates[max(keep, 1) :]:
69
+ try:
70
+ stale.unlink()
71
+ except OSError:
72
+ continue
73
+
74
+
75
+ class InstallReporter:
76
+ """Show stable phases while retaining complete command output."""
77
+
78
+ def __init__(
79
+ self,
80
+ path: Path,
81
+ *,
82
+ verbose: bool,
83
+ stream: TextIO | None = None,
84
+ ) -> None:
85
+ self.path = path
86
+ self.verbose = verbose
87
+ self.stream = sys.stdout if stream is None else stream
88
+ self._log: TextIO | None = None
89
+ self._tail: deque[str] = deque(maxlen=20)
90
+ self._reported_mirror_warning = False
91
+
92
+ def __enter__(self) -> InstallReporter:
93
+ self.path.parent.mkdir(parents=True, exist_ok=True)
94
+ self._log = self.path.open("x", encoding="utf-8", newline="\n")
95
+ self._write_log("Railmux Windows runtime installation\n")
96
+ _prune_install_logs(self.path.parent)
97
+ return self
98
+
99
+ def __exit__(self, _exc_type, _exc, _traceback) -> None:
100
+ if self._log is not None:
101
+ self._log.close()
102
+ self._log = None
103
+
104
+ def _write_log(self, text: str) -> None:
105
+ if self._log is None:
106
+ return
107
+ self._log.write(text)
108
+ self._log.flush()
109
+
110
+ def _console(self, text: str, *, flush: bool = True) -> None:
111
+ try:
112
+ print(text, file=self.stream, flush=flush)
113
+ except UnicodeEncodeError:
114
+ encoding = getattr(self.stream, "encoding", None) or "utf-8"
115
+ safe = text.encode(encoding, errors="replace").decode(encoding)
116
+ print(safe, file=self.stream, flush=flush)
117
+
118
+ def phase(self, number: int, total: int, message: str) -> None:
119
+ rendered = f"[{number}/{total}] {message}"
120
+ self._console(rendered)
121
+ self._write_log(f"\n{rendered}\n")
122
+
123
+ def done(self, detail: str | None = None) -> None:
124
+ rendered = " done" + (f" · {detail}" if detail else "")
125
+ self._console(rendered)
126
+ self._write_log(f"{rendered}\n")
127
+
128
+ def note(self, message: str) -> None:
129
+ rendered = f" {message}"
130
+ self._console(rendered)
131
+ self._write_log(f"{rendered}\n")
132
+
133
+ def command_started(self, label: str) -> None:
134
+ self._tail.clear()
135
+ self._reported_mirror_warning = False
136
+ self._write_log(f"\n--- {label} ---\n")
137
+
138
+ def command_output(self, output: bytes | str | None) -> None:
139
+ if not output:
140
+ return
141
+ if isinstance(output, bytes):
142
+ text = output.decode("utf-8", errors="replace")
143
+ else:
144
+ text = output
145
+ normalized = text.replace("\r\n", "\n").replace("\r", "\n")
146
+ for raw_line in normalized.splitlines():
147
+ line = _redact_line(raw_line)
148
+ self._write_log(f"{line}\n")
149
+ if line.strip():
150
+ self._tail.append(line)
151
+ if self.verbose:
152
+ self._console(line)
153
+ elif (
154
+ not self._reported_mirror_warning
155
+ and any(marker in line.lower() for marker in _MIRROR_WARNING_MARKERS)
156
+ ):
157
+ self.note(
158
+ "A package mirror failed; pacman is trying the next "
159
+ "approved source…"
160
+ )
161
+ self._reported_mirror_warning = True
162
+
163
+ def command_failed(self, label: str, returncode: int) -> None:
164
+ self.note(f"{label} failed with exit code {returncode}.")
165
+ if self._tail:
166
+ self.note("Last output:")
167
+ for line in list(self._tail)[-8:]:
168
+ if len(line) > _FAILURE_LINE_LIMIT:
169
+ line = f"{line[:_FAILURE_LINE_LIMIT]}… [truncated; see log]"
170
+ self._console(f" {line}")
171
+ self.note(f"Full UTF-8 log: {self.path}")
172
+
173
+ def finish(self) -> None:
174
+ self._write_log("\nInstallation completed successfully.\n")
175
+ self._console(f"Installation log: {self.path}")
176
+
177
+
178
+ def stream_process_output(
179
+ process: object,
180
+ reporter: InstallReporter,
181
+ ) -> int:
182
+ stdout: BinaryIO | None = getattr(process, "stdout", None)
183
+ if stdout is not None:
184
+ while True:
185
+ chunk = stdout.readline()
186
+ if not chunk:
187
+ break
188
+ reporter.command_output(chunk)
189
+ return process.wait()
@@ -24,6 +24,17 @@ from contextlib import contextmanager
24
24
  from dataclasses import dataclass
25
25
  from pathlib import Path
26
26
 
27
+ from railmux.windows_install_log import (
28
+ InstallReporter,
29
+ install_log_path,
30
+ stream_process_output,
31
+ )
32
+ from railmux.windows_pacman import (
33
+ PacmanMirrorDecision,
34
+ PacmanMirrorError,
35
+ optimize_pacman_mirror,
36
+ )
37
+
27
38
 
28
39
  MSYS2_RELEASE = "2026-03-22"
29
40
  MSYS2_ARCHIVE_NAME = f"msys2-base-x86_64-{MSYS2_RELEASE.replace('-', '')}.sfx.exe"
@@ -98,6 +109,7 @@ class RuntimeInstallError(RuntimeErrorBase):
98
109
  Probe = Callable[..., subprocess.CompletedProcess[bytes]]
99
110
  Runner = Callable[..., subprocess.CompletedProcess]
100
111
  Downloader = Callable[[str, Path, str], None]
112
+ MirrorOptimizer = Callable[[Path], PacmanMirrorDecision]
101
113
 
102
114
 
103
115
  @dataclass(frozen=True)
@@ -817,15 +829,32 @@ def _run_checked(
817
829
  argv: Sequence[str],
818
830
  *,
819
831
  env: Mapping[str, str],
820
- runner: Runner,
832
+ reporter: InstallReporter,
821
833
  label: str,
834
+ runner: Runner | None = None,
822
835
  ) -> None:
836
+ reporter.command_started(label)
823
837
  try:
824
- result = runner(list(argv), env=dict(env), check=False)
838
+ if runner is None:
839
+ process = subprocess.Popen(
840
+ list(argv),
841
+ env=dict(env),
842
+ stdin=subprocess.DEVNULL,
843
+ stdout=subprocess.PIPE,
844
+ stderr=subprocess.STDOUT,
845
+ bufsize=-1,
846
+ )
847
+ returncode = stream_process_output(process, reporter)
848
+ else:
849
+ result = runner(list(argv), env=dict(env), check=False)
850
+ reporter.command_output(getattr(result, "stdout", None))
851
+ reporter.command_output(getattr(result, "stderr", None))
852
+ returncode = result.returncode
825
853
  except OSError as exc:
826
854
  raise RuntimeInstallError(f"could not start {label}") from exc
827
- if result.returncode:
828
- raise RuntimeInstallError(f"{label} failed with exit code {result.returncode}")
855
+ if returncode:
856
+ reporter.command_failed(label, returncode)
857
+ raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
829
858
 
830
859
 
831
860
  def _bash_command(root: Path, command: str, *arguments: str) -> list[str]:
@@ -891,9 +920,11 @@ def install_managed_runtime(
891
920
  version: str,
892
921
  environ: Mapping[str, str],
893
922
  downloader: Downloader | None = None,
894
- runner: Runner = subprocess.run,
923
+ runner: Runner | None = None,
895
924
  probe: Probe = _probe,
896
925
  lock_factory: Callable[[Path], object] = install_lock,
926
+ mirror_optimizer: MirrorOptimizer = optimize_pacman_mirror,
927
+ verbose: bool = False,
897
928
  ) -> Msys2Runtime:
898
929
  """Install a fresh private runtime and activate it only after verification."""
899
930
  if not _VERSION_RE.fullmatch(version):
@@ -906,94 +937,163 @@ def install_managed_runtime(
906
937
  base = _managed_base(environ)
907
938
  if base is None:
908
939
  raise RuntimeInstallError("LOCALAPPDATA is unavailable")
909
- base.mkdir(parents=True, exist_ok=True)
940
+ try:
941
+ base.mkdir(parents=True, exist_ok=True)
942
+ log_path = install_log_path(environ, version=version)
943
+ except OSError as exc:
944
+ raise RuntimeInstallError("could not create the private runtime log") from exc
910
945
  final_root = managed_root(environ, version=version)
911
946
  assert final_root is not None
912
947
  final_root.parent.mkdir(parents=True, exist_ok=True)
913
948
 
914
- with lock_factory(base):
915
- existing = find_runtime(version=version, environ=environ, probe=probe)
916
- if existing is not None:
917
- return existing
918
- if final_root.exists():
919
- if _marker_matches(final_root, version=version):
920
- raise RuntimeInstallError(
921
- "the managed MSYS2 runtime is present but could not be "
922
- "verified; retry after checking antivirus or disk load"
923
- )
924
- raise RuntimeInstallError(
925
- "the managed MSYS2 directory exists but is incomplete; "
926
- "it was left untouched"
927
- )
928
-
929
- with tempfile.TemporaryDirectory(prefix=".install-", dir=base) as raw_stage:
930
- stage = Path(raw_stage)
931
- archive = stage / MSYS2_ARCHIVE_NAME
932
- print(f"Downloading verified MSYS2 {MSYS2_RELEASE} runtime…")
933
- download_from_sources(
934
- archive,
935
- MSYS2_ARCHIVE_SHA256,
936
- downloader=downloader,
937
- )
949
+ try:
950
+ with InstallReporter(log_path, verbose=verbose) as reporter:
951
+ with lock_factory(base):
952
+ existing = find_runtime(version=version, environ=environ, probe=probe)
953
+ if existing is not None:
954
+ reporter.note("The exact managed runtime is already ready.")
955
+ reporter.finish()
956
+ return existing
957
+ if final_root.exists():
958
+ if _marker_matches(final_root, version=version):
959
+ raise RuntimeInstallError(
960
+ "the managed MSYS2 runtime is present but could not be "
961
+ "verified; retry after checking antivirus or disk load"
962
+ )
963
+ raise RuntimeInstallError(
964
+ "the managed MSYS2 directory exists but is incomplete; "
965
+ "it was left untouched"
966
+ )
938
967
 
939
- _run_checked(
940
- [str(archive), "-y", f"-o{stage}"],
941
- env=environ,
942
- runner=runner,
943
- label="MSYS2 extraction",
944
- )
945
- root = stage / "msys64"
946
- if not (root / "usr" / "bin" / "bash.exe").is_file():
947
- raise RuntimeInstallError("the verified MSYS2 archive was incomplete")
948
- runtime = Msys2Runtime(root, managed=False)
949
- child_env = runtime.environment(environ)
950
-
951
- print("Updating the private MSYS2 base…")
952
- _run_checked(
953
- _bash_command(root, _INITIAL_UPDATE_COMMAND),
954
- env=child_env,
955
- runner=runner,
956
- label="MSYS2 base update",
957
- )
958
- # A new process is required after msys2-runtime/bash updates.
959
- child_env = runtime.environment(environ)
960
- print("Installing tmux and the private Python runtime…")
961
- _run_checked(
962
- _bash_command(root, _PACKAGE_INSTALL_COMMAND),
963
- env=child_env,
964
- runner=runner,
965
- label="MSYS2 package installation",
966
- )
967
- _run_checked(
968
- _bash_command(root, _VENV_COMMAND),
969
- env=child_env,
970
- runner=runner,
971
- label="Railmux virtual environment creation",
972
- )
973
- print(f"Installing Railmux {version} into the private runtime…")
974
- _run_checked(
975
- _bash_command(root, _PACKAGE_COMMAND, version),
976
- env=child_env,
977
- runner=runner,
978
- label="Railmux runtime package installation",
979
- )
980
- _write_marker(root, version=version)
981
- staged_runtime = Msys2Runtime(root, managed=True)
982
- if not probe_runtime(
983
- staged_runtime,
984
- version=version,
985
- environ=environ,
986
- probe=probe,
987
- ):
988
- raise RuntimeInstallError("the staged Railmux runtime failed validation")
989
- os.replace(root, final_root)
990
-
991
- installed = Msys2Runtime(final_root, managed=True)
992
- if not probe_runtime(
993
- installed,
994
- version=version,
995
- environ=environ,
996
- probe=probe,
997
- ):
998
- raise RuntimeInstallError("the activated Railmux runtime failed validation")
999
- return installed
968
+ with tempfile.TemporaryDirectory(
969
+ prefix=".install-", dir=base
970
+ ) as raw_stage:
971
+ stage = Path(raw_stage)
972
+ archive = stage / MSYS2_ARCHIVE_NAME
973
+ reporter.phase(1, 7, f"Downloading MSYS2 {MSYS2_RELEASE} base")
974
+ source = download_from_sources(
975
+ archive,
976
+ MSYS2_ARCHIVE_SHA256,
977
+ downloader=downloader,
978
+ )
979
+ reporter.done(source)
980
+
981
+ reporter.phase(2, 7, "Verifying and extracting the private base")
982
+ _run_checked(
983
+ [str(archive), "-y", f"-o{stage}"],
984
+ env=environ,
985
+ reporter=reporter,
986
+ runner=runner,
987
+ label="MSYS2 extraction",
988
+ )
989
+ root = stage / "msys64"
990
+ if not (root / "usr" / "bin" / "bash.exe").is_file():
991
+ raise RuntimeInstallError(
992
+ "the verified MSYS2 archive was incomplete"
993
+ )
994
+ reporter.done()
995
+
996
+ reporter.phase(3, 7, "Selecting an MSYS2 package mirror")
997
+ try:
998
+ decision = mirror_optimizer(root)
999
+ except PacmanMirrorError as exc:
1000
+ reporter.note(
1001
+ f"Mirror measurement unavailable ({exc}); using the "
1002
+ "official order."
1003
+ )
1004
+ reporter.done("official order")
1005
+ else:
1006
+ for mirror_probe in decision.probes:
1007
+ reporter.command_output(
1008
+ f"mirror probe {mirror_probe.label}: "
1009
+ f"{_format_download_rate(mirror_probe.rate)}\n"
1010
+ )
1011
+ for label, reason in decision.failures:
1012
+ reporter.command_output(
1013
+ f"mirror probe {label}: unavailable ({reason})\n"
1014
+ )
1015
+ if decision.selected is None:
1016
+ reporter.done("official order")
1017
+ else:
1018
+ reporter.done(
1019
+ f"{decision.selected.label} · "
1020
+ f"{_format_download_rate(decision.selected.rate)}"
1021
+ )
1022
+
1023
+ runtime = Msys2Runtime(root, managed=False)
1024
+ child_env = runtime.environment(environ)
1025
+ reporter.phase(4, 7, "Updating the private MSYS2 base")
1026
+ reporter.note(
1027
+ "pacman is noninteractive; displayed [Y/n] prompts do "
1028
+ "not require input."
1029
+ )
1030
+ _run_checked(
1031
+ _bash_command(root, _INITIAL_UPDATE_COMMAND),
1032
+ env=child_env,
1033
+ reporter=reporter,
1034
+ runner=runner,
1035
+ label="MSYS2 base update",
1036
+ )
1037
+ reporter.done()
1038
+
1039
+ # A new process is required after msys2-runtime/bash updates.
1040
+ child_env = runtime.environment(environ)
1041
+ reporter.phase(5, 7, "Installing tmux and private Python")
1042
+ _run_checked(
1043
+ _bash_command(root, _PACKAGE_INSTALL_COMMAND),
1044
+ env=child_env,
1045
+ reporter=reporter,
1046
+ runner=runner,
1047
+ label="MSYS2 package installation",
1048
+ )
1049
+ reporter.done()
1050
+
1051
+ reporter.phase(6, 7, f"Installing Railmux {version}")
1052
+ _run_checked(
1053
+ _bash_command(root, _VENV_COMMAND),
1054
+ env=child_env,
1055
+ reporter=reporter,
1056
+ runner=runner,
1057
+ label="Railmux virtual environment creation",
1058
+ )
1059
+ _run_checked(
1060
+ _bash_command(root, _PACKAGE_COMMAND, version),
1061
+ env=child_env,
1062
+ reporter=reporter,
1063
+ runner=runner,
1064
+ label="Railmux runtime package installation",
1065
+ )
1066
+ reporter.done()
1067
+
1068
+ reporter.phase(7, 7, "Validating and activating the runtime")
1069
+ _write_marker(root, version=version)
1070
+ staged_runtime = Msys2Runtime(root, managed=True)
1071
+ if not probe_runtime(
1072
+ staged_runtime,
1073
+ version=version,
1074
+ environ=environ,
1075
+ probe=probe,
1076
+ ):
1077
+ raise RuntimeInstallError(
1078
+ "the staged Railmux runtime failed validation"
1079
+ )
1080
+ os.replace(root, final_root)
1081
+
1082
+ installed = Msys2Runtime(final_root, managed=True)
1083
+ if not probe_runtime(
1084
+ installed,
1085
+ version=version,
1086
+ environ=environ,
1087
+ probe=probe,
1088
+ ):
1089
+ raise RuntimeInstallError(
1090
+ "the activated Railmux runtime failed validation"
1091
+ )
1092
+ reporter.done("ready")
1093
+ reporter.finish()
1094
+ return installed
1095
+ except RuntimeInstallError as exc:
1096
+ raise RuntimeInstallError(f"{exc}; full log: {log_path}") from exc
1097
+ except OSError as exc:
1098
+ detail = f"; full log: {log_path}" if log_path.is_file() else ""
1099
+ raise RuntimeInstallError(f"runtime installation failed{detail}") from exc