@hienlh/ppm 0.18.11 → 0.18.13
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 +87 -29
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +19 -1
- package/dist/web/assets/{adaptive-context-menu-DGCKKeTy.js → adaptive-context-menu-CbgPYQmj.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-BOHHTTe7.js → ai-resource-editor-unM2HNmg.js} +1 -1
- package/dist/web/assets/{ai-settings-section-BigC_nZR.js → ai-settings-section-CdktdrwF.js} +1 -1
- package/dist/web/assets/{angular-html-BYPaydvh.js → angular-html-DcpbdcNG.js} +1 -1
- package/dist/web/assets/{angular-ts-Bb4AMKlk.js → angular-ts-Bv_WIk34.js} +1 -1
- package/dist/web/assets/api-client-BogqsFna.js +1 -0
- package/dist/web/assets/{api-settings-Bd3M4YpI.js → api-settings-BdCtex9i.js} +1 -1
- package/dist/web/assets/api-settings-DfXViPKz.js +1 -0
- package/dist/web/assets/{api-themes-Cpx419SL.js → api-themes-pRRoLvgD.js} +1 -1
- package/dist/web/assets/{apl-DB2VjFCS.js → apl-D5_lLszu.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +1 -0
- package/dist/web/assets/{astro-BeoJdVpk.js → astro-CBHwddYL.js} +1 -1
- package/dist/web/assets/{audio-preview-BgFuTn-0.js → audio-preview-BD1iDovf.js} +1 -1
- package/dist/web/assets/{blade-Cf2q1sPf.js → blade-DGe1d2Pq.js} +1 -1
- package/dist/web/assets/{bsl-W1kOaNpu.js → bsl-2lyAzRJA.js} +1 -1
- package/dist/web/assets/{button-5zTrkEoN.js → button-2WCAS8dI.js} +1 -1
- package/dist/web/assets/{cairo-BoAZFcje.js → cairo-DjzZeJrq.js} +1 -1
- package/dist/web/assets/chat-tab-DzK8LGti.js +15 -0
- package/dist/web/assets/{cobol-B0Nm7RXj.js → cobol-cC3XIuAJ.js} +1 -1
- package/dist/web/assets/code-editor-CuMfPN9X.js +10 -0
- package/dist/web/assets/{coffee-BakMn3on.js → coffee-4eGboVoU.js} +1 -1
- package/dist/web/assets/{conflict-editor-BUs5k9q_.js → conflict-editor-DdJag4QK.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-DF9_G-OU.js} +1 -1
- package/dist/web/assets/{crystal-kqqO3zkK.js → crystal-OavZlzcm.js} +1 -1
- package/dist/web/assets/{csv-preview-ByoDgLAZ.js → csv-preview-DnXwouF3.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-Brkf9z41.js → data-grid-overlay-editor-B9Gpjm3l.js} +1 -1
- package/dist/web/assets/{database-viewer-Btc3h8Yu.js → database-viewer-Dk8oGyZx.js} +1 -1
- package/dist/web/assets/{dialog-DwtGlowE.js → dialog-B-l2ZxVm.js} +1 -1
- package/dist/web/assets/{diff-viewer-CZaDnR4w.js → diff-viewer-eaUBjLCH.js} +1 -1
- package/dist/web/assets/{docx-preview-DAD_vg59.js → docx-preview-DAdHPlhZ.js} +1 -1
- package/dist/web/assets/{edge-Cq1ZKZuE.js → edge-TzYh8OlN.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-B_VE7ppQ.js → edit-diff-preview-DlwP3UIU.js} +1 -1
- package/dist/web/assets/{elixir-BM_JoJRk.js → elixir-Cy3RhKKH.js} +1 -1
- package/dist/web/assets/{elm-Dw7oO08Y.js → elm-BTBZdweK.js} +1 -1
- package/dist/web/assets/{erb-CfKOGAfk.js → erb-dv1mI0y4.js} +1 -1
- package/dist/web/assets/{erlang-DlDmGVeE.js → erlang-DjeiDrkl.js} +1 -1
- package/dist/web/assets/{esm-DGAP0GFh.js → esm-FRwvcesu.js} +1 -1
- package/dist/web/assets/explorer-body-DkCkSVrN.js +2 -0
- package/dist/web/assets/explorer-window-content-MsnTVngu.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-W0qRfaAU.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-BCH9DAIJ.js} +1 -1
- package/dist/web/assets/{fsharp-CdNvy0PG.js → fsharp-C93NYKYD.js} +1 -1
- package/dist/web/assets/{gdresource-DxuG5i8i.js → gdresource-BcIEE2eX.js} +1 -1
- package/dist/web/assets/{git-commit-3lMonsur.js → git-commit-BVjlXEy8.js} +1 -1
- package/dist/web/assets/{git-log-panel-Co0PBPHC.js → git-log-panel-2_4HuP47.js} +1 -1
- package/dist/web/assets/{git-rebase-N8Z9kIMf.js → git-rebase-Bj_21B1V.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +1 -0
- package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +138 -0
- package/dist/web/assets/{glimmer-js-Dp9OXiib.js → glimmer-js-C20yj4Zv.js} +1 -1
- package/dist/web/assets/{glimmer-ts-CDVKBcDj.js → glimmer-ts-ipbYC7kh.js} +1 -1
- package/dist/web/assets/{glsl-DPJxI4us.js → glsl-Dovazu3t.js} +1 -1
- package/dist/web/assets/{graphql-BrFhaQQl.js → graphql-DpMlM7hC.js} +1 -1
- package/dist/web/assets/{group-chat-tab-rOd8J8GC.js → group-chat-tab-DRLDHOp6.js} +1 -1
- package/dist/web/assets/{hack-BzXbDMlA.js → hack-gc7LCBuV.js} +1 -1
- package/dist/web/assets/{haml-CPpkX0s5.js → haml-vtlFm9gm.js} +1 -1
- package/dist/web/assets/{handlebars-BqrApy52.js → handlebars-DGKhpCji.js} +1 -1
- package/dist/web/assets/{html-DmjUl-OA.js → html-DvfhQbXb.js} +1 -1
- package/dist/web/assets/{html-derivative-Cnd6XFR5.js → html-derivative-C3oTSSCN.js} +1 -1
- package/dist/web/assets/{http-DMLWYun2.js → http-SG6zLTo7.js} +1 -1
- package/dist/web/assets/{hurl-CKkCVxbj.js → hurl-BWXsYlqW.js} +1 -1
- package/dist/web/assets/{hxml-CAx19TtQ.js → hxml-DUdMzaoh.js} +1 -1
- package/dist/web/assets/{image-preview-DHUoylQ1.js → image-preview-DxgFOV1T.js} +1 -1
- package/dist/web/assets/index-CBn4SOoT.js +52 -0
- package/dist/web/assets/index-DWpNR3nB.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +1 -0
- package/dist/web/assets/{input-DcEk7mA3.js → input-DuAF-Stj.js} +1 -1
- package/dist/web/assets/{jinja-BmB9YN9l.js → jinja-955w9RKM.js} +1 -1
- package/dist/web/assets/{jison-BaVsKKpw.js → jison-WWfqPo50.js} +1 -1
- package/dist/web/assets/{julia-BgVBLveX.js → julia-BFrfnaNl.js} +1 -1
- package/dist/web/assets/{just-C2ENIbl9.js → just-BrZdyR_I.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-CwtPPASD.js} +1 -1
- package/dist/web/assets/{liquid-NxpapreE.js → liquid-CYZuvDDd.js} +1 -1
- package/dist/web/assets/{lua-DVepQvwH.js → lua-BV095ZyY.js} +1 -1
- package/dist/web/assets/markdown-renderer-BTS8ifdu.js +1 -0
- package/dist/web/assets/{markdown-renderer-DlcDhgrF.js → markdown-renderer-CnRcKK3d.js} +3 -3
- package/dist/web/assets/{marko-CJ2606Xl.js → marko-CNeljZ1C.js} +1 -1
- package/dist/web/assets/{mdc-D2bjqclE.js → mdc-JfGapShy.js} +1 -1
- package/dist/web/assets/{media-url-H17XJF-I.js → media-url-Bm4PsduE.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-g-r2hCFa.js → mobile-bottom-sheet-M5iZDX7y.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +1 -0
- package/dist/web/assets/{nextflow-BtJwUbfF.js → nextflow-DD5C8nqX.js} +1 -1
- package/dist/web/assets/{nginx-j4gyIhHE.js → nginx-Ca7B__x_.js} +1 -1
- package/dist/web/assets/{nim-ClH_gjP5.js → nim-B8FogsDw.js} +1 -1
- package/dist/web/assets/notification-store-Yvy1I8gd.js +1 -0
- package/dist/web/assets/{number-overlay-editor-C1EqJnWq.js → number-overlay-editor-DG9xJwEG.js} +1 -1
- package/dist/web/assets/{overview-panel-vTeeJYVZ.js → overview-panel-s53bnl0q.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.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-DYt3aBzM.js} +1 -1
- package/dist/web/assets/{perl-C9H7X7CT.js → perl-BR78hS8R.js} +1 -1
- package/dist/web/assets/{php-Bx2U-cQ_.js → php-DXBOMC9z.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +1 -0
- package/dist/web/assets/{postgres-viewer-DHwcQucn.js → postgres-viewer-BvoWSuoZ.js} +3 -3
- package/dist/web/assets/{process-table-BGRJNlpn.js → process-table-BYwKigiD.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-CvTwVVKV.js} +1 -1
- package/dist/web/assets/{qml-BXD94Eqz.js → qml-CPSoZ1-B.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +1 -0
- package/dist/web/assets/{razor-DZ0-Ge0S.js → razor-BUA1YKtF.js} +1 -1
- package/dist/web/assets/{rst-Dtdh0QUj.js → rst-BJsq7ISz.js} +1 -1
- package/dist/web/assets/{ruby-BC51BW2Q.js → ruby-DSDMeYf5.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Sd3KyjN4.js → run-in-terminal-C9xpskFn.js} +1 -1
- package/dist/web/assets/{sas-BN8i4qOU.js → sas-B1nJOjbN.js} +1 -1
- package/dist/web/assets/{scroll-area-0gljZ6lF.js → scroll-area-DBcG1fQo.js} +1 -1
- package/dist/web/assets/{scss-H-QtA7Rz.js → scss-BZFyRiQn.js} +1 -1
- package/dist/web/assets/{send-to-chat-u6m8l-rH.js → send-to-chat-BIg70MUZ.js} +1 -1
- package/dist/web/assets/{settings-store-CHY6Kzin.js → settings-store-GV5pkkke.js} +2 -2
- package/dist/web/assets/{settings-tab-IY6K1MiZ.js → settings-tab-BePvIcjd.js} +1 -1
- package/dist/web/assets/settings-tab-Br-lJSPa.js +1 -0
- package/dist/web/assets/{shaderlab-CalNsj_x.js → shaderlab-laINRXBo.js} +1 -1
- package/dist/web/assets/{shellsession-BFrcoR8x.js → shellsession-BpOOvWVR.js} +1 -1
- package/dist/web/assets/{soy-CNbku7vP.js → soy-BW4d91Sa.js} +1 -1
- package/dist/web/assets/{sparql-BN_orcVp.js → sparql-BsvvyoHn.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C6pC3I8l.js → sql-query-editor-qQce5QJ6.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DTr5PzUb.js → sqlite-viewer-yhc0YNvu.js} +1 -1
- package/dist/web/assets/{stata-C6a5Ijzq.js → stata-K-dZInUt.js} +1 -1
- package/dist/web/assets/{surrealql-ClplI_HY.js → surrealql-CpNp2sgf.js} +1 -1
- package/dist/web/assets/{svelte-DNm-UNXB.js → svelte-BG6iBA8R.js} +1 -1
- package/dist/web/assets/{switch-DKvKya9y.js → switch-_NcrktLe.js} +1 -1
- package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +2 -0
- package/dist/web/assets/system-monitor-tab-BclYXFTe.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BUqRsPK2.js → system-monitor-window-content-CaOhD0AU.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-BnqGPu8a.js → tab-host-window-content-f_Rc5rJK.js} +1 -1
- package/dist/web/assets/{tab-store-CJuJRapl.js → tab-store-tFnyPmm4.js} +1 -1
- package/dist/web/assets/team-member-sheet-QDHoYCs0.js +2 -0
- package/dist/web/assets/{team-member-window-content-T4Q5JI-G.js → team-member-window-content-B1jkKkDi.js} +1 -1
- package/dist/web/assets/{templ-CSdqVobu.js → templ-gxNKXmt3.js} +1 -1
- package/dist/web/assets/{terminal-tab-Bb2rZegQ.js → terminal-tab-UvwM0sty.js} +3 -3
- package/dist/web/assets/{tex-DzSJ4kcZ.js → tex-C0ON27g5.js} +1 -1
- package/dist/web/assets/{tool-cards-lhqhrDTT.js → tool-cards-jB7GT-wj.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +1 -0
- package/dist/web/assets/{ts-tags-C28w3bTg.js → ts-tags-BX_CTVgQ.js} +1 -1
- package/dist/web/assets/{twig-i5jY3QXB.js → twig-CwL7XnU0.js} +1 -1
- package/dist/web/assets/{use-blob-url-Dsq9OjTh.js → use-blob-url-DcRcjNTE.js} +1 -1
- package/dist/web/assets/use-coarse-long-press-BmI_GVW5.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BZvlsoBv.js → use-monaco-theme--9tOOkDA.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DkiEjFYE.js → use-open-team-member-1hRsHLKg.js} +1 -1
- package/dist/web/assets/{use-websocket-B8R50f7f.js → use-websocket-rxJLbFqy.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-By4AVvJY.js → vendor-mermaid-CHTa6D69.js} +1 -1
- package/dist/web/assets/video-preview-DMhnf2Gi.js +1 -0
- package/dist/web/assets/{vue-Ceyet_lv.js → vue-Bykm6wSS.js} +1 -1
- package/dist/web/assets/{vue-html-GuU7g_hg.js → vue-html-CETdK_iV.js} +1 -1
- package/dist/web/assets/{vue-vine-CxXoaJr2.js → vue-vine-DiJb6fO5.js} +1 -1
- package/dist/web/assets/{window-store-CsEW6jiE.js → window-store-CQ1lghvv.js} +1 -1
- package/dist/web/assets/{xml-CbqOVhkP.js → xml-BNw8J5YW.js} +1 -1
- package/dist/web/assets/{xsl-KfAmCauy.js → xsl-CsG9L31b.js} +1 -1
- package/dist/web/index.html +46 -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 +391 -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 +1 -1
- 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/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/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/layout/cloud-share-named-tunnel-row.tsx +111 -0
- package/src/web/components/layout/cloud-share-popover.tsx +56 -14
- 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/.spike/cdp-client.ts +0 -116
- package/.spike/cleanup-ppm-workspace.mjs +0 -44
- package/.spike/group-protected-probe.ts +0 -21
- package/.spike/pip-eval.ts +0 -17
- package/.spike/pip-page-helpers.js +0 -119
- package/.spike/pip-results-editor-glue.json +0 -105
- package/.spike/pip-results-editor.json +0 -158
- package/.spike/pip-results-input-isolation.json +0 -28
- package/.spike/pip-results-key-probe.json +0 -58
- package/.spike/pip-results-listener-survival.json +0 -98
- package/.spike/pip-results-listeners.json +0 -36
- package/.spike/pip-results-pagehide.json +0 -92
- package/.spike/pip-results-resize.json +0 -104
- package/.spike/pip-results-terminal-debug.json +0 -99
- package/.spike/pip-results-terminal-v2.json +0 -191
- package/.spike/pip-results-terminal.json +0 -220
- package/.spike/pip-results-xterm-trace.json +0 -73
- package/.spike/pip-spike-common.ts +0 -83
- package/.spike/pip-spike-editor-glue.ts +0 -82
- package/.spike/pip-spike-editor.ts +0 -129
- package/.spike/pip-spike-input-isolation.ts +0 -38
- package/.spike/pip-spike-key-probe.ts +0 -34
- package/.spike/pip-spike-listener-survival.ts +0 -54
- package/.spike/pip-spike-listeners.ts +0 -19
- package/.spike/pip-spike-pagehide.ts +0 -59
- package/.spike/pip-spike-resize.ts +0 -42
- package/.spike/pip-spike-setup.ts +0 -63
- package/.spike/pip-spike-terminal-debug.ts +0 -81
- package/.spike/pip-spike-terminal-v2.ts +0 -139
- package/.spike/pip-spike-terminal.ts +0 -169
- package/.spike/pip-spike-xterm-trace.ts +0 -36
- package/.spike/probe-drag-memo-error.mjs +0 -67
- package/.spike/read-dev-db.ts +0 -15
- package/.spike/s.err +0 -63
- package/.spike/scratch-editor-target.ts +0 -7
- package/.spike/smoke-unified-chrome-pip.mjs +0 -353
- package/.spike/tunnel.err +0 -63
- package/.spike/vite-restart.err +0 -663
- package/.spike/w.err +0 -571
- 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/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/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/team-member-sheet-LkAFSCvb.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-DUbnl9dz.js +0 -1
- package/dist/web/assets/video-preview-m0-3p7QZ.js +0 -1
- /package/dist/web/assets/{abap-C6nB04tn.js → abap-DGlrX_8G.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Vx4rw2sC.js → actionscript-3-CUqRtUJE.js} +0 -0
- /package/dist/web/assets/{ada-Ccd8vM20.js → ada-CY3Spa33.js} +0 -0
- /package/dist/web/assets/{andromeeda-DnBX5xMg.js → andromeeda-Lxs2TJn7.js} +0 -0
- /package/dist/web/assets/{apache-B-9_sx0g.js → apache-DfxgGdgM.js} +0 -0
- /package/dist/web/assets/{apex-_wPVBYoL.js → apex-QVDF1dnI.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-pwJ8QXh-.js} +0 -0
- /package/dist/web/assets/{apply-theme-BeLRfsSu.js → apply-theme-B_bMVL6a.js} +0 -0
- /package/dist/web/assets/{ara-C40Ztoms.js → ara-mPjeX5YX.js} +0 -0
- /package/dist/web/assets/{asciidoc-CybYkJV4.js → asciidoc-Bf7LsDkI.js} +0 -0
- /package/dist/web/assets/{asm-9eOKwzR0.js → asm-rRKC1OUx.js} +0 -0
- /package/dist/web/assets/{aurora-x-B5ZH4t7t.js → aurora-x-DwQr0DkG.js} +0 -0
- /package/dist/web/assets/{awk-Dqilob-2.js → awk-Bpt768fN.js} +0 -0
- /package/dist/web/assets/{ayu-dark-CPgRhf5r.js → ayu-dark-D9lwQHMs.js} +0 -0
- /package/dist/web/assets/{ayu-light-Do06Q52f.js → ayu-light-B4-ptrU5.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-_BhxcrxY.js → ayu-mirage-DAyRzH5Q.js} +0 -0
- /package/dist/web/assets/{ballerina-DDwK54QW.js → ballerina-Cgdpj1HC.js} +0 -0
- /package/dist/web/assets/{bat-BGyGlzGk.js → bat-4FrN36S2.js} +0 -0
- /package/dist/web/assets/{beancount-CRWBdapI.js → beancount-DkcxQER3.js} +0 -0
- /package/dist/web/assets/{berry-DbSFn2gO.js → berry-Dnuut-Ml.js} +0 -0
- /package/dist/web/assets/{bibtex-QpsV9gm9.js → bibtex-CCDwmbuA.js} +0 -0
- /package/dist/web/assets/{bicep-CxS8CUve.js → bicep-BC0ryoGM.js} +0 -0
- /package/dist/web/assets/{bird2-CuZhQzYR.js → bird2-C50V_A-0.js} +0 -0
- /package/dist/web/assets/{c-9bnDqE3g.js → c-Ddorhq0U.js} +0 -0
- /package/dist/web/assets/{c3-JMZTrOhA.js → c3-DdIf5r9E.js} +0 -0
- /package/dist/web/assets/{cadence-DsDNayJS.js → cadence-s9pd1Wng.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-BJfpynya.js → can-open-in-ppm-C6hZV1OP.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DYIKXXBe.js → catppuccin-frappe-CzJNTyu-.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-IYYOnUJ8.js → catppuccin-latte-BNpt-m7s.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-BrJ_7mfS.js → catppuccin-macchiato-FYGkfsJ5.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-gzSu7v7-.js → catppuccin-mocha-it2g5D1q.js} +0 -0
- /package/dist/web/assets/{clarity-DLzsC0Hb.js → clarity-BVFBTjMe.js} +0 -0
- /package/dist/web/assets/{clipboard-7fw0Shic.js → clipboard-BWznymz7.js} +0 -0
- /package/dist/web/assets/{clojure-DS1BvoTt.js → clojure-BSYvXMI8.js} +0 -0
- /package/dist/web/assets/{cmake-CDvsJwYp.js → cmake-DGEqH-Np.js} +0 -0
- /package/dist/web/assets/{codeowners-BjsYiflJ.js → codeowners-D8FCOxWg.js} +0 -0
- /package/dist/web/assets/{codeql-CG-7ARat.js → codeql-COV0eluG.js} +0 -0
- /package/dist/web/assets/{common-lisp-DSO1Lbh9.js → common-lisp-xHnZUlgt.js} +0 -0
- /package/dist/web/assets/{coq-DYdh3oP_.js → coq-Cs_2i3Xg.js} +0 -0
- /package/dist/web/assets/{csharp-DxEBz5s0.js → csharp-B1ONB4PT.js} +0 -0
- /package/dist/web/assets/{css-dal3Q-8C.js → css-BgGQbvrZ.js} +0 -0
- /package/dist/web/assets/{csv-CpqjMGiA.js → csv-CNHmQBzs.js} +0 -0
- /package/dist/web/assets/{csv-parser-3umBA4NY.js → csv-parser-DWR24J17.js} +0 -0
- /package/dist/web/assets/{cue-DinoJ_5k.js → cue-ZXAKzwIy.js} +0 -0
- /package/dist/web/assets/{cypher-DzxqmkUW.js → cypher-4PL5bmT5.js} +0 -0
- /package/dist/web/assets/{d-BxNwJuEC.js → d-CEsV3RT2.js} +0 -0
- /package/dist/web/assets/{dark-plus-Clmv3V-0.js → dark-plus-NtFnYSDz.js} +0 -0
- /package/dist/web/assets/{dart-BWVtXD35.js → dart-CKHn4mDf.js} +0 -0
- /package/dist/web/assets/{data-grid-types-B4n51Fck.js → data-grid-types-CxU8oo-j.js} +0 -0
- /package/dist/web/assets/{dax-DsImuWgJ.js → dax-Cmkj1I_M.js} +0 -0
- /package/dist/web/assets/{desktop-B0jJuG2r.js → desktop-DJLalvPQ.js} +0 -0
- /package/dist/web/assets/{diff-CWqAAxoE.js → diff-BnS2wpj6.js} +0 -0
- /package/dist/web/assets/{dist-C0eNNfbt.js → dist-CuIdRyUB.js} +0 -0
- /package/dist/web/assets/{dist-B-EXdSYE.js → dist-uYjs3Wv6.js} +0 -0
- /package/dist/web/assets/{docker-CYFCJdIL.js → docker-CovNy7ml.js} +0 -0
- /package/dist/web/assets/{dotenv-CKDUjURg.js → dotenv-Gh4w3I6B.js} +0 -0
- /package/dist/web/assets/{dracula-CSoPspHE.js → dracula-NF0tAHXl.js} +0 -0
- /package/dist/web/assets/{dracula-soft-BuFjw5L1.js → dracula-soft-Cj_qBurK.js} +0 -0
- /package/dist/web/assets/{dream-maker-B6PdDRZu.js → dream-maker-CRuZPcl7.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CMcXvD0G.js → emacs-lisp-BACmC9qj.js} +0 -0
- /package/dist/web/assets/{everforest-dark-cU8-wYzi.js → everforest-dark-Dd9av4QG.js} +0 -0
- /package/dist/web/assets/{everforest-light-KhorllqK.js → everforest-light-C-jsss-B.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-BJA9XnL8.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-CKlhNSc2.js} +0 -0
- /package/dist/web/assets/{fluent-BTINnwF6.js → fluent-wfglRk5D.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-utwHMTry.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BQim9wDz.js → fortran-free-form-BlXCdid3.js} +0 -0
- /package/dist/web/assets/{gdscript-03w5u8un.js → gdscript-BwAVG6Et.js} +0 -0
- /package/dist/web/assets/{gdshader-Dp2daNzU.js → gdshader-DPc_T7ib.js} +0 -0
- /package/dist/web/assets/{genie-CoNcjPiD.js → genie-B-zZSLCX.js} +0 -0
- /package/dist/web/assets/{gherkin-NrPOSPQL.js → gherkin-CJ0ajpGs.js} +0 -0
- /package/dist/web/assets/{github-dark-z4RE9ZqA.js → github-dark-Dz8ZkzSP.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DOKw-1cI.js → github-dark-default-CNUYo_kp.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-DJRNMkEA.js → github-dark-dimmed-241z_Ycx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Bj7jJ_Gs.js → github-dark-high-contrast-7r6yZZYH.js} +0 -0
- /package/dist/web/assets/{github-light-B4k_yMFu.js → github-light-C8gmZn-D.js} +0 -0
- /package/dist/web/assets/{github-light-default-BHc-Uzhs.js → github-light-default-GAEgq1eA.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-CHisIywL.js → github-light-high-contrast-3z91352E.js} +0 -0
- /package/dist/web/assets/{gleam-CfbypZ4q.js → gleam-C7vxW5zd.js} +0 -0
- /package/dist/web/assets/{gn-YoAjyhJa.js → gn-CyJIXwHP.js} +0 -0
- /package/dist/web/assets/{gnuplot-xVkc1f1c.js → gnuplot-CqLbGgPd.js} +0 -0
- /package/dist/web/assets/{go-GPCwP3Gu.js → go-DdDcK9aX.js} +0 -0
- /package/dist/web/assets/{groovy-Crh4Yww9.js → groovy-UYHggEqH.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-BluUFuDS.js → gruvbox-dark-hard-B4ierkJn.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-Cxk1FWQp.js → gruvbox-dark-medium-DYwkDSZ6.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CHXMiFmj.js → gruvbox-dark-soft-CVL4P-Lz.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-B6eKPjAD.js → gruvbox-light-hard-BJ0LewUZ.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-pDwMm1ut.js → gruvbox-light-medium-CaqqjeGJ.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-Cxu6kHqK.js → gruvbox-light-soft-ByKMoVNr.js} +0 -0
- /package/dist/web/assets/{haskell-C6JKHjbK.js → haskell-BilYRVPp.js} +0 -0
- /package/dist/web/assets/{haxe-BuBNdF0a.js → haxe-Brs9Bj1s.js} +0 -0
- /package/dist/web/assets/{hcl-DZpDdGj2.js → hcl-ClrZpol-.js} +0 -0
- /package/dist/web/assets/{hjson-CBkhYRuo.js → hjson-9PvHnYMX.js} +0 -0
- /package/dist/web/assets/{hlsl-C5nMxnp9.js → hlsl-BbBk4vqV.js} +0 -0
- /package/dist/web/assets/{horizon-VAFvA6R6.js → horizon-E5Py0cny.js} +0 -0
- /package/dist/web/assets/{horizon-bright-BC0xn7T3.js → horizon-bright-DVCsNGEk.js} +0 -0
- /package/dist/web/assets/{houston-BxrUivmz.js → houston-JdFEax9Y.js} +0 -0
- /package/dist/web/assets/{hy-DLtOXoaH.js → hy-W_W3tLKT.js} +0 -0
- /package/dist/web/assets/{image-gallery-Gnf7TdCR.js → image-gallery-5IURd6db.js} +0 -0
- /package/dist/web/assets/{imba-BdpFXM2W.js → imba-CfAqJz7T.js} +0 -0
- /package/dist/web/assets/{ini-BInJO_T0.js → ini-C_3gvKWs.js} +0 -0
- /package/dist/web/assets/{java-Dxuddnkb.js → java-CKqtXZ-j.js} +0 -0
- /package/dist/web/assets/{javascript-CVamJfLY.js → javascript-C7gSEfae.js} +0 -0
- /package/dist/web/assets/{json-BLZ0ozYk.js → json-BoN6e11T.js} +0 -0
- /package/dist/web/assets/{json5-BnXzmMKs.js → json5-CgE7C8HW.js} +0 -0
- /package/dist/web/assets/{jsonc-CSgkSga7.js → jsonc-DjyoattY.js} +0 -0
- /package/dist/web/assets/{jsonl-CY5W2nF2.js → jsonl-lI82wGSw.js} +0 -0
- /package/dist/web/assets/{jsonnet-CEJYH3sF.js → jsonnet-CszEI8va.js} +0 -0
- /package/dist/web/assets/{jssm-CsJc7Uhk.js → jssm-CZXDlszb.js} +0 -0
- /package/dist/web/assets/{jsx-jCiRaHU7.js → jsx-BeRz0eEz.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-DfoA_GxX.js → kanagawa-dragon-BOmnUiyd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-BOLYJpWd.js → kanagawa-lotus-ffU_P5w-.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-DfKlbT1G.js → kanagawa-wave-DKTJZrMu.js} +0 -0
- /package/dist/web/assets/{kdl-CmssIFTi.js → kdl-BWECJveH.js} +0 -0
- /package/dist/web/assets/{kotlin-BRsNgp6x.js → kotlin-DpVwBCSt.js} +0 -0
- /package/dist/web/assets/{kusto-T8X9jUab.js → kusto-BgVqjUZA.js} +0 -0
- /package/dist/web/assets/{laserwave-DkJMVJ8Z.js → laserwave-CEVCi8em.js} +0 -0
- /package/dist/web/assets/{lean-C65KGVPq.js → lean-CGyfRhLJ.js} +0 -0
- /package/dist/web/assets/{less-exIPdDlf.js → less-B0_H10Ku.js} +0 -0
- /package/dist/web/assets/{lib-mC1NZfuw.js → lib-hrC3bruJ.js} +0 -0
- /package/dist/web/assets/{libesm-DSutUtl1.js → libesm-DVpFmwJu.js} +0 -0
- /package/dist/web/assets/{light-plus-DiHUXAZt.js → light-plus-DqzMqPID.js} +0 -0
- /package/dist/web/assets/{llvm-CJBnEzFM.js → llvm-9ucnfNiF.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-m6A2dOSj.js} +0 -0
- /package/dist/web/assets/{logo-CC9NKzca.js → logo-ByPcsC9P.js} +0 -0
- /package/dist/web/assets/{luau-Bw10I6zf.js → luau-DFCo8nBn.js} +0 -0
- /package/dist/web/assets/{make-C_6M90m6.js → make-C90w3MeA.js} +0 -0
- /package/dist/web/assets/{markdown-bR9_R-LF.js → markdown-DT0uywZG.js} +0 -0
- /package/dist/web/assets/{material-theme-BUx2Qujn.js → material-theme-BVUO93tJ.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-CRJrPgKm.js → material-theme-darker-xXgGDwnF.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-CUAYppPa.js → material-theme-lighter-BjStPzWB.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-DuINF1fg.js → material-theme-ocean-wFmSiKjy.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-W6_icGCj.js → material-theme-palenight-e-0CtRoe.js} +0 -0
- /package/dist/web/assets/{matlab-rfqn_b3z.js → matlab-BNaNQAUH.js} +0 -0
- /package/dist/web/assets/{mdx-Bh1B_YOJ.js → mdx-DfpVWAqO.js} +0 -0
- /package/dist/web/assets/{mermaid-Dxf6Ya2k.js → mermaid-Bpq7wFCP.js} +0 -0
- /package/dist/web/assets/{min-dark-CivZ2L0h.js → min-dark-CL2Y0WH6.js} +0 -0
- /package/dist/web/assets/{min-light-B1Ykqtma.js → min-light-DOgr_IdP.js} +0 -0
- /package/dist/web/assets/{mipsasm-DynHaM0d.js → mipsasm-C9ie2zlB.js} +0 -0
- /package/dist/web/assets/{mojo-Clw5PbGH.js → mojo-Md2A2sJy.js} +0 -0
- /package/dist/web/assets/{monitor-Dv8vn_6O.js → monitor-4vWfqzuN.js} +0 -0
- /package/dist/web/assets/{monokai-DFIooFZT.js → monokai-nbXIEySB.js} +0 -0
- /package/dist/web/assets/{moonbit-DnBSZWGM.js → moonbit-CJ-T-s4A.js} +0 -0
- /package/dist/web/assets/{move-CCcL2PfL.js → move-DwEehLIK.js} +0 -0
- /package/dist/web/assets/{narrat-BDarg5Lk.js → narrat-CuF4sIKr.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-DrZOeCNW.js → nextflow-groovy-1dYhfPfq.js} +0 -0
- /package/dist/web/assets/{night-owl-BnZZCK7v.js → night-owl-gZS5mfzY.js} +0 -0
- /package/dist/web/assets/{night-owl-light-CEwBsVHS.js → night-owl-light-DDAster5.js} +0 -0
- /package/dist/web/assets/{nix-amCzOPMR.js → nix-BlbEYs2c.js} +0 -0
- /package/dist/web/assets/{nord-BIs2BC6q.js → nord-BRiPb8Vy.js} +0 -0
- /package/dist/web/assets/{nushell-Y02fo4yw.js → nushell-DgWicB1i.js} +0 -0
- /package/dist/web/assets/{objective-c-7xOZtWW1.js → objective-c-D8mPZ42Y.js} +0 -0
- /package/dist/web/assets/{objective-cpp-eVtjUvVB.js → objective-cpp-B0pNXzuj.js} +0 -0
- /package/dist/web/assets/{ocaml-Bi6ZwqG4.js → ocaml-lQd7aK5d.js} +0 -0
- /package/dist/web/assets/{odin-8xf1Y0S9.js → odin-CxtIOBjm.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-hVv-_y_b.js → one-dark-pro-T_UFnWfe.js} +0 -0
- /package/dist/web/assets/{one-light-CJJ3MtHt.js → one-light-LBVqwyV-.js} +0 -0
- /package/dist/web/assets/{openscad-B2HcywLk.js → openscad-C5dnS6J7.js} +0 -0
- /package/dist/web/assets/{pascal-C7fllV_7.js → pascal-CmWCWQML.js} +0 -0
- /package/dist/web/assets/{pkl-qvLC1IWP.js → pkl-BWWOtDQk.js} +0 -0
- /package/dist/web/assets/{plastic-BT1wHZv2.js → plastic-ekhupckF.js} +0 -0
- /package/dist/web/assets/{play-4MZi1Axp.js → play-CpCAoQRZ.js} +0 -0
- /package/dist/web/assets/{plsql-1MsmdmJD.js → plsql-BPsDHXx4.js} +0 -0
- /package/dist/web/assets/{plus-BhMZqFsN.js → plus-BR0qewJI.js} +0 -0
- /package/dist/web/assets/{po-BigskytD.js → po-BX2FiuW2.js} +0 -0
- /package/dist/web/assets/{poimandres-DuklniF7.js → poimandres-DzXOdZcN.js} +0 -0
- /package/dist/web/assets/{polar-e2pjN_sp.js → polar-DDRXgJzG.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-BHW6XBhx.js} +0 -0
- /package/dist/web/assets/{powerquery-Cr2PtehI.js → powerquery-B89c2NBb.js} +0 -0
- /package/dist/web/assets/{powershell-BxFTGLJ4.js → powershell-Dvxrd3Tu.js} +0 -0
- /package/dist/web/assets/{prisma-COWJiwdZ.js → prisma-lMK8EFLU.js} +0 -0
- /package/dist/web/assets/{prolog-DFvGKRx6.js → prolog-D0YrtMG4.js} +0 -0
- /package/dist/web/assets/{proto-CcK80lFt.js → proto-J0mM25aw.js} +0 -0
- /package/dist/web/assets/{puppet-BF60bZ4F.js → puppet-CUT0xkhh.js} +0 -0
- /package/dist/web/assets/{purescript-zL_69MzU.js → purescript-CoJWSGRp.js} +0 -0
- /package/dist/web/assets/{python-CIwKhfb_.js → python-BBrmuz6P.js} +0 -0
- /package/dist/web/assets/{qmldir-CXNe04Lo.js → qmldir-CUtzs4Q7.js} +0 -0
- /package/dist/web/assets/{qss-C8lAWe6G.js → qss-Dm1RZ8aL.js} +0 -0
- /package/dist/web/assets/{r-CL5ZVL3_.js → r-BGP7qZTe.js} +0 -0
- /package/dist/web/assets/{racket-BBfh0uh0.js → racket-K_6U1lqJ.js} +0 -0
- /package/dist/web/assets/{raku-o6E8R5Q3.js → raku-bNSiZeW_.js} +0 -0
- /package/dist/web/assets/{react-CBTnT4sY.js → react-X4jUntEf.js} +0 -0
- /package/dist/web/assets/{red-GomRF2Xp.js → red-uVeIEF0f.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-DSm9GWjh.js} +0 -0
- /package/dist/web/assets/{regexp-Bl5sF65W.js → regexp-CljRm_j_.js} +0 -0
- /package/dist/web/assets/{rel-DgMdqDuj.js → rel-d3JcopZg.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CRt_7wpd.js → resolve-theme-DLKmm-lE.js} +0 -0
- /package/dist/web/assets/{riscv-8b3-Ye4X.js → riscv-CJmaELjT.js} +0 -0
- /package/dist/web/assets/{ron-DrmU1KHP.js → ron-D0g9B1rW.js} +0 -0
- /package/dist/web/assets/{rose-pine-C_-E2tF1.js → rose-pine-DOrD2BSH.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-DEfhNlFH.js → rose-pine-dawn-CzZQ-LCF.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-BdvX3gdn.js → rose-pine-moon-CdmsYp7C.js} +0 -0
- /package/dist/web/assets/{rosmsg-B6GhGpd5.js → rosmsg-CmDth6pO.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-DGdP6jiR.js} +0 -0
- /package/dist/web/assets/{sass-B0tJOHLM.js → sass-Dkgt2FKZ.js} +0 -0
- /package/dist/web/assets/{save-CtIGOjmx.js → save-DxonD4y1.js} +0 -0
- /package/dist/web/assets/{scala-n8zaFOXv.js → scala-BQLTON6C.js} +0 -0
- /package/dist/web/assets/{scheme-BKD9ztBU.js → scheme-DKlxUGCi.js} +0 -0
- /package/dist/web/assets/{sdbl-D8gB5-rS.js → sdbl-C3iZJQeZ.js} +0 -0
- /package/dist/web/assets/{send-BlyNJAzy.js → send-B-t5Mz26.js} +0 -0
- /package/dist/web/assets/{shallow-DYo6goJ4.js → shallow-jwPzEunk.js} +0 -0
- /package/dist/web/assets/{shellscript-BfXlf_Ao.js → shellscript-BRu-ZQAx.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-qkm_u2zz.js} +0 -0
- /package/dist/web/assets/{slack-ochin-w8wg8vuC.js → slack-ochin-BP8_8e4m.js} +0 -0
- /package/dist/web/assets/{smalltalk-hiAXA64u.js → smalltalk-Bnz4gt07.js} +0 -0
- /package/dist/web/assets/{snazzy-light-BwlV3Xty.js → snazzy-light-fbPZg5io.js} +0 -0
- /package/dist/web/assets/{solarized-dark-vmNGdykz.js → solarized-dark-CM0vTRoi.js} +0 -0
- /package/dist/web/assets/{solarized-light-DGv_0JXq.js → solarized-light-C4_W9mvs.js} +0 -0
- /package/dist/web/assets/{solidity-Y7QSH0B1.js → solidity-Bz6v1PoD.js} +0 -0
- /package/dist/web/assets/{splunk-BtgpLjHi.js → splunk-DyNxu-0i.js} +0 -0
- /package/dist/web/assets/{sql-CRHYhOia.js → sql-DXRL1gw1.js} +0 -0
- /package/dist/web/assets/{ssh-config-DOqSs3au.js → ssh-config-2z1QHuOG.js} +0 -0
- /package/dist/web/assets/{stylus-crCAEaBT.js → stylus-D4I-VaeY.js} +0 -0
- /package/dist/web/assets/{swift-eE3rT7TC.js → swift-C_2G-tIc.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BWy14LBX.js → synthwave-84-osBjChRP.js} +0 -0
- /package/dist/web/assets/{system-verilog-DC0-DZM4.js → system-verilog-zQP2qPNU.js} +0 -0
- /package/dist/web/assets/{systemd-to5p6N8I.js → systemd-UGfjRSBM.js} +0 -0
- /package/dist/web/assets/{table-Bbc783ko.js → table-FLU8ei8b.js} +0 -0
- /package/dist/web/assets/{talonscript-DoTcU8wm.js → talonscript-C1XrFPwo.js} +0 -0
- /package/dist/web/assets/{tasl-cNZvUs95.js → tasl-B5raVn43.js} +0 -0
- /package/dist/web/assets/{tcl-mZE8sZH8.js → tcl-3-vbeMIc.js} +0 -0
- /package/dist/web/assets/{terraform-CqHPpIfu.js → terraform-a8VXZDm9.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-4SRhf-Hz.js} +0 -0
- /package/dist/web/assets/{toml-CU_vscgn.js → toml-B4N0i8wT.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-DA5k9Lvw.js} +0 -0
- /package/dist/web/assets/{tsx-BMvvMKiv.js → tsx-D9SZ8owC.js} +0 -0
- /package/dist/web/assets/{turtle-DBqKCq9o.js → turtle-BTHTvyY_.js} +0 -0
- /package/dist/web/assets/{typescript-DXJDmMZZ.js → typescript-DYzV9p5L.js} +0 -0
- /package/dist/web/assets/{typespec-CBseCMtZ.js → typespec-BC15eh7F.js} +0 -0
- /package/dist/web/assets/{typst-4oY25U9y.js → typst-BJUqzA6D.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-D1weanbj.js → use-is-mobile-DFWViA4S.js} +0 -0
- /package/dist/web/assets/{utils-BNRH507E.js → utils-C9OjCrou.js} +0 -0
- /package/dist/web/assets/{v-BXaZ_1oy.js → v-50Lps6oA.js} +0 -0
- /package/dist/web/assets/{vala-BImWJE7z.js → vala-8wiUvvrD.js} +0 -0
- /package/dist/web/assets/{vb-CLfj2_sy.js → vb-BlgPLt6B.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-_yyu5n32.js → vendor-xterm-BZOrwF_F.js} +0 -0
- /package/dist/web/assets/{verilog-BWa8Ed2b.js → verilog-CD9pVW5p.js} +0 -0
- /package/dist/web/assets/{vesper-BGj1cg6m.js → vesper-BrivW4PT.js} +0 -0
- /package/dist/web/assets/{vhdl-mByK7mlx.js → vhdl-Bunxfa7N.js} +0 -0
- /package/dist/web/assets/{viml-CugmlW5g.js → viml-DndUb2qM.js} +0 -0
- /package/dist/web/assets/{vitesse-black-vPdpy9TL.js → vitesse-black-BurZSkxW.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BuQK3dUh.js → vitesse-dark-8UShspzC.js} +0 -0
- /package/dist/web/assets/{vitesse-light-B3nz1XL9.js → vitesse-light-DghCAZ90.js} +0 -0
- /package/dist/web/assets/{vyper-DH_GTIHG.js → vyper-b-Clr-8u.js} +0 -0
- /package/dist/web/assets/{wasm-DfVbVJlW.js → wasm-C5v2T06Y.js} +0 -0
- /package/dist/web/assets/{wasm-C-LF9GjX.js → wasm-CpcKStkw.js} +0 -0
- /package/dist/web/assets/{wenyan-CzQkHyHx.js → wenyan-Dz2srLZR.js} +0 -0
- /package/dist/web/assets/{wgsl-Du_M1DW9.js → wgsl-CeD1-dRb.js} +0 -0
- /package/dist/web/assets/{wikitext-BWN_w5S7.js → wikitext-CyeS-9FH.js} +0 -0
- /package/dist/web/assets/{wit-BR13v5HD.js → wit-Kr-q-qBQ.js} +0 -0
- /package/dist/web/assets/{wolfram-BoXxp_Xl.js → wolfram-BKm8bmln.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-2XSxomjM.js} +0 -0
- /package/dist/web/assets/{zenscript-C-EdUtUd.js → zenscript-CKIS34vf.js} +0 -0
- /package/dist/web/assets/{zig-BD4eA5nU.js → zig-DdHGNllX.js} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime resolver: turns the persisted `tunnel` config row into the shape the
|
|
3
|
+
* supervisor and UI actually consume. This is where "incomplete" data degrades
|
|
4
|
+
* to quick — storage (`sanitizeConfig`) never makes that judgment itself, so
|
|
5
|
+
* `/disable`'s documented "keep the config so Retry works" contract survives a
|
|
6
|
+
* config round-trip.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type TunnelMode = "quick" | "named";
|
|
10
|
+
|
|
11
|
+
export interface ResolvedTunnelConfig {
|
|
12
|
+
mode: TunnelMode;
|
|
13
|
+
hostname: string | null;
|
|
14
|
+
tunnelName: string | null;
|
|
15
|
+
token: string | null;
|
|
16
|
+
zoneID: string | null;
|
|
17
|
+
accountID: string | null;
|
|
18
|
+
dismissed: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const QUICK: Omit<ResolvedTunnelConfig, "dismissed"> = {
|
|
22
|
+
mode: "quick", hostname: null, tunnelName: null, token: null, zoneID: null, accountID: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function nonEmptyString(v: unknown): v is string {
|
|
26
|
+
return typeof v === "string" && v.length > 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveTunnelConfig(raw: unknown): ResolvedTunnelConfig {
|
|
30
|
+
let obj: Record<string, unknown> | null = null;
|
|
31
|
+
if (typeof raw === "string") {
|
|
32
|
+
try { obj = JSON.parse(raw); } catch { obj = null; }
|
|
33
|
+
} else if (raw && typeof raw === "object") {
|
|
34
|
+
obj = raw as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dismissed = obj != null && typeof obj.dismissed === "boolean" ? obj.dismissed : false;
|
|
38
|
+
|
|
39
|
+
// Named mode requires BOTH the stored mode flag AND all four identity fields —
|
|
40
|
+
// checking fields alone would resurrect a tunnel the user explicitly disabled,
|
|
41
|
+
// since /disable intentionally keeps namedTunnel* fields around for Retry.
|
|
42
|
+
const named = obj != null && obj.mode === "named" &&
|
|
43
|
+
nonEmptyString(obj.namedTunnelToken) &&
|
|
44
|
+
nonEmptyString(obj.namedTunnelHostname) &&
|
|
45
|
+
nonEmptyString(obj.zoneID) &&
|
|
46
|
+
nonEmptyString(obj.accountID);
|
|
47
|
+
|
|
48
|
+
if (!named || obj == null) {
|
|
49
|
+
return { ...QUICK, dismissed };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
mode: "named",
|
|
54
|
+
hostname: obj.namedTunnelHostname as string,
|
|
55
|
+
tunnelName: nonEmptyString(obj.namedTunnelName) ? obj.namedTunnelName : null,
|
|
56
|
+
token: obj.namedTunnelToken as string,
|
|
57
|
+
zoneID: obj.zoneID as string,
|
|
58
|
+
accountID: obj.accountID as string,
|
|
59
|
+
dismissed,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `abcdef...` — first 6 chars + ellipsis; never the full secret. */
|
|
64
|
+
export function maskToken(token: string | null): string | null {
|
|
65
|
+
if (token == null) return null;
|
|
66
|
+
return `${token.slice(0, 6)}...`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision function for the named-tunnel probe's "restart once, then warn
|
|
3
|
+
* and stop" state machine. Kept separate from `supervisor.ts` (which owns the
|
|
4
|
+
* actual kill/respawn/status-write side effects) so the transition logic is
|
|
5
|
+
* unit-testable without spawning a real cloudflared process or timers.
|
|
6
|
+
*
|
|
7
|
+
* The bug this exists to prevent: resetting `restartAttempted` anywhere other
|
|
8
|
+
* than a confirmed-healthy observation lets a dark hostname (e.g. the CNAME
|
|
9
|
+
* deleted in the dashboard) restart the connector every threshold window
|
|
10
|
+
* forever, because the RESTART itself always "succeeds" (cloudflared happily
|
|
11
|
+
* reconnects to Cloudflare's edge) even though the DNS route it needs is gone —
|
|
12
|
+
* a spawn success is not proof the hostname is reachable, only the probe's own
|
|
13
|
+
* fetch against the public URL is.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface NamedProbeState {
|
|
17
|
+
/** Consecutive unhealthy probe cycles observed since the last reset. */
|
|
18
|
+
failCount: number;
|
|
19
|
+
/** Whether the one allowed restart-and-hope has already been used since the
|
|
20
|
+
* last confirmed-healthy observation. */
|
|
21
|
+
restartAttempted: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type NamedProbeAction =
|
|
25
|
+
| { type: "healthy" }
|
|
26
|
+
/** Counting a failure. `warnEarly` marks the tick that should surface a
|
|
27
|
+
* soft "checking the connection" notice without touching the connector. */
|
|
28
|
+
| { type: "watch"; warnEarly: boolean }
|
|
29
|
+
| { type: "restart-once" }
|
|
30
|
+
| { type: "warn-and-stop" };
|
|
31
|
+
|
|
32
|
+
export interface NamedProbeDecision {
|
|
33
|
+
action: NamedProbeAction;
|
|
34
|
+
nextState: NamedProbeState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Is the thing answering the public hostname *our* server? `publicId` is the
|
|
39
|
+
* `instanceId` the hostname returned, `localId` the one our loopback server
|
|
40
|
+
* returned; either may be missing.
|
|
41
|
+
*
|
|
42
|
+
* A deleted CNAME does not make the hostname go dark — it falls back to the
|
|
43
|
+
* zone's wildcard record and some unrelated host answers 200 with a body that
|
|
44
|
+
* has no `instanceId`. If we know our own identity and the public answer lacks
|
|
45
|
+
* one (or differs), the hostname is not ours. Only when our own server has no
|
|
46
|
+
* identity (a build that predates `instanceId`) do we accept bare reachability.
|
|
47
|
+
*/
|
|
48
|
+
export function publicHostnameIsOurs(publicId: unknown, localId: unknown): boolean {
|
|
49
|
+
if (typeof localId !== "string") return true;
|
|
50
|
+
return typeof publicId === "string" && publicId === localId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Decide the next action for one probe tick.
|
|
55
|
+
*
|
|
56
|
+
* - `healthy` → both counters reset to a clean slate (the only place
|
|
57
|
+
* `restartAttempted` ever goes back to `false`).
|
|
58
|
+
* - unhealthy below `threshold` → `watch`. Acting on a dark hostname is
|
|
59
|
+
* deliberately slow (a flaky minute must not kill a working connector), but
|
|
60
|
+
* *saying nothing* for that long is a different decision: `watch` carries
|
|
61
|
+
* `warnEarly` on the tick that crosses `earlyWarnAt`, so the UI can show
|
|
62
|
+
* "checking the connection" while the restart budget is still untouched.
|
|
63
|
+
* - unhealthy at/above `threshold`, first time → `restart-once` (kill +
|
|
64
|
+
* respawn the connector; arms `restartAttempted` so this never repeats
|
|
65
|
+
* until a healthy observation clears it).
|
|
66
|
+
* - unhealthy at/above `threshold`, restart already attempted → `warn-and-stop`
|
|
67
|
+
* (surface the warning; never kill again — the pinned URL stays put).
|
|
68
|
+
*/
|
|
69
|
+
export function decideNamedProbeAction(
|
|
70
|
+
healthy: boolean,
|
|
71
|
+
state: NamedProbeState,
|
|
72
|
+
threshold: number,
|
|
73
|
+
earlyWarnAt = 2,
|
|
74
|
+
): NamedProbeDecision {
|
|
75
|
+
if (healthy) {
|
|
76
|
+
return { action: { type: "healthy" }, nextState: { failCount: 0, restartAttempted: false } };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const failCount = state.failCount + 1;
|
|
80
|
+
if (failCount < threshold) {
|
|
81
|
+
// Only on the crossing tick: re-writing the same warning every 30s would
|
|
82
|
+
// rewrite status.json forever for no new information.
|
|
83
|
+
const warnEarly = failCount === earlyWarnAt;
|
|
84
|
+
return {
|
|
85
|
+
action: { type: "watch", warnEarly },
|
|
86
|
+
nextState: { failCount, restartAttempted: state.restartAttempted },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!state.restartAttempted) {
|
|
91
|
+
return { action: { type: "restart-once" }, nextState: { failCount: 0, restartAttempted: true } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { action: { type: "warn-and-stop" }, nextState: { failCount: 0, restartAttempted: true } };
|
|
95
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offset-anchored readiness reader for cloudflared's stderr log file.
|
|
3
|
+
*
|
|
4
|
+
* A quick or named tunnel is only "ready" once cloudflared writes a specific
|
|
5
|
+
* line to its log. The caller truncates that log before every spawn, but the
|
|
6
|
+
* truncation is best-effort and silently fails on Windows while a previous
|
|
7
|
+
* detached cloudflared process still holds the file open — so a scan of the
|
|
8
|
+
* WHOLE file can match a line left over from a dead connector and declare it
|
|
9
|
+
* "ready" in well under 200ms. Anchoring every read to a byte offset captured
|
|
10
|
+
* right after the truncate attempt makes that impossible: only bytes appended
|
|
11
|
+
* by THIS process generation are ever considered.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
export interface WaitForLogLineOpts {
|
|
16
|
+
/** Only bytes at or after this offset are scanned — excludes any content
|
|
17
|
+
* left behind by a previous process generation. */
|
|
18
|
+
fromByteOffset: number;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
/** Poll-time check for whether the watched process has already exited. */
|
|
21
|
+
getExitCode: () => number | null;
|
|
22
|
+
pollIntervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Poll `path` until `regex` matches within the bytes appended after
|
|
27
|
+
* `fromByteOffset`, the watched process exits, or `timeoutMs` elapses.
|
|
28
|
+
*
|
|
29
|
+
* Reads the file as a Buffer and slices by byte offset (not string index) so
|
|
30
|
+
* a multi-byte UTF-8 character straddling the offset is never corrupted —
|
|
31
|
+
* `toString("utf8")` re-decodes the whole remaining tail on every poll, so a
|
|
32
|
+
* boundary can only delay a match by one poll interval, never corrupt it.
|
|
33
|
+
*/
|
|
34
|
+
export async function waitForLogLine(
|
|
35
|
+
path: string,
|
|
36
|
+
regex: RegExp,
|
|
37
|
+
opts: WaitForLogLineOpts,
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
40
|
+
const pollMs = opts.pollIntervalMs ?? 200;
|
|
41
|
+
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
if (existsSync(path)) {
|
|
44
|
+
try {
|
|
45
|
+
const buf = readFileSync(path);
|
|
46
|
+
const tail = buf.subarray(opts.fromByteOffset).toString("utf8");
|
|
47
|
+
const match = tail.match(regex);
|
|
48
|
+
if (match) return match[0];
|
|
49
|
+
} catch {
|
|
50
|
+
// Transient read race (file replaced mid-poll) — retry next tick.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (opts.getExitCode() !== null) {
|
|
54
|
+
throw new Error(`cloudflared exited without matching ${regex}`);
|
|
55
|
+
}
|
|
56
|
+
await Bun.sleep(pollMs);
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`readiness timeout (${opts.timeoutMs}ms)`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime seam between "what's persisted" (named-tunnel-config.ts) and "what
|
|
3
|
+
* to actually spawn" (cloudflared.service.ts / named-tunnel-args.ts). Kept out
|
|
4
|
+
* of supervisor.ts (already large) and out of phase 1b's config/argv modules
|
|
5
|
+
* so neither grows to own the other's concern.
|
|
6
|
+
*/
|
|
7
|
+
import { getConfigValue } from "../db.service.ts";
|
|
8
|
+
import { resolveTunnelConfig, type ResolvedTunnelConfig, type TunnelMode } from "./named-tunnel-config.ts";
|
|
9
|
+
import { getQuickTunnelArgs } from "../cloudflared.service.ts";
|
|
10
|
+
import { namedRunArgs } from "./named-tunnel-args.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fresh (non-cached) read of the persisted tunnel config.
|
|
14
|
+
*
|
|
15
|
+
* The supervisor runs in its own process with its own `configService`
|
|
16
|
+
* singleton, which never observes writes the server process makes to
|
|
17
|
+
* SQLite — so every caller here must go through `getConfigValue` (a raw DB
|
|
18
|
+
* read), never `configService.get`, or the supervisor would spawn/adopt
|
|
19
|
+
* against a stale mode forever.
|
|
20
|
+
*/
|
|
21
|
+
export async function readTunnelConfigFresh(): Promise<ResolvedTunnelConfig> {
|
|
22
|
+
return resolveTunnelConfig(getConfigValue("tunnel"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TunnelSpawnPlan {
|
|
26
|
+
mode: TunnelMode;
|
|
27
|
+
args: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pick the argv for the next tunnel spawn attempt from a resolved config.
|
|
32
|
+
* When `config.mode` is not `"named"` (or the token is somehow absent —
|
|
33
|
+
* `resolveTunnelConfig` already guarantees this can't happen for a config row
|
|
34
|
+
* with `mode: "named"`, this is belt-and-suspenders), the output is exactly
|
|
35
|
+
* `getQuickTunnelArgs(port)` — byte-identical to today's quick-only argv.
|
|
36
|
+
*/
|
|
37
|
+
export function chooseTunnelSpawn(config: ResolvedTunnelConfig, port: number): TunnelSpawnPlan {
|
|
38
|
+
if (config.mode === "named" && config.token) {
|
|
39
|
+
return { mode: "named", args: namedRunArgs(config.token, port) };
|
|
40
|
+
}
|
|
41
|
+
return { mode: "quick", args: getQuickTunnelArgs(port) };
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached confirmation for a named-tunnel setup.
|
|
3
|
+
*
|
|
4
|
+
* Once the supervisor reload has been sent, `runSetup` returns to the caller
|
|
5
|
+
* immediately (a 45s status.json poll must never hold the HTTP response —
|
|
6
|
+
* see the setup service's own comment) and this module polls status.json in
|
|
7
|
+
* the background, broadcasting the final outcome over `/ws/global`.
|
|
8
|
+
*
|
|
9
|
+
* WHY a generation stamp: `setupInFlight`'s synchronous 409 lock is already
|
|
10
|
+
* released by the time this runs, and `isConfirmationRunning` only extends
|
|
11
|
+
* the 409 to a *same-hostname* retry (see the setup service) — a request for
|
|
12
|
+
* a *different* hostname is allowed straight through and immediately
|
|
13
|
+
* supersedes whatever confirmation was still polling. Without a generation
|
|
14
|
+
* check, that superseded confirmer would still broadcast its own result
|
|
15
|
+
* (`setup_done` or `setup_pending`) 1s-to-45s later, carrying the *old*
|
|
16
|
+
* hostname and overwriting the correct, newer result in every connected
|
|
17
|
+
* client. Each confirmation run is stamped with a monotonic generation, and
|
|
18
|
+
* a confirmer checks it is still the current one immediately before
|
|
19
|
+
* broadcasting — a superseded confirmer exits silently instead.
|
|
20
|
+
*/
|
|
21
|
+
import { readStatus } from "../supervisor-state.ts";
|
|
22
|
+
import { broadcastGlobalEvent } from "../../server/ws/global.ts";
|
|
23
|
+
|
|
24
|
+
const CONFIRM_POLL_BUDGET_MS = 45_000;
|
|
25
|
+
const CONFIRM_POLL_INTERVAL_MS = 1_000;
|
|
26
|
+
|
|
27
|
+
let generation = 0;
|
|
28
|
+
/** The hostname + deadline of the confirmation currently allowed to broadcast. */
|
|
29
|
+
let current: { hostname: string; deadline: number } | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True while a background confirmation is still running for `hostname`.
|
|
33
|
+
* Bounded by the same poll budget the confirmer itself uses, so a wedged
|
|
34
|
+
* confirmation can never lock a hostname out of retry forever. A different
|
|
35
|
+
* hostname's confirmation immediately closes this window (superseding is
|
|
36
|
+
* unconditional, not per-hostname), matching "a different hostname proceeds
|
|
37
|
+
* and takes over" below.
|
|
38
|
+
*/
|
|
39
|
+
export function isConfirmationRunning(hostname: string): boolean {
|
|
40
|
+
return current !== null && current.hostname === hostname && Date.now() < current.deadline;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function pollForConfirmation(hostname: string, budgetMs: number, intervalMs: number): Promise<boolean> {
|
|
44
|
+
const deadline = Date.now() + budgetMs;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
const status = readStatus();
|
|
47
|
+
if (status.tunnelMode === "named" && status.shareUrl === `https://${hostname}`) return true;
|
|
48
|
+
await Bun.sleep(intervalMs);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fire-and-forget: polls status.json and broadcasts the final outcome.
|
|
55
|
+
* Never throws into the caller. Silently drops its own broadcast if a newer
|
|
56
|
+
* setup (any hostname) has started since — see the module docstring.
|
|
57
|
+
* `opts` exists only so tests can shrink the poll budget/interval; production
|
|
58
|
+
* always uses the real 45s/1s constants.
|
|
59
|
+
*/
|
|
60
|
+
export function confirmReloadInBackground(
|
|
61
|
+
hostname: string,
|
|
62
|
+
opts: { pollBudgetMs?: number; pollIntervalMs?: number } = {},
|
|
63
|
+
): void {
|
|
64
|
+
const budgetMs = opts.pollBudgetMs ?? CONFIRM_POLL_BUDGET_MS;
|
|
65
|
+
const intervalMs = opts.pollIntervalMs ?? CONFIRM_POLL_INTERVAL_MS;
|
|
66
|
+
const myGeneration = ++generation;
|
|
67
|
+
current = { hostname, deadline: Date.now() + budgetMs };
|
|
68
|
+
const isSuperseded = () => generation !== myGeneration;
|
|
69
|
+
|
|
70
|
+
pollForConfirmation(hostname, budgetMs, intervalMs)
|
|
71
|
+
.then((confirmed) => {
|
|
72
|
+
if (isSuperseded()) return;
|
|
73
|
+
if (confirmed) {
|
|
74
|
+
broadcastGlobalEvent({ type: "tunnel:setup_done", hostname });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const latest = readStatus();
|
|
78
|
+
const warning = typeof latest.tunnelWarning === "string" ? latest.tunnelWarning : null;
|
|
79
|
+
broadcastGlobalEvent({
|
|
80
|
+
type: "tunnel:setup_pending",
|
|
81
|
+
hostname,
|
|
82
|
+
message: warning ?? "setup saved but the supervisor has not confirmed it yet — check again shortly",
|
|
83
|
+
});
|
|
84
|
+
})
|
|
85
|
+
.catch(() => {
|
|
86
|
+
if (isSuperseded()) return;
|
|
87
|
+
broadcastGlobalEvent({
|
|
88
|
+
type: "tunnel:setup_pending",
|
|
89
|
+
hostname,
|
|
90
|
+
message: "confirmation check failed — check status again shortly",
|
|
91
|
+
});
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
// Release the 409 window once this generation settles — but only if
|
|
95
|
+
// nothing newer has already taken `current` over.
|
|
96
|
+
if (!isSuperseded() && current) current.deadline = 0;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zone → precheck → create → route → token → persist → confirm.
|
|
3
|
+
*
|
|
4
|
+
* The DNS collision precheck runs *before* `tunnel create`, so "is this
|
|
5
|
+
* record already ours" is answered via the Cloudflare API's own tunnel
|
|
6
|
+
* lookup-by-name (an idempotent retry has an existing tunnel; a first-time
|
|
7
|
+
* setup does not) rather than by depending on `create` having already run.
|
|
8
|
+
*/
|
|
9
|
+
import { readOriginCertState } from "./cloudflared-cert.ts";
|
|
10
|
+
import { fetchZoneName } from "./cloudflare-zone-api.ts";
|
|
11
|
+
import { fetchDnsRecords, fetchTunnelIdByName } from "./cloudflare-dns-api.ts";
|
|
12
|
+
import { proposeHostname, validateHostname } from "./hostname-rules.ts";
|
|
13
|
+
import { createTunnelArgs, routeDnsArgs, tunnelTokenArgs, tunnelNameForHost } from "./named-tunnel-args.ts";
|
|
14
|
+
import { runCloudflared } from "./cloudflared-exec.ts";
|
|
15
|
+
import { configService } from "../config.service.ts";
|
|
16
|
+
import { requestTunnelReload, readStatus } from "../supervisor-state.ts";
|
|
17
|
+
import { broadcastGlobalEvent } from "../../server/ws/global.ts";
|
|
18
|
+
import { pinsMatch } from "./cloudflared-login-helpers.ts";
|
|
19
|
+
import { confirmReloadInBackground, isConfirmationRunning } from "./named-tunnel-setup-confirm.ts";
|
|
20
|
+
|
|
21
|
+
export type CertState = "none" | "invalid" | "ok" | "mismatch";
|
|
22
|
+
|
|
23
|
+
/** Thrown by the flow below; routes map `.status` straight to the HTTP response. */
|
|
24
|
+
export class SetupError extends Error {
|
|
25
|
+
status: number;
|
|
26
|
+
constructor(status: number, message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// `cloudflared tunnel token` prints one line of standard base64 (with `+`, `/`
|
|
33
|
+
// and `=` padding — a real token is ~180 chars ending in `=`), not URL-safe
|
|
34
|
+
// base64, so the alphabet must include those three or every real token is
|
|
35
|
+
// rejected as "unexpected output".
|
|
36
|
+
const TOKEN_SHAPE = /^[A-Za-z0-9+/=._-]{100,}$/;
|
|
37
|
+
const RELOAD_RETRY_DELAY_MS = 2_000;
|
|
38
|
+
|
|
39
|
+
/** Module-level in-flight guard — two concurrent setups would race `route dns`. */
|
|
40
|
+
let setupInFlight = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read-mostly cert classification for `/status` — never leaks the token.
|
|
44
|
+
* `"mismatch"` is a pure pin comparison (no network call): the parsed cert's
|
|
45
|
+
* zoneID/accountID differ from whatever `tunnel` config already pinned, i.e.
|
|
46
|
+
* cloudflared logged into a *different* Cloudflare account than the one this
|
|
47
|
+
* machine's named tunnel was set up under.
|
|
48
|
+
*/
|
|
49
|
+
export function currentCertState(): CertState {
|
|
50
|
+
const state = readOriginCertState();
|
|
51
|
+
if (state.kind === "absent") return "none";
|
|
52
|
+
if (state.kind === "unparseable") return "invalid";
|
|
53
|
+
return pinsMatch(state.cert) ? "ok" : "mismatch";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readZoneInfo(): Promise<{ zone: string; zoneID: string; accountID: string; proposedHostname: string }> {
|
|
57
|
+
const certState = readOriginCertState();
|
|
58
|
+
if (certState.kind !== "parsed") throw new SetupError(400, "Not logged in to Cloudflare");
|
|
59
|
+
if (!pinsMatch(certState.cert)) {
|
|
60
|
+
throw new SetupError(400, "cert belongs to a different Cloudflare account — log in again");
|
|
61
|
+
}
|
|
62
|
+
const zone = await fetchZoneName(certState.cert.zoneID, certState.cert.apiToken);
|
|
63
|
+
return { zone, zoneID: certState.cert.zoneID, accountID: certState.cert.accountID, proposedHostname: proposeHostname(zone) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function disableNamedTunnel(): Promise<void> {
|
|
67
|
+
const current = configService.get("tunnel");
|
|
68
|
+
configService.set("tunnel", { ...current, mode: "quick" });
|
|
69
|
+
requestTunnelReload();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type SetupOutcome =
|
|
73
|
+
| { ok: true; hostname: string; tunnelName: string }
|
|
74
|
+
| { ok: "pending"; hostname: string; tunnelName: string; message: string };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The synchronous `setupInFlight` lock only covers the create/route/token
|
|
78
|
+
* cycle — once the supervisor reload is sent, confirmation continues in the
|
|
79
|
+
* background (`named-tunnel-setup-confirm.ts`) and this function has already
|
|
80
|
+
* returned. `isConfirmationRunning` extends the 409 specifically to a retry
|
|
81
|
+
* for the *same* hostname during that bounded (<=45s) window, so mashing
|
|
82
|
+
* "Set up" doesn't kick off a second, redundant create/route/token cycle for
|
|
83
|
+
* a request that is already in flight. A *different* hostname is still let
|
|
84
|
+
* through immediately and supersedes the earlier confirmer — see the confirm
|
|
85
|
+
* module for how the stale one is silenced instead of broadcasting late.
|
|
86
|
+
*/
|
|
87
|
+
export async function runSetup(hostname: string): Promise<SetupOutcome> {
|
|
88
|
+
if (setupInFlight) throw new SetupError(409, "a setup is already running");
|
|
89
|
+
if (isConfirmationRunning(hostname)) {
|
|
90
|
+
throw new SetupError(409, "a setup for this hostname is already confirming — check again shortly");
|
|
91
|
+
}
|
|
92
|
+
setupInFlight = true;
|
|
93
|
+
try {
|
|
94
|
+
return await runSetupInner(hostname);
|
|
95
|
+
} finally {
|
|
96
|
+
setupInFlight = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runSetupInner(hostname: string): Promise<SetupOutcome> {
|
|
101
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "zone", message: "reading Cloudflare zone" });
|
|
102
|
+
const certState = readOriginCertState();
|
|
103
|
+
if (certState.kind !== "parsed") throw new SetupError(400, "Not logged in to Cloudflare");
|
|
104
|
+
const { zoneID, accountID, apiToken } = certState.cert;
|
|
105
|
+
|
|
106
|
+
const zone = await fetchZoneName(zoneID, apiToken);
|
|
107
|
+
const check = validateHostname(hostname, zone);
|
|
108
|
+
if (!check.ok) throw new SetupError(400, check.reason);
|
|
109
|
+
|
|
110
|
+
const tunnelName = tunnelNameForHost();
|
|
111
|
+
|
|
112
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "precheck", message: "checking for a DNS collision" });
|
|
113
|
+
const existingId = await fetchTunnelIdByName(accountID, apiToken, tunnelName);
|
|
114
|
+
const records = await fetchDnsRecords(zoneID, apiToken, hostname);
|
|
115
|
+
let overwrite = false;
|
|
116
|
+
if (records.length > 0) {
|
|
117
|
+
const ownTarget = existingId ? `${existingId}.cfargotunnel.com` : null;
|
|
118
|
+
const isOwn = ownTarget != null && records.some((r) => r.content === ownTarget);
|
|
119
|
+
if (!isOwn) throw new SetupError(400, "that name already points somewhere else — pick another prefix");
|
|
120
|
+
overwrite = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "create", message: "creating tunnel" });
|
|
124
|
+
const create = await runCloudflared(createTunnelArgs(tunnelName));
|
|
125
|
+
if (create.code !== 0 && !/already exists/i.test(create.stderr)) {
|
|
126
|
+
throw new SetupError(500, `cloudflared tunnel create failed: ${(create.stderr || create.stdout).trim()}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "route", message: "routing DNS" });
|
|
130
|
+
const route = await runCloudflared(routeDnsArgs(tunnelName, hostname, overwrite));
|
|
131
|
+
if (route.code !== 0) {
|
|
132
|
+
throw new SetupError(500, `cloudflared tunnel route dns failed: ${(route.stderr || route.stdout).trim()}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "token", message: "fetching run token" });
|
|
136
|
+
const tokenResult = await runCloudflared(tunnelTokenArgs(tunnelName));
|
|
137
|
+
const token = tokenResult.stdout.trim();
|
|
138
|
+
if (tokenResult.code !== 0 || !TOKEN_SHAPE.test(token)) {
|
|
139
|
+
throw new SetupError(500, "unexpected cloudflared output while fetching the run token");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
broadcastGlobalEvent({ type: "tunnel:setup_step", step: "apply", message: "applying configuration" });
|
|
143
|
+
configService.set("tunnel", {
|
|
144
|
+
mode: "named",
|
|
145
|
+
namedTunnelName: tunnelName,
|
|
146
|
+
namedTunnelHostname: hostname,
|
|
147
|
+
namedTunnelToken: token,
|
|
148
|
+
zoneID,
|
|
149
|
+
accountID,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const pending = (message: string): SetupOutcome => {
|
|
153
|
+
broadcastGlobalEvent({ type: "tunnel:setup_pending", hostname, message });
|
|
154
|
+
return { ok: "pending", hostname, tunnelName, message };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const status = readStatus();
|
|
158
|
+
const capabilities = Array.isArray(status.capabilities) ? (status.capabilities as unknown[]) : [];
|
|
159
|
+
if (!capabilities.includes("retunnel")) {
|
|
160
|
+
return pending("run `ppm restart` to apply — this PPM version needs a restart to pick up named tunnels");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let reload = requestTunnelReload();
|
|
164
|
+
if (reload === "busy") {
|
|
165
|
+
await Bun.sleep(RELOAD_RETRY_DELAY_MS);
|
|
166
|
+
reload = requestTunnelReload();
|
|
167
|
+
}
|
|
168
|
+
if (reload === "busy") {
|
|
169
|
+
return pending("supervisor busy — it will pick up the new setting shortly, or run `ppm restart`");
|
|
170
|
+
}
|
|
171
|
+
if (reload === "no-supervisor") {
|
|
172
|
+
return pending("no supervisor detected — run `ppm restart`");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// reload === "sent" — don't block the HTTP response on up to 45s of
|
|
176
|
+
// polling: a proxy/tunnel in front of PPM with a shorter idle timeout would
|
|
177
|
+
// time the browser out while setup was actually succeeding. Confirm
|
|
178
|
+
// detached and let /ws/global carry the final result (setup_done, or a
|
|
179
|
+
// follow-up setup_pending if confirmation itself times out) — the UI
|
|
180
|
+
// already listens for both events.
|
|
181
|
+
confirmReloadInBackground(hostname);
|
|
182
|
+
return pending("reload sent — waiting for the supervisor to confirm");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Aggregate for callers that need a single patchable seam (the HTTP routes) —
|
|
187
|
+
* route tests monkey-patch these properties directly rather than reaching for
|
|
188
|
+
* `mock.module`, which replaces the module for every importer process-wide and
|
|
189
|
+
* would poison this file's own direct-function unit tests.
|
|
190
|
+
*/
|
|
191
|
+
export const namedTunnelSetupService = { readZoneInfo, runSetup, disableNamedTunnel, currentCertState };
|
|
@@ -71,7 +71,10 @@ export function writeStatus(data: Record<string, unknown>) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// ─── Command file protocol ─────────────────────────────────────────────
|
|
74
|
-
|
|
74
|
+
// "upgrade" is written directly by upgrade.service.ts (bypasses writeCmd, same
|
|
75
|
+
// as restart.ts/stop.ts) and read by supervisor.ts's Windows poll loop — listed
|
|
76
|
+
// here so the type matches what's actually read/written on disk.
|
|
77
|
+
export type CmdAction = "soft_stop" | "resume" | "restart" | "retunnel" | "upgrade";
|
|
75
78
|
|
|
76
79
|
/** Atomically claim + read command file (rename to .claimed, read, delete) */
|
|
77
80
|
export function readAndDeleteCmd(): { action: CmdAction } | null {
|
|
@@ -88,8 +91,70 @@ export function readAndDeleteCmd(): { action: CmdAction } | null {
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
/**
|
|
95
|
+
* `retunnel` is the only low-priority action — it always yields to whatever
|
|
96
|
+
* is already pending, since a named-tunnel reload can wait behind a genuine
|
|
97
|
+
* lifecycle transition. The other three outrank it: a `resume` stuck behind
|
|
98
|
+
* an unclaimed `retunnel` would otherwise leave the server looking stopped
|
|
99
|
+
* (win32's poll loop is 1s; POSIX has a write/SIGUSR2 gap) for no reason.
|
|
100
|
+
*/
|
|
101
|
+
const LIFECYCLE_ACTIONS: readonly CmdAction[] = ["resume", "soft_stop", "restart", "upgrade"];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write `.supervisor-cmd`, but never clobber a *different* action that's still
|
|
105
|
+
* unclaimed — with one exception: a lifecycle action (`resume`/`soft_stop`/
|
|
106
|
+
* `restart`/`upgrade`) always overwrites a pending `retunnel`. Any other
|
|
107
|
+
* collision (two different lifecycle actions, or a `retunnel` behind a
|
|
108
|
+
* pending lifecycle action) returns `false` without writing so the caller can
|
|
109
|
+
* report "busy" instead of silently discarding someone else's request; a
|
|
110
|
+
* warning is logged either way so a dropped command is never silent. Note:
|
|
111
|
+
* `restart.ts`/`stop.ts` write this file directly and bypass this peek
|
|
112
|
+
* entirely, so the supervisor's dispatcher must still treat a stale/racing
|
|
113
|
+
* action defensively regardless of who wrote it — this is a best-effort
|
|
114
|
+
* single-slot guard, not a full queue.
|
|
115
|
+
*/
|
|
116
|
+
export function writeCmd(action: CmdAction): boolean {
|
|
117
|
+
try {
|
|
118
|
+
const existing = JSON.parse(readFileSync(CMD_FILE(), "utf-8")) as { action?: string };
|
|
119
|
+
if (existing?.action && existing.action !== action) {
|
|
120
|
+
const lifecycleOverridesRetunnel = LIFECYCLE_ACTIONS.includes(action) && existing.action === "retunnel";
|
|
121
|
+
if (!lifecycleOverridesRetunnel) {
|
|
122
|
+
try { process.stderr.write(`[writeCmd] dropped "${action}" — "${existing.action}" is already pending\n`); } catch {}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Missing file, unreadable, or invalid JSON — nothing unclaimed to protect.
|
|
128
|
+
}
|
|
92
129
|
writeFileSync(CMD_FILE(), JSON.stringify({ action }));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ask the running supervisor to reload the tunnel (e.g. after a named-tunnel
|
|
135
|
+
* setup/disable). POSIX confirms the supervisor is alive via a zero-signal
|
|
136
|
+
* probe before writing; win32 has no equivalent probe here, so it relies on
|
|
137
|
+
* the supervisor's existing 1s command-file poll loop to pick up the request.
|
|
138
|
+
*/
|
|
139
|
+
export function requestTunnelReload(): "sent" | "busy" | "no-supervisor" {
|
|
140
|
+
const status = readStatus();
|
|
141
|
+
const pid = status.supervisorPid as number | undefined;
|
|
142
|
+
if (!pid) return "no-supervisor";
|
|
143
|
+
|
|
144
|
+
if (process.platform !== "win32") {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0);
|
|
147
|
+
} catch {
|
|
148
|
+
return "no-supervisor";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!writeCmd("retunnel")) return "busy";
|
|
153
|
+
|
|
154
|
+
if (process.platform !== "win32") {
|
|
155
|
+
try { process.kill(pid, "SIGUSR2"); } catch { /* best-effort; poll loop is the fallback path */ }
|
|
156
|
+
}
|
|
157
|
+
return "sent";
|
|
93
158
|
}
|
|
94
159
|
|
|
95
160
|
// ─── Lockfile ──────────────────────────────────────────────────────────
|