@tequila99/githuman 0.4.2 → 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.
Files changed (173) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/index.js +27 -2
  3. package/dist/cli/server-runtime.js +1 -0
  4. package/dist/server/app.js +22 -1
  5. package/dist/server/errors/terminal.js +4 -0
  6. package/dist/server/hooks/terminal/local-origin.js +25 -0
  7. package/dist/server/routes/app-info.js +5 -2
  8. package/dist/server/routes/terminal.js +63 -0
  9. package/dist/server/services/terminal/backend.js +216 -0
  10. package/dist/server/services/terminal/channel.js +200 -0
  11. package/dist/server/services/terminal/escape-tail.js +93 -0
  12. package/dist/server/services/terminal/processes.js +115 -0
  13. package/dist/server/services/terminal/prompt-tracker.js +15 -0
  14. package/dist/server/services/terminal/registry.js +167 -0
  15. package/dist/server/services/terminal/session.js +238 -0
  16. package/dist/server/services/terminal/socket.js +74 -0
  17. package/dist/server/services/terminal/tokens.js +32 -0
  18. package/dist/shared/app/schemas.js +5 -1
  19. package/dist/shared/terminal/constants.js +27 -0
  20. package/dist/shared/terminal/schemas.js +114 -0
  21. package/dist/shared/terminal/types.js +1 -0
  22. package/dist/web/assets/{AppPage-BYrCfRnC.js → AppPage-D_OHhrKS.js} +2 -2
  23. package/dist/web/assets/CreateReviewFabButton-B737aIvn.js +1 -0
  24. package/dist/web/assets/ErrorNotFound-jc5_L_-4.js +1 -0
  25. package/dist/web/assets/IndexPage-DzlFTw_b.js +1 -0
  26. package/dist/web/assets/IndexPage-u_qqIPRL.css +1 -0
  27. package/dist/web/assets/MainLayout-DaSyo22r.js +8 -0
  28. package/dist/web/assets/MainLayout-DoniW026.css +1 -0
  29. package/dist/web/assets/QBadge-BY-jTQ8w.js +1 -0
  30. package/dist/web/assets/QBtn-Bgm40omM.js +1 -0
  31. package/dist/web/assets/QSelect-713AWDd_.js +1 -0
  32. package/dist/web/assets/QSpace-BLu5AyZv.css +1 -0
  33. package/dist/web/assets/QSpace-BVBU-0oC.js +17 -0
  34. package/dist/web/assets/ReviewDetailPage-CLM6aU4u.js +1 -0
  35. package/dist/web/assets/ReviewsPage-bllnaoOS.js +1 -0
  36. package/dist/web/assets/StatusSwitcher-BZ4FYVnY.js +2 -0
  37. package/dist/web/assets/{StatusSwitcher-DLU1h0rK.css → StatusSwitcher-BxRuEHHQ.css} +1 -1
  38. package/dist/web/assets/TerminalView-B11UHZv3.css +1 -0
  39. package/dist/web/assets/TerminalView-Ci9CNTyY.js +19 -0
  40. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  41. package/dist/web/assets/{abnfDiagram-O67JEVCF-dyvffE6R.js → abnfDiagram-O67JEVCF-4qpB0CGh.js} +1 -1
  42. package/dist/web/assets/agent-store-DIma0R4U.js +1 -0
  43. package/dist/web/assets/app-info-store-BzsJOxVc.js +1 -0
  44. package/dist/web/assets/architecture-7GRP2DOG-DG6nf_hE.js +1 -0
  45. package/dist/web/assets/{architectureDiagram-NJMV4G6O-Dk-QthCI.js → architectureDiagram-NJMV4G6O-CEL-_i5O.js} +1 -1
  46. package/dist/web/assets/{blockDiagram-BEXU5L5S-sF48dzNS.js → blockDiagram-BEXU5L5S-bMrUf1GU.js} +1 -1
  47. package/dist/web/assets/{c4Diagram-YGBWAQC7-BWrux-0F.js → c4Diagram-YGBWAQC7-C54mp6dl.js} +1 -1
  48. package/dist/web/assets/channel-CS4rbjhf.js +1 -0
  49. package/dist/web/assets/chunk-3FUC2YCW-BtIughAT.js +2 -0
  50. package/dist/web/assets/{chunk-5DYCD2WN-CpgXf7LJ.js → chunk-5DYCD2WN-BhyMEDuJ.js} +1 -1
  51. package/dist/web/assets/{chunk-742MDFTN-BGzG-dZE.js → chunk-742MDFTN-8MczzxE2.js} +1 -1
  52. package/dist/web/assets/{chunk-7INBJB4K-DJGc-gqS.js → chunk-7INBJB4K-BdS9oIIG.js} +1 -1
  53. package/dist/web/assets/{chunk-7M6MHVWA-CYIOaZel.js → chunk-7M6MHVWA-BU5_UwaZ.js} +1 -1
  54. package/dist/web/assets/{chunk-7PRAP22T-CJZNYL51.js → chunk-7PRAP22T-BaAw-gEj.js} +1 -1
  55. package/dist/web/assets/{chunk-GTNCS2PH-DOYGHmMk.js → chunk-GTNCS2PH-DNpH8E_x.js} +1 -1
  56. package/dist/web/assets/{chunk-GWA4HPMP-RIikrSw1.js → chunk-GWA4HPMP-BvG5tXy2.js} +1 -1
  57. package/dist/web/assets/{chunk-MBY4JIJT-ZSssyEJi.js → chunk-MBY4JIJT-B8XJMHG2.js} +1 -1
  58. package/dist/web/assets/{chunk-NETBCI7D-3tnHEq90.js → chunk-NETBCI7D-ClCXW1dx.js} +1 -1
  59. package/dist/web/assets/{chunk-O7XYJQB3-CDFTy-aQ.js → chunk-O7XYJQB3-BNcB0oiZ.js} +1 -1
  60. package/dist/web/assets/{chunk-UA2S7LBM-B8rY8Yj-.js → chunk-UA2S7LBM-CS5CtIsj.js} +1 -1
  61. package/dist/web/assets/{chunk-WEXAMYUT-BdPY0e7q.js → chunk-WEXAMYUT-CJaZJWKF.js} +1 -1
  62. package/dist/web/assets/{chunk-Z7XXMR3K-Br_E149D.js → chunk-Z7XXMR3K-DP2k1Ci5.js} +1 -1
  63. package/dist/web/assets/{chunk-ZIGJFQKS-DDjPFexL.js → chunk-ZIGJFQKS-y4KHfdEv.js} +1 -1
  64. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-D9kneDxy.js → classDiagram-v2-NBCMYWYE-C-kWcloV.js} +1 -1
  65. package/dist/web/assets/create-CSUVwdx4.js +1 -0
  66. package/dist/web/assets/{cynefin-OW5HDTMX-Dot1PBqs.js → cynefin-OW5HDTMX-C3AwH_vU.js} +1 -1
  67. package/dist/web/assets/{cynefinDiagram-VND7K2PF-6uYi0u-j.js → cynefinDiagram-VND7K2PF-DYF_CoB8.js} +1 -1
  68. package/dist/web/assets/{dagre-6A5THRUB-BDM5YkfJ.js → dagre-6A5THRUB-DWCEmsd1.js} +1 -1
  69. package/dist/web/assets/debounce-rZaFFwsz.js +1 -0
  70. package/dist/web/assets/{diagram-22UHCM2B-DxE0D8WB.js → diagram-22UHCM2B-oBFhSpao.js} +1 -1
  71. package/dist/web/assets/{diagram-3UASUU5V-CuD1fcFQ.js → diagram-3UASUU5V-Cj9rB07y.js} +1 -1
  72. package/dist/web/assets/{diagram-ATOU4E4O-DX6enGgq.js → diagram-ATOU4E4O-D4H9BqUQ.js} +1 -1
  73. package/dist/web/assets/{diagram-CDSNMT55-eLqOiVoC.js → diagram-CDSNMT55-qLgJBiIs.js} +1 -1
  74. package/dist/web/assets/{diagram-MLGK6HIB-DtIMj5y1.js → diagram-MLGK6HIB-Bhhvtx0A.js} +1 -1
  75. package/dist/web/assets/{diagram-MPIPVDR6-lYcwsjHR.js → diagram-MPIPVDR6-O4wokd54.js} +1 -1
  76. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-CGmBWaKx.js → ebnfDiagram-ZINNZB2B-BRU7SszO.js} +1 -1
  77. package/dist/web/assets/{elk-276RUBZZ-B1ntUD9z.js → elk-276RUBZZ-r0X8f7Sv.js} +1 -1
  78. package/dist/web/assets/{erDiagram-OPXOYQCR-Dqzoldin.js → erDiagram-OPXOYQCR-DbgjsGkX.js} +1 -1
  79. package/dist/web/assets/eventmodeling-NTZA5JFV-1Q8-FsiO.js +1 -0
  80. package/dist/web/assets/flowDiagram-KWPJA3E3-BbwsRPsv.js +1 -0
  81. package/dist/web/assets/{ganttDiagram-FUAMR5RP-CF6kJAoY.js → ganttDiagram-FUAMR5RP-BwhUglGo.js} +1 -1
  82. package/dist/web/assets/{gitGraph-4MIJSDKK-CUzcGxMa.js → gitGraph-4MIJSDKK-DA11BsSo.js} +1 -1
  83. package/dist/web/assets/{gitGraphDiagram-X574FWY7-D-XNa6f2.js → gitGraphDiagram-X574FWY7-BdG7d-11.js} +1 -1
  84. package/dist/web/assets/i18n-D1K2AGeW.js +1 -0
  85. package/dist/web/assets/index-BatRsx0U.css +1 -0
  86. package/dist/web/assets/index-DuCIKkvf.js +2 -0
  87. package/dist/web/assets/{info-A6RAGUB7-CH4SNmU8.js → info-A6RAGUB7-BxzfvGqu.js} +1 -1
  88. package/dist/web/assets/{infoDiagram-VRGFBTTK-35AKnD7M.js → infoDiagram-VRGFBTTK-veq817ut.js} +1 -1
  89. package/dist/web/assets/init-app-DKRVN8HV.js +1 -0
  90. package/dist/web/assets/{is-BB7rJzdU.js → is-EbmiuVzv.js} +1 -1
  91. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-D41YBWSb.js → ishikawaDiagram-OU5B5YK6-DMuzkArR.js} +1 -1
  92. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CZF6Wwk8.js → journeyDiagram-ZHPQQLJL-Cqj0kbKe.js} +1 -1
  93. package/dist/web/assets/{kanban-definition-PNTS6WVX-CVGaUle0.js → kanban-definition-PNTS6WVX-DUvral0v.js} +1 -1
  94. package/dist/web/assets/{line-CeH4KBRO.js → line-DcLaUOyJ.js} +1 -1
  95. package/dist/web/assets/{mermaid-parser.core-YrU9LTTJ.js → mermaid-parser.core-CY7nVQ2O.js} +3 -3
  96. package/dist/web/assets/{mermaid.core-BkLEn5Vh.js → mermaid.core-Cz083qmW.js} +4 -4
  97. package/dist/web/assets/{mindmap-definition-NLK3R4M7-D22lxuXn.js → mindmap-definition-NLK3R4M7--Twjhxcv.js} +1 -1
  98. package/dist/web/assets/{packet-AYTQ26CC-DMK0BdeB.js → packet-AYTQ26CC-Csf3Pf50.js} +1 -1
  99. package/dist/web/assets/{pegDiagram-GJSIUBJH-B-e5e5cW.js → pegDiagram-GJSIUBJH-vVjFUYc4.js} +1 -1
  100. package/dist/web/assets/{pie-WAS4IAKB-BYP5ND4b.js → pie-WAS4IAKB-CyUE71ZQ.js} +1 -1
  101. package/dist/web/assets/{pieDiagram-5QR66LMP-DrEY0oA_.js → pieDiagram-5QR66LMP-CV24sdEs.js} +1 -1
  102. package/dist/web/assets/pinia-UjhgYLzw.js +1 -0
  103. package/dist/web/assets/pinia-ri8AYppz.js +1 -0
  104. package/dist/web/assets/{quadrantDiagram-O4NWA36T-D12UM_7Y.js → quadrantDiagram-O4NWA36T-GVAUaTqf.js} +1 -1
  105. package/dist/web/assets/{radar-RG4KPBEZ-50JpTuMu.js → radar-RG4KPBEZ-D7YGMFYX.js} +1 -1
  106. package/dist/web/assets/{railroad-74A4TZTK-C-7jDcYE.js → railroad-74A4TZTK-BYsDf4cg.js} +1 -1
  107. package/dist/web/assets/railroad-abnf-HS5TGJTU-Bg7f5Sbu.js +1 -0
  108. package/dist/web/assets/railroad-ebnf-LZEXJU2U-ufUodNib.js +1 -0
  109. package/dist/web/assets/railroad-peg-WCYAUIDC-YvUtEf_g.js +1 -0
  110. package/dist/web/assets/{railroadDiagram-XR7U4H2S-q0-o99ts.js → railroadDiagram-XR7U4H2S-DOyP7Y6B.js} +1 -1
  111. package/dist/web/assets/{requirementDiagram-PLB6GJNP-DsO-Ejin.js → requirementDiagram-PLB6GJNP-C2CJ05_O.js} +1 -1
  112. package/dist/web/assets/runtime-core.esm-bundler-CF89Lyp2.js +1 -0
  113. package/dist/web/assets/{safe-storage-DRouskD4.js → safe-storage-BpHrcFXl.js} +1 -1
  114. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-Ct9goD-6.js → sankeyDiagram-IPEJSGJF-rYUTxo-7.js} +1 -1
  115. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BMO_A2FU.js → sequenceDiagram-PO4LG4MO-Dy-NGv9K.js} +1 -1
  116. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-BSzg0zQp.js → stateDiagram-v2-GCMORJYK-_uEXf3B4.js} +1 -1
  117. package/dist/web/assets/{swimlanes-2SLR337P-CcmfFMci.js → swimlanes-2SLR337P-CcejXyI9.js} +1 -1
  118. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-I9_cJ2VX.js +8 -0
  119. package/dist/web/assets/theme-Dv0yhgRD.js +1 -0
  120. package/dist/web/assets/{timeline-definition-EJHVYXUP-0dcHdY2G.js → timeline-definition-EJHVYXUP-CSry3jW1.js} +1 -1
  121. package/dist/web/assets/{treeView-Q6P3EWNA-KynQIJ_Z.js → treeView-Q6P3EWNA-CUCgNMVi.js} +1 -1
  122. package/dist/web/assets/{treemap-WGGIJYW6-DPpYRzcH.js → treemap-WGGIJYW6-BtG_xZt4.js} +1 -1
  123. package/dist/web/assets/use-app-theme-CSRcIN0e.js +1 -0
  124. package/dist/web/assets/use-panel-width-DNg6NE2M.js +1 -0
  125. package/dist/web/assets/use-server-events-CYiRauy1.js +1 -0
  126. package/dist/web/assets/use-virtual-scroll-DTEGubei.js +1 -0
  127. package/dist/web/assets/{usecaseDiagram-POWQR4AR-DJBtB_eN.js → usecaseDiagram-POWQR4AR-BY2X6VR2.js} +1 -1
  128. package/dist/web/assets/{vennDiagram-UO4OBE2U-BgllcrO_.js → vennDiagram-UO4OBE2U-D-khbRsr.js} +1 -1
  129. package/dist/web/assets/{vue-i18n.runtime-D0pkhc7g.js → vue-i18n.runtime-BTqPc4vG.js} +2 -2
  130. package/dist/web/assets/{wardley-WFR3VGLG-D2Je-cZt.js → wardley-WFR3VGLG-BwwTtxgX.js} +1 -1
  131. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-B7ktegci.js → wardleyDiagram-VNRHLVJA-N4eN_Mx8.js} +1 -1
  132. package/dist/web/assets/{xychartDiagram-PMCCYNJV-DL7WkDZI.js → xychartDiagram-PMCCYNJV-Bp3do5lv.js} +1 -1
  133. package/dist/web/index.html +7 -7
  134. package/package.json +11 -3
  135. package/dist/web/assets/CreateReviewFabButton-UdzUa_ra.js +0 -1
  136. package/dist/web/assets/ErrorNotFound-BAP3n0aC.js +0 -1
  137. package/dist/web/assets/IndexPage-BtoeYBg2.js +0 -1
  138. package/dist/web/assets/IndexPage-D894ONbY.css +0 -1
  139. package/dist/web/assets/MainLayout-ChxXEt_Q.css +0 -1
  140. package/dist/web/assets/MainLayout-DjoItVv0.js +0 -7
  141. package/dist/web/assets/QBtn-BDbJU6_B.js +0 -1
  142. package/dist/web/assets/QMenu-Bh-SjGqz.js +0 -1
  143. package/dist/web/assets/QSelect-CZqAOLiG.js +0 -1
  144. package/dist/web/assets/ReviewDetailPage-DvDoJ37c.js +0 -1
  145. package/dist/web/assets/ReviewsPage-CHUXLKKw.js +0 -1
  146. package/dist/web/assets/StatusSwitcher-Br_8-_CT.js +0 -2
  147. package/dist/web/assets/agent-icon-BUd-vCRS.css +0 -1
  148. package/dist/web/assets/agent-icon-Bc3evt8o.js +0 -17
  149. package/dist/web/assets/agent-store-FOng_RxK.js +0 -1
  150. package/dist/web/assets/app-info-store-xZiQfdgO.js +0 -1
  151. package/dist/web/assets/architecture-7GRP2DOG-JRGyT2Fi.js +0 -1
  152. package/dist/web/assets/channel-C9BIu6HY.js +0 -1
  153. package/dist/web/assets/chunk-3FUC2YCW-DM9oDQ0E.js +0 -2
  154. package/dist/web/assets/create-BkZH2Q0U.js +0 -1
  155. package/dist/web/assets/debounce-CDIE7CV6.js +0 -1
  156. package/dist/web/assets/eventmodeling-NTZA5JFV-DblBuyVi.js +0 -1
  157. package/dist/web/assets/flowDiagram-KWPJA3E3-D5iLm5ip.js +0 -1
  158. package/dist/web/assets/i18n-DvBemXXT.js +0 -1
  159. package/dist/web/assets/index-Bn7ByP7x.js +0 -2
  160. package/dist/web/assets/index-CgeU6Wuu.css +0 -1
  161. package/dist/web/assets/init-app-C4PeiqQX.js +0 -1
  162. package/dist/web/assets/pinia-DB8WAJFa.js +0 -1
  163. package/dist/web/assets/pinia-DmTi2T44.js +0 -1
  164. package/dist/web/assets/railroad-abnf-HS5TGJTU-Bf3pkLrz.js +0 -1
  165. package/dist/web/assets/railroad-ebnf-LZEXJU2U-f10AkgiO.js +0 -1
  166. package/dist/web/assets/railroad-peg-WCYAUIDC-C4FjCCVf.js +0 -1
  167. package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +0 -1
  168. package/dist/web/assets/single-flight-r8B7stt1.js +0 -1
  169. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-DLq4WqTk.js +0 -8
  170. package/dist/web/assets/theme-DQpF6lSZ.js +0 -1
  171. package/dist/web/assets/use-app-theme-B1Vh5WKT.js +0 -1
  172. package/dist/web/assets/use-panel-width-Cs_K2M_v.js +0 -1
  173. package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +0 -1
@@ -0,0 +1,93 @@
1
+ // Drop an unfinished control string that is too long to keep for a snapshot.
2
+ const MAX_ESCAPE_TAIL = 8192;
3
+ export class EscapeTail {
4
+ // text: no sequence is open. escape: after ESC. csi: control sequence, ends with a final byte.
5
+ // string: OSC, DCS, PM or APC data, ends with BEL or ST. string-escape: after ESC inside a string.
6
+ state = 'text';
7
+ value = '';
8
+ get tail() {
9
+ return this.value;
10
+ }
11
+ push(data) {
12
+ for (const char of data) {
13
+ // 8-bit CSI (U+009B) starts a control sequence in any state.
14
+ if (char === '\u009b') {
15
+ this.state = 'csi';
16
+ this.value = char;
17
+ continue;
18
+ }
19
+ // 8-bit OSC, DCS, SOS, PM and APC start a control string in any state.
20
+ if (['\u009d', '\u0090', '\u0098', '\u009e', '\u009f'].includes(char)) {
21
+ this.state = 'string';
22
+ this.value = char;
23
+ continue;
24
+ }
25
+ if (this.state === 'text') {
26
+ // ESC opens a new sequence. Other text is not part of the tail.
27
+ if (char === '\x1b') {
28
+ this.state = 'escape';
29
+ this.value = char;
30
+ }
31
+ continue;
32
+ }
33
+ this.value += char;
34
+ // A long OSC, DCS or APC string must not close the shell.
35
+ if (this.value.length > MAX_ESCAPE_TAIL) {
36
+ this.reset();
37
+ continue;
38
+ }
39
+ // CAN and SUB cancel the open sequence.
40
+ if (char === '\x18' || char === '\x1a') {
41
+ this.reset();
42
+ continue;
43
+ }
44
+ if (this.state === 'escape') {
45
+ if (char === '[') {
46
+ // ESC [ starts a control sequence.
47
+ this.state = 'csi';
48
+ }
49
+ else if (']PX^_'.includes(char)) {
50
+ // ESC ] P X ^ _ start OSC, DCS, SOS, PM and APC strings.
51
+ this.state = 'string';
52
+ }
53
+ else if (char >= '0' && char <= '~') {
54
+ // A final byte ends a short escape sequence, for example ESC 7.
55
+ this.reset();
56
+ }
57
+ }
58
+ else if (this.state === 'csi') {
59
+ if (char >= '@' && char <= '~') {
60
+ // A final byte ends the control sequence.
61
+ this.reset();
62
+ }
63
+ else if (char === '\x1b') {
64
+ // ESC aborts the control sequence and opens a new one.
65
+ this.state = 'escape';
66
+ this.value = char;
67
+ }
68
+ }
69
+ else if (this.state === 'string') {
70
+ if (char === '\x07' || char === '\u009c') {
71
+ // BEL or 8-bit ST ends the string.
72
+ this.reset();
73
+ }
74
+ else if (char === '\x1b') {
75
+ // ESC can start the two-character ST (ESC \).
76
+ this.state = 'string-escape';
77
+ }
78
+ }
79
+ else if (char === '\\') {
80
+ // ESC \ is ST: it ends the string.
81
+ this.reset();
82
+ }
83
+ else {
84
+ // ESC with any other character is part of the string data.
85
+ this.state = 'string';
86
+ }
87
+ }
88
+ }
89
+ reset() {
90
+ this.state = 'text';
91
+ this.value = '';
92
+ }
93
+ }
@@ -0,0 +1,115 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ // Give processes time to react to SIGHUP before SIGKILL.
4
+ const TERMINATE_GRACE_MS = 200;
5
+ // A slow `ps` must not block the caller. The caller treats a timeout as unknown.
6
+ const PS_TIMEOUT_MS = 2000;
7
+ // Bound the memory for the `ps` output on a host with many processes.
8
+ const PS_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
9
+ // PID 1 is init. PID 0 and -1 make kill() reach many processes. Never signal them.
10
+ const INIT_PID = 1;
11
+ // Share the Node callback adapter without starting a shell.
12
+ const execute = promisify(execFile);
13
+ export async function processes() {
14
+ const { stdout } = await execute('ps', ['-axo', 'pid=,ppid=,pgid='], {
15
+ timeout: PS_TIMEOUT_MS,
16
+ maxBuffer: PS_MAX_BUFFER_BYTES
17
+ });
18
+ return stdout
19
+ .trim()
20
+ .split('\n')
21
+ .map(line => {
22
+ const [pid, parent, group] = line.trim().split(/\s+/).map(Number);
23
+ return { pid, parent, group };
24
+ })
25
+ .filter(row => Number.isInteger(row.pid) &&
26
+ Number.isInteger(row.parent) &&
27
+ Number.isInteger(row.group) &&
28
+ row.pid > 0);
29
+ }
30
+ // Returns the process and its descendants, each parent before its children.
31
+ // One pass over a parent-to-children map keeps the cost linear.
32
+ export function descendants(rows, pid) {
33
+ const byPid = new Map();
34
+ const children = new Map();
35
+ for (const row of rows) {
36
+ byPid.set(row.pid, row);
37
+ const siblings = children.get(row.parent);
38
+ if (siblings) {
39
+ siblings.push(row.pid);
40
+ }
41
+ else {
42
+ children.set(row.parent, [row.pid]);
43
+ }
44
+ }
45
+ // The queue grows while the loop reads it: this is a breadth-first walk.
46
+ const order = [pid];
47
+ const seen = new Set(order);
48
+ for (const id of order) {
49
+ for (const child of children.get(id) ?? []) {
50
+ if (seen.has(child))
51
+ continue;
52
+ seen.add(child);
53
+ order.push(child);
54
+ }
55
+ }
56
+ return order.flatMap(id => byPid.get(id) ?? []);
57
+ }
58
+ export function classifyProcessActivity(rows, pid) {
59
+ if (!rows.some(row => row.pid === pid))
60
+ return 'unknown';
61
+ return descendants(rows, pid).some(row => row.pid !== pid)
62
+ ? 'running'
63
+ : 'idle';
64
+ }
65
+ export async function processActivity(pid) {
66
+ try {
67
+ return classifyProcessActivity(await processes(), pid);
68
+ }
69
+ catch {
70
+ return 'unknown';
71
+ }
72
+ }
73
+ export function signalProcess(pid, signal) {
74
+ // PID 0 and -1 would reach the server's own group or every process of the user.
75
+ if (!Number.isInteger(pid) || Math.abs(pid) <= INIT_PID)
76
+ return;
77
+ try {
78
+ process.kill(pid, signal);
79
+ }
80
+ catch {
81
+ /* The process may have exited before the signal. */
82
+ }
83
+ }
84
+ export async function terminateProcesses(pid) {
85
+ if (!Number.isInteger(pid) || pid <= INIT_PID)
86
+ return;
87
+ // One snapshot keeps the targets and the group owners consistent.
88
+ let all = [];
89
+ try {
90
+ all = await processes();
91
+ }
92
+ catch {
93
+ /* The backend still terminates its shell. */
94
+ }
95
+ const targets = descendants(all, pid);
96
+ const ids = new Set(targets.map(row => row.pid));
97
+ // Only signal groups that are wholly owned by this shell tree.
98
+ const groups = [
99
+ ...new Set(targets.map(row => row.group).filter(group => group > INIT_PID))
100
+ ].filter(group => all.filter(row => row.group === group).every(row => ids.has(row.pid)));
101
+ // oxlint-disable-next-line unicorn/no-array-reverse -- the type-aware checker lacks toReversed in this module
102
+ const childrenFirst = targets.slice().reverse();
103
+ const signalAll = (signal) => {
104
+ // A group also reaches a child that started after the snapshot.
105
+ for (const group of groups)
106
+ signalProcess(-group, signal);
107
+ for (const row of childrenFirst)
108
+ signalProcess(row.pid, signal);
109
+ signalProcess(pid, signal);
110
+ };
111
+ signalAll('SIGHUP');
112
+ await new Promise(resolve => setTimeout(resolve, TERMINATE_GRACE_MS));
113
+ // A PID can be reused in the grace time. The risk is accepted: PIDs wrap slowly.
114
+ signalAll('SIGKILL');
115
+ }
@@ -0,0 +1,15 @@
1
+ // OSC 133 marks a prompt without adding visible characters.
2
+ export const TERMINAL_PROMPT_MARKER = '\x1b]133;A\x07';
3
+ export class PromptTracker {
4
+ idle = false;
5
+ tail = '';
6
+ input() {
7
+ this.idle = false;
8
+ }
9
+ output(data) {
10
+ const combined = this.tail + data;
11
+ if (combined.includes(TERMINAL_PROMPT_MARKER))
12
+ this.idle = true;
13
+ this.tail = combined.slice(-(TERMINAL_PROMPT_MARKER.length - 1));
14
+ }
15
+ }
@@ -0,0 +1,167 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MAX_TERMINAL_SESSIONS, TERMINAL_IDLE_MS, TERMINAL_ORPHAN_MS } from "../../../shared/terminal/constants.js";
3
+ import { TerminalLimitError, TerminalNotFoundError } from "../../errors/terminal.js";
4
+ import { createBackend } from "./backend.js";
5
+ import { processes } from "./processes.js";
6
+ import { TerminalSession } from "./session.js";
7
+ // Check the activity of every session this often.
8
+ const POLL_MS = 5000;
9
+ export class TerminalRegistry {
10
+ entries = new Map();
11
+ listeners = new Set();
12
+ closing = new Set();
13
+ poll;
14
+ creating = 0;
15
+ nextNumber = 1;
16
+ stopped = false;
17
+ checking = false;
18
+ options;
19
+ constructor(options) {
20
+ this.options = options;
21
+ this.poll = setInterval(() => {
22
+ void this.checkActivity();
23
+ }, options.pollMs ?? POLL_MS);
24
+ this.poll.unref();
25
+ }
26
+ get list() {
27
+ return [...this.entries.values()].map(entry => entry.session.info);
28
+ }
29
+ subscribe(listener) {
30
+ this.listeners.add(listener);
31
+ return () => this.listeners.delete(listener);
32
+ }
33
+ publish(message) {
34
+ for (const listener of this.listeners)
35
+ listener(message);
36
+ }
37
+ publishList() {
38
+ this.publish({ type: 'list', sessions: this.list });
39
+ }
40
+ now() {
41
+ return (this.options.now ?? Date.now)();
42
+ }
43
+ get(id) {
44
+ const entry = this.entries.get(id);
45
+ if (!entry)
46
+ throw new TerminalNotFoundError('Terminal session no longer exists.');
47
+ return entry.session;
48
+ }
49
+ async create(cols, rows) {
50
+ if (this.stopped)
51
+ throw new TerminalNotFoundError('Terminal server is stopping.');
52
+ if (this.entries.size + this.creating >= MAX_TERMINAL_SESSIONS)
53
+ throw new TerminalLimitError('The terminal limit has been reached.');
54
+ this.creating++;
55
+ try {
56
+ const backend = await (this.options.backend ?? createBackend)(this.options.repositoryPath, cols, rows);
57
+ if (this.stopped) {
58
+ await backend.kill();
59
+ throw new TerminalNotFoundError('Terminal server is stopping.');
60
+ }
61
+ const id = randomUUID();
62
+ const session = new TerminalSession(id, `Terminal ${this.nextNumber++}`, backend, cols, rows, message => this.publish(message), () => {
63
+ void this.close(id);
64
+ });
65
+ this.entries.set(id, {
66
+ session,
67
+ subscribers: new Set(),
68
+ orphanedSince: this.now(),
69
+ idleSince: null
70
+ });
71
+ this.options.onLifecycle?.({ type: 'created', id, mode: backend.mode });
72
+ this.publishList();
73
+ return session;
74
+ }
75
+ finally {
76
+ this.creating--;
77
+ }
78
+ }
79
+ attach(id, subscriber) {
80
+ const entry = this.entries.get(id);
81
+ if (!entry)
82
+ throw new TerminalNotFoundError('Terminal session no longer exists.');
83
+ entry.subscribers.add(subscriber);
84
+ entry.orphanedSince = null;
85
+ entry.idleSince = null;
86
+ }
87
+ detach(subscriber) {
88
+ for (const entry of this.entries.values()) {
89
+ if (!entry.subscribers.delete(subscriber) || entry.subscribers.size)
90
+ continue;
91
+ entry.orphanedSince = this.now();
92
+ entry.idleSince = entry.session.running === 'idle' ? this.now() : null;
93
+ }
94
+ }
95
+ async checkActivity() {
96
+ if (this.checking || this.stopped)
97
+ return;
98
+ this.checking = true;
99
+ try {
100
+ // One process list serves every session. A failed read leaves them unknown.
101
+ let rows;
102
+ if (this.entries.size > 0) {
103
+ try {
104
+ rows = await (this.options.listProcesses ?? processes)();
105
+ }
106
+ catch {
107
+ rows = undefined;
108
+ }
109
+ }
110
+ for (const [id, entry] of this.entries) {
111
+ const previous = entry.session.running;
112
+ entry.session.running =
113
+ rows === undefined
114
+ ? 'unknown'
115
+ : await entry.session.backend.activity(rows);
116
+ if (previous !== entry.session.running)
117
+ this.publishList();
118
+ if (entry.orphanedSince === null)
119
+ continue;
120
+ const now = this.now();
121
+ if (entry.session.running === 'idle') {
122
+ entry.idleSince ??= now;
123
+ }
124
+ else {
125
+ entry.idleSince = null;
126
+ }
127
+ if (now - entry.orphanedSince >= TERMINAL_ORPHAN_MS ||
128
+ (entry.idleSince !== null &&
129
+ now - entry.idleSince >= TERMINAL_IDLE_MS)) {
130
+ this.options.onLifecycle?.({ type: 'timeout', id });
131
+ await this.close(id);
132
+ }
133
+ }
134
+ }
135
+ finally {
136
+ this.checking = false;
137
+ }
138
+ }
139
+ async resize(id, cols, rows) {
140
+ await this.get(id).resize(cols, rows);
141
+ this.publishList();
142
+ }
143
+ async close(id) {
144
+ const entry = this.entries.get(id);
145
+ if (!entry)
146
+ return;
147
+ this.entries.delete(id);
148
+ this.options.onLifecycle?.({ type: 'closed', id });
149
+ this.publish({ type: 'exit', terminalId: id });
150
+ this.publishList();
151
+ const closing = entry.session.dispose();
152
+ this.closing.add(closing);
153
+ try {
154
+ await closing;
155
+ }
156
+ finally {
157
+ this.closing.delete(closing);
158
+ }
159
+ }
160
+ async dispose() {
161
+ this.stopped = true;
162
+ clearInterval(this.poll);
163
+ await Promise.all([...this.entries.keys()].map(id => this.close(id)));
164
+ await Promise.all(this.closing);
165
+ this.listeners.clear();
166
+ }
167
+ }
@@ -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
+ }