@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.
- package/README.md +15 -3
- package/package.json +2 -1
- package/src/config-file.ts +98 -60
- package/src/file-selection.ts +88 -73
- package/src/manager-state.ts +155 -0
- package/src/manager-ui.ts +230 -289
- package/src/settings-ui.ts +103 -145
- package/src/storage-connections-ui.ts +157 -83
- package/src/sync-setups-ui.ts +186 -99
|
@@ -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
|
+
}
|