@narumitw/pi-sync 0.34.0 → 0.38.0

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.
@@ -0,0 +1,155 @@
1
+ import {
2
+ activeLocalConfigPath,
3
+ isCloudflareR2Endpoint,
4
+ loadConfig,
5
+ readLocalConfigObject,
6
+ readStateForConfig,
7
+ } from "./config.js";
8
+ import { inspectLock, isStaleLock } from "./lock.js";
9
+ import { errorMessage, ownRecord, safeTerminalText } from "./manager-helpers.js";
10
+ import { syncIncludeSelection } from "./sync-policy.js";
11
+ import { countValidSyncSetups } from "./sync-setups-ui.js";
12
+ import type { AnySyncConfig } from "./types.js";
13
+
14
+ export const MAIN_MENU_ACTIONS = [
15
+ "Sync now (recommended)",
16
+ "Switch sync setup",
17
+ "Status & changes",
18
+ "Settings",
19
+ "More…",
20
+ ] as const;
21
+
22
+ export async function describeManagerState(
23
+ signal?: AbortSignal,
24
+ ): Promise<{ title: string; actions: string[] }> {
25
+ let raw: Record<string, unknown> | undefined;
26
+ try {
27
+ raw = await readLocalConfigObject();
28
+ } catch (error) {
29
+ return {
30
+ title: [
31
+ "Manage sync",
32
+ "",
33
+ "Settings file needs repair. Automatic sync and settings writes are paused.",
34
+ `Error: ${safeTerminalText(errorMessage(error))}`,
35
+ `File: ${safeTerminalText(await activeLocalConfigPath())}`,
36
+ "",
37
+ "Repair the JSON file, then reopen /sync.",
38
+ ].join("\n"),
39
+ actions: ["Help"],
40
+ };
41
+ }
42
+ if (!raw) {
43
+ return {
44
+ title: ["Manage sync", "", "Not set up.", "", "What do you want to do?"].join("\n"),
45
+ actions: ["Set up sync", "Help"],
46
+ };
47
+ }
48
+ const configuredTargets = ownRecord(raw.syncSetups);
49
+ if (raw.version === 3 && configuredTargets && Object.keys(configuredTargets).length === 0) {
50
+ return {
51
+ title: [
52
+ "Manage sync",
53
+ "",
54
+ "No sync setups are configured.",
55
+ "Add a sync setup using an existing storage connection.",
56
+ "",
57
+ "What do you want to do?",
58
+ ].join("\n"),
59
+ actions: ["Sync setups…", "Storage connections…", "Help"],
60
+ };
61
+ }
62
+ try {
63
+ const config = await loadConfig();
64
+ const lock = await inspectLock();
65
+ const liveLock = lock.status === "valid" && !isStaleLock(lock.lock);
66
+ const recoverableLock =
67
+ lock.status === "unreadable" || (lock.status === "valid" && isStaleLock(lock.lock));
68
+ const selection = syncIncludeSelection(config.include);
69
+ const noSyncedContent = config.include.length === 0;
70
+ const syncState =
71
+ liveLock || recoverableLock
72
+ ? undefined
73
+ : await readStateForConfig(config).catch(() => undefined);
74
+ const lastAppliedSnapshot =
75
+ liveLock || recoverableLock
76
+ ? "Unavailable while operations are locked"
77
+ : syncState?.lastAppliedSnapshot
78
+ ? safeTerminalText(syncState.lastAppliedSnapshot)
79
+ : syncState
80
+ ? "Never synced"
81
+ : "Unavailable";
82
+ const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
83
+ const mainActions = MAIN_MENU_ACTIONS.filter(
84
+ (action) => action !== "Switch sync setup" || canSwitch,
85
+ );
86
+ return {
87
+ title: [
88
+ "Manage sync",
89
+ "",
90
+ `Current sync setup: ${safeTerminalText(config.setupName)}`,
91
+ `Storage: ${backendStorageDescription(config)}`,
92
+ `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
93
+ `Automatic sync: ${config.automatic ? "On" : "Off"}`,
94
+ `Last applied: ${lastAppliedSnapshot}`,
95
+ "Remote status: Not checked",
96
+ ...(noSyncedContent
97
+ ? [
98
+ "",
99
+ "No included content is selected. Choose included content in Settings before syncing.",
100
+ ]
101
+ : []),
102
+ ...(liveLock
103
+ ? [
104
+ "",
105
+ `Operation in progress: ${safeTerminalText(lock.lock.command)} (pid ${lock.lock.pid}). Sync and settings changes are disabled.`,
106
+ ]
107
+ : []),
108
+ ...(recoverableLock
109
+ ? ["", "Recovery required: lock metadata is stale or unreadable."]
110
+ : []),
111
+ "",
112
+ "What do you want to do?",
113
+ ].join("\n"),
114
+ actions:
115
+ liveLock || recoverableLock
116
+ ? ["Status & changes", "History & recovery…", "Help"]
117
+ : noSyncedContent
118
+ ? ["Settings", ...(canSwitch ? ["Switch sync setup"] : []), "Status & changes", "More…"]
119
+ : mainActions,
120
+ };
121
+ } catch (error) {
122
+ if (signal?.aborted) throw error;
123
+ return {
124
+ title: [
125
+ "Manage sync",
126
+ "",
127
+ "Settings need attention. Automatic sync is paused.",
128
+ `Current sync setup: ${safeTerminalText(typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : "none")}`,
129
+ `Error: ${safeTerminalText(errorMessage(error))}`,
130
+ `File: ${safeTerminalText(await activeLocalConfigPath())}`,
131
+ "",
132
+ "What do you want to do?",
133
+ ].join("\n"),
134
+ actions: ["Sync setups…", "Storage connections…", "History & recovery…", "Help"],
135
+ };
136
+ }
137
+ }
138
+
139
+ export function backendStorageDescription(config: AnySyncConfig) {
140
+ const connection = safeTerminalText(config.connectionName);
141
+ switch (config.backend.type) {
142
+ case "s3": {
143
+ const type =
144
+ config.backend.profile.kind === "r2" ||
145
+ isCloudflareR2Endpoint(config.backend.profile.endpoint)
146
+ ? "Cloudflare R2"
147
+ : "S3-compatible";
148
+ return `${type} · ${connection} · ${safeTerminalText(config.backend.destination.bucket)}`;
149
+ }
150
+ case "webdav":
151
+ return `WebDAV · ${connection} · ${safeTerminalText(config.backend.destination.path)}`;
152
+ case "git":
153
+ return `Git · ${connection} · ${safeTerminalText(config.backend.destination.branch)}`;
154
+ }
155
+ }