@narumitw/pi-sync 0.49.15 → 0.50.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.
Files changed (213) hide show
  1. package/README.md +49 -16
  2. package/dist/chunks/attention-widget-Z7G3SYYJ.ts +9 -0
  3. package/dist/chunks/attention-widget-Z7G3SYYJ.ts.map +7 -0
  4. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  5. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  6. package/dist/chunks/{chunk-ARN2672F.ts → chunk-4CHTZMEO.ts} +23 -55
  7. package/dist/chunks/chunk-4CHTZMEO.ts.map +7 -0
  8. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  9. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  10. package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  11. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  12. package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
  13. package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
  14. package/dist/chunks/chunk-CCSN3F47.ts +51 -0
  15. package/dist/chunks/chunk-CCSN3F47.ts.map +7 -0
  16. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  17. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  18. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  19. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  20. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  21. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  22. package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-ETTRJDNP.ts} +4 -4
  23. package/dist/chunks/chunk-ETTRJDNP.ts.map +7 -0
  24. package/dist/chunks/{chunk-44LPGQTI.ts → chunk-G4RNQQZS.ts} +9 -7
  25. package/dist/chunks/chunk-G4RNQQZS.ts.map +7 -0
  26. package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
  27. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  28. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-IJ6FJFA5.ts} +9 -9
  29. package/dist/chunks/chunk-IJ6FJFA5.ts.map +7 -0
  30. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  31. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  32. package/dist/chunks/chunk-KW7YQEZN.ts +1255 -0
  33. package/dist/chunks/chunk-KW7YQEZN.ts.map +7 -0
  34. package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
  35. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  36. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  37. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  38. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  39. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  40. package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
  41. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  42. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  43. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  44. package/dist/chunks/chunk-S7YBZR42.ts +146 -0
  45. package/dist/chunks/chunk-S7YBZR42.ts.map +7 -0
  46. package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
  47. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  48. package/dist/chunks/chunk-SZWBGPID.ts +57 -0
  49. package/dist/chunks/chunk-SZWBGPID.ts.map +7 -0
  50. package/dist/chunks/chunk-T2B5IU74.ts +25 -0
  51. package/dist/chunks/chunk-T2B5IU74.ts.map +7 -0
  52. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  53. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  54. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  55. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  56. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  57. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  58. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  59. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  60. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  61. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  62. package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
  63. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  64. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-GMVS4PD4.ts} +39 -14
  65. package/dist/chunks/git-backend-GMVS4PD4.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +22 -0
  67. package/dist/chunks/manager-ui-R3XPUFFS.ts +1680 -0
  68. package/dist/chunks/manager-ui-R3XPUFFS.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +21 -0
  70. package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-66O4FGBH.ts} +94 -16
  71. package/dist/chunks/s3-backend-66O4FGBH.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  73. package/dist/chunks/setup-wizard-IUB6ESEU.ts +89 -0
  74. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +7 -0
  75. package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  76. package/dist/chunks/sync-inspection-CST2KL65.ts +20 -0
  77. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-LBX3GKCM.ts} +369 -289
  78. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +7 -0
  79. package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
  80. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  81. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  82. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  83. package/dist/index.ts +476 -341
  84. package/dist/index.ts.map +4 -4
  85. package/docs/settings.md +30 -6
  86. package/package.json +1 -1
  87. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  88. package/src/backends/backend-identity.ts +40 -0
  89. package/src/backends/backend-types.ts +81 -0
  90. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  91. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  92. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  93. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  94. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  95. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  96. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  97. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  98. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  99. package/src/commands/command-execution.ts +330 -0
  100. package/src/commands/command-handler.ts +201 -0
  101. package/src/commands/command-types.ts +18 -0
  102. package/src/{command.ts → commands/command.ts} +2 -1
  103. package/src/commands/setup-completions.ts +6 -0
  104. package/src/settings/config.ts +238 -0
  105. package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
  106. package/src/settings/settings-store.ts +109 -0
  107. package/src/settings/settings-types.ts +128 -0
  108. package/src/settings/settings-validation.ts +443 -0
  109. package/src/snapshot/session-paths.ts +84 -0
  110. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  111. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  112. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  113. package/src/snapshot/snapshot-types.ts +38 -0
  114. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  115. package/src/state/json-file.ts +20 -0
  116. package/src/{lock.ts → state/lock.ts} +3 -2
  117. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  118. package/src/state/state-types.ts +20 -0
  119. package/src/state/sync-state-store.ts +62 -0
  120. package/src/sync/automatic-sync.ts +103 -0
  121. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  122. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  123. package/src/sync/signals.ts +10 -0
  124. package/src/sync/startup-check.ts +128 -0
  125. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  126. package/src/sync/sync-error-guidance.ts +53 -0
  127. package/src/sync/sync-inspection.ts +51 -0
  128. package/src/sync/sync-loaders.ts +54 -0
  129. package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
  130. package/src/sync/sync-operations.ts +12 -0
  131. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  132. package/src/sync/sync-queries.ts +235 -0
  133. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  134. package/src/sync-extension.ts +64 -708
  135. package/src/sync.ts +8 -8
  136. package/src/types.ts +47 -286
  137. package/src/ui/attention-widget.ts +1 -0
  138. package/src/ui/automatic-sync-summary.ts +6 -0
  139. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  140. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  141. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  142. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  143. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  144. package/src/ui/manager-ui.ts +386 -0
  145. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  146. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  147. package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
  148. package/src/ui/setup/git-ui.ts +293 -0
  149. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  150. package/src/ui/setup/s3-ui.ts +217 -0
  151. package/src/ui/setup/setup-actions.ts +127 -0
  152. package/src/ui/setup/setup-location-ui.ts +167 -0
  153. package/src/ui/setup/setup-name-ui.ts +34 -0
  154. package/src/ui/setup/setup-prompts.ts +92 -0
  155. package/src/ui/setup/setup-review.ts +70 -0
  156. package/src/ui/setup/setup-switcher.ts +181 -0
  157. package/src/ui/setup/setup-wizard.ts +39 -0
  158. package/src/ui/setup/text-input.ts +78 -0
  159. package/src/ui/setup/webdav-ui.ts +330 -0
  160. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  161. package/src/ui/sync-attention.ts +187 -0
  162. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  163. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  164. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  165. package/src/ui/terminal-text.ts +4 -0
  166. package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
  167. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  168. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  169. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  170. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  171. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  172. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  173. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  174. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  175. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  176. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  177. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  178. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  179. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  180. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  181. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  182. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  183. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  184. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  185. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  186. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  187. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  188. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  189. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  190. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  191. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  192. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  193. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  194. package/src/config.ts +0 -923
  195. package/src/git-ui.ts +0 -307
  196. package/src/manager-helpers.ts +0 -64
  197. package/src/manager-ui.ts +0 -997
  198. package/src/sync-attention.ts +0 -100
  199. package/src/webdav-ui.ts +0 -421
  200. /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-FKIBM3GO.ts.map} +0 -0
  201. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-D7RBRJNL.ts.map} +0 -0
  202. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  203. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  204. /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-CST2KL65.ts.map} +0 -0
  205. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  206. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  207. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  208. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  209. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  210. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  211. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  212. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  213. /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { agentDir, stateDir } from "./config.js";
5
- import { withLock } from "./lock.js";
6
- import { assertWithinRoot, isPathInside } from "./paths.js";
4
+ import { assertWithinRoot, isPathInside } from "../paths.js";
5
+ import { withLock } from "../state/lock.js";
6
+ import { stateDir } from "../state/state-directory.js";
7
+ import { agentDir } from "./session-paths.js";
7
8
  import { sessionStorageRoot } from "./snapshot-paths.js";
8
- import type { SnapshotApplyPlan } from "./types.js";
9
+ import type { SnapshotApplyPlan } from "./snapshot-types.js";
9
10
 
10
11
  const JOURNAL_VERSION = 1;
11
12
 
@@ -0,0 +1,38 @@
1
+ export interface SnapshotFile {
2
+ path: string;
3
+ contentBase64: string;
4
+ sha256: string;
5
+ }
6
+
7
+ export interface SnapshotSelection {
8
+ version: 1;
9
+ include: string[];
10
+ }
11
+
12
+ export interface Snapshot {
13
+ version: number;
14
+ id: string;
15
+ createdAt: string;
16
+ machine: string;
17
+ /** Backend-scoped remote identity retained in the snapshot wire format. */
18
+ profile: string;
19
+ syncSessions?: boolean;
20
+ /** Portable, credential-free included-content intent. Absent on legacy snapshots. */
21
+ selection?: SnapshotSelection;
22
+ files: SnapshotFile[];
23
+ }
24
+
25
+ export interface SnapshotOptions {
26
+ signal?: AbortSignal;
27
+ include?: string[];
28
+ sessionDir?: string;
29
+ /** Temporary internal projections while snapshot storage remains wire-compatible. */
30
+ syncFiles?: string[];
31
+ syncSessions?: boolean;
32
+ extraFiles?: string[];
33
+ }
34
+
35
+ export interface SnapshotApplyPlan {
36
+ writes: Array<{ target: string; content: Buffer }>;
37
+ deletes: string[];
38
+ }
@@ -3,11 +3,11 @@ import type { Dirent } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { agentDir, configuredSessionDir } from "./config.js";
7
- import { isDeniedPath, posixJoin, safeJoin, toPosix } from "./paths.js";
6
+ import { isDeniedPath, posixJoin, safeJoin, toPosix } from "../paths.js";
7
+ import { agentDir, configuredSessionDir } from "./session-paths.js";
8
8
  import { sessionStorageRoot } from "./snapshot-paths.js";
9
9
 
10
- export { isDeniedPath } from "./paths.js";
10
+ export { isDeniedPath } from "../paths.js";
11
11
  export { sessionStorageRoot } from "./snapshot-paths.js";
12
12
 
13
13
  import {
@@ -23,10 +23,10 @@ import {
23
23
  selectionForSnapshot,
24
24
  snapshotSelectionInclude,
25
25
  syncIncludeSelection,
26
- } from "./sync-policy.js";
27
- import type { Snapshot, SnapshotFile, SnapshotOptions } from "./types.js";
26
+ } from "../sync/sync-policy.js";
27
+ import type { Snapshot, SnapshotFile, SnapshotOptions } from "./snapshot-types.js";
28
28
 
29
- export { canonicalSnapshotPathForConfig, isConfiguredSnapshotPath } from "./sync-policy.js";
29
+ export { canonicalSnapshotPathForConfig, isConfiguredSnapshotPath } from "../sync/sync-policy.js";
30
30
 
31
31
  const VERSION = 1;
32
32
  const TOP_LEVEL_FILES: readonly string[] = DEFAULT_SYNC_INCLUDE.filter((name) =>
@@ -96,12 +96,13 @@ export async function createSnapshot(
96
96
  profile: string,
97
97
  options: SnapshotOptions = {},
98
98
  ): Promise<Snapshot> {
99
+ options.signal?.throwIfAborted();
99
100
  const include = effectiveInclude(options);
100
101
  const syncSessions = include.includes("sessions");
101
- const files = await collectFiles(agentDir(), {
102
- include,
103
- sessionDir: options.sessionDir ?? (await configuredSessionDir()),
104
- });
102
+ const sessionDir = options.sessionDir ?? (await configuredSessionDir());
103
+ options.signal?.throwIfAborted();
104
+ const files = await collectFiles(agentDir(), { include, sessionDir, signal: options.signal });
105
+ options.signal?.throwIfAborted();
105
106
  return {
106
107
  version: VERSION,
107
108
  id: snapshotId(),
@@ -127,48 +128,61 @@ export async function collectFiles(
127
128
  root: string,
128
129
  options: SnapshotOptions = {},
129
130
  ): Promise<SnapshotFile[]> {
131
+ const signal = options.signal;
132
+ signal?.throwIfAborted();
130
133
  const results: SnapshotFile[] = [];
131
134
  const entries = await fs.readdir(root, { withFileTypes: true });
135
+ signal?.throwIfAborted();
132
136
  const selection = syncIncludeSelection(effectiveInclude(options));
133
137
  const selectedFiles = new Set<string>(selection.builtIns);
134
138
  for (const entry of entries) {
135
139
  if (entry.isDirectory() && TOP_LEVEL_DIRS.has(entry.name) && selectedFiles.has(entry.name)) {
136
- await collectDirectory(results, root, entry.name);
140
+ await collectDirectory(results, root, entry.name, { signal });
137
141
  }
138
142
  }
139
143
  for (const fileName of TOP_LEVEL_FILES) {
140
144
  if (!selectedFiles.has(fileName)) continue;
141
145
  const entry = selectTopLevelFileEntry(entries, fileName);
142
- if (entry) await addFile(results, root, entry.name, fileName);
146
+ if (entry) await addFile(results, root, entry.name, fileName, signal);
143
147
  }
144
148
  for (const relativePath of selection.custom) {
145
- await collectIncludedPath(results, root, relativePath);
149
+ await collectIncludedPath(results, root, relativePath, signal);
146
150
  }
147
151
  if (selection.sessions) {
148
152
  try {
149
153
  await collectDirectory(results, sessionStorageRoot(root, options.sessionDir), "", {
150
154
  sessionsOnly: true,
151
155
  virtualPrefix: "sessions",
156
+ signal,
152
157
  });
153
158
  } catch (error) {
154
159
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
155
160
  }
156
161
  }
162
+ signal?.throwIfAborted();
157
163
  return results.sort((left, right) => left.path.localeCompare(right.path));
158
164
  }
159
165
 
160
- async function collectIncludedPath(results: SnapshotFile[], root: string, relativePath: string) {
166
+ async function collectIncludedPath(
167
+ results: SnapshotFile[],
168
+ root: string,
169
+ relativePath: string,
170
+ signal?: AbortSignal,
171
+ ) {
172
+ signal?.throwIfAborted();
161
173
  const absolutePath = safeJoin(root, relativePath);
162
174
  try {
163
175
  const stat = await fs.lstat(absolutePath);
164
- if (stat.isFile()) await addFile(results, root, relativePath);
165
- else if (stat.isDirectory()) await collectDirectory(results, root, relativePath);
176
+ signal?.throwIfAborted();
177
+ if (stat.isFile()) await addFile(results, root, relativePath, relativePath, signal);
178
+ else if (stat.isDirectory()) await collectDirectory(results, root, relativePath, { signal });
166
179
  } catch (error) {
167
180
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
168
181
  if (!relativePath.includes("/")) {
169
182
  const entries = await fs.readdir(root, { withFileTypes: true });
183
+ signal?.throwIfAborted();
170
184
  const entry = selectTopLevelFileEntry(entries, relativePath);
171
- if (entry) await addFile(results, root, entry.name, relativePath);
185
+ if (entry) await addFile(results, root, entry.name, relativePath, signal);
172
186
  }
173
187
  }
174
188
  }
@@ -177,10 +191,12 @@ async function collectDirectory(
177
191
  results: SnapshotFile[],
178
192
  root: string,
179
193
  relativeDirectory: string,
180
- options: { sessionsOnly?: boolean; virtualPrefix?: string } = {},
194
+ options: { sessionsOnly?: boolean; virtualPrefix?: string; signal?: AbortSignal } = {},
181
195
  ) {
196
+ options.signal?.throwIfAborted();
182
197
  const absoluteDirectory = path.join(root, relativeDirectory);
183
198
  for (const entry of await fs.readdir(absoluteDirectory, { withFileTypes: true })) {
199
+ options.signal?.throwIfAborted();
184
200
  const relativePath = relativeDirectory ? posixJoin(relativeDirectory, entry.name) : entry.name;
185
201
  const snapshotPath = options.virtualPrefix
186
202
  ? posixJoin(options.virtualPrefix, relativePath)
@@ -189,7 +205,7 @@ async function collectDirectory(
189
205
  if (entry.isDirectory()) {
190
206
  await collectDirectory(results, root, relativePath, options);
191
207
  } else if (entry.isFile() && (!options.sessionsOnly || isSessionFilePath(snapshotPath))) {
192
- await addFile(results, root, relativePath, snapshotPath);
208
+ await addFile(results, root, relativePath, snapshotPath, options.signal);
193
209
  }
194
210
  }
195
211
  }
@@ -199,10 +215,13 @@ async function addFile(
199
215
  root: string,
200
216
  relativePath: string,
201
217
  snapshotPath = relativePath,
218
+ signal?: AbortSignal,
202
219
  ) {
220
+ signal?.throwIfAborted();
203
221
  if (!isSafeSnapshotPath(snapshotPath)) return;
204
222
  const absolutePath = safeJoin(root, relativePath);
205
- const content = await fs.readFile(absolutePath);
223
+ const content = await fs.readFile(absolutePath, { signal });
224
+ signal?.throwIfAborted();
206
225
  results.push({
207
226
  path: snapshotPath,
208
227
  contentBase64: content.toString("base64"),
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
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
+ }
12
+ }
13
+
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);
20
+ }
@@ -2,10 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
4
  import lockfile from "proper-lockfile";
5
- import { ensureStateDir, lockPath } from "./config.js";
5
+ import type { CommandOptions } from "../commands/command-types.js";
6
6
  import { LOCK_GUARD_STALE_MS, LOCK_GUARD_UPDATE_MS } from "./lock-policy.js";
7
7
  import { LOCKFILE_FS_ADAPTER } from "./lockfile-fs.js";
8
- import type { CommandOptions, LockFile } from "./types.js";
8
+ import type { LockFile } from "./state-types.js";
9
+ import { ensureStateDir, lockPath } from "./sync-state-store.js";
9
10
 
10
11
  const LOCK_STALE_MS = 30 * 60 * 1000;
11
12
  const MAX_PROCESS_ID = 2_147_483_647;
@@ -1,6 +1,6 @@
1
+ import { errorMessage } from "../sync/sync-errors.js";
1
2
  import { inspectLock, isLockGuardHeld, isStaleLock, type LockInspection } from "./lock.js";
2
- import { errorMessage } from "./manager-helpers.js";
3
- import type { LockFile } from "./types.js";
3
+ import type { LockFile } from "./state-types.js";
4
4
 
5
5
  export type OperationAvailability =
6
6
  | { kind: "free" }
@@ -0,0 +1,20 @@
1
+ export interface SyncState {
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
+ }
14
+
15
+ export interface LockFile {
16
+ id: string;
17
+ pid: number;
18
+ command: string;
19
+ startedAt: string;
20
+ }
@@ -0,0 +1,62 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { backendIdentityCoordinates } from "../backends/backend-identity.js";
5
+ import { safeName } from "../paths.js";
6
+ import type { AnySyncConfig } from "../settings/settings-types.js";
7
+ import { readJsonIfExists, writeJson } from "./json-file.js";
8
+ import { stateDir } from "./state-directory.js";
9
+ import type { SyncState } from "./state-types.js";
10
+
11
+ const STATE_VERSION = 2;
12
+
13
+ /** Bounded observation token; do not retain the full baseline in passive UI state. */
14
+ export function syncStateFingerprint(state: SyncState) {
15
+ return createHash("sha256").update(JSON.stringify(state)).digest("hex");
16
+ }
17
+
18
+ export async function readState(profile: string): Promise<SyncState> {
19
+ return (
20
+ (await readJsonIfExists<SyncState>(statePath(profile))) ?? {
21
+ version: STATE_VERSION,
22
+ profile,
23
+ lastFileHashes: {},
24
+ }
25
+ );
26
+ }
27
+
28
+ export async function writeState(profile: string, state: SyncState) {
29
+ await writeJson(statePath(profile), state);
30
+ }
31
+
32
+ export async function readStateForConfig(config: AnySyncConfig): Promise<SyncState> {
33
+ return (
34
+ (await readJsonIfExists<SyncState>(statePathForConfig(config))) ?? {
35
+ version: STATE_VERSION,
36
+ profile: config.snapshotIdentity,
37
+ lastFileHashes: {},
38
+ }
39
+ );
40
+ }
41
+
42
+ export async function writeStateForConfig(config: AnySyncConfig, state: SyncState) {
43
+ await writeJson(statePathForConfig(config), state);
44
+ }
45
+
46
+ export function statePathForConfig(config: AnySyncConfig) {
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
+ }
51
+
52
+ function statePath(profile: string) {
53
+ return path.join(stateDir(), `${safeName(profile)}.state.json`);
54
+ }
55
+
56
+ export function lockPath() {
57
+ return path.join(stateDir(), "lock");
58
+ }
59
+
60
+ export async function ensureStateDir() {
61
+ await fs.mkdir(stateDir(), { recursive: true });
62
+ }
@@ -0,0 +1,103 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { setSyncSetupCompletions } from "../commands/command.js";
3
+ import type { CommandOptions } from "../commands/command-types.js";
4
+ import { loadConfig, loadPartialConfig } from "../settings/config.js";
5
+ import { isMissingConfigError } from "../settings/config-errors.js";
6
+ import { consumeLocalConfigMigrationNotice } from "../settings/config-file.js";
7
+ import { configuredSyncSetupNames } from "../settings/settings-store.js";
8
+ import { snapshotOptionsForContext } from "../snapshot/session-paths.js";
9
+ import { recoverSnapshotTransactionsOnStartup } from "../snapshot/snapshot-transaction.js";
10
+ import { withLock } from "../state/lock.js";
11
+ import { stateDirectoryMigrationNotice } from "../state/state-directory.js";
12
+ import { ensureStateDir, readStateForConfig } from "../state/sync-state-store.js";
13
+ import { safeTerminalText } from "../ui/terminal-text.js";
14
+ import { throwIfAborted } from "./signals.js";
15
+ import { errorMessage } from "./sync-errors.js";
16
+ import type { SyncLoaders } from "./sync-loaders.js";
17
+
18
+ const AUTO_SYNC_OPTIONS: CommandOptions = {
19
+ yes: true,
20
+ force: false,
21
+ stale: false,
22
+ silent: true,
23
+ reload: false,
24
+ auto: true,
25
+ args: [],
26
+ };
27
+
28
+ const STATUS_KEY = "sync";
29
+
30
+ export async function startSession(ctx: ExtensionContext, signal: AbortSignal) {
31
+ throwIfAborted(signal);
32
+ const stateNotice = stateDirectoryMigrationNotice();
33
+ if (stateNotice && ctx.hasUI) ctx.ui.notify(stateNotice, "warning");
34
+ // Recovery can restore managed files: finish it before Pi accepts user edits.
35
+ // Unlike remote inspection, it must never be detached behind startup.
36
+ await recoverSnapshotTransactionsOnStartup();
37
+ throwIfAborted(signal);
38
+ try {
39
+ const names = await configuredSyncSetupNames();
40
+ throwIfAborted(signal);
41
+ setSyncSetupCompletions(names);
42
+ } catch {
43
+ if (signal.aborted) return;
44
+ setSyncSetupCompletions([]);
45
+ }
46
+ const migrationNotice = consumeLocalConfigMigrationNotice();
47
+ if (migrationNotice && ctx.hasUI) ctx.ui.notify(migrationNotice, "warning");
48
+ throwIfAborted(signal);
49
+ if (!ctx.hasUI) return;
50
+ try {
51
+ const config = await loadConfig();
52
+ throwIfAborted(signal);
53
+ return config.automatic ? config : undefined;
54
+ } catch (error) {
55
+ throwIfAborted(signal);
56
+ if (!isMissingConfigError(error)) {
57
+ ctx.ui.notify(
58
+ `pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}`,
59
+ "warning",
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ export async function autoPushSessions(
66
+ ctx: ExtensionContext,
67
+ signal: AbortSignal,
68
+ loaders: SyncLoaders,
69
+ ) {
70
+ try {
71
+ const partial = await loadPartialConfig();
72
+ throwIfAborted(signal);
73
+ if (!partial.automatic) return;
74
+ if (!partial.include.includes("sessions")) return;
75
+ await ensureStateDir();
76
+ throwIfAborted(signal);
77
+ const config = await loadConfig();
78
+ throwIfAborted(signal);
79
+ if (!config.include.includes("sessions")) return;
80
+ const [operations, snapshotModule, syncStateModule] = await Promise.all([
81
+ loaders.operations(),
82
+ loaders.snapshot(),
83
+ loaders.syncState(),
84
+ ]);
85
+ throwIfAborted(signal);
86
+ await withLock("auto-session-push", async () => {
87
+ throwIfAborted(signal);
88
+ const state = await readStateForConfig(config);
89
+ throwIfAborted(signal);
90
+ const local = await snapshotModule.createSnapshot(config.snapshotIdentity, {
91
+ ...snapshotOptionsForContext(ctx, config),
92
+ signal,
93
+ });
94
+ throwIfAborted(signal);
95
+ if (!syncStateModule.hasLocalChanges(local, state, config)) return;
96
+ await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
97
+ });
98
+ } catch (error) {
99
+ if (signal.aborted || isMissingConfigError(error)) return;
100
+ ctx.ui.setStatus(STATUS_KEY, undefined);
101
+ ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
102
+ }
103
+ }
@@ -1,6 +1,9 @@
1
- import { syncConfigReviewFingerprint } from "./config.js";
2
- import type { RemoteHead, SyncBackend } from "./sync-backend.js";
3
- import { safeTerminalText } from "./sync-format.js";
1
+ import type { RemoteHead, SyncBackend } from "../backends/sync-backend.js";
2
+ import { syncConfigReviewFingerprint } from "../settings/config.js";
3
+ import type { AnySyncConfig } from "../settings/settings-types.js";
4
+ import { filterSnapshotForConfigPolicy } from "../snapshot/snapshot.js";
5
+ import type { Snapshot } from "../snapshot/snapshot-types.js";
6
+ import { safeTerminalText } from "../ui/terminal-text.js";
4
7
  import {
5
8
  inspectRemoteSelection,
6
9
  type RemoteSelectionState,
@@ -8,7 +11,6 @@ import {
8
11
  sameSyncInclude,
9
12
  snapshotSelectionInclude,
10
13
  } from "./sync-policy.js";
11
- import type { AnySyncConfig, Snapshot } from "./types.js";
12
14
 
13
15
  export async function readSnapshotForHead(
14
16
  backend: SyncBackend,
@@ -58,3 +60,24 @@ export function formatRemoteSelectionStatus(state: RemoteSelectionState | undefi
58
60
  function safeList(values: readonly string[]) {
59
61
  return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
60
62
  }
63
+
64
+ export async function readRemoteSnapshot(
65
+ backend: SyncBackend,
66
+ config: AnySyncConfig,
67
+ signal?: AbortSignal,
68
+ options: { allowSelectionDifference?: boolean } = {},
69
+ ) {
70
+ const head = await backend.readHead(signal);
71
+ if (!head) return { head: undefined, snapshot: undefined, selectionState: undefined };
72
+ const snapshot = await readSnapshotForHead(backend, head, signal);
73
+ const selectionState = inspectRemoteSelection(config.include, snapshot);
74
+ if (!options.allowSelectionDifference && selectionState.kind === "different") {
75
+ const configIdentity = syncConfigReviewFingerprint(config);
76
+ throw remoteSelectionMismatch(config, selectionState.include, configIdentity);
77
+ }
78
+ return {
79
+ head,
80
+ snapshot: filterSnapshotForConfigPolicy(snapshot, config),
81
+ selectionState,
82
+ };
83
+ }
@@ -1,16 +1,15 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { setSyncSetupCompletions } from "./command.js";
2
+ import { setSyncSetupCompletions } from "../commands/command.js";
3
3
  import {
4
- configuredSyncSetupNames,
5
4
  loadConfig,
6
- normalizeOnSwitch,
7
5
  syncConfigReviewIdentity,
8
6
  syncSetupReviewIdentity,
9
- updateLocalConfig,
10
- } from "./config.js";
7
+ } from "../settings/config.js";
8
+ import { configuredSyncSetupNames, updateLocalConfig } from "../settings/settings-store.js";
9
+ import type { OnSwitchAction } from "../settings/settings-types.js";
10
+ import { normalizeOnSwitch } from "../settings/settings-validation.js";
11
+ import { safeTerminalText } from "../ui/terminal-text.js";
11
12
  import { SetupPullRequiresUiError } from "./sync-errors.js";
12
- import { safeTerminalText } from "./sync-format.js";
13
- import type { OnSwitchAction } from "./types.js";
14
13
 
15
14
  export { SetupPullRequiresUiError } from "./sync-errors.js";
16
15
 
@@ -0,0 +1,10 @@
1
+ export function throwIfAborted(signal?: AbortSignal) {
2
+ if (!signal?.aborted) return;
3
+ throw signal.reason instanceof Error
4
+ ? signal.reason
5
+ : new DOMException("The operation was aborted", "AbortError");
6
+ }
7
+
8
+ export function combineSignals(primary: AbortSignal, secondary?: AbortSignal) {
9
+ return secondary ? AbortSignal.any([primary, secondary]) : primary;
10
+ }
@@ -0,0 +1,128 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadConfigForCheck, syncCheckConfigFingerprint } from "../settings/config.js";
3
+ import { isMissingConfigError } from "../settings/config-errors.js";
4
+ import type { AnySyncConfig } from "../settings/settings-types.js";
5
+ import { snapshotOptionsForContext } from "../snapshot/session-paths.js";
6
+ import { withLock } from "../state/lock.js";
7
+ import { withStateDirectoryAccess } from "../state/state-directory.js";
8
+ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
9
+ import type { SyncAttentionController } from "../ui/sync-attention.js";
10
+ import { safeTerminalText } from "../ui/terminal-text.js";
11
+ import { combineSignals, throwIfAborted } from "./signals.js";
12
+ import { errorMessage } from "./sync-errors.js";
13
+ import type { SyncLoaders } from "./sync-loaders.js";
14
+
15
+ const STATUS_KEY = "sync";
16
+
17
+ /** One owned task per extension session, never keyed by the shared headless UI. */
18
+ export function createStartupCheck(
19
+ loaders: SyncLoaders,
20
+ attention: SyncAttentionController,
21
+ timeoutMs = 30_000,
22
+ ) {
23
+ let active:
24
+ | {
25
+ controller: AbortController;
26
+ promise: Promise<void>;
27
+ owner: ExtensionContext["sessionManager"];
28
+ }
29
+ | undefined;
30
+
31
+ return {
32
+ start(ctx: ExtensionContext, sessionSignal: AbortSignal, initialConfig?: AnySyncConfig) {
33
+ if (!ctx.hasUI || sessionSignal.aborted) return;
34
+ if (active) throw new Error("Previous startup check has not been drained.");
35
+ const controller = new AbortController();
36
+ const signal = combineSignals(sessionSignal, controller.signal);
37
+ const task = { controller, owner: ctx.sessionManager, promise: Promise.resolve() };
38
+ active = task;
39
+ const isCurrent = () =>
40
+ !sessionSignal.aborted && active === task && task.owner === ctx.sessionManager;
41
+ const timer = setTimeout(
42
+ () => controller.abort(new DOMException("Startup check timed out", "TimeoutError")),
43
+ timeoutMs,
44
+ );
45
+ let config: AnySyncConfig | undefined;
46
+ let checking = false;
47
+ // Handle every detached rejection here; Pi's awaited event runner cannot catch it.
48
+ task.promise = Promise.resolve()
49
+ .then(async () => {
50
+ try {
51
+ throwIfAborted(signal);
52
+ config = initialConfig ?? (await loadConfigForCheck());
53
+ throwIfAborted(signal);
54
+ if (!config.automatic) return;
55
+ const captured = config;
56
+ const identity = syncCheckConfigFingerprint(captured);
57
+ checking = true;
58
+ ctx.ui.setStatus(STATUS_KEY, "checking in background");
59
+ const { inspectSync } = await loaders.inspection();
60
+ throwIfAborted(signal);
61
+ const currentConfig = await loadConfigForCheck();
62
+ throwIfAborted(signal);
63
+ if (syncCheckConfigFingerprint(currentConfig) !== identity) return;
64
+ // Lock order: state migration guard → sync operation → backend cache.
65
+ // Read heads may write only private cache data. Never call doctor/syncBoth.
66
+ const inspection = await withStateDirectoryAccess(() => {
67
+ throwIfAborted(signal);
68
+ return withLock("startup-check", () => {
69
+ throwIfAborted(signal);
70
+ return inspectSync(captured, snapshotOptionsForContext(ctx, captured), signal);
71
+ });
72
+ });
73
+ throwIfAborted(signal);
74
+ const latest = await loadConfigForCheck();
75
+ throwIfAborted(signal);
76
+ if (syncCheckConfigFingerprint(latest) !== identity) return;
77
+ const state = await withStateDirectoryAccess(() => readStateForConfig(latest));
78
+ throwIfAborted(signal);
79
+ if (syncStateFingerprint(state) !== inspection.stateIdentity || !isCurrent()) return;
80
+ attention.observe({
81
+ setupName: captured.setupName,
82
+ configIdentity: identity,
83
+ checkedAt: new Date().toISOString(),
84
+ inspection,
85
+ });
86
+ if (ctx.mode === "rpc") attention.notifyObservation(ctx);
87
+ } catch (error) {
88
+ if (!isCurrent() || sessionSignal.aborted || isMissingConfigError(error)) return;
89
+ if (signal.aborted && controller.signal.reason?.name !== "TimeoutError") return;
90
+ // A settings edit invalidates failures as well as successful observations.
91
+ if (config) {
92
+ const latest = await loadConfigForCheck().catch(() => undefined);
93
+ if (
94
+ !isCurrent() ||
95
+ (signal.aborted && controller.signal.reason?.name !== "TimeoutError")
96
+ )
97
+ return;
98
+ if (
99
+ !latest ||
100
+ syncCheckConfigFingerprint(latest) !== syncCheckConfigFingerprint(config)
101
+ )
102
+ return;
103
+ }
104
+ ctx.ui.notify(
105
+ `pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}. Run /sync status to retry. No startup transfer was performed.`,
106
+ "warning",
107
+ );
108
+ } finally {
109
+ clearTimeout(timer);
110
+ if (isCurrent() && checking) await attention.publish(ctx, sessionSignal);
111
+ if (active === task) active = undefined;
112
+ }
113
+ })
114
+ .catch(() => {
115
+ // UI teardown can itself throw. No unhandled rejection or retained timer.
116
+ clearTimeout(timer);
117
+ if (active === task) active = undefined;
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
+ };
128
+ }
@@ -1,9 +1,12 @@
1
- import { syncConfigReviewIdentity } from "./config.js";
1
+ import { syncConfigReviewIdentity } from "../settings/config.js";
2
+ import type { AnySyncConfig } from "../settings/settings-types.js";
3
+ import type { Snapshot } from "../snapshot/snapshot-types.js";
4
+ import type { SyncState } from "../state/state-types.js";
5
+ import { formatDiff, formatSnapshotOnlyDiff } from "../ui/sync-format.js";
6
+ import { safeTerminalText } from "../ui/terminal-text.js";
2
7
  import { SyncDecisionRequiredError } from "./sync-errors.js";
3
- import { formatDiff, formatSnapshotOnlyDiff, safeTerminalText } from "./sync-format.js";
4
8
  import { includeFromSelectionConfig } from "./sync-policy.js";
5
9
  import { syncPolicyChanged } from "./sync-state.js";
6
- import type { AnySyncConfig, Snapshot, SyncState } from "./types.js";
7
10
 
8
11
  export type SyncDecisionKind =
9
12
  | "remote-or-policy-changed"