@hienlh/ppm 0.18.13 → 0.18.15
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/.rdtest2-cf.out +0 -0
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +3 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +18 -0
- package/assets/skills/ppm/references/http-api.md +8 -1
- package/dist/web/assets/{adaptive-context-menu-CbgPYQmj.js → adaptive-context-menu-ChBZWLLq.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-unM2HNmg.js → ai-resource-editor-C8aAY8Po.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CdktdrwF.js → ai-settings-section-ERu1WiZc.js} +1 -1
- package/dist/web/assets/{angular-html-DcpbdcNG.js → angular-html-CXgHIQ2U.js} +1 -1
- package/dist/web/assets/{angular-ts-Bv_WIk34.js → angular-ts-C8renMV9.js} +1 -1
- package/dist/web/assets/api-client-WpZ2FDHg.js +1 -0
- package/dist/web/assets/api-settings-BgSEE6et.js +1 -0
- package/dist/web/assets/{api-settings-BdCtex9i.js → api-settings-R59c66DE.js} +1 -1
- package/dist/web/assets/{api-themes-pRRoLvgD.js → api-themes-DKxrwJda.js} +1 -1
- package/dist/web/assets/{apl-D5_lLszu.js → apl-DkGH8laI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CCalSKAt.js +1 -0
- package/dist/web/assets/{astro-CBHwddYL.js → astro-tHUd8Zlg.js} +1 -1
- package/dist/web/assets/{audio-preview-BD1iDovf.js → audio-preview-B_C6Dpvz.js} +1 -1
- package/dist/web/assets/{blade-DGe1d2Pq.js → blade-BbKpjIYK.js} +1 -1
- package/dist/web/assets/{bsl-2lyAzRJA.js → bsl-DA0UYto0.js} +1 -1
- package/dist/web/assets/{button-2WCAS8dI.js → button-DBSpDPS0.js} +1 -1
- package/dist/web/assets/{cairo-DjzZeJrq.js → cairo-79ayucbB.js} +1 -1
- package/dist/web/assets/chat-tab-BraiJsYG.js +15 -0
- package/dist/web/assets/{cobol-cC3XIuAJ.js → cobol-DSS2EOYo.js} +1 -1
- package/dist/web/assets/code-editor-lhmLNhXu.js +10 -0
- package/dist/web/assets/{coffee-4eGboVoU.js → coffee-DvSxq2iD.js} +1 -1
- package/dist/web/assets/{conflict-editor-DdJag4QK.js → conflict-editor-C4VCv51R.js} +1 -1
- package/dist/web/assets/{context-menu-BO1c9jKy.js → context-menu-BpwUny-o.js} +1 -1
- package/dist/web/assets/{cpp-DF9_G-OU.js → cpp-BhWZ-FSn.js} +1 -1
- package/dist/web/assets/{crystal-OavZlzcm.js → crystal-yP2OeF-f.js} +1 -1
- package/dist/web/assets/{csv-preview-DnXwouF3.js → csv-preview-D46W_icb.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-B9Gpjm3l.js → data-grid-overlay-editor-DeGkPYtA.js} +1 -1
- package/dist/web/assets/{database-viewer-Dk8oGyZx.js → database-viewer-DFI4YtrH.js} +1 -1
- package/dist/web/assets/{dialog-B-l2ZxVm.js → dialog-RIpeb87v.js} +1 -1
- package/dist/web/assets/diff-viewer-eaNOLcBL.js +4 -0
- package/dist/web/assets/{docx-preview-DAdHPlhZ.js → docx-preview-Cbj-pOK3.js} +1 -1
- package/dist/web/assets/{edge-TzYh8OlN.js → edge-D3K9cd-p.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-DlwP3UIU.js → edit-diff-preview-DDqy3KLA.js} +1 -1
- package/dist/web/assets/{elixir-Cy3RhKKH.js → elixir-D4PBmsLp.js} +1 -1
- package/dist/web/assets/{elm-BTBZdweK.js → elm-DSpXjuaH.js} +1 -1
- package/dist/web/assets/{erb-dv1mI0y4.js → erb-CaYZMTJW.js} +1 -1
- package/dist/web/assets/{erlang-DjeiDrkl.js → erlang-DX5BFqJj.js} +1 -1
- package/dist/web/assets/{esm-FRwvcesu.js → esm-Cv7HsFnk.js} +1 -1
- package/dist/web/assets/explorer-body-BxbSttfl.js +2 -0
- package/dist/web/assets/explorer-window-content-DUqQCC7A.js +1 -0
- package/dist/web/assets/{extension-store-DUaZINyC.js → extension-store-B1EClUAr.js} +1 -1
- package/dist/web/assets/{extension-webview-W0qRfaAU.js → extension-webview-BaGvDk8v.js} +1 -1
- package/dist/web/assets/{file-store-yReVbR8q.js → file-store-m9crh6uq.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-BCH9DAIJ.js → fortran-fixed-form-B0bAufUe.js} +1 -1
- package/dist/web/assets/{fsharp-C93NYKYD.js → fsharp-C2AL2b1M.js} +1 -1
- package/dist/web/assets/{gdresource-BcIEE2eX.js → gdresource-DRQsDMNI.js} +1 -1
- package/dist/web/assets/{git-commit-BVjlXEy8.js → git-commit-GtBdnrzk.js} +1 -1
- package/dist/web/assets/{git-log-panel-2_4HuP47.js → git-log-panel-hwV1flSO.js} +1 -1
- package/dist/web/assets/{git-rebase-Bj_21B1V.js → git-rebase-h_0CGA6F.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DAUrJCqi.js +1 -0
- package/dist/web/assets/glide-data-grid-YsN-xm6x.js +138 -0
- package/dist/web/assets/{glimmer-js-C20yj4Zv.js → glimmer-js-Bxwu93bS.js} +1 -1
- package/dist/web/assets/{glimmer-ts-ipbYC7kh.js → glimmer-ts-CIUYdR4E.js} +1 -1
- package/dist/web/assets/{glsl-Dovazu3t.js → glsl-CfxLI5jn.js} +1 -1
- package/dist/web/assets/{graphql-DpMlM7hC.js → graphql-CstqDPde.js} +1 -1
- package/dist/web/assets/group-chat-tab-B7mJO1gG.js +1 -0
- package/dist/web/assets/{hack-gc7LCBuV.js → hack-Bjqnssb5.js} +1 -1
- package/dist/web/assets/{haml-vtlFm9gm.js → haml-lVc-DfbM.js} +1 -1
- package/dist/web/assets/hand-x0ejq3fS.js +1 -0
- package/dist/web/assets/{handlebars-DGKhpCji.js → handlebars-Yy6fPQI_.js} +1 -1
- package/dist/web/assets/{html-derivative-C3oTSSCN.js → html-derivative-CchzCY1z.js} +1 -1
- package/dist/web/assets/{html-DvfhQbXb.js → html-lqtcy6-2.js} +1 -1
- package/dist/web/assets/{http-SG6zLTo7.js → http-ChclRrko.js} +1 -1
- package/dist/web/assets/{hurl-BWXsYlqW.js → hurl-CoOK7sXa.js} +1 -1
- package/dist/web/assets/{hxml-DUdMzaoh.js → hxml-fZSZ7Iyt.js} +1 -1
- package/dist/web/assets/{image-preview-DxgFOV1T.js → image-preview-ClAcqDlL.js} +1 -1
- package/dist/web/assets/index-CtJbMNNI.css +2 -0
- package/dist/web/assets/index-H_gScEOk.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-Cx1ZfJbp.js +1 -0
- package/dist/web/assets/{input-DuAF-Stj.js → input-DXPgUtxi.js} +1 -1
- package/dist/web/assets/{jinja-955w9RKM.js → jinja-Y0w_KBdE.js} +1 -1
- package/dist/web/assets/{jison-WWfqPo50.js → jison-BILj3F2e.js} +1 -1
- package/dist/web/assets/{julia-BFrfnaNl.js → julia-DRfOPDts.js} +1 -1
- package/dist/web/assets/{just-BrZdyR_I.js → just-n3U_oODO.js} +1 -1
- package/dist/web/assets/keybindings-store-BqctreaI.js +1 -0
- package/dist/web/assets/{keybindings-store-0Wqfa7Qp.js → keybindings-store-D-CHdq-G.js} +1 -1
- package/dist/web/assets/keyboard-DBBaoQSu.js +1 -0
- package/dist/web/assets/{latex-CwtPPASD.js → latex-D6gztEOL.js} +1 -1
- package/dist/web/assets/{liquid-CYZuvDDd.js → liquid-DPqCEVb9.js} +1 -1
- package/dist/web/assets/{lua-BV095ZyY.js → lua-I40KPUm-.js} +1 -1
- package/dist/web/assets/markdown-renderer-BwOkiZC1.js +1 -0
- package/dist/web/assets/{markdown-renderer-CnRcKK3d.js → markdown-renderer-oJ9jPdQ2.js} +3 -3
- package/dist/web/assets/{marko-CNeljZ1C.js → marko-DMjsThSW.js} +1 -1
- package/dist/web/assets/{mdc-JfGapShy.js → mdc-CN2alsbo.js} +1 -1
- package/dist/web/assets/{media-url-Bm4PsduE.js → media-url-COUvasjn.js} +1 -1
- package/dist/web/assets/mobile-bottom-sheet-Cj1YBq-c.js +1 -0
- package/dist/web/assets/mobile-explorer-sheet-D1nM0Ik1.js +1 -0
- package/dist/web/assets/{nextflow-DD5C8nqX.js → nextflow-CGa4JeZ6.js} +1 -1
- package/dist/web/assets/{nginx-Ca7B__x_.js → nginx-CmqrIj84.js} +1 -1
- package/dist/web/assets/{nim-B8FogsDw.js → nim-CqhmTeMa.js} +1 -1
- package/dist/web/assets/notification-store-n-XEQ5pH.js +1 -0
- package/dist/web/assets/{number-overlay-editor-DG9xJwEG.js → number-overlay-editor-BEzJ1YeD.js} +1 -1
- package/dist/web/assets/{overview-panel-s53bnl0q.js → overview-panel-Bf9Gn8qV.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Bqwiy7dz.js +1 -0
- package/dist/web/assets/{panel-store-B4rijFKN.js → panel-store-UWCH6B-_.js} +1 -1
- package/dist/web/assets/{pdf-preview-DYt3aBzM.js → pdf-preview-BMHM5cR2.js} +1 -1
- package/dist/web/assets/{perl-BR78hS8R.js → perl-BOwpb6Tb.js} +1 -1
- package/dist/web/assets/{php-DXBOMC9z.js → php-wT9fVxxu.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-BXSJ2God.js +1 -0
- package/dist/web/assets/{postgres-viewer-BvoWSuoZ.js → postgres-viewer-D8rfEY5b.js} +3 -3
- package/dist/web/assets/process-table-CsQ-hDJr.js +1 -0
- package/dist/web/assets/{project-store-BuY7rxn_.js → project-store-CKAOr23O.js} +1 -1
- package/dist/web/assets/{pug-CvTwVVKV.js → pug-DdlNnj-p.js} +1 -1
- package/dist/web/assets/{qml-CPSoZ1-B.js → qml-C5zBdydf.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-D6XrM7HH.js +1 -0
- package/dist/web/assets/{razor-BUA1YKtF.js → razor-DqcJX4iq.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-CTiipn4k.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-Cf5B0dKo.js +1 -0
- package/dist/web/assets/remote-desktop-stats-overlay-vhJBIW7s.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-wCJUfI6G.js +1 -0
- package/dist/web/assets/{rst-BJsq7ISz.js → rst-DiMiMYUO.js} +1 -1
- package/dist/web/assets/{ruby-DSDMeYf5.js → ruby-DK62iflA.js} +1 -1
- package/dist/web/assets/{run-in-terminal-C9xpskFn.js → run-in-terminal-DJSL6nwZ.js} +1 -1
- package/dist/web/assets/{sas-B1nJOjbN.js → sas-6QPGJRa5.js} +1 -1
- package/dist/web/assets/{scroll-area-DBcG1fQo.js → scroll-area-DXGXeiRz.js} +1 -1
- package/dist/web/assets/{scss-BZFyRiQn.js → scss-DmUqh48_.js} +1 -1
- package/dist/web/assets/{send-to-chat-BIg70MUZ.js → send-to-chat-BOony0sO.js} +1 -1
- package/dist/web/assets/settings-store-BpcY5aYE.js +2 -0
- package/dist/web/assets/settings-tab-BVqRO01j.js +1 -0
- package/dist/web/assets/settings-tab-FH581A_i.js +3 -0
- package/dist/web/assets/{shaderlab-laINRXBo.js → shaderlab-DopFR1mi.js} +1 -1
- package/dist/web/assets/{shellsession-BpOOvWVR.js → shellsession-Bp_TGuXb.js} +1 -1
- package/dist/web/assets/{soy-BW4d91Sa.js → soy-DEB9KEFb.js} +1 -1
- package/dist/web/assets/{sparql-BsvvyoHn.js → sparql-C8lEwO1p.js} +1 -1
- package/dist/web/assets/{sql-query-editor-qQce5QJ6.js → sql-query-editor-MMfk0Kbz.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-yhc0YNvu.js → sqlite-viewer-vglzrgnZ.js} +1 -1
- package/dist/web/assets/{stata-K-dZInUt.js → stata-DVep4apN.js} +1 -1
- package/dist/web/assets/{surrealql-CpNp2sgf.js → surrealql-fSlJApOA.js} +1 -1
- package/dist/web/assets/{svelte-BG6iBA8R.js → svelte-DezWBEDi.js} +1 -1
- package/dist/web/assets/{switch-_NcrktLe.js → switch-DqL_ZjB_.js} +1 -1
- package/dist/web/assets/system-monitor-body-BYUfyF8f.js +2 -0
- package/dist/web/assets/system-monitor-tab-CKGXB8lH.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-CaOhD0AU.js → system-monitor-window-content-CiJ2yIsr.js} +1 -1
- package/dist/web/assets/tab-host-window-content-DYlXR0cg.js +1 -0
- package/dist/web/assets/{tab-store-tFnyPmm4.js → tab-store-D2thn4Vv.js} +1 -1
- package/dist/web/assets/team-member-sheet-Bnygu7Xw.js +2 -0
- package/dist/web/assets/{team-member-window-content-B1jkKkDi.js → team-member-window-content-qrgFTC96.js} +1 -1
- package/dist/web/assets/{templ-gxNKXmt3.js → templ-D0zB42a9.js} +1 -1
- package/dist/web/assets/{terminal-tab-UvwM0sty.js → terminal-tab-D_-R5rGm.js} +3 -3
- package/dist/web/assets/{tex-C0ON27g5.js → tex-BJiPijcH.js} +1 -1
- package/dist/web/assets/{tool-cards-jB7GT-wj.js → tool-cards-BW7Xc20J.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-BobWhcHN.js +1 -0
- package/dist/web/assets/{ts-tags-BX_CTVgQ.js → ts-tags-Dcr4u6y5.js} +1 -1
- package/dist/web/assets/{twig-CwL7XnU0.js → twig-B-SQU0bg.js} +1 -1
- package/dist/web/assets/{use-blob-url-DcRcjNTE.js → use-blob-url-BtPlqwLr.js} +1 -1
- package/dist/web/assets/use-monaco-theme-DQY1ZSaf.js +1 -0
- package/dist/web/assets/{use-open-team-member-1hRsHLKg.js → use-open-team-member-C5LQ_TCM.js} +1 -1
- package/dist/web/assets/use-visual-viewport-pVxLfE5B.js +1 -0
- package/dist/web/assets/{use-websocket-rxJLbFqy.js → use-websocket-l39Odhlp.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-CHTa6D69.js → vendor-mermaid-CtDsSxbM.js} +1 -1
- package/dist/web/assets/{video-preview-DMhnf2Gi.js → video-preview-C5AhkIDl.js} +1 -1
- package/dist/web/assets/{vue-Bykm6wSS.js → vue-Cw10Sfbh.js} +1 -1
- package/dist/web/assets/{vue-html-CETdK_iV.js → vue-html-Cc5C_Z_w.js} +1 -1
- package/dist/web/assets/{vue-vine-DiJb6fO5.js → vue-vine-BuNMlnvu.js} +1 -1
- package/dist/web/assets/window-pip-registry-C7oz3Dfy.js +1 -0
- package/dist/web/assets/window-store-B5ZswdYg.js +1 -0
- package/dist/web/assets/{xml-BNw8J5YW.js → xml-DqT1wkky.js} +1 -1
- package/dist/web/assets/{xsl-CsG9L31b.js → xsl-DLkge3A2.js} +1 -1
- package/dist/web/index.html +45 -42
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +3 -0
- package/docs/architecture/workspace-and-ui.md +8 -0
- package/docs/design-guidelines.md +3 -2
- package/package.json +1 -1
- package/src/cli/commands/backup-cmd.ts +119 -0
- package/src/index.ts +3 -0
- package/src/providers/claude-agent-sdk.ts +29 -1
- package/src/server/index.ts +42 -15
- package/src/server/routes/accounts.ts +30 -2
- package/src/server/routes/chat.ts +8 -1
- package/src/server/routes/named-tunnel.ts +5 -2
- package/src/server/routes/remote-desktop.ts +64 -0
- package/src/server/routes/settings.ts +1 -0
- package/src/server/ws/chat.ts +126 -7
- package/src/server/ws/remote-desktop.ts +86 -0
- package/src/services/account.service.ts +106 -23
- package/src/services/db-backup/db-backup-paths.ts +66 -0
- package/src/services/db-backup/db-backup-restore.ts +105 -0
- package/src/services/db-backup/db-backup-retention.ts +96 -0
- package/src/services/db-backup/db-backup-sync.ts +113 -0
- package/src/services/db-backup/db-backup.service.ts +61 -0
- package/src/services/db.service.ts +29 -1
- package/src/services/fs-credential-path-guard.ts +19 -1
- package/src/services/named-tunnel/cloudflared-login.service.ts +2 -2
- package/src/services/nested-subagent-spy.ts +206 -0
- package/src/services/remote-desktop/access-unit-assembler.ts +90 -0
- package/src/services/remote-desktop/avc1-codec-string.ts +18 -0
- package/src/services/remote-desktop/nal-splitter.ts +100 -0
- package/src/services/remote-desktop/remote-desktop-capture.ts +120 -0
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +42 -0
- package/src/services/remote-desktop/remote-desktop-flag.ts +10 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +217 -0
- package/src/services/remote-desktop/remote-desktop-nonce.ts +38 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +189 -0
- package/src/services/remote-desktop/remote-desktop-vk-map.ts +41 -0
- package/src/services/subagent-transcript-merger.ts +92 -75
- package/src/services/subprocess-retention.ts +29 -6
- package/src/services/supervisor.ts +58 -0
- package/src/services/team-member-activity/subagent-transcript-index.ts +56 -0
- package/src/types/chat.ts +2 -0
- package/src/web/app.tsx +8 -0
- package/src/web/components/chat/account-dialogs.tsx +11 -4
- package/src/web/components/chat/question-card.tsx +9 -2
- package/src/web/components/chat/usage-badge.tsx +17 -3
- 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-content-registry.ts +2 -0
- package/src/web/components/floating-window/window-pip-registry.ts +46 -0
- package/src/web/components/floating-window/window-store-types.ts +1 -1
- package/src/web/components/layout/mobile-drawer.tsx +38 -27
- package/src/web/components/layout/nav-section-rail.tsx +10 -2
- package/src/web/components/layout/tab-pop-out-menu-item.tsx +25 -2
- package/src/web/components/remote-desktop/open-remote-desktop.ts +26 -0
- package/src/web/components/remote-desktop/remote-desktop-coords.ts +98 -0
- package/src/web/components/remote-desktop/remote-desktop-frame-timestamp.ts +21 -0
- package/src/web/components/remote-desktop/remote-desktop-gesture-classifiers.ts +56 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +131 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-sheet.tsx +43 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +79 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +190 -0
- package/src/web/components/remote-desktop/remote-desktop-stats-overlay.tsx +54 -0
- package/src/web/components/remote-desktop/remote-desktop-stats.ts +37 -0
- package/src/web/components/remote-desktop/remote-desktop-touch-gesture-tracker.ts +195 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +72 -0
- package/src/web/components/remote-desktop/remote-desktop-ws-url.ts +22 -0
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +175 -0
- package/src/web/components/remote-desktop/use-remote-desktop-available.ts +29 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +147 -0
- package/src/web/components/remote-desktop/use-remote-desktop-mobile-open-state.ts +18 -0
- package/src/web/components/remote-desktop/use-remote-desktop-touch.ts +224 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +50 -0
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +92 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +59 -59
- package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +2 -2
- package/src/web/hooks/use-notification-badge.ts +19 -11
- package/src/web/lib/feature-badges.ts +2 -0
- package/src/web/stores/settings-store.ts +13 -0
- package/src/web/stores/streaming-store.ts +21 -2
- package/dist/web/assets/api-client-BogqsFna.js +0 -1
- package/dist/web/assets/api-settings-DfXViPKz.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +0 -1
- package/dist/web/assets/chat-tab-DzK8LGti.js +0 -15
- package/dist/web/assets/code-editor-CuMfPN9X.js +0 -10
- package/dist/web/assets/diff-viewer-eaUBjLCH.js +0 -4
- package/dist/web/assets/explorer-body-DkCkSVrN.js +0 -2
- package/dist/web/assets/explorer-window-content-MsnTVngu.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +0 -1
- package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +0 -138
- package/dist/web/assets/group-chat-tab-DRLDHOp6.js +0 -1
- package/dist/web/assets/index-CBn4SOoT.js +0 -52
- package/dist/web/assets/index-DWpNR3nB.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +0 -1
- package/dist/web/assets/keybindings-store-d54pv2NK.js +0 -1
- package/dist/web/assets/markdown-renderer-BTS8ifdu.js +0 -1
- package/dist/web/assets/mobile-bottom-sheet-M5iZDX7y.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +0 -1
- package/dist/web/assets/notification-store-Yvy1I8gd.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +0 -1
- package/dist/web/assets/process-table-BYwKigiD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +0 -1
- package/dist/web/assets/settings-store-GV5pkkke.js +0 -2
- package/dist/web/assets/settings-tab-BePvIcjd.js +0 -3
- package/dist/web/assets/settings-tab-Br-lJSPa.js +0 -1
- package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +0 -2
- package/dist/web/assets/system-monitor-tab-BclYXFTe.js +0 -1
- package/dist/web/assets/tab-host-window-content-f_Rc5rJK.js +0 -1
- package/dist/web/assets/team-member-sheet-QDHoYCs0.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +0 -1
- package/dist/web/assets/use-monaco-theme--9tOOkDA.js +0 -1
- package/dist/web/assets/window-store-CQ1lghvv.js +0 -1
- /package/dist/web/assets/{abap-DGlrX_8G.js → abap-CKgBNk5K.js} +0 -0
- /package/dist/web/assets/{actionscript-3-CUqRtUJE.js → actionscript-3-oARhAY6c.js} +0 -0
- /package/dist/web/assets/{ada-CY3Spa33.js → ada-2vi8ASy3.js} +0 -0
- /package/dist/web/assets/{andromeeda-Lxs2TJn7.js → andromeeda-B3vMjPKd.js} +0 -0
- /package/dist/web/assets/{apache-DfxgGdgM.js → apache-DnIEr338.js} +0 -0
- /package/dist/web/assets/{apex-QVDF1dnI.js → apex-B7Qs5AY5.js} +0 -0
- /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-BkGKTjsq.js} +0 -0
- /package/dist/web/assets/{applescript-pwJ8QXh-.js → applescript-EbQNUCWb.js} +0 -0
- /package/dist/web/assets/{apply-theme-B_bMVL6a.js → apply-theme-Dd_cqGa-.js} +0 -0
- /package/dist/web/assets/{ara-mPjeX5YX.js → ara-CtHdhFRw.js} +0 -0
- /package/dist/web/assets/{asciidoc-Bf7LsDkI.js → asciidoc-BH6qin_O.js} +0 -0
- /package/dist/web/assets/{asm-rRKC1OUx.js → asm-D1XU1ACk.js} +0 -0
- /package/dist/web/assets/{aurora-x-DwQr0DkG.js → aurora-x-BkhcxkuS.js} +0 -0
- /package/dist/web/assets/{awk-Bpt768fN.js → awk-DijzB6o7.js} +0 -0
- /package/dist/web/assets/{ayu-dark-D9lwQHMs.js → ayu-dark-tysG77WC.js} +0 -0
- /package/dist/web/assets/{ayu-light-B4-ptrU5.js → ayu-light-Dg0WMETC.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-DAyRzH5Q.js → ayu-mirage--RsJCjC4.js} +0 -0
- /package/dist/web/assets/{ballerina-Cgdpj1HC.js → ballerina-DT_5-l82.js} +0 -0
- /package/dist/web/assets/{bat-4FrN36S2.js → bat-BRpDlkv0.js} +0 -0
- /package/dist/web/assets/{beancount-DkcxQER3.js → beancount--8Df-WGG.js} +0 -0
- /package/dist/web/assets/{berry-Dnuut-Ml.js → berry-Bc9fo35J.js} +0 -0
- /package/dist/web/assets/{bibtex-CCDwmbuA.js → bibtex-CwyBpp9X.js} +0 -0
- /package/dist/web/assets/{bicep-BC0ryoGM.js → bicep-DiyH2Bs7.js} +0 -0
- /package/dist/web/assets/{bird2-C50V_A-0.js → bird2-CHgK4a2A.js} +0 -0
- /package/dist/web/assets/{c-Ddorhq0U.js → c-BM1qL6-f.js} +0 -0
- /package/dist/web/assets/{c3-DdIf5r9E.js → c3-D2oOjrP7.js} +0 -0
- /package/dist/web/assets/{cadence-s9pd1Wng.js → cadence-nS45wqF6.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-C6hZV1OP.js → can-open-in-ppm-DwP9KU8o.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-CzJNTyu-.js → catppuccin-frappe-BpnJHiCj.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-BNpt-m7s.js → catppuccin-latte-Bb95Wj3h.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-FYGkfsJ5.js → catppuccin-macchiato-Btx8c17W.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-it2g5D1q.js → catppuccin-mocha-B4DZOUGI.js} +0 -0
- /package/dist/web/assets/{clarity-BVFBTjMe.js → clarity-1ps69jU6.js} +0 -0
- /package/dist/web/assets/{clipboard-BWznymz7.js → clipboard-CrHX8AYw.js} +0 -0
- /package/dist/web/assets/{clojure-BSYvXMI8.js → clojure-BklC0hKV.js} +0 -0
- /package/dist/web/assets/{cmake-DGEqH-Np.js → cmake-B573LYAQ.js} +0 -0
- /package/dist/web/assets/{codeowners-D8FCOxWg.js → codeowners-BoYeX89M.js} +0 -0
- /package/dist/web/assets/{codeql-COV0eluG.js → codeql-7Dhf5Udk.js} +0 -0
- /package/dist/web/assets/{common-lisp-xHnZUlgt.js → common-lisp-m1nxlthv.js} +0 -0
- /package/dist/web/assets/{coq-Cs_2i3Xg.js → coq-DNhGmd4a.js} +0 -0
- /package/dist/web/assets/{csharp-B1ONB4PT.js → csharp-P-nAurtS.js} +0 -0
- /package/dist/web/assets/{css-BgGQbvrZ.js → css-LYbSeh3-.js} +0 -0
- /package/dist/web/assets/{csv-CNHmQBzs.js → csv-DHZZZm7X.js} +0 -0
- /package/dist/web/assets/{csv-parser-DWR24J17.js → csv-parser-CY9Ggtg5.js} +0 -0
- /package/dist/web/assets/{cue-ZXAKzwIy.js → cue-D8yLUOle.js} +0 -0
- /package/dist/web/assets/{cypher-4PL5bmT5.js → cypher-9cbW8RYA.js} +0 -0
- /package/dist/web/assets/{d-CEsV3RT2.js → d-ZihzhxqI.js} +0 -0
- /package/dist/web/assets/{dark-plus-NtFnYSDz.js → dark-plus-C8xIA1u3.js} +0 -0
- /package/dist/web/assets/{dart-CKHn4mDf.js → dart-CBm6jKc7.js} +0 -0
- /package/dist/web/assets/{data-grid-types-CxU8oo-j.js → data-grid-types-DkEQ-Na9.js} +0 -0
- /package/dist/web/assets/{dax-Cmkj1I_M.js → dax-B3cx6YeV.js} +0 -0
- /package/dist/web/assets/{desktop-DJLalvPQ.js → desktop-C03SpXth.js} +0 -0
- /package/dist/web/assets/{diff-BnS2wpj6.js → diff-D8LnnvFY.js} +0 -0
- /package/dist/web/assets/{dist-uYjs3Wv6.js → dist-C4XoS_zo.js} +0 -0
- /package/dist/web/assets/{dist-CuIdRyUB.js → dist-Do3fljss.js} +0 -0
- /package/dist/web/assets/{docker-CovNy7ml.js → docker-DwFiOQWW.js} +0 -0
- /package/dist/web/assets/{dotenv-Gh4w3I6B.js → dotenv-CVJ_vFSQ.js} +0 -0
- /package/dist/web/assets/{dracula-NF0tAHXl.js → dracula-DMw8UACY.js} +0 -0
- /package/dist/web/assets/{dracula-soft-Cj_qBurK.js → dracula-soft-BnB3nDrA.js} +0 -0
- /package/dist/web/assets/{dream-maker-CRuZPcl7.js → dream-maker-2riXUQXo.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-BACmC9qj.js → emacs-lisp-BQdYaiB5.js} +0 -0
- /package/dist/web/assets/{everforest-dark-Dd9av4QG.js → everforest-dark-C2FPIWdj.js} +0 -0
- /package/dist/web/assets/{everforest-light-C-jsss-B.js → everforest-light-DMOWf9sD.js} +0 -0
- /package/dist/web/assets/{fennel-BJA9XnL8.js → fennel-JtSHp0vW.js} +0 -0
- /package/dist/web/assets/{fish-CKlhNSc2.js → fish-CflDcRfe.js} +0 -0
- /package/dist/web/assets/{fluent-wfglRk5D.js → fluent-D4eu6Vac.js} +0 -0
- /package/dist/web/assets/{format-bytes-utwHMTry.js → format-bytes-DR3228bu.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BlXCdid3.js → fortran-free-form-hO4RuC9Z.js} +0 -0
- /package/dist/web/assets/{gdscript-BwAVG6Et.js → gdscript-D7n3a8VM.js} +0 -0
- /package/dist/web/assets/{gdshader-DPc_T7ib.js → gdshader-BhUmrlzG.js} +0 -0
- /package/dist/web/assets/{genie-B-zZSLCX.js → genie-VbyftqBp.js} +0 -0
- /package/dist/web/assets/{gherkin-CJ0ajpGs.js → gherkin-BZzhvUUb.js} +0 -0
- /package/dist/web/assets/{github-dark-Dz8ZkzSP.js → github-dark-C4q41woW.js} +0 -0
- /package/dist/web/assets/{github-dark-default-CNUYo_kp.js → github-dark-default-CMNi7dQX.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-241z_Ycx.js → github-dark-dimmed-BRf_-LlY.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-7r6yZZYH.js → github-dark-high-contrast-xanKBsRI.js} +0 -0
- /package/dist/web/assets/{github-light-C8gmZn-D.js → github-light-BEQeNQCs.js} +0 -0
- /package/dist/web/assets/{github-light-default-GAEgq1eA.js → github-light-default-ceQrtdXg.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-3z91352E.js → github-light-high-contrast-doKbXzgW.js} +0 -0
- /package/dist/web/assets/{gleam-C7vxW5zd.js → gleam-Chzqf4fT.js} +0 -0
- /package/dist/web/assets/{gn-CyJIXwHP.js → gn-CCN_-6fp.js} +0 -0
- /package/dist/web/assets/{gnuplot-CqLbGgPd.js → gnuplot-CIplgwwP.js} +0 -0
- /package/dist/web/assets/{go-DdDcK9aX.js → go-Cjj-9dRs.js} +0 -0
- /package/dist/web/assets/{groovy-UYHggEqH.js → groovy-PqD4K7X1.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-B4ierkJn.js → gruvbox-dark-hard-BAbpUSF-.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-DYwkDSZ6.js → gruvbox-dark-medium-BWcWXDBB.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CVL4P-Lz.js → gruvbox-dark-soft-Dkx1okkk.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-BJ0LewUZ.js → gruvbox-light-hard-WVsDK0MM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-CaqqjeGJ.js → gruvbox-light-medium-tiR9t7gr.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-ByKMoVNr.js → gruvbox-light-soft-HzHohaX6.js} +0 -0
- /package/dist/web/assets/{haskell-BilYRVPp.js → haskell-B1eIZfr6.js} +0 -0
- /package/dist/web/assets/{haxe-Brs9Bj1s.js → haxe-BcDdi5xr.js} +0 -0
- /package/dist/web/assets/{hcl-ClrZpol-.js → hcl-Dq-HKVni.js} +0 -0
- /package/dist/web/assets/{hjson-9PvHnYMX.js → hjson-Dr9D9WVU.js} +0 -0
- /package/dist/web/assets/{hlsl-BbBk4vqV.js → hlsl-Bo8t2FuZ.js} +0 -0
- /package/dist/web/assets/{horizon-E5Py0cny.js → horizon-DWG5IXc-.js} +0 -0
- /package/dist/web/assets/{horizon-bright-DVCsNGEk.js → horizon-bright-DFr9wTTx.js} +0 -0
- /package/dist/web/assets/{houston-JdFEax9Y.js → houston-NAVNhkup.js} +0 -0
- /package/dist/web/assets/{hy-W_W3tLKT.js → hy-DnyvUk3B.js} +0 -0
- /package/dist/web/assets/{image-gallery-5IURd6db.js → image-gallery-DoFdNF9i.js} +0 -0
- /package/dist/web/assets/{imba-CfAqJz7T.js → imba-CPbR5W_A.js} +0 -0
- /package/dist/web/assets/{ini-C_3gvKWs.js → ini-lc8_Lt0y.js} +0 -0
- /package/dist/web/assets/{java-CKqtXZ-j.js → java-B0KCo2vK.js} +0 -0
- /package/dist/web/assets/{javascript-C7gSEfae.js → javascript-YqEbNRpd.js} +0 -0
- /package/dist/web/assets/{json-BoN6e11T.js → json-Co0WtOlh.js} +0 -0
- /package/dist/web/assets/{json5-CgE7C8HW.js → json5-2Jhb0QUN.js} +0 -0
- /package/dist/web/assets/{jsonc-DjyoattY.js → jsonc-e72F11nU.js} +0 -0
- /package/dist/web/assets/{jsonl-lI82wGSw.js → jsonl-Bff8IPye.js} +0 -0
- /package/dist/web/assets/{jsonnet-CszEI8va.js → jsonnet-DRCkb-_H.js} +0 -0
- /package/dist/web/assets/{jssm-CZXDlszb.js → jssm-C6UiwVEm.js} +0 -0
- /package/dist/web/assets/{jsx-BeRz0eEz.js → jsx-u1aZlnfA.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BOmnUiyd.js → kanagawa-dragon-De-RQFV3.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-ffU_P5w-.js → kanagawa-lotus-K8elE4QU.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-DKTJZrMu.js → kanagawa-wave-BrStl_zx.js} +0 -0
- /package/dist/web/assets/{kdl-BWECJveH.js → kdl-CLCfMeZy.js} +0 -0
- /package/dist/web/assets/{kotlin-DpVwBCSt.js → kotlin-DEEC3pNh.js} +0 -0
- /package/dist/web/assets/{kusto-BgVqjUZA.js → kusto-DTqRhCup.js} +0 -0
- /package/dist/web/assets/{laserwave-CEVCi8em.js → laserwave-DsgpUSbA.js} +0 -0
- /package/dist/web/assets/{lean-CGyfRhLJ.js → lean-Dl9MI0Y3.js} +0 -0
- /package/dist/web/assets/{less-B0_H10Ku.js → less-Bg21rao6.js} +0 -0
- /package/dist/web/assets/{lib-hrC3bruJ.js → lib-CXUwxw1M.js} +0 -0
- /package/dist/web/assets/{libesm-DVpFmwJu.js → libesm-CVkncAQz.js} +0 -0
- /package/dist/web/assets/{light-plus-DqzMqPID.js → light-plus-DGNbbK1V.js} +0 -0
- /package/dist/web/assets/{llvm-9ucnfNiF.js → llvm-91_ZX3p-.js} +0 -0
- /package/dist/web/assets/{loader-circle-Dpqcv0eS.js → loader-circle-DouKgIXy.js} +0 -0
- /package/dist/web/assets/{lock-DldbT31x.js → lock-CNxUGpTk.js} +0 -0
- /package/dist/web/assets/{log-m6A2dOSj.js → log-DSXzbq8u.js} +0 -0
- /package/dist/web/assets/{logo-ByPcsC9P.js → logo-BQRNd4tc.js} +0 -0
- /package/dist/web/assets/{luau-DFCo8nBn.js → luau-BE-TMpiZ.js} +0 -0
- /package/dist/web/assets/{make-C90w3MeA.js → make-ePYGKTpc.js} +0 -0
- /package/dist/web/assets/{markdown-DT0uywZG.js → markdown-Cst3wgMW.js} +0 -0
- /package/dist/web/assets/{material-theme-BVUO93tJ.js → material-theme-9XyvnNLb.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-xXgGDwnF.js → material-theme-darker-H1VN4MTS.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BjStPzWB.js → material-theme-lighter-DAreBbuF.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-wFmSiKjy.js → material-theme-ocean-BpTR-3II.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-e-0CtRoe.js → material-theme-palenight-DUNzX711.js} +0 -0
- /package/dist/web/assets/{matlab-BNaNQAUH.js → matlab-BpYNufBU.js} +0 -0
- /package/dist/web/assets/{mdx-DfpVWAqO.js → mdx-BYjfe1DK.js} +0 -0
- /package/dist/web/assets/{mermaid-Bpq7wFCP.js → mermaid-DPCcIW9B.js} +0 -0
- /package/dist/web/assets/{min-dark-CL2Y0WH6.js → min-dark-DnGOAGCo.js} +0 -0
- /package/dist/web/assets/{min-light-DOgr_IdP.js → min-light-CwV-o31y.js} +0 -0
- /package/dist/web/assets/{mipsasm-C9ie2zlB.js → mipsasm-DdeCPC97.js} +0 -0
- /package/dist/web/assets/{mojo-Md2A2sJy.js → mojo-BmxxZ3iM.js} +0 -0
- /package/dist/web/assets/{monitor-4vWfqzuN.js → monitor-C835ucWH.js} +0 -0
- /package/dist/web/assets/{monokai-nbXIEySB.js → monokai-Cx6cQ-MA.js} +0 -0
- /package/dist/web/assets/{moonbit-CJ-T-s4A.js → moonbit-D2-Gj5vj.js} +0 -0
- /package/dist/web/assets/{move-DwEehLIK.js → move-DSWGoQa8.js} +0 -0
- /package/dist/web/assets/{narrat-CuF4sIKr.js → narrat-D9TnZSSu.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-1dYhfPfq.js → nextflow-groovy-BbL-8PwD.js} +0 -0
- /package/dist/web/assets/{night-owl-gZS5mfzY.js → night-owl-CZitKm5d.js} +0 -0
- /package/dist/web/assets/{night-owl-light-DDAster5.js → night-owl-light-DJVX1Pno.js} +0 -0
- /package/dist/web/assets/{nix-BlbEYs2c.js → nix-R6hr3oln.js} +0 -0
- /package/dist/web/assets/{nord-BRiPb8Vy.js → nord-ChGHya3l.js} +0 -0
- /package/dist/web/assets/{nushell-DgWicB1i.js → nushell-QQWB-GWx.js} +0 -0
- /package/dist/web/assets/{objective-c-D8mPZ42Y.js → objective-c-C0pZ6y73.js} +0 -0
- /package/dist/web/assets/{objective-cpp-B0pNXzuj.js → objective-cpp-BdzVM31a.js} +0 -0
- /package/dist/web/assets/{ocaml-lQd7aK5d.js → ocaml-D4KktNlS.js} +0 -0
- /package/dist/web/assets/{odin-CxtIOBjm.js → odin-Y-fypmGT.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-T_UFnWfe.js → one-dark-pro-BwecIxQL.js} +0 -0
- /package/dist/web/assets/{one-light-LBVqwyV-.js → one-light-Cz7UVzLU.js} +0 -0
- /package/dist/web/assets/{openscad-C5dnS6J7.js → openscad-DtRe3qYD.js} +0 -0
- /package/dist/web/assets/{pascal-CmWCWQML.js → pascal-Ck6I1fGG.js} +0 -0
- /package/dist/web/assets/{pkl-BWWOtDQk.js → pkl-DdFK94VW.js} +0 -0
- /package/dist/web/assets/{plastic-ekhupckF.js → plastic-BQSAzZwR.js} +0 -0
- /package/dist/web/assets/{play-CpCAoQRZ.js → play-CJlWpVMw.js} +0 -0
- /package/dist/web/assets/{plsql-BPsDHXx4.js → plsql-DjXg_CP0.js} +0 -0
- /package/dist/web/assets/{po-BX2FiuW2.js → po-Bw2SuM4r.js} +0 -0
- /package/dist/web/assets/{poimandres-DzXOdZcN.js → poimandres-YMK6c5VN.js} +0 -0
- /package/dist/web/assets/{polar-DDRXgJzG.js → polar-BMu17-O0.js} +0 -0
- /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DSGdlXrJ.js} +0 -0
- /package/dist/web/assets/{postcss-BHW6XBhx.js → postcss-DWib10RZ.js} +0 -0
- /package/dist/web/assets/{powerquery-B89c2NBb.js → powerquery-Dht4CK0b.js} +0 -0
- /package/dist/web/assets/{powershell-Dvxrd3Tu.js → powershell-DvrCg8ji.js} +0 -0
- /package/dist/web/assets/{prisma-lMK8EFLU.js → prisma-DbodYvCM.js} +0 -0
- /package/dist/web/assets/{prolog-D0YrtMG4.js → prolog-DZ-m6ysB.js} +0 -0
- /package/dist/web/assets/{proto-J0mM25aw.js → proto-HHpG3tGO.js} +0 -0
- /package/dist/web/assets/{puppet-CUT0xkhh.js → puppet-BljHe_75.js} +0 -0
- /package/dist/web/assets/{purescript-CoJWSGRp.js → purescript-DzCiFR0B.js} +0 -0
- /package/dist/web/assets/{python-BBrmuz6P.js → python-B5BQ27cC.js} +0 -0
- /package/dist/web/assets/{qmldir-CUtzs4Q7.js → qmldir-CYlcXrnn.js} +0 -0
- /package/dist/web/assets/{qss-Dm1RZ8aL.js → qss-Ce7T_TEg.js} +0 -0
- /package/dist/web/assets/{r-BGP7qZTe.js → r-Bku-2Jp_.js} +0 -0
- /package/dist/web/assets/{racket-K_6U1lqJ.js → racket-DtfSL3WR.js} +0 -0
- /package/dist/web/assets/{raku-bNSiZeW_.js → raku-CnDLsBJr.js} +0 -0
- /package/dist/web/assets/{react-X4jUntEf.js → react-Dfio3PfK.js} +0 -0
- /package/dist/web/assets/{red-uVeIEF0f.js → red-vbqcE9b-.js} +0 -0
- /package/dist/web/assets/{refresh-cw-B9iv7csb.js → refresh-cw-84mz8aOE.js} +0 -0
- /package/dist/web/assets/{reg-DSm9GWjh.js → reg-DIDLT8-A.js} +0 -0
- /package/dist/web/assets/{regexp-CljRm_j_.js → regexp-CB4h-VPu.js} +0 -0
- /package/dist/web/assets/{rel-d3JcopZg.js → rel-CiQEnWXa.js} +0 -0
- /package/dist/web/assets/{resolve-theme-DLKmm-lE.js → resolve-theme-DPTQHAy1.js} +0 -0
- /package/dist/web/assets/{riscv-CJmaELjT.js → riscv-R78iq5Rf.js} +0 -0
- /package/dist/web/assets/{ron-D0g9B1rW.js → ron-DhYENxc4.js} +0 -0
- /package/dist/web/assets/{rose-pine-DOrD2BSH.js → rose-pine-5wBeyjfJ.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-CzZQ-LCF.js → rose-pine-dawn-FpKtPCuH.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-CdmsYp7C.js → rose-pine-moon-BwcIYc7C.js} +0 -0
- /package/dist/web/assets/{rosmsg-CmDth6pO.js → rosmsg-DWIGAYMl.js} +0 -0
- /package/dist/web/assets/{rotate-ccw-DnuPzJRE.js → rotate-ccw-CNTk2J7l.js} +0 -0
- /package/dist/web/assets/{rust-DGdP6jiR.js → rust-DU_ofKgt.js} +0 -0
- /package/dist/web/assets/{sass-Dkgt2FKZ.js → sass-U9SUuYEy.js} +0 -0
- /package/dist/web/assets/{save-DxonD4y1.js → save-CtW9ojKk.js} +0 -0
- /package/dist/web/assets/{scala-BQLTON6C.js → scala-op4gjA6j.js} +0 -0
- /package/dist/web/assets/{scheme-DKlxUGCi.js → scheme-tEjkR8rL.js} +0 -0
- /package/dist/web/assets/{sdbl-C3iZJQeZ.js → sdbl-C6SSRmZg.js} +0 -0
- /package/dist/web/assets/{send-B-t5Mz26.js → send-C_sLFt56.js} +0 -0
- /package/dist/web/assets/{shallow-jwPzEunk.js → shallow-C1YoJQA-.js} +0 -0
- /package/dist/web/assets/{shellscript-BRu-ZQAx.js → shellscript-Bf2KiwQj.js} +0 -0
- /package/dist/web/assets/{shield-check-CdTiK1j2.js → shield-check-Du6HKYn9.js} +0 -0
- /package/dist/web/assets/{shield-off-DuetkXKM.js → shield-off-CWKb1MQL.js} +0 -0
- /package/dist/web/assets/{slack-dark-qkm_u2zz.js → slack-dark-oGgCey8r.js} +0 -0
- /package/dist/web/assets/{slack-ochin-BP8_8e4m.js → slack-ochin-CemlB8cE.js} +0 -0
- /package/dist/web/assets/{smalltalk-Bnz4gt07.js → smalltalk-CTE-6jrZ.js} +0 -0
- /package/dist/web/assets/{snazzy-light-fbPZg5io.js → snazzy-light-DoEIGnmz.js} +0 -0
- /package/dist/web/assets/{solarized-dark-CM0vTRoi.js → solarized-dark-CStiJyBg.js} +0 -0
- /package/dist/web/assets/{solarized-light-C4_W9mvs.js → solarized-light-CfuNycS9.js} +0 -0
- /package/dist/web/assets/{solidity-Bz6v1PoD.js → solidity-DL6sO5rd.js} +0 -0
- /package/dist/web/assets/{splunk-DyNxu-0i.js → splunk-yK4ZUFBB.js} +0 -0
- /package/dist/web/assets/{sql-DXRL1gw1.js → sql-Lw03eoN8.js} +0 -0
- /package/dist/web/assets/{ssh-config-2z1QHuOG.js → ssh-config-DeRp25TR.js} +0 -0
- /package/dist/web/assets/{stylus-D4I-VaeY.js → stylus-D8OMsPh-.js} +0 -0
- /package/dist/web/assets/{swift-C_2G-tIc.js → swift-nsl9stPV.js} +0 -0
- /package/dist/web/assets/{synthwave-84-osBjChRP.js → synthwave-84-exAgsgpf.js} +0 -0
- /package/dist/web/assets/{system-verilog-zQP2qPNU.js → system-verilog-DBWf00T6.js} +0 -0
- /package/dist/web/assets/{systemd-UGfjRSBM.js → systemd-nhu27Dw5.js} +0 -0
- /package/dist/web/assets/{table-FLU8ei8b.js → table-GAR4upb6.js} +0 -0
- /package/dist/web/assets/{talonscript-C1XrFPwo.js → talonscript-CNOEcGDc.js} +0 -0
- /package/dist/web/assets/{tasl-B5raVn43.js → tasl-CjYZenSH.js} +0 -0
- /package/dist/web/assets/{tcl-3-vbeMIc.js → tcl-BVA09RDz.js} +0 -0
- /package/dist/web/assets/{terraform-a8VXZDm9.js → terraform-C5EloH1q.js} +0 -0
- /package/dist/web/assets/{text-wrap-B05VGRa-.js → text-wrap-D71Jqz5U.js} +0 -0
- /package/dist/web/assets/{tokyo-night-4SRhf-Hz.js → tokyo-night-D764DdhO.js} +0 -0
- /package/dist/web/assets/{toml-B4N0i8wT.js → toml-CCDYblSB.js} +0 -0
- /package/dist/web/assets/{trash-2-CTXlTflF.js → trash-2-C08fhIg1.js} +0 -0
- /package/dist/web/assets/{triangle-alert-BRq0VAfd.js → triangle-alert-85BQgs3T.js} +0 -0
- /package/dist/web/assets/{tsv-DA5k9Lvw.js → tsv-BZ4sA05e.js} +0 -0
- /package/dist/web/assets/{tsx-D9SZ8owC.js → tsx-DPLBzAt5.js} +0 -0
- /package/dist/web/assets/{turtle-BTHTvyY_.js → turtle-CwGHCq40.js} +0 -0
- /package/dist/web/assets/{typescript-DYzV9p5L.js → typescript-CUH0vBAl.js} +0 -0
- /package/dist/web/assets/{typespec-BC15eh7F.js → typespec-Bq31Vq4u.js} +0 -0
- /package/dist/web/assets/{typst-BJUqzA6D.js → typst-MD9BR9W5.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-DFWViA4S.js → use-is-mobile-DUuki97q.js} +0 -0
- /package/dist/web/assets/{utils-C9OjCrou.js → utils-u2fbYPrY.js} +0 -0
- /package/dist/web/assets/{v-50Lps6oA.js → v-DZV7IxIT.js} +0 -0
- /package/dist/web/assets/{vala-8wiUvvrD.js → vala-urd1zi9S.js} +0 -0
- /package/dist/web/assets/{vb-BlgPLt6B.js → vb-DIZU3zbJ.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BZOrwF_F.js → vendor-xterm-DCj0zgSC.js} +0 -0
- /package/dist/web/assets/{verilog-CD9pVW5p.js → verilog-Ndz_ciTA.js} +0 -0
- /package/dist/web/assets/{vesper-BrivW4PT.js → vesper-C86M7LJp.js} +0 -0
- /package/dist/web/assets/{vhdl-Bunxfa7N.js → vhdl-_klwDUjd.js} +0 -0
- /package/dist/web/assets/{viml-DndUb2qM.js → viml-DzRXWoQh.js} +0 -0
- /package/dist/web/assets/{vitesse-black-BurZSkxW.js → vitesse-black-TWZ20_3U.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-8UShspzC.js → vitesse-dark-Dl7R6fy9.js} +0 -0
- /package/dist/web/assets/{vitesse-light-DghCAZ90.js → vitesse-light-Cs0Dt2_n.js} +0 -0
- /package/dist/web/assets/{vyper-b-Clr-8u.js → vyper-qnCVj8ia.js} +0 -0
- /package/dist/web/assets/{wasm-CpcKStkw.js → wasm-Ckx9jBPe.js} +0 -0
- /package/dist/web/assets/{wasm-C5v2T06Y.js → wasm-Dy-SJ3W9.js} +0 -0
- /package/dist/web/assets/{wenyan-Dz2srLZR.js → wenyan-DTEy2Lx-.js} +0 -0
- /package/dist/web/assets/{wgsl-CeD1-dRb.js → wgsl-DC7n63Sy.js} +0 -0
- /package/dist/web/assets/{wikitext-CyeS-9FH.js → wikitext-1zFoZiW6.js} +0 -0
- /package/dist/web/assets/{wit-Kr-q-qBQ.js → wit-DtTfCRph.js} +0 -0
- /package/dist/web/assets/{wolfram-BKm8bmln.js → wolfram-B2Bgu1KL.js} +0 -0
- /package/dist/web/assets/{x-YDu4m2Iq.js → x-CeN0Qybc.js} +0 -0
- /package/dist/web/assets/{yaml-2XSxomjM.js → yaml-B-Jxk0IQ.js} +0 -0
- /package/dist/web/assets/{zenscript-CKIS34vf.js → zenscript-BWRfvNER.js} +0 -0
- /package/dist/web/assets/{zig-DdHGNllX.js → zig-CkwuvETm.js} +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { formatBackupStamp } from "./db-backup-paths.ts";
|
|
4
|
+
import { verifyBackupFile } from "./db-backup-sync.ts";
|
|
5
|
+
import { readStatus } from "../supervisor-state.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Restore a verified snapshot over the live database.
|
|
9
|
+
*
|
|
10
|
+
* Three properties this must not get wrong:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Verify before overwriting.** The snapshot is integrity-checked first,
|
|
13
|
+
* so a rotted backup can never destroy a database that still had a chance.
|
|
14
|
+
* 2. **Never destroy the file being replaced.** The current database is moved
|
|
15
|
+
* aside, not deleted — even a corrupt file is forensic evidence, and during
|
|
16
|
+
* the incident that motivated this module the only reason the cause could be
|
|
17
|
+
* established was that the broken file had been preserved.
|
|
18
|
+
* 3. **Delete the stale write-ahead log.** A `-wal`/`-shm` pair left beside a
|
|
19
|
+
* freshly restored database gets replayed onto it on the next open, which
|
|
20
|
+
* silently reintroduces the very corruption being recovered from. This is
|
|
21
|
+
* the single easiest way to turn a good restore into a second outage.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface RestoreResult {
|
|
25
|
+
restoredFrom: string;
|
|
26
|
+
dbPath: string;
|
|
27
|
+
archivedTo: string | null;
|
|
28
|
+
bytes: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Sidecar files SQLite keeps beside a database; they must not survive a swap. */
|
|
32
|
+
function sidecars(dbPath: string): string[] {
|
|
33
|
+
return [`${dbPath}-wal`, `${dbPath}-shm`];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* PID of a live supervisor from `status.json`, or null. Restoring under a
|
|
38
|
+
* running server would have it writing into a file that is being replaced.
|
|
39
|
+
*/
|
|
40
|
+
export function liveSupervisorPid(): number | null {
|
|
41
|
+
try {
|
|
42
|
+
const pid = readStatus().supervisorPid;
|
|
43
|
+
if (typeof pid !== "number" || pid <= 0) return null;
|
|
44
|
+
process.kill(pid, 0); // throws when the PID is gone
|
|
45
|
+
return pid;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function restoreDb(
|
|
52
|
+
backupPath: string,
|
|
53
|
+
opts: { dbPath: string; force?: boolean; now?: Date },
|
|
54
|
+
): RestoreResult {
|
|
55
|
+
const { dbPath } = opts;
|
|
56
|
+
if (!existsSync(backupPath)) throw new Error(`Backup not found: ${backupPath}`);
|
|
57
|
+
|
|
58
|
+
const check = verifyBackupFile(backupPath);
|
|
59
|
+
if (!check.ok) {
|
|
60
|
+
throw new Error(`Refusing to restore: backup failed integrity_check (${check.detail})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!opts.force) {
|
|
64
|
+
const pid = liveSupervisorPid();
|
|
65
|
+
if (pid !== null) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`PPM is running (supervisor PID ${pid}). Run \`ppm stop\` first, or pass --force to restore anyway.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const stamp = formatBackupStamp(opts.now ?? new Date());
|
|
73
|
+
let archivedTo: string | null = null;
|
|
74
|
+
if (existsSync(dbPath)) {
|
|
75
|
+
archivedTo = `${dbPath}.replaced-${stamp}`;
|
|
76
|
+
renameSync(dbPath, archivedTo);
|
|
77
|
+
// Move the sidecars alongside the file they belong to, so the archived
|
|
78
|
+
// database stays inspectable and cannot be replayed onto the new one.
|
|
79
|
+
for (const side of sidecars(dbPath)) {
|
|
80
|
+
if (existsSync(side)) {
|
|
81
|
+
try { renameSync(side, `${side.replace(dbPath, archivedTo)}`); } catch { rmSync(side, { force: true }); }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const side of sidecars(dbPath)) rmSync(side, { force: true });
|
|
86
|
+
|
|
87
|
+
copyFileSync(backupPath, dbPath);
|
|
88
|
+
|
|
89
|
+
const after = verifyBackupFile(dbPath);
|
|
90
|
+
if (!after.ok) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Restored file failed integrity_check (${after.detail}). Previous database preserved at ${archivedTo ?? "(none)"}.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { restoredFrom: backupPath, dbPath, archivedTo, bytes: statSync(dbPath).size };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Resolve a user-supplied backup argument: absolute/relative path, or a bare snapshot filename. */
|
|
100
|
+
export function resolveBackupArg(arg: string, backupsDir: string): string {
|
|
101
|
+
if (existsSync(arg)) return resolve(arg);
|
|
102
|
+
const inDir = resolve(backupsDir, arg);
|
|
103
|
+
if (existsSync(inDir)) return inDir;
|
|
104
|
+
throw new Error(`Backup not found: ${arg}`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseBackupName, type ParsedBackupName } from "./db-backup-paths.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Grandfather-father-son retention. Three tiers, unioned:
|
|
5
|
+
*
|
|
6
|
+
* - `recent`: the N newest snapshots regardless of age — the "what did I break
|
|
7
|
+
* in the last few hours" tier.
|
|
8
|
+
* - `daily`: newest snapshot of each of the last N distinct calendar days (UTC).
|
|
9
|
+
* - `weekly`: newest snapshot of each of the last N distinct ISO weeks.
|
|
10
|
+
*
|
|
11
|
+
* The daily/weekly depth is what catches slow, silent damage — the failure
|
|
12
|
+
* mode that actually happened here: a database corrupted at 23:08 and not
|
|
13
|
+
* noticed until the next morning's restart. A recent-only window would have
|
|
14
|
+
* already rotated the last good copy away.
|
|
15
|
+
*/
|
|
16
|
+
export interface RetentionPolicy {
|
|
17
|
+
recent: number;
|
|
18
|
+
daily: number;
|
|
19
|
+
weekly: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_RETENTION: RetentionPolicy = { recent: 6, daily: 7, weekly: 4 };
|
|
23
|
+
|
|
24
|
+
/** UTC calendar day key, e.g. `2026-09-06`. */
|
|
25
|
+
function dayKey(d: Date): string {
|
|
26
|
+
return d.toISOString().slice(0, 10);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ISO-week key, e.g. `2026-W37`. Uses the ISO rule (week belongs to the year
|
|
31
|
+
* containing its Thursday) so the last days of December group correctly
|
|
32
|
+
* instead of splitting a week across two year buckets.
|
|
33
|
+
*/
|
|
34
|
+
function weekKey(d: Date): string {
|
|
35
|
+
const t = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
36
|
+
const dayNum = t.getUTCDay() || 7; // Mon=1..Sun=7
|
|
37
|
+
t.setUTCDate(t.getUTCDate() + 4 - dayNum); // shift to that week's Thursday
|
|
38
|
+
const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
|
39
|
+
const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);
|
|
40
|
+
return `${t.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Newest-first, keeping only names this module wrote for the given database stem. */
|
|
44
|
+
function parseAndSort(names: string[], dbStem: string): Array<ParsedBackupName & { name: string }> {
|
|
45
|
+
return names
|
|
46
|
+
.map((name) => {
|
|
47
|
+
const parsed = parseBackupName(name);
|
|
48
|
+
return parsed && parsed.dbStem === dbStem ? { ...parsed, name } : null;
|
|
49
|
+
})
|
|
50
|
+
.filter((x): x is ParsedBackupName & { name: string } => x !== null)
|
|
51
|
+
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The snapshots the policy keeps, newest-first. */
|
|
55
|
+
export function selectBackupsToKeep(
|
|
56
|
+
names: string[],
|
|
57
|
+
dbStem: string,
|
|
58
|
+
policy: RetentionPolicy = DEFAULT_RETENTION,
|
|
59
|
+
): string[] {
|
|
60
|
+
const sorted = parseAndSort(names, dbStem);
|
|
61
|
+
const keep = new Set<string>();
|
|
62
|
+
|
|
63
|
+
for (const entry of sorted.slice(0, policy.recent)) keep.add(entry.name);
|
|
64
|
+
|
|
65
|
+
// Snapshots are newest-first, so the first one seen for a bucket is that
|
|
66
|
+
// bucket's newest — take it and skip the rest of the bucket.
|
|
67
|
+
const takeNewestPerBucket = (key: (d: Date) => string, limit: number) => {
|
|
68
|
+
const seen = new Set<string>();
|
|
69
|
+
for (const entry of sorted) {
|
|
70
|
+
if (seen.size >= limit && !seen.has(key(entry.date))) continue;
|
|
71
|
+
if (seen.has(key(entry.date))) continue;
|
|
72
|
+
seen.add(key(entry.date));
|
|
73
|
+
keep.add(entry.name);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
takeNewestPerBucket(dayKey, policy.daily);
|
|
77
|
+
takeNewestPerBucket(weekKey, policy.weekly);
|
|
78
|
+
|
|
79
|
+
return sorted.filter((e) => keep.has(e.name)).map((e) => e.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The snapshots the policy discards. Anything unparseable is left strictly
|
|
84
|
+
* alone: this function only ever proposes deleting files it can prove it
|
|
85
|
+
* wrote itself.
|
|
86
|
+
*/
|
|
87
|
+
export function selectBackupsToDelete(
|
|
88
|
+
names: string[],
|
|
89
|
+
dbStem: string,
|
|
90
|
+
policy: RetentionPolicy = DEFAULT_RETENTION,
|
|
91
|
+
): string[] {
|
|
92
|
+
const keep = new Set(selectBackupsToKeep(names, dbStem, policy));
|
|
93
|
+
return parseAndSort(names, dbStem)
|
|
94
|
+
.filter((e) => !keep.has(e.name))
|
|
95
|
+
.map((e) => e.name);
|
|
96
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { getBackupsDir, backupFileName, type BackupReason } from "./db-backup-paths.ts";
|
|
5
|
+
import { selectBackupsToDelete, DEFAULT_RETENTION, type RetentionPolicy } from "./db-backup-retention.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Synchronous core of the snapshot mechanism.
|
|
9
|
+
*
|
|
10
|
+
* Split from `db-backup.service.ts` for one structural reason: `db.service`
|
|
11
|
+
* takes a snapshot before running migrations, from inside the synchronous
|
|
12
|
+
* `getDb()` path, so that call cannot await. Keeping this core free of any
|
|
13
|
+
* import of `db.service` also means the caller passes the database path in,
|
|
14
|
+
* which removes what would otherwise be an import cycle.
|
|
15
|
+
*
|
|
16
|
+
* VACUUM INTO is SQLite's own online-backup statement: a transactionally
|
|
17
|
+
* consistent, compacted copy taken while the server keeps reading and writing.
|
|
18
|
+
* A plain file copy is NOT equivalent — it can catch the main database and its
|
|
19
|
+
* write-ahead log out of step and produce exactly the unopenable file this
|
|
20
|
+
* mechanism exists to recover from.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface BackupResult {
|
|
24
|
+
path: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
ms: number;
|
|
27
|
+
pruned: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BackupOptions {
|
|
31
|
+
/** Reuse an already-open connection — required when snapshotting state that same handle is about to migrate. */
|
|
32
|
+
sourceDb?: Database;
|
|
33
|
+
/** Source database file. */
|
|
34
|
+
dbPath: string;
|
|
35
|
+
retention?: RetentionPolicy;
|
|
36
|
+
now?: Date;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureBackupsDir(dir: string): void {
|
|
40
|
+
// Only mkdir when actually missing: recursive mkdir throws EEXIST against
|
|
41
|
+
// some Windows folders carrying the ReadOnly attribute.
|
|
42
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Open a snapshot and run a full integrity_check.
|
|
47
|
+
*
|
|
48
|
+
* Called right after writing a snapshot and again before restoring one. An
|
|
49
|
+
* unverified backup is not a backup: during the incident that motivated this
|
|
50
|
+
* module, two copies that looked present (a VSS shadow, a WAL rebuild) both
|
|
51
|
+
* turned out to be unusable only at the moment they were needed.
|
|
52
|
+
*/
|
|
53
|
+
export function verifyBackupFile(path: string): { ok: boolean; detail: string } {
|
|
54
|
+
let db: Database | null = null;
|
|
55
|
+
try {
|
|
56
|
+
db = new Database(path, { readonly: true });
|
|
57
|
+
const row = db.query("PRAGMA integrity_check").get() as { integrity_check?: string } | null;
|
|
58
|
+
const detail = row?.integrity_check ?? "no result";
|
|
59
|
+
return { ok: detail === "ok", detail };
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
return { ok: false, detail: e?.message ?? String(e) };
|
|
62
|
+
} finally {
|
|
63
|
+
try { db?.close(); } catch { /* already closed */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write one verified snapshot, then apply retention. Throws if the snapshot cannot be verified. */
|
|
68
|
+
export function backupDbSync(reason: BackupReason, opts: BackupOptions): BackupResult {
|
|
69
|
+
const { dbPath } = opts;
|
|
70
|
+
if (!existsSync(dbPath)) throw new Error(`No database to back up at ${dbPath}`);
|
|
71
|
+
|
|
72
|
+
const dir = getBackupsDir();
|
|
73
|
+
ensureBackupsDir(dir);
|
|
74
|
+
|
|
75
|
+
const dbFile = basename(dbPath);
|
|
76
|
+
const finalPath = resolve(dir, backupFileName(dbFile, opts.now ?? new Date(), reason));
|
|
77
|
+
// Snapshot under a .partial name so an interrupted run leaves nothing
|
|
78
|
+
// parseBackupName would ever offer as a restore candidate.
|
|
79
|
+
const partialPath = `${finalPath}.partial`;
|
|
80
|
+
rmSync(partialPath, { force: true });
|
|
81
|
+
|
|
82
|
+
const started = Date.now();
|
|
83
|
+
// Read-write (not readonly) on purpose: a readonly connection cannot recover
|
|
84
|
+
// a hot write-ahead log when it is the only connection, which is exactly the
|
|
85
|
+
// case for a manual backup while the server is stopped. VACUUM INTO never
|
|
86
|
+
// modifies the source either way.
|
|
87
|
+
const source = opts.sourceDb ?? new Database(dbPath);
|
|
88
|
+
try {
|
|
89
|
+
// The path is ours, but a home directory can legitimately contain an apostrophe.
|
|
90
|
+
source.exec(`VACUUM INTO '${partialPath.replace(/'/g, "''")}'`);
|
|
91
|
+
} finally {
|
|
92
|
+
if (!opts.sourceDb) source.close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const check = verifyBackupFile(partialPath);
|
|
96
|
+
if (!check.ok) {
|
|
97
|
+
rmSync(partialPath, { force: true });
|
|
98
|
+
throw new Error(`Snapshot failed integrity_check (${check.detail}) — discarded, no backup written`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
renameSync(partialPath, finalPath);
|
|
102
|
+
const ms = Date.now() - started;
|
|
103
|
+
|
|
104
|
+
const pruned: string[] = [];
|
|
105
|
+
const stem = dbFile.replace(/\.db$/, "");
|
|
106
|
+
for (const name of selectBackupsToDelete(readdirSync(dir), stem, opts.retention ?? DEFAULT_RETENTION)) {
|
|
107
|
+
// A locked or vanished snapshot is a pruning hiccup, not a backup failure —
|
|
108
|
+
// the new snapshot is already written and verified by this point.
|
|
109
|
+
try { rmSync(resolve(dir, name), { force: true }); pruned.push(name); } catch { /* keep going */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { path: finalPath, bytes: statSync(finalPath).size, ms, pruned };
|
|
113
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { getBackupsDir, parseBackupName, type BackupReason } from "./db-backup-paths.ts";
|
|
4
|
+
import { backupDbSync, type BackupOptions, type BackupResult } from "./db-backup-sync.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Async-friendly facade over the synchronous snapshot core, for callers that
|
|
8
|
+
* do not already hold the database open: the supervisor's hourly tick, the
|
|
9
|
+
* start-up snapshot, and the `ppm backup` command. It resolves the active
|
|
10
|
+
* database path lazily, so importing this module never opens a database.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { BackupResult, BackupOptions };
|
|
14
|
+
export { verifyBackupFile } from "./db-backup-sync.ts";
|
|
15
|
+
|
|
16
|
+
export interface BackupEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
date: Date;
|
|
21
|
+
reason: BackupReason;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Snapshot files present for a database stem (`ppm`, `ppm.dev`), newest-first. */
|
|
25
|
+
export function listBackups(dbStem?: string): BackupEntry[] {
|
|
26
|
+
const dir = getBackupsDir();
|
|
27
|
+
if (!existsSync(dir)) return [];
|
|
28
|
+
const out: BackupEntry[] = [];
|
|
29
|
+
for (const name of readdirSync(dir)) {
|
|
30
|
+
const parsed = parseBackupName(name);
|
|
31
|
+
if (!parsed) continue;
|
|
32
|
+
if (dbStem && parsed.dbStem !== dbStem) continue;
|
|
33
|
+
const path = resolve(dir, name);
|
|
34
|
+
let bytes = 0;
|
|
35
|
+
// A file that vanished between readdir and stat is simply not a candidate.
|
|
36
|
+
try { bytes = statSync(path).size; } catch { continue; }
|
|
37
|
+
out.push({ name, path, bytes, date: parsed.date, reason: parsed.reason });
|
|
38
|
+
}
|
|
39
|
+
return out.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Basename-derived stem of the active database, e.g. `ppm` or `ppm.dev`. */
|
|
43
|
+
export async function activeDbStem(): Promise<string> {
|
|
44
|
+
const { getDbPath } = await import("../db.service.ts");
|
|
45
|
+
return basename(getDbPath()).replace(/\.db$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Age of the newest snapshot for the active database in ms, or null when there is none. */
|
|
49
|
+
export async function newestBackupAgeMs(now: Date = new Date()): Promise<number | null> {
|
|
50
|
+
const newest = listBackups(await activeDbStem())[0];
|
|
51
|
+
return newest ? now.getTime() - newest.date.getTime() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Take one verified snapshot of the active database. */
|
|
55
|
+
export async function backupDb(
|
|
56
|
+
reason: BackupReason,
|
|
57
|
+
opts: Partial<Omit<BackupOptions, "dbPath">> & { dbPath?: string } = {},
|
|
58
|
+
): Promise<BackupResult> {
|
|
59
|
+
const { getDbPath } = await import("../db.service.ts");
|
|
60
|
+
return backupDbSync(reason, { ...opts, dbPath: opts.dbPath ?? getDbPath() });
|
|
61
|
+
}
|
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { mkdirSync, existsSync } from "node:fs";
|
|
4
4
|
import { encrypt, decrypt } from "../lib/account-crypto.ts";
|
|
5
5
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
6
|
+
import { backupDbSync } from "./db-backup/db-backup-sync.ts";
|
|
6
7
|
export const CURRENT_SCHEMA_VERSION = 42;
|
|
7
8
|
|
|
8
9
|
let db: Database | null = null;
|
|
@@ -14,7 +15,8 @@ export function setDbProfile(profile: string | null): void {
|
|
|
14
15
|
dbProfile = profile;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/** Absolute path of the active database file, honouring the dev profile. */
|
|
19
|
+
export function getDbPath(): string {
|
|
18
20
|
if (dbProfile) return resolve(getPpmDir(), `ppm.${dbProfile}.db`);
|
|
19
21
|
return resolve(getPpmDir(), "ppm.db");
|
|
20
22
|
}
|
|
@@ -27,10 +29,36 @@ export function getDb(): Database {
|
|
|
27
29
|
db = new Database(getDbPath());
|
|
28
30
|
db.exec("PRAGMA journal_mode = WAL");
|
|
29
31
|
db.exec("PRAGMA foreign_keys = ON");
|
|
32
|
+
backupBeforeMigrations(db);
|
|
30
33
|
runMigrations(db);
|
|
31
34
|
return db;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot the database before a schema migration rewrites it.
|
|
39
|
+
*
|
|
40
|
+
* Migrations are the riskiest write PPM makes to its own store: they run
|
|
41
|
+
* automatically on every open, and a crash partway through — or an older
|
|
42
|
+
* binary opening a newer schema — leaves a file no later version can read.
|
|
43
|
+
* The hourly snapshot is not enough on its own, because a migration can land
|
|
44
|
+
* within the same hour as the last one.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately synchronous and best-effort: a failure to snapshot must never
|
|
47
|
+
* stop the server from booting, and must never mask the migration error that
|
|
48
|
+
* follows it.
|
|
49
|
+
*/
|
|
50
|
+
function backupBeforeMigrations(database: Database): void {
|
|
51
|
+
const row = database.query("PRAGMA user_version").get() as { user_version: number };
|
|
52
|
+
if (row.user_version >= CURRENT_SCHEMA_VERSION) return;
|
|
53
|
+
if (row.user_version === 0) return; // brand-new database — nothing to lose yet
|
|
54
|
+
try {
|
|
55
|
+
const result = backupDbSync("premigrate", { sourceDb: database, dbPath: getDbPath() });
|
|
56
|
+
console.log(`[db] Pre-migration snapshot: ${result.path} (${(result.bytes / 1_048_576).toFixed(1)} MB)`);
|
|
57
|
+
} catch (e: any) {
|
|
58
|
+
console.error(`[db] Pre-migration snapshot FAILED (continuing): ${e?.message ?? e}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
/** Close the DB (for graceful shutdown or tests) */
|
|
35
63
|
export function closeDb(): void {
|
|
36
64
|
if (db) { db.close(); db = null; }
|
|
@@ -2,6 +2,7 @@ import { resolve, sep } from "node:path";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
4
4
|
import { getUploadsDir } from "./chat-upload-storage.service.ts";
|
|
5
|
+
import { getBackupsDir } from "./db-backup/db-backup-paths.ts";
|
|
5
6
|
import { realPathOrSelf } from "./fs-ops/fs-real-path.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -11,6 +12,9 @@ import { realPathOrSelf } from "./fs-ops/fs-real-path.ts";
|
|
|
11
12
|
* cert). Split out of `fs-path-guard.service.ts` so this one seam — "is this
|
|
12
13
|
* a credential path" — stays a single file instead of growing alongside the
|
|
13
14
|
* platform-allowlist and protected-root logic that lives there.
|
|
15
|
+
*
|
|
16
|
+
* Three roots, not two: the PPM config DB, `~/.cloudflared`, and the database
|
|
17
|
+
* snapshot directory, which holds full copies of the first one.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
/** Case-insensitive prefix test on Windows/macOS-style paths. */
|
|
@@ -50,9 +54,23 @@ export function isCloudflaredDirPath(resolved: string): boolean {
|
|
|
50
54
|
return isInside(resolved, resolve(homedir(), ".cloudflared"));
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* True when the path is the database-snapshot directory or anything inside it.
|
|
59
|
+
*
|
|
60
|
+
* A snapshot is a byte-for-byte copy of the config database, so it carries the
|
|
61
|
+
* same provider keys, encrypted accounts, and auth token. In production that
|
|
62
|
+
* directory deliberately sits OUTSIDE `~/.ppm` (so it survives a wipe of the
|
|
63
|
+
* PPM directory), which means `isPpmDirPath` does not cover it — without this
|
|
64
|
+
* branch, moving snapshots out of the PPM directory would have quietly opened
|
|
65
|
+
* a credential-read hole through the generic file routes.
|
|
66
|
+
*/
|
|
67
|
+
export function isDbBackupsDirPath(resolved: string): boolean {
|
|
68
|
+
return isInside(resolved, getBackupsDir());
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
/** True when the path holds credential material a generic file route must never serve or relocate. */
|
|
54
72
|
export function isCredentialPath(resolved: string): boolean {
|
|
55
|
-
return isPpmDirPath(resolved) || isCloudflaredDirPath(resolved);
|
|
73
|
+
return isPpmDirPath(resolved) || isCloudflaredDirPath(resolved) || isDbBackupsDirPath(resolved);
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
/**
|
|
@@ -98,7 +98,7 @@ export async function startLogin(opts: { relogin?: boolean } = {}): Promise<Logi
|
|
|
98
98
|
// itself failed to run, so cert.pem is left untouched and the user
|
|
99
99
|
// gets a clear, retryable error instead of an unwanted fresh login.
|
|
100
100
|
if (liveCheck === "unreachable") {
|
|
101
|
-
setTerminal("error", "
|
|
101
|
+
setTerminal("error", "could not reach Cloudflare to check the sign-in — try again");
|
|
102
102
|
return getLoginSnapshot();
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -117,7 +117,7 @@ export async function startLogin(opts: { relogin?: boolean } = {}): Promise<Logi
|
|
|
117
117
|
renameCertAside();
|
|
118
118
|
}
|
|
119
119
|
} else {
|
|
120
|
-
// Explicit ?relogin=1 — the user deliberately pressed "
|
|
120
|
+
// Explicit ?relogin=1 — the user deliberately pressed "Sign in again",
|
|
121
121
|
// so this renames unconditionally regardless of network reachability.
|
|
122
122
|
renameCertAside();
|
|
123
123
|
}
|