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.
- package/README.md +87 -328
- package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
- package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
- package/dist/.pushwork/config.json +15 -0
- package/dist/.pushwork/snapshot.json +7 -0
- package/dist/cli.js +231 -170
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +51 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +799 -0
- package/dist/commands.js.map +1 -0
- package/dist/core/change-detection.d.ts +6 -19
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +101 -80
- package/dist/core/change-detection.js.map +1 -1
- package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
- package/dist/core/config.d.ts.map +1 -0
- package/dist/{config/index.js → core/config.js} +55 -73
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +12 -50
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +58 -139
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +0 -4
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +2 -11
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +5 -11
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +220 -362
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -6
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +43 -67
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.d.ts +15 -3
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.d.ts +3 -21
- package/dist/types/snapshot.d.ts.map +1 -1
- package/dist/types/snapshot.js +0 -14
- package/dist/types/snapshot.js.map +1 -1
- package/dist/utils/content.d.ts.map +1 -1
- package/dist/utils/content.js +2 -6
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.d.ts +10 -0
- package/dist/utils/directory.d.ts.map +1 -0
- package/dist/utils/directory.js +37 -0
- package/dist/utils/directory.js.map +1 -0
- package/dist/utils/fs.d.ts +15 -2
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +63 -53
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.d.ts.map +1 -1
- package/dist/utils/mime-types.js +11 -4
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +0 -6
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +55 -99
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/output.d.ts +129 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +375 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +2 -6
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +8 -22
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.d.ts +14 -0
- package/dist/utils/string-similarity.d.ts.map +1 -0
- package/dist/utils/string-similarity.js +43 -0
- package/dist/utils/string-similarity.js.map +1 -0
- package/dist/utils/trace.d.ts +19 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +68 -0
- package/dist/utils/trace.js.map +1 -0
- package/package.json +17 -12
- package/src/cli.ts +326 -252
- package/src/commands.ts +988 -0
- package/src/core/change-detection.ts +199 -162
- package/src/{config/index.ts → core/config.ts} +65 -82
- package/src/core/index.ts +1 -1
- package/src/core/move-detection.ts +74 -180
- package/src/core/snapshot.ts +2 -12
- package/src/core/sync-engine.ts +248 -499
- package/src/index.ts +0 -10
- package/src/types/config.ts +50 -72
- package/src/types/documents.ts +16 -3
- package/src/types/index.ts +0 -5
- package/src/types/snapshot.ts +1 -23
- package/src/utils/content.ts +2 -6
- package/src/utils/directory.ts +50 -0
- package/src/utils/fs.ts +67 -56
- package/src/utils/index.ts +1 -6
- package/src/utils/mime-types.ts +12 -4
- package/src/utils/network-sync.ts +79 -137
- package/src/utils/output.ts +450 -0
- package/src/utils/repo-factory.ts +13 -31
- package/src/utils/string-similarity.ts +54 -0
- package/src/utils/trace.ts +70 -0
- package/test/integration/exclude-patterns.test.ts +6 -15
- package/test/integration/fuzzer.test.ts +308 -391
- package/test/integration/init-sync.test.ts +89 -0
- package/test/integration/sync-deletion.test.ts +2 -61
- package/test/integration/sync-flow.test.ts +4 -24
- package/test/jest.setup.ts +34 -0
- package/test/unit/deletion-behavior.test.ts +3 -14
- package/test/unit/enhanced-mime-detection.test.ts +0 -22
- package/test/unit/snapshot.test.ts +2 -29
- package/test/unit/sync-convergence.test.ts +3 -198
- package/test/unit/sync-timing.test.ts +0 -44
- package/test/unit/utils.test.ts +0 -2
- package/tsconfig.json +3 -3
- package/dist/browser/browser-sync-engine.d.ts +0 -64
- package/dist/browser/browser-sync-engine.d.ts.map +0 -1
- package/dist/browser/browser-sync-engine.js +0 -303
- package/dist/browser/browser-sync-engine.js.map +0 -1
- package/dist/browser/filesystem-adapter.d.ts +0 -84
- package/dist/browser/filesystem-adapter.d.ts.map +0 -1
- package/dist/browser/filesystem-adapter.js +0 -413
- package/dist/browser/filesystem-adapter.js.map +0 -1
- package/dist/browser/index.d.ts +0 -36
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -90
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/types.d.ts +0 -70
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/browser/types.js +0 -6
- package/dist/browser/types.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -77
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -904
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -19
- package/dist/cli/index.js.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js.map +0 -1
- package/dist/core/isomorphic-snapshot.d.ts +0 -58
- package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
- package/dist/core/isomorphic-snapshot.js +0 -204
- package/dist/core/isomorphic-snapshot.js.map +0 -1
- package/dist/platform/browser-filesystem.d.ts +0 -26
- package/dist/platform/browser-filesystem.d.ts.map +0 -1
- package/dist/platform/browser-filesystem.js +0 -91
- package/dist/platform/browser-filesystem.js.map +0 -1
- package/dist/platform/filesystem.d.ts +0 -29
- package/dist/platform/filesystem.d.ts.map +0 -1
- package/dist/platform/filesystem.js +0 -65
- package/dist/platform/filesystem.js.map +0 -1
- package/dist/platform/node-filesystem.d.ts +0 -21
- package/dist/platform/node-filesystem.d.ts.map +0 -1
- package/dist/platform/node-filesystem.js +0 -93
- package/dist/platform/node-filesystem.js.map +0 -1
- package/dist/utils/content-similarity.d.ts +0 -53
- package/dist/utils/content-similarity.d.ts.map +0 -1
- package/dist/utils/content-similarity.js +0 -155
- package/dist/utils/content-similarity.js.map +0 -1
- package/dist/utils/fs-browser.d.ts +0 -57
- package/dist/utils/fs-browser.d.ts.map +0 -1
- package/dist/utils/fs-browser.js +0 -311
- package/dist/utils/fs-browser.js.map +0 -1
- package/dist/utils/fs-node.d.ts +0 -53
- package/dist/utils/fs-node.d.ts.map +0 -1
- package/dist/utils/fs-node.js +0 -220
- package/dist/utils/fs-node.js.map +0 -1
- package/dist/utils/isomorphic.d.ts +0 -29
- package/dist/utils/isomorphic.d.ts.map +0 -1
- package/dist/utils/isomorphic.js +0 -139
- package/dist/utils/isomorphic.js.map +0 -1
- package/dist/utils/pure.d.ts +0 -25
- package/dist/utils/pure.d.ts.map +0 -1
- package/dist/utils/pure.js +0 -112
- package/dist/utils/pure.js.map +0 -1
- package/src/cli/commands.ts +0 -1207
- package/src/cli/index.ts +0 -2
- package/src/utils/content-similarity.ts +0 -194
- package/test/README-TESTING-GAPS.md +0 -174
- 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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
140
|
+
* Get default configuration
|
|
118
141
|
*/
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
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.
|
|
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,28 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
7
|
+
* Simplified move detection engine
|
|
12
8
|
*/
|
|
13
9
|
export class MoveDetector {
|
|
14
|
-
private
|
|
15
|
-
|
|
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 =
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
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
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
122
|
+
// For large files, sample 3 locations
|
|
123
|
+
const samples1 = this.getSamples(str1);
|
|
124
|
+
const samples2 = this.getSamples(str2);
|
|
168
125
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
|
135
|
+
* Get representative samples from content (beginning, middle, end)
|
|
224
136
|
*/
|
|
225
|
-
private
|
|
226
|
-
const
|
|
227
|
-
const
|
|
137
|
+
private getSamples(str: string): string[] {
|
|
138
|
+
const CHUNK_SIZE = 1024;
|
|
139
|
+
const length = str.length;
|
|
228
140
|
|
|
229
|
-
if (
|
|
230
|
-
return
|
|
141
|
+
if (length <= CHUNK_SIZE) {
|
|
142
|
+
return [str];
|
|
231
143
|
}
|
|
232
144
|
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
*
|
|
156
|
+
* Convert buffer to string (for text comparison)
|
|
245
157
|
*/
|
|
246
|
-
|
|
247
|
-
return
|
|
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
|
}
|
package/src/core/snapshot.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
*/
|