@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
@@ -13,6 +13,8 @@ import { collectLocalTarBuffer } from './tar.js';
13
13
  * syncRunId: string,
14
14
  * sourceHomedir: string,
15
15
  * collectTar?: typeof collectLocalTarBuffer,
16
+ * collectCredentials?: () => Promise<Buffer>,
17
+ * collectMagpie?: () => Promise<Buffer>,
16
18
  * }} args
17
19
  */
18
20
  export const applyConfigSyncPlan = async ({
@@ -21,6 +23,8 @@ export const applyConfigSyncPlan = async ({
21
23
  syncRunId,
22
24
  sourceHomedir,
23
25
  collectTar = collectLocalTarBuffer,
26
+ collectCredentials,
27
+ collectMagpie,
24
28
  }) => {
25
29
  assertPlanDirection(plan);
26
30
  const home = String(sourceHomedir || '');
@@ -31,9 +35,9 @@ export const applyConfigSyncPlan = async ({
31
35
  ];
32
36
  const hasConfigPayload = tarEntries.length > 0;
33
37
  const hasAgentsPayload = Boolean(plan.agentsRoot);
34
- const hasAuthPayload = Boolean(plan.authFile);
38
+ const hasCredentialPayload = Boolean(plan.providerCredentials);
35
39
 
36
- if (!hasConfigPayload && !hasAgentsPayload && !hasAuthPayload) {
40
+ if (!hasConfigPayload && !hasAgentsPayload && !hasCredentialPayload && !plan.magpie) {
37
41
  // Nothing to upload — preserve the historical short-circuit (no remote prepare).
38
42
  return {
39
43
  ok: true,
@@ -42,7 +46,7 @@ export const applyConfigSyncPlan = async ({
42
46
  deletes: plan.deletes.length,
43
47
  totalBytes: plan.totalBytes,
44
48
  agentsRoot: null,
45
- authFile: null,
49
+ providerCredentials: null,
46
50
  plan,
47
51
  };
48
52
  }
@@ -54,25 +58,26 @@ export const applyConfigSyncPlan = async ({
54
58
  const agentsTarBuffer = hasAgentsPayload
55
59
  ? await collectTar(['-h', '-czf', '-', '-C', home, '.agents'], { windowsHide })
56
60
  : null;
57
- const authShareDir = path.join(home, '.local', 'share', 'opencode');
58
- const authTarBuffer = hasAuthPayload
59
- ? await collectTar(['-h', '-czf', '-', '-C', authShareDir, 'auth.json'], { windowsHide })
61
+ const credentialPayload = hasCredentialPayload
62
+ ? await collectCredentials()
60
63
  : null;
64
+ const magpiePayload = plan.magpie ? await collectMagpie() : null;
61
65
 
62
66
  await executor.prepare(plan, { syncRunId });
63
67
 
68
+ if (magpiePayload) await executor.putTar({ kind: 'magpie', payload: magpiePayload });
64
69
  if (configTarBuffer) {
65
70
  await executor.putTar({ kind: 'config', payload: configTarBuffer });
66
71
  }
67
72
  if (agentsTarBuffer) {
68
73
  await executor.putTar({ kind: 'agents', payload: agentsTarBuffer });
69
74
  }
70
- if (authTarBuffer) {
71
- // Only auth.json — never the rest of ~/.local/share/opencode (session DBs live there).
72
- await executor.putTar({ kind: 'auth', payload: authTarBuffer });
75
+ if (credentialPayload) {
76
+ // Versioned credential payload; never a database or filesystem archive.
77
+ await executor.putTar({ kind: 'credentials', payload: credentialPayload });
73
78
  }
74
79
 
75
- await executor.finalize(plan, { syncRunId });
80
+ const receipt = await executor.finalize(plan, { syncRunId });
76
81
 
77
82
  return {
78
83
  ok: true,
@@ -81,8 +86,9 @@ export const applyConfigSyncPlan = async ({
81
86
  deletes: plan.deletes.length,
82
87
  totalBytes: plan.totalBytes,
83
88
  agentsRoot: plan.agentsRoot ? { fileCount: plan.agentsRoot.fileCount } : null,
84
- authFile: plan.authFile ? { bytes: plan.authFile.bytes } : null,
89
+ providerCredentials: plan.providerCredentials ? { bytes: plan.providerCredentials.bytes } : null,
85
90
  plan,
91
+ ...(receipt?.magpieWarnings?.length ? { magpieWarnings: receipt.magpieWarnings } : {}),
86
92
  };
87
93
  };
88
94
 
@@ -2,9 +2,7 @@ export {
2
2
  OPENCODE_CONFIG_SYNC_ALLOWLIST,
3
3
  OPENCODE_CONFIG_SYNC_BACKUP_DIR,
4
4
  OPENCODE_AGENTS_SYNC_BACKUP_DIR,
5
- OPENCODE_AUTH_SYNC_BACKUP_DIR,
6
5
  OPENCODE_AGENTS_ROOT_PROBE_MARKER,
7
- OPENCODE_AUTH_FILE_PROBE_MARKER,
8
6
  OPENCODE_CONFIG_SYNC_MAX_BYTES,
9
7
  OPENCODE_CONFIG_SYNC_MAX_FILES,
10
8
  OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
@@ -42,7 +40,6 @@ export {
42
40
  buildRemoteSyncInventoryScript,
43
41
  buildRemoteConfigTarScript,
44
42
  buildRemoteAgentsTarScript,
45
- buildRemoteAuthTarScript,
46
43
  sanitizeSyncRunIdForPath,
47
44
  shellQuote,
48
45
  } from './scripts.js';
@@ -5,7 +5,6 @@ import { spawn } from 'node:child_process';
5
5
 
6
6
  import {
7
7
  OPENCODE_AGENTS_SYNC_BACKUP_DIR,
8
- OPENCODE_AUTH_SYNC_BACKUP_DIR,
9
8
  OPENCODE_CONFIG_SYNC_ALLOWLIST,
10
9
  OPENCODE_CONFIG_SYNC_BACKUP_DIR,
11
10
  OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
@@ -108,18 +107,6 @@ export const prepareLocalSyncDestination = async (homedir, plan, options) => {
108
107
  await fsp.rm(path.join(home, '.agents'), { recursive: true, force: true });
109
108
  }
110
109
 
111
- if (plan?.authFile) {
112
- const authBackupRoot = path.join(home, OPENCODE_AUTH_SYNC_BACKUP_DIR);
113
- const authRun = path.join(authBackupRoot, syncRunId);
114
- await fsp.rm(authRun, { recursive: true, force: true });
115
- await fsp.mkdir(authRun, { recursive: true });
116
- await pruneGenerations(authBackupRoot, syncRunId, generations);
117
- await copyPathIfExists(
118
- path.join(home, '.local', 'share', 'opencode', 'auth.json'),
119
- path.join(authRun, 'auth.json'),
120
- );
121
- }
122
-
123
110
  return { runBackup };
124
111
  };
125
112
 
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+
6
+ const MAX_AUTH_BYTES = 8 * 1024 * 1024;
7
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ const accountKey = (value) => typeof value === 'string' && /^[\w.-]+(?:#[\w.-]+)?$/.test(value)
9
+ && !['__proto__', 'constructor', 'prototype'].includes(value);
10
+ const portableLogin = (row, plugins) => object(row) && typeof row.user === 'string'
11
+ && ((row.agent === 'codex' && !row.home && object(row.auth) && object(row.auth.tokens))
12
+ || (typeof row.agent === 'string' && row.agent.startsWith('plugin:')
13
+ && accountKey(row.home) && Object.hasOwn(plugins, row.home)
14
+ && row.agent === `plugin:${row.home.split('#')[0]}`));
15
+
16
+ const readStore = async (file, fallback) => {
17
+ try {
18
+ if ((await fs.stat(file)).size > MAX_AUTH_BYTES) throw new Error();
19
+ return JSON.parse(await fs.readFile(file, 'utf8'));
20
+ } catch (error) {
21
+ if (error.code === 'ENOENT') return fallback;
22
+ throw new Error('Unable to read Magpie authorization store');
23
+ }
24
+ };
25
+
26
+ export const validateMagpieAuth = (value) => {
27
+ if (!object(value) || !object(value.plugins) || !Array.isArray(value.logins)
28
+ || typeof value.incomplete !== 'boolean' || JSON.stringify(value).length > MAX_AUTH_BYTES
29
+ || Object.entries(value.plugins).some(([key, auth]) => !accountKey(key) || !object(auth)
30
+ || !['api', 'oauth'].includes(auth.type)
31
+ || (auth.type === 'api' ? typeof auth.key !== 'string' || !auth.key
32
+ : typeof auth.access !== 'string' || typeof auth.refresh !== 'string'))
33
+ || value.logins.some((row) => !portableLogin(row, value.plugins))) {
34
+ throw new Error('Invalid Magpie authorization payload');
35
+ }
36
+ return value;
37
+ };
38
+
39
+ export const collectMagpieAuth = async (dir, accounts) => {
40
+ if (!Array.isArray(accounts) || accounts.some((row) => !object(row) || typeof row.agent !== 'string')) {
41
+ throw new Error('Unable to read source Magpie accounts');
42
+ }
43
+ const plugins = await readStore(path.join(dir, 'plugin-auth.json'), {});
44
+ const rows = await readStore(path.join(dir, 'logins.json'), []);
45
+ if (!object(plugins) || !Array.isArray(rows) || rows.some((row) => !object(row))) {
46
+ throw new Error('Invalid Magpie authorization store');
47
+ }
48
+ const available = new Set(accounts.map((row) => row.agent));
49
+ const visiblePlugins = Object.fromEntries(Object.entries(plugins).filter(([key]) => available.has(key.split('#')[0])
50
+ && !rows.some((row) => row.agent === `plugin:${key.split('#')[0]}` && row.home === key && row.hidden)));
51
+ const logins = rows.filter((row) => !row.hidden && portableLogin(row, visiblePlugins));
52
+ return validateMagpieAuth({ plugins: visiblePlugins, logins,
53
+ incomplete: accounts.some((account) => !Object.keys(visiblePlugins).some((key) => key.split('#')[0] === account.agent)
54
+ && !logins.some((row) => row.agent === account.agent && row.user === account.user)) });
55
+ };
56
+
57
+ const writeStore = async (file, value) => {
58
+ const staging = `${file}.${randomBytes(8).toString('hex')}.tmp`;
59
+ try {
60
+ await fs.writeFile(staging, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
61
+ await fs.rename(staging, file);
62
+ } finally { await fs.rm(staging, { force: true }); }
63
+ };
64
+
65
+ // Use Magpie's own lock filenames; never steal a lock from a running token refresh.
66
+ const lockStore = async (file) => {
67
+ for (let attempt = 0; attempt < 100; attempt++) {
68
+ try {
69
+ const handle = await fs.open(`${file}.lock`, 'wx', 0o600);
70
+ await handle.close();
71
+ return () => fs.rm(`${file}.lock`, { force: true });
72
+ } catch (error) {
73
+ if (error.code !== 'EEXIST') throw new Error('Unable to lock Magpie authorization store');
74
+ await delay(50);
75
+ }
76
+ }
77
+ throw new Error('Magpie authorization store is busy; retry sync');
78
+ };
79
+
80
+ export const restoreMagpieAuth = async (dir, input, importCodex) => {
81
+ const auth = validateMagpieAuth(input);
82
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
83
+ const pluginFile = path.join(dir, 'plugin-auth.json');
84
+ const unlockPlugins = await lockStore(pluginFile);
85
+ try {
86
+ const plugins = await readStore(pluginFile, {});
87
+ if (!object(plugins)) throw new Error('Invalid destination Magpie authorization store');
88
+ await writeStore(pluginFile, { ...plugins, ...auth.plugins });
89
+ } finally { await unlockPlugins(); }
90
+ // Codex's native importer owns its OS-level logins lock, account deduplication and identity.
91
+ for (const row of auth.logins) {
92
+ if (row.agent === 'codex' && !row.hidden) await importCodex(row.auth);
93
+ }
94
+ };
95
+
96
+ export const verifyMagpieAuth = (auth, accounts) => {
97
+ if (!Array.isArray(accounts) || accounts.some((row) => !object(row) || typeof row.agent !== 'string')) {
98
+ throw new Error('Unable to verify Magpie accounts');
99
+ }
100
+ const expected = new Map();
101
+ for (const key of Object.keys(auth.plugins)) {
102
+ const id = key.split('#')[0];
103
+ expected.set(id, (expected.get(id) || 0) + 1);
104
+ }
105
+ const missingAccount = auth.logins.some((row) => !row.hidden
106
+ && !accounts.some((account) => account.agent === row.agent.replace(/^plugin:/, '')
107
+ && String(account.user).toLowerCase() === row.user.toLowerCase()));
108
+ if (missingAccount || [...expected].some(([id, count]) => accounts.filter((row) => row.agent === id).length < count)) {
109
+ throw new Error('Magpie authorization saved but some accounts are not recognized; check destination plugins and retry');
110
+ }
111
+ };
@@ -0,0 +1,79 @@
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 { collectMagpieAuth, restoreMagpieAuth, validateMagpieAuth, verifyMagpieAuth } from './magpie-auth.js';
6
+
7
+ const dirs = [];
8
+ const directory = async () => {
9
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'magpie-auth-test-'));
10
+ dirs.push(dir);
11
+ return dir;
12
+ };
13
+ const auth = { plugins: { 'cursor#one': { type: 'oauth', access: 'fixture-access', refresh: 'fixture-refresh', expires: 123 } }, logins: [], incomplete: false };
14
+ afterEach(async () => { vi.restoreAllMocks(); await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); });
15
+
16
+ describe('Magpie authorization transfer', () => {
17
+ it('exports recognized plugin accounts, excludes hidden/stale accounts and flags unsupported subscriptions', async () => {
18
+ const dir = await directory();
19
+ await fs.writeFile(path.join(dir, 'plugin-auth.json'), JSON.stringify({ ...auth.plugins,
20
+ stale: { type: 'api', key: 'fixture' }, 'cursor#hidden': { type: 'api', key: 'fixture' } }));
21
+ await fs.writeFile(path.join(dir, 'logins.json'), JSON.stringify([
22
+ { agent: 'plugin:cursor', user: 'hidden', home: 'cursor#hidden', hidden: 'mark' },
23
+ { agent: 'chatgpt-api', user: 'fixture', auth: { clientId: 'machine-specific' } },
24
+ ]));
25
+ const result = await collectMagpieAuth(dir, [{ agent: 'cursor' }, { agent: 'chatgpt-api', user: 'fixture' }]);
26
+ expect(result.plugins).toEqual(auth.plugins);
27
+ expect(result.logins).toEqual([]);
28
+ expect(result.incomplete).toBe(true);
29
+ });
30
+
31
+ it('does not turn corrupted stores into successful empty credentials', async () => {
32
+ const dir = await directory();
33
+ await fs.writeFile(path.join(dir, 'plugin-auth.json'), '{broken');
34
+ await expect(collectMagpieAuth(dir, [])).rejects.toThrow('authorization store');
35
+ await expect(restoreMagpieAuth(dir, auth, vi.fn())).rejects.toThrow('authorization store');
36
+ expect(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8')).toBe('{broken');
37
+ expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
38
+ });
39
+
40
+ it('merges repeated imports without deleting unrelated accounts and releases the plugin lock before native imports', async () => {
41
+ const dir = await directory();
42
+ await fs.writeFile(path.join(dir, 'plugin-auth.json'), '{"other":{"type":"api","key":"keep"}}');
43
+ const input = { ...auth, logins: [{ agent: 'codex', user: 'fixture', auth: { tokens: {} } }] };
44
+ const importCodex = vi.fn(async () => {
45
+ await expect(fs.stat(path.join(dir, 'plugin-auth.json.lock'))).rejects.toThrow();
46
+ });
47
+ await restoreMagpieAuth(dir, input, importCodex);
48
+ await restoreMagpieAuth(dir, input, importCodex);
49
+ expect(JSON.parse(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8'))).toEqual({ other: { type: 'api', key: 'keep' }, ...auth.plugins });
50
+ expect((await fs.stat(path.join(dir, 'plugin-auth.json'))).mode & 0o777).toBe(0o600);
51
+ expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
52
+ });
53
+
54
+ it('keeps successful plugin writes when a native import fails, so retry can finish', async () => {
55
+ const dir = await directory();
56
+ await expect(restoreMagpieAuth(dir, { ...auth, logins: [{ agent: 'codex', user: 'fixture', auth: { tokens: {} } }] },
57
+ async () => { throw new Error('fixture failure'); })).rejects.toThrow();
58
+ expect(JSON.parse(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8'))).toEqual(auth.plugins);
59
+ expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
60
+ });
61
+
62
+ it('preserves the previous store and cleans staging files when atomic publication fails', async () => {
63
+ const dir = await directory();
64
+ const file = path.join(dir, 'plugin-auth.json');
65
+ await fs.writeFile(file, '{"other":{"type":"api","key":"keep"}}');
66
+ vi.spyOn(fs, 'rename').mockRejectedValueOnce(new Error('fixture rename failure'));
67
+ await expect(restoreMagpieAuth(dir, auth, vi.fn())).rejects.toThrow();
68
+ expect(JSON.parse(await fs.readFile(file, 'utf8'))).toEqual({ other: { type: 'api', key: 'keep' } });
69
+ expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
70
+ });
71
+
72
+ it('rejects unsafe shapes and verifies every expected account rather than one account per provider', () => {
73
+ expect(() => validateMagpieAuth({ ...auth, plugins: { '../path': { type: 'api', key: 'fixture' } } })).toThrow();
74
+ const two = { ...auth, plugins: { ...auth.plugins, 'cursor#two': auth.plugins['cursor#one'] } };
75
+ expect(() => verifyMagpieAuth(two, [{ agent: 'cursor' }])).toThrow('not recognized');
76
+ expect(() => verifyMagpieAuth(two, [{ agent: 'cursor' }, { agent: 'cursor' }])).not.toThrow();
77
+ expect(() => verifyMagpieAuth(auth, {})).toThrow('verify');
78
+ });
79
+ });
@@ -0,0 +1,276 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { createHash, randomBytes } from 'node:crypto';
6
+ import { collectMagpieAuth, restoreMagpieAuth, validateMagpieAuth, verifyMagpieAuth } from './magpie-auth.js';
7
+
8
+ const MAX_BYTES = 64 * 1024 * 1024;
9
+ const CONFIG_FILES = ['providers.json', 'settings.json', 'profiles.json', 'library.json', 'plugins.json', 'plugin-auth.json', 'logins.json'];
10
+ const exists = async (file) => {
11
+ try { return await fs.stat(file); }
12
+ catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null; throw error; }
13
+ };
14
+
15
+ // Raw CLI output may contain provider names, paths or credentials. Never forward it.
16
+ const runCli = (binary, args, { input = '', env, timeout = 600_000 } = {}) => new Promise((resolve, reject) => {
17
+ const child = spawn(binary, args, { env, windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
18
+ let output = '';
19
+ let exceeded = false;
20
+ const stop = () => {
21
+ exceeded = true;
22
+ try {
23
+ if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, 'SIGKILL');
24
+ else child.kill('SIGKILL');
25
+ } catch { child.kill('SIGKILL'); }
26
+ };
27
+ const timer = setTimeout(stop, timeout);
28
+ const collect = (chunk) => {
29
+ if (output.length + chunk.length > 1024 * 1024) { stop(); return; }
30
+ output += chunk.toString();
31
+ };
32
+ child.stdout.on('data', collect);
33
+ child.stderr.on('data', collect);
34
+ child.stdin.on('error', () => {});
35
+ child.on('error', () => { clearTimeout(timer); reject(new Error('Unable to start Magpie CLI')); });
36
+ child.on('close', (code) => {
37
+ clearTimeout(timer);
38
+ if (code !== 0 || exceeded) reject(new Error('Magpie CLI operation failed or timed out'));
39
+ else resolve(output.replace(/\x1b\[[0-9;]*m/g, ''));
40
+ });
41
+ child.stdin.end(input);
42
+ });
43
+
44
+ /** Host-owned discovery and native backup/restore. No renderer-supplied paths or commands. */
45
+ export function createMagpieSync({
46
+ homedir = os.homedir,
47
+ env = process.env,
48
+ platform = process.platform,
49
+ arch = process.arch,
50
+ fetchImpl = fetch,
51
+ run = runCli,
52
+ tempdir = os.tmpdir,
53
+ } = {}) {
54
+ let installing = null;
55
+ let busy = false;
56
+ const home = () => homedir();
57
+ const configDir = () => path.join(env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME)
58
+ ? env.XDG_CONFIG_HOME : path.join(home(), '.config'), 'magpie');
59
+ const managedBinary = () => path.join(env.OPENCHAMBER_DATA_DIR || path.join(home(), '.config', 'openchamber'),
60
+ 'tools', 'magpie', platform === 'win32' ? 'magpie.exe' : 'magpie');
61
+ const hasConfig = async (dir) => {
62
+ for (const file of CONFIG_FILES) if ((await exists(path.join(dir, file)))?.isFile()) return true;
63
+ return false;
64
+ };
65
+
66
+ const locate = async () => {
67
+ const name = platform === 'win32' ? 'magpie.exe' : 'magpie';
68
+ const candidates = [
69
+ env.MAGPIE_BINARY,
70
+ ...(env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name)),
71
+ path.join(home(), '.local', 'bin', name),
72
+ ...(platform === 'darwin' ? [
73
+ '/Applications/magpie.app/Contents/MacOS/magpie',
74
+ path.join(home(), 'Applications', 'magpie.app', 'Contents', 'MacOS', 'magpie'),
75
+ '/opt/homebrew/bin/magpie', '/usr/local/bin/magpie',
76
+ ] : []),
77
+ ...(platform === 'win32' && env.LOCALAPPDATA ? [path.join(env.LOCALAPPDATA, 'Programs', 'magpie', name)] : []),
78
+ managedBinary(),
79
+ ].filter(Boolean);
80
+ for (const candidate of new Set(candidates)) {
81
+ if (!(await exists(candidate))?.isFile()) continue;
82
+ const binary = await fs.realpath(candidate);
83
+ const version = await run(binary, ['--version'], { env, timeout: 10_000 });
84
+ if (!/^magpie\s+\S+/m.test(version)) throw new Error('Invalid Magpie executable');
85
+ return binary;
86
+ }
87
+ return null;
88
+ };
89
+
90
+ const dataDir = async (binary) => {
91
+ if (!binary) return configDir();
92
+ let beside = path.dirname(binary);
93
+ if (path.basename(beside) === 'MacOS' && path.basename(path.dirname(beside)) === 'Contents') {
94
+ beside = path.dirname(path.dirname(path.dirname(beside)));
95
+ } else if (platform === 'linux' && env.APPIMAGE && env.APPDIR
96
+ && binary.startsWith(`${env.APPDIR}${path.sep}`)) beside = path.dirname(env.APPIMAGE);
97
+ const portable = path.join(beside, 'data');
98
+ if (await exists(path.join(beside, '.portable'))) return portable;
99
+ if (!(await exists(portable))?.isDirectory()) return configDir();
100
+ const shared = [home(), '/Applications', ...['Applications', 'Downloads', 'Desktop', 'Documents'].map((dir) => path.join(home(), dir))];
101
+ if (!shared.some((dir) => dir.toLowerCase() === beside.toLowerCase())) return portable;
102
+ return await hasConfig(portable) && !await hasConfig(configDir()) ? portable : configDir();
103
+ };
104
+
105
+ const inventory = async () => {
106
+ const binary = await locate();
107
+ return { available: Boolean(binary) && await hasConfig(await dataDir(binary)), installed: Boolean(binary) };
108
+ };
109
+
110
+ const downloadBytes = async (url, limit) => {
111
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
112
+ if (!response.ok || !response.body) throw new Error('Unable to download Magpie CLI');
113
+ const chunks = [];
114
+ let size = 0;
115
+ for await (const chunk of response.body) {
116
+ size += chunk.length;
117
+ if (size > limit) throw new Error('Magpie download exceeds size limit');
118
+ chunks.push(Buffer.from(chunk));
119
+ }
120
+ return Buffer.concat(chunks);
121
+ };
122
+
123
+ const publishExecutable = async (binary) => {
124
+ const entry = path.join(home(), '.local', 'bin', platform === 'win32' ? 'magpie.exe' : 'magpie');
125
+ if (await exists(entry)) {
126
+ if (await fs.realpath(entry) === binary) return binary;
127
+ const version = await run(entry, ['--version'], { env, timeout: 10_000 });
128
+ if (!/^magpie\s+\S+/m.test(version)) throw new Error('Magpie command conflicts with an existing executable');
129
+ if (await dataDir(await fs.realpath(entry)) !== await dataDir(binary)) {
130
+ throw new Error('Magpie command uses a different portable configuration; resolve the command conflict before syncing');
131
+ }
132
+ return binary;
133
+ }
134
+ await fs.mkdir(path.dirname(entry), { recursive: true });
135
+ try {
136
+ if (platform === 'win32' && await dataDir(binary) === configDir()) await fs.link(binary, entry);
137
+ else await fs.symlink(binary, entry, 'file');
138
+ } catch (error) {
139
+ if (error.code !== 'EEXIST' || !await exists(entry)) throw new Error('Unable to publish Magpie command for plugins');
140
+ }
141
+ return binary;
142
+ };
143
+
144
+ const repairInstallation = async () => {
145
+ const binary = managedBinary();
146
+ if (!(await exists(binary))?.isFile()) return;
147
+ const version = await run(binary, ['--version'], { env, timeout: 10_000 });
148
+ if (!/^magpie\s+\S+/m.test(version)) throw new Error('Invalid managed Magpie executable');
149
+ await publishExecutable(await fs.realpath(binary));
150
+ };
151
+
152
+ const ensure = async () => {
153
+ const binary = await locate();
154
+ if (binary) return publishExecutable(binary);
155
+ if (installing) return installing;
156
+ installing = (async () => {
157
+ const cpu = { x64: 'amd64', arm64: 'arm64' }[arch];
158
+ if (!cpu || !['darwin', 'linux', 'win32'].includes(platform)) throw new Error('Magpie CLI is not available for this platform');
159
+ const assetName = `magpie-cli-${platform === 'win32' ? 'windows' : platform}-${cpu}${platform === 'win32' ? '.exe' : ''}`;
160
+ let feed;
161
+ try { feed = JSON.parse((await downloadBytes('https://usemagpie.ai/api/latest', 2 * 1024 * 1024)).toString()); }
162
+ catch { throw new Error('Unable to read Magpie release metadata'); }
163
+ const asset = feed.assets?.[assetName];
164
+ if (!asset || !/^[a-f0-9]{64}$/i.test(asset.sha256 || '')
165
+ || typeof asset.url !== 'string' || !asset.url.startsWith('https://github.com/yetone/magpie-releases/releases/download/')) {
166
+ throw new Error('No verified Magpie CLI release for this platform');
167
+ }
168
+ const bytes = await downloadBytes(asset.url, 256 * 1024 * 1024);
169
+ if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256.toLowerCase()) throw new Error('Magpie CLI checksum mismatch');
170
+ const target = managedBinary();
171
+ await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
172
+ const staging = `${target}.${randomBytes(8).toString('hex')}${platform === 'win32' ? '.exe' : ''}`;
173
+ try {
174
+ await fs.writeFile(staging, bytes, { mode: 0o700, flag: 'wx' });
175
+ const version = await run(staging, ['--version'], { env, timeout: 10_000 });
176
+ if (!/^magpie\s+\S+/m.test(version)) throw new Error('Invalid downloaded Magpie CLI');
177
+ await fs.rename(staging, target);
178
+ } finally { await fs.rm(staging, { force: true }); }
179
+ return publishExecutable(target);
180
+ })();
181
+ try { return await installing; }
182
+ finally { installing = null; }
183
+ };
184
+
185
+ const exclusive = async (work) => {
186
+ if (busy) throw new Error('Magpie sync already in progress');
187
+ busy = true;
188
+ try { return await work(); } finally { busy = false; }
189
+ };
190
+ const withTemp = async (work) => {
191
+ const dir = await fs.mkdtemp(path.join(tempdir(), 'openchamber-magpie-'));
192
+ try { return await work(path.join(dir, 'setup.magpie-backup')); }
193
+ finally { await fs.rm(dir, { recursive: true, force: true }); }
194
+ };
195
+
196
+ return {
197
+ inventory,
198
+ repairInstallation,
199
+ download: () => exclusive(async () => {
200
+ if (!(await inventory()).available) throw new Error('No Magpie configuration found on source');
201
+ const binary = await ensure();
202
+ const passphrase = randomBytes(32).toString('hex');
203
+ return withTemp(async (file) => {
204
+ await run(binary, ['backup', file], { env, input: `${passphrase}\n` });
205
+ const stat = await fs.stat(file);
206
+ if (stat.size > MAX_BYTES) throw new Error('Magpie backup exceeds size limit');
207
+ const backup = await fs.readFile(file);
208
+ let authorization;
209
+ try {
210
+ const accounts = JSON.parse(await run(binary, ['accounts', '--json'], { env }));
211
+ authorization = await collectMagpieAuth(await dataDir(binary), accounts);
212
+ } catch { throw new Error('Unable to export Magpie authorizations'); }
213
+ const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v2', passphrase, backup: backup.toString('base64'), authorization }));
214
+ if (payload.length > MAX_BYTES * 2) throw new Error('Magpie sync exceeds size limit');
215
+ return payload;
216
+ });
217
+ }),
218
+ import: (payload) => exclusive(async () => {
219
+ let decoded;
220
+ let backup;
221
+ try {
222
+ if (payload.length > MAX_BYTES * 2) throw new Error();
223
+ decoded = JSON.parse(Buffer.from(payload).toString());
224
+ if (!['magpie-sync-v1', 'magpie-sync-v2'].includes(decoded.format) || !/^[a-f0-9]{64}$/.test(decoded.passphrase)
225
+ || typeof decoded.backup !== 'string') throw new Error();
226
+ if (decoded.format === 'magpie-sync-v2') validateMagpieAuth(decoded.authorization);
227
+ backup = Buffer.from(decoded.backup, 'base64');
228
+ if (!backup.length || backup.length > MAX_BYTES || JSON.parse(backup.toString()).format !== 'magpie-backup') throw new Error();
229
+ } catch { throw new Error('Invalid Magpie sync payload'); }
230
+ const binary = await ensure();
231
+ return withTemp(async (file) => {
232
+ await fs.writeFile(file, backup, { mode: 0o600, flag: 'wx' });
233
+ let output;
234
+ try { output = await run(binary, ['restore', file], { env, input: `${decoded.passphrase}\n` }); }
235
+ catch { throw new Error('Magpie restore failed; some configuration may already have been applied. Retry to finish.'); }
236
+ if (/Couldn't add the plugin/i.test(output)) throw new Error('Magpie restore partially completed: plugin installation failed. Check the target Magpie plugins.');
237
+ if (/Library:.*couldn't/i.test(output)) throw new Error('Magpie restore partially completed: library application failed. Check the target Magpie library.');
238
+ if (/Needs a key/i.test(output)) throw new Error('Magpie restore partially completed: some providers still require API keys.');
239
+ let needsSignIn = /not signed in on this computer/i.test(output);
240
+ if (decoded.format === 'magpie-sync-v2') {
241
+ const dir = await dataDir(binary);
242
+ try {
243
+ await restoreMagpieAuth(dir, decoded.authorization, async (auth) => {
244
+ const credentialFile = path.join(path.dirname(file), 'codex.json');
245
+ try {
246
+ await fs.writeFile(credentialFile, JSON.stringify(auth), { mode: 0o600, flag: 'wx' });
247
+ await run(binary, ['accounts', 'import', 'codex', credentialFile, '--yes'], { env });
248
+ } finally { await fs.rm(credentialFile, { force: true }); }
249
+ });
250
+ const accounts = JSON.parse(await run(binary, ['accounts', '--json'], { env }));
251
+ verifyMagpieAuth(decoded.authorization, accounts);
252
+ const configured = JSON.parse(await fs.readFile(path.join(dir, 'providers.json'), 'utf8').catch((error) => {
253
+ if (error.code === 'ENOENT') return '{"providers":[]}';
254
+ throw error;
255
+ }));
256
+ if (!Array.isArray(configured.providers)) throw new Error();
257
+ const signedIn = new Set(accounts.map((account) => account.agent));
258
+ needsSignIn = decoded.authorization.incomplete || configured.providers.some((provider) =>
259
+ !provider.hidden && provider.id && !['chat', 'responses', 'anthropic', 'gemini'].some((key) => provider[key])
260
+ && !signedIn.has(provider.id));
261
+ if (/Left as they are/i.test(output)) {
262
+ output = await run(binary, ['restore', file], { env, input: `${decoded.passphrase}\n` });
263
+ if (/Couldn't add the plugin|Needs a key|Library:.*couldn't/i.test(output)) throw new Error();
264
+ }
265
+ } catch {
266
+ throw new Error('Magpie authorization sync or verification failed; some configuration and accounts may already be applied. Retry to finish.');
267
+ }
268
+ }
269
+ const magpieWarnings = [];
270
+ if (needsSignIn) magpieWarnings.push('sign_in_required');
271
+ if (/Left as they are/i.test(output)) magpieWarnings.push('agent_settings_skipped');
272
+ return { ok: true, ...(magpieWarnings.length ? { magpieWarnings } : {}) };
273
+ });
274
+ }),
275
+ };
276
+ }