sshler 0.17.0__tar.gz → 1.0.2__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 (227) hide show
  1. sshler-1.0.2/PKG-INFO +1420 -0
  2. sshler-1.0.2/README.md +1369 -0
  3. {sshler-0.17.0 → sshler-1.0.2}/pyproject.toml +2 -2
  4. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/sessions.py +49 -0
  5. sshler-0.17.0/sshler/static/dist/assets/Add-Dm1kmac4.js → sshler-1.0.2/sshler/static/dist/assets/Add-DAOjsezW.js +1 -1
  6. sshler-0.17.0/sshler/static/dist/assets/Alert-D7FtyJvp.js → sshler-1.0.2/sshler/static/dist/assets/Alert-CSag0xgj.js +1 -1
  7. sshler-0.17.0/sshler/static/dist/assets/BoxesView-CDrGjJAG.js → sshler-1.0.2/sshler/static/dist/assets/BoxesView-DRK-moOl.js +1 -1
  8. sshler-0.17.0/sshler/static/dist/assets/ClaudeSessionsView-BOrEKWr8.js → sshler-1.0.2/sshler/static/dist/assets/ClaudeSessionsView-CGbHeAaj.js +1 -1
  9. sshler-0.17.0/sshler/static/dist/assets/CommanderView-DIi0CxHK.js → sshler-1.0.2/sshler/static/dist/assets/CommanderView-CeqW512i.js +1 -1
  10. sshler-0.17.0/sshler/static/dist/assets/DiffView-CG786srK.js → sshler-1.0.2/sshler/static/dist/assets/DiffView-DKQldLz_.js +3 -3
  11. sshler-0.17.0/sshler/static/dist/assets/DiffViewer-DxwSo_35.js → sshler-1.0.2/sshler/static/dist/assets/DiffViewer-BKOI30kQ.js +1 -1
  12. sshler-0.17.0/sshler/static/dist/assets/DirectoryPickerModal-LzHIzxMK.css → sshler-1.0.2/sshler/static/dist/assets/DirectoryPickerModal-BJGCZjfT.css +1 -1
  13. sshler-0.17.0/sshler/static/dist/assets/DirectoryPickerModal-vKvb-8ag.js → sshler-1.0.2/sshler/static/dist/assets/DirectoryPickerModal-g2DqKz-v.js +15 -15
  14. sshler-0.17.0/sshler/static/dist/assets/FileEditorModal-DzF8dimV.js → sshler-1.0.2/sshler/static/dist/assets/FileEditorModal-mQQlSzLp.js +1 -1
  15. sshler-0.17.0/sshler/static/dist/assets/FilesView-BIjqLja5.js → sshler-1.0.2/sshler/static/dist/assets/FilesView-BhxBCK3C.js +4 -4
  16. sshler-0.17.0/sshler/static/dist/assets/GitBadge-p9cD8NpO.js → sshler-1.0.2/sshler/static/dist/assets/GitBadge-B_4M8JOq.js +1 -1
  17. sshler-0.17.0/sshler/static/dist/assets/Grid-B3PX5w8f.js → sshler-1.0.2/sshler/static/dist/assets/Grid-86dNpU6i.js +1 -1
  18. sshler-0.17.0/sshler/static/dist/assets/InputNumber-g7gyPYSy.js → sshler-1.0.2/sshler/static/dist/assets/InputNumber-9ldqAl1a.js +1 -1
  19. sshler-0.17.0/sshler/static/dist/assets/LoginView-D0Y-adFd.js → sshler-1.0.2/sshler/static/dist/assets/LoginView-DezvuYtZ.js +1 -1
  20. sshler-1.0.2/sshler/static/dist/assets/MultiTerminalView-DDARrSaQ.css +1 -0
  21. sshler-1.0.2/sshler/static/dist/assets/MultiTerminalView-hud2MPaZ.js +1 -0
  22. sshler-0.17.0/sshler/static/dist/assets/NotFoundView-AtOfMgUM.js → sshler-1.0.2/sshler/static/dist/assets/NotFoundView-NdvchLjg.js +1 -1
  23. sshler-0.17.0/sshler/static/dist/assets/OverviewView-f8kvByZh.js → sshler-1.0.2/sshler/static/dist/assets/OverviewView-D8TLlPQz.js +1 -1
  24. sshler-0.17.0/sshler/static/dist/assets/PhArrowsClockwise.vue-BN6iWuE2.js → sshler-1.0.2/sshler/static/dist/assets/PhArrowsClockwise.vue-BLHYPaFF.js +1 -1
  25. sshler-0.17.0/sshler/static/dist/assets/PhArrowsLeftRight.vue-BgMp0jv0.js → sshler-1.0.2/sshler/static/dist/assets/PhArrowsLeftRight.vue-BBZDx8HD.js +1 -1
  26. sshler-0.17.0/sshler/static/dist/assets/PhArrowsOut.vue-gW2j8hR5.js → sshler-1.0.2/sshler/static/dist/assets/PhArrowsOut.vue-B8B678OT.js +1 -1
  27. sshler-0.17.0/sshler/static/dist/assets/PhCheck.vue-C0Bzr281.js → sshler-1.0.2/sshler/static/dist/assets/PhCheck.vue-C9DFYQgy.js +1 -1
  28. sshler-0.17.0/sshler/static/dist/assets/PhCircle.vue-CO7STcqw.js → sshler-1.0.2/sshler/static/dist/assets/PhCircle.vue-BfooAyte.js +1 -1
  29. sshler-0.17.0/sshler/static/dist/assets/PhClockCounterClockwise.vue--rQ9YiZP.js → sshler-1.0.2/sshler/static/dist/assets/PhClockCounterClockwise.vue-BLcbzc-B.js +1 -1
  30. sshler-0.17.0/sshler/static/dist/assets/PhCopy.vue-D6iFQakD.js → sshler-1.0.2/sshler/static/dist/assets/PhCopy.vue-D3RzWNcP.js +1 -1
  31. sshler-0.17.0/sshler/static/dist/assets/PhFloppyDisk.vue-Bahc2ARB.js → sshler-1.0.2/sshler/static/dist/assets/PhFloppyDisk.vue-B2uZ8xL_.js +1 -1
  32. sshler-0.17.0/sshler/static/dist/assets/PhFolderOpen.vue-C2C4Raej.js → sshler-1.0.2/sshler/static/dist/assets/PhFolderOpen.vue-DcCWZAYu.js +1 -1
  33. sshler-0.17.0/sshler/static/dist/assets/PhGitBranch.vue-in0jGuML.js → sshler-1.0.2/sshler/static/dist/assets/PhGitBranch.vue-BK8iMGps.js +1 -1
  34. sshler-0.17.0/sshler/static/dist/assets/PhList.vue-BRGwvjgo.js → sshler-1.0.2/sshler/static/dist/assets/PhList.vue-CuXqS-Yu.js +1 -1
  35. sshler-0.17.0/sshler/static/dist/assets/PhPlay.vue-DzgjCq5b.js → sshler-1.0.2/sshler/static/dist/assets/PhPlay.vue-CE6G-I1m.js +1 -1
  36. sshler-0.17.0/sshler/static/dist/assets/PhPushPinSimple.vue-PLtciArK.js → sshler-1.0.2/sshler/static/dist/assets/PhPushPinSimple.vue-CCYplIkb.js +1 -1
  37. sshler-0.17.0/sshler/static/dist/assets/PhQuestion.vue-Clsa0sLR.js → sshler-1.0.2/sshler/static/dist/assets/PhQuestion.vue-DOMPqSq0.js +1 -1
  38. sshler-0.17.0/sshler/static/dist/assets/PhTrash.vue-fjOqZANv.js → sshler-1.0.2/sshler/static/dist/assets/PhTrash.vue-EX4P1cIo.js +1 -1
  39. sshler-0.17.0/sshler/static/dist/assets/Popconfirm-Bv5eKzpy.js → sshler-1.0.2/sshler/static/dist/assets/Popconfirm-CgFPK9F0.js +1 -1
  40. sshler-0.17.0/sshler/static/dist/assets/ProgressView-GOy6Rrys.js → sshler-1.0.2/sshler/static/dist/assets/ProgressView-tCTPu2yW.js +1 -1
  41. sshler-0.17.0/sshler/static/dist/assets/RadioGroup-BAHed9XF.js → sshler-1.0.2/sshler/static/dist/assets/RadioGroup-DxCCyzGg.js +1 -1
  42. sshler-0.17.0/sshler/static/dist/assets/SettingsView-BLgva2gx.js → sshler-1.0.2/sshler/static/dist/assets/SettingsView-B1itMzkZ.js +1 -1
  43. sshler-0.17.0/sshler/static/dist/assets/Spin-Bo2AtJwN.js → sshler-1.0.2/sshler/static/dist/assets/Spin-DyIxCRqg.js +1 -1
  44. sshler-0.17.0/sshler/static/dist/assets/Switch-Db39eANt.js → sshler-1.0.2/sshler/static/dist/assets/Switch-CXzkKkdP.js +1 -1
  45. sshler-1.0.2/sshler/static/dist/assets/TerminalView-COCfd2uh.css +1 -0
  46. sshler-1.0.2/sshler/static/dist/assets/TerminalView-DWSrSgDS.js +2 -0
  47. sshler-0.17.0/sshler/static/dist/assets/arc-BGmkeJvr.js → sshler-1.0.2/sshler/static/dist/assets/arc-_G7dsxWN.js +1 -1
  48. sshler-0.17.0/sshler/static/dist/assets/architectureDiagram-3BPJPVTR-Bn6kjt_W.js → sshler-1.0.2/sshler/static/dist/assets/architectureDiagram-3BPJPVTR-BuMa823z.js +1 -1
  49. sshler-0.17.0/sshler/static/dist/assets/blockDiagram-GPEHLZMM-C4h4EXNt.js → sshler-1.0.2/sshler/static/dist/assets/blockDiagram-GPEHLZMM-BF8dyh4B.js +1 -1
  50. sshler-0.17.0/sshler/static/dist/assets/c4Diagram-AAUBKEIU-BUBkXui9.js → sshler-1.0.2/sshler/static/dist/assets/c4Diagram-AAUBKEIU-C3shTvrC.js +1 -1
  51. sshler-1.0.2/sshler/static/dist/assets/channel-OQURwrS_.js +1 -0
  52. sshler-0.17.0/sshler/static/dist/assets/chunk-2J33WTMH-CDeQj4ip.js → sshler-1.0.2/sshler/static/dist/assets/chunk-2J33WTMH-BlmNHocv.js +1 -1
  53. sshler-0.17.0/sshler/static/dist/assets/chunk-4BX2VUAB-bHpKCqlR.js → sshler-1.0.2/sshler/static/dist/assets/chunk-4BX2VUAB-FVsp0Dvs.js +1 -1
  54. sshler-0.17.0/sshler/static/dist/assets/chunk-55IACEB6-_8peECW0.js → sshler-1.0.2/sshler/static/dist/assets/chunk-55IACEB6-BeaVCTsK.js +1 -1
  55. sshler-0.17.0/sshler/static/dist/assets/chunk-727SXJPM-CbGWWMc-.js → sshler-1.0.2/sshler/static/dist/assets/chunk-727SXJPM-DOvH0YaL.js +1 -1
  56. sshler-0.17.0/sshler/static/dist/assets/chunk-AQP2D5EJ-BbMoD9jx.js → sshler-1.0.2/sshler/static/dist/assets/chunk-AQP2D5EJ-DwFGzbIM.js +1 -1
  57. sshler-0.17.0/sshler/static/dist/assets/chunk-FMBD7UC4-DUAQR3Xp.js → sshler-1.0.2/sshler/static/dist/assets/chunk-FMBD7UC4-DrOyMPnh.js +1 -1
  58. sshler-0.17.0/sshler/static/dist/assets/chunk-ND2GUHAM-Cc0fktFy.js → sshler-1.0.2/sshler/static/dist/assets/chunk-ND2GUHAM-bdPxSOzK.js +1 -1
  59. sshler-0.17.0/sshler/static/dist/assets/chunk-QZHKN3VN-DYIFhQTt.js → sshler-1.0.2/sshler/static/dist/assets/chunk-QZHKN3VN-DF1WR5Ws.js +1 -1
  60. sshler-1.0.2/sshler/static/dist/assets/classDiagram-4FO5ZUOK-BcXsVGrF.js +1 -0
  61. sshler-1.0.2/sshler/static/dist/assets/classDiagram-v2-Q7XG4LA2-BcXsVGrF.js +1 -0
  62. sshler-0.17.0/sshler/static/dist/assets/cose-bilkent-S5V4N54A-CxHAUb2k.js → sshler-1.0.2/sshler/static/dist/assets/cose-bilkent-S5V4N54A-C-3AhSiq.js +1 -1
  63. sshler-0.17.0/sshler/static/dist/assets/dagre-BM42HDAG-uPxIvrzP.js → sshler-1.0.2/sshler/static/dist/assets/dagre-BM42HDAG-BGYV-hKU.js +1 -1
  64. sshler-0.17.0/sshler/static/dist/assets/diagram-2AECGRRQ-BdIgFTeO.js → sshler-1.0.2/sshler/static/dist/assets/diagram-2AECGRRQ-HqKo4hRm.js +1 -1
  65. sshler-0.17.0/sshler/static/dist/assets/diagram-5GNKFQAL-CfrMhSJC.js → sshler-1.0.2/sshler/static/dist/assets/diagram-5GNKFQAL-DxQKZDCI.js +1 -1
  66. sshler-0.17.0/sshler/static/dist/assets/diagram-KO2AKTUF-Cb32RmsG.js → sshler-1.0.2/sshler/static/dist/assets/diagram-KO2AKTUF-BaqYu-qh.js +1 -1
  67. sshler-0.17.0/sshler/static/dist/assets/diagram-LMA3HP47-BDJW_TJV.js → sshler-1.0.2/sshler/static/dist/assets/diagram-LMA3HP47-BSXDrPx5.js +1 -1
  68. sshler-0.17.0/sshler/static/dist/assets/diagram-OG6HWLK6-BupeOZur.js → sshler-1.0.2/sshler/static/dist/assets/diagram-OG6HWLK6-fbUkjsCH.js +1 -1
  69. sshler-0.17.0/sshler/static/dist/assets/erDiagram-TEJ5UH35-DpfxF8Eo.js → sshler-1.0.2/sshler/static/dist/assets/erDiagram-TEJ5UH35-3hJqUA_3.js +1 -1
  70. sshler-0.17.0/sshler/static/dist/assets/favorites-Y47wAptK.js → sshler-1.0.2/sshler/static/dist/assets/favorites-DhWkZ_Ge.js +1 -1
  71. sshler-0.17.0/sshler/static/dist/assets/flowDiagram-I6XJVG4X-BHKR8xwX.js → sshler-1.0.2/sshler/static/dist/assets/flowDiagram-I6XJVG4X-DjLO6b70.js +1 -1
  72. sshler-0.17.0/sshler/static/dist/assets/ganttDiagram-6RSMTGT7-DtY_qM0h.js → sshler-1.0.2/sshler/static/dist/assets/ganttDiagram-6RSMTGT7-DDZ9gMyj.js +1 -1
  73. sshler-0.17.0/sshler/static/dist/assets/gitGraphDiagram-PVQCEYII-BTP-gryf.js → sshler-1.0.2/sshler/static/dist/assets/gitGraphDiagram-PVQCEYII-Z1pBUCOv.js +1 -1
  74. sshler-0.17.0/sshler/static/dist/assets/graph-D46VqQ8x.js → sshler-1.0.2/sshler/static/dist/assets/graph-1mebsZ_B.js +1 -1
  75. sshler-0.17.0/sshler/static/dist/assets/index-DM4kHmJa.js → sshler-1.0.2/sshler/static/dist/assets/index-CeAHJWSQ.js +35 -35
  76. sshler-1.0.2/sshler/static/dist/assets/infoDiagram-5YYISTIA-SEqseSvm.js +2 -0
  77. sshler-0.17.0/sshler/static/dist/assets/ishikawaDiagram-YF4QCWOH-BITvDs5E.js → sshler-1.0.2/sshler/static/dist/assets/ishikawaDiagram-YF4QCWOH-CgGECNI3.js +1 -1
  78. sshler-0.17.0/sshler/static/dist/assets/journeyDiagram-JHISSGLW-CAd4ItgJ.js → sshler-1.0.2/sshler/static/dist/assets/journeyDiagram-JHISSGLW-CnV6MMWs.js +1 -1
  79. sshler-0.17.0/sshler/static/dist/assets/kanban-definition-UN3LZRKU-DRLHY-et.js → sshler-1.0.2/sshler/static/dist/assets/kanban-definition-UN3LZRKU-CNYDNKQ8.js +1 -1
  80. sshler-0.17.0/sshler/static/dist/assets/layout-DPwttB4B.js → sshler-1.0.2/sshler/static/dist/assets/layout-CDPPC1l5.js +1 -1
  81. sshler-0.17.0/sshler/static/dist/assets/linear-De9nKNtF.js → sshler-1.0.2/sshler/static/dist/assets/linear-wLmeA8y8.js +1 -1
  82. sshler-0.17.0/sshler/static/dist/assets/mermaid.core-BK0e_Na7.js → sshler-1.0.2/sshler/static/dist/assets/mermaid.core-a_pRy7_d.js +4 -4
  83. sshler-0.17.0/sshler/static/dist/assets/mindmap-definition-RKZ34NQL-Ci7PNuZc.js → sshler-1.0.2/sshler/static/dist/assets/mindmap-definition-RKZ34NQL-CdCNJ1c3.js +1 -1
  84. sshler-0.17.0/sshler/static/dist/assets/pieDiagram-4H26LBE5-BvjS4CJM.js → sshler-1.0.2/sshler/static/dist/assets/pieDiagram-4H26LBE5-CGk3s4aU.js +1 -1
  85. sshler-0.17.0/sshler/static/dist/assets/quadrantDiagram-W4KKPZXB-C1maxTW3.js → sshler-1.0.2/sshler/static/dist/assets/quadrantDiagram-W4KKPZXB-DP62iaWi.js +1 -1
  86. sshler-0.17.0/sshler/static/dist/assets/requirementDiagram-4Y6WPE33-BSJSP__2.js → sshler-1.0.2/sshler/static/dist/assets/requirementDiagram-4Y6WPE33-D80_Wixp.js +1 -1
  87. sshler-0.17.0/sshler/static/dist/assets/sankeyDiagram-5OEKKPKP-JUmpuHCD.js → sshler-1.0.2/sshler/static/dist/assets/sankeyDiagram-5OEKKPKP-G9LZKWmT.js +1 -1
  88. sshler-0.17.0/sshler/static/dist/assets/sequenceDiagram-3UESZ5HK-DxzUiTOS.js → sshler-1.0.2/sshler/static/dist/assets/sequenceDiagram-3UESZ5HK-B82dIY8s.js +1 -1
  89. sshler-0.17.0/sshler/static/dist/assets/sessionName-C61yOUDI.js → sshler-1.0.2/sshler/static/dist/assets/sessionName-DbO6vfWs.js +1 -1
  90. sshler-0.17.0/sshler/static/dist/assets/stateDiagram-AJRCARHV-nnMoK5OM.js → sshler-1.0.2/sshler/static/dist/assets/stateDiagram-AJRCARHV-Bdc9qNnY.js +1 -1
  91. sshler-1.0.2/sshler/static/dist/assets/stateDiagram-v2-BHNVJYJU-BdScRmDh.js +1 -0
  92. sshler-0.17.0/sshler/static/dist/assets/timeline-definition-PNZ67QCA-DfgN8IKx.js → sshler-1.0.2/sshler/static/dist/assets/timeline-definition-PNZ67QCA-BLrTakYZ.js +1 -1
  93. sshler-0.17.0/sshler/static/dist/assets/vennDiagram-CIIHVFJN-DLKFRlaf.js → sshler-1.0.2/sshler/static/dist/assets/vennDiagram-CIIHVFJN-B5Ccpd4s.js +1 -1
  94. sshler-0.17.0/sshler/static/dist/assets/wardley-L42UT6IY-DhG-qN6s.js → sshler-1.0.2/sshler/static/dist/assets/wardley-L42UT6IY-CXaSjfFd.js +1 -1
  95. sshler-0.17.0/sshler/static/dist/assets/wardleyDiagram-YWT4CUSO-B3VlOuaY.js → sshler-1.0.2/sshler/static/dist/assets/wardleyDiagram-YWT4CUSO-Dyh7zrS3.js +1 -1
  96. sshler-0.17.0/sshler/static/dist/assets/xychartDiagram-2RQKCTM6-DxhnOcSQ.js → sshler-1.0.2/sshler/static/dist/assets/xychartDiagram-2RQKCTM6-C7ZRiqX6.js +1 -1
  97. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/index.html +1 -1
  98. {sshler-0.17.0 → sshler-1.0.2}/sshler/tmux.py +5 -1
  99. {sshler-0.17.0 → sshler-1.0.2}/sshler/webapp.py +21 -1
  100. {sshler-0.17.0 → sshler-1.0.2}/sshler/win_terminal_registry.py +86 -7
  101. sshler-1.0.2/sshler.egg-info/PKG-INFO +1420 -0
  102. sshler-1.0.2/sshler.egg-info/SOURCES.txt +212 -0
  103. {sshler-0.17.0 → sshler-1.0.2}/tests/test_win_terminal_registry.py +132 -1
  104. sshler-0.17.0/PKG-INFO +0 -762
  105. sshler-0.17.0/README.md +0 -711
  106. sshler-0.17.0/sshler/static/dist/assets/MultiTerminalView-Bv7pXkgA.js +0 -1
  107. sshler-0.17.0/sshler/static/dist/assets/MultiTerminalView-lbWGRsSW.css +0 -1
  108. sshler-0.17.0/sshler/static/dist/assets/TerminalView-DxuPZsIk.css +0 -1
  109. sshler-0.17.0/sshler/static/dist/assets/TerminalView-ad2SIs4y.js +0 -2
  110. sshler-0.17.0/sshler/static/dist/assets/channel-hlftFBgm.js +0 -1
  111. sshler-0.17.0/sshler/static/dist/assets/classDiagram-4FO5ZUOK-CiSO4kuX.js +0 -1
  112. sshler-0.17.0/sshler/static/dist/assets/classDiagram-v2-Q7XG4LA2-CiSO4kuX.js +0 -1
  113. sshler-0.17.0/sshler/static/dist/assets/infoDiagram-5YYISTIA-BH9QyXC2.js +0 -2
  114. sshler-0.17.0/sshler/static/dist/assets/stateDiagram-v2-BHNVJYJU-CdUDGeIj.js +0 -1
  115. sshler-0.17.0/sshler.egg-info/PKG-INFO +0 -762
  116. sshler-0.17.0/sshler.egg-info/SOURCES.txt +0 -212
  117. {sshler-0.17.0 → sshler-1.0.2}/LICENSE +0 -0
  118. {sshler-0.17.0 → sshler-1.0.2}/MANIFEST.in +0 -0
  119. {sshler-0.17.0 → sshler-1.0.2}/scripts/check_frontend_dist.py +0 -0
  120. {sshler-0.17.0 → sshler-1.0.2}/setup.cfg +0 -0
  121. {sshler-0.17.0 → sshler-1.0.2}/sshler/__init__.py +0 -0
  122. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/__init__.py +0 -0
  123. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/archive.py +0 -0
  124. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/auth.py +0 -0
  125. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/batch.py +0 -0
  126. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/boxes.py +0 -0
  127. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/claude_sessions.py +0 -0
  128. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/config.py +0 -0
  129. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/dependencies.py +0 -0
  130. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/diff.py +0 -0
  131. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/excel.py +0 -0
  132. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/files.py +0 -0
  133. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/git.py +0 -0
  134. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/grep.py +0 -0
  135. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/helpers.py +0 -0
  136. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/models.py +0 -0
  137. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/pdf.py +0 -0
  138. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/ping.py +0 -0
  139. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/progress.py +0 -0
  140. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/rate_limiting.py +0 -0
  141. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/recovery.py +0 -0
  142. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/search.py +0 -0
  143. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/snippets.py +0 -0
  144. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/stats_stream.py +0 -0
  145. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/terminal.py +0 -0
  146. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/transfer.py +0 -0
  147. {sshler-0.17.0 → sshler-1.0.2}/sshler/api/tunnels.py +0 -0
  148. {sshler-0.17.0 → sshler-1.0.2}/sshler/auth.py +0 -0
  149. {sshler-0.17.0 → sshler-1.0.2}/sshler/claude_sessions.py +0 -0
  150. {sshler-0.17.0 → sshler-1.0.2}/sshler/cli.py +0 -0
  151. {sshler-0.17.0 → sshler-1.0.2}/sshler/config.py +0 -0
  152. {sshler-0.17.0 → sshler-1.0.2}/sshler/config_cache.py +0 -0
  153. {sshler-0.17.0 → sshler-1.0.2}/sshler/pdf.py +0 -0
  154. {sshler-0.17.0 → sshler-1.0.2}/sshler/rate_limit.py +0 -0
  155. {sshler-0.17.0 → sshler-1.0.2}/sshler/scripts/install-sshler-task.ps1 +0 -0
  156. {sshler-0.17.0 → sshler-1.0.2}/sshler/scripts/remove-sshler-task.ps1 +0 -0
  157. {sshler-0.17.0 → sshler-1.0.2}/sshler/scripts/run-sshler.ps1 +0 -0
  158. {sshler-0.17.0 → sshler-1.0.2}/sshler/session.py +0 -0
  159. {sshler-0.17.0 → sshler-1.0.2}/sshler/settings.py +0 -0
  160. {sshler-0.17.0 → sshler-1.0.2}/sshler/snapshot.py +0 -0
  161. {sshler-0.17.0 → sshler-1.0.2}/sshler/spa.py +0 -0
  162. {sshler-0.17.0 → sshler-1.0.2}/sshler/ssh.py +0 -0
  163. {sshler-0.17.0 → sshler-1.0.2}/sshler/ssh_config.py +0 -0
  164. {sshler-0.17.0 → sshler-1.0.2}/sshler/ssh_pool.py +0 -0
  165. {sshler-0.17.0 → sshler-1.0.2}/sshler/state.py +0 -0
  166. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/BoxesView-Cw5NiFP6.css +0 -0
  167. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/ClaudeSessionsView-DN9qPL2F.css +0 -0
  168. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/CommanderView-DWT0Nvkn.css +0 -0
  169. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/DiffView-D1B9VV_z.css +0 -0
  170. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/DiffViewer-Ds8qyy47.css +0 -0
  171. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/FileEditorModal-CgM0I0XI.css +0 -0
  172. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/FilesView-Be4yclnr.css +0 -0
  173. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/GitBadge-sLcMA5A6.css +0 -0
  174. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/LoginView-BLrnakeI.css +0 -0
  175. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/NotFoundView-DYaoqkDb.css +0 -0
  176. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/OverviewView-CL0qAbxt.css +0 -0
  177. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/ProgressView-DdmDJk_g.css +0 -0
  178. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/SettingsView-yHE_fgSm.css +0 -0
  179. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/cytoscape.esm-ChuFH5G7.js +0 -0
  180. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/defaultLocale-DX6XiGOO.js +0 -0
  181. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/index-Dewesw9U.css +0 -0
  182. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/init-Gi6I4Gst.js +0 -0
  183. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/katex-BbnesVCl.js +0 -0
  184. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/assets/ordinal-Cboi1Yqb.js +0 -0
  185. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/favicon.png +0 -0
  186. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/fonts/MonaspaceNeon-Bold.woff2 +0 -0
  187. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/fonts/MonaspaceNeon-Regular.woff2 +0 -0
  188. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/fonts/MonaspaceNeon-Variable.woff2 +0 -0
  189. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/icon-192.png +0 -0
  190. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/icon-512-maskable.png +0 -0
  191. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/icon-512.png +0 -0
  192. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/logo.png +0 -0
  193. {sshler-0.17.0 → sshler-1.0.2}/sshler/static/dist/manifest.webmanifest +0 -0
  194. {sshler-0.17.0 → sshler-1.0.2}/sshler/validation.py +0 -0
  195. {sshler-0.17.0 → sshler-1.0.2}/sshler/winpty_proc.py +0 -0
  196. {sshler-0.17.0 → sshler-1.0.2}/sshler.egg-info/dependency_links.txt +0 -0
  197. {sshler-0.17.0 → sshler-1.0.2}/sshler.egg-info/entry_points.txt +0 -0
  198. {sshler-0.17.0 → sshler-1.0.2}/sshler.egg-info/requires.txt +0 -0
  199. {sshler-0.17.0 → sshler-1.0.2}/sshler.egg-info/top_level.txt +0 -0
  200. {sshler-0.17.0 → sshler-1.0.2}/tests/test_api_diff.py +0 -0
  201. {sshler-0.17.0 → sshler-1.0.2}/tests/test_api_progress.py +0 -0
  202. {sshler-0.17.0 → sshler-1.0.2}/tests/test_api_v1.py +0 -0
  203. {sshler-0.17.0 → sshler-1.0.2}/tests/test_archive.py +0 -0
  204. {sshler-0.17.0 → sshler-1.0.2}/tests/test_basic.py +0 -0
  205. {sshler-0.17.0 → sshler-1.0.2}/tests/test_batch_ops.py +0 -0
  206. {sshler-0.17.0 → sshler-1.0.2}/tests/test_claude_sessions.py +0 -0
  207. {sshler-0.17.0 → sshler-1.0.2}/tests/test_cli_progress.py +0 -0
  208. {sshler-0.17.0 → sshler-1.0.2}/tests/test_command_injection.py +0 -0
  209. {sshler-0.17.0 → sshler-1.0.2}/tests/test_config.py +0 -0
  210. {sshler-0.17.0 → sshler-1.0.2}/tests/test_database_indexes.py +0 -0
  211. {sshler-0.17.0 → sshler-1.0.2}/tests/test_dev_workflow.py +0 -0
  212. {sshler-0.17.0 → sshler-1.0.2}/tests/test_grep.py +0 -0
  213. {sshler-0.17.0 → sshler-1.0.2}/tests/test_handshake_status.py +0 -0
  214. {sshler-0.17.0 → sshler-1.0.2}/tests/test_httpx_ws.py +0 -0
  215. {sshler-0.17.0 → sshler-1.0.2}/tests/test_path_validation.py +0 -0
  216. {sshler-0.17.0 → sshler-1.0.2}/tests/test_progress_websocket.py +0 -0
  217. {sshler-0.17.0 → sshler-1.0.2}/tests/test_rate_limit.py +0 -0
  218. {sshler-0.17.0 → sshler-1.0.2}/tests/test_recovery.py +0 -0
  219. {sshler-0.17.0 → sshler-1.0.2}/tests/test_search.py +0 -0
  220. {sshler-0.17.0 → sshler-1.0.2}/tests/test_serve_token_cache.py +0 -0
  221. {sshler-0.17.0 → sshler-1.0.2}/tests/test_session_auth.py +0 -0
  222. {sshler-0.17.0 → sshler-1.0.2}/tests/test_snippets.py +0 -0
  223. {sshler-0.17.0 → sshler-1.0.2}/tests/test_ssh.py +0 -0
  224. {sshler-0.17.0 → sshler-1.0.2}/tests/test_stats_stream.py +0 -0
  225. {sshler-0.17.0 → sshler-1.0.2}/tests/test_tunnels.py +0 -0
  226. {sshler-0.17.0 → sshler-1.0.2}/tests/test_websocket.py +0 -0
  227. {sshler-0.17.0 → sshler-1.0.2}/tests/test_windows_terminal.py +0 -0
sshler-1.0.2/PKG-INFO ADDED
@@ -0,0 +1,1420 @@
1
+ Metadata-Version: 2.4
2
+ Name: sshler
3
+ Version: 1.0.2
4
+ Summary: A local FastAPI-powered SSH multiplexer for tmux-in-browser — from your laptop only.
5
+ Author-email: Gabu <gabu@gabu.quest>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gabu-quest/sshler
8
+ Project-URL: Repository, https://github.com/gabu-quest/sshler
9
+ Project-URL: Issues, https://github.com/gabu-quest/sshler/issues
10
+ Keywords: ssh,tmux,fastapi,sftp,vue,xterm,terminal
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Classifier: Framework :: FastAPI
22
+ Requires-Python: >=3.12
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: fastapi>=0.115.6
26
+ Requires-Dist: uvicorn[standard]>=0.30.0
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: argon2-cffi>=23.1.0
29
+ Requires-Dist: asyncssh>=2.14.0
30
+ Requires-Dist: platformdirs>=4.2
31
+ Requires-Dist: pydantic>=2.7
32
+ Requires-Dist: python-multipart>=0.0.9
33
+ Requires-Dist: sqler>=1.2025.9.24
34
+ Requires-Dist: pydantic-settings>=2.12.0
35
+ Requires-Dist: python-dotenv>=1.0.0
36
+ Requires-Dist: psutil>=5.9.0
37
+ Requires-Dist: httpx>=0.27.0
38
+ Requires-Dist: openpyxl>=3.1.5
39
+ Requires-Dist: pywinpty>=3.0.3; sys_platform == "win32"
40
+ Provides-Extra: dev
41
+ Requires-Dist: anyio>=4.4; extra == "dev"
42
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
43
+ Requires-Dist: pytest>=8.2; extra == "dev"
44
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
45
+ Requires-Dist: playwright>=1.48.0; extra == "dev"
46
+ Requires-Dist: pytest-playwright>=0.5.0; extra == "dev"
47
+ Requires-Dist: ruff>=0.5.6; extra == "dev"
48
+ Provides-Extra: pdf
49
+ Requires-Dist: playwright>=1.48.0; extra == "pdf"
50
+ Dynamic: license-file
51
+
52
+ # sshler
53
+
54
+ > 🌐 **日本語の README はこのページの末尾にあります。** → [日本語版へジャンプ](#japanese) / *Japanese README is at the bottom of this page.*
55
+
56
+ [![PyPI version](https://img.shields.io/pypi/v/sshler.svg)](https://pypi.org/project/sshler/)
57
+ [![Python versions](https://img.shields.io/pypi/pyversions/sshler.svg)](https://pypi.org/project/sshler/)
58
+ [![CI](https://github.com/gabu-quest/sshler/actions/workflows/ci.yml/badge.svg)](https://github.com/gabu-quest/sshler/actions/workflows/ci.yml)
59
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
60
+
61
+ sshler is a lightweight, local-only web UI that lets you browse remote files over SFTP and jump into tmux sessions in your browser — without installing anything on the remote host.
62
+
63
+ ## Quick Start
64
+
65
+ ```bash
66
+ # Install
67
+ pip install sshler
68
+
69
+ # Run (opens browser automatically)
70
+ sshler serve
71
+
72
+ # Or with development setup
73
+ uv sync --group dev
74
+ cd frontend && pnpm install && pnpm build && cd ..
75
+ sshler serve
76
+ ```
77
+
78
+ The app opens at `http://127.0.0.1:8822` and redirects to the Vue SPA at `/app/`.
79
+
80
+ ## Features
81
+
82
+ ### Core Features
83
+ - **Cross-platform**: Runs on Windows 11, macOS, and Linux (anywhere with Python 3.12+)
84
+ - **Local workspace**: Browse your own filesystem and launch native tmux sessions alongside remote hosts (uses WSL tmux on Windows, native tmux on Linux/macOS)
85
+ - **SSH integration**: Uses your existing SSH keys and honors OpenSSH aliases
86
+ - **Terminal in browser**: Opens `tmux new -As <session> -c <dir>` on the remote host and bridges it via WebSocket + xterm.js
87
+ - **File management**: Vue-based file browser with preview, edit, delete, and "Open Terminal Here"
88
+ - **Auto-configuration**: Creates starter config on first run
89
+ - **Alias resolution**: Falls back to `ssh -G` when DNS fails; reset overrides with one click
90
+ - **File operations**: Preview, edit (≤256 KB), and delete files with CodeMirror editor
91
+
92
+ ### Modern UI Features
93
+
94
+ **🎨 Theme & Appearance**
95
+ - **Dark/Light Theme Toggle** - Seamless theme switching with system preference detection
96
+ - **PWA Support** - Install as a standalone app with offline capabilities and app icons
97
+
98
+ **⌨️ Keyboard & Navigation**
99
+ - **Command Palette (Cmd/Ctrl+K)** - Quick access to all features with fuzzy search
100
+ - **Keyboard Shortcuts** - Press `?` to see all available shortcuts
101
+ - **Global Search (Cmd/Ctrl+Shift+F)** - Search across all files in all boxes
102
+
103
+ **📁 Enhanced File Management**
104
+ - **Drag & Drop Upload** - Drop files directly into the file browser
105
+ - **Bulk Operations** - Select multiple files with Shift+Click and Cmd/Ctrl+Click
106
+ - **Inline Rename (F2)** - Rename files without opening a modal
107
+ - **Context Menus** - Right-click for quick actions
108
+ - **Recent Files & Bookmarks** - Quick access to frequently used locations
109
+ - **File Preview Enhancements** - Toggle line numbers and word wrap in file viewer
110
+ - **Directory Download** - Download any directory as a .zip with size warnings for large dirs
111
+
112
+ **🔀 Diff Notebook**
113
+ - **`/app/diff` — multi-cell diff workspace** - Stack N file diffs vertically and review them all at once. Each diff is one `(left ↔ right)` file pair; sides are fully independent (different boxes, repos, refs). Auto-loads as you type; no Compare button.
114
+ - **Command bar** - `:add`, `:rm 2`, `:swap 1 3`, `:repo local /path`, `:clear`. Side syntax: `box:dir@ref:path`. Press `c` to focus from anywhere, `?` for help, `j`/`k` to scroll between diffs.
115
+ - **Shareable URLs** - `?n=<base64>` for self-contained share-by-paste, or hit **Save & share** for a short `/app/diff/n/<id>` URL backed by SQLite. Saves are immutable — editing a shared notebook forks. Open the **Saved** drawer to manage server saves + local recents.
116
+ - See [`docs/diff-notebook.md`](docs/diff-notebook.md) for the full user guide.
117
+
118
+ **🤖 Claude Session Dashboard**
119
+ - **`/app/claude` — resume Claude Code sessions** - Lists every resumable [Claude Code](https://claude.ai/code) conversation on this machine (read from `~/.claude/projects/*/*.jsonl`) and one-click resumes any of them into a browser terminal. Titles mirror Claude Code's own `/resume` picker: your `/rename` title first, then the AI-generated title, then the first prompt.
120
+ - **Grouped by git repo, collapsible** - Sessions are grouped by repo root (a conversation that ran in `repo/subdir` shows under `repo`, matching `/resume`), each in a collapsible section tagged with the same emoji the rest of the app uses for that directory. Sections start collapsed; a filter auto-expands matches, and Expand-all / Collapse-all buttons are one click away.
121
+ - **Timeline with metadata** - Each repo's sessions sit on a timeline with dots colored by recency (green <1h, blue <1d, amber <1wk, grey older), plus per-session tags for the subdirectory, git branch, transcript size, and Claude Code version.
122
+ - **Resume in place or jump** - **Resume** opens the conversation in the background — in the repo's top-level tmux session — and keeps you on the list; the ↗ button opens it *and* switches to the terminal. Re-resuming a session that's already open just focuses its tab, never restarting a running Claude.
123
+ - **Configurable resume command** - Defaults to `claude --resume {id}`; override it globally or per-session (inject your own launcher/flags) — `{id}` is substituted with the validated session id. Saved in your browser.
124
+ - **Shared tmux session naming** - Local sessions are named after the directory basename (the same convention the `ts` tmux-session helper uses), so if sshler is down you can attach the exact same session from a plain terminal.
125
+
126
+ **📝 Markdown Preview & PDF Export**
127
+ - **One-Click PDF Download** *(optional)* - With the `[pdf]` extra installed, every text file gets a PDF button. Works from the preview modal, files-list right-click, and the multi-select toolbar (renders selection one file at a time). Backend uses headless Chromium via Playwright, so output matches the print preview pixel-for-pixel — mermaid SVGs stay vector, code blocks stay crisp. See [Install](#install) below to enable.
128
+ - **Live Markdown Render** - Toggle source/rendered view on any `.md` file via the eyeball button
129
+ - **Mermaid Diagrams** - Flowcharts, sequence diagrams, state diagrams, timelines, class diagrams etc. render inline from ` ```mermaid ` fenced blocks. Mermaid is lazy-loaded — no bundle cost unless you actually view a markdown file with diagrams.
130
+ - **Embedded Images** - `![alt](./diagram.png)` and `![alt](../assets/foo.jpg)` resolve against the file's directory and are fetched via the preview API, then rewritten to `data:` URLs. Works for any image format the backend recognises (png/jpg/gif/webp/svg/etc.). Absolute remote paths (`/srv/docs/foo.png`) also work. Inlined images render in the modal AND survive being cloned into the print window — printed PDFs include them. Oversized images (above the preview byte limit) stay broken, same as opening them directly. `http(s)://` URLs are left alone — they load in the modal but may not appear in print depending on the print window's network access.
131
+ - **HTML Entity Decode** - Source authors can write `&lt;` / `&ge;` / `&amp;` in mermaid blocks (common dodge for markdown's `<` handling); they're decoded before mermaid sees them so the lexer doesn't choke.
132
+ - **Print to PDF** - Printer-icon button in Render mode opens a styled print window; pick "Save as PDF" in the browser print dialog. Mermaid SVGs print as vectors, stay crisp at any zoom.
133
+ - **Smart Print Layout** - A4 portrait with 18mm margins. Tall diagrams shrink to fit one page (max-height 230mm). Wide diagrams (aspect ratio > 1.4:1) auto-rotate to landscape on their own page. Print-mode CSS forces white background + black text regardless of app theme.
134
+ - **Page-Break Awareness** - `page-break-inside: avoid` on code blocks, blockquotes, mermaid diagrams, and tables keeps them from splitting across pages.
135
+ - **Inline HTML in Markdown** - Authored `<div>`/`<span>` blocks with inline styles (banners, callouts, badges) render in preview AND print. `print-color-adjust: exact` is forced so colored backgrounds aren't silently dropped by the browser's default toner-saving behavior.
136
+ - **Theme-Independent Print** - Print output always uses a light palette regardless of whether you're viewing in dark mode — avoids muddy grey-on-grey headings and table headers.
137
+ - **Print Any Text File** - The Print button in the preview modal works on any non-image file. Markdown goes through the rendered-with-diagrams path; code/text files print as monospace source with the file path in a header.
138
+ - **One-Click Print from File List** - Right-click any file → **Print**. Opens the preview modal and auto-fires the print dialog once content loads — no manual Render → Print step.
139
+
140
+ **🖥️ Terminal Features**
141
+ - **Multi-pane Layouts** - Split terminal horizontally, vertically, or in a grid
142
+ - **Session Persistence** - Restore your terminal layout on reload
143
+ - **Crash Recovery** - Periodic snapshots of tmux window state (every 30s); if WSL/system crashes, a recovery modal offers to recreate your sessions with the same window layout and working directories
144
+ - **Live Dead-Session Detection** - Detects OOM-killed or crashed tmux sessions without needing a restart; recovery modal appears automatically
145
+ - **Snapshot Freshness Indicator** - Blue dot next to the connection indicator pulses when snapshots are active, fades to grey as they age
146
+ - **Clickable File Paths** - File paths and `file://` URLs in terminal output are clickable links; file:// URLs copy to clipboard with a toast
147
+ - **Terminal Notifications** - Desktop notifications for long-running commands
148
+ - **Connection Status** - Real-time connection health indicators
149
+ - **Command Snippets** - Save and quick-insert frequently used commands per box or globally
150
+ - **Port Forwarding** - Visual SSH tunnel management (local/remote) per box
151
+ - **Per-Box Terminal Themes** - Color-code terminals by environment (prod=red, staging=green, etc.)
152
+ - **Per-Box Emoji Icons** - Deterministic emoji assigned per box for quick visual identification
153
+ - **Active Box Context** - Navigation links remember your current box when switching between views
154
+
155
+ **📱 Mobile & Touch Support**
156
+ - **Touch-Optimized** - 44px minimum touch targets for easy tapping
157
+ - **Swipe Gestures** - Swipe right to navigate back in file browser
158
+ - **Long-Press Context Menu** - Long-press files for quick actions (500ms)
159
+ - **Pull-to-Refresh** - Pull down to reload the current directory
160
+ - **Responsive Design** - Optimized layouts for tablets and phones
161
+ - **Virtual Keyboard Support** - Terminal automatically adjusts when mobile keyboard appears
162
+ - **Orientation Change** - Smooth terminal resize when rotating device
163
+ - **iOS Input Optimization** - 16px font size prevents auto-zoom on focus
164
+ - **Passive Touch Events** - Smooth scrolling with no jank
165
+ - **Mobile Fullscreen** - Minimal UI in fullscreen for maximum typing space
166
+
167
+ **📱 Mobile Terminal Input Bar**
168
+ - **Quick Keys** - Phosphor icon buttons for keys hard to type on mobile
169
+ - **Arrow Navigation** - ▲▼◀▶ for menu navigation (Claude Code, vim, etc.)
170
+ - **Enter/Tab** - Confirm selections and autocomplete
171
+ - **Escape/Stop** - Interrupt Claude Code turns or cancel operations (yellow)
172
+ - **Ctrl+C** - Kill processes (red - danger indicator)
173
+ - **Tmux Scroll Mode** - 📜 enters copy mode, ⏫⏬ for page up/down (orange group)
174
+ - **Ctrl+D** - Graceful exit/EOF (teal)
175
+ - **Help Legend** - Tap `?` to see what each button does
176
+ - **Color-Coded** - Visual grouping by function (blue=confirm, red=danger, orange=scroll)
177
+
178
+ **📱 Ultra-Thin Mobile Header**
179
+ - **14px Height** - Maximum terminal real estate (JuiceSSH-inspired)
180
+ - **Live Stats** - CPU/MEM percentages with color indicators (green/orange/red)
181
+ - **Minimal Chrome** - Just logo and stats, no buttons
182
+
183
+ **📊 Global Progress Bars**
184
+ - **Push from any script** - `sshler progress push <name> <current> <total>` from a build/CI/deploy script, the bar appears in the browser instantly. No setup, no project to configure — uses your running sshler instance.
185
+ - **Watch from anywhere** - A thin strip under the header shows the current box's bars on every page. Files, Terminal, Settings, doesn't matter — the bars are always in view.
186
+ - **Per-box subscriptions** - Bars exist server-side (global pool); each box keeps its own view of them. Subscribe `deploy` while you're on the `sshler` box, switch to `maintenance`, and the strip clears — switch back and it returns. Hit the `+` on the strip to open a picker and toggle bars for the current box. The `/app/progress` management page lists every bar with per-row subscribe toggles (scoped to the active box), delete buttons, and stale-row highlighting.
187
+ - **Bars stay until you dismiss them** - Subscribed bars persist on the strip through every status, including `done` (the green check is the payoff of subscribing). Clear one with the picker, by unsubscribing, or by deleting it from the management page. A bar flashes white once the moment it finishes.
188
+ - **Rich tooltips + metadata** - Hover any bar for a tooltip with its full label, status, count, timestamp, and any custom metadata a script attached (`--meta key=value`). Metadata is fault-tolerant — if a script sends garbage, the bar keeps advancing and just shows a small error note instead of breaking.
189
+ - **Honest percentages** - The displayed percent is rounded **down**: a 3300-step build at 3299/3300 reads 99%, never a misleading 100% until it's actually done.
190
+ - **Live updates** - One WebSocket (`/ws/progress`) fans out every upsert/delete to every connected browser tab. Multi-machine? Open `/app/progress` on your phone too.
191
+
192
+ **♿ Accessibility**
193
+ - **WCAG 2.1 AA Compliant** - Semantic HTML, ARIA labels, keyboard navigation
194
+ - **Screen Reader Support** - Proper focus management and announcements
195
+ - **Reduced Motion** - Respects `prefers-reduced-motion` system setting
196
+ - **High Contrast** - Clear visual hierarchy and color contrast
197
+
198
+ ## Install
199
+
200
+ ### PyPI (recommended)
201
+
202
+ ```bash
203
+ pip install sshler
204
+
205
+ # Launch once to create the config + systemd/service assets
206
+ sshler serve
207
+ ```
208
+
209
+ Requires Python **3.12+**.
210
+
211
+ #### Optional: PDF export
212
+
213
+ The PDF download buttons appear only when Playwright + headless Chromium are available. To enable:
214
+
215
+ ```bash
216
+ pip install "sshler[pdf]"
217
+ playwright install chromium
218
+ # (one-time, on Linux you may also need: playwright install-deps chromium)
219
+ ```
220
+
221
+ The base install works fine without this — sshler runs normally, the PDF buttons just stay hidden.
222
+
223
+ ### Development
224
+
225
+ ```bash
226
+ uv pip install -e .
227
+ # or: pip install -e .
228
+ ```
229
+
230
+ After cloning the repository, install the dev extras and run the usual tooling:
231
+
232
+ ```bash
233
+ uv sync --group dev
234
+ uv run ruff check .
235
+ uv run pytest
236
+ ```
237
+
238
+ E2E smoke test (Playwright):
239
+
240
+ ```bash
241
+ uv run playwright install chromium # one-time browser download
242
+ uv run pytest tests/e2e
243
+ # or reuse the project venv: .venv/bin/pytest tests/e2e/test_vue_app.py
244
+ ```
245
+
246
+ ## Run
247
+
248
+ ```bash
249
+ sshler serve
250
+ ```
251
+
252
+ The app will open `http://127.0.0.1:8822` in your default browser and redirect to `/app/`.
253
+
254
+ ### Building the Frontend
255
+
256
+ The Vue SPA must be built before running (pre-built in PyPI releases):
257
+
258
+ ```bash
259
+ cd frontend && pnpm install && pnpm build
260
+ # or use the CLI:
261
+ sshler build
262
+ ```
263
+
264
+ ### Development Mode
265
+
266
+ For hot-reload development:
267
+
268
+ ```bash
269
+ # Terminal 1: Backend
270
+ sshler serve --no-browser
271
+
272
+ # Terminal 2: Frontend dev server
273
+ cd frontend && pnpm dev -- --host --base /app/
274
+ # Visit http://localhost:5173/app/
275
+ ```
276
+
277
+ Or use the combined dev command:
278
+
279
+ ```bash
280
+ sshler dev # Runs both servers with hot-reload
281
+ ```
282
+
283
+ ### Pushing progress bars
284
+
285
+ Any script on the same host can push a progress bar that shows up live in the browser:
286
+
287
+ ```bash
288
+ sshler progress push build 0 100 --label "frontend build" --color blue
289
+ # ... your work ...
290
+ sshler progress push build 50 100 --color blue --meta stage=compile
291
+ sshler progress push build 100 100 --status done --color green
292
+
293
+ sshler progress list
294
+ sshler progress delete build
295
+ ```
296
+
297
+ Available flags: `--label TEXT`, `--color NAME|#hex`, `--status running|done|failed|cancelled`, `--url URL`, `--token TOKEN`.
298
+
299
+ **Metadata** — attach arbitrary fields shown in the bar's hover tooltip: `--meta KEY=VALUE` (repeatable), or `--meta-json '{"warnings":3}'`. By default each push **replaces** the metadata bag; use `--merge` to add to it, `--clear-meta` to empty it. A push with no metadata flag leaves existing metadata untouched, so a tight progress loop won't wipe it. Malformed metadata never blocks the push — the bar still advances and the tooltip shows the error.
300
+
301
+ Names must match `^[A-Za-z0-9._:-]{1,64}$` (push to the same name = update the same bar). Color names recognised: `blue green red yellow orange purple pink teal`; any CSS color (`#3b82f6`, `rgb(...)`) also works.
302
+
303
+ **Token discovery** — When sshler starts, it writes the active CSRF token to `<config_dir>/runtime-token` (`~/.config/sshler/runtime-token` on Linux, mode 0600). The CLI auto-reads it, so local pushes need no setup. Remote / cross-machine pushers can override via `--token` or `$SSHLER_TOKEN`. URL discovery similarly defaults to `http://127.0.0.1:8822`, overridable via `--url` / `$SSHLER_PROGRESS_URL`.
304
+
305
+ Subscriptions are **per box**: open a box (Files/Terminal), then hit the `+` on the header strip — or use `/app/progress` — to subscribe to the bars you want. Each box remembers its own set, so `sshler` and `maintenance` can show different bars. The thin strip under the header renders the active box's subscribed bars on every page.
306
+
307
+ See `examples/progress-bar-build-watcher.sh` for a copy-pasteable demo loop including `trap` → `--status failed` on error.
308
+
309
+ ### Key Shortcuts
310
+
311
+ - **Cmd/Ctrl+K** - Command palette
312
+ - **Alt+F** - Go to Files
313
+ - **Alt+T** - Go to Terminal
314
+ - **Alt+B** - Go to Boxes
315
+ - **Alt+L** - Go to Claude Sessions
316
+ - **?** - Show all keyboard shortcuts
317
+
318
+ ## Configuration
319
+
320
+ sshler reads your existing OpenSSH config (`~/.ssh/config`) and shows every concrete `Host` entry automatically. Any favourites, default directories, or custom hosts you add through the UI are stored in a companion YAML file.
321
+
322
+ A config file is created on first run:
323
+
324
+ - Windows: `%APPDATA%\sshler\boxes.yaml`
325
+ - macOS/Linux: `~/.config/sshler/boxes.yaml`
326
+
327
+ Example:
328
+
329
+ ```yaml
330
+ boxes:
331
+ - name: my-server
332
+ host: server.example.com # literal IP/FQDN
333
+ ssh_alias: my-server # optional: resolves via `ssh -G my-server`
334
+ user: alice
335
+ port: 22
336
+ keyfile: ~/.ssh/id_ed25519
337
+ favorites:
338
+ - /home/alice
339
+ - /home/alice/projects
340
+ - /var/www
341
+ default_dir: /home/alice
342
+ ```
343
+
344
+ > Tip: Set `default_dir` if your home path isn't `/home/<user>`.
345
+ > If you rely on an OpenSSH alias, add `ssh_alias:` and sshler will run `ssh -G` to expand it when DNS fails.
346
+
347
+ ### Resetting overrides
348
+
349
+ Boxes imported from SSH config show a highlighted border and "Refresh" button. If you change something in `~/.ssh/config`, hit Refresh to drop any stored overrides (host/user/port/key) so the new settings take effect without editing `boxes.yaml`.
350
+
351
+ ### Adding custom boxes
352
+
353
+ Hit "Add Box" in the UI to define a host that isn't in your SSH config (for example, a throwaway Docker container). Fields you leave blank fall back to your SSH defaults.
354
+
355
+ ### Security model (important)
356
+
357
+ **Localhost (127.0.0.1):** No password required. sshler binds to localhost by default and uses a random `X-SSHLER-TOKEN` for CSRF protection.
358
+
359
+ **Non-localhost:** Password REQUIRED. If you bind to `0.0.0.0` or any non-localhost address, you MUST configure authentication:
360
+
361
+ ```bash
362
+ # Set up password (recommended - creates hash in .env)
363
+ sshler hash-password
364
+
365
+ # Or use environment variables directly
366
+ export SSHLER_USERNAME=admin
367
+ export SSHLER_PASSWORD_HASH='$argon2id$...' # Use sshler hash-password to generate
368
+
369
+ # Or use CLI flag (not recommended - visible in process list)
370
+ sshler serve --host 0.0.0.0 --auth myuser:mypassword
371
+ ```
372
+
373
+ **Additional security notes:**
374
+ - **Environment variables**: Never commit your `.env` file to version control. Use `.env.example` as a template. The `.env` file may contain sensitive credentials like password hashes.
375
+ - File uploads are capped at 50 MB (tunable via `--max-upload-mb`). Uploaded content is never executed server-side.
376
+ - SSH connections still honour your system `known_hosts`. Only set `known_hosts: ignore` if you fully understand the risk.
377
+ - If you expose sshler beyond localhost, opt-in via `--allow-origin` and add `--auth user:pass` (basic auth). Use it only on networks you trust and put TLS in front (nginx, Caddy, etc.).
378
+ - There is no telemetry, analytics, or call-home behaviour.
379
+
380
+ ### CLI options
381
+
382
+ ```bash
383
+ sshler serve \
384
+ --host 127.0.0.1 \
385
+ --port 8822 \
386
+ --max-upload-mb 50 \
387
+ --allow-origin http://workstation:8822 \
388
+ --auth myuser:mypassword \
389
+ --no-ssh-alias \
390
+ --log-level info
391
+ ```
392
+
393
+ - `--host` (alias `--bind`) sets the bind address (default: `127.0.0.1` for localhost-only). Use `0.0.0.0` to expose on all interfaces, but **only on trusted networks with `--auth` and TLS**.
394
+ - `--port` sets the port number (default: `8822`).
395
+ - `--allow-origin` can be repeated to expand CORS; combine it with `--auth` if you expose the UI beyond localhost.
396
+ - `--auth user:pass` enables HTTP basic authentication (recommended if binding to `0.0.0.0`).
397
+ - `--max-upload-mb` sets the upload size limit (default: 50 MB).
398
+ - `--no-ssh-alias` disables the `ssh -G` fallback when DNS fails.
399
+ - `--token` lets you supply your own `X-SSHLER-TOKEN` (otherwise a secure random value is generated).
400
+ - `--log-level` feeds directly into uvicorn (options: `critical`, `error`, `warning`, `info`, `debug`, `trace`).
401
+
402
+ The server prints the token (and, if enabled, the basic auth username) on startup so you can copy it into API clients or browser extensions.
403
+
404
+ ### Terminal notifications
405
+
406
+ - Send a bell (`printf '\a'`) from tmux or your shell to flash the browser title and raise a desktop notification whenever the sshler tab is hidden.
407
+ - For richer messages use OSC 777: `printf '\033]777;notify=Codex%20done|Check%20the%20output\a'`. The text before the `|` becomes the title; the second part is the body.
408
+ - JSON payloads are also supported: `printf '\033]777;notify={"title":"Codex","message":"All tasks finished"}\a'`.
409
+ - The first notification prompts the browser for permission. Denying it still leaves the in-app toast and title badge when you return to the tab.
410
+
411
+ ## TLS/HTTPS Deployment
412
+
413
+ ### Why HTTPS Matters
414
+
415
+ sshler uses secure **httpOnly session cookies** for authentication. While these cookies provide strong security, browsers require the `Secure` flag to be set on cookies when serving over HTTPS. This ensures cookies are only transmitted over encrypted connections.
416
+
417
+ **For production deployments, HTTPS is strongly recommended.**
418
+
419
+ ### Deployment Options
420
+
421
+ #### 1. Localhost Development (HTTP)
422
+
423
+ For local development on `localhost` or `127.0.0.1`, you can disable the Secure cookie flag:
424
+
425
+ ```bash
426
+ # .env
427
+ SSHLER_HOST=127.0.0.1
428
+ SSHLER_PORT=8822
429
+ SSHLER_PUBLIC_URL=http://localhost:8822
430
+ SSHLER_COOKIE_SECURE=false # Only for localhost dev!
431
+ ```
432
+
433
+ **⚠️ Never use `COOKIE_SECURE=false` in production or on network-accessible interfaces.**
434
+
435
+ #### 2. Production with Caddy Reverse Proxy (Recommended)
436
+
437
+ [Caddy](https://caddyserver.com/) is the easiest way to add HTTPS to sshler. It automatically obtains and renews Let's Encrypt certificates.
438
+
439
+ **Basic Setup:**
440
+
441
+ 1. Install Caddy:
442
+ ```bash
443
+ # Ubuntu/Debian
444
+ sudo apt install caddy
445
+
446
+ # macOS
447
+ brew install caddy
448
+ ```
449
+
450
+ 2. Create a Caddyfile:
451
+ ```caddyfile
452
+ # /etc/caddy/Caddyfile or ~/Caddyfile
453
+
454
+ sshler.company.internal {
455
+ reverse_proxy localhost:8822
456
+ }
457
+ ```
458
+
459
+ 3. Configure sshler for HTTPS:
460
+ ```bash
461
+ # .env
462
+ SSHLER_HOST=127.0.0.1
463
+ SSHLER_PORT=8822
464
+ SSHLER_PUBLIC_URL=https://sshler.company.internal
465
+ SSHLER_COOKIE_SECURE=true # Required for HTTPS
466
+ ```
467
+
468
+ 4. Start Caddy:
469
+ ```bash
470
+ # System service
471
+ sudo systemctl start caddy
472
+
473
+ # Or run directly
474
+ caddy run --config /etc/caddy/Caddyfile
475
+ ```
476
+
477
+ 5. Access sshler at `https://sshler.company.internal`
478
+
479
+ **For LAN Deployments (Self-Signed Certs):**
480
+
481
+ If you're deploying on a local network without a public domain, use Caddy with a self-signed certificate:
482
+
483
+ ```caddyfile
484
+ sshler.local {
485
+ tls internal # Use Caddy's internal CA
486
+ reverse_proxy localhost:8822
487
+ }
488
+ ```
489
+
490
+ Then configure your browser to trust Caddy's local CA certificate (usually at `~/.local/share/caddy/pki/authorities/local/root.crt`).
491
+
492
+ **Advanced Caddy Configuration:**
493
+
494
+ ```caddyfile
495
+ sshler.company.internal {
496
+ # Automatic HTTPS with Let's Encrypt
497
+
498
+ # Optional: Rate limiting for API endpoints
499
+ @api {
500
+ path /api/v1/*
501
+ }
502
+ rate_limit @api 100r/m
503
+
504
+ # Stricter rate limiting for login endpoint (recommended)
505
+ @login {
506
+ path /api/v1/auth/login
507
+ }
508
+ rate_limit @login 5r/m
509
+
510
+ # Proxy to sshler
511
+ reverse_proxy localhost:8822 {
512
+ # Preserve client IP
513
+ header_up X-Real-IP {remote_host}
514
+ header_up X-Forwarded-For {remote_host}
515
+ header_up X-Forwarded-Proto {scheme}
516
+ }
517
+
518
+ # Optional: Add security headers
519
+ header {
520
+ Strict-Transport-Security "max-age=31536000;"
521
+ X-Content-Type-Options "nosniff"
522
+ X-Frame-Options "DENY"
523
+ Referrer-Policy "no-referrer"
524
+ }
525
+ }
526
+ ```
527
+
528
+ #### 3. Tailscale Deployment
529
+
530
+ If you're using [Tailscale](https://tailscale.com/), you can access sshler over your Tailscale network. Tailscale automatically provides HTTPS with MagicDNS.
531
+
532
+ 1. Configure sshler to listen on your Tailscale IP:
533
+ ```bash
534
+ # .env
535
+ SSHLER_HOST=100.64.0.1 # Your Tailscale IP
536
+ SSHLER_PORT=8822
537
+ SSHLER_PUBLIC_URL=https://yourhost.tail-scale.ts.net
538
+ SSHLER_COOKIE_SECURE=true
539
+ ```
540
+
541
+ 2. Enable Tailscale Serve (optional, for HTTPS):
542
+ ```bash
543
+ tailscale serve https / http://localhost:8822
544
+ ```
545
+
546
+ 3. Access sshler at `https://yourhost.tail-scale.ts.net`
547
+
548
+ **Note:** Tailscale provides network-level encryption, but using HTTPS ensures secure cookies work properly.
549
+
550
+ #### 4. Other Reverse Proxies
551
+
552
+ **Nginx:**
553
+
554
+ ```nginx
555
+ server {
556
+ listen 443 ssl http2;
557
+ server_name sshler.company.internal;
558
+
559
+ ssl_certificate /path/to/cert.pem;
560
+ ssl_certificate_key /path/to/key.pem;
561
+
562
+ location / {
563
+ proxy_pass http://localhost:8822;
564
+ proxy_http_version 1.1;
565
+ proxy_set_header Upgrade $http_upgrade;
566
+ proxy_set_header Connection 'upgrade';
567
+ proxy_set_header Host $host;
568
+ proxy_set_header X-Real-IP $remote_addr;
569
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
570
+ proxy_set_header X-Forwarded-Proto $scheme;
571
+ proxy_cache_bypass $http_upgrade;
572
+ }
573
+ }
574
+ ```
575
+
576
+ **Traefik:**
577
+
578
+ ```yaml
579
+ http:
580
+ routers:
581
+ sshler:
582
+ rule: "Host(`sshler.company.internal`)"
583
+ service: sshler
584
+ tls:
585
+ certResolver: letsencrypt
586
+
587
+ services:
588
+ sshler:
589
+ loadBalancer:
590
+ servers:
591
+ - url: "http://localhost:8822"
592
+ ```
593
+
594
+ ### Multi-Instance Deployments
595
+
596
+ ⚠️ **IMPORTANT**: The current session store is **in-memory** and **not suitable for multi-instance deployments** (e.g., behind a load balancer with multiple sshler processes).
597
+
598
+ **Why this matters:**
599
+ - Sessions are stored in process memory
600
+ - Each instance has its own independent session store
601
+ - Users will lose their session if requests are routed to a different instance
602
+ - Session cookies will appear invalid when load-balanced across instances
603
+
604
+ **For single-instance deployments** (most common):
605
+ - ✅ One sshler process behind a reverse proxy (Caddy, Nginx)
606
+ - ✅ Systemd service running one instance
607
+ - ✅ Docker container (single instance)
608
+
609
+ **For multi-instance/load-balanced deployments**, you must implement a shared session backend:
610
+
611
+ **Option 1: Redis (Recommended for Production)**
612
+ ```python
613
+ # Replace SessionStore with Redis-backed implementation
614
+ # See sshler/session.py for the interface to implement
615
+ ```
616
+
617
+ **Option 2: Database (PostgreSQL, MySQL)**
618
+ ```python
619
+ # Implement SessionStore backed by a database table
620
+ # Ensure all instances connect to the same database
621
+ ```
622
+
623
+ **Option 3: Sticky Sessions (Not Recommended)**
624
+ - Configure load balancer for session affinity based on cookie
625
+ - Still requires graceful handling of instance failures
626
+ - Not as robust as shared session storage
627
+
628
+ If you need multi-instance support, please open an issue or submit a PR implementing a shared session backend.
629
+
630
+ ### Security Checklist
631
+
632
+ When deploying sshler in production:
633
+
634
+ - ✅ **Use HTTPS** with a valid certificate (Let's Encrypt recommended)
635
+ - ✅ **Set `SSHLER_COOKIE_SECURE=true`** in your `.env` file
636
+ - ✅ **Set `SSHLER_PUBLIC_URL`** to your actual HTTPS URL
637
+ - ✅ **Use strong passwords** (generate with `sshler hash-password`)
638
+ - ✅ **Keep `SSHLER_REQUIRE_AUTH=true`** (never disable auth in production)
639
+ - ✅ **Bind to localhost** (`SSHLER_HOST=127.0.0.1`) when behind a reverse proxy
640
+ - ✅ **Enable firewall rules** to restrict access to trusted networks
641
+ - ✅ **Keep sshler updated** to receive security patches
642
+
643
+ ### Network Security Layers
644
+
645
+ sshler security works in layers:
646
+
647
+ 1. **Transport Security (HTTPS)** - Encrypts all traffic, protects session cookies
648
+ 2. **Application Auth (Session Cookies)** - Verifies user identity with httpOnly cookies
649
+ 3. **CSRF Protection** - Origin header validation on state-changing requests
650
+ 4. **Network Isolation** (Optional) - Tailscale, VPN, or firewall rules
651
+
652
+ **Recommendation:** Use HTTPS + session auth for most deployments. Add network isolation (Tailscale/VPN) for extra security when accessing over the internet.
653
+
654
+ ### Why Cookie Sessions Instead of JWTs?
655
+
656
+ **TL;DR**: JWTs solve distributed stateless auth. We don't have that problem. Cookie sessions are simpler, more secure, and revocable.
657
+
658
+ **Decision rationale:**
659
+
660
+ 1. **Immediate Revocation**
661
+ - Sessions can be invalidated server-side instantly (logout, security breach, admin action)
662
+ - JWTs cannot be revoked without complex deny-lists (which defeats "stateless")
663
+ - Critical for admin tools where you need emergency access control
664
+
665
+ 2. **Simpler Security Model**
666
+ - No key rotation complexity
667
+ - No JWT claims validation edge cases
668
+ - No "where do we store the JWT" bikeshedding (localStorage = XSS vulnerable, cookies = use sessions instead)
669
+
670
+ 3. **Correct Use Case**
671
+ - **JWTs are for**: Service-to-service auth, distributed microservices, mobile apps without cookie support
672
+ - **Sessions are for**: Browser-based apps talking to a single backend (sshler's architecture)
673
+
674
+ 4. **Security Benefits**
675
+ - httpOnly cookies prevent XSS token theft (JavaScript can't access them)
676
+ - SameSite=Lax prevents CSRF attacks
677
+ - Shorter attack window (8-hour default TTL vs typical JWT refresh token patterns)
678
+
679
+ **When to use JWTs:**
680
+ - Microservices passing tokens between services
681
+ - Mobile apps that can't use cookies reliably
682
+ - Truly stateless APIs serving thousands of independent clients
683
+ - Cross-domain authentication (e.g., SSO provider)
684
+
685
+ **When to use sessions (our case):**
686
+ - Browser-based admin tools
687
+ - Single backend (or shared session store)
688
+ - Need immediate revocation
689
+ - Same-origin or tightly controlled CORS deployment
690
+
691
+ **Bottom line**: We chose the boring, correct solution for browser authentication. If you need JWTs, you need a different architecture first (distributed services, mobile clients, etc.). For a browser-based SSH manager, cookie sessions are the right tool.
692
+
693
+ ## Autostart
694
+
695
+ ### Windows (Task Scheduler)
696
+
697
+ 1. Run `where sshler` to locate the installed executable (for example, `%LOCALAPPDATA%\Programs\Python\Python312\Scripts\sshler.exe`).
698
+ 2. Open **Task Scheduler → Create Task…**.
699
+ 3. Under **Triggers**, add "At log on".
700
+ 4. Under **Actions**, choose "Start a program" and point to the `sshler.exe` path. Add arguments such as `serve --no-browser` and set **Start in** to a writable directory.
701
+ 5. Tick "Run with highest privileges" if you need WSL access, then save. sshler will now launch automatically every time you sign in.
702
+
703
+ ### Linux / macOS (systemd user service)
704
+
705
+ Create `~/.config/systemd/user/sshler.service`:
706
+
707
+ ```ini
708
+ [Unit]
709
+ Description=sshler – local tmux bridge
710
+ After=network.target
711
+
712
+ [Service]
713
+ Type=simple
714
+ ExecStart=%h/.local/bin/sshler serve --bind 127.0.0.1 --no-browser
715
+ Restart=on-failure
716
+ KillMode=process
717
+
718
+ [Install]
719
+ WantedBy=default.target
720
+ ```
721
+
722
+ > **Important:** `KillMode=process` prevents systemd from killing tmux sessions when restarting the service.
723
+
724
+ Reload and enable:
725
+
726
+ ```bash
727
+ systemctl --user daemon-reload
728
+ systemctl --user enable --now sshler.service
729
+ ```
730
+
731
+ ## Claude Code skills
732
+
733
+ If you use [Claude Code](https://claude.ai/code) to develop sshler, this repo ships several skill docs that teach Claude the non-obvious parts of the codebase:
734
+
735
+ - `docs/skills/markdown-preview/SKILL.md` — what renders, what doesn't, and quirks of the print-to-PDF pipeline.
736
+ - `docs/skills/progress-bars/SKILL.md` — push protocol, WebSocket fan-out, per-box subscription model.
737
+ - `docs/skills/diff-notebook/SKILL.md` — the multi-cell diff workspace: command parser, base64 URL state, server-side persistence, the "no new backend" architecture decision.
738
+
739
+ Claude won't auto-discover these — they live in the repo but aren't registered with the Claude Code runtime. A common pattern is to **symlink them into your global skills directory** (typically `~/.claude/skills/`) so any Claude Code session on your machine can load them by name:
740
+
741
+ ```bash
742
+ # One-time setup per machine
743
+ for skill in markdown-preview progress-bars diff-notebook; do
744
+ mkdir -p ~/.claude/skills/sshler-$skill
745
+ ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
746
+ done
747
+ ```
748
+
749
+ After that, Claude sessions see skills named `sshler`, `sshler-progress-bars`, and `sshler-diff-notebook`. Restart Claude Code once (skills load at startup) and they show up alongside the built-in ones. This is the same idea as plugins/marketplaces — just hand-rolled and machine-local. The docs themselves stay versioned in the repo, so the source of truth is always `git`; the symlinks are just registration.
750
+
751
+ ## Dependencies & licenses
752
+
753
+ - FastAPI, uvicorn, asyncssh, platformdirs, pyyaml, pydantic (PyPI packages, permissive licenses)
754
+ - Vue 3 + Pinia (MIT) powers the frontend SPA
755
+ - xterm.js (MIT) provides the browser terminal
756
+ - CodeMirror (MIT) powers the file editor
757
+
758
+ All assets are used under their respective MIT/BSD-style licenses. sshler itself ships under the MIT license.
759
+
760
+ ## Why "sshler"?
761
+
762
+ Because sometimes you want less VS Code, more terminal — but still in a nice browser tab.
763
+
764
+ ---
765
+
766
+ <a id="japanese"></a>
767
+
768
+ # 日本語 (Japanese README)
769
+
770
+ > 🌐 **English README is at the top of this page.** → [英語版へ戻る](#sshler)
771
+
772
+ sshler はローカル専用の軽量 Web UI で、リモートファイルを SFTP で閲覧したり、ブラウザ上で tmux セッションに接続したりできます。リモート側に追加ソフトをインストールする必要はありません。
773
+
774
+ ## クイックスタート
775
+
776
+ ```bash
777
+ # インストール
778
+ pip install sshler
779
+
780
+ # 実行(ブラウザが自動で開きます)
781
+ sshler serve
782
+
783
+ # 開発環境でのセットアップ
784
+ uv sync --group dev
785
+ cd frontend && pnpm install && pnpm build && cd ..
786
+ sshler serve
787
+ ```
788
+
789
+ アプリは `http://127.0.0.1:8822` で開き、Vue SPA の `/app/` にリダイレクトします。
790
+
791
+ ## 特徴
792
+
793
+ ### コア機能
794
+ - **クロスプラットフォーム**: Windows 11、macOS、Linux で動作(Python 3.12+ が必要)
795
+ - **ローカルワークスペース**: ローカルファイルシステムを閲覧し、リモートホストと並べてネイティブの tmux セッションを起動(Windows では WSL tmux、Linux/macOS ではネイティブ tmux を使用)
796
+ - **SSH 統合**: 既存の SSH 鍵を使用し、OpenSSH エイリアスに対応
797
+ - **ブラウザ内ターミナル**: リモートホストで `tmux new -As <session> -c <dir>` を開き、WebSocket + xterm.js 経由で接続
798
+ - **ファイル管理**: プレビュー、編集、削除、「ここでターミナルを開く」機能を備えた Vue ベースのファイルブラウザ
799
+ - **自動設定**: 初回起動時にスターター設定を作成
800
+ - **エイリアス解決**: DNS 失敗時は `ssh -G` にフォールバック。ワンクリックで上書きをリセット
801
+ - **ファイル操作**: CodeMirror エディタでファイルのプレビュー、編集(256 KB 以下)、削除が可能
802
+
803
+ ### モダン UI 機能
804
+
805
+ **テーマと外観**
806
+ - **ダーク/ライトテーマ切替** - システム設定を検出してシームレスにテーマを切り替え
807
+ - **PWA サポート** - オフライン機能とアプリアイコンでスタンドアロンアプリとしてインストール可能
808
+
809
+ **キーボードとナビゲーション**
810
+ - **コマンドパレット (Cmd/Ctrl+K)** - あいまい検索で全機能に素早くアクセス
811
+ - **キーボードショートカット** - `?` を押すと利用可能なショートカットを表示
812
+ - **グローバル検索 (Cmd/Ctrl+Shift+F)** - 全ホストの全ファイルを横断検索
813
+
814
+ **強化されたファイル管理**
815
+ - **ドラッグ&ドロップアップロード** - ファイルブラウザに直接ファイルをドロップ
816
+ - **一括操作** - Shift+Click や Cmd/Ctrl+Click で複数ファイルを選択
817
+ - **インライン名前変更 (F2)** - モーダルを開かずにファイル名を変更
818
+ - **コンテキストメニュー** - 右クリックでクイックアクション
819
+ - **最近使ったファイルとブックマーク** - よく使う場所へのクイックアクセス
820
+ - **ファイルプレビュー強化** - ファイルビューアで行番号とワードラップを切り替え
821
+ - **ディレクトリダウンロード** - ディレクトリを .zip としてダウンロード(大容量時にサイズ警告付き)
822
+ - **PDF エクスポート(任意)** - `[pdf]` エクストラをインストールすると、プレビュー、ファイルリスト右クリック、複数選択ツールバーから PDF を 1 クリックでダウンロード可能。バックエンドが Playwright + ヘッドレス Chromium で生成するため、Mermaid 図はベクター、コードブロックは鮮明なまま。詳細は [インストール](#インストール) 参照。
823
+
824
+ **ターミナル機能**
825
+ - **マルチペインレイアウト** - ターミナルを水平、垂直、グリッドに分割
826
+ - **セッション永続化** - リロード時にターミナルレイアウトを復元
827
+ - **クラッシュリカバリ** - tmux ウィンドウ状態を30秒ごとにスナップショット。WSL/システムがクラッシュした場合、リカバリモーダルで同じウィンドウレイアウトと作業ディレクトリでセッションを再作成可能
828
+ - **ライブ検出** - OOM やクラッシュで終了した tmux セッションを再起動なしで検出、リカバリモーダルが自動表示
829
+ - **スナップショット鮮度インジケーター** - 接続インジケーターの横にある青いドットがスナップショット状態を表示
830
+ - **クリック可能なファイルパス** - ターミナル出力のファイルパスと file:// URL がクリック可能。file:// URL はクリップボードにコピー
831
+ - **ターミナル通知** - 長時間実行コマンドのデスクトップ通知
832
+ - **接続状態表示** - リアルタイムの接続状態インジケーター
833
+ - **コマンドスニペット** - よく使うコマンドをホストごとまたはグローバルに保存してワンクリック入力
834
+ - **ポートフォワーディング** - ホストごとの SSH トンネル管理(ローカル/リモート)を視覚的に操作
835
+ - **ホスト別ターミナルテーマ** - 環境ごとにターミナルを色分け(本番=赤、ステージング=緑など)
836
+ - **ホスト別絵文字アイコン** - ホストごとに固有の絵文字を割り当て、視覚的にすばやく識別
837
+ - **アクティブホストコンテキスト** - ビュー切り替え時にナビゲーションリンクが現在のホストを記憶
838
+
839
+ **モバイル & タッチサポート**
840
+ - **タッチ最適化** - 簡単にタップできる 44px 以上のタッチターゲット
841
+ - **スワイプジェスチャー** - ファイルブラウザで右スワイプして戻る
842
+ - **ロングプレスコンテキストメニュー** - ファイルを長押し(500ms)でクイックアクション
843
+ - **プルトゥリフレッシュ** - 下に引っ張って現在のディレクトリをリロード
844
+ - **レスポンシブデザイン** - タブレットやスマートフォン向けに最適化されたレイアウト
845
+ - **仮想キーボード対応** - モバイルキーボード表示時にターミナルが自動調整
846
+ - **画面回転対応** - デバイス回転時にターミナルがスムーズにリサイズ
847
+ - **iOS 入力最適化** - 16px フォントサイズでフォーカス時の自動ズームを防止
848
+ - **パッシブタッチイベント** - スクロールがスムーズでジャンクなし
849
+ - **モバイルフルスクリーン** - フルスクリーン時は最小限の UI で最大のタイピングスペース
850
+
851
+ **モバイルターミナル入力バー**
852
+ - **クイックキー** - モバイルで入力しにくいキー用の Phosphor アイコンボタン
853
+ - **矢印ナビゲーション** - メニューナビゲーション用(Claude Code、vim など)
854
+ - **Enter/Tab** - 選択確定とオートコンプリート
855
+ - **Escape/Stop** - Claude Code のターン中断や操作キャンセル(黄色)
856
+ - **Ctrl+C** - プロセス強制終了(赤 - 危険表示)
857
+ - **tmux スクロールモード** - コピーモード開始とページアップ/ダウン(オレンジ)
858
+ - **Ctrl+D** - 優雅な終了/EOF(ティール)
859
+ - **ヘルプ凡例** - `?` をタップして各ボタンの説明を表示
860
+ - **色分け** - 機能別にグループ化(青=確定、赤=危険、オレンジ=スクロール)
861
+
862
+ **超薄型モバイルヘッダー**
863
+ - **14px の高さ** - ターミナル領域を最大化(JuiceSSH にインスパイア)
864
+ - **ライブ統計** - CPU/MEM パーセンテージと色インジケーター(緑/オレンジ/赤)
865
+ - **最小限のクローム** - ロゴと統計のみ、ボタンなし
866
+
867
+ **グローバル進捗バー**
868
+ - **どんなスクリプトからでもプッシュ** - `sshler progress push <名前> <現在> <合計>` をビルド/CI/デプロイスクリプトから実行すれば、ブラウザに即座にバーが現れます。プロジェクト設定不要、起動中の sshler インスタンスを使うだけ。
869
+ - **どこからでも監視** - ヘッダー下の細いストリップが現在のホストのバーを全ページで表示します。ファイル/ターミナル/設定どこを見ていてもバーが視界に入ります。
870
+ - **ホストごとの購読** - バーはサーバー側に保存されます(グローバルプール)が、購読はホストごとに独立しています。`sshler` ホストで `deploy` を購読し `maintenance` に切り替えるとストリップは空になり、戻すと再び表示されます。ストリップの `+` ボタンでピッカーを開き、現在のホストのバーをトグルできます。`/app/progress` 管理ページでは全バーの一覧・購読トグル(アクティブなホストにスコープ)・削除・古いバーのハイライトができます。
871
+ - **消すまで残る** - 購読したバーは `done` を含むすべてのステータスでストリップに残り続けます(完了を示す緑のチェックこそ購読の目的)。ピッカー、購読解除、または管理ページからの削除でクリアできます。完了した瞬間、バーは一度だけ白く点滅します。
872
+ - **リッチなツールチップ+メタデータ** - バーにホバーすると、ラベル・ステータス・カウント・タイムスタンプ、そしてスクリプトが付加した任意のメタデータ(`--meta key=value`)を表示するツールチップが出ます。メタデータは耐障害性があり、スクリプトが不正な値を送ってもバーは進み続け、小さなエラー表示が出るだけで壊れません。
873
+ - **正直なパーセンテージ** - 表示パーセントは**切り捨て**です。3300 ステップのビルドで 3299/3300 なら 99% と表示され、実際に完了するまで誤解を招く 100% にはなりません。
874
+ - **ライブ更新** - WebSocket (`/ws/progress`) が接続中の全タブに upsert/delete イベントをファンアウト。複数マシン?スマホでも `/app/progress` を開けます。
875
+
876
+ **🤖 Claude セッションダッシュボード**
877
+ - **`/app/claude` — Claude Code セッションの再開** - このマシン上で再開可能な [Claude Code](https://claude.ai/code) の会話(`~/.claude/projects/*/*.jsonl` から読み取り)を一覧表示し、ワンクリックでブラウザ内ターミナルに再開します。タイトルは Claude Code の `/resume` ピッカーと同じ規則です(`/rename` で付けた名前 → AI 生成タイトル → 最初のプロンプトの順)。
878
+ - **Git リポジトリ単位でグループ化・折りたたみ可能** - セッションはリポジトリのルート単位でグループ化されます(`repo/subdir` で実行した会話も `/resume` と同様に `repo` の下に表示)。各グループは折りたたみ可能で、アプリの他の場所と同じディレクトリ絵文字が付きます。初期状態は折りたたみで、フィルタ入力で該当グループが自動展開され、「すべて展開/すべて折りたたむ」ボタンもあります。
879
+ - **メタデータ付きタイムライン** - 各リポジトリのセッションはタイムライン上に並び、点の色で最終更新の新しさを示します(緑 <1時間、青 <1日、橙 <1週間、それ以降は灰色)。サブディレクトリ・git ブランチ・履歴サイズ・Claude Code バージョンのタグも表示されます。
880
+ - **その場で再開/ターミナルへ移動** - **再開**ボタンは会話をバックグラウンドで(リポジトリのトップレベル tmux セッション内に)開き、一覧に留まります。↗ ボタンは開いた上でターミナルに切り替えます。すでに開いているセッションを再度再開してもそのタブにフォーカスするだけで、実行中の Claude を再起動しません。
881
+ - **再開コマンドの設定** - 既定は `claude --resume {id}`。グローバルまたはセッション単位で上書きでき(独自のランチャーやフラグを注入可能)、`{id}` は検証済みのセッション ID に置換されます。設定はブラウザに保存されます。
882
+ - **tmux セッション名の共有** - ローカルセッションはディレクトリ名(`ts` tmux セッションヘルパーと同じ規則)で命名されるため、sshler が停止していても通常のターミナルから同じセッションにアタッチできます。
883
+
884
+ **アクセシビリティ**
885
+ - **WCAG 2.1 AA 準拠** - セマンティック HTML、ARIA ラベル、キーボードナビゲーション
886
+ - **スクリーンリーダー対応** - 適切なフォーカス管理とアナウンス
887
+ - **モーション軽減** - システムの `prefers-reduced-motion` 設定を尊重
888
+ - **高コントラスト** - 明確な視覚的階層と色のコントラスト
889
+
890
+ ## インストール
891
+
892
+ ### PyPI(推奨)
893
+
894
+ ```bash
895
+ pip install sshler
896
+
897
+ # 設定ファイルと systemd/サービスアセットを作成するため一度起動
898
+ sshler serve
899
+ ```
900
+
901
+ Python **3.12+** が必要です。
902
+
903
+ #### オプション:PDF エクスポート
904
+
905
+ PDF ダウンロードボタンは Playwright + ヘッドレス Chromium が利用可能な場合のみ表示されます。有効にするには:
906
+
907
+ ```bash
908
+ pip install "sshler[pdf]"
909
+ playwright install chromium
910
+ # (初回のみ。Linux では追加で必要な場合あり: playwright install-deps chromium)
911
+ ```
912
+
913
+ ベースインストールはこれなしでも問題なく動作します — sshler は通常通り起動し、PDF ボタンが非表示になるだけです。
914
+
915
+ ### 開発用
916
+
917
+ ```bash
918
+ uv pip install -e .
919
+ # または: pip install -e .
920
+ ```
921
+
922
+ リポジトリをクローンした後、dev extras をインストールして通常のツールを実行:
923
+
924
+ ```bash
925
+ uv sync --group dev
926
+ uv run ruff check .
927
+ uv run pytest
928
+ ```
929
+
930
+ E2E スモークテスト(Playwright):
931
+
932
+ ```bash
933
+ uv run playwright install chromium # 初回のみブラウザをダウンロード
934
+ uv run pytest tests/e2e
935
+ ```
936
+
937
+ ## 実行
938
+
939
+ ```bash
940
+ sshler serve
941
+ ```
942
+
943
+ デフォルトブラウザで `http://127.0.0.1:8822` が開き、`/app/` にリダイレクトします。
944
+
945
+ ### フロントエンドのビルド
946
+
947
+ Vue SPA は実行前にビルドが必要です(PyPI リリースではプリビルド済み):
948
+
949
+ ```bash
950
+ cd frontend && pnpm install && pnpm build
951
+ # または CLI を使用:
952
+ sshler build
953
+ ```
954
+
955
+ ### 開発モード
956
+
957
+ ホットリロード開発の場合:
958
+
959
+ ```bash
960
+ # ターミナル 1: バックエンド
961
+ sshler serve --no-browser
962
+
963
+ # ターミナル 2: フロントエンド開発サーバー
964
+ cd frontend && pnpm dev -- --host --base /app/
965
+ # http://localhost:5173/app/ にアクセス
966
+ ```
967
+
968
+ または、結合された開発コマンドを使用:
969
+
970
+ ```bash
971
+ sshler dev # 両方のサーバーをホットリロードで実行
972
+ ```
973
+
974
+ ### 進捗バーをプッシュする
975
+
976
+ 同じホスト上の任意のスクリプトから、ブラウザにライブ表示される進捗バーをプッシュできます:
977
+
978
+ ```bash
979
+ sshler progress push build 0 100 --label "frontend build" --color blue
980
+ # ... 作業中 ...
981
+ sshler progress push build 50 100 --color blue --meta stage=compile
982
+ sshler progress push build 100 100 --status done --color green
983
+
984
+ sshler progress list
985
+ sshler progress delete build
986
+ ```
987
+
988
+ 利用可能なフラグ:`--label TEXT`、`--color NAME|#hex`、`--status running|done|failed|cancelled`、`--url URL`、`--token TOKEN`。
989
+
990
+ **メタデータ** — バーのホバーツールチップに表示される任意のフィールドを付加できます:`--meta KEY=VALUE`(繰り返し可)、または `--meta-json '{"warnings":3}'`。デフォルトでは各プッシュがメタデータを**置き換え**ます。`--merge` で追記、`--clear-meta` で空にします。メタデータフラグなしのプッシュは既存のメタデータをそのまま残すので、細かい進捗ループで消えることはありません。不正なメタデータがプッシュを止めることはなく、バーは進み続け、ツールチップにエラーが表示されます。
991
+
992
+ 名前は `^[A-Za-z0-9._:-]{1,64}$` にマッチする必要があります(同じ名前にプッシュ=同じバーの更新)。認識される色名:`blue green red yellow orange purple pink teal`。`#3b82f6` / `rgb(...)` のような CSS 色も使えます。
993
+
994
+ **トークンの自動検出** — sshler 起動時、有効な CSRF トークンが `<config_dir>/runtime-token`(Linux では `~/.config/sshler/runtime-token`、パーミッション 0600)に書き込まれます。CLI はこれを自動で読み取るため、ローカルからのプッシュは設定不要です。リモート/他マシンからプッシュする場合は `--token` または `$SSHLER_TOKEN` で上書き可能。URL も同様に `http://127.0.0.1:8822` がデフォルトで、`--url` / `$SSHLER_PROGRESS_URL` で上書きできます。
995
+
996
+ 購読は**ホストごと**です。ホスト(ファイル/ターミナル)を開いてから、ヘッダーストリップの `+` ボタン、または `/app/progress` で監視したいバーを購読してください。ホストごとに購読セットが記憶されるので、`sshler` と `maintenance` で異なるバーを表示できます。ヘッダー下の細いストリップが、アクティブなホストの購読済みバーを全ページで表示します。
997
+
998
+ エラー時に `trap` → `--status failed` するデモループは `examples/progress-bar-build-watcher.sh` を参照。
999
+
1000
+ ### キーショートカット
1001
+
1002
+ - **Cmd/Ctrl+K** - コマンドパレット
1003
+ - **Alt+F** - ファイルへ移動
1004
+ - **Alt+T** - ターミナルへ移動
1005
+ - **Alt+B** - ホストへ移動
1006
+ - **?** - 全キーボードショートカットを表示
1007
+
1008
+ ## 設定
1009
+
1010
+ sshler は既存の OpenSSH 設定(`~/.ssh/config`)を読み取り、すべての具体的な `Host` エントリを自動的に表示します。UI を通じて追加したお気に入り、デフォルトディレクトリ、カスタムホストは、付属の YAML ファイルに保存されます。
1011
+
1012
+ 設定ファイルは初回実行時に作成されます:
1013
+
1014
+ - Windows: `%APPDATA%\sshler\boxes.yaml`
1015
+ - macOS/Linux: `~/.config/sshler/boxes.yaml`
1016
+
1017
+ 例:
1018
+
1019
+ ```yaml
1020
+ boxes:
1021
+ - name: my-server
1022
+ host: server.example.com # IP または FQDN
1023
+ ssh_alias: my-server # オプション: `ssh -G my-server` で解決
1024
+ user: alice
1025
+ port: 22
1026
+ keyfile: ~/.ssh/id_ed25519
1027
+ favorites:
1028
+ - /home/alice
1029
+ - /home/alice/projects
1030
+ - /var/www
1031
+ default_dir: /home/alice
1032
+ ```
1033
+
1034
+ > ヒント: ホームパスが `/home/<user>` でない場合は `default_dir` を設定してください。OpenSSH エイリアスを使用する場合は `ssh_alias:` を追加すると、DNS 失敗時に `ssh -G` で解決します。
1035
+
1036
+ ### 上書き設定のリセット
1037
+
1038
+ SSH 設定から取り込まれたホストは枠が強調表示され、「Refresh」ボタンで上書き設定を削除できます。`~/.ssh/config` を更新した際はボタンを押すだけで最新状態になります。
1039
+
1040
+ ### カスタムホストの追加
1041
+
1042
+ UI の "Add Box" から SSH 設定に存在しないホストも追加できます(例: 一時的な Docker コンテナ)。未入力の項目は SSH のデフォルト値が使われます。
1043
+
1044
+ ### セキュリティモデル(重要)
1045
+
1046
+ **Localhost (127.0.0.1):** パスワード不要。sshler はデフォルトで localhost にバインドし、CSRF 保護のためにランダムな `X-SSHLER-TOKEN` を使用します。
1047
+
1048
+ **非 localhost:** パスワード必須。`0.0.0.0` またはその他の非 localhost アドレスにバインドする場合、認証の設定が必須です:
1049
+
1050
+ ```bash
1051
+ # パスワードを設定(推奨 - .env にハッシュを作成)
1052
+ sshler hash-password
1053
+
1054
+ # または環境変数を直接使用
1055
+ export SSHLER_USERNAME=admin
1056
+ export SSHLER_PASSWORD_HASH='$argon2id$...' # sshler hash-password で生成
1057
+
1058
+ # または CLI フラグを使用(非推奨 - プロセスリストに表示される)
1059
+ sshler serve --host 0.0.0.0 --auth myuser:mypassword
1060
+ ```
1061
+
1062
+ **追加のセキュリティに関する注意:**
1063
+ - **環境変数**: `.env` ファイルをバージョン管理にコミットしないでください。`.env.example` をテンプレートとして使用してください。`.env` ファイルにはパスワードハッシュなどの機密情報が含まれる可能性があります。
1064
+ - ファイルアップロードは 50 MB まで(`--max-upload-mb` で調整可能)。アップロードされたコンテンツはサーバー側で実行されません。
1065
+ - SSH 接続はシステムの `known_hosts` を尊重します。リスクを完全に理解している場合のみ `known_hosts: ignore` を設定してください。
1066
+ - ローカルホスト以外に公開する場合は、`--allow-origin` でオプトインし、`--auth user:pass`(Basic 認証)を追加してください。信頼できるネットワークでのみ使用し、TLS(nginx、Caddy など)を前段に配置してください。
1067
+ - テレメトリ、アナリティクス、コールホーム機能は一切ありません。
1068
+
1069
+ ### CLI オプション
1070
+
1071
+ ```bash
1072
+ sshler serve \
1073
+ --host 127.0.0.1 \
1074
+ --port 8822 \
1075
+ --max-upload-mb 50 \
1076
+ --allow-origin http://workstation:8822 \
1077
+ --auth myuser:mypassword \
1078
+ --no-ssh-alias \
1079
+ --log-level info
1080
+ ```
1081
+
1082
+ - `--host`(別名 `--bind`): バインドアドレスを設定(デフォルト: `127.0.0.1` でローカルホストのみ)。すべてのインターフェースに公開するには `0.0.0.0` を使用しますが、**信頼できるネットワーク上でのみ `--auth` と TLS を併用してください**。
1083
+ - `--port`: ポート番号を設定(デフォルト: `8822`)。
1084
+ - `--allow-origin`: CORS を拡張するために繰り返し使用可能。ローカルホスト以外に UI を公開する場合は `--auth` と組み合わせてください。
1085
+ - `--auth user:pass`: HTTP Basic 認証を有効化(`0.0.0.0` にバインドする場合は推奨)。
1086
+ - `--max-upload-mb`: アップロードサイズ制限を設定(デフォルト: 50 MB)。
1087
+ - `--no-ssh-alias`: DNS 失敗時の `ssh -G` フォールバックを無効化。
1088
+ - `--token`: 独自の `X-SSHLER-TOKEN` を指定(指定しない場合は安全なランダム値が生成されます)。
1089
+ - `--log-level`: uvicorn に直接渡されます(オプション: `critical`、`error`、`warning`、`info`、`debug`、`trace`)。
1090
+
1091
+ サーバーは起動時にトークン(および有効にした場合は Basic 認証のユーザー名)を出力するので、API クライアントやブラウザ拡張機能にコピーできます。
1092
+
1093
+ ### ターミナル通知
1094
+
1095
+ - tmux またはシェルからベル(`printf '\a'`)を送信すると、sshler タブが非表示のときにブラウザタイトルが点滅し、デスクトップ通知が表示されます。
1096
+ - より豊富なメッセージには OSC 777 を使用: `printf '\033]777;notify=Codex%20done|Check%20the%20output\a'`。`|` の前のテキストがタイトルになり、後半が本文になります。
1097
+ - JSON ペイロードもサポート: `printf '\033]777;notify={"title":"Codex","message":"All tasks finished"}\a'`。
1098
+ - 初回の通知はブラウザの許可を求めます。拒否した場合でも、タブに戻ったときにアプリ内トーストとタイトルバッジが表示されます。
1099
+
1100
+ ## TLS/HTTPS デプロイ
1101
+
1102
+ ### HTTPS が重要な理由
1103
+
1104
+ sshler は認証にセキュアな **httpOnly セッション Cookie** を使用します。ブラウザは HTTPS 経由で配信する際に Cookie に `Secure` フラグが設定されていることを要求します。これにより、Cookie は暗号化された接続でのみ送信されます。
1105
+
1106
+ **本番環境では HTTPS を強く推奨します。**
1107
+
1108
+ ### デプロイオプション
1109
+
1110
+ #### 1. ローカル開発(HTTP)
1111
+
1112
+ `localhost` または `127.0.0.1` でのローカル開発では、Secure Cookie フラグを無効化できます:
1113
+
1114
+ ```bash
1115
+ # .env
1116
+ SSHLER_HOST=127.0.0.1
1117
+ SSHLER_PORT=8822
1118
+ SSHLER_PUBLIC_URL=http://localhost:8822
1119
+ SSHLER_COOKIE_SECURE=false # localhost 開発専用!
1120
+ ```
1121
+
1122
+ **本番環境やネットワークアクセス可能なインターフェースでは絶対に `COOKIE_SECURE=false` を使用しないでください。**
1123
+
1124
+ #### 2. Caddy リバースプロキシによる本番運用(推奨)
1125
+
1126
+ [Caddy](https://caddyserver.com/) は sshler に HTTPS を追加する最も簡単な方法です。Let's Encrypt 証明書を自動的に取得・更新します。
1127
+
1128
+ **基本セットアップ:**
1129
+
1130
+ 1. Caddy をインストール:
1131
+ ```bash
1132
+ # Ubuntu/Debian
1133
+ sudo apt install caddy
1134
+
1135
+ # macOS
1136
+ brew install caddy
1137
+ ```
1138
+
1139
+ 2. Caddyfile を作成:
1140
+ ```caddyfile
1141
+ # /etc/caddy/Caddyfile または ~/Caddyfile
1142
+
1143
+ sshler.company.internal {
1144
+ reverse_proxy localhost:8822
1145
+ }
1146
+ ```
1147
+
1148
+ 3. sshler を HTTPS 用に設定:
1149
+ ```bash
1150
+ # .env
1151
+ SSHLER_HOST=127.0.0.1
1152
+ SSHLER_PORT=8822
1153
+ SSHLER_PUBLIC_URL=https://sshler.company.internal
1154
+ SSHLER_COOKIE_SECURE=true # HTTPS 必須
1155
+ ```
1156
+
1157
+ 4. Caddy を起動:
1158
+ ```bash
1159
+ # システムサービス
1160
+ sudo systemctl start caddy
1161
+
1162
+ # または直接実行
1163
+ caddy run --config /etc/caddy/Caddyfile
1164
+ ```
1165
+
1166
+ 5. `https://sshler.company.internal` で sshler にアクセス
1167
+
1168
+ **LAN デプロイ(自己署名証明書):**
1169
+
1170
+ パブリックドメインのないローカルネットワークにデプロイする場合は、Caddy で自己署名証明書を使用:
1171
+
1172
+ ```caddyfile
1173
+ sshler.local {
1174
+ tls internal # Caddy の内部 CA を使用
1175
+ reverse_proxy localhost:8822
1176
+ }
1177
+ ```
1178
+
1179
+ ブラウザで Caddy のローカル CA 証明書を信頼するように設定してください(通常 `~/.local/share/caddy/pki/authorities/local/root.crt` にあります)。
1180
+
1181
+ **高度な Caddy 設定:**
1182
+
1183
+ ```caddyfile
1184
+ sshler.company.internal {
1185
+ # Let's Encrypt による自動 HTTPS
1186
+
1187
+ # オプション: API エンドポイントのレート制限
1188
+ @api {
1189
+ path /api/v1/*
1190
+ }
1191
+ rate_limit @api 100r/m
1192
+
1193
+ # ログインエンドポイントのより厳格なレート制限(推奨)
1194
+ @login {
1195
+ path /api/v1/auth/login
1196
+ }
1197
+ rate_limit @login 5r/m
1198
+
1199
+ # sshler へのプロキシ
1200
+ reverse_proxy localhost:8822 {
1201
+ # クライアント IP を保持
1202
+ header_up X-Real-IP {remote_host}
1203
+ header_up X-Forwarded-For {remote_host}
1204
+ header_up X-Forwarded-Proto {scheme}
1205
+ }
1206
+
1207
+ # オプション: セキュリティヘッダーを追加
1208
+ header {
1209
+ Strict-Transport-Security "max-age=31536000;"
1210
+ X-Content-Type-Options "nosniff"
1211
+ X-Frame-Options "DENY"
1212
+ Referrer-Policy "no-referrer"
1213
+ }
1214
+ }
1215
+ ```
1216
+
1217
+ #### 3. Tailscale デプロイ
1218
+
1219
+ [Tailscale](https://tailscale.com/) を使用している場合、Tailscale ネットワーク経由で sshler にアクセスできます。Tailscale は MagicDNS で自動的に HTTPS を提供します。
1220
+
1221
+ 1. Tailscale IP でリッスンするように sshler を設定:
1222
+ ```bash
1223
+ # .env
1224
+ SSHLER_HOST=100.64.0.1 # あなたの Tailscale IP
1225
+ SSHLER_PORT=8822
1226
+ SSHLER_PUBLIC_URL=https://yourhost.tail-scale.ts.net
1227
+ SSHLER_COOKIE_SECURE=true
1228
+ ```
1229
+
1230
+ 2. Tailscale Serve を有効化(オプション、HTTPS 用):
1231
+ ```bash
1232
+ tailscale serve https / http://localhost:8822
1233
+ ```
1234
+
1235
+ 3. `https://yourhost.tail-scale.ts.net` で sshler にアクセス
1236
+
1237
+ **注意:** Tailscale はネットワークレベルの暗号化を提供しますが、HTTPS を使用することでセキュア Cookie が正しく動作します。
1238
+
1239
+ #### 4. その他のリバースプロキシ
1240
+
1241
+ **Nginx:**
1242
+
1243
+ ```nginx
1244
+ server {
1245
+ listen 443 ssl http2;
1246
+ server_name sshler.company.internal;
1247
+
1248
+ ssl_certificate /path/to/cert.pem;
1249
+ ssl_certificate_key /path/to/key.pem;
1250
+
1251
+ location / {
1252
+ proxy_pass http://localhost:8822;
1253
+ proxy_http_version 1.1;
1254
+ proxy_set_header Upgrade $http_upgrade;
1255
+ proxy_set_header Connection 'upgrade';
1256
+ proxy_set_header Host $host;
1257
+ proxy_set_header X-Real-IP $remote_addr;
1258
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1259
+ proxy_set_header X-Forwarded-Proto $scheme;
1260
+ proxy_cache_bypass $http_upgrade;
1261
+ }
1262
+ }
1263
+ ```
1264
+
1265
+ **Traefik:**
1266
+
1267
+ ```yaml
1268
+ http:
1269
+ routers:
1270
+ sshler:
1271
+ rule: "Host(`sshler.company.internal`)"
1272
+ service: sshler
1273
+ tls:
1274
+ certResolver: letsencrypt
1275
+
1276
+ services:
1277
+ sshler:
1278
+ loadBalancer:
1279
+ servers:
1280
+ - url: "http://localhost:8822"
1281
+ ```
1282
+
1283
+ ### マルチインスタンスデプロイ
1284
+
1285
+ **重要**: 現在のセッションストアは**インメモリ**であり、**マルチインスタンスデプロイには適していません**(例: ロードバランサー配下で複数の sshler プロセスを実行する場合)。
1286
+
1287
+ **影響:**
1288
+ - セッションはプロセスメモリに保存されます
1289
+ - 各インスタンスは独立したセッションストアを持ちます
1290
+ - リクエストが異なるインスタンスにルーティングされるとセッションが失われます
1291
+ - ロードバランシング時にセッション Cookie が無効として扱われます
1292
+
1293
+ **シングルインスタンスデプロイ**(最も一般的):
1294
+ - リバースプロキシ(Caddy、Nginx)の背後で sshler プロセスを 1 つ実行
1295
+ - systemd サービスで 1 インスタンスを実行
1296
+ - Docker コンテナ(シングルインスタンス)
1297
+
1298
+ マルチインスタンスサポートが必要な場合は、Issue を作成するか、共有セッションバックエンドを実装する PR を送信してください。
1299
+
1300
+ ### セキュリティチェックリスト
1301
+
1302
+ sshler を本番環境にデプロイする際:
1303
+
1304
+ - **HTTPS を使用** - 有効な証明書で(Let's Encrypt 推奨)
1305
+ - **`SSHLER_COOKIE_SECURE=true` を設定** - `.env` ファイルで
1306
+ - **`SSHLER_PUBLIC_URL` を設定** - 実際の HTTPS URL に
1307
+ - **強力なパスワードを使用** - `sshler hash-password` で生成
1308
+ - **`SSHLER_REQUIRE_AUTH=true` を維持** - 本番環境では認証を無効化しない
1309
+ - **localhost にバインド** - リバースプロキシの背後では `SSHLER_HOST=127.0.0.1`
1310
+ - **ファイアウォールルールを有効化** - 信頼できるネットワークにアクセスを制限
1311
+ - **sshler を最新に保つ** - セキュリティパッチを受け取るため
1312
+
1313
+ ### ネットワークセキュリティレイヤー
1314
+
1315
+ sshler のセキュリティは多層構造です:
1316
+
1317
+ 1. **トランスポートセキュリティ(HTTPS)** - すべてのトラフィックを暗号化し、セッション Cookie を保護
1318
+ 2. **アプリケーション認証(セッション Cookie)** - httpOnly Cookie でユーザー ID を検証
1319
+ 3. **CSRF 保護** - 状態変更リクエストに対する Origin ヘッダー検証
1320
+ 4. **ネットワーク分離**(オプション) - Tailscale、VPN、またはファイアウォールルール
1321
+
1322
+ **推奨:** ほとんどのデプロイでは HTTPS + セッション認証を使用してください。インターネット経由でアクセスする場合は、ネットワーク分離(Tailscale/VPN)を追加してセキュリティを強化してください。
1323
+
1324
+ ### Cookie セッションを選んだ理由(JWT ではなく)
1325
+
1326
+ **要約**: JWT は分散ステートレス認証の問題を解決します。sshler にはその問題がありません。Cookie セッションの方がシンプルで安全、かつ失効が可能です。
1327
+
1328
+ **判断の根拠:**
1329
+
1330
+ 1. **即時失効**
1331
+ - セッションはサーバー側で即座に無効化可能(ログアウト、セキュリティ侵害、管理者操作)
1332
+ - JWT は複雑な拒否リストなしでは失効不可能(それでは「ステートレス」の意味がない)
1333
+ - 緊急のアクセス制御が必要な管理ツールでは重要
1334
+
1335
+ 2. **シンプルなセキュリティモデル**
1336
+ - 鍵ローテーションの複雑さなし
1337
+ - JWT クレーム検証のエッジケースなし
1338
+ - 「JWT をどこに保存するか」の議論なし(localStorage = XSS 脆弱、Cookie = それならセッションを使うべき)
1339
+
1340
+ 3. **適切なユースケース**
1341
+ - **JWT が向いている場面**: サービス間認証、分散マイクロサービス、Cookie をサポートしないモバイルアプリ
1342
+ - **セッションが向いている場面**: 単一バックエンドと通信するブラウザベースアプリ(sshler のアーキテクチャ)
1343
+
1344
+ 4. **セキュリティ上の利点**
1345
+ - httpOnly Cookie は XSS によるトークン窃取を防止(JavaScript からアクセス不可)
1346
+ - SameSite=Lax は CSRF 攻撃を防止
1347
+ - 短い攻撃ウィンドウ(デフォルト 8 時間 TTL vs 一般的な JWT リフレッシュトークンパターン)
1348
+
1349
+ **結論**: ブラウザ認証のための退屈だけど正しいソリューションを選びました。JWT が必要な場合は、まず異なるアーキテクチャが必要です(分散サービス、モバイルクライアントなど)。ブラウザベースの SSH マネージャーには、Cookie セッションが適切なツールです。
1350
+
1351
+ ## 自動起動
1352
+
1353
+ ### Windows(タスク スケジューラ)
1354
+
1355
+ 1. `where sshler` を実行してインストールされた実行可能ファイルを見つけます(例: `%LOCALAPPDATA%\Programs\Python\Python312\Scripts\sshler.exe`)。
1356
+ 2. **タスク スケジューラ → タスクの作成…** を開きます。
1357
+ 3. **トリガー** で「ログオン時」を追加。
1358
+ 4. **操作** で「プログラムの開始」を選択し、`sshler.exe` のパスを指定。引数に `serve --no-browser` を追加し、**開始** を書き込み可能なディレクトリに設定。
1359
+ 5. WSL アクセスが必要な場合は「最上位の特権で実行する」にチェックを入れて保存。サインインするたびに sshler が自動起動します。
1360
+
1361
+ ### Linux / macOS(systemd ユーザーサービス)
1362
+
1363
+ `~/.config/systemd/user/sshler.service` を作成:
1364
+
1365
+ ```ini
1366
+ [Unit]
1367
+ Description=sshler – local tmux bridge
1368
+ After=network.target
1369
+
1370
+ [Service]
1371
+ Type=simple
1372
+ ExecStart=%h/.local/bin/sshler serve --bind 127.0.0.1 --no-browser
1373
+ Restart=on-failure
1374
+ KillMode=process
1375
+
1376
+ [Install]
1377
+ WantedBy=default.target
1378
+ ```
1379
+
1380
+ > **重要:** `KillMode=process` がないと、sshler を再起動したときにすべての tmux セッションが終了します。
1381
+
1382
+ リロードして有効化:
1383
+
1384
+ ```bash
1385
+ systemctl --user daemon-reload
1386
+ systemctl --user enable --now sshler.service
1387
+ ```
1388
+
1389
+ ## Claude Code スキル
1390
+
1391
+ [Claude Code](https://claude.ai/code) で sshler を開発する場合、このリポジトリにはコードベースの分かりにくい部分を Claude に教えるスキルドキュメントがいくつか同梱されています:
1392
+
1393
+ - `docs/skills/markdown-preview/SKILL.md` — 何がレンダリングされ、何がされないか、そして印刷/PDF パイプラインの癖。
1394
+ - `docs/skills/progress-bars/SKILL.md` — プッシュプロトコル、WebSocket ファンアウト、ホストごとの購読モデル。
1395
+ - `docs/skills/diff-notebook/SKILL.md` — マルチセルの差分ワークスペース:コマンドパーサー、base64 URL 状態、サーバー側の永続化、「新しいバックエンドを作らない」という設計判断。
1396
+
1397
+ Claude はこれらを自動では検出しません。リポジトリ内には存在しますが、Claude Code ランタイムには登録されていないためです。よく使われる方法は、**グローバルスキルディレクトリ(通常は `~/.claude/skills/`)へシンボリックリンクを張る**ことです。こうすると、このマシン上のどの Claude Code セッションからでも名前で読み込めるようになります:
1398
+
1399
+ ```bash
1400
+ # マシンごとに一度だけセットアップ
1401
+ for skill in markdown-preview progress-bars diff-notebook; do
1402
+ mkdir -p ~/.claude/skills/sshler-$skill
1403
+ ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
1404
+ done
1405
+ ```
1406
+
1407
+ これで Claude セッションから `sshler`、`sshler-progress-bars`、`sshler-diff-notebook` という名前のスキルが見えるようになります。Claude Code を一度再起動すると(スキルは起動時に読み込まれます)、組み込みのものと並んで表示されます。これはプラグインやマーケットプレイスと同じ考え方を、手作業かつマシンローカルで実現したものです。ドキュメント自体はリポジトリでバージョン管理され続けるので、真実の情報源は常に `git` にあり、シンボリックリンクは登録のためだけのものです。
1408
+
1409
+ ## 依存関係とライセンス
1410
+
1411
+ - FastAPI、uvicorn、asyncssh、platformdirs、pyyaml、pydantic(PyPI パッケージ、寛容なライセンス)
1412
+ - Vue 3 + Pinia(MIT)がフロントエンド SPA を駆動
1413
+ - xterm.js(MIT)がブラウザターミナルを提供
1414
+ - CodeMirror(MIT)がファイルエディタを駆動
1415
+
1416
+ すべてのアセットはそれぞれの MIT/BSD スタイルのライセンスの下で使用されています。sshler 自体は MIT ライセンスで配布されます。
1417
+
1418
+ ## 名前の由来
1419
+
1420
+ VS Code だけに頼らず、ブラウザタブの中で軽快にターミナルを扱いたい──そんな願いからこの名前になりました。