// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. import { normalizeGitBranch, normalizeGitDirectory, normalizeGitRemote, normalizeGitRemoteIdentity, stateDir } from "./chunk-F5QL2GPY.ts"; import { normalizeSyncInclude } from "./chunk-YQ6UW7IF.ts"; // src/config-file.ts import { randomUUID } from "node:crypto"; import { mkdir, mkdirSync, realpath, realpathSync, rmdir, rmdirSync, stat, statSync, utimes, utimesSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; import lockfile from "proper-lockfile"; var CONFIG_FILE_NAME = "pi-sync.json"; var LEGACY_CONFIG_FILE_NAME = "pi-sync.local.json"; var configMigrationNotices = /* @__PURE__ */ new Map(); var legacyPresenceNoticed = /* @__PURE__ */ new Set(); var CONFIG_LOCK_STALE_MS = 3e4; var CONFIG_LOCK_UPDATE_MS = 1e4; var LOCKFILE_FS_ADAPTER = { mkdir, mkdirSync, realpath, realpathSync, rmdir, rmdirSync, stat, statSync, utimes, utimesSync }; var publishConfigFile = publishFileWithoutReplacement; var beforeConfigPublicationHook = async () => void 0; var afterMissingConfigReadProbeHook = async () => void 0; var afterReplacementInstalledHook = async () => void 0; var afterConfigQuarantinedHook = async () => void 0; function localConfigPath() { return path.join(getAgentDir(), CONFIG_FILE_NAME); } function legacyLocalConfigPath() { return path.join(getAgentDir(), LEGACY_CONFIG_FILE_NAME); } async function activeLocalConfigPath() { const canonicalPath = localConfigPath(); const legacyPath = legacyLocalConfigPath(); return withLocalConfigReadLockIfNeeded(async () => { if (await pathExists(canonicalPath)) return canonicalPath; return await pathExists(legacyPath) ? legacyPath : canonicalPath; }); } function consumeLocalConfigMigrationNotice() { const configPath = localConfigPath(); const notice = configMigrationNotices.get(configPath); configMigrationNotices.delete(configPath); return notice; } async function readMigratingLocalConfigDocument(validateForMigration) { return withLocalConfigReadLockIfNeeded(async () => { const configPath = await prepareLocalConfigPath(validateForMigration); const snapshot = await readConfigSnapshotIfExists(configPath); return snapshot ? { path: configPath, ...snapshot } : void 0; }); } async function withLocalConfigReadLockIfNeeded(read) { if (await pathExists(localConfigPath()) || await pathExists(legacyLocalConfigPath())) { return withLocalConfigFileLock(read); } await afterMissingConfigReadProbeHook(); if (await pathExists(configMutationLockPath())) return withLocalConfigFileLock(read); return read(); } function updateLocalConfigDocument(defaultValue, update, validate, signal) { return withLocalConfigFileLock(async () => { signal?.throwIfAborted(); const configPath = await prepareLocalConfigPath(validate); const snapshot = await readConfigSnapshotIfExists(configPath); const document = snapshot ? { path: configPath, ...snapshot } : void 0; const current = document ? structuredClone(document.parsed) : structuredClone(defaultValue); const next = update(current); validate(next); signal?.throwIfAborted(); if (document && JSON.stringify(document.parsed) === JSON.stringify(next)) return next; if (document) await replaceLocalConfigDocumentUnlocked(document, next); else await installPrivateConfigExclusively(localConfigPath(), serializedConfig(next)); return next; }); } function createLocalConfigDocument(value) { return withLocalConfigFileLock(async () => { const bytes = serializedConfig(value); try { await installPrivateConfigExclusively(localConfigPath(), bytes); } catch (error) { if (error.code === "EEXIST") { throw new Error("Pi-sync settings were created concurrently; reopen settings and retry."); } throw error; } }); } async function replaceLocalConfigDocumentUnlocked(document, value) { const nextBytes = serializedConfig(value); const canonicalPath = localConfigPath(); if (document.path !== canonicalPath) { if (!await configDocumentStillMatches(document)) throw settingsChangedError(); let installed2; try { installed2 = await installPrivateConfigExclusively(canonicalPath, nextBytes); } catch (error) { if (error.code === "EEXIST") { throw new Error("Canonical settings were created concurrently; no settings were replaced."); } throw error; } if (!await configDocumentStillMatches(document)) { await quarantineAndRemoveConfigIfMatchesUnlocked(canonicalPath, installed2, nextBytes); throw settingsChangedError(); } return; } const quarantinePath = await claimCanonicalConfigDocument(document); let installed; try { installed = await installPrivateConfigExclusively(canonicalPath, nextBytes); } catch (error) { await restoreQuarantinedConfig(canonicalPath, quarantinePath); if (error.code === "EEXIST") { throw new Error("Canonical settings changed concurrently; no settings were replaced."); } throw error; } try { await afterReplacementInstalledHook(); if (!await fileIdentityAndContentsMatch(quarantinePath, document.identity, document.bytes)) { throw settingsChangedError(); } if (process.platform !== "win32") await fs.chmod(quarantinePath, 384); } catch (error) { await quarantineAndRemoveConfigIfMatchesUnlocked(canonicalPath, installed, nextBytes); await restoreQuarantinedConfig(canonicalPath, quarantinePath); throw error; } await fs.rm(quarantinePath).catch(() => void 0); await syncParentDirectory(canonicalPath).catch(() => void 0); } async function prepareLocalConfigPath(validateForMigration) { const canonicalPath = localConfigPath(); const legacyPath = legacyLocalConfigPath(); if (await pathExists(canonicalPath)) { const legacyStatus = await secureIgnoredLegacyIfPresent(legacyPath); if (legacyStatus !== "missing" && !legacyPresenceNoticed.has(canonicalPath)) { legacyPresenceNoticed.add(canonicalPath); recordConfigMigrationNotice( canonicalPath, 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.` ); } return canonicalPath; } const legacy = await readConfigSnapshotIfExists(legacyPath); if (!legacy) return canonicalPath; validateForMigration(legacy.parsed); let installedIdentity; try { installedIdentity = await installPrivateConfigExclusively(canonicalPath, legacy.bytes); } catch (error) { if (error.code === "EEXIST") { recordConfigMigrationNotice( canonicalPath, `${LEGACY_CONFIG_FILE_NAME} legacy settings were ignored because ${CONFIG_FILE_NAME} was created concurrently and takes precedence.` ); return canonicalPath; } recordConfigMigrationNotice( canonicalPath, `Could not migrate ${LEGACY_CONFIG_FILE_NAME} to ${CONFIG_FILE_NAME}; the legacy settings were used for this session and were not changed.` ); return legacyPath; } if (!await configSnapshotStillMatches(legacyPath, legacy)) { const removed = await quarantineAndRemoveConfigIfMatchesUnlocked( canonicalPath, installedIdentity, legacy.bytes ); recordConfigMigrationNotice( canonicalPath, 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.` ); return removed ? legacyPath : canonicalPath; } legacyPresenceNoticed.add(canonicalPath); recordConfigMigrationNotice( canonicalPath, `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.` ); return canonicalPath; } async function secureIgnoredLegacyIfPresent(filePath) { let pathStat; try { pathStat = await fs.lstat(filePath); } catch (error) { if (error.code === "ENOENT") return "missing"; return "unsafe"; } if (pathStat.isSymbolicLink() || !pathStat.isFile()) return "unsafe"; let handle; try { handle = await fs.open(filePath, "r"); const openedStat = await handle.stat(); if (openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) { return "unsafe"; } if (process.platform !== "win32" && (openedStat.mode & 511) !== 384) { await handle.chmod(384); } return "private"; } catch { return "unsafe"; } finally { await handle?.close().catch(() => void 0); } } async function readConfigSnapshotIfExists(filePath) { let pathStat; try { pathStat = await fs.lstat(filePath); } catch (error) { if (error.code === "ENOENT") return void 0; throw error; } if (pathStat.isSymbolicLink()) { throw new Error(`Refusing to read symlinked pi-sync config: ${filePath}`); } if (!pathStat.isFile()) throw new Error(`pi-sync config is not a regular file: ${filePath}`); const handle = await fs.open(filePath, "r"); try { const openedStat = await handle.stat(); if (openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) { throw new Error(`pi-sync config changed while opening: ${filePath}`); } if (process.platform !== "win32" && (openedStat.mode & 511) !== 384) { await handle.chmod(384); } const bytes = await handle.readFile(); return { bytes, identity: { dev: openedStat.dev, ino: openedStat.ino }, parsed: parseConfigObject(bytes, filePath) }; } finally { await handle.close(); } } function serializedConfig(value) { return Buffer.from(`${JSON.stringify(value, null, " ")} `, "utf8"); } function parseConfigObject(bytes, filePath) { let parsed; try { parsed = JSON.parse(bytes.toString("utf8")); } catch { throw new SyntaxError(`Invalid JSON in pi-sync config: ${filePath}`); } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`pi-sync config must contain a JSON object: ${filePath}`); } return parsed; } async function installPrivateConfigExclusively(filePath, bytes) { await fs.mkdir(path.dirname(filePath), { recursive: true }); const temporaryPath = path.join( path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.migrate` ); let handle; try { handle = await fs.open(temporaryPath, "wx", 384); await handle.writeFile(bytes); if (process.platform !== "win32") await handle.chmod(384); await handle.sync(); await handle.close(); handle = void 0; await publishConfigFile(temporaryPath, filePath); const installed = await fs.lstat(filePath); let publishedHandle; let publicationError; try { if (installed.isSymbolicLink() || !installed.isFile()) { throw new Error(`Published pi-sync settings are not a regular file: ${filePath}`); } publishedHandle = await fs.open(filePath, "r+"); const published = await publishedHandle.stat(); if (published.dev !== installed.dev || published.ino !== installed.ino) { throw new Error(`Published pi-sync settings changed while opening: ${filePath}`); } if (process.platform !== "win32") await publishedHandle.chmod(384); await publishedHandle.sync(); await syncParentDirectory(filePath); } catch (error) { publicationError = error; } finally { await publishedHandle?.close().catch(() => void 0); } if (publicationError) { await quarantineAndRemoveConfigIfMatchesUnlocked( filePath, { dev: installed.dev, ino: installed.ino }, bytes ); throw publicationError; } return { dev: installed.dev, ino: installed.ino }; } finally { await handle?.close().catch(() => void 0); await fs.rm(temporaryPath, { force: true }).catch(() => void 0); } } async function configDocumentStillMatches(document) { return fileIdentityAndContentsMatch(document.path, document.identity, document.bytes); } async function claimCanonicalConfigDocument(document) { const quarantinePath = path.join( path.dirname(document.path), `.${path.basename(document.path)}.${randomUUID()}.schema-migration-source` ); try { await fs.rename(document.path, quarantinePath); await syncParentDirectory(document.path); } catch (error) { await restoreQuarantinedConfig(document.path, quarantinePath); throw error; } if (!await fileIdentityAndContentsMatch(quarantinePath, document.identity, document.bytes)) { await restoreQuarantinedConfig(document.path, quarantinePath); throw settingsChangedError(); } if (await pathExists(document.path)) { await restoreQuarantinedConfig(document.path, quarantinePath); throw new Error("Canonical settings changed concurrently; no settings were replaced."); } return quarantinePath; } function settingsChangedError() { return new Error("pi-sync settings changed during migration; no settings were replaced."); } async function configSnapshotStillMatches(filePath, snapshot) { return fileIdentityAndContentsMatch(filePath, snapshot.identity, snapshot.bytes); } async function quarantineAndRemoveConfigIfMatchesUnlocked(filePath, identity, expectedBytes) { const quarantinePath = path.join( path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.migration-retired` ); try { await fs.rename(filePath, quarantinePath); } catch { return false; } await afterConfigQuarantinedHook(); try { await syncParentDirectory(filePath); } catch { await restoreQuarantinedConfig(filePath, quarantinePath); return false; } const matches = await fileIdentityAndContentsMatch(quarantinePath, identity, expectedBytes); if (!matches) { await restoreQuarantinedConfig(filePath, quarantinePath); return false; } if (await pathExists(filePath)) { await fs.rm(quarantinePath, { force: true }); await syncParentDirectory(filePath); return false; } await fs.rm(quarantinePath); await syncParentDirectory(filePath); return true; } async function fileIdentityAndContentsMatch(filePath, identity, expectedBytes) { try { const current = await fs.lstat(filePath); if (current.isSymbolicLink()) return false; if (current.dev !== identity.dev || current.ino !== identity.ino) return false; return (await fs.readFile(filePath)).equals(expectedBytes); } catch { return false; } } async function restoreQuarantinedConfig(filePath, quarantinePath) { try { await renameFileWithoutReplacement(quarantinePath, filePath); if (process.platform !== "win32") await fs.chmod(filePath, 384); await syncParentDirectory(filePath); return; } catch (error) { if (error.code !== "EEXIST") return; } try { if (process.platform !== "win32") await fs.chmod(quarantinePath, 384); await syncParentDirectory(filePath); } catch { } } function configMutationLockPath() { return `${localConfigPath()}.mutation-lock`; } async function withLocalConfigFileLock(run) { const configPath = localConfigPath(); await fs.mkdir(path.dirname(configPath), { recursive: true }); let compromisedError; const release = await lockfile.lock(configPath, { fs: LOCKFILE_FS_ADAPTER, lockfilePath: configMutationLockPath(), realpath: false, stale: CONFIG_LOCK_STALE_MS, update: CONFIG_LOCK_UPDATE_MS, retries: { retries: 100, factor: 1.2, minTimeout: 10, maxTimeout: 100 }, onCompromised: (error) => { compromisedError = error; } }); try { const result = await run(); if (compromisedError) throw compromisedError; return result; } finally { await release(); } } async function syncParentDirectory(filePath) { if (process.platform === "win32") return; let handle; try { handle = await fs.open(path.dirname(filePath), "r"); await handle.sync(); } finally { await handle?.close().catch(() => void 0); } } async function publishFileWithoutReplacement(source, destination) { await beforeConfigPublicationHook(); await renameFileWithoutReplacement(source, destination); } async function renameFileWithoutReplacement(source, destination) { if (await pathExists(destination)) { throw Object.assign(new Error(`Settings already exist: ${destination}`), { code: "EEXIST" }); } await fs.rename(source, destination); } async function pathExists(filePath) { try { await fs.lstat(filePath); return true; } catch (error) { if (error.code === "ENOENT") return false; throw error; } } function recordConfigMigrationNotice(configPath, notice) { if (!configMigrationNotices.has(configPath)) configMigrationNotices.set(configPath, notice); } // src/config.ts import { createHash, randomUUID as randomUUID2 } from "node:crypto"; import fs2 from "node:fs/promises"; import os from "node:os"; import path2 from "node:path"; import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent"; // src/webdav-config.ts var DEFAULT_PATH = "pi-sync"; function normalizeWebDavIdentityUrl(value) { try { const url = new URL(value.trim()); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`; return url.toString(); } catch { return value.trim(); } } function normalizeWebDavUrl(value) { const normalized = normalizeOptionalString(value); if (!normalized) return void 0; let url; try { url = new URL(normalized); } catch { throw new Error("Invalid pi-sync WebDAV URL."); } if (url.username || url.password || url.search || url.hash) { throw new Error( "Invalid pi-sync WebDAV URL: credentials, query, and fragment are not allowed." ); } const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1"; if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) { throw new Error("Invalid pi-sync WebDAV URL: HTTPS is required except for loopback."); } url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`; return url.toString(); } function normalizeWebDavPath(value) { const normalized = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_PATH); if (!normalized || normalized.includes("\\") || hasControlCharacter(normalized) || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) { throw new Error("Invalid pi-sync WebDAV path."); } return normalized; } function validateWebDavNamespace(value) { if (value === "." || value === ".." || value.includes("/") || value.includes("\\") || hasControlCharacter(value)) { throw new Error("Invalid pi-sync WebDAV namespace."); } } function validateWebDavCredentials(username, password) { if (username.includes(":") || hasControlCharacter(username) || password !== void 0 && hasControlCharacter(password)) { throw new Error("Invalid pi-sync WebDAV credentials."); } } function normalizeOptionalString(value) { const normalized = value?.trim(); return normalized || void 0; } function trimSlashes(value) { return value.replace(/^\/+|\/+$/gu, ""); } function hasControlCharacter(value) { return [...value].some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code >= 127 && code <= 159; }); } // src/config-errors.ts function isMissingConfigError(error) { return error instanceof Error && (error.message.startsWith("Missing pi-sync settings.") || error.message === "No sync setups are configured."); } // src/config.ts var STATE_VERSION = 2; var DEFAULT_ON_SWITCH = "ask-before-pull"; function sessionDirFromContext(ctx) { const manager = ctx.sessionManager; if (manager.usesDefaultSessionDir?.call(manager)) return void 0; return typeof manager.getSessionDir === "function" ? manager.getSessionDir.call(manager) : void 0; } async function loadConfig(setupName) { const settings = await requireSettings(); const selectedName = setupName ?? settings.activeSyncSetup; if (!selectedName) throw new Error("No sync setups are configured."); validateConfigName(selectedName, "sync setup"); const setup = ownObject(settings.syncSetups, selectedName); if (!setup) throw new Error(`Invalid pi-sync settings: sync setup \u201C${selectedName}\u201D was not found.`); const connectionName = setup.storage.connection; const connection = ownObject( settings.storageConnections, connectionName ); if (!connection) { throw new Error( `Invalid pi-sync settings: sync setup \u201C${selectedName}\u201D references missing storage connection \u201C${connectionName}\u201D.` ); } return resolveSyncConfig(selectedName, setup, connectionName, connection, settings.onSwitch); } async function loadPartialConfig(setupName) { const config = await loadConfig(setupName); return { setupName: config.setupName, ...storageReviewFromConfig(config), include: [...config.include], automatic: config.automatic, onSwitch: config.onSwitch }; } function syncSetupStorageReview(setupName, setup, connectionName, connection) { return storageReviewFromConfig( resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH) ); } function syncSetupReviewIdentity(setupName, setup, connectionName, connection) { return syncConfigReviewIdentity( resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH) ); } function syncConfigReviewIdentity(config) { return JSON.stringify([ config.setupName, config.connectionName, backendIdentityCoordinates(config), config.include, config.automatic ]); } function syncConfigReviewFingerprint(config) { return createHash("sha256").update(syncConfigReviewIdentity(config)).digest("hex"); } function storageReviewFromConfig(config) { return { connectionName: config.connectionName, storageKind: config.backend.type, storagePath: config.storagePath, ...config.backend.type === "s3" ? { bucket: config.backend.destination.bucket } : config.backend.type === "git" ? { branch: config.backend.destination.branch } : {} }; } async function configuredSyncSetupNames() { const settings = await readLocalConfigObject(); return settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : []; } async function loadOnSwitch() { return (await requireSettings()).onSwitch; } function normalizeOnSwitch(value) { if (value === "ask-before-pull" || value === "pull-after-switch" || value === "switch-only") { return value; } throw new Error( 'Invalid pi-sync settings: onSwitch must be "ask-before-pull", "pull-after-switch", or "switch-only".' ); } function resolveSyncConfig(setupName, setup, connectionName, connection, onSwitch) { const storagePath = normalizeStoragePath(setup.storage.path); const namespace = storagePath.slice(storagePath.lastIndexOf("/") + 1); const include = normalizeSyncInclude(setup.sync.include); const common = { setupName, connectionName, storagePath, snapshotIdentity: namespace, include, automatic: setup.sync.automatic, onSwitch }; if (connection.type === "git") { return { ...common, backend: { type: "git", profile: { kind: "git", remote: normalizeGitRemote(connection.remote) }, destination: { branch: normalizeGitBranch(setup.storage.branch), directory: normalizeGitDirectory(storagePath), namespace } } }; } if (connection.type === "webdav") { return { ...common, backend: { type: "webdav", profile: { kind: "webdav", url: normalizeWebDavUrl(connection.url), username: connection.credentials.username, password: connection.credentials.password }, destination: { path: normalizeWebDavPath(storagePath), namespace } } }; } return { ...common, backend: { type: "s3", profile: { kind: isCloudflareR2Endpoint(connection.endpoint) ? "r2" : "s3-compatible", endpoint: normalizeS3Endpoint(connection.endpoint), region: requiredString(connection.region, "S3 region"), accessKeyId: connection.credentials.accessKeyId, secretAccessKey: connection.credentials.secretAccessKey, sessionToken: optionalString(connection.credentials.sessionToken, "S3 session token") }, destination: { bucket: normalizeS3Bucket(setup.storage.bucket), prefix: storagePath, namespace } } }; } async function requireSettings() { const settings = await readLocalConfigObject(); if (!settings) { throw new Error(`Missing pi-sync settings. Use /sync setup or create ${localConfigPath()}.`); } return settings; } function validateSettingsDocument(value) { if (value.version !== 3) { throw new Error( `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.` ); } rejectLegacyFields( value, [ "profiles", "targets", "activeTarget", "targetSwitchAction", "endpoint", "bucket", "region", "accessKeyId", "secretAccessKey", "sessionToken", "profile", "prefix", "autoSync", "syncFiles", "syncSessions", "extraFiles" ], "top level" ); normalizeOnSwitch(value.onSwitch); const storageConnections = requireNamedObjectMap( value.storageConnections, "storageConnections", "storage connection" ); const syncSetups = requireNamedObjectMap(value.syncSetups, "syncSetups", "sync setup"); for (const name of Object.keys(storageConnections)) { validateStorageConnection( name, requireOwnObject(storageConnections, name, "storage connection") ); } for (const name of Object.keys(syncSetups)) { validateSyncSetup( name, requireOwnObject(syncSetups, name, "sync setup"), storageConnections ); } const names = Object.keys(syncSetups); const activeSyncSetup = optionalCanonicalReference(value.activeSyncSetup, "activeSyncSetup"); if (names.length === 0) { if (activeSyncSetup !== void 0) { throw new Error("Invalid pi-sync settings: empty syncSetups cannot have activeSyncSetup."); } } else if (!activeSyncSetup || !Object.hasOwn(syncSetups, activeSyncSetup)) { throw new Error( "Invalid pi-sync settings: activeSyncSetup must reference an existing own-property sync setup." ); } validateUniqueRemoteSyncSetups(syncSetups, storageConnections); return value; } function validateStorageConnection(name, value) { rejectLegacyFields( value, ["kind", "accessKeyId", "secretAccessKey", "sessionToken", "username", "password"], `storage connection \u201C${name}\u201D` ); const type = requiredString(value.type, `storage connection \u201C${name}\u201D type`); if (type !== "s3" && type !== "git" && type !== "webdav") { throw new Error(`Invalid pi-sync settings: storage connection \u201C${name}\u201D has unsupported type.`); } const known = ["endpoint", "region", "remote", "url", "credentials"]; const allowed = type === "s3" ? /* @__PURE__ */ new Set(["endpoint", "region", "credentials"]) : type === "git" ? /* @__PURE__ */ new Set(["remote"]) : /* @__PURE__ */ new Set(["url", "credentials"]); if (known.some((field) => Object.hasOwn(value, field) && !allowed.has(field))) { throw new Error( `Invalid pi-sync settings: ${type.toUpperCase()} storage connection \u201C${name}\u201D mixes backend fields.` ); } if (type === "git") { if (!normalizeGitRemote(requiredString(value.remote, `Git remote for \u201C${name}\u201D`))) { throw new Error(`Invalid pi-sync settings: Git remote for \u201C${name}\u201D is required.`); } return; } const credentials = requireRecord( value.credentials, `credentials for storage connection \u201C${name}\u201D` ); if (type === "webdav") { normalizeWebDavUrl(requiredString(value.url, `WebDAV URL for \u201C${name}\u201D`)); const username = requiredString(credentials.username, `WebDAV username for \u201C${name}\u201D`); const password = requiredSecret(credentials.password, `WebDAV password for \u201C${name}\u201D`); if (["accessKeyId", "secretAccessKey", "sessionToken"].some( (field) => Object.hasOwn(credentials, field) )) { throw new Error(`Invalid pi-sync settings: WebDAV credentials for \u201C${name}\u201D mix fields.`); } validateWebDavCredentials(username, password); return; } normalizeS3Endpoint(requiredString(value.endpoint, `S3 endpoint for \u201C${name}\u201D`)); requiredString(value.region, `S3 region for \u201C${name}\u201D`); requiredString(credentials.accessKeyId, `S3 access key id for \u201C${name}\u201D`); requiredSecret(credentials.secretAccessKey, `S3 secret access key for \u201C${name}\u201D`); optionalString(credentials.sessionToken, `S3 session token for \u201C${name}\u201D`); if (["username", "password"].some((field) => Object.hasOwn(credentials, field))) { throw new Error(`Invalid pi-sync settings: S3 credentials for \u201C${name}\u201D mix fields.`); } } function validateSyncSetup(name, value, connections) { rejectLegacyFields( value, [ "profile", "bucket", "branch", "path", "prefix", "directory", "namespace", "autoSync", "syncFiles", "syncSessions", "extraFiles" ], `sync setup \u201C${name}\u201D` ); const storage = requireRecord(value.storage, `storage for sync setup \u201C${name}\u201D`); const sync = requireRecord(value.sync, `sync policy for sync setup \u201C${name}\u201D`); rejectLegacyFields( storage, ["profile", "prefix", "directory", "namespace"], `storage for sync setup \u201C${name}\u201D` ); rejectLegacyFields( sync, ["autoSync", "syncFiles", "syncSessions", "extraFiles"], `sync policy for sync setup \u201C${name}\u201D` ); const connectionName = requiredCanonicalReference( storage.connection, `storage connection reference for sync setup \u201C${name}\u201D` ); validateConfigName(connectionName, "storage connection reference"); const connection = ownObject(connections, connectionName); if (!connection) { throw new Error( `Invalid pi-sync settings: sync setup \u201C${name}\u201D references missing storage connection \u201C${connectionName}\u201D.` ); } const type = connection.type; normalizeStoragePath(requiredString(storage.path, `storage path for sync setup \u201C${name}\u201D`)); if (type === "s3") { normalizeS3Bucket(requiredString(storage.bucket, `S3 bucket for sync setup \u201C${name}\u201D`)); if (Object.hasOwn(storage, "branch")) mixedSetupError("S3", name); } else if (type === "git") { normalizeGitBranch(requiredString(storage.branch, `Git branch for sync setup \u201C${name}\u201D`)); if (Object.hasOwn(storage, "bucket")) mixedSetupError("Git", name); } else if (type === "webdav") { if (Object.hasOwn(storage, "bucket") || Object.hasOwn(storage, "branch")) { mixedSetupError("WebDAV", name); } } if (!Object.hasOwn(sync, "include")) { throw new Error(`Invalid pi-sync settings: sync setup \u201C${name}\u201D is missing sync.include.`); } normalizeSyncInclude(sync.include); if (typeof sync.automatic !== "boolean") { throw new Error( `Invalid pi-sync settings: sync setup \u201C${name}\u201D sync.automatic must be boolean.` ); } } function mixedSetupError(type, name) { throw new Error(`Invalid pi-sync settings: ${type} sync setup \u201C${name}\u201D mixes backend fields.`); } function validateUniqueRemoteSyncSetups(setups, connections) { const identities = /* @__PURE__ */ new Map(); for (const name of Object.keys(setups)) { const setup = requireOwnObject(setups, name, "sync setup"); const connection = requireOwnObject( connections, setup.storage.connection, "storage connection" ); const identity = effectiveSyncSetupRemoteIdentity(setup, connection); const existing = identities.get(identity); if (existing) { throw new Error( `Invalid pi-sync settings: sync setups \u201C${existing}\u201D and \u201C${name}\u201D use the same normalized remote location.` ); } identities.set(identity, name); } } function effectiveSyncSetupRemoteIdentity(setup, connection) { const storagePath = normalizeStoragePath(setup.storage.path); if (connection.type === "git") { return JSON.stringify([ "git", normalizeGitRemoteIdentity(connection.remote), normalizeGitBranch(setup.storage.branch), normalizeGitDirectory(storagePath) ]); } if (connection.type === "webdav") { return JSON.stringify([ "webdav", normalizeWebDavIdentityUrl(connection.url), connection.credentials.username.trim(), normalizeWebDavPath(storagePath) ]); } return JSON.stringify([ "s3", normalizeEndpointIdentity(connection.endpoint), normalizeS3Bucket(setup.storage.bucket), storagePath ]); } function rejectLegacyFields(value, fields, context) { const field = fields.find((candidate) => Object.hasOwn(value, candidate)); if (field) { throw new Error( `Invalid pi-sync settings: ${context} contains unsupported version 1/2 field \u201C${field}\u201D.` ); } } function requireNamedObjectMap(value, field, itemLabel) { const result = requireRecord(value, field); for (const name of Object.keys(result)) validateConfigName(name, itemLabel); return result; } function requireOwnObject(value, key, label) { const item = ownObject(value, key); if (!item) throw new Error(`Invalid pi-sync settings: ${label} \u201C${key}\u201D must be an object.`); return item; } function ownObject(value, key) { if (!Object.hasOwn(value, key)) return void 0; const item = value[key]; return item && typeof item === "object" && !Array.isArray(item) ? item : void 0; } function requireRecord(value, field) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`Invalid pi-sync settings: ${field} must be an object.`); } return value; } function validateConfigName(value, field) { if (!value.trim() || value !== value.trim() || value.length > 100 || value === "__proto__" || value === "prototype" || value === "constructor" || hasControlCharacter2(value)) { throw new Error(`Invalid pi-sync settings: invalid ${field} name.`); } } function requiredString(value, field) { if (typeof value !== "string" || !value.trim() || hasControlCharacter2(value)) { throw new Error(`Invalid pi-sync settings: ${field} must be a non-empty string.`); } return value.trim(); } function requiredCanonicalReference(value, field) { const normalized = requiredString(value, field); if (value !== normalized) { throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`); } return normalized; } function optionalCanonicalReference(value, field) { const normalized = optionalString(value, field); if (normalized !== void 0 && value !== normalized) { throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`); } return normalized; } function requiredSecret(value, field) { if (typeof value !== "string" || !value || hasControlCharacter2(value)) { throw new Error(`Invalid pi-sync settings: ${field} must be configured.`); } return value; } function optionalString(value, field) { if (value === void 0) return void 0; if (typeof value !== "string" || hasControlCharacter2(value)) { throw new Error(`Invalid pi-sync settings: ${field} must be a string.`); } return value.trim() || void 0; } function normalizeStoragePath(value) { const normalized = value.trim().replace(/^\/+|\/+$/gu, ""); if (!normalized || normalized.length > 1024 || normalized.startsWith("-") || normalized.includes("\\") || hasControlCharacter2(normalized) || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) { throw new Error("Invalid pi-sync settings: storage.path must be a safe relative path."); } return normalized; } function normalizeS3Endpoint(value) { let url; try { url = new URL(value.trim()); } catch { throw new Error("Invalid pi-sync S3 endpoint."); } const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]"; if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) { throw new Error("Invalid pi-sync S3 endpoint: HTTPS is required except for loopback."); } url.pathname = url.pathname.replace(/\/+$/gu, ""); return url.toString().replace(/\/$/u, ""); } function normalizeS3Bucket(value) { const bucket = requiredString(value, "S3 bucket"); if (bucket.includes("/") || bucket.includes("\\") || bucket.startsWith("-")) { throw new Error("Invalid pi-sync S3 bucket."); } return bucket; } function normalizeEndpointIdentity(endpoint) { try { const url = new URL(endpoint.trim()); url.hostname = url.hostname.toLowerCase(); url.pathname = url.pathname.replace(/\/+$/gu, ""); return url.toString().replace(/\/$/u, ""); } catch { return endpoint.trim(); } } async function configuredSessionDir() { const settings = await readJsonIfExists( path2.join(agentDir(), "settings.json") ); return settings?.sessionDir ? expandHome(settings.sessionDir) : void 0; } async function sessionDirForApply(ctx, snapshot) { const contextSessionDir = sessionDirFromContext(ctx); const localSessionDir = await configuredSessionDir(); if (contextSessionDir && path2.resolve(contextSessionDir) !== path2.resolve(localSessionDir ?? "")) { return contextSessionDir; } return sessionDirFromSnapshot(snapshot) ?? contextSessionDir; } function sessionDirFromSnapshot(snapshot) { const settingsFile = snapshot.files.find((file) => file.path === "settings.json"); if (!settingsFile) return void 0; try { const settings = JSON.parse( decodeBase64Strict(settingsFile.contentBase64, settingsFile.path).toString("utf8") ); return settings.sessionDir ? expandHome(settings.sessionDir) : void 0; } catch { return void 0; } } function decodeBase64Strict(value, filePath) { if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) { throw new Error(`Invalid base64 content in snapshot file: ${filePath}`); } return Buffer.from(value, "base64"); } async function readStateForConfig(config) { return await readJsonIfExists(statePathForConfig(config)) ?? { version: STATE_VERSION, profile: config.snapshotIdentity, lastFileHashes: {} }; } async function writeStateForConfig(config, state) { await writeJson(statePathForConfig(config), state); } function statePathForConfig(config) { const identity = backendIdentityCoordinates(config); const hash = createHash("sha256").update(identity).digest("hex").slice(0, 16); return path2.join(stateDir(), "setups", `${config.backend.type}-${hash}.state.json`); } function backendIdentityCoordinates(config) { switch (config.backend.type) { case "s3": return JSON.stringify([ "s3", normalizeEndpointIdentity(config.backend.profile.endpoint), config.backend.destination.bucket, config.storagePath ]); case "git": return JSON.stringify([ "git", normalizeGitRemoteIdentity(config.backend.profile.remote), config.backend.destination.branch, config.storagePath ]); case "webdav": return JSON.stringify([ "webdav", normalizeWebDavIdentityUrl(config.backend.profile.url), config.backend.profile.username, config.storagePath ]); } } function agentDir() { return getAgentDir2(); } function expandHome(value) { return value === "~" || value.startsWith("~/") ? path2.join(os.homedir(), value.slice(2)) : value; } function localConfigTemplate() { return { version: 3, onSwitch: DEFAULT_ON_SWITCH, storageConnections: {}, syncSetups: {} }; } async function readLocalConfigDocument() { const document = await readMigratingLocalConfigDocument((settings) => { validateSettingsDocument(settings); }); if (document) validateSettingsDocument(document.parsed); return document; } async function readLocalConfigObject() { return (await readLocalConfigDocument())?.parsed; } var configUpdateQueue = Promise.resolve(); function updateLocalConfig(update, signal) { const operation = configUpdateQueue.then(() => { signal?.throwIfAborted(); return performLocalConfigUpdate(update, signal); }); configUpdateQueue = operation.then( () => void 0, () => void 0 ); return operation; } async function performLocalConfigUpdate(update, signal) { return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal); } function lockPath() { return path2.join(stateDir(), "lock"); } async function ensureStateDir() { await fs2.mkdir(stateDir(), { recursive: true }); } async function readJsonIfExists(filePath) { try { return JSON.parse(await fs2.readFile(filePath, "utf8")); } catch (error) { if (error.code === "ENOENT") return void 0; throw error; } } async function writeJson(filePath, value) { await fs2.mkdir(path2.dirname(filePath), { recursive: true }); const temp = `${filePath}.${process.pid}.${randomUUID2()}.tmp`; await fs2.writeFile(temp, `${JSON.stringify(value, null, " ")} `, { mode: 384 }); if (process.platform !== "win32") await fs2.chmod(temp, 384); await fs2.rename(temp, filePath); } function sessionTokenWarnings(config) { if (!isCloudflareR2Endpoint(config.endpoint) || !config.sessionToken) return []; return [ "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." ]; } function syncSessionsWarnings(config) { if (!config.include.includes("sessions")) return []; return [ "sessions: included; Pi session JSONL can contain prompts, tool output, file paths, images, and secrets. Sync sessions only to storage you trust." ]; } function isCloudflareR2Endpoint(endpoint) { const value = endpoint?.trim(); if (!value) return false; try { const hostname = new URL(value).hostname.toLowerCase(); return hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com"); } catch { return false; } } function hasControlCharacter2(value) { return /[\u0000-\u001f\u007f-\u009f]/u.test(value); } export { localConfigPath, activeLocalConfigPath, consumeLocalConfigMigrationNotice, createLocalConfigDocument, normalizeWebDavUrl, normalizeWebDavPath, validateWebDavNamespace, validateWebDavCredentials, isMissingConfigError, loadConfig, loadPartialConfig, syncSetupStorageReview, syncSetupReviewIdentity, syncConfigReviewIdentity, syncConfigReviewFingerprint, configuredSyncSetupNames, loadOnSwitch, normalizeOnSwitch, effectiveSyncSetupRemoteIdentity, validateConfigName, configuredSessionDir, sessionDirForApply, readStateForConfig, writeStateForConfig, agentDir, localConfigTemplate, readLocalConfigObject, updateLocalConfig, lockPath, ensureStateDir, sessionTokenWarnings, syncSessionsWarnings, isCloudflareR2Endpoint }; //# sourceMappingURL=chunk-APZVBSM6.ts.map