@hienlh/ppm 0.8.85 → 0.8.86

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 (224) hide show
  1. package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/CHANGELOG.md +5 -187
  4. package/bun.lock +0 -5
  5. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
  6. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
  7. package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
  12. package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
  13. package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
  14. package/dist/web/assets/channel-DmKoFTd_.js +1 -0
  15. package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
  45. package/dist/web/assets/clone-CijCFRT5.js +1 -0
  46. package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
  48. package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
  49. package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
  50. package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
  51. package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
  52. package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
  53. package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
  54. package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
  55. package/dist/web/assets/dist-0Va_2L7G.js +16 -0
  56. package/dist/web/assets/dist-D9irYETY.js +41 -0
  57. package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
  58. package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
  59. package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
  60. package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
  61. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
  62. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
  63. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
  64. package/dist/web/assets/index-CIkjfera.js +31 -0
  65. package/dist/web/assets/index-WKLuYsBY.css +2 -0
  66. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
  67. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
  68. package/dist/web/assets/input-CGp1nFIg.js +1 -0
  69. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
  73. package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
  74. package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
  75. package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
  76. package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
  77. package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
  78. package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
  79. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
  80. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
  81. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
  82. package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
  83. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
  84. package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
  85. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
  86. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
  87. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
  88. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
  89. package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
  90. package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
  91. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
  92. package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
  93. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
  94. package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
  95. package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
  96. package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
  97. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
  98. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
  99. package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
  100. package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
  101. package/dist/web/index.html +11 -12
  102. package/dist/web/sw.js +1 -1
  103. package/docs/code-standards.md +7 -232
  104. package/docs/codebase-summary.md +3 -9
  105. package/docs/design-guidelines.md +0 -21
  106. package/docs/project-changelog.md +1 -115
  107. package/docs/project-roadmap.md +19 -41
  108. package/docs/system-architecture.md +15 -212
  109. package/package.json +2 -3
  110. package/src/cli/commands/autostart.ts +1 -1
  111. package/src/cli/commands/restart.ts +1 -9
  112. package/src/cli/commands/status.ts +0 -19
  113. package/src/index.ts +3 -2
  114. package/src/providers/claude-agent-sdk.ts +31 -94
  115. package/src/providers/mock-provider.ts +1 -6
  116. package/src/server/index.ts +166 -38
  117. package/src/server/routes/chat.ts +3 -52
  118. package/src/server/routes/project-scoped.ts +0 -2
  119. package/src/server/routes/proxy.ts +53 -46
  120. package/src/server/routes/tunnel.ts +32 -0
  121. package/src/server/ws/chat.ts +146 -207
  122. package/src/services/account-selector.service.ts +8 -16
  123. package/src/services/account.service.ts +13 -19
  124. package/src/services/claude-usage.service.ts +11 -48
  125. package/src/services/cloud.service.ts +6 -10
  126. package/src/services/db.service.ts +6 -111
  127. package/src/services/port-tunnel.service.ts +97 -0
  128. package/src/services/proxy.service.ts +19 -4
  129. package/src/services/supervisor.ts +25 -285
  130. package/src/types/api.ts +1 -9
  131. package/src/types/chat.ts +1 -3
  132. package/src/web/app.tsx +35 -41
  133. package/src/web/components/browser/browser-tab.tsx +97 -106
  134. package/src/web/components/chat/chat-history-bar.tsx +6 -72
  135. package/src/web/components/chat/chat-tab.tsx +16 -32
  136. package/src/web/components/chat/message-input.tsx +13 -107
  137. package/src/web/components/chat/message-list.tsx +15 -27
  138. package/src/web/components/chat/session-picker.tsx +31 -78
  139. package/src/web/components/chat/usage-badge.tsx +1 -11
  140. package/src/web/components/editor/code-editor.tsx +26 -36
  141. package/src/web/components/layout/command-palette.tsx +1 -3
  142. package/src/web/components/layout/editor-panel.tsx +18 -162
  143. package/src/web/components/layout/panel-layout.tsx +1 -17
  144. package/src/web/components/settings/proxy-settings-section.tsx +42 -40
  145. package/src/web/hooks/use-chat.ts +201 -211
  146. package/src/web/hooks/use-global-keybindings.ts +2 -25
  147. package/src/web/hooks/use-server-reload.ts +0 -9
  148. package/src/web/hooks/use-url-sync.ts +21 -173
  149. package/src/web/stores/keybindings-store.ts +0 -1
  150. package/src/web/stores/panel-store.ts +19 -73
  151. package/src/web/stores/panel-utils.ts +3 -145
  152. package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
  153. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  154. package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
  155. package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
  156. package/dist/web/assets/channel-wrd-NHWf.js +0 -1
  157. package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
  158. package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
  159. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
  160. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
  161. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  162. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
  163. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
  164. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  165. package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
  166. package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
  167. package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
  168. package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
  169. package/dist/web/assets/dist-DylI9XxN.js +0 -13
  170. package/dist/web/assets/dist-lF8CoYII.js +0 -41
  171. package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
  172. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  173. package/dist/web/assets/index-CgQXpBb_.css +0 -2
  174. package/dist/web/assets/index-DEeeRoka.js +0 -37
  175. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  176. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
  177. package/dist/web/assets/input-BglMT33g.js +0 -1
  178. package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
  179. package/dist/web/assets/lib-BQ34Db2e.js +0 -4
  180. package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
  181. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  182. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  183. package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
  184. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  185. package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
  186. package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
  187. package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
  188. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
  189. package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
  190. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  191. package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
  192. package/docs/streaming-input-guide.md +0 -267
  193. package/snapshot-state.md +0 -1526
  194. package/src/server/routes/browser-preview.ts +0 -159
  195. package/src/server/routes/workspace.ts +0 -35
  196. package/src/services/cloud-ws.service.ts +0 -227
  197. package/src/web/components/chat/account-rotation-settings.tsx +0 -163
  198. package/src/web/components/chat/chat-welcome.tsx +0 -148
  199. package/src/web/components/editor/csv-preview.tsx +0 -228
  200. package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
  201. package/src/web/components/editor/editor-toolbar.tsx +0 -74
  202. package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
  203. package/src/web/hooks/use-voice-input.ts +0 -111
  204. package/src/web/lib/csv-parser.ts +0 -134
  205. package/src/web/stores/connection-store.ts +0 -39
  206. package/test-tokens.mjs +0 -212
  207. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
  208. /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
  209. /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
  210. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
  211. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
  212. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
  213. /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
  214. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
  215. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
  216. /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
  217. /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
  218. /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
  219. /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
  220. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
  221. /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
  222. /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
  223. /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
  224. /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
@@ -1,159 +0,0 @@
1
- import { Hono } from "hono";
2
- import { ok, err } from "../../types/api.ts";
3
- import { ensureCloudflared } from "../../services/cloudflared.service.ts";
4
-
5
- /**
6
- * Browser preview API — starts per-port Cloudflare Quick Tunnels so the
7
- * frontend can iframe any localhost dev server without CORS/path issues.
8
- *
9
- * POST /api/preview/tunnel { port: 3000 } → { url: "https://xxx.trycloudflare.com" }
10
- * DELETE /api/preview/tunnel/:port → stops tunnel for that port
11
- * GET /api/preview/tunnels → list active tunnels
12
- */
13
- export const browserPreviewRoutes = new Hono();
14
-
15
- const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
16
-
17
- interface ActiveTunnel {
18
- port: number;
19
- url: string;
20
- process: import("bun").Subprocess;
21
- startedAt: number;
22
- }
23
-
24
- /** Active tunnels keyed by port — exported for testing */
25
- export const activeTunnels = new Map<number, ActiveTunnel>();
26
-
27
- /** Start a tunnel for a localhost port */
28
- browserPreviewRoutes.post("/tunnel", async (c) => {
29
- const body = await c.req.json<{ port: number }>().catch(() => null);
30
- const port = body?.port;
31
- if (!port || port < 1 || port > 65535) {
32
- return c.json(err("Invalid port (1-65535)"), 400);
33
- }
34
-
35
- // Return existing tunnel if already running
36
- const existing = activeTunnels.get(port);
37
- if (existing) {
38
- return c.json(ok({ port, url: existing.url }));
39
- }
40
-
41
- try {
42
- const bin = await ensureCloudflared();
43
- const proc = Bun.spawn(
44
- [bin, "tunnel", "--url", `http://127.0.0.1:${port}`],
45
- { stderr: "pipe", stdout: "ignore", stdin: "ignore" },
46
- );
47
-
48
- // Read stderr to find tunnel URL
49
- const reader = proc.stderr.getReader();
50
- const decoder = new TextDecoder();
51
- const url = await new Promise<string>((resolve, reject) => {
52
- const timeout = setTimeout(() => {
53
- try { proc.kill(); } catch {}
54
- reject(new Error("Tunnel timed out after 30s"));
55
- }, 30_000);
56
-
57
- let buffer = "";
58
- let found = false;
59
- const read = async () => {
60
- try {
61
- while (true) {
62
- const { done, value } = await reader.read();
63
- if (done) break;
64
- if (found) continue;
65
- buffer += decoder.decode(value, { stream: true });
66
- const match = buffer.match(TUNNEL_URL_REGEX);
67
- if (match) {
68
- found = true;
69
- buffer = "";
70
- clearTimeout(timeout);
71
- resolve(match[0]);
72
- }
73
- }
74
- if (!found) {
75
- clearTimeout(timeout);
76
- reject(new Error("cloudflared exited without tunnel URL"));
77
- }
78
- } catch (e) {
79
- if (!found) { clearTimeout(timeout); reject(e); }
80
- }
81
- };
82
- read();
83
- });
84
-
85
- activeTunnels.set(port, { port, url, process: proc, startedAt: Date.now() });
86
-
87
- // Auto-cleanup when process exits
88
- proc.exited.then(() => activeTunnels.delete(port)).catch(() => activeTunnels.delete(port));
89
-
90
- console.log(`[preview] tunnel started for port ${port} → ${url}`);
91
- return c.json(ok({ port, url }));
92
- } catch (e: any) {
93
- return c.json(err(e.message || "Failed to start tunnel"), 500);
94
- }
95
- });
96
-
97
- /** Stop a tunnel */
98
- browserPreviewRoutes.delete("/tunnel/:port{[0-9]+}", (c) => {
99
- const port = parseInt(c.req.param("port"), 10);
100
- const tunnel = activeTunnels.get(port);
101
- if (!tunnel) {
102
- return c.json(err("No tunnel running for this port"), 404);
103
- }
104
-
105
- try { tunnel.process.kill(); } catch {}
106
- activeTunnels.delete(port);
107
- console.log(`[preview] tunnel stopped for port ${port}`);
108
- return c.json(ok({ port }));
109
- });
110
-
111
- /** List active tunnels */
112
- browserPreviewRoutes.get("/tunnels", (c) => {
113
- const list = Array.from(activeTunnels.values()).map((t) => ({
114
- port: t.port,
115
- url: t.url,
116
- startedAt: t.startedAt,
117
- }));
118
- return c.json(ok(list));
119
- });
120
-
121
- /** Check if a cloudflared process is still alive */
122
- function isProcessAlive(proc: import("bun").Subprocess): boolean {
123
- try { process.kill(proc.pid, 0); return true; } catch { return false; }
124
- }
125
-
126
- /** Remove ghost tunnels (process died or target port no longer listening) */
127
- async function cleanupGhostTunnels() {
128
- for (const [port, tunnel] of activeTunnels) {
129
- // Check if cloudflared process is still running
130
- if (!isProcessAlive(tunnel.process)) {
131
- console.log(`[preview] ghost cleanup: tunnel for port ${port} — process dead`);
132
- activeTunnels.delete(port);
133
- continue;
134
- }
135
- // Check if target port is still listening
136
- try {
137
- const conn = await Bun.connect({ hostname: "127.0.0.1", port, socket: {
138
- data() {}, open(s) { s.end(); }, error() {}, close() {},
139
- }});
140
- conn.end();
141
- } catch {
142
- // Port not listening — kill tunnel
143
- console.log(`[preview] ghost cleanup: tunnel for port ${port} — port not listening`);
144
- try { tunnel.process.kill(); } catch {}
145
- activeTunnels.delete(port);
146
- }
147
- }
148
- }
149
-
150
- // Run ghost cleanup every 30s
151
- setInterval(cleanupGhostTunnels, 30_000);
152
-
153
- /** Cleanup all tunnels on server shutdown */
154
- export function stopAllPreviewTunnels() {
155
- for (const [port, tunnel] of activeTunnels) {
156
- try { tunnel.process.kill(); } catch {}
157
- activeTunnels.delete(port);
158
- }
159
- }
@@ -1,35 +0,0 @@
1
- import { Hono } from "hono";
2
- import { getWorkspace, setWorkspace } from "../../services/db.service.ts";
3
- import { ok, err } from "../../types/api.ts";
4
-
5
- type Env = { Variables: { projectPath: string; projectName: string } };
6
-
7
- export const workspaceRoutes = new Hono<Env>();
8
-
9
- /** GET /workspace — load saved workspace layout */
10
- workspaceRoutes.get("/", (c) => {
11
- try {
12
- const projectName = c.get("projectName");
13
- const row = getWorkspace(projectName);
14
- if (!row) return c.json(ok(null));
15
- return c.json(ok({
16
- layout: JSON.parse(row.layout_json),
17
- updatedAt: row.updated_at,
18
- }));
19
- } catch (e) {
20
- return c.json(err((e as Error).message), 500);
21
- }
22
- });
23
-
24
- /** PUT /workspace — save workspace layout */
25
- workspaceRoutes.put("/", async (c) => {
26
- try {
27
- const projectName = c.get("projectName");
28
- const body = await c.req.json<{ layout: unknown }>();
29
- if (!body.layout) return c.json(err("Missing layout"), 400);
30
- const updatedAt = setWorkspace(projectName, JSON.stringify(body.layout));
31
- return c.json(ok({ updatedAt }));
32
- } catch (e) {
33
- return c.json(err((e as Error).message), 500);
34
- }
35
- });
@@ -1,227 +0,0 @@
1
- /**
2
- * Cloud WebSocket client — persistent connection from supervisor to PPM Cloud.
3
- * Auto-reconnects with exponential backoff + jitter. Queues messages when disconnected.
4
- */
5
- import { appendFileSync } from "node:fs";
6
- import { resolve } from "node:path";
7
- import { homedir } from "node:os";
8
-
9
- // ─── Types (must match Cloud's ws-types.ts) ─────────
10
- interface WsMessage {
11
- type: string;
12
- id?: string;
13
- timestamp: string;
14
- }
15
-
16
- interface HeartbeatMsg extends WsMessage {
17
- type: "heartbeat";
18
- tunnelUrl: string | null;
19
- state: string;
20
- appVersion: string;
21
- availableVersion: string | null;
22
- serverPid: number | null;
23
- uptime: number;
24
- }
25
-
26
- interface StateChangeMsg extends WsMessage {
27
- type: "state_change";
28
- from: string;
29
- to: string;
30
- reason: string;
31
- }
32
-
33
- interface CommandAckMsg extends WsMessage {
34
- type: "command_ack";
35
- id: string;
36
- }
37
-
38
- interface CommandResultMsg extends WsMessage {
39
- type: "command_result";
40
- id: string;
41
- success: boolean;
42
- error?: string;
43
- data?: Record<string, unknown>;
44
- }
45
-
46
- type OutboundMsg = HeartbeatMsg | StateChangeMsg | CommandAckMsg | CommandResultMsg;
47
-
48
- interface CommandMsg extends WsMessage {
49
- type: "command";
50
- id: string;
51
- action: string;
52
- params?: Record<string, unknown>;
53
- }
54
-
55
- type CommandHandler = (cmd: CommandMsg) => void;
56
-
57
- // ─── Constants ──────────────────────────────────────
58
- const BACKOFF_STEPS = [1000, 2000, 4000, 8000, 15000, 30000, 60000];
59
- const MAX_QUEUE_SIZE = 50;
60
- const HEARTBEAT_INTERVAL_MS = 60_000; // 60s via WS
61
-
62
- // ─── State ──────────────────────────────────────────
63
- let ws: WebSocket | null = null;
64
- let connected = false;
65
- let reconnecting = false;
66
- let reconnectAttempt = 0;
67
- let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
68
- let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
69
- let commandHandler: CommandHandler | null = null;
70
- let outboundQueue: OutboundMsg[] = [];
71
- let wsUrl = "";
72
- let shouldConnect = false;
73
-
74
- // Credentials for first-message auth
75
- let deviceId = "";
76
- let secretKey = "";
77
-
78
- // For heartbeat payload
79
- let getHeartbeatData: (() => HeartbeatMsg) | null = null;
80
-
81
- // ─── Public API ─────────────────────────────────────
82
-
83
- export function connect(opts: {
84
- cloudUrl: string;
85
- deviceId: string;
86
- secretKey: string;
87
- heartbeatFn: () => HeartbeatMsg;
88
- }): void {
89
- // No secret_key in URL — auth via first message after connect
90
- wsUrl = `${opts.cloudUrl.replace(/^http/, "ws")}/ws/device`;
91
- deviceId = opts.deviceId;
92
- secretKey = opts.secretKey;
93
- getHeartbeatData = opts.heartbeatFn;
94
- shouldConnect = true;
95
- reconnectAttempt = 0;
96
- doConnect();
97
- }
98
-
99
- export function disconnect(): void {
100
- shouldConnect = false;
101
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
102
- if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
103
- if (ws) {
104
- try { ws.close(1000, "shutdown"); } catch {}
105
- ws = null;
106
- }
107
- connected = false;
108
- outboundQueue = [];
109
- }
110
-
111
- export function send(msg: OutboundMsg): void {
112
- if (connected && ws?.readyState === WebSocket.OPEN) {
113
- ws.send(JSON.stringify(msg));
114
- } else {
115
- outboundQueue.push(msg);
116
- if (outboundQueue.length > MAX_QUEUE_SIZE) outboundQueue.shift();
117
- }
118
- }
119
-
120
- export function onCommand(handler: CommandHandler): void {
121
- commandHandler = handler;
122
- }
123
-
124
- export function isConnected(): boolean {
125
- return connected;
126
- }
127
-
128
- // ─── Internal ───────────────────────────────────────
129
-
130
- function doConnect(): void {
131
- if (!shouldConnect || reconnecting) return;
132
- reconnecting = true;
133
-
134
- // Capture local ref — if a reconnect replaces `ws` before this socket's
135
- // handlers fire, stale handlers must not reset module-level state.
136
- let sock: WebSocket;
137
- try {
138
- sock = new WebSocket(wsUrl);
139
- ws = sock;
140
- } catch {
141
- reconnecting = false;
142
- scheduleReconnect("constructor");
143
- return;
144
- }
145
-
146
- sock.onopen = () => {
147
- if (ws !== sock) return; // stale — newer connection replaced us
148
- reconnecting = false;
149
- reconnectAttempt = 0;
150
- log("INFO", "Cloud WS connected, sending auth");
151
-
152
- // Send auth as first message — server must process this before any other msg
153
- sock.send(JSON.stringify({
154
- type: "auth",
155
- deviceId,
156
- secretKey,
157
- timestamp: new Date().toISOString(),
158
- version: 1,
159
- }));
160
-
161
- // Delay setting connected + sending heartbeat to let server process auth.
162
- // Server's authenticateDevice() is async (DB lookup), so messages sent
163
- // immediately after auth arrive before authenticated=true → 4002 reject.
164
- setTimeout(() => {
165
- if (ws !== sock) return; // replaced during delay
166
- connected = true;
167
-
168
- // Flush queued messages
169
- while (outboundQueue.length > 0 && connected) {
170
- const msg = outboundQueue.shift()!;
171
- sock.send(JSON.stringify(msg));
172
- }
173
-
174
- // Send immediate heartbeat
175
- if (getHeartbeatData) send(getHeartbeatData());
176
-
177
- // Start periodic heartbeat
178
- if (heartbeatTimer) clearInterval(heartbeatTimer);
179
- heartbeatTimer = setInterval(() => {
180
- if (getHeartbeatData && connected) send(getHeartbeatData());
181
- }, HEARTBEAT_INTERVAL_MS);
182
- }, 500); // 500ms for DB auth round-trip
183
- };
184
-
185
- sock.onmessage = (event) => {
186
- try {
187
- const msg = JSON.parse(String(event.data)) as CommandMsg;
188
- if (msg.type === "command" && commandHandler) {
189
- commandHandler(msg);
190
- }
191
- } catch {} // ignore malformed
192
- };
193
-
194
- sock.onclose = (event) => {
195
- if (ws !== sock) return; // stale — ignore close from replaced connection
196
- log("WARN", `Cloud WS closed: code=${event.code} reason=${event.reason || ""}`);
197
- connected = false;
198
- reconnecting = false;
199
- ws = null;
200
- if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
201
- if (shouldConnect) scheduleReconnect("onclose");
202
- };
203
-
204
- sock.onerror = (event) => {
205
- log("ERROR", `Cloud WS error: ${String(event)}`);
206
- };
207
- }
208
-
209
- function scheduleReconnect(source = "unknown"): void {
210
- if (!shouldConnect || reconnectTimer) return;
211
- const base = BACKOFF_STEPS[Math.min(reconnectAttempt, BACKOFF_STEPS.length - 1)]!;
212
- // Add ±30% jitter to prevent thundering herd after Cloud deploy
213
- const jitter = base * (0.7 + Math.random() * 0.6);
214
- const delay = Math.round(jitter);
215
- reconnectAttempt++;
216
- log("WARN", `Cloud WS reconnect in ${delay}ms (attempt #${reconnectAttempt}) src=${source}`);
217
- reconnectTimer = setTimeout(() => {
218
- reconnectTimer = null;
219
- doConnect();
220
- }, delay);
221
- }
222
-
223
- function log(level: string, msg: string): void {
224
- const ts = new Date().toISOString();
225
- const logFile = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"), "ppm.log");
226
- try { appendFileSync(logFile, `[${ts}] [${level}] [cloud-ws] ${msg}\n`); } catch {}
227
- }
@@ -1,163 +0,0 @@
1
- import { useState, useEffect, useSyncExternalStore } from "react";
2
- import { Settings, X } from "lucide-react";
3
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
4
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
5
- import { cn } from "@/lib/utils";
6
- import {
7
- getAccountSettings,
8
- updateAccountSettings,
9
- type AccountSettings,
10
- } from "../../lib/api-settings";
11
-
12
- interface AccountRotationSettingsProps {
13
- open: boolean;
14
- onOpenChange: (open: boolean) => void;
15
- }
16
-
17
- const mdQuery = typeof window !== "undefined" ? window.matchMedia("(min-width: 768px)") : null;
18
- function subscribeMedia(cb: () => void) {
19
- mdQuery?.addEventListener("change", cb);
20
- return () => mdQuery?.removeEventListener("change", cb);
21
- }
22
- function getIsDesktop() {
23
- return mdQuery?.matches ?? true;
24
- }
25
-
26
- function SettingsContent() {
27
- const [settings, setSettings] = useState<AccountSettings | null>(null);
28
- const [loading, setLoading] = useState(true);
29
-
30
- useEffect(() => {
31
- setLoading(true);
32
- getAccountSettings()
33
- .then(setSettings)
34
- .finally(() => setLoading(false));
35
- }, []);
36
-
37
- if (loading) {
38
- return <p className="text-xs text-text-subtle py-4 text-center">Loading...</p>;
39
- }
40
- if (!settings) {
41
- return <p className="text-xs text-text-subtle py-4 text-center">Failed to load settings</p>;
42
- }
43
-
44
- return (
45
- <div className="space-y-4">
46
- {/* Strategy */}
47
- <div className="space-y-1.5">
48
- <label className="text-xs font-medium text-text-primary">Rotation Strategy</label>
49
- <Select
50
- value={settings.strategy}
51
- onValueChange={async (v) => {
52
- const updated = await updateAccountSettings({ strategy: v as AccountSettings["strategy"] });
53
- setSettings(updated);
54
- }}
55
- >
56
- <SelectTrigger className="w-full h-9 text-xs">
57
- <SelectValue />
58
- </SelectTrigger>
59
- <SelectContent>
60
- <SelectItem value="round-robin">Round-robin</SelectItem>
61
- <SelectItem value="fill-first">Fill-first</SelectItem>
62
- <SelectItem value="lowest-usage">Lowest usage</SelectItem>
63
- </SelectContent>
64
- </Select>
65
- <p className="text-[10px] text-text-subtle">
66
- {settings.strategy === "round-robin" && "Cycles through accounts evenly"}
67
- {settings.strategy === "fill-first" && "Uses one account until its limit, then moves on"}
68
- {settings.strategy === "lowest-usage" && "Picks the account with the lowest current usage"}
69
- </p>
70
- </div>
71
-
72
- {/* Max Retry */}
73
- <div className="space-y-1.5">
74
- <label className="text-xs font-medium text-text-primary">Max Retry</label>
75
- <input
76
- type="number"
77
- min={0}
78
- value={settings.maxRetry}
79
- className="w-full h-9 text-xs border rounded-md px-3 bg-background"
80
- onChange={async (e) => {
81
- const v = parseInt(e.target.value, 10);
82
- if (!isNaN(v) && v >= 0) {
83
- const updated = await updateAccountSettings({ maxRetry: v });
84
- setSettings(updated);
85
- }
86
- }}
87
- />
88
- <p className="text-[10px] text-text-subtle">
89
- How many accounts to try on failure. 0 = try all available accounts.
90
- </p>
91
- </div>
92
-
93
- {/* Active accounts */}
94
- <div className="flex items-center justify-between text-xs border-t border-border pt-3">
95
- <span className="text-text-subtle">Active accounts</span>
96
- <span className="font-medium text-text-primary">{settings.activeCount}</span>
97
- </div>
98
- </div>
99
- );
100
- }
101
-
102
- export function AccountRotationSettings({ open, onOpenChange }: AccountRotationSettingsProps) {
103
- const isDesktop = useSyncExternalStore(subscribeMedia, getIsDesktop);
104
-
105
- if (!open) return null;
106
-
107
- // Desktop: Dialog
108
- if (isDesktop) {
109
- return (
110
- <Dialog open={open} onOpenChange={onOpenChange}>
111
- <DialogContent className="sm:max-w-sm">
112
- <DialogHeader>
113
- <DialogTitle className="text-sm flex items-center gap-2">
114
- <Settings className="size-4" /> Rotation & Retry
115
- </DialogTitle>
116
- </DialogHeader>
117
- <SettingsContent />
118
- </DialogContent>
119
- </Dialog>
120
- );
121
- }
122
-
123
- // Mobile: Bottom sheet
124
- return (
125
- <>
126
- <div
127
- className="fixed inset-0 z-50 transition-opacity duration-200 opacity-100"
128
- onClick={() => onOpenChange(false)}
129
- style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
130
- />
131
- <div
132
- className={cn(
133
- "fixed bottom-0 left-0 right-0 z-50 bg-background rounded-t-2xl border-t border-border shadow-2xl",
134
- "transition-transform duration-300 ease-out max-h-[85vh] overflow-y-auto",
135
- "translate-y-0",
136
- )}
137
- >
138
- {/* Drag handle */}
139
- <div className="flex justify-center pt-3 pb-1">
140
- <div className="w-10 h-1 rounded-full bg-border" />
141
- </div>
142
-
143
- {/* Header */}
144
- <div className="flex items-center justify-between px-4 py-2 border-b border-border">
145
- <span className="text-sm font-semibold flex items-center gap-2">
146
- <Settings className="size-4" /> Rotation & Retry
147
- </span>
148
- <button
149
- onClick={() => onOpenChange(false)}
150
- className="flex items-center justify-center size-7 rounded-md hover:bg-surface-elevated transition-colors"
151
- >
152
- <X className="size-4" />
153
- </button>
154
- </div>
155
-
156
- {/* Content */}
157
- <div className="px-4 py-4 pb-8">
158
- <SettingsContent />
159
- </div>
160
- </div>
161
- </>
162
- );
163
- }