@narumitw/pi-sync 0.49.3 → 0.49.5
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 +34 -8
- package/package.json +7 -4
- package/src/command.ts +6 -1
- package/src/config.ts +2 -3
- package/src/file-selection.ts +51 -1
- package/src/git-backend.ts +5 -2
- package/src/git-storage.ts +17 -1
- package/src/lock-policy.ts +2 -0
- package/src/lock.ts +5 -43
- package/src/lockfile-fs.ts +25 -0
- package/src/paths.ts +2 -0
- package/src/remote-selection-ui.ts +279 -0
- package/src/remote-snapshot.ts +59 -0
- package/src/s3-backend.ts +4 -0
- package/src/settings-ui.ts +16 -1
- package/src/snapshot-codec.ts +3 -0
- package/src/snapshot.ts +33 -2
- package/src/state-directory.ts +184 -0
- package/src/sync-backend.ts +3 -1
- package/src/sync-format.ts +6 -0
- package/src/sync-operations.ts +44 -10
- package/src/sync-policy.ts +183 -7
- package/src/sync.ts +81 -17
- package/src/types.ts +9 -0
- package/src/webdav-backend.ts +4 -0
package/src/sync.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
parseOptions,
|
|
10
10
|
resolveSyncCommand,
|
|
11
11
|
setSyncSetupCompletions,
|
|
12
|
+
splitArgs,
|
|
12
13
|
usage,
|
|
13
14
|
validateCommandOptions,
|
|
14
15
|
} from "./command.js";
|
|
@@ -32,6 +33,11 @@ import { unlock, withLock } from "./lock.js";
|
|
|
32
33
|
import { SetupPullRequiresUiError, useSyncSetup } from "./setup-switch.js";
|
|
33
34
|
import { createSnapshot } from "./snapshot.js";
|
|
34
35
|
import { recoverSnapshotTransactionsOnStartup } from "./snapshot-transaction.js";
|
|
36
|
+
import {
|
|
37
|
+
migrateLegacyStateDirectory,
|
|
38
|
+
stateDirectoryMigrationNotice,
|
|
39
|
+
withStateDirectoryAccess,
|
|
40
|
+
} from "./state-directory.js";
|
|
35
41
|
import { isSyncDecisionRequiredError } from "./sync-decision.js";
|
|
36
42
|
import { errorMessage } from "./sync-format.js";
|
|
37
43
|
import { hasLocalChanges } from "./sync-state.js";
|
|
@@ -74,7 +80,9 @@ export default function sync(pi: ExtensionAPI) {
|
|
|
74
80
|
"/sync requires TUI or RPC mode so results and safety prompts are observable.",
|
|
75
81
|
);
|
|
76
82
|
}
|
|
77
|
-
|
|
83
|
+
const run = () => handleCommand(args, ctx, sessionAbort.signal);
|
|
84
|
+
if (splitArgs(args)[0] === "migrate-state") await run();
|
|
85
|
+
else await withStateDirectoryAccess(run);
|
|
78
86
|
},
|
|
79
87
|
});
|
|
80
88
|
|
|
@@ -86,24 +94,11 @@ export default function sync(pi: ExtensionAPI) {
|
|
|
86
94
|
const signal = sessionAbort.signal;
|
|
87
95
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
88
96
|
try {
|
|
89
|
-
await
|
|
90
|
-
if (signal.aborted) return;
|
|
97
|
+
await withStateDirectoryAccess(() => startSession(ctx, signal));
|
|
91
98
|
} catch (error) {
|
|
92
99
|
if (signal.aborted) return;
|
|
93
|
-
ctx.ui.notify(`pi-sync
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
setSyncSetupCompletions(await configuredSyncSetupNames());
|
|
98
|
-
if (signal.aborted) return;
|
|
99
|
-
} catch {
|
|
100
|
-
if (signal.aborted) return;
|
|
101
|
-
setSyncSetupCompletions([]);
|
|
100
|
+
ctx.ui.notify(`pi-sync state access failed: ${errorMessage(error)}`, "error");
|
|
102
101
|
}
|
|
103
|
-
const migrationNotice = consumeLocalConfigMigrationNotice();
|
|
104
|
-
if (migrationNotice) ctx.ui.notify(migrationNotice, "warning");
|
|
105
|
-
if (signal.aborted) return;
|
|
106
|
-
await autoSync(ctx, signal);
|
|
107
102
|
});
|
|
108
103
|
|
|
109
104
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
@@ -115,7 +110,16 @@ export default function sync(pi: ExtensionAPI) {
|
|
|
115
110
|
const reason =
|
|
116
111
|
typeof event === "object" && event ? (event as { reason?: string }).reason : undefined;
|
|
117
112
|
try {
|
|
118
|
-
if (reason !== "reload")
|
|
113
|
+
if (reason !== "reload") {
|
|
114
|
+
await withStateDirectoryAccess(async () => {
|
|
115
|
+
if (signal.aborted) return;
|
|
116
|
+
await autoPushSessions(ctx, signal);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!signal.aborted) {
|
|
121
|
+
ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
|
|
122
|
+
}
|
|
119
123
|
} finally {
|
|
120
124
|
if (shutdownAbort === controller) shutdownAbort = undefined;
|
|
121
125
|
}
|
|
@@ -124,6 +128,36 @@ export default function sync(pi: ExtensionAPI) {
|
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
async function startSession(ctx: ExtensionContext, signal: AbortSignal) {
|
|
132
|
+
if (signal.aborted) return;
|
|
133
|
+
try {
|
|
134
|
+
const migrationNotice = stateDirectoryMigrationNotice();
|
|
135
|
+
if (migrationNotice) ctx.ui.notify(migrationNotice, "warning");
|
|
136
|
+
} catch (error) {
|
|
137
|
+
ctx.ui.notify(`pi-sync state directory requires attention: ${errorMessage(error)}`, "error");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await recoverSnapshotTransactionsOnStartup();
|
|
142
|
+
if (signal.aborted) return;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (signal.aborted) return;
|
|
145
|
+
ctx.ui.notify(`pi-sync recovery required: ${errorMessage(error)}`, "error");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
setSyncSetupCompletions(await configuredSyncSetupNames());
|
|
150
|
+
if (signal.aborted) return;
|
|
151
|
+
} catch {
|
|
152
|
+
if (signal.aborted) return;
|
|
153
|
+
setSyncSetupCompletions([]);
|
|
154
|
+
}
|
|
155
|
+
const migrationNotice = consumeLocalConfigMigrationNotice();
|
|
156
|
+
if (migrationNotice) ctx.ui.notify(migrationNotice, "warning");
|
|
157
|
+
if (signal.aborted) return;
|
|
158
|
+
await autoSync(ctx, signal);
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
async function handleCommand(
|
|
128
162
|
rawArgs: string,
|
|
129
163
|
ctx: ExtensionCommandContext,
|
|
@@ -237,6 +271,9 @@ async function executeCommand(
|
|
|
237
271
|
await withLock("rollback", () => operations.rollback(ctx, options));
|
|
238
272
|
return { kind: "completed" };
|
|
239
273
|
}
|
|
274
|
+
case "migrate-state":
|
|
275
|
+
await migrateStateDirectory(ctx, options);
|
|
276
|
+
return { kind: "completed" };
|
|
240
277
|
case "unlock":
|
|
241
278
|
await unlock(ctx, options);
|
|
242
279
|
return { kind: "completed" };
|
|
@@ -256,6 +293,33 @@ async function executeCommand(
|
|
|
256
293
|
}
|
|
257
294
|
}
|
|
258
295
|
|
|
296
|
+
async function migrateStateDirectory(ctx: ExtensionCommandContext, options: CommandOptions) {
|
|
297
|
+
const notice = stateDirectoryMigrationNotice();
|
|
298
|
+
if (!notice) {
|
|
299
|
+
ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (
|
|
303
|
+
!options.yes &&
|
|
304
|
+
!(await ctx.ui.confirm(
|
|
305
|
+
"Migrate pi-sync state directory",
|
|
306
|
+
"Confirm that every other Pi process is closed. pi-sync will atomically rename .pisync/ to pi-sync/ without merging or deleting either root.",
|
|
307
|
+
{ signal: options.signal },
|
|
308
|
+
))
|
|
309
|
+
) {
|
|
310
|
+
ctx.ui.notify("pi-sync state migration cancelled.", "info");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
throwIfAborted(options.signal);
|
|
314
|
+
const result = await migrateLegacyStateDirectory();
|
|
315
|
+
throwIfAborted(options.signal);
|
|
316
|
+
if (result.status === "ready") {
|
|
317
|
+
ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
ctx.ui.notify(result.message, result.status === "migrated" ? "info" : "warning");
|
|
321
|
+
}
|
|
322
|
+
|
|
259
323
|
async function autoSync(ctx: ExtensionContext, signal: AbortSignal) {
|
|
260
324
|
try {
|
|
261
325
|
const partial = await loadPartialConfig();
|
package/src/types.ts
CHANGED
|
@@ -193,6 +193,11 @@ export interface SnapshotFile {
|
|
|
193
193
|
sha256: string;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
export interface SnapshotSelection {
|
|
197
|
+
version: 1;
|
|
198
|
+
include: string[];
|
|
199
|
+
}
|
|
200
|
+
|
|
196
201
|
export interface Snapshot {
|
|
197
202
|
version: number;
|
|
198
203
|
id: string;
|
|
@@ -201,6 +206,8 @@ export interface Snapshot {
|
|
|
201
206
|
/** Backend-scoped remote identity retained in the snapshot wire format. */
|
|
202
207
|
profile: string;
|
|
203
208
|
syncSessions?: boolean;
|
|
209
|
+
/** Portable, credential-free included-content intent. Absent on legacy snapshots. */
|
|
210
|
+
selection?: SnapshotSelection;
|
|
204
211
|
files: SnapshotFile[];
|
|
205
212
|
}
|
|
206
213
|
|
|
@@ -212,6 +219,8 @@ export interface LatestPointer {
|
|
|
212
219
|
createdAt: string;
|
|
213
220
|
machine: string;
|
|
214
221
|
syncSessions?: boolean;
|
|
222
|
+
/** Lightweight projection; the immutable snapshot remains authoritative. */
|
|
223
|
+
selection?: SnapshotSelection;
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
export interface RemoteObject<T> {
|
package/src/webdav-backend.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SyncBackendConflictError,
|
|
12
12
|
SyncBackendPublicationOutcomeUnknownError,
|
|
13
13
|
} from "./sync-backend.js";
|
|
14
|
+
import { portableSnapshotSelection } from "./sync-policy.js";
|
|
14
15
|
import type { LatestPointer, RemoteObject, ResolvedWebDavBackend, Snapshot } from "./types.js";
|
|
15
16
|
import { WebDavClient, WebDavHttpError, WebDavPreconditionError } from "./webdav-client.js";
|
|
16
17
|
|
|
@@ -427,6 +428,7 @@ function pointerFor(
|
|
|
427
428
|
syncSessions:
|
|
428
429
|
snapshot.syncSessions === true ||
|
|
429
430
|
snapshot.files.some((file) => file.path.startsWith("sessions/")),
|
|
431
|
+
...(snapshot.selection === undefined ? {} : { selection: snapshot.selection }),
|
|
430
432
|
};
|
|
431
433
|
}
|
|
432
434
|
|
|
@@ -438,6 +440,7 @@ function remoteHead(pointer: LatestPointer, identity: string, etag?: string): Re
|
|
|
438
440
|
createdAt: pointer.createdAt,
|
|
439
441
|
machine: pointer.machine,
|
|
440
442
|
syncSessions: pointer.syncSessions === true,
|
|
443
|
+
...(pointer.selection === undefined ? {} : { selection: pointer.selection }),
|
|
441
444
|
};
|
|
442
445
|
}
|
|
443
446
|
|
|
@@ -503,6 +506,7 @@ function requirePointer(value: LatestPointer | undefined, expectedProfile: strin
|
|
|
503
506
|
) {
|
|
504
507
|
throw new Error("Remote latest pointer is malformed.");
|
|
505
508
|
}
|
|
509
|
+
if (value.selection !== undefined) portableSnapshotSelection(value.selection);
|
|
506
510
|
return value;
|
|
507
511
|
}
|
|
508
512
|
|