obsyncd 1.0.0
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 +920 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +117 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +13 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +225 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/prompts/conflictPrompt.d.ts +10 -0
- package/dist/cli/prompts/conflictPrompt.d.ts.map +1 -0
- package/dist/cli/prompts/conflictPrompt.js +51 -0
- package/dist/cli/prompts/conflictPrompt.js.map +1 -0
- package/dist/cli/prompts/fileBrowser.d.ts +6 -0
- package/dist/cli/prompts/fileBrowser.d.ts.map +1 -0
- package/dist/cli/prompts/fileBrowser.js +91 -0
- package/dist/cli/prompts/fileBrowser.js.map +1 -0
- package/dist/cli/prompts/vaultSelector.d.ts +13 -0
- package/dist/cli/prompts/vaultSelector.d.ts.map +1 -0
- package/dist/cli/prompts/vaultSelector.js +63 -0
- package/dist/cli/prompts/vaultSelector.js.map +1 -0
- package/dist/cli/ui/colors.d.ts +50 -0
- package/dist/cli/ui/colors.d.ts.map +1 -0
- package/dist/cli/ui/colors.js +62 -0
- package/dist/cli/ui/colors.js.map +1 -0
- package/dist/cli/ui/output.d.ts +45 -0
- package/dist/cli/ui/output.d.ts.map +1 -0
- package/dist/cli/ui/output.js +116 -0
- package/dist/cli/ui/output.js.map +1 -0
- package/dist/cli/ui/spinner.d.ts +29 -0
- package/dist/cli/ui/spinner.d.ts.map +1 -0
- package/dist/cli/ui/spinner.js +80 -0
- package/dist/cli/ui/spinner.js.map +1 -0
- package/dist/cli/ui/table.d.ts +28 -0
- package/dist/cli/ui/table.d.ts.map +1 -0
- package/dist/cli/ui/table.js +123 -0
- package/dist/cli/ui/table.js.map +1 -0
- package/dist/cli/utils/terminal.d.ts +21 -0
- package/dist/cli/utils/terminal.d.ts.map +1 -0
- package/dist/cli/utils/terminal.js +59 -0
- package/dist/cli/utils/terminal.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +32 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +45 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +112 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/index.d.ts +35 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +97 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/sync/changeDetector.d.ts +29 -0
- package/dist/sync/changeDetector.d.ts.map +1 -0
- package/dist/sync/changeDetector.js +259 -0
- package/dist/sync/changeDetector.js.map +1 -0
- package/dist/sync/conflictResolver.d.ts +29 -0
- package/dist/sync/conflictResolver.d.ts.map +1 -0
- package/dist/sync/conflictResolver.js +116 -0
- package/dist/sync/conflictResolver.js.map +1 -0
- package/dist/sync/directoryLister.d.ts +18 -0
- package/dist/sync/directoryLister.d.ts.map +1 -0
- package/dist/sync/directoryLister.js +39 -0
- package/dist/sync/directoryLister.js.map +1 -0
- package/dist/sync/index.d.ts +29 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +212 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/manifest.d.ts +48 -0
- package/dist/sync/manifest.d.ts.map +1 -0
- package/dist/sync/manifest.js +137 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/types.d.ts +109 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +5 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/watchMode.d.ts +84 -0
- package/dist/sync/watchMode.d.ts.map +1 -0
- package/dist/sync/watchMode.js +364 -0
- package/dist/sync/watchMode.js.map +1 -0
- package/dist/sync/watcher.d.ts +114 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +293 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +25 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/vault/index.d.ts +38 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +106 -0
- package/dist/vault/index.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core synchronization logic for Obsidian vaults.
|
|
3
|
+
* Handles file comparison, conflict detection, and sync operations.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import { LocalStorageAdapter } from '../storage/index.js';
|
|
8
|
+
import { ObsidianVault } from '../vault/index.js';
|
|
9
|
+
import { ManifestManager } from './manifest.js';
|
|
10
|
+
import { ChangeDetector } from './changeDetector.js';
|
|
11
|
+
import { ConflictResolver } from './conflictResolver.js';
|
|
12
|
+
import { DirectoryLister } from './directoryLister.js';
|
|
13
|
+
import { computeFileHash } from '../utils/index.js';
|
|
14
|
+
export * from './types.js';
|
|
15
|
+
export * from './watcher.js';
|
|
16
|
+
export * from './watchMode.js';
|
|
17
|
+
export * from './directoryLister.js';
|
|
18
|
+
export class SyncEngine {
|
|
19
|
+
sourceAdapter;
|
|
20
|
+
destAdapter;
|
|
21
|
+
changeDetector;
|
|
22
|
+
conflictResolver;
|
|
23
|
+
constructor() {
|
|
24
|
+
this.sourceAdapter = new LocalStorageAdapter();
|
|
25
|
+
this.destAdapter = new LocalStorageAdapter();
|
|
26
|
+
this.changeDetector = new ChangeDetector(this.sourceAdapter, this.destAdapter);
|
|
27
|
+
this.conflictResolver = new ConflictResolver();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Synchronize source vault to destination
|
|
31
|
+
*/
|
|
32
|
+
async sync(options) {
|
|
33
|
+
const { source, destination, conflictResolution = 'newest', dryRun = false, remoteMode = false, } = options;
|
|
34
|
+
const result = {
|
|
35
|
+
filesAdded: 0,
|
|
36
|
+
filesUpdated: 0,
|
|
37
|
+
filesDeleted: 0,
|
|
38
|
+
conflicts: [],
|
|
39
|
+
errors: [],
|
|
40
|
+
skipped: [],
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
// Validate source is an Obsidian vault
|
|
44
|
+
const sourceVault = new ObsidianVault({ vaultPath: source });
|
|
45
|
+
const isSourceVault = await sourceVault.isObsidianVault();
|
|
46
|
+
if (!isSourceVault) {
|
|
47
|
+
result.errors.push(`Source path is not an Obsidian vault: ${source}`);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
// In remote mode, destination can be any directory
|
|
51
|
+
// Otherwise, validate it's an Obsidian vault
|
|
52
|
+
if (!remoteMode) {
|
|
53
|
+
const destVault = new ObsidianVault({ vaultPath: destination });
|
|
54
|
+
const isDestVault = await destVault.isObsidianVault();
|
|
55
|
+
if (!isDestVault) {
|
|
56
|
+
result.errors.push(`Destination path is not an Obsidian vault: ${destination}. Use --remote flag for non-vault destinations.`);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Create destination directory if it doesn't exist
|
|
62
|
+
await fs.mkdir(destination, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
// Load manifests (state from last sync)
|
|
65
|
+
const sourceManifestMgr = new ManifestManager(source);
|
|
66
|
+
const destManifestMgr = new ManifestManager(destination);
|
|
67
|
+
const sourceManifest = await sourceManifestMgr.load();
|
|
68
|
+
const destManifest = await destManifestMgr.load();
|
|
69
|
+
// Get list of files from source vault
|
|
70
|
+
const sourceFiles = await sourceVault.listFiles();
|
|
71
|
+
// Get list of files from destination
|
|
72
|
+
// Use DirectoryLister for remote mode, ObsidianVault otherwise
|
|
73
|
+
let destFiles;
|
|
74
|
+
if (remoteMode) {
|
|
75
|
+
const destLister = new DirectoryLister({ path: destination });
|
|
76
|
+
destFiles = await destLister.listFiles();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const destVault = new ObsidianVault({ vaultPath: destination });
|
|
80
|
+
destFiles = await destVault.listFiles();
|
|
81
|
+
}
|
|
82
|
+
// Combine file lists (unique paths)
|
|
83
|
+
const allFiles = Array.from(new Set([...sourceFiles, ...destFiles]));
|
|
84
|
+
// Detect changes using three-way merge
|
|
85
|
+
const changes = await this.changeDetector.detectChanges(source, destination, sourceManifest, destManifest, allFiles);
|
|
86
|
+
// Separate conflicts from regular changes
|
|
87
|
+
const conflicts = changes.filter((c) => c.type === 'conflict');
|
|
88
|
+
const regularChanges = changes.filter((c) => c.type !== 'conflict');
|
|
89
|
+
// Resolve conflicts
|
|
90
|
+
if (conflicts.length > 0) {
|
|
91
|
+
const resolutions = await this.conflictResolver.resolveConflicts(conflicts, conflictResolution);
|
|
92
|
+
for (const resolution of resolutions) {
|
|
93
|
+
const conflict = conflicts.find((c) => c.path === resolution.path);
|
|
94
|
+
if (resolution.action === 'skip') {
|
|
95
|
+
result.skipped.push(resolution.path);
|
|
96
|
+
result.conflicts.push(conflict);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Convert resolved conflict to a regular change
|
|
100
|
+
// Set the appropriate state based on which side wins
|
|
101
|
+
const resolvedChange = {
|
|
102
|
+
path: conflict.path,
|
|
103
|
+
type: 'modified',
|
|
104
|
+
sourceState: resolution.action === 'use-source' ? conflict.sourceState : null,
|
|
105
|
+
destinationState: resolution.action === 'use-destination' ? conflict.destinationState : null,
|
|
106
|
+
baseState: conflict.baseState,
|
|
107
|
+
};
|
|
108
|
+
regularChanges.push(resolvedChange);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Apply changes
|
|
113
|
+
if (!dryRun) {
|
|
114
|
+
await this.applyChanges(source, destination, regularChanges, sourceManifestMgr, destManifestMgr, result);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Dry run - just count what would be done
|
|
118
|
+
for (const change of regularChanges) {
|
|
119
|
+
if (change.type === 'added') {
|
|
120
|
+
result.filesAdded++;
|
|
121
|
+
}
|
|
122
|
+
else if (change.type === 'modified') {
|
|
123
|
+
result.filesUpdated++;
|
|
124
|
+
}
|
|
125
|
+
else if (change.type === 'deleted') {
|
|
126
|
+
result.filesDeleted++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
result.errors.push(`Sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Apply file changes to destination
|
|
138
|
+
*/
|
|
139
|
+
async applyChanges(source, destination, changes, sourceManifestMgr, destManifestMgr, result) {
|
|
140
|
+
for (const change of changes) {
|
|
141
|
+
const sourcePath = path.join(source, change.path);
|
|
142
|
+
const destPath = path.join(destination, change.path);
|
|
143
|
+
try {
|
|
144
|
+
if (change.type === 'added' || change.type === 'modified') {
|
|
145
|
+
// Copy from source to destination (or vice versa)
|
|
146
|
+
const fromPath = change.sourceState ? sourcePath : destPath;
|
|
147
|
+
const toPath = change.sourceState ? destPath : sourcePath;
|
|
148
|
+
const content = await (change.sourceState
|
|
149
|
+
? this.sourceAdapter
|
|
150
|
+
: this.destAdapter).read(fromPath);
|
|
151
|
+
await (change.sourceState ? this.destAdapter : this.sourceAdapter).write(toPath, content);
|
|
152
|
+
// Update manifests
|
|
153
|
+
const hash = computeFileHash(content);
|
|
154
|
+
const stats = await (change.sourceState
|
|
155
|
+
? this.sourceAdapter
|
|
156
|
+
: this.destAdapter).exists(fromPath);
|
|
157
|
+
if (stats) {
|
|
158
|
+
const fileState = {
|
|
159
|
+
hash,
|
|
160
|
+
size: content.length,
|
|
161
|
+
mtime: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
await sourceManifestMgr.updateFileState(change.path, fileState);
|
|
164
|
+
await destManifestMgr.updateFileState(change.path, fileState);
|
|
165
|
+
}
|
|
166
|
+
if (change.type === 'added') {
|
|
167
|
+
result.filesAdded++;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
result.filesUpdated++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (change.type === 'deleted') {
|
|
174
|
+
// Determine which side the file was deleted from and propagate
|
|
175
|
+
// If sourceState is null, file was deleted from source -> delete from dest
|
|
176
|
+
// If destinationState is null, file was deleted from dest -> delete from source
|
|
177
|
+
if (!change.sourceState && change.destinationState) {
|
|
178
|
+
// Deleted from source, remove from destination
|
|
179
|
+
await this.destAdapter.delete(destPath);
|
|
180
|
+
}
|
|
181
|
+
else if (change.sourceState && !change.destinationState) {
|
|
182
|
+
// Deleted from destination, remove from source
|
|
183
|
+
await this.sourceAdapter.delete(sourcePath);
|
|
184
|
+
}
|
|
185
|
+
// Remove from manifests
|
|
186
|
+
await sourceManifestMgr.removeFileState(change.path);
|
|
187
|
+
await destManifestMgr.removeFileState(change.path);
|
|
188
|
+
result.filesDeleted++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
result.errors.push(`Failed to sync ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Detect changes between source and destination
|
|
198
|
+
*/
|
|
199
|
+
async detectChanges(source, destination) {
|
|
200
|
+
const sourceVault = new ObsidianVault({ vaultPath: source });
|
|
201
|
+
const destVault = new ObsidianVault({ vaultPath: destination });
|
|
202
|
+
const sourceManifestMgr = new ManifestManager(source);
|
|
203
|
+
const destManifestMgr = new ManifestManager(destination);
|
|
204
|
+
const sourceManifest = await sourceManifestMgr.load();
|
|
205
|
+
const destManifest = await destManifestMgr.load();
|
|
206
|
+
const sourceFiles = await sourceVault.listFiles();
|
|
207
|
+
const destFiles = await destVault.listFiles();
|
|
208
|
+
const allFiles = Array.from(new Set([...sourceFiles, ...destFiles]));
|
|
209
|
+
return this.changeDetector.detectChanges(source, destination, sourceManifest, destManifest, allFiles);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync manifest manager for tracking sync state.
|
|
3
|
+
* Used for three-way merge algorithm.
|
|
4
|
+
*/
|
|
5
|
+
import type { SyncManifest, FileState } from './types.js';
|
|
6
|
+
export declare class ManifestManager {
|
|
7
|
+
private vaultPath;
|
|
8
|
+
private static readonly MANIFEST_DIR;
|
|
9
|
+
private static readonly MANIFEST_FILE;
|
|
10
|
+
constructor(vaultPath: string);
|
|
11
|
+
/**
|
|
12
|
+
* Load manifest from vault, returns null if doesn't exist
|
|
13
|
+
*/
|
|
14
|
+
load(): Promise<SyncManifest | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Save manifest to vault (atomic write using temp file + rename)
|
|
17
|
+
*/
|
|
18
|
+
save(manifest: SyncManifest): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Get file state from manifest
|
|
21
|
+
*/
|
|
22
|
+
getFileState(relativePath: string): Promise<FileState | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Update file state in manifest
|
|
25
|
+
*/
|
|
26
|
+
updateFileState(relativePath: string, state: FileState): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Remove file state from manifest (when file is deleted)
|
|
29
|
+
*/
|
|
30
|
+
removeFileState(relativePath: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Create a new empty manifest with UUID
|
|
33
|
+
*/
|
|
34
|
+
createEmptyManifest(): SyncManifest;
|
|
35
|
+
/**
|
|
36
|
+
* Get full path to manifest file
|
|
37
|
+
*/
|
|
38
|
+
private getManifestPath;
|
|
39
|
+
/**
|
|
40
|
+
* Check if manifest exists
|
|
41
|
+
*/
|
|
42
|
+
exists(): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Delete manifest file
|
|
45
|
+
*/
|
|
46
|
+
delete(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/sync/manifest.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1D,qBAAa,eAAe;IAId,OAAO,CAAC,SAAS;IAH7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAa;IACjD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAwB;gBAEzC,SAAS,EAAE,MAAM;IAErC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAiB1C;;OAEG;IACG,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjD;;OAEG;IACG,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAQnE;;OAEG;IACG,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5E;;OAEG;IACG,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D;;OAEG;IACH,mBAAmB,IAAI,YAAY;IASnC;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAUhC;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAU9B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync manifest manager for tracking sync state.
|
|
3
|
+
* Used for three-way merge algorithm.
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
export class ManifestManager {
|
|
9
|
+
vaultPath;
|
|
10
|
+
static MANIFEST_DIR = '.obsync';
|
|
11
|
+
static MANIFEST_FILE = 'sync-manifest.json';
|
|
12
|
+
constructor(vaultPath) {
|
|
13
|
+
this.vaultPath = vaultPath;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load manifest from vault, returns null if doesn't exist
|
|
17
|
+
*/
|
|
18
|
+
async load() {
|
|
19
|
+
const manifestPath = this.getManifestPath();
|
|
20
|
+
try {
|
|
21
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
22
|
+
const manifest = JSON.parse(content);
|
|
23
|
+
return manifest;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Save manifest to vault (atomic write using temp file + rename)
|
|
34
|
+
*/
|
|
35
|
+
async save(manifest) {
|
|
36
|
+
const manifestPath = this.getManifestPath();
|
|
37
|
+
const manifestDir = path.dirname(manifestPath);
|
|
38
|
+
const tempPath = `${manifestPath}.tmp`;
|
|
39
|
+
try {
|
|
40
|
+
// Ensure .obsync directory exists
|
|
41
|
+
await fs.mkdir(manifestDir, { recursive: true });
|
|
42
|
+
// Write to temp file first
|
|
43
|
+
await fs.writeFile(tempPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
44
|
+
// Atomic rename
|
|
45
|
+
await fs.rename(tempPath, manifestPath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Clean up temp file if it exists
|
|
49
|
+
try {
|
|
50
|
+
await fs.unlink(tempPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Ignore cleanup errors
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Failed to save manifest: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get file state from manifest
|
|
60
|
+
*/
|
|
61
|
+
async getFileState(relativePath) {
|
|
62
|
+
const manifest = await this.load();
|
|
63
|
+
if (!manifest) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return manifest.files[relativePath] ?? null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Update file state in manifest
|
|
70
|
+
*/
|
|
71
|
+
async updateFileState(relativePath, state) {
|
|
72
|
+
let manifest = await this.load();
|
|
73
|
+
if (!manifest) {
|
|
74
|
+
manifest = this.createEmptyManifest();
|
|
75
|
+
}
|
|
76
|
+
manifest.files[relativePath] = state;
|
|
77
|
+
manifest.lastSync = new Date().toISOString();
|
|
78
|
+
await this.save(manifest);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Remove file state from manifest (when file is deleted)
|
|
82
|
+
*/
|
|
83
|
+
async removeFileState(relativePath) {
|
|
84
|
+
const manifest = await this.load();
|
|
85
|
+
if (!manifest) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
delete manifest.files[relativePath];
|
|
89
|
+
manifest.lastSync = new Date().toISOString();
|
|
90
|
+
await this.save(manifest);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create a new empty manifest with UUID
|
|
94
|
+
*/
|
|
95
|
+
createEmptyManifest() {
|
|
96
|
+
return {
|
|
97
|
+
version: '1.0',
|
|
98
|
+
lastSync: new Date().toISOString(),
|
|
99
|
+
syncId: uuidv4(),
|
|
100
|
+
files: {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get full path to manifest file
|
|
105
|
+
*/
|
|
106
|
+
getManifestPath() {
|
|
107
|
+
return path.join(this.vaultPath, ManifestManager.MANIFEST_DIR, ManifestManager.MANIFEST_FILE);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if manifest exists
|
|
111
|
+
*/
|
|
112
|
+
async exists() {
|
|
113
|
+
const manifestPath = this.getManifestPath();
|
|
114
|
+
try {
|
|
115
|
+
await fs.access(manifestPath);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Delete manifest file
|
|
124
|
+
*/
|
|
125
|
+
async delete() {
|
|
126
|
+
const manifestPath = this.getManifestPath();
|
|
127
|
+
try {
|
|
128
|
+
await fs.unlink(manifestPath);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error.code !== 'ENOENT') {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/sync/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for sync operations
|
|
3
|
+
*/
|
|
4
|
+
export interface FileState {
|
|
5
|
+
hash: string;
|
|
6
|
+
size: number;
|
|
7
|
+
mtime: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SyncManifest {
|
|
10
|
+
version: string;
|
|
11
|
+
lastSync: string;
|
|
12
|
+
syncId: string;
|
|
13
|
+
files: Record<string, FileState>;
|
|
14
|
+
}
|
|
15
|
+
export type ChangeType = 'added' | 'modified' | 'deleted' | 'conflict';
|
|
16
|
+
export interface FileChange {
|
|
17
|
+
path: string;
|
|
18
|
+
type: ChangeType;
|
|
19
|
+
sourceState?: FileState | null;
|
|
20
|
+
destinationState?: FileState | null;
|
|
21
|
+
baseState?: FileState | null;
|
|
22
|
+
}
|
|
23
|
+
export type ConflictStrategy = 'source' | 'destination' | 'newest' | 'skip';
|
|
24
|
+
export interface SyncOptions {
|
|
25
|
+
source: string;
|
|
26
|
+
destination: string;
|
|
27
|
+
strategy?: 'incremental' | 'full';
|
|
28
|
+
conflictResolution?: ConflictStrategy;
|
|
29
|
+
dryRun?: boolean;
|
|
30
|
+
/** Allow destination to be a non-vault directory (e.g., Dropbox folder) */
|
|
31
|
+
remoteMode?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface SyncResult {
|
|
34
|
+
filesAdded: number;
|
|
35
|
+
filesUpdated: number;
|
|
36
|
+
filesDeleted: number;
|
|
37
|
+
conflicts: FileChange[];
|
|
38
|
+
errors: string[];
|
|
39
|
+
skipped: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface WatchModeOptions {
|
|
42
|
+
/** Source vault path */
|
|
43
|
+
source: string;
|
|
44
|
+
/** Destination vault path */
|
|
45
|
+
destination: string;
|
|
46
|
+
/** Conflict resolution strategy */
|
|
47
|
+
conflictResolution?: ConflictStrategy;
|
|
48
|
+
/** Allow destination to be a non-vault directory (e.g., Dropbox folder) */
|
|
49
|
+
remoteMode?: boolean;
|
|
50
|
+
/** Debounce delay in milliseconds (default: 300) */
|
|
51
|
+
debounceMs?: number;
|
|
52
|
+
/** Batch delay - wait for multiple changes before syncing (default: 500) */
|
|
53
|
+
batchDelayMs?: number;
|
|
54
|
+
/** Maximum time to wait before forcing a sync (default: 5000) */
|
|
55
|
+
maxWaitMs?: number;
|
|
56
|
+
/** Whether to run initial sync on start (default: true) */
|
|
57
|
+
initialSync?: boolean;
|
|
58
|
+
/** Callback for sync events */
|
|
59
|
+
onSync?: (result: SyncResult) => void;
|
|
60
|
+
/** Callback for errors */
|
|
61
|
+
onError?: (error: Error) => void;
|
|
62
|
+
/** Callback for status changes */
|
|
63
|
+
onStatusChange?: (status: WatchModeStatus) => void;
|
|
64
|
+
}
|
|
65
|
+
export type WatchModeState = 'idle' | 'watching' | 'syncing' | 'error' | 'stopped';
|
|
66
|
+
export interface WatchModeStatus {
|
|
67
|
+
/** Current state of watch mode */
|
|
68
|
+
state: WatchModeState;
|
|
69
|
+
/** Whether watch mode is active */
|
|
70
|
+
isActive: boolean;
|
|
71
|
+
/** Number of pending file changes */
|
|
72
|
+
pendingChanges: number;
|
|
73
|
+
/** Last sync time */
|
|
74
|
+
lastSyncTime: Date | null;
|
|
75
|
+
/** Last sync result */
|
|
76
|
+
lastSyncResult: SyncResult | null;
|
|
77
|
+
/** Number of syncs performed in this session */
|
|
78
|
+
syncCount: number;
|
|
79
|
+
/** Total files synced in this session */
|
|
80
|
+
totalFilesSynced: number;
|
|
81
|
+
/** Current error (if state is 'error') */
|
|
82
|
+
currentError: Error | null;
|
|
83
|
+
/** Paths being watched */
|
|
84
|
+
watchedPaths: string[];
|
|
85
|
+
}
|
|
86
|
+
export interface WatchModeEvent {
|
|
87
|
+
type: 'sync_start' | 'sync_complete' | 'sync_error' | 'file_change' | 'status_change';
|
|
88
|
+
timestamp: Date;
|
|
89
|
+
data?: SyncResult | Error | WatchModeStatus;
|
|
90
|
+
}
|
|
91
|
+
export interface WatchModeSyncSession {
|
|
92
|
+
/** Unique session ID */
|
|
93
|
+
sessionId: string;
|
|
94
|
+
/** Session start time */
|
|
95
|
+
startTime: Date;
|
|
96
|
+
/** Session end time (null if still active) */
|
|
97
|
+
endTime: Date | null;
|
|
98
|
+
/** Total syncs in this session */
|
|
99
|
+
syncCount: number;
|
|
100
|
+
/** Total errors in this session */
|
|
101
|
+
errorCount: number;
|
|
102
|
+
/** All sync results in this session */
|
|
103
|
+
syncHistory: Array<{
|
|
104
|
+
timestamp: Date;
|
|
105
|
+
result: SyncResult;
|
|
106
|
+
duration: number;
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;AAEvE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,gBAAgB,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IAClC,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAMD,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,0BAA0B;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,kCAAkC;IAClC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEnF,MAAM,WAAW,eAAe;IAC9B,kCAAkC;IAClC,KAAK,EAAE,cAAc,CAAC;IACtB,mCAAmC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,uBAAuB;IACvB,cAAc,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,YAAY,EAAE,KAAK,GAAG,IAAI,CAAC;IAC3B,0BAA0B;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,GAAG,eAAe,GAAG,YAAY,GAAG,aAAa,GAAG,eAAe,CAAC;IACtF,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,UAAU,GAAG,KAAK,GAAG,eAAe,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,8CAA8C;IAC9C,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,EAAE,KAAK,CAAC;QACjB,SAAS,EAAE,IAAI,CAAC;QAChB,MAAM,EAAE,UAAU,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;CACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatchModeSync - Orchestrates continuous file watching and automatic synchronization.
|
|
3
|
+
* Coordinates between FileWatcher and SyncEngine for real-time vault synchronization.
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
import type { WatchModeOptions, WatchModeStatus, WatchModeSyncSession, SyncResult } from './types.js';
|
|
7
|
+
export declare class WatchModeSync extends EventEmitter {
|
|
8
|
+
private watcher;
|
|
9
|
+
private syncEngine;
|
|
10
|
+
private options;
|
|
11
|
+
private state;
|
|
12
|
+
private syncCount;
|
|
13
|
+
private totalFilesSynced;
|
|
14
|
+
private lastSyncTime;
|
|
15
|
+
private lastSyncResult;
|
|
16
|
+
private currentError;
|
|
17
|
+
private isSyncing;
|
|
18
|
+
private pendingSyncRequest;
|
|
19
|
+
private session;
|
|
20
|
+
private maxWaitTimer;
|
|
21
|
+
private pendingChangesCount;
|
|
22
|
+
constructor(options: WatchModeOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Start watch mode synchronization
|
|
25
|
+
*/
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Stop watch mode synchronization
|
|
29
|
+
*/
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Pause watch mode (stop watching but keep state)
|
|
33
|
+
*/
|
|
34
|
+
pause(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Resume watch mode after pause
|
|
37
|
+
*/
|
|
38
|
+
resume(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Force an immediate sync
|
|
41
|
+
*/
|
|
42
|
+
forceSync(): Promise<SyncResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Get current status
|
|
45
|
+
*/
|
|
46
|
+
getStatus(): WatchModeStatus;
|
|
47
|
+
/**
|
|
48
|
+
* Get current session information
|
|
49
|
+
*/
|
|
50
|
+
getSession(): WatchModeSyncSession | null;
|
|
51
|
+
/**
|
|
52
|
+
* Handle batched file changes
|
|
53
|
+
*/
|
|
54
|
+
private handleBatchChanges;
|
|
55
|
+
/**
|
|
56
|
+
* Trigger a sync operation
|
|
57
|
+
*/
|
|
58
|
+
private triggerSync;
|
|
59
|
+
/**
|
|
60
|
+
* Perform the actual sync operation
|
|
61
|
+
*/
|
|
62
|
+
private performSync;
|
|
63
|
+
/**
|
|
64
|
+
* Handle errors
|
|
65
|
+
*/
|
|
66
|
+
private handleError;
|
|
67
|
+
/**
|
|
68
|
+
* Set state and emit status change
|
|
69
|
+
*/
|
|
70
|
+
private setState;
|
|
71
|
+
/**
|
|
72
|
+
* Emit status change event
|
|
73
|
+
*/
|
|
74
|
+
private emitStatusChange;
|
|
75
|
+
/**
|
|
76
|
+
* Emit an event
|
|
77
|
+
*/
|
|
78
|
+
private emitEvent;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a watch mode sync instance with default options
|
|
82
|
+
*/
|
|
83
|
+
export declare function createWatchModeSync(options: WatchModeOptions): WatchModeSync;
|
|
84
|
+
//# sourceMappingURL=watchMode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchMode.d.ts","sourceRoot":"","sources":["../../src/sync/watchMode.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAGf,oBAAoB,EACpB,UAAU,EAEX,MAAM,YAAY,CAAC;AAEpB,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAIb;IACF,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAAa;gBAE5B,OAAO,EAAE,gBAAgB;IAmBrC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6F5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAyC7B;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAItC;;OAEG;IACH,SAAS,IAAI,eAAe;IAc5B;;OAEG;IACH,UAAU,IAAI,oBAAoB,GAAG,IAAI;IAIzC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAgB1B;;OAEG;IACH,OAAO,CAAC,WAAW;IAWnB;;OAEG;YACW,WAAW;IAiFzB;;OAEG;IACH,OAAO,CAAC,WAAW;IAOnB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAOhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAMxB;;OAEG;IACH,OAAO,CAAC,SAAS;CAMlB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAE5E"}
|