@narumitw/pi-sync 0.49.9 → 0.49.11
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 +10 -1
- package/dist/chunks/chunk-3GRWMWUZ.ts +213 -0
- package/dist/chunks/chunk-3GRWMWUZ.ts.map +7 -0
- package/dist/chunks/chunk-3NDD5QPM.ts +203 -0
- package/dist/chunks/chunk-3NDD5QPM.ts.map +7 -0
- package/dist/chunks/chunk-4F3W4CSC.ts +195 -0
- package/dist/chunks/chunk-4F3W4CSC.ts.map +7 -0
- package/dist/chunks/chunk-4U6AFLJB.ts +221 -0
- package/dist/chunks/chunk-4U6AFLJB.ts.map +7 -0
- package/dist/chunks/chunk-5MQ76BK5.ts +34 -0
- package/dist/chunks/chunk-5MQ76BK5.ts.map +7 -0
- package/dist/chunks/chunk-APZVBSM6.ts +1238 -0
- package/dist/chunks/chunk-APZVBSM6.ts.map +7 -0
- package/dist/chunks/chunk-BHJUISB4.ts +134 -0
- package/dist/chunks/chunk-BHJUISB4.ts.map +7 -0
- package/dist/chunks/chunk-D3S54OXI.ts +653 -0
- package/dist/chunks/chunk-D3S54OXI.ts.map +7 -0
- package/dist/chunks/chunk-EWX3TPLQ.ts +356 -0
- package/dist/chunks/chunk-EWX3TPLQ.ts.map +7 -0
- package/dist/chunks/chunk-F5QL2GPY.ts +341 -0
- package/dist/chunks/chunk-F5QL2GPY.ts.map +7 -0
- package/dist/chunks/chunk-FR6CVXAL.ts +81 -0
- package/dist/chunks/chunk-FR6CVXAL.ts.map +7 -0
- package/dist/chunks/chunk-HGV24P4T.ts +53 -0
- package/dist/chunks/chunk-HGV24P4T.ts.map +7 -0
- package/dist/chunks/chunk-JNOOSPEW.ts +82 -0
- package/dist/chunks/chunk-JNOOSPEW.ts.map +7 -0
- package/dist/chunks/chunk-LJDSHNS3.ts +259 -0
- package/dist/chunks/chunk-LJDSHNS3.ts.map +7 -0
- package/dist/chunks/chunk-RGJ6SOYR.ts +128 -0
- package/dist/chunks/chunk-RGJ6SOYR.ts.map +7 -0
- package/dist/chunks/chunk-W64NEHXT.ts +167 -0
- package/dist/chunks/chunk-W64NEHXT.ts.map +7 -0
- package/dist/chunks/chunk-YQ6UW7IF.ts +394 -0
- package/dist/chunks/chunk-YQ6UW7IF.ts.map +7 -0
- package/dist/chunks/file-selection-4UABOP44.ts +290 -0
- package/dist/chunks/file-selection-4UABOP44.ts.map +7 -0
- package/dist/chunks/git-backend-E3UOUUOM.ts +1104 -0
- package/dist/chunks/git-backend-E3UOUUOM.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts +18 -0
- package/dist/chunks/manager-result-dispatcher-YKBLZ7KL.ts.map +7 -0
- package/dist/chunks/manager-ui-7E25VWUP.ts +2754 -0
- package/dist/chunks/manager-ui-7E25VWUP.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts +17 -0
- package/dist/chunks/remote-selection-ui-J6OW5DKZ.ts.map +7 -0
- package/dist/chunks/s3-backend-3QZGO2EF.ts +680 -0
- package/dist/chunks/s3-backend-3QZGO2EF.ts.map +7 -0
- package/dist/chunks/setup-switch-X3W3FALZ.ts +27 -0
- package/dist/chunks/setup-switch-X3W3FALZ.ts.map +7 -0
- package/dist/chunks/snapshot-WBSV4MQG.ts +45 -0
- package/dist/chunks/snapshot-WBSV4MQG.ts.map +7 -0
- package/dist/chunks/sync-operations-MPAFVVHA.ts +1112 -0
- package/dist/chunks/sync-operations-MPAFVVHA.ts.map +7 -0
- package/dist/chunks/sync-state-36H7ULG2.ts +41 -0
- package/dist/chunks/sync-state-36H7ULG2.ts.map +7 -0
- package/dist/chunks/webdav-backend-ZD5RHRIU.ts +869 -0
- package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +7 -0
- package/dist/index.ts +668 -0
- package/dist/index.ts.map +7 -0
- package/package.json +10 -6
- package/src/state-directory.ts +55 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
|
|
4
|
+
// src/git-config.ts
|
|
5
|
+
var DEFAULT_GIT_BRANCH = "pi-sync";
|
|
6
|
+
var DEFAULT_GIT_DIRECTORY = "pi-sync";
|
|
7
|
+
function normalizeGitRemote(value) {
|
|
8
|
+
const normalized = normalizeOptionalString(value);
|
|
9
|
+
if (!normalized) return void 0;
|
|
10
|
+
if (hasControlCharacter(normalized) || /\s/u.test(normalized) || normalized.startsWith("-") || normalized.includes("\\")) {
|
|
11
|
+
throw new Error("Invalid pi-sync Git remote.");
|
|
12
|
+
}
|
|
13
|
+
if (!normalized.includes("://") && /^(?:[A-Za-z0-9._-]+@)?(?:\[[0-9A-Fa-f:]+\]|[A-Za-z0-9.-]+):(?!-)[^:].+$/u.test(normalized)) {
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(normalized) && !normalized.includes("://")) {
|
|
17
|
+
throw new Error("Invalid pi-sync Git remote: unsupported transport or remote-helper syntax.");
|
|
18
|
+
}
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(normalized);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error("Invalid pi-sync Git remote: use an SSH or HTTPS remote.");
|
|
24
|
+
}
|
|
25
|
+
if (url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
26
|
+
throw new Error("Invalid pi-sync Git remote: only SSH and HTTPS are supported.");
|
|
27
|
+
}
|
|
28
|
+
if (url.password || url.protocol === "https:" && url.username) {
|
|
29
|
+
throw new Error("Invalid pi-sync Git remote: URL credentials or userinfo are not allowed.");
|
|
30
|
+
}
|
|
31
|
+
if (url.search || url.hash || !url.hostname || !url.pathname || url.pathname === "/") {
|
|
32
|
+
throw new Error("Invalid pi-sync Git remote.");
|
|
33
|
+
}
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
function normalizeGitRemoteIdentity(value) {
|
|
37
|
+
const normalized = normalizeGitRemote(value);
|
|
38
|
+
if (!normalized) return "";
|
|
39
|
+
if (!normalized.includes("://")) {
|
|
40
|
+
const match = /^(?<user>[A-Za-z0-9._-]+@)?(?<host>\[[0-9A-Fa-f:]+\]|[A-Za-z0-9.-]+):(?<path>.+)$/u.exec(
|
|
41
|
+
normalized
|
|
42
|
+
);
|
|
43
|
+
if (!match?.groups) return normalized;
|
|
44
|
+
const userAndHost = `${match.groups.user ?? ""}${match.groups.host.toLowerCase()}`;
|
|
45
|
+
const remotePath = match.groups.path.replace(/\/+$/gu, "");
|
|
46
|
+
return remotePath.startsWith("/") ? `scp-absolute://${userAndHost}${remotePath}` : `ssh://${userAndHost}/${remotePath}`;
|
|
47
|
+
}
|
|
48
|
+
const url = new URL(normalized);
|
|
49
|
+
url.hostname = url.hostname.toLowerCase();
|
|
50
|
+
if (url.protocol === "ssh:" && url.port === "22") url.port = "";
|
|
51
|
+
url.pathname = url.pathname.replace(/\/+$/gu, "");
|
|
52
|
+
if (url.protocol === "ssh:") {
|
|
53
|
+
return `ssh://${url.username ? `${url.username}@` : ""}${url.host}${url.pathname}`;
|
|
54
|
+
}
|
|
55
|
+
return url.toString();
|
|
56
|
+
}
|
|
57
|
+
function normalizeGitBranch(value) {
|
|
58
|
+
const branch = normalizeOptionalString(value) ?? DEFAULT_GIT_BRANCH;
|
|
59
|
+
if (branch === "@" || branch.startsWith("-") || branch.startsWith("refs/") || branch.startsWith("/") || branch.endsWith("/") || branch.endsWith(".") || branch.includes("..") || branch.includes("@{") || branch.includes("\\") || hasControlCharacter(branch) || /[ ~^:?*[\]]/u.test(branch) || branch.split("/").some((segment) => !segment || segment.startsWith(".") || segment.endsWith(".lock"))) {
|
|
60
|
+
throw new Error("Invalid pi-sync Git branch.");
|
|
61
|
+
}
|
|
62
|
+
return branch;
|
|
63
|
+
}
|
|
64
|
+
function normalizeGitDirectory(value) {
|
|
65
|
+
const directory = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_GIT_DIRECTORY);
|
|
66
|
+
if (!directory || directory.startsWith("-") || directory.includes("\\") || hasControlCharacter(directory) || directory.split("/").some((segment) => !segment || segment === "." || segment === ".." || segment === ".git")) {
|
|
67
|
+
throw new Error("Invalid pi-sync Git directory.");
|
|
68
|
+
}
|
|
69
|
+
return directory;
|
|
70
|
+
}
|
|
71
|
+
function validateGitNamespace(value) {
|
|
72
|
+
if (value.length > 256 || value === "." || value === ".." || value.includes("/") || value.includes("\\") || hasControlCharacter(value)) {
|
|
73
|
+
throw new Error("Invalid pi-sync Git namespace.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function normalizeOptionalString(value) {
|
|
77
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
78
|
+
throw new Error("Invalid pi-sync settings: expected a string.");
|
|
79
|
+
}
|
|
80
|
+
const normalized = value?.trim();
|
|
81
|
+
return normalized || void 0;
|
|
82
|
+
}
|
|
83
|
+
function trimSlashes(value) {
|
|
84
|
+
return value.replace(/^\/+|\/+$/gu, "");
|
|
85
|
+
}
|
|
86
|
+
function hasControlCharacter(value) {
|
|
87
|
+
return [...value].some((character) => {
|
|
88
|
+
const code = character.codePointAt(0) ?? 0;
|
|
89
|
+
return code < 32 || code >= 127 && code <= 159;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/state-directory.ts
|
|
94
|
+
import { lstatSync } from "node:fs";
|
|
95
|
+
import fs from "node:fs/promises";
|
|
96
|
+
import path from "node:path";
|
|
97
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
98
|
+
import lockfile from "proper-lockfile";
|
|
99
|
+
|
|
100
|
+
// src/lock-policy.ts
|
|
101
|
+
var LOCK_GUARD_STALE_MS = 3e4;
|
|
102
|
+
var LOCK_GUARD_UPDATE_MS = 1e4;
|
|
103
|
+
|
|
104
|
+
// src/lockfile-fs.ts
|
|
105
|
+
import {
|
|
106
|
+
mkdir,
|
|
107
|
+
mkdirSync,
|
|
108
|
+
realpath,
|
|
109
|
+
realpathSync,
|
|
110
|
+
rmdir,
|
|
111
|
+
rmdirSync,
|
|
112
|
+
stat,
|
|
113
|
+
statSync,
|
|
114
|
+
utimes,
|
|
115
|
+
utimesSync
|
|
116
|
+
} from "node:fs";
|
|
117
|
+
var LOCKFILE_FS_ADAPTER = {
|
|
118
|
+
mkdir,
|
|
119
|
+
mkdirSync,
|
|
120
|
+
realpath,
|
|
121
|
+
realpathSync,
|
|
122
|
+
rmdir,
|
|
123
|
+
rmdirSync,
|
|
124
|
+
stat,
|
|
125
|
+
statSync,
|
|
126
|
+
utimes,
|
|
127
|
+
utimesSync
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/state-directory.ts
|
|
131
|
+
var CANONICAL_DIRECTORY_NAME = "pi-sync";
|
|
132
|
+
var LEGACY_DIRECTORY_NAME = ".pisync";
|
|
133
|
+
var MIGRATION_LOCK_NAME = ".pi-sync-state-migration.lock";
|
|
134
|
+
var MIGRATION_LOCK_STALE_MS = 3e4;
|
|
135
|
+
var MIGRATION_LOCK_UPDATE_MS = 1e4;
|
|
136
|
+
var sharedStateDirectoryGuards = /* @__PURE__ */ new Map();
|
|
137
|
+
function stateDir() {
|
|
138
|
+
const roots = inspectStateRoots();
|
|
139
|
+
if (roots.canonical) return canonicalStateDir();
|
|
140
|
+
if (roots.legacy) return legacyStateDir();
|
|
141
|
+
return canonicalStateDir();
|
|
142
|
+
}
|
|
143
|
+
function legacyStateDir() {
|
|
144
|
+
return path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);
|
|
145
|
+
}
|
|
146
|
+
function stateDirectoryMigrationNotice() {
|
|
147
|
+
const roots = inspectStateRoots();
|
|
148
|
+
if (!roots.legacy) return void 0;
|
|
149
|
+
return "Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.";
|
|
150
|
+
}
|
|
151
|
+
async function withStateDirectoryAccess(fn) {
|
|
152
|
+
const roots = inspectStateRoots();
|
|
153
|
+
if (!roots.legacy) return fn();
|
|
154
|
+
return runWithStateDirectoryGuard(await acquireSharedStateDirectoryGuard(), fn);
|
|
155
|
+
}
|
|
156
|
+
async function migrateLegacyStateDirectory() {
|
|
157
|
+
const initial = inspectStateRoots();
|
|
158
|
+
if (initial.canonical || !initial.legacy) return { status: "ready" };
|
|
159
|
+
let guard;
|
|
160
|
+
try {
|
|
161
|
+
guard = await acquireStateDirectoryGuard();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!isLockHeldError(error)) throw error;
|
|
164
|
+
return {
|
|
165
|
+
status: "deferred",
|
|
166
|
+
message: "pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry."
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return runWithStateDirectoryGuard(guard, async () => {
|
|
170
|
+
const roots = inspectStateRoots();
|
|
171
|
+
if (roots.canonical || !roots.legacy) return { status: "ready" };
|
|
172
|
+
const legacyLock = path.join(legacyStateDir(), "lock");
|
|
173
|
+
const legacyGuardHeld = await lockfile.check(legacyLock, {
|
|
174
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
175
|
+
lockfilePath: `${legacyLock}.guard`,
|
|
176
|
+
realpath: false,
|
|
177
|
+
stale: LOCK_GUARD_STALE_MS
|
|
178
|
+
});
|
|
179
|
+
if (legacyGuardHeld || await pathExists(legacyLock)) {
|
|
180
|
+
return {
|
|
181
|
+
status: "deferred",
|
|
182
|
+
message: "pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry."
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
guard.throwIfCompromised();
|
|
186
|
+
await fs.rename(legacyStateDir(), canonicalStateDir());
|
|
187
|
+
guard.throwIfCompromised();
|
|
188
|
+
inspectStateRoots();
|
|
189
|
+
return {
|
|
190
|
+
status: "migrated",
|
|
191
|
+
message: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function acquireSharedStateDirectoryGuard() {
|
|
196
|
+
const key = migrationLockPath();
|
|
197
|
+
for (; ; ) {
|
|
198
|
+
let shared = sharedStateDirectoryGuards.get(key);
|
|
199
|
+
if (shared?.closing) {
|
|
200
|
+
await shared.closing;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!shared) {
|
|
204
|
+
shared = { guard: acquireStateDirectoryGuard(), users: 0 };
|
|
205
|
+
sharedStateDirectoryGuards.set(key, shared);
|
|
206
|
+
}
|
|
207
|
+
shared.users += 1;
|
|
208
|
+
let guard;
|
|
209
|
+
try {
|
|
210
|
+
guard = await shared.guard;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (sharedStateDirectoryGuards.get(key) === shared) {
|
|
213
|
+
sharedStateDirectoryGuards.delete(key);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
let released = false;
|
|
218
|
+
return {
|
|
219
|
+
throwIfCompromised: guard.throwIfCompromised,
|
|
220
|
+
release: async () => {
|
|
221
|
+
if (released) return;
|
|
222
|
+
released = true;
|
|
223
|
+
shared.users -= 1;
|
|
224
|
+
if (shared.users > 0) return;
|
|
225
|
+
const closing = guard.release();
|
|
226
|
+
shared.closing = closing;
|
|
227
|
+
try {
|
|
228
|
+
await closing;
|
|
229
|
+
} finally {
|
|
230
|
+
if (sharedStateDirectoryGuards.get(key) === shared) {
|
|
231
|
+
sharedStateDirectoryGuards.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function acquireStateDirectoryGuard() {
|
|
239
|
+
let compromisedError;
|
|
240
|
+
const release = await lockfile.lock(getAgentDir(), {
|
|
241
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
242
|
+
lockfilePath: migrationLockPath(),
|
|
243
|
+
realpath: false,
|
|
244
|
+
stale: MIGRATION_LOCK_STALE_MS,
|
|
245
|
+
update: MIGRATION_LOCK_UPDATE_MS,
|
|
246
|
+
retries: { retries: 20, minTimeout: 10, maxTimeout: 50 },
|
|
247
|
+
onCompromised: (error) => {
|
|
248
|
+
compromisedError = error;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
release,
|
|
253
|
+
throwIfCompromised: () => {
|
|
254
|
+
if (compromisedError) throw compromisedError;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function runWithStateDirectoryGuard(guard, fn) {
|
|
259
|
+
let result;
|
|
260
|
+
let failed = false;
|
|
261
|
+
let failure;
|
|
262
|
+
try {
|
|
263
|
+
guard.throwIfCompromised();
|
|
264
|
+
result = await fn();
|
|
265
|
+
guard.throwIfCompromised();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
failed = true;
|
|
268
|
+
failure = error;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await guard.release();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!failed) {
|
|
274
|
+
failed = true;
|
|
275
|
+
failure = error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (failed) throw failure;
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
function isLockHeldError(error) {
|
|
282
|
+
return error.code === "ELOCKED";
|
|
283
|
+
}
|
|
284
|
+
function canonicalStateDir() {
|
|
285
|
+
return path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);
|
|
286
|
+
}
|
|
287
|
+
function migrationLockPath() {
|
|
288
|
+
return path.join(getAgentDir(), MIGRATION_LOCK_NAME);
|
|
289
|
+
}
|
|
290
|
+
function inspectStateRoots() {
|
|
291
|
+
const canonical = isDirectoryRoot(canonicalStateDir(), "canonical");
|
|
292
|
+
const legacy = isDirectoryRoot(legacyStateDir(), "legacy");
|
|
293
|
+
if (canonical && legacy) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return { canonical, legacy };
|
|
299
|
+
}
|
|
300
|
+
function isDirectoryRoot(directory, label) {
|
|
301
|
+
try {
|
|
302
|
+
const entry = lstatSync(directory);
|
|
303
|
+
if (entry.isSymbolicLink()) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (!entry.isDirectory()) {
|
|
309
|
+
throw new Error(`${label} pi-sync state path is not a directory: ${directory}`);
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error.code === "ENOENT") return false;
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function pathExists(filePath) {
|
|
318
|
+
try {
|
|
319
|
+
await fs.lstat(filePath);
|
|
320
|
+
return true;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error.code === "ENOENT") return false;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export {
|
|
328
|
+
normalizeGitRemote,
|
|
329
|
+
normalizeGitRemoteIdentity,
|
|
330
|
+
normalizeGitBranch,
|
|
331
|
+
normalizeGitDirectory,
|
|
332
|
+
validateGitNamespace,
|
|
333
|
+
LOCK_GUARD_STALE_MS,
|
|
334
|
+
LOCK_GUARD_UPDATE_MS,
|
|
335
|
+
LOCKFILE_FS_ADAPTER,
|
|
336
|
+
stateDir,
|
|
337
|
+
stateDirectoryMigrationNotice,
|
|
338
|
+
withStateDirectoryAccess,
|
|
339
|
+
migrateLegacyStateDirectory
|
|
340
|
+
};
|
|
341
|
+
//# sourceMappingURL=chunk-F5QL2GPY.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/git-config.ts", "../../src/state-directory.ts", "../../src/lock-policy.ts", "../../src/lockfile-fs.ts"],
|
|
4
|
+
"sourcesContent": ["const DEFAULT_GIT_BRANCH = \"pi-sync\";\nconst DEFAULT_GIT_DIRECTORY = \"pi-sync\";\n\nexport function normalizeGitRemote(value: string | undefined) {\n\tconst normalized = normalizeOptionalString(value);\n\tif (!normalized) return undefined;\n\tif (\n\t\thasControlCharacter(normalized) ||\n\t\t/\\s/u.test(normalized) ||\n\t\tnormalized.startsWith(\"-\") ||\n\t\tnormalized.includes(\"\\\\\")\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync Git remote.\");\n\t}\n\tif (\n\t\t!normalized.includes(\"://\") &&\n\t\t/^(?:[A-Za-z0-9._-]+@)?(?:\\[[0-9A-Fa-f:]+\\]|[A-Za-z0-9.-]+):(?!-)[^:].+$/u.test(normalized)\n\t) {\n\t\treturn normalized;\n\t}\n\tif (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(normalized) && !normalized.includes(\"://\")) {\n\t\tthrow new Error(\"Invalid pi-sync Git remote: unsupported transport or remote-helper syntax.\");\n\t}\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(normalized);\n\t} catch {\n\t\tthrow new Error(\"Invalid pi-sync Git remote: use an SSH or HTTPS remote.\");\n\t}\n\tif (url.protocol !== \"https:\" && url.protocol !== \"ssh:\") {\n\t\tthrow new Error(\"Invalid pi-sync Git remote: only SSH and HTTPS are supported.\");\n\t}\n\tif (url.password || (url.protocol === \"https:\" && url.username)) {\n\t\tthrow new Error(\"Invalid pi-sync Git remote: URL credentials or userinfo are not allowed.\");\n\t}\n\tif (url.search || url.hash || !url.hostname || !url.pathname || url.pathname === \"/\") {\n\t\tthrow new Error(\"Invalid pi-sync Git remote.\");\n\t}\n\treturn url.toString();\n}\n\nexport function normalizeGitRemoteIdentity(value: string) {\n\tconst normalized = normalizeGitRemote(value);\n\tif (!normalized) return \"\";\n\tif (!normalized.includes(\"://\")) {\n\t\tconst match =\n\t\t\t/^(?<user>[A-Za-z0-9._-]+@)?(?<host>\\[[0-9A-Fa-f:]+\\]|[A-Za-z0-9.-]+):(?<path>.+)$/u.exec(\n\t\t\t\tnormalized,\n\t\t\t);\n\t\tif (!match?.groups) return normalized;\n\t\tconst userAndHost = `${match.groups.user ?? \"\"}${match.groups.host.toLowerCase()}`;\n\t\tconst remotePath = match.groups.path.replace(/\\/+$/gu, \"\");\n\t\treturn remotePath.startsWith(\"/\")\n\t\t\t? `scp-absolute://${userAndHost}${remotePath}`\n\t\t\t: `ssh://${userAndHost}/${remotePath}`;\n\t}\n\tconst url = new URL(normalized);\n\turl.hostname = url.hostname.toLowerCase();\n\tif (url.protocol === \"ssh:\" && url.port === \"22\") url.port = \"\";\n\turl.pathname = url.pathname.replace(/\\/+$/gu, \"\");\n\tif (url.protocol === \"ssh:\") {\n\t\treturn `ssh://${url.username ? `${url.username}@` : \"\"}${url.host}${url.pathname}`;\n\t}\n\treturn url.toString();\n}\n\nexport function normalizeGitBranch(value: string | undefined) {\n\tconst branch = normalizeOptionalString(value) ?? DEFAULT_GIT_BRANCH;\n\tif (\n\t\tbranch === \"@\" ||\n\t\tbranch.startsWith(\"-\") ||\n\t\tbranch.startsWith(\"refs/\") ||\n\t\tbranch.startsWith(\"/\") ||\n\t\tbranch.endsWith(\"/\") ||\n\t\tbranch.endsWith(\".\") ||\n\t\tbranch.includes(\"..\") ||\n\t\tbranch.includes(\"@{\") ||\n\t\tbranch.includes(\"\\\\\") ||\n\t\thasControlCharacter(branch) ||\n\t\t/[ ~^:?*[\\]]/u.test(branch) ||\n\t\tbranch\n\t\t\t.split(\"/\")\n\t\t\t.some((segment) => !segment || segment.startsWith(\".\") || segment.endsWith(\".lock\"))\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync Git branch.\");\n\t}\n\treturn branch;\n}\n\nexport function normalizeGitDirectory(value: string | undefined) {\n\tconst directory = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_GIT_DIRECTORY);\n\tif (\n\t\t!directory ||\n\t\tdirectory.startsWith(\"-\") ||\n\t\tdirectory.includes(\"\\\\\") ||\n\t\thasControlCharacter(directory) ||\n\t\tdirectory\n\t\t\t.split(\"/\")\n\t\t\t.some((segment) => !segment || segment === \".\" || segment === \"..\" || segment === \".git\")\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync Git directory.\");\n\t}\n\treturn directory;\n}\n\nexport function validateGitNamespace(value: string) {\n\tif (\n\t\tvalue.length > 256 ||\n\t\tvalue === \".\" ||\n\t\tvalue === \"..\" ||\n\t\tvalue.includes(\"/\") ||\n\t\tvalue.includes(\"\\\\\") ||\n\t\thasControlCharacter(value)\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync Git namespace.\");\n\t}\n}\n\nfunction normalizeOptionalString(value: unknown) {\n\tif (value !== undefined && typeof value !== \"string\") {\n\t\tthrow new Error(\"Invalid pi-sync settings: expected a string.\");\n\t}\n\tconst normalized = value?.trim();\n\treturn normalized || undefined;\n}\n\nfunction trimSlashes(value: string) {\n\treturn value.replace(/^\\/+|\\/+$/gu, \"\");\n}\n\nfunction hasControlCharacter(value: string) {\n\treturn [...value].some((character) => {\n\t\tconst code = character.codePointAt(0) ?? 0;\n\t\treturn code < 0x20 || (code >= 0x7f && code <= 0x9f);\n\t});\n}\n", "import { lstatSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getAgentDir } from \"@earendil-works/pi-coding-agent\";\nimport lockfile from \"proper-lockfile\";\nimport { LOCK_GUARD_STALE_MS } from \"./lock-policy.js\";\nimport { LOCKFILE_FS_ADAPTER } from \"./lockfile-fs.js\";\n\nconst CANONICAL_DIRECTORY_NAME = \"pi-sync\";\nconst LEGACY_DIRECTORY_NAME = \".pisync\";\nconst MIGRATION_LOCK_NAME = \".pi-sync-state-migration.lock\";\nconst MIGRATION_LOCK_STALE_MS = 30_000;\nconst MIGRATION_LOCK_UPDATE_MS = 10_000;\n\nexport type StateDirectoryPreparation =\n\t| { status: \"ready\" }\n\t| { status: \"migrated\"; message: string }\n\t| { status: \"deferred\"; message: string };\n\ninterface StateDirectoryGuard {\n\trelease: () => Promise<void>;\n\tthrowIfCompromised: () => void;\n}\n\ninterface SharedStateDirectoryGuard {\n\tguard: Promise<StateDirectoryGuard>;\n\tusers: number;\n\tclosing?: Promise<void>;\n}\n\nconst sharedStateDirectoryGuards = new Map<string, SharedStateDirectoryGuard>();\n\nexport function stateDir() {\n\tconst roots = inspectStateRoots();\n\tif (roots.canonical) return canonicalStateDir();\n\tif (roots.legacy) return legacyStateDir();\n\treturn canonicalStateDir();\n}\n\nexport function legacyStateDir() {\n\treturn path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);\n}\n\nexport function stateDirectoryMigrationNotice() {\n\tconst roots = inspectStateRoots();\n\tif (!roots.legacy) return undefined;\n\treturn \"Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.\";\n}\n\nexport async function withStateDirectoryAccess<T>(fn: () => Promise<T>): Promise<T> {\n\tconst roots = inspectStateRoots();\n\tif (!roots.legacy) return fn();\n\treturn runWithStateDirectoryGuard(await acquireSharedStateDirectoryGuard(), fn);\n}\n\nexport async function migrateLegacyStateDirectory(): Promise<StateDirectoryPreparation> {\n\tconst initial = inspectStateRoots();\n\tif (initial.canonical || !initial.legacy) return { status: \"ready\" };\n\n\tlet guard: StateDirectoryGuard;\n\ttry {\n\t\tguard = await acquireStateDirectoryGuard();\n\t} catch (error) {\n\t\tif (!isLockHeldError(error)) throw error;\n\t\treturn {\n\t\t\tstatus: \"deferred\",\n\t\t\tmessage:\n\t\t\t\t\"pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry.\",\n\t\t};\n\t}\n\treturn runWithStateDirectoryGuard(guard, async () => {\n\t\tconst roots = inspectStateRoots();\n\t\tif (roots.canonical || !roots.legacy) return { status: \"ready\" };\n\n\t\tconst legacyLock = path.join(legacyStateDir(), \"lock\");\n\t\tconst legacyGuardHeld = await lockfile.check(legacyLock, {\n\t\t\tfs: LOCKFILE_FS_ADAPTER,\n\t\t\tlockfilePath: `${legacyLock}.guard`,\n\t\t\trealpath: false,\n\t\t\tstale: LOCK_GUARD_STALE_MS,\n\t\t});\n\t\tif (legacyGuardHeld || (await pathExists(legacyLock))) {\n\t\t\treturn {\n\t\t\t\tstatus: \"deferred\",\n\t\t\t\tmessage:\n\t\t\t\t\t\"pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry.\",\n\t\t\t};\n\t\t}\n\t\tguard.throwIfCompromised();\n\t\tawait fs.rename(legacyStateDir(), canonicalStateDir());\n\t\tguard.throwIfCompromised();\n\t\tinspectStateRoots();\n\t\treturn {\n\t\t\tstatus: \"migrated\",\n\t\t\tmessage: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`,\n\t\t};\n\t});\n}\n\nasync function acquireSharedStateDirectoryGuard(): Promise<StateDirectoryGuard> {\n\tconst key = migrationLockPath();\n\tfor (;;) {\n\t\tlet shared = sharedStateDirectoryGuards.get(key);\n\t\tif (shared?.closing) {\n\t\t\tawait shared.closing;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!shared) {\n\t\t\tshared = { guard: acquireStateDirectoryGuard(), users: 0 };\n\t\t\tsharedStateDirectoryGuards.set(key, shared);\n\t\t}\n\t\tshared.users += 1;\n\t\tlet guard: StateDirectoryGuard;\n\t\ttry {\n\t\t\tguard = await shared.guard;\n\t\t} catch (error) {\n\t\t\tif (sharedStateDirectoryGuards.get(key) === shared) {\n\t\t\t\tsharedStateDirectoryGuards.delete(key);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tlet released = false;\n\t\treturn {\n\t\t\tthrowIfCompromised: guard.throwIfCompromised,\n\t\t\trelease: async () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tshared.users -= 1;\n\t\t\t\tif (shared.users > 0) return;\n\t\t\t\tconst closing = guard.release();\n\t\t\t\tshared.closing = closing;\n\t\t\t\ttry {\n\t\t\t\t\tawait closing;\n\t\t\t\t} finally {\n\t\t\t\t\tif (sharedStateDirectoryGuards.get(key) === shared) {\n\t\t\t\t\t\tsharedStateDirectoryGuards.delete(key);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n}\n\nasync function acquireStateDirectoryGuard(): Promise<StateDirectoryGuard> {\n\tlet compromisedError: Error | undefined;\n\tconst release = await lockfile.lock(getAgentDir(), {\n\t\tfs: LOCKFILE_FS_ADAPTER,\n\t\tlockfilePath: migrationLockPath(),\n\t\trealpath: false,\n\t\tstale: MIGRATION_LOCK_STALE_MS,\n\t\tupdate: MIGRATION_LOCK_UPDATE_MS,\n\t\tretries: { retries: 20, minTimeout: 10, maxTimeout: 50 },\n\t\tonCompromised: (error) => {\n\t\t\tcompromisedError = error;\n\t\t},\n\t});\n\treturn {\n\t\trelease,\n\t\tthrowIfCompromised: () => {\n\t\t\tif (compromisedError) throw compromisedError;\n\t\t},\n\t};\n}\n\nasync function runWithStateDirectoryGuard<T>(guard: StateDirectoryGuard, fn: () => Promise<T>) {\n\tlet result: T | undefined;\n\tlet failed = false;\n\tlet failure: unknown;\n\ttry {\n\t\tguard.throwIfCompromised();\n\t\tresult = await fn();\n\t\tguard.throwIfCompromised();\n\t} catch (error) {\n\t\tfailed = true;\n\t\tfailure = error;\n\t}\n\ttry {\n\t\tawait guard.release();\n\t} catch (error) {\n\t\tif (!failed) {\n\t\t\tfailed = true;\n\t\t\tfailure = error;\n\t\t}\n\t}\n\tif (failed) throw failure;\n\treturn result as T;\n}\n\nfunction isLockHeldError(error: unknown) {\n\treturn (error as NodeJS.ErrnoException).code === \"ELOCKED\";\n}\n\nfunction canonicalStateDir() {\n\treturn path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);\n}\n\nfunction migrationLockPath() {\n\treturn path.join(getAgentDir(), MIGRATION_LOCK_NAME);\n}\n\nfunction inspectStateRoots() {\n\tconst canonical = isDirectoryRoot(canonicalStateDir(), \"canonical\");\n\tconst legacy = isDirectoryRoot(legacyStateDir(), \"legacy\");\n\tif (canonical && legacy) {\n\t\tthrow new Error(\n\t\t\t`Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`,\n\t\t);\n\t}\n\treturn { canonical, legacy };\n}\n\nfunction isDirectoryRoot(directory: string, label: string) {\n\ttry {\n\t\tconst entry = lstatSync(directory);\n\t\tif (entry.isSymbolicLink()) {\n\t\t\tthrow new Error(\n\t\t\t\t`Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`,\n\t\t\t);\n\t\t}\n\t\tif (!entry.isDirectory()) {\n\t\t\tthrow new Error(`${label} pi-sync state path is not a directory: ${directory}`);\n\t\t}\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n\t\tthrow error;\n\t}\n}\n\nasync function pathExists(filePath: string) {\n\ttry {\n\t\tawait fs.lstat(filePath);\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n\t\tthrow error;\n\t}\n}\n", "export const LOCK_GUARD_STALE_MS = 30_000;\nexport const LOCK_GUARD_UPDATE_MS = 10_000;\n", "import {\n\tmkdir,\n\tmkdirSync,\n\trealpath,\n\trealpathSync,\n\trmdir,\n\trmdirSync,\n\tstat,\n\tstatSync,\n\tutimes,\n\tutimesSync,\n} from \"node:fs\";\n\nexport const LOCKFILE_FS_ADAPTER = {\n\tmkdir,\n\tmkdirSync,\n\trealpath,\n\trealpathSync,\n\trmdir,\n\trmdirSync,\n\tstat,\n\tstatSync,\n\tutimes,\n\tutimesSync,\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
safeTerminalText
|
|
5
|
+
} from "./chunk-W64NEHXT.js";
|
|
6
|
+
import {
|
|
7
|
+
syncConfigReviewFingerprint
|
|
8
|
+
} from "./chunk-APZVBSM6.js";
|
|
9
|
+
import {
|
|
10
|
+
inspectRemoteSelection,
|
|
11
|
+
remoteSelectionMismatch,
|
|
12
|
+
sameSyncInclude,
|
|
13
|
+
snapshotSelectionInclude
|
|
14
|
+
} from "./chunk-YQ6UW7IF.js";
|
|
15
|
+
|
|
16
|
+
// src/backend-factory.ts
|
|
17
|
+
var createSyncBackend = async (config) => {
|
|
18
|
+
switch (config.backend.type) {
|
|
19
|
+
case "s3": {
|
|
20
|
+
const { S3SyncBackend } = await import("./s3-backend-3QZGO2EF.js");
|
|
21
|
+
return new S3SyncBackend(config.backend);
|
|
22
|
+
}
|
|
23
|
+
case "webdav": {
|
|
24
|
+
const { WebDavSyncBackend } = await import("./webdav-backend-ZD5RHRIU.js");
|
|
25
|
+
return new WebDavSyncBackend(config.backend);
|
|
26
|
+
}
|
|
27
|
+
case "git": {
|
|
28
|
+
const { GitSyncBackend } = await import("./git-backend-E3UOUUOM.js");
|
|
29
|
+
return new GitSyncBackend(config.backend);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/remote-snapshot.ts
|
|
35
|
+
async function readSnapshotForHead(backend, head, signal) {
|
|
36
|
+
const snapshot = await backend.readSnapshot(head.snapshotRef, signal);
|
|
37
|
+
if (signal?.aborted) {
|
|
38
|
+
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
39
|
+
}
|
|
40
|
+
if (snapshot.id !== head.snapshotId) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (head.selection) {
|
|
46
|
+
const snapshotInclude = snapshotSelectionInclude(snapshot);
|
|
47
|
+
if (!snapshotInclude || !sameSyncInclude(head.selection.include, snapshotInclude)) {
|
|
48
|
+
throw new Error("Remote head selection does not match its immutable snapshot.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return snapshot;
|
|
52
|
+
}
|
|
53
|
+
function requireCompatibleRemoteSelection(config, snapshot) {
|
|
54
|
+
const state = inspectRemoteSelection(config.include, snapshot);
|
|
55
|
+
if (state.kind === "different") {
|
|
56
|
+
throw remoteSelectionMismatch(config, state.include, syncConfigReviewFingerprint(config));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatRemoteSelectionStatus(state) {
|
|
60
|
+
if (!state) return "remote included content: unavailable (remote is empty)";
|
|
61
|
+
if (state.kind === "same") return "remote included content: matches this setup";
|
|
62
|
+
if (state.kind === "legacy") {
|
|
63
|
+
return `remote included content: unavailable in legacy snapshot (${state.discovered.length} path${state.discovered.length === 1 ? "" : "s"} discovered, partial)`;
|
|
64
|
+
}
|
|
65
|
+
return [
|
|
66
|
+
"remote included content: differs from this setup",
|
|
67
|
+
`remote-only selection: ${safeList(state.remoteOnly)}`,
|
|
68
|
+
`local-only selection: ${safeList(state.localOnly)}`
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
function safeList(values) {
|
|
72
|
+
return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
createSyncBackend,
|
|
77
|
+
readSnapshotForHead,
|
|
78
|
+
requireCompatibleRemoteSelection,
|
|
79
|
+
formatRemoteSelectionStatus
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=chunk-FR6CVXAL.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backend-factory.ts", "../../src/remote-snapshot.ts"],
|
|
4
|
+
"sourcesContent": ["import type { SyncBackend } from \"./sync-backend.js\";\nimport type { AnySyncConfig } from \"./types.js\";\n\nexport type SyncBackendFactory = {\n\tbivarianceHack(config: AnySyncConfig): SyncBackend | Promise<SyncBackend>;\n}[\"bivarianceHack\"];\n\nexport const createSyncBackend: SyncBackendFactory = async (config) => {\n\tswitch (config.backend.type) {\n\t\tcase \"s3\": {\n\t\t\tconst { S3SyncBackend } = await import(\"./s3-backend.js\");\n\t\t\treturn new S3SyncBackend(config.backend);\n\t\t}\n\t\tcase \"webdav\": {\n\t\t\tconst { WebDavSyncBackend } = await import(\"./webdav-backend.js\");\n\t\t\treturn new WebDavSyncBackend(config.backend);\n\t\t}\n\t\tcase \"git\": {\n\t\t\tconst { GitSyncBackend } = await import(\"./git-backend.js\");\n\t\t\treturn new GitSyncBackend(config.backend);\n\t\t}\n\t}\n};\n", "import { syncConfigReviewFingerprint } from \"./config.js\";\nimport type { RemoteHead, SyncBackend } from \"./sync-backend.js\";\nimport { safeTerminalText } from \"./sync-format.js\";\nimport {\n\tinspectRemoteSelection,\n\ttype RemoteSelectionState,\n\tremoteSelectionMismatch,\n\tsameSyncInclude,\n\tsnapshotSelectionInclude,\n} from \"./sync-policy.js\";\nimport type { AnySyncConfig, Snapshot } from \"./types.js\";\n\nexport async function readSnapshotForHead(\n\tbackend: SyncBackend,\n\thead: RemoteHead,\n\tsignal?: AbortSignal,\n) {\n\tconst snapshot = await backend.readSnapshot(head.snapshotRef, signal);\n\tif (signal?.aborted) {\n\t\tthrow signal.reason instanceof Error\n\t\t\t? signal.reason\n\t\t\t: new DOMException(\"The operation was aborted\", \"AbortError\");\n\t}\n\tif (snapshot.id !== head.snapshotId) {\n\t\tthrow new Error(\n\t\t\t`Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`,\n\t\t);\n\t}\n\tif (head.selection) {\n\t\tconst snapshotInclude = snapshotSelectionInclude(snapshot);\n\t\tif (!snapshotInclude || !sameSyncInclude(head.selection.include, snapshotInclude)) {\n\t\t\tthrow new Error(\"Remote head selection does not match its immutable snapshot.\");\n\t\t}\n\t}\n\treturn snapshot;\n}\n\nexport function requireCompatibleRemoteSelection(config: AnySyncConfig, snapshot: Snapshot) {\n\tconst state = inspectRemoteSelection(config.include, snapshot);\n\tif (state.kind === \"different\") {\n\t\tthrow remoteSelectionMismatch(config, state.include, syncConfigReviewFingerprint(config));\n\t}\n}\n\nexport function formatRemoteSelectionStatus(state: RemoteSelectionState | undefined) {\n\tif (!state) return \"remote included content: unavailable (remote is empty)\";\n\tif (state.kind === \"same\") return \"remote included content: matches this setup\";\n\tif (state.kind === \"legacy\") {\n\t\treturn `remote included content: unavailable in legacy snapshot (${state.discovered.length} path${state.discovered.length === 1 ? \"\" : \"s\"} discovered, partial)`;\n\t}\n\treturn [\n\t\t\"remote included content: differs from this setup\",\n\t\t`remote-only selection: ${safeList(state.remoteOnly)}`,\n\t\t`local-only selection: ${safeList(state.localOnly)}`,\n\t].join(\"\\n\");\n}\n\nfunction safeList(values: readonly string[]) {\n\treturn values.length > 0 ? values.map(safeTerminalText).join(\", \") : \"none\";\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;AAOO,IAAM,oBAAwC,OAAO,WAAW;AACtE,UAAQ,OAAO,QAAQ,MAAM;AAAA,IAC5B,KAAK,MAAM;AACV,YAAM,EAAE,cAAc,IAAI,MAAM,OAAO,0BAAiB;AACxD,aAAO,IAAI,cAAc,OAAO,OAAO;AAAA,IACxC;AAAA,IACA,KAAK,UAAU;AACd,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,8BAAqB;AAChE,aAAO,IAAI,kBAAkB,OAAO,OAAO;AAAA,IAC5C;AAAA,IACA,KAAK,OAAO;AACX,YAAM,EAAE,eAAe,IAAI,MAAM,OAAO,2BAAkB;AAC1D,aAAO,IAAI,eAAe,OAAO,OAAO;AAAA,IACzC;AAAA,EACD;AACD;;;ACVA,eAAsB,oBACrB,SACA,MACA,QACC;AACD,QAAM,WAAW,MAAM,QAAQ,aAAa,KAAK,aAAa,MAAM;AACpE,MAAI,QAAQ,SAAS;AACpB,UAAM,OAAO,kBAAkB,QAC5B,OAAO,SACP,IAAI,aAAa,6BAA6B,YAAY;AAAA,EAC9D;AACA,MAAI,SAAS,OAAO,KAAK,YAAY;AACpC,UAAM,IAAI;AAAA,MACT,eAAe,KAAK,UAAU,oCAAoC,SAAS,EAAE;AAAA,IAC9E;AAAA,EACD;AACA,MAAI,KAAK,WAAW;AACnB,UAAM,kBAAkB,yBAAyB,QAAQ;AACzD,QAAI,CAAC,mBAAmB,CAAC,gBAAgB,KAAK,UAAU,SAAS,eAAe,GAAG;AAClF,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAC/E;AAAA,EACD;AACA,SAAO;AACR;AAEO,SAAS,iCAAiC,QAAuB,UAAoB;AAC3F,QAAM,QAAQ,uBAAuB,OAAO,SAAS,QAAQ;AAC7D,MAAI,MAAM,SAAS,aAAa;AAC/B,UAAM,wBAAwB,QAAQ,MAAM,SAAS,4BAA4B,MAAM,CAAC;AAAA,EACzF;AACD;AAEO,SAAS,4BAA4B,OAAyC;AACpF,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,SAAS,OAAQ,QAAO;AAClC,MAAI,MAAM,SAAS,UAAU;AAC5B,WAAO,4DAA4D,MAAM,WAAW,MAAM,QAAQ,MAAM,WAAW,WAAW,IAAI,KAAK,GAAG;AAAA,EAC3I;AACA,SAAO;AAAA,IACN;AAAA,IACA,0BAA0B,SAAS,MAAM,UAAU,CAAC;AAAA,IACpD,yBAAyB,SAAS,MAAM,SAAS,CAAC;AAAA,EACnD,EAAE,KAAK,IAAI;AACZ;AAEA,SAAS,SAAS,QAA2B;AAC5C,SAAO,OAAO,SAAS,IAAI,OAAO,IAAI,gBAAgB,EAAE,KAAK,IAAI,IAAI;AACtE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
snapshotSelectionInclude
|
|
5
|
+
} from "./chunk-YQ6UW7IF.js";
|
|
6
|
+
|
|
7
|
+
// src/snapshot-codec.ts
|
|
8
|
+
import { Readable, Writable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { createGunzip, gzip } from "node:zlib";
|
|
12
|
+
var VERSION = 1;
|
|
13
|
+
var MAX_DECOMPRESSED_SNAPSHOT_BYTES = 512 * 1024 * 1024;
|
|
14
|
+
var gzipAsync = promisify(gzip);
|
|
15
|
+
async function encodeSnapshot(snapshot) {
|
|
16
|
+
snapshotSelectionInclude(snapshot);
|
|
17
|
+
return gzipAsync(Buffer.from(JSON.stringify(snapshot), "utf8"));
|
|
18
|
+
}
|
|
19
|
+
async function decodeSnapshot(buffer, options = {}) {
|
|
20
|
+
throwIfAborted(options.signal);
|
|
21
|
+
const limit = options.maxOutputLength ?? MAX_DECOMPRESSED_SNAPSHOT_BYTES;
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let total = 0;
|
|
24
|
+
const sink = new Writable({
|
|
25
|
+
write(chunk, _encoding, callback) {
|
|
26
|
+
total += chunk.byteLength;
|
|
27
|
+
if (total > limit) {
|
|
28
|
+
callback(new Error(`Decompressed snapshot exceeds the ${limit}-byte limit.`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
chunks.push(Buffer.from(chunk));
|
|
32
|
+
callback();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
await pipeline(Readable.from([buffer]), createGunzip(), sink, { signal: options.signal });
|
|
36
|
+
throwIfAborted(options.signal);
|
|
37
|
+
const parsed = JSON.parse(Buffer.concat(chunks, total).toString("utf8"));
|
|
38
|
+
if (parsed.version !== VERSION || !Array.isArray(parsed.files)) {
|
|
39
|
+
throw new Error("Unsupported snapshot format.");
|
|
40
|
+
}
|
|
41
|
+
snapshotSelectionInclude(parsed);
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
function throwIfAborted(signal) {
|
|
45
|
+
if (!signal?.aborted) return;
|
|
46
|
+
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
encodeSnapshot,
|
|
51
|
+
decodeSnapshot
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=chunk-HGV24P4T.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/snapshot-codec.ts"],
|
|
4
|
+
"sourcesContent": ["import { Readable, Writable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { promisify } from \"node:util\";\nimport { createGunzip, gzip } from \"node:zlib\";\nimport { snapshotSelectionInclude } from \"./sync-policy.js\";\nimport type { Snapshot } from \"./types.js\";\n\nconst VERSION = 1;\nconst MAX_DECOMPRESSED_SNAPSHOT_BYTES = 512 * 1024 * 1024;\nconst gzipAsync = promisify(gzip);\n\nexport async function encodeSnapshot(snapshot: Snapshot) {\n\tsnapshotSelectionInclude(snapshot);\n\treturn gzipAsync(Buffer.from(JSON.stringify(snapshot), \"utf8\"));\n}\n\nexport async function decodeSnapshot(\n\tbuffer: Buffer,\n\toptions: { signal?: AbortSignal; maxOutputLength?: number } = {},\n): Promise<Snapshot> {\n\tthrowIfAborted(options.signal);\n\tconst limit = options.maxOutputLength ?? MAX_DECOMPRESSED_SNAPSHOT_BYTES;\n\tconst chunks: Buffer[] = [];\n\tlet total = 0;\n\tconst sink = new Writable({\n\t\twrite(chunk: Buffer, _encoding, callback) {\n\t\t\ttotal += chunk.byteLength;\n\t\t\tif (total > limit) {\n\t\t\t\tcallback(new Error(`Decompressed snapshot exceeds the ${limit}-byte limit.`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tchunks.push(Buffer.from(chunk));\n\t\t\tcallback();\n\t\t},\n\t});\n\tawait pipeline(Readable.from([buffer]), createGunzip(), sink, { signal: options.signal });\n\tthrowIfAborted(options.signal);\n\tconst parsed = JSON.parse(Buffer.concat(chunks, total).toString(\"utf8\")) as Snapshot;\n\tif (parsed.version !== VERSION || !Array.isArray(parsed.files)) {\n\t\tthrow new Error(\"Unsupported snapshot format.\");\n\t}\n\tsnapshotSelectionInclude(parsed);\n\treturn parsed;\n}\n\nfunction throwIfAborted(signal?: AbortSignal) {\n\tif (!signal?.aborted) return;\n\tthrow signal.reason instanceof Error\n\t\t? signal.reason\n\t\t: new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;AAAA,SAAS,UAAU,gBAAgB;AACnC,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,cAAc,YAAY;AAInC,IAAM,UAAU;AAChB,IAAM,kCAAkC,MAAM,OAAO;AACrD,IAAM,YAAY,UAAU,IAAI;AAEhC,eAAsB,eAAe,UAAoB;AACxD,2BAAyB,QAAQ;AACjC,SAAO,UAAU,OAAO,KAAK,KAAK,UAAU,QAAQ,GAAG,MAAM,CAAC;AAC/D;AAEA,eAAsB,eACrB,QACA,UAA8D,CAAC,GAC3C;AACpB,iBAAe,QAAQ,MAAM;AAC7B,QAAM,QAAQ,QAAQ,mBAAmB;AACzC,QAAM,SAAmB,CAAC;AAC1B,MAAI,QAAQ;AACZ,QAAM,OAAO,IAAI,SAAS;AAAA,IACzB,MAAM,OAAe,WAAW,UAAU;AACzC,eAAS,MAAM;AACf,UAAI,QAAQ,OAAO;AAClB,iBAAS,IAAI,MAAM,qCAAqC,KAAK,cAAc,CAAC;AAC5E;AAAA,MACD;AACA,aAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAC9B,eAAS;AAAA,IACV;AAAA,EACD,CAAC;AACD,QAAM,SAAS,SAAS,KAAK,CAAC,MAAM,CAAC,GAAG,aAAa,GAAG,MAAM,EAAE,QAAQ,QAAQ,OAAO,CAAC;AACxF,iBAAe,QAAQ,MAAM;AAC7B,QAAM,SAAS,KAAK,MAAM,OAAO,OAAO,QAAQ,KAAK,EAAE,SAAS,MAAM,CAAC;AACvE,MAAI,OAAO,YAAY,WAAW,CAAC,MAAM,QAAQ,OAAO,KAAK,GAAG;AAC/D,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAC/C;AACA,2BAAyB,MAAM;AAC/B,SAAO;AACR;AAEA,SAAS,eAAe,QAAsB;AAC7C,MAAI,CAAC,QAAQ,QAAS;AACtB,QAAM,OAAO,kBAAkB,QAC5B,OAAO,SACP,IAAI,aAAa,6BAA6B,YAAY;AAC9D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
safeTerminalText
|
|
5
|
+
} from "./chunk-W64NEHXT.js";
|
|
6
|
+
import {
|
|
7
|
+
syncConfigReviewFingerprint
|
|
8
|
+
} from "./chunk-APZVBSM6.js";
|
|
9
|
+
import {
|
|
10
|
+
compareSyncInclude,
|
|
11
|
+
sameSyncInclude
|
|
12
|
+
} from "./chunk-YQ6UW7IF.js";
|
|
13
|
+
|
|
14
|
+
// src/sync-attention.ts
|
|
15
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
var STATUS_KEY = "sync";
|
|
17
|
+
var WIDGET_KEY = "sync:attention";
|
|
18
|
+
function syncAttentionMatchesConfig(attention, config) {
|
|
19
|
+
return attention.decision.setupName === config.setupName && attention.decision.configIdentity === syncConfigReviewFingerprint(config) && sameSyncInclude(attention.decision.localInclude, config.include);
|
|
20
|
+
}
|
|
21
|
+
function createSyncAttentionController() {
|
|
22
|
+
let state;
|
|
23
|
+
return {
|
|
24
|
+
set(decision, origin) {
|
|
25
|
+
state = { decision, origin, offered: false };
|
|
26
|
+
},
|
|
27
|
+
current() {
|
|
28
|
+
return state;
|
|
29
|
+
},
|
|
30
|
+
markOffered() {
|
|
31
|
+
if (!state || state.offered) return false;
|
|
32
|
+
state = { ...state, offered: true };
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
clear(ctx) {
|
|
36
|
+
state = void 0;
|
|
37
|
+
clearAttentionPresentation(ctx);
|
|
38
|
+
},
|
|
39
|
+
reset(ctx) {
|
|
40
|
+
state = void 0;
|
|
41
|
+
clearAttentionPresentation(ctx);
|
|
42
|
+
},
|
|
43
|
+
publish(ctx) {
|
|
44
|
+
if (!state) {
|
|
45
|
+
clearAttentionPresentation(ctx);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const presentation = attentionPresentation(state.decision);
|
|
49
|
+
ctx.ui.setStatus(STATUS_KEY, presentation.status);
|
|
50
|
+
if (ctx.mode !== "tui") return;
|
|
51
|
+
ctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({
|
|
52
|
+
render(width) {
|
|
53
|
+
const safeWidth = Math.max(1, width);
|
|
54
|
+
return presentation.lines.map(
|
|
55
|
+
(line, index) => truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), safeWidth, "\u2026")
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
invalidate() {
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function attentionPresentation(decision) {
|
|
65
|
+
const setupName = safeTerminalText(decision.setupName);
|
|
66
|
+
const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
|
|
67
|
+
const difference = comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0 ? "Only list order differs" : `Remote ${comparison.remoteOnly.length} \xB7 Device ${comparison.localOnly.length}`;
|
|
68
|
+
return {
|
|
69
|
+
status: "review needed",
|
|
70
|
+
lines: [`Pi Sync needs review \xB7 ${setupName}`, difference, "No changes \xB7 Run /sync to review"]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function clearAttentionPresentation(ctx) {
|
|
74
|
+
ctx.ui.setStatus(STATUS_KEY, void 0);
|
|
75
|
+
ctx.ui.setWidget(WIDGET_KEY, void 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
syncAttentionMatchesConfig,
|
|
80
|
+
createSyncAttentionController
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=chunk-JNOOSPEW.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sync-attention.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { syncConfigReviewFingerprint } from \"./config.js\";\nimport { safeTerminalText } from \"./sync-format.js\";\nimport {\n\tcompareSyncInclude,\n\ttype RemoteSelectionDecision,\n\tsameSyncInclude,\n} from \"./sync-policy.js\";\nimport type { AnySyncConfig } from \"./types.js\";\n\nconst STATUS_KEY = \"sync\";\nconst WIDGET_KEY = \"sync:attention\";\n\nexport type SyncAttentionOrigin = \"sync\" | \"pull\" | \"push\";\n\nexport interface SyncAttentionState {\n\tdecision: RemoteSelectionDecision;\n\torigin: SyncAttentionOrigin;\n\toffered: boolean;\n}\n\nexport function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {\n\treturn (\n\t\tattention.decision.setupName === config.setupName &&\n\t\tattention.decision.configIdentity === syncConfigReviewFingerprint(config) &&\n\t\tsameSyncInclude(attention.decision.localInclude, config.include)\n\t);\n}\n\nexport interface SyncAttentionController {\n\tset(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;\n\tcurrent(): SyncAttentionState | undefined;\n\tmarkOffered(): boolean;\n\tclear(ctx: ExtensionContext): void;\n\treset(ctx: ExtensionContext): void;\n\tpublish(ctx: ExtensionContext): void;\n}\n\nexport function createSyncAttentionController(): SyncAttentionController {\n\tlet state: SyncAttentionState | undefined;\n\n\treturn {\n\t\tset(decision, origin) {\n\t\t\tstate = { decision, origin, offered: false };\n\t\t},\n\t\tcurrent() {\n\t\t\treturn state;\n\t\t},\n\t\tmarkOffered() {\n\t\t\tif (!state || state.offered) return false;\n\t\t\tstate = { ...state, offered: true };\n\t\t\treturn true;\n\t\t},\n\t\tclear(ctx) {\n\t\t\tstate = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\treset(ctx) {\n\t\t\tstate = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\tpublish(ctx) {\n\t\t\tif (!state) {\n\t\t\t\tclearAttentionPresentation(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst presentation = attentionPresentation(state.decision);\n\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\tif (ctx.mode !== \"tui\") return;\n\t\t\tctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({\n\t\t\t\trender(width: number) {\n\t\t\t\t\tconst safeWidth = Math.max(1, width);\n\t\t\t\t\treturn presentation.lines.map((line, index) =>\n\t\t\t\t\t\ttruncateToWidth(theme.fg(index === 0 ? \"warning\" : \"muted\", line), safeWidth, \"\u2026\"),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tinvalidate() {},\n\t\t\t}));\n\t\t},\n\t};\n}\n\nfunction attentionPresentation(decision: RemoteSelectionDecision) {\n\tconst setupName = safeTerminalText(decision.setupName);\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\tconst difference =\n\t\tcomparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? \"Only list order differs\"\n\t\t\t: `Remote ${comparison.remoteOnly.length} \u00B7 Device ${comparison.localOnly.length}`;\n\treturn {\n\t\tstatus: \"review needed\",\n\t\tlines: [`Pi Sync needs review \u00B7 ${setupName}`, difference, \"No changes \u00B7 Run /sync to review\"],\n\t};\n}\n\nfunction clearAttentionPresentation(ctx: ExtensionContext) {\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;AACA,SAAS,uBAAuB;AAUhC,IAAM,aAAa;AACnB,IAAM,aAAa;AAUZ,SAAS,2BAA2B,WAA+B,QAAuB;AAChG,SACC,UAAU,SAAS,cAAc,OAAO,aACxC,UAAU,SAAS,mBAAmB,4BAA4B,MAAM,KACxE,gBAAgB,UAAU,SAAS,cAAc,OAAO,OAAO;AAEjE;AAWO,SAAS,gCAAyD;AACxE,MAAI;AAEJ,SAAO;AAAA,IACN,IAAI,UAAU,QAAQ;AACrB,cAAQ,EAAE,UAAU,QAAQ,SAAS,MAAM;AAAA,IAC5C;AAAA,IACA,UAAU;AACT,aAAO;AAAA,IACR;AAAA,IACA,cAAc;AACb,UAAI,CAAC,SAAS,MAAM,QAAS,QAAO;AACpC,cAAQ,EAAE,GAAG,OAAO,SAAS,KAAK;AAClC,aAAO;AAAA,IACR;AAAA,IACA,MAAM,KAAK;AACV,cAAQ;AACR,iCAA2B,GAAG;AAAA,IAC/B;AAAA,IACA,MAAM,KAAK;AACV,cAAQ;AACR,iCAA2B,GAAG;AAAA,IAC/B;AAAA,IACA,QAAQ,KAAK;AACZ,UAAI,CAAC,OAAO;AACX,mCAA2B,GAAG;AAC9B;AAAA,MACD;AACA,YAAM,eAAe,sBAAsB,MAAM,QAAQ;AACzD,UAAI,GAAG,UAAU,YAAY,aAAa,MAAM;AAChD,UAAI,IAAI,SAAS,MAAO;AACxB,UAAI,GAAG,UAAU,YAAY,CAAC,MAAM,WAAW;AAAA,QAC9C,OAAO,OAAe;AACrB,gBAAM,YAAY,KAAK,IAAI,GAAG,KAAK;AACnC,iBAAO,aAAa,MAAM;AAAA,YAAI,CAAC,MAAM,UACpC,gBAAgB,MAAM,GAAG,UAAU,IAAI,YAAY,SAAS,IAAI,GAAG,WAAW,QAAG;AAAA,UAClF;AAAA,QACD;AAAA,QACA,aAAa;AAAA,QAAC;AAAA,MACf,EAAE;AAAA,IACH;AAAA,EACD;AACD;AAEA,SAAS,sBAAsB,UAAmC;AACjE,QAAM,YAAY,iBAAiB,SAAS,SAAS;AACrD,QAAM,aAAa,mBAAmB,SAAS,cAAc,SAAS,aAAa;AACnF,QAAM,aACL,WAAW,WAAW,WAAW,KAAK,WAAW,UAAU,WAAW,IACnE,4BACA,UAAU,WAAW,WAAW,MAAM,gBAAa,WAAW,UAAU,MAAM;AAClF,SAAO;AAAA,IACN,QAAQ;AAAA,IACR,OAAO,CAAC,6BAA0B,SAAS,IAAI,YAAY,qCAAkC;AAAA,EAC9F;AACD;AAEA,SAAS,2BAA2B,KAAuB;AAC1D,MAAI,GAAG,UAAU,YAAY,MAAS;AACtC,MAAI,GAAG,UAAU,YAAY,MAAS;AACvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|