pushwork 1.0.4 → 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 (195) hide show
  1. package/README.md +87 -328
  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 +231 -170
  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 +6 -19
  13. package/dist/core/change-detection.d.ts.map +1 -1
  14. package/dist/core/change-detection.js +101 -80
  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 +12 -50
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +58 -139
  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 +220 -362
  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 +43 -67
  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 -3
  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 +3 -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 +63 -53
  64. package/dist/utils/fs.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -1
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -4
  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 -22
  83. package/dist/utils/repo-factory.js.map +1 -1
  84. package/dist/utils/string-similarity.d.ts +14 -0
  85. package/dist/utils/string-similarity.d.ts.map +1 -0
  86. package/dist/utils/string-similarity.js +43 -0
  87. package/dist/utils/string-similarity.js.map +1 -0
  88. package/dist/utils/trace.d.ts +19 -0
  89. package/dist/utils/trace.d.ts.map +1 -0
  90. package/dist/utils/trace.js +68 -0
  91. package/dist/utils/trace.js.map +1 -0
  92. package/package.json +17 -12
  93. package/src/cli.ts +326 -252
  94. package/src/commands.ts +988 -0
  95. package/src/core/change-detection.ts +199 -162
  96. package/src/{config/index.ts → core/config.ts} +65 -82
  97. package/src/core/index.ts +1 -1
  98. package/src/core/move-detection.ts +74 -180
  99. package/src/core/snapshot.ts +2 -12
  100. package/src/core/sync-engine.ts +248 -499
  101. package/src/index.ts +0 -10
  102. package/src/types/config.ts +50 -72
  103. package/src/types/documents.ts +16 -3
  104. package/src/types/index.ts +0 -5
  105. package/src/types/snapshot.ts +1 -23
  106. package/src/utils/content.ts +2 -6
  107. package/src/utils/directory.ts +50 -0
  108. package/src/utils/fs.ts +67 -56
  109. package/src/utils/index.ts +1 -6
  110. package/src/utils/mime-types.ts +12 -4
  111. package/src/utils/network-sync.ts +79 -137
  112. package/src/utils/output.ts +450 -0
  113. package/src/utils/repo-factory.ts +13 -31
  114. package/src/utils/string-similarity.ts +54 -0
  115. package/src/utils/trace.ts +70 -0
  116. package/test/integration/exclude-patterns.test.ts +6 -15
  117. package/test/integration/fuzzer.test.ts +308 -391
  118. package/test/integration/init-sync.test.ts +89 -0
  119. package/test/integration/sync-deletion.test.ts +2 -61
  120. package/test/integration/sync-flow.test.ts +4 -24
  121. package/test/jest.setup.ts +34 -0
  122. package/test/unit/deletion-behavior.test.ts +3 -14
  123. package/test/unit/enhanced-mime-detection.test.ts +0 -22
  124. package/test/unit/snapshot.test.ts +2 -29
  125. package/test/unit/sync-convergence.test.ts +3 -198
  126. package/test/unit/sync-timing.test.ts +0 -44
  127. package/test/unit/utils.test.ts +0 -2
  128. package/tsconfig.json +3 -3
  129. package/dist/browser/browser-sync-engine.d.ts +0 -64
  130. package/dist/browser/browser-sync-engine.d.ts.map +0 -1
  131. package/dist/browser/browser-sync-engine.js +0 -303
  132. package/dist/browser/browser-sync-engine.js.map +0 -1
  133. package/dist/browser/filesystem-adapter.d.ts +0 -84
  134. package/dist/browser/filesystem-adapter.d.ts.map +0 -1
  135. package/dist/browser/filesystem-adapter.js +0 -413
  136. package/dist/browser/filesystem-adapter.js.map +0 -1
  137. package/dist/browser/index.d.ts +0 -36
  138. package/dist/browser/index.d.ts.map +0 -1
  139. package/dist/browser/index.js +0 -90
  140. package/dist/browser/index.js.map +0 -1
  141. package/dist/browser/types.d.ts +0 -70
  142. package/dist/browser/types.d.ts.map +0 -1
  143. package/dist/browser/types.js +0 -6
  144. package/dist/browser/types.js.map +0 -1
  145. package/dist/cli/commands.d.ts +0 -77
  146. package/dist/cli/commands.d.ts.map +0 -1
  147. package/dist/cli/commands.js +0 -904
  148. package/dist/cli/commands.js.map +0 -1
  149. package/dist/cli/index.d.ts +0 -2
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/index.js +0 -19
  152. package/dist/cli/index.js.map +0 -1
  153. package/dist/config/index.d.ts.map +0 -1
  154. package/dist/config/index.js.map +0 -1
  155. package/dist/core/isomorphic-snapshot.d.ts +0 -58
  156. package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
  157. package/dist/core/isomorphic-snapshot.js +0 -204
  158. package/dist/core/isomorphic-snapshot.js.map +0 -1
  159. package/dist/platform/browser-filesystem.d.ts +0 -26
  160. package/dist/platform/browser-filesystem.d.ts.map +0 -1
  161. package/dist/platform/browser-filesystem.js +0 -91
  162. package/dist/platform/browser-filesystem.js.map +0 -1
  163. package/dist/platform/filesystem.d.ts +0 -29
  164. package/dist/platform/filesystem.d.ts.map +0 -1
  165. package/dist/platform/filesystem.js +0 -65
  166. package/dist/platform/filesystem.js.map +0 -1
  167. package/dist/platform/node-filesystem.d.ts +0 -21
  168. package/dist/platform/node-filesystem.d.ts.map +0 -1
  169. package/dist/platform/node-filesystem.js +0 -93
  170. package/dist/platform/node-filesystem.js.map +0 -1
  171. package/dist/utils/content-similarity.d.ts +0 -53
  172. package/dist/utils/content-similarity.d.ts.map +0 -1
  173. package/dist/utils/content-similarity.js +0 -155
  174. package/dist/utils/content-similarity.js.map +0 -1
  175. package/dist/utils/fs-browser.d.ts +0 -57
  176. package/dist/utils/fs-browser.d.ts.map +0 -1
  177. package/dist/utils/fs-browser.js +0 -311
  178. package/dist/utils/fs-browser.js.map +0 -1
  179. package/dist/utils/fs-node.d.ts +0 -53
  180. package/dist/utils/fs-node.d.ts.map +0 -1
  181. package/dist/utils/fs-node.js +0 -220
  182. package/dist/utils/fs-node.js.map +0 -1
  183. package/dist/utils/isomorphic.d.ts +0 -29
  184. package/dist/utils/isomorphic.d.ts.map +0 -1
  185. package/dist/utils/isomorphic.js +0 -139
  186. package/dist/utils/isomorphic.js.map +0 -1
  187. package/dist/utils/pure.d.ts +0 -25
  188. package/dist/utils/pure.d.ts.map +0 -1
  189. package/dist/utils/pure.js +0 -112
  190. package/dist/utils/pure.js.map +0 -1
  191. package/src/cli/commands.ts +0 -1207
  192. package/src/cli/index.ts +0 -2
  193. package/src/utils/content-similarity.ts +0 -194
  194. package/test/README-TESTING-GAPS.md +0 -174
  195. package/test/unit/content-similarity.test.ts +0 -236
@@ -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,8 +87,7 @@ class SyncEngine {
81
87
  /**
82
88
  * Commit local changes only (no network sync)
83
89
  */
84
- async commitLocal(dryRun = false) {
85
- console.log(`🚀 Starting local commit process (dryRun: ${dryRun})`);
90
+ async commitLocal() {
86
91
  const result = {
87
92
  success: false,
88
93
  filesChanged: 0,
@@ -92,35 +97,16 @@ class SyncEngine {
92
97
  };
93
98
  try {
94
99
  // Load current snapshot
95
- console.log(`📸 Loading current snapshot...`);
96
100
  let snapshot = await this.snapshotManager.load();
97
101
  if (!snapshot) {
98
- console.log(`📸 No snapshot found, creating empty one`);
99
102
  snapshot = this.snapshotManager.createEmpty();
100
103
  }
101
- else {
102
- console.log(`📸 Snapshot loaded with ${snapshot.files.size} files`);
103
- if (snapshot.rootDirectoryUrl) {
104
- console.log(`🔗 Root directory URL: ${snapshot.rootDirectoryUrl}`);
105
- }
106
- }
107
- // Backup snapshot before starting
108
- if (!dryRun) {
109
- console.log(`💾 Backing up snapshot...`);
110
- await this.snapshotManager.backup();
111
- }
112
104
  // Detect all changes
113
- console.log(`🔍 Detecting changes...`);
114
105
  const changes = await this.changeDetector.detectChanges(snapshot);
115
- console.log(`🔍 Found ${changes.length} changes`);
116
106
  // Detect moves
117
- console.log(`📦 Detecting moves...`);
118
- const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
119
- console.log(`📦 Found ${moves.length} moves, ${remainingChanges.length} remaining changes`);
107
+ const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
120
108
  // Apply local changes only (no network sync)
121
- console.log(`💾 Committing local changes...`);
122
- const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
123
- console.log(`💾 Commit complete: ${commitResult.filesChanged} files changed`);
109
+ const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot);
124
110
  result.filesChanged += commitResult.filesChanged;
125
111
  result.directoriesChanged += commitResult.directoriesChanged;
126
112
  result.errors.push(...commitResult.errors);
@@ -128,18 +114,14 @@ class SyncEngine {
128
114
  // Touch root directory if any changes were made
129
115
  const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
130
116
  if (hasChanges) {
131
- await this.touchRootDirectory(snapshot, dryRun);
132
- }
133
- // Save updated snapshot if not dry run
134
- if (!dryRun) {
135
- await this.snapshotManager.save(snapshot);
117
+ await this.touchRootDirectory(snapshot);
136
118
  }
119
+ // Save updated snapshot
120
+ await this.snapshotManager.save(snapshot);
137
121
  result.success = result.errors.length === 0;
138
- console.log(`💾 Local commit ${result.success ? "completed" : "failed"}`);
139
122
  return result;
140
123
  }
141
124
  catch (error) {
142
- console.error(`❌ Local commit failed: ${error}`);
143
125
  result.errors.push({
144
126
  path: this.rootPath,
145
127
  operation: "commitLocal",
@@ -153,65 +135,43 @@ class SyncEngine {
153
135
  /**
154
136
  * Run full bidirectional sync
155
137
  */
156
- async sync(dryRun = false) {
157
- const syncStartTime = Date.now();
158
- const timings = {};
138
+ async sync() {
159
139
  const result = {
160
140
  success: false,
161
141
  filesChanged: 0,
162
142
  directoriesChanged: 0,
163
143
  errors: [],
164
144
  warnings: [],
145
+ timings: {},
165
146
  };
166
147
  // Reset handles to wait on
167
148
  this.handlesToWaitOn = [];
168
149
  try {
169
150
  // Load current snapshot
170
- const t0 = Date.now();
171
- let snapshot = await this.snapshotManager.load();
172
- timings["load_snapshot"] = Date.now() - t0;
173
- if (!snapshot) {
174
- snapshot = this.snapshotManager.createEmpty();
175
- }
176
- // Backup snapshot before starting
177
- const t1 = Date.now();
178
- if (!dryRun) {
179
- await this.snapshotManager.backup();
180
- }
181
- timings["backup_snapshot"] = Date.now() - t1;
151
+ const snapshot = (await this.snapshotManager.load()) ||
152
+ this.snapshotManager.createEmpty();
182
153
  // Detect all changes
183
- const t2 = Date.now();
184
154
  const changes = await this.changeDetector.detectChanges(snapshot);
185
- timings["detect_changes"] = Date.now() - t2;
186
155
  // Detect moves
187
- const t3 = Date.now();
188
- const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
189
- timings["detect_moves"] = Date.now() - t3;
190
- if (changes.length > 0) {
191
- console.log(`🔄 Syncing ${changes.length} changes...`);
192
- }
156
+ const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
193
157
  // Phase 1: Push local changes to remote
194
- const t4 = Date.now();
195
- const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
196
- timings["phase1_push"] = Date.now() - t4;
158
+ const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot);
197
159
  result.filesChanged += phase1Result.filesChanged;
198
160
  result.directoriesChanged += phase1Result.directoriesChanged;
199
161
  result.errors.push(...phase1Result.errors);
200
162
  result.warnings.push(...phase1Result.warnings);
201
163
  // Always wait for network sync when enabled (not just when local changes exist)
202
164
  // This is critical for clone scenarios where we need to pull remote changes
203
- const t5 = Date.now();
204
- if (!dryRun && this.networkSyncEnabled) {
165
+ if (this.config.sync_enabled) {
205
166
  try {
206
167
  // If we have a root directory URL, wait for it to sync
207
168
  if (snapshot.rootDirectoryUrl) {
208
- const rootHandle = await this.repo.find(snapshot.rootDirectoryUrl);
169
+ const rootDirUrl = snapshot.rootDirectoryUrl;
170
+ const rootHandle = await this.repo.find(rootDirUrl);
209
171
  this.handlesToWaitOn.push(rootHandle);
210
172
  }
211
173
  if (this.handlesToWaitOn.length > 0) {
212
- const tWaitStart = Date.now();
213
- await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, (0, network_sync_1.getSyncServerStorageId)(this.syncServerStorageId));
214
- timings["network_sync"] = Date.now() - tWaitStart;
174
+ await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, this.config.sync_server_storage_id);
215
175
  // CRITICAL: Wait a bit after our changes reach the server to allow
216
176
  // time for WebSocket to deliver OTHER peers' changes to us.
217
177
  // waitForSync only ensures OUR changes reached the server, not that
@@ -221,30 +181,22 @@ class SyncEngine {
221
181
  // each other due to timing races.
222
182
  //
223
183
  // Optimization: Only wait if we pushed changes (shorter delay if no changes)
224
- const tDelayStart = Date.now();
225
- const delayMs = phase1Result.filesChanged > 0 ? 200 : 100;
226
- await new Promise((resolve) => setTimeout(resolve, delayMs));
227
- timings["post_sync_delay"] = Date.now() - tDelayStart;
184
+ await new Promise((resolve) => setTimeout(resolve, POST_SYNC_DELAY_MS));
228
185
  }
229
186
  }
230
187
  catch (error) {
231
- console.error(`❌ Network sync failed: ${error}`);
188
+ output_1.out.taskLine(`Network sync failed: ${error}`, true);
232
189
  result.warnings.push(`Network sync failed: ${error}`);
233
190
  }
234
191
  }
235
- timings["total_network"] = Date.now() - t5;
236
192
  // Re-detect remote changes after network sync to ensure fresh state
237
193
  // This fixes race conditions where we detect changes before server propagation
238
194
  // NOTE: We DON'T update snapshot heads yet - that would prevent detecting remote changes!
239
- const t6 = Date.now();
240
195
  const freshChanges = await this.changeDetector.detectChanges(snapshot);
241
196
  const freshRemoteChanges = freshChanges.filter((c) => c.changeType === types_1.ChangeType.REMOTE_ONLY ||
242
197
  c.changeType === types_1.ChangeType.BOTH_CHANGED);
243
- timings["redetect_changes"] = Date.now() - t6;
244
198
  // Phase 2: Pull remote changes to local using fresh detection
245
- const t7 = Date.now();
246
- const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot, dryRun);
247
- timings["phase2_pull"] = Date.now() - t7;
199
+ const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot);
248
200
  result.filesChanged += phase2Result.filesChanged;
249
201
  result.directoriesChanged += phase2Result.directoriesChanged;
250
202
  result.errors.push(...phase2Result.errors);
@@ -252,71 +204,47 @@ class SyncEngine {
252
204
  // CRITICAL FIX: Update snapshot heads AFTER pulling remote changes
253
205
  // This ensures that change detection can find remote changes, and we only
254
206
  // update the snapshot after the filesystem is in sync with the documents
255
- const t8 = Date.now();
256
- if (!dryRun) {
257
- // Update file document heads
258
- for (const [filePath, snapshotEntry] of snapshot.files.entries()) {
259
- try {
260
- const handle = await this.repo.find(snapshotEntry.url);
261
- const currentHeads = handle.heads();
262
- if (!A.equals(currentHeads, snapshotEntry.head)) {
263
- // Update snapshot with current heads after pulling changes
264
- snapshot.files.set(filePath, {
265
- ...snapshotEntry,
266
- head: currentHeads,
267
- });
268
- }
269
- }
270
- catch (error) {
271
- // Handle might not exist if file was deleted, skip
272
- 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
+ });
273
218
  }
274
219
  }
275
- // Update directory document heads
276
- for (const [dirPath, snapshotEntry] of snapshot.directories.entries()) {
277
- try {
278
- const handle = await this.repo.find(snapshotEntry.url);
279
- const currentHeads = handle.heads();
280
- if (!A.equals(currentHeads, snapshotEntry.head)) {
281
- // Update snapshot with current heads after pulling changes
282
- snapshot.directories.set(dirPath, {
283
- ...snapshotEntry,
284
- head: currentHeads,
285
- });
286
- }
287
- }
288
- catch (error) {
289
- // Handle might not exist if directory was deleted, skip
290
- 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
+ });
291
235
  }
292
236
  }
237
+ catch (error) {
238
+ // Handle might not exist if directory was deleted
239
+ }
293
240
  }
294
- timings["update_snapshot_heads"] = Date.now() - t8;
295
241
  // Touch root directory if any changes were made during sync
296
- const t9 = Date.now();
297
242
  const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
298
243
  if (hasChanges) {
299
- await this.touchRootDirectory(snapshot, dryRun);
244
+ await this.touchRootDirectory(snapshot);
300
245
  }
301
- timings["touch_root"] = Date.now() - t9;
302
246
  // Save updated snapshot if not dry run
303
- const t10 = Date.now();
304
- if (!dryRun) {
305
- await this.snapshotManager.save(snapshot);
306
- }
307
- timings["save_snapshot"] = Date.now() - t10;
308
- // Output timing breakdown if enabled via environment variable
309
- if (process.env.PUSHWORK_TIMING === "1") {
310
- const totalTime = Date.now() - syncStartTime;
311
- console.error("\n⏱️ Sync Timing Breakdown:");
312
- for (const [key, ms] of Object.entries(timings)) {
313
- const pct = ((ms / totalTime) * 100).toFixed(1);
314
- console.error(` ${key.padEnd(25)} ${ms.toString().padStart(5)}ms (${pct}%)`);
315
- }
316
- console.error(` ${"TOTAL".padEnd(25)} ${totalTime
317
- .toString()
318
- .padStart(5)}ms (100.0%)\n`);
319
- }
247
+ await this.snapshotManager.save(snapshot);
320
248
  result.success = result.errors.length === 0;
321
249
  return result;
322
250
  }
@@ -333,7 +261,7 @@ class SyncEngine {
333
261
  /**
334
262
  * Phase 1: Push local changes to Automerge documents
335
263
  */
336
- async pushLocalChanges(changes, moves, snapshot, dryRun) {
264
+ async pushLocalChanges(changes, moves, snapshot) {
337
265
  const result = {
338
266
  success: true,
339
267
  filesChanged: 0,
@@ -341,26 +269,19 @@ class SyncEngine {
341
269
  errors: [],
342
270
  warnings: [],
343
271
  };
344
- // Process moves first
272
+ // Process moves first - all detected moves are applied
345
273
  for (const move of moves) {
346
- if (this.moveDetector.shouldAutoApply(move)) {
347
- try {
348
- await this.applyMoveToRemote(move, snapshot, dryRun);
349
- result.filesChanged++;
350
- }
351
- catch (error) {
352
- result.errors.push({
353
- path: move.fromPath,
354
- operation: "move",
355
- error: error,
356
- recoverable: true,
357
- });
358
- }
274
+ try {
275
+ await this.applyMoveToRemote(move, snapshot);
276
+ result.filesChanged++;
359
277
  }
360
- else if (this.moveDetector.shouldPromptUser(move)) {
361
- // Instead of creating a persistent loop, perform delete+create semantics
362
- // so the working tree converges even without auto-apply.
363
- result.warnings.push(`Potential move detected: ${this.moveDetector.formatMove(move)} (${Math.round(move.similarity * 100)}% similar)`);
278
+ catch (error) {
279
+ result.errors.push({
280
+ path: move.fromPath,
281
+ operation: "move",
282
+ error: error,
283
+ recoverable: true,
284
+ });
364
285
  }
365
286
  }
366
287
  // Process local changes
@@ -368,7 +289,7 @@ class SyncEngine {
368
289
  c.changeType === types_1.ChangeType.BOTH_CHANGED);
369
290
  for (const change of localChanges) {
370
291
  try {
371
- await this.applyLocalChangeToRemote(change, snapshot, dryRun);
292
+ await this.applyLocalChangeToRemote(change, snapshot);
372
293
  result.filesChanged++;
373
294
  }
374
295
  catch (error) {
@@ -385,7 +306,7 @@ class SyncEngine {
385
306
  /**
386
307
  * Phase 2: Pull remote changes to local filesystem
387
308
  */
388
- async pullRemoteChanges(changes, snapshot, dryRun) {
309
+ async pullRemoteChanges(changes, snapshot) {
389
310
  const result = {
390
311
  success: true,
391
312
  filesChanged: 0,
@@ -400,7 +321,7 @@ class SyncEngine {
400
321
  const sortedChanges = this.sortChangesByDependency(remoteChanges);
401
322
  for (const change of sortedChanges) {
402
323
  try {
403
- await this.applyRemoteChangeToLocal(change, snapshot, dryRun);
324
+ await this.applyRemoteChangeToLocal(change, snapshot);
404
325
  result.filesChanged++;
405
326
  }
406
327
  catch (error) {
@@ -417,33 +338,29 @@ class SyncEngine {
417
338
  /**
418
339
  * Apply local file change to remote Automerge document
419
340
  */
420
- async applyLocalChangeToRemote(change, snapshot, dryRun) {
341
+ async applyLocalChangeToRemote(change, snapshot) {
421
342
  const snapshotEntry = snapshot.files.get(change.path);
422
343
  // CRITICAL: Check for null explicitly, not falsy values
423
344
  // Empty strings "" and empty Uint8Array are valid file content!
424
345
  if (change.localContent === null) {
425
346
  // File was deleted locally
426
347
  if (snapshotEntry) {
427
- console.log(`🗑️ ${change.path}`);
428
- await this.deleteRemoteFile(snapshotEntry.url, dryRun, snapshot, change.path);
348
+ await this.deleteRemoteFile(snapshotEntry.url, snapshot, change.path);
429
349
  // Remove from directory document
430
- await this.removeFileFromDirectory(snapshot, change.path, dryRun);
431
- if (!dryRun) {
432
- this.snapshotManager.removeFileEntry(snapshot, change.path);
433
- }
350
+ await this.removeFileFromDirectory(snapshot, change.path);
351
+ this.snapshotManager.removeFileEntry(snapshot, change.path);
434
352
  }
435
353
  return;
436
354
  }
437
355
  if (!snapshotEntry) {
438
356
  // New file
439
- console.log(`➕ ${change.path}`);
440
- const handle = await this.createRemoteFile(change, dryRun);
441
- if (!dryRun && handle) {
442
- 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);
443
360
  // CRITICAL FIX: Update snapshot with heads AFTER adding to directory
444
361
  // The addFileToDirectory call above may have changed the document heads
445
362
  this.snapshotManager.updateFileEntry(snapshot, change.path, {
446
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + change.path),
363
+ path: (0, utils_1.joinAndNormalizePath)(this.rootPath, change.path),
447
364
  url: handle.url,
448
365
  head: handle.heads(),
449
366
  extension: (0, utils_1.getFileExtension)(change.path),
@@ -453,15 +370,14 @@ class SyncEngine {
453
370
  }
454
371
  else {
455
372
  // Update existing file
456
- console.log(`📝 ${change.path}`);
457
- await this.updateRemoteFile(snapshotEntry.url, change.localContent, dryRun, snapshot, change.path);
373
+ await this.updateRemoteFile(snapshotEntry.url, change.localContent, snapshot, change.path);
458
374
  }
459
375
  }
460
376
  /**
461
377
  * Apply remote change to local filesystem
462
378
  */
463
- async applyRemoteChangeToLocal(change, snapshot, dryRun) {
464
- 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);
465
381
  if (!change.remoteHead) {
466
382
  throw new Error(`No remote head found for remote change to ${change.path}`);
467
383
  }
@@ -469,130 +385,114 @@ class SyncEngine {
469
385
  // Empty strings "" and empty Uint8Array are valid file content!
470
386
  if (change.remoteContent === null) {
471
387
  // File was deleted remotely
472
- console.log(`🗑️ ${change.path}`);
473
- if (!dryRun) {
474
- await (0, utils_1.removePath)(localPath);
475
- this.snapshotManager.removeFileEntry(snapshot, change.path);
476
- }
388
+ await (0, utils_1.removePath)(localPath);
389
+ this.snapshotManager.removeFileEntry(snapshot, change.path);
477
390
  return;
478
391
  }
479
392
  // Create or update local file
480
- if (change.changeType === types_1.ChangeType.REMOTE_ONLY) {
481
- console.log(`⬇️ ${change.path}`);
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;
482
399
  }
483
400
  else {
484
- console.log(`🔀 ${change.path}`);
485
- }
486
- if (!dryRun) {
487
- await (0, utils_1.writeFileContent)(localPath, change.remoteContent);
488
- // Update or create snapshot entry for this file
489
- const snapshotEntry = snapshot.files.get(change.path);
490
- if (snapshotEntry) {
491
- // Update existing entry
492
- snapshotEntry.head = change.remoteHead;
493
- }
494
- else {
495
- // Create new snapshot entry for newly discovered remote file
496
- // We need to find the remote file's URL from the directory hierarchy
497
- if (snapshot.rootDirectoryUrl) {
498
- try {
499
- const fileEntry = await this.findFileInDirectoryHierarchy(snapshot.rootDirectoryUrl, change.path);
500
- if (fileEntry) {
501
- this.snapshotManager.updateFileEntry(snapshot, change.path, {
502
- path: localPath,
503
- url: fileEntry.url,
504
- head: change.remoteHead,
505
- extension: (0, utils_1.getFileExtension)(change.path),
506
- mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
507
- });
508
- }
509
- }
510
- catch (error) {
511
- console.warn(`Failed to update snapshot for remote file ${change.path}: ${error}`);
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
+ });
512
414
  }
513
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
+ }
514
420
  }
515
421
  }
516
422
  }
517
423
  /**
518
424
  * Apply move to remote documents
519
425
  */
520
- async applyMoveToRemote(move, snapshot, dryRun) {
426
+ async applyMoveToRemote(move, snapshot) {
521
427
  const fromEntry = snapshot.files.get(move.fromPath);
522
428
  if (!fromEntry)
523
429
  return;
524
430
  // Parse paths
525
- const fromParts = move.fromPath.split("/");
526
- const fromFileName = fromParts.pop() || "";
527
- const fromDirPath = fromParts.join("/");
528
431
  const toParts = move.toPath.split("/");
529
432
  const toFileName = toParts.pop() || "";
530
433
  const toDirPath = toParts.join("/");
531
- if (!dryRun) {
532
- // 1) Remove file entry from old directory document
533
- if (move.fromPath !== move.toPath) {
534
- await this.removeFileFromDirectory(snapshot, move.fromPath, dryRun);
535
- }
536
- // 2) Ensure destination directory document exists and add file entry there
537
- const destDirUrl = await this.ensureDirectoryDocument(snapshot, toDirPath, dryRun);
538
- await this.addFileToDirectory(snapshot, move.toPath, fromEntry.url, dryRun);
539
- // 3) Update the FileDocument name and content to match new location/state
540
- try {
541
- const handle = await this.repo.find(fromEntry.url);
542
- const heads = fromEntry.head;
543
- // Update both name and content (if content changed during move)
544
- if (heads && heads.length > 0) {
545
- handle.changeAt(heads, (doc) => {
546
- doc.name = toFileName;
547
- // If new content is provided, update it (handles move + modification case)
548
- if (move.newContent !== undefined) {
549
- const isText = this.isTextContent(move.newContent);
550
- if (isText && typeof move.newContent === "string") {
551
- (0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
552
- }
553
- else {
554
- doc.content = move.newContent;
555
- }
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);
556
453
  }
557
- });
558
- }
559
- else {
560
- handle.change((doc) => {
561
- doc.name = toFileName;
562
- // If new content is provided, update it (handles move + modification case)
563
- if (move.newContent !== undefined) {
564
- const isText = this.isTextContent(move.newContent);
565
- if (isText && typeof move.newContent === "string") {
566
- (0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
567
- }
568
- else {
569
- doc.content = move.newContent;
570
- }
454
+ else {
455
+ doc.content = move.newContent;
571
456
  }
572
- });
573
- }
574
- // Track file handle for network sync
575
- this.handlesToWaitOn.push(handle);
457
+ }
458
+ });
576
459
  }
577
- catch (e) {
578
- 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
+ });
579
473
  }
580
- // 4) Update snapshot entries
581
- this.snapshotManager.removeFileEntry(snapshot, move.fromPath);
582
- this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
583
- ...fromEntry,
584
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + move.toPath),
585
- head: fromEntry.head, // will be updated later when heads advance
586
- });
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);
587
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
+ });
588
488
  }
589
489
  /**
590
490
  * Create new remote file document
591
491
  */
592
- async createRemoteFile(change, dryRun) {
492
+ async createRemoteFile(change) {
593
493
  // CRITICAL: Check for null explicitly, not falsy values
594
494
  // Empty strings "" and empty Uint8Array are valid file content!
595
- if (dryRun || change.localContent === null)
495
+ if (change.localContent === null)
596
496
  return null;
597
497
  const isText = this.isTextContent(change.localContent);
598
498
  // Create initial document structure
@@ -601,16 +501,20 @@ class SyncEngine {
601
501
  name: change.path.split("/").pop() || "",
602
502
  extension: (0, utils_1.getFileExtension)(change.path),
603
503
  mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
604
- 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
605
509
  metadata: {
606
510
  permissions: 0o644,
607
511
  },
608
512
  };
609
513
  const handle = this.repo.create(fileDoc);
610
- // For text files, use updateText to set the content properly
514
+ // For text files, use ImmutableString for better performance
611
515
  if (isText && typeof change.localContent === "string") {
612
516
  handle.change((doc) => {
613
- (0, automerge_repo_1.updateText)(doc, ["content"], change.localContent);
517
+ doc.content = new A.ImmutableString(change.localContent);
614
518
  });
615
519
  }
616
520
  // Always track newly created files for network sync
@@ -621,9 +525,7 @@ class SyncEngine {
621
525
  /**
622
526
  * Update existing remote file document
623
527
  */
624
- async updateRemoteFile(url, content, dryRun, snapshot, filePath) {
625
- if (dryRun)
626
- return;
528
+ async updateRemoteFile(url, content, snapshot, filePath) {
627
529
  const handle = await this.repo.find(url);
628
530
  // Check if content actually changed before tracking for sync
629
531
  const doc = await handle.doc();
@@ -642,7 +544,6 @@ class SyncEngine {
642
544
  if (!contentChanged) {
643
545
  // Content is identical, but we've updated the snapshot heads above
644
546
  // This prevents fresh change detection from seeing stale heads
645
- console.log(`🔍 Content is identical, but we've updated the snapshot heads above`);
646
547
  return;
647
548
  }
648
549
  const heads = snapshotEntry?.head;
@@ -650,16 +551,15 @@ class SyncEngine {
650
551
  throw new Error(`No heads found for ${url}`);
651
552
  }
652
553
  handle.changeAt(heads, (doc) => {
653
- const isText = this.isTextContent(content);
654
- if (isText && typeof content === "string") {
655
- (0, automerge_repo_1.updateText)(doc, ["content"], content);
554
+ if (typeof content === "string") {
555
+ doc.content = new A.ImmutableString(content);
656
556
  }
657
557
  else {
658
558
  doc.content = content;
659
559
  }
660
560
  });
661
561
  // Update snapshot with new heads after content change
662
- if (!dryRun && snapshotEntry) {
562
+ if (snapshotEntry) {
663
563
  snapshot.files.set(filePath, {
664
564
  ...snapshotEntry,
665
565
  head: handle.heads(),
@@ -671,9 +571,7 @@ class SyncEngine {
671
571
  /**
672
572
  * Delete remote file document
673
573
  */
674
- async deleteRemoteFile(url, dryRun, snapshot, filePath) {
675
- if (dryRun)
676
- return;
574
+ async deleteRemoteFile(url, snapshot, filePath) {
677
575
  // In Automerge, we don't actually delete documents
678
576
  // They become orphaned and will be garbage collected
679
577
  // For now, we just mark them as deleted by clearing content
@@ -685,27 +583,26 @@ class SyncEngine {
685
583
  }
686
584
  if (heads) {
687
585
  handle.changeAt(heads, (doc) => {
688
- doc.content = "";
586
+ doc.content = new A.ImmutableString("");
689
587
  });
690
588
  }
691
589
  else {
692
590
  handle.change((doc) => {
693
- doc.content = "";
591
+ doc.content = new A.ImmutableString("");
694
592
  });
695
593
  }
696
594
  }
697
595
  /**
698
596
  * Add file entry to appropriate directory document (maintains hierarchy)
699
597
  */
700
- async addFileToDirectory(snapshot, filePath, fileUrl, dryRun) {
701
- if (dryRun || !snapshot.rootDirectoryUrl)
598
+ async addFileToDirectory(snapshot, filePath, fileUrl) {
599
+ if (!snapshot.rootDirectoryUrl)
702
600
  return;
703
601
  const pathParts = filePath.split("/");
704
602
  const fileName = pathParts.pop() || "";
705
603
  const directoryPath = pathParts.join("/");
706
604
  // Get or create the parent directory document
707
- const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath, dryRun);
708
- console.log(`🔗 Adding ${fileName} (${fileUrl}) to directory ${parentDirUrl} (path: ${directoryPath})`);
605
+ const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath);
709
606
  const dirHandle = await this.repo.find(parentDirUrl);
710
607
  let didChange = false;
711
608
  const snapshotEntry = snapshot.directories.get(directoryPath);
@@ -749,7 +646,7 @@ class SyncEngine {
749
646
  * Ensure directory document exists for the given path, creating hierarchy as needed
750
647
  * First checks for existing shared directories before creating new ones
751
648
  */
752
- async ensureDirectoryDocument(snapshot, directoryPath, dryRun) {
649
+ async ensureDirectoryDocument(snapshot, directoryPath) {
753
650
  // Root directory case
754
651
  if (!directoryPath || directoryPath === "") {
755
652
  return snapshot.rootDirectoryUrl;
@@ -764,7 +661,7 @@ class SyncEngine {
764
661
  const currentDirName = pathParts.pop() || "";
765
662
  const parentPath = pathParts.join("/");
766
663
  // Ensure parent directory exists first (recursive)
767
- const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath, dryRun);
664
+ const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath);
768
665
  // DISCOVERY: Check if directory already exists in parent on server
769
666
  try {
770
667
  const parentHandle = await this.repo.find(parentDirUrl);
@@ -778,25 +675,22 @@ class SyncEngine {
778
675
  const childDirHandle = await this.repo.find(existingDirEntry.url);
779
676
  const childHeads = childDirHandle.heads();
780
677
  // Update snapshot with discovered directory using validated heads
781
- if (!dryRun) {
782
- this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
783
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + directoryPath),
784
- url: existingDirEntry.url,
785
- head: childHeads,
786
- entries: [],
787
- });
788
- }
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
+ });
789
684
  return existingDirEntry.url;
790
685
  }
791
686
  catch (resolveErr) {
792
- console.warn(`Failed to resolve child directory ${currentDirName} at ${directoryPath}: ${resolveErr}`);
793
- // Fall through to create a fresh directory document
687
+ // Failed to resolve directory - fall through to create a fresh directory document
794
688
  }
795
689
  }
796
690
  }
797
691
  }
798
692
  catch (error) {
799
- console.warn(`Failed to check for existing directory ${currentDirName}: ${error}`);
693
+ // Failed to check for existing directory - will create new one
800
694
  }
801
695
  // CREATE: Directory doesn't exist, create new one
802
696
  const dirDoc = {
@@ -820,32 +714,30 @@ class SyncEngine {
820
714
  }
821
715
  });
822
716
  // Track directory handles for sync
823
- if (!dryRun) {
824
- this.handlesToWaitOn.push(dirHandle);
825
- if (didChange) {
826
- this.handlesToWaitOn.push(parentHandle);
827
- // CRITICAL FIX: Update parent directory heads in snapshot immediately
828
- // This prevents stale head issues when parent directory is modified
829
- const parentSnapshotEntry = snapshot.directories.get(parentPath);
830
- if (parentSnapshotEntry) {
831
- parentSnapshotEntry.head = parentHandle.heads();
832
- }
833
- }
834
- // Update snapshot with new directory
835
- this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
836
- path: (0, utils_1.normalizePath)(this.rootPath + "/" + directoryPath),
837
- url: dirHandle.url,
838
- head: dirHandle.heads(),
839
- entries: [],
840
- });
841
- }
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
+ });
842
734
  return dirHandle.url;
843
735
  }
844
736
  /**
845
737
  * Remove file entry from directory document
846
738
  */
847
- async removeFileFromDirectory(snapshot, filePath, dryRun) {
848
- if (dryRun || !snapshot.rootDirectoryUrl)
739
+ async removeFileFromDirectory(snapshot, filePath) {
740
+ if (!snapshot.rootDirectoryUrl)
849
741
  return;
850
742
  const pathParts = filePath.split("/");
851
743
  const fileName = pathParts.pop() || "";
@@ -858,7 +750,7 @@ class SyncEngine {
858
750
  else {
859
751
  const existingDir = snapshot.directories.get(directoryPath);
860
752
  if (!existingDir) {
861
- console.warn(`Directory ${directoryPath} not found in snapshot for file removal`);
753
+ // Directory not found - file may already be removed
862
754
  return;
863
755
  }
864
756
  parentDirUrl = existingDir.url;
@@ -876,7 +768,7 @@ class SyncEngine {
876
768
  if (indexToRemove !== -1) {
877
769
  doc.docs.splice(indexToRemove, 1);
878
770
  didChange = true;
879
- console.log(`🗑️ Removed ${fileName} from directory ${directoryPath || "root"}`);
771
+ output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
880
772
  }
881
773
  });
882
774
  }
@@ -886,7 +778,7 @@ class SyncEngine {
886
778
  if (indexToRemove !== -1) {
887
779
  doc.docs.splice(indexToRemove, 1);
888
780
  didChange = true;
889
- console.log(`🗑️ Removed ${fileName} from directory ${directoryPath || "root"}`);
781
+ output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
890
782
  }
891
783
  });
892
784
  }
@@ -897,43 +789,10 @@ class SyncEngine {
897
789
  }
898
790
  }
899
791
  catch (error) {
900
- console.warn(`Failed to remove ${fileName} from directory ${directoryPath || "root"}: ${error}`);
792
+ // Failed to remove from directory - re-throw for caller to handle
901
793
  throw error;
902
794
  }
903
795
  }
904
- /**
905
- * Find a file in the directory hierarchy by path
906
- */
907
- async findFileInDirectoryHierarchy(directoryUrl, filePath) {
908
- try {
909
- const pathParts = filePath.split("/");
910
- let currentDirUrl = directoryUrl;
911
- // Navigate through directories to find the parent directory
912
- for (let i = 0; i < pathParts.length - 1; i++) {
913
- const dirName = pathParts[i];
914
- const dirHandle = await this.repo.find(currentDirUrl);
915
- const dirDoc = await dirHandle.doc();
916
- if (!dirDoc)
917
- return null;
918
- const subDirEntry = dirDoc.docs.find((entry) => entry.name === dirName && entry.type === "folder");
919
- if (!subDirEntry)
920
- return null;
921
- currentDirUrl = subDirEntry.url;
922
- }
923
- // Now look for the file in the final directory
924
- const fileName = pathParts[pathParts.length - 1];
925
- const finalDirHandle = await this.repo.find(currentDirUrl);
926
- const finalDirDoc = await finalDirHandle.doc();
927
- if (!finalDirDoc)
928
- return null;
929
- const fileEntry = finalDirDoc.docs.find((entry) => entry.name === fileName && entry.type === "file");
930
- return fileEntry || null;
931
- }
932
- catch (error) {
933
- console.warn(`Failed to find file ${filePath} in directory hierarchy: ${error}`);
934
- return null;
935
- }
936
- }
937
796
  /**
938
797
  * Sort changes by dependency order
939
798
  */
@@ -979,7 +838,7 @@ class SyncEngine {
979
838
  };
980
839
  }
981
840
  const changes = await this.changeDetector.detectChanges(snapshot);
982
- const { moves } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
841
+ const { moves } = await this.moveDetector.detectMoves(changes, snapshot);
983
842
  const summary = this.generateChangeSummary(changes, moves);
984
843
  return { changes, moves, summary };
985
844
  }
@@ -1013,8 +872,8 @@ class SyncEngine {
1013
872
  /**
1014
873
  * Update the lastSyncAt timestamp on the root directory document
1015
874
  */
1016
- async touchRootDirectory(snapshot, dryRun) {
1017
- if (dryRun || !snapshot.rootDirectoryUrl) {
875
+ async touchRootDirectory(snapshot) {
876
+ if (!snapshot.rootDirectoryUrl) {
1018
877
  return;
1019
878
  }
1020
879
  try {
@@ -1039,10 +898,9 @@ class SyncEngine {
1039
898
  if (snapshotEntry) {
1040
899
  snapshotEntry.head = rootHandle.heads();
1041
900
  }
1042
- console.log(`🕒 Updated root directory lastSyncAt to ${new Date(timestamp).toISOString()}`);
1043
901
  }
1044
902
  catch (error) {
1045
- console.warn(`Failed to update root directory lastSyncAt: ${error}`);
903
+ // Failed to update root directory timestamp
1046
904
  }
1047
905
  }
1048
906
  }