@tequila99/githuman 0.4.1 → 0.5.0
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.
- package/README.md +10 -0
- package/dist/cli/index.js +27 -2
- package/dist/cli/server-runtime.js +1 -0
- package/dist/server/app.js +22 -1
- package/dist/server/errors/terminal.js +4 -0
- package/dist/server/hooks/terminal/local-origin.js +25 -0
- package/dist/server/routes/app-info.js +5 -2
- package/dist/server/routes/terminal.js +63 -0
- package/dist/server/services/diff.service.js +23 -1
- package/dist/server/services/terminal/backend.js +216 -0
- package/dist/server/services/terminal/channel.js +200 -0
- package/dist/server/services/terminal/escape-tail.js +93 -0
- package/dist/server/services/terminal/processes.js +115 -0
- package/dist/server/services/terminal/prompt-tracker.js +15 -0
- package/dist/server/services/terminal/registry.js +167 -0
- package/dist/server/services/terminal/session.js +238 -0
- package/dist/server/services/terminal/socket.js +74 -0
- package/dist/server/services/terminal/tokens.js +32 -0
- package/dist/shared/app/schemas.js +5 -1
- package/dist/shared/diff/preamble.js +77 -0
- package/dist/shared/diff/schemas.js +4 -1
- package/dist/shared/terminal/constants.js +27 -0
- package/dist/shared/terminal/schemas.js +114 -0
- package/dist/shared/terminal/types.js +1 -0
- package/dist/web/assets/AppPage-D_OHhrKS.js +93 -0
- package/dist/web/assets/CreateReviewFabButton-B737aIvn.js +1 -0
- package/dist/web/assets/ErrorNotFound-jc5_L_-4.js +1 -0
- package/dist/web/assets/IndexPage-DzlFTw_b.js +1 -0
- package/dist/web/assets/IndexPage-u_qqIPRL.css +1 -0
- package/dist/web/assets/MainLayout-DaSyo22r.js +8 -0
- package/dist/web/assets/MainLayout-DoniW026.css +1 -0
- package/dist/web/assets/QBadge-BY-jTQ8w.js +1 -0
- package/dist/web/assets/QBtn-Bgm40omM.js +1 -0
- package/dist/web/assets/QSelect-713AWDd_.js +1 -0
- package/dist/web/assets/QSpace-BLu5AyZv.css +1 -0
- package/dist/web/assets/QSpace-BVBU-0oC.js +17 -0
- package/dist/web/assets/ReviewDetailPage-CLM6aU4u.js +1 -0
- package/dist/web/assets/ReviewsPage-bllnaoOS.js +1 -0
- package/dist/web/assets/StatusSwitcher-BZ4FYVnY.js +2 -0
- package/dist/web/assets/{StatusSwitcher-Ca5k9Tjx.css → StatusSwitcher-BxRuEHHQ.css} +1 -1
- package/dist/web/assets/TerminalView-B11UHZv3.css +1 -0
- package/dist/web/assets/TerminalView-Ci9CNTyY.js +19 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/{abnfDiagram-O67JEVCF-BUX560Qb.js → abnfDiagram-O67JEVCF-4qpB0CGh.js} +1 -1
- package/dist/web/assets/agent-store-DIma0R4U.js +1 -0
- package/dist/web/assets/app-info-store-BzsJOxVc.js +1 -0
- package/dist/web/assets/{arc-dElW-85p.js → arc-DgcGdgmL.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-DG6nf_hE.js +1 -0
- package/dist/web/assets/architectureDiagram-NJMV4G6O-CEL-_i5O.js +36 -0
- package/dist/web/assets/{blockDiagram-BEXU5L5S-D4r5wIie.js → blockDiagram-BEXU5L5S-bMrUf1GU.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-ww04LJhe.js → c4Diagram-YGBWAQC7-C54mp6dl.js} +1 -1
- package/dist/web/assets/channel-CS4rbjhf.js +1 -0
- package/dist/web/assets/chunk-3FUC2YCW-BtIughAT.js +2 -0
- package/dist/web/assets/{chunk-5DYCD2WN-BmtGl0Xn.js → chunk-5DYCD2WN-BhyMEDuJ.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-5p0wX5Yc.js → chunk-742MDFTN-8MczzxE2.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-CWFVY0jp.js → chunk-7INBJB4K-BdS9oIIG.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-DUgZIYj4.js → chunk-7M6MHVWA-BU5_UwaZ.js} +17 -17
- package/dist/web/assets/{chunk-7PRAP22T-BrhA6Tm7.js → chunk-7PRAP22T-BaAw-gEj.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-B83nsnuR.js → chunk-GTNCS2PH-DNpH8E_x.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-cewRmL7f.js → chunk-GWA4HPMP-BvG5tXy2.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-D18SeqyB.js → chunk-MBY4JIJT-B8XJMHG2.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-5SjiRygT.js → chunk-NETBCI7D-ClCXW1dx.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-H6mLYErS.js → chunk-O7XYJQB3-BNcB0oiZ.js} +6 -6
- package/dist/web/assets/{chunk-UA2S7LBM-C5VQVDlC.js → chunk-UA2S7LBM-CS5CtIsj.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-CrOcvEcH.js → chunk-WEXAMYUT-CJaZJWKF.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-BuVPxQhc.js → chunk-XXDRQBXY-BvKO0v-W.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-BVP3f1-g.js → chunk-Z7XXMR3K-DP2k1Ci5.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-CF-OzGXH.js → chunk-ZIGJFQKS-y4KHfdEv.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-Bd3VDNaV.js → classDiagram-v2-NBCMYWYE-C-kWcloV.js} +7 -7
- package/dist/web/assets/{core-DiiZIGCg.js → core-C37KF-j0.js} +1 -1
- package/dist/web/assets/core-II1Hg8Ir.js +12 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-CMiVhuwL.js +1 -0
- package/dist/web/assets/create-CSUVwdx4.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-6l9c0yRf.js → cynefin-OW5HDTMX-C3AwH_vU.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-u9Gy0Odz.js → cynefinDiagram-VND7K2PF-DYF_CoB8.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-D1jsEBl4.js → dagre-6A5THRUB-DWCEmsd1.js} +1 -1
- package/dist/web/assets/debounce-rZaFFwsz.js +1 -0
- package/dist/web/assets/{diagram-22UHCM2B-DpyH5n1y.js → diagram-22UHCM2B-oBFhSpao.js} +16 -16
- package/dist/web/assets/{diagram-3UASUU5V-3X1RJV9-.js → diagram-3UASUU5V-Cj9rB07y.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-B_bvvAua.js → diagram-ATOU4E4O-D4H9BqUQ.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-DheqAuOA.js → diagram-CDSNMT55-qLgJBiIs.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-CYwe9zYy.js → diagram-MLGK6HIB-Bhhvtx0A.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-CVd9i23e.js → diagram-MPIPVDR6-O4wokd54.js} +1 -1
- package/dist/web/assets/dist-B_J_HbC0.js +1 -0
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-B5vtXULf.js → ebnfDiagram-ZINNZB2B-BRU7SszO.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-BmMM0tV5.js → elk-276RUBZZ-r0X8f7Sv.js} +1 -1
- package/dist/web/assets/engine-oniguruma-B4iNeJbd.js +1 -0
- package/dist/web/assets/engine-oniguruma-DdQrZxAE.js +1 -0
- package/dist/web/assets/{erDiagram-OPXOYQCR-1ORPVLQR.js → erDiagram-OPXOYQCR-DbgjsGkX.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-1Q8-FsiO.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-BbwsRPsv.js +1 -0
- package/dist/web/assets/ganttDiagram-FUAMR5RP-BwhUglGo.js +292 -0
- package/dist/web/assets/{gitGraph-4MIJSDKK-CXCCnZ1j.js → gitGraph-4MIJSDKK-DA11BsSo.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-DPDrqSt6.js → gitGraphDiagram-X574FWY7-BdG7d-11.js} +1 -1
- package/dist/web/assets/highlight.worker-BHCdEX06.js +2 -0
- package/dist/web/assets/html-CEES10Qm.js +1 -0
- package/dist/web/assets/html-FFOZEiuQ.js +1 -0
- package/dist/web/assets/i18n-D1K2AGeW.js +1 -0
- package/dist/web/assets/index-BatRsx0U.css +1 -0
- package/dist/web/assets/index-DuCIKkvf.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-CGg3Xa4X.js → info-A6RAGUB7-BxzfvGqu.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-vx9EUrdV.js → infoDiagram-VRGFBTTK-veq817ut.js} +1 -1
- package/dist/web/assets/init-app-DKRVN8HV.js +1 -0
- package/dist/web/assets/{is-BB7rJzdU.js → is-EbmiuVzv.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-DL7T0J63.js → ishikawaDiagram-OU5B5YK6-DMuzkArR.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-D45T1Pj5.js → journeyDiagram-ZHPQQLJL-Cqj0kbKe.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-C9ZnTE1l.js → kanban-definition-PNTS6WVX-DUvral0v.js} +1 -1
- package/dist/web/assets/{line-CLBEcmiH.js → line-DcLaUOyJ.js} +1 -1
- package/dist/web/assets/{linear-B5bBEu0i.js → linear-BUnzfXgk.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Dj-n65Ou.js → mermaid-parser.core-CY7nVQ2O.js} +3 -3
- package/dist/web/assets/mermaid.core-Cz083qmW.js +44 -0
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-ZX638ce_.js → mindmap-definition-NLK3R4M7--Twjhxcv.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-D-XZHUq_.js → packet-AYTQ26CC-Csf3Pf50.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-sE-EOKNL.js → pegDiagram-GJSIUBJH-vVjFUYc4.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-DMkVggJA.js → pie-WAS4IAKB-CyUE71ZQ.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-DSdIEQyS.js → pieDiagram-5QR66LMP-CV24sdEs.js} +1 -1
- package/dist/web/assets/pinia-UjhgYLzw.js +1 -0
- package/dist/web/assets/pinia-ri8AYppz.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-BReXSVss.js → quadrantDiagram-O4NWA36T-GVAUaTqf.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-CJzPwlj6.js → radar-RG4KPBEZ-D7YGMFYX.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CkhRuR3N.js → railroad-74A4TZTK-BYsDf4cg.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-Bg7f5Sbu.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-ufUodNib.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-YvUtEf_g.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-DCoF4AEN.js → railroadDiagram-XR7U4H2S-DOyP7Y6B.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-B15aXZXI.js → requirementDiagram-PLB6GJNP-C2CJ05_O.js} +1 -1
- package/dist/web/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/dist/web/assets/runtime-core.esm-bundler-CF89Lyp2.js +1 -0
- package/dist/web/assets/{safe-storage-ak36tVRO.js → safe-storage-BpHrcFXl.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DcWsnYYP.js → sankeyDiagram-IPEJSGJF-rYUTxo-7.js} +1 -1
- package/dist/web/assets/scss-CLKKmMvk.js +1 -0
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-CXWUNmSl.js → sequenceDiagram-PO4LG4MO-Dy-NGv9K.js} +1 -1
- package/dist/web/assets/src-oBChb5qS.js +1 -0
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-M9PkKdix.js → stateDiagram-v2-GCMORJYK-_uEXf3B4.js} +7 -7
- package/dist/web/assets/{swimlanes-2SLR337P-B13LpAqh.js → swimlanes-2SLR337P-CcejXyI9.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-I9_cJ2VX.js +8 -0
- package/dist/web/assets/theme-Dv0yhgRD.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-Dli0jMaX.js → timeline-definition-EJHVYXUP-CSry3jW1.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BwBFpWIC.js → treeView-Q6P3EWNA-CUCgNMVi.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DYGjPuTv.js → treemap-WGGIJYW6-BtG_xZt4.js} +1 -1
- package/dist/web/assets/use-app-theme-CSRcIN0e.js +1 -0
- package/dist/web/assets/use-panel-width-DNg6NE2M.js +1 -0
- package/dist/web/assets/use-server-events-CYiRauy1.js +1 -0
- package/dist/web/assets/use-virtual-scroll-DTEGubei.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-Chs4O-o-.js → usecaseDiagram-POWQR4AR-BY2X6VR2.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-D3GVZcIr.js → vennDiagram-UO4OBE2U-D-khbRsr.js} +1 -1
- package/dist/web/assets/vue-15YBeysQ.js +1 -0
- package/dist/web/assets/{vue-i18n.runtime-D0pkhc7g.js → vue-i18n.runtime-BTqPc4vG.js} +2 -2
- package/dist/web/assets/{wardley-WFR3VGLG-B5YUEVo5.js → wardley-WFR3VGLG-BwwTtxgX.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-liqH-4lm.js → wardleyDiagram-VNRHLVJA-N4eN_Mx8.js} +1 -1
- package/dist/web/assets/wasm-S0UXjx1m.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-C63N6ZvA.js → xychartDiagram-PMCCYNJV-Bp3do5lv.js} +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +11 -3
- package/dist/web/assets/AppPage-BSvF7ik1.js +0 -1
- package/dist/web/assets/CreateReviewFabButton-M0od1QEZ.js +0 -1
- package/dist/web/assets/ErrorNotFound-BAP3n0aC.js +0 -1
- package/dist/web/assets/IndexPage-CztuM7ha.css +0 -1
- package/dist/web/assets/IndexPage-Do-EZOBf.js +0 -1
- package/dist/web/assets/MainLayout-ChxXEt_Q.css +0 -1
- package/dist/web/assets/MainLayout-daEN-VtX.js +0 -7
- package/dist/web/assets/QBtn-BDbJU6_B.js +0 -1
- package/dist/web/assets/QMenu-DXgFwoTJ.js +0 -1
- package/dist/web/assets/QSelect-DzYPZHrW.js +0 -1
- package/dist/web/assets/ReviewDetailPage-DEtNg2UN.js +0 -1
- package/dist/web/assets/ReviewsPage-Dg-iCxsQ.js +0 -1
- package/dist/web/assets/StatusSwitcher-BpebV47C.js +0 -4
- package/dist/web/assets/agent-icon-BJbyp2DP.js +0 -17
- package/dist/web/assets/agent-icon-BUd-vCRS.css +0 -1
- package/dist/web/assets/agent-store-B9E-aiz3.js +0 -1
- package/dist/web/assets/app-info-store-xZiQfdgO.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-bgxww3gY.js +0 -1
- package/dist/web/assets/architectureDiagram-NJMV4G6O-CUUKpxro.js +0 -36
- package/dist/web/assets/channel-CdThk9yc.js +0 -1
- package/dist/web/assets/chunk-3FUC2YCW-9i3K5FXP.js +0 -2
- package/dist/web/assets/cose-bilkent-JH36ORCC-ifMLrEkd.js +0 -1
- package/dist/web/assets/create-BkZH2Q0U.js +0 -1
- package/dist/web/assets/debounce-CDIE7CV6.js +0 -1
- package/dist/web/assets/dist-BzuUT3GV.js +0 -1
- package/dist/web/assets/engine-oniguruma-D6DCTEub.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BH97sTDs.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-CRwhE8ka.js +0 -1
- package/dist/web/assets/ganttDiagram-FUAMR5RP-CAWNdy1S.js +0 -292
- package/dist/web/assets/i18n-D5wmXwHr.js +0 -1
- package/dist/web/assets/index-CgeU6Wuu.css +0 -1
- package/dist/web/assets/index-DWi4DKrl.js +0 -2
- package/dist/web/assets/init-app-CgDkvlRb.js +0 -1
- package/dist/web/assets/mermaid.core-CmPsESsw.js +0 -44
- package/dist/web/assets/pinia-BMN9Drn3.js +0 -1
- package/dist/web/assets/pinia-DmTi2T44.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CcS8fVgm.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BlpYA103.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-C2EznudM.js +0 -1
- package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +0 -1
- package/dist/web/assets/single-flight-r8B7stt1.js +0 -1
- package/dist/web/assets/src-CB-shhJB.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BmCQPnJC.js +0 -8
- package/dist/web/assets/theme-BMaTNJdz.js +0 -1
- package/dist/web/assets/use-app-theme-C9HTPdVu.js +0 -1
- package/dist/web/assets/use-panel-width-DGbjs_T8.js +0 -1
- package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +0 -1
package/README.md
CHANGED
|
@@ -171,3 +171,13 @@ pnpm run build # server (tsc) + web (quasar build) → dist/
|
|
|
171
171
|
## Лицензия
|
|
172
172
|
|
|
173
173
|
MIT, см. [LICENSE](LICENSE).
|
|
174
|
+
|
|
175
|
+
## Встроенный терминал
|
|
176
|
+
|
|
177
|
+
При запуске на loopback кнопка **Terminal** в шапке и меню открывает терминал в корне репозитория. ``Ctrl+Shift+` `` открывает или сворачивает окно. Окно можно перетаскивать и сворачивать; «+» добавляет вкладку, до восьми терминалов. Закрытие вкладки требует подтверждения и останавливает shell с его командами. Команда `exit` закрывает вкладку сразу.
|
|
178
|
+
|
|
179
|
+
Сессии переживают обновление страницы и доступны из нескольких страниц браузера. История ограничена 5000 строками scrollback; после остановки сервера она теряется. Без подписчиков простаивающая сессия завершается через 15 минут, работающая или с неизвестным состоянием — не позднее часа.
|
|
180
|
+
|
|
181
|
+
Если платформенный бинарник PTY недоступен, терминал работает в ограниченном режиме с баннером: команды вводятся в поле под выводом, Ctrl+C прерывает команду, Ctrl+D закрывает shell. `vim`, `less` и другие интерактивные программы требуют PTY. В LAN терминал отключён. Переключатель исходных цветов отключает минимальный контраст для программ со своей палитрой.
|
|
182
|
+
|
|
183
|
+
Для разработки на другом порту backend можно передать `GITHUMAN_DEV_API=http://localhost:3848` при запуске Quasar dev-сервера.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { errorMessage } from "../shared/utils/error-message.js";
|
|
2
3
|
import { writeFileSync } from 'node:fs';
|
|
3
4
|
import { parseServeArgs } from "./config.js";
|
|
4
5
|
import { startServer, resolveRepositoryPath, resolveReviewsDbPath } from "./server-runtime.js";
|
|
@@ -7,6 +8,8 @@ import { parseListArgs, runList } from "./commands/list.js";
|
|
|
7
8
|
import { parseExportArgs, runExport } from "./commands/export.js";
|
|
8
9
|
import { createFileDatabase } from "../server/db/index.js";
|
|
9
10
|
import { getAppVersion } from "../server/app-version.js";
|
|
11
|
+
// Force exit when a graceful close hangs. It must exceed the terminal kill time: ps timeout plus grace.
|
|
12
|
+
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
10
13
|
/** Opens the reviews DB for the repo at cwd, shared by `list` and `export`. */
|
|
11
14
|
async function openReviewsDb(dbPrefix) {
|
|
12
15
|
const repositoryPath = await resolveRepositoryPath(process.cwd());
|
|
@@ -21,7 +24,29 @@ async function main(argv) {
|
|
|
21
24
|
switch (command) {
|
|
22
25
|
case 'serve': {
|
|
23
26
|
const options = parseServeArgs(rest);
|
|
24
|
-
const { url } = await startServer(options);
|
|
27
|
+
const { app, url } = await startServer(options);
|
|
28
|
+
let stopping = false;
|
|
29
|
+
const shutdown = () => {
|
|
30
|
+
if (stopping)
|
|
31
|
+
return;
|
|
32
|
+
stopping = true;
|
|
33
|
+
// Do not wait for a stuck connection forever. Unref lets a clean close end the process by itself.
|
|
34
|
+
const deadline = setTimeout(() => process.exit(1), SHUTDOWN_TIMEOUT_MS);
|
|
35
|
+
deadline.unref();
|
|
36
|
+
void app
|
|
37
|
+
.close()
|
|
38
|
+
.then(() => {
|
|
39
|
+
clearTimeout(deadline);
|
|
40
|
+
process.exitCode = 0;
|
|
41
|
+
return undefined;
|
|
42
|
+
})
|
|
43
|
+
.catch(error => {
|
|
44
|
+
console.error(errorMessage(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
process.once('SIGINT', shutdown);
|
|
49
|
+
process.once('SIGTERM', shutdown);
|
|
25
50
|
console.log(formatStartupMessage(url, options.host));
|
|
26
51
|
return;
|
|
27
52
|
}
|
|
@@ -50,6 +75,6 @@ async function main(argv) {
|
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
main(process.argv.slice(2)).catch((error) => {
|
|
53
|
-
console.error(error
|
|
78
|
+
console.error(errorMessage(error));
|
|
54
79
|
process.exitCode = 1;
|
|
55
80
|
});
|
|
@@ -64,6 +64,7 @@ export async function startServer(options) {
|
|
|
64
64
|
db,
|
|
65
65
|
watchFiles: true,
|
|
66
66
|
apiDocs: options.apiDocs === true,
|
|
67
|
+
terminalEnabled: isLoopbackHost(options.host),
|
|
67
68
|
// Agents can run commands, so they are only offered on a loopback bind.
|
|
68
69
|
...(isLoopbackHost(options.host)
|
|
69
70
|
? {
|
package/dist/server/app.js
CHANGED
|
@@ -4,6 +4,10 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import Fastify from 'fastify';
|
|
6
6
|
import fastifyStatic from '@fastify/static';
|
|
7
|
+
import fastifyWebsocket from '@fastify/websocket';
|
|
8
|
+
import { terminalRoutes, TERMINAL_WEBSOCKET_OPTIONS } from "./routes/terminal.js";
|
|
9
|
+
import { TerminalRegistry } from "./services/terminal/registry.js";
|
|
10
|
+
import { loadPty, terminalSupported } from "./services/terminal/backend.js";
|
|
7
11
|
import fastifySwagger from '@fastify/swagger';
|
|
8
12
|
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
9
13
|
// @fastify/sse's published types declare an ESM default export that doesn't match
|
|
@@ -32,6 +36,10 @@ export function buildApp(options = {}) {
|
|
|
32
36
|
const repositoryPath = options.repositoryPath ?? process.cwd();
|
|
33
37
|
const db = options.db ?? createTestDatabase();
|
|
34
38
|
const eventBus = options.eventBus ?? createEventBus();
|
|
39
|
+
// Windows includes a PTY binary, but the shell and process list are Unix-only.
|
|
40
|
+
const terminalEnabled = options.terminalEnabled === true && terminalSupported();
|
|
41
|
+
if (terminalEnabled)
|
|
42
|
+
app.register(fastifyWebsocket, TERMINAL_WEBSOCKET_OPTIONS);
|
|
35
43
|
app.register(fastifySse);
|
|
36
44
|
// The spec collects routes as they are added, so it comes before them.
|
|
37
45
|
app.register(fastifySwagger, openApiOptions());
|
|
@@ -40,7 +48,12 @@ export function buildApp(options = {}) {
|
|
|
40
48
|
}
|
|
41
49
|
app.register(openApiRoutes);
|
|
42
50
|
app.register(healthRoutes);
|
|
43
|
-
app.register(appInfoRoutes
|
|
51
|
+
app.register(appInfoRoutes, {
|
|
52
|
+
terminalCapability: async () => ({
|
|
53
|
+
available: terminalEnabled,
|
|
54
|
+
mode: terminalEnabled ? ((await loadPty()) ? 'pty' : 'pipe') : null
|
|
55
|
+
})
|
|
56
|
+
});
|
|
44
57
|
app.register(diffRoutes, { repositoryPath });
|
|
45
58
|
app.register(gitRoutes, { repositoryPath });
|
|
46
59
|
app.register(reviewRoutes, { repositoryPath, db, eventBus });
|
|
@@ -50,6 +63,14 @@ export function buildApp(options = {}) {
|
|
|
50
63
|
eventBus,
|
|
51
64
|
instanceId: options.instanceId ?? randomUUID()
|
|
52
65
|
});
|
|
66
|
+
if (terminalEnabled)
|
|
67
|
+
app.register(terminalRoutes, {
|
|
68
|
+
registry: options.terminalRegistry ??
|
|
69
|
+
new TerminalRegistry({
|
|
70
|
+
repositoryPath,
|
|
71
|
+
onLifecycle: event => app.log.info({ terminal: event }, 'Terminal lifecycle')
|
|
72
|
+
})
|
|
73
|
+
});
|
|
53
74
|
if (options.agentPresets) {
|
|
54
75
|
app.register(agentRoutes, {
|
|
55
76
|
registry: createSessionRegistry({
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import createError from '@fastify/error';
|
|
2
|
+
export const TerminalError = createError('GHT_TERMINAL', '%s', 409);
|
|
3
|
+
export const TerminalNotFoundError = createError('GHT_TERMINAL_NOT_FOUND', '%s', 404);
|
|
4
|
+
export const TerminalLimitError = createError('GHT_TERMINAL_LIMIT', '%s', 409);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isLoopbackHost } from "../../../shared/network/loopback.js";
|
|
2
|
+
import { ForbiddenError } from "../../errors/http.js";
|
|
3
|
+
// Returns false on any malformed value: an invalid URL means an invalid origin.
|
|
4
|
+
function isTerminalOrigin(host, origin) {
|
|
5
|
+
if (!host || !origin)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
const source = new URL(origin);
|
|
9
|
+
const target = new URL(`http://${host}`);
|
|
10
|
+
return (isLoopbackHost(target.hostname) &&
|
|
11
|
+
target.host === host &&
|
|
12
|
+
source.host === target.host &&
|
|
13
|
+
source.protocol === 'http:' &&
|
|
14
|
+
source.origin === origin &&
|
|
15
|
+
!target.username &&
|
|
16
|
+
!target.password);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function requireTerminalOrigin(request) {
|
|
23
|
+
if (!isTerminalOrigin(request.headers.host, request.headers.origin))
|
|
24
|
+
throw new ForbiddenError('Terminal endpoints only accept requests from this local server. Open the app via localhost.');
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAppVersion } from "../app-version.js";
|
|
2
2
|
import { AppInfoSchema } from "../../shared/app/schemas.js";
|
|
3
|
-
export async function appInfoRoutes(app) {
|
|
3
|
+
export async function appInfoRoutes(app, options) {
|
|
4
4
|
app.withTypeProvider().get('/api/app-info', {
|
|
5
5
|
schema: {
|
|
6
6
|
tags: ['app'],
|
|
@@ -8,6 +8,9 @@ export async function appInfoRoutes(app) {
|
|
|
8
8
|
response: { 200: AppInfoSchema }
|
|
9
9
|
}
|
|
10
10
|
}, async () => {
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
version: getAppVersion(),
|
|
13
|
+
terminal: await options.terminalCapability()
|
|
14
|
+
};
|
|
12
15
|
});
|
|
13
16
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { TerminalTokenSchema } from "../../shared/terminal/schemas.js";
|
|
3
|
+
import { TERMINAL_FRAME_BYTES } from "../../shared/terminal/constants.js";
|
|
4
|
+
import { ForbiddenError } from "../errors/http.js";
|
|
5
|
+
import { requireTerminalOrigin } from "../hooks/terminal/local-origin.js";
|
|
6
|
+
import { TerminalTokens } from "../services/terminal/tokens.js";
|
|
7
|
+
import { attachTerminalSocket } from "../services/terminal/socket.js";
|
|
8
|
+
// Reject oversized values early. A real token has 43 characters.
|
|
9
|
+
const MAX_TOKEN_LENGTH = 64;
|
|
10
|
+
// Leave room for the JSON envelope around an input frame.
|
|
11
|
+
const MAX_PAYLOAD_BYTES = TERMINAL_FRAME_BYTES * 2;
|
|
12
|
+
// Apply the same frame limit to every terminal upgrade.
|
|
13
|
+
export const TERMINAL_WEBSOCKET_OPTIONS = {
|
|
14
|
+
options: { maxPayload: MAX_PAYLOAD_BYTES }
|
|
15
|
+
};
|
|
16
|
+
export async function terminalRoutes(app, options) {
|
|
17
|
+
const tokens = new TerminalTokens();
|
|
18
|
+
const connections = new Set();
|
|
19
|
+
app.addHook('onError', async (_request, _reply, error) => {
|
|
20
|
+
// Request URLs contain credentials, so log only the rejection code.
|
|
21
|
+
app.log.warn({ code: error.code }, 'Terminal request rejected');
|
|
22
|
+
});
|
|
23
|
+
const api = app.withTypeProvider();
|
|
24
|
+
api.post('/api/terminal/token', {
|
|
25
|
+
onRequest: requireTerminalOrigin,
|
|
26
|
+
schema: {
|
|
27
|
+
tags: ['terminal'],
|
|
28
|
+
summary: 'Issue a terminal connection token',
|
|
29
|
+
response: { 200: TerminalTokenSchema }
|
|
30
|
+
}
|
|
31
|
+
}, async (request) => ({ token: tokens.issue(request.headers.origin ?? '') }));
|
|
32
|
+
api.get('/api/terminal/ws', {
|
|
33
|
+
websocket: true,
|
|
34
|
+
schema: {
|
|
35
|
+
hide: true,
|
|
36
|
+
querystring: Type.Object({
|
|
37
|
+
token: Type.String({
|
|
38
|
+
maxLength: MAX_TOKEN_LENGTH,
|
|
39
|
+
description: 'One-use connection credential.'
|
|
40
|
+
})
|
|
41
|
+
}, {
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
description: 'Terminal upgrade credential.'
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
onRequest: requireTerminalOrigin,
|
|
47
|
+
preValidation: async (request) => {
|
|
48
|
+
if (!tokens.consume(request.query.token, request.headers.origin ?? ''))
|
|
49
|
+
throw new ForbiddenError('Terminal connection token is invalid or expired.');
|
|
50
|
+
}
|
|
51
|
+
}, socket => {
|
|
52
|
+
const connection = attachTerminalSocket(socket, options.registry, {
|
|
53
|
+
onDisconnect: () => connections.delete(connection)
|
|
54
|
+
});
|
|
55
|
+
connections.add(connection);
|
|
56
|
+
});
|
|
57
|
+
app.addHook('onClose', async () => {
|
|
58
|
+
for (const connection of connections)
|
|
59
|
+
connection.close();
|
|
60
|
+
tokens.clear();
|
|
61
|
+
await options.registry.dispose();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diffLines } from 'diff';
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import { assertSafeRef, listChangedSummaries, resolveWithinRepo, getFileAtRef, getFilesAtRefBatch, listChangedPaths, listUntrackedPaths } from "./git.service.js";
|
|
4
|
+
import { createPreambleReader } from "../../shared/diff/preamble.js";
|
|
4
5
|
/** Unchanged lines kept around each change in a hunk, as in `git diff`. */
|
|
5
6
|
const CONTEXT_LINES = 3;
|
|
6
7
|
/** Stamp of a worktree file that cannot be read. */
|
|
@@ -57,6 +58,27 @@ function windowHunks(lines, contextLines = CONTEXT_LINES) {
|
|
|
57
58
|
}
|
|
58
59
|
return hunks;
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Gives each hunk the unchanged lines above it, for the highlighter. A hunk with no new
|
|
63
|
+
* line (a deleted file) takes them from the old side. A side is split into lines only when
|
|
64
|
+
* a hunk needs it: a hunk at the top of a file has no preamble.
|
|
65
|
+
*/
|
|
66
|
+
function withPreambles(hunks, meta, oldContent, newContent) {
|
|
67
|
+
let oldReader;
|
|
68
|
+
let newReader;
|
|
69
|
+
return hunks.map(hunk => {
|
|
70
|
+
let preamble = [];
|
|
71
|
+
if (hunk.newStart > 1) {
|
|
72
|
+
newReader ??= createPreambleReader(meta.newPath, splitLines(newContent));
|
|
73
|
+
preamble = newReader(hunk.newStart);
|
|
74
|
+
}
|
|
75
|
+
else if (hunk.newStart === 0 && hunk.oldStart > 1) {
|
|
76
|
+
oldReader ??= createPreambleReader(meta.oldPath, splitLines(oldContent));
|
|
77
|
+
preamble = oldReader(hunk.oldStart);
|
|
78
|
+
}
|
|
79
|
+
return preamble.length > 0 ? { ...hunk, preamble } : hunk;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
60
82
|
export function computeFileDiff(oldContent, newContent, meta) {
|
|
61
83
|
const base = {
|
|
62
84
|
oldPath: meta.oldPath,
|
|
@@ -105,7 +127,7 @@ export function computeFileDiff(oldContent, newContent, meta) {
|
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
|
-
const hunks = windowHunks(lines);
|
|
130
|
+
const hunks = withPreambles(windowHunks(lines), meta, oldContent, newContent);
|
|
109
131
|
return { ...base, additions, deletions, hunks };
|
|
110
132
|
}
|
|
111
133
|
/**
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { PromptTracker, TERMINAL_PROMPT_MARKER } from "./prompt-tracker.js";
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { TerminalError } from "../../errors/terminal.js";
|
|
5
|
+
import { classifyProcessActivity, processActivity, signalProcess, terminateProcesses } from "./processes.js";
|
|
6
|
+
// Stop waiting for the first prompt marker after this time.
|
|
7
|
+
const STARTUP_TIMEOUT_MS = 2000;
|
|
8
|
+
// Stop waiting for the first prompt marker after this many output characters.
|
|
9
|
+
const STARTUP_BUFFER_CHARS = 65536;
|
|
10
|
+
// Shells that accept `-l -i` and set the prompt through `PS1`. Other shells get no prompt marker.
|
|
11
|
+
const POSIX_SHELLS = ['bash', 'zsh', 'sh', 'dash', 'ksh'];
|
|
12
|
+
// Windows includes a PTY binary, but the shell and process list are Unix-only.
|
|
13
|
+
export function terminalSupported(platform = process.platform) {
|
|
14
|
+
return platform !== 'win32';
|
|
15
|
+
}
|
|
16
|
+
function promptPrefix(shellName) {
|
|
17
|
+
if (shellName === 'bash')
|
|
18
|
+
return "'\\[\\e]133;A\\a\\]'";
|
|
19
|
+
if (shellName === 'zsh')
|
|
20
|
+
return '"%{$(printf \'\\033]133;A\\007\')%}"';
|
|
21
|
+
return '"$(printf \'\\033]133;A\\007\')"';
|
|
22
|
+
}
|
|
23
|
+
// Bash stores this typed line, so delete that entry before the marker.
|
|
24
|
+
function promptSetup(shellName) {
|
|
25
|
+
const assign = `PS1=${promptPrefix(shellName)}"$PS1"`;
|
|
26
|
+
const marker = "printf '\\033]133;A\\007'";
|
|
27
|
+
if (shellName === 'bash')
|
|
28
|
+
return `${assign}; history -d $HISTCMD || true; ${marker}`;
|
|
29
|
+
return `${assign}; ${marker}`;
|
|
30
|
+
}
|
|
31
|
+
async function shellActivity(pid, prompt, rows) {
|
|
32
|
+
const activity = rows === undefined
|
|
33
|
+
? await processActivity(pid)
|
|
34
|
+
: classifyProcessActivity(rows, pid);
|
|
35
|
+
if (activity !== 'idle')
|
|
36
|
+
return activity;
|
|
37
|
+
return prompt.idle ? 'idle' : 'unknown';
|
|
38
|
+
}
|
|
39
|
+
let modulePromise;
|
|
40
|
+
export function loadPty() {
|
|
41
|
+
modulePromise ??= import('@lydell/node-pty').catch(() => null);
|
|
42
|
+
return modulePromise;
|
|
43
|
+
}
|
|
44
|
+
export class NodePtyBackend {
|
|
45
|
+
mode = 'pty';
|
|
46
|
+
pty;
|
|
47
|
+
prompt = new PromptTracker();
|
|
48
|
+
listeners = new Set();
|
|
49
|
+
disposeData;
|
|
50
|
+
startupTimer;
|
|
51
|
+
constructor(pty, shellName) {
|
|
52
|
+
this.pty = pty;
|
|
53
|
+
const integrated = shellName !== undefined && POSIX_SHELLS.includes(shellName);
|
|
54
|
+
let ready = !integrated;
|
|
55
|
+
let startup = '';
|
|
56
|
+
const emit = (data) => {
|
|
57
|
+
for (const listener of this.listeners)
|
|
58
|
+
listener(data);
|
|
59
|
+
};
|
|
60
|
+
this.disposeData = pty.onData(data => {
|
|
61
|
+
this.prompt.output(data);
|
|
62
|
+
if (ready) {
|
|
63
|
+
emit(data);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
startup += data;
|
|
67
|
+
const boundary = startup.indexOf(TERMINAL_PROMPT_MARKER);
|
|
68
|
+
if (boundary >= 0) {
|
|
69
|
+
ready = true;
|
|
70
|
+
emit(startup.slice(boundary));
|
|
71
|
+
startup = '';
|
|
72
|
+
clearTimeout(this.startupTimer);
|
|
73
|
+
}
|
|
74
|
+
else if (startup.length > STARTUP_BUFFER_CHARS) {
|
|
75
|
+
ready = true;
|
|
76
|
+
emit(startup);
|
|
77
|
+
startup = '';
|
|
78
|
+
clearTimeout(this.startupTimer);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (integrated) {
|
|
82
|
+
this.startupTimer = setTimeout(() => {
|
|
83
|
+
ready = true;
|
|
84
|
+
emit(startup);
|
|
85
|
+
startup = '';
|
|
86
|
+
}, STARTUP_TIMEOUT_MS);
|
|
87
|
+
this.startupTimer.unref();
|
|
88
|
+
// Preserve the user's prompt and add an invisible activity marker.
|
|
89
|
+
if (shellName !== undefined)
|
|
90
|
+
pty.write(`${promptSetup(shellName)}\r`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
get pid() {
|
|
94
|
+
return this.pty.pid;
|
|
95
|
+
}
|
|
96
|
+
write(data) {
|
|
97
|
+
this.prompt.input();
|
|
98
|
+
this.pty.write(data);
|
|
99
|
+
}
|
|
100
|
+
resize(cols, rows) {
|
|
101
|
+
this.pty.resize(cols, rows);
|
|
102
|
+
}
|
|
103
|
+
pause() {
|
|
104
|
+
this.pty.pause();
|
|
105
|
+
}
|
|
106
|
+
resume() {
|
|
107
|
+
this.pty.resume();
|
|
108
|
+
}
|
|
109
|
+
interrupt() {
|
|
110
|
+
this.write('\x03');
|
|
111
|
+
}
|
|
112
|
+
activity(rows) {
|
|
113
|
+
return shellActivity(this.pid, this.prompt, rows);
|
|
114
|
+
}
|
|
115
|
+
async kill() {
|
|
116
|
+
clearTimeout(this.startupTimer);
|
|
117
|
+
await terminateProcesses(this.pid);
|
|
118
|
+
this.disposeData.dispose();
|
|
119
|
+
this.listeners.clear();
|
|
120
|
+
}
|
|
121
|
+
onData(listener) {
|
|
122
|
+
this.listeners.add(listener);
|
|
123
|
+
return () => this.listeners.delete(listener);
|
|
124
|
+
}
|
|
125
|
+
onExit(listener) {
|
|
126
|
+
const disposable = this.pty.onExit(listener);
|
|
127
|
+
return () => disposable.dispose();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export class PipeBackend {
|
|
131
|
+
mode = 'pipe';
|
|
132
|
+
prompt = new PromptTracker();
|
|
133
|
+
child;
|
|
134
|
+
dataListeners = new Set();
|
|
135
|
+
exitListeners = new Set();
|
|
136
|
+
constructor(cwd) {
|
|
137
|
+
// Merge both output descriptors so prompts cannot overtake stdout.
|
|
138
|
+
this.child = spawn('/bin/sh', ['-c', 'exec /bin/sh -i 2>&1'], {
|
|
139
|
+
cwd,
|
|
140
|
+
detached: true,
|
|
141
|
+
env: {
|
|
142
|
+
...process.env,
|
|
143
|
+
TERM: 'dumb',
|
|
144
|
+
FORCE_COLOR: '1',
|
|
145
|
+
CLICOLOR_FORCE: '1',
|
|
146
|
+
PS1: TERMINAL_PROMPT_MARKER + '$ '
|
|
147
|
+
},
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
149
|
+
});
|
|
150
|
+
this.child.stdout.setEncoding('utf8');
|
|
151
|
+
this.child.stdout.on('data', (data) => {
|
|
152
|
+
this.prompt.output(data);
|
|
153
|
+
for (const listener of this.dataListeners)
|
|
154
|
+
listener(data.replace(/\n/g, '\r\n'));
|
|
155
|
+
});
|
|
156
|
+
this.child.on('error', () => {
|
|
157
|
+
for (const listener of this.exitListeners)
|
|
158
|
+
listener();
|
|
159
|
+
});
|
|
160
|
+
this.child.on('exit', () => {
|
|
161
|
+
for (const listener of this.exitListeners)
|
|
162
|
+
listener();
|
|
163
|
+
});
|
|
164
|
+
this.child.stdin.on('error', () => { });
|
|
165
|
+
}
|
|
166
|
+
get pid() {
|
|
167
|
+
return this.child.pid ?? 0;
|
|
168
|
+
}
|
|
169
|
+
write(data) {
|
|
170
|
+
this.prompt.input();
|
|
171
|
+
this.child.stdin.write(data);
|
|
172
|
+
}
|
|
173
|
+
resize() { }
|
|
174
|
+
pause() {
|
|
175
|
+
this.child.stdout.pause();
|
|
176
|
+
}
|
|
177
|
+
resume() {
|
|
178
|
+
this.child.stdout.resume();
|
|
179
|
+
}
|
|
180
|
+
interrupt() {
|
|
181
|
+
if (this.pid > 1)
|
|
182
|
+
signalProcess(-this.pid, 'SIGINT');
|
|
183
|
+
}
|
|
184
|
+
activity(rows) {
|
|
185
|
+
return shellActivity(this.pid, this.prompt, rows);
|
|
186
|
+
}
|
|
187
|
+
async kill() {
|
|
188
|
+
if (this.pid > 1)
|
|
189
|
+
await terminateProcesses(this.pid);
|
|
190
|
+
}
|
|
191
|
+
onData(listener) {
|
|
192
|
+
this.dataListeners.add(listener);
|
|
193
|
+
return () => this.dataListeners.delete(listener);
|
|
194
|
+
}
|
|
195
|
+
onExit(listener) {
|
|
196
|
+
this.exitListeners.add(listener);
|
|
197
|
+
return () => this.exitListeners.delete(listener);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export async function createBackend(cwd, cols, rows) {
|
|
201
|
+
if (!terminalSupported())
|
|
202
|
+
throw new TerminalError('The terminal is not available on Windows.');
|
|
203
|
+
const module = await loadPty();
|
|
204
|
+
if (!module)
|
|
205
|
+
return new PipeBackend(cwd);
|
|
206
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
207
|
+
const args = POSIX_SHELLS.includes(basename(shell)) ? ['-l', '-i'] : ['-i'];
|
|
208
|
+
const pty = module.spawn(shell, args, {
|
|
209
|
+
cwd,
|
|
210
|
+
cols,
|
|
211
|
+
rows,
|
|
212
|
+
name: 'xterm-256color',
|
|
213
|
+
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }
|
|
214
|
+
});
|
|
215
|
+
return new NodePtyBackend(pty, basename(shell));
|
|
216
|
+
}
|