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
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).
|