@narumitw/pi-sync 0.50.3 → 0.51.1
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 +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +89 -101
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +197 -205
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
- package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
- package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
|
@@ -7,122 +7,108 @@ import { withLock } from "../state/lock.js";
|
|
|
7
7
|
import { withStateDirectoryAccess } from "../state/state-directory.js";
|
|
8
8
|
import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
|
|
9
9
|
import type { SyncAttentionController } from "../ui/sync-attention.js";
|
|
10
|
+
import { setSyncStatus } from "../ui/sync-status.js";
|
|
10
11
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
11
12
|
import { combineSignals, throwIfAborted } from "./signals.js";
|
|
12
13
|
import { errorMessage } from "./sync-errors.js";
|
|
13
14
|
import type { SyncLoaders } from "./sync-loaders.js";
|
|
14
15
|
|
|
15
|
-
const STATUS_KEY = "sync";
|
|
16
|
-
|
|
17
16
|
/** One owned task per extension session, never keyed by the shared headless UI. */
|
|
18
|
-
export function createStartupCheck(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
promise: Promise<void>;
|
|
27
|
-
owner: ExtensionContext["sessionManager"];
|
|
28
|
-
}
|
|
29
|
-
| undefined;
|
|
17
|
+
export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentionController, timeoutMs = 30_000) {
|
|
18
|
+
let active:
|
|
19
|
+
| {
|
|
20
|
+
controller: AbortController;
|
|
21
|
+
promise: Promise<void>;
|
|
22
|
+
owner: ExtensionContext["sessionManager"];
|
|
23
|
+
}
|
|
24
|
+
| undefined;
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
async stop() {
|
|
121
|
-
const task = active;
|
|
122
|
-
if (!task) return;
|
|
123
|
-
task.controller.abort(new DOMException("Startup check cancelled", "AbortError"));
|
|
124
|
-
// Drain the actual work, including child/ref cleanup, before foreground guards.
|
|
125
|
-
await task.promise;
|
|
126
|
-
},
|
|
127
|
-
};
|
|
26
|
+
return {
|
|
27
|
+
start(ctx: ExtensionContext, sessionSignal: AbortSignal, initialConfig?: AnySyncConfig) {
|
|
28
|
+
if (!ctx.hasUI || sessionSignal.aborted) return;
|
|
29
|
+
if (active) throw new Error("Previous startup check has not been drained.");
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const signal = combineSignals(sessionSignal, controller.signal);
|
|
32
|
+
const task = { controller, owner: ctx.sessionManager, promise: Promise.resolve() };
|
|
33
|
+
active = task;
|
|
34
|
+
const isCurrent = () => !sessionSignal.aborted && active === task && task.owner === ctx.sessionManager;
|
|
35
|
+
const timer = setTimeout(
|
|
36
|
+
() => controller.abort(new DOMException("Startup check timed out", "TimeoutError")),
|
|
37
|
+
timeoutMs,
|
|
38
|
+
);
|
|
39
|
+
let config: AnySyncConfig | undefined;
|
|
40
|
+
let checking = false;
|
|
41
|
+
// Handle every detached rejection here; Pi's awaited event runner cannot catch it.
|
|
42
|
+
task.promise = Promise.resolve()
|
|
43
|
+
.then(async () => {
|
|
44
|
+
try {
|
|
45
|
+
throwIfAborted(signal);
|
|
46
|
+
config = initialConfig ?? (await loadConfigForCheck());
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
if (!config.automatic) return;
|
|
49
|
+
const captured = config;
|
|
50
|
+
const identity = syncCheckConfigFingerprint(captured);
|
|
51
|
+
checking = true;
|
|
52
|
+
setSyncStatus(ctx, "sync ...");
|
|
53
|
+
const { inspectSync } = await loaders.inspection();
|
|
54
|
+
throwIfAborted(signal);
|
|
55
|
+
const currentConfig = await loadConfigForCheck();
|
|
56
|
+
throwIfAborted(signal);
|
|
57
|
+
if (syncCheckConfigFingerprint(currentConfig) !== identity) return;
|
|
58
|
+
// Lock order: state migration guard → sync operation → backend cache.
|
|
59
|
+
// Read heads may write only private cache data. Never call doctor/syncBoth.
|
|
60
|
+
const inspection = await withStateDirectoryAccess(() => {
|
|
61
|
+
throwIfAborted(signal);
|
|
62
|
+
return withLock("startup-check", () => {
|
|
63
|
+
throwIfAborted(signal);
|
|
64
|
+
return inspectSync(captured, snapshotOptionsForContext(ctx, captured), signal);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
throwIfAborted(signal);
|
|
68
|
+
const latest = await loadConfigForCheck();
|
|
69
|
+
throwIfAborted(signal);
|
|
70
|
+
if (syncCheckConfigFingerprint(latest) !== identity) return;
|
|
71
|
+
const state = await withStateDirectoryAccess(() => readStateForConfig(latest));
|
|
72
|
+
throwIfAborted(signal);
|
|
73
|
+
if (syncStateFingerprint(state) !== inspection.stateIdentity || !isCurrent()) return;
|
|
74
|
+
attention.observe({
|
|
75
|
+
setupName: captured.setupName,
|
|
76
|
+
configIdentity: identity,
|
|
77
|
+
checkedAt: new Date().toISOString(),
|
|
78
|
+
inspection,
|
|
79
|
+
});
|
|
80
|
+
if (ctx.mode === "rpc") attention.notifyObservation(ctx);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!isCurrent() || sessionSignal.aborted || isMissingConfigError(error)) return;
|
|
83
|
+
if (signal.aborted && controller.signal.reason?.name !== "TimeoutError") return;
|
|
84
|
+
// A settings edit invalidates failures as well as successful observations.
|
|
85
|
+
if (config) {
|
|
86
|
+
const latest = await loadConfigForCheck().catch(() => undefined);
|
|
87
|
+
if (!isCurrent() || (signal.aborted && controller.signal.reason?.name !== "TimeoutError")) return;
|
|
88
|
+
if (!latest || syncCheckConfigFingerprint(latest) !== syncCheckConfigFingerprint(config)) return;
|
|
89
|
+
}
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
`pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}. Run /sync status to retry. No startup transfer was performed.`,
|
|
92
|
+
"warning",
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (isCurrent() && checking) await attention.publish(ctx, sessionSignal);
|
|
97
|
+
if (active === task) active = undefined;
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {
|
|
101
|
+
// UI teardown can itself throw. No unhandled rejection or retained timer.
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (active === task) active = undefined;
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async stop() {
|
|
107
|
+
const task = active;
|
|
108
|
+
if (!task) return;
|
|
109
|
+
task.controller.abort(new DOMException("Startup check cancelled", "AbortError"));
|
|
110
|
+
// Drain the actual work, including child/ref cleanup, before foreground guards.
|
|
111
|
+
await task.promise;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
128
114
|
}
|
|
@@ -9,100 +9,96 @@ import { includeFromSelectionConfig } from "./sync-policy.js";
|
|
|
9
9
|
import { syncPolicyChanged } from "./sync-state.js";
|
|
10
10
|
|
|
11
11
|
export type SyncDecisionKind =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
| "remote-or-policy-changed"
|
|
13
|
+
| "both-changed"
|
|
14
|
+
| "first-sync-settings-diverged"
|
|
15
|
+
| "first-sync-sessions-diverged"
|
|
16
|
+
| "remote-empty";
|
|
17
17
|
|
|
18
18
|
export type SyncResolutionDirection = "push" | "pull";
|
|
19
19
|
|
|
20
20
|
export interface SyncDecision {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
kind: SyncDecisionKind;
|
|
22
|
+
setupName: string;
|
|
23
|
+
configIdentity: string;
|
|
24
|
+
causes: {
|
|
25
|
+
localChanged: boolean;
|
|
26
|
+
remoteChanged: boolean;
|
|
27
|
+
policyChanged: boolean;
|
|
28
|
+
};
|
|
29
|
+
previousInclude?: readonly string[];
|
|
30
|
+
currentInclude: readonly string[];
|
|
31
|
+
review: string;
|
|
32
|
+
directions: readonly SyncResolutionDirection[];
|
|
33
|
+
directMessage: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
isSyncDecisionRequiredError,
|
|
38
|
+
SyncDecisionRequiredError,
|
|
39
39
|
} from "./sync-errors.js";
|
|
40
40
|
|
|
41
41
|
interface CreateSyncDecisionOptions {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
kind: SyncDecisionKind;
|
|
43
|
+
config: AnySyncConfig;
|
|
44
|
+
state: SyncState;
|
|
45
|
+
local: Snapshot;
|
|
46
|
+
remote?: Snapshot;
|
|
47
|
+
localChanged: boolean;
|
|
48
|
+
remoteChanged: boolean;
|
|
49
|
+
directMessage: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function createSyncDecision(options: CreateSyncDecisionOptions) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
review,
|
|
101
|
-
directions: kind === "remote-empty" ? ["push"] : ["push", "pull"],
|
|
102
|
-
directMessage: options.directMessage,
|
|
103
|
-
});
|
|
53
|
+
const { config, state, local, remote, kind } = options;
|
|
54
|
+
const policyChanged = Boolean(state.lastAppliedSnapshot) && syncPolicyChanged(state, config);
|
|
55
|
+
const previousInclude = includeFromSelectionConfig(state);
|
|
56
|
+
const currentInclude = [...config.include];
|
|
57
|
+
const causes = {
|
|
58
|
+
localChanged: options.localChanged,
|
|
59
|
+
remoteChanged: options.remoteChanged,
|
|
60
|
+
policyChanged,
|
|
61
|
+
};
|
|
62
|
+
const causeLines =
|
|
63
|
+
kind === "first-sync-settings-diverged"
|
|
64
|
+
? ["This machine and the remote have different Pi settings on first sync."]
|
|
65
|
+
: kind === "first-sync-sessions-diverged"
|
|
66
|
+
? ["Pi settings match, but local and remote sessions differ on first sync."]
|
|
67
|
+
: kind === "remote-empty"
|
|
68
|
+
? ["The remote storage location is empty."]
|
|
69
|
+
: [
|
|
70
|
+
...(causes.localChanged ? ["Local content changed since the last sync."] : []),
|
|
71
|
+
...(causes.remoteChanged ? ["Remote content changed since the last sync."] : []),
|
|
72
|
+
...(causes.policyChanged ? ["Included content changed since the last sync."] : []),
|
|
73
|
+
];
|
|
74
|
+
const comparison = remote
|
|
75
|
+
? formatDiff(local, remote)
|
|
76
|
+
: formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local);
|
|
77
|
+
const review = [
|
|
78
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
79
|
+
"",
|
|
80
|
+
"Why a decision is required:",
|
|
81
|
+
...causeLines,
|
|
82
|
+
...(policyChanged
|
|
83
|
+
? ["", `Previously included: ${safeList(previousInclude)}`, `Currently included: ${safeList(currentInclude)}`]
|
|
84
|
+
: []),
|
|
85
|
+
"",
|
|
86
|
+
"Observed differences:",
|
|
87
|
+
...comparison.split("\n").map(safeTerminalText),
|
|
88
|
+
].join("\n");
|
|
89
|
+
return new SyncDecisionRequiredError({
|
|
90
|
+
kind,
|
|
91
|
+
setupName: config.setupName,
|
|
92
|
+
configIdentity: syncConfigReviewIdentity(config),
|
|
93
|
+
causes,
|
|
94
|
+
...(policyChanged ? { previousInclude: [...previousInclude] } : {}),
|
|
95
|
+
currentInclude,
|
|
96
|
+
review,
|
|
97
|
+
directions: kind === "remote-empty" ? ["push"] : ["push", "pull"],
|
|
98
|
+
directMessage: options.directMessage,
|
|
99
|
+
});
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
function safeList(values: readonly string[]) {
|
|
107
|
-
|
|
103
|
+
return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
|
|
108
104
|
}
|
|
@@ -2,52 +2,45 @@ import { errorMessage } from "./sync-errors.js";
|
|
|
2
2
|
|
|
3
3
|
/** Add recovery advice without changing backend outcome semantics or printing error causes. */
|
|
4
4
|
export function syncErrorGuidance(error: unknown): string {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} else if (/settings|config|already exists|duplicates|same normalized/iu.test(message)) {
|
|
47
|
-
next =
|
|
48
|
-
"Correct the named field or item in /sync. If the settings file is invalid, repair it before retrying; do not replace it with defaults.";
|
|
49
|
-
} else {
|
|
50
|
-
next = "Open /sync → More → Check setup and review the reported checks before retrying.";
|
|
51
|
-
}
|
|
52
|
-
return `${message}\n${next}`;
|
|
5
|
+
const detail = errorMessage(error);
|
|
6
|
+
const message = detail.length > 1200 ? `${detail.slice(0, 1200)}… (details truncated)` : detail;
|
|
7
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
8
|
+
const status = (error as { status?: number } | undefined)?.status;
|
|
9
|
+
let next: string;
|
|
10
|
+
if (code === "ENOSPC") {
|
|
11
|
+
next = "Free disk space for the private pi-sync settings/state directory, then retry.";
|
|
12
|
+
} else if (code && ["EACCES", "EPERM", "EROFS"].includes(code)) {
|
|
13
|
+
next = "Check write access to the private pi-sync settings/state directory, then retry.";
|
|
14
|
+
} else if (code && ["EBUSY", "EIO", "EMFILE", "ENFILE"].includes(code)) {
|
|
15
|
+
next = "Check local disk availability and other processes using the private settings file, then retry.";
|
|
16
|
+
} else if (/changed while|type changed|reopen/iu.test(message)) {
|
|
17
|
+
next = "Reopen the item in /sync and review its current settings before saving again.";
|
|
18
|
+
} else if (
|
|
19
|
+
status === 401 ||
|
|
20
|
+
status === 403 ||
|
|
21
|
+
/Permission denied|Authentication failed|HTTP (401|403)|failed \((401|403)\)/iu.test(message)
|
|
22
|
+
) {
|
|
23
|
+
next = "Check the storage connection's credentials and remote permissions in /sync → More → Storage connections.";
|
|
24
|
+
} else if (status === 404 || /HTTP 404|failed \(404\)/iu.test(message)) {
|
|
25
|
+
next =
|
|
26
|
+
"Verify the connection address and setup's bucket, branch, or path with your provider; this response alone does not prove which resource is missing.";
|
|
27
|
+
} else if (status !== undefined && status >= 500) {
|
|
28
|
+
next = "Check your storage provider's service status, then retry Check setup before syncing.";
|
|
29
|
+
} else if (/timed out|timeout|request failed|resolve host|Could not resolve|ENOTFOUND|ECONNREFUSED/iu.test(message)) {
|
|
30
|
+
next = "Check the server address and network connection, then run /sync doctor for the affected setup.";
|
|
31
|
+
} else if (/strong ETag|If-Match|If-None-Match|precondition|conditional header/iu.test(message)) {
|
|
32
|
+
next =
|
|
33
|
+
"Ask your provider to check strong ETags and conditional writes, including proxy behavior, or choose compatible storage. Do not bypass these safety checks.";
|
|
34
|
+
} else if (/spawn git ENOENT/iu.test(message)) {
|
|
35
|
+
next = "Install Git 2.30 or newer and make sure Pi can find git on PATH, then retry.";
|
|
36
|
+
} else if (/response exceeds.*byte limit/iu.test(message)) {
|
|
37
|
+
next =
|
|
38
|
+
"Verify the storage endpoint; the server returned an unexpectedly large response. Check your provider's logs before retrying.";
|
|
39
|
+
} else if (/settings|config|already exists|duplicates|same normalized/iu.test(message)) {
|
|
40
|
+
next =
|
|
41
|
+
"Correct the named field or item in /sync. If the settings file is invalid, repair it before retrying; do not replace it with defaults.";
|
|
42
|
+
} else {
|
|
43
|
+
next = "Open /sync → More → Check setup and review the reported checks before retrying.";
|
|
44
|
+
}
|
|
45
|
+
return `${message}\n${next}`;
|
|
53
46
|
}
|
package/src/sync/sync-errors.ts
CHANGED
|
@@ -3,21 +3,21 @@ import type { SyncDecision } from "./sync-decision.js";
|
|
|
3
3
|
export class SetupPullRequiresUiError extends Error {}
|
|
4
4
|
|
|
5
5
|
export class SyncDecisionRequiredError extends Error {
|
|
6
|
-
|
|
6
|
+
readonly decision: SyncDecision;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
constructor(decision: SyncDecision) {
|
|
9
|
+
super(decision.directMessage);
|
|
10
|
+
this.name = "SyncDecisionRequiredError";
|
|
11
|
+
this.decision = decision;
|
|
12
|
+
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function isSyncDecisionRequiredError(error: unknown): error is SyncDecisionRequiredError {
|
|
16
|
-
|
|
16
|
+
return error instanceof SyncDecisionRequiredError;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function errorMessage(error: unknown) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
|
|
22
|
+
return message.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
|
|
23
23
|
}
|
|
@@ -9,43 +9,39 @@ import { hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
|
|
|
9
9
|
|
|
10
10
|
/** Advisory baseline-relative observation, never an authorization to transfer files. */
|
|
11
11
|
export async function inspectSync(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
config: AnySyncConfig,
|
|
13
|
+
options: SnapshotOptions,
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
16
16
|
) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
stateIdentity: syncStateFingerprint(state),
|
|
39
|
-
destination: backend.destination,
|
|
40
|
-
capability: backend.capability,
|
|
41
|
-
};
|
|
17
|
+
throwIfAborted(signal);
|
|
18
|
+
const backend = await factory(config);
|
|
19
|
+
throwIfAborted(signal);
|
|
20
|
+
const local = await createSnapshot(config.snapshotIdentity, { ...options, signal });
|
|
21
|
+
throwIfAborted(signal);
|
|
22
|
+
const state = await readStateForConfig(config);
|
|
23
|
+
throwIfAborted(signal);
|
|
24
|
+
const head = await backend.readHead(signal);
|
|
25
|
+
throwIfAborted(signal);
|
|
26
|
+
return {
|
|
27
|
+
head,
|
|
28
|
+
selectionState: head ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] }) : undefined,
|
|
29
|
+
localFiles: local.files.length,
|
|
30
|
+
localChanged: hasLocalChanges(local, state, config),
|
|
31
|
+
remoteChanged: remoteChangedSinceState(head, state, config, (left, right) => backend.sameRevision(left, right)),
|
|
32
|
+
firstSync: !state.lastAppliedSnapshot,
|
|
33
|
+
emptyInclude: config.include.length === 0,
|
|
34
|
+
stateIdentity: syncStateFingerprint(state),
|
|
35
|
+
destination: backend.destination,
|
|
36
|
+
capability: backend.capability,
|
|
37
|
+
};
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export type SyncInspection = Awaited<ReturnType<typeof inspectSync>>;
|
|
45
41
|
|
|
46
42
|
export interface StartupObservation {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
setupName: string;
|
|
44
|
+
configIdentity: string;
|
|
45
|
+
checkedAt: string;
|
|
46
|
+
inspection: SyncInspection;
|
|
51
47
|
}
|