pushwork 1.0.5 → 1.0.7

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 (204) hide show
  1. package/README.md +87 -335
  2. package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
  3. package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
  4. package/dist/.pushwork/config.json +15 -0
  5. package/dist/.pushwork/snapshot.json +7 -0
  6. package/dist/cli.js +208 -213
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands.d.ts +51 -0
  9. package/dist/commands.d.ts.map +1 -0
  10. package/dist/commands.js +799 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/core/change-detection.d.ts +2 -23
  13. package/dist/core/change-detection.d.ts.map +1 -1
  14. package/dist/core/change-detection.js +73 -115
  15. package/dist/core/change-detection.js.map +1 -1
  16. package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
  17. package/dist/core/config.d.ts.map +1 -0
  18. package/dist/{config/index.js → core/config.js} +55 -73
  19. package/dist/core/config.js.map +1 -0
  20. package/dist/core/index.d.ts +1 -0
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/core/index.js +1 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/core/move-detection.d.ts +4 -3
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +8 -7
  27. package/dist/core/move-detection.js.map +1 -1
  28. package/dist/core/snapshot.d.ts +0 -4
  29. package/dist/core/snapshot.d.ts.map +1 -1
  30. package/dist/core/snapshot.js +2 -11
  31. package/dist/core/snapshot.js.map +1 -1
  32. package/dist/core/sync-engine.d.ts +5 -11
  33. package/dist/core/sync-engine.d.ts.map +1 -1
  34. package/dist/core/sync-engine.js +211 -308
  35. package/dist/core/sync-engine.js.map +1 -1
  36. package/dist/index.d.ts +0 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +0 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/types/config.d.ts +24 -88
  41. package/dist/types/config.d.ts.map +1 -1
  42. package/dist/types/config.js +6 -0
  43. package/dist/types/config.js.map +1 -1
  44. package/dist/types/documents.d.ts +15 -2
  45. package/dist/types/documents.d.ts.map +1 -1
  46. package/dist/types/documents.js.map +1 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/index.js +0 -3
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/types/snapshot.d.ts +0 -21
  51. package/dist/types/snapshot.d.ts.map +1 -1
  52. package/dist/types/snapshot.js +0 -14
  53. package/dist/types/snapshot.js.map +1 -1
  54. package/dist/utils/content.d.ts.map +1 -1
  55. package/dist/utils/content.js +2 -6
  56. package/dist/utils/content.js.map +1 -1
  57. package/dist/utils/directory.d.ts +10 -0
  58. package/dist/utils/directory.d.ts.map +1 -0
  59. package/dist/utils/directory.js +37 -0
  60. package/dist/utils/directory.js.map +1 -0
  61. package/dist/utils/fs.d.ts +15 -2
  62. package/dist/utils/fs.d.ts.map +1 -1
  63. package/dist/utils/fs.js +54 -20
  64. package/dist/utils/fs.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -0
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -3
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/utils/mime-types.d.ts.map +1 -1
  70. package/dist/utils/mime-types.js +11 -4
  71. package/dist/utils/mime-types.js.map +1 -1
  72. package/dist/utils/network-sync.d.ts +0 -6
  73. package/dist/utils/network-sync.d.ts.map +1 -1
  74. package/dist/utils/network-sync.js +55 -99
  75. package/dist/utils/network-sync.js.map +1 -1
  76. package/dist/utils/output.d.ts +129 -0
  77. package/dist/utils/output.d.ts.map +1 -0
  78. package/dist/utils/output.js +375 -0
  79. package/dist/utils/output.js.map +1 -0
  80. package/dist/utils/repo-factory.d.ts +2 -6
  81. package/dist/utils/repo-factory.d.ts.map +1 -1
  82. package/dist/utils/repo-factory.js +8 -31
  83. package/dist/utils/repo-factory.js.map +1 -1
  84. package/dist/utils/string-similarity.js +2 -2
  85. package/dist/utils/string-similarity.js.map +1 -1
  86. package/dist/utils/trace.d.ts +19 -0
  87. package/dist/utils/trace.d.ts.map +1 -0
  88. package/dist/utils/trace.js +68 -0
  89. package/dist/utils/trace.js.map +1 -0
  90. package/package.json +11 -11
  91. package/src/cli.ts +276 -308
  92. package/src/commands.ts +988 -0
  93. package/src/core/change-detection.ts +182 -240
  94. package/src/{config/index.ts → core/config.ts} +65 -82
  95. package/src/core/index.ts +1 -1
  96. package/src/core/move-detection.ts +10 -8
  97. package/src/core/snapshot.ts +2 -12
  98. package/src/core/sync-engine.ts +237 -427
  99. package/src/index.ts +0 -10
  100. package/src/types/config.ts +28 -93
  101. package/src/types/documents.ts +16 -2
  102. package/src/types/index.ts +0 -5
  103. package/src/types/snapshot.ts +0 -23
  104. package/src/utils/content.ts +2 -6
  105. package/src/utils/directory.ts +50 -0
  106. package/src/utils/fs.ts +58 -23
  107. package/src/utils/index.ts +1 -5
  108. package/src/utils/mime-types.ts +12 -4
  109. package/src/utils/network-sync.ts +79 -137
  110. package/src/utils/output.ts +450 -0
  111. package/src/utils/repo-factory.ts +13 -44
  112. package/src/utils/string-similarity.ts +2 -2
  113. package/src/utils/trace.ts +70 -0
  114. package/test/integration/exclude-patterns.test.ts +6 -15
  115. package/test/integration/fuzzer.test.ts +308 -391
  116. package/test/integration/init-sync.test.ts +89 -0
  117. package/test/integration/sync-deletion.test.ts +2 -61
  118. package/test/integration/sync-flow.test.ts +4 -24
  119. package/test/jest.setup.ts +34 -0
  120. package/test/unit/deletion-behavior.test.ts +3 -14
  121. package/test/unit/enhanced-mime-detection.test.ts +0 -22
  122. package/test/unit/snapshot.test.ts +2 -29
  123. package/test/unit/sync-convergence.test.ts +3 -198
  124. package/test/unit/sync-timing.test.ts +0 -44
  125. package/test/unit/utils.test.ts +0 -2
  126. package/tsconfig.json +3 -3
  127. package/bench/filesystem.bench.ts +0 -78
  128. package/bench/hashing.bench.ts +0 -60
  129. package/bench/move-detection.bench.ts +0 -130
  130. package/bench/runner.ts +0 -49
  131. package/dist/browser/browser-sync-engine.d.ts +0 -64
  132. package/dist/browser/browser-sync-engine.d.ts.map +0 -1
  133. package/dist/browser/browser-sync-engine.js +0 -303
  134. package/dist/browser/browser-sync-engine.js.map +0 -1
  135. package/dist/browser/filesystem-adapter.d.ts +0 -84
  136. package/dist/browser/filesystem-adapter.d.ts.map +0 -1
  137. package/dist/browser/filesystem-adapter.js +0 -413
  138. package/dist/browser/filesystem-adapter.js.map +0 -1
  139. package/dist/browser/index.d.ts +0 -36
  140. package/dist/browser/index.d.ts.map +0 -1
  141. package/dist/browser/index.js +0 -90
  142. package/dist/browser/index.js.map +0 -1
  143. package/dist/browser/types.d.ts +0 -70
  144. package/dist/browser/types.d.ts.map +0 -1
  145. package/dist/browser/types.js +0 -6
  146. package/dist/browser/types.js.map +0 -1
  147. package/dist/cli/commands.d.ts +0 -67
  148. package/dist/cli/commands.d.ts.map +0 -1
  149. package/dist/cli/commands.js +0 -794
  150. package/dist/cli/commands.js.map +0 -1
  151. package/dist/cli/index.d.ts +0 -2
  152. package/dist/cli/index.d.ts.map +0 -1
  153. package/dist/cli/index.js +0 -19
  154. package/dist/cli/index.js.map +0 -1
  155. package/dist/cli/output.d.ts +0 -75
  156. package/dist/cli/output.d.ts.map +0 -1
  157. package/dist/cli/output.js +0 -182
  158. package/dist/cli/output.js.map +0 -1
  159. package/dist/config/index.d.ts.map +0 -1
  160. package/dist/config/index.js.map +0 -1
  161. package/dist/config/remote-manager.d.ts +0 -65
  162. package/dist/config/remote-manager.d.ts.map +0 -1
  163. package/dist/config/remote-manager.js +0 -243
  164. package/dist/config/remote-manager.js.map +0 -1
  165. package/dist/core/isomorphic-snapshot.d.ts +0 -58
  166. package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
  167. package/dist/core/isomorphic-snapshot.js +0 -204
  168. package/dist/core/isomorphic-snapshot.js.map +0 -1
  169. package/dist/platform/browser-filesystem.d.ts +0 -26
  170. package/dist/platform/browser-filesystem.d.ts.map +0 -1
  171. package/dist/platform/browser-filesystem.js +0 -91
  172. package/dist/platform/browser-filesystem.js.map +0 -1
  173. package/dist/platform/filesystem.d.ts +0 -29
  174. package/dist/platform/filesystem.d.ts.map +0 -1
  175. package/dist/platform/filesystem.js +0 -65
  176. package/dist/platform/filesystem.js.map +0 -1
  177. package/dist/platform/node-filesystem.d.ts +0 -21
  178. package/dist/platform/node-filesystem.d.ts.map +0 -1
  179. package/dist/platform/node-filesystem.js +0 -93
  180. package/dist/platform/node-filesystem.js.map +0 -1
  181. package/dist/utils/content-similarity.d.ts +0 -53
  182. package/dist/utils/content-similarity.d.ts.map +0 -1
  183. package/dist/utils/content-similarity.js +0 -155
  184. package/dist/utils/content-similarity.js.map +0 -1
  185. package/dist/utils/fs-browser.d.ts +0 -57
  186. package/dist/utils/fs-browser.d.ts.map +0 -1
  187. package/dist/utils/fs-browser.js +0 -311
  188. package/dist/utils/fs-browser.js.map +0 -1
  189. package/dist/utils/fs-node.d.ts +0 -53
  190. package/dist/utils/fs-node.d.ts.map +0 -1
  191. package/dist/utils/fs-node.js +0 -220
  192. package/dist/utils/fs-node.js.map +0 -1
  193. package/dist/utils/isomorphic.d.ts +0 -29
  194. package/dist/utils/isomorphic.d.ts.map +0 -1
  195. package/dist/utils/isomorphic.js +0 -139
  196. package/dist/utils/isomorphic.js.map +0 -1
  197. package/dist/utils/pure.d.ts +0 -25
  198. package/dist/utils/pure.d.ts.map +0 -1
  199. package/dist/utils/pure.js +0 -112
  200. package/dist/utils/pure.js.map +0 -1
  201. package/src/cli/commands.ts +0 -1030
  202. package/src/cli/index.ts +0 -2
  203. package/src/cli/output.ts +0 -244
  204. package/test/README-TESTING-GAPS.md +0 -174
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SyncEngine = void 0;
37
- const automerge_repo_1 = require("@automerge/automerge-repo");
38
37
  const A = __importStar(require("@automerge/automerge"));
39
38
  const types_1 = require("../types");
40
39
  const utils_1 = require("../utils");
@@ -43,20 +42,27 @@ const network_sync_1 = require("../utils/network-sync");
43
42
  const snapshot_1 = require("./snapshot");
44
43
  const change_detection_1 = require("./change-detection");
45
44
  const move_detection_1 = require("./move-detection");
45
+ const output_1 = require("../utils/output");
46
+ /**
47
+ * Post-sync delay constants for network propagation
48
+ * These delays allow the WebSocket protocol to propagate peer changes after
49
+ * our changes reach the server. waitForSync only ensures OUR changes reached
50
+ * the server, not that we've RECEIVED changes from other peers.
51
+ * TODO: remove need for this to exist.
52
+ */
53
+ const POST_SYNC_DELAY_MS = 200; // After we pushed changes
46
54
  /**
47
55
  * Bidirectional sync engine implementing two-phase sync
48
56
  */
49
57
  class SyncEngine {
50
- constructor(repo, rootPath, excludePatterns = [], networkSyncEnabled = true, syncServerStorageId) {
58
+ constructor(repo, rootPath, config) {
51
59
  this.repo = repo;
52
60
  this.rootPath = rootPath;
53
- this.networkSyncEnabled = true;
54
61
  this.handlesToWaitOn = [];
62
+ this.config = config;
55
63
  this.snapshotManager = new snapshot_1.SnapshotManager(rootPath);
56
- this.changeDetector = new change_detection_1.ChangeDetector(repo, rootPath, excludePatterns);
57
- this.moveDetector = new move_detection_1.MoveDetector();
58
- this.networkSyncEnabled = networkSyncEnabled;
59
- this.syncServerStorageId = syncServerStorageId;
64
+ this.changeDetector = new change_detection_1.ChangeDetector(repo, rootPath, config.exclude_patterns);
65
+ this.moveDetector = new move_detection_1.MoveDetector(config.sync.move_detection_threshold);
60
66
  }
61
67
  /**
62
68
  * Determine if content should be treated as text for Automerge text operations
@@ -81,7 +87,7 @@ class SyncEngine {
81
87
  /**
82
88
  * Commit local changes only (no network sync)
83
89
  */
84
- async commitLocal(dryRun = false) {
90
+ async commitLocal() {
85
91
  const result = {
86
92
  success: false,
87
93
  filesChanged: 0,
@@ -95,16 +101,12 @@ class SyncEngine {
95
101
  if (!snapshot) {
96
102
  snapshot = this.snapshotManager.createEmpty();
97
103
  }
98
- // Backup snapshot before starting
99
- if (!dryRun) {
100
- await this.snapshotManager.backup();
101
- }
102
104
  // Detect all changes
103
105
  const changes = await this.changeDetector.detectChanges(snapshot);
104
106
  // Detect moves
105
- const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
107
+ const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
106
108
  // Apply local changes only (no network sync)
107
- const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
109
+ const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot);
108
110
  result.filesChanged += commitResult.filesChanged;
109
111
  result.directoriesChanged += commitResult.directoriesChanged;
110
112
  result.errors.push(...commitResult.errors);
@@ -112,12 +114,10 @@ class SyncEngine {
112
114
  // Touch root directory if any changes were made
113
115
  const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
114
116
  if (hasChanges) {
115
- await this.touchRootDirectory(snapshot, dryRun);
116
- }
117
- // Save updated snapshot if not dry run
118
- if (!dryRun) {
119
- await this.snapshotManager.save(snapshot);
117
+ await this.touchRootDirectory(snapshot);
120
118
  }
119
+ // Save updated snapshot
120
+ await this.snapshotManager.save(snapshot);
121
121
  result.success = result.errors.length === 0;
122
122
  return result;
123
123
  }
@@ -135,9 +135,7 @@ class SyncEngine {
135
135
  /**
136
136
  * Run full bidirectional sync
137
137
  */
138
- async sync(dryRun = false) {
139
- const syncStartTime = Date.now();
140
- const timings = {};
138
+ async sync() {
141
139
  const result = {
142
140
  success: false,
143
141
  filesChanged: 0,
@@ -150,49 +148,30 @@ class SyncEngine {
150
148
  this.handlesToWaitOn = [];
151
149
  try {
152
150
  // Load current snapshot
153
- const t0 = Date.now();
154
- let snapshot = await this.snapshotManager.load();
155
- timings["load_snapshot"] = Date.now() - t0;
156
- if (!snapshot) {
157
- snapshot = this.snapshotManager.createEmpty();
158
- }
159
- // Backup snapshot before starting
160
- const t1 = Date.now();
161
- if (!dryRun) {
162
- await this.snapshotManager.backup();
163
- }
164
- timings["backup_snapshot"] = Date.now() - t1;
151
+ const snapshot = (await this.snapshotManager.load()) ||
152
+ this.snapshotManager.createEmpty();
165
153
  // Detect all changes
166
- const t2 = Date.now();
167
154
  const changes = await this.changeDetector.detectChanges(snapshot);
168
- timings["detect_changes"] = Date.now() - t2;
169
155
  // Detect moves
170
- const t3 = Date.now();
171
- const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
172
- timings["detect_moves"] = Date.now() - t3;
156
+ const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
173
157
  // Phase 1: Push local changes to remote
174
- const t4 = Date.now();
175
- const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
176
- timings["phase1_push"] = Date.now() - t4;
158
+ const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot);
177
159
  result.filesChanged += phase1Result.filesChanged;
178
160
  result.directoriesChanged += phase1Result.directoriesChanged;
179
161
  result.errors.push(...phase1Result.errors);
180
162
  result.warnings.push(...phase1Result.warnings);
181
163
  // Always wait for network sync when enabled (not just when local changes exist)
182
164
  // This is critical for clone scenarios where we need to pull remote changes
183
- const t5 = Date.now();
184
- timings["documents_to_sync"] = this.handlesToWaitOn.length;
185
- if (!dryRun && this.networkSyncEnabled) {
165
+ if (this.config.sync_enabled) {
186
166
  try {
187
167
  // If we have a root directory URL, wait for it to sync
188
168
  if (snapshot.rootDirectoryUrl) {
189
- const rootHandle = await this.repo.find(snapshot.rootDirectoryUrl);
169
+ const rootDirUrl = snapshot.rootDirectoryUrl;
170
+ const rootHandle = await this.repo.find(rootDirUrl);
190
171
  this.handlesToWaitOn.push(rootHandle);
191
172
  }
192
173
  if (this.handlesToWaitOn.length > 0) {
193
- const tWaitStart = Date.now();
194
- await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, (0, network_sync_1.getSyncServerStorageId)(this.syncServerStorageId));
195
- timings["network_sync"] = Date.now() - tWaitStart;
174
+ await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, this.config.sync_server_storage_id);
196
175
  // CRITICAL: Wait a bit after our changes reach the server to allow
197
176
  // time for WebSocket to deliver OTHER peers' changes to us.
198
177
  // waitForSync only ensures OUR changes reached the server, not that
@@ -202,30 +181,22 @@ class SyncEngine {
202
181
  // each other due to timing races.
203
182
  //
204
183
  // Optimization: Only wait if we pushed changes (shorter delay if no changes)
205
- const tDelayStart = Date.now();
206
- const delayMs = phase1Result.filesChanged > 0 ? 200 : 100;
207
- await new Promise((resolve) => setTimeout(resolve, delayMs));
208
- timings["post_sync_delay"] = Date.now() - tDelayStart;
184
+ await new Promise((resolve) => setTimeout(resolve, POST_SYNC_DELAY_MS));
209
185
  }
210
186
  }
211
187
  catch (error) {
212
- console.error(`❌ Network sync failed: ${error}`);
188
+ output_1.out.taskLine(`Network sync failed: ${error}`, true);
213
189
  result.warnings.push(`Network sync failed: ${error}`);
214
190
  }
215
191
  }
216
- timings["total_network"] = Date.now() - t5;
217
192
  // Re-detect remote changes after network sync to ensure fresh state
218
193
  // This fixes race conditions where we detect changes before server propagation
219
194
  // NOTE: We DON'T update snapshot heads yet - that would prevent detecting remote changes!
220
- const t6 = Date.now();
221
195
  const freshChanges = await this.changeDetector.detectChanges(snapshot);
222
196
  const freshRemoteChanges = freshChanges.filter((c) => c.changeType === types_1.ChangeType.REMOTE_ONLY ||
223
197
  c.changeType === types_1.ChangeType.BOTH_CHANGED);
224
- timings["redetect_changes"] = Date.now() - t6;
225
198
  // Phase 2: Pull remote changes to local using fresh detection
226
- const t7 = Date.now();
227
- const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot, dryRun);
228
- timings["phase2_pull"] = Date.now() - t7;
199
+ const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot);
229
200
  result.filesChanged += phase2Result.filesChanged;
230
201
  result.directoriesChanged += phase2Result.directoriesChanged;
231
202
  result.errors.push(...phase2Result.errors);
@@ -233,64 +204,48 @@ class SyncEngine {
233
204
  // CRITICAL FIX: Update snapshot heads AFTER pulling remote changes
234
205
  // This ensures that change detection can find remote changes, and we only
235
206
  // update the snapshot after the filesystem is in sync with the documents
236
- const t8 = Date.now();
237
- if (!dryRun) {
238
- // Update file document heads
239
- for (const [filePath, snapshotEntry] of snapshot.files.entries()) {
240
- try {
241
- const handle = await this.repo.find(snapshotEntry.url);
242
- const currentHeads = handle.heads();
243
- if (!A.equals(currentHeads, snapshotEntry.head)) {
244
- // Update snapshot with current heads after pulling changes
245
- snapshot.files.set(filePath, {
246
- ...snapshotEntry,
247
- head: currentHeads,
248
- });
249
- }
250
- }
251
- catch (error) {
252
- // Handle might not exist if file was deleted, skip
253
- console.warn(`Could not update heads for ${filePath}: ${error}`);
207
+ // Update file document heads
208
+ for (const [filePath, snapshotEntry] of snapshot.files.entries()) {
209
+ try {
210
+ const handle = await this.repo.find(snapshotEntry.url);
211
+ const currentHeads = handle.heads();
212
+ if (!A.equals(currentHeads, snapshotEntry.head)) {
213
+ // Update snapshot with current heads after pulling changes
214
+ snapshot.files.set(filePath, {
215
+ ...snapshotEntry,
216
+ head: currentHeads,
217
+ });
254
218
  }
255
219
  }
256
- // Update directory document heads
257
- for (const [dirPath, snapshotEntry] of snapshot.directories.entries()) {
258
- try {
259
- const handle = await this.repo.find(snapshotEntry.url);
260
- const currentHeads = handle.heads();
261
- if (!A.equals(currentHeads, snapshotEntry.head)) {
262
- // Update snapshot with current heads after pulling changes
263
- snapshot.directories.set(dirPath, {
264
- ...snapshotEntry,
265
- head: currentHeads,
266
- });
267
- }
268
- }
269
- catch (error) {
270
- // Handle might not exist if directory was deleted, skip
271
- console.warn(`Could not update heads for directory ${dirPath}: ${error}`);
220
+ catch (error) {
221
+ // Handle might not exist if file was deleted
222
+ }
223
+ }
224
+ // Update directory document heads
225
+ for (const [dirPath, snapshotEntry] of snapshot.directories.entries()) {
226
+ try {
227
+ const handle = await this.repo.find(snapshotEntry.url);
228
+ const currentHeads = handle.heads();
229
+ if (!A.equals(currentHeads, snapshotEntry.head)) {
230
+ // Update snapshot with current heads after pulling changes
231
+ snapshot.directories.set(dirPath, {
232
+ ...snapshotEntry,
233
+ head: currentHeads,
234
+ });
272
235
  }
273
236
  }
237
+ catch (error) {
238
+ // Handle might not exist if directory was deleted
239
+ }
274
240
  }
275
- timings["update_snapshot_heads"] = Date.now() - t8;
276
241
  // Touch root directory if any changes were made during sync
277
- const t9 = Date.now();
278
242
  const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
279
243
  if (hasChanges) {
280
- await this.touchRootDirectory(snapshot, dryRun);
244
+ await this.touchRootDirectory(snapshot);
281
245
  }
282
- timings["touch_root"] = Date.now() - t9;
283
246
  // Save updated snapshot if not dry run
284
- const t10 = Date.now();
285
- if (!dryRun) {
286
- await this.snapshotManager.save(snapshot);
287
- }
288
- timings["save_snapshot"] = Date.now() - t10;
289
- // Calculate total time
290
- const totalTime = Date.now() - syncStartTime;
291
- timings["total"] = totalTime;
247
+ await this.snapshotManager.save(snapshot);
292
248
  result.success = result.errors.length === 0;
293
- result.timings = timings;
294
249
  return result;
295
250
  }
296
251
  catch (error) {
@@ -306,7 +261,7 @@ class SyncEngine {
306
261
  /**
307
262
  * Phase 1: Push local changes to Automerge documents
308
263
  */
309
- async pushLocalChanges(changes, moves, snapshot, dryRun) {
264
+ async pushLocalChanges(changes, moves, snapshot) {
310
265
  const result = {
311
266
  success: true,
312
267
  filesChanged: 0,
@@ -317,7 +272,7 @@ class SyncEngine {
317
272
  // Process moves first - all detected moves are applied
318
273
  for (const move of moves) {
319
274
  try {
320
- await this.applyMoveToRemote(move, snapshot, dryRun);
275
+ await this.applyMoveToRemote(move, snapshot);
321
276
  result.filesChanged++;
322
277
  }
323
278
  catch (error) {
@@ -334,7 +289,7 @@ class SyncEngine {
334
289
  c.changeType === types_1.ChangeType.BOTH_CHANGED);
335
290
  for (const change of localChanges) {
336
291
  try {
337
- await this.applyLocalChangeToRemote(change, snapshot, dryRun);
292
+ await this.applyLocalChangeToRemote(change, snapshot);
338
293
  result.filesChanged++;
339
294
  }
340
295
  catch (error) {
@@ -351,7 +306,7 @@ class SyncEngine {
351
306
  /**
352
307
  * Phase 2: Pull remote changes to local filesystem
353
308
  */
354
- async pullRemoteChanges(changes, snapshot, dryRun) {
309
+ async pullRemoteChanges(changes, snapshot) {
355
310
  const result = {
356
311
  success: true,
357
312
  filesChanged: 0,
@@ -366,7 +321,7 @@ class SyncEngine {
366
321
  const sortedChanges = this.sortChangesByDependency(remoteChanges);
367
322
  for (const change of sortedChanges) {
368
323
  try {
369
- await this.applyRemoteChangeToLocal(change, snapshot, dryRun);
324
+ await this.applyRemoteChangeToLocal(change, snapshot);
370
325
  result.filesChanged++;
371
326
  }
372
327
  catch (error) {
@@ -383,31 +338,29 @@ class SyncEngine {
383
338
  /**
384
339
  * Apply local file change to remote Automerge document
385
340
  */
386
- async applyLocalChangeToRemote(change, snapshot, dryRun) {
341
+ async applyLocalChangeToRemote(change, snapshot) {
387
342
  const snapshotEntry = snapshot.files.get(change.path);
388
343
  // CRITICAL: Check for null explicitly, not falsy values
389
344
  // Empty strings "" and empty Uint8Array are valid file content!
390
345
  if (change.localContent === null) {
391
346
  // File was deleted locally
392
347
  if (snapshotEntry) {
393
- await this.deleteRemoteFile(snapshotEntry.url, dryRun, snapshot, change.path);
348
+ await this.deleteRemoteFile(snapshotEntry.url, snapshot, change.path);
394
349
  // Remove from directory document
395
- await this.removeFileFromDirectory(snapshot, change.path, dryRun);
396
- if (!dryRun) {
397
- this.snapshotManager.removeFileEntry(snapshot, change.path);
398
- }
350
+ await this.removeFileFromDirectory(snapshot, change.path);
351
+ this.snapshotManager.removeFileEntry(snapshot, change.path);
399
352
  }
400
353
  return;
401
354
  }
402
355
  if (!snapshotEntry) {
403
356
  // New file
404
- const handle = await this.createRemoteFile(change, dryRun);
405
- if (!dryRun && handle) {
406
- await this.addFileToDirectory(snapshot, change.path, handle.url, dryRun);
357
+ const handle = await this.createRemoteFile(change);
358
+ if (handle) {
359
+ await this.addFileToDirectory(snapshot, change.path, handle.url);
407
360
  // CRITICAL FIX: Update snapshot with heads AFTER adding to directory
408
361
  // The addFileToDirectory call above may have changed the document heads
409
362
  this.snapshotManager.updateFileEntry(snapshot, change.path, {
410
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + change.path),
363
+ path: (0, utils_1.joinAndNormalizePath)(this.rootPath, change.path),
411
364
  url: handle.url,
412
365
  head: handle.heads(),
413
366
  extension: (0, utils_1.getFileExtension)(change.path),
@@ -417,14 +370,14 @@ class SyncEngine {
417
370
  }
418
371
  else {
419
372
  // Update existing file
420
- await this.updateRemoteFile(snapshotEntry.url, change.localContent, dryRun, snapshot, change.path);
373
+ await this.updateRemoteFile(snapshotEntry.url, change.localContent, snapshot, change.path);
421
374
  }
422
375
  }
423
376
  /**
424
377
  * Apply remote change to local filesystem
425
378
  */
426
- async applyRemoteChangeToLocal(change, snapshot, dryRun) {
427
- const localPath = (0, utils_1.normalizePath)(this.rootPath + "/" + change.path);
379
+ async applyRemoteChangeToLocal(change, snapshot) {
380
+ const localPath = (0, utils_1.joinAndNormalizePath)(this.rootPath, change.path);
428
381
  if (!change.remoteHead) {
429
382
  throw new Error(`No remote head found for remote change to ${change.path}`);
430
383
  }
@@ -432,123 +385,114 @@ class SyncEngine {
432
385
  // Empty strings "" and empty Uint8Array are valid file content!
433
386
  if (change.remoteContent === null) {
434
387
  // File was deleted remotely
435
- if (!dryRun) {
436
- await (0, utils_1.removePath)(localPath);
437
- this.snapshotManager.removeFileEntry(snapshot, change.path);
438
- }
388
+ await (0, utils_1.removePath)(localPath);
389
+ this.snapshotManager.removeFileEntry(snapshot, change.path);
439
390
  return;
440
391
  }
441
392
  // Create or update local file
442
- if (!dryRun) {
443
- await (0, utils_1.writeFileContent)(localPath, change.remoteContent);
444
- // Update or create snapshot entry for this file
445
- const snapshotEntry = snapshot.files.get(change.path);
446
- if (snapshotEntry) {
447
- // Update existing entry
448
- snapshotEntry.head = change.remoteHead;
449
- }
450
- else {
451
- // Create new snapshot entry for newly discovered remote file
452
- // We need to find the remote file's URL from the directory hierarchy
453
- if (snapshot.rootDirectoryUrl) {
454
- try {
455
- const fileEntry = await this.findFileInDirectoryHierarchy(snapshot.rootDirectoryUrl, change.path);
456
- if (fileEntry) {
457
- this.snapshotManager.updateFileEntry(snapshot, change.path, {
458
- path: localPath,
459
- url: fileEntry.url,
460
- head: change.remoteHead,
461
- extension: (0, utils_1.getFileExtension)(change.path),
462
- mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
463
- });
464
- }
465
- }
466
- catch (error) {
467
- console.warn(`Failed to update snapshot for remote file ${change.path}: ${error}`);
393
+ await (0, utils_1.writeFileContent)(localPath, change.remoteContent);
394
+ // Update or create snapshot entry for this file
395
+ const snapshotEntry = snapshot.files.get(change.path);
396
+ if (snapshotEntry) {
397
+ // Update existing entry
398
+ snapshotEntry.head = change.remoteHead;
399
+ }
400
+ else {
401
+ // Create new snapshot entry for newly discovered remote file
402
+ // We need to find the remote file's URL from the directory hierarchy
403
+ if (snapshot.rootDirectoryUrl) {
404
+ try {
405
+ const fileEntry = await (0, utils_1.findFileInDirectoryHierarchy)(this.repo, snapshot.rootDirectoryUrl, change.path);
406
+ if (fileEntry) {
407
+ this.snapshotManager.updateFileEntry(snapshot, change.path, {
408
+ path: localPath,
409
+ url: fileEntry.url,
410
+ head: change.remoteHead,
411
+ extension: (0, utils_1.getFileExtension)(change.path),
412
+ mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
413
+ });
468
414
  }
469
415
  }
416
+ catch (error) {
417
+ // Failed to update snapshot - file may have been deleted
418
+ output_1.out.taskLine(`Warning: Failed to update snapshot for remote file ${change.path}`, true);
419
+ }
470
420
  }
471
421
  }
472
422
  }
473
423
  /**
474
424
  * Apply move to remote documents
475
425
  */
476
- async applyMoveToRemote(move, snapshot, dryRun) {
426
+ async applyMoveToRemote(move, snapshot) {
477
427
  const fromEntry = snapshot.files.get(move.fromPath);
478
428
  if (!fromEntry)
479
429
  return;
480
430
  // Parse paths
481
- const fromParts = move.fromPath.split("/");
482
- const fromFileName = fromParts.pop() || "";
483
- const fromDirPath = fromParts.join("/");
484
431
  const toParts = move.toPath.split("/");
485
432
  const toFileName = toParts.pop() || "";
486
433
  const toDirPath = toParts.join("/");
487
- if (!dryRun) {
488
- // 1) Remove file entry from old directory document
489
- if (move.fromPath !== move.toPath) {
490
- await this.removeFileFromDirectory(snapshot, move.fromPath, dryRun);
491
- }
492
- // 2) Ensure destination directory document exists and add file entry there
493
- const destDirUrl = await this.ensureDirectoryDocument(snapshot, toDirPath, dryRun);
494
- await this.addFileToDirectory(snapshot, move.toPath, fromEntry.url, dryRun);
495
- // 3) Update the FileDocument name and content to match new location/state
496
- try {
497
- const handle = await this.repo.find(fromEntry.url);
498
- const heads = fromEntry.head;
499
- // Update both name and content (if content changed during move)
500
- if (heads && heads.length > 0) {
501
- handle.changeAt(heads, (doc) => {
502
- doc.name = toFileName;
503
- // If new content is provided, update it (handles move + modification case)
504
- if (move.newContent !== undefined) {
505
- const isText = this.isTextContent(move.newContent);
506
- if (isText && typeof move.newContent === "string") {
507
- (0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
508
- }
509
- else {
510
- doc.content = move.newContent;
511
- }
434
+ // 1) Remove file entry from old directory document
435
+ if (move.fromPath !== move.toPath) {
436
+ await this.removeFileFromDirectory(snapshot, move.fromPath);
437
+ }
438
+ // 2) Ensure destination directory document exists and add file entry there
439
+ await this.ensureDirectoryDocument(snapshot, toDirPath);
440
+ await this.addFileToDirectory(snapshot, move.toPath, fromEntry.url);
441
+ // 3) Update the FileDocument name and content to match new location/state
442
+ try {
443
+ const handle = await this.repo.find(fromEntry.url);
444
+ const heads = fromEntry.head;
445
+ // Update both name and content (if content changed during move)
446
+ if (heads && heads.length > 0) {
447
+ handle.changeAt(heads, (doc) => {
448
+ doc.name = toFileName;
449
+ // If new content is provided, update it (handles move + modification case)
450
+ if (move.newContent !== undefined) {
451
+ if (typeof move.newContent === "string") {
452
+ doc.content = new A.ImmutableString(move.newContent);
512
453
  }
513
- });
514
- }
515
- else {
516
- handle.change((doc) => {
517
- doc.name = toFileName;
518
- // If new content is provided, update it (handles move + modification case)
519
- if (move.newContent !== undefined) {
520
- const isText = this.isTextContent(move.newContent);
521
- if (isText && typeof move.newContent === "string") {
522
- (0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
523
- }
524
- else {
525
- doc.content = move.newContent;
526
- }
454
+ else {
455
+ doc.content = move.newContent;
527
456
  }
528
- });
529
- }
530
- // Track file handle for network sync
531
- this.handlesToWaitOn.push(handle);
457
+ }
458
+ });
532
459
  }
533
- catch (e) {
534
- console.warn(`Failed to update file name for move ${move.fromPath} -> ${move.toPath}: ${e}`);
460
+ else {
461
+ handle.change((doc) => {
462
+ doc.name = toFileName;
463
+ // If new content is provided, update it (handles move + modification case)
464
+ if (move.newContent !== undefined) {
465
+ if (typeof move.newContent === "string") {
466
+ doc.content = new A.ImmutableString(move.newContent);
467
+ }
468
+ else {
469
+ doc.content = move.newContent;
470
+ }
471
+ }
472
+ });
535
473
  }
536
- // 4) Update snapshot entries
537
- this.snapshotManager.removeFileEntry(snapshot, move.fromPath);
538
- this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
539
- ...fromEntry,
540
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + move.toPath),
541
- head: fromEntry.head, // will be updated later when heads advance
542
- });
474
+ // Track file handle for network sync
475
+ this.handlesToWaitOn.push(handle);
476
+ }
477
+ catch (e) {
478
+ // Failed to update file name - file may have been deleted
479
+ output_1.out.taskLine(`Warning: Failed to rename ${move.fromPath} to ${move.toPath}`, true);
543
480
  }
481
+ // 4) Update snapshot entries
482
+ this.snapshotManager.removeFileEntry(snapshot, move.fromPath);
483
+ this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
484
+ ...fromEntry,
485
+ path: (0, utils_1.joinAndNormalizePath)(this.rootPath, move.toPath),
486
+ head: fromEntry.head, // will be updated later when heads advance
487
+ });
544
488
  }
545
489
  /**
546
490
  * Create new remote file document
547
491
  */
548
- async createRemoteFile(change, dryRun) {
492
+ async createRemoteFile(change) {
549
493
  // CRITICAL: Check for null explicitly, not falsy values
550
494
  // Empty strings "" and empty Uint8Array are valid file content!
551
- if (dryRun || change.localContent === null)
495
+ if (change.localContent === null)
552
496
  return null;
553
497
  const isText = this.isTextContent(change.localContent);
554
498
  // Create initial document structure
@@ -557,16 +501,20 @@ class SyncEngine {
557
501
  name: change.path.split("/").pop() || "",
558
502
  extension: (0, utils_1.getFileExtension)(change.path),
559
503
  mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
560
- content: isText ? "" : change.localContent, // Empty string for text, actual content for binary
504
+ content: isText
505
+ ? new A.ImmutableString("")
506
+ : typeof change.localContent === "string"
507
+ ? new A.ImmutableString(change.localContent)
508
+ : change.localContent, // Empty ImmutableString for text, wrap strings for safety, actual content for binary
561
509
  metadata: {
562
510
  permissions: 0o644,
563
511
  },
564
512
  };
565
513
  const handle = this.repo.create(fileDoc);
566
- // For text files, use updateText to set the content properly
514
+ // For text files, use ImmutableString for better performance
567
515
  if (isText && typeof change.localContent === "string") {
568
516
  handle.change((doc) => {
569
- (0, automerge_repo_1.updateText)(doc, ["content"], change.localContent);
517
+ doc.content = new A.ImmutableString(change.localContent);
570
518
  });
571
519
  }
572
520
  // Always track newly created files for network sync
@@ -577,9 +525,7 @@ class SyncEngine {
577
525
  /**
578
526
  * Update existing remote file document
579
527
  */
580
- async updateRemoteFile(url, content, dryRun, snapshot, filePath) {
581
- if (dryRun)
582
- return;
528
+ async updateRemoteFile(url, content, snapshot, filePath) {
583
529
  const handle = await this.repo.find(url);
584
530
  // Check if content actually changed before tracking for sync
585
531
  const doc = await handle.doc();
@@ -598,7 +544,6 @@ class SyncEngine {
598
544
  if (!contentChanged) {
599
545
  // Content is identical, but we've updated the snapshot heads above
600
546
  // This prevents fresh change detection from seeing stale heads
601
- console.log(`🔍 Content is identical, but we've updated the snapshot heads above`);
602
547
  return;
603
548
  }
604
549
  const heads = snapshotEntry?.head;
@@ -606,16 +551,15 @@ class SyncEngine {
606
551
  throw new Error(`No heads found for ${url}`);
607
552
  }
608
553
  handle.changeAt(heads, (doc) => {
609
- const isText = this.isTextContent(content);
610
- if (isText && typeof content === "string") {
611
- (0, automerge_repo_1.updateText)(doc, ["content"], content);
554
+ if (typeof content === "string") {
555
+ doc.content = new A.ImmutableString(content);
612
556
  }
613
557
  else {
614
558
  doc.content = content;
615
559
  }
616
560
  });
617
561
  // Update snapshot with new heads after content change
618
- if (!dryRun && snapshotEntry) {
562
+ if (snapshotEntry) {
619
563
  snapshot.files.set(filePath, {
620
564
  ...snapshotEntry,
621
565
  head: handle.heads(),
@@ -627,9 +571,7 @@ class SyncEngine {
627
571
  /**
628
572
  * Delete remote file document
629
573
  */
630
- async deleteRemoteFile(url, dryRun, snapshot, filePath) {
631
- if (dryRun)
632
- return;
574
+ async deleteRemoteFile(url, snapshot, filePath) {
633
575
  // In Automerge, we don't actually delete documents
634
576
  // They become orphaned and will be garbage collected
635
577
  // For now, we just mark them as deleted by clearing content
@@ -641,26 +583,26 @@ class SyncEngine {
641
583
  }
642
584
  if (heads) {
643
585
  handle.changeAt(heads, (doc) => {
644
- doc.content = "";
586
+ doc.content = new A.ImmutableString("");
645
587
  });
646
588
  }
647
589
  else {
648
590
  handle.change((doc) => {
649
- doc.content = "";
591
+ doc.content = new A.ImmutableString("");
650
592
  });
651
593
  }
652
594
  }
653
595
  /**
654
596
  * Add file entry to appropriate directory document (maintains hierarchy)
655
597
  */
656
- async addFileToDirectory(snapshot, filePath, fileUrl, dryRun) {
657
- if (dryRun || !snapshot.rootDirectoryUrl)
598
+ async addFileToDirectory(snapshot, filePath, fileUrl) {
599
+ if (!snapshot.rootDirectoryUrl)
658
600
  return;
659
601
  const pathParts = filePath.split("/");
660
602
  const fileName = pathParts.pop() || "";
661
603
  const directoryPath = pathParts.join("/");
662
604
  // Get or create the parent directory document
663
- const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath, dryRun);
605
+ const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath);
664
606
  const dirHandle = await this.repo.find(parentDirUrl);
665
607
  let didChange = false;
666
608
  const snapshotEntry = snapshot.directories.get(directoryPath);
@@ -704,7 +646,7 @@ class SyncEngine {
704
646
  * Ensure directory document exists for the given path, creating hierarchy as needed
705
647
  * First checks for existing shared directories before creating new ones
706
648
  */
707
- async ensureDirectoryDocument(snapshot, directoryPath, dryRun) {
649
+ async ensureDirectoryDocument(snapshot, directoryPath) {
708
650
  // Root directory case
709
651
  if (!directoryPath || directoryPath === "") {
710
652
  return snapshot.rootDirectoryUrl;
@@ -719,7 +661,7 @@ class SyncEngine {
719
661
  const currentDirName = pathParts.pop() || "";
720
662
  const parentPath = pathParts.join("/");
721
663
  // Ensure parent directory exists first (recursive)
722
- const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath, dryRun);
664
+ const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath);
723
665
  // DISCOVERY: Check if directory already exists in parent on server
724
666
  try {
725
667
  const parentHandle = await this.repo.find(parentDirUrl);
@@ -733,25 +675,22 @@ class SyncEngine {
733
675
  const childDirHandle = await this.repo.find(existingDirEntry.url);
734
676
  const childHeads = childDirHandle.heads();
735
677
  // Update snapshot with discovered directory using validated heads
736
- if (!dryRun) {
737
- this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
738
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + directoryPath),
739
- url: existingDirEntry.url,
740
- head: childHeads,
741
- entries: [],
742
- });
743
- }
678
+ this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
679
+ path: (0, utils_1.joinAndNormalizePath)(this.rootPath, directoryPath),
680
+ url: existingDirEntry.url,
681
+ head: childHeads,
682
+ entries: [],
683
+ });
744
684
  return existingDirEntry.url;
745
685
  }
746
686
  catch (resolveErr) {
747
- console.warn(`Failed to resolve child directory ${currentDirName} at ${directoryPath}: ${resolveErr}`);
748
- // Fall through to create a fresh directory document
687
+ // Failed to resolve directory - fall through to create a fresh directory document
749
688
  }
750
689
  }
751
690
  }
752
691
  }
753
692
  catch (error) {
754
- console.warn(`Failed to check for existing directory ${currentDirName}: ${error}`);
693
+ // Failed to check for existing directory - will create new one
755
694
  }
756
695
  // CREATE: Directory doesn't exist, create new one
757
696
  const dirDoc = {
@@ -775,32 +714,30 @@ class SyncEngine {
775
714
  }
776
715
  });
777
716
  // Track directory handles for sync
778
- if (!dryRun) {
779
- this.handlesToWaitOn.push(dirHandle);
780
- if (didChange) {
781
- this.handlesToWaitOn.push(parentHandle);
782
- // CRITICAL FIX: Update parent directory heads in snapshot immediately
783
- // This prevents stale head issues when parent directory is modified
784
- const parentSnapshotEntry = snapshot.directories.get(parentPath);
785
- if (parentSnapshotEntry) {
786
- parentSnapshotEntry.head = parentHandle.heads();
787
- }
788
- }
789
- // Update snapshot with new directory
790
- this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
791
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + directoryPath),
792
- url: dirHandle.url,
793
- head: dirHandle.heads(),
794
- entries: [],
795
- });
796
- }
717
+ this.handlesToWaitOn.push(dirHandle);
718
+ if (didChange) {
719
+ this.handlesToWaitOn.push(parentHandle);
720
+ // CRITICAL FIX: Update parent directory heads in snapshot immediately
721
+ // This prevents stale head issues when parent directory is modified
722
+ const parentSnapshotEntry = snapshot.directories.get(parentPath);
723
+ if (parentSnapshotEntry) {
724
+ parentSnapshotEntry.head = parentHandle.heads();
725
+ }
726
+ }
727
+ // Update snapshot with new directory
728
+ this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
729
+ path: (0, utils_1.joinAndNormalizePath)(this.rootPath, directoryPath),
730
+ url: dirHandle.url,
731
+ head: dirHandle.heads(),
732
+ entries: [],
733
+ });
797
734
  return dirHandle.url;
798
735
  }
799
736
  /**
800
737
  * Remove file entry from directory document
801
738
  */
802
- async removeFileFromDirectory(snapshot, filePath, dryRun) {
803
- if (dryRun || !snapshot.rootDirectoryUrl)
739
+ async removeFileFromDirectory(snapshot, filePath) {
740
+ if (!snapshot.rootDirectoryUrl)
804
741
  return;
805
742
  const pathParts = filePath.split("/");
806
743
  const fileName = pathParts.pop() || "";
@@ -813,7 +750,7 @@ class SyncEngine {
813
750
  else {
814
751
  const existingDir = snapshot.directories.get(directoryPath);
815
752
  if (!existingDir) {
816
- console.warn(`Directory ${directoryPath} not found in snapshot for file removal`);
753
+ // Directory not found - file may already be removed
817
754
  return;
818
755
  }
819
756
  parentDirUrl = existingDir.url;
@@ -831,7 +768,7 @@ class SyncEngine {
831
768
  if (indexToRemove !== -1) {
832
769
  doc.docs.splice(indexToRemove, 1);
833
770
  didChange = true;
834
- console.log(`🗑️ Removed ${fileName} from directory ${directoryPath || "root"}`);
771
+ output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
835
772
  }
836
773
  });
837
774
  }
@@ -841,7 +778,7 @@ class SyncEngine {
841
778
  if (indexToRemove !== -1) {
842
779
  doc.docs.splice(indexToRemove, 1);
843
780
  didChange = true;
844
- console.log(`🗑️ Removed ${fileName} from directory ${directoryPath || "root"}`);
781
+ output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
845
782
  }
846
783
  });
847
784
  }
@@ -852,43 +789,10 @@ class SyncEngine {
852
789
  }
853
790
  }
854
791
  catch (error) {
855
- console.warn(`Failed to remove ${fileName} from directory ${directoryPath || "root"}: ${error}`);
792
+ // Failed to remove from directory - re-throw for caller to handle
856
793
  throw error;
857
794
  }
858
795
  }
859
- /**
860
- * Find a file in the directory hierarchy by path
861
- */
862
- async findFileInDirectoryHierarchy(directoryUrl, filePath) {
863
- try {
864
- const pathParts = filePath.split("/");
865
- let currentDirUrl = directoryUrl;
866
- // Navigate through directories to find the parent directory
867
- for (let i = 0; i < pathParts.length - 1; i++) {
868
- const dirName = pathParts[i];
869
- const dirHandle = await this.repo.find(currentDirUrl);
870
- const dirDoc = await dirHandle.doc();
871
- if (!dirDoc)
872
- return null;
873
- const subDirEntry = dirDoc.docs.find((entry) => entry.name === dirName && entry.type === "folder");
874
- if (!subDirEntry)
875
- return null;
876
- currentDirUrl = subDirEntry.url;
877
- }
878
- // Now look for the file in the final directory
879
- const fileName = pathParts[pathParts.length - 1];
880
- const finalDirHandle = await this.repo.find(currentDirUrl);
881
- const finalDirDoc = await finalDirHandle.doc();
882
- if (!finalDirDoc)
883
- return null;
884
- const fileEntry = finalDirDoc.docs.find((entry) => entry.name === fileName && entry.type === "file");
885
- return fileEntry || null;
886
- }
887
- catch (error) {
888
- console.warn(`Failed to find file ${filePath} in directory hierarchy: ${error}`);
889
- return null;
890
- }
891
- }
892
796
  /**
893
797
  * Sort changes by dependency order
894
798
  */
@@ -934,7 +838,7 @@ class SyncEngine {
934
838
  };
935
839
  }
936
840
  const changes = await this.changeDetector.detectChanges(snapshot);
937
- const { moves } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
841
+ const { moves } = await this.moveDetector.detectMoves(changes, snapshot);
938
842
  const summary = this.generateChangeSummary(changes, moves);
939
843
  return { changes, moves, summary };
940
844
  }
@@ -968,8 +872,8 @@ class SyncEngine {
968
872
  /**
969
873
  * Update the lastSyncAt timestamp on the root directory document
970
874
  */
971
- async touchRootDirectory(snapshot, dryRun) {
972
- if (dryRun || !snapshot.rootDirectoryUrl) {
875
+ async touchRootDirectory(snapshot) {
876
+ if (!snapshot.rootDirectoryUrl) {
973
877
  return;
974
878
  }
975
879
  try {
@@ -994,10 +898,9 @@ class SyncEngine {
994
898
  if (snapshotEntry) {
995
899
  snapshotEntry.head = rootHandle.heads();
996
900
  }
997
- console.log(`🕒 Updated root directory lastSyncAt to ${new Date(timestamp).toISOString()}`);
998
901
  }
999
902
  catch (error) {
1000
- console.warn(`Failed to update root directory lastSyncAt: ${error}`);
903
+ // Failed to update root directory timestamp
1001
904
  }
1002
905
  }
1003
906
  }