@openchambery/web 2.1.7 → 2.1.8-beta.11
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-CAR4F5Sn.js → AgentAvatar-mchFM3n-.js} +1 -1
- package/dist/assets/AssistantView-Buh5CnUM.js +3 -0
- package/dist/assets/{DiagramView-Dd_3w4Hk.js → DiagramView-CS-VbxSq.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CCVddfP5.js → FadeInOnReveal-BXDM9joC.js} +1 -1
- package/dist/assets/{Icon-DDB7S6Vz.js → Icon-BGp_ize2.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-C6fDEcU1.js → ImageLoadingPlaceholder-DmUxjyxq.js} +1 -1
- package/dist/assets/{LoadingImage-ByBH-5WW.js → LoadingImage-DGjlKW_O.js} +1 -1
- package/dist/assets/MarkdownRenderer-DTal450A.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-BXgUthom.js → MarkdownRendererImpl-BtNm_zUp.js} +2 -2
- package/dist/assets/{MarkstreamRendererImpl-5W9P9U-w.js → MarkstreamRendererImpl-XWeuflcN.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Bk6C9i9l.js → MobileDetailNavigation-KY7OMEcd.js} +1 -1
- package/dist/assets/{MobileShareBridge-B5nF-gT6.js → MobileShareBridge-CniyrAC2.js} +1 -1
- package/dist/assets/MobileTabPageHeader-DS83O-io.js +2 -0
- package/dist/assets/{MultiRunWindow-CpuFaAyG.js → MultiRunWindow-DCKmDwMS.js} +1 -1
- package/dist/assets/{OnboardingScreen-DINUboLT.js → OnboardingScreen-DmJgU0Jc.js} +1 -1
- package/dist/assets/{OverlayScrollbar-BkxmoyYl.js → OverlayScrollbar-BkJCkZp3.js} +1 -1
- package/dist/assets/{SettingsView--C3eizlJ.js → SettingsView-DoWqNR1h.js} +10 -9
- package/dist/assets/{SettingsWindow-DYUvg92F.js → SettingsWindow-CPZKZbtW.js} +1 -1
- package/dist/assets/{TerminalView-BqAariXf.js → TerminalView-DTsI_jRx.js} +1 -1
- package/dist/assets/{ToolOutputDialog-D6lPUZrN.js → ToolOutputDialog-BPIcNeO8.js} +1 -1
- package/dist/assets/{appThemeRegistry-wWazoyp8.js → appThemeRegistry-BekiFLtH.js} +1 -1
- package/dist/assets/{button-DymteyMv.js → button-BKjGCkH3.js} +1 -1
- package/dist/assets/{collapsible-Dzp5rO0Q.js → collapsible-03FGLsF4.js} +1 -1
- package/dist/assets/{desktopHosts-DSBZ-9-L.js → desktopHosts-BZ7iYE-l.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DK8TzHRx.js → desktopRelayRestore-S9wEiY2U.js} +1 -1
- package/dist/assets/{diagram-CQs9px6y.js → diagram-D4QHcaSQ.js} +1 -1
- package/dist/assets/{dialog-DfUeXqdE.js → dialog-nm_cQEUZ.js} +1 -1
- package/dist/assets/{dropdown-menu.styles-B6YMNOZz.js → dropdown-menu.styles-WkR8IcF2.js} +1 -1
- package/dist/assets/es-C9PRwAQm.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-CEIhM6Ov.js → fileMentionTouchSelection-D7wzKuXd.js} +1 -1
- package/dist/assets/{fileQueries-BGE5l3Jd.js → fileQueries-DZR-mK8h.js} +1 -1
- package/dist/assets/fr-2tfGTcme.js +13 -0
- package/dist/assets/{gitApi-BP68uLBQ.js → gitApi-7Eh0Gw65.js} +1 -1
- package/dist/assets/{image-loading-qNtUCy69.js → image-loading-CsxDGgek.js} +1 -1
- package/dist/assets/{input-BvpZiRHI.js → input-Buww8enN.js} +1 -1
- package/dist/assets/insertionBoundaries-CztRpooN.js +1 -0
- package/dist/assets/ja-D7NHmhJr.js +15 -0
- package/dist/assets/ko-DeGmXmv6.js +15 -0
- package/dist/assets/{main-DSY3-ZQZ.js → main-BOHCGdL_.js} +2 -2
- package/dist/assets/main-ed7J30DP.js +169 -0
- package/dist/assets/{markdownHeightCache-MW1NYOaG.js → markdownHeightCache-B6wZ-UwD.js} +1 -1
- package/dist/assets/{mcpQueries-BaQ2_bdm.js → mcpQueries-DP3Jdcpw.js} +1 -1
- package/dist/assets/miniChat-DwmYV7Hz.js +2 -0
- package/dist/assets/mobile-Bt8QvmfL.js +2 -0
- package/dist/assets/{multirun-Blywdd6A.js → multirun-CJbaII7D.js} +1 -1
- package/dist/assets/{persistence-B7ORJ371.js → persistence-B4Ni3Gmv.js} +1 -1
- package/dist/assets/{pl-BE8MC0XQ.js → pl-CyWLY1AT.js} +3 -3
- package/dist/assets/{platform-CiBU2bCp.js → platform-BQPsr8Zl.js} +1 -1
- package/dist/assets/{projectMeta-BwPtPeMh.js → projectMeta-CjpzTdsR.js} +1 -1
- package/dist/assets/pt-BR-D9VVxvts.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BKJPbAmd.js +2 -0
- package/dist/assets/renderMobileApp-DbLhg1sk.js +28 -0
- package/dist/assets/{runtimeConfig-CqpbN2lM.js → runtimeConfig-HhNAmhW8.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-bWBhgTs_.js → runtimeEndpointReset-z8Oqvm5G.js} +15 -15
- package/dist/assets/{scheduledTaskNavigation-BnKGGUlX.js → scheduledTaskNavigation-C-HGgCQA.js} +1 -1
- package/dist/assets/sessionChangeSummary-B_MgrzNf.js +1 -0
- package/dist/assets/{sessionLookup-6gcNEZnB.js → sessionLookup-DovQh7Fb.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CWR8KhJB.js → sortable-tabs-strip-cHZM0f0Z.js} +1 -1
- package/dist/assets/speechSettingsQueries-8rvNYdbZ.js +4 -0
- package/dist/assets/{src-C9BNQK0O.css → src-OjoeiMbd.css} +1 -1
- package/dist/assets/store-Ds-7Em7C.js +15 -0
- package/dist/assets/{terminalApi-bN5IYQau.js → terminalApi-kRMttlcP.js} +1 -1
- package/dist/assets/{textarea-ClPlugKE.js → textarea-BZHD_GND.js} +1 -1
- package/dist/assets/{tooltip-D-1Pgmq1.js → tooltip-DCeWvAqN.js} +1 -1
- package/dist/assets/uk-Df44svOl.js +15 -0
- package/dist/assets/{url-nbXEti6c.js → url-Bd61gNgl.js} +1 -1
- package/dist/assets/{useAppFontEffects-8UJ5f0dG.js → useAppFontEffects-CP5PzBeS.js} +51 -53
- package/dist/assets/{useDesktopWindowControlsLayout-B_hNd3pD.js → useDesktopWindowControlsLayout-DPB0QqmV.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DISvoVLe.js → useEffectiveDirectory-d4hTR00J.js} +1 -1
- package/dist/assets/{useFontPreferences-6LD7d7hP.js → useFontPreferences-lAw0Cbza.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BboICP_n.js +417 -0
- package/dist/assets/{useWorkerHighlightedLines-BFygybD8.js → useWorkerHighlightedLines-BTe1KXFO.js} +1 -1
- package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
- package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
- package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
- package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
- package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
- package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
- package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
- package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
- package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
- package/dist/assets/zh-CN-wowfzo60.js +15 -0
- package/dist/assets/zh-TW-DwojcgA_.js +15 -0
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +1 -1
- package/server/lib/assistants/harness.js +1 -1
- package/server/lib/assistants/harness.test.js +1 -1
- package/server/lib/config-sync/DOCUMENTATION.md +38 -11
- package/server/lib/config-sync/config-sync.test.js +11 -11
- package/server/lib/config-sync/constants.js +0 -3
- package/server/lib/config-sync/contract.js +8 -8
- package/server/lib/config-sync/credentials.js +101 -0
- package/server/lib/config-sync/credentials.test.js +98 -0
- package/server/lib/config-sync/engine.js +17 -11
- package/server/lib/config-sync/index.js +0 -3
- package/server/lib/config-sync/local-backup.js +0 -13
- package/server/lib/config-sync/magpie-auth.js +111 -0
- package/server/lib/config-sync/magpie-auth.test.js +79 -0
- package/server/lib/config-sync/magpie.js +276 -0
- package/server/lib/config-sync/magpie.test.js +265 -0
- package/server/lib/config-sync/plan.js +32 -37
- package/server/lib/config-sync/receiver.js +28 -25
- package/server/lib/config-sync/routes.js +23 -2
- package/server/lib/config-sync/routes.test.js +64 -5
- package/server/lib/config-sync/scripts.js +2 -30
- package/server/lib/config-sync/selections.js +14 -10
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/auth.d.ts +1 -0
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +1 -0
- package/server/lib/opencode/lifecycle.js +4 -0
- package/server/lib/opencode/lifecycle.test.js +7 -2
- package/server/lib/opencode/plugin-routes.test.js +1 -1
- package/server/lib/opencode/plugins.js +31 -21
- package/server/lib/opencode/plugins.test.js +55 -10
- package/server/lib/session-folders/DOCUMENTATION.md +20 -0
- package/server/lib/session-folders/order-routes.js +69 -0
- package/server/lib/session-folders/routes.js +3 -0
- package/server/lib/session-folders/routes.test.js +49 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -0
- package/server/lib/session-index/sync-runtime.js +9 -4
- package/server/lib/session-index/sync-runtime.test.js +27 -0
- package/dist/assets/AssistantView-DgjRT4M4.js +0 -3
- package/dist/assets/MarkdownRenderer-YbG78e0Y.js +0 -2
- package/dist/assets/MobileTabPageHeader-D7r6pKco.js +0 -2
- package/dist/assets/es-2YvsazK2.js +0 -15
- package/dist/assets/fr-Cy0M48Gw.js +0 -13
- package/dist/assets/insertionBoundaries-BywXllQ9.js +0 -1
- package/dist/assets/ja-1zLWWJ2R.js +0 -15
- package/dist/assets/ko-BLk_5Dhc.js +0 -15
- package/dist/assets/main-DdyFxokk.js +0 -169
- package/dist/assets/miniChat-B43ASec3.js +0 -2
- package/dist/assets/mobile-DloCio4f.js +0 -2
- package/dist/assets/pt-BR-ubhNymyH.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-DCRctqrx.js +0 -2
- package/dist/assets/renderMobileApp-DuFdSBXi.js +0 -28
- package/dist/assets/sessionChangeSummary-BlR9MyHu.js +0 -2
- package/dist/assets/speechSettingsQueries-Bp5LgcR7.js +0 -4
- package/dist/assets/store-n-H5RvRn.js +0 -15
- package/dist/assets/uk-BNyyigDo.js +0 -15
- package/dist/assets/useMobileNavigationStore-BE_8Q8WJ.js +0 -417
- package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
- package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
- package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
- package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
- package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
- package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
- package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
- package/dist/assets/zh-CN-CNWzMPgL.js +0 -15
- package/dist/assets/zh-TW-DrZ9S1yn.js +0 -15
|
@@ -27,7 +27,7 @@ afterEach(async () => {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const createApp = async () => {
|
|
30
|
+
const createApp = async (credentials, magpie) => {
|
|
31
31
|
const home = await makeTempDir();
|
|
32
32
|
const configDir = path.join(home, '.config', 'opencode');
|
|
33
33
|
await fsp.mkdir(configDir, { recursive: true });
|
|
@@ -36,6 +36,8 @@ const createApp = async () => {
|
|
|
36
36
|
|
|
37
37
|
const receiver = createConfigSyncReceiver({
|
|
38
38
|
homedir: () => home,
|
|
39
|
+
...(credentials ? { credentials } : {}),
|
|
40
|
+
...(magpie ? { magpie } : {}),
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
const app = express();
|
|
@@ -44,6 +46,63 @@ const createApp = async () => {
|
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
describe('config-sync HTTP routes', () => {
|
|
49
|
+
it('discovers Magpie without import and requires a selected transfer before finalizing', async () => {
|
|
50
|
+
const payload = Buffer.from('fixture-magpie-payload');
|
|
51
|
+
const received = [];
|
|
52
|
+
const { app } = await createApp(undefined, {
|
|
53
|
+
inventory: async () => ({ available: true, installed: false }),
|
|
54
|
+
download: async () => payload,
|
|
55
|
+
import: async (buffer) => { received.push(buffer); return { ok: true, magpieWarnings: ['sign_in_required'] }; },
|
|
56
|
+
});
|
|
57
|
+
const probe = await request(app).post('/api/openchamber/config-sync/probe').send({ plan: {} });
|
|
58
|
+
expect(probe.body.inventory.magpie).toEqual({ available: true, installed: false });
|
|
59
|
+
expect(received).toEqual([]);
|
|
60
|
+
const download = await request(app).get('/api/openchamber/config-sync/download/magpie').expect(200);
|
|
61
|
+
expect(download.headers['cache-control']).toBe('no-store');
|
|
62
|
+
expect(download.body).toEqual(payload);
|
|
63
|
+
await request(app).put('/api/openchamber/config-sync/magpie')
|
|
64
|
+
.set('Content-Type', 'application/octet-stream').send(payload).expect(200);
|
|
65
|
+
const plan = { files: [], directories: [], deletes: [], magpie: { bytes: 0 } };
|
|
66
|
+
await request(app).post('/api/openchamber/config-sync/prepare').send({ plan, syncRunId: 'magpie-run' }).expect(200);
|
|
67
|
+
await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'magpie-run' }).expect(500);
|
|
68
|
+
await request(app).put('/api/openchamber/config-sync/put/magpie?syncRunId=magpie-run')
|
|
69
|
+
.set('Content-Type', 'application/octet-stream').send(payload).expect(200);
|
|
70
|
+
const receipt = await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'magpie-run' }).expect(200);
|
|
71
|
+
expect(receipt.body.magpieWarnings).toEqual(['sign_in_required']);
|
|
72
|
+
expect(received).toEqual([payload, payload]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('transfers credential bytes through HTTP without putting values in inventory or filesystem backups', async () => {
|
|
76
|
+
const payload = Buffer.from(JSON.stringify({ format: 'opencode-credentials-v1', entries: [{ fixture: 'private-value' }] }));
|
|
77
|
+
const received = [];
|
|
78
|
+
const { app, home } = await createApp({
|
|
79
|
+
inventory: async () => ({ bytes: payload.length }),
|
|
80
|
+
download: async () => payload,
|
|
81
|
+
import: async (buffer) => { received.push(buffer); return { ok: true }; },
|
|
82
|
+
});
|
|
83
|
+
const plan = { direction: 'push', files: [], directories: [], deletes: [], providerCredentials: { bytes: payload.length } };
|
|
84
|
+
const inventory = await request(app).get('/api/openchamber/config-sync/credentials/inventory');
|
|
85
|
+
expect(inventory.body).toEqual({ providerCredentials: { bytes: payload.length } });
|
|
86
|
+
const download = await request(app).get('/api/openchamber/config-sync/download/credentials');
|
|
87
|
+
expect(download.headers['cache-control']).toBe('no-store');
|
|
88
|
+
expect(download.body).toEqual(payload);
|
|
89
|
+
await request(app).post('/api/openchamber/config-sync/prepare').send({ plan, syncRunId: 'credential-run' }).expect(200);
|
|
90
|
+
await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'credential-run' }).expect(500);
|
|
91
|
+
await request(app).put('/api/openchamber/config-sync/put/credentials?syncRunId=credential-run')
|
|
92
|
+
.set('Content-Type', 'application/octet-stream').send(download.body).expect(200);
|
|
93
|
+
await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'credential-run' }).expect(200);
|
|
94
|
+
await request(app).put('/api/openchamber/config-sync/credentials')
|
|
95
|
+
.set('Content-Type', 'application/octet-stream').send(payload).expect(200);
|
|
96
|
+
expect(received).toEqual([payload, payload]);
|
|
97
|
+
await expect(fsp.stat(path.join(home, '.openchamber.sync-backup-auth'))).rejects.toThrow();
|
|
98
|
+
await expect(fsp.stat(path.join(home, '.local', 'share', 'opencode', 'auth.json'))).rejects.toThrow();
|
|
99
|
+
await request(app).post('/api/openchamber/config-sync/prepare')
|
|
100
|
+
.send({ plan: { ...plan, providerCredentials: null }, syncRunId: 'unselected' }).expect(200);
|
|
101
|
+
await request(app).put('/api/openchamber/config-sync/put/credentials?syncRunId=unselected')
|
|
102
|
+
.set('Content-Type', 'application/octet-stream').send(payload).expect(500);
|
|
103
|
+
expect(received).toHaveLength(2);
|
|
104
|
+
});
|
|
105
|
+
|
|
47
106
|
it.each(['http-config', 'http-agents', 'local-pack', 'ssh-config', 'ssh-agents'])(
|
|
48
107
|
'%s archive excludes inventory-skipped files, including inside followed symlinks',
|
|
49
108
|
async (transport) => {
|
|
@@ -91,7 +150,7 @@ describe('config-sync HTTP routes', () => {
|
|
|
91
150
|
directories: [],
|
|
92
151
|
deletes: ['config.json', 'opencode.json'],
|
|
93
152
|
agentsRoot: null,
|
|
94
|
-
|
|
153
|
+
providerCredentials: null,
|
|
95
154
|
totalBytes: 3,
|
|
96
155
|
};
|
|
97
156
|
|
|
@@ -124,7 +183,7 @@ describe('config-sync HTTP routes', () => {
|
|
|
124
183
|
directories: [],
|
|
125
184
|
deletes: [],
|
|
126
185
|
agentsRoot: null,
|
|
127
|
-
|
|
186
|
+
providerCredentials: null,
|
|
128
187
|
totalBytes: 2,
|
|
129
188
|
};
|
|
130
189
|
const first = await request(app).post('/api/openchamber/config-sync/prepare').send({ plan, syncRunId: 'run-a' });
|
|
@@ -143,7 +202,7 @@ describe('config-sync HTTP routes', () => {
|
|
|
143
202
|
directories: [],
|
|
144
203
|
deletes: [],
|
|
145
204
|
agentsRoot: null,
|
|
146
|
-
|
|
205
|
+
providerCredentials: null,
|
|
147
206
|
totalBytes: 0,
|
|
148
207
|
};
|
|
149
208
|
await request(app).post('/api/openchamber/config-sync/prepare').send({ plan, syncRunId: 'run-stream' });
|
|
@@ -151,7 +210,7 @@ describe('config-sync HTTP routes', () => {
|
|
|
151
210
|
// Tiny invalid tar still proves chunk aggregation reached extract.
|
|
152
211
|
await expect(receiver.put({
|
|
153
212
|
syncRunId: 'run-stream',
|
|
154
|
-
kind: '
|
|
213
|
+
kind: 'config',
|
|
155
214
|
stream: Readable.from([Buffer.from('aa'), Buffer.from('bb')]),
|
|
156
215
|
})).rejects.toThrow(/tar|failed|Local tar extract/i);
|
|
157
216
|
|
|
@@ -3,8 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
OPENCODE_AGENTS_ROOT_PROBE_MARKER,
|
|
5
5
|
OPENCODE_AGENTS_SYNC_BACKUP_DIR,
|
|
6
|
-
OPENCODE_AUTH_FILE_PROBE_MARKER,
|
|
7
|
-
OPENCODE_AUTH_SYNC_BACKUP_DIR,
|
|
8
6
|
OPENCODE_CONFIG_SYNC_ALLOWLIST,
|
|
9
7
|
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
10
8
|
OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
|
|
@@ -60,7 +58,7 @@ const generationalBackupSetupLines = ({ backupVar, runVar, quotedRunId, generati
|
|
|
60
58
|
* {@link OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS} are pruned; prepare no longer
|
|
61
59
|
* `rm -rf`s the entire backup root at start.
|
|
62
60
|
*
|
|
63
|
-
* @param {{ files: { path: string }[], directories: { path: string }[], deletes: string[], agentsRoot?: { fileCount: number, bytes: number } | null
|
|
61
|
+
* @param {{ files: { path: string }[], directories: { path: string }[], deletes: string[], agentsRoot?: { fileCount: number, bytes: number } | null }} plan
|
|
64
62
|
* @param {{ syncRunId: string, generations?: number }} options
|
|
65
63
|
* @returns {string}
|
|
66
64
|
*/
|
|
@@ -134,19 +132,6 @@ export const buildRemoteSyncPrepareScript = (plan, options) => {
|
|
|
134
132
|
lines.push('rm -rf -- "$HOME/.agents"');
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
if (plan?.authFile) {
|
|
138
|
-
lines.push(`AUTH_BK="$HOME/${OPENCODE_AUTH_SYNC_BACKUP_DIR}"`);
|
|
139
|
-
lines.push(...generationalBackupSetupLines({
|
|
140
|
-
backupVar: 'AUTH_BK',
|
|
141
|
-
runVar: 'AUTH_RUN_BK',
|
|
142
|
-
quotedRunId,
|
|
143
|
-
generations,
|
|
144
|
-
}));
|
|
145
|
-
lines.push('if [ -e "$HOME/.local/share/opencode/auth.json" ]; then');
|
|
146
|
-
lines.push(' cp -a "$HOME/.local/share/opencode/auth.json" "$AUTH_RUN_BK/auth.json"');
|
|
147
|
-
lines.push('fi');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
135
|
lines.push("printf 'SYNC_READY\\n'");
|
|
151
136
|
return lines.join('\n');
|
|
152
137
|
};
|
|
@@ -164,9 +149,6 @@ export const buildRemoteSyncProbeScript = (uniqueProbePaths) => {
|
|
|
164
149
|
lines.push(
|
|
165
150
|
`[ -e "$HOME/.agents" ] && printf '%s\\n' ${shellQuote(OPENCODE_AGENTS_ROOT_PROBE_MARKER)}`,
|
|
166
151
|
);
|
|
167
|
-
lines.push(
|
|
168
|
-
`[ -e "$HOME/.local/share/opencode/auth.json" ] && printf '%s\\n' ${shellQuote(OPENCODE_AUTH_FILE_PROBE_MARKER)}`,
|
|
169
|
-
);
|
|
170
152
|
lines.push('exit 0');
|
|
171
153
|
return lines.join('\n');
|
|
172
154
|
};
|
|
@@ -228,14 +210,9 @@ export const buildRemoteSyncInventoryScript = () => {
|
|
|
228
210
|
lines.push(" ABYTES=\"$(find \"$HOME/.agents\" -type f ! -path '*/node_modules/*' ! -path '*/.venv-asc/*' ! -name '*.backup' -exec wc -c {} + 2>/dev/null | awk 'END{print $1+0}')\"");
|
|
229
211
|
lines.push(' AGENTS="{\\"fileCount\\":$ACOUNT,\\"bytes\\":$ABYTES}"');
|
|
230
212
|
lines.push('fi');
|
|
231
|
-
lines.push('AUTH="null"');
|
|
232
|
-
lines.push('if [ -f "$HOME/.local/share/opencode/auth.json" ]; then');
|
|
233
|
-
lines.push(' AUBYTES="$(wc -c < "$HOME/.local/share/opencode/auth.json" | tr -d \' \')"');
|
|
234
|
-
lines.push(' AUTH="{\\"bytes\\":$AUBYTES}"');
|
|
235
|
-
lines.push('fi');
|
|
236
213
|
lines.push('FILES="${FILES%,}"');
|
|
237
214
|
lines.push('DIRS="${DIRS%,}"');
|
|
238
|
-
lines.push('printf \'SYNC_INVENTORY={"files":[%s],"directories":[%s],"agentsRoot":%s
|
|
215
|
+
lines.push('printf \'SYNC_INVENTORY={"files":[%s],"directories":[%s],"agentsRoot":%s}\\n\' "$FILES" "$DIRS" "$AGENTS"');
|
|
239
216
|
lines.push('exit 0');
|
|
240
217
|
return lines.join('\n');
|
|
241
218
|
};
|
|
@@ -265,9 +242,4 @@ export const buildRemoteAgentsTarScript = () => [
|
|
|
265
242
|
`cd "$HOME" && tar ${OPENCODE_CONFIG_SYNC_TAR_EXCLUDES.map(shellQuote).join(' ')} -h -czf - .agents`,
|
|
266
243
|
].join('\n');
|
|
267
244
|
|
|
268
|
-
export const buildRemoteAuthTarScript = () => [
|
|
269
|
-
'set -e',
|
|
270
|
-
'cd "$HOME/.local/share/opencode" && tar -h -czf - auth.json',
|
|
271
|
-
].join('\n');
|
|
272
|
-
|
|
273
245
|
export { shellQuote };
|
|
@@ -6,13 +6,14 @@ import { OPENCODE_CONFIG_SYNC_ALLOWLIST } from './constants.js';
|
|
|
6
6
|
* singleFiles: boolean[],
|
|
7
7
|
* directories: boolean[],
|
|
8
8
|
* agentsRoot: boolean,
|
|
9
|
-
*
|
|
9
|
+
* providerCredentials: boolean,
|
|
10
|
+
* magpie?: boolean,
|
|
10
11
|
* }} SyncSelections
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Default: everything selected except
|
|
15
|
-
* @param {{
|
|
15
|
+
* Default: everything selected except providerCredentials (credentials stay opt-in).
|
|
16
|
+
* @param {{ includeProviderCredentials?: boolean }} [options]
|
|
16
17
|
* @returns {SyncSelections}
|
|
17
18
|
*/
|
|
18
19
|
export const buildDefaultSyncSelections = (options = {}) => ({
|
|
@@ -20,13 +21,14 @@ export const buildDefaultSyncSelections = (options = {}) => ({
|
|
|
20
21
|
singleFiles: OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles.map(() => true),
|
|
21
22
|
directories: OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.map(() => true),
|
|
22
23
|
agentsRoot: true,
|
|
23
|
-
|
|
24
|
+
providerCredentials: options.includeProviderCredentials === true,
|
|
25
|
+
magpie: false,
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Normalize a partial selections object against the allowlist shape.
|
|
28
30
|
* @param {Partial<SyncSelections> | null | undefined} raw
|
|
29
|
-
* @param {{
|
|
31
|
+
* @param {{ includeProviderCredentials?: boolean }} [options]
|
|
30
32
|
* @returns {SyncSelections}
|
|
31
33
|
*/
|
|
32
34
|
export const normalizeSyncSelections = (raw, options = {}) => {
|
|
@@ -48,7 +50,8 @@ export const normalizeSyncSelections = (raw, options = {}) => {
|
|
|
48
50
|
singleFiles,
|
|
49
51
|
directories,
|
|
50
52
|
agentsRoot: raw.agentsRoot !== false,
|
|
51
|
-
|
|
53
|
+
providerCredentials: raw.providerCredentials === true,
|
|
54
|
+
magpie: raw.magpie === true,
|
|
52
55
|
};
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -59,7 +62,7 @@ export const normalizeSyncSelections = (raw, options = {}) => {
|
|
|
59
62
|
* @param {SyncSelections} selections
|
|
60
63
|
*/
|
|
61
64
|
export const filterPlanBySelections = (plan, selections) => {
|
|
62
|
-
const selected = normalizeSyncSelections(selections, {
|
|
65
|
+
const selected = normalizeSyncSelections(selections, { includeProviderCredentials: selections?.providerCredentials === true });
|
|
63
66
|
const selectedFileNames = new Set();
|
|
64
67
|
const selectedDeleteNames = new Set();
|
|
65
68
|
|
|
@@ -91,13 +94,13 @@ export const filterPlanBySelections = (plan, selections) => {
|
|
|
91
94
|
const directories = (Array.isArray(plan?.directories) ? plan.directories : []).filter((entry) => selectedDirNames.has(entry.path));
|
|
92
95
|
const deletes = (Array.isArray(plan?.deletes) ? plan.deletes : []).filter((name) => selectedDeleteNames.has(name) || selectedDirLegacy.has(name));
|
|
93
96
|
const agentsRoot = selected.agentsRoot ? (plan?.agentsRoot ?? null) : null;
|
|
94
|
-
const
|
|
97
|
+
const providerCredentials = selected.providerCredentials ? (plan?.providerCredentials ?? null) : null;
|
|
95
98
|
|
|
96
99
|
let totalBytes = 0;
|
|
97
100
|
for (const entry of files) totalBytes += Number(entry.bytes) || 0;
|
|
98
101
|
for (const entry of directories) totalBytes += Number(entry.bytes) || 0;
|
|
99
102
|
if (agentsRoot) totalBytes += Number(agentsRoot.bytes) || 0;
|
|
100
|
-
if (
|
|
103
|
+
if (providerCredentials) totalBytes += Number(providerCredentials.bytes) || 0;
|
|
101
104
|
|
|
102
105
|
return {
|
|
103
106
|
...plan,
|
|
@@ -105,7 +108,8 @@ export const filterPlanBySelections = (plan, selections) => {
|
|
|
105
108
|
directories,
|
|
106
109
|
deletes,
|
|
107
110
|
agentsRoot,
|
|
108
|
-
|
|
111
|
+
providerCredentials,
|
|
112
|
+
magpie: selected.magpie && plan?.magpieAvailable ? { bytes: 0 } : null,
|
|
109
113
|
totalBytes,
|
|
110
114
|
selections: selected,
|
|
111
115
|
};
|
|
@@ -19,6 +19,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
19
19
|
from the authenticated client context (null for owner UI sessions), allowing
|
|
20
20
|
connection shortcuts to exclude themselves without changing management lists.
|
|
21
21
|
- `plugins.js`: config entries and plugin-directory files are inventory, not compatibility checks. They do not synthesize `compatibility` or `compatible` fields; Settings uses the OpenCode runtime inventory for actual load status and errors.
|
|
22
|
+
- Reads native V2 `plugins` (strings or `{ package, options }`) with legacy `plugin`/tuple fallback. A native array, including an empty one, wins within each document. Entry mutations write only `plugins`, convert retained legacy tuples, and remove the shadowed legacy key in the touched source file. Options, ordering, unrelated configuration and JSONC comments are preserved through the shared atomic config writer; malformed entries or write failures do not replace the original file. Web, Electron and mobile use this host implementation; VS Code mirrors the same persistence contract.
|
|
22
23
|
- `session-directory-runtime.js`: Host-side physical directory identity for session index/list/detail/live projection. Uses a runtime-scoped, bounded 256-entry cache (60s success / 5s failed resolution), cleared when the upstream URL changes. Local directory lists use the upstream project identity and physical-directory filtering to include legacy symlink sessions without mixing worktrees. One request reads one page and preserves opaque cursors; consumers must follow continuations even after filtered-empty pages. Non-loopback upstreams keep their own path namespace.
|
|
23
24
|
- `packages/web/server/lib/opencode/index.js`: public entrypoint (currently baseline placeholder).
|
|
24
25
|
- `packages/web/server/lib/opencode/auth.js`: host-only authoritative provider credential API reads and deletion.
|
|
@@ -27,6 +28,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
27
28
|
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, forces `OPENCHAMBER_RUNTIME=web` unless already `ssh-remote` (so `dev:server` cannot inherit a leftover desktop env and host a relay, while SSH-managed remotes may), parses CLI options, and starts server bootstrap.
|
|
28
29
|
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
|
|
29
30
|
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). Managed child env inherits the user shell environment unchanged; experimental flags such as `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` follow the user's own environment and are never injected by OpenChamber.
|
|
31
|
+
- Before shared-service/private startup, config-sync's Magpie adapter repairs the discoverable command entry for an existing OpenChamber-managed Magpie CLI. No download occurs at startup. Errors remain explicit rather than starting OpenCode with an undiscoverable managed dependency. External services are not modified.
|
|
30
32
|
- `packages/web/server/lib/opencode/v1-migration-gate.js`: pure V1→v2 migration admission gate (`GET /api/experimental/migration/v1` → whether transcript may be fetched).
|
|
31
33
|
- `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
|
|
32
34
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
@@ -73,11 +75,12 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
73
75
|
|
|
74
76
|
## Public exports (auth.js)
|
|
75
77
|
- `configureOpenCodeCredentials(resolveClient)`: host composition injects a call-time SDK client resolver; `null` detaches it. Web/Electron share server wiring; VS Code wires its manager in extension activation. Mobile clients use their host.
|
|
78
|
+
- `currentClient()`: trusted host-only client resolver used by config-sync to retain full V2 credential account records instead of the active-account quota projection.
|
|
76
79
|
- `openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders })`: resolves the current endpoint/auth at each read rather than capturing a previous runtime.
|
|
77
80
|
- `readOpenCodeCredentials()`: async SDK `credential.list()` read of the running service; validates entries and projects only active accounts into the quota/TTS credential shape. OAuth `metadata.accountID` becomes `accountId`. No local `auth.json`, local environment, or local provider-config fallback; external service environment-only keys are not exposed by the credential API.
|
|
78
81
|
- `getProviderAuth(providerId)`: async selected stored credential or authoritative `null`; network/auth/schema failures reject with a sanitized error, never empty success.
|
|
79
82
|
- `removeProviderAuth(providerId)`: deletes every stored account for that integration through SDK `credential.remove`, preserving unrelated integrations. Partial failure rejects explicitly; retry lists remaining accounts. Auth-only disconnect requires no process restart.
|
|
80
|
-
- Credential values remain host-only. `credential-boundary.js` blocks raw credential list/creation responses (GET/HEAD/POST `/api/credential`, including encoded/trailing-slash forms) in Web proxy and VS Code bridge because v2 replies contain keys. Provider-source responses expose only existence; errors never include upstream credential bodies/headers.
|
|
83
|
+
- Credential values remain host-only outside explicitly selected instance sync. `credential-boundary.js` blocks raw credential list/creation responses (GET/HEAD/POST `/api/credential`, including encoded/trailing-slash forms) in Web proxy and VS Code bridge because v2 replies contain keys. Provider-source responses expose only existence; errors never include upstream credential bodies/headers. The authenticated config-sync routes own explicit V2 credential transfer; previews and run logs contain only metadata.
|
|
81
84
|
|
|
82
85
|
## Public exports (shared.js)
|
|
83
86
|
- `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`, `CUSTOM_CONFIG_FILE`: Path constants.
|
|
@@ -4,6 +4,7 @@ export type ProviderCredential =
|
|
|
4
4
|
| { type: 'api'; key: string }
|
|
5
5
|
| { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string };
|
|
6
6
|
export function configureOpenCodeCredentials(resolveClient: (() => OpenCodeClient) | null): void;
|
|
7
|
+
export function currentClient(): OpenCodeClient;
|
|
7
8
|
export function openCodeCredentialSource(deps: {
|
|
8
9
|
buildOpenCodeUrl: (path: string, prefix?: string) => string;
|
|
9
10
|
getOpenCodeAuthHeaders: () => Record<string, string>;
|
|
@@ -66,7 +66,8 @@ describe('Web/Electron entity physical provenance', () => {
|
|
|
66
66
|
fs.writeFileSync(jsonc, '{ // retained\n"plugin":["higher-plugin"]}');
|
|
67
67
|
const entry = plugins.listPluginEntries().find((item) => item.spec === 'lower-plugin');
|
|
68
68
|
plugins.updatePluginEntry(entry.id, { spec: 'updated-plugin' });
|
|
69
|
-
expect(readConfigFile(json).
|
|
69
|
+
expect(readConfigFile(json).plugins).toEqual(['updated-plugin']);
|
|
70
|
+
expect(readConfigFile(json).plugin).toBeUndefined();
|
|
70
71
|
expect(readConfigFile(jsonc).plugin).toEqual(['higher-plugin']);
|
|
71
72
|
});
|
|
72
73
|
});
|
|
@@ -6,6 +6,7 @@ import { readOpenCode2BinaryVersion } from './ensure-cli.js';
|
|
|
6
6
|
import { createRevokedRuntimeContract, evaluateRuntimeContract } from './runtime-contract.js';
|
|
7
7
|
import { OPENCODE_V1_MIGRATION_PATH, fetchV1MigrationGate } from './v1-migration-gate.js';
|
|
8
8
|
import { isProcessAlive } from './shared-service.js';
|
|
9
|
+
import { createMagpieSync } from '../config-sync/magpie.js';
|
|
9
10
|
|
|
10
11
|
const parsePositiveInt = (value, fallback) => {
|
|
11
12
|
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
@@ -71,6 +72,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
71
72
|
buildAugmentedPath,
|
|
72
73
|
buildManagedOpenCodePath,
|
|
73
74
|
getManagedOpenCodeShellEnvSnapshot,
|
|
75
|
+
prepareMagpieInstallation = () => createMagpieSync().repairInstallation(),
|
|
74
76
|
managedCapabilitiesRuntime = null,
|
|
75
77
|
getActiveSessionCount = () => 0,
|
|
76
78
|
// Official background service (shared-service.js). null keeps the private
|
|
@@ -691,6 +693,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
691
693
|
* or the registration is missing/malformed; the caller decides rollback.
|
|
692
694
|
*/
|
|
693
695
|
const connectSharedOpenCodeService = async ({ binaryPath } = {}) => {
|
|
696
|
+
await prepareMagpieInstallation();
|
|
694
697
|
if (!binaryPath) {
|
|
695
698
|
await applyOpencodeBinaryFromSettings({ strict: true });
|
|
696
699
|
if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
|
|
@@ -793,6 +796,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
793
796
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
794
797
|
|
|
795
798
|
const startOpenCodeOnce = async () => {
|
|
799
|
+
await prepareMagpieInstallation();
|
|
796
800
|
// New managed instance — drop any prior permit before spawn/probes.
|
|
797
801
|
const generationAtStart = revokeRuntimeExecutionPermit('managed-start');
|
|
798
802
|
const requestedPort = env.ENV_CONFIGURED_OPENCODE_PORT;
|
|
@@ -101,6 +101,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
101
101
|
normalizeApiPrefix: vi.fn(() => ''),
|
|
102
102
|
applyOpencodeBinaryFromSettings: vi.fn(async () => null),
|
|
103
103
|
ensureOpencodeCliEnv: vi.fn(),
|
|
104
|
+
prepareMagpieInstallation: vi.fn(async () => {}),
|
|
104
105
|
ensureLocalOpenCodeServerPassword: vi.fn(async () => 'password'),
|
|
105
106
|
resolveManagedOpenCodeLaunchSpec: vi.fn((binary) => ({ binary, args: [], wrapperType: null })),
|
|
106
107
|
setOpenCodePort: vi.fn((port) => {
|
|
@@ -195,14 +196,16 @@ describe('OpenCode lifecycle', () => {
|
|
|
195
196
|
it('launches managed OpenCode with the managed PATH', async () => {
|
|
196
197
|
delete process.env.OPENCODE_BINARY;
|
|
197
198
|
const child = createMockChild();
|
|
199
|
+
const prepareMagpieInstallation = vi.fn(async () => {});
|
|
198
200
|
spawnMock.mockImplementationOnce(() => {
|
|
201
|
+
expect(prepareMagpieInstallation).toHaveBeenCalledTimes(1);
|
|
199
202
|
queueMicrotask(() => {
|
|
200
203
|
child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
|
|
201
204
|
});
|
|
202
205
|
return child;
|
|
203
206
|
});
|
|
204
207
|
|
|
205
|
-
const runtime = createRuntime();
|
|
208
|
+
const runtime = createRuntime({ prepareMagpieInstallation });
|
|
206
209
|
const server = await runtime.startOpenCode();
|
|
207
210
|
const [binary, args, options] = spawnMock.mock.calls[0];
|
|
208
211
|
|
|
@@ -1100,13 +1103,15 @@ describe('shared official OpenCode service', () => {
|
|
|
1100
1103
|
const sharedService = createSharedService();
|
|
1101
1104
|
const adoptOpenCodeServerPassword = vi.fn();
|
|
1102
1105
|
const stateRef = {};
|
|
1103
|
-
const
|
|
1106
|
+
const prepareMagpieInstallation = vi.fn(async () => {});
|
|
1107
|
+
const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword, prepareMagpieInstallation }, stateRef);
|
|
1104
1108
|
const fetchMock = stubSharedFetch();
|
|
1105
1109
|
|
|
1106
1110
|
await runtime.bootstrapOpenCodeAtStartup();
|
|
1107
1111
|
|
|
1108
1112
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1109
1113
|
expect(sharedService.start).toHaveBeenCalledTimes(1);
|
|
1114
|
+
expect(prepareMagpieInstallation.mock.invocationCallOrder[0]).toBeLessThan(sharedService.start.mock.invocationCallOrder[0]);
|
|
1110
1115
|
const startInput = sharedService.start.mock.calls[0][0];
|
|
1111
1116
|
expect(startInput.binary).toBe('/mock/opencode');
|
|
1112
1117
|
expect(startInput.env.OPENCODE_SERVER_PASSWORD).toBeUndefined();
|
|
@@ -316,7 +316,7 @@ describe('opencode plugin routes', () => {
|
|
|
316
316
|
|
|
317
317
|
const after = await request(app).get('/api/config/plugins').expect(200);
|
|
318
318
|
expect(after.body.entries).toEqual([]);
|
|
319
|
-
expect(readJson(userConfigPath).
|
|
319
|
+
expect(readJson(userConfigPath).plugins).toBeUndefined();
|
|
320
320
|
expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
|
|
321
321
|
});
|
|
322
322
|
|
|
@@ -79,6 +79,25 @@ function readPluginConfigLayers(workingDirectory) {
|
|
|
79
79
|
return readConfigLayers(workingDirectory);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function readPluginArray(config) {
|
|
83
|
+
if (Array.isArray(config.plugins)) return config.plugins;
|
|
84
|
+
return Array.isArray(config.plugin) ? config.plugin : [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writePluginArray(config, plugin, filePath) {
|
|
88
|
+
const next = { ...config };
|
|
89
|
+
delete next.plugin;
|
|
90
|
+
if (plugin.length > 0) {
|
|
91
|
+
next.plugins = plugin.map((raw) => {
|
|
92
|
+
const parsed = parsePluginRaw(raw);
|
|
93
|
+
return Array.isArray(raw) ? serializePluginEntry(parsed) : raw;
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
delete next.plugins;
|
|
97
|
+
}
|
|
98
|
+
writeConfig(next, filePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
function validateFileName(fileName) {
|
|
83
102
|
if (typeof fileName !== 'string' || !fileName) {
|
|
84
103
|
throw codedError('Plugin file name is required', 'INVALID_FILENAME');
|
|
@@ -125,9 +144,8 @@ function getPluginTarget(id, workingDirectory) {
|
|
|
125
144
|
validateScope(scope);
|
|
126
145
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
127
146
|
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
128
|
-
&&
|
|
129
|
-
|
|
130
|
-
const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
|
|
147
|
+
&& readPluginArray(candidate.config).some((raw) => parsePluginRaw(raw).spec === spec));
|
|
148
|
+
const plugin = source ? [...readPluginArray(source.config)] : [];
|
|
131
149
|
const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
|
|
132
150
|
if (!source || index === -1) {
|
|
133
151
|
return null;
|
|
@@ -181,13 +199,16 @@ function parsePluginRaw(raw) {
|
|
|
181
199
|
if (Array.isArray(raw) && raw.length === 2 && isRecord(raw[1])) {
|
|
182
200
|
return { spec: validatePluginSpec(raw[0]), options: { ...raw[1] } };
|
|
183
201
|
}
|
|
184
|
-
|
|
202
|
+
if (isRecord(raw) && (raw.options === undefined || isRecord(raw.options))) {
|
|
203
|
+
return { spec: validatePluginSpec(raw.package), ...(raw.options === undefined ? {} : { options: { ...raw.options } }) };
|
|
204
|
+
}
|
|
205
|
+
throw codedError('Plugin spec must be a string or { package, options } object', 'INVALID_SPEC');
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
function serializePluginEntry(entry) {
|
|
188
209
|
const spec = validatePluginSpec(entry?.spec);
|
|
189
210
|
if (hasOptions(entry?.options)) {
|
|
190
|
-
return
|
|
211
|
+
return { package: spec, options: { ...entry.options } };
|
|
191
212
|
}
|
|
192
213
|
return spec;
|
|
193
214
|
}
|
|
@@ -195,10 +216,7 @@ function serializePluginEntry(entry) {
|
|
|
195
216
|
function listPluginEntries(workingDirectory) {
|
|
196
217
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
197
218
|
return configSources(layers).flatMap((source) => {
|
|
198
|
-
|
|
199
|
-
return [];
|
|
200
|
-
}
|
|
201
|
-
return source.config.plugin.map((raw) => {
|
|
219
|
+
return readPluginArray(source.config).map((raw) => {
|
|
202
220
|
const parsed = parsePluginRaw(raw);
|
|
203
221
|
return {
|
|
204
222
|
id: encodePluginId('config', `${source.scope}:${parsed.spec}`),
|
|
@@ -225,8 +243,7 @@ function createPluginEntry(entry, workingDirectory) {
|
|
|
225
243
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
226
244
|
const existing = configSources(layers).find((source) => (
|
|
227
245
|
source.scope === scope
|
|
228
|
-
&&
|
|
229
|
-
&& source.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec)
|
|
246
|
+
&& readPluginArray(source.config).some((raw) => parsePluginRaw(raw).spec === spec)
|
|
230
247
|
));
|
|
231
248
|
if (existing) {
|
|
232
249
|
throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
|
|
@@ -242,11 +259,7 @@ function createPluginEntry(entry, workingDirectory) {
|
|
|
242
259
|
config = getConfigForPath(layers, targetPath);
|
|
243
260
|
}
|
|
244
261
|
|
|
245
|
-
|
|
246
|
-
config.plugin = [];
|
|
247
|
-
}
|
|
248
|
-
config.plugin.push(serializePluginEntry({ spec, options: entry.options }));
|
|
249
|
-
writeConfig(config, targetPath);
|
|
262
|
+
writePluginArray(config, [...readPluginArray(config), serializePluginEntry({ spec, options: entry.options })], targetPath);
|
|
250
263
|
}
|
|
251
264
|
|
|
252
265
|
function updatePluginEntry(id, updates, workingDirectory) {
|
|
@@ -258,7 +271,7 @@ function updatePluginEntry(id, updates, workingDirectory) {
|
|
|
258
271
|
const nextSpec = updates?.spec === undefined ? existing.spec : validatePluginSpec(updates.spec);
|
|
259
272
|
const nextOptions = updates?.options === undefined ? existing.options : updates.options;
|
|
260
273
|
target.plugin[target.index] = serializePluginEntry({ spec: nextSpec, options: nextOptions });
|
|
261
|
-
|
|
274
|
+
writePluginArray(target.source.config, target.plugin, target.source.filePath);
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
function deletePluginEntry(id, workingDirectory) {
|
|
@@ -267,10 +280,7 @@ function deletePluginEntry(id, workingDirectory) {
|
|
|
267
280
|
throw codedError('Plugin entry not found', 'NOT_FOUND');
|
|
268
281
|
}
|
|
269
282
|
target.plugin.splice(target.index, 1);
|
|
270
|
-
|
|
271
|
-
delete target.source.config.plugin;
|
|
272
|
-
}
|
|
273
|
-
writeConfig(target.source.config, target.source.filePath);
|
|
283
|
+
writePluginArray(target.source.config, target.plugin, target.source.filePath);
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
function listPluginDirFiles(workingDirectory) {
|