@hienlh/ppm 0.18.12 → 0.18.14

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 (548) hide show
  1. package/AGENTS.md +17 -5
  2. package/CHANGELOG.md +27 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +6 -6
  5. package/assets/skills/ppm/SKILL.md +1 -1
  6. package/assets/skills/ppm/references/cli-reference.md +4 -4
  7. package/assets/skills/ppm/references/http-api.md +19 -1
  8. package/dist/web/assets/{adaptive-context-menu-DGCKKeTy.js → adaptive-context-menu-DCZtgOL7.js} +1 -1
  9. package/dist/web/assets/{ai-resource-editor-BOHHTTe7.js → ai-resource-editor-D5CPBg06.js} +1 -1
  10. package/dist/web/assets/{ai-settings-section-BigC_nZR.js → ai-settings-section-DY-t1bK8.js} +1 -1
  11. package/dist/web/assets/{angular-html-BYPaydvh.js → angular-html-CTvM4ulF.js} +1 -1
  12. package/dist/web/assets/{angular-ts-Bb4AMKlk.js → angular-ts-B_vSkjco.js} +1 -1
  13. package/dist/web/assets/api-client-BogqsFna.js +1 -0
  14. package/dist/web/assets/{api-settings-Bd3M4YpI.js → api-settings-B2iNXQ0c.js} +1 -1
  15. package/dist/web/assets/api-settings-DRF9oYkn.js +1 -0
  16. package/dist/web/assets/{api-themes-Cpx419SL.js → api-themes-DrWnJbhs.js} +1 -1
  17. package/dist/web/assets/{apl-DB2VjFCS.js → apl-DywzTry0.js} +1 -1
  18. package/dist/web/assets/architecture-PBZL5I3N-nw5-j_I_.js +1 -0
  19. package/dist/web/assets/{astro-BeoJdVpk.js → astro-1JaRgaSb.js} +1 -1
  20. package/dist/web/assets/{audio-preview-BgFuTn-0.js → audio-preview-DAuGvOiK.js} +1 -1
  21. package/dist/web/assets/{blade-Cf2q1sPf.js → blade-CDeeJWBb.js} +1 -1
  22. package/dist/web/assets/{bsl-W1kOaNpu.js → bsl-BfgoPVYR.js} +1 -1
  23. package/dist/web/assets/{button-5zTrkEoN.js → button-tNy0ba3y.js} +1 -1
  24. package/dist/web/assets/{cairo-BoAZFcje.js → cairo-C26gNwWW.js} +1 -1
  25. package/dist/web/assets/chat-tab-IcnU3nqQ.js +15 -0
  26. package/dist/web/assets/{cobol-B0Nm7RXj.js → cobol-D92nBnON.js} +1 -1
  27. package/dist/web/assets/code-editor-Dno5Srkm.js +10 -0
  28. package/dist/web/assets/{coffee-BakMn3on.js → coffee-CIWH40R1.js} +1 -1
  29. package/dist/web/assets/{conflict-editor-BUs5k9q_.js → conflict-editor-CDp0I3mD.js} +1 -1
  30. package/dist/web/assets/{context-menu-Bq92km4s.js → context-menu-BO1c9jKy.js} +1 -1
  31. package/dist/web/assets/{cpp-BRAJQHK2.js → cpp-DOrBqXCn.js} +1 -1
  32. package/dist/web/assets/{crystal-kqqO3zkK.js → crystal-BNKqmHux.js} +1 -1
  33. package/dist/web/assets/{csv-preview-ByoDgLAZ.js → csv-preview-D4svNDAq.js} +1 -1
  34. package/dist/web/assets/{data-grid-overlay-editor-Brkf9z41.js → data-grid-overlay-editor-BqV0AxZg.js} +1 -1
  35. package/dist/web/assets/{database-viewer-Btc3h8Yu.js → database-viewer-BCvv_Bwf.js} +1 -1
  36. package/dist/web/assets/{dialog-DwtGlowE.js → dialog-DcLGDwaX.js} +1 -1
  37. package/dist/web/assets/diff-viewer-D41fCFAi.js +4 -0
  38. package/dist/web/assets/{docx-preview-DAD_vg59.js → docx-preview-DfaQn9v7.js} +1 -1
  39. package/dist/web/assets/{edge-Cq1ZKZuE.js → edge-Dbjp42Pt.js} +1 -1
  40. package/dist/web/assets/{edit-diff-preview-B_VE7ppQ.js → edit-diff-preview-BqcUuFCc.js} +1 -1
  41. package/dist/web/assets/{elixir-BM_JoJRk.js → elixir-na3b1pYy.js} +1 -1
  42. package/dist/web/assets/{elm-Dw7oO08Y.js → elm-_Nalil1R.js} +1 -1
  43. package/dist/web/assets/{erb-CfKOGAfk.js → erb-CW_7dubf.js} +1 -1
  44. package/dist/web/assets/{erlang-DlDmGVeE.js → erlang-DmZ7Zzb0.js} +1 -1
  45. package/dist/web/assets/{esm-DGAP0GFh.js → esm-CwyBetO1.js} +1 -1
  46. package/dist/web/assets/explorer-body-DB1L30pa.js +2 -0
  47. package/dist/web/assets/explorer-window-content-Cwlt3C3I.js +1 -0
  48. package/dist/web/assets/{extension-store-Cpfh2vMm.js → extension-store-DUaZINyC.js} +1 -1
  49. package/dist/web/assets/{extension-webview-B2OnjuCu.js → extension-webview-BW_mccb3.js} +1 -1
  50. package/dist/web/assets/{file-store-aOERIEo-.js → file-store-yReVbR8q.js} +1 -1
  51. package/dist/web/assets/{fortran-fixed-form-XnAhQWxS.js → fortran-fixed-form-DB2A8L4-.js} +1 -1
  52. package/dist/web/assets/{fsharp-CdNvy0PG.js → fsharp-BYHrti7z.js} +1 -1
  53. package/dist/web/assets/{gdresource-DxuG5i8i.js → gdresource-D5GBin40.js} +1 -1
  54. package/dist/web/assets/{git-commit-3lMonsur.js → git-commit-CUiOBoIL.js} +1 -1
  55. package/dist/web/assets/{git-log-panel-Co0PBPHC.js → git-log-panel-C1QDkmgl.js} +1 -1
  56. package/dist/web/assets/{git-rebase-N8Z9kIMf.js → git-rebase-DOxGnpdw.js} +1 -1
  57. package/dist/web/assets/gitGraph-HDMCJU4V-C1xf87gN.js +1 -0
  58. package/dist/web/assets/glide-data-grid-Dz8lx6n2.js +138 -0
  59. package/dist/web/assets/{glimmer-js-Dp9OXiib.js → glimmer-js-Begy4UT7.js} +1 -1
  60. package/dist/web/assets/{glimmer-ts-CDVKBcDj.js → glimmer-ts-rPApRY8N.js} +1 -1
  61. package/dist/web/assets/{glsl-DPJxI4us.js → glsl-wrbPvrBL.js} +1 -1
  62. package/dist/web/assets/{graphql-BrFhaQQl.js → graphql-Cu4-NR__.js} +1 -1
  63. package/dist/web/assets/group-chat-tab-DFwr-pig.js +1 -0
  64. package/dist/web/assets/{hack-BzXbDMlA.js → hack-Ddo93XKD.js} +1 -1
  65. package/dist/web/assets/{haml-CPpkX0s5.js → haml-BFX_1i8j.js} +1 -1
  66. package/dist/web/assets/{handlebars-BqrApy52.js → handlebars-5gFKA4H5.js} +1 -1
  67. package/dist/web/assets/{html-DmjUl-OA.js → html-C06I-zeO.js} +1 -1
  68. package/dist/web/assets/{html-derivative-Cnd6XFR5.js → html-derivative-e5JLAFZF.js} +1 -1
  69. package/dist/web/assets/{http-DMLWYun2.js → http-CrAAhSU7.js} +1 -1
  70. package/dist/web/assets/{hurl-CKkCVxbj.js → hurl-T1V3SX9T.js} +1 -1
  71. package/dist/web/assets/{hxml-CAx19TtQ.js → hxml-B056LLKN.js} +1 -1
  72. package/dist/web/assets/{image-preview-DHUoylQ1.js → image-preview-DWyuCYT3.js} +1 -1
  73. package/dist/web/assets/index-DWpNR3nB.css +2 -0
  74. package/dist/web/assets/index-oUKV5nIA.js +52 -0
  75. package/dist/web/assets/info-3K5VOQVL-C2z7FTDe.js +1 -0
  76. package/dist/web/assets/{input-DcEk7mA3.js → input-DouJ9Zvt.js} +1 -1
  77. package/dist/web/assets/{jinja-BmB9YN9l.js → jinja-DGdtRh43.js} +1 -1
  78. package/dist/web/assets/{jison-BaVsKKpw.js → jison-Ah6QobPS.js} +1 -1
  79. package/dist/web/assets/{julia-BgVBLveX.js → julia-BJeGOK0K.js} +1 -1
  80. package/dist/web/assets/{just-C2ENIbl9.js → just-iw9oRs7j.js} +1 -1
  81. package/dist/web/assets/{keybindings-store-BGIyqXHI.js → keybindings-store-0Wqfa7Qp.js} +1 -1
  82. package/dist/web/assets/keybindings-store-d54pv2NK.js +1 -0
  83. package/dist/web/assets/{latex-BiDIWxKL.js → latex-DHjj5hyN.js} +1 -1
  84. package/dist/web/assets/{liquid-NxpapreE.js → liquid-B-FFecDc.js} +1 -1
  85. package/dist/web/assets/{lua-DVepQvwH.js → lua-DDianJp_.js} +1 -1
  86. package/dist/web/assets/{markdown-renderer-DlcDhgrF.js → markdown-renderer-BHiUhQN8.js} +3 -3
  87. package/dist/web/assets/markdown-renderer-CnE36_gd.js +1 -0
  88. package/dist/web/assets/{marko-CJ2606Xl.js → marko-BW5SBVng.js} +1 -1
  89. package/dist/web/assets/{mdc-D2bjqclE.js → mdc-B9gL9bjl.js} +1 -1
  90. package/dist/web/assets/{media-url-H17XJF-I.js → media-url-DUK-k0bB.js} +1 -1
  91. package/dist/web/assets/{mobile-bottom-sheet-g-r2hCFa.js → mobile-bottom-sheet-CJ7ApOkI.js} +1 -1
  92. package/dist/web/assets/mobile-explorer-sheet-BzjlS1Xn.js +1 -0
  93. package/dist/web/assets/{nextflow-BtJwUbfF.js → nextflow-dTluY6qy.js} +1 -1
  94. package/dist/web/assets/{nginx-j4gyIhHE.js → nginx-DlwghGI-.js} +1 -1
  95. package/dist/web/assets/{nim-ClH_gjP5.js → nim-N9jddb5D.js} +1 -1
  96. package/dist/web/assets/notification-store-C7UBqxYP.js +1 -0
  97. package/dist/web/assets/{number-overlay-editor-C1EqJnWq.js → number-overlay-editor-BVXM7aAs.js} +1 -1
  98. package/dist/web/assets/{overview-panel-vTeeJYVZ.js → overview-panel-DCCDmtCQ.js} +1 -1
  99. package/dist/web/assets/packet-RMMSAZCW-DQUxN0wU.js +1 -0
  100. package/dist/web/assets/{panel-store-BSnkjgHt.js → panel-store-B4rijFKN.js} +1 -1
  101. package/dist/web/assets/{pdf-preview-CqFGRY0Z.js → pdf-preview-D7yLmAZW.js} +1 -1
  102. package/dist/web/assets/{perl-C9H7X7CT.js → perl-CkmpYKa_.js} +1 -1
  103. package/dist/web/assets/{php-Bx2U-cQ_.js → php-BwZ3KDPo.js} +1 -1
  104. package/dist/web/assets/pie-UPGHQEXC-BOr9ek4Q.js +1 -0
  105. package/dist/web/assets/{postgres-viewer-DHwcQucn.js → postgres-viewer--CRN1fvO.js} +3 -3
  106. package/dist/web/assets/{process-table-BGRJNlpn.js → process-table-CXC10GSy.js} +1 -1
  107. package/dist/web/assets/{project-store--nvQiy75.js → project-store-BuY7rxn_.js} +1 -1
  108. package/dist/web/assets/{pug-Cw74sKsE.js → pug-B3rBPqQo.js} +1 -1
  109. package/dist/web/assets/{qml-BXD94Eqz.js → qml-BfDbWFmg.js} +1 -1
  110. package/dist/web/assets/radar-KQ55EAFF-DSErxZFb.js +1 -0
  111. package/dist/web/assets/{razor-DZ0-Ge0S.js → razor-B1ww1r38.js} +1 -1
  112. package/dist/web/assets/{rst-Dtdh0QUj.js → rst-C0MSq6m-.js} +1 -1
  113. package/dist/web/assets/{ruby-BC51BW2Q.js → ruby-ClIZ1wb3.js} +1 -1
  114. package/dist/web/assets/{run-in-terminal-Sd3KyjN4.js → run-in-terminal-4efbW2Xv.js} +1 -1
  115. package/dist/web/assets/{sas-BN8i4qOU.js → sas-DR0j3o4_.js} +1 -1
  116. package/dist/web/assets/{scroll-area-0gljZ6lF.js → scroll-area-ZP8wcwTI.js} +1 -1
  117. package/dist/web/assets/{scss-H-QtA7Rz.js → scss-Vh3JLayR.js} +1 -1
  118. package/dist/web/assets/{send-to-chat-u6m8l-rH.js → send-to-chat-BNrEpAyC.js} +1 -1
  119. package/dist/web/assets/{settings-store-CHY6Kzin.js → settings-store-OLJE7dPH.js} +2 -2
  120. package/dist/web/assets/{settings-tab-IY6K1MiZ.js → settings-tab-B1rvHFc6.js} +1 -1
  121. package/dist/web/assets/settings-tab-BUlmPJ2w.js +1 -0
  122. package/dist/web/assets/{shaderlab-CalNsj_x.js → shaderlab-0rHRnQ18.js} +1 -1
  123. package/dist/web/assets/{shellsession-BFrcoR8x.js → shellsession-Dxdw6D8c.js} +1 -1
  124. package/dist/web/assets/{soy-CNbku7vP.js → soy-BHlSNj1y.js} +1 -1
  125. package/dist/web/assets/{sparql-BN_orcVp.js → sparql-B2QaPpni.js} +1 -1
  126. package/dist/web/assets/{sql-query-editor-C6pC3I8l.js → sql-query-editor-R_iKjPP9.js} +1 -1
  127. package/dist/web/assets/{sqlite-viewer-DTr5PzUb.js → sqlite-viewer-b2onA0SP.js} +1 -1
  128. package/dist/web/assets/{stata-C6a5Ijzq.js → stata-CwIxjsEJ.js} +1 -1
  129. package/dist/web/assets/{surrealql-ClplI_HY.js → surrealql-C6vo-rp-.js} +1 -1
  130. package/dist/web/assets/{svelte-DNm-UNXB.js → svelte-LLdJ6rlH.js} +1 -1
  131. package/dist/web/assets/{switch-DKvKya9y.js → switch-DV-BDjyk.js} +1 -1
  132. package/dist/web/assets/system-monitor-body-DPYQlBEt.js +2 -0
  133. package/dist/web/assets/system-monitor-tab-CPQdWv27.js +1 -0
  134. package/dist/web/assets/{system-monitor-window-content-BUqRsPK2.js → system-monitor-window-content-BPif0azo.js} +1 -1
  135. package/dist/web/assets/tab-host-window-content-BnAN6m4K.js +1 -0
  136. package/dist/web/assets/{tab-store-CJuJRapl.js → tab-store-tFnyPmm4.js} +1 -1
  137. package/dist/web/assets/team-member-sheet-Bu2wG1sm.js +2 -0
  138. package/dist/web/assets/{team-member-window-content-T4Q5JI-G.js → team-member-window-content-BSyU-5Uc.js} +1 -1
  139. package/dist/web/assets/{templ-CSdqVobu.js → templ-CJGvhEP5.js} +1 -1
  140. package/dist/web/assets/{terminal-tab-Bb2rZegQ.js → terminal-tab-Ncor0Rfu.js} +3 -3
  141. package/dist/web/assets/{tex-DzSJ4kcZ.js → tex-psXWhc4t.js} +1 -1
  142. package/dist/web/assets/{tool-cards-lhqhrDTT.js → tool-cards-DdVP1wrD.js} +4 -4
  143. package/dist/web/assets/treemap-KZPCXAKY-C17p0LQw.js +1 -0
  144. package/dist/web/assets/{ts-tags-C28w3bTg.js → ts-tags-D5Ar_ZCO.js} +1 -1
  145. package/dist/web/assets/{twig-i5jY3QXB.js → twig-vJgGC-qY.js} +1 -1
  146. package/dist/web/assets/{use-blob-url-Dsq9OjTh.js → use-blob-url-BOVGWg9F.js} +1 -1
  147. package/dist/web/assets/use-coarse-long-press-BmI_GVW5.js +1 -0
  148. package/dist/web/assets/use-monaco-theme-BshnLXoY.js +1 -0
  149. package/dist/web/assets/{use-open-team-member-DkiEjFYE.js → use-open-team-member-CY3wOdWL.js} +1 -1
  150. package/dist/web/assets/{use-websocket-B8R50f7f.js → use-websocket-B7xjNsyD.js} +1 -1
  151. package/dist/web/assets/{vendor-mermaid-By4AVvJY.js → vendor-mermaid-999ksD45.js} +1 -1
  152. package/dist/web/assets/video-preview-7F72sRwf.js +1 -0
  153. package/dist/web/assets/{vue-Ceyet_lv.js → vue-BWngFyav.js} +1 -1
  154. package/dist/web/assets/{vue-html-GuU7g_hg.js → vue-html-BN5ZLu-H.js} +1 -1
  155. package/dist/web/assets/{vue-vine-CxXoaJr2.js → vue-vine-BZHnO8yV.js} +1 -1
  156. package/dist/web/assets/window-pip-registry-BnYU83LE.js +1 -0
  157. package/dist/web/assets/{window-store-CsEW6jiE.js → window-store-CQ1lghvv.js} +1 -1
  158. package/dist/web/assets/{xml-CbqOVhkP.js → xml-DS2beZHp.js} +1 -1
  159. package/dist/web/assets/{xsl-KfAmCauy.js → xsl-Ciiku1Bq.js} +1 -1
  160. package/dist/web/index.html +47 -45
  161. package/dist/web/sw.js +1 -1
  162. package/docs/architecture/ai-chat-and-providers.md +309 -0
  163. package/docs/architecture/data-and-storage.md +356 -0
  164. package/docs/architecture/extensions.md +496 -0
  165. package/docs/architecture/integrations.md +214 -0
  166. package/docs/architecture/workspace-and-ui.md +399 -0
  167. package/docs/claude-agent-sdk-reference.md +1 -1
  168. package/docs/code-patterns.md +361 -0
  169. package/docs/code-standards.md +8 -358
  170. package/docs/codebase-subsystems.md +364 -0
  171. package/docs/codebase-summary.md +34 -379
  172. package/docs/deployment-guide.md +280 -490
  173. package/docs/design-guidelines.md +4 -3
  174. package/docs/lessons-learned.md +27 -10
  175. package/docs/project-changelog.md +18 -806
  176. package/docs/project-overview-pdr.md +117 -111
  177. package/docs/project-roadmap.md +89 -133
  178. package/docs/system-architecture.md +77 -1826
  179. package/package.json +2 -2
  180. package/src/cli/commands/autostart.ts +2 -2
  181. package/src/cli/commands/config-cmd.ts +12 -3
  182. package/src/cli/commands/status.ts +3 -2
  183. package/src/cli/commands/stop.ts +32 -4
  184. package/src/server/index.ts +17 -3
  185. package/src/server/routes/chat-unread.ts +36 -0
  186. package/src/server/routes/named-tunnel.ts +132 -0
  187. package/src/server/routes/tunnel.ts +13 -0
  188. package/src/server/ws/chat.ts +5 -2
  189. package/src/services/autostart-register.ts +47 -29
  190. package/src/services/cloud.service.ts +26 -1
  191. package/src/services/config-secret-keys.ts +48 -0
  192. package/src/services/config.service.ts +1 -1
  193. package/src/services/db.service.ts +62 -12
  194. package/src/services/edge-forwarder.ts +191 -191
  195. package/src/services/extension-rpc-handlers.ts +9 -1
  196. package/src/services/file-watcher/watch-tree.ts +67 -33
  197. package/src/services/file-watcher.service.ts +28 -10
  198. package/src/services/fs-credential-path-guard.ts +103 -0
  199. package/src/services/fs-path-guard.service.ts +14 -66
  200. package/src/services/named-tunnel/cloudflare-dns-api.ts +40 -0
  201. package/src/services/named-tunnel/cloudflare-zone-api.ts +28 -0
  202. package/src/services/named-tunnel/cloudflared-cert.ts +95 -0
  203. package/src/services/named-tunnel/cloudflared-exec.ts +50 -0
  204. package/src/services/named-tunnel/cloudflared-login-helpers.ts +98 -0
  205. package/src/services/named-tunnel/cloudflared-login.service.ts +182 -0
  206. package/src/services/named-tunnel/hostname-rules.ts +58 -0
  207. package/src/services/named-tunnel/login-output-parser.ts +36 -0
  208. package/src/services/named-tunnel/named-tunnel-args.ts +110 -0
  209. package/src/services/named-tunnel/named-tunnel-config.ts +67 -0
  210. package/src/services/named-tunnel/named-tunnel-probe-state.ts +95 -0
  211. package/src/services/named-tunnel/named-tunnel-readiness.ts +59 -0
  212. package/src/services/named-tunnel/named-tunnel-runtime.ts +42 -0
  213. package/src/services/named-tunnel/named-tunnel-setup-confirm.ts +98 -0
  214. package/src/services/named-tunnel/named-tunnel-setup.service.ts +191 -0
  215. package/src/services/supervisor-state.ts +67 -2
  216. package/src/services/supervisor.ts +454 -157
  217. package/src/services/tunnel-registry-parse.ts +27 -0
  218. package/src/services/tunnel-registry.service.ts +2 -15
  219. package/src/services/tunnel.service.ts +12 -3
  220. package/src/services/windows-process-tree.ts +351 -351
  221. package/src/types/config.ts +31 -0
  222. package/src/web/app.tsx +5 -0
  223. package/src/web/components/chat/chat-history-bar.tsx +95 -98
  224. package/src/web/components/chat/chat-toolbar-overflow-menu.tsx +57 -0
  225. package/src/web/components/chat/session-debug-dialog.tsx +35 -27
  226. package/src/web/components/floating-window/floating-window.tsx +8 -1
  227. package/src/web/components/floating-window/open-tab-in-pip.ts +78 -0
  228. package/src/web/components/floating-window/window-pip-registry.ts +46 -0
  229. package/src/web/components/layout/cloud-share-named-tunnel-row.tsx +111 -0
  230. package/src/web/components/layout/cloud-share-popover.tsx +56 -14
  231. package/src/web/components/layout/tab-pop-out-menu-item.tsx +25 -2
  232. package/src/web/components/shared/connection-lost-overlay.tsx +44 -5
  233. package/src/web/components/tunnels/named-tunnel/hostname-validation.ts +14 -0
  234. package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +96 -0
  235. package/src/web/components/tunnels/named-tunnel/named-tunnel-hostname-field.tsx +51 -0
  236. package/src/web/components/tunnels/named-tunnel/named-tunnel-login-step.tsx +105 -0
  237. package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +159 -0
  238. package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-content.tsx +197 -0
  239. package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-popup.tsx +57 -0
  240. package/src/web/components/tunnels/named-tunnel/named-tunnel-step-reducer.ts +191 -0
  241. package/src/web/components/tunnels/named-tunnel/use-named-tunnel-setup.ts +212 -0
  242. package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -0
  243. package/src/web/hooks/use-global-events.ts +3 -1
  244. package/src/web/lib/api-named-tunnel.ts +54 -0
  245. package/src/web/lib/last-known-local-url.ts +23 -0
  246. package/src/web/stores/notification-store.ts +20 -1
  247. package/dist/web/assets/api-client-DTZEgjBQ.js +0 -1
  248. package/dist/web/assets/api-settings-kd1j4iR8.js +0 -1
  249. package/dist/web/assets/architecture-PBZL5I3N-8dCMT9r7.js +0 -1
  250. package/dist/web/assets/chat-tab-E3bf_V8X.js +0 -15
  251. package/dist/web/assets/code-editor-BUlM2gXk.js +0 -10
  252. package/dist/web/assets/diff-viewer-CZaDnR4w.js +0 -4
  253. package/dist/web/assets/explorer-body-9aBmyVpZ.js +0 -2
  254. package/dist/web/assets/explorer-window-content-DRtqoSPA.js +0 -1
  255. package/dist/web/assets/gitGraph-HDMCJU4V-2e_8dfbs.js +0 -1
  256. package/dist/web/assets/glide-data-grid-C4_zIneN.js +0 -138
  257. package/dist/web/assets/group-chat-tab-rOd8J8GC.js +0 -1
  258. package/dist/web/assets/index-BImUaigO.js +0 -52
  259. package/dist/web/assets/index-CSfi6DSU.css +0 -2
  260. package/dist/web/assets/info-3K5VOQVL-BZi1Gha1.js +0 -1
  261. package/dist/web/assets/keybindings-store-CzV2sPls.js +0 -1
  262. package/dist/web/assets/markdown-renderer-CcbR33VG.js +0 -1
  263. package/dist/web/assets/mobile-explorer-sheet-SIwQXlwA.js +0 -1
  264. package/dist/web/assets/notification-store-DV5QtUTg.js +0 -1
  265. package/dist/web/assets/packet-RMMSAZCW-o5htEcin.js +0 -1
  266. package/dist/web/assets/pie-UPGHQEXC-BXAIFSAD.js +0 -1
  267. package/dist/web/assets/radar-KQ55EAFF-Dbt2Aebl.js +0 -1
  268. package/dist/web/assets/settings-tab-Dic6TKkU.js +0 -1
  269. package/dist/web/assets/system-monitor-body-iwGiCns9.js +0 -2
  270. package/dist/web/assets/system-monitor-tab-DdXQhEel.js +0 -1
  271. package/dist/web/assets/tab-host-window-content-BnqGPu8a.js +0 -1
  272. package/dist/web/assets/team-member-sheet-LkAFSCvb.js +0 -2
  273. package/dist/web/assets/treemap-KZPCXAKY-DUbnl9dz.js +0 -1
  274. package/dist/web/assets/use-monaco-theme-BZvlsoBv.js +0 -1
  275. package/dist/web/assets/video-preview-m0-3p7QZ.js +0 -1
  276. package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
  277. package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
  278. package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
  279. package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
  280. /package/dist/web/assets/{abap-C6nB04tn.js → abap-CjzrhAS2.js} +0 -0
  281. /package/dist/web/assets/{actionscript-3-Vx4rw2sC.js → actionscript-3-B59Kbl-9.js} +0 -0
  282. /package/dist/web/assets/{ada-Ccd8vM20.js → ada-B7gItKAE.js} +0 -0
  283. /package/dist/web/assets/{andromeeda-DnBX5xMg.js → andromeeda-Dy5zcy0Q.js} +0 -0
  284. /package/dist/web/assets/{apache-B-9_sx0g.js → apache-B6T9jrc0.js} +0 -0
  285. /package/dist/web/assets/{apex-_wPVBYoL.js → apex-CTsIxDQw.js} +0 -0
  286. /package/dist/web/assets/{api-client-aQb5hHE5.js → api-client-oY1tC3tM.js} +0 -0
  287. /package/dist/web/assets/{applescript-CcvD-qr0.js → applescript-DuycKmmb.js} +0 -0
  288. /package/dist/web/assets/{apply-theme-BeLRfsSu.js → apply-theme-r8tKDrCR.js} +0 -0
  289. /package/dist/web/assets/{ara-C40Ztoms.js → ara-CISL3599.js} +0 -0
  290. /package/dist/web/assets/{asciidoc-CybYkJV4.js → asciidoc-BXyE2Lpr.js} +0 -0
  291. /package/dist/web/assets/{asm-9eOKwzR0.js → asm-y258phtP.js} +0 -0
  292. /package/dist/web/assets/{aurora-x-B5ZH4t7t.js → aurora-x-Dq3Jc78M.js} +0 -0
  293. /package/dist/web/assets/{awk-Dqilob-2.js → awk-DxL6XSJv.js} +0 -0
  294. /package/dist/web/assets/{ayu-dark-CPgRhf5r.js → ayu-dark-DjoK6Evo.js} +0 -0
  295. /package/dist/web/assets/{ayu-light-Do06Q52f.js → ayu-light-CKKXBLMI.js} +0 -0
  296. /package/dist/web/assets/{ayu-mirage-_BhxcrxY.js → ayu-mirage-DGXSrrXB.js} +0 -0
  297. /package/dist/web/assets/{ballerina-DDwK54QW.js → ballerina-DZSXFmUf.js} +0 -0
  298. /package/dist/web/assets/{bat-BGyGlzGk.js → bat-BNsZk6nY.js} +0 -0
  299. /package/dist/web/assets/{beancount-CRWBdapI.js → beancount-cPl4gJct.js} +0 -0
  300. /package/dist/web/assets/{berry-DbSFn2gO.js → berry-DGAodERk.js} +0 -0
  301. /package/dist/web/assets/{bibtex-QpsV9gm9.js → bibtex-DftVoRif.js} +0 -0
  302. /package/dist/web/assets/{bicep-CxS8CUve.js → bicep-Suioyyzh.js} +0 -0
  303. /package/dist/web/assets/{bird2-CuZhQzYR.js → bird2-DjU24I2P.js} +0 -0
  304. /package/dist/web/assets/{c-9bnDqE3g.js → c-DFi8Y_nN.js} +0 -0
  305. /package/dist/web/assets/{c3-JMZTrOhA.js → c3-1eSHnH5V.js} +0 -0
  306. /package/dist/web/assets/{cadence-DsDNayJS.js → cadence-DLLsFwQP.js} +0 -0
  307. /package/dist/web/assets/{can-open-in-ppm-BJfpynya.js → can-open-in-ppm-in727eFQ.js} +0 -0
  308. /package/dist/web/assets/{catppuccin-frappe-DYIKXXBe.js → catppuccin-frappe-DDj0JEiY.js} +0 -0
  309. /package/dist/web/assets/{catppuccin-latte-IYYOnUJ8.js → catppuccin-latte-DPyANHyR.js} +0 -0
  310. /package/dist/web/assets/{catppuccin-macchiato-BrJ_7mfS.js → catppuccin-macchiato-CO-h8VTT.js} +0 -0
  311. /package/dist/web/assets/{catppuccin-mocha-gzSu7v7-.js → catppuccin-mocha-BCmbN2IZ.js} +0 -0
  312. /package/dist/web/assets/{clarity-DLzsC0Hb.js → clarity-C0pYsZjs.js} +0 -0
  313. /package/dist/web/assets/{clipboard-7fw0Shic.js → clipboard-DBLGKMrH.js} +0 -0
  314. /package/dist/web/assets/{clojure-DS1BvoTt.js → clojure-JFr6s8iZ.js} +0 -0
  315. /package/dist/web/assets/{cmake-CDvsJwYp.js → cmake-EEcLEgVy.js} +0 -0
  316. /package/dist/web/assets/{codeowners-BjsYiflJ.js → codeowners-Deh3Lx0u.js} +0 -0
  317. /package/dist/web/assets/{codeql-CG-7ARat.js → codeql-C317igT5.js} +0 -0
  318. /package/dist/web/assets/{common-lisp-DSO1Lbh9.js → common-lisp-DepbATZs.js} +0 -0
  319. /package/dist/web/assets/{coq-DYdh3oP_.js → coq-MxgxU5ya.js} +0 -0
  320. /package/dist/web/assets/{csharp-DxEBz5s0.js → csharp-eEFXdsAq.js} +0 -0
  321. /package/dist/web/assets/{css-dal3Q-8C.js → css-Db-syWW9.js} +0 -0
  322. /package/dist/web/assets/{csv-CpqjMGiA.js → csv-DHeYcbaq.js} +0 -0
  323. /package/dist/web/assets/{csv-parser-3umBA4NY.js → csv-parser-_JWMLGl3.js} +0 -0
  324. /package/dist/web/assets/{cue-DinoJ_5k.js → cue-BmUNowrt.js} +0 -0
  325. /package/dist/web/assets/{cypher-DzxqmkUW.js → cypher-XjXHgCNS.js} +0 -0
  326. /package/dist/web/assets/{d-BxNwJuEC.js → d-BWJnZtig.js} +0 -0
  327. /package/dist/web/assets/{dark-plus-Clmv3V-0.js → dark-plus-Db7umas1.js} +0 -0
  328. /package/dist/web/assets/{dart-BWVtXD35.js → dart-DSI4HYh1.js} +0 -0
  329. /package/dist/web/assets/{data-grid-types-B4n51Fck.js → data-grid-types-CfVHh9gv.js} +0 -0
  330. /package/dist/web/assets/{dax-DsImuWgJ.js → dax-yw1JsAni.js} +0 -0
  331. /package/dist/web/assets/{desktop-B0jJuG2r.js → desktop-C2QO4RvD.js} +0 -0
  332. /package/dist/web/assets/{diff-CWqAAxoE.js → diff-Drt8aEyp.js} +0 -0
  333. /package/dist/web/assets/{dist-B-EXdSYE.js → dist-DNRvhVdz.js} +0 -0
  334. /package/dist/web/assets/{dist-C0eNNfbt.js → dist-VBxOHLmN.js} +0 -0
  335. /package/dist/web/assets/{docker-CYFCJdIL.js → docker-ByDKbmLl.js} +0 -0
  336. /package/dist/web/assets/{dotenv-CKDUjURg.js → dotenv-BkfOLytR.js} +0 -0
  337. /package/dist/web/assets/{dracula-CSoPspHE.js → dracula-DAmrm-Un.js} +0 -0
  338. /package/dist/web/assets/{dracula-soft-BuFjw5L1.js → dracula-soft-BUOEnKqR.js} +0 -0
  339. /package/dist/web/assets/{dream-maker-B6PdDRZu.js → dream-maker-DQBIfSiO.js} +0 -0
  340. /package/dist/web/assets/{emacs-lisp-CMcXvD0G.js → emacs-lisp-Blh5kBD3.js} +0 -0
  341. /package/dist/web/assets/{everforest-dark-cU8-wYzi.js → everforest-dark-CEzhDxHq.js} +0 -0
  342. /package/dist/web/assets/{everforest-light-KhorllqK.js → everforest-light-CPk_5Khm.js} +0 -0
  343. /package/dist/web/assets/{external-link-B8BynhvY.js → external-link-B6Hl0U4X.js} +0 -0
  344. /package/dist/web/assets/{fennel-BCELZK87.js → fennel-B1MT_EBZ.js} +0 -0
  345. /package/dist/web/assets/{file-exclamation-point-CZ4DiR75.js → file-exclamation-point-JzhTU2TX.js} +0 -0
  346. /package/dist/web/assets/{fish-BQc0y_jy.js → fish-C_HGcewt.js} +0 -0
  347. /package/dist/web/assets/{fluent-BTINnwF6.js → fluent-Dp1Iclmx.js} +0 -0
  348. /package/dist/web/assets/{folder-symlink-B8XrtvQK.js → folder-symlink-Dx1hOk8X.js} +0 -0
  349. /package/dist/web/assets/{format-bytes-WVcFxJuT.js → format-bytes-53WBtyi2.js} +0 -0
  350. /package/dist/web/assets/{fortran-free-form-BQim9wDz.js → fortran-free-form-B_-UA4XC.js} +0 -0
  351. /package/dist/web/assets/{gdscript-03w5u8un.js → gdscript-Bm_JpjS_.js} +0 -0
  352. /package/dist/web/assets/{gdshader-Dp2daNzU.js → gdshader-C_PNmfYU.js} +0 -0
  353. /package/dist/web/assets/{genie-CoNcjPiD.js → genie-B5rmR2TF.js} +0 -0
  354. /package/dist/web/assets/{gherkin-NrPOSPQL.js → gherkin-BTJ34TJ9.js} +0 -0
  355. /package/dist/web/assets/{github-dark-z4RE9ZqA.js → github-dark-DOkNVj9H.js} +0 -0
  356. /package/dist/web/assets/{github-dark-default-DOKw-1cI.js → github-dark-default-DTsmiyVo.js} +0 -0
  357. /package/dist/web/assets/{github-dark-dimmed-DJRNMkEA.js → github-dark-dimmed-CjbxAe9s.js} +0 -0
  358. /package/dist/web/assets/{github-dark-high-contrast-Bj7jJ_Gs.js → github-dark-high-contrast-r_M8F_k7.js} +0 -0
  359. /package/dist/web/assets/{github-light-B4k_yMFu.js → github-light-BzfxyMyb.js} +0 -0
  360. /package/dist/web/assets/{github-light-default-BHc-Uzhs.js → github-light-default-BOqiBIy5.js} +0 -0
  361. /package/dist/web/assets/{github-light-high-contrast-CHisIywL.js → github-light-high-contrast-DQzBrDM4.js} +0 -0
  362. /package/dist/web/assets/{gleam-CfbypZ4q.js → gleam-BWmuAtD8.js} +0 -0
  363. /package/dist/web/assets/{gn-YoAjyhJa.js → gn-CNQPjiZW.js} +0 -0
  364. /package/dist/web/assets/{gnuplot-xVkc1f1c.js → gnuplot-BlHjcPMe.js} +0 -0
  365. /package/dist/web/assets/{go-GPCwP3Gu.js → go-Bn8Ba9uz.js} +0 -0
  366. /package/dist/web/assets/{groovy-Crh4Yww9.js → groovy-C1Y1_F4V.js} +0 -0
  367. /package/dist/web/assets/{gruvbox-dark-hard-BluUFuDS.js → gruvbox-dark-hard-DueOsHav.js} +0 -0
  368. /package/dist/web/assets/{gruvbox-dark-medium-Cxk1FWQp.js → gruvbox-dark-medium-DaQjD538.js} +0 -0
  369. /package/dist/web/assets/{gruvbox-dark-soft-CHXMiFmj.js → gruvbox-dark-soft-CbbX2WOm.js} +0 -0
  370. /package/dist/web/assets/{gruvbox-light-hard-B6eKPjAD.js → gruvbox-light-hard-E2M70ljZ.js} +0 -0
  371. /package/dist/web/assets/{gruvbox-light-medium-pDwMm1ut.js → gruvbox-light-medium-B_DDLY6h.js} +0 -0
  372. /package/dist/web/assets/{gruvbox-light-soft-Cxu6kHqK.js → gruvbox-light-soft-2SZmyC1F.js} +0 -0
  373. /package/dist/web/assets/{haskell-C6JKHjbK.js → haskell-D8DeoXtG.js} +0 -0
  374. /package/dist/web/assets/{haxe-BuBNdF0a.js → haxe-BmZm7B0W.js} +0 -0
  375. /package/dist/web/assets/{hcl-DZpDdGj2.js → hcl-CKnBM3ng.js} +0 -0
  376. /package/dist/web/assets/{hjson-CBkhYRuo.js → hjson-Ct9t4-ci.js} +0 -0
  377. /package/dist/web/assets/{hlsl-C5nMxnp9.js → hlsl-BZSASBqS.js} +0 -0
  378. /package/dist/web/assets/{horizon-VAFvA6R6.js → horizon-CzX1AZ08.js} +0 -0
  379. /package/dist/web/assets/{horizon-bright-BC0xn7T3.js → horizon-bright-CCVNeRIP.js} +0 -0
  380. /package/dist/web/assets/{houston-BxrUivmz.js → houston-DBlJN7xc.js} +0 -0
  381. /package/dist/web/assets/{hy-DLtOXoaH.js → hy-Dyc6h6Xb.js} +0 -0
  382. /package/dist/web/assets/{image-gallery-Gnf7TdCR.js → image-gallery-DujlrjOC.js} +0 -0
  383. /package/dist/web/assets/{imba-BdpFXM2W.js → imba-Bauru7s2.js} +0 -0
  384. /package/dist/web/assets/{ini-BInJO_T0.js → ini-SjdwtaTp.js} +0 -0
  385. /package/dist/web/assets/{java-Dxuddnkb.js → java-D2r0ocpf.js} +0 -0
  386. /package/dist/web/assets/{javascript-CVamJfLY.js → javascript-y-lof7iP.js} +0 -0
  387. /package/dist/web/assets/{json-BLZ0ozYk.js → json-DO4LSx5A.js} +0 -0
  388. /package/dist/web/assets/{json5-BnXzmMKs.js → json5-BEwWf-W3.js} +0 -0
  389. /package/dist/web/assets/{jsonc-CSgkSga7.js → jsonc-OBEFv7gP.js} +0 -0
  390. /package/dist/web/assets/{jsonl-CY5W2nF2.js → jsonl-DOyW4Mg3.js} +0 -0
  391. /package/dist/web/assets/{jsonnet-CEJYH3sF.js → jsonnet-BL5R2CpP.js} +0 -0
  392. /package/dist/web/assets/{jssm-CsJc7Uhk.js → jssm-ksPusAyg.js} +0 -0
  393. /package/dist/web/assets/{jsx-jCiRaHU7.js → jsx-B6XEu6O-.js} +0 -0
  394. /package/dist/web/assets/{kanagawa-dragon-DfoA_GxX.js → kanagawa-dragon-JBVe_ji_.js} +0 -0
  395. /package/dist/web/assets/{kanagawa-lotus-BOLYJpWd.js → kanagawa-lotus-ChprO68_.js} +0 -0
  396. /package/dist/web/assets/{kanagawa-wave-DfKlbT1G.js → kanagawa-wave-D_uB_R0Q.js} +0 -0
  397. /package/dist/web/assets/{kdl-CmssIFTi.js → kdl-pf5B8N_m.js} +0 -0
  398. /package/dist/web/assets/{kotlin-BRsNgp6x.js → kotlin-BdF0METC.js} +0 -0
  399. /package/dist/web/assets/{kusto-T8X9jUab.js → kusto-BneVP-OU.js} +0 -0
  400. /package/dist/web/assets/{laserwave-DkJMVJ8Z.js → laserwave-C4-UPIyG.js} +0 -0
  401. /package/dist/web/assets/{lean-C65KGVPq.js → lean-BN6Jdymq.js} +0 -0
  402. /package/dist/web/assets/{less-exIPdDlf.js → less-Cx-fdlDC.js} +0 -0
  403. /package/dist/web/assets/{lib-mC1NZfuw.js → lib-CMuBa85c.js} +0 -0
  404. /package/dist/web/assets/{libesm-DSutUtl1.js → libesm-BB74PRsH.js} +0 -0
  405. /package/dist/web/assets/{light-plus-DiHUXAZt.js → light-plus-DFnnn_iL.js} +0 -0
  406. /package/dist/web/assets/{llvm-CJBnEzFM.js → llvm-BGxtW0Nw.js} +0 -0
  407. /package/dist/web/assets/{loader-circle-gSPVxfvH.js → loader-circle-Dpqcv0eS.js} +0 -0
  408. /package/dist/web/assets/{lock-Did-87UV.js → lock-DldbT31x.js} +0 -0
  409. /package/dist/web/assets/{log-DxMyUMuP.js → log-kQb4eJnj.js} +0 -0
  410. /package/dist/web/assets/{logo-CC9NKzca.js → logo-DqAzaq7s.js} +0 -0
  411. /package/dist/web/assets/{luau-Bw10I6zf.js → luau-Dc2cp-zS.js} +0 -0
  412. /package/dist/web/assets/{make-C_6M90m6.js → make-DYg9dwaJ.js} +0 -0
  413. /package/dist/web/assets/{markdown-bR9_R-LF.js → markdown-O4EPpS3K.js} +0 -0
  414. /package/dist/web/assets/{material-theme-BUx2Qujn.js → material-theme-ClP8yfQ2.js} +0 -0
  415. /package/dist/web/assets/{material-theme-darker-CRJrPgKm.js → material-theme-darker-LOK55hGu.js} +0 -0
  416. /package/dist/web/assets/{material-theme-lighter-CUAYppPa.js → material-theme-lighter-j72tzZqi.js} +0 -0
  417. /package/dist/web/assets/{material-theme-ocean-DuINF1fg.js → material-theme-ocean-C19LKClA.js} +0 -0
  418. /package/dist/web/assets/{material-theme-palenight-W6_icGCj.js → material-theme-palenight-DJ5tN2ax.js} +0 -0
  419. /package/dist/web/assets/{matlab-rfqn_b3z.js → matlab-DQNZ_NU8.js} +0 -0
  420. /package/dist/web/assets/{mdx-Bh1B_YOJ.js → mdx-CJayB7XK.js} +0 -0
  421. /package/dist/web/assets/{mermaid-Dxf6Ya2k.js → mermaid-Cb0TRX92.js} +0 -0
  422. /package/dist/web/assets/{min-dark-CivZ2L0h.js → min-dark-CWM8Bo-m.js} +0 -0
  423. /package/dist/web/assets/{min-light-B1Ykqtma.js → min-light-BBR1sjS2.js} +0 -0
  424. /package/dist/web/assets/{mipsasm-DynHaM0d.js → mipsasm-Jc7u7YSS.js} +0 -0
  425. /package/dist/web/assets/{mojo-Clw5PbGH.js → mojo-CTB0cutd.js} +0 -0
  426. /package/dist/web/assets/{monitor-Dv8vn_6O.js → monitor-4vWfqzuN.js} +0 -0
  427. /package/dist/web/assets/{monokai-DFIooFZT.js → monokai-CvM_YNKx.js} +0 -0
  428. /package/dist/web/assets/{moonbit-DnBSZWGM.js → moonbit-CbFEfYYM.js} +0 -0
  429. /package/dist/web/assets/{move-CCcL2PfL.js → move-BT38p0kT.js} +0 -0
  430. /package/dist/web/assets/{narrat-BDarg5Lk.js → narrat-55QZ_kOG.js} +0 -0
  431. /package/dist/web/assets/{nextflow-groovy-DrZOeCNW.js → nextflow-groovy-Bg8peyqg.js} +0 -0
  432. /package/dist/web/assets/{night-owl-BnZZCK7v.js → night-owl-DzS77iPK.js} +0 -0
  433. /package/dist/web/assets/{night-owl-light-CEwBsVHS.js → night-owl-light-DER7fGnx.js} +0 -0
  434. /package/dist/web/assets/{nix-amCzOPMR.js → nix-BSI8YesN.js} +0 -0
  435. /package/dist/web/assets/{nord-BIs2BC6q.js → nord-9bn7iNWo.js} +0 -0
  436. /package/dist/web/assets/{nushell-Y02fo4yw.js → nushell-qDVZn_lw.js} +0 -0
  437. /package/dist/web/assets/{objective-c-7xOZtWW1.js → objective-c-Blf-ATos.js} +0 -0
  438. /package/dist/web/assets/{objective-cpp-eVtjUvVB.js → objective-cpp-Bz8bAq47.js} +0 -0
  439. /package/dist/web/assets/{ocaml-Bi6ZwqG4.js → ocaml-B9heAsc3.js} +0 -0
  440. /package/dist/web/assets/{odin-8xf1Y0S9.js → odin-BaFYnOwO.js} +0 -0
  441. /package/dist/web/assets/{one-dark-pro-hVv-_y_b.js → one-dark-pro-D6B_hSe-.js} +0 -0
  442. /package/dist/web/assets/{one-light-CJJ3MtHt.js → one-light-Ba-f0wyk.js} +0 -0
  443. /package/dist/web/assets/{openscad-B2HcywLk.js → openscad-UvYyL2MK.js} +0 -0
  444. /package/dist/web/assets/{pascal-C7fllV_7.js → pascal-B4-qvEoG.js} +0 -0
  445. /package/dist/web/assets/{pkl-qvLC1IWP.js → pkl-B146nqGx.js} +0 -0
  446. /package/dist/web/assets/{plastic-BT1wHZv2.js → plastic-L38t-Id3.js} +0 -0
  447. /package/dist/web/assets/{play-4MZi1Axp.js → play-CpCAoQRZ.js} +0 -0
  448. /package/dist/web/assets/{plsql-1MsmdmJD.js → plsql-ItOG5Cca.js} +0 -0
  449. /package/dist/web/assets/{plus-BhMZqFsN.js → plus-BR0qewJI.js} +0 -0
  450. /package/dist/web/assets/{po-BigskytD.js → po-imiCTaNX.js} +0 -0
  451. /package/dist/web/assets/{poimandres-DuklniF7.js → poimandres-Bu6sIba9.js} +0 -0
  452. /package/dist/web/assets/{polar-e2pjN_sp.js → polar-CTeGoFxV.js} +0 -0
  453. /package/dist/web/assets/{portal-container-context-Cm5HipFr.js → portal-container-context-a399Hd9F.js} +0 -0
  454. /package/dist/web/assets/{postcss-DtAeYjWw.js → postcss-jvFe6GNE.js} +0 -0
  455. /package/dist/web/assets/{powerquery-Cr2PtehI.js → powerquery-D9Dw1hJp.js} +0 -0
  456. /package/dist/web/assets/{powershell-BxFTGLJ4.js → powershell-CZqvYQtg.js} +0 -0
  457. /package/dist/web/assets/{prisma-COWJiwdZ.js → prisma-B4D2nO_7.js} +0 -0
  458. /package/dist/web/assets/{prolog-DFvGKRx6.js → prolog-CQbyUcCF.js} +0 -0
  459. /package/dist/web/assets/{proto-CcK80lFt.js → proto-Cwpo0cW1.js} +0 -0
  460. /package/dist/web/assets/{puppet-BF60bZ4F.js → puppet-CKxCwBod.js} +0 -0
  461. /package/dist/web/assets/{purescript-zL_69MzU.js → purescript-B-MyXMMp.js} +0 -0
  462. /package/dist/web/assets/{python-CIwKhfb_.js → python-D6QbSR2S.js} +0 -0
  463. /package/dist/web/assets/{qmldir-CXNe04Lo.js → qmldir-BpTSVcw_.js} +0 -0
  464. /package/dist/web/assets/{qss-C8lAWe6G.js → qss-DBc4gbpe.js} +0 -0
  465. /package/dist/web/assets/{r-CL5ZVL3_.js → r-i9jMMi0s.js} +0 -0
  466. /package/dist/web/assets/{racket-BBfh0uh0.js → racket-BWNaxU7C.js} +0 -0
  467. /package/dist/web/assets/{raku-o6E8R5Q3.js → raku-fqTZLn7L.js} +0 -0
  468. /package/dist/web/assets/{react-CBTnT4sY.js → react-X4jUntEf.js} +0 -0
  469. /package/dist/web/assets/{red-GomRF2Xp.js → red-D-JEcuzA.js} +0 -0
  470. /package/dist/web/assets/{refresh-cw-B06yaBrW.js → refresh-cw-B9iv7csb.js} +0 -0
  471. /package/dist/web/assets/{reg-_ffDIvx9.js → reg-BbirlU1S.js} +0 -0
  472. /package/dist/web/assets/{regexp-Bl5sF65W.js → regexp-D8VTFrtg.js} +0 -0
  473. /package/dist/web/assets/{rel-DgMdqDuj.js → rel-ByQGKiNM.js} +0 -0
  474. /package/dist/web/assets/{resolve-theme-CRt_7wpd.js → resolve-theme-Cqb9zWYc.js} +0 -0
  475. /package/dist/web/assets/{riscv-8b3-Ye4X.js → riscv-B5nqCfLE.js} +0 -0
  476. /package/dist/web/assets/{ron-DrmU1KHP.js → ron-DOVh_iZR.js} +0 -0
  477. /package/dist/web/assets/{rose-pine-dawn-DEfhNlFH.js → rose-pine-dawn-B7hkmmWy.js} +0 -0
  478. /package/dist/web/assets/{rose-pine-C_-E2tF1.js → rose-pine-jp51ZUzi.js} +0 -0
  479. /package/dist/web/assets/{rose-pine-moon-BdvX3gdn.js → rose-pine-moon-Ff063yLj.js} +0 -0
  480. /package/dist/web/assets/{rosmsg-B6GhGpd5.js → rosmsg-Cyk7Il8z.js} +0 -0
  481. /package/dist/web/assets/{rotate-ccw-COXu_9oj.js → rotate-ccw-DnuPzJRE.js} +0 -0
  482. /package/dist/web/assets/{rust-DmC97M1-.js → rust-DRNM5epF.js} +0 -0
  483. /package/dist/web/assets/{sass-B0tJOHLM.js → sass-D_cB0-M1.js} +0 -0
  484. /package/dist/web/assets/{save-CtIGOjmx.js → save-DxonD4y1.js} +0 -0
  485. /package/dist/web/assets/{scala-n8zaFOXv.js → scala-Crl-1kKS.js} +0 -0
  486. /package/dist/web/assets/{scheme-BKD9ztBU.js → scheme-DCaWyEJ0.js} +0 -0
  487. /package/dist/web/assets/{sdbl-D8gB5-rS.js → sdbl-BbXNqwtX.js} +0 -0
  488. /package/dist/web/assets/{send-BlyNJAzy.js → send-B-t5Mz26.js} +0 -0
  489. /package/dist/web/assets/{shallow-DYo6goJ4.js → shallow-4zAfs2KW.js} +0 -0
  490. /package/dist/web/assets/{shellscript-BfXlf_Ao.js → shellscript-BAgksBhW.js} +0 -0
  491. /package/dist/web/assets/{shield-check-B0iGUiOK.js → shield-check-CdTiK1j2.js} +0 -0
  492. /package/dist/web/assets/{shield-off-BNLWBhcq.js → shield-off-DuetkXKM.js} +0 -0
  493. /package/dist/web/assets/{slack-dark-CylEddse.js → slack-dark-GB-ElUW_.js} +0 -0
  494. /package/dist/web/assets/{slack-ochin-w8wg8vuC.js → slack-ochin-tU6CFD3S.js} +0 -0
  495. /package/dist/web/assets/{smalltalk-hiAXA64u.js → smalltalk-DSVGUbtL.js} +0 -0
  496. /package/dist/web/assets/{snazzy-light-BwlV3Xty.js → snazzy-light-DZhi3PoS.js} +0 -0
  497. /package/dist/web/assets/{solarized-dark-vmNGdykz.js → solarized-dark-D6AMCco7.js} +0 -0
  498. /package/dist/web/assets/{solarized-light-DGv_0JXq.js → solarized-light-DepuTWSE.js} +0 -0
  499. /package/dist/web/assets/{solidity-Y7QSH0B1.js → solidity-CsxN618l.js} +0 -0
  500. /package/dist/web/assets/{splunk-BtgpLjHi.js → splunk-CF1m0Kkv.js} +0 -0
  501. /package/dist/web/assets/{sql-CRHYhOia.js → sql-DvyMDgrB.js} +0 -0
  502. /package/dist/web/assets/{ssh-config-DOqSs3au.js → ssh-config-a8sDo-Q8.js} +0 -0
  503. /package/dist/web/assets/{stylus-crCAEaBT.js → stylus-DCJ-6AjT.js} +0 -0
  504. /package/dist/web/assets/{swift-eE3rT7TC.js → swift-c-ZFxyPP.js} +0 -0
  505. /package/dist/web/assets/{synthwave-84-BWy14LBX.js → synthwave-84-9NPgBcuv.js} +0 -0
  506. /package/dist/web/assets/{system-verilog-DC0-DZM4.js → system-verilog-DPD02IOo.js} +0 -0
  507. /package/dist/web/assets/{systemd-to5p6N8I.js → systemd-BUr5HRtU.js} +0 -0
  508. /package/dist/web/assets/{table-Bbc783ko.js → table-FLU8ei8b.js} +0 -0
  509. /package/dist/web/assets/{talonscript-DoTcU8wm.js → talonscript-BNBQV5YJ.js} +0 -0
  510. /package/dist/web/assets/{tasl-cNZvUs95.js → tasl-BETjpSyu.js} +0 -0
  511. /package/dist/web/assets/{tcl-mZE8sZH8.js → tcl-Clp_AZiC.js} +0 -0
  512. /package/dist/web/assets/{terraform-CqHPpIfu.js → terraform-B0kZ0lim.js} +0 -0
  513. /package/dist/web/assets/{text-wrap-CBT4Gxtg.js → text-wrap-B05VGRa-.js} +0 -0
  514. /package/dist/web/assets/{tokyo-night-Tp2rTIZd.js → tokyo-night-oOJ0a7Dk.js} +0 -0
  515. /package/dist/web/assets/{toml-CU_vscgn.js → toml-DOPCSumy.js} +0 -0
  516. /package/dist/web/assets/{trash-2-Bk4yYlck.js → trash-2-CTXlTflF.js} +0 -0
  517. /package/dist/web/assets/{triangle-alert-B8EYecpe.js → triangle-alert-BRq0VAfd.js} +0 -0
  518. /package/dist/web/assets/{tsv-ik7iFB9D.js → tsv-D6qUGf84.js} +0 -0
  519. /package/dist/web/assets/{tsx-BMvvMKiv.js → tsx-B6qYhT6O.js} +0 -0
  520. /package/dist/web/assets/{turtle-DBqKCq9o.js → turtle-C0Rjzw73.js} +0 -0
  521. /package/dist/web/assets/{typescript-DXJDmMZZ.js → typescript-BiydShEu.js} +0 -0
  522. /package/dist/web/assets/{typespec-CBseCMtZ.js → typespec-DjmF6Aww.js} +0 -0
  523. /package/dist/web/assets/{typst-4oY25U9y.js → typst-CPuscfcL.js} +0 -0
  524. /package/dist/web/assets/{use-is-mobile-D1weanbj.js → use-is-mobile-b94q8RvF.js} +0 -0
  525. /package/dist/web/assets/{utils-BNRH507E.js → utils-C9OjCrou.js} +0 -0
  526. /package/dist/web/assets/{v-BXaZ_1oy.js → v-DE4vpLLQ.js} +0 -0
  527. /package/dist/web/assets/{vala-BImWJE7z.js → vala-CmUF2pS5.js} +0 -0
  528. /package/dist/web/assets/{vb-CLfj2_sy.js → vb-BvcRoX4b.js} +0 -0
  529. /package/dist/web/assets/{vendor-xterm-_yyu5n32.js → vendor-xterm-K8kpLxdb.js} +0 -0
  530. /package/dist/web/assets/{verilog-BWa8Ed2b.js → verilog-BGIzyH3R.js} +0 -0
  531. /package/dist/web/assets/{vesper-BGj1cg6m.js → vesper-CG7IrHQQ.js} +0 -0
  532. /package/dist/web/assets/{vhdl-mByK7mlx.js → vhdl-DMSumomw.js} +0 -0
  533. /package/dist/web/assets/{viml-CugmlW5g.js → viml-Drt7AEmU.js} +0 -0
  534. /package/dist/web/assets/{vitesse-black-vPdpy9TL.js → vitesse-black-DmsjfN0b.js} +0 -0
  535. /package/dist/web/assets/{vitesse-dark-BuQK3dUh.js → vitesse-dark-Dl55_WeJ.js} +0 -0
  536. /package/dist/web/assets/{vitesse-light-B3nz1XL9.js → vitesse-light-Crbv9aGg.js} +0 -0
  537. /package/dist/web/assets/{vyper-DH_GTIHG.js → vyper-CB7psEJj.js} +0 -0
  538. /package/dist/web/assets/{wasm-DfVbVJlW.js → wasm-BY1tuM5m.js} +0 -0
  539. /package/dist/web/assets/{wasm-C-LF9GjX.js → wasm-Dck8KPgP.js} +0 -0
  540. /package/dist/web/assets/{wenyan-CzQkHyHx.js → wenyan-DhewQ68b.js} +0 -0
  541. /package/dist/web/assets/{wgsl-Du_M1DW9.js → wgsl-DlmiP456.js} +0 -0
  542. /package/dist/web/assets/{wikitext-BWN_w5S7.js → wikitext-cQCnqNgE.js} +0 -0
  543. /package/dist/web/assets/{wit-BR13v5HD.js → wit-S5MaCngB.js} +0 -0
  544. /package/dist/web/assets/{wolfram-BoXxp_Xl.js → wolfram-Bh8n_9zr.js} +0 -0
  545. /package/dist/web/assets/{x-nhriOX9o.js → x-YDu4m2Iq.js} +0 -0
  546. /package/dist/web/assets/{yaml-DpR_EF-d.js → yaml-BoK_17l9.js} +0 -0
  547. /package/dist/web/assets/{zenscript-C-EdUtUd.js → zenscript-DB3ZFwNJ.js} +0 -0
  548. /package/dist/web/assets/{zig-BD4eA5nU.js → zig-Bv1BWmfT.js} +0 -0
@@ -8,7 +8,7 @@ import type { Subprocess } from "bun";
8
8
  import { resolve } from "node:path";
9
9
  import {
10
10
  readFileSync, writeFileSync, existsSync, mkdirSync, openSync, closeSync, appendFileSync,
11
- unlinkSync,
11
+ unlinkSync, statSync,
12
12
  } from "node:fs";
13
13
  import { getPpmDir } from "./ppm-dir.ts";
14
14
  import { isCompiledBinary } from "./autostart-generator.ts";
@@ -19,6 +19,12 @@ import {
19
19
  readAndDeleteCmd, readStatus, updateStatus, writeStatus,
20
20
  STATUS_FILE, PID_FILE,
21
21
  } from "./supervisor-state.ts";
22
+ import type { ResolvedTunnelConfig, TunnelMode } from "./named-tunnel/named-tunnel-config.ts";
23
+ import { readTunnelConfigFresh, chooseTunnelSpawn } from "./named-tunnel/named-tunnel-runtime.ts";
24
+ import { waitForLogLine } from "./named-tunnel/named-tunnel-readiness.ts";
25
+ import { decideNamedProbeAction, publicHostnameIsOurs } from "./named-tunnel/named-tunnel-probe-state.ts";
26
+ import { isCloudflaredPid } from "./tunnel-registry.service.ts";
27
+ import { getQuickTunnelArgs } from "./cloudflared.service.ts";
22
28
  import { startStoppedPage, stopStoppedPage } from "./supervisor-stopped-page.ts";
23
29
  import { sdNotify } from "./sd-notify.ts";
24
30
  import {
@@ -43,6 +49,7 @@ const SERVER_REVIVE_AFTER_MS = 90_000; // > BACKOFF_MAX_MS, so crash backof
43
49
  const TUNNEL_PROBE_INTERVAL_MS = 30_000; // 30s — adopted tunnels have no `exited` promise
44
50
  const TUNNEL_ZOMBIE_THRESHOLD = 10; // ~5min @ 30s probe — only regenerate a truly-zombied URL (process alive, edge dropped). cloudflared self-heals transient QUIC drops, so don't kill it early.
45
51
  const TUNNEL_URL_REGEX = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/;
52
+ const NAMED_TUNNEL_READY_REGEX = /Registered tunnel connection/;
46
53
  const UPGRADE_CHECK_INTERVAL_MS = 900_000; // 15min
47
54
  const UPGRADE_SKIP_INITIAL_MS = 300_000; // 5min delay before first check
48
55
  const SELF_REPLACE_TIMEOUT_MS = 30_000; // 30s to wait for new supervisor
@@ -81,6 +88,25 @@ let tunnelGeneration = 0;
81
88
  let lastTunnelRegenAt = 0;
82
89
  const TUNNEL_REGEN_MIN_INTERVAL_MS = 300_000; // 5min
83
90
 
91
+ // Named-tunnel state. `namedTunnelMode` is a synchronous cache refreshed at
92
+ // exactly three points (startup, start of spawnTunnel, retunnel dispatch) —
93
+ // every OTHER reader (restartTunnel, adoptTunnel, the probe) must read this
94
+ // cache, never `configService` (a separate process, stale here) nor a fresh
95
+ // async DB read (would reorder the startup adopt/probe-before-spawn race).
96
+ let namedTunnelMode: ResolvedTunnelConfig | null = null;
97
+ // The mode of the tunnel actually spawned (as opposed to `namedTunnelMode`,
98
+ // which is merely the persisted intent) — a token failure leaves config=named
99
+ // but live=quick, and every status write / throttle / restart decision must
100
+ // key off what's actually running.
101
+ let lastSpawnMode: TunnelMode = "quick";
102
+ // Set once the named probe has already tried a restart-and-hope; a second
103
+ // consecutive failure then warns and stops instead of looping kills forever.
104
+ let namedProbeRestartAttempted = false;
105
+ // Two distinct states, deliberately worded differently: the first is "we are
106
+ // looking into it", the second is "we tried and it is still dark".
107
+ const NAMED_WARNING_CHECKING = "checking the connection to your domain…";
108
+ const NAMED_WARNING_UNREACHABLE = "hostname unreachable — check DNS/Cloudflare";
109
+
84
110
  // Module-level refs for softStop (needs access to respawn args)
85
111
  let _serverArgs: string[] = [];
86
112
  let _logFd: number = -1;
@@ -268,7 +294,14 @@ function restartTunnel(port: number) {
268
294
  if (tunnelChild) { try { tunnelChild.kill(); } catch {} tunnelChild = null; }
269
295
  if (adoptedTunnelPid) { try { process.kill(adoptedTunnelPid, "SIGTERM"); } catch {} adoptedTunnelPid = null; }
270
296
  tunnelUrl = null;
271
- updateStatus({ shareUrl: null, tunnelPid: null, tunnelPort: null });
297
+ if (lastSpawnMode === "named") {
298
+ // The named URL is pinned to the configured hostname — it must never
299
+ // flicker null mid-respawn, or every client watching status.json sees a
300
+ // dead share link for the few seconds the connector takes to reconnect.
301
+ updateStatus({ tunnelPid: null });
302
+ } else {
303
+ updateStatus({ shareUrl: null, tunnelPid: null, tunnelPort: null });
304
+ }
272
305
  spawnTunnel(port).catch((e) => log("ERROR", `restartTunnel failed: ${e}`));
273
306
  }
274
307
 
@@ -586,25 +619,16 @@ export async function spawnServer(
586
619
  const cloudflaredLogPath = () => resolve(getPpmDir(), "cloudflared.log");
587
620
 
588
621
  /**
589
- * Poll cloudflared log file for trycloudflare URL.
590
- * Stderr is redirected to this file (not piped) so cloudflared survives
591
- * parent supervisor exit during self-replace (no SIGPIPE on closed pipe).
622
+ * Wait for the quick-mode trycloudflare URL in the (offset-anchored) log —
623
+ * thin wrapper over phase 2a's `waitForLogLine` so quick mode keeps its own
624
+ * name/shape while sharing the stale-log-safe implementation with named mode.
592
625
  */
593
- async function extractUrlFromLogFile(getExitCode: () => number | null): Promise<string> {
594
- const path = cloudflaredLogPath();
595
- const deadline = Date.now() + 30_000;
596
- while (Date.now() < deadline) {
597
- if (existsSync(path)) {
598
- try {
599
- const content = readFileSync(path, "utf8");
600
- const match = content.match(TUNNEL_URL_REGEX);
601
- if (match) return match[0];
602
- } catch {}
603
- }
604
- if (getExitCode() !== null) throw new Error("cloudflared exited without providing URL");
605
- await Bun.sleep(200);
606
- }
607
- throw new Error("Tunnel URL timeout (30s)");
626
+ async function extractUrlFromLogFile(offset: number, getExitCode: () => number | null): Promise<string> {
627
+ return waitForLogLine(cloudflaredLogPath(), TUNNEL_URL_REGEX, {
628
+ fromByteOffset: offset,
629
+ timeoutMs: 30_000,
630
+ getExitCode,
631
+ });
608
632
  }
609
633
 
610
634
  async function syncUrlToCloud(url: string) {
@@ -620,26 +644,109 @@ async function syncUrlToCloud(url: string) {
620
644
 
621
645
  // HTTP heartbeat removed — WS is the sole heartbeat mechanism (Phase 4)
622
646
 
647
+ /**
648
+ * Shared backoff-and-retry after a spawn attempt (or attempt sequence, named
649
+ * then quick fallback) produced no usable URL. Reuses the same crash budget
650
+ * quick-only mode always used — a persistent failure still retries forever,
651
+ * just slower — so named mode's extra fallback attempt never changes the
652
+ * retry cadence quick-only installs already depend on.
653
+ */
654
+ async function retryTunnelAfterFailure(generation: number): Promise<void> {
655
+ if (shuttingDown) return;
656
+ const now = Date.now();
657
+ if (now - lastTunnelCrash > STABLE_WINDOW_MS) tunnelRestarts = 0;
658
+ lastTunnelCrash = now;
659
+ tunnelRestarts++;
660
+ if (tunnelRestarts > MAX_RESTARTS) tunnelRestarts = MAX_RESTARTS;
661
+ const delay = backoffDelay(tunnelRestarts) + Math.floor(Math.random() * 1000);
662
+ log("WARN", `Tunnel failed, retry in ${delay}ms (#${tunnelRestarts})`);
663
+ await Bun.sleep(delay);
664
+ if (generation !== tunnelGeneration) return; // superseded during backoff
665
+ // Re-read the live server port: spawnServer may have moved it since this
666
+ // attempt started, and a retry at the stale port would split-brain the tunnel.
667
+ return spawnTunnel(_opts.port, generation);
668
+ }
669
+
670
+ /** One spawn attempt's shape — named (identity from config) or quick (identity from log regex). */
671
+ export interface TunnelAttempt {
672
+ mode: TunnelMode;
673
+ args: string[];
674
+ regex: RegExp;
675
+ urlFrom: "hostname" | "regex";
676
+ }
677
+
678
+ /**
679
+ * Build the ordered attempt list for this spawn: named first (if configured),
680
+ * falling back to quick — or quick-only when no named config exists. Quick's
681
+ * argv/regex are identical whichever position it takes.
682
+ *
683
+ * Exported so a unit test can assert argv parity with `getQuickTunnelArgs`
684
+ * against `spawnTunnel`'s actual call site, without spawning a real process.
685
+ */
686
+ export function buildTunnelAttempts(config: ResolvedTunnelConfig, port: number): TunnelAttempt[] {
687
+ const plan = chooseTunnelSpawn(config, port);
688
+ if (plan.mode === "quick") {
689
+ return [{ mode: "quick", args: plan.args, regex: TUNNEL_URL_REGEX, urlFrom: "regex" }];
690
+ }
691
+ return [
692
+ { mode: "named", args: plan.args, regex: NAMED_TUNNEL_READY_REGEX, urlFrom: "hostname" },
693
+ { mode: "quick", args: getQuickTunnelArgs(port), regex: TUNNEL_URL_REGEX, urlFrom: "regex" },
694
+ ];
695
+ }
696
+
697
+ /**
698
+ * Decide the `tunnelWarning` field (if any) for a spawnTunnel success write.
699
+ *
700
+ * - `downgraded` (named was attempted first but failed, quick succeeded as
701
+ * THIS spawn's fallback) is the only case a spawn ever WRITES a warning —
702
+ * that text must persist until the user acts, so it is the sole owner here.
703
+ * - A quick success that was NOT a downgrade means the persisted config
704
+ * itself resolved to quick outright (`attempts` was quick-only) — whether
705
+ * that spawn was triggered by a deliberate retunnel-to-quick, a cold boot,
706
+ * or any later regen while quick stays configured, it reconfirms the user
707
+ * is no longer on named mode, so any warning left over from before they
708
+ * switched (e.g. a stale "hostname unreachable") no longer applies.
709
+ * - A plain named success (not a downgrade) never touches the warning —
710
+ * clearing a NAMED-mode warning is exclusively the probe's job once it
711
+ * confirms the hostname is actually reachable (`named-tunnel-probe-state.ts`).
712
+ */
713
+ export function tunnelWarningPatchForSpawnSuccess(
714
+ successfulMode: TunnelMode,
715
+ downgraded: boolean,
716
+ ): { tunnelWarning: string | null } | Record<string, never> {
717
+ if (downgraded) {
718
+ return { tunnelWarning: "Named tunnel failed to start — using a temporary quick URL" };
719
+ }
720
+ if (successfulMode === "quick") {
721
+ return { tunnelWarning: null };
722
+ }
723
+ return {};
724
+ }
725
+
623
726
  export async function spawnTunnel(port: number, generation: number = ++tunnelGeneration): Promise<void> {
624
727
  tunnelPort = port; // remember origin port so resume/port-move can re-point
728
+ // Refresh the cached config here (not just at startup) — a fresh named
729
+ // setup, disable, or token rotation since the last spawn must be picked up
730
+ // before deciding what to spawn next.
731
+ namedTunnelMode = await readTunnelConfigFresh();
732
+
625
733
  let bin: string;
626
- let quickArgs: string[];
627
734
  try {
628
- const { ensureCloudflared, getQuickTunnelArgs } = await import("./cloudflared.service.ts");
735
+ const { ensureCloudflared } = await import("./cloudflared.service.ts");
629
736
  bin = await ensureCloudflared();
630
- quickArgs = getQuickTunnelArgs(port);
631
737
  } catch (err) {
632
738
  log("ERROR", `Failed to get cloudflared: ${err}`);
633
739
  return;
634
740
  }
635
741
 
636
- // Redirect cloudflared stderr to a log file (not pipe). This way cloudflared
637
- // survives parent supervisor exit during self-replace — a piped stderr would
638
- // close when parent exits, causing SIGPIPE on next cloudflared log write and
639
- // killing the tunnel ~10-15s later (silently breaking adoption).
742
+ const attempts = buildTunnelAttempts(namedTunnelMode, port);
640
743
  const logPath = cloudflaredLogPath();
641
- try { unlinkSync(logPath); } catch {} // truncate stale URLs from prior run
642
- const tunnelLogFd = openSync(logPath, "a");
744
+ // Truncate stale content from a prior generation; best-effort Windows can
745
+ // silently fail this while a previous detached cloudflared still holds the
746
+ // file open, which is exactly why every readiness read below is anchored to
747
+ // a byte offset captured AFTER this attempt rather than trusting the file
748
+ // to actually be empty.
749
+ try { unlinkSync(logPath); } catch {}
643
750
 
644
751
  // ── Windows: spawn detached + windowless via node:child_process ────────
645
752
  // Bun.spawn ties children to the supervisor's job object, so the tunnel
@@ -652,109 +759,140 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
652
759
  // when the supervisor itself was started consoleless by the upgrade path.
653
760
  if (process.platform === "win32") {
654
761
  const { spawn: nodeSpawn } = require("node:child_process") as typeof import("node:child_process");
655
- // Gated: fd stdio enables handle inheritance — spawning while a port
656
- // probe is open would hand cloudflared the listener handle (see gate).
657
- const proc = await withProbeSpawnGate(() => nodeSpawn(bin, quickArgs, {
658
- detached: true,
659
- windowsHide: true,
660
- stdio: ["ignore", "ignore", tunnelLogFd] as ["ignore", "ignore", number],
661
- }));
662
- proc.unref();
663
- try { closeSync(tunnelLogFd); } catch {} // child keeps its own fd
664
- const pid = proc.pid ?? null;
665
762
 
666
- try {
667
- tunnelUrl = await extractUrlFromLogFile(() => proc.exitCode);
668
- } catch (err) {
669
- log("ERROR", `Tunnel URL extraction failed: ${err}`);
670
- tunnelUrl = null;
671
- try { if (pid) process.kill(pid, "SIGKILL"); } catch {}
672
- if (shuttingDown) return;
673
- const now = Date.now();
674
- if (now - lastTunnelCrash > STABLE_WINDOW_MS) tunnelRestarts = 0;
675
- lastTunnelCrash = now;
676
- tunnelRestarts++;
677
- if (tunnelRestarts > MAX_RESTARTS) tunnelRestarts = MAX_RESTARTS;
678
- const delay = backoffDelay(tunnelRestarts) + Math.floor(Math.random() * 1000);
679
- log("WARN", `Tunnel failed, retry in ${delay}ms (#${tunnelRestarts})`);
680
- await Bun.sleep(delay);
681
- if (generation !== tunnelGeneration) return; // superseded during backoff
682
- // Re-read the live server port: spawnServer may have moved it since this
683
- // attempt started, and a retry at the stale port would split-brain the tunnel.
684
- return spawnTunnel(_opts.port, generation);
763
+ let winPid: number | null = null;
764
+ let successfulMode: TunnelMode | null = null;
765
+ let resolvedUrl: string | null = null;
766
+ let downgraded = false;
767
+
768
+ for (let i = 0; i < attempts.length; i++) {
769
+ const attempt = attempts[i]!;
770
+ const offset = existsSync(logPath) ? statSync(logPath).size : 0;
771
+ const attemptLogFd = openSync(logPath, "a");
772
+ // Gated: fd stdio enables handle inheritance — spawning while a port
773
+ // probe is open would hand cloudflared the listener handle (see gate).
774
+ const proc = await withProbeSpawnGate(() => nodeSpawn(bin, attempt.args, {
775
+ detached: true,
776
+ windowsHide: true,
777
+ stdio: ["ignore", "ignore", attemptLogFd] as ["ignore", "ignore", number],
778
+ }));
779
+ proc.unref();
780
+ try { closeSync(attemptLogFd); } catch {} // child keeps its own fd
781
+ const pid = proc.pid ?? null;
782
+
783
+ try {
784
+ const matched = attempt.urlFrom === "hostname"
785
+ ? await waitForLogLine(logPath, attempt.regex, {
786
+ fromByteOffset: offset, timeoutMs: 30_000, getExitCode: () => proc.exitCode,
787
+ }).then(() => `https://${namedTunnelMode!.hostname}`)
788
+ : await extractUrlFromLogFile(offset, () => proc.exitCode);
789
+ tunnelUrl = matched;
790
+ resolvedUrl = matched;
791
+ winPid = pid;
792
+ successfulMode = attempt.mode;
793
+ downgraded = i > 0;
794
+ break;
795
+ } catch (err) {
796
+ log("ERROR", `${attempt.mode} tunnel failed to start: ${err}`);
797
+ try { if (pid) process.kill(pid, "SIGKILL"); } catch {}
798
+ tunnelUrl = null;
799
+ if (generation !== tunnelGeneration) return; // superseded — don't bother with the next attempt
800
+ }
801
+ }
802
+
803
+ if (winPid === null || !successfulMode || resolvedUrl === null) {
804
+ return retryTunnelAfterFailure(generation);
685
805
  }
686
806
 
687
807
  // A newer authoritative (re)start superseded us while we extracted the URL —
688
808
  // don't register as the live tunnel; kill our now-orphan child and bail.
689
809
  if (generation !== tunnelGeneration) {
690
- try { if (pid) process.kill(pid, "SIGKILL"); } catch {}
810
+ try { process.kill(winPid, "SIGKILL"); } catch {}
691
811
  return;
692
812
  }
693
813
 
694
814
  // The detached tunnel is independent of this supervisor, so there is no
695
815
  // `.exited` promise to await for crash detection. Model it as adopted from
696
816
  // the start: the tunnel probe (startTunnelProbe) owns liveness + respawn.
697
- adoptedTunnelPid = pid;
817
+ adoptedTunnelPid = winPid;
698
818
  tunnelChild = null;
699
- updateStatus({ shareUrl: tunnelUrl, tunnelPid: pid, tunnelPort: port });
700
- log("INFO", `Tunnel ready: ${tunnelUrl} (PID: ${pid}, detached)`);
701
- await syncUrlToCloud(tunnelUrl);
819
+ lastSpawnMode = successfulMode;
820
+ // `namedProbeRestartAttempted` is deliberately NOT touched here — see the
821
+ // probe's `decideNamedProbeAction` state machine. A successful spawn only
822
+ // proves cloudflared reconnected to Cloudflare's edge, not that the
823
+ // configured hostname actually routes to it (that's a DNS/CNAME concern
824
+ // the probe alone can verify), so a bare respawn success must not re-arm
825
+ // the one-restart budget or the CNAME-deleted case would restart forever.
826
+ updateStatus({
827
+ shareUrl: resolvedUrl, tunnelPid: winPid, tunnelPort: port,
828
+ tunnelMode: successfulMode,
829
+ ...tunnelWarningPatchForSpawnSuccess(successfulMode, downgraded),
830
+ });
831
+ log("INFO", `Tunnel ready: ${resolvedUrl} (PID: ${winPid}, detached${downgraded ? ", downgraded from named" : ""})`);
832
+ await syncUrlToCloud(resolvedUrl);
702
833
  return;
703
834
  }
704
835
 
836
+ // ── POSIX ────────────────────────────────────────────────────────────────
705
837
  // Under systemd, wrap tunnel in a transient user scope so it lives in its
706
838
  // own cgroup instead of ppm.service. This prevents systemd from SIGKILLing
707
839
  // the tunnel when ppm.service cgroup is torn down during upgrade/restart,
708
- // preserving the cloudflared trycloudflare URL across the new supervisor.
840
+ // preserving the tunnel URL across the new supervisor.
709
841
  // INVOCATION_ID is set by systemd; absence means we're not under systemd.
710
842
  const underSystemd = !!process.env.INVOCATION_ID && process.platform === "linux";
711
- const tunnelCmd = underSystemd
712
- ? [
713
- "systemd-run", "--user", "--scope", "--quiet", "--collect",
714
- "--",
715
- bin, ...quickArgs,
716
- ]
717
- : [bin, ...quickArgs];
718
-
719
- // Own this cloudflared via a LOCAL ref for the whole loop. `tunnelChild` is a
720
- // mutable global that a concurrent restartTunnel() nulls/reassigns; awaiting
721
- // `tunnelChild.exited` on it would throw `TypeError: null is not an object`
722
- // mid-flight, killing this loop WITHOUT reaping the child we spawned →
723
- // orphaned cloudflared (PID 1). The local ref keeps our lifecycle
724
- // self-contained; we only touch the global when it still points at us.
725
- let child: Subprocess;
726
- try {
727
- child = await withProbeSpawnGate(() =>
728
- Bun.spawn(tunnelCmd, { stderr: tunnelLogFd, stdout: "ignore", stdin: "ignore" }));
729
- tunnelChild = child; // publish so restartTunnel/killStaleTunnel can reach the live child
730
- } finally {
731
- // Close our handle; cloudflared keeps its own via dup2
732
- try { closeSync(tunnelLogFd); } catch {}
733
- }
734
- if (underSystemd) log("INFO", "Tunnel spawned inside transient systemd-run scope (escapes ppm.service cgroup)");
735
-
736
- try {
737
- tunnelUrl = await extractUrlFromLogFile(() => child.exitCode);
738
- } catch (err) {
739
- log("ERROR", `Tunnel URL extraction failed: ${err}`);
740
- tunnelUrl = null;
741
- try { child.kill(); } catch {}
742
- if (tunnelChild === child) tunnelChild = null;
843
+ const buildCmd = (args: string[]): string[] =>
844
+ underSystemd
845
+ ? ["systemd-run", "--user", "--scope", "--quiet", "--collect", "--", bin, ...args]
846
+ : [bin, ...args];
847
+
848
+ let child: Subprocess | null = null;
849
+ let successfulMode: TunnelMode | null = null;
850
+ let resolvedUrl: string | null = null;
851
+ let downgraded = false;
852
+
853
+ for (let i = 0; i < attempts.length; i++) {
854
+ const attempt = attempts[i]!;
855
+ const offset = existsSync(logPath) ? statSync(logPath).size : 0;
856
+ const attemptLogFd = openSync(logPath, "a");
857
+ // Own this cloudflared via a LOCAL ref for the whole loop. `tunnelChild` is
858
+ // a mutable global that a concurrent restartTunnel() nulls/reassigns;
859
+ // awaiting `tunnelChild.exited` on it would throw mid-flight, killing this
860
+ // loop WITHOUT reaping the child we spawned — orphaned cloudflared. The
861
+ // local ref keeps lifecycle self-contained; only touch the global when it
862
+ // still points at us.
863
+ let attemptChild: Subprocess;
864
+ try {
865
+ attemptChild = await withProbeSpawnGate(() =>
866
+ Bun.spawn(buildCmd(attempt.args), { stderr: attemptLogFd, stdout: "ignore", stdin: "ignore" }));
867
+ tunnelChild = attemptChild; // publish so restartTunnel/killStaleTunnel can reach the live child
868
+ } finally {
869
+ try { closeSync(attemptLogFd); } catch {} // cloudflared keeps its own via dup2
870
+ }
871
+ if (underSystemd && i === 0) log("INFO", "Tunnel spawned inside transient systemd-run scope (escapes ppm.service cgroup)");
743
872
 
744
- if (shuttingDown) return;
873
+ try {
874
+ const matched = attempt.urlFrom === "hostname"
875
+ ? await waitForLogLine(logPath, attempt.regex, {
876
+ fromByteOffset: offset, timeoutMs: 30_000, getExitCode: () => attemptChild.exitCode,
877
+ }).then(() => `https://${namedTunnelMode!.hostname}`)
878
+ : await extractUrlFromLogFile(offset, () => attemptChild.exitCode);
879
+ tunnelUrl = matched;
880
+ resolvedUrl = matched;
881
+ child = attemptChild;
882
+ successfulMode = attempt.mode;
883
+ downgraded = i > 0;
884
+ break;
885
+ } catch (err) {
886
+ log("ERROR", `${attempt.mode} tunnel failed to start: ${err}`);
887
+ try { attemptChild.kill(); } catch {}
888
+ if (tunnelChild === attemptChild) tunnelChild = null;
889
+ tunnelUrl = null;
890
+ if (generation !== tunnelGeneration) return; // superseded — don't bother with the next attempt
891
+ }
892
+ }
745
893
 
746
- const now = Date.now();
747
- if (now - lastTunnelCrash > STABLE_WINDOW_MS) tunnelRestarts = 0;
748
- lastTunnelCrash = now;
749
- tunnelRestarts++;
750
-
751
- // Never give up: cap the counter so backoff plateaus at BACKOFF_MAX_MS (no 10-min dark window).
752
- if (tunnelRestarts > MAX_RESTARTS) tunnelRestarts = MAX_RESTARTS;
753
- const delay = backoffDelay(tunnelRestarts) + Math.floor(Math.random() * 1000);
754
- log("WARN", `Tunnel failed, retry in ${delay}ms (#${tunnelRestarts})`);
755
- await Bun.sleep(delay);
756
- if (generation !== tunnelGeneration) return; // superseded during backoff
757
- return spawnTunnel(_opts.port, generation); // live port — may have moved since this attempt
894
+ if (!child || !successfulMode || resolvedUrl === null) {
895
+ return retryTunnelAfterFailure(generation);
758
896
  }
759
897
 
760
898
  // A newer authoritative (re)start superseded us while we extracted the URL —
@@ -765,11 +903,19 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
765
903
  return;
766
904
  }
767
905
 
768
- updateStatus({ shareUrl: tunnelUrl, tunnelPid: child.pid, tunnelPort: port });
769
- log("INFO", `Tunnel ready: ${tunnelUrl} (PID: ${child.pid})`);
906
+ lastSpawnMode = successfulMode;
907
+ // `namedProbeRestartAttempted` is deliberately NOT touched here — see the
908
+ // probe's `decideNamedProbeAction` state machine and the win32 branch above
909
+ // for why a bare spawn success must never re-arm the one-restart budget.
910
+ updateStatus({
911
+ shareUrl: resolvedUrl, tunnelPid: child.pid, tunnelPort: port,
912
+ tunnelMode: successfulMode,
913
+ ...tunnelWarningPatchForSpawnSuccess(successfulMode, downgraded),
914
+ });
915
+ log("INFO", `Tunnel ready: ${resolvedUrl} (PID: ${child.pid}${downgraded ? ", downgraded from named" : ""})`);
770
916
 
771
917
  // One-time sync of tunnel URL to cloud (WS handles periodic heartbeat)
772
- await syncUrlToCloud(tunnelUrl);
918
+ await syncUrlToCloud(resolvedUrl);
773
919
 
774
920
  const exitCode = await child.exited;
775
921
  if (tunnelChild === child) tunnelChild = null;
@@ -786,22 +932,8 @@ export async function spawnTunnel(port: number, generation: number = ++tunnelGen
786
932
  return;
787
933
  }
788
934
 
789
- // Crash apply backoff
790
- const now = Date.now();
791
- if (now - lastTunnelCrash > STABLE_WINDOW_MS) tunnelRestarts = 0;
792
- lastTunnelCrash = now;
793
- tunnelRestarts++;
794
-
795
- // Never give up: cap the counter so backoff plateaus at BACKOFF_MAX_MS (no 10-min dark window).
796
- if (tunnelRestarts > MAX_RESTARTS) tunnelRestarts = MAX_RESTARTS;
797
- const delay = backoffDelay(tunnelRestarts) + Math.floor(Math.random() * 1000);
798
- log("WARN", `Tunnel process exited (code=${exitCode}, url=${deadUrl}), restart in ${delay}ms (#${tunnelRestarts})`);
799
- await Bun.sleep(delay);
800
-
801
- if (generation !== tunnelGeneration) return; // superseded during backoff
802
- // Respawn at the server's live port, not this spawn's original target —
803
- // the server may have moved (zombie-port fallback) while the tunnel was up.
804
- if (!shuttingDown) return spawnTunnel(_opts.port, generation);
935
+ log("WARN", `Tunnel process exited (code=${exitCode}, url=${deadUrl}), applying backoff`);
936
+ return retryTunnelAfterFailure(generation);
805
937
  }
806
938
 
807
939
  // ─── Health checks ─────────────────────────────────────────────────────
@@ -863,6 +995,44 @@ function startServerHealthCheck() {
863
995
  }, SERVER_HEALTH_INTERVAL_MS);
864
996
  }
865
997
 
998
+ /**
999
+ * Named-mode identity probe: fetch the public hostname's health and our own
1000
+ * loopback health, then compare `instanceId`. A mismatch means someone else's
1001
+ * connector is answering our hostname (treat as unreachable, same as a fetch
1002
+ * failure); this is what lets a dead-but-adopted named connector be told apart
1003
+ * from a live one that Cloudflare has quietly repointed. Falls back to a bare
1004
+ * reachability check when either side omits `instanceId` (older server build,
1005
+ * or the loopback port isn't resolvable yet) rather than hard-failing.
1006
+ */
1007
+ async function probeNamedTunnelHealth(): Promise<boolean> {
1008
+ try {
1009
+ // `Connection: close` is load-bearing: this probe runs every 30s for the
1010
+ // life of the supervisor, and a pooled keep-alive socket pins us to
1011
+ // whatever IP the hostname resolved to when the pool opened it. Observed
1012
+ // live: after a deleted CNAME fell back to the zone's wildcard host, the
1013
+ // pooled socket kept answering from that host for minutes after the CNAME
1014
+ // was restored — the probe never saw the recovery. A fresh connection per
1015
+ // probe follows DNS.
1016
+ const publicRes = await fetch(`${tunnelUrl}/api/health`, {
1017
+ signal: AbortSignal.timeout(10_000),
1018
+ headers: { connection: "close" },
1019
+ });
1020
+ if (!publicRes.ok) return false;
1021
+ _resetTargetCache();
1022
+ const checkPort = resolveTargetPort();
1023
+ if (checkPort === null) return true; // can't compare identity yet — reachability alone is enough for now
1024
+ const publicBody = await publicRes.json().catch(() => null) as { data?: { instanceId?: unknown } } | null;
1025
+ const localRes = await fetch(`http://127.0.0.1:${checkPort}/api/health`, { signal: AbortSignal.timeout(5000) });
1026
+ if (!localRes.ok) return true; // our own server's health is startServerHealthCheck's job, not this probe's
1027
+ const localBody = await localRes.json().catch(() => null) as { data?: { instanceId?: unknown } } | null;
1028
+ // A 200 alone proves nothing: a deleted CNAME falls back to the zone's
1029
+ // wildcard record and an unrelated host answers. Identity decides.
1030
+ return publicHostnameIsOurs(publicBody?.data?.instanceId, localBody?.data?.instanceId);
1031
+ } catch {
1032
+ return false;
1033
+ }
1034
+ }
1035
+
866
1036
  function startTunnelProbe() {
867
1037
  tunnelProbeTimer = setInterval(async () => {
868
1038
  if (shuttingDown || !tunnelUrl) { tunnelFailCount = 0; return; }
@@ -876,7 +1046,8 @@ function startTunnelProbe() {
876
1046
  log("WARN", "Adopted tunnel process died, respawning");
877
1047
  adoptedTunnelPid = null;
878
1048
  tunnelUrl = null;
879
- updateStatus({ shareUrl: null, tunnelPid: null });
1049
+ // Named URL is pinned — don't flicker it null for the brief respawn window.
1050
+ updateStatus(lastSpawnMode === "named" ? { tunnelPid: null } : { shareUrl: null, tunnelPid: null });
880
1051
  tunnelFailCount = 0;
881
1052
  spawnTunnel(_opts.port); // live server port, not the startup config port
882
1053
  return;
@@ -889,31 +1060,79 @@ function startTunnelProbe() {
889
1060
  // on every probe window while the real problem is the server.
890
1061
  if (getState() !== "running" || !serverChild) { tunnelFailCount = 0; return; }
891
1062
 
892
- try {
893
- const res = await fetch(`${tunnelUrl}/api/health`, {
894
- signal: AbortSignal.timeout(10_000),
895
- });
896
- if (res.ok) {
897
- tunnelFailCount = 0;
898
- tunnelRestarts = 0;
899
- return;
900
- }
901
- } catch {}
902
- tunnelFailCount++;
903
- if (tunnelFailCount >= TUNNEL_ZOMBIE_THRESHOLD) {
904
- log("WARN", `Tunnel URL zombie (${tunnelFailCount} fails ≈ ${tunnelFailCount * (TUNNEL_PROBE_INTERVAL_MS / 1000)}s, process alive but edge dropped), regenerating`);
905
- if (tunnelChild) {
906
- try { tunnelChild.kill(); } catch {}
907
- // spawnTunnel loop handles respawn via exited promise
908
- } else if (adoptedTunnelPid) {
909
- try { process.kill(adoptedTunnelPid, "SIGTERM"); } catch {}
910
- adoptedTunnelPid = null;
911
- tunnelUrl = null;
912
- updateStatus({ shareUrl: null, tunnelPid: null });
913
- spawnTunnel(_opts.port); // live server port, not the startup config port
1063
+ if (lastSpawnMode === "named") {
1064
+ const healthy = await probeNamedTunnelHealth();
1065
+ // All state transitions for the restart-once-then-warn budget live in
1066
+ // this pure helper — the ONLY place `restartAttempted` (and the
1067
+ // warning) is ever cleared is a confirmed-healthy observation here,
1068
+ // never a bare spawn success (see spawnTunnel's success writes).
1069
+ const { action, nextState } = decideNamedProbeAction(
1070
+ healthy,
1071
+ { failCount: tunnelFailCount, restartAttempted: namedProbeRestartAttempted },
1072
+ TUNNEL_ZOMBIE_THRESHOLD,
1073
+ );
1074
+ tunnelFailCount = nextState.failCount;
1075
+ namedProbeRestartAttempted = nextState.restartAttempted;
1076
+
1077
+ switch (action.type) {
1078
+ case "healthy":
1079
+ tunnelRestarts = 0;
1080
+ // Only write when there's actually a warning to clear — otherwise
1081
+ // this fires every healthy 30s tick forever (tmp-write + rename on
1082
+ // every single cycle) even though nothing ever changes.
1083
+ if (readStatus().tunnelWarning != null) updateStatus({ tunnelWarning: null });
1084
+ return;
1085
+ case "watch":
1086
+ // Say something after ~1 minute even though the connector is left
1087
+ // alone until the 5-minute threshold: an unreachable hostname that
1088
+ // reports nothing is indistinguishable from a working one.
1089
+ if (action.warnEarly && readStatus().tunnelWarning == null) {
1090
+ updateStatus({ tunnelWarning: NAMED_WARNING_CHECKING });
1091
+ }
1092
+ return;
1093
+ case "restart-once":
1094
+ log("WARN", "Named tunnel unreachable at threshold — restarting the connector once");
1095
+ if (tunnelChild) {
1096
+ try { tunnelChild.kill(); } catch {}
1097
+ // spawnTunnel loop handles respawn via exited promise
1098
+ } else if (adoptedTunnelPid) {
1099
+ try { process.kill(adoptedTunnelPid, "SIGTERM"); } catch {}
1100
+ adoptedTunnelPid = null;
1101
+ spawnTunnel(_opts.port); // live server port, not the startup config port
1102
+ }
1103
+ return;
1104
+ case "warn-and-stop":
1105
+ // Already tried a restart — the hostname is still dark. Never loop
1106
+ // further and never null the pinned URL; just surface the warning.
1107
+ log("WARN", "Named tunnel still unreachable after one restart — warning and stopping (shareUrl stays pinned)");
1108
+ updateStatus({ tunnelWarning: NAMED_WARNING_UNREACHABLE });
1109
+ return;
914
1110
  }
1111
+ return;
1112
+ }
1113
+
1114
+ // Quick mode: existing zombie-regen behavior, unchanged.
1115
+ const healthy = await fetch(`${tunnelUrl}/api/health`, { signal: AbortSignal.timeout(10_000) })
1116
+ .then((res) => res.ok).catch(() => false);
1117
+ if (healthy) {
915
1118
  tunnelFailCount = 0;
1119
+ tunnelRestarts = 0;
1120
+ return;
916
1121
  }
1122
+ tunnelFailCount++;
1123
+ if (tunnelFailCount < TUNNEL_ZOMBIE_THRESHOLD) return;
1124
+ log("WARN", `Tunnel URL zombie (${tunnelFailCount} fails ≈ ${tunnelFailCount * (TUNNEL_PROBE_INTERVAL_MS / 1000)}s, process alive but edge dropped), regenerating`);
1125
+ if (tunnelChild) {
1126
+ try { tunnelChild.kill(); } catch {}
1127
+ // spawnTunnel loop handles respawn via exited promise
1128
+ } else if (adoptedTunnelPid) {
1129
+ try { process.kill(adoptedTunnelPid, "SIGTERM"); } catch {}
1130
+ adoptedTunnelPid = null;
1131
+ tunnelUrl = null;
1132
+ updateStatus({ shareUrl: null, tunnelPid: null });
1133
+ spawnTunnel(_opts.port); // live server port, not the startup config port
1134
+ }
1135
+ tunnelFailCount = 0;
917
1136
  }, TUNNEL_PROBE_INTERVAL_MS);
918
1137
  }
919
1138
 
@@ -944,12 +1163,30 @@ function adoptTunnel(): boolean {
944
1163
  return false;
945
1164
  }
946
1165
  process.kill(pid, 0); // throws if process is dead
1166
+ // Liveness alone is not proof of identity — Windows/Linux both reuse PIDs,
1167
+ // and a recycled PID happening to belong to some unrelated process would
1168
+ // get "adopted" as the tunnel while the real one (if any) sits orphaned.
1169
+ if (!isCloudflaredPid(pid)) {
1170
+ log("WARN", `adoptTunnel: PID ${pid} is alive but its executable is not cloudflared — refusing to adopt`);
1171
+ return false;
1172
+ }
1173
+ // Named mode's URL is pinned to the configured hostname. A stale quick URL
1174
+ // left over from before named was configured (or from a downgrade) must
1175
+ // never be adopted as if it were the named tunnel.
1176
+ if (namedTunnelMode?.mode === "named") {
1177
+ const expected = `https://${namedTunnelMode.hostname}`;
1178
+ if (url !== expected) {
1179
+ log("WARN", `adoptTunnel: status.shareUrl ${url} does not match configured named hostname ${expected} — refusing to adopt`);
1180
+ return false;
1181
+ }
1182
+ }
947
1183
  adoptedTunnelPid = pid;
948
1184
  tunnelUrl = url;
949
1185
  // Remember which origin port the adopted tunnel targets so spawnServer can
950
1186
  // bind THERE (not the configured port) and keep the public URL alive.
951
1187
  if (typeof status.tunnelPort === "number") tunnelPort = status.tunnelPort;
952
- log("INFO", `Adopted existing tunnel (PID: ${pid}, URL: ${url}, origin port: ${tunnelPort ?? "unknown"})`);
1188
+ if (typeof status.tunnelMode === "string") lastSpawnMode = status.tunnelMode as TunnelMode;
1189
+ log("INFO", `Adopted existing tunnel (PID: ${pid}, URL: ${url}, origin port: ${tunnelPort ?? "unknown"}, mode: ${lastSpawnMode})`);
953
1190
  return true;
954
1191
  } catch (e) {
955
1192
  log("WARN", `adoptTunnel: tunnel PID ${(readStatus().tunnelPid)} unreachable: ${e}`);
@@ -969,6 +1206,18 @@ function killStaleTunnel() {
969
1206
  updateStatus({ tunnelPid: null, shareUrl: null });
970
1207
  }
971
1208
 
1209
+ /**
1210
+ * `pgrep -f` pattern matching PPM's own cloudflared, running EITHER mode's
1211
+ * long-lived connector. `.*` spans the `--config <path>`/`--origincert` args
1212
+ * between bin and `tunnel`. Deliberately does NOT match `tunnel
1213
+ * login`/`create`/`route`/`token` — those are short-lived management
1214
+ * subcommands the reaper must never SIGTERM mid-flight. Exported so a unit
1215
+ * test can assert the match/exclude set without shelling out to pgrep.
1216
+ */
1217
+ export function orphanedTunnelPgrepPattern(bin: string): string {
1218
+ return `${bin}.*tunnel (run|--url)`;
1219
+ }
1220
+
972
1221
  /** Reap orphaned cloudflared processes left by crashed supervisors or stale
973
1222
  * spawn loops. Matches PPM's own cloudflared bin path and SIGTERMs every match
974
1223
  * except `keepPid`. POSIX only — on win32 the tunnel is tied to the job object
@@ -980,8 +1229,7 @@ async function reapOrphanedTunnels(keepPid: number | null): Promise<void> {
980
1229
  try {
981
1230
  const { getCloudflaredPath } = await import("./cloudflared.service.ts");
982
1231
  const bin = getCloudflaredPath();
983
- // `.*` spans the `--config <path>` args that sit between bin and `tunnel`.
984
- const res = Bun.spawnSync(["pgrep", "-f", `${bin}.*tunnel --url`]);
1232
+ const res = Bun.spawnSync(["pgrep", "-f", orphanedTunnelPgrepPattern(bin)]);
985
1233
  // pgrep exits 1 when nothing matches — not an error.
986
1234
  const pids = new TextDecoder().decode(res.stdout)
987
1235
  .split("\n")
@@ -1546,6 +1794,15 @@ export async function runSupervisor(opts: {
1546
1794
  // so its PID must survive this wholesale rewrite or the new supervisor
1547
1795
  // would spawn a second edge and collide on the public port.
1548
1796
  edgePid: isUpgrade ? (prevStatus.edgePid ?? null) : null,
1797
+ // tunnelMode/tunnelWarning survive an upgrade the same way tunnelPid/shareUrl
1798
+ // do above — otherwise a live named tunnel would report as unset mode with
1799
+ // no warning history the instant the new supervisor takes over.
1800
+ tunnelMode: isUpgrade ? (prevStatus.tunnelMode ?? null) : null,
1801
+ tunnelWarning: isUpgrade ? (prevStatus.tunnelWarning ?? null) : null,
1802
+ // Unconditional (not upgrade-gated): every boot of this code understands
1803
+ // "retunnel" — its ABSENCE is how phase 3's UI detects a pre-upgrade
1804
+ // supervisor that predates the retunnel command entirely.
1805
+ capabilities: ["retunnel"],
1549
1806
  serverPort: null, // republished by the server on every spawn
1550
1807
  });
1551
1808
  // Diagnostic: a cold start (isUpgrade=false) always nulls the tunnel and forces
@@ -1583,8 +1840,8 @@ export async function runSupervisor(opts: {
1583
1840
  process.on("SIGINT", () => forceShutdown("SIGINT"));
1584
1841
 
1585
1842
  // SIGUSR2 = command file dispatch OR graceful server restart
1586
- process.on("SIGUSR2", () => {
1587
- // Check for command file first (soft_stop, resume)
1843
+ process.on("SIGUSR2", async () => {
1844
+ // Check for command file first (soft_stop, resume, retunnel)
1588
1845
  const cmd = readAndDeleteCmd();
1589
1846
  if (cmd) {
1590
1847
  if (cmd.action === "soft_stop") {
@@ -1599,9 +1856,26 @@ export async function runSupervisor(opts: {
1599
1856
  }
1600
1857
  return;
1601
1858
  }
1859
+ if (cmd.action === "retunnel") {
1860
+ log("INFO", "SIGUSR2: retunnel command received");
1861
+ namedTunnelMode = await readTunnelConfigFresh();
1862
+ // A deliberate corrective action (e.g. the user just fixed DNS) gets a
1863
+ // fresh one-restart budget rather than inheriting a stale exhausted one —
1864
+ // both the flag and the fail counter, or a nearly-full counter carried
1865
+ // over from the outage trips "restart once" after a handful of probes.
1866
+ namedProbeRestartAttempted = false;
1867
+ tunnelFailCount = 0;
1868
+ restartTunnel(_opts.port);
1869
+ // Deliberate fall-through (no return): a bare `ppm restart` sends a
1870
+ // bare SIGUSR2 with no command file of its own, and a `retunnel` that
1871
+ // happens to still be unclaimed at that instant must not silently
1872
+ // swallow the user's restart intent. Retunneling first costs nothing
1873
+ // extra — it's a fast no-op when the tunnel is already correct — and
1874
+ // this still restarts the server exactly like a plain SIGUSR2 would.
1875
+ }
1602
1876
  }
1603
1877
 
1604
- // Default: restart server (existing behavior)
1878
+ // Default: restart server (existing behavior, and retunnel's fall-through target)
1605
1879
  if (getState() === "paused") {
1606
1880
  log("INFO", "SIGUSR2 received while paused, resuming server");
1607
1881
  triggerResume();
@@ -1715,6 +1989,21 @@ export async function runSupervisor(opts: {
1715
1989
  requestServerShutdown(serverChild).catch(() => {});
1716
1990
  }
1717
1991
  }
1992
+ else if (cmd.action === "retunnel") {
1993
+ log("INFO", "Windows command: retunnel");
1994
+ // No POSIX-style fall-through ambiguity here — every command on this
1995
+ // poll loop is an explicit file, one command per tick, already claimed
1996
+ // atomically by readAndDeleteCmd. Just retunnel; don't also trigger
1997
+ // the unrelated "restart" branch.
1998
+ readTunnelConfigFresh().then((cfg) => {
1999
+ namedTunnelMode = cfg;
2000
+ // A deliberate corrective action gets a fresh one-restart budget
2001
+ // rather than inheriting a stale exhausted one (flag AND counter).
2002
+ namedProbeRestartAttempted = false;
2003
+ tunnelFailCount = 0;
2004
+ restartTunnel(_opts.port);
2005
+ });
2006
+ }
1718
2007
  else if (cmd.action === "upgrade") {
1719
2008
  log("INFO", "Windows command: upgrade, starting self-replace");
1720
2009
  selfReplace().then((result) => {
@@ -1742,6 +2031,14 @@ export async function runSupervisor(opts: {
1742
2031
  // settled first — the old order raced spawnServer's port selection.
1743
2032
  let tunnelAdopted = false;
1744
2033
  if (opts.share) {
2034
+ // Populate the cache BEFORE adoptTunnel/probe — an adopted tunnel skips
2035
+ // spawnTunnel entirely for this whole generation, so without this the
2036
+ // cache would stay null (adoptTunnel's named-hostname gate always refuses,
2037
+ // and the probe can never tell named from quick) until the next spawn.
2038
+ namedTunnelMode = await readTunnelConfigFresh();
2039
+ // Defensive — already `false` at module init, but a fresh boot must never
2040
+ // start with an inherited restart-once budget "spent".
2041
+ namedProbeRestartAttempted = false;
1745
2042
  startTunnelProbe();
1746
2043
  // Try adopting tunnel kept alive from previous upgrade; spawn new if dead
1747
2044
  tunnelAdopted = adoptTunnel();