@narumitw/pi-sync 0.49.10 → 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
|
@@ -0,0 +1,1238 @@
|
|
|
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
|
+
normalizeGitBranch,
|
|
5
|
+
normalizeGitDirectory,
|
|
6
|
+
normalizeGitRemote,
|
|
7
|
+
normalizeGitRemoteIdentity,
|
|
8
|
+
stateDir
|
|
9
|
+
} from "./chunk-F5QL2GPY.js";
|
|
10
|
+
import {
|
|
11
|
+
normalizeSyncInclude
|
|
12
|
+
} from "./chunk-YQ6UW7IF.js";
|
|
13
|
+
|
|
14
|
+
// src/config-file.ts
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
mkdir,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
realpath,
|
|
20
|
+
realpathSync,
|
|
21
|
+
rmdir,
|
|
22
|
+
rmdirSync,
|
|
23
|
+
stat,
|
|
24
|
+
statSync,
|
|
25
|
+
utimes,
|
|
26
|
+
utimesSync
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import fs from "node:fs/promises";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import lockfile from "proper-lockfile";
|
|
32
|
+
var CONFIG_FILE_NAME = "pi-sync.json";
|
|
33
|
+
var LEGACY_CONFIG_FILE_NAME = "pi-sync.local.json";
|
|
34
|
+
var configMigrationNotices = /* @__PURE__ */ new Map();
|
|
35
|
+
var legacyPresenceNoticed = /* @__PURE__ */ new Set();
|
|
36
|
+
var CONFIG_LOCK_STALE_MS = 3e4;
|
|
37
|
+
var CONFIG_LOCK_UPDATE_MS = 1e4;
|
|
38
|
+
var LOCKFILE_FS_ADAPTER = {
|
|
39
|
+
mkdir,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
realpath,
|
|
42
|
+
realpathSync,
|
|
43
|
+
rmdir,
|
|
44
|
+
rmdirSync,
|
|
45
|
+
stat,
|
|
46
|
+
statSync,
|
|
47
|
+
utimes,
|
|
48
|
+
utimesSync
|
|
49
|
+
};
|
|
50
|
+
var publishConfigFile = publishFileWithoutReplacement;
|
|
51
|
+
var beforeConfigPublicationHook = async () => void 0;
|
|
52
|
+
var afterMissingConfigReadProbeHook = async () => void 0;
|
|
53
|
+
var afterReplacementInstalledHook = async () => void 0;
|
|
54
|
+
var afterConfigQuarantinedHook = async () => void 0;
|
|
55
|
+
function localConfigPath() {
|
|
56
|
+
return path.join(getAgentDir(), CONFIG_FILE_NAME);
|
|
57
|
+
}
|
|
58
|
+
function legacyLocalConfigPath() {
|
|
59
|
+
return path.join(getAgentDir(), LEGACY_CONFIG_FILE_NAME);
|
|
60
|
+
}
|
|
61
|
+
async function activeLocalConfigPath() {
|
|
62
|
+
const canonicalPath = localConfigPath();
|
|
63
|
+
const legacyPath = legacyLocalConfigPath();
|
|
64
|
+
return withLocalConfigReadLockIfNeeded(async () => {
|
|
65
|
+
if (await pathExists(canonicalPath)) return canonicalPath;
|
|
66
|
+
return await pathExists(legacyPath) ? legacyPath : canonicalPath;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function consumeLocalConfigMigrationNotice() {
|
|
70
|
+
const configPath = localConfigPath();
|
|
71
|
+
const notice = configMigrationNotices.get(configPath);
|
|
72
|
+
configMigrationNotices.delete(configPath);
|
|
73
|
+
return notice;
|
|
74
|
+
}
|
|
75
|
+
async function readMigratingLocalConfigDocument(validateForMigration) {
|
|
76
|
+
return withLocalConfigReadLockIfNeeded(async () => {
|
|
77
|
+
const configPath = await prepareLocalConfigPath(validateForMigration);
|
|
78
|
+
const snapshot = await readConfigSnapshotIfExists(configPath);
|
|
79
|
+
return snapshot ? { path: configPath, ...snapshot } : void 0;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function withLocalConfigReadLockIfNeeded(read) {
|
|
83
|
+
if (await pathExists(localConfigPath()) || await pathExists(legacyLocalConfigPath())) {
|
|
84
|
+
return withLocalConfigFileLock(read);
|
|
85
|
+
}
|
|
86
|
+
await afterMissingConfigReadProbeHook();
|
|
87
|
+
if (await pathExists(configMutationLockPath())) return withLocalConfigFileLock(read);
|
|
88
|
+
return read();
|
|
89
|
+
}
|
|
90
|
+
function updateLocalConfigDocument(defaultValue, update, validate, signal) {
|
|
91
|
+
return withLocalConfigFileLock(async () => {
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
const configPath = await prepareLocalConfigPath(validate);
|
|
94
|
+
const snapshot = await readConfigSnapshotIfExists(configPath);
|
|
95
|
+
const document = snapshot ? { path: configPath, ...snapshot } : void 0;
|
|
96
|
+
const current = document ? structuredClone(document.parsed) : structuredClone(defaultValue);
|
|
97
|
+
const next = update(current);
|
|
98
|
+
validate(next);
|
|
99
|
+
signal?.throwIfAborted();
|
|
100
|
+
if (document && JSON.stringify(document.parsed) === JSON.stringify(next)) return next;
|
|
101
|
+
if (document) await replaceLocalConfigDocumentUnlocked(document, next);
|
|
102
|
+
else await installPrivateConfigExclusively(localConfigPath(), serializedConfig(next));
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function createLocalConfigDocument(value) {
|
|
107
|
+
return withLocalConfigFileLock(async () => {
|
|
108
|
+
const bytes = serializedConfig(value);
|
|
109
|
+
try {
|
|
110
|
+
await installPrivateConfigExclusively(localConfigPath(), bytes);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.code === "EEXIST") {
|
|
113
|
+
throw new Error("Pi-sync settings were created concurrently; reopen settings and retry.");
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function replaceLocalConfigDocumentUnlocked(document, value) {
|
|
120
|
+
const nextBytes = serializedConfig(value);
|
|
121
|
+
const canonicalPath = localConfigPath();
|
|
122
|
+
if (document.path !== canonicalPath) {
|
|
123
|
+
if (!await configDocumentStillMatches(document)) throw settingsChangedError();
|
|
124
|
+
let installed2;
|
|
125
|
+
try {
|
|
126
|
+
installed2 = await installPrivateConfigExclusively(canonicalPath, nextBytes);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === "EEXIST") {
|
|
129
|
+
throw new Error("Canonical settings were created concurrently; no settings were replaced.");
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
if (!await configDocumentStillMatches(document)) {
|
|
134
|
+
await quarantineAndRemoveConfigIfMatchesUnlocked(canonicalPath, installed2, nextBytes);
|
|
135
|
+
throw settingsChangedError();
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const quarantinePath = await claimCanonicalConfigDocument(document);
|
|
140
|
+
let installed;
|
|
141
|
+
try {
|
|
142
|
+
installed = await installPrivateConfigExclusively(canonicalPath, nextBytes);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
await restoreQuarantinedConfig(canonicalPath, quarantinePath);
|
|
145
|
+
if (error.code === "EEXIST") {
|
|
146
|
+
throw new Error("Canonical settings changed concurrently; no settings were replaced.");
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
await afterReplacementInstalledHook();
|
|
152
|
+
if (!await fileIdentityAndContentsMatch(quarantinePath, document.identity, document.bytes)) {
|
|
153
|
+
throw settingsChangedError();
|
|
154
|
+
}
|
|
155
|
+
if (process.platform !== "win32") await fs.chmod(quarantinePath, 384);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await quarantineAndRemoveConfigIfMatchesUnlocked(canonicalPath, installed, nextBytes);
|
|
158
|
+
await restoreQuarantinedConfig(canonicalPath, quarantinePath);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
await fs.rm(quarantinePath).catch(() => void 0);
|
|
162
|
+
await syncParentDirectory(canonicalPath).catch(() => void 0);
|
|
163
|
+
}
|
|
164
|
+
async function prepareLocalConfigPath(validateForMigration) {
|
|
165
|
+
const canonicalPath = localConfigPath();
|
|
166
|
+
const legacyPath = legacyLocalConfigPath();
|
|
167
|
+
if (await pathExists(canonicalPath)) {
|
|
168
|
+
const legacyStatus = await secureIgnoredLegacyIfPresent(legacyPath);
|
|
169
|
+
if (legacyStatus !== "missing" && !legacyPresenceNoticed.has(canonicalPath)) {
|
|
170
|
+
legacyPresenceNoticed.add(canonicalPath);
|
|
171
|
+
recordConfigMigrationNotice(
|
|
172
|
+
canonicalPath,
|
|
173
|
+
legacyStatus === "private" ? `${LEGACY_CONFIG_FILE_NAME} legacy settings were ignored because ${CONFIG_FILE_NAME} takes precedence. Delete ${LEGACY_CONFIG_FILE_NAME} after confirming your settings.` : `${LEGACY_CONFIG_FILE_NAME} legacy settings were ignored because ${CONFIG_FILE_NAME} takes precedence, but pi-sync could not verify them as a private regular file. Secure or delete the legacy path after confirming your settings.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return canonicalPath;
|
|
177
|
+
}
|
|
178
|
+
const legacy = await readConfigSnapshotIfExists(legacyPath);
|
|
179
|
+
if (!legacy) return canonicalPath;
|
|
180
|
+
validateForMigration(legacy.parsed);
|
|
181
|
+
let installedIdentity;
|
|
182
|
+
try {
|
|
183
|
+
installedIdentity = await installPrivateConfigExclusively(canonicalPath, legacy.bytes);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error.code === "EEXIST") {
|
|
186
|
+
recordConfigMigrationNotice(
|
|
187
|
+
canonicalPath,
|
|
188
|
+
`${LEGACY_CONFIG_FILE_NAME} legacy settings were ignored because ${CONFIG_FILE_NAME} was created concurrently and takes precedence.`
|
|
189
|
+
);
|
|
190
|
+
return canonicalPath;
|
|
191
|
+
}
|
|
192
|
+
recordConfigMigrationNotice(
|
|
193
|
+
canonicalPath,
|
|
194
|
+
`Could not migrate ${LEGACY_CONFIG_FILE_NAME} to ${CONFIG_FILE_NAME}; the legacy settings were used for this session and were not changed.`
|
|
195
|
+
);
|
|
196
|
+
return legacyPath;
|
|
197
|
+
}
|
|
198
|
+
if (!await configSnapshotStillMatches(legacyPath, legacy)) {
|
|
199
|
+
const removed = await quarantineAndRemoveConfigIfMatchesUnlocked(
|
|
200
|
+
canonicalPath,
|
|
201
|
+
installedIdentity,
|
|
202
|
+
legacy.bytes
|
|
203
|
+
);
|
|
204
|
+
recordConfigMigrationNotice(
|
|
205
|
+
canonicalPath,
|
|
206
|
+
removed ? `${LEGACY_CONFIG_FILE_NAME} changed during migration; the stale ${CONFIG_FILE_NAME} copy was removed and the legacy settings were used for this session.` : `${LEGACY_CONFIG_FILE_NAME} changed during migration, but ${CONFIG_FILE_NAME} was replaced concurrently and takes precedence.`
|
|
207
|
+
);
|
|
208
|
+
return removed ? legacyPath : canonicalPath;
|
|
209
|
+
}
|
|
210
|
+
legacyPresenceNoticed.add(canonicalPath);
|
|
211
|
+
recordConfigMigrationNotice(
|
|
212
|
+
canonicalPath,
|
|
213
|
+
`pi-sync settings migrated from ${LEGACY_CONFIG_FILE_NAME} to ${CONFIG_FILE_NAME}; the private legacy file was retained as a recovery copy and can be deleted after verification.`
|
|
214
|
+
);
|
|
215
|
+
return canonicalPath;
|
|
216
|
+
}
|
|
217
|
+
async function secureIgnoredLegacyIfPresent(filePath) {
|
|
218
|
+
let pathStat;
|
|
219
|
+
try {
|
|
220
|
+
pathStat = await fs.lstat(filePath);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error.code === "ENOENT") return "missing";
|
|
223
|
+
return "unsafe";
|
|
224
|
+
}
|
|
225
|
+
if (pathStat.isSymbolicLink() || !pathStat.isFile()) return "unsafe";
|
|
226
|
+
let handle;
|
|
227
|
+
try {
|
|
228
|
+
handle = await fs.open(filePath, "r");
|
|
229
|
+
const openedStat = await handle.stat();
|
|
230
|
+
if (openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) {
|
|
231
|
+
return "unsafe";
|
|
232
|
+
}
|
|
233
|
+
if (process.platform !== "win32" && (openedStat.mode & 511) !== 384) {
|
|
234
|
+
await handle.chmod(384);
|
|
235
|
+
}
|
|
236
|
+
return "private";
|
|
237
|
+
} catch {
|
|
238
|
+
return "unsafe";
|
|
239
|
+
} finally {
|
|
240
|
+
await handle?.close().catch(() => void 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function readConfigSnapshotIfExists(filePath) {
|
|
244
|
+
let pathStat;
|
|
245
|
+
try {
|
|
246
|
+
pathStat = await fs.lstat(filePath);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error.code === "ENOENT") return void 0;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (pathStat.isSymbolicLink()) {
|
|
252
|
+
throw new Error(`Refusing to read symlinked pi-sync config: ${filePath}`);
|
|
253
|
+
}
|
|
254
|
+
if (!pathStat.isFile()) throw new Error(`pi-sync config is not a regular file: ${filePath}`);
|
|
255
|
+
const handle = await fs.open(filePath, "r");
|
|
256
|
+
try {
|
|
257
|
+
const openedStat = await handle.stat();
|
|
258
|
+
if (openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) {
|
|
259
|
+
throw new Error(`pi-sync config changed while opening: ${filePath}`);
|
|
260
|
+
}
|
|
261
|
+
if (process.platform !== "win32" && (openedStat.mode & 511) !== 384) {
|
|
262
|
+
await handle.chmod(384);
|
|
263
|
+
}
|
|
264
|
+
const bytes = await handle.readFile();
|
|
265
|
+
return {
|
|
266
|
+
bytes,
|
|
267
|
+
identity: { dev: openedStat.dev, ino: openedStat.ino },
|
|
268
|
+
parsed: parseConfigObject(bytes, filePath)
|
|
269
|
+
};
|
|
270
|
+
} finally {
|
|
271
|
+
await handle.close();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function serializedConfig(value) {
|
|
275
|
+
return Buffer.from(`${JSON.stringify(value, null, " ")}
|
|
276
|
+
`, "utf8");
|
|
277
|
+
}
|
|
278
|
+
function parseConfigObject(bytes, filePath) {
|
|
279
|
+
let parsed;
|
|
280
|
+
try {
|
|
281
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
282
|
+
} catch {
|
|
283
|
+
throw new SyntaxError(`Invalid JSON in pi-sync config: ${filePath}`);
|
|
284
|
+
}
|
|
285
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
286
|
+
throw new Error(`pi-sync config must contain a JSON object: ${filePath}`);
|
|
287
|
+
}
|
|
288
|
+
return parsed;
|
|
289
|
+
}
|
|
290
|
+
async function installPrivateConfigExclusively(filePath, bytes) {
|
|
291
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
292
|
+
const temporaryPath = path.join(
|
|
293
|
+
path.dirname(filePath),
|
|
294
|
+
`.${path.basename(filePath)}.${process.pid}.${randomUUID()}.migrate`
|
|
295
|
+
);
|
|
296
|
+
let handle;
|
|
297
|
+
try {
|
|
298
|
+
handle = await fs.open(temporaryPath, "wx", 384);
|
|
299
|
+
await handle.writeFile(bytes);
|
|
300
|
+
if (process.platform !== "win32") await handle.chmod(384);
|
|
301
|
+
await handle.sync();
|
|
302
|
+
await handle.close();
|
|
303
|
+
handle = void 0;
|
|
304
|
+
await publishConfigFile(temporaryPath, filePath);
|
|
305
|
+
const installed = await fs.lstat(filePath);
|
|
306
|
+
let publishedHandle;
|
|
307
|
+
let publicationError;
|
|
308
|
+
try {
|
|
309
|
+
if (installed.isSymbolicLink() || !installed.isFile()) {
|
|
310
|
+
throw new Error(`Published pi-sync settings are not a regular file: ${filePath}`);
|
|
311
|
+
}
|
|
312
|
+
publishedHandle = await fs.open(filePath, "r+");
|
|
313
|
+
const published = await publishedHandle.stat();
|
|
314
|
+
if (published.dev !== installed.dev || published.ino !== installed.ino) {
|
|
315
|
+
throw new Error(`Published pi-sync settings changed while opening: ${filePath}`);
|
|
316
|
+
}
|
|
317
|
+
if (process.platform !== "win32") await publishedHandle.chmod(384);
|
|
318
|
+
await publishedHandle.sync();
|
|
319
|
+
await syncParentDirectory(filePath);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
publicationError = error;
|
|
322
|
+
} finally {
|
|
323
|
+
await publishedHandle?.close().catch(() => void 0);
|
|
324
|
+
}
|
|
325
|
+
if (publicationError) {
|
|
326
|
+
await quarantineAndRemoveConfigIfMatchesUnlocked(
|
|
327
|
+
filePath,
|
|
328
|
+
{ dev: installed.dev, ino: installed.ino },
|
|
329
|
+
bytes
|
|
330
|
+
);
|
|
331
|
+
throw publicationError;
|
|
332
|
+
}
|
|
333
|
+
return { dev: installed.dev, ino: installed.ino };
|
|
334
|
+
} finally {
|
|
335
|
+
await handle?.close().catch(() => void 0);
|
|
336
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function configDocumentStillMatches(document) {
|
|
340
|
+
return fileIdentityAndContentsMatch(document.path, document.identity, document.bytes);
|
|
341
|
+
}
|
|
342
|
+
async function claimCanonicalConfigDocument(document) {
|
|
343
|
+
const quarantinePath = path.join(
|
|
344
|
+
path.dirname(document.path),
|
|
345
|
+
`.${path.basename(document.path)}.${randomUUID()}.schema-migration-source`
|
|
346
|
+
);
|
|
347
|
+
try {
|
|
348
|
+
await fs.rename(document.path, quarantinePath);
|
|
349
|
+
await syncParentDirectory(document.path);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
if (!await fileIdentityAndContentsMatch(quarantinePath, document.identity, document.bytes)) {
|
|
355
|
+
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
356
|
+
throw settingsChangedError();
|
|
357
|
+
}
|
|
358
|
+
if (await pathExists(document.path)) {
|
|
359
|
+
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
360
|
+
throw new Error("Canonical settings changed concurrently; no settings were replaced.");
|
|
361
|
+
}
|
|
362
|
+
return quarantinePath;
|
|
363
|
+
}
|
|
364
|
+
function settingsChangedError() {
|
|
365
|
+
return new Error("pi-sync settings changed during migration; no settings were replaced.");
|
|
366
|
+
}
|
|
367
|
+
async function configSnapshotStillMatches(filePath, snapshot) {
|
|
368
|
+
return fileIdentityAndContentsMatch(filePath, snapshot.identity, snapshot.bytes);
|
|
369
|
+
}
|
|
370
|
+
async function quarantineAndRemoveConfigIfMatchesUnlocked(filePath, identity, expectedBytes) {
|
|
371
|
+
const quarantinePath = path.join(
|
|
372
|
+
path.dirname(filePath),
|
|
373
|
+
`.${path.basename(filePath)}.${randomUUID()}.migration-retired`
|
|
374
|
+
);
|
|
375
|
+
try {
|
|
376
|
+
await fs.rename(filePath, quarantinePath);
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
await afterConfigQuarantinedHook();
|
|
381
|
+
try {
|
|
382
|
+
await syncParentDirectory(filePath);
|
|
383
|
+
} catch {
|
|
384
|
+
await restoreQuarantinedConfig(filePath, quarantinePath);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const matches = await fileIdentityAndContentsMatch(quarantinePath, identity, expectedBytes);
|
|
388
|
+
if (!matches) {
|
|
389
|
+
await restoreQuarantinedConfig(filePath, quarantinePath);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (await pathExists(filePath)) {
|
|
393
|
+
await fs.rm(quarantinePath, { force: true });
|
|
394
|
+
await syncParentDirectory(filePath);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
await fs.rm(quarantinePath);
|
|
398
|
+
await syncParentDirectory(filePath);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
async function fileIdentityAndContentsMatch(filePath, identity, expectedBytes) {
|
|
402
|
+
try {
|
|
403
|
+
const current = await fs.lstat(filePath);
|
|
404
|
+
if (current.isSymbolicLink()) return false;
|
|
405
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino) return false;
|
|
406
|
+
return (await fs.readFile(filePath)).equals(expectedBytes);
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function restoreQuarantinedConfig(filePath, quarantinePath) {
|
|
412
|
+
try {
|
|
413
|
+
await renameFileWithoutReplacement(quarantinePath, filePath);
|
|
414
|
+
if (process.platform !== "win32") await fs.chmod(filePath, 384);
|
|
415
|
+
await syncParentDirectory(filePath);
|
|
416
|
+
return;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error.code !== "EEXIST") return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
if (process.platform !== "win32") await fs.chmod(quarantinePath, 384);
|
|
422
|
+
await syncParentDirectory(filePath);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function configMutationLockPath() {
|
|
427
|
+
return `${localConfigPath()}.mutation-lock`;
|
|
428
|
+
}
|
|
429
|
+
async function withLocalConfigFileLock(run) {
|
|
430
|
+
const configPath = localConfigPath();
|
|
431
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
432
|
+
let compromisedError;
|
|
433
|
+
const release = await lockfile.lock(configPath, {
|
|
434
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
435
|
+
lockfilePath: configMutationLockPath(),
|
|
436
|
+
realpath: false,
|
|
437
|
+
stale: CONFIG_LOCK_STALE_MS,
|
|
438
|
+
update: CONFIG_LOCK_UPDATE_MS,
|
|
439
|
+
retries: { retries: 100, factor: 1.2, minTimeout: 10, maxTimeout: 100 },
|
|
440
|
+
onCompromised: (error) => {
|
|
441
|
+
compromisedError = error;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
try {
|
|
445
|
+
const result = await run();
|
|
446
|
+
if (compromisedError) throw compromisedError;
|
|
447
|
+
return result;
|
|
448
|
+
} finally {
|
|
449
|
+
await release();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function syncParentDirectory(filePath) {
|
|
453
|
+
if (process.platform === "win32") return;
|
|
454
|
+
let handle;
|
|
455
|
+
try {
|
|
456
|
+
handle = await fs.open(path.dirname(filePath), "r");
|
|
457
|
+
await handle.sync();
|
|
458
|
+
} finally {
|
|
459
|
+
await handle?.close().catch(() => void 0);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function publishFileWithoutReplacement(source, destination) {
|
|
463
|
+
await beforeConfigPublicationHook();
|
|
464
|
+
await renameFileWithoutReplacement(source, destination);
|
|
465
|
+
}
|
|
466
|
+
async function renameFileWithoutReplacement(source, destination) {
|
|
467
|
+
if (await pathExists(destination)) {
|
|
468
|
+
throw Object.assign(new Error(`Settings already exist: ${destination}`), { code: "EEXIST" });
|
|
469
|
+
}
|
|
470
|
+
await fs.rename(source, destination);
|
|
471
|
+
}
|
|
472
|
+
async function pathExists(filePath) {
|
|
473
|
+
try {
|
|
474
|
+
await fs.lstat(filePath);
|
|
475
|
+
return true;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error.code === "ENOENT") return false;
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function recordConfigMigrationNotice(configPath, notice) {
|
|
482
|
+
if (!configMigrationNotices.has(configPath)) configMigrationNotices.set(configPath, notice);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/config.ts
|
|
486
|
+
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
487
|
+
import fs2 from "node:fs/promises";
|
|
488
|
+
import os from "node:os";
|
|
489
|
+
import path2 from "node:path";
|
|
490
|
+
import {
|
|
491
|
+
getAgentDir as getAgentDir2
|
|
492
|
+
} from "@earendil-works/pi-coding-agent";
|
|
493
|
+
|
|
494
|
+
// src/webdav-config.ts
|
|
495
|
+
var DEFAULT_PATH = "pi-sync";
|
|
496
|
+
function normalizeWebDavIdentityUrl(value) {
|
|
497
|
+
try {
|
|
498
|
+
const url = new URL(value.trim());
|
|
499
|
+
url.username = "";
|
|
500
|
+
url.password = "";
|
|
501
|
+
url.search = "";
|
|
502
|
+
url.hash = "";
|
|
503
|
+
url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
|
|
504
|
+
return url.toString();
|
|
505
|
+
} catch {
|
|
506
|
+
return value.trim();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function normalizeWebDavUrl(value) {
|
|
510
|
+
const normalized = normalizeOptionalString(value);
|
|
511
|
+
if (!normalized) return void 0;
|
|
512
|
+
let url;
|
|
513
|
+
try {
|
|
514
|
+
url = new URL(normalized);
|
|
515
|
+
} catch {
|
|
516
|
+
throw new Error("Invalid pi-sync WebDAV URL.");
|
|
517
|
+
}
|
|
518
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
"Invalid pi-sync WebDAV URL: credentials, query, and fragment are not allowed."
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
524
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
525
|
+
throw new Error("Invalid pi-sync WebDAV URL: HTTPS is required except for loopback.");
|
|
526
|
+
}
|
|
527
|
+
url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
|
|
528
|
+
return url.toString();
|
|
529
|
+
}
|
|
530
|
+
function normalizeWebDavPath(value) {
|
|
531
|
+
const normalized = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_PATH);
|
|
532
|
+
if (!normalized || normalized.includes("\\") || hasControlCharacter(normalized) || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
533
|
+
throw new Error("Invalid pi-sync WebDAV path.");
|
|
534
|
+
}
|
|
535
|
+
return normalized;
|
|
536
|
+
}
|
|
537
|
+
function validateWebDavNamespace(value) {
|
|
538
|
+
if (value === "." || value === ".." || value.includes("/") || value.includes("\\") || hasControlCharacter(value)) {
|
|
539
|
+
throw new Error("Invalid pi-sync WebDAV namespace.");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function validateWebDavCredentials(username, password) {
|
|
543
|
+
if (username.includes(":") || hasControlCharacter(username) || password !== void 0 && hasControlCharacter(password)) {
|
|
544
|
+
throw new Error("Invalid pi-sync WebDAV credentials.");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function normalizeOptionalString(value) {
|
|
548
|
+
const normalized = value?.trim();
|
|
549
|
+
return normalized || void 0;
|
|
550
|
+
}
|
|
551
|
+
function trimSlashes(value) {
|
|
552
|
+
return value.replace(/^\/+|\/+$/gu, "");
|
|
553
|
+
}
|
|
554
|
+
function hasControlCharacter(value) {
|
|
555
|
+
return [...value].some((character) => {
|
|
556
|
+
const code = character.codePointAt(0) ?? 0;
|
|
557
|
+
return code < 32 || code >= 127 && code <= 159;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/config-errors.ts
|
|
562
|
+
function isMissingConfigError(error) {
|
|
563
|
+
return error instanceof Error && (error.message.startsWith("Missing pi-sync settings.") || error.message === "No sync setups are configured.");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/config.ts
|
|
567
|
+
var STATE_VERSION = 2;
|
|
568
|
+
var DEFAULT_ON_SWITCH = "ask-before-pull";
|
|
569
|
+
function sessionDirFromContext(ctx) {
|
|
570
|
+
const manager = ctx.sessionManager;
|
|
571
|
+
if (manager.usesDefaultSessionDir?.call(manager)) return void 0;
|
|
572
|
+
return typeof manager.getSessionDir === "function" ? manager.getSessionDir.call(manager) : void 0;
|
|
573
|
+
}
|
|
574
|
+
async function loadConfig(setupName) {
|
|
575
|
+
const settings = await requireSettings();
|
|
576
|
+
const selectedName = setupName ?? settings.activeSyncSetup;
|
|
577
|
+
if (!selectedName) throw new Error("No sync setups are configured.");
|
|
578
|
+
validateConfigName(selectedName, "sync setup");
|
|
579
|
+
const setup = ownObject(settings.syncSetups, selectedName);
|
|
580
|
+
if (!setup)
|
|
581
|
+
throw new Error(`Invalid pi-sync settings: sync setup \u201C${selectedName}\u201D was not found.`);
|
|
582
|
+
const connectionName = setup.storage.connection;
|
|
583
|
+
const connection = ownObject(
|
|
584
|
+
settings.storageConnections,
|
|
585
|
+
connectionName
|
|
586
|
+
);
|
|
587
|
+
if (!connection) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`Invalid pi-sync settings: sync setup \u201C${selectedName}\u201D references missing storage connection \u201C${connectionName}\u201D.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return resolveSyncConfig(selectedName, setup, connectionName, connection, settings.onSwitch);
|
|
593
|
+
}
|
|
594
|
+
async function loadPartialConfig(setupName) {
|
|
595
|
+
const config = await loadConfig(setupName);
|
|
596
|
+
return {
|
|
597
|
+
setupName: config.setupName,
|
|
598
|
+
...storageReviewFromConfig(config),
|
|
599
|
+
include: [...config.include],
|
|
600
|
+
automatic: config.automatic,
|
|
601
|
+
onSwitch: config.onSwitch
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function syncSetupStorageReview(setupName, setup, connectionName, connection) {
|
|
605
|
+
return storageReviewFromConfig(
|
|
606
|
+
resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
function syncSetupReviewIdentity(setupName, setup, connectionName, connection) {
|
|
610
|
+
return syncConfigReviewIdentity(
|
|
611
|
+
resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
function syncConfigReviewIdentity(config) {
|
|
615
|
+
return JSON.stringify([
|
|
616
|
+
config.setupName,
|
|
617
|
+
config.connectionName,
|
|
618
|
+
backendIdentityCoordinates(config),
|
|
619
|
+
config.include,
|
|
620
|
+
config.automatic
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
623
|
+
function syncConfigReviewFingerprint(config) {
|
|
624
|
+
return createHash("sha256").update(syncConfigReviewIdentity(config)).digest("hex");
|
|
625
|
+
}
|
|
626
|
+
function storageReviewFromConfig(config) {
|
|
627
|
+
return {
|
|
628
|
+
connectionName: config.connectionName,
|
|
629
|
+
storageKind: config.backend.type,
|
|
630
|
+
storagePath: config.storagePath,
|
|
631
|
+
...config.backend.type === "s3" ? { bucket: config.backend.destination.bucket } : config.backend.type === "git" ? { branch: config.backend.destination.branch } : {}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
async function configuredSyncSetupNames() {
|
|
635
|
+
const settings = await readLocalConfigObject();
|
|
636
|
+
return settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
|
|
637
|
+
}
|
|
638
|
+
async function loadOnSwitch() {
|
|
639
|
+
return (await requireSettings()).onSwitch;
|
|
640
|
+
}
|
|
641
|
+
function normalizeOnSwitch(value) {
|
|
642
|
+
if (value === "ask-before-pull" || value === "pull-after-switch" || value === "switch-only") {
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(
|
|
646
|
+
'Invalid pi-sync settings: onSwitch must be "ask-before-pull", "pull-after-switch", or "switch-only".'
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function resolveSyncConfig(setupName, setup, connectionName, connection, onSwitch) {
|
|
650
|
+
const storagePath = normalizeStoragePath(setup.storage.path);
|
|
651
|
+
const namespace = storagePath.slice(storagePath.lastIndexOf("/") + 1);
|
|
652
|
+
const include = normalizeSyncInclude(setup.sync.include);
|
|
653
|
+
const common = {
|
|
654
|
+
setupName,
|
|
655
|
+
connectionName,
|
|
656
|
+
storagePath,
|
|
657
|
+
snapshotIdentity: namespace,
|
|
658
|
+
include,
|
|
659
|
+
automatic: setup.sync.automatic,
|
|
660
|
+
onSwitch
|
|
661
|
+
};
|
|
662
|
+
if (connection.type === "git") {
|
|
663
|
+
return {
|
|
664
|
+
...common,
|
|
665
|
+
backend: {
|
|
666
|
+
type: "git",
|
|
667
|
+
profile: { kind: "git", remote: normalizeGitRemote(connection.remote) },
|
|
668
|
+
destination: {
|
|
669
|
+
branch: normalizeGitBranch(setup.storage.branch),
|
|
670
|
+
directory: normalizeGitDirectory(storagePath),
|
|
671
|
+
namespace
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (connection.type === "webdav") {
|
|
677
|
+
return {
|
|
678
|
+
...common,
|
|
679
|
+
backend: {
|
|
680
|
+
type: "webdav",
|
|
681
|
+
profile: {
|
|
682
|
+
kind: "webdav",
|
|
683
|
+
url: normalizeWebDavUrl(connection.url),
|
|
684
|
+
username: connection.credentials.username,
|
|
685
|
+
password: connection.credentials.password
|
|
686
|
+
},
|
|
687
|
+
destination: { path: normalizeWebDavPath(storagePath), namespace }
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
...common,
|
|
693
|
+
backend: {
|
|
694
|
+
type: "s3",
|
|
695
|
+
profile: {
|
|
696
|
+
kind: isCloudflareR2Endpoint(connection.endpoint) ? "r2" : "s3-compatible",
|
|
697
|
+
endpoint: normalizeS3Endpoint(connection.endpoint),
|
|
698
|
+
region: requiredString(connection.region, "S3 region"),
|
|
699
|
+
accessKeyId: connection.credentials.accessKeyId,
|
|
700
|
+
secretAccessKey: connection.credentials.secretAccessKey,
|
|
701
|
+
sessionToken: optionalString(connection.credentials.sessionToken, "S3 session token")
|
|
702
|
+
},
|
|
703
|
+
destination: {
|
|
704
|
+
bucket: normalizeS3Bucket(setup.storage.bucket),
|
|
705
|
+
prefix: storagePath,
|
|
706
|
+
namespace
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
async function requireSettings() {
|
|
712
|
+
const settings = await readLocalConfigObject();
|
|
713
|
+
if (!settings) {
|
|
714
|
+
throw new Error(`Missing pi-sync settings. Use /sync setup or create ${localConfigPath()}.`);
|
|
715
|
+
}
|
|
716
|
+
return settings;
|
|
717
|
+
}
|
|
718
|
+
function validateSettingsDocument(value) {
|
|
719
|
+
if (value.version !== 3) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
`Unsupported pi-sync settings: version 3 is required. Keep the existing file for recovery, then create a new version 3 ${path2.basename(localConfigPath())}; pi-sync will not migrate or overwrite old settings.`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
rejectLegacyFields(
|
|
725
|
+
value,
|
|
726
|
+
[
|
|
727
|
+
"profiles",
|
|
728
|
+
"targets",
|
|
729
|
+
"activeTarget",
|
|
730
|
+
"targetSwitchAction",
|
|
731
|
+
"endpoint",
|
|
732
|
+
"bucket",
|
|
733
|
+
"region",
|
|
734
|
+
"accessKeyId",
|
|
735
|
+
"secretAccessKey",
|
|
736
|
+
"sessionToken",
|
|
737
|
+
"profile",
|
|
738
|
+
"prefix",
|
|
739
|
+
"autoSync",
|
|
740
|
+
"syncFiles",
|
|
741
|
+
"syncSessions",
|
|
742
|
+
"extraFiles"
|
|
743
|
+
],
|
|
744
|
+
"top level"
|
|
745
|
+
);
|
|
746
|
+
normalizeOnSwitch(value.onSwitch);
|
|
747
|
+
const storageConnections = requireNamedObjectMap(
|
|
748
|
+
value.storageConnections,
|
|
749
|
+
"storageConnections",
|
|
750
|
+
"storage connection"
|
|
751
|
+
);
|
|
752
|
+
const syncSetups = requireNamedObjectMap(value.syncSetups, "syncSetups", "sync setup");
|
|
753
|
+
for (const name of Object.keys(storageConnections)) {
|
|
754
|
+
validateStorageConnection(
|
|
755
|
+
name,
|
|
756
|
+
requireOwnObject(storageConnections, name, "storage connection")
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
for (const name of Object.keys(syncSetups)) {
|
|
760
|
+
validateSyncSetup(
|
|
761
|
+
name,
|
|
762
|
+
requireOwnObject(syncSetups, name, "sync setup"),
|
|
763
|
+
storageConnections
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const names = Object.keys(syncSetups);
|
|
767
|
+
const activeSyncSetup = optionalCanonicalReference(value.activeSyncSetup, "activeSyncSetup");
|
|
768
|
+
if (names.length === 0) {
|
|
769
|
+
if (activeSyncSetup !== void 0) {
|
|
770
|
+
throw new Error("Invalid pi-sync settings: empty syncSetups cannot have activeSyncSetup.");
|
|
771
|
+
}
|
|
772
|
+
} else if (!activeSyncSetup || !Object.hasOwn(syncSetups, activeSyncSetup)) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
"Invalid pi-sync settings: activeSyncSetup must reference an existing own-property sync setup."
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
validateUniqueRemoteSyncSetups(syncSetups, storageConnections);
|
|
778
|
+
return value;
|
|
779
|
+
}
|
|
780
|
+
function validateStorageConnection(name, value) {
|
|
781
|
+
rejectLegacyFields(
|
|
782
|
+
value,
|
|
783
|
+
["kind", "accessKeyId", "secretAccessKey", "sessionToken", "username", "password"],
|
|
784
|
+
`storage connection \u201C${name}\u201D`
|
|
785
|
+
);
|
|
786
|
+
const type = requiredString(value.type, `storage connection \u201C${name}\u201D type`);
|
|
787
|
+
if (type !== "s3" && type !== "git" && type !== "webdav") {
|
|
788
|
+
throw new Error(`Invalid pi-sync settings: storage connection \u201C${name}\u201D has unsupported type.`);
|
|
789
|
+
}
|
|
790
|
+
const known = ["endpoint", "region", "remote", "url", "credentials"];
|
|
791
|
+
const allowed = type === "s3" ? /* @__PURE__ */ new Set(["endpoint", "region", "credentials"]) : type === "git" ? /* @__PURE__ */ new Set(["remote"]) : /* @__PURE__ */ new Set(["url", "credentials"]);
|
|
792
|
+
if (known.some((field) => Object.hasOwn(value, field) && !allowed.has(field))) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Invalid pi-sync settings: ${type.toUpperCase()} storage connection \u201C${name}\u201D mixes backend fields.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (type === "git") {
|
|
798
|
+
if (!normalizeGitRemote(requiredString(value.remote, `Git remote for \u201C${name}\u201D`))) {
|
|
799
|
+
throw new Error(`Invalid pi-sync settings: Git remote for \u201C${name}\u201D is required.`);
|
|
800
|
+
}
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const credentials = requireRecord(
|
|
804
|
+
value.credentials,
|
|
805
|
+
`credentials for storage connection \u201C${name}\u201D`
|
|
806
|
+
);
|
|
807
|
+
if (type === "webdav") {
|
|
808
|
+
normalizeWebDavUrl(requiredString(value.url, `WebDAV URL for \u201C${name}\u201D`));
|
|
809
|
+
const username = requiredString(credentials.username, `WebDAV username for \u201C${name}\u201D`);
|
|
810
|
+
const password = requiredSecret(credentials.password, `WebDAV password for \u201C${name}\u201D`);
|
|
811
|
+
if (["accessKeyId", "secretAccessKey", "sessionToken"].some(
|
|
812
|
+
(field) => Object.hasOwn(credentials, field)
|
|
813
|
+
)) {
|
|
814
|
+
throw new Error(`Invalid pi-sync settings: WebDAV credentials for \u201C${name}\u201D mix fields.`);
|
|
815
|
+
}
|
|
816
|
+
validateWebDavCredentials(username, password);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
normalizeS3Endpoint(requiredString(value.endpoint, `S3 endpoint for \u201C${name}\u201D`));
|
|
820
|
+
requiredString(value.region, `S3 region for \u201C${name}\u201D`);
|
|
821
|
+
requiredString(credentials.accessKeyId, `S3 access key id for \u201C${name}\u201D`);
|
|
822
|
+
requiredSecret(credentials.secretAccessKey, `S3 secret access key for \u201C${name}\u201D`);
|
|
823
|
+
optionalString(credentials.sessionToken, `S3 session token for \u201C${name}\u201D`);
|
|
824
|
+
if (["username", "password"].some((field) => Object.hasOwn(credentials, field))) {
|
|
825
|
+
throw new Error(`Invalid pi-sync settings: S3 credentials for \u201C${name}\u201D mix fields.`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function validateSyncSetup(name, value, connections) {
|
|
829
|
+
rejectLegacyFields(
|
|
830
|
+
value,
|
|
831
|
+
[
|
|
832
|
+
"profile",
|
|
833
|
+
"bucket",
|
|
834
|
+
"branch",
|
|
835
|
+
"path",
|
|
836
|
+
"prefix",
|
|
837
|
+
"directory",
|
|
838
|
+
"namespace",
|
|
839
|
+
"autoSync",
|
|
840
|
+
"syncFiles",
|
|
841
|
+
"syncSessions",
|
|
842
|
+
"extraFiles"
|
|
843
|
+
],
|
|
844
|
+
`sync setup \u201C${name}\u201D`
|
|
845
|
+
);
|
|
846
|
+
const storage = requireRecord(value.storage, `storage for sync setup \u201C${name}\u201D`);
|
|
847
|
+
const sync = requireRecord(value.sync, `sync policy for sync setup \u201C${name}\u201D`);
|
|
848
|
+
rejectLegacyFields(
|
|
849
|
+
storage,
|
|
850
|
+
["profile", "prefix", "directory", "namespace"],
|
|
851
|
+
`storage for sync setup \u201C${name}\u201D`
|
|
852
|
+
);
|
|
853
|
+
rejectLegacyFields(
|
|
854
|
+
sync,
|
|
855
|
+
["autoSync", "syncFiles", "syncSessions", "extraFiles"],
|
|
856
|
+
`sync policy for sync setup \u201C${name}\u201D`
|
|
857
|
+
);
|
|
858
|
+
const connectionName = requiredCanonicalReference(
|
|
859
|
+
storage.connection,
|
|
860
|
+
`storage connection reference for sync setup \u201C${name}\u201D`
|
|
861
|
+
);
|
|
862
|
+
validateConfigName(connectionName, "storage connection reference");
|
|
863
|
+
const connection = ownObject(connections, connectionName);
|
|
864
|
+
if (!connection) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
`Invalid pi-sync settings: sync setup \u201C${name}\u201D references missing storage connection \u201C${connectionName}\u201D.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const type = connection.type;
|
|
870
|
+
normalizeStoragePath(requiredString(storage.path, `storage path for sync setup \u201C${name}\u201D`));
|
|
871
|
+
if (type === "s3") {
|
|
872
|
+
normalizeS3Bucket(requiredString(storage.bucket, `S3 bucket for sync setup \u201C${name}\u201D`));
|
|
873
|
+
if (Object.hasOwn(storage, "branch")) mixedSetupError("S3", name);
|
|
874
|
+
} else if (type === "git") {
|
|
875
|
+
normalizeGitBranch(requiredString(storage.branch, `Git branch for sync setup \u201C${name}\u201D`));
|
|
876
|
+
if (Object.hasOwn(storage, "bucket")) mixedSetupError("Git", name);
|
|
877
|
+
} else if (type === "webdav") {
|
|
878
|
+
if (Object.hasOwn(storage, "bucket") || Object.hasOwn(storage, "branch")) {
|
|
879
|
+
mixedSetupError("WebDAV", name);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (!Object.hasOwn(sync, "include")) {
|
|
883
|
+
throw new Error(`Invalid pi-sync settings: sync setup \u201C${name}\u201D is missing sync.include.`);
|
|
884
|
+
}
|
|
885
|
+
normalizeSyncInclude(sync.include);
|
|
886
|
+
if (typeof sync.automatic !== "boolean") {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Invalid pi-sync settings: sync setup \u201C${name}\u201D sync.automatic must be boolean.`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function mixedSetupError(type, name) {
|
|
893
|
+
throw new Error(`Invalid pi-sync settings: ${type} sync setup \u201C${name}\u201D mixes backend fields.`);
|
|
894
|
+
}
|
|
895
|
+
function validateUniqueRemoteSyncSetups(setups, connections) {
|
|
896
|
+
const identities = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const name of Object.keys(setups)) {
|
|
898
|
+
const setup = requireOwnObject(setups, name, "sync setup");
|
|
899
|
+
const connection = requireOwnObject(
|
|
900
|
+
connections,
|
|
901
|
+
setup.storage.connection,
|
|
902
|
+
"storage connection"
|
|
903
|
+
);
|
|
904
|
+
const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
|
|
905
|
+
const existing = identities.get(identity);
|
|
906
|
+
if (existing) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`Invalid pi-sync settings: sync setups \u201C${existing}\u201D and \u201C${name}\u201D use the same normalized remote location.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
identities.set(identity, name);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function effectiveSyncSetupRemoteIdentity(setup, connection) {
|
|
915
|
+
const storagePath = normalizeStoragePath(setup.storage.path);
|
|
916
|
+
if (connection.type === "git") {
|
|
917
|
+
return JSON.stringify([
|
|
918
|
+
"git",
|
|
919
|
+
normalizeGitRemoteIdentity(connection.remote),
|
|
920
|
+
normalizeGitBranch(setup.storage.branch),
|
|
921
|
+
normalizeGitDirectory(storagePath)
|
|
922
|
+
]);
|
|
923
|
+
}
|
|
924
|
+
if (connection.type === "webdav") {
|
|
925
|
+
return JSON.stringify([
|
|
926
|
+
"webdav",
|
|
927
|
+
normalizeWebDavIdentityUrl(connection.url),
|
|
928
|
+
connection.credentials.username.trim(),
|
|
929
|
+
normalizeWebDavPath(storagePath)
|
|
930
|
+
]);
|
|
931
|
+
}
|
|
932
|
+
return JSON.stringify([
|
|
933
|
+
"s3",
|
|
934
|
+
normalizeEndpointIdentity(connection.endpoint),
|
|
935
|
+
normalizeS3Bucket(setup.storage.bucket),
|
|
936
|
+
storagePath
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
function rejectLegacyFields(value, fields, context) {
|
|
940
|
+
const field = fields.find((candidate) => Object.hasOwn(value, candidate));
|
|
941
|
+
if (field) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Invalid pi-sync settings: ${context} contains unsupported version 1/2 field \u201C${field}\u201D.`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function requireNamedObjectMap(value, field, itemLabel) {
|
|
948
|
+
const result = requireRecord(value, field);
|
|
949
|
+
for (const name of Object.keys(result)) validateConfigName(name, itemLabel);
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
function requireOwnObject(value, key, label) {
|
|
953
|
+
const item = ownObject(value, key);
|
|
954
|
+
if (!item) throw new Error(`Invalid pi-sync settings: ${label} \u201C${key}\u201D must be an object.`);
|
|
955
|
+
return item;
|
|
956
|
+
}
|
|
957
|
+
function ownObject(value, key) {
|
|
958
|
+
if (!Object.hasOwn(value, key)) return void 0;
|
|
959
|
+
const item = value[key];
|
|
960
|
+
return item && typeof item === "object" && !Array.isArray(item) ? item : void 0;
|
|
961
|
+
}
|
|
962
|
+
function requireRecord(value, field) {
|
|
963
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
964
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be an object.`);
|
|
965
|
+
}
|
|
966
|
+
return value;
|
|
967
|
+
}
|
|
968
|
+
function validateConfigName(value, field) {
|
|
969
|
+
if (!value.trim() || value !== value.trim() || value.length > 100 || value === "__proto__" || value === "prototype" || value === "constructor" || hasControlCharacter2(value)) {
|
|
970
|
+
throw new Error(`Invalid pi-sync settings: invalid ${field} name.`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function requiredString(value, field) {
|
|
974
|
+
if (typeof value !== "string" || !value.trim() || hasControlCharacter2(value)) {
|
|
975
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a non-empty string.`);
|
|
976
|
+
}
|
|
977
|
+
return value.trim();
|
|
978
|
+
}
|
|
979
|
+
function requiredCanonicalReference(value, field) {
|
|
980
|
+
const normalized = requiredString(value, field);
|
|
981
|
+
if (value !== normalized) {
|
|
982
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
983
|
+
}
|
|
984
|
+
return normalized;
|
|
985
|
+
}
|
|
986
|
+
function optionalCanonicalReference(value, field) {
|
|
987
|
+
const normalized = optionalString(value, field);
|
|
988
|
+
if (normalized !== void 0 && value !== normalized) {
|
|
989
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
990
|
+
}
|
|
991
|
+
return normalized;
|
|
992
|
+
}
|
|
993
|
+
function requiredSecret(value, field) {
|
|
994
|
+
if (typeof value !== "string" || !value || hasControlCharacter2(value)) {
|
|
995
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be configured.`);
|
|
996
|
+
}
|
|
997
|
+
return value;
|
|
998
|
+
}
|
|
999
|
+
function optionalString(value, field) {
|
|
1000
|
+
if (value === void 0) return void 0;
|
|
1001
|
+
if (typeof value !== "string" || hasControlCharacter2(value)) {
|
|
1002
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a string.`);
|
|
1003
|
+
}
|
|
1004
|
+
return value.trim() || void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function normalizeStoragePath(value) {
|
|
1007
|
+
const normalized = value.trim().replace(/^\/+|\/+$/gu, "");
|
|
1008
|
+
if (!normalized || normalized.length > 1024 || normalized.startsWith("-") || normalized.includes("\\") || hasControlCharacter2(normalized) || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
1009
|
+
throw new Error("Invalid pi-sync settings: storage.path must be a safe relative path.");
|
|
1010
|
+
}
|
|
1011
|
+
return normalized;
|
|
1012
|
+
}
|
|
1013
|
+
function normalizeS3Endpoint(value) {
|
|
1014
|
+
let url;
|
|
1015
|
+
try {
|
|
1016
|
+
url = new URL(value.trim());
|
|
1017
|
+
} catch {
|
|
1018
|
+
throw new Error("Invalid pi-sync S3 endpoint.");
|
|
1019
|
+
}
|
|
1020
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
1021
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) {
|
|
1022
|
+
throw new Error("Invalid pi-sync S3 endpoint: HTTPS is required except for loopback.");
|
|
1023
|
+
}
|
|
1024
|
+
url.pathname = url.pathname.replace(/\/+$/gu, "");
|
|
1025
|
+
return url.toString().replace(/\/$/u, "");
|
|
1026
|
+
}
|
|
1027
|
+
function normalizeS3Bucket(value) {
|
|
1028
|
+
const bucket = requiredString(value, "S3 bucket");
|
|
1029
|
+
if (bucket.includes("/") || bucket.includes("\\") || bucket.startsWith("-")) {
|
|
1030
|
+
throw new Error("Invalid pi-sync S3 bucket.");
|
|
1031
|
+
}
|
|
1032
|
+
return bucket;
|
|
1033
|
+
}
|
|
1034
|
+
function normalizeEndpointIdentity(endpoint) {
|
|
1035
|
+
try {
|
|
1036
|
+
const url = new URL(endpoint.trim());
|
|
1037
|
+
url.hostname = url.hostname.toLowerCase();
|
|
1038
|
+
url.pathname = url.pathname.replace(/\/+$/gu, "");
|
|
1039
|
+
return url.toString().replace(/\/$/u, "");
|
|
1040
|
+
} catch {
|
|
1041
|
+
return endpoint.trim();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function configuredSessionDir() {
|
|
1045
|
+
const settings = await readJsonIfExists(
|
|
1046
|
+
path2.join(agentDir(), "settings.json")
|
|
1047
|
+
);
|
|
1048
|
+
return settings?.sessionDir ? expandHome(settings.sessionDir) : void 0;
|
|
1049
|
+
}
|
|
1050
|
+
async function sessionDirForApply(ctx, snapshot) {
|
|
1051
|
+
const contextSessionDir = sessionDirFromContext(ctx);
|
|
1052
|
+
const localSessionDir = await configuredSessionDir();
|
|
1053
|
+
if (contextSessionDir && path2.resolve(contextSessionDir) !== path2.resolve(localSessionDir ?? "")) {
|
|
1054
|
+
return contextSessionDir;
|
|
1055
|
+
}
|
|
1056
|
+
return sessionDirFromSnapshot(snapshot) ?? contextSessionDir;
|
|
1057
|
+
}
|
|
1058
|
+
function sessionDirFromSnapshot(snapshot) {
|
|
1059
|
+
const settingsFile = snapshot.files.find((file) => file.path === "settings.json");
|
|
1060
|
+
if (!settingsFile) return void 0;
|
|
1061
|
+
try {
|
|
1062
|
+
const settings = JSON.parse(
|
|
1063
|
+
decodeBase64Strict(settingsFile.contentBase64, settingsFile.path).toString("utf8")
|
|
1064
|
+
);
|
|
1065
|
+
return settings.sessionDir ? expandHome(settings.sessionDir) : void 0;
|
|
1066
|
+
} catch {
|
|
1067
|
+
return void 0;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function decodeBase64Strict(value, filePath) {
|
|
1071
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
1072
|
+
throw new Error(`Invalid base64 content in snapshot file: ${filePath}`);
|
|
1073
|
+
}
|
|
1074
|
+
return Buffer.from(value, "base64");
|
|
1075
|
+
}
|
|
1076
|
+
async function readStateForConfig(config) {
|
|
1077
|
+
return await readJsonIfExists(statePathForConfig(config)) ?? {
|
|
1078
|
+
version: STATE_VERSION,
|
|
1079
|
+
profile: config.snapshotIdentity,
|
|
1080
|
+
lastFileHashes: {}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
async function writeStateForConfig(config, state) {
|
|
1084
|
+
await writeJson(statePathForConfig(config), state);
|
|
1085
|
+
}
|
|
1086
|
+
function statePathForConfig(config) {
|
|
1087
|
+
const identity = backendIdentityCoordinates(config);
|
|
1088
|
+
const hash = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
1089
|
+
return path2.join(stateDir(), "setups", `${config.backend.type}-${hash}.state.json`);
|
|
1090
|
+
}
|
|
1091
|
+
function backendIdentityCoordinates(config) {
|
|
1092
|
+
switch (config.backend.type) {
|
|
1093
|
+
case "s3":
|
|
1094
|
+
return JSON.stringify([
|
|
1095
|
+
"s3",
|
|
1096
|
+
normalizeEndpointIdentity(config.backend.profile.endpoint),
|
|
1097
|
+
config.backend.destination.bucket,
|
|
1098
|
+
config.storagePath
|
|
1099
|
+
]);
|
|
1100
|
+
case "git":
|
|
1101
|
+
return JSON.stringify([
|
|
1102
|
+
"git",
|
|
1103
|
+
normalizeGitRemoteIdentity(config.backend.profile.remote),
|
|
1104
|
+
config.backend.destination.branch,
|
|
1105
|
+
config.storagePath
|
|
1106
|
+
]);
|
|
1107
|
+
case "webdav":
|
|
1108
|
+
return JSON.stringify([
|
|
1109
|
+
"webdav",
|
|
1110
|
+
normalizeWebDavIdentityUrl(config.backend.profile.url),
|
|
1111
|
+
config.backend.profile.username,
|
|
1112
|
+
config.storagePath
|
|
1113
|
+
]);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function agentDir() {
|
|
1117
|
+
return getAgentDir2();
|
|
1118
|
+
}
|
|
1119
|
+
function expandHome(value) {
|
|
1120
|
+
return value === "~" || value.startsWith("~/") ? path2.join(os.homedir(), value.slice(2)) : value;
|
|
1121
|
+
}
|
|
1122
|
+
function localConfigTemplate() {
|
|
1123
|
+
return {
|
|
1124
|
+
version: 3,
|
|
1125
|
+
onSwitch: DEFAULT_ON_SWITCH,
|
|
1126
|
+
storageConnections: {},
|
|
1127
|
+
syncSetups: {}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
async function readLocalConfigDocument() {
|
|
1131
|
+
const document = await readMigratingLocalConfigDocument((settings) => {
|
|
1132
|
+
validateSettingsDocument(settings);
|
|
1133
|
+
});
|
|
1134
|
+
if (document) validateSettingsDocument(document.parsed);
|
|
1135
|
+
return document;
|
|
1136
|
+
}
|
|
1137
|
+
async function readLocalConfigObject() {
|
|
1138
|
+
return (await readLocalConfigDocument())?.parsed;
|
|
1139
|
+
}
|
|
1140
|
+
var configUpdateQueue = Promise.resolve();
|
|
1141
|
+
function updateLocalConfig(update, signal) {
|
|
1142
|
+
const operation = configUpdateQueue.then(() => {
|
|
1143
|
+
signal?.throwIfAborted();
|
|
1144
|
+
return performLocalConfigUpdate(update, signal);
|
|
1145
|
+
});
|
|
1146
|
+
configUpdateQueue = operation.then(
|
|
1147
|
+
() => void 0,
|
|
1148
|
+
() => void 0
|
|
1149
|
+
);
|
|
1150
|
+
return operation;
|
|
1151
|
+
}
|
|
1152
|
+
async function performLocalConfigUpdate(update, signal) {
|
|
1153
|
+
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
1154
|
+
}
|
|
1155
|
+
function lockPath() {
|
|
1156
|
+
return path2.join(stateDir(), "lock");
|
|
1157
|
+
}
|
|
1158
|
+
async function ensureStateDir() {
|
|
1159
|
+
await fs2.mkdir(stateDir(), { recursive: true });
|
|
1160
|
+
}
|
|
1161
|
+
async function readJsonIfExists(filePath) {
|
|
1162
|
+
try {
|
|
1163
|
+
return JSON.parse(await fs2.readFile(filePath, "utf8"));
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
if (error.code === "ENOENT") return void 0;
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
async function writeJson(filePath, value) {
|
|
1170
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
1171
|
+
const temp = `${filePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1172
|
+
await fs2.writeFile(temp, `${JSON.stringify(value, null, " ")}
|
|
1173
|
+
`, { mode: 384 });
|
|
1174
|
+
if (process.platform !== "win32") await fs2.chmod(temp, 384);
|
|
1175
|
+
await fs2.rename(temp, filePath);
|
|
1176
|
+
}
|
|
1177
|
+
function sessionTokenWarnings(config) {
|
|
1178
|
+
if (!isCloudflareR2Endpoint(config.endpoint) || !config.sessionToken) return [];
|
|
1179
|
+
return [
|
|
1180
|
+
"session token: configured for Cloudflare R2; if R2 rejects X-Amz-Security-Token, pi-sync retries once without it. R2 static access keys usually do not need a session token."
|
|
1181
|
+
];
|
|
1182
|
+
}
|
|
1183
|
+
function syncSessionsWarnings(config) {
|
|
1184
|
+
if (!config.include.includes("sessions")) return [];
|
|
1185
|
+
return [
|
|
1186
|
+
"sessions: included; Pi session JSONL can contain prompts, tool output, file paths, images, and secrets. Sync sessions only to storage you trust."
|
|
1187
|
+
];
|
|
1188
|
+
}
|
|
1189
|
+
function isCloudflareR2Endpoint(endpoint) {
|
|
1190
|
+
const value = endpoint?.trim();
|
|
1191
|
+
if (!value) return false;
|
|
1192
|
+
try {
|
|
1193
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
1194
|
+
return hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com");
|
|
1195
|
+
} catch {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function hasControlCharacter2(value) {
|
|
1200
|
+
return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export {
|
|
1204
|
+
localConfigPath,
|
|
1205
|
+
activeLocalConfigPath,
|
|
1206
|
+
consumeLocalConfigMigrationNotice,
|
|
1207
|
+
createLocalConfigDocument,
|
|
1208
|
+
normalizeWebDavUrl,
|
|
1209
|
+
normalizeWebDavPath,
|
|
1210
|
+
validateWebDavNamespace,
|
|
1211
|
+
validateWebDavCredentials,
|
|
1212
|
+
isMissingConfigError,
|
|
1213
|
+
loadConfig,
|
|
1214
|
+
loadPartialConfig,
|
|
1215
|
+
syncSetupStorageReview,
|
|
1216
|
+
syncSetupReviewIdentity,
|
|
1217
|
+
syncConfigReviewIdentity,
|
|
1218
|
+
syncConfigReviewFingerprint,
|
|
1219
|
+
configuredSyncSetupNames,
|
|
1220
|
+
loadOnSwitch,
|
|
1221
|
+
normalizeOnSwitch,
|
|
1222
|
+
effectiveSyncSetupRemoteIdentity,
|
|
1223
|
+
validateConfigName,
|
|
1224
|
+
configuredSessionDir,
|
|
1225
|
+
sessionDirForApply,
|
|
1226
|
+
readStateForConfig,
|
|
1227
|
+
writeStateForConfig,
|
|
1228
|
+
agentDir,
|
|
1229
|
+
localConfigTemplate,
|
|
1230
|
+
readLocalConfigObject,
|
|
1231
|
+
updateLocalConfig,
|
|
1232
|
+
lockPath,
|
|
1233
|
+
ensureStateDir,
|
|
1234
|
+
sessionTokenWarnings,
|
|
1235
|
+
syncSessionsWarnings,
|
|
1236
|
+
isCloudflareR2Endpoint
|
|
1237
|
+
};
|
|
1238
|
+
//# sourceMappingURL=chunk-APZVBSM6.ts.map
|