@lisang233/pi-sync 0.1.1 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lisang233/pi-sync",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Personal Pi extension that syncs Pi configuration through Git with background auto-sync and git-style fetch/merge conflict handling.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/extension.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  import { loadConfig } from "./config.js";
7
7
  import { runConfigEditor } from "./config-ui.js";
8
8
  import * as operations from "./operations.js";
9
+ import { syncBusyText } from "./status.js";
9
10
  import { runSetupWizard } from "./wizard.js";
10
11
 
11
12
  const STATUS_KEY = "sync";
@@ -39,6 +40,9 @@ export default function sync(pi: ExtensionAPI): void {
39
40
  let backgroundSync: BackgroundSync | undefined;
40
41
 
41
42
  const startBackgroundSync = (ctx: ExtensionContext, signal: AbortSignal) => {
43
+ // While the automatic fetch is in flight the indicator shows the busy
44
+ // state; refreshIndicator replaces it with the real state when done.
45
+ ctx.ui.setStatus(STATUS_KEY, syncBusyText());
42
46
  const settled = (async () => {
43
47
  try {
44
48
  await runAutomaticSync(ctx, signal);
package/src/operations.ts CHANGED
@@ -35,7 +35,13 @@ import {
35
35
  } from "./merge-session.js";
36
36
  import { agentDir, syncRootPath } from "./paths.js";
37
37
  import { runBlockResolver } from "./resolve.js";
38
- import { createSnapshot, type Snapshot, snapshotSha256 } from "./snapshot.js";
38
+ import {
39
+ createSnapshot,
40
+ pathMatchesInclude,
41
+ projectSnapshot,
42
+ type Snapshot,
43
+ snapshotSha256,
44
+ } from "./snapshot.js";
39
45
  import { loadState, saveState } from "./state.js";
40
46
  import { deriveSyncStatus, type SyncStatusInfo, syncIndicatorText } from "./status.js";
41
47
 
@@ -65,10 +71,15 @@ export async function refreshIndicator(ctx: OperationContext, config: SyncConfig
65
71
  readRemoteSnapshot(config),
66
72
  loadState(),
67
73
  ]);
74
+ const projectedRemote = remote ? projectSnapshot(remote, config.include) : undefined;
68
75
  const base = state?.lastRemoteRevision
69
76
  ? await readSnapshotAt(state.lastRemoteRevision, { signal: ctx.signal })
70
77
  : undefined;
71
- ctx.ui.setStatus("sync", syncIndicatorText(deriveSyncStatus(local, remote, base)));
78
+ const projectedBase = base ? projectSnapshot(base, config.include) : undefined;
79
+ ctx.ui.setStatus(
80
+ "sync",
81
+ syncIndicatorText(deriveSyncStatus(local, projectedRemote, projectedBase)),
82
+ );
72
83
  } catch {
73
84
  ctx.ui.setStatus(
74
85
  "sync",
@@ -90,10 +101,12 @@ export async function status(
90
101
  readRemoteSnapshot(config),
91
102
  loadState(),
92
103
  ]);
104
+ const projectedRemote = remote ? projectSnapshot(remote, config.include) : undefined;
93
105
  const base = state?.lastRemoteRevision
94
106
  ? await readSnapshotAt(state.lastRemoteRevision, { signal: ctx.signal })
95
107
  : undefined;
96
- const info = deriveSyncStatus(local, remote, base);
108
+ const projectedBase = base ? projectSnapshot(base, config.include) : undefined;
109
+ const info = deriveSyncStatus(local, projectedRemote, projectedBase);
97
110
  const mergeSession = await loadMergeSession();
98
111
  await refreshIndicator(ctx, config);
99
112
  const lines = [
@@ -107,8 +120,8 @@ export async function status(
107
120
  );
108
121
  }
109
122
  lines.push(nextStepHint(info, mergeSession !== undefined));
110
- if (options.diff && remote) {
111
- lines.push("", formatSnapshotDiff(local, remote));
123
+ if (options.diff && projectedRemote) {
124
+ lines.push("", formatSnapshotDiff(local, projectedRemote));
112
125
  }
113
126
  const level = info.label === "up-to-date" && !mergeSession ? "info" : "warning";
114
127
  ctx.ui.notify(lines.join("\n"), level);
@@ -196,10 +209,12 @@ export async function pull(
196
209
  ctx.ui.notify(message, "warning");
197
210
  return { pushed: false, pulled: false, merged: false, message };
198
211
  }
212
+ const projectedRemote = projectSnapshot(remote, config.include);
199
213
  const base = state?.lastRemoteRevision
200
214
  ? await readSnapshotAt(state.lastRemoteRevision, { signal: ctx.signal })
201
215
  : undefined;
202
- const plan = planMerge(local, remote, base);
216
+ const projectedBase = base ? projectSnapshot(base, config.include) : undefined;
217
+ const plan = planMerge(local, projectedRemote, projectedBase);
203
218
  const diverged =
204
219
  plan.conflicts.length > 0 || (plan.takeLocal.length > 0 && plan.takeRemote.length > 0);
205
220
 
@@ -214,7 +229,7 @@ export async function pull(
214
229
  return { pushed: false, pulled: false, merged: false, message };
215
230
  }
216
231
  // Fast-forward: apply the merged snapshot (remote-only changes).
217
- const merged = mergeSnapshot(local, remote, plan);
232
+ const merged = mergeSnapshot(local, projectedRemote, plan);
218
233
  await applySnapshot(merged, config);
219
234
  await saveState({
220
235
  version: 1,
@@ -231,22 +246,30 @@ export async function pull(
231
246
 
232
247
  if (options.force) {
233
248
  const backup = await backupLocalFiles(local);
234
- await applySnapshot(remote, config);
249
+ await applySnapshot(projectedRemote, config);
235
250
  await saveState({
236
251
  version: 1,
237
- lastAppliedSnapshot: snapshotSha256(remote),
252
+ lastAppliedSnapshot: snapshotSha256(projectedRemote),
238
253
  lastRemoteRevision: remoteRevision,
239
- lastHashes: Object.fromEntries(remote.files.map((file) => [file.path, file.sha256])),
254
+ lastHashes: Object.fromEntries(projectedRemote.files.map((file) => [file.path, file.sha256])),
240
255
  });
241
256
  await clearMergeSession();
242
257
  await refreshIndicator(ctx, config);
243
- const message = `Overwrote local files with the remote snapshot (${remote.files.length} files). Backup: ${backup}`;
258
+ const message = `Overwrote local files with the remote snapshot (${projectedRemote.files.length} files). Backup: ${backup}`;
244
259
  ctx.ui.notify(message, "warning");
245
260
  return { pushed: false, pulled: true, merged: false, message };
246
261
  }
247
262
 
248
263
  if (options.merge) {
249
- return startMergeFlow(ctx, config, local, remote, base, plan, remoteRevision ?? "");
264
+ return startMergeFlow(
265
+ ctx,
266
+ config,
267
+ local,
268
+ projectedRemote,
269
+ projectedBase,
270
+ plan,
271
+ remoteRevision ?? "",
272
+ );
250
273
  }
251
274
 
252
275
  const message =
@@ -402,14 +425,15 @@ export async function fetch(
402
425
  readRemoteRevision(config),
403
426
  createSnapshot(config),
404
427
  ]);
428
+ const projectedRemote = remote ? projectSnapshot(remote, config.include) : undefined;
405
429
  await refreshIndicator(ctx, config);
406
- if (!remote) {
430
+ if (!projectedRemote) {
407
431
  const message = "Remote is empty. Run /sync push to publish local content.";
408
432
  if (!options.quiet) ctx.ui.notify(message, "info");
409
433
  return { pushed: false, pulled: false, merged: false, message };
410
434
  }
411
- const summary = diffSummary(local, remote);
412
- const message = `Fetched ${remote.files.length} files from ${config.branch} (${shortId(remoteRevision ?? "")}). ${describeChanges(summary)}.`;
435
+ const summary = diffSummary(local, projectedRemote);
436
+ const message = `Fetched ${projectedRemote.files.length} files from ${config.branch} (${shortId(remoteRevision ?? "")}). ${describeChanges(summary)}.`;
413
437
  if (!options.quiet) ctx.ui.notify(message, summary.identical ? "info" : "warning");
414
438
  return { pushed: false, pulled: false, merged: false, message };
415
439
  }
@@ -489,12 +513,7 @@ export async function applySnapshot(snapshot: Snapshot, config: SyncConfig): Pro
489
513
 
490
514
  function resolveSnapshotTarget(relativePath: string, config: SyncConfig): string | undefined {
491
515
  if (relativePath.split("/").some((segment) => segment === "..")) return undefined;
492
- const entry = config.include.find((candidate) => {
493
- const lower = candidate.toLowerCase();
494
- return (
495
- relativePath.toLowerCase() === lower || relativePath.toLowerCase().startsWith(`${lower}/`)
496
- );
497
- });
516
+ const entry = config.include.find((candidate) => pathMatchesInclude(relativePath, candidate));
498
517
  if (!entry) return undefined;
499
518
  const root = syncRootPath(entry);
500
519
  const suffix = relativePath.slice(entry.length);
package/src/snapshot.ts CHANGED
@@ -31,6 +31,31 @@ export function fileHashMap(snapshot: Snapshot): Map<string, string> {
31
31
  return new Map(snapshot.files.map((file) => [file.path, file.sha256]));
32
32
  }
33
33
 
34
+ /**
35
+ * True when a snapshot path falls under an include entry: exact match or a
36
+ * directory prefix, case-insensitive. This is the single include-matching
37
+ * rule used by projection, write-back targeting and content mapping.
38
+ */
39
+ export function pathMatchesInclude(relativePath: string, entry: string): boolean {
40
+ const lower = entry.toLowerCase();
41
+ const pathLower = relativePath.toLowerCase();
42
+ return pathLower === lower || pathLower.startsWith(`${lower}/`);
43
+ }
44
+
45
+ /**
46
+ * Project a snapshot onto the include set: keep only paths covered by the
47
+ * declaration. The local snapshot is always a projection (`createSnapshot`
48
+ * scans include only); remote and historical snapshots are projected before
49
+ * merge and reporting so out-of-include leftovers never participate.
50
+ */
51
+ export function projectSnapshot(snapshot: Snapshot, include: string[]): Snapshot {
52
+ const files = snapshot.files.filter((file) =>
53
+ include.some((entry) => pathMatchesInclude(file.path, entry)),
54
+ );
55
+ if (files.length === snapshot.files.length) return snapshot;
56
+ return { ...snapshot, files };
57
+ }
58
+
34
59
  /** Build the current snapshot of the configured include paths under the agent dir. */
35
60
  export async function createSnapshot(config: SyncConfig): Promise<Snapshot> {
36
61
  const files: SnapshotFile[] = [];
package/src/status.ts CHANGED
@@ -49,6 +49,11 @@ export function deriveSyncStatus(
49
49
  return { label: "up-to-date", ahead: 0, behind: 0, conflicts: 0 };
50
50
  }
51
51
 
52
+ /** Short status-bar text while a background sync (automatic fetch) is in flight. */
53
+ export function syncBusyText(): string {
54
+ return "sync: fetching…";
55
+ }
56
+
52
57
  /** Short status-bar text for the sync indicator. */
53
58
  export function syncIndicatorText(info: SyncStatusInfo): string {
54
59
  switch (info.label) {