@vantran-se/openchamber-web 2.0.4 → 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/dist/assets/{CodeMirrorCommentWidgets-DZF8ntaq.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-DwQwqDvu.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-CoXM0gh_.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-Cq_27v0y.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-CsSSRMhn.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-ClatMQAX.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff-DOwlrIK9.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CrcGqqD_.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-BGLsMgQk.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-B0BAy-f9.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-fcdDzCSW.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-B7xaUz2H.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-CkWCaUmZ.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-DONC4eI2.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-CXDImOXW.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-Br2TLSlb.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-_-20umCs.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-OKDYjcMF.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D77M6q1f.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-C54B4Mns.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-CGEvLMd2.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-BSxkRQLj.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-C5KRTNOB.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-CnkAQV6o.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-4O0ZM14D.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-yNq7b0DZ.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-Bzyszv2N.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-zOf6ORAq.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-DCaZ4BWi.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-6cEFPNUR.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-BEo4-ISu.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.js +82 -0
- package/server/lib/agent-tool/DOCUMENTATION.md +132 -47
- package/server/lib/agent-tool/runtime.js +34 -15
- package/server/lib/agent-tool/runtime.test.js +15 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/global-hub.js +23 -1
- package/server/lib/event-stream/global-hub.test.js +35 -0
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- 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/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 +30 -2
- package/server/lib/opencode/lifecycle.test.js +27 -0
- 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/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 +38 -2
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -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.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- 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/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 +113 -0
- package/dist/assets/DiffView-D2p4Ar0H.js +0 -4
- package/dist/assets/FilesView-BeTtbsip.js +0 -17
- package/dist/assets/GuestSurfacePane-DIJvkkM-.js +0 -1
- package/dist/assets/MarkdownImageGallery-CY0dWTaE.js +0 -1
- package/dist/assets/MarkdownRendererImpl-ILdXzLrt.js +0 -12
- package/dist/assets/PluginPane-B39JEUL8.js +0 -3
- package/dist/assets/SettingsView-13B2_G-A.js +0 -29
- package/dist/assets/commitAndPush-CcI4ol7c.js +0 -1
- package/dist/assets/de-BtYnUgII.js +0 -10
- package/dist/assets/es-crP5qPye.js +0 -16
- package/dist/assets/fr-CLSRuALk.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-SZ1-ucLV.js +0 -16
- package/dist/assets/ko-HthbT_Ve.js +0 -16
- package/dist/assets/main-BJklC7Cl.js +0 -896
- package/dist/assets/markdownCore-BzQuETm0.js +0 -5
- package/dist/assets/patchFileDiff-DGw2KryM.js +0 -13
- package/dist/assets/pl-C8eYfjL1.js +0 -16
- package/dist/assets/pt-BR-cYOrZdxS.js +0 -16
- package/dist/assets/renderMobileApp-CMxjwisj.js +0 -25
- package/dist/assets/runtimeEndpointReset-C45jrFq4.js +0 -27
- package/dist/assets/scheduledTasksApi-CkK1F9s2.js +0 -1
- package/dist/assets/tr-B4IQ5t-T.js +0 -7
- package/dist/assets/uk-DCoXG6ve.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-BXBoxsFk.js +0 -16
- package/dist/assets/zh-TW-C6oelpnL.js +0 -16
|
@@ -2,6 +2,17 @@ import { appendManagedPlugin } from './managed-plugin-config.js';
|
|
|
2
2
|
|
|
3
3
|
export const MANAGED_CONFIG_FILE_NAME = 'opencode.managed.json';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Built-in OpenCode plugins OpenChamber switches off for its managed child.
|
|
7
|
+
*
|
|
8
|
+
* `opencode.browser` only works when OpenCode's own desktop app attaches a
|
|
9
|
+
* browser to the session; OpenChamber does not implement that protocol, so its
|
|
10
|
+
* tools always fail with `browser.disconnected` and agents conclude browsing is
|
|
11
|
+
* unavailable instead of using `openchamber_web`. A project config that lists
|
|
12
|
+
* `opencode.browser` still re-enables it, because that layer sits above ours.
|
|
13
|
+
*/
|
|
14
|
+
const DISABLED_BUILTIN_PLUGINS = ['-opencode.browser'];
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* The managed OpenCode config layer.
|
|
7
18
|
*
|
|
@@ -12,7 +23,8 @@ export const MANAGED_CONFIG_FILE_NAME = 'opencode.managed.json';
|
|
|
12
23
|
* process — where `OPENCODE_CONFIG_CONTENT` (an environment variable) could
|
|
13
24
|
* only ever change across a restart.
|
|
14
25
|
*
|
|
15
|
-
* The file is OpenChamber-owned and holds nothing but `plugins
|
|
26
|
+
* The file is OpenChamber-owned and holds nothing but `plugins`: the built-in
|
|
27
|
+
* plugins OpenChamber disables, then its own plugin directories. It sits above
|
|
16
28
|
* the user's global `opencode.json` and below their project config, so it never
|
|
17
29
|
* shadows a project-level choice.
|
|
18
30
|
*
|
|
@@ -46,7 +58,7 @@ export const createManagedConfigRuntime = ({
|
|
|
46
58
|
|
|
47
59
|
const writeConfigFile = async (pluginDirectories) => {
|
|
48
60
|
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
49
|
-
const text = `${JSON.stringify({ plugins: pluginDirectories }, null, 2)}\n`;
|
|
61
|
+
const text = `${JSON.stringify({ plugins: [...DISABLED_BUILTIN_PLUGINS, ...pluginDirectories] }, null, 2)}\n`;
|
|
50
62
|
// Temp + rename: OpenCode reacts to the write immediately, and a partial
|
|
51
63
|
// read would make it drop every managed plugin until the next change.
|
|
52
64
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -69,10 +81,11 @@ export const createManagedConfigRuntime = ({
|
|
|
69
81
|
const includeControl = settings?.agentControlToolEnabled !== false;
|
|
70
82
|
const includeWeb = settings?.agentWebToolEnabled !== false;
|
|
71
83
|
const includeMemory = isAgentMemoryAvailable() && settings?.agentMemoryToolEnabled === true;
|
|
84
|
+
const includeNotify = settings?.agentNotifyToolEnabled === true;
|
|
72
85
|
|
|
73
86
|
const directories = [];
|
|
74
|
-
if (agentToolRuntime && (includeControl || includeWeb || includeMemory)) {
|
|
75
|
-
directories.push(await agentToolRuntime.materializePlugin({ includeControl, includeWeb, includeMemory }));
|
|
87
|
+
if (agentToolRuntime && (includeControl || includeWeb || includeMemory || includeNotify)) {
|
|
88
|
+
directories.push(await agentToolRuntime.materializePlugin({ includeControl, includeWeb, includeMemory, includeNotify }));
|
|
76
89
|
}
|
|
77
90
|
return directories;
|
|
78
91
|
};
|
|
@@ -89,9 +102,8 @@ export const createManagedConfigRuntime = ({
|
|
|
89
102
|
await writeConfigFile(directories);
|
|
90
103
|
return { ...childEnv, OPENCODE_CONFIG: filePath };
|
|
91
104
|
}
|
|
92
|
-
if (directories.length === 0) return childEnv;
|
|
93
105
|
let content = env.OPENCODE_CONFIG_CONTENT;
|
|
94
|
-
for (const directory of directories) {
|
|
106
|
+
for (const directory of [...DISABLED_BUILTIN_PLUGINS, ...directories]) {
|
|
95
107
|
content = appendManagedPlugin(content, directory, 'managed plugin');
|
|
96
108
|
}
|
|
97
109
|
return { ...childEnv, OPENCODE_CONFIG_CONTENT: content };
|
|
@@ -52,7 +52,7 @@ describe('managed OpenCode config file', () => {
|
|
|
52
52
|
expect(childEnv.OPENCODE_CONFIG_CONTENT).toBeUndefined();
|
|
53
53
|
expect(childEnv.OPENCHAMBER_AGENT_TOOL_TOKEN).toBe('token');
|
|
54
54
|
expect(await readConfigFile()).toEqual({
|
|
55
|
-
plugins: [path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
55
|
+
plugins: ['-opencode.browser', path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -61,7 +61,7 @@ describe('managed OpenCode config file', () => {
|
|
|
61
61
|
|
|
62
62
|
await runtime.buildManagedChildEnv();
|
|
63
63
|
|
|
64
|
-
for (const directory of (await readConfigFile()).plugins) {
|
|
64
|
+
for (const directory of (await readConfigFile()).plugins.filter((entry) => !entry.startsWith('-'))) {
|
|
65
65
|
expect((await fs.stat(directory)).isDirectory()).toBe(true);
|
|
66
66
|
}
|
|
67
67
|
});
|
|
@@ -74,7 +74,7 @@ describe('managed OpenCode config file', () => {
|
|
|
74
74
|
const childEnv = await runtime.buildManagedChildEnv();
|
|
75
75
|
|
|
76
76
|
expect(agentToolRuntime.materializePlugin).not.toHaveBeenCalled();
|
|
77
|
-
expect(await readConfigFile()).toEqual({ plugins: [] });
|
|
77
|
+
expect(await readConfigFile()).toEqual({ plugins: ['-opencode.browser'] });
|
|
78
78
|
// A tool switched on later reaches a process that can already call back.
|
|
79
79
|
expect(childEnv.OPENCHAMBER_AGENT_TOOL_TOKEN).toBe('token');
|
|
80
80
|
});
|
|
@@ -89,7 +89,7 @@ describe('managed OpenCode config file', () => {
|
|
|
89
89
|
expect(await runtime.refreshManagedConfigFile()).toEqual({ updated: true });
|
|
90
90
|
|
|
91
91
|
const { plugins } = await readConfigFile();
|
|
92
|
-
expect(plugins).toEqual([]);
|
|
92
|
+
expect(plugins).toEqual(['-opencode.browser']);
|
|
93
93
|
expect(agentToolRuntime.createChildEnv).toHaveBeenCalledTimes(1);
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -102,11 +102,27 @@ describe('managed OpenCode config file', () => {
|
|
|
102
102
|
current.settings = { agentControlToolEnabled: false, agentWebToolEnabled: true };
|
|
103
103
|
await runtime.refreshManagedConfigFile();
|
|
104
104
|
|
|
105
|
-
expect((await readConfigFile()).plugins).toHaveLength(
|
|
105
|
+
expect((await readConfigFile()).plugins).toHaveLength(2);
|
|
106
106
|
expect(agentToolRuntime.materializePlugin).toHaveBeenCalledWith({
|
|
107
107
|
includeControl: false,
|
|
108
108
|
includeWeb: true,
|
|
109
109
|
includeMemory: false,
|
|
110
|
+
includeNotify: false,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('injects the notify tool only when it is switched on', async () => {
|
|
115
|
+
const { runtime, agentToolRuntime } = await createHarness({
|
|
116
|
+
settings: { agentControlToolEnabled: false, agentWebToolEnabled: false, agentNotifyToolEnabled: true },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await runtime.buildManagedChildEnv();
|
|
120
|
+
|
|
121
|
+
expect(agentToolRuntime.materializePlugin).toHaveBeenCalledWith({
|
|
122
|
+
includeControl: false,
|
|
123
|
+
includeWeb: false,
|
|
124
|
+
includeMemory: false,
|
|
125
|
+
includeNotify: true,
|
|
110
126
|
});
|
|
111
127
|
});
|
|
112
128
|
|
|
@@ -121,7 +137,7 @@ describe('managed OpenCode config file', () => {
|
|
|
121
137
|
expect(childEnv.OPENCODE_CONFIG).toBeUndefined();
|
|
122
138
|
expect(JSON.parse(childEnv.OPENCODE_CONFIG_CONTENT)).toEqual({
|
|
123
139
|
model: 'test/model',
|
|
124
|
-
plugins: [path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
140
|
+
plugins: ['-opencode.browser', path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
125
141
|
});
|
|
126
142
|
await expect(fs.stat(path.join(dataDir, MANAGED_CONFIG_FILE_NAME))).rejects.toThrow();
|
|
127
143
|
expect(await runtime.refreshManagedConfigFile()).toEqual({ updated: false, reason: 'external-config' });
|
|
@@ -147,6 +163,6 @@ describe('managed OpenCode config file', () => {
|
|
|
147
163
|
await runtime.buildManagedChildEnv();
|
|
148
164
|
|
|
149
165
|
expect(agentToolRuntime.materializePlugin).not.toHaveBeenCalled();
|
|
150
|
-
expect(await readConfigFile()).toEqual({ plugins: [] });
|
|
166
|
+
expect(await readConfigFile()).toEqual({ plugins: ['-opencode.browser'] });
|
|
151
167
|
});
|
|
152
168
|
});
|
|
@@ -48,9 +48,14 @@ function ensureProjectMcpConfigPath(workingDirectory) {
|
|
|
48
48
|
return path.join(configDir, 'opencode.json');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Same precedence as `getJsonEntrySource`: custom > project > user. */
|
|
51
|
+
/** Same precedence as `getJsonEntrySource`: custom > project > user override > user. */
|
|
52
52
|
function readMcpEntriesAcrossLayers(layers) {
|
|
53
|
-
return readLayeredMcpEntries([
|
|
53
|
+
return readLayeredMcpEntries([
|
|
54
|
+
layers?.userConfig,
|
|
55
|
+
layers?.userOverrideConfig,
|
|
56
|
+
layers?.projectConfig,
|
|
57
|
+
layers?.customConfig,
|
|
58
|
+
]);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function listMcpConfigs(workingDirectory) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function resolveNpmRegistryRequest(packageName: string): { url: string; headers: Record<string, string> };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
6
|
+
|
|
7
|
+
function configPath() {
|
|
8
|
+
const configured = [process.env.npm_config_userconfig, process.env.NPM_CONFIG_USERCONFIG]
|
|
9
|
+
.map((value) => value?.trim())
|
|
10
|
+
.find(Boolean);
|
|
11
|
+
return configured || path.join(os.homedir(), '.npmrc');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// npm's ini parser accepts `key = "value"`; the quotes are not part of the value.
|
|
15
|
+
function unquote(value) {
|
|
16
|
+
const quoted = value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.at(-1) === value[0];
|
|
17
|
+
return quoted ? value.slice(1, -1) : value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function configValues() {
|
|
21
|
+
try {
|
|
22
|
+
const values = new Map();
|
|
23
|
+
for (const rawLine of fs.readFileSync(configPath(), 'utf8').split(/\r?\n/)) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
|
|
26
|
+
|
|
27
|
+
const separator = line.indexOf('=');
|
|
28
|
+
if (separator < 1) continue;
|
|
29
|
+
values.set(line.slice(0, separator).trim(), unquote(line.slice(separator + 1).trim()));
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
} catch {
|
|
33
|
+
return new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configuredValue(values, key) {
|
|
38
|
+
const configured = values.get(key);
|
|
39
|
+
if (!configured) return configured;
|
|
40
|
+
|
|
41
|
+
return configured.replace(/\$\{([^}]+)\}/g, (_match, variable) => {
|
|
42
|
+
const value = process.env[variable];
|
|
43
|
+
if (value === undefined) throw new Error('Missing npm configuration environment variable');
|
|
44
|
+
return value;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function registryFor(packageName, values) {
|
|
49
|
+
const scope = packageName.startsWith('@') ? packageName.split('/', 1)[0] : null;
|
|
50
|
+
const scopedEnvironmentRegistry = scope && [
|
|
51
|
+
process.env[`npm_config_${scope}:registry`],
|
|
52
|
+
process.env[`NPM_CONFIG_${scope}:REGISTRY`],
|
|
53
|
+
].map((value) => value?.trim()).find(Boolean);
|
|
54
|
+
if (scopedEnvironmentRegistry) return scopedEnvironmentRegistry;
|
|
55
|
+
|
|
56
|
+
const scopedRegistry = scope && configuredValue(values, `${scope}:registry`);
|
|
57
|
+
if (scopedRegistry) return scopedRegistry;
|
|
58
|
+
|
|
59
|
+
const environmentRegistry = [process.env.npm_config_registry, process.env.NPM_CONFIG_REGISTRY]
|
|
60
|
+
.map((value) => value?.trim())
|
|
61
|
+
.find(Boolean);
|
|
62
|
+
if (environmentRegistry) return environmentRegistry;
|
|
63
|
+
|
|
64
|
+
return configuredValue(values, 'registry') || DEFAULT_NPM_REGISTRY;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function authFor(registry, values) {
|
|
68
|
+
const registryScope = `//${registry.host}${registry.pathname.replace(/\/+$/, '')}/`;
|
|
69
|
+
let bestMatch = null;
|
|
70
|
+
|
|
71
|
+
for (const key of values.keys()) {
|
|
72
|
+
const match = /^(\/\/.*\/):(?:_authToken|_auth|username|_password)$/.exec(key);
|
|
73
|
+
if (match && registryScope.startsWith(match[1]) && (!bestMatch || match[1].length > bestMatch.length)) {
|
|
74
|
+
bestMatch = match[1];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!bestMatch) return {};
|
|
79
|
+
|
|
80
|
+
const token = configuredValue(values, `${bestMatch}:_authToken`);
|
|
81
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
82
|
+
|
|
83
|
+
const authorization = configuredValue(values, `${bestMatch}:_auth`);
|
|
84
|
+
if (authorization) return { Authorization: `Basic ${authorization}` };
|
|
85
|
+
|
|
86
|
+
const username = configuredValue(values, `${bestMatch}:username`);
|
|
87
|
+
const password = configuredValue(values, `${bestMatch}:_password`);
|
|
88
|
+
if (username && password) {
|
|
89
|
+
const decodedPassword = Buffer.from(password, 'base64').toString('utf8');
|
|
90
|
+
return { Authorization: `Basic ${Buffer.from(`${username}:${decodedPassword}`).toString('base64')}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a credential-safe package metadata request using npm's standard user configuration.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} packageName
|
|
100
|
+
* @returns {{ url: string, headers: Record<string, string> }}
|
|
101
|
+
*/
|
|
102
|
+
export function resolveNpmRegistryRequest(packageName) {
|
|
103
|
+
const values = configValues();
|
|
104
|
+
let registry;
|
|
105
|
+
try {
|
|
106
|
+
registry = new URL(registryFor(packageName, values));
|
|
107
|
+
if (!['http:', 'https:'].includes(registry.protocol) || registry.search || registry.hash) {
|
|
108
|
+
throw new Error('invalid registry');
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('Invalid npm registry URL');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const headers = authFor(registry, values);
|
|
115
|
+
registry.username = '';
|
|
116
|
+
registry.password = '';
|
|
117
|
+
registry.pathname = `${registry.pathname.replace(/\/+$/, '')}/`;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
url: `${registry.toString()}${encodeURIComponent(packageName).replace(/^%40/i, '@')}`,
|
|
121
|
+
headers,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { resolveNpmRegistryRequest } from './npm-registry-config.js';
|
|
7
|
+
|
|
8
|
+
const originalLowerRegistry = process.env.npm_config_registry;
|
|
9
|
+
const originalUpperRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
10
|
+
const originalUserConfig = process.env.NPM_CONFIG_USERCONFIG;
|
|
11
|
+
const originalLowerUserConfig = process.env.npm_config_userconfig;
|
|
12
|
+
const originalPrivateRegistry = process.env.PRIVATE_NPM_REGISTRY;
|
|
13
|
+
const originalPrivateToken = process.env.PRIVATE_NPM_TOKEN;
|
|
14
|
+
const originalScopedRegistry = process.env['npm_config_@scope:registry'];
|
|
15
|
+
let userConfigPath;
|
|
16
|
+
|
|
17
|
+
describe('npm registry configuration', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
userConfigPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-npmrc-')), '.npmrc');
|
|
20
|
+
process.env.NPM_CONFIG_USERCONFIG = userConfigPath;
|
|
21
|
+
delete process.env.npm_config_userconfig;
|
|
22
|
+
delete process.env.npm_config_registry;
|
|
23
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
24
|
+
delete process.env.PRIVATE_NPM_REGISTRY;
|
|
25
|
+
delete process.env.PRIVATE_NPM_TOKEN;
|
|
26
|
+
delete process.env['npm_config_@scope:registry'];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(path.dirname(userConfigPath), { recursive: true, force: true });
|
|
31
|
+
if (originalLowerRegistry === undefined) delete process.env.npm_config_registry;
|
|
32
|
+
else process.env.npm_config_registry = originalLowerRegistry;
|
|
33
|
+
if (originalUpperRegistry === undefined) delete process.env.NPM_CONFIG_REGISTRY;
|
|
34
|
+
else process.env.NPM_CONFIG_REGISTRY = originalUpperRegistry;
|
|
35
|
+
if (originalUserConfig === undefined) delete process.env.NPM_CONFIG_USERCONFIG;
|
|
36
|
+
else process.env.NPM_CONFIG_USERCONFIG = originalUserConfig;
|
|
37
|
+
if (originalLowerUserConfig === undefined) delete process.env.npm_config_userconfig;
|
|
38
|
+
else process.env.npm_config_userconfig = originalLowerUserConfig;
|
|
39
|
+
if (originalPrivateRegistry === undefined) delete process.env.PRIVATE_NPM_REGISTRY;
|
|
40
|
+
else process.env.PRIVATE_NPM_REGISTRY = originalPrivateRegistry;
|
|
41
|
+
if (originalPrivateToken === undefined) delete process.env.PRIVATE_NPM_TOKEN;
|
|
42
|
+
else process.env.PRIVATE_NPM_TOKEN = originalPrivateToken;
|
|
43
|
+
if (originalScopedRegistry === undefined) delete process.env['npm_config_@scope:registry'];
|
|
44
|
+
else process.env['npm_config_@scope:registry'] = originalScopedRegistry;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('uses the default public registry without npm configuration', () => {
|
|
48
|
+
expect(resolveNpmRegistryRequest('@scope/pkg')).toEqual({
|
|
49
|
+
url: 'https://registry.npmjs.org/@scope%2Fpkg',
|
|
50
|
+
headers: {},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('uses the user npm registry for an unscoped package', () => {
|
|
55
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/custom/npm/\n');
|
|
56
|
+
|
|
57
|
+
expect(resolveNpmRegistryRequest('private-plugin')).toEqual({
|
|
58
|
+
url: 'https://mirror.example.com/custom/npm/private-plugin',
|
|
59
|
+
headers: {},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('prefers a scoped user registry', () => {
|
|
64
|
+
fs.writeFileSync(userConfigPath, [
|
|
65
|
+
'registry=https://mirror.example.com/npm/',
|
|
66
|
+
'@scope:registry=https://packages.example.com/npm/',
|
|
67
|
+
].join('\n'));
|
|
68
|
+
|
|
69
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('prefers a scoped user registry over an inherited general registry', () => {
|
|
73
|
+
fs.writeFileSync(userConfigPath, '@scope:registry=https://packages.example.com/npm/\n');
|
|
74
|
+
process.env.NPM_CONFIG_REGISTRY = 'https://inherited.example.com/npm/';
|
|
75
|
+
|
|
76
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('prefers a scoped inherited registry over user npm configuration', () => {
|
|
80
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
81
|
+
process.env['npm_config_@scope:registry'] = 'https://packages.example.com/npm/';
|
|
82
|
+
|
|
83
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('prefers an inherited npm registry over user configuration', () => {
|
|
87
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
88
|
+
process.env.NPM_CONFIG_REGISTRY = 'https://inherited.example.com/npm/';
|
|
89
|
+
|
|
90
|
+
expect(resolveNpmRegistryRequest('plugin').url).toBe('https://inherited.example.com/npm/plugin');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('uses a matching npm token without exposing it in the request URL', () => {
|
|
94
|
+
fs.writeFileSync(userConfigPath, [
|
|
95
|
+
'registry=https://packages.example.com/npm/',
|
|
96
|
+
'//packages.example.com/npm/:_authToken=private-token',
|
|
97
|
+
].join('\n'));
|
|
98
|
+
|
|
99
|
+
const request = resolveNpmRegistryRequest('private-plugin');
|
|
100
|
+
|
|
101
|
+
expect(request.url).toBe('https://packages.example.com/npm/private-plugin');
|
|
102
|
+
expect(request.url).not.toContain('private-token');
|
|
103
|
+
expect(request.headers).toEqual({ Authorization: 'Bearer private-token' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('uses npm basic authentication with its base64-encoded password', () => {
|
|
107
|
+
fs.writeFileSync(userConfigPath, [
|
|
108
|
+
'registry=https://packages.example.com/npm/',
|
|
109
|
+
'//packages.example.com/npm/:username=registry-user',
|
|
110
|
+
'//packages.example.com/npm/:_password=c2VjcmV0',
|
|
111
|
+
].join('\n'));
|
|
112
|
+
|
|
113
|
+
expect(resolveNpmRegistryRequest('private-plugin').headers).toEqual({
|
|
114
|
+
Authorization: 'Basic cmVnaXN0cnktdXNlcjpzZWNyZXQ=',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('expands environment variables in user npm configuration', () => {
|
|
119
|
+
process.env.PRIVATE_NPM_TOKEN = 'expanded-token';
|
|
120
|
+
fs.writeFileSync(userConfigPath, [
|
|
121
|
+
'registry=${PRIVATE_NPM_REGISTRY}',
|
|
122
|
+
'//packages.example.com/npm/:_authToken=${PRIVATE_NPM_TOKEN}',
|
|
123
|
+
].join('\n'));
|
|
124
|
+
process.env.PRIVATE_NPM_REGISTRY = 'https://packages.example.com/npm/';
|
|
125
|
+
|
|
126
|
+
expect(resolveNpmRegistryRequest('private-plugin')).toEqual({
|
|
127
|
+
url: 'https://packages.example.com/npm/private-plugin',
|
|
128
|
+
headers: { Authorization: 'Bearer expanded-token' },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('reads quoted values the way npm does', () => {
|
|
133
|
+
fs.writeFileSync(userConfigPath, [
|
|
134
|
+
'@scope:registry = "https://packages.example.com/npm/"',
|
|
135
|
+
"//packages.example.com/npm/:_authToken='quoted-token'",
|
|
136
|
+
].join('\n'));
|
|
137
|
+
|
|
138
|
+
expect(resolveNpmRegistryRequest('@scope/plugin')).toEqual({
|
|
139
|
+
url: 'https://packages.example.com/npm/@scope%2Fplugin',
|
|
140
|
+
headers: { Authorization: 'Bearer quoted-token' },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('honors the lowercase userconfig variable npm sets for child processes', () => {
|
|
145
|
+
delete process.env.NPM_CONFIG_USERCONFIG;
|
|
146
|
+
process.env.npm_config_userconfig = userConfigPath;
|
|
147
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
expect(resolveNpmRegistryRequest('plugin').url).toBe('https://mirror.example.com/npm/plugin');
|
|
151
|
+
} finally {
|
|
152
|
+
delete process.env.npm_config_userconfig;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('rejects unresolved npm configuration environment variables', () => {
|
|
157
|
+
fs.writeFileSync(userConfigPath, 'registry=${MISSING_NPM_REGISTRY}\n');
|
|
158
|
+
|
|
159
|
+
expect(() => resolveNpmRegistryRequest('private-plugin')).toThrow('Invalid npm registry URL');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('rejects malformed registry values without exposing their contents', () => {
|
|
163
|
+
fs.writeFileSync(userConfigPath, 'registry=not-a-url-private-token\n');
|
|
164
|
+
|
|
165
|
+
expect(() => resolveNpmRegistryRequest('plugin')).toThrow('Invalid npm registry URL');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -2,9 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
|
+
import { resolveNpmRegistryRequest } from './npm-registry-config.js';
|
|
6
|
+
|
|
5
7
|
const NPM_CACHE_TTL_MS = 3_600_000;
|
|
6
8
|
const NPM_FETCH_TIMEOUT_MS = 5_000;
|
|
7
|
-
const NPM_REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {Object} NpmPackagePayload
|
|
@@ -50,10 +51,6 @@ function _getUserAgent() {
|
|
|
50
51
|
return _userAgent;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function encodeName(name) {
|
|
54
|
-
return encodeURIComponent(name).replace(/^%40/, '@');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
function parseDistTags(value) {
|
|
58
55
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
59
56
|
return {};
|
|
@@ -87,10 +84,12 @@ function cacheResult(name, payload) {
|
|
|
87
84
|
*/
|
|
88
85
|
export async function lookupNpmPackage(name) {
|
|
89
86
|
try {
|
|
90
|
-
const
|
|
87
|
+
const request = resolveNpmRegistryRequest(name);
|
|
88
|
+
const response = await fetch(request.url, {
|
|
91
89
|
headers: {
|
|
92
90
|
'User-Agent': _getUserAgent(),
|
|
93
91
|
Accept: 'application/json',
|
|
92
|
+
...request.headers,
|
|
94
93
|
},
|
|
95
94
|
signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
|
|
96
95
|
});
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
2
5
|
import * as npm from './npm-registry.js';
|
|
3
6
|
|
|
4
7
|
const originalFetch = globalThis.fetch;
|
|
5
8
|
const originalDateNow = Date.now;
|
|
9
|
+
const originalUserConfig = process.env.NPM_CONFIG_USERCONFIG;
|
|
10
|
+
const originalLowerUserConfig = process.env.npm_config_userconfig;
|
|
11
|
+
const originalLowerRegistry = process.env.npm_config_registry;
|
|
12
|
+
const originalUpperRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
6
13
|
|
|
7
14
|
let fetchMock;
|
|
15
|
+
let userConfigPath;
|
|
8
16
|
|
|
9
17
|
function jsonResponse(body, status = 200) {
|
|
10
18
|
return Promise.resolve(new Response(JSON.stringify(body), {
|
|
@@ -17,6 +25,11 @@ describe('npm registry client', () => {
|
|
|
17
25
|
beforeEach(() => {
|
|
18
26
|
npm.clearCache();
|
|
19
27
|
Date.now = originalDateNow;
|
|
28
|
+
userConfigPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-npmrc-')), '.npmrc');
|
|
29
|
+
process.env.NPM_CONFIG_USERCONFIG = userConfigPath;
|
|
30
|
+
delete process.env.npm_config_userconfig;
|
|
31
|
+
delete process.env.npm_config_registry;
|
|
32
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
20
33
|
fetchMock = mock(() => jsonResponse({}));
|
|
21
34
|
globalThis.fetch = fetchMock;
|
|
22
35
|
});
|
|
@@ -25,6 +38,15 @@ describe('npm registry client', () => {
|
|
|
25
38
|
npm.clearCache();
|
|
26
39
|
globalThis.fetch = originalFetch;
|
|
27
40
|
Date.now = originalDateNow;
|
|
41
|
+
fs.rmSync(path.dirname(userConfigPath), { recursive: true, force: true });
|
|
42
|
+
if (originalUserConfig === undefined) delete process.env.NPM_CONFIG_USERCONFIG;
|
|
43
|
+
else process.env.NPM_CONFIG_USERCONFIG = originalUserConfig;
|
|
44
|
+
if (originalLowerUserConfig === undefined) delete process.env.npm_config_userconfig;
|
|
45
|
+
else process.env.npm_config_userconfig = originalLowerUserConfig;
|
|
46
|
+
if (originalLowerRegistry === undefined) delete process.env.npm_config_registry;
|
|
47
|
+
else process.env.npm_config_registry = originalLowerRegistry;
|
|
48
|
+
if (originalUpperRegistry === undefined) delete process.env.NPM_CONFIG_REGISTRY;
|
|
49
|
+
else process.env.NPM_CONFIG_REGISTRY = originalUpperRegistry;
|
|
28
50
|
});
|
|
29
51
|
|
|
30
52
|
test('200 success returns latest versions and dist tags', async () => {
|
|
@@ -171,6 +193,14 @@ describe('npm registry client', () => {
|
|
|
171
193
|
expect(fetchMock.mock.calls[0][0]).toBe('https://registry.npmjs.org/@scope%2Fpkg');
|
|
172
194
|
});
|
|
173
195
|
|
|
196
|
+
test('user npm configuration changes the package metadata registry', async () => {
|
|
197
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
198
|
+
|
|
199
|
+
await npm.getNpmInfo('private-plugin');
|
|
200
|
+
|
|
201
|
+
expect(fetchMock.mock.calls[0][0]).toBe('https://mirror.example.com/npm/private-plugin');
|
|
202
|
+
});
|
|
203
|
+
|
|
174
204
|
test('user-agent header is present', async () => {
|
|
175
205
|
await npm.getNpmInfo('foo');
|
|
176
206
|
|
|
@@ -71,6 +71,26 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
71
71
|
|
|
72
72
|
let desktopRestartError = null;
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How this server was launched, read from its instance file. A foreground
|
|
76
|
+
* server outside systemd has no process that can restart it after an
|
|
77
|
+
* install, so it cannot update itself.
|
|
78
|
+
*/
|
|
79
|
+
const readLaunchState = async () => {
|
|
80
|
+
const currentPort = server.address()?.port || 3000;
|
|
81
|
+
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
|
82
|
+
let storedOptions = { port: currentPort, daemon: true };
|
|
83
|
+
try {
|
|
84
|
+
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
|
85
|
+
storedOptions = JSON.parse(content);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
|
89
|
+
const isForegroundService = launchMode === 'foreground';
|
|
90
|
+
const systemdServiceUnit = isForegroundService ? resolveSystemdServiceUnit(process.env) : null;
|
|
91
|
+
return { storedOptions, launchMode, isForegroundService, systemdServiceUnit };
|
|
92
|
+
};
|
|
93
|
+
|
|
74
94
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
75
95
|
try {
|
|
76
96
|
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
@@ -121,6 +141,14 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
121
141
|
} else {
|
|
122
142
|
const { checkForUpdates } = await import('../package-manager.js');
|
|
123
143
|
updateInfo = await checkForUpdates(updateRequest);
|
|
144
|
+
// Tell clients up front that the install route will refuse, so they
|
|
145
|
+
// show the manual command instead of an Update button that fails.
|
|
146
|
+
if (updateInfo?.available && updateRequest.appType === 'web') {
|
|
147
|
+
const { isForegroundService, systemdServiceUnit } = await readLaunchState();
|
|
148
|
+
if (isForegroundService && !systemdServiceUnit) {
|
|
149
|
+
updateInfo = { ...updateInfo, installBlocked: 'service-manager' };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
124
152
|
}
|
|
125
153
|
res.json(updateInfo);
|
|
126
154
|
} catch (error) {
|
|
@@ -215,17 +243,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
215
243
|
return;
|
|
216
244
|
}
|
|
217
245
|
|
|
218
|
-
const
|
|
219
|
-
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
|
220
|
-
let storedOptions = { port: currentPort, daemon: true };
|
|
221
|
-
try {
|
|
222
|
-
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
|
223
|
-
storedOptions = JSON.parse(content);
|
|
224
|
-
} catch {
|
|
225
|
-
}
|
|
226
|
-
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
|
227
|
-
const isForegroundService = launchMode === 'foreground';
|
|
228
|
-
const systemdServiceUnit = isForegroundService ? resolveSystemdServiceUnit(process.env) : null;
|
|
246
|
+
const { storedOptions, launchMode, isForegroundService, systemdServiceUnit } = await readLaunchState();
|
|
229
247
|
|
|
230
248
|
if (isForegroundService) {
|
|
231
249
|
if (!systemdServiceUnit) {
|
|
@@ -203,6 +203,24 @@ describe('OpenChamber desktop host update route', () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
describe('OpenChamber foreground update route', () => {
|
|
206
|
+
it('marks an available update as blocked when the foreground server has no service manager', async () => {
|
|
207
|
+
const { app } = createApp();
|
|
208
|
+
|
|
209
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web').expect(200);
|
|
210
|
+
|
|
211
|
+
expect(response.body).toMatchObject({ available: true, installBlocked: 'service-manager' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('leaves the update installable for a systemd-owned or daemon server', async () => {
|
|
215
|
+
const systemd = createApp({ environment: { INVOCATION_ID: 'systemd-invocation' } });
|
|
216
|
+
const daemon = createApp({ storedOptions: { launchMode: 'daemon' } });
|
|
217
|
+
|
|
218
|
+
for (const { app } of [systemd, daemon]) {
|
|
219
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web').expect(200);
|
|
220
|
+
expect(response.body.installBlocked).toBeUndefined();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
206
224
|
it('rejects a foreground update when the server is not owned by systemd', async () => {
|
|
207
225
|
const { app } = createApp();
|
|
208
226
|
|
|
@@ -16,6 +16,10 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
|
|
16
16
|
readSettingsFromDiskMigrated,
|
|
17
17
|
getReadSettingsFromDiskMigrated,
|
|
18
18
|
sanitizeProjects,
|
|
19
|
+
// `refuseDirectory(candidate)` answers the reason a directory may not be used on this host,
|
|
20
|
+
// or null. It runs before the path is looked at, so a refused directory is never touched and
|
|
21
|
+
// never falls back to another one. The isolated-spaces host refuses `/spaces/...` with it.
|
|
22
|
+
refuseDirectory = () => null,
|
|
19
23
|
} = dependencies;
|
|
20
24
|
const realpathCache = createRealpathCache({
|
|
21
25
|
realpath: fsPromises.realpath.bind(fsPromises),
|
|
@@ -38,6 +42,10 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
|
|
38
42
|
if (!resolved) {
|
|
39
43
|
return { ok: false, error: 'Directory parameter is required' };
|
|
40
44
|
}
|
|
45
|
+
const refusal = refuseDirectory(resolved);
|
|
46
|
+
if (typeof refusal === 'string' && refusal.length > 0) {
|
|
47
|
+
return { ok: false, error: refusal };
|
|
48
|
+
}
|
|
41
49
|
try {
|
|
42
50
|
const stats = await fsPromises.stat(resolved);
|
|
43
51
|
if (!stats.isDirectory()) {
|