@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,264 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
OPENCODE_AGENTS_ROOT_PROBE_MARKER,
|
|
5
|
+
OPENCODE_AGENTS_SYNC_BACKUP_DIR,
|
|
6
|
+
OPENCODE_AUTH_FILE_PROBE_MARKER,
|
|
7
|
+
OPENCODE_AUTH_SYNC_BACKUP_DIR,
|
|
8
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST,
|
|
9
|
+
OPENCODE_CONFIG_SYNC_BACKUP_DIR,
|
|
10
|
+
OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS,
|
|
11
|
+
} from './constants.js';
|
|
12
|
+
|
|
13
|
+
const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize syncRunId for use as a single path segment under backup roots.
|
|
17
|
+
* @param {string} syncRunId
|
|
18
|
+
*/
|
|
19
|
+
export const sanitizeSyncRunIdForPath = (syncRunId) => {
|
|
20
|
+
const trimmed = String(syncRunId || '').trim();
|
|
21
|
+
if (!trimmed) throw new Error('syncRunId is required for generational backup');
|
|
22
|
+
if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
23
|
+
throw new Error('syncRunId contains unsupported characters for backup paths');
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emit POSIX lines that create `<$backupVar>/<syncRunId>` as `$runVar` and prune older generations.
|
|
30
|
+
* Does not wipe the entire backup root — only replaces the current run dir and
|
|
31
|
+
* deletes generations beyond the retention limit (failed runs keep their scene).
|
|
32
|
+
*
|
|
33
|
+
* @param {{ backupVar: string, runVar: string, quotedRunId: string, generations: number }} args
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
const generationalBackupSetupLines = ({ backupVar, runVar, quotedRunId, generations }) => [
|
|
37
|
+
`mkdir -p "$${backupVar}"`,
|
|
38
|
+
`${runVar}="$${backupVar}"/${quotedRunId}`,
|
|
39
|
+
`rm -rf -- "$${runVar}"`,
|
|
40
|
+
`mkdir -p "$${runVar}"`,
|
|
41
|
+
`(cd "$${backupVar}" && ls -1t 2>/dev/null | awk 'NR>${generations} {print}' | while IFS= read -r old; do`,
|
|
42
|
+
' [ -n "$old" ] || continue',
|
|
43
|
+
` [ "$old" = ${quotedRunId} ] && continue`,
|
|
44
|
+
` rm -rf -- "$${backupVar}/$old"`,
|
|
45
|
+
'done) || true',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the remote POSIX prepare script: generational backup, delete stale counterparts, print SYNC_READY.
|
|
50
|
+
* Backups land under `<backupRoot>/<syncRunId>/…`. Older generations beyond
|
|
51
|
+
* {@link OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS} are pruned; prepare no longer
|
|
52
|
+
* `rm -rf`s the entire backup root at start.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ files: { path: string }[], directories: { path: string }[], deletes: string[], agentsRoot?: { fileCount: number, bytes: number } | null, authFile?: { bytes: number } | null }} plan
|
|
55
|
+
* @param {{ syncRunId: string, generations?: number }} options
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export const buildRemoteSyncPrepareScript = (plan, options) => {
|
|
59
|
+
const syncRunId = sanitizeSyncRunIdForPath(options?.syncRunId);
|
|
60
|
+
const generations = Number.isFinite(options?.generations) && Number(options.generations) > 0
|
|
61
|
+
? Math.trunc(Number(options.generations))
|
|
62
|
+
: OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS;
|
|
63
|
+
const backupDir = OPENCODE_CONFIG_SYNC_BACKUP_DIR;
|
|
64
|
+
const directoryDeleteNames = new Set();
|
|
65
|
+
for (const dirSpec of OPENCODE_CONFIG_SYNC_ALLOWLIST.directories) {
|
|
66
|
+
directoryDeleteNames.add(dirSpec.path);
|
|
67
|
+
if (typeof dirSpec.legacy === 'string' && dirSpec.legacy) {
|
|
68
|
+
directoryDeleteNames.add(dirSpec.legacy);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const quotedRunId = shellQuote(syncRunId);
|
|
73
|
+
const lines = [
|
|
74
|
+
'set -e',
|
|
75
|
+
'CFG="$HOME/.config/opencode"',
|
|
76
|
+
'mkdir -p "$CFG"',
|
|
77
|
+
`BK="$CFG/${backupDir}"`,
|
|
78
|
+
...generationalBackupSetupLines({
|
|
79
|
+
backupVar: 'BK',
|
|
80
|
+
runVar: 'RUN_BK',
|
|
81
|
+
quotedRunId,
|
|
82
|
+
generations,
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const backupPaths = [
|
|
87
|
+
...(Array.isArray(plan?.files) ? plan.files.map((entry) => entry.path) : []),
|
|
88
|
+
...(Array.isArray(plan?.directories) ? plan.directories.map((entry) => entry.path) : []),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
for (const rel of backupPaths) {
|
|
92
|
+
const quoted = shellQuote(rel);
|
|
93
|
+
const dirname = path.posix.dirname(rel);
|
|
94
|
+
const quotedDir = shellQuote(dirname === '.' ? '' : dirname);
|
|
95
|
+
lines.push(`if [ -e "$CFG"/${quoted} ]; then`);
|
|
96
|
+
if (dirname === '.' || dirname === '') {
|
|
97
|
+
lines.push(' mkdir -p "$RUN_BK"');
|
|
98
|
+
} else {
|
|
99
|
+
lines.push(` mkdir -p "$RUN_BK"/${quotedDir}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(` cp -a "$CFG"/${quoted} "$RUN_BK"/${quoted}`);
|
|
102
|
+
lines.push('fi');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const rel of Array.isArray(plan?.deletes) ? plan.deletes : []) {
|
|
106
|
+
const quoted = shellQuote(rel);
|
|
107
|
+
if (directoryDeleteNames.has(rel)) {
|
|
108
|
+
lines.push(`rm -rf -- "$CFG"/${quoted}`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(`rm -f -- "$CFG"/${quoted}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (plan?.agentsRoot) {
|
|
115
|
+
lines.push(`AGENTS_BK="$HOME/${OPENCODE_AGENTS_SYNC_BACKUP_DIR}"`);
|
|
116
|
+
lines.push(...generationalBackupSetupLines({
|
|
117
|
+
backupVar: 'AGENTS_BK',
|
|
118
|
+
runVar: 'AGENTS_RUN_BK',
|
|
119
|
+
quotedRunId,
|
|
120
|
+
generations,
|
|
121
|
+
}));
|
|
122
|
+
lines.push('if [ -e "$HOME/.agents" ]; then');
|
|
123
|
+
lines.push(' cp -a "$HOME/.agents" "$AGENTS_RUN_BK/agents"');
|
|
124
|
+
lines.push('fi');
|
|
125
|
+
lines.push('rm -rf -- "$HOME/.agents"');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (plan?.authFile) {
|
|
129
|
+
lines.push(`AUTH_BK="$HOME/${OPENCODE_AUTH_SYNC_BACKUP_DIR}"`);
|
|
130
|
+
lines.push(...generationalBackupSetupLines({
|
|
131
|
+
backupVar: 'AUTH_BK',
|
|
132
|
+
runVar: 'AUTH_RUN_BK',
|
|
133
|
+
quotedRunId,
|
|
134
|
+
generations,
|
|
135
|
+
}));
|
|
136
|
+
lines.push('if [ -e "$HOME/.local/share/opencode/auth.json" ]; then');
|
|
137
|
+
lines.push(' cp -a "$HOME/.local/share/opencode/auth.json" "$AUTH_RUN_BK/auth.json"');
|
|
138
|
+
lines.push('fi');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push("printf 'SYNC_READY\\n'");
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build the remote existence probe script for the sync preview.
|
|
147
|
+
* @param {string[]} uniqueProbePaths
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export const buildRemoteSyncProbeScript = (uniqueProbePaths) => {
|
|
151
|
+
const lines = (Array.isArray(uniqueProbePaths) ? uniqueProbePaths : []).map((rel) => {
|
|
152
|
+
const quoted = shellQuote(rel);
|
|
153
|
+
return `[ -e "$HOME/.config/opencode"/${quoted} ] && printf '%s\\n' ${quoted}`;
|
|
154
|
+
});
|
|
155
|
+
lines.push(
|
|
156
|
+
`[ -e "$HOME/.agents" ] && printf '%s\\n' ${shellQuote(OPENCODE_AGENTS_ROOT_PROBE_MARKER)}`,
|
|
157
|
+
);
|
|
158
|
+
lines.push(
|
|
159
|
+
`[ -e "$HOME/.local/share/opencode/auth.json" ] && printf '%s\\n' ${shellQuote(OPENCODE_AUTH_FILE_PROBE_MARKER)}`,
|
|
160
|
+
);
|
|
161
|
+
lines.push('exit 0');
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Finalize script: confirm generational backup dir exists for this syncRunId and print SYNC_DONE.
|
|
167
|
+
* @param {{ syncRunId: string }} options
|
|
168
|
+
*/
|
|
169
|
+
export const buildRemoteSyncFinalizeScript = (options) => {
|
|
170
|
+
const syncRunId = sanitizeSyncRunIdForPath(options?.syncRunId);
|
|
171
|
+
const quotedRunId = shellQuote(syncRunId);
|
|
172
|
+
return [
|
|
173
|
+
'set -e',
|
|
174
|
+
`BK="$HOME/.config/opencode/${OPENCODE_CONFIG_SYNC_BACKUP_DIR}"/${quotedRunId}`,
|
|
175
|
+
'if [ ! -d "$BK" ]; then',
|
|
176
|
+
" printf 'SYNC_FINALIZE_MISSING_BACKUP\\n' >&2",
|
|
177
|
+
' exit 1',
|
|
178
|
+
'fi',
|
|
179
|
+
"printf 'SYNC_DONE\\n'",
|
|
180
|
+
].join('\n');
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build a remote inventory script that prints a JSON object describing which
|
|
185
|
+
* allowlist paths exist on the remote (for pull planning). Uses only POSIX sh.
|
|
186
|
+
* Output is a single JSON line prefixed by SYNC_INVENTORY=.
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export const buildRemoteSyncInventoryScript = () => {
|
|
190
|
+
const filePaths = [
|
|
191
|
+
...OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups.flat(),
|
|
192
|
+
...OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles,
|
|
193
|
+
];
|
|
194
|
+
const dirPaths = OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.map((entry) => entry.path);
|
|
195
|
+
const lines = [
|
|
196
|
+
'set -e',
|
|
197
|
+
'CFG="$HOME/.config/opencode"',
|
|
198
|
+
'FILES=""',
|
|
199
|
+
'DIRS=""',
|
|
200
|
+
];
|
|
201
|
+
for (const rel of filePaths) {
|
|
202
|
+
const quoted = shellQuote(rel);
|
|
203
|
+
lines.push(`if [ -f "$CFG"/${quoted} ]; then`);
|
|
204
|
+
lines.push(` BYTES="$(wc -c < "$CFG"/${quoted} | tr -d ' ')"`);
|
|
205
|
+
lines.push(` FILES="$FILES{\\"path\\":${JSON.stringify(rel)},\\"bytes\\":$BYTES},"`);
|
|
206
|
+
lines.push('fi');
|
|
207
|
+
}
|
|
208
|
+
for (const rel of dirPaths) {
|
|
209
|
+
const quoted = shellQuote(rel);
|
|
210
|
+
lines.push(`if [ -d "$CFG"/${quoted} ]; then`);
|
|
211
|
+
lines.push(` COUNT="$(find "$CFG"/${quoted} -type f ! -path '*/node_modules/*' ! -name '*.backup' 2>/dev/null | wc -l | tr -d ' ')"`);
|
|
212
|
+
lines.push(` BYTES="$(find "$CFG"/${quoted} -type f ! -path '*/node_modules/*' ! -name '*.backup' -exec wc -c {} + 2>/dev/null | awk 'END{print $1+0}')"`);
|
|
213
|
+
lines.push(` DIRS="$DIRS{\\"path\\":${JSON.stringify(rel)},\\"fileCount\\":$COUNT,\\"bytes\\":$BYTES},"`);
|
|
214
|
+
lines.push('fi');
|
|
215
|
+
}
|
|
216
|
+
lines.push('AGENTS="null"');
|
|
217
|
+
lines.push('if [ -d "$HOME/.agents" ]; then');
|
|
218
|
+
lines.push(" ACOUNT=\"$(find \"$HOME/.agents\" -type f ! -path '*/node_modules/*' ! -name '*.backup' 2>/dev/null | wc -l | tr -d ' ')\"");
|
|
219
|
+
lines.push(" ABYTES=\"$(find \"$HOME/.agents\" -type f ! -path '*/node_modules/*' ! -name '*.backup' -exec wc -c {} + 2>/dev/null | awk 'END{print $1+0}')\"");
|
|
220
|
+
lines.push(' AGENTS="{\\"fileCount\\":$ACOUNT,\\"bytes\\":$ABYTES}"');
|
|
221
|
+
lines.push('fi');
|
|
222
|
+
lines.push('AUTH="null"');
|
|
223
|
+
lines.push('if [ -f "$HOME/.local/share/opencode/auth.json" ]; then');
|
|
224
|
+
lines.push(' AUBYTES="$(wc -c < "$HOME/.local/share/opencode/auth.json" | tr -d \' \')"');
|
|
225
|
+
lines.push(' AUTH="{\\"bytes\\":$AUBYTES}"');
|
|
226
|
+
lines.push('fi');
|
|
227
|
+
lines.push('FILES="${FILES%,}"');
|
|
228
|
+
lines.push('DIRS="${DIRS%,}"');
|
|
229
|
+
lines.push('printf \'SYNC_INVENTORY={"files":[%s],"directories":[%s],"agentsRoot":%s,"authFile":%s}\\n\' "$FILES" "$DIRS" "$AGENTS" "$AUTH"');
|
|
230
|
+
lines.push('exit 0');
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Stream a remote tar.gz of selected config paths to stdout.
|
|
236
|
+
* @param {{ files?: { path: string }[], directories?: { path: string }[] }} plan
|
|
237
|
+
*/
|
|
238
|
+
export const buildRemoteConfigTarScript = (plan) => {
|
|
239
|
+
const entries = [
|
|
240
|
+
...(Array.isArray(plan?.files) ? plan.files.map((entry) => entry.path) : []),
|
|
241
|
+
...(Array.isArray(plan?.directories) ? plan.directories.map((entry) => entry.path) : []),
|
|
242
|
+
];
|
|
243
|
+
if (entries.length === 0) {
|
|
244
|
+
return "printf '' ; exit 0";
|
|
245
|
+
}
|
|
246
|
+
const quotedEntries = entries.map((rel) => shellQuote(rel)).join(' ');
|
|
247
|
+
return [
|
|
248
|
+
'set -e',
|
|
249
|
+
'CFG="$HOME/.config/opencode"',
|
|
250
|
+
`cd "$CFG" && tar -h -czf - ${quotedEntries}`,
|
|
251
|
+
].join('\n');
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const buildRemoteAgentsTarScript = () => [
|
|
255
|
+
'set -e',
|
|
256
|
+
'cd "$HOME" && tar -h -czf - .agents',
|
|
257
|
+
].join('\n');
|
|
258
|
+
|
|
259
|
+
export const buildRemoteAuthTarScript = () => [
|
|
260
|
+
'set -e',
|
|
261
|
+
'cd "$HOME/.local/share/opencode" && tar -h -czf - auth.json',
|
|
262
|
+
].join('\n');
|
|
263
|
+
|
|
264
|
+
export { shellQuote };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { OPENCODE_CONFIG_SYNC_ALLOWLIST } from './constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* fileGroups: boolean[],
|
|
6
|
+
* singleFiles: boolean[],
|
|
7
|
+
* directories: boolean[],
|
|
8
|
+
* agentsRoot: boolean,
|
|
9
|
+
* authFile: boolean,
|
|
10
|
+
* }} SyncSelections
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default: everything selected except authFile (credentials stay opt-in).
|
|
15
|
+
* @param {{ includeAuthFile?: boolean }} [options]
|
|
16
|
+
* @returns {SyncSelections}
|
|
17
|
+
*/
|
|
18
|
+
export const buildDefaultSyncSelections = (options = {}) => ({
|
|
19
|
+
fileGroups: OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups.map(() => true),
|
|
20
|
+
singleFiles: OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles.map(() => true),
|
|
21
|
+
directories: OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.map(() => true),
|
|
22
|
+
agentsRoot: true,
|
|
23
|
+
authFile: options.includeAuthFile === true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a partial selections object against the allowlist shape.
|
|
28
|
+
* @param {Partial<SyncSelections> | null | undefined} raw
|
|
29
|
+
* @param {{ includeAuthFile?: boolean }} [options]
|
|
30
|
+
* @returns {SyncSelections}
|
|
31
|
+
*/
|
|
32
|
+
export const normalizeSyncSelections = (raw, options = {}) => {
|
|
33
|
+
const defaults = buildDefaultSyncSelections(options);
|
|
34
|
+
if (!raw || typeof raw !== 'object') return defaults;
|
|
35
|
+
|
|
36
|
+
const fileGroups = OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups.map((_, index) => (
|
|
37
|
+
Array.isArray(raw.fileGroups) ? raw.fileGroups[index] !== false : defaults.fileGroups[index]
|
|
38
|
+
));
|
|
39
|
+
const singleFiles = OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles.map((_, index) => (
|
|
40
|
+
Array.isArray(raw.singleFiles) ? raw.singleFiles[index] !== false : defaults.singleFiles[index]
|
|
41
|
+
));
|
|
42
|
+
const directories = OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.map((_, index) => (
|
|
43
|
+
Array.isArray(raw.directories) ? raw.directories[index] !== false : defaults.directories[index]
|
|
44
|
+
));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
fileGroups,
|
|
48
|
+
singleFiles,
|
|
49
|
+
directories,
|
|
50
|
+
agentsRoot: raw.agentsRoot !== false,
|
|
51
|
+
authFile: raw.authFile === true,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Filter a full plan down to the selected allowlist groups.
|
|
57
|
+
* Mutual-exclusion within a fileGroup is preserved (plan already picked a winner).
|
|
58
|
+
* @param {object} plan
|
|
59
|
+
* @param {SyncSelections} selections
|
|
60
|
+
*/
|
|
61
|
+
export const filterPlanBySelections = (plan, selections) => {
|
|
62
|
+
const selected = normalizeSyncSelections(selections, { includeAuthFile: selections?.authFile === true });
|
|
63
|
+
const selectedFileNames = new Set();
|
|
64
|
+
const selectedDeleteNames = new Set();
|
|
65
|
+
|
|
66
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST.fileGroups.forEach((group, index) => {
|
|
67
|
+
if (!selected.fileGroups[index]) return;
|
|
68
|
+
for (const name of group) {
|
|
69
|
+
selectedFileNames.add(name);
|
|
70
|
+
selectedDeleteNames.add(name);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST.singleFiles.forEach((name, index) => {
|
|
74
|
+
if (!selected.singleFiles[index]) return;
|
|
75
|
+
selectedFileNames.add(name);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const selectedDirNames = new Set();
|
|
79
|
+
const selectedDirLegacy = new Set();
|
|
80
|
+
OPENCODE_CONFIG_SYNC_ALLOWLIST.directories.forEach((dirSpec, index) => {
|
|
81
|
+
if (!selected.directories[index]) return;
|
|
82
|
+
selectedDirNames.add(dirSpec.path);
|
|
83
|
+
selectedDeleteNames.add(dirSpec.path);
|
|
84
|
+
if (typeof dirSpec.legacy === 'string' && dirSpec.legacy) {
|
|
85
|
+
selectedDirLegacy.add(dirSpec.legacy);
|
|
86
|
+
selectedDeleteNames.add(dirSpec.legacy);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const files = (Array.isArray(plan?.files) ? plan.files : []).filter((entry) => selectedFileNames.has(entry.path));
|
|
91
|
+
const directories = (Array.isArray(plan?.directories) ? plan.directories : []).filter((entry) => selectedDirNames.has(entry.path));
|
|
92
|
+
const deletes = (Array.isArray(plan?.deletes) ? plan.deletes : []).filter((name) => selectedDeleteNames.has(name) || selectedDirLegacy.has(name));
|
|
93
|
+
const agentsRoot = selected.agentsRoot ? (plan?.agentsRoot ?? null) : null;
|
|
94
|
+
const authFile = selected.authFile ? (plan?.authFile ?? null) : null;
|
|
95
|
+
|
|
96
|
+
let totalBytes = 0;
|
|
97
|
+
for (const entry of files) totalBytes += Number(entry.bytes) || 0;
|
|
98
|
+
for (const entry of directories) totalBytes += Number(entry.bytes) || 0;
|
|
99
|
+
if (agentsRoot) totalBytes += Number(agentsRoot.bytes) || 0;
|
|
100
|
+
if (authFile) totalBytes += Number(authFile.bytes) || 0;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...plan,
|
|
104
|
+
files,
|
|
105
|
+
directories,
|
|
106
|
+
deletes,
|
|
107
|
+
agentsRoot,
|
|
108
|
+
authFile,
|
|
109
|
+
totalBytes,
|
|
110
|
+
selections: selected,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collect stdout from a local `tar …` into a Buffer.
|
|
5
|
+
*
|
|
6
|
+
* Callers buffer the entire archive in memory; TargetExecutor.putTar accepts
|
|
7
|
+
* Buffer | Uint8Array only.
|
|
8
|
+
*
|
|
9
|
+
* @param {string[]} tarArgs
|
|
10
|
+
* @param {{ windowsHide?: boolean }} [spawnOptions]
|
|
11
|
+
* @returns {Promise<Buffer>}
|
|
12
|
+
*/
|
|
13
|
+
export const collectLocalTarBuffer = (tarArgs, spawnOptions = {}) => new Promise((resolve, reject) => {
|
|
14
|
+
const child = spawn('tar', tarArgs, {
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
...(spawnOptions.windowsHide ? { windowsHide: true } : {}),
|
|
17
|
+
});
|
|
18
|
+
/** @type {Buffer[]} */
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let stderr = '';
|
|
21
|
+
child.stdout?.on('data', (chunk) => {
|
|
22
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
23
|
+
});
|
|
24
|
+
child.stderr?.on('data', (chunk) => {
|
|
25
|
+
stderr += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
child.on('error', reject);
|
|
28
|
+
child.on('close', (code) => {
|
|
29
|
+
if (code !== 0) {
|
|
30
|
+
reject(new Error((stderr || 'Local tar failed').trim()));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
resolve(Buffer.concat(chunks));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @param {string} instanceId */
|
|
2
|
+
export const syncTargetIdForSshInstance = (instanceId) => {
|
|
3
|
+
const id = String(instanceId || '').trim();
|
|
4
|
+
if (!id) throw new Error('SSH instance id is required');
|
|
5
|
+
return `ssh:${id}`;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** @param {string} hostId */
|
|
9
|
+
export const syncTargetIdForDirectHost = (hostId) => {
|
|
10
|
+
const id = String(hostId || '').trim();
|
|
11
|
+
if (!id) throw new Error('Desktop host id is required');
|
|
12
|
+
return `host:${id}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} serverId relay host signing-key fingerprint
|
|
17
|
+
* Mirrored in packages/ui/src/lib/relay/relay-config-sync.ts (browser bundle cannot
|
|
18
|
+
* import this server module); keep both sides in sync when changing the prefix.
|
|
19
|
+
*/
|
|
20
|
+
export const syncTargetIdForRelayServer = (serverId) => {
|
|
21
|
+
const id = String(serverId || '').trim();
|
|
22
|
+
if (!id) throw new Error('Relay serverId is required');
|
|
23
|
+
return `relay:${id}`;
|
|
24
|
+
};
|
|
@@ -9,7 +9,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
9
9
|
- `packages/web/server/lib/event-stream/global-ws-bridge.js`: browser-facing global WS bridge that subscribes clients to the shared global hub.
|
|
10
10
|
- `packages/web/server/lib/event-stream/directory-ws-bridge.js`: browser-facing per-directory WS bridge that owns one scoped upstream reader per connection.
|
|
11
11
|
- `packages/web/server/lib/event-stream/protocol.js`: path constants, SSE envelope parsing, and WebSocket frame serialization helpers.
|
|
12
|
-
- `packages/web/server/lib/event-stream/diff-summary.js`: pure outbound FileDiff
|
|
12
|
+
- `packages/web/server/lib/event-stream/diff-summary.js`: pure outbound FileDiff helpers. L1 message/session `summary.diffs` keep a slim file list `{ file, status?, additions, deletions }` plus additive `diffCount`/`hasDiffs` (never patch/before/after/from/to). `session.diff` event frames still keep file/status/additions/deletions and drop patch bodies. `summarizeFileDiff(s)` remains for explicit L2-style file lists.
|
|
13
13
|
- `packages/web/server/lib/event-stream/upstream-reader.js`: reusable upstream SSE reader with event-id tracking, stall recovery, and reconnect handling.
|
|
14
14
|
- `packages/web/server/lib/event-stream/runtime.js`: thin WebSocket server runtime for upgrade handling and path dispatch to the global/directory bridges.
|
|
15
15
|
- `packages/web/server/lib/event-stream/protocol.test.js`: unit tests for protocol helpers.
|
|
@@ -26,7 +26,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
|
|
|
26
26
|
- `MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS`: heartbeat interval for browser-facing WS connections.
|
|
27
27
|
- `parseSseEventEnvelope(block)`: parses an SSE block into `{ eventId, directory, payload }`.
|
|
28
28
|
- `sendMessageStreamWsFrame(socket, payload)`: serializes and sends a JSON WS frame.
|
|
29
|
-
- `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`. Outbound `session.diff`
|
|
29
|
+
- `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`. Outbound `session.diff` keeps file/status/additions/deletions only (no full patch bodies). Message/session `summary.diffs` are L1-projected to a slim file list plus additive `diffCount`/`hasDiffs` (patch bodies never leave the host).
|
|
30
30
|
|
|
31
31
|
### Runtime helpers
|
|
32
32
|
- `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Outbound FileDiff summarization for message-stream fan-out.
|
|
2
|
+
// Outbound FileDiff summarization for message-stream fan-out and Host L1.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// L1 (ordinary first-packet / SSE / exact message GET):
|
|
5
|
+
// keep `info.summary.diffs` as a slim file list
|
|
6
|
+
// `{ file, status?, additions, deletions }` plus additive
|
|
7
|
+
// `diffCount` / `hasDiffs` markers. Never ship patch bodies on these paths.
|
|
8
|
+
//
|
|
9
|
+
// Explicit L2/L3 APIs may still use summarizeFileDiff(s) for file-list / patch
|
|
10
|
+
// responses. Pure JS — do not import UI TypeScript from the server package.
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
|
|
9
13
|
/** Lightweight FileDiff fields safe for outbound event frames (preview only). */
|
|
@@ -62,7 +66,15 @@ export function summarizeFileDiffs(diffs) {
|
|
|
62
66
|
return changed ? next : diffs;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
/**
|
|
70
|
+
* L1 projection: keep `summary.diffs` as a slim file-list array and set
|
|
71
|
+
* additive `diffCount` / `hasDiffs` markers. Drops patch/before/after/from/to.
|
|
72
|
+
* Keeps every other lightweight summary field. Identity when unchanged.
|
|
73
|
+
*
|
|
74
|
+
* @param {unknown} owner message info / session info / summary-bearing object
|
|
75
|
+
* @returns {unknown}
|
|
76
|
+
*/
|
|
77
|
+
export const projectMessageSummaryDiffSlim = (owner) => {
|
|
66
78
|
if (!owner || typeof owner !== 'object' || Array.isArray(owner)) {
|
|
67
79
|
return owner;
|
|
68
80
|
}
|
|
@@ -72,12 +84,18 @@ const summarizeSummaryDiffs = (owner) => {
|
|
|
72
84
|
return owner;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
if (!
|
|
87
|
+
if (!Object.prototype.hasOwnProperty.call(summary, 'diffs')) {
|
|
76
88
|
return owner;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
const
|
|
80
|
-
|
|
91
|
+
const rawDiffs = summary.diffs;
|
|
92
|
+
const slimDiffs = Array.isArray(rawDiffs) ? summarizeFileDiffs(rawDiffs) : [];
|
|
93
|
+
const diffCount = slimDiffs.length;
|
|
94
|
+
const hasDiffs = diffCount > 0;
|
|
95
|
+
|
|
96
|
+
const diffsUnchanged = slimDiffs === rawDiffs;
|
|
97
|
+
const markersMatch = summary.diffCount === diffCount && summary.hasDiffs === hasDiffs;
|
|
98
|
+
if (diffsUnchanged && markersMatch) {
|
|
81
99
|
return owner;
|
|
82
100
|
}
|
|
83
101
|
|
|
@@ -85,17 +103,27 @@ const summarizeSummaryDiffs = (owner) => {
|
|
|
85
103
|
...owner,
|
|
86
104
|
summary: {
|
|
87
105
|
...summary,
|
|
88
|
-
diffs:
|
|
106
|
+
diffs: slimDiffs,
|
|
107
|
+
diffCount,
|
|
108
|
+
hasDiffs,
|
|
89
109
|
},
|
|
90
110
|
};
|
|
91
111
|
};
|
|
92
112
|
|
|
113
|
+
/** @deprecated Prefer projectMessageSummaryDiffSlim — same L1 slim-list behavior. */
|
|
114
|
+
export const projectMessageSummaryDiffCounts = projectMessageSummaryDiffSlim;
|
|
115
|
+
|
|
116
|
+
/** @deprecated Prefer projectMessageSummaryDiffSlim — L1 keeps slim file list + markers. */
|
|
117
|
+
export const summarizeMessageDiffSnapshots = projectMessageSummaryDiffSlim;
|
|
118
|
+
|
|
93
119
|
/**
|
|
94
120
|
* Summarize FileDiff bodies on outbound event payloads.
|
|
95
121
|
*
|
|
96
122
|
* Handles:
|
|
97
123
|
* - `session.diff` with legacy `properties.diff` or current `data.diff`
|
|
124
|
+
* (keeps file/status/additions/deletions; drops patch bodies)
|
|
98
125
|
* - message/session objects carrying `summary.diffs` under properties/data info
|
|
126
|
+
* (L1: slim file list + diffCount/hasDiffs; never patch bodies)
|
|
99
127
|
*
|
|
100
128
|
* Non-diff events keep identity.
|
|
101
129
|
*/
|
|
@@ -154,7 +182,7 @@ export function summarizeOutboundEventPayload(payload) {
|
|
|
154
182
|
|
|
155
183
|
// Common shape: properties.info.summary.diffs
|
|
156
184
|
if (body.info && typeof body.info === 'object' && !Array.isArray(body.info)) {
|
|
157
|
-
const nextInfo =
|
|
185
|
+
const nextInfo = projectMessageSummaryDiffSlim(body.info);
|
|
158
186
|
if (nextInfo !== body.info) {
|
|
159
187
|
next = {
|
|
160
188
|
...next,
|
|
@@ -169,7 +197,7 @@ export function summarizeOutboundEventPayload(payload) {
|
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
// Rare: properties.summary.diffs directly on the properties/data body
|
|
172
|
-
const nextBody =
|
|
200
|
+
const nextBody = projectMessageSummaryDiffSlim(body);
|
|
173
201
|
if (nextBody !== body) {
|
|
174
202
|
next = {
|
|
175
203
|
...next,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
projectMessageSummaryDiffCounts,
|
|
5
|
+
projectMessageSummaryDiffSlim,
|
|
4
6
|
summarizeFileDiff,
|
|
5
7
|
summarizeFileDiffs,
|
|
6
8
|
summarizeOutboundEventPayload,
|
|
@@ -48,6 +50,67 @@ describe('summarizeFileDiffs', () => {
|
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
|
|
53
|
+
describe('projectMessageSummaryDiffSlim', () => {
|
|
54
|
+
it('keeps slim summary.diffs and sets diffCount/hasDiffs', () => {
|
|
55
|
+
const owner = {
|
|
56
|
+
id: 'msg_1',
|
|
57
|
+
summary: {
|
|
58
|
+
title: 'turn',
|
|
59
|
+
diffs: [
|
|
60
|
+
{ file: 'a.ts', additions: 1, deletions: 0, patch: '@@' },
|
|
61
|
+
{ file: 'b.ts', additions: 2, deletions: 1, before: 'x', after: 'y' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const next = projectMessageSummaryDiffSlim(owner);
|
|
67
|
+
expect(next).not.toBe(owner);
|
|
68
|
+
expect(next.summary).toEqual({
|
|
69
|
+
title: 'turn',
|
|
70
|
+
diffs: [
|
|
71
|
+
{ file: 'a.ts', additions: 1, deletions: 0 },
|
|
72
|
+
{ file: 'b.ts', additions: 2, deletions: 1 },
|
|
73
|
+
],
|
|
74
|
+
diffCount: 2,
|
|
75
|
+
hasDiffs: true,
|
|
76
|
+
});
|
|
77
|
+
expect(JSON.stringify(next)).not.toContain('patch');
|
|
78
|
+
expect(JSON.stringify(next)).not.toContain('"before"');
|
|
79
|
+
expect(JSON.stringify(next)).not.toContain('"after"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('projects empty arrays to slim list + diffCount 0 / hasDiffs false', () => {
|
|
83
|
+
const owner = {
|
|
84
|
+
summary: { diffs: [] },
|
|
85
|
+
};
|
|
86
|
+
expect(projectMessageSummaryDiffSlim(owner).summary).toEqual({
|
|
87
|
+
diffs: [],
|
|
88
|
+
diffCount: 0,
|
|
89
|
+
hasDiffs: false,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('keeps identity when summary has no diffs key', () => {
|
|
94
|
+
const owner = { summary: { title: 'x', diffCount: 3, hasDiffs: true } };
|
|
95
|
+
expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('keeps identity when diffs already slim and markers match', () => {
|
|
99
|
+
const owner = {
|
|
100
|
+
summary: {
|
|
101
|
+
diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
|
|
102
|
+
diffCount: 1,
|
|
103
|
+
hasDiffs: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('exports projectMessageSummaryDiffCounts as a compatible alias', () => {
|
|
110
|
+
expect(projectMessageSummaryDiffCounts).toBe(projectMessageSummaryDiffSlim);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
51
114
|
describe('summarizeOutboundEventPayload', () => {
|
|
52
115
|
it('summarizes session.diff properties.diff', () => {
|
|
53
116
|
const payload = {
|
|
@@ -97,7 +160,7 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
97
160
|
}]);
|
|
98
161
|
});
|
|
99
162
|
|
|
100
|
-
it('
|
|
163
|
+
it('projects message.updated info.summary.diffs to L1 slim list + markers', () => {
|
|
101
164
|
const payload = {
|
|
102
165
|
type: 'message.updated',
|
|
103
166
|
properties: {
|
|
@@ -109,6 +172,8 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
109
172
|
additions: 1,
|
|
110
173
|
deletions: 0,
|
|
111
174
|
patch: '@@',
|
|
175
|
+
from: 'blob-a',
|
|
176
|
+
to: 'blob-b',
|
|
112
177
|
}],
|
|
113
178
|
},
|
|
114
179
|
},
|
|
@@ -116,11 +181,13 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
116
181
|
};
|
|
117
182
|
|
|
118
183
|
const next = summarizeOutboundEventPayload(payload);
|
|
119
|
-
expect(next.properties.info.summary
|
|
120
|
-
file: 'a.ts',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
184
|
+
expect(next.properties.info.summary).toEqual({
|
|
185
|
+
diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
|
|
186
|
+
diffCount: 1,
|
|
187
|
+
hasDiffs: true,
|
|
188
|
+
});
|
|
189
|
+
expect(JSON.stringify(next)).not.toContain('patch');
|
|
190
|
+
expect(JSON.stringify(next)).not.toContain('blob-a');
|
|
124
191
|
});
|
|
125
192
|
|
|
126
193
|
it('returns identity for session.status', () => {
|