@hienlh/ppm 0.9.0-beta.9 → 0.9.2
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/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +240 -0
- package/bun.lock +17 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/browser-tab-CjUzlPYv.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-moB4W7-w.js +8 -0
- package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-aQQZUc2m.js +2 -0
- package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
- package/dist/web/assets/{csv-preview-DUbHtTAS.js → csv-preview-ncSOnJSC.js} +2 -2
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-ChyP1N3c.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-ktwO5JbX.js +4 -0
- package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
- package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/extension-webview-Bx1TlP6q.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-BIrGMX6e.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-C6KLr58u.js +37 -0
- package/dist/web/assets/index-DpBKDbIW.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-D3Y5c5uS.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-VIZB1GXE.js → markdown-renderer-A7J2gdKT.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/postgres-viewer-C9-Acry_.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-C17exmRv.js +1 -0
- package/dist/web/assets/sqlite-viewer-Dr5oWCWA.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
- package/dist/web/assets/table-DFevCOMd.js +1 -0
- package/dist/web/assets/tag-CXMT0QB6.js +1 -0
- package/dist/web/assets/{terminal-tab-XhKfb4ei.js → terminal-tab-CpyKvyfC.js} +1 -1
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/{use-monaco-theme-0p0-84jJ.js → use-monaco-theme-BjPAik5w.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +10 -9
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +128 -1
- package/docs/codebase-summary.md +79 -12
- package/docs/extension-development-guide.md +532 -0
- package/docs/project-changelog.md +51 -1
- package/docs/project-roadmap.md +9 -3
- package/docs/system-architecture.md +432 -3
- package/package.json +6 -3
- package/packages/ext-database/package.json +41 -0
- package/packages/ext-database/src/connection-tree.ts +142 -0
- package/packages/ext-database/src/extension.ts +346 -0
- package/packages/ext-database/src/query-panel.ts +120 -0
- package/packages/ext-database/src/table-viewer-panel.ts +410 -0
- package/packages/ext-database/tsconfig.json +8 -0
- package/packages/vscode-compat/package.json +16 -0
- package/packages/vscode-compat/src/commands.ts +39 -0
- package/packages/vscode-compat/src/context.ts +65 -0
- package/packages/vscode-compat/src/disposable.ts +21 -0
- package/packages/vscode-compat/src/env.ts +20 -0
- package/packages/vscode-compat/src/event-emitter.ts +28 -0
- package/packages/vscode-compat/src/index.ts +93 -0
- package/packages/vscode-compat/src/not-supported.ts +15 -0
- package/packages/vscode-compat/src/types.ts +167 -0
- package/packages/vscode-compat/src/uri.ts +65 -0
- package/packages/vscode-compat/src/window.ts +229 -0
- package/packages/vscode-compat/src/workspace.ts +76 -0
- package/packages/vscode-compat/tsconfig.json +10 -0
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/ext-cmd.ts +121 -0
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +221 -17
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/index.ts +55 -155
- package/src/server/routes/chat.ts +81 -11
- package/src/server/routes/extensions.ts +81 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/settings.ts +27 -0
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +9 -3
- package/src/server/ws/extensions.ts +175 -0
- package/src/services/account-selector.service.ts +14 -5
- package/src/services/account.service.ts +7 -7
- package/src/services/claude-usage.service.ts +11 -11
- package/src/services/cloud-ws.service.ts +228 -0
- package/src/services/cloud.service.ts +1 -0
- package/src/services/contribution-registry.ts +110 -0
- package/src/services/db.service.ts +181 -4
- package/src/services/extension-host-worker.ts +160 -0
- package/src/services/extension-installer.ts +112 -0
- package/src/services/extension-manifest.ts +65 -0
- package/src/services/extension-rpc-handlers.ts +235 -0
- package/src/services/extension-rpc.ts +105 -0
- package/src/services/extension.service.ts +228 -0
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +271 -25
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +4 -0
- package/src/types/extension-messages.ts +64 -0
- package/src/types/extension.ts +131 -0
- package/src/web/app.tsx +69 -48
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -10
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-input.tsx +29 -29
- package/src/web/components/chat/message-list.tsx +19 -6
- package/src/web/components/chat/session-picker.tsx +80 -32
- package/src/web/components/chat/usage-badge.tsx +83 -10
- package/src/web/components/extensions/extension-inputbox.tsx +92 -0
- package/src/web/components/extensions/extension-quickpick.tsx +194 -0
- package/src/web/components/extensions/extension-tree-view.tsx +240 -0
- package/src/web/components/extensions/extension-webview.tsx +83 -0
- package/src/web/components/layout/command-palette.tsx +22 -2
- package/src/web/components/layout/editor-panel.tsx +163 -18
- package/src/web/components/layout/mobile-nav.tsx +2 -1
- package/src/web/components/layout/sidebar.tsx +21 -3
- package/src/web/components/layout/status-bar.tsx +64 -0
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- package/src/web/components/settings/change-password-section.tsx +128 -0
- package/src/web/components/settings/extension-manager-section.tsx +214 -0
- package/src/web/components/settings/settings-tab.tsx +9 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +28 -0
- package/src/web/hooks/use-extension-ws.ts +181 -0
- package/src/web/hooks/use-global-keybindings.ts +18 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/extension-store.ts +204 -0
- package/src/web/stores/panel-store.ts +63 -9
- package/src/web/stores/panel-utils.ts +145 -3
- package/src/web/stores/settings-store.ts +7 -2
- package/src/web/stores/tab-store.ts +2 -1
- package/tsconfig.json +3 -1
- package/dist/web/assets/api-settings-CEMxVMCV.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-D1Zua62g.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-BnD27Vp9.js +0 -7
- package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DGRg8stf.js +0 -2
- package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
- package/dist/web/assets/database-viewer-DxCXZQcE.js +0 -1
- package/dist/web/assets/diff-viewer-C1sDJG35.js +0 -4
- package/dist/web/assets/git-graph-BDn-EiGE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-Bun94AK3.js +0 -37
- package/dist/web/assets/index-Db8uky1a.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
- package/dist/web/assets/keybindings-store-COmK4Dte.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-RCnvZ29H.js +0 -1
- package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/tab-store-Bjh6bXFP.js +0 -1
- package/dist/web/assets/table-CQVQM2SB.js +0 -1
- package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-BeaDXEkP.js → lib-BQ34Db2e.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { mkdirSync, existsSync } from "node:fs";
|
|
5
5
|
|
|
6
6
|
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
7
|
-
const CURRENT_SCHEMA_VERSION =
|
|
7
|
+
const CURRENT_SCHEMA_VERSION = 12;
|
|
8
8
|
|
|
9
9
|
let db: Database | null = null;
|
|
10
10
|
let dbProfile: string | null = null;
|
|
@@ -248,6 +248,95 @@ function runMigrations(database: Database): void {
|
|
|
248
248
|
PRAGMA user_version = 8;
|
|
249
249
|
`);
|
|
250
250
|
}
|
|
251
|
+
|
|
252
|
+
if (current < 9) {
|
|
253
|
+
database.exec(`
|
|
254
|
+
CREATE TABLE IF NOT EXISTS session_pins (
|
|
255
|
+
session_id TEXT PRIMARY KEY,
|
|
256
|
+
pinned_at TEXT DEFAULT (datetime('now'))
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
PRAGMA user_version = 9;
|
|
260
|
+
`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (current < 10) {
|
|
264
|
+
database.exec(`
|
|
265
|
+
CREATE TABLE IF NOT EXISTS workspace_state (
|
|
266
|
+
project_name TEXT PRIMARY KEY,
|
|
267
|
+
layout_json TEXT NOT NULL,
|
|
268
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
PRAGMA user_version = 10;
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (current < 11) {
|
|
276
|
+
try {
|
|
277
|
+
database.exec(`ALTER TABLE session_map ADD COLUMN project_path TEXT`);
|
|
278
|
+
} catch {
|
|
279
|
+
// Column may already exist
|
|
280
|
+
}
|
|
281
|
+
// Backfill project_path from projects table where project_name matches
|
|
282
|
+
database.exec(`
|
|
283
|
+
UPDATE session_map SET project_path = (
|
|
284
|
+
SELECT path FROM projects WHERE projects.name = session_map.project_name
|
|
285
|
+
) WHERE project_path IS NULL AND project_name IS NOT NULL
|
|
286
|
+
`);
|
|
287
|
+
database.exec(`PRAGMA user_version = 11`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (current < 12) {
|
|
291
|
+
database.exec(`
|
|
292
|
+
CREATE TABLE IF NOT EXISTS extensions (
|
|
293
|
+
id TEXT PRIMARY KEY,
|
|
294
|
+
version TEXT NOT NULL,
|
|
295
|
+
display_name TEXT,
|
|
296
|
+
description TEXT,
|
|
297
|
+
icon TEXT,
|
|
298
|
+
enabled INTEGER DEFAULT 1,
|
|
299
|
+
manifest TEXT NOT NULL,
|
|
300
|
+
installed_at TEXT DEFAULT (datetime('now')),
|
|
301
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
CREATE TABLE IF NOT EXISTS extension_storage (
|
|
305
|
+
ext_id TEXT NOT NULL,
|
|
306
|
+
scope TEXT NOT NULL,
|
|
307
|
+
key TEXT NOT NULL,
|
|
308
|
+
value TEXT,
|
|
309
|
+
PRIMARY KEY (ext_id, scope, key),
|
|
310
|
+
FOREIGN KEY (ext_id) REFERENCES extensions(id) ON DELETE CASCADE
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
PRAGMA user_version = 12;
|
|
314
|
+
`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Workspace helpers
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
export interface WorkspaceRow {
|
|
323
|
+
project_name: string;
|
|
324
|
+
layout_json: string;
|
|
325
|
+
updated_at: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function getWorkspace(projectName: string): WorkspaceRow | null {
|
|
329
|
+
return getDb().query(
|
|
330
|
+
"SELECT project_name, layout_json, updated_at FROM workspace_state WHERE project_name = ?",
|
|
331
|
+
).get(projectName) as WorkspaceRow | null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function setWorkspace(projectName: string, layoutJson: string): string {
|
|
335
|
+
const now = new Date().toISOString();
|
|
336
|
+
getDb().query(
|
|
337
|
+
"INSERT INTO workspace_state (project_name, layout_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(project_name) DO UPDATE SET layout_json = excluded.layout_json, updated_at = excluded.updated_at",
|
|
338
|
+
).run(projectName, layoutJson, now);
|
|
339
|
+
return now;
|
|
251
340
|
}
|
|
252
341
|
|
|
253
342
|
// ---------------------------------------------------------------------------
|
|
@@ -318,10 +407,15 @@ export function getSessionMapping(ppmId: string): string | null {
|
|
|
318
407
|
return row?.sdk_id ?? null;
|
|
319
408
|
}
|
|
320
409
|
|
|
321
|
-
export function
|
|
410
|
+
export function getSessionProjectPath(ppmId: string): string | null {
|
|
411
|
+
const row = getDb().query("SELECT project_path FROM session_map WHERE ppm_id = ?").get(ppmId) as { project_path: string } | null;
|
|
412
|
+
return row?.project_path ?? null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function setSessionMapping(ppmId: string, sdkId: string, projectName?: string, projectPath?: string): void {
|
|
322
416
|
getDb().query(
|
|
323
|
-
"INSERT INTO session_map (ppm_id, sdk_id, project_name) VALUES (?, ?, ?) ON CONFLICT(ppm_id) DO UPDATE SET sdk_id = excluded.sdk_id, project_name = excluded.project_name",
|
|
324
|
-
).run(ppmId, sdkId, projectName ?? null);
|
|
417
|
+
"INSERT INTO session_map (ppm_id, sdk_id, project_name, project_path) VALUES (?, ?, ?, ?) ON CONFLICT(ppm_id) DO UPDATE SET sdk_id = excluded.sdk_id, project_name = COALESCE(excluded.project_name, session_map.project_name), project_path = COALESCE(excluded.project_path, session_map.project_path)",
|
|
418
|
+
).run(ppmId, sdkId, projectName ?? null, projectPath ?? null);
|
|
325
419
|
}
|
|
326
420
|
|
|
327
421
|
export function getAllSessionMappings(): Record<string, string> {
|
|
@@ -358,6 +452,33 @@ export function getSessionTitles(sessionIds: string[]): Record<string, string> {
|
|
|
358
452
|
return result;
|
|
359
453
|
}
|
|
360
454
|
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
// Session pin helpers
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
export function pinSession(sessionId: string): void {
|
|
460
|
+
getDb().query(
|
|
461
|
+
"INSERT INTO session_pins (session_id, pinned_at) VALUES (?, datetime('now')) ON CONFLICT(session_id) DO UPDATE SET pinned_at = datetime('now')",
|
|
462
|
+
).run(sessionId);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function unpinSession(sessionId: string): void {
|
|
466
|
+
getDb().query("DELETE FROM session_pins WHERE session_id = ?").run(sessionId);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function getPinnedSessionIds(): Set<string> {
|
|
470
|
+
const rows = getDb().query("SELECT session_id FROM session_pins ORDER BY pinned_at DESC").all() as { session_id: string }[];
|
|
471
|
+
return new Set(rows.map((r) => r.session_id));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function deleteSessionMapping(ppmId: string): void {
|
|
475
|
+
getDb().query("DELETE FROM session_map WHERE ppm_id = ?").run(ppmId);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function deleteSessionTitle(sessionId: string): void {
|
|
479
|
+
getDb().query("DELETE FROM session_titles WHERE session_id = ?").run(sessionId);
|
|
480
|
+
}
|
|
481
|
+
|
|
361
482
|
// ---------------------------------------------------------------------------
|
|
362
483
|
// Push subscription helpers
|
|
363
484
|
// ---------------------------------------------------------------------------
|
|
@@ -717,5 +838,61 @@ export function incrementAccountRequests(id: string): void {
|
|
|
717
838
|
getDb().query("UPDATE accounts SET total_requests = total_requests + 1 WHERE id = ?").run(id);
|
|
718
839
|
}
|
|
719
840
|
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
// Extension helpers
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
|
|
845
|
+
import type { ExtensionRow, ExtensionStorageRow } from "../types/extension.ts";
|
|
846
|
+
|
|
847
|
+
export function getExtensions(): ExtensionRow[] {
|
|
848
|
+
return getDb().query("SELECT * FROM extensions ORDER BY display_name, id").all() as ExtensionRow[];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function getExtensionById(id: string): ExtensionRow | null {
|
|
852
|
+
return getDb().query("SELECT * FROM extensions WHERE id = ?").get(id) as ExtensionRow | null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function insertExtension(row: Omit<ExtensionRow, "installed_at" | "updated_at">): void {
|
|
856
|
+
getDb().query(
|
|
857
|
+
`INSERT INTO extensions (id, version, display_name, description, icon, enabled, manifest)
|
|
858
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
859
|
+
).run(row.id, row.version, row.display_name, row.description, row.icon, row.enabled, row.manifest);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export function updateExtension(id: string, updates: Partial<Pick<ExtensionRow, "version" | "display_name" | "description" | "icon" | "enabled" | "manifest">>): void {
|
|
863
|
+
const sets: string[] = [];
|
|
864
|
+
const vals: unknown[] = [];
|
|
865
|
+
if (updates.version !== undefined) { sets.push("version = ?"); vals.push(updates.version); }
|
|
866
|
+
if (updates.display_name !== undefined) { sets.push("display_name = ?"); vals.push(updates.display_name); }
|
|
867
|
+
if (updates.description !== undefined) { sets.push("description = ?"); vals.push(updates.description); }
|
|
868
|
+
if (updates.icon !== undefined) { sets.push("icon = ?"); vals.push(updates.icon); }
|
|
869
|
+
if (updates.enabled !== undefined) { sets.push("enabled = ?"); vals.push(updates.enabled); }
|
|
870
|
+
if (updates.manifest !== undefined) { sets.push("manifest = ?"); vals.push(updates.manifest); }
|
|
871
|
+
if (sets.length === 0) return;
|
|
872
|
+
sets.push("updated_at = datetime('now')");
|
|
873
|
+
vals.push(id);
|
|
874
|
+
getDb().query(`UPDATE extensions SET ${sets.join(", ")} WHERE id = ?`).run(...(vals as SQLQueryBindings[]));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function deleteExtension(id: string): void {
|
|
878
|
+
getDb().query("DELETE FROM extensions WHERE id = ?").run(id);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function getExtensionStorage(extId: string, scope: string): ExtensionStorageRow[] {
|
|
882
|
+
return getDb().query("SELECT * FROM extension_storage WHERE ext_id = ? AND scope = ?").all(extId, scope) as ExtensionStorageRow[];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function setExtensionStorageValue(extId: string, scope: string, key: string, value: string | null): void {
|
|
886
|
+
getDb().query(
|
|
887
|
+
`INSERT INTO extension_storage (ext_id, scope, key, value)
|
|
888
|
+
VALUES (?, ?, ?, ?)
|
|
889
|
+
ON CONFLICT(ext_id, scope, key) DO UPDATE SET value = excluded.value`,
|
|
890
|
+
).run(extId, scope, key, value);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function deleteExtensionStorage(extId: string): void {
|
|
894
|
+
getDb().query("DELETE FROM extension_storage WHERE ext_id = ?").run(extId);
|
|
895
|
+
}
|
|
896
|
+
|
|
720
897
|
// Auto-close on process exit
|
|
721
898
|
process.on("beforeExit", closeDb);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension Host Worker — runs inside a Bun Worker thread.
|
|
3
|
+
* Loads, activates, and deactivates extensions in isolation.
|
|
4
|
+
* Communicates with the main process via typed RPC (postMessage).
|
|
5
|
+
*/
|
|
6
|
+
import { RpcChannel } from "./extension-rpc.ts";
|
|
7
|
+
import { createVscodeCompat } from "@ppm/vscode-compat";
|
|
8
|
+
import type { WindowService } from "@ppm/vscode-compat/src/window.ts";
|
|
9
|
+
import type { CommandService } from "@ppm/vscode-compat/src/commands.ts";
|
|
10
|
+
import type { Disposable, RpcMessage } from "../types/extension.ts";
|
|
11
|
+
|
|
12
|
+
// Active extension instances: id → { module, context, deactivate, services }
|
|
13
|
+
const activeExtensions = new Map<string, {
|
|
14
|
+
deactivate?: () => void | Promise<void>;
|
|
15
|
+
context: { subscriptions: Disposable[] };
|
|
16
|
+
window?: WindowService;
|
|
17
|
+
commands?: CommandService;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const rpc = new RpcChannel((msg) => postMessage(msg));
|
|
21
|
+
|
|
22
|
+
// Listen for messages from main process
|
|
23
|
+
declare const self: Worker;
|
|
24
|
+
self.addEventListener("message", (event: MessageEvent<RpcMessage>) => {
|
|
25
|
+
rpc.handleMessage(event.data);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// --- RPC handlers ---
|
|
29
|
+
|
|
30
|
+
rpc.onRequest("ext:activate", async (params) => {
|
|
31
|
+
const [extId, entryPath, extensionPath, storedState, baseUrl] = params as [string, string, string, Record<string, Record<string, string | null>>?, string?];
|
|
32
|
+
if (activeExtensions.has(extId)) return { ok: true, already: true };
|
|
33
|
+
|
|
34
|
+
// Expose server base URL so extensions can use fetch() with absolute URLs
|
|
35
|
+
if (baseUrl) (globalThis as any).__PPM_BASE_URL__ = baseUrl;
|
|
36
|
+
|
|
37
|
+
// Create RpcClient adapter for vscode-compat (Worker's RPC → vscode-compat interface)
|
|
38
|
+
const rpcClient = {
|
|
39
|
+
request: <T = unknown>(method: string, ...p: unknown[]) => rpc.sendRequest<T>(method, ...p),
|
|
40
|
+
notify: (event: string, data: unknown) => rpc.sendEvent(event, data),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Create vscode-compat API scoped to this extension
|
|
44
|
+
const api = createVscodeCompat({
|
|
45
|
+
extensionId: extId,
|
|
46
|
+
extensionPath,
|
|
47
|
+
storagePath: `${extensionPath}/.storage`,
|
|
48
|
+
rpc: rpcClient,
|
|
49
|
+
storedState: storedState as { global?: Record<string, string | null>; workspace?: Record<string, string | null> },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const context = api._createContext();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const mod = await import(entryPath);
|
|
56
|
+
const activateFn = mod.activate || mod.default?.activate;
|
|
57
|
+
if (typeof activateFn === "function") {
|
|
58
|
+
// Activation timeout: 10s max to prevent hanging extensions
|
|
59
|
+
const activatePromise = Promise.resolve(activateFn(context, api));
|
|
60
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
61
|
+
setTimeout(() => reject(new Error(`Activation timeout (10s) for ${extId}`)), 10_000),
|
|
62
|
+
);
|
|
63
|
+
await Promise.race([activatePromise, timeoutPromise]);
|
|
64
|
+
}
|
|
65
|
+
activeExtensions.set(extId, {
|
|
66
|
+
deactivate: mod.deactivate || mod.default?.deactivate,
|
|
67
|
+
context,
|
|
68
|
+
window: api.window as WindowService,
|
|
69
|
+
commands: api.commands as CommandService,
|
|
70
|
+
});
|
|
71
|
+
return { ok: true };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
74
|
+
console.error(`[ExtHost] Failed to activate ${extId}:`, msg);
|
|
75
|
+
return { ok: false, error: msg };
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
rpc.onRequest("ext:deactivate", async (params) => {
|
|
80
|
+
const [extId] = params as [string];
|
|
81
|
+
const ext = activeExtensions.get(extId);
|
|
82
|
+
if (!ext) return { ok: true, already: true };
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
if (typeof ext.deactivate === "function") {
|
|
86
|
+
await ext.deactivate();
|
|
87
|
+
}
|
|
88
|
+
// Dispose all subscriptions
|
|
89
|
+
for (const sub of ext.context.subscriptions) {
|
|
90
|
+
try { (sub as Disposable).dispose(); } catch {}
|
|
91
|
+
}
|
|
92
|
+
activeExtensions.delete(extId);
|
|
93
|
+
return { ok: true };
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
96
|
+
console.error(`[ExtHost] Failed to deactivate ${extId}:`, msg);
|
|
97
|
+
activeExtensions.delete(extId);
|
|
98
|
+
return { ok: false, error: msg };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
rpc.onRequest("ext:command:execute", async (params) => {
|
|
103
|
+
const [command, ...args] = params as [string, ...unknown[]];
|
|
104
|
+
for (const [, ext] of activeExtensions) {
|
|
105
|
+
if (ext.commands) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await (ext.commands as any).executeCommand(command, ...args);
|
|
108
|
+
return { ok: true, result };
|
|
109
|
+
} catch {
|
|
110
|
+
// Command not found in this extension, try next
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { ok: false, error: `Command not found: ${command}` };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Deliver webview messages from browser → extension's onDidReceiveMessage
|
|
118
|
+
rpc.onRequest("ext:webview:message", async (params) => {
|
|
119
|
+
const [panelId, message] = params as [string, unknown];
|
|
120
|
+
for (const [, ext] of activeExtensions) {
|
|
121
|
+
if (!ext.window) continue;
|
|
122
|
+
try {
|
|
123
|
+
if ((ext.window as any)._deliverWebviewMessage(panelId, message)) {
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(`[ExtHost] webview:message error (${panelId}):`, e);
|
|
128
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: false, error: `No handler for panel ${panelId}` };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Handle tree:expand — get children for a tree node
|
|
135
|
+
rpc.onRequest("ext:tree:expand", async (params) => {
|
|
136
|
+
const [viewId, itemId] = params as [string, string | undefined];
|
|
137
|
+
for (const [, ext] of activeExtensions) {
|
|
138
|
+
if (ext.window) {
|
|
139
|
+
try {
|
|
140
|
+
const items = await (ext.window as any)._getTreeChildren(viewId, itemId);
|
|
141
|
+
if (items.length > 0) return { ok: true, items };
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error(`[ExtHost] tree:expand error (${viewId}):`, e);
|
|
144
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, items: [] };
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
rpc.onRequest("ext:list-active", () => {
|
|
152
|
+
return [...activeExtensions.keys()];
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
rpc.onRequest("ext:ping", () => "pong");
|
|
156
|
+
|
|
157
|
+
// ExtensionContext is now created by @ppm/vscode-compat's createVscodeCompat()._createContext()
|
|
158
|
+
|
|
159
|
+
// Notify main process that worker is ready
|
|
160
|
+
rpc.sendEvent("worker:ready", {});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
|
3
|
+
import type { ExtensionManifest } from "../types/extension.ts";
|
|
4
|
+
import { getExtensionById, insertExtension, updateExtension, deleteExtension, deleteExtensionStorage } from "./db.service.ts";
|
|
5
|
+
import { readManifestAt } from "./extension-manifest.ts";
|
|
6
|
+
|
|
7
|
+
const INSTALL_TIMEOUT = 60_000;
|
|
8
|
+
const NPM_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[^@]+)?$/;
|
|
9
|
+
|
|
10
|
+
/** Ensure ~/.ppm/extensions/ dir + isolated package.json exist */
|
|
11
|
+
export function ensureExtensionsDir(extensionsDir: string): void {
|
|
12
|
+
if (!existsSync(extensionsDir)) {
|
|
13
|
+
mkdirSync(extensionsDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
const pkgJsonPath = resolve(extensionsDir, "package.json");
|
|
16
|
+
if (!existsSync(pkgJsonPath)) {
|
|
17
|
+
writeFileSync(pkgJsonPath, JSON.stringify({ name: "ppm-extensions", private: true, dependencies: {} }, null, 2));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Install an npm package into the extensions directory and persist to DB */
|
|
22
|
+
export async function installExtension(name: string, extensionsDir: string): Promise<ExtensionManifest> {
|
|
23
|
+
if (!NPM_PACKAGE_RE.test(name)) throw new Error(`Invalid package name: ${name}`);
|
|
24
|
+
ensureExtensionsDir(extensionsDir);
|
|
25
|
+
|
|
26
|
+
const proc = Bun.spawn(["bun", "add", name], {
|
|
27
|
+
cwd: extensionsDir,
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const timeout = setTimeout(() => proc.kill(), INSTALL_TIMEOUT);
|
|
33
|
+
const exitCode = await proc.exited;
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
|
|
36
|
+
if (exitCode !== 0) {
|
|
37
|
+
const stderr = await new Response(proc.stderr).text();
|
|
38
|
+
throw new Error(`Install failed (exit ${exitCode}): ${stderr.slice(0, 500)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pkgDir = resolve(extensionsDir, "node_modules", name);
|
|
42
|
+
const manifest = readManifestAt(pkgDir);
|
|
43
|
+
if (!manifest) throw new Error(`Installed ${name} but no valid manifest found`);
|
|
44
|
+
|
|
45
|
+
upsertExtensionInDb(manifest);
|
|
46
|
+
console.log(`[ExtService] Installed ${manifest.id}@${manifest.version}`);
|
|
47
|
+
return manifest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Remove an extension from disk + DB */
|
|
51
|
+
export async function removeExtension(id: string, extensionsDir: string): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
const proc = Bun.spawn(["bun", "remove", id], {
|
|
54
|
+
cwd: extensionsDir,
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "pipe",
|
|
57
|
+
});
|
|
58
|
+
await proc.exited;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(`[ExtService] npm remove ${id} failed (DB record still removed):`, e);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
deleteExtensionStorage(id);
|
|
64
|
+
deleteExtension(id);
|
|
65
|
+
console.log(`[ExtService] Removed ${id}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Symlink a local extension path for development */
|
|
69
|
+
export function devLinkExtension(localPath: string, extensionsDir: string): ExtensionManifest {
|
|
70
|
+
const absPath = resolve(localPath);
|
|
71
|
+
const manifest = readManifestAt(absPath);
|
|
72
|
+
if (!manifest) throw new Error(`No valid package.json at ${absPath}`);
|
|
73
|
+
|
|
74
|
+
ensureExtensionsDir(extensionsDir);
|
|
75
|
+
const nodeModules = resolve(extensionsDir, "node_modules");
|
|
76
|
+
if (!existsSync(nodeModules)) mkdirSync(nodeModules, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const targetDir = resolve(nodeModules, manifest.id);
|
|
79
|
+
const parentDir = resolve(targetDir, "..");
|
|
80
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
83
|
+
symlinkSync(absPath, targetDir, "dir");
|
|
84
|
+
|
|
85
|
+
upsertExtensionInDb(manifest);
|
|
86
|
+
console.log(`[ExtService] Dev-linked ${manifest.id} → ${absPath}`);
|
|
87
|
+
return manifest;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Insert or update extension record in DB */
|
|
91
|
+
function upsertExtensionInDb(manifest: ExtensionManifest): void {
|
|
92
|
+
const existing = getExtensionById(manifest.id);
|
|
93
|
+
if (existing) {
|
|
94
|
+
updateExtension(manifest.id, {
|
|
95
|
+
version: manifest.version,
|
|
96
|
+
display_name: manifest.displayName ?? null,
|
|
97
|
+
description: manifest.description ?? null,
|
|
98
|
+
icon: manifest.icon ?? null,
|
|
99
|
+
manifest: JSON.stringify(manifest),
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
insertExtension({
|
|
103
|
+
id: manifest.id,
|
|
104
|
+
version: manifest.version,
|
|
105
|
+
display_name: manifest.displayName ?? null,
|
|
106
|
+
description: manifest.description ?? null,
|
|
107
|
+
icon: manifest.icon ?? null,
|
|
108
|
+
enabled: 1,
|
|
109
|
+
manifest: JSON.stringify(manifest),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import type { ExtensionManifest } from "../types/extension.ts";
|
|
5
|
+
|
|
6
|
+
/** Parse a package.json object into an ExtensionManifest (or null if invalid) */
|
|
7
|
+
export function parseManifest(pkg: Record<string, unknown>): ExtensionManifest | null {
|
|
8
|
+
const name = pkg.name as string | undefined;
|
|
9
|
+
const version = pkg.version as string | undefined;
|
|
10
|
+
const main = pkg.main as string | undefined;
|
|
11
|
+
if (!name || !version || !main) return null;
|
|
12
|
+
|
|
13
|
+
const ppmField = pkg.ppm as Record<string, unknown> | undefined;
|
|
14
|
+
return {
|
|
15
|
+
id: name,
|
|
16
|
+
version,
|
|
17
|
+
main,
|
|
18
|
+
displayName: (ppmField?.displayName as string) || (pkg.displayName as string) || name,
|
|
19
|
+
description: pkg.description as string | undefined,
|
|
20
|
+
icon: (ppmField?.icon as string) || undefined,
|
|
21
|
+
engines: pkg.engines as ExtensionManifest["engines"],
|
|
22
|
+
activationEvents: pkg.activationEvents as string[] | undefined,
|
|
23
|
+
contributes: pkg.contributes as ExtensionManifest["contributes"],
|
|
24
|
+
ppm: ppmField as ExtensionManifest["ppm"],
|
|
25
|
+
permissions: pkg.permissions as string[] | undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read and parse manifest from a directory containing package.json */
|
|
30
|
+
export function readManifestAt(dir: string): ExtensionManifest | null {
|
|
31
|
+
const pkgPath = resolve(dir, "package.json");
|
|
32
|
+
if (!existsSync(pkgPath)) return null;
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
return parseManifest(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Scan extensions directory for all valid manifests */
|
|
42
|
+
export async function discoverManifests(extensionsDir: string): Promise<ExtensionManifest[]> {
|
|
43
|
+
const manifests: ExtensionManifest[] = [];
|
|
44
|
+
if (!existsSync(extensionsDir)) return manifests;
|
|
45
|
+
|
|
46
|
+
const entries = await readdir(extensionsDir, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
49
|
+
if (entry.name === "node_modules" || entry.name === "package.json") continue;
|
|
50
|
+
|
|
51
|
+
// Handle scoped packages (@scope/name)
|
|
52
|
+
const entryPath = resolve(extensionsDir, entry.name);
|
|
53
|
+
if (entry.name.startsWith("@")) {
|
|
54
|
+
const scopedEntries = await readdir(entryPath, { withFileTypes: true });
|
|
55
|
+
for (const scoped of scopedEntries) {
|
|
56
|
+
const manifest = readManifestAt(resolve(entryPath, scoped.name));
|
|
57
|
+
if (manifest) manifests.push(manifest);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const manifest = readManifestAt(entryPath);
|
|
61
|
+
if (manifest) manifests.push(manifest);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return manifests;
|
|
65
|
+
}
|