@openchambery/web 2.1.8-beta.2 → 2.1.8-beta.4

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 (115) hide show
  1. package/dist/assets/{AgentAvatar-ndlnVkjO.js → AgentAvatar-qSjSZvIG.js} +1 -1
  2. package/dist/assets/{AssistantView-6ta2Y6q7.js → AssistantView-Dx-mzVKK.js} +3 -3
  3. package/dist/assets/{DiagramView-DvZU3Yxr.js → DiagramView-CDmixuZQ.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-CcvpP9n1.js → FadeInOnReveal-CD5d1k_u.js} +1 -1
  5. package/dist/assets/{Icon-2Obqh9Fi.js → Icon-QASb6r3F.js} +1 -1
  6. package/dist/assets/{ImageLoadingPlaceholder-D916bbhl.js → ImageLoadingPlaceholder-BuGiEI7w.js} +1 -1
  7. package/dist/assets/{LoadingImage-BeOr4G0q.js → LoadingImage-FRySi1BM.js} +1 -1
  8. package/dist/assets/{MarkdownRenderer-Cl5V8BKz.js → MarkdownRenderer-CggK1I6D.js} +2 -2
  9. package/dist/assets/{MarkdownRendererImpl-losB1IeW.js → MarkdownRendererImpl-Cms-VuSl.js} +1 -1
  10. package/dist/assets/{MarkstreamRendererImpl-PHsV91V_.js → MarkstreamRendererImpl-CrC7_3Y7.js} +1 -1
  11. package/dist/assets/{MobileDetailNavigation-W-DaFFg5.js → MobileDetailNavigation-CunQ1OIb.js} +1 -1
  12. package/dist/assets/{MobileShareBridge-Bjzb3TK6.js → MobileShareBridge-CoRbF_fc.js} +1 -1
  13. package/dist/assets/MobileTabPageHeader-Bj_Lu8tt.js +2 -0
  14. package/dist/assets/{MultiRunWindow-BbfM6ISu.js → MultiRunWindow-BgNee8Nh.js} +1 -1
  15. package/dist/assets/{OnboardingScreen-DEolMFq_.js → OnboardingScreen-CHa767GT.js} +1 -1
  16. package/dist/assets/{OverlayScrollbar-DB7fWZUw.js → OverlayScrollbar-C7Ls30Hy.js} +1 -1
  17. package/dist/assets/{SettingsView-Bjo-fXIC.js → SettingsView-D0Rk6ERT.js} +10 -9
  18. package/dist/assets/{SettingsWindow-DdN8IePL.js → SettingsWindow-8i92GNln.js} +1 -1
  19. package/dist/assets/{TerminalView-BUHkhkkZ.js → TerminalView-mLwQ0UU9.js} +1 -1
  20. package/dist/assets/{ToolOutputDialog-DGY_JzQg.js → ToolOutputDialog-DH6ASkUk.js} +1 -1
  21. package/dist/assets/{appThemeRegistry-DiuhF7sJ.js → appThemeRegistry-CHeMJdPY.js} +1 -1
  22. package/dist/assets/{button-Bdq9SUvZ.js → button-DOrwfkre.js} +1 -1
  23. package/dist/assets/{collapsible-C7PP5l16.js → collapsible-BJwPZMpo.js} +1 -1
  24. package/dist/assets/{diagram-BBlwWp3m.js → diagram-BFevUcKu.js} +1 -1
  25. package/dist/assets/{dialog-DHmUjm6M.js → dialog-FSXwOR2i.js} +1 -1
  26. package/dist/assets/{dropdown-menu.styles-XSr3iA4S.js → dropdown-menu.styles-CfTQwJHe.js} +1 -1
  27. package/dist/assets/{es-BxLn_G_Q.js → es-VbQQdCdi.js} +1 -1
  28. package/dist/assets/{fileMentionTouchSelection-BHA_5cBo.js → fileMentionTouchSelection-CSelAE3Z.js} +1 -1
  29. package/dist/assets/{fr-LROiZho7.js → fr-BotRbS9t.js} +1 -1
  30. package/dist/assets/{gitApi-BdyssA-O.js → gitApi-B123oHQk.js} +1 -1
  31. package/dist/assets/{input-De00oOal.js → input-Ctgb06ot.js} +1 -1
  32. package/dist/assets/{insertionBoundaries-DIohQnVE.js → insertionBoundaries-cfka-ff_.js} +1 -1
  33. package/dist/assets/{ja-DiErdLtA.js → ja-l_gq6Coh.js} +1 -1
  34. package/dist/assets/{ko-srYeEs4u.js → ko-yhU5rfe5.js} +1 -1
  35. package/dist/assets/{main-BETi-fsf.js → main-DJhLxkOQ.js} +10 -10
  36. package/dist/assets/{main-DUrFgqZL.js → main-IO37JIWf.js} +2 -2
  37. package/dist/assets/{markdownHeightCache-D44E3PrO.js → markdownHeightCache-B6drDnVO.js} +1 -1
  38. package/dist/assets/{mcpQueries-qCvadKyt.js → mcpQueries-Digedyij.js} +1 -1
  39. package/dist/assets/miniChat-mE8pRELI.js +2 -0
  40. package/dist/assets/mobile-Cia2889g.js +2 -0
  41. package/dist/assets/{multirun-C2aiem4S.js → multirun-C1I9Attc.js} +1 -1
  42. package/dist/assets/{pl-CzKfWMm9.js → pl-BkynZgOJ.js} +1 -1
  43. package/dist/assets/{projectMeta-DV6HExaM.js → projectMeta-C2WeOasQ.js} +1 -1
  44. package/dist/assets/{pt-BR-CUmrnVae.js → pt-BR-Dw_Dokez.js} +1 -1
  45. package/dist/assets/renderElectronMiniChatApp-FpoAJUKI.js +2 -0
  46. package/dist/assets/{renderMobileApp-Ds-6h0OM.js → renderMobileApp-DrT0ZwHw.js} +6 -6
  47. package/dist/assets/{runtimeConfig-CBy91BoB.js → runtimeConfig-yW48patn.js} +1 -1
  48. package/dist/assets/{runtimeEndpointReset-C7wwKdBg.js → runtimeEndpointReset-CMmN7tH1.js} +5 -5
  49. package/dist/assets/{scheduledTaskNavigation-D7yhTPgz.js → scheduledTaskNavigation-DEMPeoot.js} +1 -1
  50. package/dist/assets/sessionChangeSummary-D1YrkHbB.js +1 -0
  51. package/dist/assets/{sessionLookup-D0uMihAO.js → sessionLookup-DrKLmlMv.js} +1 -1
  52. package/dist/assets/{sortable-tabs-strip-aIcw2DJh.js → sortable-tabs-strip-BVnwfMAa.js} +1 -1
  53. package/dist/assets/speechSettingsQueries-BNPsqHp5.js +4 -0
  54. package/dist/assets/{store-BYLl4QL2.js → store-COtDAfPx.js} +2 -2
  55. package/dist/assets/{textarea-DNadM0DL.js → textarea-DJ52TDJj.js} +1 -1
  56. package/dist/assets/{tooltip-nHGca5md.js → tooltip-JgW0lJaV.js} +1 -1
  57. package/dist/assets/{uk-DGZ3Hl33.js → uk-DSl09YqU.js} +1 -1
  58. package/dist/assets/{useAppFontEffects-ByC3DE9D.js → useAppFontEffects-BUJapti0.js} +51 -53
  59. package/dist/assets/{useDesktopWindowControlsLayout-CbrmthFZ.js → useDesktopWindowControlsLayout-Dlh9xNoE.js} +1 -1
  60. package/dist/assets/{useEffectiveDirectory-BEpQGzay.js → useEffectiveDirectory-2dkaAaxr.js} +1 -1
  61. package/dist/assets/{useMobileNavigationStore-Cm0CuwA9.js → useMobileNavigationStore-BRQBoMI1.js} +6 -6
  62. package/dist/assets/{useWorkerHighlightedLines-D5mEoRi2.js → useWorkerHighlightedLines-BRAfFOVn.js} +1 -1
  63. package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
  64. package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
  65. package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
  66. package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
  67. package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
  68. package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
  69. package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
  70. package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
  71. package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
  72. package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
  73. package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
  74. package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
  75. package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
  76. package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
  77. package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
  78. package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
  79. package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
  84. package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
  85. package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
  86. package/dist/assets/{zh-CN-UwxgbjmI.js → zh-CN-NtO1bA00.js} +1 -1
  87. package/dist/assets/{zh-TW-Bc9XoqY8.js → zh-TW-CF0z_L0k.js} +1 -1
  88. package/dist/index.html +2 -2
  89. package/dist/mini-chat.html +2 -2
  90. package/dist/mobile.html +2 -2
  91. package/package.json +1 -1
  92. package/server/lib/config-sync/DOCUMENTATION.md +10 -5
  93. package/server/lib/config-sync/engine.js +3 -2
  94. package/server/lib/config-sync/magpie-auth.js +111 -0
  95. package/server/lib/config-sync/magpie-auth.test.js +79 -0
  96. package/server/lib/config-sync/magpie.js +81 -9
  97. package/server/lib/config-sync/magpie.test.js +120 -5
  98. package/server/lib/config-sync/receiver.js +3 -1
  99. package/server/lib/config-sync/routes.test.js +3 -2
  100. package/server/lib/opencode/DOCUMENTATION.md +1 -0
  101. package/server/lib/opencode/lifecycle.js +4 -0
  102. package/server/lib/opencode/lifecycle.test.js +7 -2
  103. package/dist/assets/MobileTabPageHeader-qbGYMq9u.js +0 -2
  104. package/dist/assets/miniChat-jC1_fkBZ.js +0 -2
  105. package/dist/assets/mobile-D-5-hKu6.js +0 -2
  106. package/dist/assets/renderElectronMiniChatApp-CI-5mVAG.js +0 -2
  107. package/dist/assets/sessionChangeSummary-BSFJiKHQ.js +0 -2
  108. package/dist/assets/speechSettingsQueries-Bd3icbFp.js +0 -4
  109. package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
  110. package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
  111. package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
  112. package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
  113. package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
  114. package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
  115. package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
@@ -23,13 +23,13 @@ const write = async (file, content = '{}') => {
23
23
  afterEach(async () => { await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); });
24
24
 
25
25
  describe('Magpie discovery and native transfer', () => {
26
- it('discovers config-only sources through XDG without installing during preview', async () => {
26
+ it('hides config-only sources until Magpie is installed, without installing during preview', async () => {
27
27
  const { home, options } = await machine();
28
28
  const fetchImpl = vi.fn();
29
29
  const sync = createMagpieSync({ ...options, fetchImpl });
30
30
  expect(await sync.inventory()).toEqual({ available: false, installed: false });
31
31
  await write(path.join(home, '.config', 'magpie', 'providers.json'));
32
- expect(await sync.inventory()).toEqual({ available: true, installed: false });
32
+ expect(await sync.inventory()).toEqual({ available: false, installed: false });
33
33
  expect(fetchImpl).not.toHaveBeenCalled();
34
34
  });
35
35
 
@@ -55,6 +55,7 @@ describe('Magpie discovery and native transfer', () => {
55
55
  await write(path.join(source.home, '.config', 'magpie', 'providers.json'));
56
56
  const runSource = vi.fn(async (_binary, args, ctx) => {
57
57
  if (args[0] === '--version') return 'magpie 0.1.0';
58
+ if (args[0] === 'accounts') return '[]';
58
59
  expect(args[0]).toBe('backup');
59
60
  expect(ctx.input).toMatch(/^[a-f0-9]{64}\n$/);
60
61
  expect(args).not.toContain(ctx.input.trim());
@@ -70,14 +71,17 @@ describe('Magpie discovery and native transfer', () => {
70
71
  } } }) : new Response(bytes));
71
72
  const runTarget = vi.fn(async (_binary, args, ctx) => {
72
73
  if (args[0] === '--version') return 'magpie 0.1.0';
74
+ if (args[0] === 'accounts') return '[]';
73
75
  expect(args[0]).toBe('restore');
74
76
  expect(ctx.input.trim()).toBe(JSON.parse(payload).passphrase);
75
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":[]}');
76
79
  return 'restored';
77
80
  });
78
81
  const destination = createMagpieSync({ ...target.options, fetchImpl, run: runTarget });
79
82
  await expect(destination.import(payload)).resolves.toEqual({ ok: true });
80
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')));
81
85
  await expect(destination.import(payload)).resolves.toEqual({ ok: true });
82
86
  expect(fetchImpl).toHaveBeenCalledTimes(2);
83
87
  for (const item of [source, target]) {
@@ -85,6 +89,98 @@ describe('Magpie discovery and native transfer', () => {
85
89
  }
86
90
  });
87
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
+
88
184
  it('does not install on malformed payload and refuses partial restore without leaking CLI output', async () => {
89
185
  const { home, options } = await machine();
90
186
  const fetchImpl = vi.fn();
@@ -100,6 +196,18 @@ describe('Magpie discovery and native transfer', () => {
100
196
  expect((await fs.readdir(home)).filter((name) => name.startsWith('openchamber-magpie-'))).toEqual([]);
101
197
  });
102
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
+
103
211
  it('requires current source discovery and supports a Magpie-only plan', async () => {
104
212
  const { home } = await machine();
105
213
  const selections = { fileGroups: [], singleFiles: [], directories: [], agentsRoot: false, providerCredentials: false, magpie: true };
@@ -107,9 +215,14 @@ describe('Magpie discovery and native transfer', () => {
107
215
  const plan = planOpenCodeConfigSync(home, { selections, magpie: { available: true, installed: true } });
108
216
  expect(plan.magpie).toEqual({ bytes: 0 });
109
217
  const received = [];
110
- const executor = { prepare: vi.fn(), putTar: async (value) => received.push(value), finalize: vi.fn() };
111
- await applyConfigSyncPlan({ plan, executor, syncRunId: 'test', sourceHomedir: home, collectMagpie: async () => Buffer.from('fixture') });
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']);
112
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']);
113
226
  const pull = planOpenCodeConfigSyncFromInventory({ files: [], directories: [], magpie: { available: true } }, { selections });
114
227
  expect(pull.magpieAvailable).toBe(true);
115
228
  expect(pull.magpie).toEqual({ bytes: 0 });
@@ -141,9 +254,11 @@ describe('Magpie discovery and native transfer', () => {
141
254
  }
142
255
  await write(path.join(source.home, '.config', 'magpie', 'providers.json'), JSON.stringify({ providers: [
143
256
  { id: 'sync-fixture', name: 'Sync fixture', chat: 'https://provider.invalid/v1', key: 'fixture-not-a-real-key' },
257
+ { id: 'fixture-subscription' },
144
258
  ] }));
145
259
  const payload = await createMagpieSync(source.options).download();
146
- await createMagpieSync(target.options).import(payload);
260
+ const receipt = await createMagpieSync(target.options).import(payload);
261
+ expect(receipt).toEqual({ ok: true, magpieWarnings: ['sign_in_required'] });
147
262
  const restored = JSON.parse(await fs.readFile(path.join(target.home, '.config', 'magpie', 'providers.json'), 'utf8'));
148
263
  expect(restored.providers).toContainEqual(expect.objectContaining({ id: 'sync-fixture', key: 'fixture-not-a-real-key' }));
149
264
  }, 60_000);
@@ -195,7 +195,8 @@ export const createConfigSyncReceiver = (deps = {}) => {
195
195
  await credentials.import(buffer);
196
196
  } else if (kind === 'magpie') {
197
197
  if (!flight.plan?.magpie) throw new Error('Magpie was not selected');
198
- await magpie.import(buffer);
198
+ const result = await magpie.import(buffer);
199
+ flight.magpieWarnings = result.magpieWarnings;
199
200
  } else {
200
201
  const error = new Error(`Unsupported put kind: ${String(kind)}`);
201
202
  error.code = 'sync_invalid_kind';
@@ -248,6 +249,7 @@ export const createConfigSyncReceiver = (deps = {}) => {
248
249
  syncRunId: flight.syncRunId,
249
250
  kinds: [...flight.kinds],
250
251
  endedAt: new Date().toISOString(),
252
+ ...(flight.magpieWarnings?.length ? { magpieWarnings: flight.magpieWarnings } : {}),
251
253
  };
252
254
  clearInflight(syncRunId);
253
255
  return receipt;
@@ -52,7 +52,7 @@ describe('config-sync HTTP routes', () => {
52
52
  const { app } = await createApp(undefined, {
53
53
  inventory: async () => ({ available: true, installed: false }),
54
54
  download: async () => payload,
55
- import: async (buffer) => { received.push(buffer); return { ok: true }; },
55
+ import: async (buffer) => { received.push(buffer); return { ok: true, magpieWarnings: ['sign_in_required'] }; },
56
56
  });
57
57
  const probe = await request(app).post('/api/openchamber/config-sync/probe').send({ plan: {} });
58
58
  expect(probe.body.inventory.magpie).toEqual({ available: true, installed: false });
@@ -67,7 +67,8 @@ describe('config-sync HTTP routes', () => {
67
67
  await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'magpie-run' }).expect(500);
68
68
  await request(app).put('/api/openchamber/config-sync/put/magpie?syncRunId=magpie-run')
69
69
  .set('Content-Type', 'application/octet-stream').send(payload).expect(200);
70
- await request(app).post('/api/openchamber/config-sync/finalize').send({ syncRunId: 'magpie-run' }).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']);
71
72
  expect(received).toEqual([payload, payload]);
72
73
  });
73
74
 
@@ -27,6 +27,7 @@ This module provides OpenCode server integration utilities for the web server ru
27
27
  - `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
28
  - `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
29
29
  - `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.
30
+ - 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
31
  - `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
32
  - `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
32
33
  - `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
@@ -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 runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
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();