pushwork 2.0.0-a.sub.0 → 2.0.0-preview

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 (251) hide show
  1. package/dist/branches.d.ts +19 -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 +238 -272
  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 +34 -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 +115 -0
  30. package/dist/pushwork.d.ts.map +1 -0
  31. package/dist/pushwork.js +918 -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 +37 -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 +88 -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/flake.lock +128 -0
  66. package/flake.nix +66 -0
  67. package/package.json +15 -48
  68. package/patches/@automerge__automerge-repo@2.6.0-subduction.15.patch +26 -0
  69. package/pnpm-workspace.yaml +5 -0
  70. package/src/branches.ts +93 -0
  71. package/src/cli.ts +258 -408
  72. package/src/config.ts +64 -0
  73. package/src/fs-tree.ts +70 -0
  74. package/src/ignore.ts +33 -0
  75. package/src/index.ts +38 -4
  76. package/src/log.ts +8 -0
  77. package/src/pushwork.ts +1055 -0
  78. package/src/repo.ts +76 -0
  79. package/src/shapes/custom.ts +29 -0
  80. package/src/shapes/file.ts +115 -0
  81. package/src/shapes/index.ts +19 -0
  82. package/src/shapes/patchwork-folder.ts +156 -0
  83. package/src/shapes/types.ts +79 -0
  84. package/src/shapes/vfs.ts +93 -0
  85. package/src/stash.ts +106 -0
  86. package/test/integration/branches.test.ts +389 -0
  87. package/test/integration/pushwork.test.ts +547 -0
  88. package/test/setup.ts +29 -0
  89. package/test/unit/doc-shape.test.ts +612 -0
  90. package/tsconfig.json +2 -3
  91. package/vitest.config.ts +14 -0
  92. package/ARCHITECTURE-ACCORDING-TO-CLAUDE.md +0 -248
  93. package/CLAUDE.md +0 -141
  94. package/README.md +0 -221
  95. package/babel.config.js +0 -5
  96. package/dist/cli/commands.d.ts +0 -71
  97. package/dist/cli/commands.d.ts.map +0 -1
  98. package/dist/cli/commands.js +0 -794
  99. package/dist/cli/commands.js.map +0 -1
  100. package/dist/cli/index.d.ts +0 -2
  101. package/dist/cli/index.d.ts.map +0 -1
  102. package/dist/cli/index.js +0 -19
  103. package/dist/cli/index.js.map +0 -1
  104. package/dist/commands.d.ts +0 -61
  105. package/dist/commands.d.ts.map +0 -1
  106. package/dist/commands.js +0 -861
  107. package/dist/commands.js.map +0 -1
  108. package/dist/config/index.d.ts +0 -71
  109. package/dist/config/index.d.ts.map +0 -1
  110. package/dist/config/index.js +0 -314
  111. package/dist/config/index.js.map +0 -1
  112. package/dist/core/change-detection.d.ts +0 -80
  113. package/dist/core/change-detection.d.ts.map +0 -1
  114. package/dist/core/change-detection.js +0 -523
  115. package/dist/core/change-detection.js.map +0 -1
  116. package/dist/core/config.d.ts +0 -81
  117. package/dist/core/config.d.ts.map +0 -1
  118. package/dist/core/config.js +0 -258
  119. package/dist/core/config.js.map +0 -1
  120. package/dist/core/index.d.ts +0 -6
  121. package/dist/core/index.d.ts.map +0 -1
  122. package/dist/core/index.js +0 -6
  123. package/dist/core/index.js.map +0 -1
  124. package/dist/core/move-detection.d.ts +0 -34
  125. package/dist/core/move-detection.d.ts.map +0 -1
  126. package/dist/core/move-detection.js +0 -121
  127. package/dist/core/move-detection.js.map +0 -1
  128. package/dist/core/snapshot.d.ts +0 -105
  129. package/dist/core/snapshot.d.ts.map +0 -1
  130. package/dist/core/snapshot.js +0 -217
  131. package/dist/core/snapshot.js.map +0 -1
  132. package/dist/core/sync-engine.d.ts +0 -151
  133. package/dist/core/sync-engine.d.ts.map +0 -1
  134. package/dist/core/sync-engine.js +0 -1346
  135. package/dist/core/sync-engine.js.map +0 -1
  136. package/dist/types/config.d.ts +0 -99
  137. package/dist/types/config.d.ts.map +0 -1
  138. package/dist/types/config.js +0 -5
  139. package/dist/types/config.js.map +0 -1
  140. package/dist/types/documents.d.ts +0 -88
  141. package/dist/types/documents.d.ts.map +0 -1
  142. package/dist/types/documents.js +0 -20
  143. package/dist/types/documents.js.map +0 -1
  144. package/dist/types/index.d.ts +0 -4
  145. package/dist/types/index.d.ts.map +0 -1
  146. package/dist/types/index.js +0 -4
  147. package/dist/types/index.js.map +0 -1
  148. package/dist/types/snapshot.d.ts +0 -64
  149. package/dist/types/snapshot.d.ts.map +0 -1
  150. package/dist/types/snapshot.js +0 -2
  151. package/dist/types/snapshot.js.map +0 -1
  152. package/dist/utils/content-similarity.d.ts +0 -53
  153. package/dist/utils/content-similarity.d.ts.map +0 -1
  154. package/dist/utils/content-similarity.js +0 -155
  155. package/dist/utils/content-similarity.js.map +0 -1
  156. package/dist/utils/content.d.ts +0 -10
  157. package/dist/utils/content.d.ts.map +0 -1
  158. package/dist/utils/content.js +0 -31
  159. package/dist/utils/content.js.map +0 -1
  160. package/dist/utils/directory.d.ts +0 -24
  161. package/dist/utils/directory.d.ts.map +0 -1
  162. package/dist/utils/directory.js +0 -52
  163. package/dist/utils/directory.js.map +0 -1
  164. package/dist/utils/fs.d.ts +0 -74
  165. package/dist/utils/fs.d.ts.map +0 -1
  166. package/dist/utils/fs.js +0 -248
  167. package/dist/utils/fs.js.map +0 -1
  168. package/dist/utils/index.d.ts +0 -5
  169. package/dist/utils/index.d.ts.map +0 -1
  170. package/dist/utils/index.js +0 -5
  171. package/dist/utils/index.js.map +0 -1
  172. package/dist/utils/mime-types.d.ts +0 -13
  173. package/dist/utils/mime-types.d.ts.map +0 -1
  174. package/dist/utils/mime-types.js +0 -209
  175. package/dist/utils/mime-types.js.map +0 -1
  176. package/dist/utils/network-sync.d.ts +0 -36
  177. package/dist/utils/network-sync.d.ts.map +0 -1
  178. package/dist/utils/network-sync.js +0 -250
  179. package/dist/utils/network-sync.js.map +0 -1
  180. package/dist/utils/node-polyfills.d.ts +0 -9
  181. package/dist/utils/node-polyfills.d.ts.map +0 -1
  182. package/dist/utils/node-polyfills.js +0 -9
  183. package/dist/utils/node-polyfills.js.map +0 -1
  184. package/dist/utils/output.d.ts +0 -129
  185. package/dist/utils/output.d.ts.map +0 -1
  186. package/dist/utils/output.js +0 -368
  187. package/dist/utils/output.js.map +0 -1
  188. package/dist/utils/repo-factory.d.ts +0 -13
  189. package/dist/utils/repo-factory.d.ts.map +0 -1
  190. package/dist/utils/repo-factory.js +0 -46
  191. package/dist/utils/repo-factory.js.map +0 -1
  192. package/dist/utils/string-similarity.d.ts +0 -14
  193. package/dist/utils/string-similarity.d.ts.map +0 -1
  194. package/dist/utils/string-similarity.js +0 -39
  195. package/dist/utils/string-similarity.js.map +0 -1
  196. package/dist/utils/text-diff.d.ts +0 -37
  197. package/dist/utils/text-diff.d.ts.map +0 -1
  198. package/dist/utils/text-diff.js +0 -93
  199. package/dist/utils/text-diff.js.map +0 -1
  200. package/dist/utils/trace.d.ts +0 -19
  201. package/dist/utils/trace.d.ts.map +0 -1
  202. package/dist/utils/trace.js +0 -63
  203. package/dist/utils/trace.js.map +0 -1
  204. package/src/commands.ts +0 -1134
  205. package/src/core/change-detection.ts +0 -712
  206. package/src/core/config.ts +0 -313
  207. package/src/core/index.ts +0 -5
  208. package/src/core/move-detection.ts +0 -169
  209. package/src/core/snapshot.ts +0 -275
  210. package/src/core/sync-engine.ts +0 -1758
  211. package/src/types/config.ts +0 -111
  212. package/src/types/documents.ts +0 -91
  213. package/src/types/index.ts +0 -3
  214. package/src/types/snapshot.ts +0 -67
  215. package/src/utils/content.ts +0 -34
  216. package/src/utils/directory.ts +0 -73
  217. package/src/utils/fs.ts +0 -297
  218. package/src/utils/index.ts +0 -4
  219. package/src/utils/mime-types.ts +0 -244
  220. package/src/utils/network-sync.ts +0 -319
  221. package/src/utils/node-polyfills.ts +0 -8
  222. package/src/utils/output.ts +0 -450
  223. package/src/utils/repo-factory.ts +0 -73
  224. package/src/utils/string-similarity.ts +0 -54
  225. package/src/utils/text-diff.ts +0 -101
  226. package/src/utils/trace.ts +0 -70
  227. package/test/integration/README.md +0 -328
  228. package/test/integration/clone-test.sh +0 -310
  229. package/test/integration/conflict-resolution-test.sh +0 -309
  230. package/test/integration/debug-both-nested.sh +0 -74
  231. package/test/integration/debug-concurrent-nested.sh +0 -87
  232. package/test/integration/debug-nested.sh +0 -73
  233. package/test/integration/deletion-behavior-test.sh +0 -487
  234. package/test/integration/deletion-sync-test-simple.sh +0 -193
  235. package/test/integration/deletion-sync-test.sh +0 -297
  236. package/test/integration/exclude-patterns.test.ts +0 -144
  237. package/test/integration/full-integration-test.sh +0 -363
  238. package/test/integration/fuzzer.test.ts +0 -818
  239. package/test/integration/in-memory-sync.test.ts +0 -830
  240. package/test/integration/init-sync.test.ts +0 -89
  241. package/test/integration/manual-sync-test.sh +0 -84
  242. package/test/integration/sync-deletion.test.ts +0 -280
  243. package/test/integration/sync-flow.test.ts +0 -291
  244. package/test/jest.setup.ts +0 -34
  245. package/test/run-tests.sh +0 -225
  246. package/test/unit/deletion-behavior.test.ts +0 -249
  247. package/test/unit/enhanced-mime-detection.test.ts +0 -244
  248. package/test/unit/snapshot.test.ts +0 -404
  249. package/test/unit/sync-convergence.test.ts +0 -298
  250. package/test/unit/sync-timing.test.ts +0 -134
  251. package/test/unit/utils.test.ts +0 -366
@@ -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
- }