@openchambery/web 1.18.2-beta.2 → 1.18.2-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-B2kZjgaf.js → AgentAvatar-B4fj9PIe.js} +1 -1
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-C00aiEHK.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BDFeDKu-.js} +39 -39
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-CUfSFI3e.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-CKKU6fKI.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DiIuWOuC.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-Cen6E9Lj.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-DILi0tN7.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-PZMRiieI.js} +1 -1
- package/dist/assets/MobileSurface-BLYcR0tO.js +2 -0
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-D_2mWZyv.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-CYeRZznl.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-BgMLtMIq.js} +1 -1
- package/dist/assets/SettingsView-CbDBfQp1.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-Br7XLcSd.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CM5jRKlj.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-D3U6aTZ4.js} +1 -1
- package/dist/assets/appThemeRegistry-CDxBJNNB.js +2 -0
- package/dist/assets/assistant-guide-hero-BeUHQcxE.jpg +0 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DaAoVF4X.js} +1 -1
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-BnElHSnO.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-BmuQu0dq.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-DBOMervd.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-ZK_vkdzL.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-CewM6-g5.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-DAE4CZy2.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog-COrLBm9O.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DIVYdoxi.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-CmWYnRVz.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-CNTlVypq.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BQT3QXD2.js} +1 -1
- package/dist/assets/gitApiHttp-BxGAFLTf.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-XiL2-2gG.js} +1 -1
- package/dist/assets/insertionBoundaries-9a0RuIDc.js +1 -0
- package/dist/assets/{ja-CzMPz7bT.js → ja-C_eW5ZM8.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-CE3cOv7v.js} +1 -1
- package/dist/assets/{main-LDphyc-v.js → main-BGfX81Y9.js} +20 -20
- package/dist/assets/{main-CO6z5yRB.js → main-CEqSDnE2.js} +2 -2
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-BDyY0vt6.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-D-aLQZaM.js} +1 -1
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-CKucSAhg.js} +2 -2
- package/dist/assets/mobile-C4bTWKry.js +2 -0
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-DeNtSkgb.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-D1lIuyqO.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-BZKZOE99.js} +1 -1
- package/dist/assets/projectMeta-BunN9pH5.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-DHDgp8S3.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-Bziafaje.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-D8-FDgWE.js +2 -0
- package/dist/assets/renderMobileApp-CdU0aJ-e.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-BXCt7ZsQ.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CK1GhIsm.js +145 -0
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-pWlBorwQ.js} +1 -1
- package/dist/assets/src-8kQFmtjw.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-CUWqzRt7.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-DpiAJlil.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-BLugzWq-.js} +1 -1
- package/dist/assets/{uk-xGc9Pzge.js → uk-CfQK6cz1.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DMs_PgIC.js} +1 -1
- package/dist/assets/useAppFontEffects-IEUQDvE8.js +7 -0
- package/dist/assets/useDesktopWindowControlsLayout-MlqJX89k.js +1 -0
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-GbhkdO6m.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-92Qg-98H.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DQHF_ETu.js +476 -0
- package/dist/assets/useSessionAutoCleanup-BuU_S_Sh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-Ca6OgNHF.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-BxJEIWv5.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-DvY2fKWp.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-eWe0lm3-.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.d.ts +6 -0
- package/server/index.js +13 -0
- package/server/lib/config-sync/DOCUMENTATION.md +70 -0
- package/server/lib/config-sync/config-sync.test.js +197 -0
- package/server/lib/config-sync/constants.js +39 -0
- package/server/lib/config-sync/contract.js +130 -0
- package/server/lib/config-sync/credential-auth.js +39 -0
- package/server/lib/config-sync/engine.js +108 -0
- package/server/lib/config-sync/index.js +67 -0
- package/server/lib/config-sync/local-backup.js +171 -0
- package/server/lib/config-sync/plan.js +318 -0
- package/server/lib/config-sync/receiver.js +330 -0
- package/server/lib/config-sync/routes.js +141 -0
- package/server/lib/config-sync/routes.test.js +154 -0
- package/server/lib/config-sync/scripts.js +264 -0
- package/server/lib/config-sync/selections.js +112 -0
- package/server/lib/config-sync/tar.js +35 -0
- package/server/lib/config-sync/target-id.js +24 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/diff-summary.js +39 -11
- package/server/lib/event-stream/diff-summary.test.js +73 -6
- package/server/lib/event-stream/protocol.test.js +70 -7
- package/server/lib/notifications/DOCUMENTATION.md +2 -2
- package/server/lib/notifications/runtime.js +68 -27
- package/server/lib/notifications/runtime.test.js +176 -0
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +10 -1
- package/server/lib/opencode/openchamber-routes.js +7 -0
- package/server/lib/opencode/settings-runtime.js +82 -51
- package/server/lib/opencode/settings-runtime.test.js +42 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/session-index/DOCUMENTATION.md +12 -6
- package/server/lib/session-index/routes.js +34 -0
- package/server/lib/session-index/routes.test.js +76 -0
- package/server/lib/session-index/service.js +53 -4
- package/server/lib/session-index/service.test.js +72 -2
- package/server/lib/session-title/DOCUMENTATION.md +19 -14
- package/server/lib/session-title/runtime.js +8 -8
- package/server/lib/session-title/runtime.test.js +231 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +26 -6
- package/server/lib/session-turn-pages/changes.service.js +156 -0
- package/server/lib/session-turn-pages/changes.service.test.js +188 -0
- package/server/lib/session-turn-pages/reconcile.routes.test.js +52 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +44 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +387 -0
- package/server/lib/session-turn-pages/service.js +31 -7
- package/server/lib/session-turn-pages/service.test.js +40 -0
- package/server/lib/small-model/DOCUMENTATION.md +93 -38
- package/server/lib/small-model/catalog.js +10 -2
- package/server/lib/small-model/catalog.test.js +3 -1
- package/server/lib/small-model/index.js +52 -28
- package/server/lib/small-model/opencode-session.js +339 -0
- package/server/lib/small-model/opencode-session.test.js +177 -0
- package/server/lib/small-model/resolve.js +116 -5
- package/server/lib/small-model/resolve.test.js +76 -10
- package/server/lib/small-model/summary-settings.test.js +60 -3
- package/dist/assets/MobileSurface-htGkf7Ua.js +0 -2
- package/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/appThemeRegistry-CgfOR_0L.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/insertionBoundaries-DzXMTleb.js +0 -1
- package/dist/assets/mobile-BktO3_4R.js +0 -2
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-Db8uO-6q.js +0 -2
- package/dist/assets/renderMobileApp-BCV81Ryl.js +0 -23
- package/dist/assets/runtimeEndpointReset-Dg3DMy9v.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useAppFontEffects-CM0VKsdo.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-Cq-r4Hcb.js +0 -1
- package/dist/assets/useMobileNavigationStore-DnzT4jO2.js +0 -475
- package/dist/assets/useSessionAutoCleanup-DxZeceKO.js +0 -1
package/dist/index.html
CHANGED
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-CEqSDnE2.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -560,15 +560,15 @@
|
|
|
560
560
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
561
561
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
562
562
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
563
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
564
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
565
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
563
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-D1lIuyqO.js">
|
|
564
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-ZK_vkdzL.js">
|
|
565
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CewM6-g5.js">
|
|
566
566
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
567
567
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
568
|
-
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-
|
|
569
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
570
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
571
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
568
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
569
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
570
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
571
|
+
<link rel="stylesheet" crossorigin href="/assets/src-8kQFmtjw.css">
|
|
572
572
|
</head>
|
|
573
573
|
<body class="h-full bg-background text-foreground">
|
|
574
574
|
<!-- Keep the startup layer outside #root. React replaces every child of
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-CKucSAhg.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -82,15 +82,15 @@
|
|
|
82
82
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
85
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-D1lIuyqO.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-ZK_vkdzL.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CewM6-g5.js">
|
|
88
88
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
89
89
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
92
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
93
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
93
|
+
<link rel="stylesheet" crossorigin href="/assets/src-8kQFmtjw.css">
|
|
94
94
|
</head>
|
|
95
95
|
<body class="h-full bg-background text-foreground">
|
|
96
96
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-C4bTWKry.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -71,15 +71,15 @@
|
|
|
71
71
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
73
73
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
74
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
75
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
76
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
74
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-D1lIuyqO.js">
|
|
75
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-ZK_vkdzL.js">
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CewM6-g5.js">
|
|
77
77
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
79
|
-
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-
|
|
80
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
82
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
79
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
80
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
82
|
+
<link rel="stylesheet" crossorigin href="/assets/src-8kQFmtjw.css">
|
|
83
83
|
</head>
|
|
84
84
|
<body class="h-full bg-background text-foreground">
|
|
85
85
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
package/server/index.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface StartWebUiServerOptions {
|
|
|
25
25
|
getSshRoutingTable?: () => { id: string; localPort: number }[];
|
|
26
26
|
/** Mint a stored SSH host clientToken for a ready session (Electron). */
|
|
27
27
|
mintSshHostToken?: (hostId: string) => Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Optional process-local lock for settings persistence (Electron in-process).
|
|
30
|
+
* When provided, every settings write/migrate joins this exclusive chain so
|
|
31
|
+
* concurrent desktop writers cannot clobber sibling fields in settings.json.
|
|
32
|
+
*/
|
|
33
|
+
settingsPersistLock?: <T>(work: () => Promise<T> | T) => Promise<T>;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export declare function startWebUiServer(
|
package/server/index.js
CHANGED
|
@@ -1240,6 +1240,11 @@ async function main(options = {}) {
|
|
|
1240
1240
|
if (typeof options.getIsWindowFocused === 'function') {
|
|
1241
1241
|
notificationTriggerRuntime.setGetIsWindowFocused(options.getIsWindowFocused);
|
|
1242
1242
|
}
|
|
1243
|
+
// Electron in-process: join the desktop settings mutation chain so web
|
|
1244
|
+
// persistSettings cannot race main/ssh-manager writers on settings.json.
|
|
1245
|
+
if (typeof options.settingsPersistLock === 'function') {
|
|
1246
|
+
settingsRuntime.setRunExclusivePersist(options.settingsPersistLock);
|
|
1247
|
+
}
|
|
1243
1248
|
const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
|
|
1244
1249
|
? options.getDesktopRuntimeConfig
|
|
1245
1250
|
: null;
|
|
@@ -1441,6 +1446,7 @@ async function main(options = {}) {
|
|
|
1441
1446
|
// is not an instance identity — one machine can run several servers.
|
|
1442
1447
|
getServerLabel: () => 'OpenChamber',
|
|
1443
1448
|
readSettingsFromDiskMigrated,
|
|
1449
|
+
persistSettings,
|
|
1444
1450
|
normalizeTunnelSessionTtlMs,
|
|
1445
1451
|
authorizeManagedOpenCodeBridgeRequest: managedCapabilitiesRuntime.authorizeManagedOpenCodeBridgeRequest,
|
|
1446
1452
|
sayTTSCapability,
|
|
@@ -1708,6 +1714,13 @@ async function main(options = {}) {
|
|
|
1708
1714
|
} catch {
|
|
1709
1715
|
// best-effort shutdown of the dictation worker
|
|
1710
1716
|
}
|
|
1717
|
+
try {
|
|
1718
|
+
if (smallModelServiceInstance?.stop) {
|
|
1719
|
+
await smallModelServiceInstance.stop();
|
|
1720
|
+
}
|
|
1721
|
+
} catch {
|
|
1722
|
+
// best-effort cleanup of the small-model temp OpenCode directory
|
|
1723
|
+
}
|
|
1711
1724
|
return gracefulShutdown({
|
|
1712
1725
|
exitProcess: shutdownOptions.exitProcess ?? false,
|
|
1713
1726
|
forceCloseConnections: shutdownOptions.forceCloseConnections === true,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Config Sync Module
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Own the direction-agnostic OpenCode config sync contract: allowlist planning on the source, target identity/capabilities, POSIX prepare/probe/finalize scripts, tar collection, selection filtering, local/remote generational backups, and apply orchestration. Electron's managed SSH push **and pull** paths are the first executors; non-SSH targets reuse the same shapes.
|
|
6
|
+
|
|
7
|
+
## Entrypoints
|
|
8
|
+
|
|
9
|
+
| File | Role |
|
|
10
|
+
|------|------|
|
|
11
|
+
| `index.js` | Public re-exports |
|
|
12
|
+
| `constants.js` | Allowlist, backup roots, size limits, direction constants, generation retention (`N=5`) |
|
|
13
|
+
| `contract.js` | `SyncTarget`, capabilities, `TargetExecutor` typedefs, capability asserts |
|
|
14
|
+
| `plan.js` | `planOpenCodeConfigSync` / `planOpenCodeConfigSyncFromInventory` |
|
|
15
|
+
| `selections.js` | Default/normalize/filter whitelist selections |
|
|
16
|
+
| `scripts.js` | Probe / inventory / generational prepare / finalize / remote tar scripts |
|
|
17
|
+
| `local-backup.js` | Local (pull destination) generational backup + tar extract |
|
|
18
|
+
| `tar.js` | `collectLocalTarBuffer` (buffered) |
|
|
19
|
+
| `engine.js` | `applyConfigSyncPlan` orchestration for **push** (prepare → putTar* → finalize) |
|
|
20
|
+
| `credential-auth.js` | Credential grant enforcement helpers |
|
|
21
|
+
| `target-id.js` | `ssh:<instanceId>` namespace helper |
|
|
22
|
+
|
|
23
|
+
## Contract shape
|
|
24
|
+
|
|
25
|
+
- **SyncTarget**: `{ id, kind, capabilities: { posixShell, tarExtract, authFileWrite } }`
|
|
26
|
+
- **TargetExecutor**: `{ probe(plan), prepare(plan, { syncRunId }), putTar({ kind, payload }), finalize(plan, { syncRunId }) }`
|
|
27
|
+
- `payload`: `Buffer | Uint8Array` — all executors currently buffer; reintroduce streaming only when a transport needs it.
|
|
28
|
+
- **Plan** (source-computed): `{ direction: 'push' | 'pull', syncRunId?, sourceTargetId?, targetId?, files, directories, agentsRoot, authFile, deletes, totalBytes, selections? }`
|
|
29
|
+
- **Selections**: `{ fileGroups: boolean[], singleFiles: boolean[], directories: boolean[], agentsRoot: boolean, authFile: boolean }`
|
|
30
|
+
- Preview and apply must share one selections snapshot so the confirmed scope cannot drift.
|
|
31
|
+
|
|
32
|
+
## Direction flow
|
|
33
|
+
|
|
34
|
+
| Direction | Source plan | Destination mutate |
|
|
35
|
+
|-----------|-------------|--------------------|
|
|
36
|
+
| `push` | Local home walk (`planOpenCodeConfigSync`) | Remote prepare + putTar + finalize |
|
|
37
|
+
| `pull` | Remote inventory script → `planOpenCodeConfigSyncFromInventory` | Local `prepareLocalSyncDestination` + extract + finalize |
|
|
38
|
+
|
|
39
|
+
Switching direction in the wizard **must** discard the previous preview and re-call preview IPC (no client-side flip of an old plan).
|
|
40
|
+
|
|
41
|
+
## Invariants
|
|
42
|
+
|
|
43
|
+
- Plan is computed on the source filesystem/inventory snapshot; destination executors never re-walk the source.
|
|
44
|
+
- Managed SSH readiness is gated by `posixShell` (and `tarExtract` / `authFileWrite` as needed).
|
|
45
|
+
- Prepare (remote or local) writes generational backups under `<backupRoot>/<syncRunId>/` and prunes older than `OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS`. Failed runs keep their scene until pruned.
|
|
46
|
+
- Finalize confirms the run's backup directory exists.
|
|
47
|
+
- Run records (Electron `sync-run-store`) persist `direction` alongside `syncRunId` / summary; they are not stored in `settings.json`.
|
|
48
|
+
- **Credential sync:** target reachability ≠ credential transfer. `auth.json` is omitted unless the SSH target has an explicit grant. Push and pull reuse the same grant. Unauthorized credential plans fail with `code: credential_sync_unauthorized`.
|
|
49
|
+
|
|
50
|
+
## HTTP receive protocol (direct / later relay)
|
|
51
|
+
|
|
52
|
+
Routes under `/api/openchamber/config-sync/*` (global `/api` auth gate: session or clientToken bearer):
|
|
53
|
+
|
|
54
|
+
| Method | Path | Role |
|
|
55
|
+
|--------|------|------|
|
|
56
|
+
| POST | `/probe` | Inventory + existence vs plan; reports `inboundCredentialAuthorized` |
|
|
57
|
+
| POST | `/prepare` | Generational local backup + deletes; one inflight `syncRunId` |
|
|
58
|
+
| PUT | `/put/:kind?syncRunId=` | Stream tar.gz (`config` \| `agents` \| `auth`) |
|
|
59
|
+
| GET | `/download/:kind` | Stream tar.gz for pull source |
|
|
60
|
+
| POST | `/finalize` | Confirm backup generation + success receipt |
|
|
61
|
+
| POST | `/abort` | Clear inflight |
|
|
62
|
+
| GET/PUT | `/credential-auth` | Host inbound credential grant (`configSyncInboundCredentialAuthorized`) |
|
|
63
|
+
|
|
64
|
+
`auth` put/download requires inbound credential authorization (`credential_sync_unauthorized`). Concurrent prepare → `sync_in_progress`.
|
|
65
|
+
|
|
66
|
+
## Composition
|
|
67
|
+
|
|
68
|
+
- Electron SSH: `TargetExecutor` push + inventory/tar pull.
|
|
69
|
+
- Electron direct hosts: `direct-config-sync.mjs` calls these HTTP routes (`host:<id>` grants in `desktopCredentialSyncGrants`, channel `host-settings`).
|
|
70
|
+
- **Relay hosts (ticket 06):** UI opens an E2EE tunnel (`packages/ui/src/lib/relay/relay-config-sync.ts`) and calls the same HTTP routes via `tunnel.fetch`. Target id is `relay:<serverId>` (signing-key fingerprint). Preview/apply refresh candidates and refuse on `serverId` mismatch (`relay_identity_changed`) with no fallback identity. Outbound credential grants use channel `pairing-settings` (pairing trust UX / host trust settings for the paired relay). Inbound credential grants remain `configSyncInboundCredentialAuthorized` on the receiving Host. Auth is always bearer + grant — never “reachable ⇒ trusted”.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
10
|
+
OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
|
|
11
|
+
SYNC_DIRECTION_PULL,
|
|
12
|
+
SYNC_DIRECTION_PUSH,
|
|
13
|
+
assertPlanDirection,
|
|
14
|
+
assertTargetCapability,
|
|
15
|
+
buildDefaultSyncSelections,
|
|
16
|
+
buildRemoteSyncFinalizeScript,
|
|
17
|
+
buildRemoteSyncPrepareScript,
|
|
18
|
+
createSshSyncTarget,
|
|
19
|
+
filterPlanBySelections,
|
|
20
|
+
planOpenCodeConfigSync,
|
|
21
|
+
planOpenCodeConfigSyncFromInventory,
|
|
22
|
+
prepareLocalSyncDestination,
|
|
23
|
+
} from './index.js';
|
|
24
|
+
|
|
25
|
+
const tempDirs = [];
|
|
26
|
+
|
|
27
|
+
const makeTempDir = async () => {
|
|
28
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'oc-config-sync-'));
|
|
29
|
+
tempDirs.push(dir);
|
|
30
|
+
return dir;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
while (tempDirs.length > 0) {
|
|
35
|
+
await fsp.rm(tempDirs.pop(), { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('config-sync contract', () => {
|
|
40
|
+
it('requires direction on plans and accepts push/pull', () => {
|
|
41
|
+
expect(assertPlanDirection({ direction: SYNC_DIRECTION_PUSH })).toBe('push');
|
|
42
|
+
expect(assertPlanDirection({ direction: SYNC_DIRECTION_PULL })).toBe('pull');
|
|
43
|
+
expect(() => assertPlanDirection({})).toThrow(/direction/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('gates SSH targets by capability derived from managed mode', () => {
|
|
47
|
+
const managed = createSshSyncTarget('abc', { remoteOpenchamber: { mode: 'managed' } });
|
|
48
|
+
expect(managed.id).toBe('ssh:abc');
|
|
49
|
+
expect(managed.capabilities.posixShell).toBe(true);
|
|
50
|
+
assertTargetCapability(managed, 'posixShell');
|
|
51
|
+
|
|
52
|
+
const plain = createSshSyncTarget('abc', { remoteOpenchamber: { mode: 'external' } });
|
|
53
|
+
expect(plain.capabilities.posixShell).toBe(false);
|
|
54
|
+
expect(() => assertTargetCapability(plain, 'posixShell')).toThrow(/posixShell/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('plans from an arbitrary directory snapshot with direction (source/target decoupling)', async () => {
|
|
58
|
+
const home = await makeTempDir();
|
|
59
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
60
|
+
await fsp.mkdir(configDir, { recursive: true });
|
|
61
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{}\n');
|
|
62
|
+
|
|
63
|
+
const pushPlan = planOpenCodeConfigSync(home, {
|
|
64
|
+
direction: SYNC_DIRECTION_PUSH,
|
|
65
|
+
syncRunId: 'dry-push',
|
|
66
|
+
sourceTargetId: 'snapshot:a',
|
|
67
|
+
targetId: 'ssh:remote',
|
|
68
|
+
});
|
|
69
|
+
expect(pushPlan.direction).toBe('push');
|
|
70
|
+
expect(pushPlan.syncRunId).toBe('dry-push');
|
|
71
|
+
expect(pushPlan.sourceTargetId).toBe('snapshot:a');
|
|
72
|
+
expect(pushPlan.targetId).toBe('ssh:remote');
|
|
73
|
+
expect(pushPlan.files.map((entry) => entry.path)).toContain('opencode.jsonc');
|
|
74
|
+
|
|
75
|
+
// Same snapshot as a pull source — contract accepts reverse direction without shape change.
|
|
76
|
+
const pullPlan = planOpenCodeConfigSync(home, {
|
|
77
|
+
direction: SYNC_DIRECTION_PULL,
|
|
78
|
+
syncRunId: 'dry-pull',
|
|
79
|
+
sourceTargetId: 'ssh:remote',
|
|
80
|
+
targetId: 'local',
|
|
81
|
+
});
|
|
82
|
+
expect(pullPlan.direction).toBe('pull');
|
|
83
|
+
expect(pullPlan.files.map((entry) => entry.path)).toEqual(pushPlan.files.map((entry) => entry.path));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('filters plans by whitelist selections and builds pull plans from inventory', async () => {
|
|
87
|
+
const home = await makeTempDir();
|
|
88
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
89
|
+
await fsp.mkdir(path.join(configDir, 'skills'), { recursive: true });
|
|
90
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{}\n');
|
|
91
|
+
await fsp.writeFile(path.join(configDir, 'AGENTS.md'), 'hi\n');
|
|
92
|
+
await fsp.writeFile(path.join(configDir, 'skills', 'a.md'), 'a\n');
|
|
93
|
+
|
|
94
|
+
const full = planOpenCodeConfigSync(home, { direction: SYNC_DIRECTION_PUSH, includeAuthFile: false });
|
|
95
|
+
const selections = buildDefaultSyncSelections({ includeAuthFile: false });
|
|
96
|
+
selections.fileGroups = selections.fileGroups.map((_, index) => index === 0);
|
|
97
|
+
selections.singleFiles = selections.singleFiles.map(() => false);
|
|
98
|
+
selections.directories = selections.directories.map((__, index) => index === 2); // skills
|
|
99
|
+
selections.agentsRoot = false;
|
|
100
|
+
|
|
101
|
+
const filtered = filterPlanBySelections(full, selections);
|
|
102
|
+
expect(filtered.files.map((entry) => entry.path)).toEqual(['opencode.jsonc']);
|
|
103
|
+
expect(filtered.directories.map((entry) => entry.path)).toEqual(['skills']);
|
|
104
|
+
expect(filtered.agentsRoot).toBeNull();
|
|
105
|
+
expect(filtered.authFile).toBeNull();
|
|
106
|
+
|
|
107
|
+
const inventoryPlan = planOpenCodeConfigSyncFromInventory({
|
|
108
|
+
files: [{ path: 'opencode.jsonc', bytes: 3 }],
|
|
109
|
+
directories: [{ path: 'skills', fileCount: 1, bytes: 2 }],
|
|
110
|
+
agentsRoot: null,
|
|
111
|
+
authFile: { bytes: 9 },
|
|
112
|
+
}, {
|
|
113
|
+
direction: SYNC_DIRECTION_PULL,
|
|
114
|
+
selections: { ...selections, authFile: true },
|
|
115
|
+
includeAuthFile: true,
|
|
116
|
+
});
|
|
117
|
+
expect(inventoryPlan.direction).toBe('pull');
|
|
118
|
+
expect(inventoryPlan.authFile).toEqual({ bytes: 9 });
|
|
119
|
+
expect(inventoryPlan.files.map((entry) => entry.path)).toEqual(['opencode.jsonc']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('local pull destination backups', () => {
|
|
124
|
+
it('creates generational local backups and preserves failed-run scenes until pruned', async () => {
|
|
125
|
+
const home = await makeTempDir();
|
|
126
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
127
|
+
await fsp.mkdir(configDir, { recursive: true });
|
|
128
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{}\n');
|
|
129
|
+
const plan = {
|
|
130
|
+
files: [{ path: 'opencode.jsonc', bytes: 3 }],
|
|
131
|
+
directories: [],
|
|
132
|
+
deletes: ['config.json', 'opencode.json'],
|
|
133
|
+
agentsRoot: null,
|
|
134
|
+
authFile: null,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await prepareLocalSyncDestination(home, plan, { syncRunId: 'pull-01', generations: 2 });
|
|
138
|
+
expect(fs.existsSync(path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR, 'pull-01', 'opencode.jsonc'))).toBe(true);
|
|
139
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{v2}\n');
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
141
|
+
await prepareLocalSyncDestination(home, plan, { syncRunId: 'pull-02', generations: 2 });
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
143
|
+
await prepareLocalSyncDestination(home, plan, { syncRunId: 'pull-03', generations: 2 });
|
|
144
|
+
|
|
145
|
+
const remaining = fs.readdirSync(path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR)).sort();
|
|
146
|
+
expect(remaining).toHaveLength(2);
|
|
147
|
+
expect(remaining).not.toContain('pull-01');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('generational prepare backups', () => {
|
|
152
|
+
it('keeps N generations and preserves older failed-run dirs until pruned', async () => {
|
|
153
|
+
const home = await makeTempDir();
|
|
154
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
155
|
+
await fsp.mkdir(configDir, { recursive: true });
|
|
156
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{}\n');
|
|
157
|
+
|
|
158
|
+
const plan = planOpenCodeConfigSync(home, { direction: SYNC_DIRECTION_PUSH });
|
|
159
|
+
const generations = OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS;
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < generations + 2; index += 1) {
|
|
162
|
+
// Stagger mtimes so ls -1t ordering is stable across platforms.
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
164
|
+
const syncRunId = `run-${String(index).padStart(2, '0')}`;
|
|
165
|
+
const script = buildRemoteSyncPrepareScript(plan, { syncRunId, generations });
|
|
166
|
+
execFileSync('sh', ['-c', script], { env: { ...process.env, HOME: home } });
|
|
167
|
+
// Simulate a failed run leaving its backup scene in place (no wipe of root).
|
|
168
|
+
const runBackup = path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR, syncRunId);
|
|
169
|
+
expect(fs.existsSync(runBackup)).toBe(true);
|
|
170
|
+
expect(fs.existsSync(path.join(runBackup, 'opencode.jsonc'))).toBe(true);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const backupRoot = path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR);
|
|
174
|
+
const remaining = fs.readdirSync(backupRoot).sort();
|
|
175
|
+
expect(remaining).toHaveLength(generations);
|
|
176
|
+
expect(remaining).toEqual(
|
|
177
|
+
Array.from({ length: generations }, (_, index) => `run-${String(index + 2).padStart(2, '0')}`),
|
|
178
|
+
);
|
|
179
|
+
// Earliest runs pruned; a mid-generation failed scene that remained until prune is gone only after exceeding N.
|
|
180
|
+
expect(remaining).not.toContain('run-00');
|
|
181
|
+
expect(remaining).not.toContain('run-01');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('finalize confirms the run backup directory', async () => {
|
|
185
|
+
const home = await makeTempDir();
|
|
186
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
187
|
+
await fsp.mkdir(path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR, 'fin-1'), { recursive: true });
|
|
188
|
+
const ok = execFileSync('sh', ['-c', buildRemoteSyncFinalizeScript({ syncRunId: 'fin-1' })], {
|
|
189
|
+
env: { ...process.env, HOME: home },
|
|
190
|
+
});
|
|
191
|
+
expect(ok.toString()).toContain('SYNC_DONE');
|
|
192
|
+
|
|
193
|
+
expect(() => execFileSync('sh', ['-c', buildRemoteSyncFinalizeScript({ syncRunId: 'missing' })], {
|
|
194
|
+
env: { ...process.env, HOME: home },
|
|
195
|
+
})).toThrow();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Allowlist for mirroring `~/.config/opencode` (non-session) across sync targets. */
|
|
2
|
+
export const OPENCODE_CONFIG_SYNC_ALLOWLIST = {
|
|
3
|
+
// Mutually exclusive groups: first existing local file wins; other members are deleted on remote.
|
|
4
|
+
fileGroups: [
|
|
5
|
+
['config.json', 'opencode.json', 'opencode.jsonc'],
|
|
6
|
+
['oh-my-opencode-slim.json', 'oh-my-opencode-slim.jsonc'],
|
|
7
|
+
['oh-my-openagent.json', 'oh-my-openagent.jsonc'],
|
|
8
|
+
],
|
|
9
|
+
singleFiles: ['AGENTS.md', 'cursor-models.json'],
|
|
10
|
+
directories: [
|
|
11
|
+
{ path: 'agents', legacy: 'agent' },
|
|
12
|
+
{ path: 'commands', legacy: 'command' },
|
|
13
|
+
{ path: 'skills', legacy: 'skill' },
|
|
14
|
+
{ path: 'snippet' },
|
|
15
|
+
{ path: 'snippets' },
|
|
16
|
+
{ path: 'plugins', excludeNames: ['node_modules'] },
|
|
17
|
+
{ path: '.oh-my-opencode-slim' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const OPENCODE_CONFIG_SYNC_BACKUP_DIR = '.openchamber.sync-backup';
|
|
22
|
+
/** Backup root for `$HOME/.agents` (outside `~/.agents` so rm -rf does not eat it). */
|
|
23
|
+
export const OPENCODE_AGENTS_SYNC_BACKUP_DIR = '.openchamber.sync-backup-agents';
|
|
24
|
+
/** Backup root for provider `auth.json` (outside `~/.local/share`). */
|
|
25
|
+
export const OPENCODE_AUTH_SYNC_BACKUP_DIR = '.openchamber.sync-backup-auth';
|
|
26
|
+
|
|
27
|
+
export const OPENCODE_AGENTS_ROOT_PROBE_MARKER = '__AGENTS_ROOT__';
|
|
28
|
+
export const OPENCODE_AUTH_FILE_PROBE_MARKER = '__AUTH_FILE__';
|
|
29
|
+
|
|
30
|
+
export const OPENCODE_CONFIG_SYNC_MAX_BYTES = 512 * 1024 * 1024;
|
|
31
|
+
export const OPENCODE_CONFIG_SYNC_MAX_FILES = 20000;
|
|
32
|
+
|
|
33
|
+
/** Generational backup retention per backup root (newest kept). */
|
|
34
|
+
export const OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS = 5;
|
|
35
|
+
|
|
36
|
+
/** Plan direction. Ticket 02 implements push only; pull reuses the same contract. */
|
|
37
|
+
export const SYNC_DIRECTION_PUSH = 'push';
|
|
38
|
+
export const SYNC_DIRECTION_PULL = 'pull';
|
|
39
|
+
export const SYNC_DIRECTIONS = Object.freeze([SYNC_DIRECTION_PUSH, SYNC_DIRECTION_PULL]);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
syncTargetIdForDirectHost,
|
|
3
|
+
syncTargetIdForRelayServer,
|
|
4
|
+
syncTargetIdForSshInstance,
|
|
5
|
+
} from './target-id.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* posixShell: boolean,
|
|
10
|
+
* tarExtract: boolean,
|
|
11
|
+
* authFileWrite: boolean,
|
|
12
|
+
* ocHttp?: boolean,
|
|
13
|
+
* }} SyncTargetCapabilities
|
|
14
|
+
*
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* id: string,
|
|
17
|
+
* kind: string,
|
|
18
|
+
* capabilities: SyncTargetCapabilities,
|
|
19
|
+
* }} SyncTarget
|
|
20
|
+
*
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* remoteExisting: string[],
|
|
23
|
+
* remoteAgentsRootExists: boolean,
|
|
24
|
+
* remoteAuthFileExists: boolean,
|
|
25
|
+
* }} SyncProbeResult
|
|
26
|
+
*
|
|
27
|
+
* Payload for putTar. Callers always hand a fully buffered archive
|
|
28
|
+
* (SSH stdin and direct/relay HTTP bodies alike).
|
|
29
|
+
* @typedef {Buffer | Uint8Array} SyncTarPayload
|
|
30
|
+
*
|
|
31
|
+
* @typedef {{
|
|
32
|
+
* probe: (plan: object) => Promise<SyncProbeResult>,
|
|
33
|
+
* prepare: (plan: object, ctx: { syncRunId: string }) => Promise<void>,
|
|
34
|
+
* putTar: (args: { kind: 'config' | 'agents' | 'auth', payload: SyncTarPayload }) => Promise<void>,
|
|
35
|
+
* finalize: (plan: object, ctx: { syncRunId: string }) => Promise<{ ok: true }>,
|
|
36
|
+
* }} TargetExecutor
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const EMPTY_SYNC_CAPABILITIES = Object.freeze({
|
|
40
|
+
posixShell: false,
|
|
41
|
+
tarExtract: false,
|
|
42
|
+
authFileWrite: false,
|
|
43
|
+
ocHttp: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const MANAGED_SSH_SYNC_CAPABILITIES = Object.freeze({
|
|
47
|
+
posixShell: true,
|
|
48
|
+
tarExtract: true,
|
|
49
|
+
authFileWrite: true,
|
|
50
|
+
ocHttp: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const DIRECT_HOST_SYNC_CAPABILITIES = Object.freeze({
|
|
54
|
+
posixShell: false,
|
|
55
|
+
tarExtract: false,
|
|
56
|
+
authFileWrite: true,
|
|
57
|
+
ocHttp: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const RELAY_HOST_SYNC_CAPABILITIES = Object.freeze({
|
|
61
|
+
posixShell: false,
|
|
62
|
+
tarExtract: false,
|
|
63
|
+
authFileWrite: true,
|
|
64
|
+
ocHttp: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a namespaced SyncTarget for an SSH instance.
|
|
69
|
+
* Managed remotes expose posixShell/tarExtract/authFileWrite; others expose none
|
|
70
|
+
* (semantic equivalent of the former mode==='managed' hard gate).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} instanceId
|
|
73
|
+
* @param {{ remoteOpenchamber?: { mode?: string } } | null | undefined} [instance]
|
|
74
|
+
* @returns {SyncTarget}
|
|
75
|
+
*/
|
|
76
|
+
export const createSshSyncTarget = (instanceId, instance) => {
|
|
77
|
+
const managed = instance?.remoteOpenchamber?.mode === 'managed';
|
|
78
|
+
return {
|
|
79
|
+
id: syncTargetIdForSshInstance(instanceId),
|
|
80
|
+
kind: 'ssh',
|
|
81
|
+
capabilities: managed ? { ...MANAGED_SSH_SYNC_CAPABILITIES } : { ...EMPTY_SYNC_CAPABILITIES },
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Direct OpenChamber host reached over HTTP (apiUrl + clientToken).
|
|
87
|
+
* @param {string} hostId
|
|
88
|
+
* @returns {SyncTarget}
|
|
89
|
+
*/
|
|
90
|
+
export const createDirectHostSyncTarget = (hostId) => ({
|
|
91
|
+
id: syncTargetIdForDirectHost(hostId),
|
|
92
|
+
kind: 'direct',
|
|
93
|
+
capabilities: { ...DIRECT_HOST_SYNC_CAPABILITIES },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Relay OpenChamber host pinned by serverId (signing-key fingerprint).
|
|
98
|
+
* @param {string} serverId
|
|
99
|
+
* @returns {SyncTarget}
|
|
100
|
+
*/
|
|
101
|
+
export const createRelayHostSyncTarget = (serverId) => ({
|
|
102
|
+
id: syncTargetIdForRelayServer(serverId),
|
|
103
|
+
kind: 'relay',
|
|
104
|
+
capabilities: { ...RELAY_HOST_SYNC_CAPABILITIES },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {SyncTarget | null | undefined} target
|
|
109
|
+
* @param {keyof SyncTargetCapabilities} capability
|
|
110
|
+
*/
|
|
111
|
+
export const assertTargetCapability = (target, capability) => {
|
|
112
|
+
if (!target || typeof target !== 'object') {
|
|
113
|
+
throw new Error('Sync target is required');
|
|
114
|
+
}
|
|
115
|
+
if (!target.capabilities?.[capability]) {
|
|
116
|
+
throw new Error(`OpenCode config sync requires target capability: ${capability}`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate that a plan is direction-aware and uses a known direction value.
|
|
122
|
+
* @param {object} plan
|
|
123
|
+
*/
|
|
124
|
+
export const assertPlanDirection = (plan) => {
|
|
125
|
+
const direction = plan?.direction;
|
|
126
|
+
if (direction !== 'push' && direction !== 'pull') {
|
|
127
|
+
throw new Error(`Sync plan requires direction 'push' or 'pull', got ${String(direction)}`);
|
|
128
|
+
}
|
|
129
|
+
return direction;
|
|
130
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential sync authorization is separate from target reachability.
|
|
3
|
+
* Access to an SSH/managed remote does NOT imply permission to transfer
|
|
4
|
+
* provider tokens (auth.json). Grants are per SyncTarget id and must be
|
|
5
|
+
* established through a trust channel (instance settings / pairing).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CREDENTIAL_SYNC_UNAUTHORIZED_CODE = 'credential_sync_unauthorized';
|
|
9
|
+
export const CREDENTIAL_SYNC_GRANT_CHANNEL_INSTANCE_SETTINGS = 'instance-settings';
|
|
10
|
+
|
|
11
|
+
export class CredentialSyncUnauthorizedError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} targetId
|
|
14
|
+
*/
|
|
15
|
+
constructor(targetId) {
|
|
16
|
+
super(`Credential sync is not authorized for ${targetId}`);
|
|
17
|
+
this.name = 'CredentialSyncUnauthorizedError';
|
|
18
|
+
this.code = CREDENTIAL_SYNC_UNAUTHORIZED_CODE;
|
|
19
|
+
this.targetId = targetId;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object | null | undefined} plan
|
|
25
|
+
*/
|
|
26
|
+
export const planIncludesCredentials = (plan) => Boolean(plan?.authFile);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enforce authorization when a plan carries credentials.
|
|
30
|
+
* Call from preview and apply before probe/prepare/putTar.
|
|
31
|
+
*
|
|
32
|
+
* @param {object | null | undefined} plan
|
|
33
|
+
* @param {{ targetId: string, authorized: boolean }} ctx
|
|
34
|
+
*/
|
|
35
|
+
export const assertCredentialSyncAuthorized = (plan, ctx) => {
|
|
36
|
+
if (!planIncludesCredentials(plan)) return;
|
|
37
|
+
if (ctx?.authorized === true) return;
|
|
38
|
+
throw new CredentialSyncUnauthorizedError(String(ctx?.targetId || plan?.targetId || 'unknown'));
|
|
39
|
+
};
|