@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.
- package/AGENTS.md +17 -5
- package/CHANGELOG.md +27 -0
- package/CLAUDE.md +3 -0
- package/README.md +6 -6
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +19 -1
- package/dist/web/assets/{adaptive-context-menu-DGCKKeTy.js → adaptive-context-menu-DCZtgOL7.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-BOHHTTe7.js → ai-resource-editor-D5CPBg06.js} +1 -1
- package/dist/web/assets/{ai-settings-section-BigC_nZR.js → ai-settings-section-DY-t1bK8.js} +1 -1
- package/dist/web/assets/{angular-html-BYPaydvh.js → angular-html-CTvM4ulF.js} +1 -1
- package/dist/web/assets/{angular-ts-Bb4AMKlk.js → angular-ts-B_vSkjco.js} +1 -1
- package/dist/web/assets/api-client-BogqsFna.js +1 -0
- package/dist/web/assets/{api-settings-Bd3M4YpI.js → api-settings-B2iNXQ0c.js} +1 -1
- package/dist/web/assets/api-settings-DRF9oYkn.js +1 -0
- package/dist/web/assets/{api-themes-Cpx419SL.js → api-themes-DrWnJbhs.js} +1 -1
- package/dist/web/assets/{apl-DB2VjFCS.js → apl-DywzTry0.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-nw5-j_I_.js +1 -0
- package/dist/web/assets/{astro-BeoJdVpk.js → astro-1JaRgaSb.js} +1 -1
- package/dist/web/assets/{audio-preview-BgFuTn-0.js → audio-preview-DAuGvOiK.js} +1 -1
- package/dist/web/assets/{blade-Cf2q1sPf.js → blade-CDeeJWBb.js} +1 -1
- package/dist/web/assets/{bsl-W1kOaNpu.js → bsl-BfgoPVYR.js} +1 -1
- package/dist/web/assets/{button-5zTrkEoN.js → button-tNy0ba3y.js} +1 -1
- package/dist/web/assets/{cairo-BoAZFcje.js → cairo-C26gNwWW.js} +1 -1
- package/dist/web/assets/chat-tab-IcnU3nqQ.js +15 -0
- package/dist/web/assets/{cobol-B0Nm7RXj.js → cobol-D92nBnON.js} +1 -1
- package/dist/web/assets/code-editor-Dno5Srkm.js +10 -0
- package/dist/web/assets/{coffee-BakMn3on.js → coffee-CIWH40R1.js} +1 -1
- package/dist/web/assets/{conflict-editor-BUs5k9q_.js → conflict-editor-CDp0I3mD.js} +1 -1
- package/dist/web/assets/{context-menu-Bq92km4s.js → context-menu-BO1c9jKy.js} +1 -1
- package/dist/web/assets/{cpp-BRAJQHK2.js → cpp-DOrBqXCn.js} +1 -1
- package/dist/web/assets/{crystal-kqqO3zkK.js → crystal-BNKqmHux.js} +1 -1
- package/dist/web/assets/{csv-preview-ByoDgLAZ.js → csv-preview-D4svNDAq.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-Brkf9z41.js → data-grid-overlay-editor-BqV0AxZg.js} +1 -1
- package/dist/web/assets/{database-viewer-Btc3h8Yu.js → database-viewer-BCvv_Bwf.js} +1 -1
- package/dist/web/assets/{dialog-DwtGlowE.js → dialog-DcLGDwaX.js} +1 -1
- package/dist/web/assets/diff-viewer-D41fCFAi.js +4 -0
- package/dist/web/assets/{docx-preview-DAD_vg59.js → docx-preview-DfaQn9v7.js} +1 -1
- package/dist/web/assets/{edge-Cq1ZKZuE.js → edge-Dbjp42Pt.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-B_VE7ppQ.js → edit-diff-preview-BqcUuFCc.js} +1 -1
- package/dist/web/assets/{elixir-BM_JoJRk.js → elixir-na3b1pYy.js} +1 -1
- package/dist/web/assets/{elm-Dw7oO08Y.js → elm-_Nalil1R.js} +1 -1
- package/dist/web/assets/{erb-CfKOGAfk.js → erb-CW_7dubf.js} +1 -1
- package/dist/web/assets/{erlang-DlDmGVeE.js → erlang-DmZ7Zzb0.js} +1 -1
- package/dist/web/assets/{esm-DGAP0GFh.js → esm-CwyBetO1.js} +1 -1
- package/dist/web/assets/explorer-body-DB1L30pa.js +2 -0
- package/dist/web/assets/explorer-window-content-Cwlt3C3I.js +1 -0
- package/dist/web/assets/{extension-store-Cpfh2vMm.js → extension-store-DUaZINyC.js} +1 -1
- package/dist/web/assets/{extension-webview-B2OnjuCu.js → extension-webview-BW_mccb3.js} +1 -1
- package/dist/web/assets/{file-store-aOERIEo-.js → file-store-yReVbR8q.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-XnAhQWxS.js → fortran-fixed-form-DB2A8L4-.js} +1 -1
- package/dist/web/assets/{fsharp-CdNvy0PG.js → fsharp-BYHrti7z.js} +1 -1
- package/dist/web/assets/{gdresource-DxuG5i8i.js → gdresource-D5GBin40.js} +1 -1
- package/dist/web/assets/{git-commit-3lMonsur.js → git-commit-CUiOBoIL.js} +1 -1
- package/dist/web/assets/{git-log-panel-Co0PBPHC.js → git-log-panel-C1QDkmgl.js} +1 -1
- package/dist/web/assets/{git-rebase-N8Z9kIMf.js → git-rebase-DOxGnpdw.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C1xf87gN.js +1 -0
- package/dist/web/assets/glide-data-grid-Dz8lx6n2.js +138 -0
- package/dist/web/assets/{glimmer-js-Dp9OXiib.js → glimmer-js-Begy4UT7.js} +1 -1
- package/dist/web/assets/{glimmer-ts-CDVKBcDj.js → glimmer-ts-rPApRY8N.js} +1 -1
- package/dist/web/assets/{glsl-DPJxI4us.js → glsl-wrbPvrBL.js} +1 -1
- package/dist/web/assets/{graphql-BrFhaQQl.js → graphql-Cu4-NR__.js} +1 -1
- package/dist/web/assets/group-chat-tab-DFwr-pig.js +1 -0
- package/dist/web/assets/{hack-BzXbDMlA.js → hack-Ddo93XKD.js} +1 -1
- package/dist/web/assets/{haml-CPpkX0s5.js → haml-BFX_1i8j.js} +1 -1
- package/dist/web/assets/{handlebars-BqrApy52.js → handlebars-5gFKA4H5.js} +1 -1
- package/dist/web/assets/{html-DmjUl-OA.js → html-C06I-zeO.js} +1 -1
- package/dist/web/assets/{html-derivative-Cnd6XFR5.js → html-derivative-e5JLAFZF.js} +1 -1
- package/dist/web/assets/{http-DMLWYun2.js → http-CrAAhSU7.js} +1 -1
- package/dist/web/assets/{hurl-CKkCVxbj.js → hurl-T1V3SX9T.js} +1 -1
- package/dist/web/assets/{hxml-CAx19TtQ.js → hxml-B056LLKN.js} +1 -1
- package/dist/web/assets/{image-preview-DHUoylQ1.js → image-preview-DWyuCYT3.js} +1 -1
- package/dist/web/assets/index-DWpNR3nB.css +2 -0
- package/dist/web/assets/index-oUKV5nIA.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-C2z7FTDe.js +1 -0
- package/dist/web/assets/{input-DcEk7mA3.js → input-DouJ9Zvt.js} +1 -1
- package/dist/web/assets/{jinja-BmB9YN9l.js → jinja-DGdtRh43.js} +1 -1
- package/dist/web/assets/{jison-BaVsKKpw.js → jison-Ah6QobPS.js} +1 -1
- package/dist/web/assets/{julia-BgVBLveX.js → julia-BJeGOK0K.js} +1 -1
- package/dist/web/assets/{just-C2ENIbl9.js → just-iw9oRs7j.js} +1 -1
- package/dist/web/assets/{keybindings-store-BGIyqXHI.js → keybindings-store-0Wqfa7Qp.js} +1 -1
- package/dist/web/assets/keybindings-store-d54pv2NK.js +1 -0
- package/dist/web/assets/{latex-BiDIWxKL.js → latex-DHjj5hyN.js} +1 -1
- package/dist/web/assets/{liquid-NxpapreE.js → liquid-B-FFecDc.js} +1 -1
- package/dist/web/assets/{lua-DVepQvwH.js → lua-DDianJp_.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DlcDhgrF.js → markdown-renderer-BHiUhQN8.js} +3 -3
- package/dist/web/assets/markdown-renderer-CnE36_gd.js +1 -0
- package/dist/web/assets/{marko-CJ2606Xl.js → marko-BW5SBVng.js} +1 -1
- package/dist/web/assets/{mdc-D2bjqclE.js → mdc-B9gL9bjl.js} +1 -1
- package/dist/web/assets/{media-url-H17XJF-I.js → media-url-DUK-k0bB.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-g-r2hCFa.js → mobile-bottom-sheet-CJ7ApOkI.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BzjlS1Xn.js +1 -0
- package/dist/web/assets/{nextflow-BtJwUbfF.js → nextflow-dTluY6qy.js} +1 -1
- package/dist/web/assets/{nginx-j4gyIhHE.js → nginx-DlwghGI-.js} +1 -1
- package/dist/web/assets/{nim-ClH_gjP5.js → nim-N9jddb5D.js} +1 -1
- package/dist/web/assets/notification-store-C7UBqxYP.js +1 -0
- package/dist/web/assets/{number-overlay-editor-C1EqJnWq.js → number-overlay-editor-BVXM7aAs.js} +1 -1
- package/dist/web/assets/{overview-panel-vTeeJYVZ.js → overview-panel-DCCDmtCQ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DQUxN0wU.js +1 -0
- package/dist/web/assets/{panel-store-BSnkjgHt.js → panel-store-B4rijFKN.js} +1 -1
- package/dist/web/assets/{pdf-preview-CqFGRY0Z.js → pdf-preview-D7yLmAZW.js} +1 -1
- package/dist/web/assets/{perl-C9H7X7CT.js → perl-CkmpYKa_.js} +1 -1
- package/dist/web/assets/{php-Bx2U-cQ_.js → php-BwZ3KDPo.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-BOr9ek4Q.js +1 -0
- package/dist/web/assets/{postgres-viewer-DHwcQucn.js → postgres-viewer--CRN1fvO.js} +3 -3
- package/dist/web/assets/{process-table-BGRJNlpn.js → process-table-CXC10GSy.js} +1 -1
- package/dist/web/assets/{project-store--nvQiy75.js → project-store-BuY7rxn_.js} +1 -1
- package/dist/web/assets/{pug-Cw74sKsE.js → pug-B3rBPqQo.js} +1 -1
- package/dist/web/assets/{qml-BXD94Eqz.js → qml-BfDbWFmg.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DSErxZFb.js +1 -0
- package/dist/web/assets/{razor-DZ0-Ge0S.js → razor-B1ww1r38.js} +1 -1
- package/dist/web/assets/{rst-Dtdh0QUj.js → rst-C0MSq6m-.js} +1 -1
- package/dist/web/assets/{ruby-BC51BW2Q.js → ruby-ClIZ1wb3.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Sd3KyjN4.js → run-in-terminal-4efbW2Xv.js} +1 -1
- package/dist/web/assets/{sas-BN8i4qOU.js → sas-DR0j3o4_.js} +1 -1
- package/dist/web/assets/{scroll-area-0gljZ6lF.js → scroll-area-ZP8wcwTI.js} +1 -1
- package/dist/web/assets/{scss-H-QtA7Rz.js → scss-Vh3JLayR.js} +1 -1
- package/dist/web/assets/{send-to-chat-u6m8l-rH.js → send-to-chat-BNrEpAyC.js} +1 -1
- package/dist/web/assets/{settings-store-CHY6Kzin.js → settings-store-OLJE7dPH.js} +2 -2
- package/dist/web/assets/{settings-tab-IY6K1MiZ.js → settings-tab-B1rvHFc6.js} +1 -1
- package/dist/web/assets/settings-tab-BUlmPJ2w.js +1 -0
- package/dist/web/assets/{shaderlab-CalNsj_x.js → shaderlab-0rHRnQ18.js} +1 -1
- package/dist/web/assets/{shellsession-BFrcoR8x.js → shellsession-Dxdw6D8c.js} +1 -1
- package/dist/web/assets/{soy-CNbku7vP.js → soy-BHlSNj1y.js} +1 -1
- package/dist/web/assets/{sparql-BN_orcVp.js → sparql-B2QaPpni.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C6pC3I8l.js → sql-query-editor-R_iKjPP9.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DTr5PzUb.js → sqlite-viewer-b2onA0SP.js} +1 -1
- package/dist/web/assets/{stata-C6a5Ijzq.js → stata-CwIxjsEJ.js} +1 -1
- package/dist/web/assets/{surrealql-ClplI_HY.js → surrealql-C6vo-rp-.js} +1 -1
- package/dist/web/assets/{svelte-DNm-UNXB.js → svelte-LLdJ6rlH.js} +1 -1
- package/dist/web/assets/{switch-DKvKya9y.js → switch-DV-BDjyk.js} +1 -1
- package/dist/web/assets/system-monitor-body-DPYQlBEt.js +2 -0
- package/dist/web/assets/system-monitor-tab-CPQdWv27.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BUqRsPK2.js → system-monitor-window-content-BPif0azo.js} +1 -1
- package/dist/web/assets/tab-host-window-content-BnAN6m4K.js +1 -0
- package/dist/web/assets/{tab-store-CJuJRapl.js → tab-store-tFnyPmm4.js} +1 -1
- package/dist/web/assets/team-member-sheet-Bu2wG1sm.js +2 -0
- package/dist/web/assets/{team-member-window-content-T4Q5JI-G.js → team-member-window-content-BSyU-5Uc.js} +1 -1
- package/dist/web/assets/{templ-CSdqVobu.js → templ-CJGvhEP5.js} +1 -1
- package/dist/web/assets/{terminal-tab-Bb2rZegQ.js → terminal-tab-Ncor0Rfu.js} +3 -3
- package/dist/web/assets/{tex-DzSJ4kcZ.js → tex-psXWhc4t.js} +1 -1
- package/dist/web/assets/{tool-cards-lhqhrDTT.js → tool-cards-DdVP1wrD.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-C17p0LQw.js +1 -0
- package/dist/web/assets/{ts-tags-C28w3bTg.js → ts-tags-D5Ar_ZCO.js} +1 -1
- package/dist/web/assets/{twig-i5jY3QXB.js → twig-vJgGC-qY.js} +1 -1
- package/dist/web/assets/{use-blob-url-Dsq9OjTh.js → use-blob-url-BOVGWg9F.js} +1 -1
- package/dist/web/assets/use-coarse-long-press-BmI_GVW5.js +1 -0
- package/dist/web/assets/use-monaco-theme-BshnLXoY.js +1 -0
- package/dist/web/assets/{use-open-team-member-DkiEjFYE.js → use-open-team-member-CY3wOdWL.js} +1 -1
- package/dist/web/assets/{use-websocket-B8R50f7f.js → use-websocket-B7xjNsyD.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-By4AVvJY.js → vendor-mermaid-999ksD45.js} +1 -1
- package/dist/web/assets/video-preview-7F72sRwf.js +1 -0
- package/dist/web/assets/{vue-Ceyet_lv.js → vue-BWngFyav.js} +1 -1
- package/dist/web/assets/{vue-html-GuU7g_hg.js → vue-html-BN5ZLu-H.js} +1 -1
- package/dist/web/assets/{vue-vine-CxXoaJr2.js → vue-vine-BZHnO8yV.js} +1 -1
- package/dist/web/assets/window-pip-registry-BnYU83LE.js +1 -0
- package/dist/web/assets/{window-store-CsEW6jiE.js → window-store-CQ1lghvv.js} +1 -1
- package/dist/web/assets/{xml-CbqOVhkP.js → xml-DS2beZHp.js} +1 -1
- package/dist/web/assets/{xsl-KfAmCauy.js → xsl-Ciiku1Bq.js} +1 -1
- package/dist/web/index.html +47 -45
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +309 -0
- package/docs/architecture/data-and-storage.md +356 -0
- package/docs/architecture/extensions.md +496 -0
- package/docs/architecture/integrations.md +214 -0
- package/docs/architecture/workspace-and-ui.md +399 -0
- package/docs/claude-agent-sdk-reference.md +1 -1
- package/docs/code-patterns.md +361 -0
- package/docs/code-standards.md +8 -358
- package/docs/codebase-subsystems.md +364 -0
- package/docs/codebase-summary.md +34 -379
- package/docs/deployment-guide.md +280 -490
- package/docs/design-guidelines.md +4 -3
- package/docs/lessons-learned.md +27 -10
- package/docs/project-changelog.md +18 -806
- package/docs/project-overview-pdr.md +117 -111
- package/docs/project-roadmap.md +89 -133
- package/docs/system-architecture.md +77 -1826
- package/package.json +2 -2
- package/src/cli/commands/autostart.ts +2 -2
- package/src/cli/commands/config-cmd.ts +12 -3
- package/src/cli/commands/status.ts +3 -2
- package/src/cli/commands/stop.ts +32 -4
- package/src/server/index.ts +17 -3
- package/src/server/routes/chat-unread.ts +36 -0
- package/src/server/routes/named-tunnel.ts +132 -0
- package/src/server/routes/tunnel.ts +13 -0
- package/src/server/ws/chat.ts +5 -2
- package/src/services/autostart-register.ts +47 -29
- package/src/services/cloud.service.ts +26 -1
- package/src/services/config-secret-keys.ts +48 -0
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +62 -12
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/extension-rpc-handlers.ts +9 -1
- package/src/services/file-watcher/watch-tree.ts +67 -33
- package/src/services/file-watcher.service.ts +28 -10
- package/src/services/fs-credential-path-guard.ts +103 -0
- package/src/services/fs-path-guard.service.ts +14 -66
- package/src/services/named-tunnel/cloudflare-dns-api.ts +40 -0
- package/src/services/named-tunnel/cloudflare-zone-api.ts +28 -0
- package/src/services/named-tunnel/cloudflared-cert.ts +95 -0
- package/src/services/named-tunnel/cloudflared-exec.ts +50 -0
- package/src/services/named-tunnel/cloudflared-login-helpers.ts +98 -0
- package/src/services/named-tunnel/cloudflared-login.service.ts +182 -0
- package/src/services/named-tunnel/hostname-rules.ts +58 -0
- package/src/services/named-tunnel/login-output-parser.ts +36 -0
- package/src/services/named-tunnel/named-tunnel-args.ts +110 -0
- package/src/services/named-tunnel/named-tunnel-config.ts +67 -0
- package/src/services/named-tunnel/named-tunnel-probe-state.ts +95 -0
- package/src/services/named-tunnel/named-tunnel-readiness.ts +59 -0
- package/src/services/named-tunnel/named-tunnel-runtime.ts +42 -0
- package/src/services/named-tunnel/named-tunnel-setup-confirm.ts +98 -0
- package/src/services/named-tunnel/named-tunnel-setup.service.ts +191 -0
- package/src/services/supervisor-state.ts +67 -2
- package/src/services/supervisor.ts +454 -157
- package/src/services/tunnel-registry-parse.ts +27 -0
- package/src/services/tunnel-registry.service.ts +2 -15
- package/src/services/tunnel.service.ts +12 -3
- package/src/services/windows-process-tree.ts +351 -351
- package/src/types/config.ts +31 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/chat/chat-history-bar.tsx +95 -98
- package/src/web/components/chat/chat-toolbar-overflow-menu.tsx +57 -0
- package/src/web/components/chat/session-debug-dialog.tsx +35 -27
- package/src/web/components/floating-window/floating-window.tsx +8 -1
- package/src/web/components/floating-window/open-tab-in-pip.ts +78 -0
- package/src/web/components/floating-window/window-pip-registry.ts +46 -0
- package/src/web/components/layout/cloud-share-named-tunnel-row.tsx +111 -0
- package/src/web/components/layout/cloud-share-popover.tsx +56 -14
- package/src/web/components/layout/tab-pop-out-menu-item.tsx +25 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +44 -5
- package/src/web/components/tunnels/named-tunnel/hostname-validation.ts +14 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +96 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-hostname-field.tsx +51 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-login-step.tsx +105 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +159 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-content.tsx +197 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-popup.tsx +57 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-step-reducer.ts +191 -0
- package/src/web/components/tunnels/named-tunnel/use-named-tunnel-setup.ts +212 -0
- package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -0
- package/src/web/hooks/use-global-events.ts +3 -1
- package/src/web/lib/api-named-tunnel.ts +54 -0
- package/src/web/lib/last-known-local-url.ts +23 -0
- package/src/web/stores/notification-store.ts +20 -1
- package/dist/web/assets/api-client-DTZEgjBQ.js +0 -1
- package/dist/web/assets/api-settings-kd1j4iR8.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-8dCMT9r7.js +0 -1
- package/dist/web/assets/chat-tab-E3bf_V8X.js +0 -15
- package/dist/web/assets/code-editor-BUlM2gXk.js +0 -10
- package/dist/web/assets/diff-viewer-CZaDnR4w.js +0 -4
- package/dist/web/assets/explorer-body-9aBmyVpZ.js +0 -2
- package/dist/web/assets/explorer-window-content-DRtqoSPA.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-2e_8dfbs.js +0 -1
- package/dist/web/assets/glide-data-grid-C4_zIneN.js +0 -138
- package/dist/web/assets/group-chat-tab-rOd8J8GC.js +0 -1
- package/dist/web/assets/index-BImUaigO.js +0 -52
- package/dist/web/assets/index-CSfi6DSU.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BZi1Gha1.js +0 -1
- package/dist/web/assets/keybindings-store-CzV2sPls.js +0 -1
- package/dist/web/assets/markdown-renderer-CcbR33VG.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-SIwQXlwA.js +0 -1
- package/dist/web/assets/notification-store-DV5QtUTg.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-o5htEcin.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BXAIFSAD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Dbt2Aebl.js +0 -1
- package/dist/web/assets/settings-tab-Dic6TKkU.js +0 -1
- package/dist/web/assets/system-monitor-body-iwGiCns9.js +0 -2
- package/dist/web/assets/system-monitor-tab-DdXQhEel.js +0 -1
- package/dist/web/assets/tab-host-window-content-BnqGPu8a.js +0 -1
- package/dist/web/assets/team-member-sheet-LkAFSCvb.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-DUbnl9dz.js +0 -1
- package/dist/web/assets/use-monaco-theme-BZvlsoBv.js +0 -1
- package/dist/web/assets/video-preview-m0-3p7QZ.js +0 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- /package/dist/web/assets/{abap-C6nB04tn.js → abap-CjzrhAS2.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Vx4rw2sC.js → actionscript-3-B59Kbl-9.js} +0 -0
- /package/dist/web/assets/{ada-Ccd8vM20.js → ada-B7gItKAE.js} +0 -0
- /package/dist/web/assets/{andromeeda-DnBX5xMg.js → andromeeda-Dy5zcy0Q.js} +0 -0
- /package/dist/web/assets/{apache-B-9_sx0g.js → apache-B6T9jrc0.js} +0 -0
- /package/dist/web/assets/{apex-_wPVBYoL.js → apex-CTsIxDQw.js} +0 -0
- /package/dist/web/assets/{api-client-aQb5hHE5.js → api-client-oY1tC3tM.js} +0 -0
- /package/dist/web/assets/{applescript-CcvD-qr0.js → applescript-DuycKmmb.js} +0 -0
- /package/dist/web/assets/{apply-theme-BeLRfsSu.js → apply-theme-r8tKDrCR.js} +0 -0
- /package/dist/web/assets/{ara-C40Ztoms.js → ara-CISL3599.js} +0 -0
- /package/dist/web/assets/{asciidoc-CybYkJV4.js → asciidoc-BXyE2Lpr.js} +0 -0
- /package/dist/web/assets/{asm-9eOKwzR0.js → asm-y258phtP.js} +0 -0
- /package/dist/web/assets/{aurora-x-B5ZH4t7t.js → aurora-x-Dq3Jc78M.js} +0 -0
- /package/dist/web/assets/{awk-Dqilob-2.js → awk-DxL6XSJv.js} +0 -0
- /package/dist/web/assets/{ayu-dark-CPgRhf5r.js → ayu-dark-DjoK6Evo.js} +0 -0
- /package/dist/web/assets/{ayu-light-Do06Q52f.js → ayu-light-CKKXBLMI.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-_BhxcrxY.js → ayu-mirage-DGXSrrXB.js} +0 -0
- /package/dist/web/assets/{ballerina-DDwK54QW.js → ballerina-DZSXFmUf.js} +0 -0
- /package/dist/web/assets/{bat-BGyGlzGk.js → bat-BNsZk6nY.js} +0 -0
- /package/dist/web/assets/{beancount-CRWBdapI.js → beancount-cPl4gJct.js} +0 -0
- /package/dist/web/assets/{berry-DbSFn2gO.js → berry-DGAodERk.js} +0 -0
- /package/dist/web/assets/{bibtex-QpsV9gm9.js → bibtex-DftVoRif.js} +0 -0
- /package/dist/web/assets/{bicep-CxS8CUve.js → bicep-Suioyyzh.js} +0 -0
- /package/dist/web/assets/{bird2-CuZhQzYR.js → bird2-DjU24I2P.js} +0 -0
- /package/dist/web/assets/{c-9bnDqE3g.js → c-DFi8Y_nN.js} +0 -0
- /package/dist/web/assets/{c3-JMZTrOhA.js → c3-1eSHnH5V.js} +0 -0
- /package/dist/web/assets/{cadence-DsDNayJS.js → cadence-DLLsFwQP.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-BJfpynya.js → can-open-in-ppm-in727eFQ.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DYIKXXBe.js → catppuccin-frappe-DDj0JEiY.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-IYYOnUJ8.js → catppuccin-latte-DPyANHyR.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-BrJ_7mfS.js → catppuccin-macchiato-CO-h8VTT.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-gzSu7v7-.js → catppuccin-mocha-BCmbN2IZ.js} +0 -0
- /package/dist/web/assets/{clarity-DLzsC0Hb.js → clarity-C0pYsZjs.js} +0 -0
- /package/dist/web/assets/{clipboard-7fw0Shic.js → clipboard-DBLGKMrH.js} +0 -0
- /package/dist/web/assets/{clojure-DS1BvoTt.js → clojure-JFr6s8iZ.js} +0 -0
- /package/dist/web/assets/{cmake-CDvsJwYp.js → cmake-EEcLEgVy.js} +0 -0
- /package/dist/web/assets/{codeowners-BjsYiflJ.js → codeowners-Deh3Lx0u.js} +0 -0
- /package/dist/web/assets/{codeql-CG-7ARat.js → codeql-C317igT5.js} +0 -0
- /package/dist/web/assets/{common-lisp-DSO1Lbh9.js → common-lisp-DepbATZs.js} +0 -0
- /package/dist/web/assets/{coq-DYdh3oP_.js → coq-MxgxU5ya.js} +0 -0
- /package/dist/web/assets/{csharp-DxEBz5s0.js → csharp-eEFXdsAq.js} +0 -0
- /package/dist/web/assets/{css-dal3Q-8C.js → css-Db-syWW9.js} +0 -0
- /package/dist/web/assets/{csv-CpqjMGiA.js → csv-DHeYcbaq.js} +0 -0
- /package/dist/web/assets/{csv-parser-3umBA4NY.js → csv-parser-_JWMLGl3.js} +0 -0
- /package/dist/web/assets/{cue-DinoJ_5k.js → cue-BmUNowrt.js} +0 -0
- /package/dist/web/assets/{cypher-DzxqmkUW.js → cypher-XjXHgCNS.js} +0 -0
- /package/dist/web/assets/{d-BxNwJuEC.js → d-BWJnZtig.js} +0 -0
- /package/dist/web/assets/{dark-plus-Clmv3V-0.js → dark-plus-Db7umas1.js} +0 -0
- /package/dist/web/assets/{dart-BWVtXD35.js → dart-DSI4HYh1.js} +0 -0
- /package/dist/web/assets/{data-grid-types-B4n51Fck.js → data-grid-types-CfVHh9gv.js} +0 -0
- /package/dist/web/assets/{dax-DsImuWgJ.js → dax-yw1JsAni.js} +0 -0
- /package/dist/web/assets/{desktop-B0jJuG2r.js → desktop-C2QO4RvD.js} +0 -0
- /package/dist/web/assets/{diff-CWqAAxoE.js → diff-Drt8aEyp.js} +0 -0
- /package/dist/web/assets/{dist-B-EXdSYE.js → dist-DNRvhVdz.js} +0 -0
- /package/dist/web/assets/{dist-C0eNNfbt.js → dist-VBxOHLmN.js} +0 -0
- /package/dist/web/assets/{docker-CYFCJdIL.js → docker-ByDKbmLl.js} +0 -0
- /package/dist/web/assets/{dotenv-CKDUjURg.js → dotenv-BkfOLytR.js} +0 -0
- /package/dist/web/assets/{dracula-CSoPspHE.js → dracula-DAmrm-Un.js} +0 -0
- /package/dist/web/assets/{dracula-soft-BuFjw5L1.js → dracula-soft-BUOEnKqR.js} +0 -0
- /package/dist/web/assets/{dream-maker-B6PdDRZu.js → dream-maker-DQBIfSiO.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CMcXvD0G.js → emacs-lisp-Blh5kBD3.js} +0 -0
- /package/dist/web/assets/{everforest-dark-cU8-wYzi.js → everforest-dark-CEzhDxHq.js} +0 -0
- /package/dist/web/assets/{everforest-light-KhorllqK.js → everforest-light-CPk_5Khm.js} +0 -0
- /package/dist/web/assets/{external-link-B8BynhvY.js → external-link-B6Hl0U4X.js} +0 -0
- /package/dist/web/assets/{fennel-BCELZK87.js → fennel-B1MT_EBZ.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-CZ4DiR75.js → file-exclamation-point-JzhTU2TX.js} +0 -0
- /package/dist/web/assets/{fish-BQc0y_jy.js → fish-C_HGcewt.js} +0 -0
- /package/dist/web/assets/{fluent-BTINnwF6.js → fluent-Dp1Iclmx.js} +0 -0
- /package/dist/web/assets/{folder-symlink-B8XrtvQK.js → folder-symlink-Dx1hOk8X.js} +0 -0
- /package/dist/web/assets/{format-bytes-WVcFxJuT.js → format-bytes-53WBtyi2.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BQim9wDz.js → fortran-free-form-B_-UA4XC.js} +0 -0
- /package/dist/web/assets/{gdscript-03w5u8un.js → gdscript-Bm_JpjS_.js} +0 -0
- /package/dist/web/assets/{gdshader-Dp2daNzU.js → gdshader-C_PNmfYU.js} +0 -0
- /package/dist/web/assets/{genie-CoNcjPiD.js → genie-B5rmR2TF.js} +0 -0
- /package/dist/web/assets/{gherkin-NrPOSPQL.js → gherkin-BTJ34TJ9.js} +0 -0
- /package/dist/web/assets/{github-dark-z4RE9ZqA.js → github-dark-DOkNVj9H.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DOKw-1cI.js → github-dark-default-DTsmiyVo.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-DJRNMkEA.js → github-dark-dimmed-CjbxAe9s.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Bj7jJ_Gs.js → github-dark-high-contrast-r_M8F_k7.js} +0 -0
- /package/dist/web/assets/{github-light-B4k_yMFu.js → github-light-BzfxyMyb.js} +0 -0
- /package/dist/web/assets/{github-light-default-BHc-Uzhs.js → github-light-default-BOqiBIy5.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-CHisIywL.js → github-light-high-contrast-DQzBrDM4.js} +0 -0
- /package/dist/web/assets/{gleam-CfbypZ4q.js → gleam-BWmuAtD8.js} +0 -0
- /package/dist/web/assets/{gn-YoAjyhJa.js → gn-CNQPjiZW.js} +0 -0
- /package/dist/web/assets/{gnuplot-xVkc1f1c.js → gnuplot-BlHjcPMe.js} +0 -0
- /package/dist/web/assets/{go-GPCwP3Gu.js → go-Bn8Ba9uz.js} +0 -0
- /package/dist/web/assets/{groovy-Crh4Yww9.js → groovy-C1Y1_F4V.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-BluUFuDS.js → gruvbox-dark-hard-DueOsHav.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-Cxk1FWQp.js → gruvbox-dark-medium-DaQjD538.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CHXMiFmj.js → gruvbox-dark-soft-CbbX2WOm.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-B6eKPjAD.js → gruvbox-light-hard-E2M70ljZ.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-pDwMm1ut.js → gruvbox-light-medium-B_DDLY6h.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-Cxu6kHqK.js → gruvbox-light-soft-2SZmyC1F.js} +0 -0
- /package/dist/web/assets/{haskell-C6JKHjbK.js → haskell-D8DeoXtG.js} +0 -0
- /package/dist/web/assets/{haxe-BuBNdF0a.js → haxe-BmZm7B0W.js} +0 -0
- /package/dist/web/assets/{hcl-DZpDdGj2.js → hcl-CKnBM3ng.js} +0 -0
- /package/dist/web/assets/{hjson-CBkhYRuo.js → hjson-Ct9t4-ci.js} +0 -0
- /package/dist/web/assets/{hlsl-C5nMxnp9.js → hlsl-BZSASBqS.js} +0 -0
- /package/dist/web/assets/{horizon-VAFvA6R6.js → horizon-CzX1AZ08.js} +0 -0
- /package/dist/web/assets/{horizon-bright-BC0xn7T3.js → horizon-bright-CCVNeRIP.js} +0 -0
- /package/dist/web/assets/{houston-BxrUivmz.js → houston-DBlJN7xc.js} +0 -0
- /package/dist/web/assets/{hy-DLtOXoaH.js → hy-Dyc6h6Xb.js} +0 -0
- /package/dist/web/assets/{image-gallery-Gnf7TdCR.js → image-gallery-DujlrjOC.js} +0 -0
- /package/dist/web/assets/{imba-BdpFXM2W.js → imba-Bauru7s2.js} +0 -0
- /package/dist/web/assets/{ini-BInJO_T0.js → ini-SjdwtaTp.js} +0 -0
- /package/dist/web/assets/{java-Dxuddnkb.js → java-D2r0ocpf.js} +0 -0
- /package/dist/web/assets/{javascript-CVamJfLY.js → javascript-y-lof7iP.js} +0 -0
- /package/dist/web/assets/{json-BLZ0ozYk.js → json-DO4LSx5A.js} +0 -0
- /package/dist/web/assets/{json5-BnXzmMKs.js → json5-BEwWf-W3.js} +0 -0
- /package/dist/web/assets/{jsonc-CSgkSga7.js → jsonc-OBEFv7gP.js} +0 -0
- /package/dist/web/assets/{jsonl-CY5W2nF2.js → jsonl-DOyW4Mg3.js} +0 -0
- /package/dist/web/assets/{jsonnet-CEJYH3sF.js → jsonnet-BL5R2CpP.js} +0 -0
- /package/dist/web/assets/{jssm-CsJc7Uhk.js → jssm-ksPusAyg.js} +0 -0
- /package/dist/web/assets/{jsx-jCiRaHU7.js → jsx-B6XEu6O-.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-DfoA_GxX.js → kanagawa-dragon-JBVe_ji_.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-BOLYJpWd.js → kanagawa-lotus-ChprO68_.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-DfKlbT1G.js → kanagawa-wave-D_uB_R0Q.js} +0 -0
- /package/dist/web/assets/{kdl-CmssIFTi.js → kdl-pf5B8N_m.js} +0 -0
- /package/dist/web/assets/{kotlin-BRsNgp6x.js → kotlin-BdF0METC.js} +0 -0
- /package/dist/web/assets/{kusto-T8X9jUab.js → kusto-BneVP-OU.js} +0 -0
- /package/dist/web/assets/{laserwave-DkJMVJ8Z.js → laserwave-C4-UPIyG.js} +0 -0
- /package/dist/web/assets/{lean-C65KGVPq.js → lean-BN6Jdymq.js} +0 -0
- /package/dist/web/assets/{less-exIPdDlf.js → less-Cx-fdlDC.js} +0 -0
- /package/dist/web/assets/{lib-mC1NZfuw.js → lib-CMuBa85c.js} +0 -0
- /package/dist/web/assets/{libesm-DSutUtl1.js → libesm-BB74PRsH.js} +0 -0
- /package/dist/web/assets/{light-plus-DiHUXAZt.js → light-plus-DFnnn_iL.js} +0 -0
- /package/dist/web/assets/{llvm-CJBnEzFM.js → llvm-BGxtW0Nw.js} +0 -0
- /package/dist/web/assets/{loader-circle-gSPVxfvH.js → loader-circle-Dpqcv0eS.js} +0 -0
- /package/dist/web/assets/{lock-Did-87UV.js → lock-DldbT31x.js} +0 -0
- /package/dist/web/assets/{log-DxMyUMuP.js → log-kQb4eJnj.js} +0 -0
- /package/dist/web/assets/{logo-CC9NKzca.js → logo-DqAzaq7s.js} +0 -0
- /package/dist/web/assets/{luau-Bw10I6zf.js → luau-Dc2cp-zS.js} +0 -0
- /package/dist/web/assets/{make-C_6M90m6.js → make-DYg9dwaJ.js} +0 -0
- /package/dist/web/assets/{markdown-bR9_R-LF.js → markdown-O4EPpS3K.js} +0 -0
- /package/dist/web/assets/{material-theme-BUx2Qujn.js → material-theme-ClP8yfQ2.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-CRJrPgKm.js → material-theme-darker-LOK55hGu.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-CUAYppPa.js → material-theme-lighter-j72tzZqi.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-DuINF1fg.js → material-theme-ocean-C19LKClA.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-W6_icGCj.js → material-theme-palenight-DJ5tN2ax.js} +0 -0
- /package/dist/web/assets/{matlab-rfqn_b3z.js → matlab-DQNZ_NU8.js} +0 -0
- /package/dist/web/assets/{mdx-Bh1B_YOJ.js → mdx-CJayB7XK.js} +0 -0
- /package/dist/web/assets/{mermaid-Dxf6Ya2k.js → mermaid-Cb0TRX92.js} +0 -0
- /package/dist/web/assets/{min-dark-CivZ2L0h.js → min-dark-CWM8Bo-m.js} +0 -0
- /package/dist/web/assets/{min-light-B1Ykqtma.js → min-light-BBR1sjS2.js} +0 -0
- /package/dist/web/assets/{mipsasm-DynHaM0d.js → mipsasm-Jc7u7YSS.js} +0 -0
- /package/dist/web/assets/{mojo-Clw5PbGH.js → mojo-CTB0cutd.js} +0 -0
- /package/dist/web/assets/{monitor-Dv8vn_6O.js → monitor-4vWfqzuN.js} +0 -0
- /package/dist/web/assets/{monokai-DFIooFZT.js → monokai-CvM_YNKx.js} +0 -0
- /package/dist/web/assets/{moonbit-DnBSZWGM.js → moonbit-CbFEfYYM.js} +0 -0
- /package/dist/web/assets/{move-CCcL2PfL.js → move-BT38p0kT.js} +0 -0
- /package/dist/web/assets/{narrat-BDarg5Lk.js → narrat-55QZ_kOG.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-DrZOeCNW.js → nextflow-groovy-Bg8peyqg.js} +0 -0
- /package/dist/web/assets/{night-owl-BnZZCK7v.js → night-owl-DzS77iPK.js} +0 -0
- /package/dist/web/assets/{night-owl-light-CEwBsVHS.js → night-owl-light-DER7fGnx.js} +0 -0
- /package/dist/web/assets/{nix-amCzOPMR.js → nix-BSI8YesN.js} +0 -0
- /package/dist/web/assets/{nord-BIs2BC6q.js → nord-9bn7iNWo.js} +0 -0
- /package/dist/web/assets/{nushell-Y02fo4yw.js → nushell-qDVZn_lw.js} +0 -0
- /package/dist/web/assets/{objective-c-7xOZtWW1.js → objective-c-Blf-ATos.js} +0 -0
- /package/dist/web/assets/{objective-cpp-eVtjUvVB.js → objective-cpp-Bz8bAq47.js} +0 -0
- /package/dist/web/assets/{ocaml-Bi6ZwqG4.js → ocaml-B9heAsc3.js} +0 -0
- /package/dist/web/assets/{odin-8xf1Y0S9.js → odin-BaFYnOwO.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-hVv-_y_b.js → one-dark-pro-D6B_hSe-.js} +0 -0
- /package/dist/web/assets/{one-light-CJJ3MtHt.js → one-light-Ba-f0wyk.js} +0 -0
- /package/dist/web/assets/{openscad-B2HcywLk.js → openscad-UvYyL2MK.js} +0 -0
- /package/dist/web/assets/{pascal-C7fllV_7.js → pascal-B4-qvEoG.js} +0 -0
- /package/dist/web/assets/{pkl-qvLC1IWP.js → pkl-B146nqGx.js} +0 -0
- /package/dist/web/assets/{plastic-BT1wHZv2.js → plastic-L38t-Id3.js} +0 -0
- /package/dist/web/assets/{play-4MZi1Axp.js → play-CpCAoQRZ.js} +0 -0
- /package/dist/web/assets/{plsql-1MsmdmJD.js → plsql-ItOG5Cca.js} +0 -0
- /package/dist/web/assets/{plus-BhMZqFsN.js → plus-BR0qewJI.js} +0 -0
- /package/dist/web/assets/{po-BigskytD.js → po-imiCTaNX.js} +0 -0
- /package/dist/web/assets/{poimandres-DuklniF7.js → poimandres-Bu6sIba9.js} +0 -0
- /package/dist/web/assets/{polar-e2pjN_sp.js → polar-CTeGoFxV.js} +0 -0
- /package/dist/web/assets/{portal-container-context-Cm5HipFr.js → portal-container-context-a399Hd9F.js} +0 -0
- /package/dist/web/assets/{postcss-DtAeYjWw.js → postcss-jvFe6GNE.js} +0 -0
- /package/dist/web/assets/{powerquery-Cr2PtehI.js → powerquery-D9Dw1hJp.js} +0 -0
- /package/dist/web/assets/{powershell-BxFTGLJ4.js → powershell-CZqvYQtg.js} +0 -0
- /package/dist/web/assets/{prisma-COWJiwdZ.js → prisma-B4D2nO_7.js} +0 -0
- /package/dist/web/assets/{prolog-DFvGKRx6.js → prolog-CQbyUcCF.js} +0 -0
- /package/dist/web/assets/{proto-CcK80lFt.js → proto-Cwpo0cW1.js} +0 -0
- /package/dist/web/assets/{puppet-BF60bZ4F.js → puppet-CKxCwBod.js} +0 -0
- /package/dist/web/assets/{purescript-zL_69MzU.js → purescript-B-MyXMMp.js} +0 -0
- /package/dist/web/assets/{python-CIwKhfb_.js → python-D6QbSR2S.js} +0 -0
- /package/dist/web/assets/{qmldir-CXNe04Lo.js → qmldir-BpTSVcw_.js} +0 -0
- /package/dist/web/assets/{qss-C8lAWe6G.js → qss-DBc4gbpe.js} +0 -0
- /package/dist/web/assets/{r-CL5ZVL3_.js → r-i9jMMi0s.js} +0 -0
- /package/dist/web/assets/{racket-BBfh0uh0.js → racket-BWNaxU7C.js} +0 -0
- /package/dist/web/assets/{raku-o6E8R5Q3.js → raku-fqTZLn7L.js} +0 -0
- /package/dist/web/assets/{react-CBTnT4sY.js → react-X4jUntEf.js} +0 -0
- /package/dist/web/assets/{red-GomRF2Xp.js → red-D-JEcuzA.js} +0 -0
- /package/dist/web/assets/{refresh-cw-B06yaBrW.js → refresh-cw-B9iv7csb.js} +0 -0
- /package/dist/web/assets/{reg-_ffDIvx9.js → reg-BbirlU1S.js} +0 -0
- /package/dist/web/assets/{regexp-Bl5sF65W.js → regexp-D8VTFrtg.js} +0 -0
- /package/dist/web/assets/{rel-DgMdqDuj.js → rel-ByQGKiNM.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CRt_7wpd.js → resolve-theme-Cqb9zWYc.js} +0 -0
- /package/dist/web/assets/{riscv-8b3-Ye4X.js → riscv-B5nqCfLE.js} +0 -0
- /package/dist/web/assets/{ron-DrmU1KHP.js → ron-DOVh_iZR.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-DEfhNlFH.js → rose-pine-dawn-B7hkmmWy.js} +0 -0
- /package/dist/web/assets/{rose-pine-C_-E2tF1.js → rose-pine-jp51ZUzi.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-BdvX3gdn.js → rose-pine-moon-Ff063yLj.js} +0 -0
- /package/dist/web/assets/{rosmsg-B6GhGpd5.js → rosmsg-Cyk7Il8z.js} +0 -0
- /package/dist/web/assets/{rotate-ccw-COXu_9oj.js → rotate-ccw-DnuPzJRE.js} +0 -0
- /package/dist/web/assets/{rust-DmC97M1-.js → rust-DRNM5epF.js} +0 -0
- /package/dist/web/assets/{sass-B0tJOHLM.js → sass-D_cB0-M1.js} +0 -0
- /package/dist/web/assets/{save-CtIGOjmx.js → save-DxonD4y1.js} +0 -0
- /package/dist/web/assets/{scala-n8zaFOXv.js → scala-Crl-1kKS.js} +0 -0
- /package/dist/web/assets/{scheme-BKD9ztBU.js → scheme-DCaWyEJ0.js} +0 -0
- /package/dist/web/assets/{sdbl-D8gB5-rS.js → sdbl-BbXNqwtX.js} +0 -0
- /package/dist/web/assets/{send-BlyNJAzy.js → send-B-t5Mz26.js} +0 -0
- /package/dist/web/assets/{shallow-DYo6goJ4.js → shallow-4zAfs2KW.js} +0 -0
- /package/dist/web/assets/{shellscript-BfXlf_Ao.js → shellscript-BAgksBhW.js} +0 -0
- /package/dist/web/assets/{shield-check-B0iGUiOK.js → shield-check-CdTiK1j2.js} +0 -0
- /package/dist/web/assets/{shield-off-BNLWBhcq.js → shield-off-DuetkXKM.js} +0 -0
- /package/dist/web/assets/{slack-dark-CylEddse.js → slack-dark-GB-ElUW_.js} +0 -0
- /package/dist/web/assets/{slack-ochin-w8wg8vuC.js → slack-ochin-tU6CFD3S.js} +0 -0
- /package/dist/web/assets/{smalltalk-hiAXA64u.js → smalltalk-DSVGUbtL.js} +0 -0
- /package/dist/web/assets/{snazzy-light-BwlV3Xty.js → snazzy-light-DZhi3PoS.js} +0 -0
- /package/dist/web/assets/{solarized-dark-vmNGdykz.js → solarized-dark-D6AMCco7.js} +0 -0
- /package/dist/web/assets/{solarized-light-DGv_0JXq.js → solarized-light-DepuTWSE.js} +0 -0
- /package/dist/web/assets/{solidity-Y7QSH0B1.js → solidity-CsxN618l.js} +0 -0
- /package/dist/web/assets/{splunk-BtgpLjHi.js → splunk-CF1m0Kkv.js} +0 -0
- /package/dist/web/assets/{sql-CRHYhOia.js → sql-DvyMDgrB.js} +0 -0
- /package/dist/web/assets/{ssh-config-DOqSs3au.js → ssh-config-a8sDo-Q8.js} +0 -0
- /package/dist/web/assets/{stylus-crCAEaBT.js → stylus-DCJ-6AjT.js} +0 -0
- /package/dist/web/assets/{swift-eE3rT7TC.js → swift-c-ZFxyPP.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BWy14LBX.js → synthwave-84-9NPgBcuv.js} +0 -0
- /package/dist/web/assets/{system-verilog-DC0-DZM4.js → system-verilog-DPD02IOo.js} +0 -0
- /package/dist/web/assets/{systemd-to5p6N8I.js → systemd-BUr5HRtU.js} +0 -0
- /package/dist/web/assets/{table-Bbc783ko.js → table-FLU8ei8b.js} +0 -0
- /package/dist/web/assets/{talonscript-DoTcU8wm.js → talonscript-BNBQV5YJ.js} +0 -0
- /package/dist/web/assets/{tasl-cNZvUs95.js → tasl-BETjpSyu.js} +0 -0
- /package/dist/web/assets/{tcl-mZE8sZH8.js → tcl-Clp_AZiC.js} +0 -0
- /package/dist/web/assets/{terraform-CqHPpIfu.js → terraform-B0kZ0lim.js} +0 -0
- /package/dist/web/assets/{text-wrap-CBT4Gxtg.js → text-wrap-B05VGRa-.js} +0 -0
- /package/dist/web/assets/{tokyo-night-Tp2rTIZd.js → tokyo-night-oOJ0a7Dk.js} +0 -0
- /package/dist/web/assets/{toml-CU_vscgn.js → toml-DOPCSumy.js} +0 -0
- /package/dist/web/assets/{trash-2-Bk4yYlck.js → trash-2-CTXlTflF.js} +0 -0
- /package/dist/web/assets/{triangle-alert-B8EYecpe.js → triangle-alert-BRq0VAfd.js} +0 -0
- /package/dist/web/assets/{tsv-ik7iFB9D.js → tsv-D6qUGf84.js} +0 -0
- /package/dist/web/assets/{tsx-BMvvMKiv.js → tsx-B6qYhT6O.js} +0 -0
- /package/dist/web/assets/{turtle-DBqKCq9o.js → turtle-C0Rjzw73.js} +0 -0
- /package/dist/web/assets/{typescript-DXJDmMZZ.js → typescript-BiydShEu.js} +0 -0
- /package/dist/web/assets/{typespec-CBseCMtZ.js → typespec-DjmF6Aww.js} +0 -0
- /package/dist/web/assets/{typst-4oY25U9y.js → typst-CPuscfcL.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-D1weanbj.js → use-is-mobile-b94q8RvF.js} +0 -0
- /package/dist/web/assets/{utils-BNRH507E.js → utils-C9OjCrou.js} +0 -0
- /package/dist/web/assets/{v-BXaZ_1oy.js → v-DE4vpLLQ.js} +0 -0
- /package/dist/web/assets/{vala-BImWJE7z.js → vala-CmUF2pS5.js} +0 -0
- /package/dist/web/assets/{vb-CLfj2_sy.js → vb-BvcRoX4b.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-_yyu5n32.js → vendor-xterm-K8kpLxdb.js} +0 -0
- /package/dist/web/assets/{verilog-BWa8Ed2b.js → verilog-BGIzyH3R.js} +0 -0
- /package/dist/web/assets/{vesper-BGj1cg6m.js → vesper-CG7IrHQQ.js} +0 -0
- /package/dist/web/assets/{vhdl-mByK7mlx.js → vhdl-DMSumomw.js} +0 -0
- /package/dist/web/assets/{viml-CugmlW5g.js → viml-Drt7AEmU.js} +0 -0
- /package/dist/web/assets/{vitesse-black-vPdpy9TL.js → vitesse-black-DmsjfN0b.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BuQK3dUh.js → vitesse-dark-Dl55_WeJ.js} +0 -0
- /package/dist/web/assets/{vitesse-light-B3nz1XL9.js → vitesse-light-Crbv9aGg.js} +0 -0
- /package/dist/web/assets/{vyper-DH_GTIHG.js → vyper-CB7psEJj.js} +0 -0
- /package/dist/web/assets/{wasm-DfVbVJlW.js → wasm-BY1tuM5m.js} +0 -0
- /package/dist/web/assets/{wasm-C-LF9GjX.js → wasm-Dck8KPgP.js} +0 -0
- /package/dist/web/assets/{wenyan-CzQkHyHx.js → wenyan-DhewQ68b.js} +0 -0
- /package/dist/web/assets/{wgsl-Du_M1DW9.js → wgsl-DlmiP456.js} +0 -0
- /package/dist/web/assets/{wikitext-BWN_w5S7.js → wikitext-cQCnqNgE.js} +0 -0
- /package/dist/web/assets/{wit-BR13v5HD.js → wit-S5MaCngB.js} +0 -0
- /package/dist/web/assets/{wolfram-BoXxp_Xl.js → wolfram-Bh8n_9zr.js} +0 -0
- /package/dist/web/assets/{x-nhriOX9o.js → x-YDu4m2Iq.js} +0 -0
- /package/dist/web/assets/{yaml-DpR_EF-d.js → yaml-BoK_17l9.js} +0 -0
- /package/dist/web/assets/{zenscript-C-EdUtUd.js → zenscript-DB3ZFwNJ.js} +0 -0
- /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
|
-
|
|
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
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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 {
|
|
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 =
|
|
817
|
+
adoptedTunnelPid = winPid;
|
|
698
818
|
tunnelChild = null;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
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
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
try {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
log("
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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(
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|