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
@@ -4,34 +4,19 @@ import {
4
4
  ChangeType,
5
5
  FileType,
6
6
  SyncSnapshot,
7
- SnapshotFileEntry,
8
- SnapshotDirectoryEntry,
9
7
  FileDocument,
10
8
  DirectoryDocument,
9
+ DetectedChange,
11
10
  } from "../types";
12
11
  import {
13
12
  readFileContent,
14
- getFileSystemEntry,
15
13
  listDirectory,
16
14
  getRelativePath,
17
- normalizePath,
15
+ findFileInDirectoryHierarchy,
16
+ joinAndNormalizePath,
18
17
  } from "../utils";
19
-
20
- // Re-export ChangeType for other modules
21
- export { ChangeType } from "../types";
22
-
23
- /**
24
- * Represents a detected change
25
- */
26
- export interface DetectedChange {
27
- path: string;
28
- changeType: ChangeType;
29
- fileType: FileType;
30
- localContent: string | Uint8Array | null;
31
- remoteContent: string | Uint8Array | null;
32
- localHead?: UrlHeads;
33
- remoteHead?: UrlHeads;
34
- }
18
+ import { isContentEqual } from "../utils/content";
19
+ import { out } from "../utils/output";
35
20
 
36
21
  /**
37
22
  * Change detection engine
@@ -76,36 +61,82 @@ export class ChangeDetector {
76
61
  ): Promise<DetectedChange[]> {
77
62
  const changes: DetectedChange[] = [];
78
63
 
79
- // Check for new and modified files
80
- for (const [relativePath, fileInfo] of currentFiles.entries()) {
81
- const snapshotEntry = snapshot.files.get(relativePath);
82
-
83
- if (!snapshotEntry) {
84
- // New file
85
- changes.push({
86
- path: relativePath,
87
- changeType: ChangeType.LOCAL_ONLY,
88
- fileType: fileInfo.type,
89
- localContent: fileInfo.content,
90
- remoteContent: null,
91
- });
92
- } else {
93
- // Check if content changed
94
- const lastKnownContent = await this.getContentAtHead(
95
- snapshotEntry.url,
96
- snapshotEntry.head
97
- );
98
- const contentChanged = !this.isContentEqual(
99
- fileInfo.content,
100
- lastKnownContent
101
- );
102
-
103
- if (contentChanged) {
104
- // Check remote state too
64
+ // Check for new and modified files in parallel for better performance
65
+ await Promise.all(
66
+ Array.from(currentFiles.entries()).map(
67
+ async ([relativePath, fileInfo]) => {
68
+ const snapshotEntry = snapshot.files.get(relativePath);
69
+
70
+ if (!snapshotEntry) {
71
+ // New file
72
+ changes.push({
73
+ path: relativePath,
74
+ changeType: ChangeType.LOCAL_ONLY,
75
+ fileType: fileInfo.type,
76
+ localContent: fileInfo.content,
77
+ remoteContent: null,
78
+ });
79
+ } else {
80
+ // Check if content changed
81
+ const lastKnownContent = await this.getContentAtHead(
82
+ snapshotEntry.url,
83
+ snapshotEntry.head
84
+ );
85
+
86
+ const contentChanged = !isContentEqual(
87
+ fileInfo.content,
88
+ lastKnownContent
89
+ );
90
+
91
+ if (contentChanged) {
92
+ // Check remote state too
93
+ const currentRemoteContent = await this.getCurrentRemoteContent(
94
+ snapshotEntry.url
95
+ );
96
+
97
+ const remoteChanged = !isContentEqual(
98
+ lastKnownContent,
99
+ currentRemoteContent
100
+ );
101
+
102
+ const changeType = remoteChanged
103
+ ? ChangeType.BOTH_CHANGED
104
+ : ChangeType.LOCAL_ONLY;
105
+
106
+ const remoteHead = await this.getCurrentRemoteHead(
107
+ snapshotEntry.url
108
+ );
109
+
110
+ changes.push({
111
+ path: relativePath,
112
+ changeType,
113
+ fileType: fileInfo.type,
114
+ localContent: fileInfo.content,
115
+ remoteContent: currentRemoteContent,
116
+ localHead: snapshotEntry.head,
117
+ remoteHead,
118
+ });
119
+ }
120
+ }
121
+ }
122
+ )
123
+ );
124
+
125
+ // Check for deleted files in parallel
126
+ await Promise.all(
127
+ Array.from(snapshot.files.entries())
128
+ .filter(([relativePath]) => !currentFiles.has(relativePath))
129
+ .map(async ([relativePath, snapshotEntry]) => {
130
+ // File was deleted locally
105
131
  const currentRemoteContent = await this.getCurrentRemoteContent(
106
132
  snapshotEntry.url
107
133
  );
108
- const remoteChanged = !this.isContentEqual(
134
+ const lastKnownContent = await this.getContentAtHead(
135
+ snapshotEntry.url,
136
+ snapshotEntry.head
137
+ );
138
+
139
+ const remoteChanged = !isContentEqual(
109
140
  lastKnownContent,
110
141
  currentRemoteContent
111
142
  );
@@ -117,47 +148,14 @@ export class ChangeDetector {
117
148
  changes.push({
118
149
  path: relativePath,
119
150
  changeType,
120
- fileType: fileInfo.type,
121
- localContent: fileInfo.content,
151
+ fileType: FileType.TEXT, // Will be determined from document
152
+ localContent: null,
122
153
  remoteContent: currentRemoteContent,
123
154
  localHead: snapshotEntry.head,
124
155
  remoteHead: await this.getCurrentRemoteHead(snapshotEntry.url),
125
156
  });
126
- }
127
- }
128
- }
129
-
130
- // Check for deleted files
131
- for (const [relativePath, snapshotEntry] of snapshot.files.entries()) {
132
- if (!currentFiles.has(relativePath)) {
133
- // File was deleted locally
134
- const currentRemoteContent = await this.getCurrentRemoteContent(
135
- snapshotEntry.url
136
- );
137
- const lastKnownContent = await this.getContentAtHead(
138
- snapshotEntry.url,
139
- snapshotEntry.head
140
- );
141
- const remoteChanged = !this.isContentEqual(
142
- lastKnownContent,
143
- currentRemoteContent
144
- );
145
-
146
- const changeType = remoteChanged
147
- ? ChangeType.BOTH_CHANGED
148
- : ChangeType.LOCAL_ONLY;
149
-
150
- changes.push({
151
- path: relativePath,
152
- changeType,
153
- fileType: FileType.TEXT, // Will be determined from document
154
- localContent: null,
155
- remoteContent: currentRemoteContent,
156
- localHead: snapshotEntry.head,
157
- remoteHead: await this.getCurrentRemoteHead(snapshotEntry.url),
158
- });
159
- }
160
- }
157
+ })
158
+ );
161
159
 
162
160
  return changes;
163
161
  }
@@ -170,41 +168,72 @@ export class ChangeDetector {
170
168
  ): Promise<DetectedChange[]> {
171
169
  const changes: DetectedChange[] = [];
172
170
 
173
- for (const [relativePath, snapshotEntry] of snapshot.files.entries()) {
174
- const currentRemoteHead = await this.getCurrentRemoteHead(
175
- snapshotEntry.url
176
- );
171
+ await Promise.all(
172
+ Array.from(snapshot.files.entries()).map(
173
+ async ([relativePath, snapshotEntry]) => {
174
+ // CRITICAL FIX: Check if file still exists in remote directory listing
175
+ // Files can be removed from the directory without their document heads changing
176
+ const stillExistsInDirectory = await this.fileExistsInRemoteDirectory(
177
+ snapshot.rootDirectoryUrl,
178
+ relativePath
179
+ );
177
180
 
178
- if (!A.equals(currentRemoteHead, snapshotEntry.head)) {
179
- // Remote document has changed
180
- const currentRemoteContent = await this.getCurrentRemoteContent(
181
- snapshotEntry.url
182
- );
183
- const localContent = await this.getLocalContent(relativePath);
184
- const lastKnownContent = await this.getContentAtHead(
185
- snapshotEntry.url,
186
- snapshotEntry.head
187
- );
188
-
189
- const localChanged = localContent
190
- ? !this.isContentEqual(localContent, lastKnownContent)
191
- : false;
192
-
193
- const changeType = localChanged
194
- ? ChangeType.BOTH_CHANGED
195
- : ChangeType.REMOTE_ONLY;
196
-
197
- changes.push({
198
- path: relativePath,
199
- changeType,
200
- fileType: await this.getFileTypeFromContent(currentRemoteContent),
201
- localContent,
202
- remoteContent: currentRemoteContent,
203
- localHead: snapshotEntry.head,
204
- remoteHead: currentRemoteHead,
205
- });
206
- }
207
- }
181
+ if (!stillExistsInDirectory) {
182
+ // File was removed from remote directory listing
183
+ const localContent = await this.getLocalContent(relativePath);
184
+
185
+ // Only report as deleted if local file still exists
186
+ // (if local file is also deleted, detectLocalChanges handles it)
187
+ if (localContent !== null) {
188
+ changes.push({
189
+ path: relativePath,
190
+ changeType: ChangeType.REMOTE_ONLY,
191
+ fileType: FileType.TEXT,
192
+ localContent,
193
+ remoteContent: null, // File deleted remotely
194
+ localHead: snapshotEntry.head,
195
+ remoteHead: snapshotEntry.head,
196
+ });
197
+ }
198
+ return;
199
+ }
200
+
201
+ const currentRemoteHead = await this.getCurrentRemoteHead(
202
+ snapshotEntry.url
203
+ );
204
+
205
+ if (!A.equals(currentRemoteHead, snapshotEntry.head)) {
206
+ // Remote document has changed
207
+ const currentRemoteContent = await this.getCurrentRemoteContent(
208
+ snapshotEntry.url
209
+ );
210
+ const localContent = await this.getLocalContent(relativePath);
211
+ const lastKnownContent = await this.getContentAtHead(
212
+ snapshotEntry.url,
213
+ snapshotEntry.head
214
+ );
215
+
216
+ const localChanged = localContent
217
+ ? !isContentEqual(localContent, lastKnownContent)
218
+ : false;
219
+
220
+ const changeType = localChanged
221
+ ? ChangeType.BOTH_CHANGED
222
+ : ChangeType.REMOTE_ONLY;
223
+
224
+ changes.push({
225
+ path: relativePath,
226
+ changeType,
227
+ fileType: await this.getFileTypeFromContent(currentRemoteContent),
228
+ localContent,
229
+ remoteContent: currentRemoteContent,
230
+ localHead: snapshotEntry.head,
231
+ remoteHead: currentRemoteHead,
232
+ });
233
+ }
234
+ }
235
+ )
236
+ );
208
237
 
209
238
  return changes;
210
239
  }
@@ -232,7 +261,7 @@ export class ChangeDetector {
232
261
  changes
233
262
  );
234
263
  } catch (error) {
235
- console.warn(`❌ Failed to discover remote documents: ${error}`);
264
+ out.taskLine(`Failed to discover remote documents: ${error}`, true);
236
265
  }
237
266
 
238
267
  return changes;
@@ -314,7 +343,7 @@ export class ChangeDetector {
314
343
  }
315
344
  }
316
345
  } catch (error) {
317
- console.warn(`❌ Failed to process directory ${currentPath}: ${error}`);
346
+ out.taskLine(`Failed to process directory: ${error}`, true);
318
347
  }
319
348
  }
320
349
 
@@ -336,8 +365,12 @@ export class ChangeDetector {
336
365
  this.excludePatterns
337
366
  );
338
367
 
339
- for (const entry of entries) {
340
- if (entry.type !== FileType.DIRECTORY) {
368
+ const fileEntries = entries.filter(
369
+ (entry) => entry.type !== FileType.DIRECTORY
370
+ );
371
+
372
+ await Promise.all(
373
+ fileEntries.map(async (entry) => {
341
374
  const relativePath = getRelativePath(this.rootPath, entry.path);
342
375
  const content = await readFileContent(entry.path);
343
376
 
@@ -345,10 +378,17 @@ export class ChangeDetector {
345
378
  content,
346
379
  type: entry.type,
347
380
  });
381
+ })
382
+ );
383
+ } catch (error) {
384
+ out.taskLine(`Failed to scan filesystem: ${error}`, true);
385
+ // Log more details about the error
386
+ if (error instanceof Error) {
387
+ out.taskLine(`Error details: ${error.message}`, true);
388
+ if (error.stack) {
389
+ out.taskLine(`Stack: ${error.stack}`, true);
348
390
  }
349
391
  }
350
- } catch (error) {
351
- console.warn(`Failed to scan filesystem: ${error}`);
352
392
  }
353
393
 
354
394
  return fileMap;
@@ -361,7 +401,7 @@ export class ChangeDetector {
361
401
  relativePath: string
362
402
  ): Promise<string | Uint8Array | null> {
363
403
  try {
364
- const fullPath = normalizePath(this.rootPath + "/" + relativePath);
404
+ const fullPath = joinAndNormalizePath(this.rootPath, relativePath);
365
405
  return await readFileContent(fullPath);
366
406
  } catch {
367
407
  return null;
@@ -377,7 +417,13 @@ export class ChangeDetector {
377
417
  ): Promise<string | Uint8Array | null> {
378
418
  const handle = await this.repo.find<FileDocument>(url);
379
419
  const doc = await handle.view(heads).doc();
380
- return doc?.content as string | Uint8Array;
420
+
421
+ const content = (doc as FileDocument | undefined)?.content;
422
+ // Convert ImmutableString to regular string
423
+ if (A.isImmutableString(content)) {
424
+ return content.toString();
425
+ }
426
+ return content as string | Uint8Array;
381
427
  }
382
428
 
383
429
  /**
@@ -393,11 +439,14 @@ export class ChangeDetector {
393
439
  if (!doc) return null;
394
440
 
395
441
  const fileDoc = doc as FileDocument;
396
- return fileDoc.content as string | Uint8Array;
442
+ const content = fileDoc.content;
443
+ // Convert ImmutableString to regular string
444
+ if (A.isImmutableString(content)) {
445
+ return content.toString();
446
+ }
447
+ return content as string | Uint8Array;
397
448
  } catch (error) {
398
- console.warn(
399
- `❌ Failed to get current remote content for ${url}: ${error}`
400
- );
449
+ out.taskLine(`Failed to get remote content: ${error}`, true);
401
450
  return null;
402
451
  }
403
452
  }
@@ -406,7 +455,8 @@ export class ChangeDetector {
406
455
  * Get current head of Automerge document
407
456
  */
408
457
  private async getCurrentRemoteHead(url: AutomergeUrl): Promise<UrlHeads> {
409
- return (await this.repo.find<FileDocument>(url)).heads();
458
+ const handle = await this.repo.find<FileDocument>(url);
459
+ return handle.heads();
410
460
  }
411
461
 
412
462
  /**
@@ -424,35 +474,6 @@ export class ChangeDetector {
424
474
  }
425
475
  }
426
476
 
427
- /**
428
- * Compare two content pieces for equality
429
- */
430
- private isContentEqual(
431
- content1: string | Uint8Array | null,
432
- content2: string | Uint8Array | null
433
- ): boolean {
434
- if (content1 === content2) return true;
435
- if (!content1 || !content2) return false;
436
-
437
- if (typeof content1 !== typeof content2) return false;
438
-
439
- if (typeof content1 === "string") {
440
- return content1 === content2;
441
- } else {
442
- // Compare Uint8Array
443
- const buf1 = content1 as Uint8Array;
444
- const buf2 = content2 as Uint8Array;
445
-
446
- if (buf1.length !== buf2.length) return false;
447
-
448
- for (let i = 0; i < buf1.length; i++) {
449
- if (buf1[i] !== buf2[i]) return false;
450
- }
451
-
452
- return true;
453
- }
454
- }
455
-
456
477
  /**
457
478
  * Classify change type for a path
458
479
  */
@@ -477,9 +498,9 @@ export class ChangeDetector {
477
498
  );
478
499
 
479
500
  const localChanged = localContent
480
- ? !this.isContentEqual(localContent, lastKnownContent)
501
+ ? !isContentEqual(localContent, lastKnownContent)
481
502
  : true;
482
- const remoteChanged = !this.isContentEqual(
503
+ const remoteChanged = !isContentEqual(
483
504
  lastKnownContent,
484
505
  currentRemoteContent
485
506
  );
@@ -494,4 +515,20 @@ export class ChangeDetector {
494
515
  return ChangeType.BOTH_CHANGED;
495
516
  }
496
517
  }
518
+
519
+ /**
520
+ * Check if a file exists in the remote directory hierarchy
521
+ */
522
+ private async fileExistsInRemoteDirectory(
523
+ rootDirectoryUrl: AutomergeUrl | undefined,
524
+ filePath: string
525
+ ): Promise<boolean> {
526
+ if (!rootDirectoryUrl) return false;
527
+ const entry = await findFileInDirectoryHierarchy(
528
+ this.repo,
529
+ rootDirectoryUrl,
530
+ filePath
531
+ );
532
+ return entry !== null;
533
+ }
497
534
  }