|
1 | | -Sync dotfiles with Gist |
2 | | -======================= |
| 1 | +# dotfilesync (v3.0) |
3 | 2 |
|
4 | | -What is dotfilesync? |
5 | | --------------------- |
| 3 | +[](https://www.gnu.org/software/bash/) |
| 4 | +[](https://opensource.org/licenses/MIT) |
6 | 5 |
|
7 | | -dotfilesync is a bash script that syncs a list of local files that _you_ define, into a secret gist, that _you_ own. |
| 6 | +**dotfilesync** is a high-performance, security-focused Bash utility designed to synchronize your local configuration files into a single, private GitHub Gist. |
8 | 7 |
|
9 | | -dotfilesync is very easy to use as it stores your gist credentials into an encrypted Keychain, so you don't have to provide it each time. |
| 8 | +Unlike traditional dotfile managers that require complex Git repo management, `dfsync` treats your Gist as an atomic key-value store. It is lightweight, configuration-driven, and utilizes your system's native encrypted Keychain for credential storage. |
10 | 9 |
|
11 | | -You are not limited to syncing only dotfiles. Although this tool is meant for you to sync local dotfiles, it can also be used to sync any file that can be synced with gist. |
| 10 | +--- |
12 | 11 |
|
13 | | -This script is inspired by [this](https://hassansin.github.io/syncing-my-dotfiles-using-gist) post on how the author syncs zshrc to their gist. |
| 12 | +## 🚀 Key Features (Refactored v3.0) |
14 | 13 |
|
15 | | -Features |
16 | | --------- |
| 14 | +### ⚡️ Atomic Batch Updates |
| 15 | +Version 3.0 introduces a massive performance leap. Instead of N-number of HTTP requests for N-files, `dfsync` now: |
| 16 | +1. Aggregates all selected local files into a single JSON object. |
| 17 | +2. Performs **ONE** single `PATCH` request to the GitHub API. |
| 18 | +3. Ensures atomicity: either all files update, or none do. |
17 | 19 |
|
18 | | -### Config driven |
| 20 | +### 🤖 Automation Friendly (`-y` flag) |
| 21 | +Full support for non-interactive environments (CRON jobs, LaunchAgents). Use the `-y` or `--yes` flag to bypass all confirmation prompts. |
19 | 22 |
|
20 | | -dotfilesync uses a JSON config using a hardcoded path - `${HOME}/.dotfilesync/config.json` - to retrieve metadata needed to sync local files to gist. |
| 23 | +### 🔐 Secure Keychain Integration |
| 24 | +Your GitHub Personal Access Token (PAT) is never stored in plain text. |
| 25 | +* **macOS:** Uses the native **macOS Keychain** (`security` utility). |
| 26 | +* **Linux:** Uses **Gnome Keyring** (`secret-tool`). |
21 | 27 |
|
22 | | -A sample config shape can look like this: |
| 28 | +--- |
23 | 29 |
|
24 | | -```js |
25 | | -{ |
26 | | - "githubUser": "snvishna", |
27 | | - "gistId": "8f1bd18cf47f9d3efb8dc0a88a4e57aa", |
28 | | - "dotFilePaths": [ |
29 | | - "~/.dotfilesync/config.json", |
30 | | - "~/.zshrc", |
31 | | - "~/.bash_profile", |
32 | | - "~/.ssh/config", |
33 | | - "~/.scripts/a.sh", |
34 | | - "~/.scripts/b.sh" |
35 | | - ] |
36 | | -} |
37 | | -``` |
38 | | - |
39 | | -### Tools used as Keychain |
40 | | - |
41 | | -This tool stores and manage your GitHub Personal Access Token within into an encrypted Keychain. This way your token is safe and encrypted by the Keychain, and you don't have to provide it each time you run the command to sync files. |
42 | | - |
43 | | -For OS X based operating systems, the [OSX security](https://ss64.com/osx/security.html) command is used. |
44 | | - |
45 | | -For Linux based operating systems, the [secret-tools](http://www.linuxfromscratch.org/blfs/view/svn/gnome/libsecret.html) command is used to interact with [GnomeKeyring](https://wiki.gnome.org/Projects/GnomeKeyring). For more info about keyrings used on Linux systems [read this article](https://rtfm.co.ua/en/what-is-linux-keyring-gnome-keyring-secret-service-and-d-bus/#Linux_keyring_vs_gnome-keyring). |
46 | | - |
47 | | -### Sync multiple files into a single secret gist |
48 | | - |
49 | | -All local files that are listed in the `config.json` file are synced into a single gist. As long as you have a valid `config.json` file in the `${HOME}/.dotfilesync` directory |
50 | | - |
51 | | -### Auto-generate gist filenames |
52 | | - |
53 | | -The file names in gist are automatically created. This script is opinionated on the file names being created. The characters "/" and "~" are replaced with periods (.). Duplicate consecutive periods in the name are also removed. |
54 | | - |
55 | | -### Prompts before syncing each file |
56 | | - |
57 | | -Every file listed in the `config.json`, whether being pushed to or pulled from gist, is synced only after a confirmation prompt. A sync is performed __only__ after you enter a "y" or a "yes" (case-insensitive). Any other input is ignored from sync. |
58 | | - |
59 | | -### Creates a backup of the local file contents before overriding |
60 | | - |
61 | | -During a fetch operation (syncing local files from gist), a sync is performed, only after creating a backup of the local file. The backup file name is auto-generated based on the current timestamp. |
62 | | - |
63 | | -Prerequisites |
64 | | -------------- |
65 | | - |
66 | | -This script uses [jq](https://stedolan.github.io/jq/download/) to parse the `config.json` on the local filesystem. |
67 | | - |
68 | | -## OS X |
69 | | -You can run the following command on OS X, if you have [Homebrew](https://brew.sh/) installed: |
70 | | - |
71 | | - brew install jq |
72 | | - |
73 | | -## Linux |
74 | | -You can install the required packages on a Debian based distro running the following command: |
75 | | - |
76 | | - apt-get install jq libsecret-tools |
77 | | - |
78 | | -Installation |
79 | | ------------- |
80 | | - |
81 | | -Ensure the [prerequisite](#prerequisites) tools are setup. Installing dotfilesync is easy and a one-time effort: |
82 | | - |
83 | | -1. Start a Zsh shell: |
84 | | - |
85 | | - zsh |
86 | | - |
87 | | -2. Fetch the script locally: |
| 30 | +## 🛠 Prerequisites |
88 | 31 |
|
89 | | - * With curl: |
| 32 | +Requires `jq` for JSON processing and `curl` for API interaction. |
90 | 33 |
|
91 | | - mkdir -p ${HOME}/.dotfilesync \ |
92 | | - && curl -fsSL https://raw.githubusercontent.com/snvishna/dotfilesync/master/src/dfsync.sh \ |
93 | | - >| ${HOME}/.dotfilesync/dfsync.sh |
94 | | - |
95 | | - * With wget: |
96 | | - |
97 | | - mkdir -p ${HOME}/.dotfilesync \ |
98 | | - && wget -nv -O - https://raw.githubusercontent.com/snvishna/dotfilesync/master/src/dfsync.sh \ |
99 | | - >| ${HOME}/.dotfilesync/dfsync.sh |
100 | | - |
101 | | -3. Add an entry in zshrc: |
102 | | - |
103 | | - You'll find the zshrc file in your $HOME directory. Open it with your favorite text editor and add the following alias in there: |
104 | | - |
105 | | - alias dfsync='bash ${HOME}/.dotfilesync/dfsync.sh' |
106 | | - |
107 | | - You can now use the `dfsync` command after you restart the terminal, or source your zsh config. |
108 | | - |
109 | | -4. Create Person Access Token on GitHub: |
110 | | - |
111 | | - You can create a new [person access tokens page](https://github.com/settings/tokens/new) for running the script on the command line. Follow [these instructions](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) on how to create one. Make sure you have the __gist__ scope selected to grant permission on this token. |
112 | | - |
113 | | - Once you generate the personal access token, either copy it on your clipboard, or save it somewhere, since the script will need this to store within the Keychain, before syncing your files in gist. |
114 | | - |
115 | | -  |
116 | | - |
117 | | -5. Run setup: |
118 | | - |
119 | | - Run the command `dfsync setup` from your terminal. It prompts for your GitHub username and the personal access token. Refer to the [Commands](#commands) section for more details on this command. |
120 | | - |
121 | | -  |
122 | | - |
123 | | -6. You're done! Enjoy dotfilesync! |
124 | | - |
125 | | -Usage |
126 | | ------ |
127 | | - |
128 | | -* Update the `config.json` file with a list of local file paths that you'd like to sync with gist. You can now run `dfsync push` to upload these files in the gist. |
129 | | - |
130 | | -  |
131 | | - |
132 | | -Commands |
133 | | --------- |
134 | | - |
135 | | -* __dfsync setup__ |
136 | | - |
137 | | - It is run only __once__ as part of the installation instructions. This command does the following: |
138 | | - |
139 | | - * Prompts for your GitHub username and the personal access token. It then stores this information securely in the Keychain. |
| 34 | +### macOS |
| 35 | +```bash |
| 36 | +brew install jq |
| 37 | +``` |
140 | 38 |
|
141 | | - * Creates a new secret gist in your account with the description - "Generated by dotfilesync utility". |
| 39 | +### Linux (Debian/Ubuntu) |
| 40 | +```bash |
| 41 | +sudo apt-get install jq libsecret-tools |
| 42 | +``` |
142 | 43 |
|
143 | | - * It then creates a new `config.json` file, and auto-populates your Github username and the gistId fields. |
| 44 | +--- |
144 | 45 |
|
145 | | - * Saves this file in the `${HOME}/.dotfilesync` directory. |
| 46 | +## 📦 Installation |
146 | 47 |
|
147 | | - * It also syncs this config file to this gist. |
| 48 | +1. **Fetch the script:** |
| 49 | + ```bash |
| 50 | + mkdir -p ${HOME}/.dotfilesync \ |
| 51 | + && curl -fsSL https://raw.githubusercontent.com/snvishna/dotfilesync/master/src/dfsync.sh \ |
| 52 | + >| ${HOME}/.dotfilesync/dfsync.sh \ |
| 53 | + && chmod +x ${HOME}/.dotfilesync/dfsync.sh |
| 54 | + ``` |
148 | 55 |
|
149 | | - > It is a good practice to leave the `config.json` file to sync with your gist, so you can recover or download these files with the `dfsync pull` command when you need them. |
| 56 | +2. **Add the alias to your shell config:** |
| 57 | + ```bash |
| 58 | + alias dfsync='bash ${HOME}/.dotfilesync/dfsync.sh' |
| 59 | + ``` |
150 | 60 |
|
151 | | -* __dfsync push__ |
| 61 | +3. **Run Setup:** |
| 62 | + ```bash |
| 63 | + dfsync setup |
| 64 | + ``` |
| 65 | + |
| 66 | +  |
152 | 67 |
|
153 | | - Use this command to push all local file contents into the secret gist defined in the `config.json` file. This command will prompt you before syncing each file. You can type "Y" or "y" for the file contents to be pushed. You can type any other character, or just hit enter to skip syncing this file. This command will work only after the `dfsync password save` is run once, so the personal access token is saved. |
| 68 | +--- |
154 | 69 |
|
155 | | -* __dfsync pull__ |
| 70 | +## 📖 Usage & Commands |
156 | 71 |
|
157 | | - Use this command to fetch all local file contents into the secret gist defined in the `config.json` file. This command will prompt you before syncing each file. You can type "Y" or "y" for the file contents to be fetched. You can type any other character, or just hit enter to skip syncing this file. |
| 72 | +### Pushing to Gist |
| 73 | +```bash |
| 74 | +# Interactive Mode (Prompt for each file, then batch upload) |
| 75 | +dfsync push |
158 | 76 |
|
159 | | - To be safe and not corrupt your local file contents, the command will initiate a backup of the local files (using a timestamp), and only then overwrites the contents of the file. Use can use these backup files to recover to the previous state. |
| 77 | +# Automated Mode (Bypass prompts, sync everything instantly) |
| 78 | +dfsync push -y |
| 79 | +``` |
160 | 80 |
|
161 | | - This command will work only after the `dfsync password save` is run once, so the personal access token is saved. |
| 81 | +### Pulling from Gist |
| 82 | +```bash |
| 83 | +# Interactive Mode (Prompt before overwriting local files) |
| 84 | +dfsync pull |
162 | 85 |
|
163 | | -* __dfsync cleanup__ |
| 86 | +# Automated Mode (Sync all files, creating local backups automatically) |
| 87 | +dfsync pull -y |
| 88 | +``` |
164 | 89 |
|
165 | | - This command will do the following: |
| 90 | +### Command Reference |
166 | 91 |
|
167 | | - * Delete the saved GitHub gist credentials from the Keychain. |
168 | | - * Delete `config.json` file from the `${HOME}/.dotfilesync` directory. |
169 | | - * It does not automatically delete the saved gist from your GitHub account. Rather, it prints out the HTTP link to your gist, so you can choose to delete it. |
170 | | - * It provides a link to the uninstall instructions in this README, so you can run the commands to delete the script and update the zsh config. |
| 92 | +| Command | Flag | Description | |
| 93 | +| :--- | :--- | :--- | |
| 94 | +| `setup` | N/A | Initial config, keychain storage, and Gist creation. | |
| 95 | +| `push` | `-y`, `--yes` | **Local → Gist.** Batch uploads local changes. | |
| 96 | +| `pull` | `-y`, `--yes` | **Gist → Local.** Syncs remote changes and creates backups. | |
| 97 | +| `cleanup` | N/A | Safely removes Keychain credentials and local config. | |
171 | 98 |
|
172 | | -Uninstall |
173 | | ---------- |
| 99 | +--- |
174 | 100 |
|
175 | | -You can cleanup the script and all resources created by it, using the following instructions: |
| 101 | +## 🔍 Technical Implementation |
176 | 102 |
|
177 | | -* __Run__ `dfsync cleanup` |
| 103 | +### Filename Mapping |
| 104 | +`dfsync` maps nested local paths to a flat Gist structure by replacing `/` and `~` with periods. |
| 105 | +* `~/.zshrc` → `.zshrc` |
| 106 | +* `~/.config/wezterm/wezterm.lua` → `.config.wezterm.wezterm.lua` |
178 | 107 |
|
179 | | - You can run the command to 1) Delete the saved GitHub gist credentials from the Keychain 2) Delete `config.json` file from the `${HOME}/.dotfilesync` directory. |
| 108 | +### Resilience |
| 109 | +The script uses `set -o pipefail` and `set -o errexit`. If the GitHub API returns an error during the batch upload, the script terminates immediately to protect the integrity of your local configuration. |
180 | 110 |
|
181 | | -* __Delete the gist__ |
| 111 | +### Stdin Handling |
| 112 | +To support interactive prompts inside loops, `dfsync` explicitly reads from `/dev/tty`. This ensures that `read` commands don't consume the file-list stream, allowing for stable `y/n` confirmation. |
182 | 113 |
|
183 | | - To be safe, the clean up command __does not automatically__ delete your gist from your account. You can choose to do this manually. The URL to your gist will be printed on the terminal when you run `dfsync cleanup`. |
| 114 | +--- |
184 | 115 |
|
185 | | -* __Delete the local file__ |
| 116 | +## 🧹 Uninstallation |
186 | 117 |
|
187 | | - You can now delete the dotfilesync directory from your machine. Run the following command: `rm -rf ${HOME}/.dotfilesync` |
| 118 | +1. **Run Cleanup:** `dfsync cleanup` |
| 119 | +2. **Delete Gist:** Delete the Gist manually via the URL provided by the cleanup command. |
| 120 | +3. **Remove Files:** `rm -rf ${HOME}/.dotfilesync` |
188 | 121 |
|
189 | | -* __Remove alias from zsh config__ |
| 122 | +--- |
190 | 123 |
|
191 | | - You should now remove the `dfsync` alias from the zsh config. Otherwise this command will fail on the missing file path. |
| 124 | +*Inspired by [Hassan Sani's post](https://hassansin.github.io/syncing-my-dotfiles-using-gist).* |
0 commit comments