README.md (16050B)
1 <div align="center"> 2 3 <h1>esync</h1> 4 <h3>Watch local files and sync them with rsync, shown in a live TUI</h3> 5 6 [](https://claude.ai) 7 8 <a href="demo/demo.gif"> 9 <img src="demo/demo.gif" width="800" alt="Demo of esync TUI" /> 10 </a> 11 12 </div> 13 14 *** 15 16 ## Installation 17 18 Install with `go install`: 19 20 ```bash 21 go install github.com/louloulibs/esync@latest 22 ``` 23 24 Or build from source: 25 26 ```bash 27 git clone https://github.com/louloulibs/esync.git 28 cd esync 29 go build -o esync . 30 ``` 31 32 ## Quick Start 33 34 ```bash 35 # 1. Generate a config file (imports .gitignore, detects common dirs) 36 esync init -r user@host:/path/to/dest 37 38 # 2. Preview what will be synced 39 esync check 40 41 # 3. Start watching and syncing 42 esync sync 43 ``` 44 45 ## Commands Reference 46 47 ### `esync sync` 48 49 Watch a local directory for changes and sync them to a destination using rsync. Launches an interactive TUI by default. 50 51 ```bash 52 esync sync # use config file, launch TUI 53 esync sync -c project.toml # use a specific config file 54 esync sync -l ./src -r server:/opt # quick mode, no config file needed 55 esync sync --daemon # run in background (no TUI) 56 esync sync --dry-run # show what would sync, don't transfer 57 esync sync --initial-sync # force a full sync on startup 58 esync sync -v # verbose output (daemon mode) 59 ``` 60 61 | Flag | Short | Description | 62 |-------------------|-------|------------------------------------------| 63 | `--local` | `-l` | Local path to watch | 64 | `--remote` | `-r` | Remote destination path | 65 | `--daemon` | | Run in daemon mode (no TUI) | 66 | `--dry-run` | | Show what would be synced without syncing | 67 | `--initial-sync` | | Force a full sync on startup | 68 | `--verbose` | `-v` | Verbose output | 69 | `--config` | `-c` | Config file path (global flag) | 70 71 When both `-l` and `-r` are provided, esync runs without a config file (quick mode). Otherwise it searches for a config file automatically. 72 73 ### `esync init` 74 75 Generate an `.esync.toml` configuration file in the current directory. Inspects the project for `.gitignore` patterns and common directories (`.venv`, `build`, `__pycache__`, etc.) to auto-populate ignore rules. 76 77 ```bash 78 esync init # interactive prompt for remote 79 esync init -r user@host:/path # pre-fill the remote destination 80 esync init -c ~/.config/esync/config.toml -r server:/data # custom path 81 ``` 82 83 | Flag | Short | Description | 84 |------------|-------|------------------------------------| 85 | `--remote` | `-r` | Pre-fill remote destination | 86 | `--config` | `-c` | Output file path (default: `./.esync.toml`) | 87 88 ### `esync check` 89 90 Validate your configuration and preview which files would be included or excluded by the ignore patterns. 91 92 ```bash 93 esync check # auto-detect config 94 esync check -c project.toml # check a specific config file 95 ``` 96 97 | Flag | Short | Description | 98 |------------|-------|------------------------------------| 99 | `--config` | `-c` | Config file path | 100 101 ### `esync edit` 102 103 Open the config file in your `$EDITOR` (defaults to `vi`). After saving, the config is validated and a file preview is shown. If validation fails, you can re-edit or cancel. 104 105 ```bash 106 esync edit # auto-detect config 107 esync edit -c project.toml # edit a specific config file 108 ``` 109 110 | Flag | Short | Description | 111 |------------|-------|------------------------------------| 112 | `--config` | `-c` | Config file path | 113 114 ### `esync status` 115 116 Check if an esync daemon is currently running. Reads the PID file from the system temp directory. 117 118 ```bash 119 esync status 120 # esync daemon running (PID 12345) 121 # — or — 122 # No esync daemon running. 123 ``` 124 125 ## Configuration 126 127 esync uses TOML configuration files. The config file is searched in this order: 128 129 1. Path given via `-c` / `--config` flag 130 2. `./.esync.toml` (current directory) 131 3. `~/.config/esync/config.toml` 132 4. `/etc/esync/config.toml` 133 134 ### Full Annotated Example 135 136 This shows every available field with explanatory comments: 137 138 ```toml 139 # ============================================================================= 140 # esync configuration file 141 # ============================================================================= 142 143 [sync] 144 # Local directory to watch for changes (required) 145 local = "/home/user/projects/myapp" 146 147 # Remote destination — can be a local path or an scp-style remote (required) 148 # Examples: 149 # "/backup/myapp" — local path 150 # "server:/opt/myapp" — remote using SSH config alias 151 # "user@192.168.1.50:/opt/myapp" — remote with explicit user 152 remote = "deploy@prod.example.com:/var/www/myapp" 153 154 # Polling interval in seconds (default: 1) 155 # This is used internally; the watcher reacts to filesystem events, 156 # so you rarely need to change this. 157 interval = 1 158 159 # --- SSH Configuration (optional) --- 160 # Use this section for fine-grained SSH control. 161 # If omitted, esync infers SSH from the remote string (e.g. user@host:/path). 162 [sync.ssh] 163 host = "prod.example.com" 164 user = "deploy" 165 port = 22 166 identity_file = "~/.ssh/id_ed25519" 167 interactive_auth = false # set to true for 2FA / keyboard-interactive auth 168 169 # ============================================================================= 170 [settings] 171 172 # Debounce interval in milliseconds (default: 500) 173 # After a file change, esync waits this long for more changes before syncing. 174 # Lower = more responsive, higher = fewer rsync invocations during rapid edits. 175 watcher_debounce = 500 176 177 # Run a full sync when esync starts (default: false) 178 initial_sync = false 179 180 # Path prefixes to sync, relative to local. Empty means everything. 181 # Keep include simple and explicit; use ignore for fine-grained filtering. 182 include = [] 183 184 # Patterns to ignore — applied to both the watcher and rsync --exclude flags. 185 # Supports glob patterns. Matched against file/directory base names. 186 ignore = [ 187 ".git", 188 "node_modules", 189 ".DS_Store", 190 "__pycache__", 191 "*.pyc", 192 ".venv", 193 "build", 194 "dist", 195 ".tox", 196 ".mypy_cache", 197 ] 198 199 # --- Rsync Settings --- 200 [settings.rsync] 201 archive = true # rsync --archive (preserves symlinks, permissions, timestamps) 202 compress = true # rsync --compress (compress data during transfer) 203 backup = false # rsync --backup (make backups of replaced files) 204 backup_dir = ".rsync_backup" # directory for backup files when backup = true 205 progress = true # rsync --progress (show transfer progress) 206 207 # Extra arguments passed directly to rsync. 208 # Useful for flags esync doesn't expose directly. 209 extra_args = [] 210 211 # Additional rsync-specific ignore patterns (merged with settings.ignore). 212 ignore = [] 213 214 # --- Logging --- 215 [settings.log] 216 # Log file path. If omitted, no log file is written. 217 # Logs are only written in daemon mode. 218 # file = "/var/log/esync.log" 219 220 # Log format: "text" or "json" (default: "text") 221 format = "text" 222 ``` 223 224 ### Minimal Config 225 226 The smallest usable config file: 227 228 ```toml 229 [sync] 230 local = "." 231 remote = "user@host:/path/to/dest" 232 ``` 233 234 Everything else uses sensible defaults: archive mode, compression, 500ms debounce, and standard ignore patterns (`.git`, `node_modules`, `.DS_Store`). 235 236 ### SSH Config Example 237 238 For remote servers with a specific SSH key and non-standard port: 239 240 ```toml 241 [sync] 242 local = "." 243 remote = "/var/www/myapp" 244 245 [sync.ssh] 246 host = "myserver.example.com" 247 user = "deploy" 248 port = 2222 249 identity_file = "~/.ssh/deploy_key" 250 ``` 251 252 When `[sync.ssh]` is present, esync constructs the full destination as `deploy@myserver.example.com:/var/www/myapp` and passes SSH options (port, identity file, ControlMaster) to rsync automatically. 253 254 ### 2FA / Keyboard-Interactive Authentication 255 256 If your server requires two-factor authentication: 257 258 ```toml 259 [sync] 260 local = "." 261 remote = "/home/user/project" 262 263 [sync.ssh] 264 host = "secure-server.example.com" 265 user = "admin" 266 identity_file = "~/.ssh/id_ed25519" 267 interactive_auth = true 268 ``` 269 270 ### Custom Rsync Flags 271 272 Pass extra arguments directly to rsync using `extra_args`: 273 274 ```toml 275 [sync] 276 local = "./src" 277 remote = "server:/opt/app/src" 278 279 [settings.rsync] 280 archive = true 281 compress = true 282 extra_args = [ 283 "--delete", # delete files on remote that don't exist locally 284 "--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r", # set permissions on remote 285 "--exclude-from=.rsyncignore", # additional exclude file 286 "--bwlimit=5000", # bandwidth limit in KBytes/sec 287 ] 288 ``` 289 290 ### Include Filters (Monorepo Support) 291 292 In a large repo, you may only want to sync specific subtrees. Use `include` to name the directories you care about, then use `ignore` for fine-grained filtering within them: 293 294 ```toml 295 [sync] 296 local = "/home/user/monorepo" 297 remote = "server:/opt/monorepo" 298 299 [settings] 300 include = ["src", "docs/api"] 301 ignore = [".git", "node_modules", ".DS_Store"] 302 ``` 303 304 - `include` takes path prefixes relative to `local` (not globs) 305 - Empty `include` (the default) means sync everything — fully backwards compatible 306 - When set, only files under the listed prefixes are watched and synced 307 - `ignore` then further refines within the included paths 308 309 ### Separate Watcher and Rsync Ignore Patterns 310 311 The top-level `settings.ignore` patterns are used by both the file watcher and rsync. If you need rsync-specific excludes (patterns the watcher should still see), use `settings.rsync.ignore`: 312 313 ```toml 314 [settings] 315 # These patterns are used by BOTH the watcher and rsync 316 ignore = [".git", "node_modules", ".DS_Store"] 317 318 [settings.rsync] 319 # These patterns are ONLY passed to rsync as --exclude flags 320 ignore = ["*.log", "*.tmp", "cache/"] 321 ``` 322 323 ### Logging Config 324 325 ```toml 326 [settings.log] 327 file = "/var/log/esync.log" 328 format = "json" 329 ``` 330 331 Text format output: 332 333 ``` 334 15:04:05 INF started local=/home/user/project pid=12345 remote=server:/opt/app 335 15:04:07 INF sync_complete bytes=2048 duration=150ms files=3 336 15:04:12 ERR sync_failed error=rsync error: ... 337 ``` 338 339 JSON format output: 340 341 ```json 342 {"time":"15:04:05","level":"info","event":"started","local":"/home/user/project","remote":"server:/opt/app","pid":12345} 343 {"time":"15:04:07","level":"info","event":"sync_complete","files":3,"bytes":2048,"duration":"150ms"} 344 ``` 345 346 ## TUI Keyboard Shortcuts 347 348 The interactive TUI (default mode) provides two views: Dashboard and Logs. 349 350 ### Dashboard View 351 352 | Key | Action | 353 |--------------|--------------------------------| 354 | `q` | Quit | 355 | `Ctrl+C` | Quit | 356 | `p` | Pause / resume watching | 357 | `r` | Force a full resync | 358 | `l` | Switch to log view | 359 | `j` / `Down` | Scroll down | 360 | `k` / `Up` | Scroll up | 361 | `/` | Enter filter mode | 362 | `Enter` | Apply filter (in filter mode) | 363 | `Esc` | Clear filter (in filter mode) | 364 365 ### Log View 366 367 | Key | Action | 368 |--------------|--------------------------------| 369 | `q` | Quit | 370 | `Ctrl+C` | Quit | 371 | `l` | Switch back to dashboard | 372 | `j` / `Down` | Scroll down | 373 | `k` / `Up` | Scroll up | 374 | `PgDn` | Scroll down one page | 375 | `PgUp` | Scroll up one page | 376 | `G` | Jump to end | 377 | `g` | Jump to top | 378 | `f` | Toggle follow mode (tail -f) | 379 | `/` | Enter filter mode | 380 | `Enter` | Apply filter (in filter mode) | 381 | `Esc` | Clear filter (in filter mode) | 382 383 ## Daemon Mode 384 385 Run esync in the background without the TUI: 386 387 ```bash 388 # Start daemon 389 esync sync --daemon 390 391 # Start daemon with verbose output and JSON logging 392 esync sync --daemon -v -c project.toml 393 394 # Check if the daemon is running 395 esync status 396 397 # Stop the daemon 398 kill $(cat /tmp/esync.pid) 399 ``` 400 401 The daemon writes its PID to `/tmp/esync.pid` so you can check status and stop it later. On receiving `SIGINT` or `SIGTERM` the daemon shuts down gracefully. 402 403 When a log file is configured, the daemon writes structured entries for every sync event: 404 405 ```bash 406 # Monitor logs in real-time 407 tail -f /var/log/esync.log 408 ``` 409 410 ## SSH Setup 411 412 esync uses rsync's SSH transport for remote syncing. There are two ways to configure SSH. 413 414 ### Inline (via remote string) 415 416 If your `~/.ssh/config` is already set up, just use the host alias: 417 418 ```toml 419 [sync] 420 local = "." 421 remote = "myserver:/opt/app" 422 ``` 423 424 This works when `myserver` is defined in `~/.ssh/config`: 425 426 ``` 427 Host myserver 428 HostName 192.168.1.50 429 User deploy 430 IdentityFile ~/.ssh/id_ed25519 431 ``` 432 433 ### Explicit SSH Section 434 435 For full control without relying on `~/.ssh/config`: 436 437 ```toml 438 [sync.ssh] 439 host = "192.168.1.50" 440 user = "deploy" 441 port = 22 442 identity_file = "~/.ssh/id_ed25519" 443 ``` 444 445 When the `[sync.ssh]` section is present, esync automatically enables SSH ControlMaster with these options: 446 447 - `ControlMaster=auto` -- reuse existing SSH connections 448 - `ControlPath=/tmp/esync-ssh-%r@%h:%p` -- socket path for multiplexing 449 - `ControlPersist=600` -- keep the connection alive for 10 minutes 450 451 This avoids re-authenticating on every sync and significantly speeds up repeated transfers. 452 453 ### 2FA Authentication 454 455 Set `interactive_auth = true` in the SSH config to enable keyboard-interactive authentication for servers that require a second factor: 456 457 ```toml 458 [sync.ssh] 459 host = "secure.example.com" 460 user = "admin" 461 identity_file = "~/.ssh/id_ed25519" 462 interactive_auth = true 463 ``` 464 465 ## Examples 466 467 ### Local directory sync 468 469 Sync a source directory to a local backup: 470 471 ```bash 472 esync sync -l ./src -r /backup/src 473 ``` 474 475 ### Remote sync with SSH 476 477 Sync to a remote server using a config file: 478 479 ```toml 480 # .esync.toml 481 [sync] 482 local = "." 483 remote = "deploy@prod.example.com:/var/www/mysite" 484 485 [settings] 486 ignore = [".git", "node_modules", ".DS_Store", ".env"] 487 ``` 488 489 ```bash 490 esync sync 491 ``` 492 493 ### Quick sync (no config file) 494 495 Sync without a config file by passing both paths on the command line: 496 497 ```bash 498 esync sync -l ./project -r user@server:/opt/project 499 ``` 500 501 This uses sensible defaults: archive mode, compression, 500ms debounce, and ignores `.git`, `node_modules`, `.DS_Store`. 502 503 ### Daemon mode with JSON logs 504 505 Run in the background with structured logging: 506 507 ```toml 508 # .esync.toml 509 [sync] 510 local = "/home/user/code" 511 remote = "server:/opt/code" 512 513 [settings] 514 initial_sync = true 515 516 [settings.log] 517 file = "/var/log/esync.log" 518 format = "json" 519 ``` 520 521 ```bash 522 esync sync --daemon -v 523 # esync daemon started (PID 54321) 524 # Watching: /home/user/code -> server:/opt/code 525 ``` 526 527 ### Custom rsync flags (delete extraneous files) 528 529 Keep the remote directory in exact sync by deleting files that no longer exist locally: 530 531 ```toml 532 # .esync.toml 533 [sync] 534 local = "./dist" 535 remote = "cdn-server:/var/www/static" 536 537 [settings.rsync] 538 extra_args = ["--delete", "--chmod=Fu=rw,Fgo=r,Du=rwx,Dgo=rx"] 539 ``` 540 541 ```bash 542 esync sync --initial-sync 543 ``` 544 545 ### Dry run to preview changes 546 547 See what rsync would do without actually transferring anything: 548 549 ```bash 550 esync sync --dry-run 551 ``` 552 553 ## System Requirements 554 555 - **Go** 1.22+ (for building from source) 556 - **rsync** 3.1+ (required for `--info=progress2` real-time transfer progress) 557 - **macOS** or **Linux** (uses fsnotify for filesystem events) 558 559 > **macOS note:** The built-in `/usr/bin/rsync` is Apple's `openrsync` which is too old. 560 > Install a modern rsync via Homebrew: `brew install rsync`. esync will automatically 561 > prefer the Homebrew version when available.