@narumitw/pi-sync 0.50.3 → 0.51.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 +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
package/src/state/json-file.ts
CHANGED
|
@@ -3,18 +3,18 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
export async function readJsonIfExists<T>(filePath: string): Promise<T | undefined> {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(await fs.readFile(filePath, "utf8")) as T;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function writeJson(filePath: string, value: unknown) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
const temp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
17
|
+
await fs.writeFile(temp, `${JSON.stringify(value, null, "\t")}\n`, { mode: 0o600 });
|
|
18
|
+
if (process.platform !== "win32") await fs.chmod(temp, 0o600);
|
|
19
|
+
await fs.rename(temp, filePath);
|
|
20
20
|
}
|
package/src/state/lock.ts
CHANGED
|
@@ -12,294 +12,278 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
|
12
12
|
const MAX_PROCESS_ID = 2_147_483_647;
|
|
13
13
|
|
|
14
14
|
interface Guard {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
release: () => Promise<void>;
|
|
16
|
+
throwIfCompromised: () => void;
|
|
17
|
+
isCompromised: () => boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export type LockInspection =
|
|
21
|
-
| { status: "missing" }
|
|
22
|
-
| { status: "unreadable" }
|
|
23
|
-
| { status: "valid"; lock: LockFile };
|
|
20
|
+
export type LockInspection = { status: "missing" } | { status: "unreadable" } | { status: "valid"; lock: LockFile };
|
|
24
21
|
|
|
25
22
|
export async function withLock<T>(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
command: string,
|
|
24
|
+
fn: () => Promise<T>,
|
|
25
|
+
options: { reclaimStale?: boolean } = {},
|
|
29
26
|
): Promise<T> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
await ensureStateDir();
|
|
28
|
+
const lock: LockFile = {
|
|
29
|
+
id: randomUUID(),
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
command,
|
|
32
|
+
startedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
let guard: Guard | undefined;
|
|
35
|
+
let result: T | undefined;
|
|
36
|
+
let failed = false;
|
|
37
|
+
let failure: unknown;
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
guard = await acquireGuard();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!isLockHeldError(error)) throw error;
|
|
43
|
+
throw await describeHeldLock();
|
|
44
|
+
}
|
|
48
45
|
|
|
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
|
-
if (inspection.status === "unreadable") {
|
|
74
|
-
throw new Error(
|
|
75
|
-
"pi-sync lock metadata is unreadable. Run /sync unlock --stale after verifying no sync is running.",
|
|
76
|
-
);
|
|
77
|
-
}
|
|
46
|
+
let inspection = await inspectLock();
|
|
47
|
+
if (inspection.status === "valid" && isStaleLock(inspection.lock)) {
|
|
48
|
+
if (!options.reclaimStale) {
|
|
49
|
+
throw new Error(`pi-sync lock is stale (pid ${inspection.lock.pid}). Run /sync unlock --stale, then retry.`);
|
|
50
|
+
}
|
|
51
|
+
guard.throwIfCompromised();
|
|
52
|
+
const rechecked = await inspectLock();
|
|
53
|
+
if (rechecked.status !== "valid" || rechecked.lock.id !== inspection.lock.id || !isStaleLock(rechecked.lock)) {
|
|
54
|
+
throw new Error("pi-sync lock changed while preparing transaction recovery; retry.");
|
|
55
|
+
}
|
|
56
|
+
await fs.rm(lockPath(), { force: true });
|
|
57
|
+
inspection = { status: "missing" };
|
|
58
|
+
}
|
|
59
|
+
if (inspection.status === "valid") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (inspection.status === "unreadable") {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"pi-sync lock metadata is unreadable. Run /sync unlock --stale after verifying no sync is running.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
await fs.writeFile(lockPath(), JSON.stringify(lock, null, "\t"), { flag: "wx" });
|
|
71
|
+
guard.throwIfCompromised();
|
|
72
|
+
result = await fn();
|
|
73
|
+
guard.throwIfCompromised();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
failed = true;
|
|
76
|
+
failure = error;
|
|
77
|
+
}
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
79
|
+
try {
|
|
80
|
+
const current = await readLock();
|
|
81
|
+
if (current?.id === lock.id) await fs.rm(lockPath(), { force: true });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (!failed) {
|
|
84
|
+
failed = true;
|
|
85
|
+
failure = error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (guard) {
|
|
89
|
+
const releaseError = await releaseGuard(guard);
|
|
90
|
+
if (releaseError && !failed) {
|
|
91
|
+
failed = true;
|
|
92
|
+
failure = releaseError;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (failed) throw failure;
|
|
96
|
+
return result as T;
|
|
106
97
|
}
|
|
107
98
|
|
|
108
99
|
export async function inspectLock(): Promise<LockInspection> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
try {
|
|
101
|
+
const text = await fs.readFile(lockPath(), "utf8");
|
|
102
|
+
if (text.trim().length === 0) return { status: "unreadable" };
|
|
103
|
+
const parsed = JSON.parse(text) as unknown;
|
|
104
|
+
return isLockFile(parsed) ? { status: "valid", lock: parsed } : { status: "unreadable" };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { status: "missing" };
|
|
107
|
+
if (error instanceof SyntaxError) return { status: "unreadable" };
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
119
110
|
}
|
|
120
111
|
|
|
121
112
|
export async function readLock(): Promise<LockFile | undefined> {
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
const inspection = await inspectLock();
|
|
114
|
+
return inspection.status === "valid" ? inspection.lock : undefined;
|
|
124
115
|
}
|
|
125
116
|
|
|
126
117
|
export async function lockFileExists(): Promise<boolean> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
try {
|
|
119
|
+
await fs.stat(lockPath());
|
|
120
|
+
return true;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
export function isLockGuardHeld() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
return lockfile.check(lockPath(), {
|
|
129
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
130
|
+
lockfilePath: `${lockPath()}.guard`,
|
|
131
|
+
realpath: false,
|
|
132
|
+
stale: LOCK_GUARD_STALE_MS,
|
|
133
|
+
});
|
|
143
134
|
}
|
|
144
135
|
|
|
145
136
|
export function isStaleLock(lock: LockFile) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
try {
|
|
138
|
+
process.kill(lock.pid, 0);
|
|
139
|
+
return false;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return true;
|
|
142
|
+
return Date.now() - Date.parse(lock.startedAt) > LOCK_STALE_MS;
|
|
143
|
+
}
|
|
153
144
|
}
|
|
154
145
|
|
|
155
146
|
export async function unlock(ctx: ExtensionCommandContext, options: CommandOptions) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
throwIfAborted(options.signal);
|
|
148
|
+
await ensureStateDir();
|
|
149
|
+
throwIfAborted(options.signal);
|
|
150
|
+
let guard: Guard;
|
|
151
|
+
try {
|
|
152
|
+
guard = await acquireGuard();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (!isLockHeldError(error)) throw error;
|
|
155
|
+
ctx.ui.notify((await describeHeldLock()).message, "warning");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
167
158
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
159
|
+
let failed = false;
|
|
160
|
+
let failure: unknown;
|
|
161
|
+
try {
|
|
162
|
+
await unlockGuarded(ctx, options);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
failed = true;
|
|
165
|
+
failure = error;
|
|
166
|
+
}
|
|
167
|
+
const releaseError = await releaseGuard(guard);
|
|
168
|
+
if (releaseError && !failed) {
|
|
169
|
+
failed = true;
|
|
170
|
+
failure = releaseError;
|
|
171
|
+
}
|
|
172
|
+
if (failed) throw failure;
|
|
182
173
|
}
|
|
183
174
|
|
|
184
175
|
async function unlockGuarded(ctx: ExtensionCommandContext, options: CommandOptions) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"info",
|
|
231
|
-
);
|
|
232
|
-
}
|
|
176
|
+
throwIfAborted(options.signal);
|
|
177
|
+
let inspection = await inspectLock();
|
|
178
|
+
throwIfAborted(options.signal);
|
|
179
|
+
if (inspection.status === "missing") {
|
|
180
|
+
ctx.ui.notify("No pi-sync lock is present.", "info");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (inspection.status === "unreadable") {
|
|
184
|
+
if (!options.stale) {
|
|
185
|
+
ctx.ui.notify(
|
|
186
|
+
"Pi-sync lock metadata is unreadable. Use /sync unlock --stale only after verifying no sync is running.",
|
|
187
|
+
"warning",
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
inspection = await inspectLock();
|
|
192
|
+
throwIfAborted(options.signal);
|
|
193
|
+
if (inspection.status === "unreadable") {
|
|
194
|
+
// Legacy writers expose an empty file before writing owner metadata, so no
|
|
195
|
+
// automatic test can prove this file is abandoned. The explicit --stale
|
|
196
|
+
// flag is the user's confirmation that no legacy sync is still running.
|
|
197
|
+
throwIfAborted(options.signal);
|
|
198
|
+
await fs.rm(lockPath(), { force: true });
|
|
199
|
+
if (!options.signal?.aborted) {
|
|
200
|
+
ctx.ui.notify(
|
|
201
|
+
"Removed unreadable pi-sync lock. No settings, files, sync state, or remote data were changed.",
|
|
202
|
+
"info",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (inspection.status === "missing") {
|
|
208
|
+
ctx.ui.notify("No pi-sync lock is present.", "info");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!isStaleLock(inspection.lock)) {
|
|
213
|
+
ctx.ui.notify("Lock owner is still live; refusing to remove it.", "warning");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
throwIfAborted(options.signal);
|
|
217
|
+
await fs.rm(lockPath(), { force: true });
|
|
218
|
+
if (!options.signal?.aborted) {
|
|
219
|
+
ctx.ui.notify("Removed stale pi-sync lock. No settings, files, sync state, or remote data were changed.", "info");
|
|
220
|
+
}
|
|
233
221
|
}
|
|
234
222
|
|
|
235
223
|
function throwIfAborted(signal?: AbortSignal) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
? signal.reason
|
|
239
|
-
: new DOMException("The operation was aborted", "AbortError");
|
|
224
|
+
if (!signal?.aborted) return;
|
|
225
|
+
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
240
226
|
}
|
|
241
227
|
|
|
242
228
|
async function releaseGuard(guard: Guard) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
229
|
+
try {
|
|
230
|
+
await guard.release();
|
|
231
|
+
return undefined;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return guard.isCompromised() ? undefined : error;
|
|
234
|
+
}
|
|
249
235
|
}
|
|
250
236
|
|
|
251
237
|
async function acquireGuard(): Promise<Guard> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
238
|
+
let compromisedError: Error | undefined;
|
|
239
|
+
const release = await lockfile.lock(lockPath(), {
|
|
240
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
241
|
+
lockfilePath: `${lockPath()}.guard`,
|
|
242
|
+
realpath: false,
|
|
243
|
+
stale: LOCK_GUARD_STALE_MS,
|
|
244
|
+
update: LOCK_GUARD_UPDATE_MS,
|
|
245
|
+
onCompromised: (error) => {
|
|
246
|
+
compromisedError = error;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
release,
|
|
251
|
+
throwIfCompromised: () => {
|
|
252
|
+
if (compromisedError) throw compromisedError;
|
|
253
|
+
},
|
|
254
|
+
isCompromised: () => compromisedError !== undefined,
|
|
255
|
+
};
|
|
270
256
|
}
|
|
271
257
|
|
|
272
258
|
async function describeHeldLock() {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"Pi-sync is currently running (lock metadata is unreadable or still being written).",
|
|
284
|
-
);
|
|
259
|
+
const current = await readLock();
|
|
260
|
+
if (current && isStaleLock(current)) {
|
|
261
|
+
return new Error("pi-sync lock owner exited; retry shortly while the lock guard expires.");
|
|
262
|
+
}
|
|
263
|
+
if (current) {
|
|
264
|
+
return new Error(
|
|
265
|
+
`Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return new Error("Pi-sync is currently running (lock metadata is unreadable or still being written).");
|
|
285
269
|
}
|
|
286
270
|
|
|
287
271
|
function isLockHeldError(error: unknown) {
|
|
288
|
-
|
|
272
|
+
return (error as NodeJS.ErrnoException).code === "ELOCKED";
|
|
289
273
|
}
|
|
290
274
|
|
|
291
275
|
function isLockFile(value: unknown): value is LockFile {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
276
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
277
|
+
const lock = value as Partial<LockFile>;
|
|
278
|
+
return (
|
|
279
|
+
typeof lock.id === "string" &&
|
|
280
|
+
lock.id.length > 0 &&
|
|
281
|
+
Number.isInteger(lock.pid) &&
|
|
282
|
+
(lock.pid ?? 0) > 0 &&
|
|
283
|
+
(lock.pid ?? 0) <= MAX_PROCESS_ID &&
|
|
284
|
+
typeof lock.command === "string" &&
|
|
285
|
+
lock.command.length > 0 &&
|
|
286
|
+
typeof lock.startedAt === "string" &&
|
|
287
|
+
Number.isFinite(Date.parse(lock.startedAt))
|
|
288
|
+
);
|
|
305
289
|
}
|
package/src/state/lockfile-fs.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
mkdir,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
realpath,
|
|
5
|
+
realpathSync,
|
|
6
|
+
rmdir,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
stat,
|
|
9
|
+
statSync,
|
|
10
|
+
utimes,
|
|
11
|
+
utimesSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
|
|
14
14
|
export const LOCKFILE_FS_ADAPTER = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
mkdir,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
realpath,
|
|
18
|
+
realpathSync,
|
|
19
|
+
rmdir,
|
|
20
|
+
rmdirSync,
|
|
21
|
+
stat,
|
|
22
|
+
statSync,
|
|
23
|
+
utimes,
|
|
24
|
+
utimesSync,
|
|
25
25
|
};
|