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
@@ -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,68 +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
- // 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
- );
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
+ );
180
180
 
181
- if (!stillExistsInDirectory) {
182
- // File was removed from remote directory listing
183
- const localContent = await this.getLocalContent(relativePath);
181
+ if (!stillExistsInDirectory) {
182
+ // File was removed from remote directory listing
183
+ const localContent = await this.getLocalContent(relativePath);
184
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
- continue;
199
- }
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
200
 
201
- const currentRemoteHead = await this.getCurrentRemoteHead(
202
- snapshotEntry.url
203
- );
201
+ const currentRemoteHead = await this.getCurrentRemoteHead(
202
+ snapshotEntry.url
203
+ );
204
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
- ? !this.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
- }
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
+ );
235
237
 
236
238
  return changes;
237
239
  }
@@ -259,7 +261,7 @@ export class ChangeDetector {
259
261
  changes
260
262
  );
261
263
  } catch (error) {
262
- console.warn(`❌ Failed to discover remote documents: ${error}`);
264
+ out.taskLine(`Failed to discover remote documents: ${error}`, true);
263
265
  }
264
266
 
265
267
  return changes;
@@ -341,7 +343,7 @@ export class ChangeDetector {
341
343
  }
342
344
  }
343
345
  } catch (error) {
344
- console.warn(`❌ Failed to process directory ${currentPath}: ${error}`);
346
+ out.taskLine(`Failed to process directory: ${error}`, true);
345
347
  }
346
348
  }
347
349
 
@@ -363,8 +365,12 @@ export class ChangeDetector {
363
365
  this.excludePatterns
364
366
  );
365
367
 
366
- for (const entry of entries) {
367
- 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) => {
368
374
  const relativePath = getRelativePath(this.rootPath, entry.path);
369
375
  const content = await readFileContent(entry.path);
370
376
 
@@ -372,10 +378,17 @@ export class ChangeDetector {
372
378
  content,
373
379
  type: entry.type,
374
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);
375
390
  }
376
391
  }
377
- } catch (error) {
378
- console.warn(`Failed to scan filesystem: ${error}`);
379
392
  }
380
393
 
381
394
  return fileMap;
@@ -388,7 +401,7 @@ export class ChangeDetector {
388
401
  relativePath: string
389
402
  ): Promise<string | Uint8Array | null> {
390
403
  try {
391
- const fullPath = normalizePath(this.rootPath + "/" + relativePath);
404
+ const fullPath = joinAndNormalizePath(this.rootPath, relativePath);
392
405
  return await readFileContent(fullPath);
393
406
  } catch {
394
407
  return null;
@@ -404,7 +417,13 @@ export class ChangeDetector {
404
417
  ): Promise<string | Uint8Array | null> {
405
418
  const handle = await this.repo.find<FileDocument>(url);
406
419
  const doc = await handle.view(heads).doc();
407
- 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;
408
427
  }
409
428
 
410
429
  /**
@@ -420,11 +439,14 @@ export class ChangeDetector {
420
439
  if (!doc) return null;
421
440
 
422
441
  const fileDoc = doc as FileDocument;
423
- 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;
424
448
  } catch (error) {
425
- console.warn(
426
- `❌ Failed to get current remote content for ${url}: ${error}`
427
- );
449
+ out.taskLine(`Failed to get remote content: ${error}`, true);
428
450
  return null;
429
451
  }
430
452
  }
@@ -433,7 +455,8 @@ export class ChangeDetector {
433
455
  * Get current head of Automerge document
434
456
  */
435
457
  private async getCurrentRemoteHead(url: AutomergeUrl): Promise<UrlHeads> {
436
- return (await this.repo.find<FileDocument>(url)).heads();
458
+ const handle = await this.repo.find<FileDocument>(url);
459
+ return handle.heads();
437
460
  }
438
461
 
439
462
  /**
@@ -451,35 +474,6 @@ export class ChangeDetector {
451
474
  }
452
475
  }
453
476
 
454
- /**
455
- * Compare two content pieces for equality
456
- */
457
- private isContentEqual(
458
- content1: string | Uint8Array | null,
459
- content2: string | Uint8Array | null
460
- ): boolean {
461
- if (content1 === content2) return true;
462
- if (!content1 || !content2) return false;
463
-
464
- if (typeof content1 !== typeof content2) return false;
465
-
466
- if (typeof content1 === "string") {
467
- return content1 === content2;
468
- } else {
469
- // Compare Uint8Array
470
- const buf1 = content1 as Uint8Array;
471
- const buf2 = content2 as Uint8Array;
472
-
473
- if (buf1.length !== buf2.length) return false;
474
-
475
- for (let i = 0; i < buf1.length; i++) {
476
- if (buf1[i] !== buf2[i]) return false;
477
- }
478
-
479
- return true;
480
- }
481
- }
482
-
483
477
  /**
484
478
  * Classify change type for a path
485
479
  */
@@ -504,9 +498,9 @@ export class ChangeDetector {
504
498
  );
505
499
 
506
500
  const localChanged = localContent
507
- ? !this.isContentEqual(localContent, lastKnownContent)
501
+ ? !isContentEqual(localContent, lastKnownContent)
508
502
  : true;
509
- const remoteChanged = !this.isContentEqual(
503
+ const remoteChanged = !isContentEqual(
510
504
  lastKnownContent,
511
505
  currentRemoteContent
512
506
  );
@@ -530,63 +524,11 @@ export class ChangeDetector {
530
524
  filePath: string
531
525
  ): Promise<boolean> {
532
526
  if (!rootDirectoryUrl) return false;
533
- const entry = await this.findFileInDirectoryHierarchy(
527
+ const entry = await findFileInDirectoryHierarchy(
528
+ this.repo,
534
529
  rootDirectoryUrl,
535
530
  filePath
536
531
  );
537
532
  return entry !== null;
538
533
  }
539
-
540
- /**
541
- * Find a file in the directory hierarchy by path
542
- */
543
- private async findFileInDirectoryHierarchy(
544
- directoryUrl: AutomergeUrl,
545
- filePath: string
546
- ): Promise<{ name: string; type: string; url: AutomergeUrl } | null> {
547
- try {
548
- const pathParts = filePath.split("/");
549
- let currentDirUrl = directoryUrl;
550
-
551
- // Navigate through directories to find the parent directory
552
- for (let i = 0; i < pathParts.length - 1; i++) {
553
- const dirName = pathParts[i];
554
- const dirHandle = await this.repo.find<DirectoryDocument>(
555
- currentDirUrl
556
- );
557
- const dirDoc = await dirHandle.doc();
558
-
559
- if (!dirDoc) return null;
560
-
561
- const subDirEntry = dirDoc.docs.find(
562
- (entry: { name: string; type: string; url: AutomergeUrl }) =>
563
- entry.name === dirName && entry.type === "folder"
564
- );
565
-
566
- if (!subDirEntry) return null;
567
- currentDirUrl = subDirEntry.url;
568
- }
569
-
570
- // Now look for the file in the final directory
571
- const fileName = pathParts[pathParts.length - 1];
572
- const finalDirHandle = await this.repo.find<DirectoryDocument>(
573
- currentDirUrl
574
- );
575
- const finalDirDoc = await finalDirHandle.doc();
576
-
577
- if (!finalDirDoc) return null;
578
-
579
- const fileEntry = finalDirDoc.docs.find(
580
- (entry: { name: string; type: string; url: AutomergeUrl }) =>
581
- entry.name === fileName && entry.type === "file"
582
- );
583
-
584
- return fileEntry || null;
585
- } catch (error) {
586
- console.warn(
587
- `Failed to find file ${filePath} in directory hierarchy: ${error}`
588
- );
589
- return null;
590
- }
591
- }
592
534
  }