@narumitw/pi-sync 0.50.2 → 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.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-4CHTZMEO.ts → chunk-7KHKWL3Q.ts} +37 -55
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-G4RNQQZS.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-KW7YQEZN.ts → chunk-UA7RXOIT.ts} +48 -100
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-5KLKVMRC.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-IUB6ESEU.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +92 -112
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +198 -214
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-4CHTZMEO.ts.map +0 -7
  176. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  177. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  178. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  179. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  180. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  181. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  182. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  183. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-G4RNQQZS.ts.map +0 -7
  186. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  187. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  188. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  189. package/dist/chunks/chunk-KW7YQEZN.ts.map +0 -7
  190. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  191. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  192. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  193. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  194. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  195. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  196. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  197. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  198. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  199. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  200. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +0 -22
  206. package/dist/chunks/manager-ui-5KLKVMRC.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-FKIBM3GO.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-D7RBRJNL.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -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
- 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
- }
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
- 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);
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
- release: () => Promise<void>;
16
- throwIfCompromised: () => void;
17
- isCompromised: () => boolean;
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
- command: string,
27
- fn: () => Promise<T>,
28
- options: { reclaimStale?: boolean } = {},
23
+ command: string,
24
+ fn: () => Promise<T>,
25
+ options: { reclaimStale?: boolean } = {},
29
26
  ): Promise<T> {
30
- await ensureStateDir();
31
- const lock: LockFile = {
32
- id: randomUUID(),
33
- pid: process.pid,
34
- command,
35
- startedAt: new Date().toISOString(),
36
- };
37
- let guard: Guard | undefined;
38
- let result: T | undefined;
39
- let failed = false;
40
- let failure: unknown;
41
- try {
42
- try {
43
- guard = await acquireGuard();
44
- } catch (error) {
45
- if (!isLockHeldError(error)) throw error;
46
- throw await describeHeldLock();
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
- let inspection = await inspectLock();
50
- if (inspection.status === "valid" && isStaleLock(inspection.lock)) {
51
- if (!options.reclaimStale) {
52
- throw new Error(
53
- `pi-sync lock is stale (pid ${inspection.lock.pid}). Run /sync unlock --stale, then retry.`,
54
- );
55
- }
56
- guard.throwIfCompromised();
57
- const rechecked = await inspectLock();
58
- if (
59
- rechecked.status !== "valid" ||
60
- rechecked.lock.id !== inspection.lock.id ||
61
- !isStaleLock(rechecked.lock)
62
- ) {
63
- throw new Error("pi-sync lock changed while preparing transaction recovery; retry.");
64
- }
65
- await fs.rm(lockPath(), { force: true });
66
- inspection = { status: "missing" };
67
- }
68
- if (inspection.status === "valid") {
69
- throw new Error(
70
- `pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,
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
- await fs.writeFile(lockPath(), JSON.stringify(lock, null, "\t"), { flag: "wx" });
80
- guard.throwIfCompromised();
81
- result = await fn();
82
- guard.throwIfCompromised();
83
- } catch (error) {
84
- failed = true;
85
- failure = error;
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
- try {
89
- const current = await readLock();
90
- if (current?.id === lock.id) await fs.rm(lockPath(), { force: true });
91
- } catch (error) {
92
- if (!failed) {
93
- failed = true;
94
- failure = error;
95
- }
96
- }
97
- if (guard) {
98
- const releaseError = await releaseGuard(guard);
99
- if (releaseError && !failed) {
100
- failed = true;
101
- failure = releaseError;
102
- }
103
- }
104
- if (failed) throw failure;
105
- return result as T;
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
- try {
110
- const text = await fs.readFile(lockPath(), "utf8");
111
- if (text.trim().length === 0) return { status: "unreadable" };
112
- const parsed = JSON.parse(text) as unknown;
113
- return isLockFile(parsed) ? { status: "valid", lock: parsed } : { status: "unreadable" };
114
- } catch (error) {
115
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return { status: "missing" };
116
- if (error instanceof SyntaxError) return { status: "unreadable" };
117
- throw error;
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
- const inspection = await inspectLock();
123
- return inspection.status === "valid" ? inspection.lock : undefined;
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
- try {
128
- await fs.stat(lockPath());
129
- return true;
130
- } catch (error) {
131
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
132
- throw error;
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
- return lockfile.check(lockPath(), {
138
- fs: LOCKFILE_FS_ADAPTER,
139
- lockfilePath: `${lockPath()}.guard`,
140
- realpath: false,
141
- stale: LOCK_GUARD_STALE_MS,
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
- try {
147
- process.kill(lock.pid, 0);
148
- return false;
149
- } catch (error) {
150
- if ((error as NodeJS.ErrnoException).code === "ESRCH") return true;
151
- return Date.now() - Date.parse(lock.startedAt) > LOCK_STALE_MS;
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
- throwIfAborted(options.signal);
157
- await ensureStateDir();
158
- throwIfAborted(options.signal);
159
- let guard: Guard;
160
- try {
161
- guard = await acquireGuard();
162
- } catch (error) {
163
- if (!isLockHeldError(error)) throw error;
164
- ctx.ui.notify((await describeHeldLock()).message, "warning");
165
- return;
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
- let failed = false;
169
- let failure: unknown;
170
- try {
171
- await unlockGuarded(ctx, options);
172
- } catch (error) {
173
- failed = true;
174
- failure = error;
175
- }
176
- const releaseError = await releaseGuard(guard);
177
- if (releaseError && !failed) {
178
- failed = true;
179
- failure = releaseError;
180
- }
181
- if (failed) throw failure;
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
- throwIfAborted(options.signal);
186
- let inspection = await inspectLock();
187
- throwIfAborted(options.signal);
188
- if (inspection.status === "missing") {
189
- ctx.ui.notify("No pi-sync lock is present.", "info");
190
- return;
191
- }
192
- if (inspection.status === "unreadable") {
193
- if (!options.stale) {
194
- ctx.ui.notify(
195
- "Pi-sync lock metadata is unreadable. Use /sync unlock --stale only after verifying no sync is running.",
196
- "warning",
197
- );
198
- return;
199
- }
200
- inspection = await inspectLock();
201
- throwIfAborted(options.signal);
202
- if (inspection.status === "unreadable") {
203
- // Legacy writers expose an empty file before writing owner metadata, so no
204
- // automatic test can prove this file is abandoned. The explicit --stale
205
- // flag is the user's confirmation that no legacy sync is still running.
206
- throwIfAborted(options.signal);
207
- await fs.rm(lockPath(), { force: true });
208
- if (!options.signal?.aborted) {
209
- ctx.ui.notify(
210
- "Removed unreadable pi-sync lock. No settings, files, sync state, or remote data were changed.",
211
- "info",
212
- );
213
- }
214
- return;
215
- }
216
- if (inspection.status === "missing") {
217
- ctx.ui.notify("No pi-sync lock is present.", "info");
218
- return;
219
- }
220
- }
221
- if (!isStaleLock(inspection.lock)) {
222
- ctx.ui.notify("Lock owner is still live; refusing to remove it.", "warning");
223
- return;
224
- }
225
- throwIfAborted(options.signal);
226
- await fs.rm(lockPath(), { force: true });
227
- if (!options.signal?.aborted) {
228
- ctx.ui.notify(
229
- "Removed stale pi-sync lock. No settings, files, sync state, or remote data were changed.",
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
- if (!signal?.aborted) return;
237
- throw signal.reason instanceof Error
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
- try {
244
- await guard.release();
245
- return undefined;
246
- } catch (error) {
247
- return guard.isCompromised() ? undefined : error;
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
- let compromisedError: Error | undefined;
253
- const release = await lockfile.lock(lockPath(), {
254
- fs: LOCKFILE_FS_ADAPTER,
255
- lockfilePath: `${lockPath()}.guard`,
256
- realpath: false,
257
- stale: LOCK_GUARD_STALE_MS,
258
- update: LOCK_GUARD_UPDATE_MS,
259
- onCompromised: (error) => {
260
- compromisedError = error;
261
- },
262
- });
263
- return {
264
- release,
265
- throwIfCompromised: () => {
266
- if (compromisedError) throw compromisedError;
267
- },
268
- isCompromised: () => compromisedError !== undefined,
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
- const current = await readLock();
274
- if (current && isStaleLock(current)) {
275
- return new Error("pi-sync lock owner exited; retry shortly while the lock guard expires.");
276
- }
277
- if (current) {
278
- return new Error(
279
- `Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`,
280
- );
281
- }
282
- return new Error(
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
- return (error as NodeJS.ErrnoException).code === "ELOCKED";
272
+ return (error as NodeJS.ErrnoException).code === "ELOCKED";
289
273
  }
290
274
 
291
275
  function isLockFile(value: unknown): value is LockFile {
292
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
293
- const lock = value as Partial<LockFile>;
294
- return (
295
- typeof lock.id === "string" &&
296
- lock.id.length > 0 &&
297
- Number.isInteger(lock.pid) &&
298
- (lock.pid ?? 0) > 0 &&
299
- (lock.pid ?? 0) <= MAX_PROCESS_ID &&
300
- typeof lock.command === "string" &&
301
- lock.command.length > 0 &&
302
- typeof lock.startedAt === "string" &&
303
- Number.isFinite(Date.parse(lock.startedAt))
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
  }
@@ -1,25 +1,25 @@
1
1
  import {
2
- mkdir,
3
- mkdirSync,
4
- realpath,
5
- realpathSync,
6
- rmdir,
7
- rmdirSync,
8
- stat,
9
- statSync,
10
- utimes,
11
- utimesSync,
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
- mkdir,
16
- mkdirSync,
17
- realpath,
18
- realpathSync,
19
- rmdir,
20
- rmdirSync,
21
- stat,
22
- statSync,
23
- utimes,
24
- utimesSync,
15
+ mkdir,
16
+ mkdirSync,
17
+ realpath,
18
+ realpathSync,
19
+ rmdir,
20
+ rmdirSync,
21
+ stat,
22
+ statSync,
23
+ utimes,
24
+ utimesSync,
25
25
  };