@narumitw/pi-sync 0.28.0 → 0.29.1
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 +227 -142
- package/package.json +1 -1
- package/src/command.ts +102 -7
- package/src/config.ts +363 -33
- package/src/file-selection.ts +158 -70
- package/src/lock.ts +22 -5
- package/src/manager-ui.ts +961 -0
- package/src/s3-client.ts +47 -13
- package/src/settings-management.ts +302 -0
- package/src/snapshot-apply.ts +6 -6
- package/src/snapshot-transaction.ts +210 -0
- package/src/sync-format.ts +168 -0
- package/src/sync.ts +217 -136
- package/src/types.ts +43 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { agentDir } from "./config.js";
|
|
3
|
+
import { fileHashMap } from "./sync-state.js";
|
|
4
|
+
import type { LatestPointer, RemoteObject, Snapshot, SyncConfig } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export function formatDiff(local: Snapshot, remote: Snapshot) {
|
|
7
|
+
const localMap = fileHashMap(local);
|
|
8
|
+
const remoteMap = fileHashMap(remote);
|
|
9
|
+
const allPaths = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
10
|
+
const lines = [
|
|
11
|
+
`local: ${local.files.length} files`,
|
|
12
|
+
`remote: ${remote.id} (${remote.files.length} files)`,
|
|
13
|
+
"",
|
|
14
|
+
];
|
|
15
|
+
let changed = 0;
|
|
16
|
+
for (const filePath of allPaths) {
|
|
17
|
+
if (!localMap[filePath]) {
|
|
18
|
+
lines.push(`Remote only: ${filePath}`);
|
|
19
|
+
changed += 1;
|
|
20
|
+
} else if (!remoteMap[filePath]) {
|
|
21
|
+
lines.push(`Local only: ${filePath}`);
|
|
22
|
+
changed += 1;
|
|
23
|
+
} else if (localMap[filePath] !== remoteMap[filePath]) {
|
|
24
|
+
lines.push(`Different: ${filePath}`);
|
|
25
|
+
changed += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (changed === 0) lines.push("No file differences.");
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatSnapshotOnlyDiff(title: string, snapshot: Snapshot) {
|
|
33
|
+
return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${file.path}`)].join(
|
|
34
|
+
"\n",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatPushSummary(
|
|
39
|
+
config: SyncConfig,
|
|
40
|
+
upload: Snapshot,
|
|
41
|
+
latest: RemoteObject<LatestPointer>,
|
|
42
|
+
preservedRemoteFileCount = 0,
|
|
43
|
+
remote?: Snapshot,
|
|
44
|
+
) {
|
|
45
|
+
return [
|
|
46
|
+
`Target: ${safeTerminalText(config.target ?? "default")}`,
|
|
47
|
+
`Destination: ${formatDestination(config)}`,
|
|
48
|
+
`Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
|
|
49
|
+
`Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
50
|
+
latest.value ? `Remote latest: ${latest.value.snapshot}` : "Remote latest: empty",
|
|
51
|
+
"Publication effect: latest.json will point to the new immutable snapshot.",
|
|
52
|
+
formatPublicationPreview(remote, upload),
|
|
53
|
+
preservedRemoteFileCount > 0
|
|
54
|
+
? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
|
|
55
|
+
: "Possible secrets were scanned before this prompt.",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatApplyPreview(local: Snapshot, remote: Snapshot) {
|
|
60
|
+
return formatDirectionalChanges(local, remote, {
|
|
61
|
+
add: "Add locally",
|
|
62
|
+
update: "Update locally",
|
|
63
|
+
remove: "Remove locally",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatPullSummary(
|
|
68
|
+
config: SyncConfig,
|
|
69
|
+
local: Snapshot,
|
|
70
|
+
remote: Snapshot,
|
|
71
|
+
protectedSessionCount: number,
|
|
72
|
+
) {
|
|
73
|
+
return [
|
|
74
|
+
`Target: ${safeTerminalText(config.target ?? "default")}`,
|
|
75
|
+
`Destination: ${formatDestination(config)}`,
|
|
76
|
+
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
77
|
+
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
78
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
79
|
+
formatApplyPreview(local, remote),
|
|
80
|
+
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged.",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatRollbackSummary(
|
|
85
|
+
config: SyncConfig,
|
|
86
|
+
local: Snapshot,
|
|
87
|
+
remote: Snapshot,
|
|
88
|
+
requestedSnapshot: string,
|
|
89
|
+
protectedSessionCount: number,
|
|
90
|
+
) {
|
|
91
|
+
return [
|
|
92
|
+
`Target: ${safeTerminalText(config.target ?? "default")}`,
|
|
93
|
+
`Destination: ${formatDestination(config)}`,
|
|
94
|
+
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
95
|
+
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
96
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
97
|
+
formatApplyPreview(local, remote),
|
|
98
|
+
"A local backup is created before applying; the remote latest pointer will change.",
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatPublicationPreview(remote: Snapshot | undefined, upload: Snapshot) {
|
|
103
|
+
if (!remote) {
|
|
104
|
+
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${file.path}`)].join(
|
|
105
|
+
"\n",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return formatDirectionalChanges(remote, upload, {
|
|
109
|
+
add: "Add remotely",
|
|
110
|
+
update: "Update remotely",
|
|
111
|
+
remove: "Remove remotely",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatDirectionalChanges(
|
|
116
|
+
before: Snapshot,
|
|
117
|
+
after: Snapshot,
|
|
118
|
+
labels: { add: string; update: string; remove: string },
|
|
119
|
+
) {
|
|
120
|
+
const beforeMap = fileHashMap(before);
|
|
121
|
+
const afterMap = fileHashMap(after);
|
|
122
|
+
const paths = [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
for (const filePath of paths) {
|
|
125
|
+
if (!beforeMap[filePath]) lines.push(`${labels.add}: ${filePath}`);
|
|
126
|
+
else if (!afterMap[filePath]) lines.push(`${labels.remove}: ${filePath}`);
|
|
127
|
+
else if (beforeMap[filePath] !== afterMap[filePath])
|
|
128
|
+
lines.push(`${labels.update}: ${filePath}`);
|
|
129
|
+
}
|
|
130
|
+
if (lines.length === 0) lines.push("No file changes.");
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function formatDestination(config: SyncConfig) {
|
|
135
|
+
let host = config.endpoint;
|
|
136
|
+
try {
|
|
137
|
+
host = new URL(config.endpoint).hostname;
|
|
138
|
+
} catch {
|
|
139
|
+
// Preserve a sanitized invalid endpoint for actionable repair output.
|
|
140
|
+
}
|
|
141
|
+
return safeTerminalText(`${host} · ${config.bucket}/${config.prefix}/profiles/${config.profile}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function countPreservedRemoteFiles(local: Snapshot, upload: Snapshot) {
|
|
145
|
+
const localPaths = new Set(local.files.map((file) => file.path));
|
|
146
|
+
return upload.files.filter((file) => !localPaths.has(file.path)).length;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function safeTerminalText(value: string) {
|
|
150
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
|
|
151
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function redact(value: string) {
|
|
155
|
+
return value.length <= 8 ? "configured" : `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function remoteIdentity(remote: RemoteObject<LatestPointer>) {
|
|
159
|
+
return remote.missing ? "missing" : (remote.value?.snapshot ?? "unknown");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function sha256(value: Buffer) {
|
|
163
|
+
return createHash("sha256").update(value).digest("hex");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function errorMessage(error: unknown) {
|
|
167
|
+
return safeTerminalText(error instanceof Error ? error.message : String(error));
|
|
168
|
+
}
|