@vantran-se/openchamber-web 2.0.3 → 2.0.5
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 +20 -28
- package/bin/cli.js +3 -1
- package/bin/cli.test.js +13 -4
- package/bin/lib/DOCUMENTATION.md +4 -1
- package/bin/lib/commands-serve.js +9 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DCAQTLMM.js} +1 -1
- package/dist/assets/DiffView-uzNpHBlV.js +4 -0
- package/dist/assets/FilesView-Bkq0sjNx.js +17 -0
- package/dist/assets/{GitView-DKIjpnu4.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-D3Azksju.js} +1 -1
- package/dist/assets/MarkdownImageGallery-BGY6tuiK.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DJmhQ37f.js +12 -0
- package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-Bqc-xuDe.js} +4 -4
- package/dist/assets/PluginPane-DQ0k41Q-.js +3 -0
- package/dist/assets/SettingsView-BAiJU5hJ.js +30 -0
- package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-CiuyzfSW.js} +1 -1
- package/dist/assets/changesTree-CePHbFcj.js +1 -0
- package/dist/assets/commitAndPush-DTKb6IwT.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-kM_zJNMt.js} +2 -2
- package/dist/assets/de-BMnfXm-G.js +10 -0
- package/dist/assets/es-DKEvCCL6.js +16 -0
- package/dist/assets/fr-D9fgE2xE.js +14 -0
- package/dist/assets/index-DUke1nme.css +1 -0
- package/dist/assets/{index-CL7ZQ94-.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-bt5VHvcd.js} +1 -1
- package/dist/assets/ja-BEXad-KW.js +16 -0
- package/dist/assets/ko-DGrQ0lEe.js +16 -0
- package/dist/assets/{main-quLSA7jc.js → main-BpLQbZC2.js} +2 -2
- package/dist/assets/main-CI9DtHJv.js +896 -0
- package/dist/assets/{markdown-shiki.worker-CahLvHIq.js → markdown-shiki.worker-Bg8SoAJJ.js} +8 -5
- package/dist/assets/markdownCore-DK2KK5RW.js +7 -0
- package/dist/assets/{miniChat-D-lJJibr.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-VTtphbW6.js → mobile-CnWKEYIP.js} +2 -2
- package/dist/assets/patchFileDiff-Cqn0C0az.js +13 -0
- package/dist/assets/pl-BlhkmR5b.js +16 -0
- package/dist/assets/pt-BR-Bs96Ew9I.js +16 -0
- package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-D9t0-9nV.js} +2 -2
- package/dist/assets/renderMobileApp-CW7fVs_s.js +25 -0
- package/dist/assets/runtimeEndpointReset-D0PcCb3K.js +27 -0
- package/dist/assets/scheduledTasksApi-Dahv3ws0.js +1 -0
- package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-C-kGcOpD.js} +1 -1
- package/dist/assets/tr-CehUK1M9.js +7 -0
- package/dist/assets/uk-Bno5WGNz.js +16 -0
- package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-oKF0MAwd.js} +2 -2
- package/dist/assets/vendor-opencode-client-DSZl7jSi.js +12 -0
- package/dist/assets/vendor-zod-BSoePkpP.js +39 -0
- package/dist/assets/{web-components-C583Pmfm.js → web-components-Bc1WPd4i.js} +1 -1
- package/dist/assets/zh-CN-DnAweIWo.js +16 -0
- package/dist/assets/zh-TW-BbBBK25p.js +16 -0
- package/dist/index.html +5 -5
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.d.ts +2 -0
- package/server/index.js +196 -52
- package/server/lib/agent-tool/DOCUMENTATION.md +5 -0
- package/server/lib/agent-tool/runtime.js +89 -51
- package/server/lib/agent-tool/runtime.test.js +17 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/global-hub.js +45 -7
- package/server/lib/event-stream/global-hub.test.js +128 -2
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- package/server/lib/event-stream/rebind.test.js +1 -0
- package/server/lib/event-stream/runtime.js +1 -2
- package/server/lib/event-stream/upstream-reader.js +3 -2
- package/server/lib/git/DOCUMENTATION.md +1 -1
- package/server/lib/git/service.js +22 -0
- package/server/lib/git/service.test.js +8 -0
- package/server/lib/guests/DOCUMENTATION.md +4 -3
- package/server/lib/guests/catalog.js +19 -1
- package/server/lib/guests/catalog.test.js +22 -0
- package/server/lib/guests/html-styles.js +5 -3
- package/server/lib/guests/html-styles.test.js +3 -1
- package/server/lib/guests/routes.js +11 -3
- package/server/lib/guests/service.js +5 -0
- package/server/lib/guests/surface.js +55 -9
- package/server/lib/guests/surface.test.js +56 -6
- package/server/lib/notifications/DOCUMENTATION.md +10 -0
- package/server/lib/notifications/emit-route.js +118 -0
- package/server/lib/notifications/emit-route.test.js +112 -0
- package/server/lib/openchamber-control/actions.js +16 -2
- package/server/lib/openchamber-control/service.js +23 -0
- package/server/lib/openchamber-control/service.test.js +31 -0
- package/server/lib/openchamber-sessions/fork-inheritance.js +103 -0
- package/server/lib/openchamber-sessions/fork-inheritance.test.js +69 -0
- package/server/lib/openchamber-sessions/routes.js +11 -0
- package/server/lib/openchamber-sessions/routes.test.js +18 -0
- package/server/lib/opencode/DOCUMENTATION.md +23 -23
- package/server/lib/opencode/bootstrap-runtime.js +14 -1
- package/server/lib/opencode/compatibility.js +6 -3
- package/server/lib/opencode/compatibility.test.js +8 -0
- package/server/lib/opencode/config-entity-v2.test.js +27 -0
- package/server/lib/opencode/config-paths.test.js +12 -0
- package/server/lib/opencode/config-v2.d.ts +2 -0
- package/server/lib/opencode/config-v2.js +42 -0
- package/server/lib/opencode/config-v2.test.js +17 -0
- package/server/lib/opencode/core-routes.js +20 -4
- package/server/lib/opencode/core-routes.test.js +41 -0
- package/server/lib/opencode/env-runtime.js +1 -8
- package/server/lib/opencode/feature-routes-runtime.js +2 -1
- package/server/lib/opencode/lifecycle.js +156 -2
- package/server/lib/opencode/lifecycle.test.js +255 -1
- package/server/lib/opencode/managed-config-file.js +18 -6
- package/server/lib/opencode/managed-config-file.test.js +23 -7
- package/server/lib/opencode/mcp.js +7 -2
- package/server/lib/opencode/network-runtime.js +16 -7
- package/server/lib/opencode/network-runtime.test.js +29 -1
- package/server/lib/opencode/npm-registry-config.d.ts +1 -0
- package/server/lib/opencode/npm-registry-config.js +123 -0
- package/server/lib/opencode/npm-registry-config.test.js +167 -0
- package/server/lib/opencode/npm-registry.js +5 -6
- package/server/lib/opencode/npm-registry.test.js +30 -0
- package/server/lib/opencode/openchamber-routes.js +29 -11
- package/server/lib/opencode/openchamber-routes.test.js +18 -0
- package/server/lib/opencode/project-directory-runtime.js +8 -0
- package/server/lib/opencode/project-directory-runtime.test.js +13 -0
- package/server/lib/opencode/provider-source-route.test.js +59 -0
- package/server/lib/opencode/providers.js +3 -0
- package/server/lib/opencode/providers.test.js +62 -0
- package/server/lib/opencode/proxy.js +53 -16
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -1
- package/server/lib/opencode/server-startup-runtime.js +25 -0
- package/server/lib/opencode/server-startup-runtime.test.js +67 -1
- package/server/lib/opencode/server-utils-runtime.js +5 -0
- package/server/lib/opencode/settings-helpers.js +10 -0
- package/server/lib/opencode/settings-helpers.test.js +2 -2
- package/server/lib/opencode/settings-registry.json +17 -0
- package/server/lib/opencode/settings-runtime.js +1 -0
- package/server/lib/opencode/shared-service-runtime.js +155 -0
- package/server/lib/opencode/shared-service-runtime.test.js +180 -0
- package/server/lib/opencode/shared.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +14 -5
- package/server/lib/opencode/shutdown-runtime.test.js +77 -2
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
- package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
- package/server/lib/opencode/upgrade-capability.js +0 -14
- package/server/lib/opencode/v1-migration-topup.js +21 -15
- package/server/lib/opencode/v1-migration-topup.test.js +14 -6
- package/server/lib/opencode/v2-install.d.ts +1 -0
- package/server/lib/opencode/v2-install.js +8 -7
- package/server/lib/opencode/v2-install.test.js +13 -4
- package/server/lib/opencode/watcher.js +3 -1
- package/server/lib/opencode/watcher.test.js +54 -0
- package/server/lib/opencode/websearch-config.js +13 -1
- package/server/lib/quota/providers/kimi.js +1 -1
- package/server/lib/realtime-proxy.js +5 -1
- package/server/lib/realtime-proxy.test.js +29 -0
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +11 -0
- package/server/lib/session-assist/DOCUMENTATION.md +17 -5
- package/server/lib/session-assist/runtime.js +19 -2
- package/server/lib/session-assist/runtime.test.js +8 -0
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/small-model/DOCUMENTATION.md +11 -10
- package/server/lib/small-model/index.js +27 -32
- package/server/lib/small-model/index.test.js +28 -16
- package/server/lib/spaces/DOCUMENTATION.md +404 -32
- package/server/lib/spaces/code-in-bait.js +145 -6
- package/server/lib/spaces/code-in.js +10 -150
- package/server/lib/spaces/code-in.test.js +7 -83
- package/server/lib/spaces/code-out.js +1819 -0
- package/server/lib/spaces/code-out.test.js +3945 -0
- package/server/lib/spaces/code-transfer.js +173 -0
- package/server/lib/spaces/command-stream.test.js +181 -0
- package/server/lib/spaces/dispatcher.js +670 -0
- package/server/lib/spaces/dispatcher.test.js +582 -0
- package/server/lib/spaces/gatekeeper-channel.js +61 -7
- package/server/lib/spaces/gatekeeper-channel.test.js +43 -7
- package/server/lib/spaces/gatekeeper-program.cjs +68 -14
- package/server/lib/spaces/gatekeeper-program.test.js +118 -3
- package/server/lib/spaces/hardening.js +20 -4
- package/server/lib/spaces/hardening.test.js +18 -3
- package/server/lib/spaces/host-git.js +7 -4
- package/server/lib/spaces/host-git.test.js +6 -2
- package/server/lib/spaces/host.js +310 -0
- package/server/lib/spaces/host.test.js +273 -0
- package/server/lib/spaces/journey.js +552 -0
- package/server/lib/spaces/journey.test.js +589 -0
- package/server/lib/spaces/layout.js +42 -8
- package/server/lib/spaces/manager.js +4 -3
- package/server/lib/spaces/manager.test.js +6 -0
- package/server/lib/spaces/places/code-out.docker.live.test.js +412 -0
- package/server/lib/spaces/places/contract-suite.js +36 -0
- package/server/lib/spaces/places/dispatcher.docker.live.test.js +296 -0
- package/server/lib/spaces/places/docker-live-support.js +27 -4
- package/server/lib/spaces/places/docker.js +33 -5
- package/server/lib/spaces/places/docker.test.js +42 -3
- package/server/lib/spaces/places/escape-suite.js +118 -14
- package/server/lib/spaces/places/fake-docker.js +3 -1
- package/server/lib/spaces/places/grants.docker.live.test.js +197 -0
- package/server/lib/spaces/places/memory-place.js +24 -1
- package/server/lib/spaces/places/registry.js +1 -1
- package/server/lib/spaces/places/server.docker.live.test.js +69 -33
- package/server/lib/spaces/places/sessions.docker.live.test.js +222 -0
- package/server/lib/spaces/routes.js +216 -0
- package/server/lib/spaces/routes.test.js +213 -0
- package/server/lib/spaces/run-command.js +261 -8
- package/server/lib/spaces/run-command.test.js +193 -3
- package/server/lib/spaces/space-events.js +153 -0
- package/server/lib/spaces/space-events.test.js +160 -0
- package/server/lib/spaces/space-opencode.js +52 -0
- package/server/lib/spaces/space-opencode.test.js +41 -0
- package/server/lib/spaces/space-records.js +150 -0
- package/server/lib/spaces/space-records.test.js +113 -0
- package/server/lib/spaces/space-sessions.js +210 -0
- package/server/lib/spaces/space-sessions.test.js +161 -0
- package/server/lib/spaces/websocket.js +329 -0
- package/server/lib/spaces/websocket.test.js +345 -0
- package/server/lib/ui-auth/ui-auth.js +8 -2
- package/server/lib/ui-auth/ui-auth.test.js +29 -0
- package/server/opencode-proxy.test.js +271 -0
- package/server/proxy-headers.js +8 -2
- package/server/proxy-headers.test.js +20 -0
- package/dist/assets/DiffView-CX3wAX2S.js +0 -4
- package/dist/assets/FilesView-OL3CON3r.js +0 -17
- package/dist/assets/GuestSurfacePane-B_cqX5I5.js +0 -1
- package/dist/assets/MarkdownImageGallery-DFiTFZgj.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DZm5sf1W.js +0 -12
- package/dist/assets/PluginPane-BibJit86.js +0 -3
- package/dist/assets/SettingsView-h-VpVO47.js +0 -29
- package/dist/assets/commitAndPush-BIPfOMXY.js +0 -1
- package/dist/assets/de-BqCNGROu.js +0 -10
- package/dist/assets/es-BTPpTRgN.js +0 -16
- package/dist/assets/fr-hxbzMdAg.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-_zLwwYiy.js +0 -16
- package/dist/assets/ko-CFn3Dzhc.js +0 -16
- package/dist/assets/main-BLJaug9I.js +0 -896
- package/dist/assets/markdownCore-DS9FdwrI.js +0 -5
- package/dist/assets/patchFileDiff-D9iBcTEi.js +0 -13
- package/dist/assets/pl-DFHE7xac.js +0 -16
- package/dist/assets/pt-BR-CUjZF179.js +0 -16
- package/dist/assets/renderMobileApp-CNQhdXsi.js +0 -25
- package/dist/assets/runtimeEndpointReset-DeaERc0M.js +0 -27
- package/dist/assets/scheduledTasksApi-BD6Vx9pu.js +0 -1
- package/dist/assets/tr-BB6sA_zH.js +0 -7
- package/dist/assets/uk-CB4o8g-o.js +0 -16
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +0 -12
- package/dist/assets/vendor-zod-Gea9uBlY.js +0 -39
- package/dist/assets/zh-CN-BRyDuWhc.js +0 -16
- package/dist/assets/zh-TW-BDGbWA1Q.js +0 -16
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { Service } from '@opencode/client/service';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import { isSupportedOpenCodeVersion, readOpenCodeInfo } from './compatibility.js';
|
|
9
|
+
|
|
10
|
+
const endpointSchema = z.object({
|
|
11
|
+
url: z.string(),
|
|
12
|
+
auth: z.object({
|
|
13
|
+
type: z.literal('basic'),
|
|
14
|
+
username: z.string(),
|
|
15
|
+
password: z.string(),
|
|
16
|
+
}).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const createConnectionError = (code, message, details = {}, cause) => {
|
|
20
|
+
const error = new Error(message, cause === undefined ? undefined : { cause });
|
|
21
|
+
error.code = code;
|
|
22
|
+
Object.assign(error, details);
|
|
23
|
+
return error;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const unavailableError = (message, cause) => createConnectionError(
|
|
27
|
+
'OPENCODE_SERVICE_UNAVAILABLE',
|
|
28
|
+
message,
|
|
29
|
+
{},
|
|
30
|
+
cause,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const normalizeEndpoint = (endpoint) => {
|
|
34
|
+
const parsed = endpointSchema.safeParse(endpoint);
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
throw unavailableError('OpenCode shared service did not return an endpoint');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let url;
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(parsed.data.url);
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
throw unavailableError('OpenCode shared service returned an invalid URL', cause);
|
|
44
|
+
}
|
|
45
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || !url.hostname) {
|
|
46
|
+
throw unavailableError('OpenCode shared service returned an unsupported URL');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { ...parsed.data, url: url.origin };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const defaultRegistrationFile = () => join(
|
|
53
|
+
process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'),
|
|
54
|
+
'opencode',
|
|
55
|
+
'service.json',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const registrationExists = async (options = {}) => {
|
|
59
|
+
try {
|
|
60
|
+
await access(options.file ?? defaultRegistrationFile());
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code === 'ENOENT') return false;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const createSharedOpenCodeServiceRuntime = ({
|
|
69
|
+
service = Service,
|
|
70
|
+
fetchImpl = fetch,
|
|
71
|
+
ensureOptions,
|
|
72
|
+
readInfo = readOpenCodeInfo,
|
|
73
|
+
isSupportedVersion = isSupportedOpenCodeVersion,
|
|
74
|
+
hasRegistration = registrationExists,
|
|
75
|
+
}) => {
|
|
76
|
+
let activeConnection = null;
|
|
77
|
+
|
|
78
|
+
const resolveEndpoint = async () => {
|
|
79
|
+
let discovered;
|
|
80
|
+
try {
|
|
81
|
+
discovered = await service.discover(ensureOptions);
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
throw unavailableError('Could not discover the OpenCode shared service', cause);
|
|
84
|
+
}
|
|
85
|
+
if (discovered) return normalizeEndpoint(discovered);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (await hasRegistration(ensureOptions)) {
|
|
89
|
+
throw unavailableError(
|
|
90
|
+
'An incompatible or unhealthy OpenCode shared service is already registered',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
if (cause?.code === 'OPENCODE_SERVICE_UNAVAILABLE') throw cause;
|
|
95
|
+
throw unavailableError('Could not inspect the OpenCode shared service registration', cause);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return normalizeEndpoint(await service.ensure(ensureOptions));
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (cause?.code === 'OPENCODE_SERVICE_UNAVAILABLE') throw cause;
|
|
101
|
+
throw unavailableError('Could not start the OpenCode shared service', cause);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const probe = async (endpoint) => {
|
|
106
|
+
let headers;
|
|
107
|
+
try {
|
|
108
|
+
headers = { ...(service.headers(endpoint) ?? {}) };
|
|
109
|
+
} catch (cause) {
|
|
110
|
+
throw unavailableError('Could not authenticate with the OpenCode shared service', cause);
|
|
111
|
+
}
|
|
112
|
+
let info;
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetchImpl(new URL('/api/info', endpoint.url), {
|
|
115
|
+
headers: { Accept: 'application/json', ...headers },
|
|
116
|
+
});
|
|
117
|
+
info = await readInfo(response);
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
throw unavailableError('Could not reach the OpenCode shared service', cause);
|
|
120
|
+
}
|
|
121
|
+
if (!info) {
|
|
122
|
+
throw unavailableError('OpenCode shared service did not return valid service information');
|
|
123
|
+
}
|
|
124
|
+
if (!isSupportedVersion(info.version)) {
|
|
125
|
+
throw createConnectionError(
|
|
126
|
+
'OPENCODE_INCOMPATIBLE',
|
|
127
|
+
`OpenCode shared service version ${info.version} is incompatible`,
|
|
128
|
+
{ foundVersion: info.version },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
kind: 'shared-local',
|
|
134
|
+
endpoint,
|
|
135
|
+
version: info.version,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const establishConnection = async () => {
|
|
140
|
+
const replacement = await probe(await resolveEndpoint());
|
|
141
|
+
activeConnection = replacement;
|
|
142
|
+
return replacement;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
connect: establishConnection,
|
|
147
|
+
recover: establishConnection,
|
|
148
|
+
getConnection: () => activeConnection,
|
|
149
|
+
getBaseUrl: () => activeConnection?.endpoint.url ?? null,
|
|
150
|
+
getHeaders: () => activeConnection ? { ...(service.headers(activeConnection.endpoint) ?? {}) } : {},
|
|
151
|
+
dispose: async () => {
|
|
152
|
+
activeConnection = null;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createSharedOpenCodeServiceRuntime } from './shared-service-runtime.js';
|
|
4
|
+
|
|
5
|
+
const endpointA = {
|
|
6
|
+
url: 'http://127.0.0.1:4096',
|
|
7
|
+
auth: { type: 'basic', username: 'opencode', password: 'original' },
|
|
8
|
+
};
|
|
9
|
+
const endpointB = {
|
|
10
|
+
url: 'http://127.0.0.1:5096',
|
|
11
|
+
auth: { type: 'basic', username: 'opencode', password: 'replacement' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const createRuntime = ({
|
|
15
|
+
discover = vi.fn().mockResolvedValue(endpointA),
|
|
16
|
+
ensure = vi.fn(),
|
|
17
|
+
stop = vi.fn(),
|
|
18
|
+
headers = vi.fn((endpoint) => endpoint.auth
|
|
19
|
+
? { authorization: `Basic ${endpoint.auth.password}` }
|
|
20
|
+
: undefined),
|
|
21
|
+
fetchImpl = vi.fn().mockResolvedValue(Response.json({ version: '2.0.15' })),
|
|
22
|
+
infoVersion,
|
|
23
|
+
readInfo = async (response) => infoVersion === undefined
|
|
24
|
+
? response.json()
|
|
25
|
+
: { version: infoVersion },
|
|
26
|
+
isSupportedVersion = (version) => version === '2.0.15',
|
|
27
|
+
ensureOptions = { command: ['opencode', 'serve', '--service'] },
|
|
28
|
+
hasRegistration = vi.fn().mockResolvedValue(false),
|
|
29
|
+
} = {}) => createSharedOpenCodeServiceRuntime({
|
|
30
|
+
service: { discover, ensure, stop, headers },
|
|
31
|
+
fetchImpl,
|
|
32
|
+
ensureOptions,
|
|
33
|
+
readInfo,
|
|
34
|
+
isSupportedVersion,
|
|
35
|
+
hasRegistration,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('shared OpenCode service runtime', () => {
|
|
39
|
+
it('uses a compatible discovered service without ensuring another one', async () => {
|
|
40
|
+
const discover = vi.fn().mockResolvedValue(endpointA);
|
|
41
|
+
const ensure = vi.fn();
|
|
42
|
+
const runtime = createRuntime({ discover, ensure, infoVersion: '2.0.15' });
|
|
43
|
+
|
|
44
|
+
await expect(runtime.connect()).resolves.toMatchObject({
|
|
45
|
+
kind: 'shared-local',
|
|
46
|
+
endpoint: endpointA,
|
|
47
|
+
version: '2.0.15',
|
|
48
|
+
});
|
|
49
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects an incompatible discovered service without replacing it', async () => {
|
|
53
|
+
const discover = vi.fn().mockResolvedValue(endpointA);
|
|
54
|
+
const ensure = vi.fn();
|
|
55
|
+
const runtime = createRuntime({ discover, ensure, infoVersion: '1.99.0' });
|
|
56
|
+
|
|
57
|
+
await expect(runtime.connect()).rejects.toMatchObject({
|
|
58
|
+
code: 'OPENCODE_INCOMPATIBLE',
|
|
59
|
+
foundVersion: '1.99.0',
|
|
60
|
+
});
|
|
61
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('refuses to ensure when discovery hides an existing incompatible or unhealthy registration', async () => {
|
|
65
|
+
const discover = vi.fn().mockResolvedValue(undefined);
|
|
66
|
+
const ensure = vi.fn();
|
|
67
|
+
const runtime = createRuntime({
|
|
68
|
+
discover,
|
|
69
|
+
ensure,
|
|
70
|
+
hasRegistration: vi.fn().mockResolvedValue(true),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await expect(runtime.connect()).rejects.toMatchObject({
|
|
74
|
+
code: 'OPENCODE_SERVICE_UNAVAILABLE',
|
|
75
|
+
});
|
|
76
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('ensures a service only when discovery returns no endpoint', async () => {
|
|
80
|
+
const discover = vi.fn().mockResolvedValue(undefined);
|
|
81
|
+
const ensure = vi.fn().mockResolvedValue(endpointA);
|
|
82
|
+
const runtime = createRuntime({ discover, ensure, infoVersion: '2.0.15' });
|
|
83
|
+
|
|
84
|
+
await expect(runtime.connect()).resolves.toMatchObject({ endpoint: endpointA });
|
|
85
|
+
expect(ensure).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('refreshes URL and auth when recovery discovers a replacement endpoint', async () => {
|
|
89
|
+
const discover = vi.fn()
|
|
90
|
+
.mockResolvedValueOnce(endpointA)
|
|
91
|
+
.mockResolvedValueOnce(endpointB);
|
|
92
|
+
const runtime = createRuntime({ discover, infoVersion: '2.0.15' });
|
|
93
|
+
|
|
94
|
+
await runtime.connect();
|
|
95
|
+
await runtime.recover();
|
|
96
|
+
|
|
97
|
+
expect(runtime.getBaseUrl()).toBe(endpointB.url);
|
|
98
|
+
expect(runtime.getHeaders()).toEqual({ authorization: 'Basic replacement' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('keeps the active connection when a replacement is incompatible', async () => {
|
|
102
|
+
const discover = vi.fn()
|
|
103
|
+
.mockResolvedValueOnce(endpointA)
|
|
104
|
+
.mockResolvedValueOnce(endpointB);
|
|
105
|
+
const readInfo = vi.fn()
|
|
106
|
+
.mockResolvedValueOnce({ version: '2.0.15' })
|
|
107
|
+
.mockResolvedValueOnce({ version: '1.99.0' });
|
|
108
|
+
const runtime = createRuntime({ discover, readInfo });
|
|
109
|
+
|
|
110
|
+
await runtime.connect();
|
|
111
|
+
await expect(runtime.recover()).rejects.toMatchObject({ code: 'OPENCODE_INCOMPATIBLE' });
|
|
112
|
+
|
|
113
|
+
expect(runtime.getBaseUrl()).toBe(endpointA.url);
|
|
114
|
+
expect(runtime.getHeaders()).toEqual({ authorization: 'Basic original' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('dispose does not stop the shared service', async () => {
|
|
118
|
+
const stop = vi.fn();
|
|
119
|
+
const runtime = createRuntime({ stop, infoVersion: '2.0.15' });
|
|
120
|
+
await runtime.connect();
|
|
121
|
+
await runtime.dispose();
|
|
122
|
+
expect(stop).not.toHaveBeenCalled();
|
|
123
|
+
expect(runtime.getConnection()).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it.each([
|
|
127
|
+
[undefined],
|
|
128
|
+
[{}],
|
|
129
|
+
[{ url: '' }],
|
|
130
|
+
[{ url: 'file:///tmp/opencode.sock' }],
|
|
131
|
+
[{ url: 'not a url' }],
|
|
132
|
+
])('rejects a malformed endpoint (%j)', async (endpoint) => {
|
|
133
|
+
const runtime = createRuntime({ discover: vi.fn().mockResolvedValue(endpoint) });
|
|
134
|
+
|
|
135
|
+
await expect(runtime.connect()).rejects.toMatchObject({ code: 'OPENCODE_SERVICE_UNAVAILABLE' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('reports an authenticated service as unavailable when /api/info returns 401', async () => {
|
|
139
|
+
const runtime = createRuntime({
|
|
140
|
+
fetchImpl: vi.fn().mockResolvedValue(new Response(null, { status: 401 })),
|
|
141
|
+
readInfo: async () => null,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await expect(runtime.connect()).rejects.toMatchObject({ code: 'OPENCODE_SERVICE_UNAVAILABLE' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('reports discovery failures as unavailable without ensuring a replacement', async () => {
|
|
148
|
+
const ensure = vi.fn();
|
|
149
|
+
const runtime = createRuntime({
|
|
150
|
+
discover: vi.fn().mockRejectedValue(new Error('registration unavailable')),
|
|
151
|
+
ensure,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await expect(runtime.connect()).rejects.toMatchObject({ code: 'OPENCODE_SERVICE_UNAVAILABLE' });
|
|
155
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('reports an unreachable discovered endpoint as unavailable without ensuring a replacement', async () => {
|
|
159
|
+
const ensure = vi.fn();
|
|
160
|
+
const runtime = createRuntime({
|
|
161
|
+
ensure,
|
|
162
|
+
fetchImpl: vi.fn().mockRejectedValue(new Error('connection refused')),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expect(runtime.connect()).rejects.toMatchObject({ code: 'OPENCODE_SERVICE_UNAVAILABLE' });
|
|
166
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('uses empty headers when Service.headers returns undefined', async () => {
|
|
170
|
+
const fetchImpl = vi.fn().mockResolvedValue(Response.json({ version: '2.0.15' }));
|
|
171
|
+
const runtime = createRuntime({ headers: vi.fn().mockReturnValue(undefined), fetchImpl });
|
|
172
|
+
|
|
173
|
+
await runtime.connect();
|
|
174
|
+
|
|
175
|
+
expect(runtime.getHeaders()).toEqual({});
|
|
176
|
+
expect(fetchImpl).toHaveBeenCalledWith(new URL(`${endpointA.url}/api/info`), {
|
|
177
|
+
headers: { Accept: 'application/json' },
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -280,11 +280,16 @@ function readConfigLayer(filePath) {
|
|
|
280
280
|
function readConfigLayers(workingDirectory) {
|
|
281
281
|
const { userPaths, projectPath, customPath } = getConfigPaths(workingDirectory);
|
|
282
282
|
const userPath = getPrimaryUserConfigPath(userPaths);
|
|
283
|
+
// OpenCode loads every global config file in order, so an `opencode.jsonc`
|
|
284
|
+
// next to `opencode.json` overrides it. New entries still go to the primary
|
|
285
|
+
// file; entries found in the override are edited where they live.
|
|
286
|
+
const userOverridePath = userPaths.find((candidate) => candidate !== userPath && fs.existsSync(candidate)) ?? null;
|
|
283
287
|
const userLayer = readConfigLayer(userPath);
|
|
288
|
+
const userOverrideLayer = readConfigLayer(userOverridePath);
|
|
284
289
|
const projectLayer = readConfigLayer(projectPath);
|
|
285
290
|
const customLayer = readConfigLayer(customPath);
|
|
286
291
|
const mergedConfig = mergeConfigs(
|
|
287
|
-
mergeConfigs(userLayer.config, projectLayer.config),
|
|
292
|
+
mergeConfigs(mergeConfigs(userLayer.config, userOverrideLayer.config), projectLayer.config),
|
|
288
293
|
customLayer.config,
|
|
289
294
|
);
|
|
290
295
|
|
|
@@ -292,6 +297,9 @@ function readConfigLayers(workingDirectory) {
|
|
|
292
297
|
if (userLayer.error) {
|
|
293
298
|
layerErrors.push({ path: userPath, code: userLayer.error.code, message: userLayer.error.message });
|
|
294
299
|
}
|
|
300
|
+
if (userOverrideLayer.error && userOverridePath) {
|
|
301
|
+
layerErrors.push({ path: userOverridePath, code: userOverrideLayer.error.code, message: userOverrideLayer.error.message });
|
|
302
|
+
}
|
|
295
303
|
if (projectLayer.error && projectPath) {
|
|
296
304
|
layerErrors.push({ path: projectPath, code: projectLayer.error.code, message: projectLayer.error.message });
|
|
297
305
|
}
|
|
@@ -301,10 +309,11 @@ function readConfigLayers(workingDirectory) {
|
|
|
301
309
|
|
|
302
310
|
return {
|
|
303
311
|
userConfig: userLayer.config,
|
|
312
|
+
userOverrideConfig: userOverrideLayer.config,
|
|
304
313
|
projectConfig: projectLayer.config,
|
|
305
314
|
customConfig: customLayer.config,
|
|
306
315
|
mergedConfig,
|
|
307
|
-
paths: { userPath, projectPath, customPath },
|
|
316
|
+
paths: { userPath, userOverridePath, projectPath, customPath },
|
|
308
317
|
layerErrors,
|
|
309
318
|
};
|
|
310
319
|
}
|
|
@@ -407,6 +416,12 @@ function getJsonEntrySource(layers, sectionKind, entryName) {
|
|
|
407
416
|
if (project) return project;
|
|
408
417
|
}
|
|
409
418
|
|
|
419
|
+
if (paths.userOverridePath) {
|
|
420
|
+
throwIfLayerError(layers, paths.userOverridePath);
|
|
421
|
+
const userOverride = found(layers.userOverrideConfig, paths.userOverridePath);
|
|
422
|
+
if (userOverride) return userOverride;
|
|
423
|
+
}
|
|
424
|
+
|
|
410
425
|
throwIfLayerError(layers, paths.userPath);
|
|
411
426
|
const user = found(userConfig, paths.userPath);
|
|
412
427
|
if (user) return user;
|
|
@@ -509,7 +524,20 @@ function walkSkillMdFiles(rootDir) {
|
|
|
509
524
|
if (!rootDir || !fs.existsSync(rootDir)) return [];
|
|
510
525
|
|
|
511
526
|
const results = [];
|
|
527
|
+
// Real paths of the directories on the current walk path. Links (symlinks and
|
|
528
|
+
// Windows junctions) are followed at any depth; a link back to one of its own
|
|
529
|
+
// ancestors is skipped instead of recursing forever. Two links to the same
|
|
530
|
+
// target elsewhere in the tree are both walked, as the top level always did.
|
|
531
|
+
const ancestors = new Set();
|
|
512
532
|
const walk = (dir) => {
|
|
533
|
+
let realDir;
|
|
534
|
+
try {
|
|
535
|
+
realDir = fs.realpathSync(dir);
|
|
536
|
+
} catch {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (ancestors.has(realDir)) return;
|
|
540
|
+
|
|
513
541
|
let entries = [];
|
|
514
542
|
try {
|
|
515
543
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -517,16 +545,33 @@ function walkSkillMdFiles(rootDir) {
|
|
|
517
545
|
return;
|
|
518
546
|
}
|
|
519
547
|
|
|
548
|
+
ancestors.add(realDir);
|
|
549
|
+
|
|
520
550
|
for (const entry of entries) {
|
|
521
551
|
const fullPath = path.join(dir, entry.name);
|
|
522
|
-
|
|
552
|
+
// Junctions report as links, not directories. A link whose target cannot be
|
|
553
|
+
// stat'ed is skipped, the way an unreadable directory is, instead of failing
|
|
554
|
+
// the whole scan.
|
|
555
|
+
let isDirectoryEntry = entry.isDirectory();
|
|
556
|
+
let isFileEntry = entry.isFile();
|
|
557
|
+
if (entry.isSymbolicLink()) {
|
|
558
|
+
try {
|
|
559
|
+
const target = fs.statSync(fullPath);
|
|
560
|
+
isDirectoryEntry = target.isDirectory();
|
|
561
|
+
isFileEntry = target.isFile();
|
|
562
|
+
} catch {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (isDirectoryEntry) {
|
|
523
567
|
walk(fullPath);
|
|
524
568
|
continue;
|
|
525
569
|
}
|
|
526
|
-
if (
|
|
570
|
+
if (isFileEntry && entry.name === 'SKILL.md') {
|
|
527
571
|
results.push(fullPath);
|
|
528
572
|
}
|
|
529
573
|
}
|
|
574
|
+
ancestors.delete(realDir);
|
|
530
575
|
};
|
|
531
576
|
|
|
532
577
|
walk(rootDir);
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
|
-
import { parseMdFile, writeMdFile, readConfigFile, readConfigLayers, writeConfig } from './shared.js';
|
|
6
|
+
import { parseMdFile, writeMdFile, readConfigFile, readConfigLayers, writeConfig, walkSkillMdFiles } from './shared.js';
|
|
7
7
|
import { updateAgent } from './agents.js';
|
|
8
8
|
import { updateMcpConfig } from './mcp.js';
|
|
9
9
|
|
|
@@ -388,3 +388,80 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
|
|
|
388
388
|
}
|
|
389
389
|
});
|
|
390
390
|
});
|
|
391
|
+
|
|
392
|
+
describe('walkSkillMdFiles', () => {
|
|
393
|
+
beforeEach(() => {
|
|
394
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
395
|
+
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
afterEach(() => {
|
|
399
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const writeSkill = (relativeDir) => writeFixture(path.join(relativeDir, 'SKILL.md'), STANDARD_MD);
|
|
403
|
+
// A junction needs no elevation on Windows, which is what Skills Manager deploys;
|
|
404
|
+
// the type argument is ignored on POSIX.
|
|
405
|
+
const linkDirectory = (target, relativeLink) =>
|
|
406
|
+
fs.symlinkSync(target, path.join(FIXTURE_DIR, relativeLink), process.platform === 'win32' ? 'junction' : 'dir');
|
|
407
|
+
const foundRelative = () =>
|
|
408
|
+
walkSkillMdFiles(FIXTURE_DIR)
|
|
409
|
+
.map((found) => path.relative(FIXTURE_DIR, found).split(path.sep).join('/'))
|
|
410
|
+
.sort();
|
|
411
|
+
|
|
412
|
+
it('walks a skill deployed as a link inside the scanned root', () => {
|
|
413
|
+
writeSkill('real-skill');
|
|
414
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'collection'), { recursive: true });
|
|
415
|
+
writeSkill(path.join('collection', 'nested-skill'));
|
|
416
|
+
linkDirectory(path.join(FIXTURE_DIR, 'real-skill'), 'linked-skill');
|
|
417
|
+
linkDirectory(path.join(FIXTURE_DIR, 'collection'), 'collection-link');
|
|
418
|
+
|
|
419
|
+
expect(foundRelative()).toEqual([
|
|
420
|
+
'collection-link/nested-skill/SKILL.md',
|
|
421
|
+
'collection/nested-skill/SKILL.md',
|
|
422
|
+
'linked-skill/SKILL.md',
|
|
423
|
+
'real-skill/SKILL.md',
|
|
424
|
+
]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('ends on a link that points back into the scanned tree', () => {
|
|
428
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'loopdir'), { recursive: true });
|
|
429
|
+
writeSkill('loopdir');
|
|
430
|
+
linkDirectory(FIXTURE_DIR, path.join('loopdir', 'back'));
|
|
431
|
+
|
|
432
|
+
expect(foundRelative()).toEqual(['loopdir/SKILL.md']);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('skips a link whose target is gone and keeps the rest of the scan', () => {
|
|
436
|
+
writeSkill('real-skill');
|
|
437
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'gone-skill'), { recursive: true });
|
|
438
|
+
linkDirectory(path.join(FIXTURE_DIR, 'gone-skill'), 'dangling-link');
|
|
439
|
+
fs.rmSync(path.join(FIXTURE_DIR, 'gone-skill'), { recursive: true, force: true });
|
|
440
|
+
|
|
441
|
+
expect(foundRelative()).toEqual(['real-skill/SKILL.md']);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('follows a link nested below the top level of the scanned root', () => {
|
|
445
|
+
const outside = `${FIXTURE_DIR}-outside`;
|
|
446
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
447
|
+
fs.mkdirSync(path.join(outside, 'deep-skill'), { recursive: true });
|
|
448
|
+
fs.writeFileSync(path.join(outside, 'deep-skill', 'SKILL.md'), STANDARD_MD);
|
|
449
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'group'), { recursive: true });
|
|
450
|
+
try {
|
|
451
|
+
linkDirectory(path.join(outside, 'deep-skill'), path.join('group', 'linked'));
|
|
452
|
+
expect(foundRelative()).toEqual(['group/linked/SKILL.md']);
|
|
453
|
+
} finally {
|
|
454
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('ends on a nested link loop between two directories', () => {
|
|
459
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'a'), { recursive: true });
|
|
460
|
+
fs.mkdirSync(path.join(FIXTURE_DIR, 'b'), { recursive: true });
|
|
461
|
+
writeSkill('a');
|
|
462
|
+
linkDirectory(path.join(FIXTURE_DIR, 'b'), path.join('a', 'to-b'));
|
|
463
|
+
linkDirectory(path.join(FIXTURE_DIR, 'a'), path.join('b', 'to-a'));
|
|
464
|
+
|
|
465
|
+
expect(foundRelative()).toEqual(['a/SKILL.md', 'b/to-a/SKILL.md']);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -13,18 +13,20 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
13
13
|
contextObligatoryRuntime,
|
|
14
14
|
messageQueueRuntime,
|
|
15
15
|
scheduledTasksRuntime,
|
|
16
|
+
globalEventHub,
|
|
16
17
|
getHealthCheckInterval,
|
|
17
18
|
clearHealthCheckInterval,
|
|
18
19
|
getTerminalRuntime,
|
|
19
20
|
setTerminalRuntime,
|
|
20
21
|
getMessageStreamRuntime,
|
|
21
22
|
setMessageStreamRuntime,
|
|
22
|
-
|
|
23
|
+
getOpenCodeConnectionKind,
|
|
23
24
|
getOpenCodePort,
|
|
24
25
|
getOpenCodeProcess,
|
|
25
26
|
setOpenCodeProcess,
|
|
26
27
|
killProcessOnPort,
|
|
27
28
|
waitForPortRelease,
|
|
29
|
+
disposeSharedOpenCodeService,
|
|
28
30
|
getServer,
|
|
29
31
|
getUiAuthController,
|
|
30
32
|
setUiAuthController,
|
|
@@ -38,6 +40,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
38
40
|
getDictationRuntime,
|
|
39
41
|
getRelayService,
|
|
40
42
|
getRelayReconcileTimer,
|
|
43
|
+
getSpacesHost = () => null,
|
|
41
44
|
} = dependencies;
|
|
42
45
|
|
|
43
46
|
let shutdownPromise = null;
|
|
@@ -66,7 +69,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
66
69
|
beginGuestServiceShutdown();
|
|
67
70
|
syncToHmrState();
|
|
68
71
|
console.log('Starting graceful shutdown...');
|
|
69
|
-
const exitProcess =
|
|
72
|
+
const exitProcess = options.exitProcess === true || options.exitProcess === false
|
|
73
|
+
? options.exitProcess
|
|
74
|
+
: getExitOnShutdown();
|
|
70
75
|
|
|
71
76
|
// Both embedded stop() and daemon exits use this sequence. Close admission
|
|
72
77
|
// synchronously above, then stop viewers before draining their services.
|
|
@@ -74,9 +79,12 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
74
79
|
() => clearInterval(getRelayReconcileTimer()),
|
|
75
80
|
() => getGuestSurfaceRuntime()?.stop(),
|
|
76
81
|
() => getRealtimeProxyRuntime()?.stop(),
|
|
82
|
+
// The isolated-spaces host, when the switch is on: its connections into spaces end here.
|
|
83
|
+
() => getSpacesHost()?.close(),
|
|
77
84
|
() => getRelayService()?.stop(),
|
|
78
85
|
() => getDictationRuntime()?.stop(),
|
|
79
86
|
() => openCodeWatcherRuntime.stop(),
|
|
87
|
+
() => globalEventHub.stop(),
|
|
80
88
|
() => sessionRuntime.dispose(),
|
|
81
89
|
() => sessionAssistRuntime?.stop?.(),
|
|
82
90
|
() => sessionGoalRuntime?.stop?.(),
|
|
@@ -118,7 +126,8 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
const connectionKind = getOpenCodeConnectionKind();
|
|
130
|
+
if (connectionKind === 'managed-owned') {
|
|
122
131
|
const portToKill = getOpenCodePort();
|
|
123
132
|
const openCodeProcess = getOpenCodeProcess();
|
|
124
133
|
|
|
@@ -136,8 +145,8 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
136
145
|
if (!(await waitForPortRelease(portToKill, 5000))) {
|
|
137
146
|
console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released during shutdown`);
|
|
138
147
|
}
|
|
139
|
-
} else {
|
|
140
|
-
|
|
148
|
+
} else if (connectionKind === 'shared-local') {
|
|
149
|
+
await disposeSharedOpenCodeService();
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
const server = getServer();
|