@narumitw/pi-sync 0.49.10 → 0.49.11

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.
Files changed (60) hide show
  1. package/README.md +10 -1
  2. package/dist/chunks/chunk-3GRWMWUZ.ts +213 -0
  3. package/dist/chunks/chunk-3GRWMWUZ.ts.map +7 -0
  4. package/dist/chunks/chunk-3NDD5QPM.ts +203 -0
  5. package/dist/chunks/chunk-3NDD5QPM.ts.map +7 -0
  6. package/dist/chunks/chunk-4F3W4CSC.ts +195 -0
  7. package/dist/chunks/chunk-4F3W4CSC.ts.map +7 -0
  8. package/dist/chunks/chunk-4U6AFLJB.ts +221 -0
  9. package/dist/chunks/chunk-4U6AFLJB.ts.map +7 -0
  10. package/dist/chunks/chunk-5MQ76BK5.ts +34 -0
  11. package/dist/chunks/chunk-5MQ76BK5.ts.map +7 -0
  12. package/dist/chunks/chunk-APZVBSM6.ts +1238 -0
  13. package/dist/chunks/chunk-APZVBSM6.ts.map +7 -0
  14. package/dist/chunks/chunk-BHJUISB4.ts +134 -0
  15. package/dist/chunks/chunk-BHJUISB4.ts.map +7 -0
  16. package/dist/chunks/chunk-D3S54OXI.ts +653 -0
  17. package/dist/chunks/chunk-D3S54OXI.ts.map +7 -0
  18. package/dist/chunks/chunk-EWX3TPLQ.ts +356 -0
  19. package/dist/chunks/chunk-EWX3TPLQ.ts.map +7 -0
  20. package/dist/chunks/chunk-F5QL2GPY.ts +341 -0
  21. package/dist/chunks/chunk-F5QL2GPY.ts.map +7 -0
  22. package/dist/chunks/chunk-FR6CVXAL.ts +81 -0
  23. package/dist/chunks/chunk-FR6CVXAL.ts.map +7 -0
  24. package/dist/chunks/chunk-HGV24P4T.ts +53 -0
  25. package/dist/chunks/chunk-HGV24P4T.ts.map +7 -0
  26. package/dist/chunks/chunk-JNOOSPEW.ts +82 -0
  27. package/dist/chunks/chunk-JNOOSPEW.ts.map +7 -0
  28. package/dist/chunks/chunk-LJDSHNS3.ts +259 -0
  29. package/dist/chunks/chunk-LJDSHNS3.ts.map +7 -0
  30. package/dist/chunks/chunk-RGJ6SOYR.ts +128 -0
  31. package/dist/chunks/chunk-RGJ6SOYR.ts.map +7 -0
  32. package/dist/chunks/chunk-W64NEHXT.ts +167 -0
  33. package/dist/chunks/chunk-W64NEHXT.ts.map +7 -0
  34. package/dist/chunks/chunk-YQ6UW7IF.ts +394 -0
  35. package/dist/chunks/chunk-YQ6UW7IF.ts.map +7 -0
  36. package/dist/chunks/file-selection-4UABOP44.ts +290 -0
  37. package/dist/chunks/file-selection-4UABOP44.ts.map +7 -0
  38. package/dist/chunks/git-backend-E3UOUUOM.ts +1104 -0
  39. package/dist/chunks/git-backend-E3UOUUOM.ts.map +7 -0
  40. package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts +18 -0
  41. package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts.map +7 -0
  42. package/dist/chunks/manager-ui-7E25VWUP.ts +2754 -0
  43. package/dist/chunks/manager-ui-7E25VWUP.ts.map +7 -0
  44. package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts +17 -0
  45. package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts.map +7 -0
  46. package/dist/chunks/s3-backend-3QZGO2EF.ts +680 -0
  47. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +7 -0
  48. package/dist/chunks/setup-switch-X3W3FALZ.ts +27 -0
  49. package/dist/chunks/setup-switch-X3W3FALZ.ts.map +7 -0
  50. package/dist/chunks/snapshot-WBSV4MQG.ts +45 -0
  51. package/dist/chunks/snapshot-WBSV4MQG.ts.map +7 -0
  52. package/dist/chunks/sync-operations-MPAFVVHA.ts +1112 -0
  53. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +7 -0
  54. package/dist/chunks/sync-state-36H7ULG2.ts +41 -0
  55. package/dist/chunks/sync-state-36H7ULG2.ts.map +7 -0
  56. package/dist/chunks/webdav-backend-ZD5RHRIU.ts +869 -0
  57. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +7 -0
  58. package/dist/index.ts +668 -0
  59. package/dist/index.ts.map +7 -0
  60. package/package.json +10 -6
@@ -0,0 +1,653 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ SyncSetupReviewChangedError,
5
+ updateSyncSetup
6
+ } from "./chunk-4F3W4CSC.js";
7
+ import {
8
+ createSyncBackend,
9
+ readSnapshotForHead
10
+ } from "./chunk-FR6CVXAL.js";
11
+ import {
12
+ errorMessage,
13
+ safeTerminalText
14
+ } from "./chunk-W64NEHXT.js";
15
+ import {
16
+ loadConfig,
17
+ loadPartialConfig,
18
+ syncConfigReviewFingerprint
19
+ } from "./chunk-APZVBSM6.js";
20
+ import {
21
+ compareSyncInclude,
22
+ inspectRemoteSelection,
23
+ sameSyncInclude
24
+ } from "./chunk-YQ6UW7IF.js";
25
+
26
+ // src/remote-selection-ui.ts
27
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
28
+
29
+ // src/cancellable-operation.ts
30
+ import {
31
+ BorderedLoader
32
+ } from "@earendil-works/pi-coding-agent";
33
+ import { truncateToWidth } from "@earendil-works/pi-tui";
34
+ import { runCustomInteraction } from "@narumitw/pi-tui-kit";
35
+
36
+ // src/manager-helpers.ts
37
+ async function requiredExistingBucket(ctx, example, signal) {
38
+ const value = await ctx.ui.input("Existing bucket", `Example: ${example}`, { signal });
39
+ if (signal?.aborted) {
40
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
41
+ }
42
+ if (value === void 0) return void 0;
43
+ const normalized = value.trim();
44
+ if (!normalized) {
45
+ ctx.ui.notify("Enter the name of an existing R2/S3 bucket, or cancel setup.", "warning");
46
+ return void 0;
47
+ }
48
+ return normalized;
49
+ }
50
+ async function requiredInput(ctx, title, placeholder, signal) {
51
+ const value = await ctx.ui.input(title, placeholder, { signal });
52
+ if (signal?.aborted) {
53
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
54
+ }
55
+ if (value === void 0) return void 0;
56
+ const normalized = value.trim() || placeholder;
57
+ return normalized.includes("<") || normalized.includes(">") ? void 0 : normalized;
58
+ }
59
+ function ownRecord(value) {
60
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
61
+ }
62
+ function safeTerminalText2(value) {
63
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
64
+ }
65
+ function errorMessage2(error) {
66
+ return error instanceof Error ? error.message : String(error);
67
+ }
68
+
69
+ // src/cancellable-operation.ts
70
+ async function runCancellableOperation(ctx, message, route, runRoute, options = {}) {
71
+ const {
72
+ commitAware = false,
73
+ cancelledMessage = "Check cancelled; no settings or files were changed.",
74
+ target,
75
+ signal
76
+ } = options;
77
+ if (ctx.mode !== "tui") {
78
+ return await runRoute(route, signal, void 0, target) ?? { kind: "failed" };
79
+ }
80
+ let commitStarted = false;
81
+ let routeResult;
82
+ const interaction = await runCustomInteraction(ctx, {
83
+ signal,
84
+ isCurrent: () => !signal?.aborted,
85
+ create: ({ tui, theme, keybindings, signal: interactionSignal, complete }) => {
86
+ const loader = new BorderedLoader(tui, theme, message, { cancellable: false });
87
+ const cancelHint = `${keybindingText(keybindings, "tui.select.cancel", "esc")} cancel`;
88
+ const operation = runRoute(
89
+ route,
90
+ interactionSignal,
91
+ commitAware ? () => commitStarted = true : void 0,
92
+ target
93
+ ).then(
94
+ (result) => {
95
+ routeResult = result;
96
+ complete({});
97
+ },
98
+ (error) => complete({ error })
99
+ );
100
+ return {
101
+ render(width) {
102
+ const safeWidth = Math.max(1, width);
103
+ const lines = loader.render(safeWidth);
104
+ const bottomBorder = lines.at(-1);
105
+ return [
106
+ ...lines.slice(0, -1),
107
+ truncateToWidth(theme.fg("dim", cancelHint), safeWidth, ""),
108
+ ...bottomBorder === void 0 ? [] : [bottomBorder]
109
+ ];
110
+ },
111
+ invalidate: () => loader.invalidate(),
112
+ handleInput(data) {
113
+ if (!keybindings.matches(data, "tui.select.cancel")) return;
114
+ if (commitStarted) {
115
+ ctx.ui.notify(
116
+ "Applying or publishing has started and cannot be cancelled safely.",
117
+ "warning"
118
+ );
119
+ return;
120
+ }
121
+ complete({ cancelled: true });
122
+ },
123
+ dispose: () => loader.dispose(),
124
+ waitForPending: () => operation
125
+ };
126
+ }
127
+ });
128
+ if (interaction.kind === "error") throw interaction.error;
129
+ if (interaction.kind !== "completed") return { kind: "closed" };
130
+ if (interaction.value.cancelled) {
131
+ if (cancelledMessage) ctx.ui.notify(cancelledMessage, "info");
132
+ return { kind: "cancelled" };
133
+ }
134
+ if (interaction.value.error) throw interaction.value.error;
135
+ return routeResult ?? { kind: "failed" };
136
+ }
137
+ function keybindingText(keybindings, binding, fallback) {
138
+ const keys = keybindings.getKeys(binding).map(String).map((key) => {
139
+ if (key === "return") return "enter";
140
+ if (key === "escape") return "esc";
141
+ return safeTerminalText2(key);
142
+ }).filter(Boolean);
143
+ return keys.join("/") || fallback;
144
+ }
145
+
146
+ // src/remote-selection-ui.ts
147
+ var STATUS_KEY = "sync";
148
+ async function showRemoteSelectionReview(ctx, setupName, signal, factory = createSyncBackend, options = {}) {
149
+ try {
150
+ let decision = options.decision;
151
+ if (!decision) {
152
+ const inspected = await inspectConfiguredRemoteSelection(ctx, setupName, signal, factory);
153
+ if (!inspected || signal?.aborted) return { kind: "stale" };
154
+ if (inspected.kind === "empty") {
155
+ ctx.ui.notify("Remote storage has no snapshot or synced-content list yet.", "info");
156
+ return { kind: "back" };
157
+ }
158
+ if (inspected.state.kind === "same") {
159
+ ctx.ui.notify("Remote synced content already matches this sync setup.", "info");
160
+ return { kind: "back" };
161
+ }
162
+ if (inspected.state.kind === "legacy") {
163
+ if (ctx.mode !== "tui") {
164
+ ctx.ui.notify(formatLegacySummary(inspected.config, inspected.state.discovered), "info");
165
+ return { kind: "back" };
166
+ }
167
+ await showLegacyDiscovery(ctx, inspected.config, inspected.state.discovered, signal);
168
+ return signal?.aborted ? { kind: "stale" } : { kind: "back" };
169
+ }
170
+ decision = decisionFromState(inspected.config, inspected.state);
171
+ }
172
+ if (ctx.mode !== "tui") {
173
+ ctx.ui.notify(formatRemoteSelectionSummary(decision), "warning");
174
+ return { kind: "back" };
175
+ }
176
+ let currentDecision = decision;
177
+ for (; ; ) {
178
+ if (signal?.aborted) return { kind: "stale" };
179
+ const result = await showSelectionDifference(
180
+ ctx,
181
+ currentDecision,
182
+ options.origin ?? "settings",
183
+ options.runRoute,
184
+ signal,
185
+ factory,
186
+ options
187
+ );
188
+ if (result.kind !== "refresh") return result;
189
+ const refreshed = await runWithOptionalStateAccess(
190
+ options,
191
+ () => inspectConfiguredRemoteSelection(ctx, currentDecision.setupName, signal, factory)
192
+ );
193
+ if (!refreshed || signal?.aborted) return { kind: "stale" };
194
+ if (refreshed.kind === "empty") {
195
+ ctx.ui.notify("Remote storage no longer has a snapshot or synced-content list.", "warning");
196
+ return { kind: "back" };
197
+ }
198
+ if (refreshed.state.kind !== "different") {
199
+ ctx.ui.notify(
200
+ refreshed.state.kind === "same" ? "Remote synced content now matches this sync setup." : "The refreshed legacy snapshot has no authoritative synced-content list.",
201
+ "info"
202
+ );
203
+ options.onSelectionResolved?.();
204
+ return { kind: "done" };
205
+ }
206
+ currentDecision = decisionFromState(refreshed.config, refreshed.state);
207
+ }
208
+ } catch (error) {
209
+ if (signal?.aborted) return { kind: "stale" };
210
+ ctx.ui.notify(`Could not review synced content: ${errorMessage(error)}`, "error");
211
+ return { kind: "back" };
212
+ } finally {
213
+ ctx.ui.setStatus(STATUS_KEY, void 0);
214
+ }
215
+ }
216
+ async function showSelectionDifference(ctx, initialDecision, origin, runRoute, sessionSignal, factory, options) {
217
+ let flowState = { decision: initialDecision, saved: false };
218
+ let continuationReview;
219
+ let nextResult;
220
+ let refreshRequested = false;
221
+ const route = origin === "settings" ? "sync" : origin;
222
+ const menu = defineMenu({
223
+ start: "choice",
224
+ screens: {
225
+ choice: ({ state }) => ({
226
+ kind: "actions",
227
+ title: "Synced content differs",
228
+ lines: selectionSummaryLines(state.decision),
229
+ items: [
230
+ {
231
+ id: "review",
232
+ label: "Review all paths (recommended)",
233
+ description: "Compare exact remote-only, device-only, and ordered lists.",
234
+ to: "review"
235
+ },
236
+ {
237
+ id: "adopt",
238
+ label: "Use remote content list",
239
+ description: "Save the reviewed list on this device without pulling files.",
240
+ action: "adopt"
241
+ },
242
+ {
243
+ id: "keep",
244
+ label: "Keep this device's content list and update remote\u2026",
245
+ description: "Open the existing exact force-push preview without skipping confirmation.",
246
+ action: "keep"
247
+ },
248
+ { id: "cancel", label: options.cancelLabel ?? "Cancel", action: "cancel" }
249
+ ],
250
+ hint: "back"
251
+ }),
252
+ review: ({ state }) => ({
253
+ kind: "review",
254
+ title: `Review synced content \xB7 ${safeTerminalText(state.decision.setupName)}`,
255
+ content: formatSelectionDifference(state.decision),
256
+ format: { kind: "text" },
257
+ viewportSize: "adaptive",
258
+ hint: "back"
259
+ }),
260
+ saved: ({ state }) => ({
261
+ kind: "actions",
262
+ title: "Remote content list saved",
263
+ lines: [
264
+ `Sync setup: ${safeTerminalText(state.decision.setupName)}`,
265
+ "Only the included-content setting was saved.",
266
+ "No files were pulled and sync state was not changed."
267
+ ],
268
+ items: [
269
+ ...runRoute ? [
270
+ {
271
+ id: "continue",
272
+ label: continueLabel(origin),
273
+ description: "Start a fresh check and exact preview for this sync setup.",
274
+ action: "continue"
275
+ }
276
+ ] : [],
277
+ { id: "done", label: "Done", action: "done" }
278
+ ],
279
+ hint: "close"
280
+ })
281
+ },
282
+ actions: {
283
+ adopt: async ({ state, signal: actionSignal }) => {
284
+ const signal = combineSignals(sessionSignal, actionSignal);
285
+ try {
286
+ const currentConfig = await loadConfig(state.decision.setupName);
287
+ if (signal.aborted) return { kind: "close" };
288
+ assertLocalSelectionCurrent(currentConfig, state.decision);
289
+ if (!currentConfig.include.includes("sessions") && state.decision.remoteInclude.includes("sessions")) {
290
+ const acknowledged = await ctx.ui.confirm(
291
+ "Use a content list that includes session conversations?",
292
+ "Session JSONL may contain prompts, tool output, file paths, images, and secrets. This saves the list only; it does not pull files.",
293
+ { signal }
294
+ );
295
+ if (signal.aborted) return { kind: "close" };
296
+ if (!acknowledged) return { kind: "stay" };
297
+ }
298
+ const review = await runWithOptionalStateAccess(options, async () => {
299
+ const loaded = await loadAdoptionReview(state.decision, signal, factory);
300
+ if (signal.aborted) throw signal.reason;
301
+ await revalidateAndAdopt(loaded, state.decision, signal);
302
+ return loaded;
303
+ });
304
+ if (signal.aborted) return { kind: "close" };
305
+ options.onSelectionResolved?.();
306
+ continuationReview = {
307
+ ...review.storageReview,
308
+ setupName: state.decision.setupName,
309
+ include: [...state.decision.remoteInclude],
310
+ automatic: review.config.automatic,
311
+ onSwitch: review.config.onSwitch
312
+ };
313
+ flowState = { decision: state.decision, saved: true };
314
+ return { kind: "to", screen: "saved" };
315
+ } catch (error) {
316
+ if (signal.aborted) return { kind: "close" };
317
+ if (isStaleReviewError(error)) {
318
+ ctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, "warning");
319
+ refreshRequested = true;
320
+ return { kind: "close" };
321
+ }
322
+ ctx.ui.notify(`Could not save the remote content list: ${errorMessage(error)}`, "error");
323
+ return { kind: "stay" };
324
+ }
325
+ },
326
+ keep: async ({ state, signal: actionSignal }) => {
327
+ if (!runRoute) {
328
+ ctx.ui.notify("The reviewed update-remote route is unavailable.", "error");
329
+ return { kind: "stay" };
330
+ }
331
+ const signal = combineSignals(sessionSignal, actionSignal);
332
+ try {
333
+ const latest = await loadConfig(state.decision.setupName);
334
+ if (signal.aborted) return { kind: "close" };
335
+ assertLocalSelectionCurrent(latest, state.decision);
336
+ const result = await runCancellableOperation(
337
+ ctx,
338
+ "Preparing this device's push preview\u2026",
339
+ "push --force",
340
+ runRoute,
341
+ {
342
+ commitAware: true,
343
+ cancelledMessage: "Push preparation cancelled; no remote files were changed.",
344
+ target: state.decision.setupName,
345
+ signal
346
+ }
347
+ );
348
+ return handleNestedRouteResult(result, "push");
349
+ } catch (error) {
350
+ if (signal.aborted) return { kind: "close" };
351
+ if (isStaleReviewError(error)) {
352
+ ctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, "warning");
353
+ refreshRequested = true;
354
+ return { kind: "close" };
355
+ }
356
+ ctx.ui.notify(`Could not prepare the remote update: ${errorMessage(error)}`, "error");
357
+ return { kind: "stay" };
358
+ }
359
+ },
360
+ continue: async ({ state, signal: actionSignal }) => {
361
+ if (!runRoute || !continuationReview) return { kind: "stay" };
362
+ const signal = combineSignals(sessionSignal, actionSignal);
363
+ try {
364
+ const latest = await loadPartialConfig(state.decision.setupName);
365
+ if (signal.aborted) return { kind: "close" };
366
+ if (!sameContinuationReview(latest, continuationReview)) {
367
+ throw new StaleRemoteSelectionReviewError(
368
+ `Sync setup \u201C${safeTerminalText(state.decision.setupName)}\u201D changed after the remote content list was saved.`
369
+ );
370
+ }
371
+ const result = await runCancellableOperation(
372
+ ctx,
373
+ continueBusyLabel(origin),
374
+ route,
375
+ runRoute,
376
+ {
377
+ commitAware: true,
378
+ cancelledMessage: continuationCancelledMessage(route),
379
+ target: state.decision.setupName,
380
+ signal
381
+ }
382
+ );
383
+ return handleNestedRouteResult(result, route);
384
+ } catch (error) {
385
+ if (signal.aborted) return { kind: "close" };
386
+ ctx.ui.notify(`Could not continue: ${errorMessage(error)}`, "error");
387
+ return { kind: "stay" };
388
+ }
389
+ },
390
+ cancel: async () => ({ kind: "back" }),
391
+ done: async () => {
392
+ nextResult = { kind: "done" };
393
+ return { kind: "close" };
394
+ }
395
+ }
396
+ });
397
+ function handleNestedRouteResult(result, nestedRoute) {
398
+ if (result.kind === "completed" && result.outcome === "applied") {
399
+ options.onSelectionResolved?.();
400
+ }
401
+ if (result.kind === "closed") {
402
+ nextResult = { kind: "closed" };
403
+ return { kind: "close" };
404
+ }
405
+ if (result.kind === "cancelled" || result.kind === "completed" && result.outcome === "cancelled" || result.kind === "failed") {
406
+ return { kind: "stay" };
407
+ }
408
+ nextResult = { kind: "route-result", result, route: nestedRoute };
409
+ return { kind: "close" };
410
+ }
411
+ const menuResult = await runMenu(ctx, menu, {
412
+ getState: () => flowState,
413
+ signal: sessionSignal,
414
+ isCurrent: () => !sessionSignal?.aborted,
415
+ onError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), "error")
416
+ });
417
+ if (sessionSignal?.aborted || menuResult.kind === "stale") return { kind: "stale" };
418
+ if (refreshRequested) return { kind: "refresh" };
419
+ if (nextResult) return nextResult;
420
+ if (menuResult.kind === "closed") {
421
+ return menuResult.reason === "back" ? { kind: "back" } : { kind: "closed" };
422
+ }
423
+ return { kind: "closed" };
424
+ }
425
+ async function loadAdoptionReview(decision, signal, factory) {
426
+ const config = await loadConfig(decision.setupName);
427
+ throwIfAborted(signal);
428
+ assertLocalSelectionCurrent(config, decision);
429
+ const partial = await loadPartialConfig(decision.setupName);
430
+ throwIfAborted(signal);
431
+ if (!sameSyncInclude(partial.include, decision.localInclude)) {
432
+ throw new StaleRemoteSelectionReviewError(
433
+ `Sync setup \u201C${safeTerminalText(decision.setupName)}\u201D changed while the comparison was open.`
434
+ );
435
+ }
436
+ const backend = await factory(config);
437
+ throwIfAborted(signal);
438
+ const reviewedHead = await backend.readHead(signal);
439
+ throwIfAborted(signal);
440
+ if (!reviewedHead) {
441
+ throw new StaleRemoteSelectionReviewError(
442
+ "Remote storage changed while the comparison was open."
443
+ );
444
+ }
445
+ const snapshot = await readSnapshotForHead(backend, reviewedHead, signal);
446
+ throwIfAborted(signal);
447
+ const state = inspectRemoteSelection(config.include, snapshot);
448
+ if (state.kind !== "different" || !sameSyncInclude(state.include, decision.remoteInclude)) {
449
+ throw new StaleRemoteSelectionReviewError(
450
+ "Remote synced content changed while the comparison was open."
451
+ );
452
+ }
453
+ return { config, storageReview: partial, backend, reviewedHead };
454
+ }
455
+ async function revalidateAndAdopt(review, decision, signal) {
456
+ const currentHead = await review.backend.readHead(signal);
457
+ throwIfAborted(signal);
458
+ if (!currentHead || !review.backend.sameRevision(review.reviewedHead.revision, currentHead.revision)) {
459
+ throw new StaleRemoteSelectionReviewError(
460
+ "Remote storage changed while the comparison was open."
461
+ );
462
+ }
463
+ const currentSnapshot = await readSnapshotForHead(review.backend, currentHead, signal);
464
+ throwIfAborted(signal);
465
+ const currentState = inspectRemoteSelection(review.config.include, currentSnapshot);
466
+ if (currentState.kind !== "different" || !sameSyncInclude(currentState.include, decision.remoteInclude)) {
467
+ throw new StaleRemoteSelectionReviewError(
468
+ "Remote synced content changed while the comparison was open."
469
+ );
470
+ }
471
+ await updateSyncSetup(
472
+ decision.setupName,
473
+ (setup) => ({
474
+ ...setup,
475
+ sync: { ...setup.sync, include: [...decision.remoteInclude] }
476
+ }),
477
+ {
478
+ expectedStorage: review.storageReview,
479
+ expectedInclude: decision.localInclude,
480
+ signal
481
+ }
482
+ );
483
+ }
484
+ async function inspectConfiguredRemoteSelection(ctx, setupName, signal, factory) {
485
+ const config = await loadConfig(setupName);
486
+ if (signal?.aborted) return void 0;
487
+ ctx.ui.setStatus(STATUS_KEY, `checking synced content for ${safeTerminalText(config.setupName)}`);
488
+ const backend = await factory(config);
489
+ if (signal?.aborted) return void 0;
490
+ const head = await backend.readHead(signal);
491
+ if (signal?.aborted) return void 0;
492
+ if (!head) return { kind: "empty", config };
493
+ const snapshot = await readSnapshotForHead(backend, head, signal);
494
+ if (signal?.aborted) return void 0;
495
+ return {
496
+ kind: "selection",
497
+ config,
498
+ state: inspectRemoteSelection(config.include, snapshot)
499
+ };
500
+ }
501
+ async function showLegacyDiscovery(ctx, config, discovered, signal) {
502
+ const menu = defineMenu({
503
+ start: "choice",
504
+ screens: {
505
+ choice: () => ({
506
+ kind: "actions",
507
+ title: `Compare synced content \xB7 ${safeTerminalText(config.setupName)}`,
508
+ lines: [
509
+ "This legacy snapshot has no portable synced-content list.",
510
+ "Discovered paths are partial and read-only; preserved files may not have been selected."
511
+ ],
512
+ items: [
513
+ { id: "review", label: "Review discovered paths", to: "review" },
514
+ { id: "back", label: "Back", action: "back" }
515
+ ],
516
+ hint: "close"
517
+ }),
518
+ review: () => ({
519
+ kind: "review",
520
+ title: "Partial discovery from legacy snapshot",
521
+ content: [
522
+ "Partial discovery only \u2014 not an authoritative selection.",
523
+ "",
524
+ ...discovered.length > 0 ? discovered.map((item) => `Discovered: ${safeTerminalText(item)}`) : ["No safe paths were discovered."],
525
+ "",
526
+ "Use Add custom path\u2026 in the local Included Content editor if needed."
527
+ ].join("\n"),
528
+ format: { kind: "text" },
529
+ viewportSize: "adaptive",
530
+ hint: "back"
531
+ })
532
+ },
533
+ actions: { back: async () => ({ kind: "close" }) }
534
+ });
535
+ await runMenu(ctx, menu, {
536
+ getState: () => void 0,
537
+ signal,
538
+ isCurrent: () => !signal?.aborted
539
+ });
540
+ }
541
+ function selectionSummaryLines(decision) {
542
+ const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
543
+ return [
544
+ `Sync setup: ${safeTerminalText(decision.setupName)}`,
545
+ "Nothing changed. Review both lists before choosing what happens next.",
546
+ ...comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0 ? ["Only the ordering differs; membership is the same."] : [
547
+ `Remote-only paths: ${comparison.remoteOnly.length} \xB7 Device-only paths: ${comparison.localOnly.length}`
548
+ ]
549
+ ];
550
+ }
551
+ function formatSelectionDifference(decision) {
552
+ const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
553
+ return [
554
+ ...comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0 ? ["Only ordering differs; both lists contain the same paths.", ""] : [],
555
+ "Remote-only paths:",
556
+ ...comparison.remoteOnly.length > 0 ? comparison.remoteOnly.map((item) => `+ ${safeTerminalText(item)}`) : ["(none)"],
557
+ "",
558
+ "Device-only paths:",
559
+ ...comparison.localOnly.length > 0 ? comparison.localOnly.map((item) => `- ${safeTerminalText(item)}`) : ["(none)"],
560
+ "",
561
+ "Remote ordered list:",
562
+ ...decision.remoteInclude.length > 0 ? decision.remoteInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`) : ["(none)"],
563
+ "",
564
+ "This device's ordered list:",
565
+ ...decision.localInclude.length > 0 ? decision.localInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`) : ["(none)"],
566
+ "",
567
+ "Using the remote list saves settings only and does not pull files."
568
+ ].join("\n");
569
+ }
570
+ function formatRemoteSelectionSummary(decision) {
571
+ const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
572
+ return [
573
+ `Synced content for \u201C${safeTerminalText(decision.setupName)}\u201D differs from this device.`,
574
+ `Remote-only: ${safeList(comparison.remoteOnly)}`,
575
+ `Device-only: ${safeList(comparison.localOnly)}`,
576
+ ...comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0 ? [
577
+ "Only ordering differs.",
578
+ `Remote order: ${safeList(decision.remoteInclude)}`,
579
+ `Device order: ${safeList(decision.localInclude)}`
580
+ ] : [],
581
+ "Run /sync in TUI to choose a content list; RPC review is read-only."
582
+ ].join("\n");
583
+ }
584
+ function formatLegacySummary(config, discovered) {
585
+ return `Remote snapshot for \u201C${safeTerminalText(config.setupName)}\u201D has no portable synced-content list; ${discovered.length} safe path${discovered.length === 1 ? " was" : "s were"} discovered, but the result is partial and read-only.`;
586
+ }
587
+ function decisionFromState(config, state) {
588
+ return {
589
+ setupName: config.setupName,
590
+ configIdentity: syncConfigReviewFingerprint(config),
591
+ localInclude: [...config.include],
592
+ remoteInclude: [...state.include]
593
+ };
594
+ }
595
+ function assertLocalSelectionCurrent(config, decision) {
596
+ if (syncConfigReviewFingerprint(config) === decision.configIdentity && sameSyncInclude(config.include, decision.localInclude)) {
597
+ return;
598
+ }
599
+ throw new StaleRemoteSelectionReviewError(
600
+ `Sync setup \u201C${safeTerminalText(config.setupName)}\u201D changed while the comparison was open.`
601
+ );
602
+ }
603
+ function sameContinuationReview(left, right) {
604
+ return left.setupName === right.setupName && left.connectionName === right.connectionName && left.storageKind === right.storageKind && left.storagePath === right.storagePath && left.bucket === right.bucket && left.branch === right.branch && sameSyncInclude(left.include, right.include);
605
+ }
606
+ function continueLabel(origin) {
607
+ if (origin === "pull") return "Continue Pull now\u2026";
608
+ if (origin === "push") return "Continue Push now\u2026";
609
+ return "Continue Sync now\u2026";
610
+ }
611
+ function continueBusyLabel(origin) {
612
+ if (origin === "pull") return "Checking remote changes\u2026";
613
+ if (origin === "push") return "Preparing push preview\u2026";
614
+ return "Checking current sync setup\u2026";
615
+ }
616
+ function continuationCancelledMessage(route) {
617
+ if (route === "pull") return "Pull check cancelled; no local files were changed.";
618
+ if (route === "push") return "Push preparation cancelled; no remote files were changed.";
619
+ return "Sync check cancelled; no settings or files were changed.";
620
+ }
621
+ function safeList(values) {
622
+ return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
623
+ }
624
+ var StaleRemoteSelectionReviewError = class extends Error {
625
+ constructor(message) {
626
+ super(message);
627
+ this.name = "StaleRemoteSelectionReviewError";
628
+ }
629
+ };
630
+ function isStaleReviewError(error) {
631
+ return error instanceof StaleRemoteSelectionReviewError || error instanceof SyncSetupReviewChangedError;
632
+ }
633
+ function runWithOptionalStateAccess(options, task) {
634
+ return options.withStateAccess ? options.withStateAccess(task) : task();
635
+ }
636
+ function combineSignals(sessionSignal, actionSignal) {
637
+ return sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
638
+ }
639
+ function throwIfAborted(signal) {
640
+ if (!signal.aborted) return;
641
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
642
+ }
643
+
644
+ export {
645
+ requiredExistingBucket,
646
+ requiredInput,
647
+ ownRecord,
648
+ safeTerminalText2 as safeTerminalText,
649
+ errorMessage2 as errorMessage,
650
+ runCancellableOperation,
651
+ showRemoteSelectionReview
652
+ };
653
+ //# sourceMappingURL=chunk-D3S54OXI.ts.map