@openchambery/web 1.18.2-beta.2 → 1.18.2-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-zQDlmCA-.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BmLxb00F.js} +4 -4
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-GH3vG8Z5.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-tgkSfBku.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-CyDXHPw9.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-DT2ySSn1.js} +1 -1
- package/dist/assets/{MobileSurface-htGkf7Ua.js → MobileSurface-Bhur0fxY.js} +1 -1
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-CJQ8RU1e.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-DiPnIwza.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-DC12_DLb.js} +1 -1
- package/dist/assets/SettingsView-BywZ5HTE.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-zVXnRr6V.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CE3CCTsi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-C3_vf7HM.js} +1 -1
- package/dist/assets/{appThemeRegistry-CgfOR_0L.js → appThemeRegistry-Y2U1kIMh.js} +2 -2
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-CbqhDr1g.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-wdR3gf8a.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog--cf4dygO.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DCoGZljP.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-dkEoZpPz.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BL_etF0B.js} +1 -1
- package/dist/assets/{insertionBoundaries-DzXMTleb.js → insertionBoundaries-CjCybFIP.js} +1 -1
- package/dist/assets/{ja-CzMPz7bT.js → ja-bQwSsvRr.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-Kq5C3hsv.js} +1 -1
- package/dist/assets/{main-CO6z5yRB.js → main-Bpwq1Xh1.js} +2 -2
- package/dist/assets/{main-LDphyc-v.js → main-DIVJIea3.js} +7 -7
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-BrYw8M4I.js} +2 -2
- package/dist/assets/{mobile-BktO3_4R.js → mobile-i4Pu_Tak.js} +2 -2
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-OJ8angjt.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-CYpMAgVn.js} +1 -1
- package/dist/assets/projectMeta-Dt0loPh9.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-MSPTLfTm.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-SXFBBh2O.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-Db8uO-6q.js → renderElectronMiniChatApp-BBb29zbe.js} +2 -2
- package/dist/assets/{renderMobileApp-BCV81Ryl.js → renderMobileApp-Ctj7sqAE.js} +5 -5
- package/dist/assets/{runtimeEndpointReset-Dg3DMy9v.js → runtimeEndpointReset-By8IWO59.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-8qgYIxgY.js} +1 -1
- package/dist/assets/src-DOs1ZW0T.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-OURVnLSN.js} +2 -2
- package/dist/assets/{uk-xGc9Pzge.js → uk-cSkRAYFY.js} +1 -1
- package/dist/assets/{useAppFontEffects-CM0VKsdo.js → useAppFontEffects-D81CYdad.js} +3 -3
- package/dist/assets/{useDesktopWindowControlsLayout-Cq-r4Hcb.js → useDesktopWindowControlsLayout-BWOeSiA5.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-BNDb_82A.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DnzT4jO2.js → useMobileNavigationStore-DBYUoTQ0.js} +4 -4
- package/dist/assets/useSessionAutoCleanup-Cc_AwEoh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-DEhp2GTh.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-BKqeT_uz.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-BaNjJ8Y4.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/index.d.ts +6 -0
- package/server/index.js +6 -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/opencode/DOCUMENTATION.md +3 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- 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/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/src-oHrYIqHF.css +0 -2
- 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
|
+
};
|
|
@@ -184,13 +184,16 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
184
184
|
|
|
185
185
|
## Public exports (settings-runtime.js)
|
|
186
186
|
- `createSettingsRuntime(dependencies)`: creates settings lifecycle runtime for read/migrate/persist concerns.
|
|
187
|
+
- Optional dependency `runExclusivePersist` / `getRunExclusivePersist`, plus returned `setRunExclusivePersist(fn)`, let an in-process host (Electron) replace the default `persistSettingsLock` with a shared exclusive runner so desktop main/ssh writers and web settings persistence serialize on one chain.
|
|
187
188
|
- Returned API:
|
|
188
189
|
- `readSettingsFromDisk()`
|
|
189
190
|
- `readSettingsFromDiskMigrated()`
|
|
190
191
|
- `writeSettingsToDisk(settings)`
|
|
191
192
|
- `persistSettings(changes)`
|
|
193
|
+
- `setRunExclusivePersist(fn)`
|
|
192
194
|
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
|
193
195
|
- One-shot compact-chat defaults migration: when disk marker `compactChatDefaultsMigrationVersion` is missing, rewrite legacy/absent `chatRenderMode`/`activityRenderMode`/`showTurnChangedFiles` to `sorted`/`collapsed`/`true` and persist marker `1` (marker stays on disk; response allowlist still hides it). Marker already `1` preserves user values; `persistSettings` runs the same migration before the first write.
|
|
196
|
+
- `startWebUiServer({ settingsPersistLock })` late-binds that shared runner after module load.
|
|
194
197
|
|
|
195
198
|
## Public exports (settings-helpers.js)
|
|
196
199
|
- `createSettingsHelpers(dependencies)`: creates settings helper runtime for settings request/response shaping.
|
|
@@ -30,6 +30,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
30
30
|
getServerId,
|
|
31
31
|
getServerLabel,
|
|
32
32
|
readSettingsFromDiskMigrated,
|
|
33
|
+
persistSettings,
|
|
33
34
|
normalizeTunnelSessionTtlMs,
|
|
34
35
|
authorizeManagedOpenCodeBridgeRequest,
|
|
35
36
|
sayTTSCapability,
|
|
@@ -147,6 +148,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
147
148
|
__dirname,
|
|
148
149
|
openchamberDataDir,
|
|
149
150
|
readSettingsFromDiskMigrated,
|
|
151
|
+
persistSettings,
|
|
150
152
|
fetchFreeZenModels,
|
|
151
153
|
getCachedZenModels,
|
|
152
154
|
sessionIndexService,
|
|
@@ -1127,6 +1127,7 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1127
1127
|
} else if (
|
|
1128
1128
|
(req.method === 'PUT' && req.path.match(/^\/api\/openchamber\/message-queue\/attachments\/uploads\/[^/]+\/?$/))
|
|
1129
1129
|
|| (req.method === 'PUT' && req.path.match(/^\/api\/fs\/prompt-attachments\/[^/]+\/?$/))
|
|
1130
|
+
|| (req.method === 'PUT' && req.path.match(/^\/api\/openchamber\/config-sync\/put\/[^/]+\/?$/))
|
|
1130
1131
|
) {
|
|
1131
1132
|
// Binary attachment uploads stream as the request body. Parsing JSON here
|
|
1132
1133
|
// would buffer the entire payload and block the relay tunnel.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerSessionIndexRoutes } from '../session-index/routes.js';
|
|
2
2
|
import { registerTranscriptCacheRoutes } from '../transcript-cache/routes.js';
|
|
3
3
|
import { registerDesktopHostRoutes } from '../desktop-hosts/routes.js';
|
|
4
|
+
import { registerConfigSyncRoutes } from '../config-sync/routes.js';
|
|
4
5
|
|
|
5
6
|
export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
6
7
|
const {
|
|
@@ -11,6 +12,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
11
12
|
__dirname,
|
|
12
13
|
openchamberDataDir,
|
|
13
14
|
readSettingsFromDiskMigrated,
|
|
15
|
+
persistSettings,
|
|
14
16
|
fetchFreeZenModels,
|
|
15
17
|
getCachedZenModels,
|
|
16
18
|
sessionIndexService,
|
|
@@ -31,6 +33,11 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
31
33
|
getPairingSession,
|
|
32
34
|
express,
|
|
33
35
|
});
|
|
36
|
+
registerConfigSyncRoutes(app, {
|
|
37
|
+
readSettingsFromDiskMigrated,
|
|
38
|
+
persistSettings,
|
|
39
|
+
express,
|
|
40
|
+
});
|
|
34
41
|
|
|
35
42
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
36
43
|
try {
|