@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30
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/README.md +14 -0
- package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
- package/dist/assets/AssistantView-pBywAsqy.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.js} +1 -1
- package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
- package/dist/assets/Icon-BN87CyS4.js +1 -0
- package/dist/assets/MarkdownRenderer-DHiNU8n3.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
- package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.js} +1 -1
- package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
- package/dist/assets/SettingsView-pjD0LrHE.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
- package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
- package/dist/assets/appThemeRegistry-BqDCGocy.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
- package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
- package/dist/assets/collapsible-nU99prkD.js +1 -0
- package/dist/assets/context-menu-DMzZN1oi.js +1 -0
- package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
- package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
- package/dist/assets/dialog-ByGUkS3c.js +1 -0
- package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
- package/dist/assets/es-DCjl24dV.js +15 -0
- package/dist/assets/fileMentionTouchSelection-Cgkxwzqb.js +2 -0
- package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/fr-DfJ5j0t5.js +13 -0
- package/dist/assets/{gitApi-CM42qUed.js → gitApi-Dwqkc2nZ.js} +3 -3
- package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
- package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
- package/dist/assets/insertionBoundaries-BhWfAOPY.js +1 -0
- package/dist/assets/ja-B_9_w2Xf.js +15 -0
- package/dist/assets/ko-DlGbo3kV.js +15 -0
- package/dist/assets/main-CGMinhgQ.js +169 -0
- package/dist/assets/main-pMpcLJha.js +2 -0
- package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-DfrW3VHF.js} +1 -1
- package/dist/assets/miniChat-YOt30ECc.js +2 -0
- package/dist/assets/mobile-BK1bBCYD.js +2 -0
- package/dist/assets/multirun-Dua24A69.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/pl-DjUsS8Ei.js +15 -0
- package/dist/assets/platform-X3Y3GyM5.js +6 -0
- package/dist/assets/projectMeta-DXdoPmbz.js +1 -0
- package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-BOuEz_oT.js +2 -0
- package/dist/assets/renderMobileApp-C8Alpv-B.js +28 -0
- package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
- package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
- package/dist/assets/runtimeConfig-DX_NP2Zm.js +2 -0
- package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
- package/dist/assets/src-OzbnJsph.css +2 -0
- package/dist/assets/store-Cuktref-.js +15 -0
- package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
- package/dist/assets/textarea-biAqiqnJ.js +1 -0
- package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
- package/dist/assets/uk-Bh7HXyLp.js +15 -0
- package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-bafOEpgK.js} +57 -52
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
- package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BC5OOH3M.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-BNQUAMEB.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
- package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
- package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
- package/dist/index.html +15 -15
- package/dist/mini-chat.html +15 -15
- package/dist/mobile.html +15 -15
- package/package.json +3 -2
- package/server/index.js +46 -27
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/message-queue/service.js +1 -1
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +128 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +36 -19
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +16 -124
- package/server/lib/opencode/config-entity-routes.test.js +48 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +18 -1
- package/server/lib/opencode/feature-routes-runtime.js +19 -7
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +13 -1
- package/server/lib/opencode/proxy.test.js +31 -3
- package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
- package/server/lib/opencode/routes.js +40 -8
- package/server/lib/opencode/routes.test.js +51 -10
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/update-discovery.js +39 -0
- package/server/lib/opencode/update-discovery.test.js +39 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +292 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +3 -1
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
- package/server/lib/scheduled-tasks/runtime.js +12 -4
- package/server/lib/scheduled-tasks/runtime.test.js +81 -6
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +49 -2
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +52 -14
- package/server/lib/session-title/native-runtime.test.js +59 -5
- package/server/lib/session-title/runtime.js +65 -19
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
- package/dist/assets/Icon-BiEQEcih.js +0 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
- package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
- package/dist/assets/SettingsView-BRxELjow.js +0 -25
- package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
- package/dist/assets/collapsible-DXbDynN4.js +0 -3
- package/dist/assets/context-menu-CqolsLCm.js +0 -1
- package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
- package/dist/assets/dialog-DQYl3W_o.js +0 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-BILgBI3B.js +0 -169
- package/dist/assets/main-BeveiW6F.js +0 -2
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-BgrzM3u7.js +0 -2
- package/dist/assets/mobile-DtYoQE8C.js +0 -2
- package/dist/assets/multirun-jy8dEI96.js +0 -4
- package/dist/assets/persistence-C9LQpiOS.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-BQghHRfp.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
- package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
- package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
- package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
- package/dist/assets/src-CLN7Z-zx.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/textarea-CakKvqFw.js +0 -1
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import yaml from 'yaml';
|
|
5
|
-
|
|
6
|
-
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
7
|
-
const GLOBAL_SNIPPET_DIR = path.join(OPENCODE_CONFIG_DIR, 'snippet');
|
|
8
|
-
const GLOBAL_SNIPPET_DIR_ALT = path.join(OPENCODE_CONFIG_DIR, 'snippets');
|
|
9
|
-
const SNIPPET_EXTENSION = '.md';
|
|
10
|
-
const SNIPPET_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,79}$/i;
|
|
11
|
-
const HASHTAG_PATTERN = /#([a-z0-9_-]+)/gi;
|
|
12
|
-
const MAX_EXPANSION_COUNT = 15;
|
|
13
|
-
|
|
14
|
-
function getProjectSnippetDirs(workingDirectory) {
|
|
15
|
-
if (!workingDirectory) return [];
|
|
16
|
-
return [
|
|
17
|
-
path.join(workingDirectory, '.opencode', 'snippets'),
|
|
18
|
-
path.join(workingDirectory, '.opencode', 'snippet'),
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getGlobalSnippetDirs() {
|
|
23
|
-
return [GLOBAL_SNIPPET_DIR_ALT, GLOBAL_SNIPPET_DIR];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getLoadDirs(workingDirectory) {
|
|
27
|
-
return [
|
|
28
|
-
...getGlobalSnippetDirs().map((dir) => ({ dir, source: 'global' })),
|
|
29
|
-
...getProjectSnippetDirs(workingDirectory).map((dir) => ({ dir, source: 'project' })),
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function assertValidSnippetName(name) {
|
|
34
|
-
if (typeof name !== 'string' || !SNIPPET_NAME_PATTERN.test(name)) {
|
|
35
|
-
throw new Error('Snippet name must use letters, numbers, dashes, or underscores');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseMarkdownFile(filePath) {
|
|
40
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
41
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
42
|
-
if (!match) {
|
|
43
|
-
return { frontmatter: {}, body: content.trim() };
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
frontmatter: yaml.parse(match[1]) || {},
|
|
47
|
-
body: match[2].trim(),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeAliases(frontmatter) {
|
|
52
|
-
const raw = frontmatter.aliases ?? frontmatter.alias;
|
|
53
|
-
if (!raw) return [];
|
|
54
|
-
const aliases = Array.isArray(raw) ? raw : [raw];
|
|
55
|
-
return aliases.map((alias) => String(alias).trim()).filter(Boolean);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function writeMarkdownFile(filePath, { content, aliases = [], description }) {
|
|
59
|
-
const frontmatter = {};
|
|
60
|
-
const normalizedAliases = aliases.map((alias) => String(alias).trim()).filter(Boolean);
|
|
61
|
-
if (normalizedAliases.length > 0) frontmatter.aliases = normalizedAliases;
|
|
62
|
-
if (description?.trim()) frontmatter.description = description.trim();
|
|
63
|
-
|
|
64
|
-
const body = content ?? '';
|
|
65
|
-
const output = Object.keys(frontmatter).length > 0
|
|
66
|
-
? `---\n${yaml.stringify(frontmatter)}---\n${body ? `\n${body}` : ''}`
|
|
67
|
-
: body;
|
|
68
|
-
|
|
69
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
70
|
-
fs.writeFileSync(filePath, output, 'utf8');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function loadSnippetFile(dir, filename, source) {
|
|
74
|
-
const name = path.basename(filename, SNIPPET_EXTENSION);
|
|
75
|
-
if (!SNIPPET_NAME_PATTERN.test(name)) return null;
|
|
76
|
-
const filePath = path.join(dir, filename);
|
|
77
|
-
const { frontmatter, body } = parseMarkdownFile(filePath);
|
|
78
|
-
return {
|
|
79
|
-
name,
|
|
80
|
-
content: body,
|
|
81
|
-
aliases: normalizeAliases(frontmatter),
|
|
82
|
-
description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
|
|
83
|
-
filePath,
|
|
84
|
-
source,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function removeSnippetAliases(registry, snippet, canonicalNames) {
|
|
89
|
-
for (const alias of snippet.aliases) {
|
|
90
|
-
const aliasKey = alias.toLowerCase();
|
|
91
|
-
if (canonicalNames.has(aliasKey)) continue;
|
|
92
|
-
if (registry.get(aliasKey) === snippet) registry.delete(aliasKey);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function registerSnippet(registry, snippet, canonicalNames) {
|
|
97
|
-
const key = snippet.name.toLowerCase();
|
|
98
|
-
const existing = registry.get(key);
|
|
99
|
-
if (existing?.name.toLowerCase() === key) {
|
|
100
|
-
removeSnippetAliases(registry, existing, canonicalNames);
|
|
101
|
-
}
|
|
102
|
-
registry.set(key, snippet);
|
|
103
|
-
canonicalNames.add(key);
|
|
104
|
-
for (const alias of snippet.aliases) {
|
|
105
|
-
const aliasKey = alias.toLowerCase();
|
|
106
|
-
if (SNIPPET_NAME_PATTERN.test(alias) && !canonicalNames.has(aliasKey)) registry.set(aliasKey, snippet);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function loadSnippetRegistry(workingDirectory) {
|
|
111
|
-
const registry = new Map();
|
|
112
|
-
const canonicalNames = new Set();
|
|
113
|
-
for (const { dir, source } of getLoadDirs(workingDirectory)) {
|
|
114
|
-
if (!fs.existsSync(dir)) continue;
|
|
115
|
-
for (const filename of fs.readdirSync(dir)) {
|
|
116
|
-
if (!filename.endsWith(SNIPPET_EXTENSION)) continue;
|
|
117
|
-
try {
|
|
118
|
-
const snippet = loadSnippetFile(dir, filename, source);
|
|
119
|
-
if (snippet) registerSnippet(registry, snippet, canonicalNames);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.warn(`[Snippets] Failed to load ${path.join(dir, filename)}:`, error);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return registry;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function listUniqueSnippets(registry) {
|
|
129
|
-
const seen = new Set();
|
|
130
|
-
const snippets = [];
|
|
131
|
-
for (const snippet of registry.values()) {
|
|
132
|
-
const key = `${snippet.source}:${snippet.filePath}`;
|
|
133
|
-
if (seen.has(key)) continue;
|
|
134
|
-
seen.add(key);
|
|
135
|
-
snippets.push(snippet);
|
|
136
|
-
}
|
|
137
|
-
return snippets.sort((a, b) => a.name.localeCompare(b.name));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getWritableSnippetDir(scope, workingDirectory) {
|
|
141
|
-
if (scope === 'project') {
|
|
142
|
-
if (!workingDirectory) throw new Error('Project directory is required for project snippets');
|
|
143
|
-
const preferred = path.join(workingDirectory, '.opencode', 'snippet');
|
|
144
|
-
const alternate = path.join(workingDirectory, '.opencode', 'snippets');
|
|
145
|
-
return fs.existsSync(alternate) && !fs.existsSync(preferred) ? alternate : preferred;
|
|
146
|
-
}
|
|
147
|
-
return fs.existsSync(GLOBAL_SNIPPET_DIR_ALT) && !fs.existsSync(GLOBAL_SNIPPET_DIR)
|
|
148
|
-
? GLOBAL_SNIPPET_DIR_ALT
|
|
149
|
-
: GLOBAL_SNIPPET_DIR;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function findSnippetByName(name, workingDirectory) {
|
|
153
|
-
assertValidSnippetName(name);
|
|
154
|
-
const registry = loadSnippetRegistry(workingDirectory);
|
|
155
|
-
return registry.get(name.toLowerCase()) ?? null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseSnippetBlocks(content) {
|
|
159
|
-
const blocks = { prepend: [], append: [] };
|
|
160
|
-
let inline = content;
|
|
161
|
-
for (const type of ['prepend', 'append']) {
|
|
162
|
-
const regex = new RegExp(`<${type}>([\\s\\S]*?)(?:<\\/${type}>|$)`, 'gi');
|
|
163
|
-
inline = inline.replace(regex, (_match, value) => {
|
|
164
|
-
const normalized = String(value).trim();
|
|
165
|
-
if (normalized) blocks[type].push(normalized);
|
|
166
|
-
return '';
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
inline = inline.replace(/<inject>[\s\S]*?(?:<\/inject>|$)/gi, '').trim();
|
|
170
|
-
return { inline, prepend: blocks.prepend, append: blocks.append };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function expandText(text, registry, expansionCounts, collector) {
|
|
174
|
-
let expanded = text;
|
|
175
|
-
let changed = true;
|
|
176
|
-
|
|
177
|
-
while (changed) {
|
|
178
|
-
const previous = expanded;
|
|
179
|
-
let loopDetected = false;
|
|
180
|
-
HASHTAG_PATTERN.lastIndex = 0;
|
|
181
|
-
|
|
182
|
-
expanded = expanded.replace(HASHTAG_PATTERN, (match, name, offset, input) => {
|
|
183
|
-
if (name.toLowerCase() === 'skill' && input[offset + match.length] === '(') return match;
|
|
184
|
-
const snippet = registry.get(name.toLowerCase());
|
|
185
|
-
if (!snippet) return match;
|
|
186
|
-
|
|
187
|
-
const key = snippet.name.toLowerCase();
|
|
188
|
-
const count = (expansionCounts.get(key) || 0) + 1;
|
|
189
|
-
if (count > MAX_EXPANSION_COUNT) {
|
|
190
|
-
loopDetected = true;
|
|
191
|
-
return match;
|
|
192
|
-
}
|
|
193
|
-
expansionCounts.set(key, count);
|
|
194
|
-
|
|
195
|
-
const parsed = parseSnippetBlocks(snippet.content);
|
|
196
|
-
for (const block of parsed.prepend) collector.prepend.push(expandText(block, registry, expansionCounts, collector));
|
|
197
|
-
for (const block of parsed.append) collector.append.push(expandText(block, registry, expansionCounts, collector));
|
|
198
|
-
return expandText(parsed.inline, registry, expansionCounts, collector);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
changed = expanded !== previous && !loopDetected;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return expanded;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function listSnippets(workingDirectory) {
|
|
208
|
-
return listUniqueSnippets(loadSnippetRegistry(workingDirectory));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function getSnippet(name, workingDirectory) {
|
|
212
|
-
return findSnippetByName(name, workingDirectory);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function createSnippet(name, config, workingDirectory, scope = 'global') {
|
|
216
|
-
assertValidSnippetName(name);
|
|
217
|
-
const dir = getWritableSnippetDir(scope, workingDirectory);
|
|
218
|
-
const filePath = path.join(dir, `${name}${SNIPPET_EXTENSION}`);
|
|
219
|
-
if (fs.existsSync(filePath)) throw new Error(`Snippet "${name}" already exists`);
|
|
220
|
-
writeMarkdownFile(filePath, config || {});
|
|
221
|
-
return getSnippet(name, workingDirectory);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function updateSnippet(name, updates, workingDirectory) {
|
|
225
|
-
const existing = findSnippetByName(name, workingDirectory);
|
|
226
|
-
if (!existing) throw new Error(`Snippet "${name}" not found`);
|
|
227
|
-
writeMarkdownFile(existing.filePath, { ...existing, ...(updates || {}) });
|
|
228
|
-
return getSnippet(name, workingDirectory);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function deleteSnippet(name, workingDirectory) {
|
|
232
|
-
const existing = findSnippetByName(name, workingDirectory);
|
|
233
|
-
if (!existing) throw new Error(`Snippet "${name}" not found`);
|
|
234
|
-
fs.unlinkSync(existing.filePath);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function expandSnippets(text, workingDirectory) {
|
|
238
|
-
const registry = loadSnippetRegistry(workingDirectory);
|
|
239
|
-
const collector = { prepend: [], append: [] };
|
|
240
|
-
const expanded = expandText(text || '', registry, new Map(), collector).trim();
|
|
241
|
-
return [...collector.prepend, expanded, ...collector.append].filter(Boolean).join('\n\n');
|
|
242
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
createSnippet,
|
|
7
|
-
deleteSnippet,
|
|
8
|
-
expandSnippets,
|
|
9
|
-
getSnippet,
|
|
10
|
-
listSnippets,
|
|
11
|
-
updateSnippet,
|
|
12
|
-
} from './snippets.js';
|
|
13
|
-
|
|
14
|
-
let projectDir;
|
|
15
|
-
|
|
16
|
-
function writeSnippet(relativePath, content) {
|
|
17
|
-
const filePath = path.join(projectDir, relativePath);
|
|
18
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
19
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('snippets', () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-snippets-'));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('loads project snippets with aliases and description', () => {
|
|
32
|
-
writeSnippet('.opencode/snippet/review.md', '---\naliases: [rev]\ndescription: Review helper\n---\nReview carefully.');
|
|
33
|
-
|
|
34
|
-
expect(listSnippets(projectDir)).toContainEqual(
|
|
35
|
-
expect.objectContaining({ name: 'review', aliases: ['rev'], description: 'Review helper', source: 'project' }),
|
|
36
|
-
);
|
|
37
|
-
expect(getSnippet('rev', projectDir)).toEqual(expect.objectContaining({ name: 'review' }));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('snippet directory wins over snippets directory', () => {
|
|
41
|
-
writeSnippet('.opencode/snippets/same.md', 'Old');
|
|
42
|
-
writeSnippet('.opencode/snippet/same.md', 'New');
|
|
43
|
-
|
|
44
|
-
expect(getSnippet('same', projectDir)?.content).toBe('New');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('canonical snippet names take precedence over colliding aliases', () => {
|
|
48
|
-
writeSnippet('.opencode/snippets/review.md', 'Review by name');
|
|
49
|
-
writeSnippet('.opencode/snippet/helper.md', '---\naliases: [review, help]\n---\nReview by alias');
|
|
50
|
-
|
|
51
|
-
expect(getSnippet('review', projectDir)).toEqual(expect.objectContaining({ name: 'review', content: 'Review by name' }));
|
|
52
|
-
expect(getSnippet('help', projectDir)).toEqual(expect.objectContaining({ name: 'helper', content: 'Review by alias' }));
|
|
53
|
-
expect(expandSnippets('Use #review and #help', projectDir)).toBe('Use Review by name and Review by alias');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('canonical snippet names override earlier same-directory aliases', () => {
|
|
57
|
-
writeSnippet('.opencode/snippet/alias-first.md', '---\naliases: [review, assist]\n---\nReview by alias');
|
|
58
|
-
writeSnippet('.opencode/snippet/review.md', 'Review by name');
|
|
59
|
-
|
|
60
|
-
const snippetDir = path.join(projectDir, '.opencode', 'snippet');
|
|
61
|
-
const originalReaddirSync = fs.readdirSync;
|
|
62
|
-
fs.readdirSync = function readdirSync(dir, ...args) {
|
|
63
|
-
if (dir === snippetDir) return ['alias-first.md', 'review.md'];
|
|
64
|
-
return originalReaddirSync.call(fs, dir, ...args);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
expect(getSnippet('review', projectDir)).toEqual(expect.objectContaining({ name: 'review', content: 'Review by name' }));
|
|
69
|
-
expect(getSnippet('assist', projectDir)).toEqual(expect.objectContaining({ name: 'alias-first', content: 'Review by alias' }));
|
|
70
|
-
expect(expandSnippets('Use #review and #assist', projectDir)).toBe('Use Review by name and Review by alias');
|
|
71
|
-
} finally {
|
|
72
|
-
fs.readdirSync = originalReaddirSync;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('creates updates and deletes snippets', () => {
|
|
77
|
-
expect(createSnippet('custom-one', { content: 'Body', aliases: ['co'] }, projectDir, 'project')).toEqual(
|
|
78
|
-
expect.objectContaining({ name: 'custom-one', content: 'Body', aliases: ['co'] }),
|
|
79
|
-
);
|
|
80
|
-
expect(updateSnippet('custom-one', { content: 'Updated' }, projectDir)).toEqual(
|
|
81
|
-
expect.objectContaining({ name: 'custom-one', content: 'Updated', aliases: ['co'] }),
|
|
82
|
-
);
|
|
83
|
-
deleteSnippet('custom-one', projectDir);
|
|
84
|
-
expect(getSnippet('custom-one', projectDir)).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('expands snippets recursively with prepend and append blocks', () => {
|
|
88
|
-
writeSnippet('.opencode/snippet/base.md', 'Base text');
|
|
89
|
-
writeSnippet('.opencode/snippet/review.md', '<prepend>Before</prepend>Review #base<append>After</append>');
|
|
90
|
-
|
|
91
|
-
expect(expandSnippets('Please #review', projectDir)).toBe('Before\n\nPlease Review Base text\n\nAfter');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('rejects invalid snippet names', () => {
|
|
95
|
-
expect(() => createSnippet('../bad', { content: '' }, projectDir, 'project')).toThrow('Snippet name');
|
|
96
|
-
});
|
|
97
|
-
});
|