@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
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 +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +33 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v2-client.js +14 -3
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +124 -240
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildSharedServiceStartEnv,
|
|
7
|
+
isSharedServiceEnabled,
|
|
8
|
+
parseSharedServiceRegistration,
|
|
9
|
+
readSharedServiceRegistration,
|
|
10
|
+
resolveSharedServiceRegistrationPath,
|
|
11
|
+
startSharedOpenCodeService,
|
|
12
|
+
} from './shared-service.js';
|
|
13
|
+
|
|
14
|
+
const fakeChild = () => {
|
|
15
|
+
const child = new EventEmitter();
|
|
16
|
+
child.stderr = new EventEmitter();
|
|
17
|
+
child.kill = vi.fn();
|
|
18
|
+
return child;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('shared OpenCode service registration', () => {
|
|
22
|
+
it('resolves the stable-channel registration under the XDG state root', () => {
|
|
23
|
+
expect(resolveSharedServiceRegistrationPath({}, '/home/u')).toBe(path.join('/home/u', '.local', 'state', 'opencode', 'service.json'));
|
|
24
|
+
expect(resolveSharedServiceRegistrationPath({ XDG_STATE_HOME: '/xdg' }, '/home/u')).toBe(path.join('/xdg', 'opencode', 'service.json'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('parses a registration and rejects malformed ones', () => {
|
|
28
|
+
expect(parseSharedServiceRegistration(JSON.stringify({
|
|
29
|
+
id: 'a', version: '2.0.12', url: 'http://127.0.0.1:49374', pid: 42, password: 'pw',
|
|
30
|
+
}))).toEqual({ origin: 'http://127.0.0.1:49374', port: 49374, pid: 42, password: 'pw', version: '2.0.12' });
|
|
31
|
+
expect(parseSharedServiceRegistration(JSON.stringify({ url: 'http://127.0.0.1:49374' }))?.password).toBeNull();
|
|
32
|
+
expect(parseSharedServiceRegistration('not json')).toBeNull();
|
|
33
|
+
expect(parseSharedServiceRegistration(JSON.stringify({ url: 'ftp://x:1' }))).toBeNull();
|
|
34
|
+
expect(parseSharedServiceRegistration(JSON.stringify({ url: 'http://127.0.0.1' }))).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('treats a missing registration file as absent, not as a service', async () => {
|
|
38
|
+
const fsLike = { readFile: vi.fn(async () => { throw Object.assign(new Error('nope'), { code: 'ENOENT' }); }) };
|
|
39
|
+
await expect(readSharedServiceRegistration({ file: '/x/service.json', fsLike })).resolves.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('is enabled by default and can be opted out', () => {
|
|
43
|
+
expect(isSharedServiceEnabled({})).toBe(true);
|
|
44
|
+
expect(isSharedServiceEnabled({ OPENCHAMBER_OPENCODE_SHARED_SERVICE: '0' })).toBe(false);
|
|
45
|
+
expect(isSharedServiceEnabled({ OPENCHAMBER_OPENCODE_SHARED_SERVICE: 'false' })).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('shared OpenCode service start', () => {
|
|
50
|
+
it('never hands OpenChamber credentials to the long-lived service', () => {
|
|
51
|
+
expect(buildSharedServiceStartEnv({
|
|
52
|
+
PATH: '/bin',
|
|
53
|
+
HOME: '/home/u',
|
|
54
|
+
OPENCODE_SERVER_PASSWORD: 'managed',
|
|
55
|
+
OPENCODE_PASSWORD: 'managed',
|
|
56
|
+
OPENCHAMBER_UI_PASSWORD: 'ui',
|
|
57
|
+
OPENCHAMBER_DATA_DIR: '/data',
|
|
58
|
+
})).toEqual({ PATH: '/bin', HOME: '/home/u' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('runs `service start` and resolves on a clean exit', async () => {
|
|
62
|
+
const child = fakeChild();
|
|
63
|
+
const spawnImpl = vi.fn(() => {
|
|
64
|
+
queueMicrotask(() => child.emit('exit', 0));
|
|
65
|
+
return child;
|
|
66
|
+
});
|
|
67
|
+
await startSharedOpenCodeService({ binary: '/bin/opencode', args: ['--x'], env: { PATH: '/bin' }, spawnImpl });
|
|
68
|
+
expect(spawnImpl).toHaveBeenCalledWith('/bin/opencode', ['--x', 'service', 'start'], expect.objectContaining({ env: { PATH: '/bin' } }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('surfaces the CLI error when `service start` fails', async () => {
|
|
72
|
+
const child = fakeChild();
|
|
73
|
+
const spawnImpl = vi.fn(() => {
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
child.stderr.emit('data', 'Background service failed to start');
|
|
76
|
+
child.emit('exit', 1);
|
|
77
|
+
});
|
|
78
|
+
return child;
|
|
79
|
+
});
|
|
80
|
+
await expect(startSharedOpenCodeService({ binary: 'opencode', env: {}, spawnImpl }))
|
|
81
|
+
.rejects.toThrow(/exited with code 1: Background service failed to start/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('replaces the registered service only for an explicit upgrade', async () => {
|
|
85
|
+
const child = fakeChild();
|
|
86
|
+
const spawnImpl = vi.fn(() => {
|
|
87
|
+
queueMicrotask(() => child.emit('exit', 0));
|
|
88
|
+
return child;
|
|
89
|
+
});
|
|
90
|
+
await startSharedOpenCodeService({ binary: '/cache/opencode', env: {}, replace: true, spawnImpl });
|
|
91
|
+
expect(spawnImpl).toHaveBeenCalledWith('/cache/opencode', ['service', 'restart'], expect.objectContaining({ windowsHide: true }));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -371,9 +371,9 @@ function addSkillFromMdFile(skillsMap, skillMdPath, scope, source) {
|
|
|
371
371
|
return;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
const name =
|
|
375
|
-
?
|
|
376
|
-
: '';
|
|
374
|
+
const name = path.basename(skillMdPath) === 'SKILL.md'
|
|
375
|
+
? path.basename(path.dirname(skillMdPath))
|
|
376
|
+
: path.basename(skillMdPath, '.md');
|
|
377
377
|
const description = typeof parsed.frontmatter?.description === 'string'
|
|
378
378
|
? parsed.frontmatter.description
|
|
379
379
|
: '';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { forceCloseHttpServerConnections } from './http-server-connections.js';
|
|
2
|
+
|
|
1
3
|
export const createGracefulShutdownRuntime = (dependencies) => {
|
|
2
4
|
const {
|
|
3
5
|
process,
|
|
@@ -22,7 +24,6 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
22
24
|
getOpenCodePort,
|
|
23
25
|
getOpenCodeProcess,
|
|
24
26
|
setOpenCodeProcess,
|
|
25
|
-
killProcessOnPort,
|
|
26
27
|
waitForPortRelease,
|
|
27
28
|
getServer,
|
|
28
29
|
getUiAuthController,
|
|
@@ -78,12 +79,14 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
if (!shouldSkipOpenCodeStop()) {
|
|
81
|
-
const
|
|
82
|
+
const portToRelease = getOpenCodePort();
|
|
82
83
|
const openCodeProcess = getOpenCodeProcess();
|
|
83
84
|
|
|
84
85
|
if (openCodeProcess) {
|
|
85
86
|
console.log('Stopping OpenCode process...');
|
|
86
87
|
try {
|
|
88
|
+
// close() owns SIGTERM→SIGKILL on this child handle. Never re-kill a
|
|
89
|
+
// numeric pid captured before close — it may already be recycled.
|
|
87
90
|
await openCodeProcess.close();
|
|
88
91
|
} catch (error) {
|
|
89
92
|
console.warn('Error closing OpenCode process:', error);
|
|
@@ -91,9 +94,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
91
94
|
setOpenCodeProcess(null);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
if (!(await waitForPortRelease(
|
|
96
|
-
console.warn(`Timed out waiting for OpenCode port ${
|
|
97
|
+
// Probe only after owned close. Do not mass-kill by port (clients / external).
|
|
98
|
+
if (!(await waitForPortRelease(portToRelease, 5000))) {
|
|
99
|
+
console.warn(`Timed out waiting for OpenCode port ${portToRelease} to be released during shutdown`);
|
|
97
100
|
}
|
|
98
101
|
} else {
|
|
99
102
|
console.log('Skipping OpenCode shutdown (external server)');
|
|
@@ -105,12 +108,18 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
105
108
|
try {
|
|
106
109
|
await Promise.race([
|
|
107
110
|
new Promise((resolve) => {
|
|
111
|
+
// Stop accepting first. closeAllConnections drops plain HTTP sockets
|
|
112
|
+
// but leaves upgraded WS/SSE; destroy tracked server-owned leftovers
|
|
113
|
+
// so the close callback can fire without waiting SHUTDOWN_TIMEOUT.
|
|
108
114
|
server.close(() => {
|
|
109
115
|
console.log('HTTP server closed');
|
|
110
116
|
resolve();
|
|
111
117
|
});
|
|
112
118
|
if (options.forceCloseConnections === true) {
|
|
113
|
-
server
|
|
119
|
+
const { destroyedTracked } = forceCloseHttpServerConnections(server);
|
|
120
|
+
if (destroyedTracked > 0) {
|
|
121
|
+
console.log(`Forced close of ${destroyedTracked} remaining server socket(s)`);
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
}),
|
|
116
125
|
new Promise((resolve) => {
|
|
@@ -22,7 +22,6 @@ const createRuntime = (server, shutdownTimeoutMs = 1000, overrides = {}) => crea
|
|
|
22
22
|
getOpenCodePort: () => null,
|
|
23
23
|
getOpenCodeProcess: () => null,
|
|
24
24
|
setOpenCodeProcess: vi.fn(),
|
|
25
|
-
killProcessOnPort: vi.fn(),
|
|
26
25
|
waitForPortRelease: vi.fn(async () => true),
|
|
27
26
|
getServer: () => server,
|
|
28
27
|
getUiAuthController: () => null,
|
|
@@ -96,4 +95,53 @@ describe('graceful shutdown runtime', () => {
|
|
|
96
95
|
expect(scheduledTasksRuntime.stop).toHaveBeenCalledOnce();
|
|
97
96
|
expect(runHistoryStore.close).not.toHaveBeenCalled();
|
|
98
97
|
});
|
|
98
|
+
|
|
99
|
+
it('awaits managed OpenCode close with zero extra kill, then probes port release for 5s', async () => {
|
|
100
|
+
const close = vi.fn(async () => undefined);
|
|
101
|
+
const waitForPortRelease = vi.fn(async (_port, timeoutMs) => {
|
|
102
|
+
expect(timeoutMs).toBe(5000);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
const setOpenCodeProcess = vi.fn();
|
|
106
|
+
const server = {
|
|
107
|
+
close: vi.fn((callback) => {
|
|
108
|
+
callback();
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const runtime = createRuntime(server, 1000, {
|
|
113
|
+
shouldSkipOpenCodeStop: () => false,
|
|
114
|
+
getOpenCodePort: () => 45678,
|
|
115
|
+
getOpenCodeProcess: () => ({ close, pid: 99 }),
|
|
116
|
+
setOpenCodeProcess,
|
|
117
|
+
waitForPortRelease,
|
|
118
|
+
});
|
|
119
|
+
await runtime.gracefulShutdown({ exitProcess: false });
|
|
120
|
+
|
|
121
|
+
expect(close).toHaveBeenCalledOnce();
|
|
122
|
+
expect(setOpenCodeProcess).toHaveBeenCalledWith(null);
|
|
123
|
+
expect(waitForPortRelease).toHaveBeenCalledWith(45678, 5000);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not kill anything when OpenCode close already finished and a later server stage fails', async () => {
|
|
127
|
+
const close = vi.fn(async () => undefined);
|
|
128
|
+
const waitForPortRelease = vi.fn(async () => true);
|
|
129
|
+
const server = {
|
|
130
|
+
close: vi.fn(() => {
|
|
131
|
+
throw new Error('server close blew up after opencode stop');
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const runtime = createRuntime(server, 1000, {
|
|
136
|
+
shouldSkipOpenCodeStop: () => false,
|
|
137
|
+
getOpenCodePort: () => 45678,
|
|
138
|
+
getOpenCodeProcess: () => ({ close, pid: 99 }),
|
|
139
|
+
setOpenCodeProcess: vi.fn(),
|
|
140
|
+
waitForPortRelease,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await expect(runtime.gracefulShutdown({ exitProcess: false })).rejects.toThrow(/server close blew up/);
|
|
144
|
+
expect(close).toHaveBeenCalledOnce();
|
|
145
|
+
expect(waitForPortRelease).toHaveBeenCalledWith(45678, 5000);
|
|
146
|
+
});
|
|
99
147
|
});
|
|
@@ -16,8 +16,6 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
16
16
|
readSettingsFromDisk,
|
|
17
17
|
sanitizeSkillCatalogs,
|
|
18
18
|
isUnsafeSkillRelativePath,
|
|
19
|
-
refreshOpenCodeAfterConfigChange,
|
|
20
|
-
clientReloadDelayMs,
|
|
21
19
|
buildOpenCodeUrl,
|
|
22
20
|
getOpenCodeAuthHeaders,
|
|
23
21
|
getOpenCodePort,
|
|
@@ -130,11 +128,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
130
128
|
const client = OpenCode.make({
|
|
131
129
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
132
130
|
headers: getOpenCodeAuthHeaders(),
|
|
133
|
-
fetch:
|
|
131
|
+
fetch: globalThis.fetch,
|
|
134
132
|
});
|
|
135
133
|
|
|
136
134
|
const response = await client.skill.list(
|
|
137
135
|
workingDirectory ? { location: { directory: workingDirectory } } : undefined,
|
|
136
|
+
{ signal: AbortSignal.timeout(8_000) },
|
|
138
137
|
);
|
|
139
138
|
const payload = response?.data;
|
|
140
139
|
if (!Array.isArray(payload)) {
|
|
@@ -143,8 +142,9 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
143
142
|
|
|
144
143
|
return payload
|
|
145
144
|
.map((item) => {
|
|
146
|
-
|
|
147
|
-
const
|
|
145
|
+
// OpenChamber's name is the invocation key, not the V2 display label.
|
|
146
|
+
const name = typeof item?.id === 'string' ? item.id : '';
|
|
147
|
+
const location = typeof item?.path === 'string' ? item.path : '';
|
|
148
148
|
const description = typeof item?.description === 'string' ? item.description : '';
|
|
149
149
|
const content = typeof item?.content === 'string' ? item.content : '';
|
|
150
150
|
if (!name || !location) {
|
|
@@ -461,19 +461,14 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
461
461
|
|
|
462
462
|
const installed = result.installed || [];
|
|
463
463
|
const skipped = result.skipped || [];
|
|
464
|
-
const requiresReload = installed.length > 0;
|
|
465
|
-
|
|
466
|
-
if (requiresReload) {
|
|
467
|
-
await refreshOpenCodeAfterConfigChange('skills install');
|
|
468
|
-
}
|
|
469
464
|
|
|
470
465
|
return res.json({
|
|
471
466
|
ok: true,
|
|
472
467
|
installed,
|
|
473
468
|
skipped,
|
|
474
|
-
requiresReload,
|
|
475
|
-
|
|
476
|
-
|
|
469
|
+
requiresReload: false,
|
|
470
|
+
application: 'watch',
|
|
471
|
+
message: installed.length > 0 ? 'Skills installed successfully.' : 'No skills were installed',
|
|
477
472
|
});
|
|
478
473
|
}
|
|
479
474
|
|
|
@@ -512,19 +507,14 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
512
507
|
|
|
513
508
|
const installed = result.installed || [];
|
|
514
509
|
const skipped = result.skipped || [];
|
|
515
|
-
const requiresReload = installed.length > 0;
|
|
516
|
-
|
|
517
|
-
if (requiresReload) {
|
|
518
|
-
await refreshOpenCodeAfterConfigChange('skills install');
|
|
519
|
-
}
|
|
520
510
|
|
|
521
511
|
res.json({
|
|
522
512
|
ok: true,
|
|
523
513
|
installed,
|
|
524
514
|
skipped,
|
|
525
|
-
requiresReload,
|
|
526
|
-
|
|
527
|
-
|
|
515
|
+
requiresReload: false,
|
|
516
|
+
application: 'watch',
|
|
517
|
+
message: installed.length > 0 ? 'Skills installed successfully.' : 'No skills were installed',
|
|
528
518
|
});
|
|
529
519
|
} catch (error) {
|
|
530
520
|
console.error('Failed to install skills:', error);
|
|
@@ -605,13 +595,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
605
595
|
console.log('[Server] Scope:', scope, 'Working directory:', directory);
|
|
606
596
|
|
|
607
597
|
createSkill(skillName, { ...config, source: skillSource }, directory, scope);
|
|
608
|
-
await refreshOpenCodeAfterConfigChange('skill creation');
|
|
609
598
|
|
|
610
599
|
res.json({
|
|
611
600
|
success: true,
|
|
612
|
-
requiresReload:
|
|
613
|
-
|
|
614
|
-
|
|
601
|
+
requiresReload: false,
|
|
602
|
+
application: 'watch',
|
|
603
|
+
message: `Skill ${skillName} created successfully.`,
|
|
615
604
|
});
|
|
616
605
|
} catch (error) {
|
|
617
606
|
console.error('Failed to create skill:', error);
|
|
@@ -632,13 +621,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
632
621
|
console.log('[Server] Working directory:', directory);
|
|
633
622
|
|
|
634
623
|
updateSkill(skillName, updates, directory, updates?.targetPath);
|
|
635
|
-
await refreshOpenCodeAfterConfigChange('skill update');
|
|
636
624
|
|
|
637
625
|
res.json({
|
|
638
626
|
success: true,
|
|
639
|
-
requiresReload:
|
|
640
|
-
|
|
641
|
-
|
|
627
|
+
requiresReload: false,
|
|
628
|
+
application: 'watch',
|
|
629
|
+
message: `Skill ${skillName} updated successfully.`,
|
|
642
630
|
});
|
|
643
631
|
} catch (error) {
|
|
644
632
|
console.error('[Server] Failed to update skill:', error);
|
|
@@ -724,13 +712,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
724
712
|
}
|
|
725
713
|
|
|
726
714
|
deleteSkill(skillName, directory);
|
|
727
|
-
await refreshOpenCodeAfterConfigChange('skill deletion');
|
|
728
715
|
|
|
729
716
|
res.json({
|
|
730
717
|
success: true,
|
|
731
|
-
requiresReload:
|
|
732
|
-
|
|
733
|
-
|
|
718
|
+
requiresReload: false,
|
|
719
|
+
application: 'watch',
|
|
720
|
+
message: `Skill ${skillName} deleted successfully.`,
|
|
734
721
|
});
|
|
735
722
|
} catch (error) {
|
|
736
723
|
console.error('Failed to delete skill:', error);
|
|
@@ -8,12 +8,45 @@ const { OpenCode } = await import('@opencode/client');
|
|
|
8
8
|
const { registerSkillRoutes } = await import('./skill-routes.js');
|
|
9
9
|
|
|
10
10
|
describe('skill summary route', () => {
|
|
11
|
+
it('preserves authentication and directory through the real V2 SDK transport', async () => {
|
|
12
|
+
const { OpenCode: RealOpenCode } = await vi.importActual('@opencode/client');
|
|
13
|
+
const upstreamRequests = [];
|
|
14
|
+
const originalFetch = globalThis.fetch;
|
|
15
|
+
globalThis.fetch = async (url, init) => {
|
|
16
|
+
upstreamRequests.push(new Request(url, init));
|
|
17
|
+
return Response.json({ location: { directory: '/repo' }, data: [{
|
|
18
|
+
id: 'agent-tracker-api', name: 'Runmark', path: '/skills/agent-tracker-api/SKILL.md',
|
|
19
|
+
description: 'Query Runmark API', content: 'Instructions',
|
|
20
|
+
}] });
|
|
21
|
+
};
|
|
22
|
+
OpenCode.make.mockImplementation(RealOpenCode.make);
|
|
23
|
+
try {
|
|
24
|
+
const app = express();
|
|
25
|
+
registerSkillRoutes(app, {
|
|
26
|
+
fs: { existsSync: () => false }, path: await import('node:path'), os: await import('node:os'),
|
|
27
|
+
resolveOptionalProjectDirectory: async () => ({ directory: '/repo' }),
|
|
28
|
+
buildOpenCodeUrl: () => 'http://opencode-upstream:4096/',
|
|
29
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic example' }), getOpenCodePort: () => 4096,
|
|
30
|
+
SKILL_SCOPE: { PROJECT: 'project', USER: 'user' }, discoverSkills: () => [], mergeDiscoveredSkills: (skills) => skills,
|
|
31
|
+
});
|
|
32
|
+
const response = await request(app).get('/api/config/skills?summary=true').expect(200);
|
|
33
|
+
expect(response.body.skills[0].name).toBe('agent-tracker-api');
|
|
34
|
+
expect(upstreamRequests).toHaveLength(1);
|
|
35
|
+
expect(upstreamRequests[0].headers.get('authorization')).toBe('Basic example');
|
|
36
|
+
expect(upstreamRequests[0].url).toContain(encodeURIComponent('/repo'));
|
|
37
|
+
} finally {
|
|
38
|
+
globalThis.fetch = originalFetch;
|
|
39
|
+
OpenCode.make.mockReset();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
11
43
|
it('returns compact normalized skill data without content or sources', async () => {
|
|
12
44
|
OpenCode.make.mockReturnValue({
|
|
13
45
|
skill: {
|
|
14
46
|
list: vi.fn(async () => ({ data: [{
|
|
15
|
-
|
|
16
|
-
|
|
47
|
+
id: 'skill',
|
|
48
|
+
name: 'Skill Display Name',
|
|
49
|
+
path: '/repo/.opencode/skills/skill/SKILL.md',
|
|
17
50
|
description: ` ${'😀'.repeat(161)}\nnext `,
|
|
18
51
|
content: 'secret skill content',
|
|
19
52
|
}] })),
|
|
@@ -3,8 +3,23 @@ import fsPromises from 'fs/promises';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { getSkillSources, mergeDiscoveredSkills } from './skills.js';
|
|
6
|
+
import { addSkillFromMdFile } from './shared.js';
|
|
6
7
|
|
|
7
8
|
describe('skills', () => {
|
|
9
|
+
it('uses the path-derived id even when frontmatter has a different display name', async () => {
|
|
10
|
+
const root = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skill-id-'));
|
|
11
|
+
try {
|
|
12
|
+
const dir = path.join(root, 'release');
|
|
13
|
+
await fsPromises.mkdir(dir);
|
|
14
|
+
const file = path.join(dir, 'SKILL.md');
|
|
15
|
+
await fsPromises.writeFile(file, '---\nname: Git Release\ndescription: Release notes\n---\nInstructions');
|
|
16
|
+
const skills = new Map();
|
|
17
|
+
addSkillFromMdFile(skills, file, 'project', 'opencode');
|
|
18
|
+
expect([...skills.keys()]).toEqual(['release']);
|
|
19
|
+
} finally {
|
|
20
|
+
await fsPromises.rm(root, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
8
23
|
it('merges locally discovered skills missing from OpenCode live discovery', () => {
|
|
9
24
|
const merged = mergeDiscoveredSkills(
|
|
10
25
|
[
|
|
@@ -23,6 +23,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
|
|
|
23
23
|
getOpenCodeAuthHeaders,
|
|
24
24
|
globalEventHub,
|
|
25
25
|
processForwardedEventPayload,
|
|
26
|
+
projectOutboundSessionPayload = null,
|
|
27
|
+
ensureSessionMetadataReady = null,
|
|
26
28
|
messageStreamWsClients,
|
|
27
29
|
triggerHealthCheck,
|
|
28
30
|
upstreamStallTimeoutMs,
|
|
@@ -83,6 +85,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
|
|
|
83
85
|
getOpenCodeAuthHeaders,
|
|
84
86
|
globalEventHub,
|
|
85
87
|
processForwardedEventPayload,
|
|
88
|
+
projectOutboundSessionPayload,
|
|
89
|
+
ensureSessionMetadataReady,
|
|
86
90
|
wsClients: messageStreamWsClients,
|
|
87
91
|
triggerHealthCheck,
|
|
88
92
|
upstreamStallTimeoutMs,
|
|
@@ -47,8 +47,10 @@ export const createStaticRoutesRuntime = (dependencies) => {
|
|
|
47
47
|
normalizePwaOrientation,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
// `root` limits send's dotfile check to `index.html`; an absolute path
|
|
51
|
+
// would 404 whenever the install lives under a dot directory (~/.bun, ~/.local).
|
|
50
52
|
app.get(/^(?!\/api|.*\.(js|css|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|map)).*$/, (_req, res) => {
|
|
51
|
-
res.sendFile(
|
|
53
|
+
res.sendFile('index.html', { root: distPath });
|
|
52
54
|
});
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
import express from 'express';
|
|
3
6
|
import request from 'supertest';
|
|
4
7
|
import { createStaticRoutesRuntime } from './static-routes-runtime.js';
|
|
@@ -57,4 +60,34 @@ describe('static routes runtime', () => {
|
|
|
57
60
|
expect(auth.body).not.toEqual({ ok: true, mode: 'api-only', message: 'OpenChamber is running in API-only mode' });
|
|
58
61
|
expect(health.body).not.toEqual({ ok: true, mode: 'api-only', message: 'OpenChamber is running in API-only mode' });
|
|
59
62
|
});
|
|
63
|
+
|
|
64
|
+
it('serves index.html for deep links when dist lives under a dot directory', async () => {
|
|
65
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-static-'));
|
|
66
|
+
const distPath = path.join(root, '.bun', 'install', 'dist');
|
|
67
|
+
fs.mkdirSync(distPath, { recursive: true });
|
|
68
|
+
fs.writeFileSync(path.join(distPath, 'index.html'), '<!doctype html><title>OpenChamber</title>');
|
|
69
|
+
try {
|
|
70
|
+
const app = express();
|
|
71
|
+
createStaticRoutesRuntime({
|
|
72
|
+
fs,
|
|
73
|
+
path,
|
|
74
|
+
process: { env: { OPENCHAMBER_DIST_DIR: distPath } },
|
|
75
|
+
__dirname: '/server',
|
|
76
|
+
express,
|
|
77
|
+
resolveProjectDirectory: () => '',
|
|
78
|
+
buildOpenCodeUrl: () => '',
|
|
79
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
80
|
+
readSettingsFromDiskMigrated: async () => ({}),
|
|
81
|
+
normalizePwaAppName: (value) => value,
|
|
82
|
+
normalizePwaOrientation: (value) => value,
|
|
83
|
+
}).registerStaticRoutes(app);
|
|
84
|
+
|
|
85
|
+
const response = await request(app).get('/session/ses_abc');
|
|
86
|
+
|
|
87
|
+
expect(response.status).toBe(200);
|
|
88
|
+
expect(response.text).toContain('<title>OpenChamber</title>');
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
60
93
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OpenCode } from '@opencode/client';
|
|
2
|
+
import { wrapFetchWithRuntimeContractGate } from './server-opencode-fetch.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {import('@opencode/client').OpenCodeClient} OpenCodeV2ClientRaw
|
|
@@ -19,15 +20,25 @@ import { OpenCode } from '@opencode/client';
|
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Create the single server-side client for official OpenCode v2 APIs.
|
|
23
|
+
* Fetch is always wrapped with runtime-contract write admission so Host
|
|
24
|
+
* background clients (queue / scheduled / assistants) cannot bypass the proxy gate.
|
|
22
25
|
*
|
|
23
|
-
* @param {{
|
|
26
|
+
* @param {{
|
|
27
|
+
* baseUrl: string,
|
|
28
|
+
* authHeaders?: Record<string, string>,
|
|
29
|
+
* fetchImpl?: typeof fetch,
|
|
30
|
+
* getRuntimeContract?: () => (object | null | undefined),
|
|
31
|
+
* }} input
|
|
24
32
|
* @returns {OpenCodeV2Client}
|
|
25
33
|
*/
|
|
26
|
-
export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl }) {
|
|
34
|
+
export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl, getRuntimeContract }) {
|
|
35
|
+
const gatedFetch = wrapFetchWithRuntimeContractGate(fetchImpl || fetch, {
|
|
36
|
+
...(typeof getRuntimeContract === 'function' ? { getRuntimeContract } : {}),
|
|
37
|
+
});
|
|
27
38
|
const client = OpenCode.make({
|
|
28
39
|
baseUrl: baseUrl.replace(/\/$/, ''),
|
|
29
40
|
...(authHeaders ? { headers: authHeaders } : {}),
|
|
30
|
-
|
|
41
|
+
fetch: gatedFetch,
|
|
31
42
|
});
|
|
32
43
|
|
|
33
44
|
const messageGet = client.session.message?.get?.bind(client.session.message);
|