railmux 0.4.0.dev22__tar.gz → 0.4.0.dev25__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.
- {railmux-0.4.0.dev22/src/railmux.egg-info → railmux-0.4.0.dev25}/PKG-INFO +1 -1
- railmux-0.4.0.dev25/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/cli.py +62 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/config_cli.py +12 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/diagnostics.py +35 -3
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_client.py +227 -8
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_server.py +5 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/provider_paths.py +23 -2
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/remote_config.py +35 -15
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_doctor.py +54 -18
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/app.py +24 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_bootstrap.py +189 -10
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_msys2.py +501 -3
- railmux-0.4.0.dev25/src/railmux/windows_ui_transition.py +739 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_cli.py +20 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_config_cli.py +21 -2
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_diagnostics.py +2 -1
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fast_full_window.py +277 -20
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_remote_config.py +113 -6
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_doctor.py +57 -49
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_bootstrap.py +68 -8
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_msys2.py +266 -1
- railmux-0.4.0.dev25/tests/test_windows_ui_transition.py +326 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/wheel_smoke.py +1 -1
- railmux-0.4.0.dev22/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/LICENSE +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/README.md +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/pyproject.toml +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/setup.cfg +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/release_notes.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev25"
|
|
@@ -442,9 +442,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
442
442
|
metavar="ARGS",
|
|
443
443
|
help="a quoted group of ssh arguments for --remote",
|
|
444
444
|
)
|
|
445
|
+
doctor_parser.add_argument(
|
|
446
|
+
"--remote-platform",
|
|
447
|
+
choices=("auto", "posix", "windows"),
|
|
448
|
+
default="auto",
|
|
449
|
+
help=(
|
|
450
|
+
"remote shell family for --remote: auto, posix, or windows "
|
|
451
|
+
"(default: auto)"
|
|
452
|
+
),
|
|
453
|
+
)
|
|
445
454
|
doctor_args = doctor_parser.parse_args(raw_args[1:])
|
|
446
455
|
if doctor_args.ssh_arg and not doctor_args.remote:
|
|
447
456
|
doctor_parser.error("--ssh-args requires --remote")
|
|
457
|
+
if doctor_args.remote_platform != "auto" and not doctor_args.remote:
|
|
458
|
+
doctor_parser.error("--remote-platform requires --remote")
|
|
448
459
|
if doctor_args.remote:
|
|
449
460
|
from railmux.ssh_doctor import run_remote_ssh_doctor
|
|
450
461
|
|
|
@@ -452,6 +463,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
452
463
|
doctor_args.remote,
|
|
453
464
|
ssh_args=doctor_args.ssh_arg,
|
|
454
465
|
json_output=doctor_args.json,
|
|
466
|
+
remote_platform=doctor_args.remote_platform,
|
|
455
467
|
)
|
|
456
468
|
return run_doctor(
|
|
457
469
|
claude_home=Path(doctor_args.claude_home),
|
|
@@ -630,6 +642,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
630
642
|
tmux_server.target_session_id(dedicated_target, "railmux")
|
|
631
643
|
if dedicated_target is not None else None
|
|
632
644
|
)
|
|
645
|
+
created_detached_session = False
|
|
633
646
|
if dedicated_target is not None and dedicated_session_id is None:
|
|
634
647
|
from railmux.provider_paths import (
|
|
635
648
|
running_in_managed_windows_wrapper,
|
|
@@ -644,6 +657,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
644
657
|
initial_size=_interactive_terminal_size(),
|
|
645
658
|
)
|
|
646
659
|
)
|
|
660
|
+
created_detached_session = dedicated_session_id is not None
|
|
661
|
+
if (
|
|
662
|
+
dedicated_target is not None
|
|
663
|
+
and dedicated_session_id is not None
|
|
664
|
+
and not created_detached_session
|
|
665
|
+
):
|
|
666
|
+
from railmux.provider_paths import running_in_managed_windows_wrapper
|
|
667
|
+
|
|
668
|
+
if running_in_managed_windows_wrapper():
|
|
669
|
+
from railmux.windows_ui_transition import ensure_current_ui
|
|
670
|
+
|
|
671
|
+
runtime_id = os.environ.get("RAILMUX_MSYS2_RUNTIME_ID", "")
|
|
672
|
+
app_id = os.environ.get("RAILMUX_MSYS2_APP_ID", "")
|
|
673
|
+
transition = ensure_current_ui(
|
|
674
|
+
dedicated_target,
|
|
675
|
+
dedicated_session_id,
|
|
676
|
+
runtime=runtime_id,
|
|
677
|
+
target_app=app_id,
|
|
678
|
+
target_version=__version__,
|
|
679
|
+
)
|
|
680
|
+
if transition.status in {"legacy", "pending", "blocked"}:
|
|
681
|
+
detail = transition.detail or "the running UI was unchanged"
|
|
682
|
+
print(
|
|
683
|
+
"info: the Windows app-layer update is pending; "
|
|
684
|
+
f"{detail}. Soft Quit the existing UI, then run "
|
|
685
|
+
"'railmux' again to finish it.",
|
|
686
|
+
file=sys.stderr,
|
|
687
|
+
)
|
|
647
688
|
cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
|
|
648
689
|
return _run_tmux_client_with_watchdog(
|
|
649
690
|
cmd,
|
|
@@ -676,6 +717,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
676
717
|
),
|
|
677
718
|
)
|
|
678
719
|
app.run()
|
|
720
|
+
request = getattr(app, "_ui_upgrade_request", None)
|
|
721
|
+
if request is not None:
|
|
722
|
+
from railmux.windows_ui_transition import UpgradeRequest, upgrade_exec_argv
|
|
723
|
+
|
|
724
|
+
# Mocked or partially constructed App objects must not manufacture an
|
|
725
|
+
# upgrade merely by dynamically answering arbitrary attributes.
|
|
726
|
+
if not isinstance(request, UpgradeRequest):
|
|
727
|
+
return 0
|
|
728
|
+
|
|
729
|
+
upgrade_argv = upgrade_exec_argv(request, [sys.argv[0], *raw_args])
|
|
730
|
+
if upgrade_argv is None:
|
|
731
|
+
print(
|
|
732
|
+
"error: the requested Windows app layer no longer validates; "
|
|
733
|
+
"run 'railmux runtime status --verify'",
|
|
734
|
+
file=sys.stderr,
|
|
735
|
+
)
|
|
736
|
+
return 2
|
|
737
|
+
upgrade_env = os.environ.copy()
|
|
738
|
+
upgrade_env["RAILMUX_MSYS2_RUNTIME_ID"] = request.runtime
|
|
739
|
+
upgrade_env["RAILMUX_MSYS2_APP_ID"] = request.app
|
|
740
|
+
os.execve(upgrade_argv[0], upgrade_argv, upgrade_env)
|
|
679
741
|
return 0
|
|
680
742
|
|
|
681
743
|
|
|
@@ -782,6 +782,15 @@ def main(
|
|
|
782
782
|
metavar="HOST",
|
|
783
783
|
help="edit the configuration owned by an SSH destination",
|
|
784
784
|
)
|
|
785
|
+
parser.add_argument(
|
|
786
|
+
"--remote-platform",
|
|
787
|
+
choices=("auto", "posix", "windows"),
|
|
788
|
+
default="auto",
|
|
789
|
+
help=(
|
|
790
|
+
"remote shell family for --remote: auto, posix, or windows "
|
|
791
|
+
"(default: auto)"
|
|
792
|
+
),
|
|
793
|
+
)
|
|
785
794
|
parser.add_argument(
|
|
786
795
|
"--ssh-arg",
|
|
787
796
|
action=AppendSshArgument,
|
|
@@ -810,6 +819,8 @@ def main(
|
|
|
810
819
|
parser.error("--remote and --remote-context cannot be combined")
|
|
811
820
|
if args.ssh_arg and not args.remote:
|
|
812
821
|
parser.error("--ssh-args requires --remote")
|
|
822
|
+
if args.remote_platform != "auto" and not args.remote:
|
|
823
|
+
parser.error("--remote-platform requires --remote")
|
|
813
824
|
except SystemExit as exc:
|
|
814
825
|
return int(exc.code)
|
|
815
826
|
if args.remote:
|
|
@@ -819,6 +830,7 @@ def main(
|
|
|
819
830
|
args.remote,
|
|
820
831
|
ssh_args=args.ssh_arg,
|
|
821
832
|
raw_argv=tuple([] if argv is None else argv),
|
|
833
|
+
remote_platform=args.remote_platform,
|
|
822
834
|
)
|
|
823
835
|
remote_context = bool(args.remote_context)
|
|
824
836
|
stdin = sys.stdin if stdin is None else stdin
|
|
@@ -38,7 +38,7 @@ from railmux.terminal_status import (
|
|
|
38
38
|
_VERSION_RE = re.compile(
|
|
39
39
|
r"(?<![A-Za-z0-9])v?(\d+(?:\.\d+){1,3}(?:[A-Za-z]|[-+][0-9A-Za-z.-]+)?)"
|
|
40
40
|
)
|
|
41
|
-
DOCTOR_SCHEMA_VERSION =
|
|
41
|
+
DOCTOR_SCHEMA_VERSION = 4
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
@dataclass(frozen=True)
|
|
@@ -79,6 +79,15 @@ class DirectoryDiagnostic:
|
|
|
79
79
|
writable: bool
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class ManagedWindowsDiagnostic:
|
|
84
|
+
runtime_id: str | None
|
|
85
|
+
app_version: str | None
|
|
86
|
+
base_content_id: str | None
|
|
87
|
+
running_ui_version: str | None
|
|
88
|
+
transition_status: str | None
|
|
89
|
+
|
|
90
|
+
|
|
82
91
|
@dataclass(frozen=True)
|
|
83
92
|
class DoctorSnapshot:
|
|
84
93
|
"""Versioned, privacy-safe authority shared by text and JSON output."""
|
|
@@ -103,6 +112,7 @@ class DoctorSnapshot:
|
|
|
103
112
|
preferred_agent_display: str
|
|
104
113
|
data_directories: dict[str, DirectoryDiagnostic]
|
|
105
114
|
ssh_display: SshDisplayDiagnostic
|
|
115
|
+
managed_windows: ManagedWindowsDiagnostic | None = None
|
|
106
116
|
|
|
107
117
|
|
|
108
118
|
def is_ssh_session(environ: dict[str, str] | None = None) -> bool:
|
|
@@ -350,6 +360,11 @@ def collect_doctor_snapshot(
|
|
|
350
360
|
else _legacy_tmux_diagnostic()
|
|
351
361
|
)
|
|
352
362
|
)
|
|
363
|
+
managed_windows = None
|
|
364
|
+
if running_in_windows_wrapper(env):
|
|
365
|
+
from railmux.windows_ui_transition import diagnostic_status
|
|
366
|
+
|
|
367
|
+
managed_windows = ManagedWindowsDiagnostic(**diagnostic_status(env))
|
|
353
368
|
return DoctorSnapshot(
|
|
354
369
|
schema_version=DOCTOR_SCHEMA_VERSION,
|
|
355
370
|
railmux_version=__version__,
|
|
@@ -376,6 +391,7 @@ def collect_doctor_snapshot(
|
|
|
376
391
|
),
|
|
377
392
|
},
|
|
378
393
|
ssh_display=read_ssh_display_diagnostic(),
|
|
394
|
+
managed_windows=managed_windows,
|
|
379
395
|
)
|
|
380
396
|
|
|
381
397
|
|
|
@@ -495,7 +511,7 @@ def _ssh_display_text(diagnostic: SshDisplayDiagnostic) -> str:
|
|
|
495
511
|
|
|
496
512
|
def render_doctor_text(snapshot: DoctorSnapshot) -> str:
|
|
497
513
|
"""Render the stable human report from the structured authority."""
|
|
498
|
-
lines =
|
|
514
|
+
lines = [
|
|
499
515
|
"Railmux diagnostics",
|
|
500
516
|
f"Railmux: {snapshot.railmux_version}",
|
|
501
517
|
f"Python: {snapshot.python_version}",
|
|
@@ -525,6 +541,22 @@ def render_doctor_text(snapshot: DoctorSnapshot) -> str:
|
|
|
525
541
|
f"Config: {_config_text(snapshot.config)}",
|
|
526
542
|
"Settings repair: run 'railmux config' (no tmux required)",
|
|
527
543
|
f"Preferred agent display: {snapshot.preferred_agent_display}",
|
|
544
|
+
]
|
|
545
|
+
if snapshot.managed_windows is not None:
|
|
546
|
+
managed = snapshot.managed_windows
|
|
547
|
+
identity = (
|
|
548
|
+
managed.base_content_id[:12]
|
|
549
|
+
if managed.base_content_id is not None
|
|
550
|
+
else "unavailable"
|
|
551
|
+
)
|
|
552
|
+
lines.extend((
|
|
553
|
+
f"Windows managed runtime: {managed.runtime_id or 'unavailable'}; "
|
|
554
|
+
f"content={identity}",
|
|
555
|
+
f"Windows app layer: installed={managed.app_version or 'unavailable'}, "
|
|
556
|
+
f"running={managed.running_ui_version or 'not running'}, "
|
|
557
|
+
f"transition={managed.transition_status or 'none'}",
|
|
558
|
+
))
|
|
559
|
+
lines.extend((
|
|
528
560
|
(
|
|
529
561
|
"Most recent railmux ssh (host not recorded): "
|
|
530
562
|
f"{_ssh_display_text(snapshot.ssh_display)}"
|
|
@@ -535,7 +567,7 @@ def render_doctor_text(snapshot: DoctorSnapshot) -> str:
|
|
|
535
567
|
"Privacy: session IDs, transcript content, credentials, hostnames, "
|
|
536
568
|
"and raw custom paths are omitted; review before sharing."
|
|
537
569
|
),
|
|
538
|
-
)
|
|
570
|
+
))
|
|
539
571
|
return "\n".join(lines)
|
|
540
572
|
|
|
541
573
|
|
|
@@ -281,6 +281,7 @@ class RemoteHello:
|
|
|
281
281
|
config_status: str = "valid"
|
|
282
282
|
tmux_configured: bool = False
|
|
283
283
|
config_protocol: int = 0
|
|
284
|
+
platform: str = "posix"
|
|
284
285
|
|
|
285
286
|
|
|
286
287
|
@dataclass(frozen=True)
|
|
@@ -296,6 +297,11 @@ class RemoteStartKind(Enum):
|
|
|
296
297
|
TIMEOUT = "timeout"
|
|
297
298
|
|
|
298
299
|
|
|
300
|
+
class RemoteLaunchMode(Enum):
|
|
301
|
+
POSIX = "posix"
|
|
302
|
+
DIRECT = "direct"
|
|
303
|
+
|
|
304
|
+
|
|
299
305
|
class RemoteAttachKind(Enum):
|
|
300
306
|
ACCEPTED = "accepted"
|
|
301
307
|
BUSY = "busy"
|
|
@@ -310,6 +316,15 @@ class RemoteStartup:
|
|
|
310
316
|
returncode: int | None = None
|
|
311
317
|
|
|
312
318
|
|
|
319
|
+
@dataclass(frozen=True)
|
|
320
|
+
class RemoteProbe:
|
|
321
|
+
"""One cooked-mode launch-family probe before any attach mutation."""
|
|
322
|
+
|
|
323
|
+
process: subprocess.Popen
|
|
324
|
+
startup: RemoteStartup
|
|
325
|
+
launch_mode: RemoteLaunchMode
|
|
326
|
+
|
|
327
|
+
|
|
313
328
|
def await_remote_attach_status(
|
|
314
329
|
process: subprocess.Popen,
|
|
315
330
|
timeout: float = _REMOTE_ATTACH_TIMEOUT,
|
|
@@ -369,6 +384,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
|
|
|
369
384
|
config_status = value.get("config_status", "valid")
|
|
370
385
|
tmux_configured = value.get("tmux_configured", False)
|
|
371
386
|
config_protocol = value.get("config_protocol", 0)
|
|
387
|
+
platform = value.get("platform", "posix")
|
|
372
388
|
if (
|
|
373
389
|
not isinstance(version, str)
|
|
374
390
|
or not version
|
|
@@ -383,6 +399,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
|
|
|
383
399
|
or not isinstance(config_protocol, int)
|
|
384
400
|
or isinstance(config_protocol, bool)
|
|
385
401
|
or not 0 <= config_protocol <= 65535
|
|
402
|
+
or platform not in {"posix", "windows-msys2"}
|
|
386
403
|
):
|
|
387
404
|
raise ValueError("invalid Railmux remote hello")
|
|
388
405
|
try:
|
|
@@ -397,6 +414,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
|
|
|
397
414
|
config_status,
|
|
398
415
|
tmux_configured,
|
|
399
416
|
config_protocol,
|
|
417
|
+
platform,
|
|
400
418
|
)
|
|
401
419
|
|
|
402
420
|
|
|
@@ -1540,12 +1558,18 @@ def _remote_launch_command(server_args: Sequence[str]) -> str:
|
|
|
1540
1558
|
return "; ".join(branches)
|
|
1541
1559
|
|
|
1542
1560
|
|
|
1561
|
+
def _remote_direct_launch_command(server_args: Sequence[str]) -> str:
|
|
1562
|
+
"""Build a shell-neutral command accepted by Windows OpenSSH PowerShell."""
|
|
1563
|
+
return shlex.join(["railmux", *server_args])
|
|
1564
|
+
|
|
1565
|
+
|
|
1543
1566
|
def build_remote_command_argv(
|
|
1544
1567
|
destination: str,
|
|
1545
1568
|
*,
|
|
1546
1569
|
remote_args: Sequence[str],
|
|
1547
1570
|
ssh_args: Sequence[str],
|
|
1548
1571
|
force_tty: bool = False,
|
|
1572
|
+
launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
|
|
1549
1573
|
) -> list[str]:
|
|
1550
1574
|
"""Build SSH argv through the shared remote Railmux discovery ladder."""
|
|
1551
1575
|
return [
|
|
@@ -1556,10 +1580,83 @@ def build_remote_command_argv(
|
|
|
1556
1580
|
# user arguments so a copied -t/-T flag cannot invert the transport.
|
|
1557
1581
|
"-tt" if force_tty else "-T",
|
|
1558
1582
|
destination,
|
|
1559
|
-
|
|
1583
|
+
(
|
|
1584
|
+
_remote_direct_launch_command(remote_args)
|
|
1585
|
+
if launch_mode is RemoteLaunchMode.DIRECT
|
|
1586
|
+
else _remote_launch_command(remote_args)
|
|
1587
|
+
),
|
|
1560
1588
|
]
|
|
1561
1589
|
|
|
1562
1590
|
|
|
1591
|
+
def probe_remote_launch(
|
|
1592
|
+
destination: str,
|
|
1593
|
+
*,
|
|
1594
|
+
remote_args: Sequence[str],
|
|
1595
|
+
ssh_args: Sequence[str],
|
|
1596
|
+
remote_platform: str = "auto",
|
|
1597
|
+
force_tty: bool = False,
|
|
1598
|
+
timeout: float = _REMOTE_HELLO_TIMEOUT,
|
|
1599
|
+
) -> RemoteProbe:
|
|
1600
|
+
"""Probe one remote Railmux command and pin its shell launch family.
|
|
1601
|
+
|
|
1602
|
+
``auto`` preserves the POSIX discovery ladder, then makes one direct
|
|
1603
|
+
shell-neutral attempt only after a non-SSH launch failure. Each shell
|
|
1604
|
+
family receives the full bounded startup timeout. Explicit Windows mode
|
|
1605
|
+
requires a managed Windows hello; auto mode accepts an authoritative POSIX
|
|
1606
|
+
hello reached through a shell-neutral direct command.
|
|
1607
|
+
"""
|
|
1608
|
+
if remote_platform not in {"auto", "posix", "windows"}:
|
|
1609
|
+
raise ValueError("invalid remote platform")
|
|
1610
|
+
launch_mode = (
|
|
1611
|
+
RemoteLaunchMode.DIRECT
|
|
1612
|
+
if remote_platform == "windows"
|
|
1613
|
+
else RemoteLaunchMode.POSIX
|
|
1614
|
+
)
|
|
1615
|
+
effective_ssh_args = (
|
|
1616
|
+
*ssh_args,
|
|
1617
|
+
"-o",
|
|
1618
|
+
"ServerAliveInterval=5",
|
|
1619
|
+
"-o",
|
|
1620
|
+
"ServerAliveCountMax=3",
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
def start(mode: RemoteLaunchMode) -> tuple[subprocess.Popen, RemoteStartup]:
|
|
1624
|
+
argv = build_remote_command_argv(
|
|
1625
|
+
destination,
|
|
1626
|
+
remote_args=remote_args,
|
|
1627
|
+
ssh_args=effective_ssh_args,
|
|
1628
|
+
force_tty=force_tty,
|
|
1629
|
+
launch_mode=mode,
|
|
1630
|
+
)
|
|
1631
|
+
process = _spawn_remote(argv)
|
|
1632
|
+
startup = await_remote_startup(process, timeout=timeout)
|
|
1633
|
+
return process, startup
|
|
1634
|
+
|
|
1635
|
+
process, startup = start(launch_mode)
|
|
1636
|
+
if (
|
|
1637
|
+
remote_platform == "auto"
|
|
1638
|
+
and launch_mode is RemoteLaunchMode.POSIX
|
|
1639
|
+
and startup.kind is RemoteStartKind.FAILED
|
|
1640
|
+
and startup.returncode != 255
|
|
1641
|
+
):
|
|
1642
|
+
_stop_unstarted_remote(process)
|
|
1643
|
+
launch_mode = RemoteLaunchMode.DIRECT
|
|
1644
|
+
process, startup = start(launch_mode)
|
|
1645
|
+
|
|
1646
|
+
if (
|
|
1647
|
+
startup.kind is RemoteStartKind.HELLO
|
|
1648
|
+
and startup.hello is not None
|
|
1649
|
+
and remote_platform == "windows"
|
|
1650
|
+
and startup.hello.platform != "windows-msys2"
|
|
1651
|
+
):
|
|
1652
|
+
_stop_unstarted_remote(process)
|
|
1653
|
+
raise ProbeError(
|
|
1654
|
+
"the direct remote launcher answered from a non-Windows Railmux "
|
|
1655
|
+
"runtime; use --remote-platform posix for that host"
|
|
1656
|
+
)
|
|
1657
|
+
return RemoteProbe(process, startup, launch_mode)
|
|
1658
|
+
|
|
1659
|
+
|
|
1563
1660
|
def build_ssh_argv(
|
|
1564
1661
|
destination: str,
|
|
1565
1662
|
*,
|
|
@@ -1570,6 +1667,7 @@ def build_ssh_argv(
|
|
|
1570
1667
|
ssh_args: Sequence[str],
|
|
1571
1668
|
replace_existing_client: bool = False,
|
|
1572
1669
|
existing_session_only: bool = False,
|
|
1670
|
+
launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
|
|
1573
1671
|
) -> list[str]:
|
|
1574
1672
|
server_args = _remote_server_args(
|
|
1575
1673
|
session=session,
|
|
@@ -1592,6 +1690,7 @@ def build_ssh_argv(
|
|
|
1592
1690
|
"-o",
|
|
1593
1691
|
"ServerAliveCountMax=3",
|
|
1594
1692
|
),
|
|
1693
|
+
launch_mode=launch_mode,
|
|
1595
1694
|
)
|
|
1596
1695
|
|
|
1597
1696
|
|
|
@@ -1757,6 +1856,55 @@ def remote_install_help(destination: str, version: str) -> str:
|
|
|
1757
1856
|
)
|
|
1758
1857
|
|
|
1759
1858
|
|
|
1859
|
+
def remote_windows_install_help(destination: str, version: str) -> str:
|
|
1860
|
+
return (
|
|
1861
|
+
f"Update the user-level Windows preview on {destination} from "
|
|
1862
|
+
"PowerShell, then retry:\n"
|
|
1863
|
+
f" py -m pip install --upgrade railmux=={version}\n"
|
|
1864
|
+
" railmux runtime install --yes\n"
|
|
1865
|
+
"The runtime command installs only the matching Railmux app layer and "
|
|
1866
|
+
"reuses a verified managed MSYS2 base when its pinned version is "
|
|
1867
|
+
"unchanged. It does not modify Codex or Claude session files. If 'py' "
|
|
1868
|
+
"is unavailable, use the same Windows Python executable that installed "
|
|
1869
|
+
"the Railmux command."
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
|
|
1873
|
+
def local_windows_update_help(version: str) -> str:
|
|
1874
|
+
"""Return the native-owner update path for a managed Windows client."""
|
|
1875
|
+
return (
|
|
1876
|
+
"Update this Windows Railmux installation from PowerShell, then "
|
|
1877
|
+
"retry:\n"
|
|
1878
|
+
f" py -m pip install --upgrade railmux=={version}\n"
|
|
1879
|
+
" railmux runtime install --yes\n"
|
|
1880
|
+
"Do not update the versioned MSYS2 app environment with its private "
|
|
1881
|
+
"pip; the native bootstrap owns application-layer installation."
|
|
1882
|
+
)
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
def _remote_launch_mode(args: argparse.Namespace) -> RemoteLaunchMode:
|
|
1886
|
+
selected = getattr(args, "_selected_remote_launch_mode", None)
|
|
1887
|
+
if isinstance(selected, RemoteLaunchMode):
|
|
1888
|
+
return selected
|
|
1889
|
+
if getattr(args, "remote_platform", "auto") == "windows":
|
|
1890
|
+
return RemoteLaunchMode.DIRECT
|
|
1891
|
+
return RemoteLaunchMode.POSIX
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _remember_remote_launch_mode(
|
|
1895
|
+
args: argparse.Namespace, mode: RemoteLaunchMode
|
|
1896
|
+
) -> None:
|
|
1897
|
+
args._selected_remote_launch_mode = mode
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def _remote_uses_windows_runtime(
|
|
1901
|
+
args: argparse.Namespace, hello: RemoteHello | None = None
|
|
1902
|
+
) -> bool:
|
|
1903
|
+
if hello is not None:
|
|
1904
|
+
return hello.platform == "windows-msys2"
|
|
1905
|
+
return _remote_launch_mode(args) is RemoteLaunchMode.DIRECT
|
|
1906
|
+
|
|
1907
|
+
|
|
1760
1908
|
def remote_tmux_help(destination: str) -> str:
|
|
1761
1909
|
return (
|
|
1762
1910
|
f"tmux is not installed or not on PATH on {destination}. Install it "
|
|
@@ -1823,8 +1971,12 @@ def _upgrade_local_and_restart(
|
|
|
1823
1971
|
*,
|
|
1824
1972
|
subcommand: str = "ssh",
|
|
1825
1973
|
) -> NoReturn:
|
|
1974
|
+
from railmux.provider_paths import running_in_windows_wrapper
|
|
1826
1975
|
from railmux.self_update import installed_version_matches
|
|
1827
1976
|
|
|
1977
|
+
if running_in_windows_wrapper():
|
|
1978
|
+
raise ProbeError(local_windows_update_help(version))
|
|
1979
|
+
|
|
1828
1980
|
argv = _local_upgrade_argv(version)
|
|
1829
1981
|
command_label = f"railmux {subcommand}"
|
|
1830
1982
|
print(
|
|
@@ -1989,6 +2141,7 @@ def _reconnect_remote_attach(
|
|
|
1989
2141
|
ssh_args=ssh_args,
|
|
1990
2142
|
replace_existing_client=replace_existing_client,
|
|
1991
2143
|
existing_session_only=existing_session_only,
|
|
2144
|
+
launch_mode=_remote_launch_mode(args),
|
|
1992
2145
|
)
|
|
1993
2146
|
process = (
|
|
1994
2147
|
_spawn_remote(argv, suppress_stderr=True)
|
|
@@ -2215,18 +2368,55 @@ def prepare_remote_process(
|
|
|
2215
2368
|
if before_interaction is not None:
|
|
2216
2369
|
before_interaction()
|
|
2217
2370
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2371
|
+
if on_stage is not None:
|
|
2372
|
+
on_stage("Connecting to remote host…")
|
|
2373
|
+
remote_args = _remote_server_args(
|
|
2220
2374
|
session=args.session,
|
|
2221
2375
|
width=current_size.columns,
|
|
2222
2376
|
height=current_size.lines,
|
|
2223
2377
|
fps=args.fps,
|
|
2378
|
+
)
|
|
2379
|
+
probe = probe_remote_launch(
|
|
2380
|
+
args.destination,
|
|
2381
|
+
remote_args=remote_args,
|
|
2224
2382
|
ssh_args=args.ssh_arg,
|
|
2383
|
+
remote_platform=getattr(args, "remote_platform", "auto"),
|
|
2225
2384
|
)
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
startup
|
|
2385
|
+
process = probe.process
|
|
2386
|
+
startup = probe.startup
|
|
2387
|
+
launch_mode = probe.launch_mode
|
|
2388
|
+
if startup.kind is RemoteStartKind.HELLO:
|
|
2389
|
+
_remember_remote_launch_mode(args, launch_mode)
|
|
2390
|
+
elif (
|
|
2391
|
+
getattr(args, "remote_platform", "auto") == "auto"
|
|
2392
|
+
and launch_mode is RemoteLaunchMode.DIRECT
|
|
2393
|
+
):
|
|
2394
|
+
_stop_unstarted_remote(process)
|
|
2395
|
+
if startup.returncode == 255:
|
|
2396
|
+
raise ProbeError("ssh could not connect to the remote host")
|
|
2397
|
+
if startup.kind is RemoteStartKind.TIMEOUT:
|
|
2398
|
+
raise ProbeError(
|
|
2399
|
+
"timed out while checking the remote host's direct Railmux launcher"
|
|
2400
|
+
)
|
|
2401
|
+
raise ProbeError(
|
|
2402
|
+
"the remote shell did not accept the POSIX discovery command, "
|
|
2403
|
+
"and a direct Railmux launch did not complete. If this is a "
|
|
2404
|
+
"Windows preview host:\n"
|
|
2405
|
+
f"{remote_windows_install_help(args.destination, __version__)}"
|
|
2406
|
+
)
|
|
2407
|
+
|
|
2408
|
+
if (
|
|
2409
|
+
getattr(args, "remote_platform", "auto") == "windows"
|
|
2410
|
+
and startup.kind is not RemoteStartKind.HELLO
|
|
2411
|
+
):
|
|
2412
|
+
_stop_unstarted_remote(process)
|
|
2413
|
+
if startup.returncode == 255:
|
|
2414
|
+
raise ProbeError("ssh could not connect to the remote host")
|
|
2415
|
+
if startup.kind is RemoteStartKind.TIMEOUT:
|
|
2416
|
+
raise ProbeError(
|
|
2417
|
+
"timed out waiting for the Windows Railmux compatibility handshake"
|
|
2418
|
+
)
|
|
2419
|
+
raise ProbeError(remote_windows_install_help(args.destination, __version__))
|
|
2230
2420
|
install_version = __version__
|
|
2231
2421
|
install_reason: str | None = None
|
|
2232
2422
|
if startup.kind is RemoteStartKind.MISSING:
|
|
@@ -2281,6 +2471,12 @@ def prepare_remote_process(
|
|
|
2281
2471
|
if decision.action == "prompt" and decision.prompt == "remote_install":
|
|
2282
2472
|
assert decision.reason is not None
|
|
2283
2473
|
assert decision.install_version is not None
|
|
2474
|
+
if _remote_uses_windows_runtime(args, hello):
|
|
2475
|
+
_stop_unstarted_remote(process)
|
|
2476
|
+
raise ProbeError(
|
|
2477
|
+
f"{decision.reason}\n"
|
|
2478
|
+
f"{remote_windows_install_help(args.destination, decision.install_version)}"
|
|
2479
|
+
)
|
|
2284
2480
|
consents["remote_install"] = _confirm_remote_install(
|
|
2285
2481
|
args,
|
|
2286
2482
|
decision.reason,
|
|
@@ -2295,9 +2491,14 @@ def prepare_remote_process(
|
|
|
2295
2491
|
_stop_unstarted_remote(process)
|
|
2296
2492
|
message = decision.reason or "incompatible remote Railmux"
|
|
2297
2493
|
if decision.install_version is not None:
|
|
2494
|
+
install_help = (
|
|
2495
|
+
remote_windows_install_help
|
|
2496
|
+
if _remote_uses_windows_runtime(args, hello)
|
|
2497
|
+
else remote_install_help
|
|
2498
|
+
)
|
|
2298
2499
|
message = (
|
|
2299
2500
|
f"{message}\n"
|
|
2300
|
-
f"{
|
|
2501
|
+
f"{install_help(args.destination, decision.install_version)}"
|
|
2301
2502
|
)
|
|
2302
2503
|
raise ProbeError(message)
|
|
2303
2504
|
if decision.warning is not None:
|
|
@@ -2320,6 +2521,14 @@ def prepare_remote_process(
|
|
|
2320
2521
|
raise AssertionError(f"unknown compatibility action: {decision.action}")
|
|
2321
2522
|
|
|
2322
2523
|
assert install_reason is not None
|
|
2524
|
+
if _remote_uses_windows_runtime(
|
|
2525
|
+
args, startup.hello if startup.kind is RemoteStartKind.HELLO else None
|
|
2526
|
+
):
|
|
2527
|
+
_stop_unstarted_remote(process)
|
|
2528
|
+
raise ProbeError(
|
|
2529
|
+
f"{install_reason}\n"
|
|
2530
|
+
f"{remote_windows_install_help(args.destination, install_version)}"
|
|
2531
|
+
)
|
|
2323
2532
|
if startup.kind is not RemoteStartKind.HELLO and not _confirm_remote_install(
|
|
2324
2533
|
args,
|
|
2325
2534
|
install_reason,
|
|
@@ -2430,6 +2639,16 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
|
|
2430
2639
|
default=20.0,
|
|
2431
2640
|
help="maximum coalesced display update rate, 1-60 (default: 20)",
|
|
2432
2641
|
)
|
|
2642
|
+
parser.add_argument(
|
|
2643
|
+
"--remote-platform",
|
|
2644
|
+
choices=("auto", "posix", "windows"),
|
|
2645
|
+
default="auto",
|
|
2646
|
+
help=(
|
|
2647
|
+
"remote shell family: auto tries POSIX discovery then direct "
|
|
2648
|
+
"Windows launch; windows avoids the failed POSIX probe and an "
|
|
2649
|
+
"extra password prompt (default: auto)"
|
|
2650
|
+
),
|
|
2651
|
+
)
|
|
2433
2652
|
parser.add_argument(
|
|
2434
2653
|
"--no-mouse",
|
|
2435
2654
|
action="store_true",
|
|
@@ -248,6 +248,11 @@ def _emit_remote_hello(
|
|
|
248
248
|
"config_status": config_status,
|
|
249
249
|
"tmux_configured": tmux_configured,
|
|
250
250
|
"config_protocol": REMOTE_CONFIG_PROTOCOL,
|
|
251
|
+
"platform": (
|
|
252
|
+
"windows-msys2"
|
|
253
|
+
if os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
|
|
254
|
+
else "posix"
|
|
255
|
+
),
|
|
251
256
|
"version": __version__,
|
|
252
257
|
},
|
|
253
258
|
separators=(",", ":"),
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
_DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
|
|
14
14
|
_MANAGED_BASE_MARKER = Path("/railmux-base.json")
|
|
15
|
+
_MANAGED_BASE_CONTENT_MARKER = Path("/railmux-base-content-v1.json")
|
|
15
16
|
_MANAGED_APP_ROOT = Path("/opt/railmux/apps")
|
|
16
17
|
_MANAGED_APP_ID = re.compile(
|
|
17
18
|
r"railmux-[0-9]+(?:\.[0-9]+)*(?:\.dev[0-9]+)?\Z"
|
|
@@ -59,14 +60,34 @@ def running_in_managed_windows_wrapper(
|
|
|
59
60
|
)
|
|
60
61
|
from railmux import __version__
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
base_valid = base_payload == {
|
|
63
64
|
"schema": 1,
|
|
64
65
|
"runtime": runtime_id,
|
|
65
|
-
}
|
|
66
|
+
}
|
|
67
|
+
legacy_app = app_payload == {
|
|
66
68
|
"schema": 1,
|
|
67
69
|
"runtime": runtime_id,
|
|
68
70
|
"railmux": __version__,
|
|
69
71
|
}
|
|
72
|
+
content_payload = read_safe_marker(_MANAGED_BASE_CONTENT_MARKER)
|
|
73
|
+
content_id = (
|
|
74
|
+
content_payload.get("content_id")
|
|
75
|
+
if isinstance(content_payload, dict)
|
|
76
|
+
and content_payload.get("schema") == 1
|
|
77
|
+
and content_payload.get("runtime") == runtime_id
|
|
78
|
+
else None
|
|
79
|
+
)
|
|
80
|
+
exact_app = bool(
|
|
81
|
+
isinstance(content_id, str)
|
|
82
|
+
and re.fullmatch(r"[0-9a-f]{64}", content_id)
|
|
83
|
+
and app_payload == {
|
|
84
|
+
"schema": 2,
|
|
85
|
+
"runtime": runtime_id,
|
|
86
|
+
"railmux": __version__,
|
|
87
|
+
"base_content_id": content_id,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return base_valid and (legacy_app or exact_app)
|
|
70
91
|
|
|
71
92
|
|
|
72
93
|
def private_mode_is_safe(mode: int) -> bool:
|