@narumitw/pi-sync 0.54.0 → 0.54.2
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/README.md +8 -0
- package/dist/chunks/{chunk-ANWNQIR7.ts → chunk-CL7BZRRQ.ts} +3 -3
- package/dist/chunks/chunk-CL7BZRRQ.ts.map +7 -0
- package/dist/chunks/{chunk-QDPED3XJ.ts → chunk-MYPSMJHJ.ts} +408 -143
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +7 -0
- package/dist/chunks/{conflict-review-JANNPKKS.ts → conflict-review-4PLVLMRO.ts} +3 -3
- package/dist/chunks/{sync-operations-OWTE3ZL2.ts → sync-operations-47OUX76C.ts} +3 -3
- package/dist/index.ts +3 -3
- package/package.json +1 -1
- package/src/snapshot/snapshot-transaction.ts +4 -2
- package/src/sync/merged-sync.ts +28 -24
- package/src/ui/merge-review.ts +79 -19
- package/src/ui/merged-sync-summary.ts +128 -0
- package/src/ui/sync-confirmation.ts +153 -0
- package/dist/chunks/chunk-ANWNQIR7.ts.map +0 -7
- package/dist/chunks/chunk-QDPED3XJ.ts.map +0 -7
- /package/dist/chunks/{conflict-review-JANNPKKS.ts.map → conflict-review-4PLVLMRO.ts.map} +0 -0
- /package/dist/chunks/{sync-operations-OWTE3ZL2.ts.map → sync-operations-47OUX76C.ts.map} +0 -0
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
applySnapshotTransaction,
|
|
31
31
|
recoverPendingSnapshotTransactions,
|
|
32
32
|
syncMutationParents
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-CL7BZRRQ.ts";
|
|
34
34
|
import {
|
|
35
35
|
backupLocal,
|
|
36
36
|
captureMutationOwner,
|
|
@@ -111,135 +111,284 @@ import {
|
|
|
111
111
|
} from "./chunk-HUZ5CR4W.ts";
|
|
112
112
|
|
|
113
113
|
// src/ui/merge-review.ts
|
|
114
|
-
import {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
import { truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
115
|
+
import { defineMenu, runCustomInteraction, runDocumentReview, runMenu } from "@narumitw/pi-tui-kit";
|
|
116
|
+
|
|
117
|
+
// src/ui/sync-confirmation.ts
|
|
118
|
+
import { Key, matchesKey, SelectList, truncateToWidth } from "@earendil-works/pi-tui";
|
|
119
|
+
import { HorizontalRule, hardWrapTerminalDocument, sanitizeTerminalText } from "@narumitw/pi-tui-kit";
|
|
120
|
+
function createSyncConfirmation(options) {
|
|
121
|
+
const { tui, theme, keybindings } = options;
|
|
122
|
+
const choices = ["cancel", "approve", "details"];
|
|
123
|
+
const list = new SelectList(
|
|
124
|
+
[
|
|
125
|
+
{ value: "cancel", label: "No, cancel" },
|
|
126
|
+
{ value: "approve", label: `Yes, ${sanitizeTerminalText(options.confirmationLabel)}` },
|
|
127
|
+
{ value: "details", label: "View details" }
|
|
128
|
+
],
|
|
129
|
+
3,
|
|
130
|
+
{
|
|
131
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
132
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
133
|
+
description: (text) => theme.fg("muted", text),
|
|
134
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
135
|
+
noMatch: (text) => theme.fg("muted", text)
|
|
126
136
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
);
|
|
138
|
+
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
139
|
+
let selected = 0;
|
|
140
|
+
let offset = 0;
|
|
141
|
+
let maximumOffset = 0;
|
|
142
|
+
let lastWidth = 0;
|
|
143
|
+
let lastRows = 0;
|
|
144
|
+
let seen = /* @__PURE__ */ new Set();
|
|
145
|
+
let summary = [];
|
|
146
|
+
let reviewed = false;
|
|
147
|
+
let disposed = false;
|
|
148
|
+
let finished = false;
|
|
149
|
+
let summaryStart = 0;
|
|
150
|
+
let summaryEnd = 0;
|
|
151
|
+
let actionsStart = 0;
|
|
152
|
+
const finish = (choice) => {
|
|
153
|
+
if (disposed || finished) return;
|
|
154
|
+
finished = true;
|
|
155
|
+
options.complete(choice);
|
|
156
|
+
};
|
|
157
|
+
const select = (index) => {
|
|
158
|
+
selected = (index + choices.length) % choices.length;
|
|
159
|
+
list.setSelectedIndex(selected);
|
|
160
|
+
};
|
|
161
|
+
list.onSelectionChange = (item) => {
|
|
162
|
+
selected = Math.max(0, choices.indexOf(item.value));
|
|
163
|
+
};
|
|
164
|
+
list.onSelect = (item) => {
|
|
165
|
+
const choice = choices[selected] ?? "cancel";
|
|
166
|
+
if (item.value === choice && (choice !== "approve" || reviewed)) finish(choice);
|
|
167
|
+
};
|
|
168
|
+
const component = {
|
|
169
|
+
render(width, expose = true) {
|
|
170
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
171
|
+
reviewed = false;
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const cells = Math.floor(width);
|
|
175
|
+
const rows = Math.max(1, Math.floor(tui.terminal.rows) - 3);
|
|
176
|
+
if (cells !== lastWidth || rows !== lastRows) {
|
|
177
|
+
summary = options.lines.flatMap((line) => hardWrapTerminalDocument(line, cells));
|
|
178
|
+
seen = /* @__PURE__ */ new Set();
|
|
179
|
+
offset = 0;
|
|
180
|
+
select(0);
|
|
181
|
+
lastWidth = cells;
|
|
182
|
+
lastRows = rows;
|
|
183
|
+
}
|
|
184
|
+
const framed = rows >= 9;
|
|
185
|
+
const actions = list.render(cells);
|
|
186
|
+
const viewport = Math.max(0, rows - actions.length - 3 - (framed ? 2 : 0));
|
|
187
|
+
maximumOffset = Math.max(0, summary.length - viewport);
|
|
188
|
+
offset = Math.min(offset, maximumOffset);
|
|
189
|
+
const visible = summary.slice(offset, offset + viewport);
|
|
190
|
+
if (expose) for (let index = offset; index < offset + visible.length; index++) seen.add(index);
|
|
191
|
+
reviewed = viewport > 0 && seen.size === summary.length;
|
|
192
|
+
const hint = reviewed ? "Select an action." : "Navigation keys scroll the summary before selecting Yes.";
|
|
193
|
+
const body = [
|
|
194
|
+
theme.fg("accent", theme.bold(hardWrapTerminalDocument(options.title, cells).join(" "))),
|
|
195
|
+
...visible.map((line) => theme.fg("text", line)),
|
|
196
|
+
theme.fg(
|
|
197
|
+
"muted",
|
|
198
|
+
`Summary ${visible.length ? offset + 1 : 0}-${offset + visible.length}/${summary.length}${reviewed ? "" : " \u2014 review before Yes"}`
|
|
199
|
+
),
|
|
200
|
+
...actions,
|
|
201
|
+
theme.fg("dim", hint)
|
|
202
|
+
];
|
|
203
|
+
summaryStart = Number(framed) + 1;
|
|
204
|
+
summaryEnd = summaryStart + visible.length;
|
|
205
|
+
actionsStart = summaryEnd + 1;
|
|
206
|
+
const frame = framed ? [...rule.render(cells), ...body, ...rule.render(cells)] : body;
|
|
207
|
+
return frame.slice(0, rows).map((line) => truncateToWidth(line, cells, ""));
|
|
208
|
+
},
|
|
209
|
+
invalidate() {
|
|
210
|
+
list.invalidate();
|
|
211
|
+
rule.invalidate();
|
|
212
|
+
},
|
|
213
|
+
handleInput(data) {
|
|
214
|
+
if (disposed || finished) return;
|
|
215
|
+
if (matchesKey(data, Key.ctrl("c")) || keybindings.matches(data, "tui.select.cancel")) {
|
|
216
|
+
finish("cancel");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
component.render(tui.terminal.columns ?? lastWidth, false);
|
|
220
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
221
|
+
if (!reviewed || selected === 0 && offset > 0) offset = Math.max(0, offset - 1);
|
|
222
|
+
else select(selected - 1);
|
|
223
|
+
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
224
|
+
if (!reviewed || selected === 0 && offset < maximumOffset) offset = Math.min(maximumOffset, offset + 1);
|
|
225
|
+
else select(selected + 1);
|
|
226
|
+
} else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
227
|
+
const choice = choices[selected] ?? "cancel";
|
|
228
|
+
if (choice !== "approve" || reviewed) finish(choice);
|
|
229
|
+
}
|
|
230
|
+
tui.requestRender();
|
|
231
|
+
},
|
|
232
|
+
handleMouse(event) {
|
|
233
|
+
if (disposed || finished || event.width !== lastWidth) return void 0;
|
|
234
|
+
component.render(event.width, false);
|
|
235
|
+
if (event.y >= summaryStart && event.y < summaryEnd && event.type === "wheel" && event.wheelDelta) {
|
|
236
|
+
offset = Math.max(0, Math.min(maximumOffset, offset + (event.wheelDelta < 0 ? -1 : 1)));
|
|
237
|
+
select(0);
|
|
238
|
+
tui.requestRender();
|
|
239
|
+
return { handled: true, render: true };
|
|
240
|
+
}
|
|
241
|
+
if (event.y < actionsStart || event.y >= actionsStart + choices.length || event.y >= lastRows) return void 0;
|
|
242
|
+
return list.handleMouse({ ...event, y: event.y - actionsStart, height: choices.length });
|
|
243
|
+
},
|
|
244
|
+
dispose() {
|
|
245
|
+
disposed = true;
|
|
246
|
+
seen.clear();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
return component;
|
|
132
250
|
}
|
|
133
251
|
|
|
134
|
-
// src/ui/
|
|
135
|
-
function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
252
|
+
// src/ui/merge-review.ts
|
|
253
|
+
async function confirmMergeReview(ctx, title, content, signal, isCurrent, confirmationLabel = "apply changes", summaryLines) {
|
|
254
|
+
const lines = summaryLines ?? [
|
|
255
|
+
...content.split("\n").slice(0, 6).map((line) => truncateToWidth2(safeTerminalText(line), 100)),
|
|
256
|
+
"View details for the complete review."
|
|
257
|
+
];
|
|
258
|
+
while (isCurrent() && !signal?.aborted) {
|
|
259
|
+
const choice = { value: "cancel" };
|
|
260
|
+
const menu = defineMenu({
|
|
261
|
+
start: "confirm",
|
|
262
|
+
screens: {
|
|
263
|
+
confirm: () => ({
|
|
264
|
+
kind: "choice",
|
|
265
|
+
title,
|
|
266
|
+
lines,
|
|
267
|
+
initialItemId: "cancel",
|
|
268
|
+
items: [
|
|
269
|
+
{ id: "cancel", label: "No, cancel" },
|
|
270
|
+
{ id: "approve", label: `Yes, ${confirmationLabel}` },
|
|
271
|
+
{ id: "details", label: "View details" }
|
|
272
|
+
],
|
|
273
|
+
action: "choose",
|
|
274
|
+
hint: "close"
|
|
275
|
+
})
|
|
276
|
+
},
|
|
277
|
+
actions: {
|
|
278
|
+
choose: ({ itemId }) => {
|
|
279
|
+
if (itemId === "approve" || itemId === "details") choice.value = itemId;
|
|
280
|
+
return { kind: "close" };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const result = ctx.mode === "tui" && ctx.hasUI ? await runCustomInteraction(ctx, {
|
|
285
|
+
create: ({ tui, theme, keybindings, complete }) => createSyncConfirmation({ title, lines, confirmationLabel, tui, theme, keybindings, complete }),
|
|
286
|
+
signal,
|
|
287
|
+
isCurrent,
|
|
288
|
+
onError: () => {
|
|
289
|
+
}
|
|
290
|
+
}) : await runMenu(ctx, menu, { getState: () => void 0, signal, isCurrent, onError: () => {
|
|
291
|
+
} });
|
|
292
|
+
if (!isCurrent() || signal?.aborted) return false;
|
|
293
|
+
if (result.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
|
|
294
|
+
if (result.kind === "unsupported")
|
|
295
|
+
throw new Error("Merge review requires observable TUI or RPC; review the plan before using --yes.");
|
|
296
|
+
if (result.kind === "completed") choice.value = result.value;
|
|
297
|
+
else if (result.kind !== "closed") return false;
|
|
298
|
+
if (choice.value === "approve") return true;
|
|
299
|
+
if (choice.value !== "details") return false;
|
|
300
|
+
const detail = await runDocumentReview(ctx, {
|
|
301
|
+
title: "Sync change details",
|
|
302
|
+
content,
|
|
303
|
+
format: { kind: "text" },
|
|
304
|
+
viewportSize: "adaptive",
|
|
305
|
+
hint: "back",
|
|
306
|
+
signal,
|
|
307
|
+
isCurrent,
|
|
308
|
+
onError: () => {
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
if (!isCurrent() || signal?.aborted) return false;
|
|
312
|
+
if (detail.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
|
|
313
|
+
if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
|
|
152
314
|
}
|
|
153
|
-
|
|
154
|
-
return lines.join("\n");
|
|
315
|
+
return false;
|
|
155
316
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
317
|
+
|
|
318
|
+
// src/ui/merged-sync-summary.ts
|
|
319
|
+
import { truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
320
|
+
function mergedSyncSummary(input) {
|
|
321
|
+
const local = changes(input.localBefore, input.localAfter);
|
|
322
|
+
const remote = changes(input.remoteBefore, input.remoteAfter);
|
|
323
|
+
const all = [...local, ...remote];
|
|
324
|
+
const updatesOnly = all.length > 0 && all.every((change) => change.action === "Update");
|
|
325
|
+
const verb = updatesOnly ? "Update" : "Apply changes to";
|
|
326
|
+
const title = local.length && remote.length ? "Apply local and remote changes?" : all.length ? `${verb} ${all.length} ${local.length ? "local" : "remote"} ${all.length === 1 ? "file" : "files"}?` : input.publish ? "Publish a new remote snapshot?" : "Pi Sync is already up to date.";
|
|
327
|
+
const confirmationLabel = local.length && remote.length ? "apply local and remote changes" : all.length ? `${updatesOnly ? "update" : "apply changes to"} ${local.length ? "local" : "remote"} ${all.length === 1 ? "file" : "files"}` : "publish remote snapshot";
|
|
328
|
+
const sources = new Map(
|
|
329
|
+
input.decisions.filter((item) => item.kind === "accepted").map((item) => [item.path, item.source])
|
|
159
330
|
);
|
|
331
|
+
const label = (change, side) => {
|
|
332
|
+
const source = sources.get(change.path);
|
|
333
|
+
const suffix = source === "merged" ? " (merged content)" : side === "local" && source === "remote" ? change.action === "Delete" ? " (deleted remotely)" : " from remote" : "";
|
|
334
|
+
return `${change.action} ${snapshotPathLabel(change.path)}${suffix}`;
|
|
335
|
+
};
|
|
336
|
+
const section = (items, side, compact) => {
|
|
337
|
+
const heading = side === "local" ? "Local changes" : "Remote file changes";
|
|
338
|
+
if (!items.length) return [`${heading}: none`];
|
|
339
|
+
const counts = ["Add", "Update", "Delete"].flatMap((action) => {
|
|
340
|
+
const count = items.filter((item) => item.action === action).length;
|
|
341
|
+
return count ? [`${count} ${action.toLowerCase()}`] : [];
|
|
342
|
+
});
|
|
343
|
+
return [
|
|
344
|
+
`${heading}: ${counts.join(", ")}`,
|
|
345
|
+
...(compact ? items.slice(0, 3) : items).map(
|
|
346
|
+
(item) => ` ${compact ? truncateToWidth3(label(item, side), 80) : label(item, side)}`
|
|
347
|
+
),
|
|
348
|
+
...compact && items.length > 3 ? [` ${items.length - 3} more files \u2014 View details`] : []
|
|
349
|
+
];
|
|
350
|
+
};
|
|
351
|
+
const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
|
|
352
|
+
const safety = [
|
|
353
|
+
`Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
|
|
354
|
+
`Sessions: ${input.sessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
355
|
+
input.groups.length ? `Unresolved groups: ${input.groups.length} \u2014 these paths stay unchanged on each side` : "Conflicts: none",
|
|
356
|
+
...input.publish && input.capability === "read-check-write-verify" ? ["Warning: simultaneous remote writes can still race; visible changes are rejected."] : [],
|
|
357
|
+
...input.publish && input.capability === "conditional-required" ? ["Remote publication requires verified conditional writes; verification failure stops the transfer."] : [],
|
|
358
|
+
"The current session is protected. A backup and recovery journal will be saved.",
|
|
359
|
+
"Resources will not reload automatically."
|
|
360
|
+
];
|
|
361
|
+
return {
|
|
362
|
+
title,
|
|
363
|
+
confirmationLabel,
|
|
364
|
+
lines: [
|
|
365
|
+
...context.map((line) => truncateToWidth3(line, 100)),
|
|
366
|
+
...section(local, "local", true),
|
|
367
|
+
...section(remote, "remote", true),
|
|
368
|
+
...safety
|
|
369
|
+
],
|
|
370
|
+
content: [
|
|
371
|
+
...context,
|
|
372
|
+
"",
|
|
373
|
+
...section(local, "local", false),
|
|
374
|
+
"",
|
|
375
|
+
...section(remote, "remote", false),
|
|
376
|
+
"",
|
|
377
|
+
...safety,
|
|
378
|
+
...input.groups.flatMap(
|
|
379
|
+
(group) => group.paths.map((filePath) => `Unchanged (unresolved): ${snapshotPathLabel(filePath)}`)
|
|
380
|
+
)
|
|
381
|
+
].join("\n")
|
|
382
|
+
};
|
|
160
383
|
}
|
|
161
|
-
function
|
|
162
|
-
return [
|
|
163
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
164
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
165
|
-
`Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
|
|
166
|
-
`Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
167
|
-
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
168
|
-
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
169
|
-
...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
|
|
170
|
-
formatPublicationPreview(remote, upload),
|
|
171
|
-
preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
|
|
172
|
-
].join("\n");
|
|
173
|
-
}
|
|
174
|
-
function formatApplyPreview(local, remote) {
|
|
175
|
-
return formatDirectionalChanges(local, remote, {
|
|
176
|
-
add: "Add locally",
|
|
177
|
-
update: "Update locally",
|
|
178
|
-
remove: "Remove locally"
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
function formatPullSummary(config, destination, local, remote, protectedSessionCount) {
|
|
182
|
-
return [
|
|
183
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
184
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
185
|
-
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
186
|
-
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
187
|
-
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
188
|
-
formatApplyPreview(local, remote),
|
|
189
|
-
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
|
|
190
|
-
].join("\n");
|
|
191
|
-
}
|
|
192
|
-
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedSessionCount) {
|
|
193
|
-
return [
|
|
194
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
195
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
196
|
-
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
197
|
-
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
198
|
-
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
199
|
-
formatApplyPreview(local, remote),
|
|
200
|
-
"A local backup is created before applying; the backend's active head will change."
|
|
201
|
-
].join("\n");
|
|
202
|
-
}
|
|
203
|
-
function formatPublicationPreview(remote, upload) {
|
|
204
|
-
if (!remote) {
|
|
205
|
-
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
|
|
206
|
-
"\n"
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return formatDirectionalChanges(remote, upload, {
|
|
210
|
-
add: "Add remotely",
|
|
211
|
-
update: "Update remotely",
|
|
212
|
-
remove: "Remove remotely"
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
function formatDirectionalChanges(before, after, labels) {
|
|
384
|
+
function changes(before, after) {
|
|
216
385
|
const beforeMap = fileHashMap(before);
|
|
217
386
|
const afterMap = fileHashMap(after);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
|
|
224
|
-
}
|
|
225
|
-
if (lines.length === 0) lines.push("No file changes.");
|
|
226
|
-
return lines.join("\n");
|
|
227
|
-
}
|
|
228
|
-
function countPreservedRemoteFiles(local, upload) {
|
|
229
|
-
const localPaths = new Set(local.files.map((file) => file.path));
|
|
230
|
-
return upload.files.filter((file) => !localPaths.has(file.path)).length;
|
|
231
|
-
}
|
|
232
|
-
function publicationCapabilityDescription(capability) {
|
|
233
|
-
switch (capability) {
|
|
234
|
-
case "lease-protected":
|
|
235
|
-
return "lease-protected (exact expected-ref update)";
|
|
236
|
-
case "atomic-conditional":
|
|
237
|
-
return "atomic-conditional (verified atomic precondition)";
|
|
238
|
-
case "conditional-required":
|
|
239
|
-
return "conditional-required (read-only until atomic preconditions are verified)";
|
|
240
|
-
case "read-check-write-verify":
|
|
241
|
-
return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
|
|
242
|
-
}
|
|
387
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
|
|
388
|
+
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
389
|
+
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
390
|
+
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
391
|
+
});
|
|
243
392
|
}
|
|
244
393
|
|
|
245
394
|
// src/sync/conflict-artifact-ownership.ts
|
|
@@ -1197,6 +1346,117 @@ async function resolveSettingsConflicts(config, state, local, remote, plan) {
|
|
|
1197
1346
|
};
|
|
1198
1347
|
}
|
|
1199
1348
|
|
|
1349
|
+
// src/ui/sync-format.ts
|
|
1350
|
+
function formatDiff(local, remote) {
|
|
1351
|
+
const localMap = fileHashMap(local);
|
|
1352
|
+
const remoteMap = fileHashMap(remote);
|
|
1353
|
+
const allPaths = [.../* @__PURE__ */ new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
1354
|
+
const lines = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
|
|
1355
|
+
let changed = 0;
|
|
1356
|
+
for (const filePath of allPaths) {
|
|
1357
|
+
if (!Object.hasOwn(localMap, filePath)) {
|
|
1358
|
+
lines.push(`Remote only: ${snapshotPathLabel(filePath)}`);
|
|
1359
|
+
changed += 1;
|
|
1360
|
+
} else if (!Object.hasOwn(remoteMap, filePath)) {
|
|
1361
|
+
lines.push(`Local only: ${snapshotPathLabel(filePath)}`);
|
|
1362
|
+
changed += 1;
|
|
1363
|
+
} else if (localMap[filePath] !== remoteMap[filePath]) {
|
|
1364
|
+
lines.push(`Different: ${snapshotPathLabel(filePath)}`);
|
|
1365
|
+
changed += 1;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (changed === 0) lines.push("No file differences.");
|
|
1369
|
+
return lines.join("\n");
|
|
1370
|
+
}
|
|
1371
|
+
function formatSnapshotOnlyDiff(title, snapshot) {
|
|
1372
|
+
return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)].join(
|
|
1373
|
+
"\n"
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
function formatPushSummary(config, destination, upload, head, preservedRemoteFileCount = 0, remote) {
|
|
1377
|
+
return [
|
|
1378
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1379
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
1380
|
+
`Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
|
|
1381
|
+
`Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1382
|
+
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
1383
|
+
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
1384
|
+
...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
|
|
1385
|
+
formatPublicationPreview(remote, upload),
|
|
1386
|
+
preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
|
|
1387
|
+
].join("\n");
|
|
1388
|
+
}
|
|
1389
|
+
function formatApplyPreview(local, remote) {
|
|
1390
|
+
return formatDirectionalChanges(local, remote, {
|
|
1391
|
+
add: "Add locally",
|
|
1392
|
+
update: "Update locally",
|
|
1393
|
+
remove: "Remove locally"
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function formatPullSummary(config, destination, local, remote, protectedSessionCount) {
|
|
1397
|
+
return [
|
|
1398
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1399
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
1400
|
+
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
1401
|
+
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1402
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
1403
|
+
formatApplyPreview(local, remote),
|
|
1404
|
+
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
|
|
1405
|
+
].join("\n");
|
|
1406
|
+
}
|
|
1407
|
+
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedSessionCount) {
|
|
1408
|
+
return [
|
|
1409
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1410
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
1411
|
+
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
1412
|
+
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1413
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
1414
|
+
formatApplyPreview(local, remote),
|
|
1415
|
+
"A local backup is created before applying; the backend's active head will change."
|
|
1416
|
+
].join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
function formatPublicationPreview(remote, upload) {
|
|
1419
|
+
if (!remote) {
|
|
1420
|
+
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
|
|
1421
|
+
"\n"
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return formatDirectionalChanges(remote, upload, {
|
|
1425
|
+
add: "Add remotely",
|
|
1426
|
+
update: "Update remotely",
|
|
1427
|
+
remove: "Remove remotely"
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
function formatDirectionalChanges(before, after, labels) {
|
|
1431
|
+
const beforeMap = fileHashMap(before);
|
|
1432
|
+
const afterMap = fileHashMap(after);
|
|
1433
|
+
const paths = [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
|
|
1434
|
+
const lines = [];
|
|
1435
|
+
for (const filePath of paths) {
|
|
1436
|
+
if (!Object.hasOwn(beforeMap, filePath)) lines.push(`${labels.add}: ${snapshotPathLabel(filePath)}`);
|
|
1437
|
+
else if (!Object.hasOwn(afterMap, filePath)) lines.push(`${labels.remove}: ${snapshotPathLabel(filePath)}`);
|
|
1438
|
+
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
|
|
1439
|
+
}
|
|
1440
|
+
if (lines.length === 0) lines.push("No file changes.");
|
|
1441
|
+
return lines.join("\n");
|
|
1442
|
+
}
|
|
1443
|
+
function countPreservedRemoteFiles(local, upload) {
|
|
1444
|
+
const localPaths = new Set(local.files.map((file) => file.path));
|
|
1445
|
+
return upload.files.filter((file) => !localPaths.has(file.path)).length;
|
|
1446
|
+
}
|
|
1447
|
+
function publicationCapabilityDescription(capability) {
|
|
1448
|
+
switch (capability) {
|
|
1449
|
+
case "lease-protected":
|
|
1450
|
+
return "lease-protected (exact expected-ref update)";
|
|
1451
|
+
case "atomic-conditional":
|
|
1452
|
+
return "atomic-conditional (verified atomic precondition)";
|
|
1453
|
+
case "conditional-required":
|
|
1454
|
+
return "conditional-required (read-only until atomic preconditions are verified)";
|
|
1455
|
+
case "read-check-write-verify":
|
|
1456
|
+
return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1200
1460
|
// src/sync/sync-decision.ts
|
|
1201
1461
|
function createSyncDecision(options) {
|
|
1202
1462
|
const { config, state, local, remote, kind } = options;
|
|
@@ -1536,26 +1796,31 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1536
1796
|
await validate();
|
|
1537
1797
|
if (!config.skipSecretScan && scanSnapshot(upload).length)
|
|
1538
1798
|
throw new Error("Refusing to merge possible secrets. Review managed content before syncing.");
|
|
1539
|
-
if (!options.yes
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1799
|
+
if (!options.yes) {
|
|
1800
|
+
const summary = mergedSyncSummary({
|
|
1801
|
+
setupName: config.setupName,
|
|
1802
|
+
destination: backend.destination,
|
|
1803
|
+
localBefore: localRaw,
|
|
1804
|
+
localAfter: after,
|
|
1805
|
+
remoteBefore: rawRemote,
|
|
1806
|
+
remoteAfter: upload,
|
|
1807
|
+
publish,
|
|
1808
|
+
sessions: config.include.includes("sessions"),
|
|
1809
|
+
groups,
|
|
1810
|
+
decisions: plan.decisions,
|
|
1811
|
+
capability: backend.capability
|
|
1812
|
+
});
|
|
1813
|
+
if (!await confirmMergeReview(
|
|
1814
|
+
ctx,
|
|
1815
|
+
summary.title,
|
|
1816
|
+
summary.content,
|
|
1817
|
+
options.signal,
|
|
1818
|
+
isCurrent,
|
|
1819
|
+
summary.confirmationLabel,
|
|
1820
|
+
summary.lines
|
|
1821
|
+
))
|
|
1822
|
+
return "cancelled";
|
|
1823
|
+
}
|
|
1559
1824
|
await validate();
|
|
1560
1825
|
const refreshed = await createSnapshot(config.snapshotIdentity, snapshotOptions);
|
|
1561
1826
|
if (!sameHashes(fileHashMap(refreshed), fileHashMap(localRaw)))
|
|
@@ -1791,6 +2056,7 @@ async function writeAcceptedState(config, head, snapshot, validate, progress) {
|
|
|
1791
2056
|
|
|
1792
2057
|
export {
|
|
1793
2058
|
confirmMergeReview,
|
|
2059
|
+
applySnapshot,
|
|
1794
2060
|
formatDiff,
|
|
1795
2061
|
formatSnapshotOnlyDiff,
|
|
1796
2062
|
formatPushSummary,
|
|
@@ -1798,8 +2064,7 @@ export {
|
|
|
1798
2064
|
formatRollbackSummary,
|
|
1799
2065
|
countPreservedRemoteFiles,
|
|
1800
2066
|
publicationCapabilityDescription,
|
|
1801
|
-
applySnapshot,
|
|
1802
2067
|
createSyncDecision,
|
|
1803
2068
|
mergeSync
|
|
1804
2069
|
};
|
|
1805
|
-
//# sourceMappingURL=chunk-
|
|
2070
|
+
//# sourceMappingURL=chunk-MYPSMJHJ.ts.map
|