@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/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
- await handleCommand(args, ctx, sessionAbort.signal);
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 recoverSnapshotTransactionsOnStartup();
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 recovery required: ${errorMessage(error)}`, "error");
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") await autoPushSessions(ctx, signal);
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> {
@@ -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