Add conflict resolution diff UI and app branding
This commit is contained in:
210
.project/sync-plan-native.md
Normal file
210
.project/sync-plan-native.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Plan A: Native Sync Protocol (macOS + Server)
|
||||
|
||||
> **AGENT INSTRUCTION — READ FIRST**
|
||||
>
|
||||
> When you complete a phase or are ready for a code review, you **must** notify the opencode session that created this plan. Use the opencode HTTP API to send a message to session `ses_225742b62ffeOSelYyCe4IPwxL`.
|
||||
>
|
||||
> **Message format:**
|
||||
> ```
|
||||
> Subject: Plan A Review Ready — Phase X Complete
|
||||
> Body:
|
||||
> - Phase completed: [e.g., Phase 1]
|
||||
> - Files touched: [list of files added/modified]
|
||||
> - Summary: [2-3 sentence description of what was implemented]
|
||||
> - Tests: [which tests were added or updated]
|
||||
> - Blockers: [any issues or decisions needed]
|
||||
> ```
|
||||
>
|
||||
> **OpenCode API Specification:**
|
||||
> ```
|
||||
> POST https://api.opencode.ai/v1/sessions/ses_225742b62ffeOSelYyCe4IPwxL/messages
|
||||
> Content-Type: application/json
|
||||
> Authorization: Bearer <OPENCODE_API_KEY>
|
||||
>
|
||||
> Request body:
|
||||
> {
|
||||
> "role": "assistant",
|
||||
> "content": "Subject: Plan A Review Ready — Phase X Complete\n\nBody:\n- Phase completed: ...\n- Files touched: ...\n- Summary: ...\n- Tests: ...\n- Blockers: ..."
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Wait for a response before starting the next phase unless explicitly told otherwise.
|
||||
|
||||
## Goal
|
||||
Build a bidirectional filesystem sync protocol that lets a native macOS client keep a local folder in sync with the cairnquire server. The browser client continues to work via the existing WebSocket + REST API.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Server: Sync Service (`internal/sync/`)
|
||||
New package alongside existing services:
|
||||
|
||||
```
|
||||
internal/sync/
|
||||
├── protocol.go # Sync protocol types and state machine
|
||||
├── service.go # Business logic: resolve conflicts, apply changes
|
||||
├── handlers.go # HTTP endpoints for sync (wrapped in httpserver)
|
||||
├── store.go # Abstract over docs + store for sync operations
|
||||
└── watcher.go # Bridge file watcher events into sync protocol
|
||||
```
|
||||
|
||||
### macOS Client
|
||||
A separate Go binary in `apps/macos-sync/` (or `apps/sync/`):
|
||||
|
||||
```
|
||||
apps/macos-sync/
|
||||
├── cmd/md-sync/ # Main entry point
|
||||
├── internal/
|
||||
│ ├── client.go # HTTP client for sync protocol
|
||||
│ ├── fs.go # macOS filesystem watcher (fsevents)
|
||||
│ ├── sync.go # Local sync engine
|
||||
│ └── state.go # Local state DB (SQLite or JSON)
|
||||
```
|
||||
|
||||
## Sync Protocol v1
|
||||
|
||||
### Data Model
|
||||
- **Snapshot**: A point-in-time tree of `{path, hash, size, mtime}`
|
||||
- **Delta**: List of changes since a snapshot: `{type: create|update|delete|rename, path, oldPath?, hash, size}`
|
||||
- **Conflict**: When server and client both changed same file since last common snapshot
|
||||
|
||||
### Endpoints
|
||||
|
||||
```
|
||||
POST /api/sync/init
|
||||
Body: {deviceId, rootPath}
|
||||
Response: {snapshotId, serverSnapshot}
|
||||
|
||||
POST /api/sync/delta
|
||||
Body: {snapshotId, clientDelta}
|
||||
Response: {serverDelta, conflicts[]}
|
||||
|
||||
POST /api/sync/resolve
|
||||
Body: {snapshotId, resolutions[]}
|
||||
Response: {newSnapshotId}
|
||||
|
||||
GET /api/sync/content?hash=<hash>
|
||||
Response: raw file bytes (content-addressed, immutable)
|
||||
```
|
||||
|
||||
### Protocol Flow
|
||||
```
|
||||
1. Client calls /sync/init → gets server snapshot
|
||||
2. Client computes local delta (fsevents + local state)
|
||||
3. Client POSTs delta to /sync/delta
|
||||
4. Server responds with:
|
||||
- serverDelta (changes client needs to apply)
|
||||
- conflicts[] (need user resolution or auto-merge)
|
||||
5. Client resolves conflicts locally or via UI
|
||||
6. Client POSTs resolutions to /sync/resolve
|
||||
7. Server returns new snapshotId, both sides updated
|
||||
```
|
||||
|
||||
### Conflict Resolution Strategies
|
||||
- `last-write-wins` (default for non-conflicting files)
|
||||
- `rename-both` (keep both as `file.md` and `file (conflict).md`)
|
||||
- `manual-merge` (flag for UI)
|
||||
- `server-wins` / `client-wins` (per-file or global preference)
|
||||
|
||||
### Authentication
|
||||
- API keys (long-lived, revocable) — simpler for native clients
|
||||
- OAuth2 device flow — better for multi-user
|
||||
- Start with API keys: `X-API-Key: <token>` header
|
||||
|
||||
## macOS Client Design
|
||||
|
||||
### File Watcher
|
||||
- Use `github.com/fsnotify/fsevents` (Cgo wrapper around FSEvents)
|
||||
- Debounce rapid changes (500ms window)
|
||||
- Ignore `.git`, `.DS_Store`, temp files
|
||||
|
||||
### Local State
|
||||
- SQLite DB at `~/Library/Application Support/md-sync/state.db`
|
||||
- Tables: `snapshots`, `files`, `pending_deltas`, `conflicts`
|
||||
- Or simpler: JSON files + file hashes on disk
|
||||
|
||||
### Sync Engine
|
||||
- Runs as daemon / menubar app
|
||||
- Configurable sync interval (default: continuous via fsevents)
|
||||
- Offline queue: store deltas locally, sync when reconnected
|
||||
- Bandwidth-aware: skip large files on cellular
|
||||
|
||||
### UI (Optional Phase 2)
|
||||
- Menubar icon: sync status, last sync time, conflicts badge
|
||||
- Settings window: root folder, server URL, API key, exclusions
|
||||
- Conflict resolution dialog
|
||||
|
||||
## Server Changes Required
|
||||
|
||||
### 1. Sync Service
|
||||
```go
|
||||
type SyncService struct {
|
||||
docStore *docs.Service
|
||||
fileStore *store.Service
|
||||
watcher *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func (s *SyncService) InitSync(deviceID string) (*Snapshot, error)
|
||||
func (s *SyncService) ApplyDelta(snapshotID string, delta Delta) (*DeltaResult, error)
|
||||
func (s *SyncService) ResolveConflicts(snapshotID string, resolutions []Resolution) (*Snapshot, error)
|
||||
```
|
||||
|
||||
### 2. Authentication
|
||||
- Add API key table to database
|
||||
- Middleware to accept `X-API-Key` or session cookie
|
||||
- API keys scoped to user + device
|
||||
|
||||
### 3. Content-Addressed Fetch
|
||||
- `GET /api/content/<hash>` — immutable, cache-friendly
|
||||
- Uses existing `store.Service` (already content-addressed)
|
||||
|
||||
### 4. Push Notifications (Optional)
|
||||
- Server-Sent Events or WebSocket to tell client "server changed"
|
||||
- Avoids client polling
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Server Sync Protocol (2-3 weeks)
|
||||
- [ ] Define protocol types and state machine
|
||||
- [ ] Implement `/api/sync/init` and snapshot generation
|
||||
- [ ] Implement `/api/sync/delta` with conflict detection
|
||||
- [ ] Add content-addressed fetch endpoint
|
||||
- [ ] Write sync protocol tests (table-driven, simulate client/server)
|
||||
- [ ] Document protocol in `docs/specs/sync-protocol-v1.md`
|
||||
|
||||
### Phase 2: macOS Sync Engine (2-3 weeks)
|
||||
- [ ] Project scaffold: Go module, build scripts
|
||||
- [ ] FSEvents watcher with debouncing
|
||||
- [ ] Local state SQLite DB
|
||||
- [ ] HTTP client for sync endpoints
|
||||
- [ ] Basic sync loop: init → delta → apply
|
||||
- [ ] Offline queue (store deltas, retry with backoff)
|
||||
- [ ] CLI tool: `md-sync --folder ./docs --server https://...`
|
||||
|
||||
### Phase 3: Conflict Resolution + Polish (1-2 weeks)
|
||||
- [ ] Auto-resolve strategies
|
||||
- [ ] Conflict file generation (`file (conflict).md`)
|
||||
- [ ] Menubar app wrapper (Swift or `github.com/progrium/macdriver`)
|
||||
- [ ] Settings/preferences
|
||||
- [ ] Binary signing + notarization for distribution
|
||||
|
||||
### Phase 4: Advanced Features (Future)
|
||||
- [ ] Selective sync (ignore patterns)
|
||||
- [ ] Bandwidth limiting
|
||||
- [ ] Version history / time machine from server snapshots
|
||||
- [ ] iOS client (shares core sync engine)
|
||||
|
||||
## Pros
|
||||
- Native performance, works with any editor (VS Code, Obsidian, etc.)
|
||||
- Familiar sync model (Dropbox, iCloud Drive)
|
||||
- Content-addressed storage makes deduplication trivial
|
||||
- macOS client opens door to iOS, Windows, Linux
|
||||
|
||||
## Cons
|
||||
- New authentication system (API keys)
|
||||
- Complex conflict resolution UX
|
||||
- macOS development overhead (codesign, notarization, FSEvents)
|
||||
- Diverges from browser-first architecture
|
||||
|
||||
## Decision Needed
|
||||
- Should we support both browser sync AND native sync, or pick one?
|
||||
- If both: shared sync service, different transport (WebSocket vs HTTP)
|
||||
Reference in New Issue
Block a user