@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { assertPlanDirection } from './contract.js';
|
|
4
|
+
import { assertCredentialSyncAuthorized } from './credential-auth.js';
|
|
5
|
+
import { collectLocalTarBuffer } from './tar.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run a source-side plan against a TargetExecutor (probe → prepare → putTar* → finalize).
|
|
9
|
+
* Direction-agnostic orchestration; ticket 02 only drives push from a local home snapshot.
|
|
10
|
+
* Credential-bearing plans require an explicit per-target grant (ticket 03).
|
|
11
|
+
*
|
|
12
|
+
* @param {{
|
|
13
|
+
* plan: object,
|
|
14
|
+
* executor: import('./contract.js').TargetExecutor,
|
|
15
|
+
* syncRunId: string,
|
|
16
|
+
* sourceHomedir: string,
|
|
17
|
+
* collectTar?: typeof collectLocalTarBuffer,
|
|
18
|
+
* credentialSyncAuthorized?: boolean,
|
|
19
|
+
* }} args
|
|
20
|
+
*/
|
|
21
|
+
export const applyConfigSyncPlan = async ({
|
|
22
|
+
plan,
|
|
23
|
+
executor,
|
|
24
|
+
syncRunId,
|
|
25
|
+
sourceHomedir,
|
|
26
|
+
collectTar = collectLocalTarBuffer,
|
|
27
|
+
credentialSyncAuthorized = false,
|
|
28
|
+
}) => {
|
|
29
|
+
assertPlanDirection(plan);
|
|
30
|
+
assertCredentialSyncAuthorized(plan, {
|
|
31
|
+
targetId: plan?.targetId || 'unknown',
|
|
32
|
+
authorized: credentialSyncAuthorized === true,
|
|
33
|
+
});
|
|
34
|
+
const home = String(sourceHomedir || '');
|
|
35
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
36
|
+
const tarEntries = [
|
|
37
|
+
...plan.files.map((entry) => entry.path),
|
|
38
|
+
...plan.directories.map((entry) => entry.path),
|
|
39
|
+
];
|
|
40
|
+
const hasConfigPayload = tarEntries.length > 0;
|
|
41
|
+
const hasAgentsPayload = Boolean(plan.agentsRoot);
|
|
42
|
+
const hasAuthPayload = Boolean(plan.authFile);
|
|
43
|
+
|
|
44
|
+
if (!hasConfigPayload && !hasAgentsPayload && !hasAuthPayload) {
|
|
45
|
+
// Nothing to upload — preserve the historical short-circuit (no remote prepare).
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
files: plan.files.length,
|
|
49
|
+
directories: plan.directories.length,
|
|
50
|
+
deletes: plan.deletes.length,
|
|
51
|
+
totalBytes: plan.totalBytes,
|
|
52
|
+
agentsRoot: null,
|
|
53
|
+
authFile: null,
|
|
54
|
+
plan,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const windowsHide = process.platform === 'win32';
|
|
59
|
+
const configTarBuffer = hasConfigPayload
|
|
60
|
+
? await collectTar(['-h', '-czf', '-', '-C', configDir, ...tarEntries], { windowsHide })
|
|
61
|
+
: null;
|
|
62
|
+
const agentsTarBuffer = hasAgentsPayload
|
|
63
|
+
? await collectTar(['-h', '-czf', '-', '-C', home, '.agents'], { windowsHide })
|
|
64
|
+
: null;
|
|
65
|
+
const authShareDir = path.join(home, '.local', 'share', 'opencode');
|
|
66
|
+
const authTarBuffer = hasAuthPayload
|
|
67
|
+
? await collectTar(['-h', '-czf', '-', '-C', authShareDir, 'auth.json'], { windowsHide })
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
await executor.prepare(plan, { syncRunId });
|
|
71
|
+
|
|
72
|
+
if (configTarBuffer) {
|
|
73
|
+
await executor.putTar({ kind: 'config', payload: configTarBuffer });
|
|
74
|
+
}
|
|
75
|
+
if (agentsTarBuffer) {
|
|
76
|
+
await executor.putTar({ kind: 'agents', payload: agentsTarBuffer });
|
|
77
|
+
}
|
|
78
|
+
if (authTarBuffer) {
|
|
79
|
+
// Only auth.json — never the rest of ~/.local/share/opencode (session DBs live there).
|
|
80
|
+
await executor.putTar({ kind: 'auth', payload: authTarBuffer });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await executor.finalize(plan, { syncRunId });
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
files: plan.files.length,
|
|
88
|
+
directories: plan.directories.length,
|
|
89
|
+
deletes: plan.deletes.length,
|
|
90
|
+
totalBytes: plan.totalBytes,
|
|
91
|
+
agentsRoot: plan.agentsRoot ? { fileCount: plan.agentsRoot.fileCount } : null,
|
|
92
|
+
authFile: plan.authFile ? { bytes: plan.authFile.bytes } : null,
|
|
93
|
+
plan,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Preview helper: compute probe path list from a plan.
|
|
99
|
+
* @param {object} plan
|
|
100
|
+
*/
|
|
101
|
+
export const probePathsForPlan = (plan) => {
|
|
102
|
+
const probePaths = [
|
|
103
|
+
...(Array.isArray(plan?.files) ? plan.files.map((entry) => entry.path) : []),
|
|
104
|
+
...(Array.isArray(plan?.directories) ? plan.directories.map((entry) => entry.path) : []),
|
|
105
|
+
...(Array.isArray(plan?.deletes) ? plan.deletes : []),
|
|
106
|
+
];
|
|
107
|
+
return [...new Set(probePaths)];
|
|
108
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export {
|
|
2
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST,
|
|
3
|
+
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
4
|
+
OPENCODE_AGENTS_SYNC_BACKUP_DIR,
|
|
5
|
+
OPENCODE_AUTH_SYNC_BACKUP_DIR,
|
|
6
|
+
OPENCODE_AGENTS_ROOT_PROBE_MARKER,
|
|
7
|
+
OPENCODE_AUTH_FILE_PROBE_MARKER,
|
|
8
|
+
OPENCODE_CONFIG_SYNC_MAX_BYTES,
|
|
9
|
+
OPENCODE_CONFIG_SYNC_MAX_FILES,
|
|
10
|
+
OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
|
|
11
|
+
SYNC_DIRECTION_PUSH,
|
|
12
|
+
SYNC_DIRECTION_PULL,
|
|
13
|
+
SYNC_DIRECTIONS,
|
|
14
|
+
} from './constants.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
EMPTY_SYNC_CAPABILITIES,
|
|
18
|
+
MANAGED_SSH_SYNC_CAPABILITIES,
|
|
19
|
+
DIRECT_HOST_SYNC_CAPABILITIES,
|
|
20
|
+
RELAY_HOST_SYNC_CAPABILITIES,
|
|
21
|
+
createSshSyncTarget,
|
|
22
|
+
createDirectHostSyncTarget,
|
|
23
|
+
createRelayHostSyncTarget,
|
|
24
|
+
assertTargetCapability,
|
|
25
|
+
assertPlanDirection,
|
|
26
|
+
} from './contract.js';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
syncTargetIdForSshInstance,
|
|
30
|
+
syncTargetIdForDirectHost,
|
|
31
|
+
syncTargetIdForRelayServer,
|
|
32
|
+
} from './target-id.js';
|
|
33
|
+
export {
|
|
34
|
+
planOpenCodeConfigSync,
|
|
35
|
+
planOpenCodeConfigSyncFromInventory,
|
|
36
|
+
walkAllowlistDirectory,
|
|
37
|
+
} from './plan.js';
|
|
38
|
+
export {
|
|
39
|
+
buildRemoteSyncPrepareScript,
|
|
40
|
+
buildRemoteSyncProbeScript,
|
|
41
|
+
buildRemoteSyncFinalizeScript,
|
|
42
|
+
buildRemoteSyncInventoryScript,
|
|
43
|
+
buildRemoteConfigTarScript,
|
|
44
|
+
buildRemoteAgentsTarScript,
|
|
45
|
+
buildRemoteAuthTarScript,
|
|
46
|
+
sanitizeSyncRunIdForPath,
|
|
47
|
+
shellQuote,
|
|
48
|
+
} from './scripts.js';
|
|
49
|
+
export { collectLocalTarBuffer } from './tar.js';
|
|
50
|
+
export { applyConfigSyncPlan, probePathsForPlan } from './engine.js';
|
|
51
|
+
export {
|
|
52
|
+
CREDENTIAL_SYNC_UNAUTHORIZED_CODE,
|
|
53
|
+
CREDENTIAL_SYNC_GRANT_CHANNEL_INSTANCE_SETTINGS,
|
|
54
|
+
CredentialSyncUnauthorizedError,
|
|
55
|
+
planIncludesCredentials,
|
|
56
|
+
assertCredentialSyncAuthorized,
|
|
57
|
+
} from './credential-auth.js';
|
|
58
|
+
export {
|
|
59
|
+
buildDefaultSyncSelections,
|
|
60
|
+
normalizeSyncSelections,
|
|
61
|
+
filterPlanBySelections,
|
|
62
|
+
} from './selections.js';
|
|
63
|
+
export {
|
|
64
|
+
prepareLocalSyncDestination,
|
|
65
|
+
finalizeLocalSyncDestination,
|
|
66
|
+
extractTarGzBuffer,
|
|
67
|
+
} from './local-backup.js';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
OPENCODE_AGENTS_SYNC_BACKUP_DIR,
|
|
8
|
+
OPENCODE_AUTH_SYNC_BACKUP_DIR,
|
|
9
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST,
|
|
10
|
+
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
11
|
+
OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
|
|
12
|
+
} from './constants.js';
|
|
13
|
+
import { sanitizeSyncRunIdForPath } from './scripts.js';
|
|
14
|
+
|
|
15
|
+
const copyPathIfExists = async (from, to) => {
|
|
16
|
+
try {
|
|
17
|
+
await fsp.access(from);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
await fsp.mkdir(path.dirname(to), { recursive: true });
|
|
22
|
+
await fsp.cp(from, to, { recursive: true, force: true });
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const pruneGenerations = async (backupRoot, syncRunId, generations) => {
|
|
27
|
+
await fsp.mkdir(backupRoot, { recursive: true });
|
|
28
|
+
let entries = [];
|
|
29
|
+
try {
|
|
30
|
+
entries = await fsp.readdir(backupRoot, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const dirs = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
const abs = path.join(backupRoot, entry.name);
|
|
38
|
+
let mtimeMs = 0;
|
|
39
|
+
try {
|
|
40
|
+
mtimeMs = (await fsp.stat(abs)).mtimeMs;
|
|
41
|
+
} catch {
|
|
42
|
+
mtimeMs = 0;
|
|
43
|
+
}
|
|
44
|
+
dirs.push({ name: entry.name, abs, mtimeMs });
|
|
45
|
+
}
|
|
46
|
+
dirs.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
47
|
+
for (const [index, entry] of dirs.entries()) {
|
|
48
|
+
if (index < generations) continue;
|
|
49
|
+
if (entry.name === syncRunId) continue;
|
|
50
|
+
await fsp.rm(entry.abs, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Local (pull destination) generational backup + delete stale counterparts.
|
|
56
|
+
* Mirrors remote prepare semantics under the local home.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} homedir
|
|
59
|
+
* @param {object} plan
|
|
60
|
+
* @param {{ syncRunId: string, generations?: number }} options
|
|
61
|
+
*/
|
|
62
|
+
export const prepareLocalSyncDestination = async (homedir, plan, options) => {
|
|
63
|
+
const home = String(homedir || '');
|
|
64
|
+
const syncRunId = sanitizeSyncRunIdForPath(options?.syncRunId);
|
|
65
|
+
const generations = Number.isFinite(options?.generations) && Number(options.generations) > 0
|
|
66
|
+
? Math.trunc(Number(options.generations))
|
|
67
|
+
: OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS;
|
|
68
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
69
|
+
const backupRoot = path.join(configDir, OPENCODE_CONFIG_SYNC_BACKUP_DIR);
|
|
70
|
+
const runBackup = path.join(backupRoot, syncRunId);
|
|
71
|
+
|
|
72
|
+
await fsp.mkdir(configDir, { recursive: true });
|
|
73
|
+
await fsp.rm(runBackup, { recursive: true, force: true });
|
|
74
|
+
await fsp.mkdir(runBackup, { recursive: true });
|
|
75
|
+
await pruneGenerations(backupRoot, syncRunId, generations);
|
|
76
|
+
|
|
77
|
+
const backupPaths = [
|
|
78
|
+
...(Array.isArray(plan?.files) ? plan.files.map((entry) => entry.path) : []),
|
|
79
|
+
...(Array.isArray(plan?.directories) ? plan.directories.map((entry) => entry.path) : []),
|
|
80
|
+
];
|
|
81
|
+
for (const rel of backupPaths) {
|
|
82
|
+
await copyPathIfExists(path.join(configDir, rel), path.join(runBackup, rel));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const directoryDeleteNames = new Set();
|
|
86
|
+
for (const dirSpec of OPENCODE_CONFIG_SYNC_ALLOWLIST.directories) {
|
|
87
|
+
directoryDeleteNames.add(dirSpec.path);
|
|
88
|
+
if (typeof dirSpec.legacy === 'string' && dirSpec.legacy) {
|
|
89
|
+
directoryDeleteNames.add(dirSpec.legacy);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const rel of Array.isArray(plan?.deletes) ? plan.deletes : []) {
|
|
93
|
+
const abs = path.join(configDir, rel);
|
|
94
|
+
if (directoryDeleteNames.has(rel)) {
|
|
95
|
+
await fsp.rm(abs, { recursive: true, force: true });
|
|
96
|
+
} else {
|
|
97
|
+
await fsp.rm(abs, { force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (plan?.agentsRoot) {
|
|
102
|
+
const agentsBackupRoot = path.join(home, OPENCODE_AGENTS_SYNC_BACKUP_DIR);
|
|
103
|
+
const agentsRun = path.join(agentsBackupRoot, syncRunId);
|
|
104
|
+
await fsp.rm(agentsRun, { recursive: true, force: true });
|
|
105
|
+
await fsp.mkdir(agentsRun, { recursive: true });
|
|
106
|
+
await pruneGenerations(agentsBackupRoot, syncRunId, generations);
|
|
107
|
+
await copyPathIfExists(path.join(home, '.agents'), path.join(agentsRun, 'agents'));
|
|
108
|
+
await fsp.rm(path.join(home, '.agents'), { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
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
|
+
return { runBackup };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Confirm local pull backup generation exists.
|
|
128
|
+
* @param {string} homedir
|
|
129
|
+
* @param {{ syncRunId: string }} options
|
|
130
|
+
*/
|
|
131
|
+
export const finalizeLocalSyncDestination = async (homedir, options) => {
|
|
132
|
+
const syncRunId = sanitizeSyncRunIdForPath(options?.syncRunId);
|
|
133
|
+
const runBackup = path.join(
|
|
134
|
+
String(homedir || ''),
|
|
135
|
+
'.config',
|
|
136
|
+
'opencode',
|
|
137
|
+
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
138
|
+
syncRunId,
|
|
139
|
+
);
|
|
140
|
+
const st = await fsp.stat(runBackup);
|
|
141
|
+
if (!st.isDirectory()) {
|
|
142
|
+
throw new Error('Local sync finalize missing backup generation');
|
|
143
|
+
}
|
|
144
|
+
return { ok: true };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract a tar.gz buffer into a destination directory.
|
|
149
|
+
* @param {Buffer} buffer
|
|
150
|
+
* @param {string} destinationDir
|
|
151
|
+
*/
|
|
152
|
+
export const extractTarGzBuffer = (buffer, destinationDir) => new Promise((resolve, reject) => {
|
|
153
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
154
|
+
const child = spawn('tar', ['-xzf', '-', '-C', destinationDir], {
|
|
155
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
156
|
+
...(process.platform === 'win32' ? { windowsHide: true } : {}),
|
|
157
|
+
});
|
|
158
|
+
let stderr = '';
|
|
159
|
+
child.stderr?.on('data', (chunk) => {
|
|
160
|
+
stderr += chunk.toString();
|
|
161
|
+
});
|
|
162
|
+
child.on('error', reject);
|
|
163
|
+
child.on('close', (code) => {
|
|
164
|
+
if (code !== 0) {
|
|
165
|
+
reject(new Error((stderr || 'Local tar extract failed').trim()));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
child.stdin?.end(buffer);
|
|
171
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST,
|
|
6
|
+
OPENCODE_CONFIG_SYNC_MAX_BYTES,
|
|
7
|
+
OPENCODE_CONFIG_SYNC_MAX_FILES,
|
|
8
|
+
SYNC_DIRECTION_PUSH,
|
|
9
|
+
SYNC_DIRECTIONS,
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
import { filterPlanBySelections, normalizeSyncSelections } from './selections.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Walk an allowlist directory with symlink dereference (fs.stat), cycle-guard, excludeNames, and `.backup` skip.
|
|
15
|
+
* @param {string} absPath
|
|
16
|
+
* @param {Set<string>} excludeNames
|
|
17
|
+
* @param {Set<string>} seenRealpaths
|
|
18
|
+
* @returns {{ fileCount: number, bytes: number }}
|
|
19
|
+
*/
|
|
20
|
+
export const walkAllowlistDirectory = (absPath, excludeNames, seenRealpaths) => {
|
|
21
|
+
let real;
|
|
22
|
+
try {
|
|
23
|
+
real = fs.realpathSync(absPath);
|
|
24
|
+
} catch {
|
|
25
|
+
return { fileCount: 0, bytes: 0 };
|
|
26
|
+
}
|
|
27
|
+
if (seenRealpaths.has(real)) return { fileCount: 0, bytes: 0 };
|
|
28
|
+
seenRealpaths.add(real);
|
|
29
|
+
|
|
30
|
+
let fileCount = 0;
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
let names;
|
|
33
|
+
try {
|
|
34
|
+
names = fs.readdirSync(absPath);
|
|
35
|
+
} catch {
|
|
36
|
+
return { fileCount: 0, bytes: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (excludeNames.has(name) || name === 'node_modules') continue;
|
|
41
|
+
if (/\.backup$/.test(name)) continue;
|
|
42
|
+
const childAbs = path.join(absPath, name);
|
|
43
|
+
let st;
|
|
44
|
+
try {
|
|
45
|
+
st = fs.statSync(childAbs);
|
|
46
|
+
} catch {
|
|
47
|
+
// Broken symlink or unreadable entry — skip.
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (st.isDirectory()) {
|
|
51
|
+
const nested = walkAllowlistDirectory(childAbs, excludeNames, seenRealpaths);
|
|
52
|
+
fileCount += nested.fileCount;
|
|
53
|
+
bytes += nested.bytes;
|
|
54
|
+
} else if (st.isFile()) {
|
|
55
|
+
fileCount += 1;
|
|
56
|
+
bytes += st.size;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { fileCount, bytes };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pure planner: source-home allowlist + optional `~/.agents` + optional provider auth.json.
|
|
64
|
+
* Plan is computed on the source side; executors run it on the target side.
|
|
65
|
+
* `direction` is a first-class field (`push` today; `pull` reuses the same shape).
|
|
66
|
+
*
|
|
67
|
+
* @param {string} sourceHomedir
|
|
68
|
+
* @param {{
|
|
69
|
+
* direction?: 'push' | 'pull',
|
|
70
|
+
* syncRunId?: string,
|
|
71
|
+
* sourceTargetId?: string,
|
|
72
|
+
* targetId?: string,
|
|
73
|
+
* includeAuthFile?: boolean,
|
|
74
|
+
* selections?: import('./selections.js').SyncSelections,
|
|
75
|
+
* }} [options]
|
|
76
|
+
* @returns {{
|
|
77
|
+
* direction: 'push' | 'pull',
|
|
78
|
+
* syncRunId?: string,
|
|
79
|
+
* sourceTargetId?: string,
|
|
80
|
+
* targetId?: string,
|
|
81
|
+
* files: { path: string, bytes: number }[],
|
|
82
|
+
* directories: { path: string, fileCount: number, bytes: number }[],
|
|
83
|
+
* agentsRoot: { fileCount: number, bytes: number } | null,
|
|
84
|
+
* authFile: { bytes: number } | null,
|
|
85
|
+
* deletes: string[],
|
|
86
|
+
* totalBytes: number,
|
|
87
|
+
* selections?: import('./selections.js').SyncSelections,
|
|
88
|
+
* }}
|
|
89
|
+
*/
|
|
90
|
+
export const planOpenCodeConfigSync = (sourceHomedir, options = {}) => {
|
|
91
|
+
const home = String(sourceHomedir || '');
|
|
92
|
+
const direction = options.direction || SYNC_DIRECTION_PUSH;
|
|
93
|
+
if (!SYNC_DIRECTIONS.includes(direction)) {
|
|
94
|
+
throw new Error(`Unsupported sync direction: ${String(direction)}`);
|
|
95
|
+
}
|
|
96
|
+
// Credential transfer requires an explicit per-target grant (ticket 03).
|
|
97
|
+
// Callers that have not authorized the target pass includeAuthFile: false so
|
|
98
|
+
// auth.json is skipped rather than failing the whole sync.
|
|
99
|
+
const selections = options.selections
|
|
100
|
+
? normalizeSyncSelections(options.selections, { includeAuthFile: options.selections.authFile === true })
|
|
101
|
+
: null;
|
|
102
|
+
const includeAuthFile = selections
|
|
103
|
+
? selections.authFile === true
|
|
104
|
+
: options.includeAuthFile !== false;
|
|
105
|
+
|
|
106
|
+
const root = path.join(home, '.config', 'opencode');
|
|
107
|
+
/** @type {{ path: string, bytes: number }[]} */
|
|
108
|
+
const files = [];
|
|
109
|
+
/** @type {{ path: string, fileCount: number, bytes: number }[]} */
|
|
110
|
+
const directories = [];
|
|
111
|
+
/** @type {string[]} */
|
|
112
|
+
const deletes = [];
|
|
113
|
+
let totalBytes = 0;
|
|
114
|
+
let totalFileCount = 0;
|
|
115
|
+
|
|
116
|
+
for (const group of OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups) {
|
|
117
|
+
let winner = null;
|
|
118
|
+
for (const member of group) {
|
|
119
|
+
const abs = path.join(root, member);
|
|
120
|
+
let st;
|
|
121
|
+
try {
|
|
122
|
+
st = fs.statSync(abs);
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!st.isFile()) continue;
|
|
127
|
+
winner = { path: member, bytes: st.size };
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (!winner) continue;
|
|
131
|
+
files.push(winner);
|
|
132
|
+
totalBytes += winner.bytes;
|
|
133
|
+
totalFileCount += 1;
|
|
134
|
+
for (const member of group) {
|
|
135
|
+
if (member !== winner.path) deletes.push(member);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const name of OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles) {
|
|
140
|
+
const abs = path.join(root, name);
|
|
141
|
+
let st;
|
|
142
|
+
try {
|
|
143
|
+
st = fs.statSync(abs);
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!st.isFile()) continue;
|
|
148
|
+
files.push({ path: name, bytes: st.size });
|
|
149
|
+
totalBytes += st.size;
|
|
150
|
+
totalFileCount += 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const dirSpec of OPENCODE_CONFIG_SYNC_ALLOWLIST.directories) {
|
|
154
|
+
const abs = path.join(root, dirSpec.path);
|
|
155
|
+
let st;
|
|
156
|
+
try {
|
|
157
|
+
st = fs.statSync(abs);
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!st.isDirectory()) continue;
|
|
162
|
+
const excludeNames = new Set(Array.isArray(dirSpec.excludeNames) ? dirSpec.excludeNames : []);
|
|
163
|
+
const walked = walkAllowlistDirectory(abs, excludeNames, new Set());
|
|
164
|
+
directories.push({
|
|
165
|
+
path: dirSpec.path,
|
|
166
|
+
fileCount: walked.fileCount,
|
|
167
|
+
bytes: walked.bytes,
|
|
168
|
+
});
|
|
169
|
+
totalBytes += walked.bytes;
|
|
170
|
+
totalFileCount += walked.fileCount;
|
|
171
|
+
deletes.push(dirSpec.path);
|
|
172
|
+
if (typeof dirSpec.legacy === 'string' && dirSpec.legacy) {
|
|
173
|
+
deletes.push(dirSpec.legacy);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @type {{ fileCount: number, bytes: number } | null} */
|
|
178
|
+
let agentsRoot = null;
|
|
179
|
+
const agentsAbs = path.join(home, '.agents');
|
|
180
|
+
let agentsStat;
|
|
181
|
+
try {
|
|
182
|
+
agentsStat = fs.statSync(agentsAbs);
|
|
183
|
+
} catch {
|
|
184
|
+
agentsStat = null;
|
|
185
|
+
}
|
|
186
|
+
if (agentsStat?.isDirectory()) {
|
|
187
|
+
const walked = walkAllowlistDirectory(agentsAbs, new Set(), new Set());
|
|
188
|
+
agentsRoot = { fileCount: walked.fileCount, bytes: walked.bytes };
|
|
189
|
+
totalBytes += walked.bytes;
|
|
190
|
+
totalFileCount += walked.fileCount;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @type {{ bytes: number } | null} */
|
|
194
|
+
let authFile = null;
|
|
195
|
+
if (includeAuthFile) {
|
|
196
|
+
const authAbs = path.join(home, '.local', 'share', 'opencode', 'auth.json');
|
|
197
|
+
let authStat;
|
|
198
|
+
try {
|
|
199
|
+
// fs.stat follows symlinks (auth.json is sometimes a symlink).
|
|
200
|
+
authStat = fs.statSync(authAbs);
|
|
201
|
+
} catch {
|
|
202
|
+
authStat = null;
|
|
203
|
+
}
|
|
204
|
+
if (authStat?.isFile()) {
|
|
205
|
+
authFile = { bytes: authStat.size };
|
|
206
|
+
totalBytes += authStat.size;
|
|
207
|
+
// Single credential file — does not count against the 20000 file-count cap.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (totalBytes > OPENCODE_CONFIG_SYNC_MAX_BYTES || totalFileCount > OPENCODE_CONFIG_SYNC_MAX_FILES) {
|
|
212
|
+
throw new Error('OpenCode config sync exceeds size limit (512MB)');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const full = {
|
|
216
|
+
direction,
|
|
217
|
+
...(typeof options.syncRunId === 'string' && options.syncRunId ? { syncRunId: options.syncRunId } : {}),
|
|
218
|
+
...(typeof options.sourceTargetId === 'string' && options.sourceTargetId
|
|
219
|
+
? { sourceTargetId: options.sourceTargetId }
|
|
220
|
+
: {}),
|
|
221
|
+
...(typeof options.targetId === 'string' && options.targetId ? { targetId: options.targetId } : {}),
|
|
222
|
+
files,
|
|
223
|
+
directories,
|
|
224
|
+
agentsRoot,
|
|
225
|
+
authFile,
|
|
226
|
+
deletes,
|
|
227
|
+
totalBytes,
|
|
228
|
+
};
|
|
229
|
+
return selections ? filterPlanBySelections(full, selections) : full;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build a pull plan from a remote inventory payload (no local filesystem walk).
|
|
234
|
+
* @param {object} inventory
|
|
235
|
+
* @param {{
|
|
236
|
+
* direction?: 'pull',
|
|
237
|
+
* syncRunId?: string,
|
|
238
|
+
* sourceTargetId?: string,
|
|
239
|
+
* targetId?: string,
|
|
240
|
+
* includeAuthFile?: boolean,
|
|
241
|
+
* selections?: import('./selections.js').SyncSelections,
|
|
242
|
+
* }} [options]
|
|
243
|
+
*/
|
|
244
|
+
export const planOpenCodeConfigSyncFromInventory = (inventory, options = {}) => {
|
|
245
|
+
const direction = options.direction || 'pull';
|
|
246
|
+
if (!SYNC_DIRECTIONS.includes(direction)) {
|
|
247
|
+
throw new Error(`Unsupported sync direction: ${String(direction)}`);
|
|
248
|
+
}
|
|
249
|
+
const selections = options.selections
|
|
250
|
+
? normalizeSyncSelections(options.selections, { includeAuthFile: options.selections.authFile === true })
|
|
251
|
+
: null;
|
|
252
|
+
const includeAuthFile = selections
|
|
253
|
+
? selections.authFile === true
|
|
254
|
+
: options.includeAuthFile !== false;
|
|
255
|
+
|
|
256
|
+
const files = Array.isArray(inventory?.files) ? inventory.files.filter((entry) => entry?.path) : [];
|
|
257
|
+
const directories = Array.isArray(inventory?.directories)
|
|
258
|
+
? inventory.directories.filter((entry) => entry?.path)
|
|
259
|
+
: [];
|
|
260
|
+
const agentsRoot = inventory?.agentsRoot && typeof inventory.agentsRoot === 'object'
|
|
261
|
+
? inventory.agentsRoot
|
|
262
|
+
: null;
|
|
263
|
+
const authFile = includeAuthFile && inventory?.authFile && typeof inventory.authFile === 'object'
|
|
264
|
+
? inventory.authFile
|
|
265
|
+
: null;
|
|
266
|
+
|
|
267
|
+
/** @type {string[]} */
|
|
268
|
+
const deletes = [];
|
|
269
|
+
for (const group of OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups) {
|
|
270
|
+
const winner = files.find((entry) => group.includes(entry.path));
|
|
271
|
+
if (!winner) continue;
|
|
272
|
+
for (const member of group) {
|
|
273
|
+
if (member !== winner.path) deletes.push(member);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const dir of directories) {
|
|
277
|
+
deletes.push(dir.path);
|
|
278
|
+
const spec = OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.find((entry) => entry.path === dir.path);
|
|
279
|
+
if (typeof spec?.legacy === 'string' && spec.legacy) deletes.push(spec.legacy);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let totalBytes = 0;
|
|
283
|
+
let totalFileCount = 0;
|
|
284
|
+
for (const entry of files) {
|
|
285
|
+
totalBytes += Number(entry.bytes) || 0;
|
|
286
|
+
totalFileCount += 1;
|
|
287
|
+
}
|
|
288
|
+
for (const entry of directories) {
|
|
289
|
+
totalBytes += Number(entry.bytes) || 0;
|
|
290
|
+
totalFileCount += Number(entry.fileCount) || 0;
|
|
291
|
+
}
|
|
292
|
+
if (agentsRoot) {
|
|
293
|
+
totalBytes += Number(agentsRoot.bytes) || 0;
|
|
294
|
+
totalFileCount += Number(agentsRoot.fileCount) || 0;
|
|
295
|
+
}
|
|
296
|
+
if (authFile) {
|
|
297
|
+
totalBytes += Number(authFile.bytes) || 0;
|
|
298
|
+
}
|
|
299
|
+
if (totalBytes > OPENCODE_CONFIG_SYNC_MAX_BYTES || totalFileCount > OPENCODE_CONFIG_SYNC_MAX_FILES) {
|
|
300
|
+
throw new Error('OpenCode config sync exceeds size limit (512MB)');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const full = {
|
|
304
|
+
direction,
|
|
305
|
+
...(typeof options.syncRunId === 'string' && options.syncRunId ? { syncRunId: options.syncRunId } : {}),
|
|
306
|
+
...(typeof options.sourceTargetId === 'string' && options.sourceTargetId
|
|
307
|
+
? { sourceTargetId: options.sourceTargetId }
|
|
308
|
+
: {}),
|
|
309
|
+
...(typeof options.targetId === 'string' && options.targetId ? { targetId: options.targetId } : {}),
|
|
310
|
+
files,
|
|
311
|
+
directories,
|
|
312
|
+
agentsRoot,
|
|
313
|
+
authFile,
|
|
314
|
+
deletes,
|
|
315
|
+
totalBytes,
|
|
316
|
+
};
|
|
317
|
+
return selections ? filterPlanBySelections(full, selections) : full;
|
|
318
|
+
};
|