@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 +1 -1
- package/src/extension.ts +4 -0
- package/src/operations.ts +40 -21
- package/src/snapshot.ts +25 -0
- package/src/status.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lisang233/pi-sync",
|
|
3
|
-
"version": "0.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 {
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
111
|
-
lines.push("", formatSnapshotDiff(local,
|
|
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
|
|
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,
|
|
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(
|
|
249
|
+
await applySnapshot(projectedRemote, config);
|
|
235
250
|
await saveState({
|
|
236
251
|
version: 1,
|
|
237
|
-
lastAppliedSnapshot: snapshotSha256(
|
|
252
|
+
lastAppliedSnapshot: snapshotSha256(projectedRemote),
|
|
238
253
|
lastRemoteRevision: remoteRevision,
|
|
239
|
-
lastHashes: Object.fromEntries(
|
|
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 (${
|
|
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(
|
|
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 (!
|
|
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,
|
|
412
|
-
const message = `Fetched ${
|
|
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) {
|