cfgsync is a small cross-platform CLI utility for backing up and restoring text-based configuration files.
It is designed for a simple local workflow: track important config files, save copies into a dedicated storage directory, and restore them later after a reinstall, migration, or accidental loss.
This project is not a Git replacement. It focuses on local configuration synchronization and recovery with a clean, minimal MVP.
The v0.3.1 workflow is implemented:
- initialize a cfgsync storage directory
- persist the active storage root in a user-level app config
- add and remove ordinary files from the registry
- list tracked files
- back up tracked files into storage
- show tracked file status
- show unified text diffs for tracked files
- watch tracked files and automatically back up changes
- adopt an existing storage directory on a fresh system
- restore one tracked file or all tracked files
The current version intentionally supports ordinary files only. Directory tracking, symlink tracking, snapshots, remote sync, and encryption are outside the v0 scope.
- CMake 3.31 or newer
- A C++20-capable compiler
- Git and network access during the first CMake configure, because dependencies are fetched with CMake
FetchContent
The project fetches these dependencies during configuration:
- CLI11
- fmt
- spdlog
- nlohmann/json
- efsw
Configure and build from the repository root:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config ReleaseOn single-config generators, such as Unix Makefiles or Ninja, the executable is usually written to:
build/cfgsync
On multi-config generators, such as Visual Studio, the executable is usually written under the selected configuration directory, for example:
build/Release/cfgsync.exe
Tests are enabled by default. To configure a smaller build without the GoogleTest suite:
cmake -S . -B build-no-tests -DCFGSYNC_BUILD_TESTS=OFF
cmake --build build-no-testsOptional clang-tidy checks can be enabled with:
cmake -S . -B build -DCFGSYNC_ENABLE_CLANG_TIDY=ONAfter building, install the executable and basic project documentation with:
cmake --install build --config ReleaseTo install into a local test prefix instead of the system default prefix:
cmake --install build --config Release --prefix /tmp/cfgsync-installBuild the project and run CTest:
cmake -S . -B build -DCFGSYNC_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failureThe test suite uses temporary directories and is designed not to write to the real user home directory or real cfgsync app config.
The examples below assume cfgsync is available on your PATH. If it is not, run the executable from your build directory instead.
cfgsync init --storage ~/cfgsync-store
cfgsync add ~/.gitconfig
cfgsync add ~/.config/nvim
cfgsync list
cfgsync backup
cfgsync status
cfgsync diff ~/.gitconfig
cfgsync watch
cfgsync restore ~/.gitconfig
cfgsync restore --all
cfgsync remove ~/.gitconfigTypical flow:
- Run
cfgsync init --storage <dir>once to create the storage directory and record it as the active storage root. - Run
cfgsync add <file>for one ordinary config file, orcfgsync add <directory>to recursively import existing ordinary files under a directory. - Run
cfgsync backupto create stored copies for newly added or otherwise unbacked-up tracked files. - Run
cfgsync statusto check which tracked files differ from stored backups. - Run
cfgsync diff <file>to inspect a tracked file's text changes. - Optionally run
cfgsync watchas a long-running foreground watch that keeps backing up tracked files as they change until you stop it. - Run
cfgsync restore <file>to restore one tracked file, orcfgsync restore --allto restore every tracked file. - Run
cfgsync remove <file>when a file should no longer be tracked.
Fresh-system restore flow:
cfgsync use --storage ~/cfgsync-store
cfgsync list
cfgsync restore --allUse cfgsync use --storage <dir> when a cfgsync storage directory already exists, for example after reinstalling an operating system or copying storage onto a new machine. The command validates the storage, records it as active in the app config, and does not restore files by itself.
After use, inspect tracked paths with cfgsync list, then restore everything with cfgsync restore --all or restore one tracked file with:
cfgsync restore ~/.gitconfigInitializes cfgsync storage at the given directory.
This creates:
- the storage root directory, if needed
registry.jsonfiles/- a user-level cfgsync app config that records the active storage root
After init, later commands do not need --storage. They load the active storage root from the app config.
The app config is stored at:
- Linux/macOS:
$HOME/.config/cfgsync/config.json - Windows:
%APPDATA%/cfgsync/config.json
Running init again against an existing valid storage root preserves the registry and ensures the storage layout exists.
Uses an existing cfgsync storage directory as the active storage root.
This validates:
- the storage directory exists
registry.jsonexists- the registry format is valid and supported
If the storage directory was copied or moved, use updates the registry storage_root to the selected storage path and saves the app config. It also ensures the storage files/ directory exists.
The command does not restore files. Run cfgsync list to inspect tracked original paths, then run cfgsync restore --all or cfgsync restore <file> when you are ready to overwrite destination files.
In v0, restore uses the exact original paths recorded in the registry. cfgsync use does not remap paths across different usernames, home directories, or operating systems.
Registers an existing ordinary file for tracking, or recursively imports existing ordinary files under a directory.
The path is expanded for ~, normalized, and stored in the registry. Direct file adds still accept ordinary files only; symlinks, missing paths, and special files are rejected.
When a directory is passed, cfgsync performs a one-time recursive import of current ordinary files. It stores each imported file as an ordinary tracked file entry; it does not store directory-root metadata, does not enable persistent directory tracking, and does not automatically track files created later under that directory. Run cfgsync add <directory> again to import newly created files.
Directory import skips already tracked files without failing. Symlinks, special files, and entries that cannot be inspected are skipped with warnings while the import continues.
Adding files does not create backup copies in storage. Run cfgsync backup to create the first stored copy, or cfgsync watch when you want tracked files backed up as they change.
Removes a tracked file from the registry.
This does not delete the original file and does not delete any previously stored backup copy.
Prints tracked original file paths, one per line.
If no files are tracked, it prints:
No files tracked.
Creates missing stored copies for tracked files in the storage files/ tree.
Existing stored backup files are left unchanged. If all tracked files already have stored copies, it prints:
No new files to back up.
If one tracked file cannot be backed up, cfgsync reports that file, continues with the remaining entries, and exits with a failure after the batch finishes.
Shows tracked files whose current original content differs from the stored backup.
The command is read-only. It byte-compares each tracked original file against its stored backup and prints only changed or problem files in registry order:
modified <path>
missing-original <path>
missing-backup <path>
If every tracked file has an existing original, an existing backup, and matching bytes, it prints:
Clean.
Shows a unified text diff between a tracked file's stored backup and its current original file.
The stored backup is shown as the old side and the current original file is shown as the new side. The diff is generated internally; cfgsync does not require Git, system diff, or any external diff tool at runtime. Identical files produce no output and still exit successfully.
The file path is normalized before lookup. The command fails if the file is not tracked, if the original file is missing, if the tracked file has no stored backup yet, or if the file contains unsupported binary content.
Runs as a long-running foreground watch for tracked original files. It is not a daemon or background service.
The command runs until interrupted with Ctrl+C/SIGTERM, watches tracked files' parent directories, ignores untracked files in those directories, and backs up tracked files on add/modify/move-into-place events. Duplicate events for the same file are debounced with a short delay. The command does not perform an initial backup on startup.
If a tracked file is deleted, becomes non-ordinary, or cannot be backed up, cfgsync warns and continues watching. Press Ctrl+C to stop watching.
The production watcher uses efsw through cfgsync's local watcher adapter. It is configured with the generic polling backend for predictable cross-platform foreground behavior.
Restores every tracked file from storage back to its original location.
Parent directories are created before files are restored. Existing destination files are overwritten.
If one tracked file cannot be restored, cfgsync reports that file, continues with the remaining entries, and exits with a failure after the batch finishes.
Restores one tracked file from storage back to its original location.
The file path is normalized before lookup. The command fails if the file is not tracked or if no stored backup exists.
The storage root is intentionally readable in v0:
storage/
registry.json
files/
home/
user/
.gitconfig
.config/
nvim/
init.lua
For POSIX paths, the absolute path is mapped under files/ without the leading slash:
/home/user/.gitconfig -> files/home/user/.gitconfig
For Windows-style drive paths, the drive letter becomes a directory segment:
C:\Users\Oleksii\.gitconfig -> files/C/Users/Oleksii/.gitconfig
The layout favors readability over hashing for v0.
The registry is stored as JSON at <storage>/registry.json.
Example:
{
"version": 1,
"storage_root": "/absolute/path/to/storage",
"tracked_files": [
{
"original_path": "/home/user/.gitconfig",
"stored_relative_path": "files/home/user/.gitconfig"
}
]
}The registry records:
- the registry format version
- the storage root associated with the registry
- each tracked original path
- the relative path where that file is stored under the storage root
Users normally do not need to edit this file manually, but it is kept readable for inspection and future migration.
The v0 scope is intentionally small:
- ordinary files only
- no persistent directory tracking
- no symlink tracking
- no special file handling
- no snapshots or history
- no merge or conflict resolution
- no background service or daemon lifecycle for watch
- no original-path remapping across users, home directories, or operating systems
- no remote sync
- no encryption
- no packaged installer flow yet; CMake install is supported
The codebase is organized around a small layered design:
src/cli/defines command-line structure and argument parsingsrc/commands/contains thin command handlerssrc/core/contains registry and app configuration logicsrc/diff/contains byte comparison and internal unified diff renderingsrc/storage/maps tracked entries to storage paths and performs backup/restore copiessrc/watch/contains the watcher abstraction,efswadapter, and debounced watch backup processingsrc/utils/contains path, filesystem, logging, and app config path helperstests/contains focused GoogleTest coverage and CLI-level tests
The MVP prioritizes correctness, clear errors, cross-platform path handling through std::filesystem, and a maintainable structure before advanced features.
