sshler 1.0.2__tar.gz → 1.0.6__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.
- {sshler-1.0.2/sshler.egg-info → sshler-1.0.6}/PKG-INFO +85 -5
- {sshler-1.0.2 → sshler-1.0.6}/README.md +83 -4
- {sshler-1.0.2 → sshler-1.0.6}/pyproject.toml +14 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/__init__.py +8 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/archive.py +54 -32
- sshler-1.0.6/sshler/api/artifacts.py +516 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/auth.py +25 -13
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/batch.py +43 -7
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/boxes.py +60 -38
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/claude_sessions.py +16 -6
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/config.py +5 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/dependencies.py +74 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/diff.py +6 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/excel.py +11 -4
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/files.py +481 -190
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/git.py +47 -20
- sshler-1.0.6/sshler/api/grep.py +386 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/helpers.py +6 -4
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/models.py +5 -1
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/rate_limiting.py +5 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/recovery.py +17 -5
- sshler-1.0.6/sshler/api/search.py +401 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/sessions.py +127 -38
- sshler-1.0.6/sshler/api/stats_stream.py +176 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/transfer.py +3 -1
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/tunnels.py +12 -0
- sshler-1.0.6/sshler/artifacts.py +492 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/auth.py +1 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/claude_sessions.py +2 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/cli.py +472 -90
- {sshler-1.0.2 → sshler-1.0.6}/sshler/config.py +6 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/config_cache.py +0 -1
- {sshler-1.0.2 → sshler-1.0.6}/sshler/pdf.py +4 -1
- {sshler-1.0.2 → sshler-1.0.6}/sshler/rate_limit.py +17 -5
- {sshler-1.0.2 → sshler-1.0.6}/sshler/session.py +23 -9
- {sshler-1.0.2 → sshler-1.0.6}/sshler/settings.py +14 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/snapshot.py +56 -59
- {sshler-1.0.2 → sshler-1.0.6}/sshler/spa.py +2 -4
- {sshler-1.0.2 → sshler-1.0.6}/sshler/ssh.py +12 -6
- sshler-1.0.6/sshler/ssh_pool.py +609 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/state.py +471 -4
- sshler-1.0.2/sshler/static/dist/assets/Add-DAOjsezW.js → sshler-1.0.6/sshler/static/dist/assets/Add-DGoUmgap.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/Alert-CSag0xgj.js → sshler-1.0.6/sshler/static/dist/assets/Alert-0KqwC3YO.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/ArtifactsView-Br1b7kfM.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/ArtifactsView-DMnfzE9w.css +1 -0
- sshler-1.0.2/sshler/static/dist/assets/BoxesView-DRK-moOl.js → sshler-1.0.6/sshler/static/dist/assets/BoxesView-DjjiCMdg.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/ClaudeSessionsView-BcS0-CLq.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/ClaudeSessionsView-CKmx68D_.js +137 -0
- sshler-1.0.6/sshler/static/dist/assets/CommanderView-BpsWdFho.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/CommanderView-DWT0Nvkn.css → sshler-1.0.6/sshler/static/dist/assets/CommanderView-Bvzg3bq2.css +1 -1
- sshler-1.0.2/sshler/static/dist/assets/DiffView-D1B9VV_z.css → sshler-1.0.6/sshler/static/dist/assets/DiffView-9QDQycKp.css +1 -1
- sshler-1.0.6/sshler/static/dist/assets/DiffView-CG63VPJD.js +10 -0
- sshler-1.0.2/sshler/static/dist/assets/DiffViewer-BKOI30kQ.js → sshler-1.0.6/sshler/static/dist/assets/DiffViewer-CXULVvdb.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/DirectoryPickerModal-B1GuKICB.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/DirectoryPickerModal-BLFEzn26.js +79 -0
- sshler-1.0.6/sshler/static/dist/assets/FileEditorModal-Bh_rOoK8.js +352 -0
- sshler-1.0.6/sshler/static/dist/assets/FileEditorModal-D7TNr5fU.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/FilesView-B9vdgK9X.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/FilesView-CsXrHG8e.js +751 -0
- sshler-1.0.6/sshler/static/dist/assets/GitBadge-BfbyoB28.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/Grid-86dNpU6i.js → sshler-1.0.6/sshler/static/dist/assets/Grid-C2irvtWE.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/InputNumber-9ldqAl1a.js → sshler-1.0.6/sshler/static/dist/assets/InputNumber-DqRo5amt.js +4 -4
- sshler-1.0.2/sshler/static/dist/assets/LoginView-DezvuYtZ.js → sshler-1.0.6/sshler/static/dist/assets/LoginView-f6RcdVpb.js +11 -11
- sshler-1.0.6/sshler/static/dist/assets/MultiTerminalView-Co_RmbGo.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/MultiTerminalView-DBkuEwlF.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/NotFoundView-w7Cl7Ym7.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/OverviewView-C-tLZP2F.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/OverviewView-CgWwBOTj.css +1 -0
- sshler-1.0.2/sshler/static/dist/assets/PhArrowsClockwise.vue-BLHYPaFF.js → sshler-1.0.6/sshler/static/dist/assets/PhArrowsClockwise.vue-BLzKHOQI.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhArrowsLeftRight.vue-BBZDx8HD.js → sshler-1.0.6/sshler/static/dist/assets/PhArrowsLeftRight.vue-BMQwztWr.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhArrowsOut.vue-B8B678OT.js → sshler-1.0.6/sshler/static/dist/assets/PhArrowsOut.vue-CfXBtK9v.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhCheck.vue-C9DFYQgy.js → sshler-1.0.6/sshler/static/dist/assets/PhCheck.vue-D10oKwxK.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhCircle.vue-BfooAyte.js → sshler-1.0.6/sshler/static/dist/assets/PhCircle.vue-CDDAxWJ4.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhClockCounterClockwise.vue-BLcbzc-B.js → sshler-1.0.6/sshler/static/dist/assets/PhClockCounterClockwise.vue-ozOziCVh.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhCopy.vue-D3RzWNcP.js → sshler-1.0.6/sshler/static/dist/assets/PhCopy.vue-CqBWFqLj.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhFloppyDisk.vue-B2uZ8xL_.js → sshler-1.0.6/sshler/static/dist/assets/PhFloppyDisk.vue-DVHuBUZ8.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhFolderOpen.vue-DcCWZAYu.js → sshler-1.0.6/sshler/static/dist/assets/PhFolderOpen.vue-KUDm7_MU.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhGitBranch.vue-BK8iMGps.js → sshler-1.0.6/sshler/static/dist/assets/PhGitBranch.vue-CM7CswoU.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/PhList.vue-BONFOegp.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/PhPencilSimple.vue-C8Ra8brU.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/PhPlay.vue-CE6G-I1m.js → sshler-1.0.6/sshler/static/dist/assets/PhPlay.vue-DI0IIVIo.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhPushPinSimple.vue-CCYplIkb.js → sshler-1.0.6/sshler/static/dist/assets/PhPushPinSimple.vue-dmWHfMI6.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhQuestion.vue-DOMPqSq0.js → sshler-1.0.6/sshler/static/dist/assets/PhQuestion.vue-B9Lhkd5u.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/PhTrash.vue-EX4P1cIo.js → sshler-1.0.6/sshler/static/dist/assets/PhTrash.vue-CT8G9xgK.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/Popconfirm-BmiKMWil.js +16 -0
- sshler-1.0.6/sshler/static/dist/assets/ProgressView-D7O3-bB2.css +1 -0
- sshler-1.0.6/sshler/static/dist/assets/ProgressView-DNrZCAJg.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/RadioGroup-DxCCyzGg.js → sshler-1.0.6/sshler/static/dist/assets/RadioGroup-F433hdNw.js +3 -3
- sshler-1.0.2/sshler/static/dist/assets/SettingsView-B1itMzkZ.js → sshler-1.0.6/sshler/static/dist/assets/SettingsView-DGgZTd12.js +6 -6
- sshler-1.0.2/sshler/static/dist/assets/Spin-DyIxCRqg.js → sshler-1.0.6/sshler/static/dist/assets/Spin-DfJo_6oJ.js +3 -3
- sshler-1.0.2/sshler/static/dist/assets/Switch-CXzkKkdP.js → sshler-1.0.6/sshler/static/dist/assets/Switch-D52CI86b.js +6 -6
- sshler-1.0.6/sshler/static/dist/assets/TerminalView-BNzxpxUp.js +2 -0
- sshler-1.0.6/sshler/static/dist/assets/TerminalView-BqAcovL_.css +1 -0
- sshler-1.0.2/sshler/static/dist/assets/arc-_G7dsxWN.js → sshler-1.0.6/sshler/static/dist/assets/arc-DoC24Idu.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/architectureDiagram-3BPJPVTR-CxqAp6ok.js +36 -0
- sshler-1.0.2/sshler/static/dist/assets/blockDiagram-GPEHLZMM-BF8dyh4B.js → sshler-1.0.6/sshler/static/dist/assets/blockDiagram-GPEHLZMM-BPsYLd3R.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/c4Diagram-AAUBKEIU-C3shTvrC.js → sshler-1.0.6/sshler/static/dist/assets/c4Diagram-AAUBKEIU-CaadjczC.js +5 -5
- sshler-1.0.6/sshler/static/dist/assets/channel-Ddemxpr4.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/chunk-2J33WTMH-BlmNHocv.js → sshler-1.0.6/sshler/static/dist/assets/chunk-2J33WTMH-AqzQ1jMU.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-4BX2VUAB-FVsp0Dvs.js → sshler-1.0.6/sshler/static/dist/assets/chunk-4BX2VUAB-rgggKmJc.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-55IACEB6-BeaVCTsK.js → sshler-1.0.6/sshler/static/dist/assets/chunk-55IACEB6-C4URB_Xu.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-727SXJPM-DOvH0YaL.js → sshler-1.0.6/sshler/static/dist/assets/chunk-727SXJPM-NfBIzwJo.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-AQP2D5EJ-DwFGzbIM.js → sshler-1.0.6/sshler/static/dist/assets/chunk-AQP2D5EJ-BdhL-cEx.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-FMBD7UC4-DrOyMPnh.js → sshler-1.0.6/sshler/static/dist/assets/chunk-FMBD7UC4-CQhutD92.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-ND2GUHAM-bdPxSOzK.js → sshler-1.0.6/sshler/static/dist/assets/chunk-ND2GUHAM-BNWn_lJ6.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/chunk-QZHKN3VN-DF1WR5Ws.js → sshler-1.0.6/sshler/static/dist/assets/chunk-QZHKN3VN-CkVgZO1f.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/classDiagram-4FO5ZUOK-BvViZFGy.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/classDiagram-v2-Q7XG4LA2-BvViZFGy.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/composables-CD_j1SQC.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/cose-bilkent-S5V4N54A-cjfguF9m.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/dagre-BM42HDAG-B1uYyW53.js +4 -0
- sshler-1.0.6/sshler/static/dist/assets/diagram-2AECGRRQ-CPLREfvT.js +43 -0
- sshler-1.0.6/sshler/static/dist/assets/diagram-5GNKFQAL-D5Rnk309.js +10 -0
- sshler-1.0.2/sshler/static/dist/assets/diagram-KO2AKTUF-BaqYu-qh.js → sshler-1.0.6/sshler/static/dist/assets/diagram-KO2AKTUF-Ca9qhE-b.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/diagram-LMA3HP47-DioIhere.js +24 -0
- sshler-1.0.6/sshler/static/dist/assets/diagram-OG6HWLK6-hQ4p88dE.js +24 -0
- sshler-1.0.2/sshler/static/dist/assets/erDiagram-TEJ5UH35-3hJqUA_3.js → sshler-1.0.6/sshler/static/dist/assets/erDiagram-TEJ5UH35-C7mAHSNg.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/favorites-DhWkZ_Ge.js → sshler-1.0.6/sshler/static/dist/assets/favorites-PFBt8R3Y.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/flowDiagram-I6XJVG4X-DjLO6b70.js → sshler-1.0.6/sshler/static/dist/assets/flowDiagram-I6XJVG4X-278Uq6BX.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/ganttDiagram-6RSMTGT7-Ia5pxiF_.js +292 -0
- sshler-1.0.6/sshler/static/dist/assets/gitGraphDiagram-PVQCEYII-BSDQcuPz.js +106 -0
- sshler-1.0.2/sshler/static/dist/assets/graph-1mebsZ_B.js → sshler-1.0.6/sshler/static/dist/assets/graph-CoRzpK5X.js +1 -1
- sshler-1.0.6/sshler/static/dist/assets/index-C9aYRSRY.js +2136 -0
- sshler-1.0.2/sshler/static/dist/assets/index-Dewesw9U.css → sshler-1.0.6/sshler/static/dist/assets/index-DgzHEFf2.css +1 -1
- sshler-1.0.6/sshler/static/dist/assets/infoDiagram-5YYISTIA-CuQ4nHjX.js +2 -0
- sshler-1.0.2/sshler/static/dist/assets/ishikawaDiagram-YF4QCWOH-CgGECNI3.js → sshler-1.0.6/sshler/static/dist/assets/ishikawaDiagram-YF4QCWOH-Y4_8uIsJ.js +4 -4
- sshler-1.0.2/sshler/static/dist/assets/journeyDiagram-JHISSGLW-CnV6MMWs.js → sshler-1.0.6/sshler/static/dist/assets/journeyDiagram-JHISSGLW-d6TRjbDM.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/kanban-definition-UN3LZRKU-CNYDNKQ8.js → sshler-1.0.6/sshler/static/dist/assets/kanban-definition-UN3LZRKU-CsU9Ka-o.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/layout-CDPPC1l5.js → sshler-1.0.6/sshler/static/dist/assets/layout-C-0fEkyA.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/linear-wLmeA8y8.js → sshler-1.0.6/sshler/static/dist/assets/linear-DnGsrReS.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/mermaid.core-a_pRy7_d.js → sshler-1.0.6/sshler/static/dist/assets/mermaid.core-CP-W_xk4.js +4 -4
- sshler-1.0.2/sshler/static/dist/assets/mindmap-definition-RKZ34NQL-CdCNJ1c3.js → sshler-1.0.6/sshler/static/dist/assets/mindmap-definition-RKZ34NQL-C-kVN-13.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/pieDiagram-4H26LBE5-DWu3sP_u.js +30 -0
- sshler-1.0.2/sshler/static/dist/assets/quadrantDiagram-W4KKPZXB-DP62iaWi.js → sshler-1.0.6/sshler/static/dist/assets/quadrantDiagram-W4KKPZXB-CA5p5ZZG.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/requirementDiagram-4Y6WPE33-D80_Wixp.js → sshler-1.0.6/sshler/static/dist/assets/requirementDiagram-4Y6WPE33-CRknzv2z.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/sankeyDiagram-5OEKKPKP-BrAEn3K4.js +40 -0
- sshler-1.0.2/sshler/static/dist/assets/sequenceDiagram-3UESZ5HK-B82dIY8s.js → sshler-1.0.6/sshler/static/dist/assets/sequenceDiagram-3UESZ5HK-CxCetFEa.js +2 -2
- sshler-1.0.6/sshler/static/dist/assets/sessionName-CGRTFJ4P.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/stateDiagram-AJRCARHV-DEJmR75A.js +1 -0
- sshler-1.0.6/sshler/static/dist/assets/stateDiagram-v2-BHNVJYJU-C_YqTgOQ.js +1 -0
- sshler-1.0.2/sshler/static/dist/assets/timeline-definition-PNZ67QCA-BLrTakYZ.js → sshler-1.0.6/sshler/static/dist/assets/timeline-definition-PNZ67QCA-DvBRNETl.js +10 -10
- sshler-1.0.2/sshler/static/dist/assets/vennDiagram-CIIHVFJN-B5Ccpd4s.js → sshler-1.0.6/sshler/static/dist/assets/vennDiagram-CIIHVFJN-CVEff8DW.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/wardley-L42UT6IY-CXaSjfFd.js → sshler-1.0.6/sshler/static/dist/assets/wardley-L42UT6IY-BouMTXtd.js +1 -1
- sshler-1.0.2/sshler/static/dist/assets/wardleyDiagram-YWT4CUSO-Dyh7zrS3.js → sshler-1.0.6/sshler/static/dist/assets/wardleyDiagram-YWT4CUSO-DllsKsFW.js +2 -2
- sshler-1.0.2/sshler/static/dist/assets/xychartDiagram-2RQKCTM6-C7ZRiqX6.js → sshler-1.0.6/sshler/static/dist/assets/xychartDiagram-2RQKCTM6-1aWX1pcd.js +2 -2
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/index.html +2 -2
- sshler-1.0.6/sshler/tmux.py +388 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/validation.py +4 -4
- {sshler-1.0.2 → sshler-1.0.6}/sshler/webapp.py +351 -151
- {sshler-1.0.2 → sshler-1.0.6}/sshler/win_terminal_registry.py +2 -1
- {sshler-1.0.2 → sshler-1.0.6}/sshler/winpty_proc.py +2 -2
- {sshler-1.0.2 → sshler-1.0.6/sshler.egg-info}/PKG-INFO +85 -5
- sshler-1.0.6/sshler.egg-info/SOURCES.txt +238 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler.egg-info/requires.txt +1 -0
- sshler-1.0.6/tests/test_api_artifacts.py +276 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_api_diff.py +67 -46
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_api_progress.py +43 -44
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_api_v1.py +80 -41
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_archive.py +25 -26
- sshler-1.0.6/tests/test_artifacts.py +325 -0
- sshler-1.0.6/tests/test_basic.py +38 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_batch_ops.py +21 -22
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_claude_sessions.py +52 -9
- sshler-1.0.6/tests/test_cli_artifacts.py +170 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_cli_progress.py +0 -3
- sshler-1.0.6/tests/test_client_address.py +345 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_command_injection.py +78 -66
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_database_indexes.py +21 -11
- sshler-1.0.6/tests/test_dev_workflow.py +261 -0
- sshler-1.0.6/tests/test_fake_tmux.py +47 -0
- sshler-1.0.6/tests/test_files_folder_zip_remote.py +1328 -0
- sshler-1.0.6/tests/test_files_upload_local.py +131 -0
- sshler-1.0.6/tests/test_force_kill.py +453 -0
- sshler-1.0.6/tests/test_grep.py +376 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_handshake_status.py +12 -8
- sshler-1.0.6/tests/test_httpx_ws.py +72 -0
- sshler-1.0.6/tests/test_origin_csrf.py +105 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_path_validation.py +12 -11
- sshler-1.0.6/tests/test_platform_support.py +122 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_progress_websocket.py +14 -16
- sshler-1.0.6/tests/test_rate_limit.py +231 -0
- sshler-1.0.6/tests/test_rate_limit_middleware.py +255 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_recovery.py +26 -8
- sshler-1.0.6/tests/test_remote_git_info_quoting.py +82 -0
- sshler-1.0.6/tests/test_remote_tilde_routes.py +284 -0
- sshler-1.0.6/tests/test_search.py +646 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_serve_token_cache.py +0 -1
- sshler-1.0.6/tests/test_session_auth.py +416 -0
- sshler-1.0.6/tests/test_session_name_parity.py +50 -0
- sshler-1.0.6/tests/test_snapshot_loop_settings.py +139 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_ssh.py +41 -1
- sshler-1.0.6/tests/test_ssh_connection_leaks.py +1242 -0
- sshler-1.0.6/tests/test_ssh_pool_age.py +218 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_stats_stream.py +8 -9
- sshler-1.0.6/tests/test_terminal_teardown.py +509 -0
- sshler-1.0.6/tests/test_tmux_subprocess.py +439 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_tunnels.py +3 -2
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_websocket.py +31 -38
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_win_terminal_registry.py +111 -57
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_windows_terminal.py +87 -45
- sshler-1.0.6/tests/test_ws_term_auth.py +244 -0
- sshler-1.0.2/sshler/api/grep.py +0 -160
- sshler-1.0.2/sshler/api/search.py +0 -192
- sshler-1.0.2/sshler/api/stats_stream.py +0 -98
- sshler-1.0.2/sshler/ssh_pool.py +0 -409
- sshler-1.0.2/sshler/static/dist/assets/ClaudeSessionsView-CGbHeAaj.js +0 -137
- sshler-1.0.2/sshler/static/dist/assets/ClaudeSessionsView-DN9qPL2F.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/CommanderView-CeqW512i.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/DiffView-DKQldLz_.js +0 -10
- sshler-1.0.2/sshler/static/dist/assets/DirectoryPickerModal-BJGCZjfT.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/DirectoryPickerModal-g2DqKz-v.js +0 -79
- sshler-1.0.2/sshler/static/dist/assets/FileEditorModal-CgM0I0XI.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/FileEditorModal-mQQlSzLp.js +0 -352
- sshler-1.0.2/sshler/static/dist/assets/FilesView-Be4yclnr.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/FilesView-BhxBCK3C.js +0 -751
- sshler-1.0.2/sshler/static/dist/assets/GitBadge-B_4M8JOq.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/MultiTerminalView-DDARrSaQ.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/MultiTerminalView-hud2MPaZ.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/NotFoundView-NdvchLjg.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/OverviewView-CL0qAbxt.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/OverviewView-D8TLlPQz.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/PhList.vue-CuXqS-Yu.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/Popconfirm-CgFPK9F0.js +0 -16
- sshler-1.0.2/sshler/static/dist/assets/ProgressView-DdmDJk_g.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/ProgressView-tCTPu2yW.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/TerminalView-COCfd2uh.css +0 -1
- sshler-1.0.2/sshler/static/dist/assets/TerminalView-DWSrSgDS.js +0 -2
- sshler-1.0.2/sshler/static/dist/assets/architectureDiagram-3BPJPVTR-BuMa823z.js +0 -36
- sshler-1.0.2/sshler/static/dist/assets/channel-OQURwrS_.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/classDiagram-4FO5ZUOK-BcXsVGrF.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/classDiagram-v2-Q7XG4LA2-BcXsVGrF.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/cose-bilkent-S5V4N54A-C-3AhSiq.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/dagre-BM42HDAG-BGYV-hKU.js +0 -4
- sshler-1.0.2/sshler/static/dist/assets/diagram-2AECGRRQ-HqKo4hRm.js +0 -43
- sshler-1.0.2/sshler/static/dist/assets/diagram-5GNKFQAL-DxQKZDCI.js +0 -10
- sshler-1.0.2/sshler/static/dist/assets/diagram-LMA3HP47-BSXDrPx5.js +0 -24
- sshler-1.0.2/sshler/static/dist/assets/diagram-OG6HWLK6-fbUkjsCH.js +0 -24
- sshler-1.0.2/sshler/static/dist/assets/ganttDiagram-6RSMTGT7-DDZ9gMyj.js +0 -292
- sshler-1.0.2/sshler/static/dist/assets/gitGraphDiagram-PVQCEYII-Z1pBUCOv.js +0 -106
- sshler-1.0.2/sshler/static/dist/assets/index-CeAHJWSQ.js +0 -2136
- sshler-1.0.2/sshler/static/dist/assets/infoDiagram-5YYISTIA-SEqseSvm.js +0 -2
- sshler-1.0.2/sshler/static/dist/assets/pieDiagram-4H26LBE5-CGk3s4aU.js +0 -30
- sshler-1.0.2/sshler/static/dist/assets/sankeyDiagram-5OEKKPKP-G9LZKWmT.js +0 -40
- sshler-1.0.2/sshler/static/dist/assets/sessionName-DbO6vfWs.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/stateDiagram-AJRCARHV-Bdc9qNnY.js +0 -1
- sshler-1.0.2/sshler/static/dist/assets/stateDiagram-v2-BHNVJYJU-BdScRmDh.js +0 -1
- sshler-1.0.2/sshler/tmux.py +0 -268
- sshler-1.0.2/sshler.egg-info/SOURCES.txt +0 -212
- sshler-1.0.2/tests/test_basic.py +0 -36
- sshler-1.0.2/tests/test_dev_workflow.py +0 -257
- sshler-1.0.2/tests/test_grep.py +0 -231
- sshler-1.0.2/tests/test_httpx_ws.py +0 -46
- sshler-1.0.2/tests/test_rate_limit.py +0 -226
- sshler-1.0.2/tests/test_search.py +0 -347
- sshler-1.0.2/tests/test_session_auth.py +0 -180
- {sshler-1.0.2 → sshler-1.0.6}/LICENSE +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/MANIFEST.in +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/scripts/check_frontend_dist.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/setup.cfg +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/__init__.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/pdf.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/ping.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/progress.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/snippets.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/api/terminal.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/scripts/install-sshler-task.ps1 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/scripts/remove-sshler-task.ps1 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/scripts/run-sshler.ps1 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/ssh_config.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/BoxesView-Cw5NiFP6.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/DiffViewer-Ds8qyy47.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/GitBadge-sLcMA5A6.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/LoginView-BLrnakeI.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/NotFoundView-DYaoqkDb.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/SettingsView-yHE_fgSm.css +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/cytoscape.esm-ChuFH5G7.js +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/defaultLocale-DX6XiGOO.js +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/init-Gi6I4Gst.js +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/katex-BbnesVCl.js +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/assets/ordinal-Cboi1Yqb.js +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/favicon.png +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/fonts/MonaspaceNeon-Bold.woff2 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/fonts/MonaspaceNeon-Regular.woff2 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/fonts/MonaspaceNeon-Variable.woff2 +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/icon-192.png +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/icon-512-maskable.png +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/icon-512.png +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/logo.png +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler/static/dist/manifest.webmanifest +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler.egg-info/dependency_links.txt +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler.egg-info/entry_points.txt +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/sshler.egg-info/top_level.txt +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_config.py +0 -0
- {sshler-1.0.2 → sshler-1.0.6}/tests/test_snippets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshler
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: A local FastAPI-powered SSH multiplexer for tmux-in-browser — from your laptop only.
|
|
5
5
|
Author-email: Gabu <gabu@gabu.quest>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,6 +45,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
|
45
45
|
Requires-Dist: playwright>=1.48.0; extra == "dev"
|
|
46
46
|
Requires-Dist: pytest-playwright>=0.5.0; extra == "dev"
|
|
47
47
|
Requires-Dist: ruff>=0.5.6; extra == "dev"
|
|
48
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
48
49
|
Provides-Extra: pdf
|
|
49
50
|
Requires-Dist: playwright>=1.48.0; extra == "pdf"
|
|
50
51
|
Dynamic: license-file
|
|
@@ -189,6 +190,12 @@ The app opens at `http://127.0.0.1:8822` and redirects to the Vue SPA at `/app/`
|
|
|
189
190
|
- **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
191
|
- **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
|
|
|
193
|
+
**🌐 Local HTML Artifacts**
|
|
194
|
+
- **One catalog for generated HTML** - Register a single page, a static site, or a recursively discovered collection under a required project and optional nested group.
|
|
195
|
+
- **Agent- and script-friendly CLI** - `sshler artifact add ./report.html --project demo --group reports` registers a completed output; list, show, update, rescan, open, and remove commands cover its lifecycle.
|
|
196
|
+
- **Safe local viewing** - HTML is served from a separate loopback-only origin with strict path containment and no CORS permission. Inline JavaScript works without granting access to the main API origin.
|
|
197
|
+
- **Non-destructive cleanup** - Trash actions and CLI removal unregister catalog metadata only. Source files and directories remain untouched.
|
|
198
|
+
|
|
192
199
|
**♿ Accessibility**
|
|
193
200
|
- **WCAG 2.1 AA Compliant** - Semantic HTML, ARIA labels, keyboard navigation
|
|
194
201
|
- **Screen Reader Support** - Proper focus management and announcements
|
|
@@ -306,6 +313,34 @@ Subscriptions are **per box**: open a box (Files/Terminal), then hit the `+` on
|
|
|
306
313
|
|
|
307
314
|
See `examples/progress-bar-build-watcher.sh` for a copy-pasteable demo loop including `trap` → `--status failed` on error.
|
|
308
315
|
|
|
316
|
+
### Registering generated HTML
|
|
317
|
+
|
|
318
|
+
Start sshler, create HTML anywhere inside your workspace, and register it by path:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# One self-contained page
|
|
322
|
+
sshler artifact add ./report.html --project demo --group reports
|
|
323
|
+
|
|
324
|
+
# A static site with assets
|
|
325
|
+
sshler artifact add ./site --project demo --group prototypes --mode site --entry index.html
|
|
326
|
+
|
|
327
|
+
# Recursively discover HTML pages
|
|
328
|
+
sshler artifact add ./reports --project demo --group runs --discover
|
|
329
|
+
|
|
330
|
+
sshler artifact find "quarterly summary" --project demo
|
|
331
|
+
sshler artifact list --project demo
|
|
332
|
+
sshler artifact open <id>
|
|
333
|
+
sshler artifact remove <id>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Open `/app/artifacts` to search and filter registrations, expand page lists by clicking an
|
|
337
|
+
artifact row, preview pages, and unregister with the trash action. Stable cross-links use
|
|
338
|
+
`/r/<project-slug>/<artifact-slug>/`; `artifact add --mount <path>` can preserve existing
|
|
339
|
+
root-relative links. Opening and previewing require a loopback address. `remove` always leaves
|
|
340
|
+
source files unchanged.
|
|
341
|
+
|
|
342
|
+
See [`ROADMAP-ARTIFACTS.md`](ROADMAP-ARTIFACTS.md) for the architecture and security model.
|
|
343
|
+
|
|
309
344
|
### Key Shortcuts
|
|
310
345
|
|
|
311
346
|
- **Cmd/Ctrl+K** - Command palette
|
|
@@ -452,7 +487,10 @@ SSHLER_COOKIE_SECURE=false # Only for localhost dev!
|
|
|
452
487
|
# /etc/caddy/Caddyfile or ~/Caddyfile
|
|
453
488
|
|
|
454
489
|
sshler.company.internal {
|
|
455
|
-
reverse_proxy localhost:8822
|
|
490
|
+
reverse_proxy localhost:8822 {
|
|
491
|
+
# Overwrite any X-Real-IP the client sent (see SSHLER_TRUST_PROXY_HEADERS)
|
|
492
|
+
header_up X-Real-IP {remote_host}
|
|
493
|
+
}
|
|
456
494
|
}
|
|
457
495
|
```
|
|
458
496
|
|
|
@@ -463,6 +501,10 @@ SSHLER_COOKIE_SECURE=false # Only for localhost dev!
|
|
|
463
501
|
SSHLER_PORT=8822
|
|
464
502
|
SSHLER_PUBLIC_URL=https://sshler.company.internal
|
|
465
503
|
SSHLER_COOKIE_SECURE=true # Required for HTTPS
|
|
504
|
+
# Caddy overwrites X-Real-IP (header_up above), so sshler may trust it for
|
|
505
|
+
# rate limits and login lockouts. Leave unset unless a proxy on this host
|
|
506
|
+
# overwrites X-Real-IP; otherwise sshler keys on the connection's address.
|
|
507
|
+
SSHLER_TRUST_PROXY_HEADERS=true
|
|
466
508
|
```
|
|
467
509
|
|
|
468
510
|
4. Start Caddy:
|
|
@@ -573,6 +615,11 @@ server {
|
|
|
573
615
|
}
|
|
574
616
|
```
|
|
575
617
|
|
|
618
|
+
`proxy_set_header X-Real-IP $remote_addr` overwrites any `X-Real-IP` the client sent,
|
|
619
|
+
so set `SSHLER_TRUST_PROXY_HEADERS=true` with this config. Without that setting sshler
|
|
620
|
+
ignores `X-Real-IP` and keys rate limits and login lockouts on the connection's address,
|
|
621
|
+
which behind a proxy is the proxy's.
|
|
622
|
+
|
|
576
623
|
**Traefik:**
|
|
577
624
|
|
|
578
625
|
```yaml
|
|
@@ -735,12 +782,13 @@ If you use [Claude Code](https://claude.ai/code) to develop sshler, this repo sh
|
|
|
735
782
|
- `docs/skills/markdown-preview/SKILL.md` — what renders, what doesn't, and quirks of the print-to-PDF pipeline.
|
|
736
783
|
- `docs/skills/progress-bars/SKILL.md` — push protocol, WebSocket fan-out, per-box subscription model.
|
|
737
784
|
- `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.
|
|
785
|
+
- `docs/skills/artifacts/SKILL.md` — create HTML in the current workspace, choose a serving mode, and register it in the catalog.
|
|
738
786
|
|
|
739
787
|
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
788
|
|
|
741
789
|
```bash
|
|
742
790
|
# One-time setup per machine
|
|
743
|
-
for skill in markdown-preview progress-bars diff-notebook; do
|
|
791
|
+
for skill in markdown-preview progress-bars diff-notebook artifacts; do
|
|
744
792
|
mkdir -p ~/.claude/skills/sshler-$skill
|
|
745
793
|
ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
|
|
746
794
|
done
|
|
@@ -873,6 +921,12 @@ sshler serve
|
|
|
873
921
|
- **正直なパーセンテージ** - 表示パーセントは**切り捨て**です。3300 ステップのビルドで 3299/3300 なら 99% と表示され、実際に完了するまで誤解を招く 100% にはなりません。
|
|
874
922
|
- **ライブ更新** - WebSocket (`/ws/progress`) が接続中の全タブに upsert/delete イベントをファンアウト。複数マシン?スマホでも `/app/progress` を開けます。
|
|
875
923
|
|
|
924
|
+
**ローカル HTML 成果物**
|
|
925
|
+
- **生成 HTML を一か所で管理** - 単一ページ、静的サイト、再帰的に検出する HTML コレクションを、必須のプロジェクトと任意のネストしたグループに登録できます。
|
|
926
|
+
- **エージェントやスクリプト向け CLI** - `sshler artifact add ./report.html --project demo --group reports` で完成した出力を登録し、list、show、update、rescan、open、remove で管理できます。
|
|
927
|
+
- **安全なローカル表示** - HTML は厳密なパス制約と CORS 許可なしの別ループバックオリジンから配信されます。
|
|
928
|
+
- **非破壊の整理** - ゴミ箱操作と CLI の remove はカタログ情報だけを解除し、元のファイルやディレクトリは変更しません。
|
|
929
|
+
|
|
876
930
|
**🤖 Claude セッションダッシュボード**
|
|
877
931
|
- **`/app/claude` — Claude Code セッションの再開** - このマシン上で再開可能な [Claude Code](https://claude.ai/code) の会話(`~/.claude/projects/*/*.jsonl` から読み取り)を一覧表示し、ワンクリックでブラウザ内ターミナルに再開します。タイトルは Claude Code の `/resume` ピッカーと同じ規則です(`/rename` で付けた名前 → AI 生成タイトル → 最初のプロンプトの順)。
|
|
878
932
|
- **Git リポジトリ単位でグループ化・折りたたみ可能** - セッションはリポジトリのルート単位でグループ化されます(`repo/subdir` で実行した会話も `/resume` と同様に `repo` の下に表示)。各グループは折りたたみ可能で、アプリの他の場所と同じディレクトリ絵文字が付きます。初期状態は折りたたみで、フィルタ入力で該当グループが自動展開され、「すべて展開/すべて折りたたむ」ボタンもあります。
|
|
@@ -997,6 +1051,19 @@ sshler progress delete build
|
|
|
997
1051
|
|
|
998
1052
|
エラー時に `trap` → `--status failed` するデモループは `examples/progress-bar-build-watcher.sh` を参照。
|
|
999
1053
|
|
|
1054
|
+
### 生成 HTML を登録する
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
sshler artifact add ./report.html --project demo --group reports
|
|
1058
|
+
sshler artifact add ./site --project demo --group prototypes --mode site --entry index.html
|
|
1059
|
+
sshler artifact add ./reports --project demo --group runs --discover
|
|
1060
|
+
sshler artifact list --project demo
|
|
1061
|
+
sshler artifact open <id>
|
|
1062
|
+
sshler artifact remove <id>
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
`/app/artifacts` でプロジェクト、登録、ページ検出、プレビュー、ゴミ箱操作を管理できます。開く・プレビューする機能はループバックアドレス経由で利用でき、`remove` は元のファイルを残します。設計とセキュリティモデルは [`ROADMAP-ARTIFACTS.md`](ROADMAP-ARTIFACTS.md) を参照してください。
|
|
1066
|
+
|
|
1000
1067
|
### キーショートカット
|
|
1001
1068
|
|
|
1002
1069
|
- **Cmd/Ctrl+K** - コマンドパレット
|
|
@@ -1141,7 +1208,10 @@ SSHLER_COOKIE_SECURE=false # localhost 開発専用!
|
|
|
1141
1208
|
# /etc/caddy/Caddyfile または ~/Caddyfile
|
|
1142
1209
|
|
|
1143
1210
|
sshler.company.internal {
|
|
1144
|
-
reverse_proxy localhost:8822
|
|
1211
|
+
reverse_proxy localhost:8822 {
|
|
1212
|
+
# クライアントが送った X-Real-IP を上書き(SSHLER_TRUST_PROXY_HEADERS 参照)
|
|
1213
|
+
header_up X-Real-IP {remote_host}
|
|
1214
|
+
}
|
|
1145
1215
|
}
|
|
1146
1216
|
```
|
|
1147
1217
|
|
|
@@ -1152,6 +1222,10 @@ SSHLER_COOKIE_SECURE=false # localhost 開発専用!
|
|
|
1152
1222
|
SSHLER_PORT=8822
|
|
1153
1223
|
SSHLER_PUBLIC_URL=https://sshler.company.internal
|
|
1154
1224
|
SSHLER_COOKIE_SECURE=true # HTTPS 必須
|
|
1225
|
+
# Caddy が X-Real-IP を上書きする(上記 header_up)ので、レート制限と
|
|
1226
|
+
# ログインロックアウトにその値を使ってよい。同じホストのプロキシが X-Real-IP を
|
|
1227
|
+
# 上書きしない構成では設定しないこと(接続元アドレスが使われる)。
|
|
1228
|
+
SSHLER_TRUST_PROXY_HEADERS=true
|
|
1155
1229
|
```
|
|
1156
1230
|
|
|
1157
1231
|
4. Caddy を起動:
|
|
@@ -1262,6 +1336,11 @@ server {
|
|
|
1262
1336
|
}
|
|
1263
1337
|
```
|
|
1264
1338
|
|
|
1339
|
+
`proxy_set_header X-Real-IP $remote_addr` はクライアントが送った `X-Real-IP` を上書きするので、
|
|
1340
|
+
この設定では `SSHLER_TRUST_PROXY_HEADERS=true` を設定してください。この設定がないと sshler は
|
|
1341
|
+
`X-Real-IP` を無視し、レート制限とログインロックアウトを接続元アドレス(プロキシの背後では
|
|
1342
|
+
プロキシのアドレス)で数えます。
|
|
1343
|
+
|
|
1265
1344
|
**Traefik:**
|
|
1266
1345
|
|
|
1267
1346
|
```yaml
|
|
@@ -1393,12 +1472,13 @@ systemctl --user enable --now sshler.service
|
|
|
1393
1472
|
- `docs/skills/markdown-preview/SKILL.md` — 何がレンダリングされ、何がされないか、そして印刷/PDF パイプラインの癖。
|
|
1394
1473
|
- `docs/skills/progress-bars/SKILL.md` — プッシュプロトコル、WebSocket ファンアウト、ホストごとの購読モデル。
|
|
1395
1474
|
- `docs/skills/diff-notebook/SKILL.md` — マルチセルの差分ワークスペース:コマンドパーサー、base64 URL 状態、サーバー側の永続化、「新しいバックエンドを作らない」という設計判断。
|
|
1475
|
+
- `docs/skills/artifacts/SKILL.md` — 現在のワークスペースで HTML を作成し、配信モードを選んでカタログへ登録する手順。
|
|
1396
1476
|
|
|
1397
1477
|
Claude はこれらを自動では検出しません。リポジトリ内には存在しますが、Claude Code ランタイムには登録されていないためです。よく使われる方法は、**グローバルスキルディレクトリ(通常は `~/.claude/skills/`)へシンボリックリンクを張る**ことです。こうすると、このマシン上のどの Claude Code セッションからでも名前で読み込めるようになります:
|
|
1398
1478
|
|
|
1399
1479
|
```bash
|
|
1400
1480
|
# マシンごとに一度だけセットアップ
|
|
1401
|
-
for skill in markdown-preview progress-bars diff-notebook; do
|
|
1481
|
+
for skill in markdown-preview progress-bars diff-notebook artifacts; do
|
|
1402
1482
|
mkdir -p ~/.claude/skills/sshler-$skill
|
|
1403
1483
|
ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
|
|
1404
1484
|
done
|
|
@@ -138,6 +138,12 @@ The app opens at `http://127.0.0.1:8822` and redirects to the Vue SPA at `/app/`
|
|
|
138
138
|
- **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.
|
|
139
139
|
- **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.
|
|
140
140
|
|
|
141
|
+
**🌐 Local HTML Artifacts**
|
|
142
|
+
- **One catalog for generated HTML** - Register a single page, a static site, or a recursively discovered collection under a required project and optional nested group.
|
|
143
|
+
- **Agent- and script-friendly CLI** - `sshler artifact add ./report.html --project demo --group reports` registers a completed output; list, show, update, rescan, open, and remove commands cover its lifecycle.
|
|
144
|
+
- **Safe local viewing** - HTML is served from a separate loopback-only origin with strict path containment and no CORS permission. Inline JavaScript works without granting access to the main API origin.
|
|
145
|
+
- **Non-destructive cleanup** - Trash actions and CLI removal unregister catalog metadata only. Source files and directories remain untouched.
|
|
146
|
+
|
|
141
147
|
**♿ Accessibility**
|
|
142
148
|
- **WCAG 2.1 AA Compliant** - Semantic HTML, ARIA labels, keyboard navigation
|
|
143
149
|
- **Screen Reader Support** - Proper focus management and announcements
|
|
@@ -255,6 +261,34 @@ Subscriptions are **per box**: open a box (Files/Terminal), then hit the `+` on
|
|
|
255
261
|
|
|
256
262
|
See `examples/progress-bar-build-watcher.sh` for a copy-pasteable demo loop including `trap` → `--status failed` on error.
|
|
257
263
|
|
|
264
|
+
### Registering generated HTML
|
|
265
|
+
|
|
266
|
+
Start sshler, create HTML anywhere inside your workspace, and register it by path:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# One self-contained page
|
|
270
|
+
sshler artifact add ./report.html --project demo --group reports
|
|
271
|
+
|
|
272
|
+
# A static site with assets
|
|
273
|
+
sshler artifact add ./site --project demo --group prototypes --mode site --entry index.html
|
|
274
|
+
|
|
275
|
+
# Recursively discover HTML pages
|
|
276
|
+
sshler artifact add ./reports --project demo --group runs --discover
|
|
277
|
+
|
|
278
|
+
sshler artifact find "quarterly summary" --project demo
|
|
279
|
+
sshler artifact list --project demo
|
|
280
|
+
sshler artifact open <id>
|
|
281
|
+
sshler artifact remove <id>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Open `/app/artifacts` to search and filter registrations, expand page lists by clicking an
|
|
285
|
+
artifact row, preview pages, and unregister with the trash action. Stable cross-links use
|
|
286
|
+
`/r/<project-slug>/<artifact-slug>/`; `artifact add --mount <path>` can preserve existing
|
|
287
|
+
root-relative links. Opening and previewing require a loopback address. `remove` always leaves
|
|
288
|
+
source files unchanged.
|
|
289
|
+
|
|
290
|
+
See [`ROADMAP-ARTIFACTS.md`](ROADMAP-ARTIFACTS.md) for the architecture and security model.
|
|
291
|
+
|
|
258
292
|
### Key Shortcuts
|
|
259
293
|
|
|
260
294
|
- **Cmd/Ctrl+K** - Command palette
|
|
@@ -401,7 +435,10 @@ SSHLER_COOKIE_SECURE=false # Only for localhost dev!
|
|
|
401
435
|
# /etc/caddy/Caddyfile or ~/Caddyfile
|
|
402
436
|
|
|
403
437
|
sshler.company.internal {
|
|
404
|
-
reverse_proxy localhost:8822
|
|
438
|
+
reverse_proxy localhost:8822 {
|
|
439
|
+
# Overwrite any X-Real-IP the client sent (see SSHLER_TRUST_PROXY_HEADERS)
|
|
440
|
+
header_up X-Real-IP {remote_host}
|
|
441
|
+
}
|
|
405
442
|
}
|
|
406
443
|
```
|
|
407
444
|
|
|
@@ -412,6 +449,10 @@ SSHLER_COOKIE_SECURE=false # Only for localhost dev!
|
|
|
412
449
|
SSHLER_PORT=8822
|
|
413
450
|
SSHLER_PUBLIC_URL=https://sshler.company.internal
|
|
414
451
|
SSHLER_COOKIE_SECURE=true # Required for HTTPS
|
|
452
|
+
# Caddy overwrites X-Real-IP (header_up above), so sshler may trust it for
|
|
453
|
+
# rate limits and login lockouts. Leave unset unless a proxy on this host
|
|
454
|
+
# overwrites X-Real-IP; otherwise sshler keys on the connection's address.
|
|
455
|
+
SSHLER_TRUST_PROXY_HEADERS=true
|
|
415
456
|
```
|
|
416
457
|
|
|
417
458
|
4. Start Caddy:
|
|
@@ -522,6 +563,11 @@ server {
|
|
|
522
563
|
}
|
|
523
564
|
```
|
|
524
565
|
|
|
566
|
+
`proxy_set_header X-Real-IP $remote_addr` overwrites any `X-Real-IP` the client sent,
|
|
567
|
+
so set `SSHLER_TRUST_PROXY_HEADERS=true` with this config. Without that setting sshler
|
|
568
|
+
ignores `X-Real-IP` and keys rate limits and login lockouts on the connection's address,
|
|
569
|
+
which behind a proxy is the proxy's.
|
|
570
|
+
|
|
525
571
|
**Traefik:**
|
|
526
572
|
|
|
527
573
|
```yaml
|
|
@@ -684,12 +730,13 @@ If you use [Claude Code](https://claude.ai/code) to develop sshler, this repo sh
|
|
|
684
730
|
- `docs/skills/markdown-preview/SKILL.md` — what renders, what doesn't, and quirks of the print-to-PDF pipeline.
|
|
685
731
|
- `docs/skills/progress-bars/SKILL.md` — push protocol, WebSocket fan-out, per-box subscription model.
|
|
686
732
|
- `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.
|
|
733
|
+
- `docs/skills/artifacts/SKILL.md` — create HTML in the current workspace, choose a serving mode, and register it in the catalog.
|
|
687
734
|
|
|
688
735
|
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:
|
|
689
736
|
|
|
690
737
|
```bash
|
|
691
738
|
# One-time setup per machine
|
|
692
|
-
for skill in markdown-preview progress-bars diff-notebook; do
|
|
739
|
+
for skill in markdown-preview progress-bars diff-notebook artifacts; do
|
|
693
740
|
mkdir -p ~/.claude/skills/sshler-$skill
|
|
694
741
|
ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
|
|
695
742
|
done
|
|
@@ -822,6 +869,12 @@ sshler serve
|
|
|
822
869
|
- **正直なパーセンテージ** - 表示パーセントは**切り捨て**です。3300 ステップのビルドで 3299/3300 なら 99% と表示され、実際に完了するまで誤解を招く 100% にはなりません。
|
|
823
870
|
- **ライブ更新** - WebSocket (`/ws/progress`) が接続中の全タブに upsert/delete イベントをファンアウト。複数マシン?スマホでも `/app/progress` を開けます。
|
|
824
871
|
|
|
872
|
+
**ローカル HTML 成果物**
|
|
873
|
+
- **生成 HTML を一か所で管理** - 単一ページ、静的サイト、再帰的に検出する HTML コレクションを、必須のプロジェクトと任意のネストしたグループに登録できます。
|
|
874
|
+
- **エージェントやスクリプト向け CLI** - `sshler artifact add ./report.html --project demo --group reports` で完成した出力を登録し、list、show、update、rescan、open、remove で管理できます。
|
|
875
|
+
- **安全なローカル表示** - HTML は厳密なパス制約と CORS 許可なしの別ループバックオリジンから配信されます。
|
|
876
|
+
- **非破壊の整理** - ゴミ箱操作と CLI の remove はカタログ情報だけを解除し、元のファイルやディレクトリは変更しません。
|
|
877
|
+
|
|
825
878
|
**🤖 Claude セッションダッシュボード**
|
|
826
879
|
- **`/app/claude` — Claude Code セッションの再開** - このマシン上で再開可能な [Claude Code](https://claude.ai/code) の会話(`~/.claude/projects/*/*.jsonl` から読み取り)を一覧表示し、ワンクリックでブラウザ内ターミナルに再開します。タイトルは Claude Code の `/resume` ピッカーと同じ規則です(`/rename` で付けた名前 → AI 生成タイトル → 最初のプロンプトの順)。
|
|
827
880
|
- **Git リポジトリ単位でグループ化・折りたたみ可能** - セッションはリポジトリのルート単位でグループ化されます(`repo/subdir` で実行した会話も `/resume` と同様に `repo` の下に表示)。各グループは折りたたみ可能で、アプリの他の場所と同じディレクトリ絵文字が付きます。初期状態は折りたたみで、フィルタ入力で該当グループが自動展開され、「すべて展開/すべて折りたたむ」ボタンもあります。
|
|
@@ -946,6 +999,19 @@ sshler progress delete build
|
|
|
946
999
|
|
|
947
1000
|
エラー時に `trap` → `--status failed` するデモループは `examples/progress-bar-build-watcher.sh` を参照。
|
|
948
1001
|
|
|
1002
|
+
### 生成 HTML を登録する
|
|
1003
|
+
|
|
1004
|
+
```bash
|
|
1005
|
+
sshler artifact add ./report.html --project demo --group reports
|
|
1006
|
+
sshler artifact add ./site --project demo --group prototypes --mode site --entry index.html
|
|
1007
|
+
sshler artifact add ./reports --project demo --group runs --discover
|
|
1008
|
+
sshler artifact list --project demo
|
|
1009
|
+
sshler artifact open <id>
|
|
1010
|
+
sshler artifact remove <id>
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
`/app/artifacts` でプロジェクト、登録、ページ検出、プレビュー、ゴミ箱操作を管理できます。開く・プレビューする機能はループバックアドレス経由で利用でき、`remove` は元のファイルを残します。設計とセキュリティモデルは [`ROADMAP-ARTIFACTS.md`](ROADMAP-ARTIFACTS.md) を参照してください。
|
|
1014
|
+
|
|
949
1015
|
### キーショートカット
|
|
950
1016
|
|
|
951
1017
|
- **Cmd/Ctrl+K** - コマンドパレット
|
|
@@ -1090,7 +1156,10 @@ SSHLER_COOKIE_SECURE=false # localhost 開発専用!
|
|
|
1090
1156
|
# /etc/caddy/Caddyfile または ~/Caddyfile
|
|
1091
1157
|
|
|
1092
1158
|
sshler.company.internal {
|
|
1093
|
-
reverse_proxy localhost:8822
|
|
1159
|
+
reverse_proxy localhost:8822 {
|
|
1160
|
+
# クライアントが送った X-Real-IP を上書き(SSHLER_TRUST_PROXY_HEADERS 参照)
|
|
1161
|
+
header_up X-Real-IP {remote_host}
|
|
1162
|
+
}
|
|
1094
1163
|
}
|
|
1095
1164
|
```
|
|
1096
1165
|
|
|
@@ -1101,6 +1170,10 @@ SSHLER_COOKIE_SECURE=false # localhost 開発専用!
|
|
|
1101
1170
|
SSHLER_PORT=8822
|
|
1102
1171
|
SSHLER_PUBLIC_URL=https://sshler.company.internal
|
|
1103
1172
|
SSHLER_COOKIE_SECURE=true # HTTPS 必須
|
|
1173
|
+
# Caddy が X-Real-IP を上書きする(上記 header_up)ので、レート制限と
|
|
1174
|
+
# ログインロックアウトにその値を使ってよい。同じホストのプロキシが X-Real-IP を
|
|
1175
|
+
# 上書きしない構成では設定しないこと(接続元アドレスが使われる)。
|
|
1176
|
+
SSHLER_TRUST_PROXY_HEADERS=true
|
|
1104
1177
|
```
|
|
1105
1178
|
|
|
1106
1179
|
4. Caddy を起動:
|
|
@@ -1211,6 +1284,11 @@ server {
|
|
|
1211
1284
|
}
|
|
1212
1285
|
```
|
|
1213
1286
|
|
|
1287
|
+
`proxy_set_header X-Real-IP $remote_addr` はクライアントが送った `X-Real-IP` を上書きするので、
|
|
1288
|
+
この設定では `SSHLER_TRUST_PROXY_HEADERS=true` を設定してください。この設定がないと sshler は
|
|
1289
|
+
`X-Real-IP` を無視し、レート制限とログインロックアウトを接続元アドレス(プロキシの背後では
|
|
1290
|
+
プロキシのアドレス)で数えます。
|
|
1291
|
+
|
|
1214
1292
|
**Traefik:**
|
|
1215
1293
|
|
|
1216
1294
|
```yaml
|
|
@@ -1342,12 +1420,13 @@ systemctl --user enable --now sshler.service
|
|
|
1342
1420
|
- `docs/skills/markdown-preview/SKILL.md` — 何がレンダリングされ、何がされないか、そして印刷/PDF パイプラインの癖。
|
|
1343
1421
|
- `docs/skills/progress-bars/SKILL.md` — プッシュプロトコル、WebSocket ファンアウト、ホストごとの購読モデル。
|
|
1344
1422
|
- `docs/skills/diff-notebook/SKILL.md` — マルチセルの差分ワークスペース:コマンドパーサー、base64 URL 状態、サーバー側の永続化、「新しいバックエンドを作らない」という設計判断。
|
|
1423
|
+
- `docs/skills/artifacts/SKILL.md` — 現在のワークスペースで HTML を作成し、配信モードを選んでカタログへ登録する手順。
|
|
1345
1424
|
|
|
1346
1425
|
Claude はこれらを自動では検出しません。リポジトリ内には存在しますが、Claude Code ランタイムには登録されていないためです。よく使われる方法は、**グローバルスキルディレクトリ(通常は `~/.claude/skills/`)へシンボリックリンクを張る**ことです。こうすると、このマシン上のどの Claude Code セッションからでも名前で読み込めるようになります:
|
|
1347
1426
|
|
|
1348
1427
|
```bash
|
|
1349
1428
|
# マシンごとに一度だけセットアップ
|
|
1350
|
-
for skill in markdown-preview progress-bars diff-notebook; do
|
|
1429
|
+
for skill in markdown-preview progress-bars diff-notebook artifacts; do
|
|
1351
1430
|
mkdir -p ~/.claude/skills/sshler-$skill
|
|
1352
1431
|
ln -sf "$PWD/docs/skills/$skill/SKILL.md" ~/.claude/skills/sshler-$skill/SKILL.md
|
|
1353
1432
|
done
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "sshler"
|
|
8
|
-
version = "1.0.
|
|
8
|
+
version = "1.0.6"
|
|
9
9
|
description = "A local FastAPI-powered SSH multiplexer for tmux-in-browser — from your laptop only."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.12"
|
|
@@ -57,6 +57,7 @@ dev = [
|
|
|
57
57
|
"playwright>=1.48.0",
|
|
58
58
|
"pytest-playwright>=0.5.0",
|
|
59
59
|
"ruff>=0.5.6",
|
|
60
|
+
"pytest-timeout>=2.3",
|
|
60
61
|
]
|
|
61
62
|
pdf = [
|
|
62
63
|
"playwright>=1.48.0",
|
|
@@ -76,6 +77,7 @@ dev = [
|
|
|
76
77
|
"types-pyyaml>=6.0.12.20250915",
|
|
77
78
|
"pre-commit>=4.0.0",
|
|
78
79
|
"build>=1.4.0",
|
|
80
|
+
"pytest-timeout>=2.3",
|
|
79
81
|
]
|
|
80
82
|
|
|
81
83
|
[project.scripts]
|
|
@@ -91,7 +93,7 @@ exclude = ["frontend*", "tests*"]
|
|
|
91
93
|
sshler = ["static/**/*", "scripts/*.ps1"]
|
|
92
94
|
|
|
93
95
|
[tool.ruff]
|
|
94
|
-
target-version = "
|
|
96
|
+
target-version = "py312"
|
|
95
97
|
line-length = 100
|
|
96
98
|
|
|
97
99
|
[tool.ruff.lint]
|
|
@@ -116,3 +118,13 @@ warn_redundant_casts = true
|
|
|
116
118
|
warn_unused_ignores = true
|
|
117
119
|
no_implicit_reexport = true
|
|
118
120
|
strict_equality = true
|
|
121
|
+
|
|
122
|
+
[tool.pytest.ini_options]
|
|
123
|
+
testpaths = ["tests"]
|
|
124
|
+
asyncio_mode = "strict"
|
|
125
|
+
timeout = 60
|
|
126
|
+
timeout_method = "thread"
|
|
127
|
+
addopts = "--strict-markers"
|
|
128
|
+
markers = [
|
|
129
|
+
"e2e: Playwright end-to-end tests under tests/e2e (deselect with -m \"not e2e\")",
|
|
130
|
+
]
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
from fastapi import APIRouter, Depends
|
|
4
4
|
|
|
5
5
|
from .archive import get_router as archive_router
|
|
6
|
-
from .
|
|
6
|
+
from .artifacts import get_router as artifacts_router
|
|
7
7
|
from .batch import get_router as batch_router
|
|
8
8
|
from .boxes import get_router as boxes_router
|
|
9
9
|
from .claude_sessions import get_router as claude_sessions_router
|
|
10
10
|
from .config import get_router as config_router
|
|
11
11
|
from .dependencies import APIDependencies
|
|
12
12
|
from .diff import get_router as diff_router
|
|
13
|
+
from .excel import get_router as excel_router
|
|
13
14
|
from .files import get_router as files_router
|
|
14
15
|
from .git import get_router as git_router
|
|
15
16
|
from .grep import get_router as grep_router
|
|
@@ -27,7 +28,12 @@ from .tunnels import get_router as tunnels_router
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def create_api_router(deps: APIDependencies) -> APIRouter:
|
|
30
|
-
api_router = APIRouter(
|
|
31
|
+
api_router = APIRouter(
|
|
32
|
+
prefix="/api/v1",
|
|
33
|
+
tags=["api"],
|
|
34
|
+
dependencies=[Depends(deps.require_token)],
|
|
35
|
+
)
|
|
36
|
+
api_router.include_router(artifacts_router(deps))
|
|
31
37
|
api_router.include_router(archive_router(deps))
|
|
32
38
|
api_router.include_router(excel_router(deps))
|
|
33
39
|
api_router.include_router(batch_router(deps))
|
|
@@ -18,7 +18,8 @@ from ..ssh import SSHError
|
|
|
18
18
|
from ..ssh_pool import get_pool
|
|
19
19
|
from ..validation import PathValidator, ValidationError
|
|
20
20
|
from .dependencies import APIDependencies
|
|
21
|
-
from .
|
|
21
|
+
from .files import _remote_path
|
|
22
|
+
from .helpers import _normalize_local_path
|
|
22
23
|
from .rate_limiting import rate_limit_file_ops
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -45,12 +46,14 @@ def _validate_archive_name(name: str, fmt: str) -> str:
|
|
|
45
46
|
try:
|
|
46
47
|
validated = PathValidator.validate_filename(name)
|
|
47
48
|
except ValidationError as exc:
|
|
48
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
49
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
49
50
|
|
|
50
51
|
expected_exts = {"tar.gz": (".tar.gz",), "tgz": (".tgz",), "zip": (".zip",)}
|
|
51
52
|
valid_exts = expected_exts.get(fmt, ())
|
|
52
53
|
if not any(validated.lower().endswith(ext) for ext in valid_exts):
|
|
53
|
-
raise HTTPException(
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=400, detail=f"Archive name must end in {' or '.join(valid_exts)}"
|
|
56
|
+
)
|
|
54
57
|
|
|
55
58
|
return validated
|
|
56
59
|
|
|
@@ -94,7 +97,10 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
94
97
|
if not payload.paths:
|
|
95
98
|
raise HTTPException(status_code=400, detail="No paths provided")
|
|
96
99
|
if payload.format not in VALID_FORMATS:
|
|
97
|
-
raise HTTPException(
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=400,
|
|
102
|
+
detail=f"Invalid format. Must be one of: {', '.join(VALID_FORMATS)}",
|
|
103
|
+
)
|
|
98
104
|
|
|
99
105
|
archive_name = _validate_archive_name(payload.archive_name, payload.format)
|
|
100
106
|
box = deps.get_box_or_404(application_config, name)
|
|
@@ -126,7 +132,7 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
126
132
|
try:
|
|
127
133
|
await asyncio.to_thread(_create_local)
|
|
128
134
|
except Exception as exc:
|
|
129
|
-
raise HTTPException(status_code=500, detail=str(exc))
|
|
135
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
130
136
|
|
|
131
137
|
return {"status": "ok", "message": f"Created {archive_name}", "path": str(archive_path)}
|
|
132
138
|
|
|
@@ -134,41 +140,48 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
134
140
|
try:
|
|
135
141
|
dest_validated = PathValidator.validate_remote_path(payload.destination)
|
|
136
142
|
except ValidationError as exc:
|
|
137
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
143
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
138
144
|
|
|
139
145
|
validated_paths: list[str] = []
|
|
140
146
|
for p in payload.paths:
|
|
141
147
|
try:
|
|
142
148
|
validated_paths.append(PathValidator.validate_remote_path(p))
|
|
143
149
|
except ValidationError as exc:
|
|
144
|
-
raise HTTPException(status_code=400, detail=f"Invalid path {p}: {exc}")
|
|
145
|
-
|
|
146
|
-
archive_target = str(PurePosixPath(dest_validated) / archive_name)
|
|
147
|
-
quoted_paths = " ".join(shlex.quote(p) for p in validated_paths)
|
|
148
|
-
|
|
149
|
-
if payload.format in ("tar.gz", "tgz"):
|
|
150
|
-
cmd = f"tar czf {shlex.quote(archive_target)} -C / {quoted_paths}"
|
|
151
|
-
else:
|
|
152
|
-
cmd = f"cd / && zip -r {shlex.quote(archive_target)} {quoted_paths}"
|
|
150
|
+
raise HTTPException(status_code=400, detail=f"Invalid path {p}: {exc}") from exc
|
|
153
151
|
|
|
154
152
|
ssh_pool = get_pool()
|
|
155
153
|
try:
|
|
156
154
|
async with ssh_pool.connection(
|
|
157
155
|
box, lambda: deps.connect_for_box(box, application_config)
|
|
158
156
|
) as connection:
|
|
157
|
+
# SFTP and the quoted shell command do not expand `~`: resolve it here.
|
|
158
|
+
dest_resolved = await _remote_path(connection, dest_validated)
|
|
159
|
+
resolved_paths = [await _remote_path(connection, p) for p in validated_paths]
|
|
160
|
+
archive_target = str(PurePosixPath(dest_resolved) / archive_name)
|
|
161
|
+
quoted_paths = " ".join(shlex.quote(p) for p in resolved_paths)
|
|
162
|
+
|
|
163
|
+
if payload.format in ("tar.gz", "tgz"):
|
|
164
|
+
cmd = f"tar czf {shlex.quote(archive_target)} -C / {quoted_paths}"
|
|
165
|
+
else:
|
|
166
|
+
cmd = f"cd / && zip -r {shlex.quote(archive_target)} {quoted_paths}"
|
|
167
|
+
|
|
159
168
|
try:
|
|
160
|
-
|
|
169
|
+
await asyncio.wait_for(
|
|
161
170
|
connection.run(cmd, check=True),
|
|
162
171
|
timeout=ARCHIVE_TIMEOUT,
|
|
163
172
|
)
|
|
164
|
-
except
|
|
165
|
-
raise HTTPException(
|
|
173
|
+
except TimeoutError as exc:
|
|
174
|
+
raise HTTPException(
|
|
175
|
+
status_code=504, detail="Archive creation timed out"
|
|
176
|
+
) from exc
|
|
166
177
|
except HTTPException:
|
|
167
178
|
raise
|
|
179
|
+
except ValidationError as exc:
|
|
180
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
168
181
|
except SSHError as exc:
|
|
169
|
-
raise HTTPException(status_code=502, detail=str(exc))
|
|
182
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
170
183
|
except Exception as exc:
|
|
171
|
-
raise HTTPException(status_code=500, detail=str(exc))
|
|
184
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
172
185
|
|
|
173
186
|
return {"status": "ok", "message": f"Created {archive_name}", "path": archive_target}
|
|
174
187
|
|
|
@@ -205,7 +218,7 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
205
218
|
except HTTPException:
|
|
206
219
|
raise
|
|
207
220
|
except Exception as exc:
|
|
208
|
-
raise HTTPException(status_code=500, detail=str(exc))
|
|
221
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
209
222
|
|
|
210
223
|
return {"status": "ok", "message": "Extracted", "path": str(dest_dir)}
|
|
211
224
|
|
|
@@ -214,14 +227,10 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
214
227
|
validated_archive = PathValidator.validate_remote_path(payload.archive_path)
|
|
215
228
|
validated_dest = PathValidator.validate_remote_path(payload.destination)
|
|
216
229
|
except ValidationError as exc:
|
|
217
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
230
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
218
231
|
|
|
219
232
|
name_lower = PurePosixPath(validated_archive).name.lower()
|
|
220
|
-
if name_lower.endswith((".tar.gz", ".tgz")):
|
|
221
|
-
cmd = f"tar xzf {shlex.quote(validated_archive)} -C {shlex.quote(validated_dest)}"
|
|
222
|
-
elif name_lower.endswith(".zip"):
|
|
223
|
-
cmd = f"unzip -o {shlex.quote(validated_archive)} -d {shlex.quote(validated_dest)}"
|
|
224
|
-
else:
|
|
233
|
+
if not name_lower.endswith((".tar.gz", ".tgz", ".zip")):
|
|
225
234
|
raise HTTPException(status_code=400, detail="Unsupported archive format")
|
|
226
235
|
|
|
227
236
|
ssh_pool = get_pool()
|
|
@@ -229,20 +238,33 @@ def get_router(deps: APIDependencies) -> APIRouter:
|
|
|
229
238
|
async with ssh_pool.connection(
|
|
230
239
|
box, lambda: deps.connect_for_box(box, application_config)
|
|
231
240
|
) as connection:
|
|
241
|
+
# SFTP and the quoted shell command do not expand `~`: resolve it here.
|
|
242
|
+
archive_resolved = await _remote_path(connection, validated_archive)
|
|
243
|
+
dest_resolved = await _remote_path(connection, validated_dest)
|
|
244
|
+
if name_lower.endswith((".tar.gz", ".tgz")):
|
|
245
|
+
cmd = (
|
|
246
|
+
f"tar xzf {shlex.quote(archive_resolved)} -C {shlex.quote(dest_resolved)}"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
cmd = (
|
|
250
|
+
f"unzip -o {shlex.quote(archive_resolved)} -d {shlex.quote(dest_resolved)}"
|
|
251
|
+
)
|
|
232
252
|
try:
|
|
233
253
|
await asyncio.wait_for(
|
|
234
254
|
connection.run(cmd, check=True),
|
|
235
255
|
timeout=ARCHIVE_TIMEOUT,
|
|
236
256
|
)
|
|
237
|
-
except
|
|
238
|
-
raise HTTPException(status_code=504, detail="Extraction timed out")
|
|
257
|
+
except TimeoutError as exc:
|
|
258
|
+
raise HTTPException(status_code=504, detail="Extraction timed out") from exc
|
|
239
259
|
except HTTPException:
|
|
240
260
|
raise
|
|
261
|
+
except ValidationError as exc:
|
|
262
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
241
263
|
except SSHError as exc:
|
|
242
|
-
raise HTTPException(status_code=502, detail=str(exc))
|
|
264
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
243
265
|
except Exception as exc:
|
|
244
|
-
raise HTTPException(status_code=500, detail=str(exc))
|
|
266
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
245
267
|
|
|
246
|
-
return {"status": "ok", "message": "Extracted", "path":
|
|
268
|
+
return {"status": "ok", "message": "Extracted", "path": dest_resolved}
|
|
247
269
|
|
|
248
270
|
return router
|