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
@@ -1,7 +1,12 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import * as os from "os";
4
- import { GlobalConfig, DirectoryConfig } from "../types";
4
+ import {
5
+ GlobalConfig,
6
+ DirectoryConfig,
7
+ DEFAULT_SYNC_SERVER,
8
+ DEFAULT_SYNC_SERVER_STORAGE_ID,
9
+ } from "../types";
5
10
  import { pathExists, ensureDirectoryExists } from "../utils";
6
11
 
7
12
  /**
@@ -10,7 +15,8 @@ import { pathExists, ensureDirectoryExists } from "../utils";
10
15
  export class ConfigManager {
11
16
  private static readonly GLOBAL_CONFIG_DIR = ".pushwork";
12
17
  private static readonly CONFIG_FILENAME = "config.json";
13
- private static readonly LOCAL_CONFIG_DIR = ".pushwork";
18
+
19
+ static readonly CONFIG_DIR = ".pushwork";
14
20
 
15
21
  constructor(private workingDir?: string) {}
16
22
 
@@ -34,7 +40,7 @@ export class ConfigManager {
34
40
  }
35
41
  return path.join(
36
42
  this.workingDir,
37
- ConfigManager.LOCAL_CONFIG_DIR,
43
+ ConfigManager.CONFIG_DIR,
38
44
  ConfigManager.CONFIG_FILENAME
39
45
  );
40
46
  }
@@ -52,7 +58,7 @@ export class ConfigManager {
52
58
  const content = await fs.readFile(configPath, "utf8");
53
59
  return JSON.parse(content) as GlobalConfig;
54
60
  } catch (error) {
55
- console.warn(`Failed to load global config: ${error}`);
61
+ // Failed to load global config
56
62
  return null;
57
63
  }
58
64
  }
@@ -89,7 +95,7 @@ export class ConfigManager {
89
95
  const content = await fs.readFile(configPath, "utf8");
90
96
  return JSON.parse(content) as DirectoryConfig;
91
97
  } catch (error) {
92
- console.warn(`Failed to load local config: ${error}`);
98
+ // Failed to load local config
93
99
  return null;
94
100
  }
95
101
  }
@@ -113,34 +119,53 @@ export class ConfigManager {
113
119
  }
114
120
  }
115
121
 
122
+ private getDefaultGlobalConfig(): GlobalConfig {
123
+ return {
124
+ exclude_patterns: [
125
+ ".git",
126
+ "node_modules",
127
+ "*.tmp",
128
+ ".DS_Store",
129
+ ".pushwork",
130
+ ],
131
+ sync_server: DEFAULT_SYNC_SERVER,
132
+ sync_server_storage_id: DEFAULT_SYNC_SERVER_STORAGE_ID,
133
+ sync: {
134
+ move_detection_threshold: 0.7,
135
+ },
136
+ };
137
+ }
138
+
116
139
  /**
117
- * Get merged configuration (global + local)
140
+ * Get default configuration
118
141
  */
119
- async getMerged(): Promise<DirectoryConfig> {
120
- const globalConfig = await this.loadGlobal();
121
- const localConfig = await this.load();
122
-
123
- // Create default configuration
124
- const defaultConfig: DirectoryConfig = {
142
+ private getDefaultConfig(): DirectoryConfig {
143
+ return {
125
144
  sync_enabled: true,
126
- sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
127
- defaults: {
128
- exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
129
- large_file_threshold: "100MB",
130
- },
131
- diff: {
132
- show_binary: false,
133
- },
145
+ sync_server: DEFAULT_SYNC_SERVER,
146
+ sync_server_storage_id: DEFAULT_SYNC_SERVER_STORAGE_ID,
147
+ exclude_patterns: [
148
+ ".git",
149
+ "node_modules",
150
+ "*.tmp",
151
+ ".pushwork",
152
+ ".DS_Store",
153
+ ],
134
154
  sync: {
135
- move_detection_threshold: 0.8,
136
- prompt_threshold: 0.5,
137
- auto_sync: false,
138
- parallel_operations: 4,
155
+ move_detection_threshold: 0.7,
139
156
  },
140
157
  };
158
+ }
159
+
160
+ /**
161
+ * Get merged configuration (global + local)
162
+ */
163
+ async getMerged(): Promise<DirectoryConfig> {
164
+ const globalConfig = await this.loadGlobal();
165
+ const localConfig = await this.load();
141
166
 
142
167
  // Merge configurations: default < global < local
143
- let merged = { ...defaultConfig };
168
+ let merged = this.getDefaultConfig();
144
169
 
145
170
  if (globalConfig) {
146
171
  merged = this.mergeConfigs(merged, globalConfig);
@@ -153,6 +178,18 @@ export class ConfigManager {
153
178
  return merged;
154
179
  }
155
180
 
181
+ /**
182
+ * Initialize with CLI option overrides
183
+ * Creates a new config with defaults + CLI overrides and saves it
184
+ */
185
+ async initializeWithOverrides(
186
+ overrides: Partial<DirectoryConfig> = {}
187
+ ): Promise<DirectoryConfig> {
188
+ const config = this.mergeConfigs(this.getDefaultConfig(), overrides);
189
+ await this.save(config);
190
+ return config;
191
+ }
192
+
156
193
  /**
157
194
  * Merge two configuration objects
158
195
  */
@@ -179,25 +216,7 @@ export class ConfigManager {
179
216
 
180
217
  // Handle GlobalConfig structure
181
218
  if ("exclude_patterns" in override && override.exclude_patterns) {
182
- merged.defaults.exclude_patterns = override.exclude_patterns;
183
- }
184
-
185
- if ("large_file_threshold" in override && override.large_file_threshold) {
186
- merged.defaults.large_file_threshold = override.large_file_threshold;
187
- }
188
-
189
- // Handle DirectoryConfig structure
190
- if ("defaults" in override && override.defaults) {
191
- merged.defaults = { ...merged.defaults, ...override.defaults };
192
- }
193
-
194
- if ("diff" in override && override.diff) {
195
- // Merge diff settings, ensuring show_binary has a default
196
- merged.diff = {
197
- ...merged.diff,
198
- ...override.diff,
199
- show_binary: override.diff.show_binary ?? merged.diff.show_binary,
200
- };
219
+ merged.exclude_patterns = override.exclude_patterns;
201
220
  }
202
221
 
203
222
  if ("sync" in override && override.sync) {
@@ -211,28 +230,7 @@ export class ConfigManager {
211
230
  * Create default global configuration
212
231
  */
213
232
  async createDefaultGlobal(): Promise<void> {
214
- const defaultGlobal: GlobalConfig = {
215
- exclude_patterns: [
216
- ".git",
217
- "node_modules",
218
- "*.tmp",
219
- ".DS_Store",
220
- ".pushwork",
221
- ],
222
- large_file_threshold: "100MB",
223
- sync_server: "wss://sync3.automerge.org",
224
- sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
225
- diff: {
226
- show_binary: false,
227
- },
228
- sync: {
229
- move_detection_threshold: 0.8,
230
- prompt_threshold: 0.5,
231
- auto_sync: false,
232
- parallel_operations: 4,
233
- },
234
- };
235
-
233
+ const defaultGlobal = this.getDefaultGlobalConfig();
236
234
  await this.saveGlobal(defaultGlobal);
237
235
  }
238
236
 
@@ -252,7 +250,7 @@ export class ConfigManager {
252
250
  }
253
251
 
254
252
  /**
255
- * Get configuration value by path (e.g., 'sync.auto_sync')
253
+ * Get configuration value by path (e.g., 'sync.move_detection_threshold')
256
254
  */
257
255
  async getValue(keyPath: string): Promise<any> {
258
256
  const config = await this.getMerged();
@@ -311,21 +309,6 @@ export class ConfigManager {
311
309
  }
312
310
  }
313
311
 
314
- if (config.sync?.prompt_threshold !== undefined) {
315
- if (
316
- config.sync.prompt_threshold < 0 ||
317
- config.sync.prompt_threshold > 1
318
- ) {
319
- errors.push("prompt_threshold must be between 0 and 1");
320
- }
321
- }
322
-
323
- if (config.sync?.parallel_operations !== undefined) {
324
- if (config.sync.parallel_operations < 1) {
325
- errors.push("parallel_operations must be at least 1");
326
- }
327
- }
328
-
329
312
  return {
330
313
  valid: errors.length === 0,
331
314
  errors,
package/src/core/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Core sync components
2
1
  export * from "./snapshot";
3
2
  export * from "./change-detection";
4
3
  export * from "./move-detection";
5
4
  export * from "./sync-engine";
5
+ export * from "./config";
@@ -1,28 +1,25 @@
1
- import {
2
- SyncSnapshot,
3
- MoveCandidate,
4
- FileType,
5
- SnapshotFileEntry,
6
- } from "../types";
7
- import { ContentSimilarity, readFileContent, getRelativePath } from "../utils";
8
- import { DetectedChange, ChangeType } from "./change-detection";
1
+ import { SyncSnapshot, MoveCandidate } from "../types";
2
+ import { isTextFile } from "../utils";
3
+ import { stringSimilarity } from "../utils/string-similarity";
4
+ import { ChangeType, DetectedChange } from "../types";
9
5
 
10
6
  /**
11
- * Move detection engine
7
+ * Simplified move detection engine
12
8
  */
13
9
  export class MoveDetector {
14
- private static readonly AUTO_THRESHOLD = 0.8;
15
- private static readonly PROMPT_THRESHOLD = 0.5;
10
+ private readonly moveThreshold: number;
11
+
12
+ constructor(moveThreshold: number = 0.7) {
13
+ this.moveThreshold = moveThreshold;
14
+ }
16
15
 
17
16
  /**
18
17
  * Detect file moves by analyzing deleted and created files
19
18
  */
20
19
  async detectMoves(
21
20
  changes: DetectedChange[],
22
- snapshot: SyncSnapshot,
23
- rootPath: string
21
+ snapshot: SyncSnapshot
24
22
  ): Promise<{ moves: MoveCandidate[]; remainingChanges: DetectedChange[] }> {
25
- // Separate deletions and creations
26
23
  const deletedFiles = changes.filter(
27
24
  (c) => !c.localContent && c.changeType === ChangeType.LOCAL_ONLY
28
25
  );
@@ -43,28 +40,22 @@ export class MoveDetector {
43
40
 
44
41
  // Find potential moves by comparing content
45
42
  for (const deletedFile of deletedFiles) {
46
- const deletedContent = await this.getDeletedFileContent(
47
- deletedFile,
48
- snapshot
49
- );
50
- // CRITICAL: Check for null explicitly, not falsy values
51
- // Empty strings "" are valid file content!
43
+ const deletedContent = deletedFile.remoteContent;
52
44
  if (deletedContent === null) continue;
53
45
 
54
46
  let bestMatch: { file: DetectedChange; similarity: number } | null = null;
55
47
 
56
48
  for (const createdFile of createdFiles) {
57
49
  if (usedCreations.has(createdFile.path)) continue;
58
- // CRITICAL: Check for null explicitly, not falsy values
59
- // Empty strings "" are valid file content!
60
50
  if (createdFile.localContent === null) continue;
61
51
 
62
- const similarity = await ContentSimilarity.calculateSimilarity(
52
+ const similarity = await this.calculateSimilarity(
63
53
  deletedContent,
64
- createdFile.localContent
54
+ createdFile.localContent,
55
+ deletedFile.path
65
56
  );
66
57
 
67
- if (similarity >= MoveDetector.PROMPT_THRESHOLD) {
58
+ if (similarity >= this.moveThreshold) {
68
59
  if (!bestMatch || similarity > bestMatch.similarity) {
69
60
  bestMatch = { file: createdFile, similarity };
70
61
  }
@@ -72,31 +63,20 @@ export class MoveDetector {
72
63
  }
73
64
 
74
65
  if (bestMatch) {
75
- const confidence = ContentSimilarity.getConfidenceLevel(
76
- bestMatch.similarity
77
- );
78
-
79
- // Always report the potential move (for logging/prompting)
66
+ // If we detected a move above threshold, we apply it
80
67
  moves.push({
81
68
  fromPath: deletedFile.path,
82
69
  toPath: bestMatch.file.path,
83
70
  similarity: bestMatch.similarity,
84
- confidence,
85
- // Capture new content (may include modifications)
86
71
  newContent: bestMatch.file.localContent || undefined,
87
72
  });
88
73
 
89
- // Only consume the deletion/creation pair when we would auto-apply the move.
90
- // If we only want to prompt, leave the original changes in place so
91
- // sync can still proceed as delete+create (avoids infinite warning loop).
92
- if (bestMatch.similarity >= MoveDetector.AUTO_THRESHOLD) {
93
- usedCreations.add(bestMatch.file.path);
94
- usedDeletions.add(deletedFile.path);
95
- }
74
+ // Consume the deletion and creation (move replaces both)
75
+ usedCreations.add(bestMatch.file.path);
76
+ usedDeletions.add(deletedFile.path);
96
77
  }
97
78
  }
98
79
 
99
- // Filter out changes that are part of moves
100
80
  const remainingChanges = changes.filter(
101
81
  (change) =>
102
82
  !usedCreations.has(change.path) && !usedDeletions.has(change.path)
@@ -106,145 +86,77 @@ export class MoveDetector {
106
86
  }
107
87
 
108
88
  /**
109
- * Get content of a deleted file from snapshot
110
- */
111
- private async getDeletedFileContent(
112
- deletedFile: DetectedChange,
113
- snapshot: SyncSnapshot
114
- ): Promise<string | Uint8Array | null> {
115
- const snapshotEntry = snapshot.files.get(deletedFile.path);
116
- if (!snapshotEntry) return null;
117
-
118
- // Return remote content if available, otherwise null
119
- return deletedFile.remoteContent || null;
120
- }
121
-
122
- /**
123
- * Group moves by confidence level
89
+ * Calculate similarity between two content pieces
90
+ * Optimized for speed while maintaining accuracy
124
91
  */
125
- groupMovesByConfidence(moves: MoveCandidate[]): {
126
- autoMoves: MoveCandidate[];
127
- promptMoves: MoveCandidate[];
128
- lowConfidenceMoves: MoveCandidate[];
129
- } {
130
- const autoMoves = moves.filter((m) => m.confidence === "auto");
131
- const promptMoves = moves.filter((m) => m.confidence === "prompt");
132
- const lowConfidenceMoves = moves.filter((m) => m.confidence === "low");
133
-
134
- return { autoMoves, promptMoves, lowConfidenceMoves };
135
- }
136
-
137
- /**
138
- * Validate move candidates to avoid conflicts
139
- */
140
- validateMoves(moves: MoveCandidate[]): {
141
- validMoves: MoveCandidate[];
142
- conflicts: Array<{ moves: MoveCandidate[]; reason: string }>;
143
- } {
144
- const validMoves: MoveCandidate[] = [];
145
- const conflicts: Array<{ moves: MoveCandidate[]; reason: string }> = [];
146
-
147
- // Check for multiple sources mapping to same destination
148
- const destinationMap = new Map<string, MoveCandidate[]>();
149
- for (const move of moves) {
150
- if (!destinationMap.has(move.toPath)) {
151
- destinationMap.set(move.toPath, []);
152
- }
153
- destinationMap.get(move.toPath)!.push(move);
154
- }
155
-
156
- // Check for multiple destinations from same source
157
- const sourceMap = new Map<string, MoveCandidate[]>();
158
- for (const move of moves) {
159
- if (!sourceMap.has(move.fromPath)) {
160
- sourceMap.set(move.fromPath, []);
161
- }
162
- sourceMap.get(move.fromPath)!.push(move);
92
+ private async calculateSimilarity(
93
+ content1: string | Uint8Array,
94
+ content2: string | Uint8Array,
95
+ path: string
96
+ ): Promise<number> {
97
+ if (content1 === content2) return 1.0;
98
+
99
+ // Early exit: size difference too large
100
+ const size1 =
101
+ typeof content1 === "string" ? content1.length : content1.length;
102
+ const size2 =
103
+ typeof content2 === "string" ? content2.length : content2.length;
104
+ const sizeDiff = Math.abs(size1 - size2) / Math.max(size1, size2);
105
+ if (sizeDiff > 0.5) return 0.0;
106
+
107
+ // Binary files: hash mismatch = not a move
108
+ const isText = await isTextFile(path);
109
+ if (!isText) return 0.0;
110
+
111
+ // Text files: use string similarity
112
+ const str1 =
113
+ typeof content1 === "string" ? content1 : this.bufferToString(content1);
114
+ const str2 =
115
+ typeof content2 === "string" ? content2 : this.bufferToString(content2);
116
+
117
+ // For small files (<4KB), compare full content
118
+ if (size1 < 4096 && size2 < 4096) {
119
+ return stringSimilarity(str1, str2);
163
120
  }
164
121
 
165
- for (const move of moves) {
166
- const destinationConflicts = destinationMap.get(move.toPath)!;
167
- const sourceConflicts = sourceMap.get(move.fromPath)!;
122
+ // For large files, sample 3 locations
123
+ const samples1 = this.getSamples(str1);
124
+ const samples2 = this.getSamples(str2);
168
125
 
169
- if (destinationConflicts.length > 1) {
170
- conflicts.push({
171
- moves: destinationConflicts,
172
- reason: `Multiple files moving to ${move.toPath}`,
173
- });
174
- } else if (sourceConflicts.length > 1) {
175
- conflicts.push({
176
- moves: sourceConflicts,
177
- reason: `File ${move.fromPath} has multiple potential destinations`,
178
- });
179
- } else {
180
- validMoves.push(move);
181
- }
126
+ let totalSimilarity = 0;
127
+ for (let i = 0; i < Math.min(samples1.length, samples2.length); i++) {
128
+ totalSimilarity += stringSimilarity(samples1[i], samples2[i]);
182
129
  }
183
130
 
184
- return { validMoves, conflicts };
185
- }
186
-
187
- /**
188
- * Apply move detection heuristics
189
- */
190
- applyHeuristics(moves: MoveCandidate[]): MoveCandidate[] {
191
- return moves
192
- .filter((move) => {
193
- // Filter out moves within the same directory unless similarity is very high
194
- const fromDir = this.getDirectoryPath(move.fromPath);
195
- const toDir = this.getDirectoryPath(move.toPath);
196
-
197
- if (fromDir === toDir && move.similarity < 0.9) {
198
- return false;
199
- }
200
-
201
- // Filter out moves with very different file extensions unless similarity is perfect
202
- const fromExt = this.getFileExtension(move.fromPath);
203
- const toExt = this.getFileExtension(move.toPath);
204
-
205
- if (fromExt !== toExt && move.similarity < 1.0) {
206
- return false;
207
- }
208
-
209
- return true;
210
- })
211
- .sort((a, b) => b.similarity - a.similarity); // Sort by similarity descending
212
- }
213
-
214
- /**
215
- * Get directory path from file path
216
- */
217
- private getDirectoryPath(filePath: string): string {
218
- const lastSlash = filePath.lastIndexOf("/");
219
- return lastSlash >= 0 ? filePath.substring(0, lastSlash) : "";
131
+ return totalSimilarity / Math.min(samples1.length, samples2.length);
220
132
  }
221
133
 
222
134
  /**
223
- * Get file extension from file path
135
+ * Get representative samples from content (beginning, middle, end)
224
136
  */
225
- private getFileExtension(filePath: string): string {
226
- const lastDot = filePath.lastIndexOf(".");
227
- const lastSlash = filePath.lastIndexOf("/");
137
+ private getSamples(str: string): string[] {
138
+ const CHUNK_SIZE = 1024;
139
+ const length = str.length;
228
140
 
229
- if (lastDot > lastSlash && lastDot >= 0) {
230
- return filePath.substring(lastDot + 1).toLowerCase();
141
+ if (length <= CHUNK_SIZE) {
142
+ return [str];
231
143
  }
232
144
 
233
- return "";
234
- }
235
-
236
- /**
237
- * Check if a move should be auto-applied
238
- */
239
- shouldAutoApply(move: MoveCandidate): boolean {
240
- return move.confidence === "auto";
145
+ return [
146
+ str.slice(0, CHUNK_SIZE), // Beginning
147
+ str.slice(
148
+ Math.floor(length / 2) - Math.floor(CHUNK_SIZE / 2),
149
+ Math.floor(length / 2) + Math.floor(CHUNK_SIZE / 2)
150
+ ), // Middle
151
+ str.slice(-CHUNK_SIZE), // End
152
+ ];
241
153
  }
242
154
 
243
155
  /**
244
- * Check if a move should prompt the user
156
+ * Convert buffer to string (for text comparison)
245
157
  */
246
- shouldPromptUser(move: MoveCandidate): boolean {
247
- return move.confidence === "prompt";
158
+ private bufferToString(buffer: Uint8Array): string {
159
+ return new TextDecoder().decode(buffer);
248
160
  }
249
161
 
250
162
  /**
@@ -254,22 +166,4 @@ export class MoveDetector {
254
166
  const percentage = Math.round(move.similarity * 100);
255
167
  return `${move.fromPath} → ${move.toPath} (${percentage}% similar)`;
256
168
  }
257
-
258
- /**
259
- * Calculate move statistics
260
- */
261
- calculateStats(moves: MoveCandidate[]): {
262
- total: number;
263
- auto: number;
264
- prompt: number;
265
- averageSimilarity: number;
266
- } {
267
- const total = moves.length;
268
- const auto = moves.filter((m) => m.confidence === "auto").length;
269
- const prompt = moves.filter((m) => m.confidence === "prompt").length;
270
- const averageSimilarity =
271
- total > 0 ? moves.reduce((sum, m) => sum + m.similarity, 0) / total : 0;
272
-
273
- return { total, auto, prompt, averageSimilarity };
274
- }
275
169
  }
@@ -11,6 +11,7 @@ import {
11
11
  SnapshotDirectoryEntry,
12
12
  } from "../types";
13
13
  import { pathExists, ensureDirectoryExists } from "../utils";
14
+ import { out } from "../utils/output";
14
15
 
15
16
  /**
16
17
  * Manages sync snapshots for local state tracking
@@ -57,7 +58,7 @@ export class SnapshotManager {
57
58
 
58
59
  return this.deserializeSnapshot(serializable);
59
60
  } catch (error) {
60
- console.warn(`Failed to load snapshot: ${error}`);
61
+ out.taskLine(`Failed to load snapshot: ${error}`);
61
62
  return null;
62
63
  }
63
64
  }
@@ -189,17 +190,6 @@ export class SnapshotManager {
189
190
  };
190
191
  }
191
192
 
192
- /**
193
- * Backup current snapshot
194
- */
195
- async backup(): Promise<void> {
196
- const snapshotPath = this.getSnapshotPath();
197
- if (await pathExists(snapshotPath)) {
198
- const backupPath = `${snapshotPath}.backup.${Date.now()}`;
199
- await fs.copyFile(snapshotPath, backupPath);
200
- }
201
- }
202
-
203
193
  /**
204
194
  * Validate snapshot integrity
205
195
  */