pushwork 2.0.0-a.sub.1 → 2.0.0-preview.2

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 (234) hide show
  1. package/dist/branches.d.ts +20 -0
  2. package/dist/branches.d.ts.map +1 -0
  3. package/dist/branches.js +111 -0
  4. package/dist/branches.js.map +1 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +245 -270
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +84 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/fs-tree.d.ts +6 -0
  14. package/dist/fs-tree.d.ts.map +1 -0
  15. package/dist/fs-tree.js +99 -0
  16. package/dist/fs-tree.js.map +1 -0
  17. package/dist/ignore.d.ts +6 -0
  18. package/dist/ignore.d.ts.map +1 -0
  19. package/dist/ignore.js +74 -0
  20. package/dist/ignore.js.map +1 -0
  21. package/dist/index.d.ts +8 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +35 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/log.d.ts +3 -0
  26. package/dist/log.d.ts.map +1 -0
  27. package/dist/log.js +14 -0
  28. package/dist/log.js.map +1 -0
  29. package/dist/pushwork.d.ts +129 -0
  30. package/dist/pushwork.d.ts.map +1 -0
  31. package/dist/pushwork.js +1062 -0
  32. package/dist/pushwork.js.map +1 -0
  33. package/dist/repo.d.ts +14 -0
  34. package/dist/repo.d.ts.map +1 -0
  35. package/dist/repo.js +60 -0
  36. package/dist/repo.js.map +1 -0
  37. package/dist/shapes/custom.d.ts +3 -0
  38. package/dist/shapes/custom.d.ts.map +1 -0
  39. package/dist/shapes/custom.js +57 -0
  40. package/dist/shapes/custom.js.map +1 -0
  41. package/dist/shapes/file.d.ts +20 -0
  42. package/dist/shapes/file.d.ts.map +1 -0
  43. package/dist/shapes/file.js +140 -0
  44. package/dist/shapes/file.js.map +1 -0
  45. package/dist/shapes/index.d.ts +10 -0
  46. package/dist/shapes/index.d.ts.map +1 -0
  47. package/dist/shapes/index.js +35 -0
  48. package/dist/shapes/index.js.map +1 -0
  49. package/dist/shapes/patchwork-folder.d.ts +3 -0
  50. package/dist/shapes/patchwork-folder.d.ts.map +1 -0
  51. package/dist/shapes/patchwork-folder.js +160 -0
  52. package/dist/shapes/patchwork-folder.js.map +1 -0
  53. package/dist/shapes/types.d.ts +38 -0
  54. package/dist/shapes/types.d.ts.map +1 -0
  55. package/dist/shapes/types.js +52 -0
  56. package/dist/shapes/types.js.map +1 -0
  57. package/dist/shapes/vfs.d.ts +3 -0
  58. package/dist/shapes/vfs.d.ts.map +1 -0
  59. package/dist/shapes/vfs.js +92 -0
  60. package/dist/shapes/vfs.js.map +1 -0
  61. package/dist/stash.d.ts +23 -0
  62. package/dist/stash.d.ts.map +1 -0
  63. package/dist/stash.js +118 -0
  64. package/dist/stash.js.map +1 -0
  65. package/dist/version.d.ts +11 -0
  66. package/dist/version.d.ts.map +1 -0
  67. package/dist/version.js +93 -0
  68. package/dist/version.js.map +1 -0
  69. package/package.json +19 -48
  70. package/patches/@automerge__automerge-repo@2.6.0-subduction.15.patch +26 -0
  71. package/.prettierrc +0 -9
  72. package/ARCHITECTURE-ACCORDING-TO-CLAUDE.md +0 -248
  73. package/CLAUDE.md +0 -141
  74. package/README.md +0 -221
  75. package/babel.config.js +0 -5
  76. package/dist/cli/commands.d.ts +0 -71
  77. package/dist/cli/commands.d.ts.map +0 -1
  78. package/dist/cli/commands.js +0 -794
  79. package/dist/cli/commands.js.map +0 -1
  80. package/dist/cli/index.d.ts +0 -2
  81. package/dist/cli/index.d.ts.map +0 -1
  82. package/dist/cli/index.js +0 -19
  83. package/dist/cli/index.js.map +0 -1
  84. package/dist/commands.d.ts +0 -61
  85. package/dist/commands.d.ts.map +0 -1
  86. package/dist/commands.js +0 -861
  87. package/dist/commands.js.map +0 -1
  88. package/dist/config/index.d.ts +0 -71
  89. package/dist/config/index.d.ts.map +0 -1
  90. package/dist/config/index.js +0 -314
  91. package/dist/config/index.js.map +0 -1
  92. package/dist/core/change-detection.d.ts +0 -80
  93. package/dist/core/change-detection.d.ts.map +0 -1
  94. package/dist/core/change-detection.js +0 -523
  95. package/dist/core/change-detection.js.map +0 -1
  96. package/dist/core/config.d.ts +0 -81
  97. package/dist/core/config.d.ts.map +0 -1
  98. package/dist/core/config.js +0 -258
  99. package/dist/core/config.js.map +0 -1
  100. package/dist/core/index.d.ts +0 -6
  101. package/dist/core/index.d.ts.map +0 -1
  102. package/dist/core/index.js +0 -6
  103. package/dist/core/index.js.map +0 -1
  104. package/dist/core/move-detection.d.ts +0 -34
  105. package/dist/core/move-detection.d.ts.map +0 -1
  106. package/dist/core/move-detection.js +0 -121
  107. package/dist/core/move-detection.js.map +0 -1
  108. package/dist/core/snapshot.d.ts +0 -105
  109. package/dist/core/snapshot.d.ts.map +0 -1
  110. package/dist/core/snapshot.js +0 -217
  111. package/dist/core/snapshot.js.map +0 -1
  112. package/dist/core/sync-engine.d.ts +0 -157
  113. package/dist/core/sync-engine.d.ts.map +0 -1
  114. package/dist/core/sync-engine.js +0 -1379
  115. package/dist/core/sync-engine.js.map +0 -1
  116. package/dist/types/config.d.ts +0 -99
  117. package/dist/types/config.d.ts.map +0 -1
  118. package/dist/types/config.js +0 -5
  119. package/dist/types/config.js.map +0 -1
  120. package/dist/types/documents.d.ts +0 -88
  121. package/dist/types/documents.d.ts.map +0 -1
  122. package/dist/types/documents.js +0 -20
  123. package/dist/types/documents.js.map +0 -1
  124. package/dist/types/index.d.ts +0 -4
  125. package/dist/types/index.d.ts.map +0 -1
  126. package/dist/types/index.js +0 -4
  127. package/dist/types/index.js.map +0 -1
  128. package/dist/types/snapshot.d.ts +0 -64
  129. package/dist/types/snapshot.d.ts.map +0 -1
  130. package/dist/types/snapshot.js +0 -2
  131. package/dist/types/snapshot.js.map +0 -1
  132. package/dist/utils/content-similarity.d.ts +0 -53
  133. package/dist/utils/content-similarity.d.ts.map +0 -1
  134. package/dist/utils/content-similarity.js +0 -155
  135. package/dist/utils/content-similarity.js.map +0 -1
  136. package/dist/utils/content.d.ts +0 -10
  137. package/dist/utils/content.d.ts.map +0 -1
  138. package/dist/utils/content.js +0 -31
  139. package/dist/utils/content.js.map +0 -1
  140. package/dist/utils/directory.d.ts +0 -24
  141. package/dist/utils/directory.d.ts.map +0 -1
  142. package/dist/utils/directory.js +0 -52
  143. package/dist/utils/directory.js.map +0 -1
  144. package/dist/utils/fs.d.ts +0 -74
  145. package/dist/utils/fs.d.ts.map +0 -1
  146. package/dist/utils/fs.js +0 -248
  147. package/dist/utils/fs.js.map +0 -1
  148. package/dist/utils/index.d.ts +0 -5
  149. package/dist/utils/index.d.ts.map +0 -1
  150. package/dist/utils/index.js +0 -5
  151. package/dist/utils/index.js.map +0 -1
  152. package/dist/utils/mime-types.d.ts +0 -13
  153. package/dist/utils/mime-types.d.ts.map +0 -1
  154. package/dist/utils/mime-types.js +0 -209
  155. package/dist/utils/mime-types.js.map +0 -1
  156. package/dist/utils/network-sync.d.ts +0 -36
  157. package/dist/utils/network-sync.d.ts.map +0 -1
  158. package/dist/utils/network-sync.js +0 -250
  159. package/dist/utils/network-sync.js.map +0 -1
  160. package/dist/utils/node-polyfills.d.ts +0 -9
  161. package/dist/utils/node-polyfills.d.ts.map +0 -1
  162. package/dist/utils/node-polyfills.js +0 -9
  163. package/dist/utils/node-polyfills.js.map +0 -1
  164. package/dist/utils/output.d.ts +0 -129
  165. package/dist/utils/output.d.ts.map +0 -1
  166. package/dist/utils/output.js +0 -368
  167. package/dist/utils/output.js.map +0 -1
  168. package/dist/utils/repo-factory.d.ts +0 -13
  169. package/dist/utils/repo-factory.d.ts.map +0 -1
  170. package/dist/utils/repo-factory.js +0 -46
  171. package/dist/utils/repo-factory.js.map +0 -1
  172. package/dist/utils/string-similarity.d.ts +0 -14
  173. package/dist/utils/string-similarity.d.ts.map +0 -1
  174. package/dist/utils/string-similarity.js +0 -39
  175. package/dist/utils/string-similarity.js.map +0 -1
  176. package/dist/utils/text-diff.d.ts +0 -37
  177. package/dist/utils/text-diff.d.ts.map +0 -1
  178. package/dist/utils/text-diff.js +0 -93
  179. package/dist/utils/text-diff.js.map +0 -1
  180. package/dist/utils/trace.d.ts +0 -19
  181. package/dist/utils/trace.d.ts.map +0 -1
  182. package/dist/utils/trace.js +0 -63
  183. package/dist/utils/trace.js.map +0 -1
  184. package/src/cli.ts +0 -442
  185. package/src/commands.ts +0 -1134
  186. package/src/core/change-detection.ts +0 -712
  187. package/src/core/config.ts +0 -313
  188. package/src/core/index.ts +0 -5
  189. package/src/core/move-detection.ts +0 -169
  190. package/src/core/snapshot.ts +0 -275
  191. package/src/core/sync-engine.ts +0 -1795
  192. package/src/index.ts +0 -4
  193. package/src/types/config.ts +0 -111
  194. package/src/types/documents.ts +0 -91
  195. package/src/types/index.ts +0 -3
  196. package/src/types/snapshot.ts +0 -67
  197. package/src/utils/content.ts +0 -34
  198. package/src/utils/directory.ts +0 -73
  199. package/src/utils/fs.ts +0 -297
  200. package/src/utils/index.ts +0 -4
  201. package/src/utils/mime-types.ts +0 -244
  202. package/src/utils/network-sync.ts +0 -319
  203. package/src/utils/node-polyfills.ts +0 -8
  204. package/src/utils/output.ts +0 -450
  205. package/src/utils/repo-factory.ts +0 -73
  206. package/src/utils/string-similarity.ts +0 -54
  207. package/src/utils/text-diff.ts +0 -101
  208. package/src/utils/trace.ts +0 -70
  209. package/test/integration/README.md +0 -328
  210. package/test/integration/clone-test.sh +0 -310
  211. package/test/integration/conflict-resolution-test.sh +0 -309
  212. package/test/integration/debug-both-nested.sh +0 -74
  213. package/test/integration/debug-concurrent-nested.sh +0 -87
  214. package/test/integration/debug-nested.sh +0 -73
  215. package/test/integration/deletion-behavior-test.sh +0 -487
  216. package/test/integration/deletion-sync-test-simple.sh +0 -193
  217. package/test/integration/deletion-sync-test.sh +0 -297
  218. package/test/integration/exclude-patterns.test.ts +0 -144
  219. package/test/integration/full-integration-test.sh +0 -363
  220. package/test/integration/fuzzer.test.ts +0 -818
  221. package/test/integration/in-memory-sync.test.ts +0 -830
  222. package/test/integration/init-sync.test.ts +0 -89
  223. package/test/integration/manual-sync-test.sh +0 -84
  224. package/test/integration/sync-deletion.test.ts +0 -280
  225. package/test/integration/sync-flow.test.ts +0 -291
  226. package/test/jest.setup.ts +0 -34
  227. package/test/run-tests.sh +0 -225
  228. package/test/unit/deletion-behavior.test.ts +0 -249
  229. package/test/unit/enhanced-mime-detection.test.ts +0 -244
  230. package/test/unit/snapshot.test.ts +0 -404
  231. package/test/unit/sync-convergence.test.ts +0 -298
  232. package/test/unit/sync-timing.test.ts +0 -134
  233. package/test/unit/utils.test.ts +0 -366
  234. package/tsconfig.json +0 -23
@@ -1,313 +0,0 @@
1
- import * as fs from "fs/promises";
2
- import * as path from "path";
3
- import * as os from "os";
4
- import {
5
- GlobalConfig,
6
- DirectoryConfig,
7
- DEFAULT_SYNC_SERVER,
8
- } from "../types/index.js";
9
- import { pathExists, ensureDirectoryExists } from "../utils/index.js";
10
-
11
- /**
12
- * Configuration manager for pushwork
13
- */
14
- export class ConfigManager {
15
- private static readonly GLOBAL_CONFIG_DIR = ".pushwork";
16
- private static readonly CONFIG_FILENAME = "config.json";
17
-
18
- static readonly CONFIG_DIR = ".pushwork";
19
-
20
- constructor(private workingDir?: string) {}
21
-
22
- /**
23
- * Get global configuration path
24
- */
25
- private getGlobalConfigPath(): string {
26
- return path.join(
27
- os.homedir(),
28
- ConfigManager.GLOBAL_CONFIG_DIR,
29
- ConfigManager.CONFIG_FILENAME
30
- );
31
- }
32
-
33
- /**
34
- * Get local configuration path
35
- */
36
- private getLocalConfigPath(): string {
37
- if (!this.workingDir) {
38
- throw new Error("Working directory not set for local config");
39
- }
40
- return path.join(
41
- this.workingDir,
42
- ConfigManager.CONFIG_DIR,
43
- ConfigManager.CONFIG_FILENAME
44
- );
45
- }
46
-
47
- /**
48
- * Load global configuration
49
- */
50
- async loadGlobal(): Promise<GlobalConfig | null> {
51
- try {
52
- const configPath = this.getGlobalConfigPath();
53
- if (!(await pathExists(configPath))) {
54
- return null;
55
- }
56
-
57
- const content = await fs.readFile(configPath, "utf8");
58
- return JSON.parse(content) as GlobalConfig;
59
- } catch (error) {
60
- // Failed to load global config
61
- return null;
62
- }
63
- }
64
-
65
- /**
66
- * Save global configuration
67
- */
68
- async saveGlobal(config: GlobalConfig): Promise<void> {
69
- try {
70
- const configPath = this.getGlobalConfigPath();
71
- await ensureDirectoryExists(path.dirname(configPath));
72
-
73
- const content = JSON.stringify(config, null, 2);
74
- await fs.writeFile(configPath, content, "utf8");
75
- } catch (error) {
76
- throw new Error(`Failed to save global config: ${error}`);
77
- }
78
- }
79
-
80
- /**
81
- * Load local/directory configuration
82
- */
83
- async load(): Promise<DirectoryConfig | null> {
84
- if (!this.workingDir) {
85
- return null;
86
- }
87
-
88
- try {
89
- const configPath = this.getLocalConfigPath();
90
- if (!(await pathExists(configPath))) {
91
- return null;
92
- }
93
-
94
- const content = await fs.readFile(configPath, "utf8");
95
- return JSON.parse(content) as DirectoryConfig;
96
- } catch (error) {
97
- // Failed to load local config
98
- return null;
99
- }
100
- }
101
-
102
- /**
103
- * Save local/directory configuration
104
- */
105
- async save(config: DirectoryConfig): Promise<void> {
106
- if (!this.workingDir) {
107
- throw new Error("Working directory not set for local config");
108
- }
109
-
110
- try {
111
- const configPath = this.getLocalConfigPath();
112
- await ensureDirectoryExists(path.dirname(configPath));
113
-
114
- const content = JSON.stringify(config, null, 2);
115
- await fs.writeFile(configPath, content, "utf8");
116
- } catch (error) {
117
- throw new Error(`Failed to save local config: ${error}`);
118
- }
119
- }
120
-
121
- private getDefaultGlobalConfig(): GlobalConfig {
122
- return {
123
- exclude_patterns: [
124
- ".git",
125
- "node_modules",
126
- "*.tmp",
127
- ".DS_Store",
128
- ".pushwork",
129
- ],
130
- artifact_directories: ["dist"],
131
- sync_server: DEFAULT_SYNC_SERVER,
132
- sync: {
133
- move_detection_threshold: 0.7,
134
- },
135
- };
136
- }
137
-
138
- /**
139
- * Get default configuration
140
- */
141
- getDefaultDirectoryConfig(): DirectoryConfig {
142
- return {
143
- sync_enabled: true,
144
- sync_server: DEFAULT_SYNC_SERVER,
145
- exclude_patterns: [
146
- ".git",
147
- "node_modules",
148
- "*.tmp",
149
- ".pushwork",
150
- ".DS_Store",
151
- ],
152
- artifact_directories: ["dist"],
153
- sync: {
154
- move_detection_threshold: 0.7,
155
- },
156
- };
157
- }
158
-
159
- /**
160
- * Get merged configuration (global + local)
161
- */
162
- async getMerged(): Promise<DirectoryConfig> {
163
- const globalConfig = await this.loadGlobal();
164
- const localConfig = await this.load();
165
-
166
- // Merge configurations: default < global < local
167
- let merged = this.getDefaultDirectoryConfig();
168
-
169
- if (globalConfig) {
170
- merged = this.mergeConfigs(merged, globalConfig);
171
- }
172
-
173
- if (localConfig) {
174
- merged = this.mergeConfigs(merged, localConfig);
175
- }
176
-
177
- return merged;
178
- }
179
-
180
- /**
181
- * Initialize with CLI option overrides
182
- * Creates a new config with defaults + CLI overrides and saves it
183
- */
184
- async initializeWithOverrides(
185
- overrides: Partial<DirectoryConfig> = {}
186
- ): Promise<DirectoryConfig> {
187
- const config = this.mergeConfigs(this.getDefaultDirectoryConfig(), overrides);
188
- await this.save(config);
189
- return config;
190
- }
191
-
192
- /**
193
- * Merge two configuration objects
194
- */
195
- private mergeConfigs(
196
- base: DirectoryConfig,
197
- override: Partial<DirectoryConfig> | GlobalConfig
198
- ): DirectoryConfig {
199
- const merged = { ...base };
200
-
201
- if ("sync_server" in override && override.sync_server !== undefined) {
202
- merged.sync_server = override.sync_server;
203
- }
204
-
205
- if ("sync_enabled" in override && override.sync_enabled !== undefined) {
206
- merged.sync_enabled = override.sync_enabled;
207
- }
208
-
209
- // Handle GlobalConfig structure
210
- if ("exclude_patterns" in override && override.exclude_patterns) {
211
- merged.exclude_patterns = override.exclude_patterns;
212
- }
213
-
214
- if ("artifact_directories" in override && override.artifact_directories) {
215
- merged.artifact_directories = override.artifact_directories;
216
- }
217
-
218
- if ("sync" in override && override.sync) {
219
- merged.sync = { ...merged.sync, ...override.sync };
220
- }
221
-
222
- return merged;
223
- }
224
-
225
- /**
226
- * Create default global configuration
227
- */
228
- async createDefaultGlobal(): Promise<void> {
229
- const defaultGlobal = this.getDefaultGlobalConfig();
230
- await this.saveGlobal(defaultGlobal);
231
- }
232
-
233
- /**
234
- * Check if global configuration exists
235
- */
236
- async globalConfigExists(): Promise<boolean> {
237
- return await pathExists(this.getGlobalConfigPath());
238
- }
239
-
240
- /**
241
- * Check if local configuration exists
242
- */
243
- async localConfigExists(): Promise<boolean> {
244
- if (!this.workingDir) return false;
245
- return await pathExists(this.getLocalConfigPath());
246
- }
247
-
248
- /**
249
- * Get configuration value by path (e.g., 'sync.move_detection_threshold')
250
- */
251
- async getValue(keyPath: string): Promise<any> {
252
- const config = await this.getMerged();
253
-
254
- const keys = keyPath.split(".");
255
- let value: any = config;
256
-
257
- for (const key of keys) {
258
- if (value && typeof value === "object" && key in value) {
259
- value = value[key];
260
- } else {
261
- return undefined;
262
- }
263
- }
264
-
265
- return value;
266
- }
267
-
268
- /**
269
- * Set configuration value by path
270
- */
271
- async setValue(keyPath: string, value: any): Promise<void> {
272
- const config = (await this.load()) || ({} as DirectoryConfig);
273
-
274
- const keys = keyPath.split(".");
275
- let current: any = config;
276
-
277
- // Navigate to the parent of the target key
278
- for (let i = 0; i < keys.length - 1; i++) {
279
- const key = keys[i];
280
- if (!(key in current) || typeof current[key] !== "object") {
281
- current[key] = {};
282
- }
283
- current = current[key];
284
- }
285
-
286
- // Set the value
287
- const finalKey = keys[keys.length - 1];
288
- current[finalKey] = value;
289
-
290
- await this.save(config);
291
- }
292
-
293
- /**
294
- * Validate configuration
295
- */
296
- validate(config: DirectoryConfig): { valid: boolean; errors: string[] } {
297
- const errors: string[] = [];
298
-
299
- if (config.sync?.move_detection_threshold !== undefined) {
300
- if (
301
- config.sync.move_detection_threshold < 0 ||
302
- config.sync.move_detection_threshold > 1
303
- ) {
304
- errors.push("move_detection_threshold must be between 0 and 1");
305
- }
306
- }
307
-
308
- return {
309
- valid: errors.length === 0,
310
- errors,
311
- };
312
- }
313
- }
package/src/core/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./snapshot.js";
2
- export * from "./change-detection.js";
3
- export * from "./move-detection.js";
4
- export * from "./sync-engine.js";
5
- export * from "./config.js";
@@ -1,169 +0,0 @@
1
- import { SyncSnapshot, MoveCandidate } from "../types/index.js";
2
- import { isTextFile } from "../utils/index.js";
3
- import { stringSimilarity } from "../utils/string-similarity.js";
4
- import { ChangeType, DetectedChange } from "../types/index.js";
5
-
6
- /**
7
- * Simplified move detection engine
8
- */
9
- export class MoveDetector {
10
- private readonly moveThreshold: number;
11
-
12
- constructor(moveThreshold: number = 0.7) {
13
- this.moveThreshold = moveThreshold;
14
- }
15
-
16
- /**
17
- * Detect file moves by analyzing deleted and created files
18
- */
19
- async detectMoves(
20
- changes: DetectedChange[],
21
- snapshot: SyncSnapshot
22
- ): Promise<{ moves: MoveCandidate[]; remainingChanges: DetectedChange[] }> {
23
- const deletedFiles = changes.filter(
24
- (c) => !c.localContent && c.changeType === ChangeType.LOCAL_ONLY
25
- );
26
- const createdFiles = changes.filter(
27
- (c) =>
28
- c.localContent &&
29
- c.changeType === ChangeType.LOCAL_ONLY &&
30
- !snapshot.files.has(c.path)
31
- );
32
-
33
- if (deletedFiles.length === 0 || createdFiles.length === 0) {
34
- return { moves: [], remainingChanges: changes };
35
- }
36
-
37
- const moves: MoveCandidate[] = [];
38
- const usedCreations = new Set<string>();
39
- const usedDeletions = new Set<string>();
40
-
41
- // Find potential moves by comparing content
42
- for (const deletedFile of deletedFiles) {
43
- const deletedContent = deletedFile.remoteContent;
44
- if (deletedContent === null) continue;
45
-
46
- let bestMatch: { file: DetectedChange; similarity: number } | null = null;
47
-
48
- for (const createdFile of createdFiles) {
49
- if (usedCreations.has(createdFile.path)) continue;
50
- if (createdFile.localContent === null) continue;
51
-
52
- const similarity = await this.calculateSimilarity(
53
- deletedContent,
54
- createdFile.localContent,
55
- deletedFile.path
56
- );
57
-
58
- if (similarity >= this.moveThreshold) {
59
- if (!bestMatch || similarity > bestMatch.similarity) {
60
- bestMatch = { file: createdFile, similarity };
61
- }
62
- }
63
- }
64
-
65
- if (bestMatch) {
66
- // If we detected a move above threshold, we apply it
67
- moves.push({
68
- fromPath: deletedFile.path,
69
- toPath: bestMatch.file.path,
70
- similarity: bestMatch.similarity,
71
- newContent: bestMatch.file.localContent || undefined,
72
- });
73
-
74
- // Consume the deletion and creation (move replaces both)
75
- usedCreations.add(bestMatch.file.path);
76
- usedDeletions.add(deletedFile.path);
77
- }
78
- }
79
-
80
- const remainingChanges = changes.filter(
81
- (change) =>
82
- !usedCreations.has(change.path) && !usedDeletions.has(change.path)
83
- );
84
-
85
- return { moves, remainingChanges };
86
- }
87
-
88
- /**
89
- * Calculate similarity between two content pieces
90
- * Optimized for speed while maintaining accuracy
91
- */
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);
120
- }
121
-
122
- // For large files, sample 3 locations
123
- const samples1 = this.getSamples(str1);
124
- const samples2 = this.getSamples(str2);
125
-
126
- let totalSimilarity = 0;
127
- for (let i = 0; i < Math.min(samples1.length, samples2.length); i++) {
128
- totalSimilarity += stringSimilarity(samples1[i], samples2[i]);
129
- }
130
-
131
- return totalSimilarity / Math.min(samples1.length, samples2.length);
132
- }
133
-
134
- /**
135
- * Get representative samples from content (beginning, middle, end)
136
- */
137
- private getSamples(str: string): string[] {
138
- const CHUNK_SIZE = 1024;
139
- const length = str.length;
140
-
141
- if (length <= CHUNK_SIZE) {
142
- return [str];
143
- }
144
-
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
- ];
153
- }
154
-
155
- /**
156
- * Convert buffer to string (for text comparison)
157
- */
158
- private bufferToString(buffer: Uint8Array): string {
159
- return new TextDecoder().decode(buffer);
160
- }
161
-
162
- /**
163
- * Format move for display
164
- */
165
- formatMove(move: MoveCandidate): string {
166
- const percentage = Math.round(move.similarity * 100);
167
- return `${move.fromPath} → ${move.toPath} (${percentage}% similar)`;
168
- }
169
- }