A CLI utility to check out and check in work directories over network drives
(e.g. a locally-mounted Samba/NAS share) or rsync endpoints (ssh://, rsync://),
using rsync to copy files between a remote root and a local working copy, and leaving
a marker behind so others can see a folder is checked out and by whom.
A macOS cask is published into this repository on every tagged release. Because the repo
isn't named homebrew-*, tap it with an explicit URL, then install:
brew tap andresbott/dibs https://github.com/andresbott/dibs
brew install --cask andresbott/dibs/dibsrsync is pulled in as a dependency, and brew upgrade will track future releases.
Download the .deb for your architecture from the
releases page and install it
(this also pulls in rsync):
sudo apt install ./dibs_*_amd64.debGrab a prebuilt tar.gz archive from the
releases page.
The workflow is: checkout (claim the remote folder with a marker — no files move), sync (pull the tree down; later, push your work back), work locally, sync again, and checkin (verify everything is synced, then release the marker).
dibs checkout work # lock the "work" profile (run sync to pull files)
dibs sync work # pull the remote down / push local changes back
dibs status work # preview what the next sync would do (read-only)
dibs checkin work # verify fully synced, then release the lock-
dibs checkout <profile> [relpath]— write the per-profile marker (the cooperative lock) on the remote and record an empty local baseline. No files are copied — that issync's job.relpathnarrows what later syncs cover; the lock is always the whole profile. Re-running on a profile this machine already holds widens the covered set.--forceoverrides a lock held by someone else (with a loud warning naming the holder); it never bypasses the "local target must be empty" guard. -
dibs sync <profile> [relpath]— the single data-moving command: a three-way reconcile of local and remote against the checkout baseline. One-sided changes are pushed or pulled; a file changed on both sides is a conflict and stops the sync without writing either version (--forceresolves conflicts local-wins — it never overrides the lock check).Deletions are opt-in per run: without
--allow-deletesevery planned deletion is skipped and reported as pending (transfers still apply; nothing is ever removed) — re-run with the flag to carry them out. Even then, a plan that would delete all previously synced files on one side (e.g. an emptied or unmounted folder) is refused unconditionally; nothing waives that wipe valve. -
dibs status <profile>— read-only preview of what a sync would do (pushes, pulls, deletions, conflicts), grouped per subpath, plus the checkout/baseline state. Uses the same engine assync, so the preview and the action never diverge. -
dibs checkin <profile>— verify local and remote are fully in sync (same engine, read-only), then remove the marker and local state. Refuses and lists the pending changes if anything is unsynced — runsyncfirst. There is no--force.--cleanadditionally removes the local working copy after a successful release;--abandonreleases the lock without the in-sync check (the "start over" path — unsynced local work is knowingly left behind, the remote is untouched). -
dibs init— write a starter config file if none exists. -
dibs list— print the configured profiles as plain text. -
dibs version— print version/build information.
Every mutating command takes --dry-run (show the plan, change nothing);
--config <path> selects an alternate config file.
Running dibs with no arguments opens an interactive TUI that manages profiles and runs
all of the actions above:
a— add a profile,e— edit,d— delete (with confirmation),i— set the identityenter— open the selected profile's actions (Checkout when the profile isn't held; Status / Sync / Check-in while it is);↑/↓select,enterruns,tabswitches to the Activity panel,escgoes back- each action's options (steal lock, allow deletes, local wins, clean, abandon) appear as checkboxes in its confirm dialog
- in dialogs:
tab/arrow keys move between fields,enter/spaceactivates,esccancels esc/q— quit from the list
When stdout isn't a terminal (e.g. piped or redirected), dibs prints the
profile list as plain text instead of opening the TUI. dibs list always prints
that plain-text list, TUI or not:
dibs # interactive TUI (plain-text list when not a terminal)
dibs list # always prints the profile list as plain textProfiles are stored in a YAML file at os.UserConfigDir()/dibs/config.yaml:
| OS | Default path |
|---|---|
| Linux | ~/.config/dibs/config.yaml |
| macOS | ~/Library/Application Support/dibs/config.yaml |
| Windows | %AppData%\dibs\config.yaml |
Override the location with --config <path> or the $DIBS_CONFIG environment
variable.
A profile may optionally scope itself to a few sub-folders with a subpaths list —
relative paths under both roots (nested allowed; omit for the whole root). See
zarf/sample/config.yaml for an example.
The list is a hard scope by design: folders that exist (or later appear) on the remote
but are not listed are never pulled in — add them to subpaths when you want them.
Local content outside the scope is different: it would be stranded by a scoped sync,
so status and the TUI warn about it and sync/checkin refuse to run until it is
listed, moved, or removed.
Requires Go 1.26+. The full toolchain also needs golangci-lint, goreleaser, and
go-licence-detector; at runtime rsync must be on PATH.
make verify # test → license-check → lint → benchmark → coverage → e2e
make help # list all targetsmake run # or: go run main.go versionmake build # goreleaser snapshot build for the current OS/arch → ./dist
# or plain go:
go build ./...Releases are published by pushing a semver tag from a clean main branch. The tag
triggers the Release workflow, which runs GoReleaser
to build and publish the archives, .deb, and Homebrew cask.
make tag version="v1.2.3"make tag refuses to run unless you're on main with a clean working tree, then creates
and pushes the vX.Y.Z tag.
