@openchambery/web 2.0.0-beta.26 → 2.0.0-beta.28
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/{AgentAvatar-FyTdEOMr.js → AgentAvatar-CqD6Pfom.js} +1 -1
- package/dist/assets/{AssistantView-DvOO2Kf5.js → AssistantView-BW9f9vF2.js} +3 -3
- package/dist/assets/{DiagramView-BqjeSOIg.js → DiagramView-BP3WDmFe.js} +1 -1
- package/dist/assets/{FadeInOnReveal-DHDffzHs.js → FadeInOnReveal-ezW_e-yz.js} +1 -1
- package/dist/assets/{Icon-D7NdMmbi.js → Icon-ChOAx1jX.js} +1 -1
- package/dist/assets/{MarkdownRenderer-kpkRqS42.js → MarkdownRenderer-BBHZa1Ts.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-CzBos-Gt.js → MarkdownRendererImpl-DfxEsttZ.js} +2 -2
- package/dist/assets/{MarkstreamRendererImpl-RY_rAgKD.js → MarkstreamRendererImpl-6k-n071l.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DgtRh0zu.js → MobileDetailNavigation-s48GkQY9.js} +1 -1
- package/dist/assets/{MobileShareBridge-BM3cN-gG.js → MobileShareBridge-DAgShkFE.js} +2 -2
- package/dist/assets/MobileSurface-DZug2f4O.js +2 -0
- package/dist/assets/{MultiRunWindow-Bq4gz78C.js → MultiRunWindow-CIhHw9mo.js} +1 -1
- package/dist/assets/{OnboardingScreen-DMbPMwlp.js → OnboardingScreen-ozxwbzDw.js} +1 -1
- package/dist/assets/{OverlayScrollbar-mI9WVa7E.js → OverlayScrollbar-BL-QnnrN.js} +1 -1
- package/dist/assets/SettingsView-DmUdWSji.js +25 -0
- package/dist/assets/{SettingsWindow-CHPjbEnI.js → SettingsWindow-CuIZwGhM.js} +1 -1
- package/dist/assets/{TerminalView-DCJuL2nP.js → TerminalView-BIJ-ILgl.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Cg_Q1t3X.js → ToolOutputDialog-B6KIxVUo.js} +1 -1
- package/dist/assets/{appThemeRegistry-BxuflaQt.js → appThemeRegistry-BuMGUGUK.js} +1 -1
- package/dist/assets/{button-e8fYWIMM.js → button-DoDvdeG-.js} +1 -1
- package/dist/assets/{collapsible-B6wLXHi3.js → collapsible-CZkyFVK0.js} +1 -1
- package/dist/assets/{context-menu-Bq7W1d7y.js → context-menu-DTjON-Ta.js} +1 -1
- package/dist/assets/{desktopHosts-C8vf9vRY.js → desktopHosts-CP1YHQ4Z.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DDJtljeu.js → desktopRelayRestore-BeOGO7oA.js} +1 -1
- package/dist/assets/{diagram-BBL-LfsC.js → diagram-DUi801df.js} +1 -1
- package/dist/assets/{dialog-MRsJpHzu.js → dialog-DGzGqOhG.js} +1 -1
- package/dist/assets/{es-DvPwAj0y.js → es-DCjl24dV.js} +3 -3
- package/dist/assets/fileMentionTouchSelection-C3A6XKRM.js +2 -0
- package/dist/assets/{fileQueries-CBu-ytXS.js → fileQueries-CAH8-p27.js} +1 -1
- package/dist/assets/{fr-XQsoaNKY.js → fr-DfJ5j0t5.js} +3 -3
- package/dist/assets/{gitApi-CuIUGt04.js → gitApi-B1Nyw0PD.js} +3 -3
- package/dist/assets/{input-DbOe1mdT.js → input-Ct2uaiDc.js} +1 -1
- package/dist/assets/insertionBoundaries-BypTPUfp.js +1 -0
- package/dist/assets/{ja-DslzO96s.js → ja-B_9_w2Xf.js} +3 -3
- package/dist/assets/{ko-C_9XOAL0.js → ko-DlGbo3kV.js} +3 -3
- package/dist/assets/main-BREqmQUm.js +169 -0
- package/dist/assets/{main-Sd4Auh_x.js → main-BodZWzei.js} +2 -2
- package/dist/assets/main-qSeeJRW1.css +1 -0
- package/dist/assets/{markdownHeightCache-B5VSLl79.js → markdownHeightCache-CMoWScss.js} +1 -1
- package/dist/assets/{mcpQueries-BYNrkehM.js → mcpQueries-nScpWEqu.js} +1 -1
- package/dist/assets/{miniChat-Hp9bNmS0.js → miniChat-Ca-kdWf0.js} +2 -2
- package/dist/assets/{mobile-fBLEOx9_.js → mobile-BWi6F8Lj.js} +2 -2
- package/dist/assets/multirun-BdSrR6jN.js +3 -0
- package/dist/assets/persistence-B3_CPgm1.js +3 -0
- package/dist/assets/{pl-Dst9Wk53.js → pl-DjUsS8Ei.js} +3 -3
- package/dist/assets/platform-Bxdc4SbV.js +6 -0
- package/dist/assets/projectMeta-CIKLMweH.js +1 -0
- package/dist/assets/{pt-BR-Dq394BPb.js → pt-BR-B8sV7qo1.js} +3 -3
- package/dist/assets/{renderElectronMiniChatApp-D1xipCO5.js → renderElectronMiniChatApp-DtYCIW4n.js} +2 -2
- package/dist/assets/renderMobileApp-D47vojsP.js +28 -0
- package/dist/assets/{runtimeConfig-pTYamMLy.js → runtimeConfig-u9DrZmkS.js} +1 -1
- package/dist/assets/runtimeEndpointReset-Bn4vJ-gl.js +148 -0
- package/dist/assets/runtimeEndpointReset-D1_Y5I4F.css +1 -0
- package/dist/assets/{sessionChangeSummary-DcnZyuWt.js → sessionChangeSummary-C0OxmHed.js} +2 -2
- package/dist/assets/{sessionLookup-B3t-FerN.js → sessionLookup-D1NHRgWf.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-B0sZ7mm6.js → sortable-tabs-strip-Cp5i5hI-.js} +1 -1
- package/dist/assets/{store-D8fmTWK3.js → store-BTqPn8Uq.js} +3 -3
- package/dist/assets/{terminalApi-CJyWCuc8.js → terminalApi-CqyW3_W3.js} +1 -1
- package/dist/assets/{textarea-B5GMntHk.js → textarea-jEbc6gqf.js} +1 -1
- package/dist/assets/{tooltip-C64SdRvR.js → tooltip-DKYT7xYe.js} +1 -1
- package/dist/assets/{uk-8XAR75Qh.js → uk-Bh7HXyLp.js} +3 -3
- package/dist/assets/{url-DF1NFwt_.js → url-hhmiocZY.js} +1 -1
- package/dist/assets/{useAppFontEffects-BQklk6XB.js → useAppFontEffects-BBX1KjLV.js} +53 -53
- package/dist/assets/{useDesktopWindowControlsLayout-CrzYwZDQ.js → useDesktopWindowControlsLayout-uKYzUX-m.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-Ddx7sZ5A.js → useEffectiveDirectory-BOolveMp.js} +1 -1
- package/dist/assets/{useFontPreferences-CrteQiAp.js → useFontPreferences-BMJQP6t6.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CC8QT7VC.js +415 -0
- package/dist/assets/useSessionAutoCleanup-CydMAlLf.js +2 -0
- package/dist/assets/{useWorkerHighlightedLines-VOtVUGkO.js → useWorkerHighlightedLines-BF1ClmMe.js} +1 -1
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{zh-CN-B-ILfrqC.js → zh-CN-Uzia0gk4.js} +3 -3
- package/dist/assets/{zh-TW-5HeTVP4z.js → zh-TW-CiI7DLdB.js} +3 -3
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +3 -3
- package/server/index.js +2 -20
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/opencode/DOCUMENTATION.md +24 -20
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +9 -122
- package/server/lib/opencode/config-entity-routes.test.js +7 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +9 -7
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +7 -0
- package/server/lib/opencode/proxy.test.js +13 -0
- package/server/lib/opencode/routes.js +5 -5
- package/server/lib/opencode/routes.test.js +18 -18
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/upgrade-screen.js +4 -11
- package/server/lib/opencode/upgrade-screen.test.js +25 -25
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +1 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/MobileSurface-ocVWenHA.js +0 -2
- package/dist/assets/SettingsView-CIZz40Jg.js +0 -25
- package/dist/assets/fileMentionTouchSelection-Bj67pPo7.js +0 -3
- package/dist/assets/insertionBoundaries-nzs1Q3YR.js +0 -1
- package/dist/assets/main-D2BzpnUt.css +0 -1
- package/dist/assets/main-DjilfYN7.js +0 -169
- package/dist/assets/multirun-D7W7oCj4.js +0 -4
- package/dist/assets/persistence-CMWO3jFo.js +0 -3
- package/dist/assets/platform-B0p43squ.js +0 -6
- package/dist/assets/projectMeta-DrUKrYt0.js +0 -3
- package/dist/assets/renderMobileApp-DSk7Qs-_.js +0 -28
- package/dist/assets/runtimeEndpointReset-B5HEMuiB.js +0 -149
- package/dist/assets/useMobileNavigationStore-i4cXNoOf.js +0 -414
- package/dist/assets/useSessionAutoCleanup-Dzd0fGSe.js +0 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -197,23 +197,12 @@ function getAgentPermissionSource(agentName, workingDirectory, lookupCache = nul
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// Check
|
|
201
|
-
// user -> project -> custom, so custom wins over project, project over user.
|
|
200
|
+
// Check physical documents in effective override order, including both global files.
|
|
202
201
|
const layers = readConfigLayers(workingDirectory);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const projectJsonPermission = layers.projectConfig?.agent?.[agentName]?.permission;
|
|
210
|
-
if (projectJsonPermission !== undefined && layers.paths.projectPath) {
|
|
211
|
-
return { source: 'json', scope: AGENT_SCOPE.PROJECT, path: layers.paths.projectPath };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const userJsonPermission = layers.userConfig?.agent?.[agentName]?.permission;
|
|
215
|
-
if (userJsonPermission !== undefined) {
|
|
216
|
-
return { source: 'json', scope: AGENT_SCOPE.USER, path: layers.paths.userPath };
|
|
202
|
+
for (const layer of layers.documents.toReversed()) {
|
|
203
|
+
if (layer.config?.agent?.[agentName]?.permission !== undefined) {
|
|
204
|
+
return { source: 'json', scope: layer.scope, path: layer.path };
|
|
205
|
+
}
|
|
217
206
|
}
|
|
218
207
|
|
|
219
208
|
return { source: null, scope: null, path: null };
|
|
@@ -300,7 +289,7 @@ function getAgentSources(agentName, workingDirectory, lookupCache = createAgentL
|
|
|
300
289
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
|
301
290
|
const jsonSection = jsonSource.section;
|
|
302
291
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
|
303
|
-
const jsonScope =
|
|
292
|
+
const jsonScope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
304
293
|
|
|
305
294
|
const sources = {
|
|
306
295
|
md: {
|
|
@@ -444,7 +433,7 @@ function getAgentConfig(agentName, workingDirectory, lookupCache = createAgentLo
|
|
|
444
433
|
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
|
|
445
434
|
|
|
446
435
|
if (jsonSource.exists && jsonSource.section) {
|
|
447
|
-
const scope =
|
|
436
|
+
const scope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
448
437
|
return {
|
|
449
438
|
source: 'json',
|
|
450
439
|
scope,
|
|
@@ -756,8 +745,9 @@ function deleteAgent(agentName, workingDirectory, scope) {
|
|
|
756
745
|
const layers = readConfigLayers(workingDirectory);
|
|
757
746
|
|
|
758
747
|
if (requestedScope === AGENT_SCOPE.PROJECT) {
|
|
759
|
-
|
|
760
|
-
|
|
748
|
+
const target = layers.projectLayers.toReversed().find((layer) => layer.config?.agent?.[agentName] !== undefined);
|
|
749
|
+
if (target && deleteJsonAgentEntry(target.config, agentName)) {
|
|
750
|
+
writeConfig(target.config, target.path);
|
|
761
751
|
console.log(`Removed project-level agent from opencode.json: ${agentName}`);
|
|
762
752
|
return;
|
|
763
753
|
}
|
|
@@ -765,8 +755,9 @@ function deleteAgent(agentName, workingDirectory, scope) {
|
|
|
765
755
|
}
|
|
766
756
|
|
|
767
757
|
if (requestedScope === AGENT_SCOPE.USER) {
|
|
768
|
-
const
|
|
769
|
-
const
|
|
758
|
+
const target = layers.documents.toReversed().find((layer) => layer.scope !== 'project' && layer.config?.agent?.[agentName] !== undefined);
|
|
759
|
+
const userJsonPath = target?.path;
|
|
760
|
+
const userJsonConfig = target?.config;
|
|
770
761
|
if (userJsonPath && deleteJsonAgentEntry(userJsonConfig, agentName)) {
|
|
771
762
|
writeConfig(userJsonConfig, userJsonPath);
|
|
772
763
|
console.log(`Removed user-level agent from opencode.json: ${agentName}`);
|
|
@@ -31,7 +31,7 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
|
|
|
31
31
|
const normalized = normalizeOpenCodePassword(password);
|
|
32
32
|
if (!isValidOpenCodePassword(normalized)) {
|
|
33
33
|
setAuthPassword(null);
|
|
34
|
-
setAuthSource(null);
|
|
34
|
+
setAuthSource(source || null);
|
|
35
35
|
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
36
36
|
syncToHmrState();
|
|
37
37
|
return null;
|
|
@@ -45,13 +45,13 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const getOpenCodeAuthHeaders = () => {
|
|
48
|
-
const password = normalizeOpenCodePassword(getAuthPassword() ||
|
|
48
|
+
const password = normalizeOpenCodePassword(getAuthPassword() || '');
|
|
49
49
|
|
|
50
50
|
if (!password) {
|
|
51
51
|
return {};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const username = process.env.OPENCODE_SERVER_USERNAME?.trim() || 'opencode';
|
|
54
|
+
const username = getAuthSource() === 'shared-service' ? 'opencode' : process.env.OPENCODE_SERVER_USERNAME?.trim() || 'opencode';
|
|
55
55
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
56
56
|
return { Authorization: `Basic ${credentials}` };
|
|
57
57
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createOpenCodeAuthStateRuntime } from './auth-state-runtime.js';
|
|
4
|
+
import { createHmrStateRuntime } from './hmr-state-runtime.js';
|
|
5
|
+
|
|
6
|
+
const setup = (env = {}) => {
|
|
7
|
+
const processLike = { env: { ...env } };
|
|
8
|
+
const hmr = createHmrStateRuntime({ globalThisLike: {}, os: { homedir: () => '/example' }, processLike, stateKey: 'test' });
|
|
9
|
+
const state = hmr.getOrCreateHmrState();
|
|
10
|
+
hmr.ensureUserProvidedOpenCodePassword(state);
|
|
11
|
+
const restore = () => hmr.restoreRuntimeFromState({ hmrState: state, userProvidedOpenCodePassword: hmr.getUserProvidedOpenCodePassword(state) });
|
|
12
|
+
const runtimeState = restore();
|
|
13
|
+
const auth = createOpenCodeAuthStateRuntime({ crypto, process: processLike,
|
|
14
|
+
getAuthPassword: () => runtimeState.openCodeAuthPassword,
|
|
15
|
+
setAuthPassword: (value) => { runtimeState.openCodeAuthPassword = value; },
|
|
16
|
+
getAuthSource: () => runtimeState.openCodeAuthSource,
|
|
17
|
+
setAuthSource: (value) => { runtimeState.openCodeAuthSource = value; },
|
|
18
|
+
getUserProvidedPassword: () => hmr.getUserProvidedOpenCodePassword(state),
|
|
19
|
+
syncToHmrState: () => hmr.syncStateFromRuntime(state, runtimeState),
|
|
20
|
+
});
|
|
21
|
+
return { auth, restore, runtimeState, processLike, hmr, state };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('OpenCode auth ownership', () => {
|
|
25
|
+
it('uses v2 before legacy credentials and preserves the captured choice through HMR/restart', async () => {
|
|
26
|
+
const test = setup({ OPENCODE_PASSWORD: 'v2-fixture', OPENCODE_SERVER_PASSWORD: 'legacy-fixture' });
|
|
27
|
+
const password = await test.auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true });
|
|
28
|
+
expect(password === 'v2-fixture').toBe(true);
|
|
29
|
+
expect(test.auth.getOpenCodeAuthHeaders().Authorization === `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`).toBe(true);
|
|
30
|
+
test.processLike.env.OPENCODE_PASSWORD = 'later-shell-fixture';
|
|
31
|
+
test.hmr.ensureUserProvidedOpenCodePassword(test.state);
|
|
32
|
+
expect((await test.auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true })) === password).toBe(true);
|
|
33
|
+
expect(test.restore().openCodeAuthPassword === password).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rotates generated credentials while old credentials fail a controlled auth check', async () => {
|
|
37
|
+
const { auth, restore } = setup();
|
|
38
|
+
const old = await auth.ensureLocalOpenCodeServerPassword();
|
|
39
|
+
const previousHeader = auth.getOpenCodeAuthHeaders().Authorization;
|
|
40
|
+
const password = await auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true });
|
|
41
|
+
const accepts = (header) => header === `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`;
|
|
42
|
+
expect(password !== old).toBe(true);
|
|
43
|
+
expect(accepts(previousHeader)).toBe(false);
|
|
44
|
+
expect(accepts(auth.getOpenCodeAuthHeaders().Authorization)).toBe(true);
|
|
45
|
+
expect(restore().openCodeAuthPassword === password).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('adopts only shared registration credentials and never falls back to private env after clearing', () => {
|
|
49
|
+
const { auth, processLike, restore } = setup({ OPENCODE_PASSWORD: 'private-fixture', OPENCODE_SERVER_USERNAME: 'private-user' });
|
|
50
|
+
auth.adoptOpenCodeServerPassword('registration-fixture', 'shared-service');
|
|
51
|
+
expect(auth.getOpenCodeAuthHeaders().Authorization === `Basic ${Buffer.from('opencode:registration-fixture').toString('base64')}`).toBe(true);
|
|
52
|
+
auth.adoptOpenCodeServerPassword(null, 'shared-service');
|
|
53
|
+
expect(restore().openCodeAuthPassword).toBeNull();
|
|
54
|
+
expect(restore().openCodeAuthSource).toBe('shared-service');
|
|
55
|
+
auth.adoptOpenCodeServerPassword(null, null);
|
|
56
|
+
processLike.env.OPENCODE_SERVER_PASSWORD = 'stale-fixture';
|
|
57
|
+
expect(auth.getOpenCodeAuthHeaders()).toEqual({});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OpenCodeClient } from '@opencode/client';
|
|
2
|
+
|
|
3
|
+
export type ProviderCredential =
|
|
4
|
+
| { type: 'api'; key: string }
|
|
5
|
+
| { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string };
|
|
6
|
+
export function configureOpenCodeCredentials(resolveClient: (() => OpenCodeClient) | null): void;
|
|
7
|
+
export function openCodeCredentialSource(deps: {
|
|
8
|
+
buildOpenCodeUrl: (path: string, prefix?: string) => string;
|
|
9
|
+
getOpenCodeAuthHeaders: () => Record<string, string>;
|
|
10
|
+
}): () => OpenCodeClient;
|
|
11
|
+
export function readOpenCodeCredentials(): Promise<Record<string, ProviderCredential>>;
|
|
12
|
+
export function getProviderAuth(providerId: string): Promise<ProviderCredential | null>;
|
|
13
|
+
export function removeProviderAuth(providerId: string): Promise<boolean>;
|
|
@@ -1,81 +1,91 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
1
|
+
import { makeOpenCodeV2Client } from './v2-client.js';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/** @typedef {{ type: 'api', key: string } | { type: 'oauth', access: string, refresh: string, expires: number, accountId?: string, enterpriseUrl?: string }} ProviderCredential */
|
|
4
|
+
/** @type {null | (() => import('@opencode/client').OpenCodeClient)} */
|
|
5
|
+
let getClient = null;
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
try {
|
|
13
|
-
const content = fs.readFileSync(AUTH_FILE, 'utf8');
|
|
14
|
-
const trimmed = content.trim();
|
|
15
|
-
if (!trimmed) {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
return JSON.parse(trimmed);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.error('Failed to read auth file:', error);
|
|
21
|
-
throw new Error('Failed to read OpenCode auth configuration');
|
|
22
|
-
}
|
|
7
|
+
/** Configure only in the trusted host. Resolve endpoint and authentication on every call. */
|
|
8
|
+
export function configureOpenCodeCredentials(resolveClient) {
|
|
9
|
+
getClient = resolveClient;
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
export function openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) {
|
|
13
|
+
return () => makeOpenCodeV2Client({
|
|
14
|
+
baseUrl: buildOpenCodeUrl('', '').replace(/\/+$/, ''),
|
|
15
|
+
authHeaders: getOpenCodeAuthHeaders(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
fs.copyFileSync(AUTH_FILE, backupFile);
|
|
34
|
-
console.log(`Created auth backup: ${backupFile}`);
|
|
35
|
-
}
|
|
19
|
+
const credentialError = () => new Error('Unable to read OpenCode credentials');
|
|
20
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
/** Validate before projection: malformed/unsupported responses are never empty success. */
|
|
23
|
+
function validateCredentialEntries(entries) {
|
|
24
|
+
if (!Array.isArray(entries)) throw credentialError();
|
|
25
|
+
const active = new Set();
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!record(entry) || typeof entry.id !== 'string' || !entry.id || typeof entry.integrationID !== 'string'
|
|
28
|
+
|| !entry.integrationID || typeof entry.active !== 'boolean' || !record(entry.value)) throw credentialError();
|
|
29
|
+
const value = entry.value;
|
|
30
|
+
if (value.type === 'key') {
|
|
31
|
+
if (typeof value.key !== 'string') throw credentialError();
|
|
32
|
+
} else if (value.type === 'oauth') {
|
|
33
|
+
if (typeof value.access !== 'string' || typeof value.refresh !== 'string' || !Number.isFinite(value.expires)) throw credentialError();
|
|
34
|
+
} else throw credentialError();
|
|
35
|
+
if (entry.active) {
|
|
36
|
+
if (active.has(entry.integrationID)) throw credentialError();
|
|
37
|
+
active.add(entry.integrationID);
|
|
38
|
+
}
|
|
42
39
|
}
|
|
40
|
+
return entries;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!auth[providerId]) {
|
|
53
|
-
console.log(`Provider ${providerId} not found in auth file, nothing to remove`);
|
|
54
|
-
return false;
|
|
43
|
+
async function listCredentials(client) {
|
|
44
|
+
try {
|
|
45
|
+
return validateCredentialEntries(await client.credential.list({ signal: AbortSignal.timeout(8000) }));
|
|
46
|
+
} catch {
|
|
47
|
+
// Upstream errors can contain response bodies or request headers with secrets.
|
|
48
|
+
throw credentialError();
|
|
55
49
|
}
|
|
50
|
+
}
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return true;
|
|
52
|
+
function currentClient() {
|
|
53
|
+
if (!getClient) throw credentialError();
|
|
54
|
+
try { return getClient(); } catch { throw credentialError(); }
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
/** Host-only selected provider credentials. Never serialize this result to a client. */
|
|
58
|
+
export async function readOpenCodeCredentials() {
|
|
59
|
+
const entries = await listCredentials(currentClient());
|
|
60
|
+
/** @type {Record<string, ProviderCredential>} */
|
|
61
|
+
const result = Object.create(null);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.active) continue;
|
|
64
|
+
const value = entry.value;
|
|
65
|
+
result[entry.integrationID] = value.type === 'key' ? { type: 'api', key: value.key } : {
|
|
66
|
+
type: 'oauth', access: value.access, refresh: value.refresh, expires: value.expires,
|
|
67
|
+
...(typeof value.metadata?.accountID === 'string' ? { accountId: value.metadata.accountID } : {}),
|
|
68
|
+
...(typeof value.metadata?.enterpriseUrl === 'string' ? { enterpriseUrl: value.metadata.enterpriseUrl } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
return Object.keys(auth);
|
|
74
|
+
export async function getProviderAuth(providerId) {
|
|
75
|
+
return (await readOpenCodeCredentials())[providerId] || null;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
78
|
+
/** Disconnect every saved account for the provider, so deleting active cannot select another. */
|
|
79
|
+
export async function removeProviderAuth(providerId) {
|
|
80
|
+
if (typeof providerId !== 'string' || !providerId.trim()) throw new Error('Provider ID is required');
|
|
81
|
+
const client = currentClient();
|
|
82
|
+
const entries = (await listCredentials(client)).filter((entry) => entry.integrationID === providerId);
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
try {
|
|
85
|
+
await client.credential.remove({ credentialID: entry.id }, { signal: AbortSignal.timeout(8000) });
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error('Unable to remove OpenCode credentials; some accounts may already have been removed. Retry to finish disconnecting.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return entries.length > 0;
|
|
91
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { configureOpenCodeCredentials, getProviderAuth, readOpenCodeCredentials, removeProviderAuth, openCodeCredentialSource } from './auth.js';
|
|
3
|
+
|
|
4
|
+
const key = (id, active, value = id) => ({ id, integrationID: 'openai', label: id, active, value: { type: 'key', key: value } });
|
|
5
|
+
afterEach(() => { configureOpenCodeCredentials(null); vi.unstubAllGlobals(); });
|
|
6
|
+
|
|
7
|
+
describe('authoritative host credential API', () => {
|
|
8
|
+
it('observes active account switching and logout without cached or legacy fallback', async () => {
|
|
9
|
+
let entries = [key('old', true), key('new', false)];
|
|
10
|
+
configureOpenCodeCredentials(() => ({ credential: { list: async () => entries } }));
|
|
11
|
+
await expect(getProviderAuth('openai')).resolves.toEqual({ type: 'api', key: 'old' });
|
|
12
|
+
entries = [key('old', false), key('new', true)];
|
|
13
|
+
await expect(getProviderAuth('openai')).resolves.toEqual({ type: 'api', key: 'new' });
|
|
14
|
+
entries = [];
|
|
15
|
+
await expect(getProviderAuth('openai')).resolves.toBeNull();
|
|
16
|
+
});
|
|
17
|
+
it('maps OAuth metadata without copying arbitrary secret-bearing fields', async () => {
|
|
18
|
+
configureOpenCodeCredentials(() => ({ credential: { list: async () => [{ ...key('a', true), value: { type: 'oauth', access: 'access', refresh: 'refresh', expires: 123, metadata: { accountID: 'account', enterpriseUrl: 'https://example.com', unrelated: 'private' } } }] } }));
|
|
19
|
+
await expect(readOpenCodeCredentials()).resolves.toEqual({ openai: { type: 'oauth', access: 'access', refresh: 'refresh', expires: 123, accountId: 'account', enterpriseUrl: 'https://example.com' } });
|
|
20
|
+
});
|
|
21
|
+
it.each([null, {}, { data: [] }, [{ ...key('a', true), value: { type: 'unknown' } }], [key('a', true), key('b', true)]])('rejects unsupported payloads without inventing empty success', async (entries) => {
|
|
22
|
+
configureOpenCodeCredentials(() => ({ credential: { list: async () => entries } }));
|
|
23
|
+
await expect(readOpenCodeCredentials()).rejects.toThrow('Unable to read OpenCode credentials');
|
|
24
|
+
});
|
|
25
|
+
it('redacts transport/auth failures and leaves them retryable', async () => {
|
|
26
|
+
const list = vi.fn().mockRejectedValueOnce(new Error('Authorization: private-token')).mockResolvedValueOnce([]);
|
|
27
|
+
configureOpenCodeCredentials(() => ({ credential: { list } }));
|
|
28
|
+
await expect(readOpenCodeCredentials()).rejects.toThrow(/^Unable to read OpenCode credentials$/);
|
|
29
|
+
await expect(readOpenCodeCredentials()).resolves.toEqual({});
|
|
30
|
+
});
|
|
31
|
+
it('removes all provider accounts, preserves other integrations and supports retry after partial failure', async () => {
|
|
32
|
+
let entries = [key('old', false), key('new', true), { ...key('other', true), integrationID: 'anthropic' }];
|
|
33
|
+
const remove = vi.fn(async ({ credentialID }) => { entries = entries.filter((e) => e.id !== credentialID); });
|
|
34
|
+
configureOpenCodeCredentials(() => ({ credential: { list: async () => entries, remove } }));
|
|
35
|
+
remove.mockImplementationOnce(async ({ credentialID }) => { entries = entries.filter((e) => e.id !== credentialID); });
|
|
36
|
+
remove.mockRejectedValueOnce(new Error('private-token'));
|
|
37
|
+
await expect(removeProviderAuth('openai')).rejects.toThrow('some accounts may already have been removed');
|
|
38
|
+
await expect(removeProviderAuth('openai')).resolves.toBe(true);
|
|
39
|
+
expect(entries.map((e) => e.id)).toEqual(['other']);
|
|
40
|
+
await expect(removeProviderAuth('openai')).resolves.toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('uses the current endpoint/auth through the real SDK for every read', async () => {
|
|
43
|
+
let origin = 'http://first.invalid';
|
|
44
|
+
let authorization = 'Basic first';
|
|
45
|
+
const requests = [];
|
|
46
|
+
vi.stubGlobal('fetch', vi.fn(async (input, init) => {
|
|
47
|
+
const req = new Request(input, init);
|
|
48
|
+
requests.push([req.url, req.headers.get('authorization')]);
|
|
49
|
+
return Response.json({ data: [key('current', true)] });
|
|
50
|
+
}));
|
|
51
|
+
configureOpenCodeCredentials(openCodeCredentialSource({ buildOpenCodeUrl: () => origin, getOpenCodeAuthHeaders: () => ({ Authorization: authorization }) }));
|
|
52
|
+
await readOpenCodeCredentials();
|
|
53
|
+
origin = 'http://second.invalid'; authorization = 'Basic second';
|
|
54
|
+
await readOpenCodeCredentials();
|
|
55
|
+
expect(requests).toEqual([['http://first.invalid/api/credential', 'Basic first'], ['http://second.invalid/api/credential', 'Basic second']]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -114,7 +114,7 @@ function getCommandSources(commandName, workingDirectory) {
|
|
|
114
114
|
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
|
115
115
|
const jsonSection = jsonSource.section;
|
|
116
116
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
|
117
|
-
const jsonScope =
|
|
117
|
+
const jsonScope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
|
|
118
118
|
|
|
119
119
|
const sources = {
|
|
120
120
|
md: {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readConfigFile } from './config-files.js';
|
|
6
|
+
|
|
7
|
+
let root;
|
|
8
|
+
let directory;
|
|
9
|
+
let mcp;
|
|
10
|
+
let agents;
|
|
11
|
+
let providers;
|
|
12
|
+
let plugins;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-entity-layers-'));
|
|
15
|
+
directory = path.join(root, 'xdg', 'opencode');
|
|
16
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
17
|
+
vi.stubEnv('XDG_CONFIG_HOME', path.dirname(directory));
|
|
18
|
+
vi.stubEnv('OPENCODE_CONFIG', '');
|
|
19
|
+
vi.stubEnv('OPENCODE_CONFIG_DIR', '');
|
|
20
|
+
vi.spyOn(os, 'homedir').mockReturnValue(root);
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
[mcp, agents, providers, plugins] = await Promise.all([import('./mcp.js'), import('./agents.js'), import('./providers.js'), import('./plugins.js')]);
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
vi.unstubAllEnvs();
|
|
27
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Web/Electron entity physical provenance', () => {
|
|
31
|
+
it('MCP CRUD edits only the source document and preserves comments', () => {
|
|
32
|
+
const json = path.join(directory, 'opencode.json');
|
|
33
|
+
const jsonc = path.join(directory, 'opencode.jsonc');
|
|
34
|
+
fs.writeFileSync(json, '{"mcp":{"lower":{"type":"local","command":["lower"]}}}');
|
|
35
|
+
fs.writeFileSync(jsonc, '{ // retained\n"mcp":{"higher":{"type":"local","command":["higher"]}}}');
|
|
36
|
+
expect(mcp.listMcpConfigs().map((entry) => entry.name).sort()).toEqual(['higher', 'lower']);
|
|
37
|
+
mcp.updateMcpConfig('lower', { enabled: false });
|
|
38
|
+
expect(readConfigFile(json)).toMatchObject({ mcp: { lower: { enabled: false } } });
|
|
39
|
+
mcp.createMcpConfig('new', { type: 'local', command: ['new'] });
|
|
40
|
+
expect(readConfigFile(jsonc).mcp.lower).toBeUndefined();
|
|
41
|
+
mcp.deleteMcpConfig('higher');
|
|
42
|
+
expect(readConfigFile(jsonc).mcp.higher).toBeUndefined();
|
|
43
|
+
expect(fs.readFileSync(jsonc, 'utf8')).toContain('// retained');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('agent and provider metadata point to physical layers, including scoped deletion', () => {
|
|
47
|
+
const json = path.join(directory, 'opencode.json');
|
|
48
|
+
const jsonc = path.join(directory, 'opencode.jsonc');
|
|
49
|
+
fs.writeFileSync(json, '{"agent":{"lower":{"description":"lower"}},"provider":{"lower":{}}}');
|
|
50
|
+
fs.writeFileSync(jsonc, '{ // retained\n"agent":{"higher":{"description":"higher"}},"provider":{"higher":{}}}');
|
|
51
|
+
expect(agents.getAgentSources('lower').json.path).toBe(json);
|
|
52
|
+
expect(agents.getAgentSources('higher').json.path).toBe(jsonc);
|
|
53
|
+
agents.deleteAgent('lower', undefined, 'user');
|
|
54
|
+
expect(readConfigFile(json).agent).toBeUndefined();
|
|
55
|
+
expect(readConfigFile(jsonc).agent.higher).toBeDefined();
|
|
56
|
+
expect(providers.getProviderSources('lower').sources.user.path).toBe(json);
|
|
57
|
+
providers.removeProviderConfig('lower');
|
|
58
|
+
expect(readConfigFile(json).provider).toBeUndefined();
|
|
59
|
+
expect(readConfigFile(jsonc).provider.higher).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('plugin edits locate the source containing the requested spec', () => {
|
|
63
|
+
const json = path.join(directory, 'opencode.json');
|
|
64
|
+
const jsonc = path.join(directory, 'opencode.jsonc');
|
|
65
|
+
fs.writeFileSync(json, '{"plugin":["lower-plugin"]}');
|
|
66
|
+
fs.writeFileSync(jsonc, '{ // retained\n"plugin":["higher-plugin"]}');
|
|
67
|
+
const entry = plugins.listPluginEntries().find((item) => item.spec === 'lower-plugin');
|
|
68
|
+
plugins.updatePluginEntry(entry.id, { spec: 'updated-plugin' });
|
|
69
|
+
expect(readConfigFile(json).plugin).toEqual(['updated-plugin']);
|
|
70
|
+
expect(readConfigFile(jsonc).plugin).toEqual(['higher-plugin']);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { parse as parseJsonc } from 'jsonc-parser';
|
|
5
4
|
import { OpenCode } from '@opencode/client';
|
|
6
5
|
import { projectProviderCatalog } from './provider-catalog.js';
|
|
6
|
+
import { getGlobalConfigDirectory, writeConfigText } from './config-files.js';
|
|
7
7
|
|
|
8
8
|
// Compose official v2 provider.list + model.list (+ optional default) into the
|
|
9
9
|
// catalog shape consumed by projectProviderCatalog. Missing arrays are failure.
|
|
@@ -60,7 +60,7 @@ const createOpenCodeClient = ({ baseUrl, headers, fetch: fetchImpl }) =>
|
|
|
60
60
|
|
|
61
61
|
const MAX_GLOBAL_CONFIG_SIZE = 2 * 1024 * 1024;
|
|
62
62
|
const GLOBAL_CONFIG_FILES = {
|
|
63
|
-
opencode: ['opencode.
|
|
63
|
+
opencode: ['opencode.jsonc', 'opencode.json'],
|
|
64
64
|
'oh-my-opencode-slim': ['oh-my-opencode-slim.json', 'oh-my-opencode-slim.jsonc'],
|
|
65
65
|
'oh-my-openagent': ['oh-my-openagent.json', 'oh-my-openagent.jsonc'],
|
|
66
66
|
};
|
|
@@ -84,6 +84,10 @@ function resolveGlobalConfigPath(target, configDirectory) {
|
|
|
84
84
|
if (!fileNames) {
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
|
+
if (target === 'opencode' && process.env.OPENCODE_CONFIG) {
|
|
88
|
+
const filePath = path.resolve(process.env.OPENCODE_CONFIG);
|
|
89
|
+
return { target, fileNames: [path.basename(filePath)], fileName: path.basename(filePath), filePath };
|
|
90
|
+
}
|
|
87
91
|
return {
|
|
88
92
|
target,
|
|
89
93
|
fileNames,
|
|
@@ -99,7 +103,7 @@ async function findGlobalConfigPath(target, configDirectory) {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
for (const fileName of configPath.fileNames) {
|
|
102
|
-
const filePath = path.join(
|
|
106
|
+
const filePath = path.join(path.dirname(configPath.filePath), fileName);
|
|
103
107
|
try {
|
|
104
108
|
if ((await fs.stat(filePath)).isFile()) {
|
|
105
109
|
return { ...configPath, fileName, filePath };
|
|
@@ -153,13 +157,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
153
157
|
createMcpConfig,
|
|
154
158
|
updateMcpConfig,
|
|
155
159
|
deleteMcpConfig,
|
|
156
|
-
|
|
157
|
-
getSnippet,
|
|
158
|
-
createSnippet,
|
|
159
|
-
updateSnippet,
|
|
160
|
-
deleteSnippet,
|
|
161
|
-
expandSnippets,
|
|
162
|
-
configDirectory = path.join(os.homedir(), '.config', 'opencode'),
|
|
160
|
+
configDirectory = getGlobalConfigDirectory(),
|
|
163
161
|
} = dependencies;
|
|
164
162
|
|
|
165
163
|
app.get('/api/config/catalog/providers', async (req, res) => {
|
|
@@ -254,10 +252,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
254
252
|
}
|
|
255
253
|
|
|
256
254
|
try {
|
|
257
|
-
|
|
258
|
-
const temporaryPath = `${target.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
259
|
-
await fs.writeFile(temporaryPath, content, 'utf8');
|
|
260
|
-
await fs.rename(temporaryPath, target.filePath);
|
|
255
|
+
writeConfigText(content, target.filePath);
|
|
261
256
|
return res.json({
|
|
262
257
|
target: req.params.target,
|
|
263
258
|
fileName: target.fileName,
|
|
@@ -679,112 +674,4 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
679
674
|
}
|
|
680
675
|
});
|
|
681
676
|
|
|
682
|
-
app.get('/api/config/snippets', async (req, res) => {
|
|
683
|
-
try {
|
|
684
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
685
|
-
if (error) {
|
|
686
|
-
return res.status(400).json({ error });
|
|
687
|
-
}
|
|
688
|
-
res.json(listSnippets(directory));
|
|
689
|
-
} catch (error) {
|
|
690
|
-
console.error('[API:GET /api/config/snippets] Failed:', error);
|
|
691
|
-
res.status(500).json({ error: error.message || 'Failed to list snippets' });
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
app.post('/api/config/snippets/expand', async (req, res) => {
|
|
696
|
-
try {
|
|
697
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
698
|
-
if (error) {
|
|
699
|
-
return res.status(400).json({ error });
|
|
700
|
-
}
|
|
701
|
-
res.json({ text: expandSnippets(req.body?.text ?? '', directory) });
|
|
702
|
-
} catch (error) {
|
|
703
|
-
console.error('[API:POST /api/config/snippets/expand] Failed:', error);
|
|
704
|
-
res.status(500).json({ error: error.message || 'Failed to expand snippets' });
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
app.get('/api/config/snippets/:name', async (req, res) => {
|
|
709
|
-
try {
|
|
710
|
-
const name = req.params.name;
|
|
711
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
712
|
-
if (error) {
|
|
713
|
-
return res.status(400).json({ error });
|
|
714
|
-
}
|
|
715
|
-
const snippet = getSnippet(name, directory);
|
|
716
|
-
if (!snippet) {
|
|
717
|
-
return res.status(404).json({ error: `Snippet "${name}" not found` });
|
|
718
|
-
}
|
|
719
|
-
res.json(snippet);
|
|
720
|
-
} catch (error) {
|
|
721
|
-
console.error('[API:GET /api/config/snippets/:name] Failed:', error);
|
|
722
|
-
if (error.message?.includes('Snippet name')) {
|
|
723
|
-
return res.status(400).json({ error: error.message });
|
|
724
|
-
}
|
|
725
|
-
res.status(500).json({ error: error.message || 'Failed to get snippet' });
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
app.post('/api/config/snippets/:name', async (req, res) => {
|
|
730
|
-
try {
|
|
731
|
-
const name = req.params.name;
|
|
732
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
733
|
-
if (error) {
|
|
734
|
-
return res.status(400).json({ error });
|
|
735
|
-
}
|
|
736
|
-
const snippet = createSnippet(name, req.body || {}, directory, req.body?.scope || 'global');
|
|
737
|
-
res.json({ success: true, snippet });
|
|
738
|
-
} catch (error) {
|
|
739
|
-
console.error('[API:POST /api/config/snippets/:name] Failed:', error);
|
|
740
|
-
if (error.message?.includes('already exists')) {
|
|
741
|
-
return res.status(409).json({ error: error.message });
|
|
742
|
-
}
|
|
743
|
-
if (error.message?.includes('Snippet name') || error.message?.includes('Project directory')) {
|
|
744
|
-
return res.status(400).json({ error: error.message });
|
|
745
|
-
}
|
|
746
|
-
res.status(500).json({ error: error.message || 'Failed to create snippet' });
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
app.patch('/api/config/snippets/:name', async (req, res) => {
|
|
751
|
-
try {
|
|
752
|
-
const name = req.params.name;
|
|
753
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
754
|
-
if (error) {
|
|
755
|
-
return res.status(400).json({ error });
|
|
756
|
-
}
|
|
757
|
-
res.json({ success: true, snippet: updateSnippet(name, req.body || {}, directory) });
|
|
758
|
-
} catch (error) {
|
|
759
|
-
console.error('[API:PATCH /api/config/snippets/:name] Failed:', error);
|
|
760
|
-
if (error.message?.includes('not found')) {
|
|
761
|
-
return res.status(404).json({ error: error.message });
|
|
762
|
-
}
|
|
763
|
-
if (error.message?.includes('Snippet name')) {
|
|
764
|
-
return res.status(400).json({ error: error.message });
|
|
765
|
-
}
|
|
766
|
-
res.status(500).json({ error: error.message || 'Failed to update snippet' });
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
app.delete('/api/config/snippets/:name', async (req, res) => {
|
|
771
|
-
try {
|
|
772
|
-
const name = req.params.name;
|
|
773
|
-
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
774
|
-
if (error) {
|
|
775
|
-
return res.status(400).json({ error });
|
|
776
|
-
}
|
|
777
|
-
deleteSnippet(name, directory);
|
|
778
|
-
res.json({ success: true });
|
|
779
|
-
} catch (error) {
|
|
780
|
-
console.error('[API:DELETE /api/config/snippets/:name] Failed:', error);
|
|
781
|
-
if (error.message?.includes('not found')) {
|
|
782
|
-
return res.status(404).json({ error: error.message });
|
|
783
|
-
}
|
|
784
|
-
if (error.message?.includes('Snippet name')) {
|
|
785
|
-
return res.status(400).json({ error: error.message });
|
|
786
|
-
}
|
|
787
|
-
res.status(500).json({ error: error.message || 'Failed to delete snippet' });
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
677
|
};
|