@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.
Files changed (168) hide show
  1. package/dist/assets/{AgentAvatar-CAR4F5Sn.js → AgentAvatar-mchFM3n-.js} +1 -1
  2. package/dist/assets/AssistantView-Buh5CnUM.js +3 -0
  3. package/dist/assets/{DiagramView-Dd_3w4Hk.js → DiagramView-CS-VbxSq.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-CCVddfP5.js → FadeInOnReveal-BXDM9joC.js} +1 -1
  5. package/dist/assets/{Icon-DDB7S6Vz.js → Icon-BGp_ize2.js} +1 -1
  6. package/dist/assets/{ImageLoadingPlaceholder-C6fDEcU1.js → ImageLoadingPlaceholder-DmUxjyxq.js} +1 -1
  7. package/dist/assets/{LoadingImage-ByBH-5WW.js → LoadingImage-DGjlKW_O.js} +1 -1
  8. package/dist/assets/MarkdownRenderer-DTal450A.js +2 -0
  9. package/dist/assets/{MarkdownRendererImpl-BXgUthom.js → MarkdownRendererImpl-BtNm_zUp.js} +2 -2
  10. package/dist/assets/{MarkstreamRendererImpl-5W9P9U-w.js → MarkstreamRendererImpl-XWeuflcN.js} +1 -1
  11. package/dist/assets/{MobileDetailNavigation-Bk6C9i9l.js → MobileDetailNavigation-KY7OMEcd.js} +1 -1
  12. package/dist/assets/{MobileShareBridge-B5nF-gT6.js → MobileShareBridge-CniyrAC2.js} +1 -1
  13. package/dist/assets/MobileTabPageHeader-DS83O-io.js +2 -0
  14. package/dist/assets/{MultiRunWindow-CpuFaAyG.js → MultiRunWindow-DCKmDwMS.js} +1 -1
  15. package/dist/assets/{OnboardingScreen-DINUboLT.js → OnboardingScreen-DmJgU0Jc.js} +1 -1
  16. package/dist/assets/{OverlayScrollbar-BkxmoyYl.js → OverlayScrollbar-BkJCkZp3.js} +1 -1
  17. package/dist/assets/{SettingsView--C3eizlJ.js → SettingsView-DoWqNR1h.js} +10 -9
  18. package/dist/assets/{SettingsWindow-DYUvg92F.js → SettingsWindow-CPZKZbtW.js} +1 -1
  19. package/dist/assets/{TerminalView-BqAariXf.js → TerminalView-DTsI_jRx.js} +1 -1
  20. package/dist/assets/{ToolOutputDialog-D6lPUZrN.js → ToolOutputDialog-BPIcNeO8.js} +1 -1
  21. package/dist/assets/{appThemeRegistry-wWazoyp8.js → appThemeRegistry-BekiFLtH.js} +1 -1
  22. package/dist/assets/{button-DymteyMv.js → button-BKjGCkH3.js} +1 -1
  23. package/dist/assets/{collapsible-Dzp5rO0Q.js → collapsible-03FGLsF4.js} +1 -1
  24. package/dist/assets/{desktopHosts-DSBZ-9-L.js → desktopHosts-BZ7iYE-l.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DK8TzHRx.js → desktopRelayRestore-S9wEiY2U.js} +1 -1
  26. package/dist/assets/{diagram-CQs9px6y.js → diagram-D4QHcaSQ.js} +1 -1
  27. package/dist/assets/{dialog-DfUeXqdE.js → dialog-nm_cQEUZ.js} +1 -1
  28. package/dist/assets/{dropdown-menu.styles-B6YMNOZz.js → dropdown-menu.styles-WkR8IcF2.js} +1 -1
  29. package/dist/assets/es-C9PRwAQm.js +15 -0
  30. package/dist/assets/{fileMentionTouchSelection-CEIhM6Ov.js → fileMentionTouchSelection-D7wzKuXd.js} +1 -1
  31. package/dist/assets/{fileQueries-BGE5l3Jd.js → fileQueries-DZR-mK8h.js} +1 -1
  32. package/dist/assets/fr-2tfGTcme.js +13 -0
  33. package/dist/assets/{gitApi-BP68uLBQ.js → gitApi-7Eh0Gw65.js} +1 -1
  34. package/dist/assets/{image-loading-qNtUCy69.js → image-loading-CsxDGgek.js} +1 -1
  35. package/dist/assets/{input-BvpZiRHI.js → input-Buww8enN.js} +1 -1
  36. package/dist/assets/insertionBoundaries-CztRpooN.js +1 -0
  37. package/dist/assets/ja-D7NHmhJr.js +15 -0
  38. package/dist/assets/ko-DeGmXmv6.js +15 -0
  39. package/dist/assets/{main-DSY3-ZQZ.js → main-BOHCGdL_.js} +2 -2
  40. package/dist/assets/main-ed7J30DP.js +169 -0
  41. package/dist/assets/{markdownHeightCache-MW1NYOaG.js → markdownHeightCache-B6wZ-UwD.js} +1 -1
  42. package/dist/assets/{mcpQueries-BaQ2_bdm.js → mcpQueries-DP3Jdcpw.js} +1 -1
  43. package/dist/assets/miniChat-DwmYV7Hz.js +2 -0
  44. package/dist/assets/mobile-Bt8QvmfL.js +2 -0
  45. package/dist/assets/{multirun-Blywdd6A.js → multirun-CJbaII7D.js} +1 -1
  46. package/dist/assets/{persistence-B7ORJ371.js → persistence-B4Ni3Gmv.js} +1 -1
  47. package/dist/assets/{pl-BE8MC0XQ.js → pl-CyWLY1AT.js} +3 -3
  48. package/dist/assets/{platform-CiBU2bCp.js → platform-BQPsr8Zl.js} +1 -1
  49. package/dist/assets/{projectMeta-BwPtPeMh.js → projectMeta-CjpzTdsR.js} +1 -1
  50. package/dist/assets/pt-BR-D9VVxvts.js +15 -0
  51. package/dist/assets/renderElectronMiniChatApp-BKJPbAmd.js +2 -0
  52. package/dist/assets/renderMobileApp-DbLhg1sk.js +28 -0
  53. package/dist/assets/{runtimeConfig-CqpbN2lM.js → runtimeConfig-HhNAmhW8.js} +1 -1
  54. package/dist/assets/{runtimeEndpointReset-bWBhgTs_.js → runtimeEndpointReset-z8Oqvm5G.js} +15 -15
  55. package/dist/assets/{scheduledTaskNavigation-BnKGGUlX.js → scheduledTaskNavigation-C-HGgCQA.js} +1 -1
  56. package/dist/assets/sessionChangeSummary-B_MgrzNf.js +1 -0
  57. package/dist/assets/{sessionLookup-6gcNEZnB.js → sessionLookup-DovQh7Fb.js} +1 -1
  58. package/dist/assets/{sortable-tabs-strip-CWR8KhJB.js → sortable-tabs-strip-cHZM0f0Z.js} +1 -1
  59. package/dist/assets/speechSettingsQueries-8rvNYdbZ.js +4 -0
  60. package/dist/assets/{src-C9BNQK0O.css → src-OjoeiMbd.css} +1 -1
  61. package/dist/assets/store-Ds-7Em7C.js +15 -0
  62. package/dist/assets/{terminalApi-bN5IYQau.js → terminalApi-kRMttlcP.js} +1 -1
  63. package/dist/assets/{textarea-ClPlugKE.js → textarea-BZHD_GND.js} +1 -1
  64. package/dist/assets/{tooltip-D-1Pgmq1.js → tooltip-DCeWvAqN.js} +1 -1
  65. package/dist/assets/uk-Df44svOl.js +15 -0
  66. package/dist/assets/{url-nbXEti6c.js → url-Bd61gNgl.js} +1 -1
  67. package/dist/assets/{useAppFontEffects-8UJ5f0dG.js → useAppFontEffects-CP5PzBeS.js} +51 -53
  68. package/dist/assets/{useDesktopWindowControlsLayout-B_hNd3pD.js → useDesktopWindowControlsLayout-DPB0QqmV.js} +1 -1
  69. package/dist/assets/{useEffectiveDirectory-DISvoVLe.js → useEffectiveDirectory-d4hTR00J.js} +1 -1
  70. package/dist/assets/{useFontPreferences-6LD7d7hP.js → useFontPreferences-lAw0Cbza.js} +1 -1
  71. package/dist/assets/useMobileNavigationStore-BboICP_n.js +417 -0
  72. package/dist/assets/{useWorkerHighlightedLines-BFygybD8.js → useWorkerHighlightedLines-BTe1KXFO.js} +1 -1
  73. package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
  74. package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
  75. package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
  76. package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
  77. package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
  78. package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
  79. package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
  83. package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
  84. package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
  85. package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
  86. package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
  87. package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
  88. package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
  94. package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
  95. package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
  96. package/dist/assets/zh-CN-wowfzo60.js +15 -0
  97. package/dist/assets/zh-TW-DwojcgA_.js +15 -0
  98. package/dist/index.html +8 -8
  99. package/dist/mini-chat.html +8 -8
  100. package/dist/mobile.html +8 -8
  101. package/package.json +1 -1
  102. package/server/lib/assistants/DOCUMENTATION.md +1 -1
  103. package/server/lib/assistants/harness.js +1 -1
  104. package/server/lib/assistants/harness.test.js +1 -1
  105. package/server/lib/config-sync/DOCUMENTATION.md +38 -11
  106. package/server/lib/config-sync/config-sync.test.js +11 -11
  107. package/server/lib/config-sync/constants.js +0 -3
  108. package/server/lib/config-sync/contract.js +8 -8
  109. package/server/lib/config-sync/credentials.js +101 -0
  110. package/server/lib/config-sync/credentials.test.js +98 -0
  111. package/server/lib/config-sync/engine.js +17 -11
  112. package/server/lib/config-sync/index.js +0 -3
  113. package/server/lib/config-sync/local-backup.js +0 -13
  114. package/server/lib/config-sync/magpie-auth.js +111 -0
  115. package/server/lib/config-sync/magpie-auth.test.js +79 -0
  116. package/server/lib/config-sync/magpie.js +276 -0
  117. package/server/lib/config-sync/magpie.test.js +265 -0
  118. package/server/lib/config-sync/plan.js +32 -37
  119. package/server/lib/config-sync/receiver.js +28 -25
  120. package/server/lib/config-sync/routes.js +23 -2
  121. package/server/lib/config-sync/routes.test.js +64 -5
  122. package/server/lib/config-sync/scripts.js +2 -30
  123. package/server/lib/config-sync/selections.js +14 -10
  124. package/server/lib/opencode/DOCUMENTATION.md +4 -1
  125. package/server/lib/opencode/auth.d.ts +1 -0
  126. package/server/lib/opencode/auth.js +1 -1
  127. package/server/lib/opencode/config-entities-layering.test.js +2 -1
  128. package/server/lib/opencode/feature-routes-runtime.js +1 -0
  129. package/server/lib/opencode/lifecycle.js +4 -0
  130. package/server/lib/opencode/lifecycle.test.js +7 -2
  131. package/server/lib/opencode/plugin-routes.test.js +1 -1
  132. package/server/lib/opencode/plugins.js +31 -21
  133. package/server/lib/opencode/plugins.test.js +55 -10
  134. package/server/lib/session-folders/DOCUMENTATION.md +20 -0
  135. package/server/lib/session-folders/order-routes.js +69 -0
  136. package/server/lib/session-folders/routes.js +3 -0
  137. package/server/lib/session-folders/routes.test.js +49 -0
  138. package/server/lib/session-index/DOCUMENTATION.md +3 -0
  139. package/server/lib/session-index/sync-runtime.js +9 -4
  140. package/server/lib/session-index/sync-runtime.test.js +27 -0
  141. package/dist/assets/AssistantView-DgjRT4M4.js +0 -3
  142. package/dist/assets/MarkdownRenderer-YbG78e0Y.js +0 -2
  143. package/dist/assets/MobileTabPageHeader-D7r6pKco.js +0 -2
  144. package/dist/assets/es-2YvsazK2.js +0 -15
  145. package/dist/assets/fr-Cy0M48Gw.js +0 -13
  146. package/dist/assets/insertionBoundaries-BywXllQ9.js +0 -1
  147. package/dist/assets/ja-1zLWWJ2R.js +0 -15
  148. package/dist/assets/ko-BLk_5Dhc.js +0 -15
  149. package/dist/assets/main-DdyFxokk.js +0 -169
  150. package/dist/assets/miniChat-B43ASec3.js +0 -2
  151. package/dist/assets/mobile-DloCio4f.js +0 -2
  152. package/dist/assets/pt-BR-ubhNymyH.js +0 -15
  153. package/dist/assets/renderElectronMiniChatApp-DCRctqrx.js +0 -2
  154. package/dist/assets/renderMobileApp-DuFdSBXi.js +0 -28
  155. package/dist/assets/sessionChangeSummary-BlR9MyHu.js +0 -2
  156. package/dist/assets/speechSettingsQueries-Bp5LgcR7.js +0 -4
  157. package/dist/assets/store-n-H5RvRn.js +0 -15
  158. package/dist/assets/uk-BNyyigDo.js +0 -15
  159. package/dist/assets/useMobileNavigationStore-BE_8Q8WJ.js +0 -417
  160. package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
  161. package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
  162. package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
  163. package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
  164. package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
  165. package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
  166. package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
  167. package/dist/assets/zh-CN-CNWzMPgL.js +0 -15
  168. package/dist/assets/zh-TW-DrZ9S1yn.js +0 -15
@@ -0,0 +1,265 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import { createMagpieSync } from './magpie.js';
7
+ import { planOpenCodeConfigSync, planOpenCodeConfigSyncFromInventory } from './plan.js';
8
+ import { applyConfigSyncPlan } from './engine.js';
9
+
10
+ const dirs = [];
11
+ const machine = async () => {
12
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-magpie-test-'));
13
+ dirs.push(home);
14
+ return { home, options: {
15
+ homedir: () => home, tempdir: () => home, platform: 'linux', arch: 'x64',
16
+ env: { HOME: home, PATH: '', XDG_CONFIG_HOME: path.join(home, '.config') },
17
+ } };
18
+ };
19
+ const write = async (file, content = '{}') => {
20
+ await fs.mkdir(path.dirname(file), { recursive: true });
21
+ await fs.writeFile(file, content);
22
+ };
23
+ afterEach(async () => { await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); });
24
+
25
+ describe('Magpie discovery and native transfer', () => {
26
+ it('hides config-only sources until Magpie is installed, without installing during preview', async () => {
27
+ const { home, options } = await machine();
28
+ const fetchImpl = vi.fn();
29
+ const sync = createMagpieSync({ ...options, fetchImpl });
30
+ expect(await sync.inventory()).toEqual({ available: false, installed: false });
31
+ await write(path.join(home, '.config', 'magpie', 'providers.json'));
32
+ expect(await sync.inventory()).toEqual({ available: false, installed: false });
33
+ expect(fetchImpl).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it('resolves a symlinked client executable and its portable configuration', async () => {
37
+ const { home, options } = await machine();
38
+ const app = path.join(home, 'portable', 'magpie.app', 'Contents', 'MacOS', 'magpie');
39
+ await write(app, 'fixture-binary');
40
+ await write(path.join(home, 'portable', '.portable'), '');
41
+ await write(path.join(home, 'portable', 'data', 'providers.json'));
42
+ const link = path.join(home, 'magpie');
43
+ await fs.symlink(app, link);
44
+ const run = vi.fn(async () => 'magpie 0.1.0');
45
+ const sync = createMagpieSync({ ...options, env: { ...options.env, MAGPIE_BINARY: link }, run });
46
+ expect(await sync.inventory()).toEqual({ available: true, installed: true });
47
+ expect(run.mock.calls[0][0]).toBe(await fs.realpath(app));
48
+ });
49
+
50
+ it('exports noninteractively, installs a verified destination CLI and cleans temporary files', async () => {
51
+ const source = await machine();
52
+ const target = await machine();
53
+ const binary = path.join(source.home, '.local', 'bin', 'magpie');
54
+ await write(binary, 'fixture-cli');
55
+ await write(path.join(source.home, '.config', 'magpie', 'providers.json'));
56
+ const runSource = vi.fn(async (_binary, args, ctx) => {
57
+ if (args[0] === '--version') return 'magpie 0.1.0';
58
+ if (args[0] === 'accounts') return '[]';
59
+ expect(args[0]).toBe('backup');
60
+ expect(ctx.input).toMatch(/^[a-f0-9]{64}\n$/);
61
+ expect(args).not.toContain(ctx.input.trim());
62
+ await write(args[1], JSON.stringify({ format: 'magpie-backup', data: 'fixture-encrypted' }));
63
+ return 'saved';
64
+ });
65
+ const payload = await createMagpieSync({ ...source.options, run: runSource }).download();
66
+ const bytes = Buffer.from('fixture-installed-cli');
67
+ const fetchImpl = vi.fn(async (url) => url === 'https://usemagpie.ai/api/latest'
68
+ ? Response.json({ assets: { 'magpie-cli-linux-amd64': {
69
+ url: 'https://github.com/yetone/magpie-releases/releases/download/v0.1.0/magpie-cli-linux-amd64',
70
+ sha256: createHash('sha256').update(bytes).digest('hex'),
71
+ } } }) : new Response(bytes));
72
+ const runTarget = vi.fn(async (_binary, args, ctx) => {
73
+ if (args[0] === '--version') return 'magpie 0.1.0';
74
+ if (args[0] === 'accounts') return '[]';
75
+ expect(args[0]).toBe('restore');
76
+ expect(ctx.input.trim()).toBe(JSON.parse(payload).passphrase);
77
+ expect(JSON.parse(await fs.readFile(args[1], 'utf8')).format).toBe('magpie-backup');
78
+ await write(path.join(target.home, '.config', 'magpie', 'providers.json'), '{"providers":[]}');
79
+ return 'restored';
80
+ });
81
+ const destination = createMagpieSync({ ...target.options, fetchImpl, run: runTarget });
82
+ await expect(destination.import(payload)).resolves.toEqual({ ok: true });
83
+ expect(fetchImpl).toHaveBeenCalledTimes(2);
84
+ expect(await fs.realpath(path.join(target.home, '.local/bin/magpie'))).toBe(await fs.realpath(path.join(target.home, '.config/openchamber/tools/magpie/magpie')));
85
+ await expect(destination.import(payload)).resolves.toEqual({ ok: true });
86
+ expect(fetchImpl).toHaveBeenCalledTimes(2);
87
+ for (const item of [source, target]) {
88
+ expect((await fs.readdir(item.home)).filter((name) => name.startsWith('openchamber-magpie-'))).toEqual([]);
89
+ }
90
+ });
91
+
92
+ it('repairs a previously managed installation without downloading or overwriting a user command', async () => {
93
+ const { home, options } = await machine();
94
+ const managed = path.join(home, '.config/openchamber/tools/magpie/magpie');
95
+ const entry = path.join(home, '.local/bin/magpie');
96
+ const fetchImpl = vi.fn();
97
+ const run = vi.fn(async () => 'magpie 0.1.1319');
98
+ const sync = createMagpieSync({ ...options, fetchImpl, run });
99
+ await sync.repairInstallation();
100
+ expect(run).not.toHaveBeenCalled();
101
+ await write(managed, 'fixture');
102
+ await sync.repairInstallation();
103
+ await sync.repairInstallation();
104
+ expect(await fs.realpath(entry)).toBe(await fs.realpath(managed));
105
+ await fs.unlink(entry);
106
+ await write(entry, 'user installation');
107
+ await sync.repairInstallation();
108
+ expect(await fs.readFile(entry, 'utf8')).toBe('user installation');
109
+ expect(fetchImpl).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('round-trips plugin accounts and Codex through v2 and verifies the final account inventory', async () => {
113
+ const source = await machine();
114
+ const target = await machine();
115
+ for (const item of [source, target]) await write(path.join(item.home, '.local/bin/magpie'), 'fixture');
116
+ const sourceDir = path.join(source.home, '.config/magpie');
117
+ const targetDir = path.join(target.home, '.config/magpie');
118
+ await write(path.join(sourceDir, 'providers.json'), '{"providers":[{"id":"astra"},{"id":"codex"}]}');
119
+ await write(path.join(sourceDir, 'plugin-auth.json'), JSON.stringify({ 'astra#one': { type: 'api', key: 'fixture-key' } }));
120
+ await write(path.join(sourceDir, 'logins.json'), JSON.stringify([
121
+ { agent: 'codex', user: 'fixture@example.invalid', auth: { tokens: { access_token: 'fixture-token' } } },
122
+ ]));
123
+ const payload = await createMagpieSync({ ...source.options, run: async (_binary, args) => {
124
+ if (args[0] === '--version') return 'magpie 0.1.1319';
125
+ if (args[0] === 'accounts') return '[{"agent":"astra"},{"agent":"codex","user":"fixture@example.invalid"}]';
126
+ await write(args[1], '{"format":"magpie-backup"}');
127
+ return '';
128
+ } }).download();
129
+ expect(JSON.parse(payload).format).toBe('magpie-sync-v2');
130
+ await write(path.join(targetDir, 'plugin-auth.json'), '{"destination-only":{"type":"api","key":"keep"}}');
131
+ let recognized = true;
132
+ const run = vi.fn(async (_binary, args) => {
133
+ if (args[0] === '--version') return 'magpie 0.1.1319';
134
+ if (args[0] === 'restore') {
135
+ await write(path.join(targetDir, 'providers.json'), await fs.readFile(path.join(sourceDir, 'providers.json')));
136
+ return 'not signed in on this computer';
137
+ }
138
+ if (args[1] === 'import') {
139
+ expect(args).not.toContain('fixture-token');
140
+ expect(JSON.parse(await fs.readFile(args[3], 'utf8')).tokens.access_token).toBe('fixture-token');
141
+ expect((await fs.stat(args[3])).mode & 0o777).toBe(0o600);
142
+ return '';
143
+ }
144
+ return JSON.stringify(recognized ? [{ agent: 'astra' }, { agent: 'codex', user: 'fixture@example.invalid' }] : []);
145
+ });
146
+ const sync = createMagpieSync({ ...target.options, run });
147
+ await expect(sync.import(payload)).resolves.toEqual({ ok: true });
148
+ await expect(sync.import(payload)).resolves.toEqual({ ok: true });
149
+ const stored = JSON.parse(await fs.readFile(path.join(targetDir, 'plugin-auth.json'), 'utf8'));
150
+ expect(Object.keys(stored)).toEqual(['destination-only', 'astra#one']);
151
+ recognized = false;
152
+ await expect(sync.import(payload)).rejects.toThrow('authorization sync or verification failed');
153
+ expect((await fs.readdir(target.home)).filter((name) => name.startsWith('openchamber-magpie-'))).toEqual([]);
154
+ });
155
+
156
+ it('rejects malformed v2 authorization before installing or restoring', async () => {
157
+ const { options } = await machine();
158
+ const run = vi.fn();
159
+ const fetchImpl = vi.fn();
160
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v2', passphrase: 'a'.repeat(64),
161
+ backup: Buffer.from('{"format":"magpie-backup"}').toString('base64'),
162
+ authorization: { plugins: {}, logins: [{ agent: 'codex', home: '/source/path', auth: {} }], incomplete: false } }));
163
+ await expect(createMagpieSync({ ...options, run, fetchImpl }).import(payload)).rejects.toThrow('Invalid Magpie sync payload');
164
+ expect(run).not.toHaveBeenCalled();
165
+ expect(fetchImpl).not.toHaveBeenCalled();
166
+ });
167
+
168
+ it('retries agent settings after authorizations instead of keeping a pre-login skip warning', async () => {
169
+ const { home, options } = await machine();
170
+ await write(path.join(home, '.local/bin/magpie'), 'fixture');
171
+ let restores = 0;
172
+ const run = async (_binary, args) => {
173
+ if (args[0] === '--version') return 'magpie 0.1.1319';
174
+ if (args[0] === 'accounts') return '[]';
175
+ return ++restores === 1 ? 'Left as they are' : 'restored';
176
+ };
177
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v2', passphrase: 'a'.repeat(64),
178
+ backup: Buffer.from('{"format":"magpie-backup"}').toString('base64'),
179
+ authorization: { plugins: {}, logins: [], incomplete: false } }));
180
+ await expect(createMagpieSync({ ...options, run }).import(payload)).resolves.toEqual({ ok: true });
181
+ expect(restores).toBe(2);
182
+ });
183
+
184
+ it('does not install on malformed payload and refuses partial restore without leaking CLI output', async () => {
185
+ const { home, options } = await machine();
186
+ const fetchImpl = vi.fn();
187
+ const run = vi.fn(async (_binary, args) => args[0] === '--version'
188
+ ? 'magpie 0.1.0' : "Couldn't add the plugin private-fixture-value");
189
+ const sync = createMagpieSync({ ...options, fetchImpl, run });
190
+ await expect(sync.import(Buffer.from('{}'))).rejects.toThrow('Invalid Magpie sync payload');
191
+ expect(fetchImpl).not.toHaveBeenCalled();
192
+ await write(path.join(home, '.local', 'bin', 'magpie'), 'fixture-cli');
193
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v1', passphrase: 'a'.repeat(64),
194
+ backup: Buffer.from('{"format":"magpie-backup"}').toString('base64') }));
195
+ await expect(sync.import(payload)).rejects.toThrow('Magpie restore partially completed');
196
+ expect((await fs.readdir(home)).filter((name) => name.startsWith('openchamber-magpie-'))).toEqual([]);
197
+ });
198
+
199
+ it('reports excluded sign-ins as a warning after a successful native restore', async () => {
200
+ const { home, options } = await machine();
201
+ await write(path.join(home, '.local', 'bin', 'magpie'), 'fixture-cli');
202
+ const run = async (_binary, args) => args[0] === '--version' ? 'magpie 0.1.1319'
203
+ : "restored providers: 0 added, 8 replaced\nThe backup's settings use these, not signed in on this computer (a backup carries settings, not sign-ins; to use one here: magpie accounts add <id>): private-fixture-account\n";
204
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v1', passphrase: 'a'.repeat(64),
205
+ backup: Buffer.from('{"format":"magpie-backup"}').toString('base64') }));
206
+ await expect(createMagpieSync({ ...options, run }).import(payload)).resolves.toEqual({
207
+ ok: true, magpieWarnings: ['sign_in_required'],
208
+ });
209
+ });
210
+
211
+ it('requires current source discovery and supports a Magpie-only plan', async () => {
212
+ const { home } = await machine();
213
+ const selections = { fileGroups: [], singleFiles: [], directories: [], agentsRoot: false, providerCredentials: false, magpie: true };
214
+ expect(() => planOpenCodeConfigSync(home, { selections })).toThrow('unavailable');
215
+ const plan = planOpenCodeConfigSync(home, { selections, magpie: { available: true, installed: true } });
216
+ expect(plan.magpie).toEqual({ bytes: 0 });
217
+ const received = [];
218
+ const executor = { prepare: vi.fn(), putTar: async (value) => received.push(value), finalize: vi.fn(async () => ({ magpieWarnings: ['sign_in_required'] })) };
219
+ const result = await applyConfigSyncPlan({ plan, executor, syncRunId: 'test', sourceHomedir: home, collectMagpie: async () => Buffer.from('fixture') });
220
+ expect(result.magpieWarnings).toEqual(['sign_in_required']);
221
+ expect(received).toEqual([{ kind: 'magpie', payload: Buffer.from('fixture') }]);
222
+ received.length = 0;
223
+ await applyConfigSyncPlan({ plan: { ...plan, files: [{ path: 'opencode.json' }] }, executor, syncRunId: 'ordered', sourceHomedir: home,
224
+ collectTar: async () => Buffer.from('config'), collectMagpie: async () => Buffer.from('fixture') });
225
+ expect(received.map((entry) => entry.kind)).toEqual(['magpie', 'config']);
226
+ const pull = planOpenCodeConfigSyncFromInventory({ files: [], directories: [], magpie: { available: true } }, { selections });
227
+ expect(pull.magpieAvailable).toBe(true);
228
+ expect(pull.magpie).toEqual({ bytes: 0 });
229
+ });
230
+
231
+ it('rejects a corrupt installer before executing or publishing it', async () => {
232
+ const { home, options } = await machine();
233
+ const run = vi.fn();
234
+ const fetchImpl = async (url) => url === 'https://usemagpie.ai/api/latest'
235
+ ? Response.json({ assets: { 'magpie-cli-linux-amd64': {
236
+ url: 'https://github.com/yetone/magpie-releases/releases/download/v0.1.0/magpie-cli-linux-amd64',
237
+ sha256: '0'.repeat(64),
238
+ } } }) : new Response('corrupt');
239
+ const sync = createMagpieSync({ ...options, fetchImpl, run });
240
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v1', passphrase: 'a'.repeat(64),
241
+ backup: Buffer.from('{"format":"magpie-backup"}').toString('base64') }));
242
+ await expect(sync.import(payload)).rejects.toThrow('checksum mismatch');
243
+ expect(run).not.toHaveBeenCalled();
244
+ await expect(fs.stat(path.join(home, '.config', 'openchamber', 'tools', 'magpie', 'magpie'))).rejects.toThrow();
245
+ });
246
+
247
+ it.skipIf(!process.env.OPENCHAMBER_MAGPIE_TEST_BINARY)('round-trips isolated configuration with the installed real CLI', async () => {
248
+ const source = await machine();
249
+ const target = await machine();
250
+ for (const item of [source, target]) {
251
+ item.options.env = { ...item.options.env, MAGPIE_BINARY: process.env.OPENCHAMBER_MAGPIE_TEST_BINARY,
252
+ MAGPIE_NO_ACCOUNTS: '1', PATH: '/usr/bin:/bin', XDG_CACHE_HOME: path.join(item.home, '.cache') };
253
+ await write(path.join(item.home, '.config', 'magpie', 'settings.json'), JSON.stringify({ noAccountDiscovery: true, noStats: true, noUsageStats: true, noAutoUpdate: true }));
254
+ }
255
+ await write(path.join(source.home, '.config', 'magpie', 'providers.json'), JSON.stringify({ providers: [
256
+ { id: 'sync-fixture', name: 'Sync fixture', chat: 'https://provider.invalid/v1', key: 'fixture-not-a-real-key' },
257
+ { id: 'fixture-subscription' },
258
+ ] }));
259
+ const payload = await createMagpieSync(source.options).download();
260
+ const receipt = await createMagpieSync(target.options).import(payload);
261
+ expect(receipt).toEqual({ ok: true, magpieWarnings: ['sign_in_required'] });
262
+ const restored = JSON.parse(await fs.readFile(path.join(target.home, '.config', 'magpie', 'providers.json'), 'utf8'));
263
+ expect(restored.providers).toContainEqual(expect.objectContaining({ id: 'sync-fixture', key: 'fixture-not-a-real-key' }));
264
+ }, 60_000);
265
+ });
@@ -60,7 +60,7 @@ export const walkAllowlistDirectory = (absPath, excludeNames, seenRealpaths) =>
60
60
  };
61
61
 
62
62
  /**
63
- * Pure planner: source-home allowlist + optional `~/.agents` + optional provider auth.json.
63
+ * Pure planner: source-home allowlist + optional `~/.agents` + provider credential metadata.
64
64
  * Plan is computed on the source side; executors run it on the target side.
65
65
  * `direction` is a first-class field (`push` today; `pull` reuses the same shape).
66
66
  *
@@ -70,7 +70,9 @@ export const walkAllowlistDirectory = (absPath, excludeNames, seenRealpaths) =>
70
70
  * syncRunId?: string,
71
71
  * sourceTargetId?: string,
72
72
  * targetId?: string,
73
- * includeAuthFile?: boolean,
73
+ * includeProviderCredentials?: boolean,
74
+ * providerCredentials?: { bytes: number } | null,
75
+ * magpie?: { available: boolean, installed: boolean },
74
76
  * selections?: import('./selections.js').SyncSelections,
75
77
  * }} [options]
76
78
  * @returns {{
@@ -81,7 +83,7 @@ export const walkAllowlistDirectory = (absPath, excludeNames, seenRealpaths) =>
81
83
  * files: { path: string, bytes: number }[],
82
84
  * directories: { path: string, fileCount: number, bytes: number }[],
83
85
  * agentsRoot: { fileCount: number, bytes: number } | null,
84
- * authFile: { bytes: number } | null,
86
+ * providerCredentials: { bytes: number } | null,
85
87
  * deletes: string[],
86
88
  * totalBytes: number,
87
89
  * selections?: import('./selections.js').SyncSelections,
@@ -93,15 +95,13 @@ export const planOpenCodeConfigSync = (sourceHomedir, options = {}) => {
93
95
  if (!SYNC_DIRECTIONS.includes(direction)) {
94
96
  throw new Error(`Unsupported sync direction: ${String(direction)}`);
95
97
  }
96
- // Credential transfer requires an explicit per-target grant (ticket 03).
97
- // Callers that have not authorized the target pass includeAuthFile: false so
98
- // auth.json is skipped rather than failing the whole sync.
98
+ // Credential values never enter the filesystem inventory or preview plan.
99
99
  const selections = options.selections
100
- ? normalizeSyncSelections(options.selections, { includeAuthFile: options.selections.authFile === true })
100
+ ? normalizeSyncSelections(options.selections, { includeProviderCredentials: options.selections.providerCredentials === true })
101
101
  : null;
102
- const includeAuthFile = selections
103
- ? selections.authFile === true
104
- : options.includeAuthFile !== false;
102
+ const includeProviderCredentials = selections
103
+ ? selections.providerCredentials === true
104
+ : options.includeProviderCredentials !== false;
105
105
 
106
106
  const root = path.join(home, '.config', 'opencode');
107
107
  /** @type {{ path: string, bytes: number }[]} */
@@ -191,22 +191,9 @@ export const planOpenCodeConfigSync = (sourceHomedir, options = {}) => {
191
191
  }
192
192
 
193
193
  /** @type {{ bytes: number } | null} */
194
- let authFile = null;
195
- if (includeAuthFile) {
196
- const authAbs = path.join(home, '.local', 'share', 'opencode', 'auth.json');
197
- let authStat;
198
- try {
199
- // fs.stat follows symlinks (auth.json is sometimes a symlink).
200
- authStat = fs.statSync(authAbs);
201
- } catch {
202
- authStat = null;
203
- }
204
- if (authStat?.isFile()) {
205
- authFile = { bytes: authStat.size };
206
- totalBytes += authStat.size;
207
- // Single credential file — does not count against the 20000 file-count cap.
208
- }
209
- }
194
+ const providerCredentials = includeProviderCredentials && options.providerCredentials ? { bytes: options.providerCredentials.bytes } : null;
195
+ if (selections?.magpie && !options.magpie?.available) throw new Error('Magpie source configuration is unavailable');
196
+ if (providerCredentials) totalBytes += providerCredentials.bytes;
210
197
 
211
198
  if (totalBytes > OPENCODE_CONFIG_SYNC_MAX_BYTES || totalFileCount > OPENCODE_CONFIG_SYNC_MAX_FILES) {
212
199
  throw new Error('OpenCode config sync exceeds size limit (512MB)');
@@ -222,7 +209,9 @@ export const planOpenCodeConfigSync = (sourceHomedir, options = {}) => {
222
209
  files,
223
210
  directories,
224
211
  agentsRoot,
225
- authFile,
212
+ providerCredentials,
213
+ magpieAvailable: options.magpie?.available === true,
214
+ magpie: options.selections?.magpie === true && options.magpie?.available === true ? { bytes: 0 } : null,
226
215
  deletes,
227
216
  totalBytes,
228
217
  };
@@ -237,7 +226,7 @@ export const planOpenCodeConfigSync = (sourceHomedir, options = {}) => {
237
226
  * syncRunId?: string,
238
227
  * sourceTargetId?: string,
239
228
  * targetId?: string,
240
- * includeAuthFile?: boolean,
229
+ * includeProviderCredentials?: boolean,
241
230
  * selections?: import('./selections.js').SyncSelections,
242
231
  * }} [options]
243
232
  */
@@ -247,21 +236,25 @@ export const planOpenCodeConfigSyncFromInventory = (inventory, options = {}) =>
247
236
  throw new Error(`Unsupported sync direction: ${String(direction)}`);
248
237
  }
249
238
  const selections = options.selections
250
- ? normalizeSyncSelections(options.selections, { includeAuthFile: options.selections.authFile === true })
239
+ ? normalizeSyncSelections(options.selections, { includeProviderCredentials: options.selections.providerCredentials === true })
251
240
  : null;
252
- const includeAuthFile = selections
253
- ? selections.authFile === true
254
- : options.includeAuthFile !== false;
241
+ const includeProviderCredentials = selections
242
+ ? selections.providerCredentials === true
243
+ : options.includeProviderCredentials !== false;
255
244
 
256
245
  const files = Array.isArray(inventory?.files) ? inventory.files.filter((entry) => entry?.path) : [];
246
+ if (selections?.magpie && !inventory?.magpie?.available) throw new Error('Magpie source configuration is unavailable');
247
+ if (includeProviderCredentials && !Object.hasOwn(inventory || {}, 'providerCredentials')) {
248
+ throw new Error('Provider credential inventory is missing');
249
+ }
257
250
  const directories = Array.isArray(inventory?.directories)
258
251
  ? inventory.directories.filter((entry) => entry?.path)
259
252
  : [];
260
253
  const agentsRoot = inventory?.agentsRoot && typeof inventory.agentsRoot === 'object'
261
254
  ? inventory.agentsRoot
262
255
  : null;
263
- const authFile = includeAuthFile && inventory?.authFile && typeof inventory.authFile === 'object'
264
- ? inventory.authFile
256
+ const providerCredentials = includeProviderCredentials && inventory?.providerCredentials && typeof inventory.providerCredentials === 'object'
257
+ ? inventory.providerCredentials
265
258
  : null;
266
259
 
267
260
  /** @type {string[]} */
@@ -293,8 +286,8 @@ export const planOpenCodeConfigSyncFromInventory = (inventory, options = {}) =>
293
286
  totalBytes += Number(agentsRoot.bytes) || 0;
294
287
  totalFileCount += Number(agentsRoot.fileCount) || 0;
295
288
  }
296
- if (authFile) {
297
- totalBytes += Number(authFile.bytes) || 0;
289
+ if (providerCredentials) {
290
+ totalBytes += Number(providerCredentials.bytes) || 0;
298
291
  }
299
292
  if (totalBytes > OPENCODE_CONFIG_SYNC_MAX_BYTES || totalFileCount > OPENCODE_CONFIG_SYNC_MAX_FILES) {
300
293
  throw new Error('OpenCode config sync exceeds size limit (512MB)');
@@ -310,7 +303,9 @@ export const planOpenCodeConfigSyncFromInventory = (inventory, options = {}) =>
310
303
  files,
311
304
  directories,
312
305
  agentsRoot,
313
- authFile,
306
+ providerCredentials,
307
+ magpieAvailable: inventory?.magpie?.available === true,
308
+ magpie: options.selections?.magpie === true && inventory?.magpie?.available === true ? { bytes: 0 } : null,
314
309
  deletes,
315
310
  totalBytes,
316
311
  };
@@ -1,11 +1,9 @@
1
1
  import fs from 'node:fs';
2
- import fsp from 'node:fs/promises';
3
2
  import os from 'node:os';
4
3
  import path from 'node:path';
5
4
 
6
5
  import {
7
6
  OPENCODE_AGENTS_ROOT_PROBE_MARKER,
8
- OPENCODE_AUTH_FILE_PROBE_MARKER,
9
7
  OPENCODE_CONFIG_SYNC_ALLOWLIST,
10
8
  OPENCODE_CONFIG_SYNC_MAX_BYTES,
11
9
  OPENCODE_CONFIG_SYNC_MAX_FILES,
@@ -18,6 +16,8 @@ import {
18
16
  import { walkAllowlistDirectory } from './plan.js';
19
17
  import { sanitizeSyncRunIdForPath } from './scripts.js';
20
18
  import { collectLocalTarBuffer } from './tar.js';
19
+ import { createCredentialSync } from './credentials.js';
20
+ import { createMagpieSync } from './magpie.js';
21
21
 
22
22
  /**
23
23
  * In-process receiver for HTTP config-sync (direct/relay hosts).
@@ -26,6 +26,8 @@ import { collectLocalTarBuffer } from './tar.js';
26
26
  export const createConfigSyncReceiver = (deps = {}) => {
27
27
  const {
28
28
  homedir = () => os.homedir(),
29
+ credentials = createCredentialSync(),
30
+ magpie = createMagpieSync({ homedir }),
29
31
  } = deps;
30
32
 
31
33
  let inflight = null;
@@ -58,7 +60,7 @@ export const createConfigSyncReceiver = (deps = {}) => {
58
60
  }
59
61
  };
60
62
 
61
- const buildInventory = () => {
63
+ const buildInventory = async ({ includeCredentials = false } = {}) => {
62
64
  const home = homedir();
63
65
  const root = path.join(home, '.config', 'opencode');
64
66
  const files = [];
@@ -118,33 +120,22 @@ export const createConfigSyncReceiver = (deps = {}) => {
118
120
  // missing
119
121
  }
120
122
 
121
- let authFile = null;
122
- let remoteAuthFileExists = false;
123
- try {
124
- const authAbs = path.join(home, '.local', 'share', 'opencode', 'auth.json');
125
- const st = fs.statSync(authAbs);
126
- if (st.isFile()) {
127
- remoteAuthFileExists = true;
128
- authFile = { bytes: st.size };
129
- }
130
- } catch {
131
- // missing
132
- }
123
+ const providerCredentials = includeCredentials ? await credentials.inventory() : null;
124
+ const remoteProviderCredentialsExists = Boolean(providerCredentials);
133
125
 
134
126
  return {
135
- inventory: { files, directories, agentsRoot, authFile },
127
+ inventory: { files, directories, agentsRoot, providerCredentials, magpie: await magpie.inventory() },
136
128
  remoteExisting,
137
129
  remoteAgentsRootExists,
138
- remoteAuthFileExists,
130
+ remoteProviderCredentialsExists,
139
131
  markers: {
140
132
  agentsRoot: OPENCODE_AGENTS_ROOT_PROBE_MARKER,
141
- authFile: OPENCODE_AUTH_FILE_PROBE_MARKER,
142
133
  },
143
134
  };
144
135
  };
145
136
 
146
137
  const probe = async (plan) => {
147
- const snapshot = buildInventory();
138
+ const snapshot = await buildInventory({ includeCredentials: plan?.selections?.providerCredentials === true || Boolean(plan?.providerCredentials) });
148
139
  const existing = new Set(snapshot.remoteExisting);
149
140
  const planPaths = [
150
141
  ...(Array.isArray(plan?.files) ? plan.files.map((entry) => entry.path) : []),
@@ -199,9 +190,13 @@ export const createConfigSyncReceiver = (deps = {}) => {
199
190
  await extractTarGzBuffer(buffer, path.join(home, '.config', 'opencode'));
200
191
  } else if (kind === 'agents') {
201
192
  await extractTarGzBuffer(buffer, home);
202
- } else if (kind === 'auth') {
203
- await fsp.mkdir(path.join(home, '.local', 'share', 'opencode'), { recursive: true });
204
- await extractTarGzBuffer(buffer, path.join(home, '.local', 'share', 'opencode'));
193
+ } else if (kind === 'credentials') {
194
+ if (!flight.plan?.providerCredentials) throw new Error('Provider credentials were not selected');
195
+ await credentials.import(buffer);
196
+ } else if (kind === 'magpie') {
197
+ if (!flight.plan?.magpie) throw new Error('Magpie was not selected');
198
+ const result = await magpie.import(buffer);
199
+ flight.magpieWarnings = result.magpieWarnings;
205
200
  } else {
206
201
  const error = new Error(`Unsupported put kind: ${String(kind)}`);
207
202
  error.code = 'sync_invalid_kind';
@@ -220,7 +215,7 @@ export const createConfigSyncReceiver = (deps = {}) => {
220
215
  const download = async ({ kind }) => {
221
216
  const home = homedir();
222
217
  if (kind === 'config') {
223
- const snapshot = buildInventory();
218
+ const snapshot = await buildInventory();
224
219
  const entries = [
225
220
  ...snapshot.inventory.files.map((entry) => entry.path),
226
221
  ...snapshot.inventory.directories.map((entry) => entry.path),
@@ -235,9 +230,10 @@ export const createConfigSyncReceiver = (deps = {}) => {
235
230
  if (kind === 'agents') {
236
231
  return collectTarGz(home, ['.agents']);
237
232
  }
238
- if (kind === 'auth') {
239
- return collectTarGz(path.join(home, '.local', 'share', 'opencode'), ['auth.json']);
233
+ if (kind === 'credentials') {
234
+ return credentials.download();
240
235
  }
236
+ if (kind === 'magpie') return magpie.download();
241
237
  const error = new Error(`Unsupported download kind: ${String(kind)}`);
242
238
  error.code = 'sync_invalid_kind';
243
239
  throw error;
@@ -245,12 +241,15 @@ export const createConfigSyncReceiver = (deps = {}) => {
245
241
 
246
242
  const finalize = async ({ syncRunId }) => {
247
243
  const flight = requireInflight(syncRunId);
244
+ if (flight.plan?.providerCredentials && !flight.kinds.has('credentials')) throw new Error('Provider credentials transfer incomplete');
245
+ if (flight.plan?.magpie && !flight.kinds.has('magpie')) throw new Error('Magpie transfer incomplete');
248
246
  await finalizeLocalSyncDestination(homedir(), { syncRunId: flight.syncRunId });
249
247
  const receipt = {
250
248
  ok: true,
251
249
  syncRunId: flight.syncRunId,
252
250
  kinds: [...flight.kinds],
253
251
  endedAt: new Date().toISOString(),
252
+ ...(flight.magpieWarnings?.length ? { magpieWarnings: flight.magpieWarnings } : {}),
254
253
  };
255
254
  clearInflight(syncRunId);
256
255
  return receipt;
@@ -274,5 +273,9 @@ export const createConfigSyncReceiver = (deps = {}) => {
274
273
  abort,
275
274
  getInflight: () => inflight,
276
275
  buildInventory,
276
+ credentialInventory: () => credentials.inventory(),
277
+ magpieInventory: () => magpie.inventory(),
278
+ importMagpie: async (stream) => magpie.import(await readStreamToBuffer(stream, { maxBytes: OPENCODE_CONFIG_SYNC_MAX_BYTES })),
279
+ importCredentials: async (stream) => credentials.import(await readStreamToBuffer(stream, { maxBytes: OPENCODE_CONFIG_SYNC_MAX_BYTES })),
277
280
  };
278
281
  };
@@ -2,7 +2,7 @@ import { createConfigSyncReceiver } from './receiver.js';
2
2
 
3
3
  /**
4
4
  * Register HTTP config-sync receive/download routes for direct (and later relay) hosts.
5
- * Auth is the global `/api` UI/client-token gate. Selected kinds (including auth)
5
+ * Auth is the global `/api` UI/client-token gate. Selected kinds (including credentials)
6
6
  * sync when present in the plan — no extra credential grant checks.
7
7
  *
8
8
  * @param {import('express').Express} app
@@ -70,11 +70,32 @@ export const registerConfigSyncRoutes = (app, deps = {}) => {
70
70
  }
71
71
  });
72
72
 
73
+ app.get('/api/openchamber/config-sync/magpie/inventory', async (_req, res) => {
74
+ try { res.json({ magpie: await receiver.magpieInventory() }); }
75
+ catch (error) { sendError(res, error); }
76
+ });
77
+
78
+ app.put('/api/openchamber/config-sync/magpie', async (req, res) => {
79
+ try { res.json(await receiver.importMagpie(req)); }
80
+ catch (error) { sendError(res, error); }
81
+ });
82
+
83
+ app.get('/api/openchamber/config-sync/credentials/inventory', async (_req, res) => {
84
+ try { res.json({ providerCredentials: await receiver.credentialInventory() }); }
85
+ catch (error) { sendError(res, error); }
86
+ });
87
+
88
+ app.put('/api/openchamber/config-sync/credentials', async (req, res) => {
89
+ try { res.json(await receiver.importCredentials(req)); }
90
+ catch (error) { sendError(res, error); }
91
+ });
92
+
73
93
  app.get('/api/openchamber/config-sync/download/:kind', async (req, res) => {
74
94
  try {
75
95
  const kind = String(req.params.kind || '').trim();
76
96
  const buffer = await receiver.download({ kind });
77
- res.setHeader('Content-Type', 'application/gzip');
97
+ res.setHeader('Content-Type', kind === 'credentials' || kind === 'magpie' ? 'application/octet-stream' : 'application/gzip');
98
+ res.setHeader('Cache-Control', 'no-store');
78
99
  res.setHeader('Content-Length', String(buffer.byteLength));
79
100
  res.send(buffer);
80
101
  } catch (error) {