@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import headless from '@xterm/headless';
|
|
2
|
+
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
3
|
+
import { TERMINAL_SCROLLBACK, TERMINAL_QUEUE_BYTES, TERMINAL_SNAPSHOT_BYTES, TERMINAL_FRAME_BYTES, TERMINAL_TITLE_LENGTH } from "../../../shared/terminal/constants.js";
|
|
4
|
+
import { EscapeTail } from "./escape-tail.js";
|
|
5
|
+
import { TerminalError } from "../../errors/terminal.js";
|
|
6
|
+
// Join output that arrives within one display frame (about 60 Hz) into one write.
|
|
7
|
+
const FLUSH_DELAY_MS = 16;
|
|
8
|
+
// One UTF-16 unit takes up to 3 UTF-8 bytes. A divisor of 4 keeps a part below the frame limit.
|
|
9
|
+
const FRAME_PART_UNITS = TERMINAL_FRAME_BYTES / 4;
|
|
10
|
+
// Close the session when the parser queue grows to twice the pause limit.
|
|
11
|
+
const QUEUE_CLOSE_BYTES = TERMINAL_QUEUE_BYTES * 2;
|
|
12
|
+
// Resume the backend only after the parser queue falls well below the pause limit.
|
|
13
|
+
const QUEUE_RESUME_BYTES = TERMINAL_QUEUE_BYTES / 4;
|
|
14
|
+
// A high surrogate starts a pair. A part must not end after it.
|
|
15
|
+
const HIGH_SURROGATE_FIRST = 0xd800;
|
|
16
|
+
const HIGH_SURROGATE_LAST = 0xdbff;
|
|
17
|
+
export class TerminalSession {
|
|
18
|
+
terminal;
|
|
19
|
+
serializer;
|
|
20
|
+
tail = new EscapeTail();
|
|
21
|
+
queue = Promise.resolve();
|
|
22
|
+
pendingBytes = 0;
|
|
23
|
+
staged = '';
|
|
24
|
+
flushTimer;
|
|
25
|
+
blocked = new Set();
|
|
26
|
+
sequence = 0;
|
|
27
|
+
stopped = false;
|
|
28
|
+
disposers = [];
|
|
29
|
+
title;
|
|
30
|
+
cols;
|
|
31
|
+
rows;
|
|
32
|
+
running = 'unknown';
|
|
33
|
+
id;
|
|
34
|
+
backend;
|
|
35
|
+
publish;
|
|
36
|
+
onExit;
|
|
37
|
+
constructor(id, title, backend, cols, rows, publish, onExit) {
|
|
38
|
+
this.id = id;
|
|
39
|
+
this.backend = backend;
|
|
40
|
+
this.publish = publish;
|
|
41
|
+
this.onExit = onExit;
|
|
42
|
+
this.title = title;
|
|
43
|
+
this.cols = cols;
|
|
44
|
+
this.rows = rows;
|
|
45
|
+
this.terminal = new headless.Terminal({
|
|
46
|
+
cols,
|
|
47
|
+
rows,
|
|
48
|
+
scrollback: TERMINAL_SCROLLBACK,
|
|
49
|
+
allowProposedApi: true
|
|
50
|
+
});
|
|
51
|
+
this.serializer = new SerializeAddon();
|
|
52
|
+
this.terminal.loadAddon(this.serializer);
|
|
53
|
+
this.disposers.push(backend.onData(data => this.output(data)), backend.onExit(onExit));
|
|
54
|
+
// Only the server answers terminal queries, even with multiple browsers.
|
|
55
|
+
this.disposers.push(() => this.terminal.dispose());
|
|
56
|
+
this.terminal.onData(data => {
|
|
57
|
+
if (!this.stopped && backend.mode === 'pty')
|
|
58
|
+
backend.write(data);
|
|
59
|
+
});
|
|
60
|
+
this.terminal.onTitleChange(shellTitle => {
|
|
61
|
+
this.title = shellTitle.slice(0, TERMINAL_TITLE_LENGTH);
|
|
62
|
+
publish({ type: 'title', terminalId: id, title: this.title });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
get info() {
|
|
66
|
+
return {
|
|
67
|
+
id: this.id,
|
|
68
|
+
title: this.title,
|
|
69
|
+
cols: this.cols,
|
|
70
|
+
rows: this.rows,
|
|
71
|
+
mode: this.backend.mode,
|
|
72
|
+
running: this.running
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
output(data) {
|
|
76
|
+
if (this.stopped)
|
|
77
|
+
return;
|
|
78
|
+
this.staged += data;
|
|
79
|
+
if (Buffer.byteLength(this.staged) >= TERMINAL_FRAME_BYTES) {
|
|
80
|
+
this.flush();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.flushTimer ??= setTimeout(() => this.flush(), FLUSH_DELAY_MS);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
flush() {
|
|
87
|
+
clearTimeout(this.flushTimer);
|
|
88
|
+
this.flushTimer = undefined;
|
|
89
|
+
const data = this.staged;
|
|
90
|
+
this.staged = '';
|
|
91
|
+
if (data)
|
|
92
|
+
this.enqueue(data);
|
|
93
|
+
}
|
|
94
|
+
enqueue(data) {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return undefined;
|
|
97
|
+
// Bound each frame by UTF-8 bytes without splitting a surrogate pair.
|
|
98
|
+
for (let offset = 0; offset < data.length;) {
|
|
99
|
+
let end = Math.min(data.length, offset + FRAME_PART_UNITS);
|
|
100
|
+
if (end < data.length &&
|
|
101
|
+
data.charCodeAt(end - 1) >= HIGH_SURROGATE_FIRST &&
|
|
102
|
+
data.charCodeAt(end - 1) <= HIGH_SURROGATE_LAST)
|
|
103
|
+
end--;
|
|
104
|
+
const part = data.slice(offset, end);
|
|
105
|
+
offset = end;
|
|
106
|
+
const bytes = Buffer.byteLength(part);
|
|
107
|
+
this.pendingBytes += bytes;
|
|
108
|
+
if (this.pendingBytes > TERMINAL_QUEUE_BYTES)
|
|
109
|
+
this.updateFlow();
|
|
110
|
+
if (this.pendingBytes > QUEUE_CLOSE_BYTES) {
|
|
111
|
+
this.onExit();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.queue = this.queue
|
|
115
|
+
.then(async () => {
|
|
116
|
+
if (this.stopped)
|
|
117
|
+
return undefined;
|
|
118
|
+
this.tail.push(part);
|
|
119
|
+
await new Promise(resolve => this.terminal.write(part, resolve));
|
|
120
|
+
this.sequence++;
|
|
121
|
+
this.publish({
|
|
122
|
+
type: 'output',
|
|
123
|
+
terminalId: this.id,
|
|
124
|
+
data: part,
|
|
125
|
+
cols: this.cols,
|
|
126
|
+
rows: this.rows,
|
|
127
|
+
sequence: this.sequence
|
|
128
|
+
});
|
|
129
|
+
return undefined;
|
|
130
|
+
})
|
|
131
|
+
.catch(() => this.onExit())
|
|
132
|
+
.finally(() => {
|
|
133
|
+
this.pendingBytes -= bytes;
|
|
134
|
+
if (!this.stopped && this.pendingBytes < QUEUE_RESUME_BYTES)
|
|
135
|
+
this.updateFlow();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
block(owner, blocked) {
|
|
140
|
+
if (blocked) {
|
|
141
|
+
this.blocked.add(owner);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.blocked.delete(owner);
|
|
145
|
+
}
|
|
146
|
+
this.updateFlow();
|
|
147
|
+
}
|
|
148
|
+
updateFlow() {
|
|
149
|
+
if (this.stopped)
|
|
150
|
+
return;
|
|
151
|
+
if (this.blocked.size || this.pendingBytes > TERMINAL_QUEUE_BYTES) {
|
|
152
|
+
this.backend.pause();
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.backend.resume();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
snapshot(send) {
|
|
159
|
+
this.flush();
|
|
160
|
+
this.queue = this.queue.then(() => {
|
|
161
|
+
if (this.stopped)
|
|
162
|
+
return undefined;
|
|
163
|
+
let scrollback = TERMINAL_SCROLLBACK;
|
|
164
|
+
let data = this.serializer.serialize({ scrollback }) + this.tail.tail;
|
|
165
|
+
while (Buffer.byteLength(data) > TERMINAL_SNAPSHOT_BYTES &&
|
|
166
|
+
scrollback > 0) {
|
|
167
|
+
scrollback = Math.floor(scrollback / 2);
|
|
168
|
+
data = this.serializer.serialize({ scrollback }) + this.tail.tail;
|
|
169
|
+
}
|
|
170
|
+
if (Buffer.byteLength(data) > TERMINAL_SNAPSHOT_BYTES)
|
|
171
|
+
throw new TerminalError('Terminal snapshot exceeds its limit.');
|
|
172
|
+
send({
|
|
173
|
+
type: 'snapshot',
|
|
174
|
+
terminalId: this.id,
|
|
175
|
+
data,
|
|
176
|
+
cols: this.cols,
|
|
177
|
+
rows: this.rows,
|
|
178
|
+
sequence: this.sequence
|
|
179
|
+
});
|
|
180
|
+
return undefined;
|
|
181
|
+
});
|
|
182
|
+
return this.queue;
|
|
183
|
+
}
|
|
184
|
+
write(data) {
|
|
185
|
+
if (!this.stopped)
|
|
186
|
+
this.backend.write(data);
|
|
187
|
+
}
|
|
188
|
+
line(data) {
|
|
189
|
+
if (this.backend.mode !== 'pipe')
|
|
190
|
+
throw new TerminalError('Line input requires limited mode.');
|
|
191
|
+
this.output(data.replace(/\n/g, '\r\n') + '\r\n');
|
|
192
|
+
this.backend.write(data + '\n');
|
|
193
|
+
}
|
|
194
|
+
signal(signal) {
|
|
195
|
+
if (signal === 'interrupt') {
|
|
196
|
+
this.backend.interrupt();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this.onExit();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
resize(cols, rows) {
|
|
203
|
+
this.flush();
|
|
204
|
+
this.queue = this.queue
|
|
205
|
+
.then(() => {
|
|
206
|
+
if (this.stopped || (cols === this.cols && rows === this.rows))
|
|
207
|
+
return undefined;
|
|
208
|
+
this.backend.resize(cols, rows);
|
|
209
|
+
this.terminal.resize(cols, rows);
|
|
210
|
+
this.cols = cols;
|
|
211
|
+
this.rows = rows;
|
|
212
|
+
// Order geometry changes with output so every viewer parses the same screen.
|
|
213
|
+
this.sequence++;
|
|
214
|
+
this.publish({
|
|
215
|
+
type: 'output',
|
|
216
|
+
terminalId: this.id,
|
|
217
|
+
data: '',
|
|
218
|
+
cols,
|
|
219
|
+
rows,
|
|
220
|
+
sequence: this.sequence
|
|
221
|
+
});
|
|
222
|
+
return undefined;
|
|
223
|
+
})
|
|
224
|
+
.catch(() => this.onExit());
|
|
225
|
+
return this.queue;
|
|
226
|
+
}
|
|
227
|
+
async dispose() {
|
|
228
|
+
if (this.stopped)
|
|
229
|
+
return undefined;
|
|
230
|
+
this.stopped = true;
|
|
231
|
+
clearTimeout(this.flushTimer);
|
|
232
|
+
this.staged = '';
|
|
233
|
+
await this.backend.kill();
|
|
234
|
+
await this.queue.catch(() => { });
|
|
235
|
+
for (const dispose of this.disposers)
|
|
236
|
+
dispose();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { TERMINAL_SNAPSHOT_BYTES } from "../../../shared/terminal/constants.js";
|
|
2
|
+
import { TerminalChannel } from "./channel.js";
|
|
3
|
+
// Detect a dead peer: close the socket after one missed pong.
|
|
4
|
+
const HEARTBEAT_MS = 30_000;
|
|
5
|
+
function rawDataToText(data) {
|
|
6
|
+
if (Buffer.isBuffer(data))
|
|
7
|
+
return data.toString('utf8');
|
|
8
|
+
if (data instanceof ArrayBuffer)
|
|
9
|
+
return Buffer.from(data).toString('utf8');
|
|
10
|
+
return Buffer.concat(data).toString('utf8');
|
|
11
|
+
}
|
|
12
|
+
export function attachTerminalSocket(socket, registry, options = {}) {
|
|
13
|
+
let alive = true;
|
|
14
|
+
// oxlint-disable-next-line prefer-const -- close captures the channel while its constructor sends the first message
|
|
15
|
+
let channel;
|
|
16
|
+
const close = () => {
|
|
17
|
+
channel?.dispose();
|
|
18
|
+
socket.terminate();
|
|
19
|
+
};
|
|
20
|
+
const send = (message) => {
|
|
21
|
+
if (socket.readyState !== socket.OPEN)
|
|
22
|
+
return;
|
|
23
|
+
// A slow client must not make the server buffer without limit.
|
|
24
|
+
if (socket.bufferedAmount > TERMINAL_SNAPSHOT_BYTES) {
|
|
25
|
+
close();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
socket.send(JSON.stringify(message), error => {
|
|
29
|
+
if (error)
|
|
30
|
+
close();
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
channel = new TerminalChannel(registry, send, close);
|
|
34
|
+
socket.on('message', (data, binary) => {
|
|
35
|
+
// The protocol uses text frames only.
|
|
36
|
+
if (binary) {
|
|
37
|
+
close();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
// The channel checks the shape of the message.
|
|
42
|
+
channel?.receive(JSON.parse(rawDataToText(data)));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
socket.on('pong', () => {
|
|
49
|
+
alive = true;
|
|
50
|
+
});
|
|
51
|
+
const heartbeat = setInterval(() => {
|
|
52
|
+
if (!alive) {
|
|
53
|
+
close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// The next pong must arrive before the next tick.
|
|
57
|
+
alive = false;
|
|
58
|
+
socket.ping();
|
|
59
|
+
}, options.heartbeatMs ?? HEARTBEAT_MS);
|
|
60
|
+
heartbeat.unref();
|
|
61
|
+
let ended = false;
|
|
62
|
+
// Both `close` and `error` events call this function, so it must run only once.
|
|
63
|
+
const cleanup = () => {
|
|
64
|
+
if (ended)
|
|
65
|
+
return;
|
|
66
|
+
ended = true;
|
|
67
|
+
clearInterval(heartbeat);
|
|
68
|
+
channel?.dispose();
|
|
69
|
+
options.onDisconnect?.();
|
|
70
|
+
};
|
|
71
|
+
socket.on('close', cleanup);
|
|
72
|
+
socket.on('error', cleanup);
|
|
73
|
+
return { close };
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { TERMINAL_TOKEN_MS } from "../../../shared/terminal/constants.js";
|
|
3
|
+
// Bound token memory when a page repeatedly requests credentials.
|
|
4
|
+
const MAX_TOKENS = 256;
|
|
5
|
+
export class TerminalTokens {
|
|
6
|
+
tokens = new Map();
|
|
7
|
+
now;
|
|
8
|
+
constructor(now = Date.now) {
|
|
9
|
+
this.now = now;
|
|
10
|
+
}
|
|
11
|
+
issue(origin) {
|
|
12
|
+
this.prune();
|
|
13
|
+
if (this.tokens.size >= MAX_TOKENS)
|
|
14
|
+
this.tokens.delete(this.tokens.keys().next().value ?? '');
|
|
15
|
+
const token = randomBytes(32).toString('base64url');
|
|
16
|
+
this.tokens.set(token, { origin, expires: this.now() + TERMINAL_TOKEN_MS });
|
|
17
|
+
return token;
|
|
18
|
+
}
|
|
19
|
+
consume(token, origin) {
|
|
20
|
+
const info = this.tokens.get(token);
|
|
21
|
+
this.tokens.delete(token);
|
|
22
|
+
return !!info && info.expires > this.now() && info.origin === origin;
|
|
23
|
+
}
|
|
24
|
+
prune() {
|
|
25
|
+
for (const [token, info] of this.tokens)
|
|
26
|
+
if (info.expires <= this.now())
|
|
27
|
+
this.tokens.delete(token);
|
|
28
|
+
}
|
|
29
|
+
clear() {
|
|
30
|
+
this.tokens.clear();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { TerminalCapabilitySchema } from "../terminal/schemas.js";
|
|
1
2
|
import { Type } from '@sinclair/typebox';
|
|
2
3
|
export const AppInfoSchema = Type.Object({
|
|
3
|
-
version: Type.String({
|
|
4
|
+
version: Type.String({
|
|
5
|
+
description: 'githuman version from package.json.'
|
|
6
|
+
}),
|
|
7
|
+
terminal: TerminalCapabilitySchema
|
|
4
8
|
}, { description: 'Facts about the running server.' });
|
|
5
9
|
export const HealthSchema = Type.Object({
|
|
6
10
|
status: Type.Literal('ok', { description: 'Always "ok".' })
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The farthest line, above a hunk, that its preamble may reach (ADR 0036). The reach is a
|
|
2
|
+
// limit, not the size: the preamble starts at a safe line within it. A reach of 60 gave the
|
|
3
|
+
// fewest wrong lines in a test on the history of this repository.
|
|
4
|
+
export const PREAMBLE_REACH = 60;
|
|
5
|
+
// A budget for one preamble, in characters. A minified file has lines of many kilobytes.
|
|
6
|
+
const PREAMBLE_MAX_CHARS = 4000;
|
|
7
|
+
// A line that closes a tag. At the start of a preamble it has no opening tag, and a markup
|
|
8
|
+
// grammar then loses its state.
|
|
9
|
+
const CLOSER = /^\s*(<\/|\/>|>)/;
|
|
10
|
+
// A top-level block of a Vue file: the tag stands alone on its line, at column 0.
|
|
11
|
+
const VUE_BLOCK_OPEN = /^<(template|script|style)\b[^>]*>\s*$/;
|
|
12
|
+
/**
|
|
13
|
+
* For each line index, the opening tag of the Vue block that is open before that line.
|
|
14
|
+
* The list has one more entry than there are lines, so the end of the file has an entry too.
|
|
15
|
+
*/
|
|
16
|
+
function vueOpenTags(lines) {
|
|
17
|
+
const tags = [undefined];
|
|
18
|
+
let open;
|
|
19
|
+
for (const text of lines) {
|
|
20
|
+
const match = VUE_BLOCK_OPEN.exec(text);
|
|
21
|
+
if (match) {
|
|
22
|
+
open = { tag: match[1], text };
|
|
23
|
+
}
|
|
24
|
+
else if (open && text.startsWith(`</${open.tag}>`)) {
|
|
25
|
+
open = undefined;
|
|
26
|
+
}
|
|
27
|
+
tags.push(open?.text);
|
|
28
|
+
}
|
|
29
|
+
return tags;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Makes a reader of preambles for one side of a file. Use one reader for all hunks of the
|
|
33
|
+
* file: it scans the Vue blocks once, and only when a hunk needs them.
|
|
34
|
+
*
|
|
35
|
+
* The preamble starts at the farthest safe line within {@link PREAMBLE_REACH} lines above
|
|
36
|
+
* the hunk. A line is safe when it is indented no deeper than every line after it, up to the
|
|
37
|
+
* hunk, and does not close a tag. Then the preamble holds no line that closes a block
|
|
38
|
+
* opened above it, and the grammar keeps a sound state. In a Vue file the opening tag of the
|
|
39
|
+
* enclosing block comes first, so the parser knows the language. `sideLines` are all lines
|
|
40
|
+
* of the file on the side of the hunk.
|
|
41
|
+
*/
|
|
42
|
+
export function createPreambleReader(path, sideLines) {
|
|
43
|
+
const isVue = path.toLowerCase().endsWith('.vue');
|
|
44
|
+
let openTags;
|
|
45
|
+
return firstLine => {
|
|
46
|
+
const end = Math.min(firstLine - 1, sideLines.length);
|
|
47
|
+
if (end <= 0)
|
|
48
|
+
return [];
|
|
49
|
+
let start = end;
|
|
50
|
+
let chars = 0;
|
|
51
|
+
let shallowest = Infinity;
|
|
52
|
+
for (let i = end - 1; i >= Math.max(0, end - PREAMBLE_REACH); i--) {
|
|
53
|
+
const line = sideLines[i];
|
|
54
|
+
chars += line.length;
|
|
55
|
+
if (chars > PREAMBLE_MAX_CHARS)
|
|
56
|
+
break;
|
|
57
|
+
if (line.trim() === '')
|
|
58
|
+
continue;
|
|
59
|
+
const indent = line.length - line.trimStart().length;
|
|
60
|
+
if (indent <= shallowest && !CLOSER.test(line))
|
|
61
|
+
start = i;
|
|
62
|
+
shallowest = Math.min(shallowest, indent);
|
|
63
|
+
}
|
|
64
|
+
const lines = sideLines.slice(start, end);
|
|
65
|
+
if (isVue) {
|
|
66
|
+
openTags ??= vueOpenTags(sideLines);
|
|
67
|
+
const tag = openTags[start];
|
|
68
|
+
if (tag !== undefined)
|
|
69
|
+
lines.unshift(tag);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** The preamble of one hunk. For many hunks of one file, use {@link createPreambleReader}. */
|
|
75
|
+
export function hunkPreamble(path, sideLines, firstLine) {
|
|
76
|
+
return createPreambleReader(path, sideLines)(firstLine);
|
|
77
|
+
}
|
|
@@ -27,7 +27,10 @@ export const DiffHunkSchema = Type.Object({
|
|
|
27
27
|
newLines: Type.Integer({ description: 'Line count in the new file.' }),
|
|
28
28
|
lines: Type.Array(DiffLineSchema, {
|
|
29
29
|
description: 'Lines of the hunk in file order.'
|
|
30
|
-
})
|
|
30
|
+
}),
|
|
31
|
+
preamble: Type.Optional(Type.Array(Type.String(), {
|
|
32
|
+
description: 'Unchanged lines above the hunk, for syntax highlighting only. Bounded in count and size. In a Vue file the opening tag of the enclosing block comes first. Absent when there are none.'
|
|
33
|
+
}))
|
|
31
34
|
}, { description: 'One hunk of a file diff.' });
|
|
32
35
|
const diffFileFields = {
|
|
33
36
|
oldPath: Type.String({
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Bound process and emulator memory for one repository.
|
|
2
|
+
export const MAX_TERMINAL_SESSIONS = 8;
|
|
3
|
+
// Keep useful history without retaining unlimited command output.
|
|
4
|
+
export const TERMINAL_SCROLLBACK = 5000;
|
|
5
|
+
// Confirmed idle sessions need less orphan time than running jobs.
|
|
6
|
+
export const TERMINAL_IDLE_MS = 15 * 60 * 1000;
|
|
7
|
+
// Output must not extend the lifetime of an orphaned command.
|
|
8
|
+
export const TERMINAL_ORPHAN_MS = 60 * 60 * 1000;
|
|
9
|
+
// Tokens are used only for an immediate connection attempt.
|
|
10
|
+
export const TERMINAL_TOKEN_MS = 30_000;
|
|
11
|
+
// Bound the shell title that the server stores and publishes.
|
|
12
|
+
export const TERMINAL_TITLE_LENGTH = 256;
|
|
13
|
+
// A smaller screen cannot show a shell prompt.
|
|
14
|
+
export const TERMINAL_MIN_COLS = 2;
|
|
15
|
+
export const TERMINAL_MIN_ROWS = 2;
|
|
16
|
+
// Small screens and bounded snapshots need bounded cell dimensions.
|
|
17
|
+
export const TERMINAL_MAX_COLS = 300;
|
|
18
|
+
// Bound the visible buffer as well as scrollback.
|
|
19
|
+
export const TERMINAL_MAX_ROWS = 100;
|
|
20
|
+
// Keep individual input frames and output batches small.
|
|
21
|
+
export const TERMINAL_FRAME_BYTES = 64 * 1024;
|
|
22
|
+
// Pause the backend before parser queues grow without limit.
|
|
23
|
+
export const TERMINAL_QUEUE_BYTES = 1024 * 1024;
|
|
24
|
+
// Credit bounds each client's unacknowledged output.
|
|
25
|
+
export const TERMINAL_CLIENT_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
// Snapshot frames may contain a bounded screen and history.
|
|
27
|
+
export const TERMINAL_SNAPSHOT_BYTES = 16 * 1024 * 1024;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Nullable } from "../utils/schemas.js";
|
|
3
|
+
import { TERMINAL_MAX_COLS, TERMINAL_MIN_COLS, TERMINAL_MIN_ROWS, TERMINAL_MAX_ROWS, TERMINAL_FRAME_BYTES, TERMINAL_TITLE_LENGTH, MAX_TERMINAL_SESSIONS } from "./constants.js";
|
|
4
|
+
// Every command is validated before it reaches a shell.
|
|
5
|
+
const id = Type.String({
|
|
6
|
+
minLength: 1,
|
|
7
|
+
maxLength: 64,
|
|
8
|
+
description: 'Terminal session identifier.'
|
|
9
|
+
});
|
|
10
|
+
const requestId = Type.String({
|
|
11
|
+
minLength: 1,
|
|
12
|
+
maxLength: 64,
|
|
13
|
+
description: 'Client operation identifier.'
|
|
14
|
+
});
|
|
15
|
+
const cols = Type.Integer({
|
|
16
|
+
minimum: TERMINAL_MIN_COLS,
|
|
17
|
+
maximum: TERMINAL_MAX_COLS,
|
|
18
|
+
description: 'Screen columns.'
|
|
19
|
+
});
|
|
20
|
+
const rows = Type.Integer({
|
|
21
|
+
minimum: TERMINAL_MIN_ROWS,
|
|
22
|
+
maximum: TERMINAL_MAX_ROWS,
|
|
23
|
+
description: 'Screen rows.'
|
|
24
|
+
});
|
|
25
|
+
const data = Type.String({
|
|
26
|
+
maxLength: TERMINAL_FRAME_BYTES,
|
|
27
|
+
description: 'Terminal data.'
|
|
28
|
+
});
|
|
29
|
+
function message(type, fields) {
|
|
30
|
+
return Type.Object({
|
|
31
|
+
type: Type.Literal(type, { description: 'Message discriminator.' }),
|
|
32
|
+
...fields
|
|
33
|
+
}, { additionalProperties: false, description: `${type} terminal message.` });
|
|
34
|
+
}
|
|
35
|
+
export const TerminalModeSchema = Type.Union([Type.Literal('pty'), Type.Literal('pipe')], { description: 'Backend mode.' });
|
|
36
|
+
export const TerminalRunningSchema = Type.Union([Type.Literal('idle'), Type.Literal('running'), Type.Literal('unknown')], { description: 'Confirmed process activity or unknown.' });
|
|
37
|
+
export const TerminalInfoSchema = Type.Object({
|
|
38
|
+
id,
|
|
39
|
+
title: Type.String({
|
|
40
|
+
maxLength: TERMINAL_TITLE_LENGTH,
|
|
41
|
+
description: 'Shell title or fallback label.'
|
|
42
|
+
}),
|
|
43
|
+
cols,
|
|
44
|
+
rows,
|
|
45
|
+
mode: TerminalModeSchema,
|
|
46
|
+
running: TerminalRunningSchema
|
|
47
|
+
}, { description: 'Live terminal session.' });
|
|
48
|
+
export const TerminalCapabilitySchema = Type.Object({
|
|
49
|
+
available: Type.Boolean({
|
|
50
|
+
description: 'Terminal access is enabled on this bind.'
|
|
51
|
+
}),
|
|
52
|
+
mode: Nullable(TerminalModeSchema)
|
|
53
|
+
}, { description: 'Server terminal capability.' });
|
|
54
|
+
export const TerminalTokenSchema = Type.Object({ token: Type.String({ description: 'One-use connection token.' }) }, { description: 'WebSocket connection credential.' });
|
|
55
|
+
export const TerminalClientMessageSchema = Type.Union([
|
|
56
|
+
message('create', { requestId, cols, rows }),
|
|
57
|
+
message('subscribe', { terminalId: id }),
|
|
58
|
+
message('snapshot', { terminalId: id }),
|
|
59
|
+
message('input', { terminalId: id, data }),
|
|
60
|
+
message('line', { terminalId: id, data }),
|
|
61
|
+
message('signal', {
|
|
62
|
+
terminalId: id,
|
|
63
|
+
signal: Type.Union([Type.Literal('interrupt'), Type.Literal('eof')], {
|
|
64
|
+
description: 'Limited-mode control action.'
|
|
65
|
+
})
|
|
66
|
+
}),
|
|
67
|
+
message('resize', { terminalId: id, cols, rows }),
|
|
68
|
+
message('close', { terminalId: id, requestId }),
|
|
69
|
+
message('ack', {
|
|
70
|
+
terminalId: id,
|
|
71
|
+
sequence: Type.Integer({
|
|
72
|
+
minimum: 0,
|
|
73
|
+
description: 'Last processed output sequence.'
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
message('ping', {})
|
|
77
|
+
], { description: 'Validated client terminal protocol.' });
|
|
78
|
+
export const TerminalServerMessageSchema = Type.Union([
|
|
79
|
+
message('list', {
|
|
80
|
+
sessions: Type.Array(TerminalInfoSchema, {
|
|
81
|
+
maxItems: MAX_TERMINAL_SESSIONS,
|
|
82
|
+
description: 'Current server sessions.'
|
|
83
|
+
})
|
|
84
|
+
}),
|
|
85
|
+
message('created', { requestId, terminalId: id }),
|
|
86
|
+
message('snapshot', {
|
|
87
|
+
terminalId: id,
|
|
88
|
+
data: Type.String({ description: 'Serialized screen and parser tail.' }),
|
|
89
|
+
cols,
|
|
90
|
+
rows,
|
|
91
|
+
sequence: Type.Integer({
|
|
92
|
+
minimum: 0,
|
|
93
|
+
description: 'Snapshot output boundary.'
|
|
94
|
+
})
|
|
95
|
+
}),
|
|
96
|
+
message('output', {
|
|
97
|
+
terminalId: id,
|
|
98
|
+
data,
|
|
99
|
+
cols,
|
|
100
|
+
rows,
|
|
101
|
+
sequence: Type.Integer({ minimum: 1, description: 'Output sequence.' })
|
|
102
|
+
}),
|
|
103
|
+
message('title', {
|
|
104
|
+
terminalId: id,
|
|
105
|
+
title: Type.String({ description: 'Updated shell title.' })
|
|
106
|
+
}),
|
|
107
|
+
message('exit', { terminalId: id }),
|
|
108
|
+
message('error', {
|
|
109
|
+
requestId: Type.Optional(requestId),
|
|
110
|
+
code: Type.String({ description: 'Domain error code.' }),
|
|
111
|
+
message: Type.String({ description: 'Operation failure.' })
|
|
112
|
+
}),
|
|
113
|
+
message('pong', {})
|
|
114
|
+
], { description: 'Server terminal protocol.' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|