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.
Files changed (102) hide show
  1. package/README.md +920 -0
  2. package/dist/cli/commands/init.d.ts +8 -0
  3. package/dist/cli/commands/init.d.ts.map +1 -0
  4. package/dist/cli/commands/init.js +104 -0
  5. package/dist/cli/commands/init.js.map +1 -0
  6. package/dist/cli/commands/status.d.ts +8 -0
  7. package/dist/cli/commands/status.d.ts.map +1 -0
  8. package/dist/cli/commands/status.js +117 -0
  9. package/dist/cli/commands/status.js.map +1 -0
  10. package/dist/cli/commands/sync.d.ts +13 -0
  11. package/dist/cli/commands/sync.d.ts.map +1 -0
  12. package/dist/cli/commands/sync.js +225 -0
  13. package/dist/cli/commands/sync.js.map +1 -0
  14. package/dist/cli/prompts/conflictPrompt.d.ts +10 -0
  15. package/dist/cli/prompts/conflictPrompt.d.ts.map +1 -0
  16. package/dist/cli/prompts/conflictPrompt.js +51 -0
  17. package/dist/cli/prompts/conflictPrompt.js.map +1 -0
  18. package/dist/cli/prompts/fileBrowser.d.ts +6 -0
  19. package/dist/cli/prompts/fileBrowser.d.ts.map +1 -0
  20. package/dist/cli/prompts/fileBrowser.js +91 -0
  21. package/dist/cli/prompts/fileBrowser.js.map +1 -0
  22. package/dist/cli/prompts/vaultSelector.d.ts +13 -0
  23. package/dist/cli/prompts/vaultSelector.d.ts.map +1 -0
  24. package/dist/cli/prompts/vaultSelector.js +63 -0
  25. package/dist/cli/prompts/vaultSelector.js.map +1 -0
  26. package/dist/cli/ui/colors.d.ts +50 -0
  27. package/dist/cli/ui/colors.d.ts.map +1 -0
  28. package/dist/cli/ui/colors.js +62 -0
  29. package/dist/cli/ui/colors.js.map +1 -0
  30. package/dist/cli/ui/output.d.ts +45 -0
  31. package/dist/cli/ui/output.d.ts.map +1 -0
  32. package/dist/cli/ui/output.js +116 -0
  33. package/dist/cli/ui/output.js.map +1 -0
  34. package/dist/cli/ui/spinner.d.ts +29 -0
  35. package/dist/cli/ui/spinner.d.ts.map +1 -0
  36. package/dist/cli/ui/spinner.js +80 -0
  37. package/dist/cli/ui/spinner.js.map +1 -0
  38. package/dist/cli/ui/table.d.ts +28 -0
  39. package/dist/cli/ui/table.d.ts.map +1 -0
  40. package/dist/cli/ui/table.js +123 -0
  41. package/dist/cli/ui/table.js.map +1 -0
  42. package/dist/cli/utils/terminal.d.ts +21 -0
  43. package/dist/cli/utils/terminal.d.ts.map +1 -0
  44. package/dist/cli/utils/terminal.js +59 -0
  45. package/dist/cli/utils/terminal.js.map +1 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +32 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/config/index.d.ts +45 -0
  51. package/dist/config/index.d.ts.map +1 -0
  52. package/dist/config/index.js +112 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/storage/index.d.ts +35 -0
  59. package/dist/storage/index.d.ts.map +1 -0
  60. package/dist/storage/index.js +97 -0
  61. package/dist/storage/index.js.map +1 -0
  62. package/dist/sync/changeDetector.d.ts +29 -0
  63. package/dist/sync/changeDetector.d.ts.map +1 -0
  64. package/dist/sync/changeDetector.js +259 -0
  65. package/dist/sync/changeDetector.js.map +1 -0
  66. package/dist/sync/conflictResolver.d.ts +29 -0
  67. package/dist/sync/conflictResolver.d.ts.map +1 -0
  68. package/dist/sync/conflictResolver.js +116 -0
  69. package/dist/sync/conflictResolver.js.map +1 -0
  70. package/dist/sync/directoryLister.d.ts +18 -0
  71. package/dist/sync/directoryLister.d.ts.map +1 -0
  72. package/dist/sync/directoryLister.js +39 -0
  73. package/dist/sync/directoryLister.js.map +1 -0
  74. package/dist/sync/index.d.ts +29 -0
  75. package/dist/sync/index.d.ts.map +1 -0
  76. package/dist/sync/index.js +212 -0
  77. package/dist/sync/index.js.map +1 -0
  78. package/dist/sync/manifest.d.ts +48 -0
  79. package/dist/sync/manifest.d.ts.map +1 -0
  80. package/dist/sync/manifest.js +137 -0
  81. package/dist/sync/manifest.js.map +1 -0
  82. package/dist/sync/types.d.ts +109 -0
  83. package/dist/sync/types.d.ts.map +1 -0
  84. package/dist/sync/types.js +5 -0
  85. package/dist/sync/types.js.map +1 -0
  86. package/dist/sync/watchMode.d.ts +84 -0
  87. package/dist/sync/watchMode.d.ts.map +1 -0
  88. package/dist/sync/watchMode.js +364 -0
  89. package/dist/sync/watchMode.js.map +1 -0
  90. package/dist/sync/watcher.d.ts +114 -0
  91. package/dist/sync/watcher.d.ts.map +1 -0
  92. package/dist/sync/watcher.js +293 -0
  93. package/dist/sync/watcher.js.map +1 -0
  94. package/dist/utils/index.d.ts +8 -0
  95. package/dist/utils/index.d.ts.map +1 -0
  96. package/dist/utils/index.js +25 -0
  97. package/dist/utils/index.js.map +1 -0
  98. package/dist/vault/index.d.ts +38 -0
  99. package/dist/vault/index.d.ts.map +1 -0
  100. package/dist/vault/index.js +106 -0
  101. package/dist/vault/index.js.map +1 -0
  102. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,920 @@
1
+ # obsyncd
2
+
3
+ A bidirectional synchronization tool for Obsidian vaults with conflict detection and resolution. Sync your vaults between computers via Dropbox, Google Drive, iCloud, or any shared folder.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g obsyncd
10
+
11
+ # Initialize your vault
12
+ cd ~/Obsidian/MyVault
13
+ obsyncd init
14
+
15
+ # Sync to Dropbox/Google Drive folder
16
+ obsyncd sync -s ~/Obsidian/MyVault -d ~/Dropbox/ObsidianSync --remote
17
+
18
+ # Continuous sync (watch mode)
19
+ obsyncd sync -s ~/Obsidian/MyVault -d ~/Dropbox/ObsidianSync --remote --watch
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Table of Contents
25
+
26
+ - [User Documentation](#user-documentation)
27
+ - [Installation](#installation)
28
+ - [Syncing via Dropbox/Google Drive](#syncing-via-dropboxgoogle-drive)
29
+ - [Commands](#commands)
30
+ - [Conflict Resolution](#conflict-resolution)
31
+ - [Developer Documentation](#developer-documentation)
32
+ - [Architecture Overview](#architecture-overview)
33
+ - [Development Setup](#development-setup)
34
+ - [Testing](#testing)
35
+ - [Contributing](#contributing)
36
+
37
+ ---
38
+
39
+ # User Documentation
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ # Install globally via npm
45
+ npm install -g obsyncd
46
+
47
+ # Or run directly with npx
48
+ npx obsyncd --help
49
+ ```
50
+
51
+ ## Syncing via Dropbox/Google Drive
52
+
53
+ obsyncd makes it easy to sync your Obsidian vaults between computers using any cloud storage service (Dropbox, Google Drive, iCloud, OneDrive, etc.).
54
+
55
+ ### Why not just put the vault in Dropbox directly?
56
+
57
+ Syncing an Obsidian vault directly with cloud storage causes problems:
58
+ - **Constant conflicts** - Obsidian writes to workspace files constantly
59
+ - **Corrupted vaults** - Simultaneous edits on different devices cause issues
60
+ - **"Conflicted copy" files** - You end up with duplicates everywhere
61
+
62
+ obsyncd solves this by:
63
+ - Syncing only your notes (not Obsidian's internal files)
64
+ - Letting you control when sync happens
65
+ - Smart conflict resolution when the same file is edited on both sides
66
+
67
+ ### Architecture
68
+
69
+ ```
70
+ Work Computer Cloud Personal Computer
71
+ +------------------+ +-------------+ +------------------+
72
+ | ~/Obsidian/ | | Dropbox | | ~/Obsidian/ |
73
+ | MyVault/ |<------->| ObsidianSync|<------->| MyVault/ |
74
+ | (Obsidian vault) | obsyncd| (sync folder| Dropbox | (Obsidian vault) |
75
+ +------------------+ +-------------+ +------------------+
76
+ ```
77
+
78
+ ### Setup (One-time per computer)
79
+
80
+ **On your first computer (e.g., work):**
81
+
82
+ ```bash
83
+ # 1. Install obsyncd
84
+ npm install -g obsyncd
85
+
86
+ # 2. Initialize your vault
87
+ cd ~/Obsidian/MyVault
88
+ obsyncd init
89
+
90
+ # 3. Create a sync folder in Dropbox/Google Drive
91
+ mkdir -p ~/Dropbox/ObsidianSync
92
+
93
+ # 4. Push your vault to the sync folder
94
+ obsyncd sync -s ~/Obsidian/MyVault -d ~/Dropbox/ObsidianSync --remote
95
+ ```
96
+
97
+ **On your second computer (e.g., personal):**
98
+
99
+ ```bash
100
+ # 1. Install obsyncd
101
+ npm install -g obsyncd
102
+
103
+ # 2. Create your local vault folder
104
+ mkdir -p ~/Obsidian/MyVault
105
+
106
+ # 3. Wait for Dropbox to sync, then pull
107
+ obsyncd sync -s ~/Dropbox/ObsidianSync -d ~/Obsidian/MyVault --remote
108
+
109
+ # 4. Initialize the vault
110
+ cd ~/Obsidian/MyVault
111
+ obsyncd init
112
+ ```
113
+
114
+ ### Daily Workflow
115
+
116
+ **Option A: Manual sync**
117
+
118
+ ```bash
119
+ # Before starting work - pull latest changes
120
+ obsyncd sync -s ~/Dropbox/ObsidianSync -d ~/Obsidian/MyVault --remote
121
+
122
+ # After finishing work - push your changes
123
+ obsyncd sync -s ~/Obsidian/MyVault -d ~/Dropbox/ObsidianSync --remote
124
+ ```
125
+
126
+ **Option B: Continuous sync (watch mode)**
127
+
128
+ ```bash
129
+ # Start watch mode - syncs automatically on file changes
130
+ obsyncd sync -s ~/Obsidian/MyVault -d ~/Dropbox/ObsidianSync --remote --watch
131
+
132
+ # Press Ctrl+C to stop
133
+ ```
134
+
135
+ ## Commands
136
+
137
+ ### `obsyncd init`
138
+
139
+ Initialize a vault for syncing. Creates `.obsync.json` config and `.obsync/sync-manifest.json`.
140
+
141
+ ```bash
142
+ obsyncd init # Current directory
143
+ obsyncd init --path ~/MyVault # Specific path
144
+ ```
145
+
146
+ ### `obsyncd sync`
147
+
148
+ Synchronize files between source and destination.
149
+
150
+ ```bash
151
+ obsyncd sync -s <source> -d <destination> [options]
152
+
153
+ Options:
154
+ -s, --source <path> Source vault path
155
+ -d, --destination <path> Destination path
156
+ -r, --remote Allow non-vault destination (for Dropbox folders)
157
+ -c, --conflict <strategy> Conflict resolution: newest|source|destination|skip
158
+ -w, --watch Continuous sync mode
159
+ --dry-run Show what would sync without making changes
160
+ ```
161
+
162
+ ### `obsyncd status`
163
+
164
+ Show sync status of a vault.
165
+
166
+ ```bash
167
+ obsyncd status # Current directory
168
+ obsyncd status --path ~/MyVault # Specific path
169
+ ```
170
+
171
+ ## Conflict Resolution
172
+
173
+ When the same file is modified on both sides, obsyncd can resolve conflicts automatically:
174
+
175
+ | Strategy | Description |
176
+ |----------|-------------|
177
+ | `newest` (default) | Use the version with the most recent modification time |
178
+ | `source` | Always use the source version |
179
+ | `destination` | Always use the destination version |
180
+ | `skip` | Skip conflicting files for manual review |
181
+
182
+ ```bash
183
+ obsyncd sync -s ~/vault -d ~/sync --remote --conflict newest
184
+ ```
185
+
186
+ ---
187
+
188
+ # Developer Documentation
189
+
190
+ ## What is obsync?
191
+
192
+ obsync is a TypeScript/Node.js CLI tool designed to synchronize Obsidian vaults bidirectionally across devices with intelligent conflict detection. Unlike traditional one-way sync tools, obsync implements a **three-way merge algorithm** to detect changes on both source and destination, ensuring no data loss during synchronization.
193
+
194
+ ### Key Design Goals
195
+
196
+ 1. **Cloud-Agnostic**: Works with local filesystems initially, with planned support for Google Drive, UploadThing, S3, and other cloud providers
197
+ 2. **Bidirectional Sync**: Changes on either side are detected and propagated
198
+ 3. **Conflict-Aware**: Detects when the same file is modified on both sides
199
+ 4. **Obsidian-Specific**: Understands Obsidian vault structure and excludes system files (`.obsidian/workspace*`, `.trash/`)
200
+ 5. **Extensible Architecture**: Storage adapter pattern allows easy addition of new backends
201
+
202
+ ## Planned Features
203
+
204
+ ### Phase 1: Local Sync ✅ COMPLETE
205
+ - ✅ Local filesystem storage adapter
206
+ - ✅ Vault detection and file listing
207
+ - ✅ Sync manifest management (state tracking)
208
+ - ✅ Three-way merge change detection
209
+ - ✅ Conflict resolution strategies (newest, source, destination, skip)
210
+ - ✅ Core sync engine
211
+ - ✅ CLI commands (`init`, `sync`, `status`)
212
+
213
+ ### Phase 2: Watch Mode ✅ COMPLETE
214
+ - ✅ Continuous file monitoring with chokidar
215
+ - ✅ Automatic sync on file changes
216
+ - ✅ Debounced change detection
217
+ - ✅ Batched file change processing
218
+ - ✅ Graceful start/stop/pause/resume
219
+ - ✅ Real-time bidirectional sync
220
+ - ✅ Session tracking and statistics
221
+
222
+ ### Phase 3: Cloud Storage (Future)
223
+ - ⏳ Google Drive adapter
224
+ - ⏳ UploadThing adapter
225
+ - ⏳ AWS S3 adapter
226
+ - ⏳ WebDAV support
227
+
228
+ ## Installation
229
+
230
+ Phases 1 and 2 are complete and ready for local testing:
231
+
232
+ ```bash
233
+ # Clone and build
234
+ git clone <repo-url>
235
+ cd obsync
236
+ bun install
237
+ bun run build
238
+
239
+ # Link for global use (optional)
240
+ bun link
241
+ ```
242
+
243
+ ### Usage
244
+
245
+ ```bash
246
+ # Initialize a vault for syncing
247
+ obsync init --path ./my-vault
248
+
249
+ # Sync between two local vaults
250
+ obsync sync --source ~/work-vault --destination ~/personal-vault
251
+
252
+ # Sync with conflict resolution strategy
253
+ obsync sync --source ./vault-a --destination ./vault-b --conflict newest
254
+
255
+ # Continuous sync with watch mode
256
+ obsync sync --source ./vault --destination ./backup --watch
257
+
258
+ # Check sync status
259
+ obsync status --path ./my-vault
260
+ ```
261
+
262
+ ---
263
+
264
+ # Developer Documentation
265
+
266
+ ## Architecture Overview
267
+
268
+ obsync uses a **modular, layered architecture** with clear separation of concerns:
269
+
270
+ ```
271
+ ┌─────────────────────────────────────────────────────────────┐
272
+ │ CLI Layer (src/cli.ts) │
273
+ │ Commander.js interface, command parsing │
274
+ └────────────────────────┬────────────────────────────────────┘
275
+
276
+ ┌────────────────────────┴────────────────────────────────────┐
277
+ │ Orchestration Layer (src/sync/) │
278
+ │ SyncEngine, ChangeDetector, ConflictResolver, Manifest │
279
+ └────────────────────────┬────────────────────────────────────┘
280
+
281
+ ┌────────────────┼────────────────┐
282
+ │ │ │
283
+ ┌───────▼──────┐ ┌──────▼──────┐ ┌─────▼──────┐
284
+ │ Storage │ │ Vault │ │ Config │
285
+ │ (src/storage)│ │ (src/vault) │ │(src/config)│
286
+ │ Adapters │ │ Operations │ │ Management │
287
+ └──────────────┘ └─────────────┘ └────────────┘
288
+
289
+ ┌───────▼──────────────────────────────────────┐
290
+ │ Utilities (src/utils/) │
291
+ │ Hash computation, path normalization, etc. │
292
+ └──────────────────────────────────────────────┘
293
+ ```
294
+
295
+ ### Core Design Patterns
296
+
297
+ 1. **Storage Adapter Pattern**: All storage backends implement `StorageAdapter` interface
298
+ - Enables plug-and-play cloud storage support
299
+ - No changes to sync logic when adding new backends
300
+
301
+ 2. **Three-Way Merge Algorithm**: Tracks three states (source, destination, base)
302
+ - Base state stored in sync manifest (`.obsync/sync-manifest.json`)
303
+ - Enables conflict detection without false positives
304
+
305
+ 3. **Strategy Pattern**: Pluggable conflict resolution strategies
306
+ - `newest`: Use most recent modification time
307
+ - `source`: Always prefer source version
308
+ - `destination`: Always prefer destination version
309
+ - `skip`: Skip conflicts, log for manual review
310
+
311
+ ## Implementation Status
312
+
313
+ ### ✓ Phase 1: Local Storage Adapter (COMPLETE)
314
+
315
+ **File**: `src/storage/index.ts`
316
+
317
+ **Status**: 100% implemented, 22 unit tests passing
318
+
319
+ **Implementation Details**:
320
+ - Uses Node.js `fs/promises` for async filesystem operations
321
+ - All paths normalized via `sanitizePath()` utility (backslash → forward slash)
322
+ - Parent directories created automatically on write
323
+ - Graceful error handling (e.g., delete succeeds even if file doesn't exist)
324
+ - Recursive directory traversal for `list()` operation
325
+ - Returns sorted file lists for consistency
326
+
327
+ **Methods**:
328
+ - `read(path: string): Promise<Buffer>` - Read file contents
329
+ - `write(path: string, content: Buffer): Promise<void>` - Write with dir creation
330
+ - `delete(path: string): Promise<void>` - Delete file (idempotent)
331
+ - `list(prefix: string): Promise<string[]>` - Recursive listing, relative paths
332
+ - `exists(path: string): Promise<boolean>` - Check file/directory existence
333
+
334
+ **Error Handling**:
335
+ - `ENOENT` (file not found) → Clear error message on read, silent success on delete
336
+ - Permission errors → Propagated to caller
337
+ - Non-existent directories in `list()` → Returns empty array
338
+
339
+ **Performance Characteristics**:
340
+ - `list()`: O(n) where n = total files in directory tree
341
+ - `read()`: O(1) filesystem operation
342
+ - `write()`: O(d) where d = directory depth (mkdir operations)
343
+
344
+ ### ✓ Phase 2: Vault Operations (COMPLETE)
345
+
346
+ **File**: `src/vault/index.ts`
347
+
348
+ **Status**: 100% implemented, 22 unit tests passing
349
+
350
+ **Implementation Details**:
351
+ - Uses `picomatch` library for glob pattern matching (fast, widely-used)
352
+ - Default exclusions: `.obsidian/**`, `.trash/**`, `.git/**`, `.obsync/**`
353
+ - Supports both include and exclude patterns
354
+ - Computes metadata by aggregating stats from all files
355
+
356
+ **Methods**:
357
+ - `isObsidianVault(path?: string): Promise<boolean>` - Check for `.obsidian` directory
358
+ - `validateStructure(): Promise<boolean>` - Verify vault has config files
359
+ - `listFiles(): Promise<string[]>` - List with pattern filtering
360
+ - `getMetadata(): Promise<VaultMetadata>` - Extract file count, size, last modified
361
+
362
+ **Pattern Matching Algorithm**:
363
+ 1. Get all files via `LocalStorageAdapter.list()`
364
+ 2. Apply include patterns (if specified) - file must match at least one
365
+ 3. Apply exclude patterns (defaults + custom) - file must not match any
366
+ 4. Return filtered, sorted list
367
+
368
+ **Metadata Extraction**:
369
+ - **File Count**: Count of files after pattern filtering
370
+ - **Total Size**: Sum of all file sizes (via `fs.stat()`)
371
+ - **Last Modified**: Most recent `mtime` across all files
372
+ - Gracefully skips files that can't be read (permissions, etc.)
373
+
374
+ **Vault Detection Logic**:
375
+ - **isObsidianVault**: Checks for `.obsidian/` directory
376
+ - **validateStructure**: Checks for config files (`app.json`, `appearance.json`, `config`)
377
+ - New vaults (empty `.obsidian/`) are considered valid
378
+
379
+ ### ⏳ Phase 3: Sync Manifest Manager (NOT YET IMPLEMENTED)
380
+
381
+ **Planned File**: `src/sync/manifest.ts`
382
+
383
+ **Purpose**: Track sync state for three-way merge algorithm
384
+
385
+ **Manifest Schema**:
386
+ ```typescript
387
+ {
388
+ "version": "1.0",
389
+ "lastSync": "2026-01-09T10:30:00Z",
390
+ "syncId": "uuid-v4", // Identifies sync pair
391
+ "files": {
392
+ "relative/path.md": {
393
+ "hash": "sha256-hash",
394
+ "size": 1234,
395
+ "mtime": "2026-01-09T10:29:00Z"
396
+ }
397
+ }
398
+ }
399
+ ```
400
+
401
+ **Storage Location**: `{vault}/.obsync/sync-manifest.json`
402
+
403
+ **Planned Methods**:
404
+ - `load(): Promise<SyncManifest | null>` - Read from vault
405
+ - `save(manifest: SyncManifest): Promise<void>` - Atomic write (temp + rename)
406
+ - `getFileState(path: string): Promise<FileState | null>` - Lookup single file
407
+ - `updateFileState(path: string, state: FileState): Promise<void>` - Update single entry
408
+ - `removeFileState(path: string): Promise<void>` - Remove deleted file
409
+ - `createEmptyManifest(): SyncManifest` - Generate new manifest with UUID
410
+
411
+ ### ⏳ Phases 4-9 (NOT YET IMPLEMENTED)
412
+
413
+ See [CLAUDE.md](CLAUDE.md) for detailed implementation plan.
414
+
415
+ ## Implemented Components
416
+
417
+ ### 1. LocalStorageAdapter
418
+
419
+ **Type**: Class implementing `StorageAdapter` interface
420
+
421
+ **Location**: `src/storage/index.ts`
422
+
423
+ **Dependencies**:
424
+ - Node.js `fs/promises`
425
+ - Node.js `path`
426
+ - `sanitizePath()` from utils
427
+
428
+ **Usage Example**:
429
+ ```typescript
430
+ import { LocalStorageAdapter } from './storage/index.js';
431
+
432
+ const storage = new LocalStorageAdapter();
433
+
434
+ // Read file
435
+ const content = await storage.read('/path/to/file.txt');
436
+ console.log(content.toString());
437
+
438
+ // Write file (creates parent dirs)
439
+ await storage.write('/path/to/new/file.txt', Buffer.from('content'));
440
+
441
+ // List all files recursively
442
+ const files = await storage.list('/vault/path');
443
+ // Returns: ['note1.md', 'folder/note2.md', ...]
444
+
445
+ // Check existence
446
+ const exists = await storage.exists('/path/to/file.txt');
447
+
448
+ // Delete file
449
+ await storage.delete('/path/to/file.txt');
450
+ ```
451
+
452
+ **Implementation Notes**:
453
+ - All methods are async (return Promises)
454
+ - Paths normalized automatically via `sanitizePath()`
455
+ - `list()` returns relative paths sorted alphabetically
456
+ - `write()` creates intermediate directories recursively
457
+ - `delete()` never throws on ENOENT (already deleted = success)
458
+
459
+ **Testing**: 22 unit tests in `tests/unit/storage.test.ts`
460
+ - Covers all methods with edge cases
461
+ - Tests binary file handling
462
+ - Tests deep nesting, special characters
463
+ - Tests error conditions (permissions, missing files)
464
+
465
+ ### 2. ObsidianVault
466
+
467
+ **Type**: Class for Obsidian-specific operations
468
+
469
+ **Location**: `src/vault/index.ts`
470
+
471
+ **Dependencies**:
472
+ - `LocalStorageAdapter`
473
+ - `picomatch` (glob matching)
474
+ - Node.js `fs/promises`, `path`
475
+
476
+ **Constructor**:
477
+ ```typescript
478
+ interface VaultConfig {
479
+ vaultPath: string;
480
+ excludePatterns?: string[]; // Additional patterns beyond defaults
481
+ includePatterns?: string[]; // If set, only include matching files
482
+ }
483
+
484
+ const vault = new ObsidianVault({
485
+ vaultPath: '/path/to/vault',
486
+ excludePatterns: ['private/**'] // Optional
487
+ });
488
+ ```
489
+
490
+ **Usage Example**:
491
+ ```typescript
492
+ import { ObsidianVault } from './vault/index.js';
493
+
494
+ const vault = new ObsidianVault({ vaultPath: '/my/vault' });
495
+
496
+ // Check if path is Obsidian vault
497
+ if (await vault.isObsidianVault()) {
498
+ console.log('Valid vault!');
499
+ }
500
+
501
+ // Validate structure
502
+ const isValid = await vault.validateStructure();
503
+
504
+ // List all files (excluding .obsidian, .trash, etc.)
505
+ const files = await vault.listFiles();
506
+ // Returns: ['note1.md', 'folder/note2.md', ...]
507
+
508
+ // Get metadata
509
+ const metadata = await vault.getMetadata();
510
+ console.log(`Files: ${metadata.fileCount}`);
511
+ console.log(`Size: ${metadata.totalSize} bytes`);
512
+ console.log(`Last modified: ${metadata.lastModified}`);
513
+ ```
514
+
515
+ **Default Exclusions**:
516
+ - `.obsidian/**` - All Obsidian config/cache files
517
+ - `.trash/**` - Obsidian trash folder
518
+ - `.git/**` - Git repository files
519
+ - `.obsync/**` - obsync metadata (sync manifests)
520
+
521
+ **Custom Pattern Example**:
522
+ ```typescript
523
+ const vault = new ObsidianVault({
524
+ vaultPath: '/vault',
525
+ excludePatterns: ['archive/**', 'templates/**'],
526
+ includePatterns: ['*.md'] // Only markdown files
527
+ });
528
+ ```
529
+
530
+ **Pattern Matching Logic**:
531
+ 1. If `includePatterns` specified, file must match at least one
532
+ 2. File must NOT match any pattern in `excludePatterns` + defaults
533
+ 3. Uses `picomatch` for fast, reliable glob matching
534
+
535
+ **Testing**: 22 unit tests in `tests/unit/vault.test.ts`
536
+ - Tests vault detection and validation
537
+ - Tests file listing with various pattern combinations
538
+ - Tests metadata extraction accuracy
539
+ - Tests with real fixture vault (`tests/fixtures/sample-vault/`)
540
+
541
+ ### 3. Utility Functions
542
+
543
+ **Location**: `src/utils/index.ts`
544
+
545
+ **Functions**:
546
+
547
+ #### `computeFileHash(content: Buffer): string`
548
+ - Computes SHA-256 hash of file content
549
+ - Returns hex-encoded hash string
550
+ - Used for change detection in sync algorithm
551
+ - **Usage**: `const hash = computeFileHash(Buffer.from('content'));`
552
+
553
+ #### `sanitizePath(path: string): string`
554
+ - Normalizes path separators (backslash → forward slash)
555
+ - Ensures consistent paths across Windows/Unix
556
+ - **Usage**: `const normalized = sanitizePath('path\\to\\file');`
557
+
558
+ #### `isMarkdownFile(filename: string): boolean`
559
+ - Checks if filename ends with `.md`
560
+ - Simple extension check
561
+ - **Usage**: `if (isMarkdownFile('note.md')) { ... }`
562
+
563
+ #### `formatBytes(bytes: number): string`
564
+ - Formats byte count to human-readable string (KB, MB, GB)
565
+ - **Usage**: `formatBytes(1048576) // "1.00 MB"`
566
+
567
+ **Testing**: 4 unit tests in `tests/unit/utils.test.ts`
568
+
569
+ ## API Reference
570
+
571
+ ### StorageAdapter Interface
572
+
573
+ ```typescript
574
+ interface StorageAdapter {
575
+ /**
576
+ * Read file contents as Buffer
577
+ * @throws Error if file doesn't exist
578
+ */
579
+ read(path: string): Promise<Buffer>;
580
+
581
+ /**
582
+ * Write content to file, creating parent directories
583
+ * @param path - Absolute or relative file path
584
+ * @param content - File content as Buffer
585
+ */
586
+ write(path: string, content: Buffer): Promise<void>;
587
+
588
+ /**
589
+ * Delete file (idempotent - succeeds even if file doesn't exist)
590
+ * @param path - File path to delete
591
+ */
592
+ delete(path: string): Promise<void>;
593
+
594
+ /**
595
+ * List all files recursively under prefix
596
+ * @param prefix - Directory path to list
597
+ * @returns Array of relative file paths, sorted
598
+ */
599
+ list(prefix: string): Promise<string[]>;
600
+
601
+ /**
602
+ * Check if file or directory exists
603
+ * @param path - Path to check
604
+ * @returns true if exists, false otherwise
605
+ */
606
+ exists(path: string): Promise<boolean>;
607
+ }
608
+ ```
609
+
610
+ ### VaultMetadata Type
611
+
612
+ ```typescript
613
+ interface VaultMetadata {
614
+ name: string; // Vault directory name
615
+ path: string; // Absolute path to vault
616
+ fileCount: number; // Number of files (after exclusions)
617
+ totalSize: number; // Total size in bytes
618
+ lastModified: Date; // Most recent file modification
619
+ }
620
+ ```
621
+
622
+ ### VaultConfig Type
623
+
624
+ ```typescript
625
+ interface VaultConfig {
626
+ vaultPath: string; // Path to Obsidian vault
627
+ excludePatterns?: string[]; // Additional exclude globs
628
+ includePatterns?: string[]; // If set, only include matching
629
+ }
630
+ ```
631
+
632
+ ### WatchModeOptions Type
633
+
634
+ ```typescript
635
+ interface WatchModeOptions {
636
+ source: string; // Source vault path
637
+ destination: string; // Destination vault path
638
+ conflictResolution?: ConflictStrategy; // Conflict resolution strategy
639
+ debounceMs?: number; // Debounce delay (default: 300)
640
+ batchDelayMs?: number; // Batch delay (default: 500)
641
+ maxWaitMs?: number; // Max wait before sync (default: 5000)
642
+ initialSync?: boolean; // Run initial sync (default: true)
643
+ onSync?: (result: SyncResult) => void; // Sync callback
644
+ onError?: (error: Error) => void; // Error callback
645
+ onStatusChange?: (status: WatchModeStatus) => void; // Status callback
646
+ }
647
+ ```
648
+
649
+ ### WatchModeStatus Type
650
+
651
+ ```typescript
652
+ interface WatchModeStatus {
653
+ state: 'idle' | 'watching' | 'syncing' | 'error' | 'stopped';
654
+ isActive: boolean; // Whether watch mode is active
655
+ pendingChanges: number; // Number of pending changes
656
+ lastSyncTime: Date | null; // Last sync timestamp
657
+ lastSyncResult: SyncResult | null; // Last sync result
658
+ syncCount: number; // Total syncs in session
659
+ totalFilesSynced: number; // Total files synced
660
+ currentError: Error | null; // Current error if any
661
+ watchedPaths: string[]; // Paths being watched
662
+ }
663
+ ```
664
+
665
+ ### WatchModeSync Usage Example
666
+
667
+ ```typescript
668
+ import { WatchModeSync } from 'obsync';
669
+
670
+ const watchMode = new WatchModeSync({
671
+ source: '/path/to/source-vault',
672
+ destination: '/path/to/dest-vault',
673
+ conflictResolution: 'newest',
674
+ onSync: (result) => {
675
+ console.log(`Synced: ${result.filesAdded} added, ${result.filesUpdated} updated`);
676
+ },
677
+ onStatusChange: (status) => {
678
+ console.log(`State: ${status.state}, Pending: ${status.pendingChanges}`);
679
+ },
680
+ });
681
+
682
+ // Start watching
683
+ await watchMode.start();
684
+
685
+ // Get status
686
+ console.log(watchMode.getStatus());
687
+
688
+ // Force immediate sync
689
+ await watchMode.forceSync();
690
+
691
+ // Pause/resume
692
+ await watchMode.pause();
693
+ await watchMode.resume();
694
+
695
+ // Stop watching
696
+ await watchMode.stop();
697
+
698
+ // Get session info
699
+ console.log(watchMode.getSession());
700
+ ```
701
+
702
+ ## Testing
703
+
704
+ ### Test Structure
705
+
706
+ ```
707
+ tests/
708
+ ├── unit/ # Unit tests for individual modules
709
+ │ ├── storage.test.ts # LocalStorageAdapter tests (22 tests)
710
+ │ ├── vault.test.ts # ObsidianVault tests (22 tests)
711
+ │ ├── config.test.ts # ConfigManager tests
712
+ │ ├── manifest.test.ts # ManifestManager tests
713
+ │ ├── watcher.test.ts # FileWatcher tests (29 tests)
714
+ │ ├── watchMode.test.ts # WatchModeSync tests
715
+ │ └── utils.test.ts # Utility function tests (4 tests)
716
+ ├── integration/ # Integration tests
717
+ │ ├── sync.test.ts # Full sync workflow tests
718
+ │ └── watchMode.test.ts # Watch mode integration tests
719
+ └── fixtures/ # Test data
720
+ └── sample-vault/ # Example Obsidian vault
721
+ ├── .obsidian/
722
+ │ └── app.json
723
+ ├── note1.md
724
+ ├── note2.md
725
+ └── folder/
726
+ └── note3.md
727
+ ```
728
+
729
+ ### Running Tests
730
+
731
+ ```bash
732
+ # Run all tests
733
+ bun test
734
+
735
+ # Run specific test file
736
+ bun test storage.test.ts
737
+
738
+ # Run tests in watch mode
739
+ bun test --watch
740
+
741
+ # Run tests with UI
742
+ bun run test:ui
743
+ ```
744
+
745
+ ### Test Coverage
746
+
747
+ **Current**: 165 passing tests, comprehensive coverage for all Phase 1 and 2 modules
748
+
749
+ **Coverage Breakdown**:
750
+ - `LocalStorageAdapter`: 100% (all methods, all branches)
751
+ - `ObsidianVault`: 100% (all methods, all branches)
752
+ - `ManifestManager`: 100% (all methods, all branches)
753
+ - `ConfigManager`: 100% (all methods, all branches)
754
+ - `SyncEngine`: Integration tests covering all sync scenarios
755
+ - `FileWatcher`: 100% (all methods, debouncing, ignore patterns)
756
+ - `BatchFileWatcher`: 100% (batch collection, flushing)
757
+ - `WatchModeSync`: 100% (start/stop/pause/resume, auto-sync, session tracking)
758
+ - `Utils`: 100%
759
+
760
+ ### Testing Philosophy
761
+
762
+ 1. **Unit Tests**: Test individual methods in isolation
763
+ 2. **Edge Cases**: Test error conditions, empty inputs, boundary cases
764
+ 3. **Integration**: Tests with real filesystem operations (temp directories)
765
+ 4. **Fixtures**: Reusable test data in `tests/fixtures/`
766
+
767
+ ## Development Setup
768
+
769
+ ### Prerequisites
770
+
771
+ - **Bun >= 1.0.0** (runtime and package manager)
772
+ - Node.js is not required - Bun is a complete replacement
773
+
774
+ ### Setup
775
+
776
+ This project uses Bun as the primary runtime and package manager:
777
+
778
+ ```bash
779
+ # Install Bun (if not already installed)
780
+ curl -fsSL https://bun.sh/install | bash
781
+
782
+ # Clone repository
783
+ git clone <repo-url>
784
+ cd obsync
785
+
786
+ # Install dependencies
787
+ bun install
788
+
789
+ # Build TypeScript
790
+ bun run build
791
+
792
+ # Run tests
793
+ bun test
794
+
795
+ # Lint code
796
+ bun run lint
797
+
798
+ # Format code
799
+ bun run format
800
+ ```
801
+
802
+ **See [BUN_SETUP.md](BUN_SETUP.md) for detailed Bun usage, performance comparisons, and troubleshooting.**
803
+
804
+ ### Project Structure
805
+
806
+ ```
807
+ obsync/
808
+ ├── src/
809
+ │ ├── cli.ts # CLI interface (placeholder)
810
+ │ ├── index.ts # Main exports
811
+ │ ├── storage/
812
+ │ │ └── index.ts # ✓ StorageAdapter + LocalStorageAdapter
813
+ │ ├── sync/
814
+ │ │ └── index.ts # ⏳ SyncEngine (placeholder)
815
+ │ ├── vault/
816
+ │ │ └── index.ts # ✓ ObsidianVault
817
+ │ ├── config/
818
+ │ │ └── index.ts # ⏳ ConfigManager (placeholder)
819
+ │ └── utils/
820
+ │ └── index.ts # ✓ Utility functions
821
+ ├── tests/
822
+ │ ├── unit/ # ✓ Unit tests
823
+ │ ├── integration/ # ⏳ Integration tests (future)
824
+ │ └── fixtures/ # Test data
825
+ ├── dist/ # Build output
826
+ ├── package.json
827
+ ├── tsconfig.json # TypeScript config
828
+ ├── vitest.config.ts # Test config
829
+ ├── eslint.config.js # Linting rules
830
+ ├── .prettierrc # Code formatting
831
+ ├── README.md # This file
832
+ └── CLAUDE.md # Development guide
833
+ ```
834
+
835
+ ### Build System
836
+
837
+ - **TypeScript**: Compiles to ES modules
838
+ - **Target**: ES2022
839
+ - **Module Resolution**: Bundler (ESM with .js extensions)
840
+ - **Source Maps**: Enabled
841
+ - **Declarations**: Generated (.d.ts files)
842
+
843
+ ### Code Quality Tools
844
+
845
+ - **TypeScript**: Strict mode enabled, no unused parameters/locals
846
+ - **ESLint**: TypeScript-specific rules, flat config format
847
+ - **Prettier**: Consistent code formatting (2-space indent, single quotes)
848
+ - **Vitest**: Fast unit test runner with V8 coverage
849
+
850
+ ## Contributing
851
+
852
+ ### Adding a New Storage Adapter
853
+
854
+ To add support for a new storage backend (e.g., Google Drive, S3):
855
+
856
+ 1. **Create adapter file**: `src/storage/<backend>.ts`
857
+
858
+ 2. **Implement interface**:
859
+ ```typescript
860
+ import { StorageAdapter } from './index.js';
861
+
862
+ export class MyStorageAdapter implements StorageAdapter {
863
+ async read(path: string): Promise<Buffer> {
864
+ // Implement backend-specific read
865
+ }
866
+
867
+ async write(path: string, content: Buffer): Promise<void> {
868
+ // Implement backend-specific write
869
+ }
870
+
871
+ // ... implement other methods
872
+ }
873
+ ```
874
+
875
+ 3. **Add tests**: `tests/unit/<backend>.test.ts`
876
+
877
+ 4. **Export**: Add to `src/storage/index.ts`:
878
+ ```typescript
879
+ export * from './<backend>.js';
880
+ ```
881
+
882
+ ### Sync Algorithm Implementation
883
+
884
+ When implementing Phase 3+ (sync engine), follow this sequence:
885
+
886
+ 1. **Manifest Manager** (`src/sync/manifest.ts`) - State tracking
887
+ 2. **Change Detector** (`src/sync/changeDetector.ts`) - Three-way merge logic
888
+ 3. **Conflict Resolver** (`src/sync/conflictResolver.ts`) - Strategy pattern
889
+ 4. **Sync Engine** (`src/sync/index.ts`) - Orchestration
890
+
891
+ See [CLAUDE.md](CLAUDE.md) for detailed implementation plan.
892
+
893
+ ### Code Style
894
+
895
+ - Use async/await (not callbacks or raw Promises)
896
+ - Prefer `const` over `let`, avoid `var`
897
+ - Use TypeScript strict types, avoid `any`
898
+ - ESM imports with `.js` extension (TypeScript requirement)
899
+ - Document complex algorithms with comments
900
+ - Keep functions focused (single responsibility)
901
+
902
+ ## License
903
+
904
+ MIT
905
+
906
+ ---
907
+
908
+ ## Project Roadmap
909
+
910
+ **Phase 1 (✅ COMPLETE)**: Local-to-local sync with bidirectional support
911
+ - Storage layer, vault operations, sync engine, conflict resolution, CLI
912
+
913
+ **Phase 2 (✅ COMPLETE)**: Watch mode for continuous sync
914
+ - File monitoring with chokidar, automatic sync on changes, debouncing
915
+ - Batch processing, session tracking, pause/resume support
916
+
917
+ **Phase 3+ (Future)**: Cloud storage adapters
918
+ - Google Drive, AWS S3, UploadThing, WebDAV support
919
+
920
+ For detailed technical planning, see [CLAUDE.md](CLAUDE.md).