railmux 0.4.0.dev5__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 (173) hide show
  1. {railmux-0.4.0.dev5/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.dev5 → 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.dev7/src/railmux/windows_msys2.py +1099 -0
  6. railmux-0.4.0.dev7/src/railmux/windows_pacman.py +259 -0
  7. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7/src/railmux.egg-info}/PKG-INFO +1 -1
  8. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/SOURCES.txt +4 -0
  9. {railmux-0.4.0.dev5 → 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.dev5 → railmux-0.4.0.dev7}/tests/test_windows_msys2.py +272 -10
  12. railmux-0.4.0.dev7/tests/test_windows_pacman.py +223 -0
  13. railmux-0.4.0.dev5/src/railmux/__init__.py +0 -1
  14. railmux-0.4.0.dev5/src/railmux/windows_msys2.py +0 -580
  15. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/LICENSE +0 -0
  16. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/MANIFEST.in +0 -0
  17. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/README.md +0 -0
  18. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/pyproject.toml +0 -0
  19. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/setup.cfg +0 -0
  20. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/__main__.py +0 -0
  21. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/atomic_file.py +0 -0
  22. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/background_index.py +0 -0
  23. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/cli.py +0 -0
  24. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/codex_index.py +0 -0
  25. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/config.py +0 -0
  26. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/config_cli.py +0 -0
  27. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/diagnostics.py +0 -0
  28. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/discovery.py +0 -0
  29. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/display_transport.py +0 -0
  30. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/entrypoint.py +0 -0
  31. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_client.py +0 -0
  32. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_history.py +0 -0
  33. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_input.py +0 -0
  34. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_protocol.py +0 -0
  35. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fast_display_server.py +0 -0
  36. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/favorites.py +0 -0
  37. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/launcher.py +0 -0
  40. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/local_open.py +0 -0
  43. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/models.py +0 -0
  44. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/modes.py +0 -0
  45. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/orphan_marker.py +0 -0
  47. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/pane_surface.py +0 -0
  48. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/path_codec.py +0 -0
  49. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/provider_paths.py +0 -0
  50. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/remote_config.py +0 -0
  51. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/renames.py +0 -0
  52. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/restart_state.py +0 -0
  53. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/runtime_config.py +0 -0
  54. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/scroll_agent.py +0 -0
  55. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/scroll_manager.py +0 -0
  56. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/selection_isolation.py +0 -0
  57. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/self_update.py +0 -0
  58. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/session_cache.py +0 -0
  59. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/session_index.py +0 -0
  60. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/setting_contracts.py +0 -0
  61. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/settings.py +0 -0
  62. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_args.py +0 -0
  63. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_compat.py +0 -0
  64. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_display_diagnostics.py +0 -0
  65. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ssh_doctor.py +0 -0
  66. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/system_deps.py +0 -0
  67. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/terminal_status.py +0 -0
  68. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_binding_manager.py +0 -0
  69. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_ctl.py +0 -0
  70. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_health.py +0 -0
  71. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tmux_server.py +0 -0
  72. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/tool_panes.py +0 -0
  73. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/transcript.py +0 -0
  74. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/__init__.py +0 -0
  75. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/_widgets.py +0 -0
  76. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/app.py +0 -0
  77. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/keymap.py +0 -0
  78. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/modals.py +0 -0
  79. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/projects_pane.py +0 -0
  80. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/running_pane.py +0 -0
  81. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/sessions_pane.py +0 -0
  82. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/sidebar.py +0 -0
  83. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/statusbar.py +0 -0
  84. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux/ui/workspace.py +0 -0
  85. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/dependency_links.txt +0 -0
  86. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/entry_points.txt +0 -0
  87. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/requires.txt +0 -0
  88. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/src/railmux.egg-info/top_level.txt +0 -0
  89. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_agent_workspace.py +0 -0
  90. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_atomic_file.py +0 -0
  91. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_background_index.py +0 -0
  92. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_cli.py +0 -0
  93. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_delete.py +0 -0
  94. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_index.py +0 -0
  95. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_lineage.py +0 -0
  96. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_codex_transcript.py +0 -0
  97. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_config.py +0 -0
  98. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_config_cli.py +0 -0
  99. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_denested_transport_benchmark.py +0 -0
  100. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_diagnostics.py +0 -0
  101. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_discovery.py +0 -0
  102. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_display_transport.py +0 -0
  103. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fast_full_window.py +0 -0
  104. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_focus_highlight.py +0 -0
  105. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fullscreen_binding.py +0 -0
  106. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_fuzzy.py +0 -0
  107. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_help_agent.py +0 -0
  108. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_help_workspace.py +0 -0
  109. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_history_cleanup.py +0 -0
  110. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_keymap.py +0 -0
  111. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_launcher.py +0 -0
  112. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_layout_profile.py +0 -0
  113. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_legacy_sessions.py +0 -0
  114. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_local_clipboard.py +0 -0
  115. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_local_open.py +0 -0
  116. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_modals.py +0 -0
  117. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_mode_selection.py +0 -0
  118. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_modes.py +0 -0
  119. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_mouse_manager.py +0 -0
  120. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_new_project.py +0 -0
  121. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_options.py +0 -0
  122. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_orphan_recovery.py +0 -0
  123. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_pane_surface.py +0 -0
  124. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_path_codec.py +0 -0
  125. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_projects_pane.py +0 -0
  126. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_provider_paths.py +0 -0
  127. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_release_notes.py +0 -0
  128. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_release_workflow.py +0 -0
  129. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_remote_config.py +0 -0
  130. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_renames.py +0 -0
  131. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_restart_state.py +0 -0
  132. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_running_pane.py +0 -0
  133. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_running_sort.py +0 -0
  134. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_runtime_config.py +0 -0
  135. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_scroll_agent.py +0 -0
  136. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_scroll_manager.py +0 -0
  137. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_selection_isolation.py +0 -0
  138. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_self_update.py +0 -0
  139. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_session_cache.py +0 -0
  140. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_session_index.py +0 -0
  141. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_sessions_pane.py +0 -0
  142. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_setting_contracts.py +0 -0
  143. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_settings.py +0 -0
  144. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_sidebar.py +0 -0
  145. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_soft_quit.py +0 -0
  146. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_args.py +0 -0
  147. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_compat.py +0 -0
  148. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_display_benchmark.py +0 -0
  149. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_display_diagnostics.py +0 -0
  150. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_ssh_doctor.py +0 -0
  151. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_status.py +0 -0
  152. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_statusbar.py +0 -0
  153. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_system_deps.py +0 -0
  154. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_terminal_size.py +0 -0
  155. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_terminal_status.py +0 -0
  156. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_termux_local_touch.py +0 -0
  157. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_binding_manager.py +0 -0
  158. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_ctl.py +0 -0
  159. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_health.py +0 -0
  160. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_integration.py +0 -0
  161. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_server.py +0 -0
  162. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tmux_status.py +0 -0
  163. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_tool_panes.py +0 -0
  164. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_transcript.py +0 -0
  165. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_utf8_state.py +0 -0
  166. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_web_demo_recorder.py +0 -0
  167. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_wheel_smoke.py +0 -0
  168. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_widgets.py +0 -0
  169. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_windows_wrapper_contract.py +0 -0
  170. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tests/test_yolo_prompt.py +0 -0
  171. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tools/denested_transport_benchmark.py +0 -0
  172. {railmux-0.4.0.dev5 → railmux-0.4.0.dev7}/tools/release_notes.py +0 -0
  173. {railmux-0.4.0.dev5 → 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.dev5
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()