esync

Directory watching and remote syncing
Log | Files | Refs | README | LICENSE

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 [![Vibecoded](https://img.shields.io/badge/vibecoded-%E2%9C%A8-blueviolet)](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.