// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. import { createSyncAttentionController, syncAttentionMatchesConfig } from "./chunks/chunk-JNOOSPEW.ts"; import { completeSyncArguments, parseOptions, resolveSyncCommand, setSyncSetupCompletions, splitArgs, usage, validateCommandOptions } from "./chunks/chunk-4U6AFLJB.ts"; import { recoverSnapshotTransactionsOnStartup } from "./chunks/chunk-3NDD5QPM.ts"; import { unlock, withLock } from "./chunks/chunk-LJDSHNS3.ts"; import { SetupPullRequiresUiError, errorMessage, isSyncDecisionRequiredError } from "./chunks/chunk-W64NEHXT.ts"; import "./chunks/chunk-RGJ6SOYR.ts"; import "./chunks/chunk-EWX3TPLQ.ts"; import { activeLocalConfigPath, configuredSyncSetupNames, consumeLocalConfigMigrationNotice, createLocalConfigDocument, ensureStateDir, isMissingConfigError, loadConfig, loadPartialConfig, localConfigPath, localConfigTemplate, readLocalConfigObject, readStateForConfig, sessionTokenWarnings, syncSessionsWarnings } from "./chunks/chunk-APZVBSM6.ts"; import { migrateLegacyStateDirectory, stateDirectoryMigrationNotice, withStateDirectoryAccess } from "./chunks/chunk-F5QL2GPY.ts"; import { RemoteSelectionMismatchError, formatRemoteSelectionMismatch } from "./chunks/chunk-YQ6UW7IF.ts"; // src/sync-extension.ts var STATUS_KEY = "sync"; var AUTO_SYNC_OPTIONS = { yes: true, force: false, stale: false, silent: true, reload: false, auto: true, args: [] }; function sync(pi, dependencies = {}) { const loaders = { setupSwitch: cachedModuleLoader( dependencies.loadSetupSwitch ?? (() => import("./chunks/setup-switch-X3W3FALZ.ts")) ), snapshot: cachedModuleLoader(dependencies.loadSnapshot ?? (() => import("./chunks/snapshot-WBSV4MQG.ts"))), syncState: cachedModuleLoader(dependencies.loadSyncState ?? (() => import("./chunks/sync-state-36H7ULG2.ts"))), operations: cachedModuleLoader( dependencies.loadSyncOperations ?? (() => import("./chunks/sync-operations-MPAFVVHA.ts")) ) }; const attention = createSyncAttentionController(); let sessionAbort = new AbortController(); let shutdownAbort; pi.registerCommand("sync", { description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage", getArgumentCompletions: completeSyncArguments, handler: async (args, ctx) => { if (!ctx.hasUI) { throw new Error( "/sync requires TUI or RPC mode so results and safety prompts are observable." ); } const run = () => handleCommand(args, ctx, sessionAbort.signal, loaders, attention); if (splitArgs(args)[0] === "migrate-state") await run(); else await withStateDirectoryAccess(run); } }); pi.on("session_start", async (_event, ctx) => { shutdownAbort?.abort(new DOMException("Session replaced", "AbortError")); shutdownAbort = void 0; sessionAbort.abort(new DOMException("Session replaced", "AbortError")); sessionAbort = new AbortController(); const signal = sessionAbort.signal; attention.reset(ctx); let decision; try { decision = await withStateDirectoryAccess(() => startSession(ctx, signal, loaders)); } catch (error) { if (signal.aborted) return; ctx.ui.notify(`pi-sync state access failed: ${errorMessage(error)}`, "error"); return; } if (!decision || signal.aborted) return; attention.set(decision, "sync"); if (ctx.mode !== "tui") { ctx.ui.notify( `pi-sync auto sync skipped: ${formatRemoteSelectionMismatch( decision.setupName, decision.localInclude, decision.remoteInclude )} RPC review is read-only.`, "warning" ); } else if (attention.markOffered()) { await resolveSelectionAttention(ctx, attention, signal, loaders, { cancelLabel: "Later", withStateAccess: withStateDirectoryAccess }); } if (!signal.aborted) attention.publish(ctx); }); pi.on("session_shutdown", async (event, ctx) => { sessionAbort.abort(new DOMException("Session shut down", "AbortError")); attention.reset(ctx); shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError")); const controller = new AbortController(); shutdownAbort = controller; const signal = combineSignals(controller.signal, AbortSignal.timeout(3e4)); const reason = typeof event === "object" && event ? event.reason : void 0; try { if (reason !== "reload") { await withStateDirectoryAccess(async () => { if (signal.aborted) return; await autoPushSessions(ctx, signal, loaders); }); } } catch (error) { if (!signal.aborted) { ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning"); } } finally { if (shutdownAbort === controller) shutdownAbort = void 0; } if (signal.aborted) return; ctx.ui.setStatus(STATUS_KEY, void 0); }); } async function startSession(ctx, signal, loaders) { if (signal.aborted) return; try { const migrationNotice2 = stateDirectoryMigrationNotice(); if (migrationNotice2) ctx.ui.notify(migrationNotice2, "warning"); } catch (error) { ctx.ui.notify(`pi-sync state directory requires attention: ${errorMessage(error)}`, "error"); return; } try { await recoverSnapshotTransactionsOnStartup(); if (signal.aborted) return; } catch (error) { if (signal.aborted) return; ctx.ui.notify(`pi-sync recovery required: ${errorMessage(error)}`, "error"); return; } try { setSyncSetupCompletions(await configuredSyncSetupNames()); if (signal.aborted) return; } catch { if (signal.aborted) return; setSyncSetupCompletions([]); } const migrationNotice = consumeLocalConfigMigrationNotice(); if (migrationNotice) ctx.ui.notify(migrationNotice, "warning"); if (signal.aborted) return; return autoSync(ctx, signal, loaders); } async function handleCommand(rawArgs, ctx, sessionSignal, loaders, attention) { if (!rawArgs.trim()) { try { const { showSyncManager } = await import("./chunks/manager-ui-7E25VWUP.ts"); if (sessionSignal.aborted) return; await showSyncManager( ctx, (route, signal, onCommit, target) => executeCommand( route, ctx, combineSignals(sessionSignal, signal), loaders, onCommit, target ), sessionSignal, { getAttention: () => attention.current(), onSelectionResolved: (expected) => { if (attention.current() === expected) attention.clear(ctx); } } ); } catch (error) { if (sessionSignal.aborted) return; ctx.ui.setStatus(STATUS_KEY, void 0); ctx.ui.notify(errorMessage(error), "error"); } if (!sessionSignal.aborted) attention.publish(ctx); return; } const result = await executeCommand(rawArgs, ctx, sessionSignal, loaders); if (result.kind === "decision-required") { ctx.ui.notify(result.decision.directMessage, "error"); } else if (result.kind === "remote-selection-required") { const origin = directSelectionOrigin(rawArgs); if (origin) attention.set(result.decision, origin); const deterministic = splitArgs(rawArgs).some((arg) => arg === "--yes" || arg === "-y"); if (origin && ctx.mode === "tui" && !deterministic) { await resolveSelectionAttention(ctx, attention, sessionSignal, loaders); } else { ctx.ui.notify( formatRemoteSelectionMismatch( result.decision.setupName, result.decision.localInclude, result.decision.remoteInclude ), "error" ); } } await clearAttentionAfterCompletedOperation(rawArgs, result, ctx, attention, sessionSignal); await reconcileSelectionAttention(ctx, attention, sessionSignal); if (!sessionSignal.aborted) attention.publish(ctx); } async function clearAttentionAfterCompletedOperation(rawArgs, result, ctx, attention, signal) { if (result.kind !== "completed" || result.outcome === "cancelled" || signal.aborted) return; const [command, ...rest] = splitArgs(rawArgs); if (command !== "sync" && command !== "pull" && command !== "push") return; const current = attention.current(); if (!current) return; try { const options = parseOptions(rest); const setupName = options.setup ?? (await loadConfig()).setupName; if (signal.aborted || attention.current() !== current) return; if (current.decision.setupName === setupName) attention.clear(ctx); } catch { } } async function reconcileSelectionAttention(ctx, attention, signal) { const current = attention.current(); if (!current || signal.aborted) return; try { const config = await loadConfig(current.decision.setupName); if (signal.aborted || attention.current() !== current) return; if (!syncAttentionMatchesConfig(current, config)) attention.clear(ctx); } catch { if (!signal.aborted && attention.current() === current) attention.clear(ctx); } } function directSelectionOrigin(rawArgs) { const command = splitArgs(rawArgs)[0]; return command === "sync" || command === "pull" || command === "push" ? command : void 0; } async function resolveSelectionAttention(ctx, attention, signal, loaders, options = {}) { const current = attention.current(); if (!current || signal.aborted) return; const { dispatchManagerResult } = await import("./chunks/manager-result-dispatcher-YKBLZ7KL.ts"); if (signal.aborted || attention.current() !== current) return; await dispatchManagerResult( ctx, { kind: "remote-selection-required", decision: current.decision }, current.origin, (route, actionSignal, onCommit, target) => { const execute = () => executeRecoveryCommand( route, ctx, combineSignals(signal, actionSignal), loaders, onCommit, target ); return options.withStateAccess ? options.withStateAccess(execute) : execute(); }, signal, { cancelLabel: options.cancelLabel, withStateAccess: options.withStateAccess, onSelectionResolved: () => { if (attention.current() === current) attention.clear(ctx); } } ); } async function executeRecoveryCommand(rawArgs, ctx, signal, loaders, onCommit, setup) { try { const [subcommand, ...rest] = splitArgs(rawArgs); if (subcommand !== "sync" && subcommand !== "pull" && subcommand !== "push") { throw new Error(`Unsupported sync recovery route: ${subcommand ?? "missing"}`); } const options = parseOptions(rest); if (setup !== void 0) options.setup = setup; if (signal) options.signal = signal; if (onCommit) options.onCommit = onCommit; options.reload = false; options.auto = false; validateCommandOptions(subcommand, options); const operations = await loaders.operations(); throwIfAborted(options.signal); if (subcommand === "push") { const outcome = await withLock("push", () => operations.push(ctx, options)); return { kind: "completed", ...outcome ? { outcome } : {} }; } if (subcommand === "pull") { const outcome = await withLock("pull", () => operations.pull(ctx, options)); return { kind: "completed", ...outcome ? { outcome } : {} }; } await withLock("sync", () => operations.syncBoth(ctx, options)); return { kind: "completed" }; } catch (error) { if (signal?.aborted) return { kind: "failed" }; ctx.ui.setStatus(STATUS_KEY, void 0); if (error instanceof RemoteSelectionMismatchError) { return { kind: "remote-selection-required", decision: error.decision }; } if (isSyncDecisionRequiredError(error)) { return { kind: "decision-required", decision: error.decision }; } ctx.ui.notify(errorMessage(error), "error"); return { kind: "failed" }; } } async function executeCommand(rawArgs, ctx, signal, loaders, onCommit, setup) { try { const command = await resolveSyncCommand(rawArgs, ctx); if (signal?.aborted || !command) return { kind: "completed" }; const { subcommand, rest } = command; const options = parseOptions(rest); if (setup !== void 0) options.setup = setup; if (signal) options.signal = signal; if (onCommit) options.onCommit = onCommit; validateCommandOptions(subcommand, options); switch (subcommand) { case "help": ctx.ui.notify(usage(), "info"); return { kind: "completed" }; case "use": { const { useSyncSetup } = await loaders.setupSwitch(); throwIfAborted(options.signal); await useSyncSetup( ctx, options.args[0] ?? "", async (selectedSetup) => { const operations = await loaders.operations(); throwIfAborted(options.signal); return withLock( "pull", () => operations.pull(ctx, { ...options, setup: selectedSetup }) ); }, void 0, options.signal ); return { kind: "completed" }; } case "init": await initConfig(ctx, signal); return { kind: "completed" }; case "config": await showConfig(ctx, options); return { kind: "completed" }; case "files": { const { showFileSelection } = await import("./chunks/file-selection-4UABOP44.ts"); throwIfAborted(options.signal); await showFileSelection(ctx, options.setup, options.signal); return { kind: "completed" }; } case "status": { const operations = await loaders.operations(); throwIfAborted(options.signal); await operations.status(ctx, options); return { kind: "completed" }; } case "diff": { const operations = await loaders.operations(); throwIfAborted(options.signal); await operations.diff(ctx, options); return { kind: "completed" }; } case "doctor": { const operations = await loaders.operations(); throwIfAborted(options.signal); await operations.doctor(ctx, options); return { kind: "completed" }; } case "push": { const operations = await loaders.operations(); throwIfAborted(options.signal); const outcome = await withLock("push", () => operations.push(ctx, options)); return { kind: "completed", ...outcome ? { outcome } : {} }; } case "pull": { const operations = await loaders.operations(); throwIfAborted(options.signal); const outcome = await withLock("pull", () => operations.pull(ctx, options)); return { kind: "completed", ...outcome ? { outcome } : {} }; } case "sync": { const operations = await loaders.operations(); throwIfAborted(options.signal); await withLock("sync", () => operations.syncBoth(ctx, options)); return { kind: "completed" }; } case "history": { const operations = await loaders.operations(); throwIfAborted(options.signal); await operations.history(ctx, options); return { kind: "completed" }; } case "rollback": { const operations = await loaders.operations(); throwIfAborted(options.signal); await withLock("rollback", () => operations.rollback(ctx, options)); return { kind: "completed" }; } case "migrate-state": await migrateStateDirectory(ctx, options); return { kind: "completed" }; case "unlock": await unlock(ctx, options); return { kind: "completed" }; default: ctx.ui.notify(`Unknown /sync command: ${subcommand} ${usage()}`, "warning"); return { kind: "failed" }; } } catch (error) { if (signal?.aborted) return { kind: "failed" }; ctx.ui.setStatus(STATUS_KEY, void 0); if (error instanceof SetupPullRequiresUiError) throw error; if (error instanceof RemoteSelectionMismatchError) { return { kind: "remote-selection-required", decision: error.decision }; } if (isSyncDecisionRequiredError(error)) { return { kind: "decision-required", decision: error.decision }; } ctx.ui.notify(errorMessage(error), "error"); return { kind: "failed" }; } } async function migrateStateDirectory(ctx, options) { const notice = stateDirectoryMigrationNotice(); if (!notice) { ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info"); return; } if (!options.yes && !await ctx.ui.confirm( "Migrate pi-sync state directory", "Confirm that every other Pi process is closed. pi-sync will atomically rename .pisync/ to pi-sync/ without merging or deleting either root.", { signal: options.signal } )) { ctx.ui.notify("pi-sync state migration cancelled.", "info"); return; } throwIfAborted(options.signal); const result = await migrateLegacyStateDirectory(); throwIfAborted(options.signal); if (result.status === "ready") { ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info"); return; } ctx.ui.notify(result.message, result.status === "migrated" ? "info" : "warning"); } async function autoSync(ctx, signal, loaders) { try { const partial = await loadPartialConfig(); throwIfAborted(signal); if (!partial.automatic) return; await ensureStateDir(); throwIfAborted(signal); await loadConfig(); throwIfAborted(signal); const operations = await loaders.operations(); throwIfAborted(signal); await withLock("auto-sync", () => { throwIfAborted(signal); return operations.syncBoth(ctx, { ...AUTO_SYNC_OPTIONS, signal }); }); } catch (error) { if (signal.aborted || isMissingConfigError(error)) return; ctx.ui.setStatus(STATUS_KEY, void 0); if (error instanceof RemoteSelectionMismatchError) return error.decision; ctx.ui.notify(`pi-sync auto sync skipped: ${errorMessage(error)}`, "warning"); } } async function autoPushSessions(ctx, signal, loaders) { try { const partial = await loadPartialConfig(); throwIfAborted(signal); if (!partial.automatic) return; if (!partial.include.includes("sessions")) return; await ensureStateDir(); throwIfAborted(signal); const config = await loadConfig(); throwIfAborted(signal); if (!config.include.includes("sessions")) return; const [operations, snapshotModule, syncStateModule] = await Promise.all([ loaders.operations(), loaders.snapshot(), loaders.syncState() ]); throwIfAborted(signal); await withLock("auto-session-push", async () => { throwIfAborted(signal); const state = await readStateForConfig(config); throwIfAborted(signal); const local = await snapshotModule.createSnapshot( config.snapshotIdentity, snapshotOptionsForContext(ctx, config) ); throwIfAborted(signal); if (!syncStateModule.hasLocalChanges(local, state, config)) return; await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local }); }); } catch (error) { if (signal.aborted || isMissingConfigError(error)) return; ctx.ui.setStatus(STATUS_KEY, void 0); ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning"); } } async function initConfig(ctx, signal) { const configPath = localConfigPath(); if (await readLocalConfigObject()) { ctx.ui.notify(`Config already exists: ${await activeLocalConfigPath()}`, "info"); return; } if (ctx.mode === "tui") { const { showSetupWizard } = await import("./chunks/manager-ui-7E25VWUP.ts"); throwIfAborted(signal); await showSetupWizard(ctx, signal); return; } await createLocalConfigDocument(localConfigTemplate()); ctx.ui.notify( `Created ${configPath}. Add a storage connection and sync setup before syncing.`, "info" ); } async function showConfig(ctx, options) { const config = await loadConfig(options.setup); const warnings = [ ...config.backend.type === "s3" ? sessionTokenWarnings(config.backend.profile) : [], ...syncSessionsWarnings(config) ]; const storageLines = configStorageLines(config); ctx.ui.notify( [ "pi-sync config:", `sync setup: ${config.setupName}`, `storage connection: ${config.connectionName}`, ...storageLines, `storage path: ${config.storagePath}`, `automatic sync: ${config.automatic ? "enabled" : "disabled"}`, `included content: ${config.include.join(", ") || "none"}`, `sessions: ${config.include.includes("sessions") ? "included" : "not included"}`, `settings file: ${localConfigPath()}`, ...warnings ].join("\n"), warnings.length > 0 ? "warning" : "info" ); } function configStorageLines(config) { switch (config.backend.type) { case "git": return [ "kind: git", `remote: ${displayGitRemote(config.backend.profile.remote)}`, "authentication: existing Git/SSH credentials (not stored)", `branch: ${config.backend.destination.branch}` ]; case "webdav": return [ "kind: webdav", `url: ${displayWebDavUrl(config.backend.profile.url, config.backend.profile.username)}`, "username: configured (value hidden)", "password: configured" ]; case "s3": return [ "kind: s3", `endpoint: ${config.backend.profile.endpoint}`, `bucket: ${config.backend.destination.bucket}`, `region: ${config.backend.profile.region}`, "access key id: configured", "secret access key: configured", `session token: ${config.backend.profile.sessionToken ? "configured" : "not configured"}` ]; } } function displayGitRemote(value) { if (!value) return "missing"; try { const url = new URL(value); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return url.toString(); } catch { return value.replace(/^(?:[^@\s]+@)?(?[^:]+):.+$/u, "$:\u2026"); } } function displayWebDavUrl(value, username) { if (!value) return "missing"; try { const url = new URL(value); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return username ? `${url.origin}/\u2026` : `${url.origin}${url.pathname}`; } catch { return "invalid (value hidden)"; } } function throwIfAborted(signal) { if (!signal?.aborted) return; throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError"); } function combineSignals(primary, secondary) { return secondary ? AbortSignal.any([primary, secondary]) : primary; } function snapshotOptionsForContext(ctx, config) { return { include: config.include, sessionDir: sessionDirFromContext(ctx) }; } function sessionDirFromContext(ctx) { const manager = ctx.sessionManager; const usesDefaultSessionDir = manager.usesDefaultSessionDir; if (typeof usesDefaultSessionDir === "function" && usesDefaultSessionDir.call(manager)) { return void 0; } const getSessionDir = manager.getSessionDir; return typeof getSessionDir === "function" ? getSessionDir.call(manager) : void 0; } function cachedModuleLoader(load) { let pending; return () => { if (!pending) { pending = load().catch((error) => { pending = void 0; throw error; }); } return pending; }; } export { sync as default }; //# sourceMappingURL=index.ts.map