@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
|
@@ -3,62 +3,57 @@ import { inspectLock, isLockGuardHeld, isStaleLock, type LockInspection } from "
|
|
|
3
3
|
import type { LockFile } from "./state-types.js";
|
|
4
4
|
|
|
5
5
|
export type OperationAvailability =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
| { kind: "free" }
|
|
7
|
+
| { kind: "live"; lock: LockFile }
|
|
8
|
+
| { kind: "busy"; lock?: LockFile; metadata: LockInspection["status"] }
|
|
9
|
+
| { kind: "recoverable-stale"; lock: LockFile }
|
|
10
|
+
| { kind: "recoverable-unreadable" }
|
|
11
|
+
| { kind: "inspection-error"; message: string };
|
|
12
12
|
|
|
13
13
|
interface OperationInspectionDependencies {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
inspectMetadata(): Promise<LockInspection>;
|
|
15
|
+
inspectGuard(): Promise<boolean>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const DEFAULT_INSPECTION_DEPENDENCIES: OperationInspectionDependencies = {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
inspectMetadata: inspectLock,
|
|
20
|
+
inspectGuard: isLockGuardHeld,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export async function inspectOperationAvailability(
|
|
24
|
-
|
|
24
|
+
dependencies: OperationInspectionDependencies = DEFAULT_INSPECTION_DEPENDENCIES,
|
|
25
25
|
): Promise<OperationAvailability> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
try {
|
|
27
|
+
const metadata = await dependencies.inspectMetadata();
|
|
28
|
+
const guardHeld = await dependencies.inspectGuard();
|
|
29
|
+
return classifyOperationAvailability(metadata, guardHeld);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return { kind: "inspection-error", message: errorMessage(error) };
|
|
32
|
+
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function classifyOperationAvailability(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (metadata.status === "unreadable") return { kind: "recoverable-unreadable" };
|
|
53
|
-
return { kind: "free" };
|
|
35
|
+
export function classifyOperationAvailability(metadata: LockInspection, guardHeld: boolean): OperationAvailability {
|
|
36
|
+
if (metadata.status === "valid" && !isStaleLock(metadata.lock)) {
|
|
37
|
+
return { kind: "live", lock: metadata.lock };
|
|
38
|
+
}
|
|
39
|
+
if (guardHeld) {
|
|
40
|
+
return {
|
|
41
|
+
kind: "busy",
|
|
42
|
+
metadata: metadata.status,
|
|
43
|
+
...(metadata.status === "valid" ? { lock: metadata.lock } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (metadata.status === "valid") {
|
|
47
|
+
return { kind: "recoverable-stale", lock: metadata.lock };
|
|
48
|
+
}
|
|
49
|
+
if (metadata.status === "unreadable") return { kind: "recoverable-unreadable" };
|
|
50
|
+
return { kind: "free" };
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
export function operationBlocksChanges(availability: OperationAvailability) {
|
|
57
|
-
|
|
54
|
+
return availability.kind !== "free";
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
export function operationCanRecover(availability: OperationAvailability) {
|
|
61
|
-
|
|
62
|
-
availability.kind === "recoverable-stale" || availability.kind === "recoverable-unreadable"
|
|
63
|
-
);
|
|
58
|
+
return availability.kind === "recoverable-stale" || availability.kind === "recoverable-unreadable";
|
|
64
59
|
}
|
|
@@ -13,226 +13,224 @@ const MIGRATION_LOCK_STALE_MS = 30_000;
|
|
|
13
13
|
const MIGRATION_LOCK_UPDATE_MS = 10_000;
|
|
14
14
|
|
|
15
15
|
export type StateDirectoryPreparation =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
| { status: "ready" }
|
|
17
|
+
| { status: "migrated"; message: string }
|
|
18
|
+
| { status: "deferred"; message: string };
|
|
19
19
|
|
|
20
20
|
interface StateDirectoryGuard {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
release: () => Promise<void>;
|
|
22
|
+
throwIfCompromised: () => void;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
interface SharedStateDirectoryGuard {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
guard: Promise<StateDirectoryGuard>;
|
|
27
|
+
users: number;
|
|
28
|
+
closing?: Promise<void>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const sharedStateDirectoryGuards = new Map<string, SharedStateDirectoryGuard>();
|
|
32
32
|
|
|
33
33
|
export function stateDir() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const roots = inspectStateRoots();
|
|
35
|
+
if (roots.canonical) return canonicalStateDir();
|
|
36
|
+
if (roots.legacy) return legacyStateDir();
|
|
37
|
+
return canonicalStateDir();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function legacyStateDir() {
|
|
41
|
-
|
|
41
|
+
return path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function stateDirectoryMigrationNotice() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const roots = inspectStateRoots();
|
|
46
|
+
if (!roots.legacy) return undefined;
|
|
47
|
+
return "Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function withStateDirectoryAccess<T>(fn: () => Promise<T>): Promise<T> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const roots = inspectStateRoots();
|
|
52
|
+
if (!roots.legacy) return fn();
|
|
53
|
+
return runWithStateDirectoryGuard(await acquireSharedStateDirectoryGuard(), fn);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export async function migrateLegacyStateDirectory(): Promise<StateDirectoryPreparation> {
|
|
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
|
-
|
|
57
|
+
const initial = inspectStateRoots();
|
|
58
|
+
if (initial.canonical || !initial.legacy) return { status: "ready" };
|
|
59
|
+
|
|
60
|
+
let guard: StateDirectoryGuard;
|
|
61
|
+
try {
|
|
62
|
+
guard = await acquireStateDirectoryGuard();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!isLockHeldError(error)) throw error;
|
|
65
|
+
return {
|
|
66
|
+
status: "deferred",
|
|
67
|
+
message:
|
|
68
|
+
"pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return runWithStateDirectoryGuard(guard, async () => {
|
|
72
|
+
const roots = inspectStateRoots();
|
|
73
|
+
if (roots.canonical || !roots.legacy) return { status: "ready" };
|
|
74
|
+
|
|
75
|
+
const legacyLock = path.join(legacyStateDir(), "lock");
|
|
76
|
+
const legacyGuardHeld = await lockfile.check(legacyLock, {
|
|
77
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
78
|
+
lockfilePath: `${legacyLock}.guard`,
|
|
79
|
+
realpath: false,
|
|
80
|
+
stale: LOCK_GUARD_STALE_MS,
|
|
81
|
+
});
|
|
82
|
+
if (legacyGuardHeld || (await pathExists(legacyLock))) {
|
|
83
|
+
return {
|
|
84
|
+
status: "deferred",
|
|
85
|
+
message:
|
|
86
|
+
"pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
guard.throwIfCompromised();
|
|
90
|
+
await fs.rename(legacyStateDir(), canonicalStateDir());
|
|
91
|
+
guard.throwIfCompromised();
|
|
92
|
+
inspectStateRoots();
|
|
93
|
+
return {
|
|
94
|
+
status: "migrated",
|
|
95
|
+
message: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
async function acquireSharedStateDirectoryGuard(): Promise<StateDirectoryGuard> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
101
|
+
const key = migrationLockPath();
|
|
102
|
+
for (;;) {
|
|
103
|
+
let shared = sharedStateDirectoryGuards.get(key);
|
|
104
|
+
if (shared?.closing) {
|
|
105
|
+
await shared.closing;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!shared) {
|
|
109
|
+
shared = { guard: acquireStateDirectoryGuard(), users: 0 };
|
|
110
|
+
sharedStateDirectoryGuards.set(key, shared);
|
|
111
|
+
}
|
|
112
|
+
shared.users += 1;
|
|
113
|
+
let guard: StateDirectoryGuard;
|
|
114
|
+
try {
|
|
115
|
+
guard = await shared.guard;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (sharedStateDirectoryGuards.get(key) === shared) {
|
|
118
|
+
sharedStateDirectoryGuards.delete(key);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
let released = false;
|
|
123
|
+
return {
|
|
124
|
+
throwIfCompromised: guard.throwIfCompromised,
|
|
125
|
+
release: async () => {
|
|
126
|
+
if (released) return;
|
|
127
|
+
released = true;
|
|
128
|
+
shared.users -= 1;
|
|
129
|
+
if (shared.users > 0) return;
|
|
130
|
+
const closing = guard.release();
|
|
131
|
+
shared.closing = closing;
|
|
132
|
+
try {
|
|
133
|
+
await closing;
|
|
134
|
+
} finally {
|
|
135
|
+
if (sharedStateDirectoryGuards.get(key) === shared) {
|
|
136
|
+
sharedStateDirectoryGuards.delete(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
async function acquireStateDirectoryGuard(): Promise<StateDirectoryGuard> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
let compromisedError: Error | undefined;
|
|
146
|
+
const release = await lockfile.lock(getAgentDir(), {
|
|
147
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
148
|
+
lockfilePath: migrationLockPath(),
|
|
149
|
+
realpath: false,
|
|
150
|
+
stale: MIGRATION_LOCK_STALE_MS,
|
|
151
|
+
update: MIGRATION_LOCK_UPDATE_MS,
|
|
152
|
+
retries: { retries: 20, minTimeout: 10, maxTimeout: 50 },
|
|
153
|
+
onCompromised: (error) => {
|
|
154
|
+
compromisedError = error;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
release,
|
|
159
|
+
throwIfCompromised: () => {
|
|
160
|
+
if (compromisedError) throw compromisedError;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async function runWithStateDirectoryGuard<T>(guard: StateDirectoryGuard, fn: () => Promise<T>) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
166
|
+
let result: T | undefined;
|
|
167
|
+
let failed = false;
|
|
168
|
+
let failure: unknown;
|
|
169
|
+
try {
|
|
170
|
+
guard.throwIfCompromised();
|
|
171
|
+
result = await fn();
|
|
172
|
+
guard.throwIfCompromised();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
failed = true;
|
|
175
|
+
failure = error;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await guard.release();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (!failed) {
|
|
181
|
+
failed = true;
|
|
182
|
+
failure = error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (failed) throw failure;
|
|
186
|
+
return result as T;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function isLockHeldError(error: unknown) {
|
|
190
|
-
|
|
190
|
+
return (error as NodeJS.ErrnoException).code === "ELOCKED";
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
function canonicalStateDir() {
|
|
194
|
-
|
|
194
|
+
return path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
function migrationLockPath() {
|
|
198
|
-
|
|
198
|
+
return path.join(getAgentDir(), MIGRATION_LOCK_NAME);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function inspectStateRoots() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
const canonical = isDirectoryRoot(canonicalStateDir(), "canonical");
|
|
203
|
+
const legacy = isDirectoryRoot(legacyStateDir(), "legacy");
|
|
204
|
+
if (canonical && legacy) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return { canonical, legacy };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
function isDirectoryRoot(directory: string, label: string) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
213
|
+
try {
|
|
214
|
+
const entry = lstatSync(directory);
|
|
215
|
+
if (entry.isSymbolicLink()) {
|
|
216
|
+
throw new Error(`Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`);
|
|
217
|
+
}
|
|
218
|
+
if (!entry.isDirectory()) {
|
|
219
|
+
throw new Error(`${label} pi-sync state path is not a directory: ${directory}`);
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
async function pathExists(filePath: string) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
try {
|
|
230
|
+
await fs.lstat(filePath);
|
|
231
|
+
return true;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
238
236
|
}
|
package/src/state/state-types.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
export interface SyncState {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
version: number;
|
|
3
|
+
profile: string;
|
|
4
|
+
lastAppliedSnapshot?: string;
|
|
5
|
+
lastRemoteRevision?: string;
|
|
6
|
+
lastRemoteEtag?: string;
|
|
7
|
+
lastFileHashes: Record<string, string>;
|
|
8
|
+
include?: string[];
|
|
9
|
+
/** Legacy state fields are read only so v3 can detect and replace stale policy state. */
|
|
10
|
+
syncFiles?: string[];
|
|
11
|
+
syncSessions?: boolean;
|
|
12
|
+
extraFiles?: string[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface LockFile {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
id: string;
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
startedAt: string;
|
|
20
20
|
}
|
|
@@ -12,51 +12,51 @@ const STATE_VERSION = 2;
|
|
|
12
12
|
|
|
13
13
|
/** Bounded observation token; do not retain the full baseline in passive UI state. */
|
|
14
14
|
export function syncStateFingerprint(state: SyncState) {
|
|
15
|
-
|
|
15
|
+
return createHash("sha256").update(JSON.stringify(state)).digest("hex");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export async function readState(profile: string): Promise<SyncState> {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
return (
|
|
20
|
+
(await readJsonIfExists<SyncState>(statePath(profile))) ?? {
|
|
21
|
+
version: STATE_VERSION,
|
|
22
|
+
profile,
|
|
23
|
+
lastFileHashes: {},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export async function writeState(profile: string, state: SyncState) {
|
|
29
|
-
|
|
29
|
+
await writeJson(statePath(profile), state);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function readStateForConfig(config: AnySyncConfig): Promise<SyncState> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
return (
|
|
34
|
+
(await readJsonIfExists<SyncState>(statePathForConfig(config))) ?? {
|
|
35
|
+
version: STATE_VERSION,
|
|
36
|
+
profile: config.snapshotIdentity,
|
|
37
|
+
lastFileHashes: {},
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export async function writeStateForConfig(config: AnySyncConfig, state: SyncState) {
|
|
43
|
-
|
|
43
|
+
await writeJson(statePathForConfig(config), state);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function statePathForConfig(config: AnySyncConfig) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const identity = backendIdentityCoordinates(config);
|
|
48
|
+
const hash = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
49
|
+
return path.join(stateDir(), "setups", `${config.backend.type}-${hash}.state.json`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function statePath(profile: string) {
|
|
53
|
-
|
|
53
|
+
return path.join(stateDir(), `${safeName(profile)}.state.json`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function lockPath() {
|
|
57
|
-
|
|
57
|
+
return path.join(stateDir(), "lock");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export async function ensureStateDir() {
|
|
61
|
-
|
|
61
|
+
await fs.mkdir(stateDir(), { recursive: true });
|
|
62
62
|
}
|