dotling 0.6.1

A dotfiles management CLI — track, link, and sync your config files across machines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
<p align="center">
  <h1 align="center">dotling</h1>
  <p align="center">
    A zero-dependency dotfiles management CLI — track, link, and sync your config files across machines.
  </p>
</p>

<p align="center">
  <a href="https://crates.io/crates/dotling"><img alt="crates.io" src="https://img.shields.io/crates/v/dotling.svg?style=flat-square&logo=rust"></a>
  <a href="https://github.com/auricvex/dotling/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/auricvex/dotling/ci.yml?branch=main&style=flat-square&logo=github&label=CI"></a>
  <a href="https://github.com/auricvex/dotling/blob/main/LICENSE-MIT"><img alt="License" src="https://img.shields.io/crates/l/dotling?style=flat-square"></a>
  <a href="https://crates.io/crates/dotling"><img alt="Downloads" src="https://img.shields.io/crates/d/dotling?style=flat-square&color=blue"></a>
</p>

---

**dotling** v0.6.1 is a zero-dependency dotfiles management CLI. It moves your config files into a central git repository and replaces them with symlinks (or copies). It handles the tedious parts — path mapping, conflict detection, encryption, templating, backups, hooks, and multi-OS support — so you can set up a new machine in seconds.

## Features

- **Symlink & copy deployment** — choose per file, switch anytime
- **Bidirectional sync**`dotling sync` pushes from repo → actual and pulls from actual → repo automatically
- **Automatic path mapping**`~/.config/nvim``config/nvim`, `~/.zshrc``shell/zshrc`
- **Multi-OS support** — tag entries as `linux`, `macos`, or `windows`; skip irrelevant files automatically
- **Secure Password Vault** — encrypt sensitive files (API keys, .env) using an Argon2id + ChaCha20-Poly1305 Vault
- **Encrypted sync** — sync handles encrypted entries in both directions; modified plaintext is re-encrypted back into the repo automatically
- **Portable Secrets** — export your vault to easily unlock secrets on a new machine
- **Native Git integration** — dotling manages the symlinks, you manage the repo with native `git` commands
- **Dotfile Templating** — add machine-specific values (`hostname`, `username`, custom vars) to any dotfile using `{{ var.key }}` syntax; render on every sync via `~/.dotling/vars.toml`
- **Health checks**`dotling doctor` audits broken links, orphaned entries, and repo issues
- **Conflict-safe** — refuses to overwrite unmanaged files without explicit confirmation
- **Automated Backups** — protects local files from accidental overwrites by saving them to chronological backup sessions
- **Lifecycle Hooks** — run custom commands before/after syncing at a repository or entry level with safe trust verification
- **Interactive 3-way Merge** — cleanly merge changes between repo and local files with standard git-style conflict markers
- **Fingerprint-based Status** — speed up encrypted and copy-mode sync checks using lightweight Blake2s-256 fingerprints, without prompting for passwords

## Installation

### From crates.io (recommended)

```sh
cargo install dotling
```

### Prebuilt binaries

Download a prebuilt binary from the [latest GitHub release](https://github.com/auricvex/dotling/releases/latest) for your platform:

| Platform | Binary |
|---|---|
| Linux (x86_64, glibc) | `dotling-x86_64-linux.tar.gz` |
| Linux (x86_64, musl) | `dotling-x86_64-linux-musl.tar.gz` |
| Linux (aarch64) | `dotling-aarch64-linux.tar.gz` |
| macOS (Intel) | `dotling-x86_64-macos.tar.gz` |
| macOS (Apple Silicon) | `dotling-aarch64-macos.tar.gz` |
| Windows (x86_64) | `dotling-x86_64-windows.zip` |
| Windows (ARM64) | `dotling-aarch64-windows.zip` |

```sh
# Example: Linux x86_64
curl -fsSL https://github.com/auricvex/dotling/releases/latest/download/dotling-x86_64-linux.tar.gz \
  | tar xz -C ~/.local/bin/
```

### Nix

```sh
nix run github:auricvex/dotling
# or add to your flake inputs
```

## Quick Start

```sh
# Initialize a new dotfiles repo
dotling init ~/dotfiles

# Or clone an existing one
dotling init git@github.com:you/dotfiles.git

# Track files
dotling add ~/.zshrc
dotling add ~/.config/nvim

# Sync everything (repo → actual and actual → repo)
dotling sync

# Since dotling doesn't wrap git, you can commit and push directly!
cd ~/dotfiles
git add .
git commit -m "initial setup"
git push
```

## Commands

| Command | Description |
|---|---|
| `dotling init <path\|url>` | Initialize a new repo or clone an existing one |
| `dotling add <paths>` | Move files into the repo and deploy a symlink back |
| `dotling remove <entries>` | Undeploy, safely restore tracked files/folders recursively to their original target locations (decrypting if encrypted), and remove from tracking |
| `dotling sync` | Bidirectional sync — push repo → actual and pull actual → repo |
| `dotling status` | Show deployment status of all tracked entries |
| `dotling edit <entry>` | Edit any tracked entry in `$EDITOR` — decrypts/re-encrypts automatically for encrypted entries |
| `dotling encrypt <paths>` | Encrypt tracked entries using your Vault |
| `dotling decrypt <paths>` | Decrypt encrypted entries back to plaintext |
| `dotling vault <action>` | Manage your password-protected encryption Vault |
| `dotling doctor` | Audit repository health and report issues |
| `dotling vars <action>` | Manage machine-local template variables |
| `dotling backup <action>` | Manage local file backups created by dotling before overwriting |

### Key Flags

| Command | Flag | Description |
|---|---|---|
| `all` | `-v, --verbose` | Show hints and additional details |
| `add` | `--copy` | Deploy as a copy instead of a symlink |
| `add` | `--encrypt` | Encrypt the file(s) using the vault password |
| `add` | `--template` | Track as a template (`.dtmpl`): rendered on each sync with machine-local variables |
| `add` | `--os <platform>` | Target OS: `all`, `linux`, `macos`, `windows` |
| `sync` | `--dry-run` | Preview changes without modifying anything |
| `sync` | `--force` | Overwrite conflicting files (repo wins; local backups created automatically) |
| `sync` | `--prefer-actual` | When both sides conflict, prefer the actual file (pull direction) |
| `sync` | `--no-interactive` | Do not prompt for conflict resolution; skip conflicting entries and print a warning |
| `sync` | `--backup` | Always back up the local file before any push that would overwrite it |
| `sync` | `--allow-hooks` | Allow executing all hooks without prompting |
| `sync` | `--no-hooks` | Disable executing any hooks |
| `status` | `--diff` | Show inline diffs for modified copy entries |

## How It Works

dotling moves your config files into a central git repository and replaces them with symlinks (or copies). Each tracked file is recorded in a `dotling.toml` config at the repo root.

**Symlinks** (default): the deployed file points to the repo — edits are instantly reflected in your repo. `dotling sync` ensures the symlink is present and correct.

**Copies** (`--copy`): the deployed file is a standalone copy. Useful for apps that don't support symlinks. `dotling sync` compares modification times and copies in whichever direction is newer.

### Sync Direction

`dotling sync` decides the direction per entry:

| Entry type | Push (repo → actual) | Pull (actual → repo) |
|---|---|---|
| **Symlink** | Create/fix symlink | Never (symlink always reads repo) |
| **Copy** | Source newer or target missing | Target newer |
| **Encrypted** | `.enc` newer or target missing → decrypt | Target newer → re-encrypt into `.enc` |

When both sides differ and timestamps are equal, dotling defaults to **repo wins** (push). Pass `--prefer-actual` to flip this.

### Path Mapping

Files are organized into categories automatically:

| Home path | Repo path |
|---|---|
| `~/.config/nvim/init.lua` | `config/nvim/init.lua` |
| `~/.zshrc` | `shell/zshrc` |
| `~/.gitconfig` | `git/gitconfig` |
| `~/.vimrc` | `vim/vimrc` |
| `~/.tmux.conf` | `tmux/tmux.conf` |
| `~/.somerc` | `home/somerc` |

### Configuration Format

Tracked entries and settings are stored in `dotling.toml` at the repo root:

```toml
# dotling.toml — managed by dotling, safe to hand-edit

[settings]
method = "symlink" # Default sync method

[hooks]
init = "echo 'Initializing repo...'"
before = "echo 'Starting global before-sync hook...'"
after = "echo 'Global after-sync hook completed.'"

[[entries]]
source = "shell/zshrc"
target = "~/.zshrc"
before = "echo 'Updating zshrc...'"
after = "echo 'zshrc updated!'"

[[entries]]
source = "config/nvim/init.lua"
target = "~/.config/nvim/init.lua"
method = "copy"
permissions = "0600" # Apply octal permissions on sync

[[entries]]
source = "shell/bashrc"
target = "~/.bashrc"
os = "linux"
```

### Multi-OS Support

Tag entries with `--os` to restrict them to a specific platform:

```sh
dotling add ~/.zshrc --os macos
dotling add ~/.bashrc --os linux
```

When deploying, dotling automatically skips entries that don't match the current OS. Entries tagged `all` (the default) deploy everywhere.

### Encryption Vault

dotling includes a built-in portable encryption Vault protected by Argon2id and ChaCha20-Poly1305. This lets you safely commit API keys, `.env` files, or ssh configs to your public dotfiles repo.

1. **Initialize your Vault:**
   ```sh
   dotling vault init
   ```
   You'll be prompted for a password. This creates a secure identity in `~/.dotling/vault/`.

2. **Add a file with encryption:**
   ```sh
   dotling add ~/.ssh/config --encrypt
   ```
   dotling will read your local file, encrypt it, store the ciphertext (`config.enc`) in your git repo, and deploy the decrypted file locally with secure permissions.

3. **Sync encrypted entries:**
   `dotling sync` handles encrypted entries in both directions. If you edit the deployed plaintext file, running `sync` will re-encrypt it back into the repo:
   ```sh
   # Edit your deployed file, then sync it back
   vim ~/.ssh/config
   dotling sync   # detects the file is newer → re-encrypts into ssh/config.enc
   ```

4. **Migrating to a new machine:**
   Export your vault bundle from your old machine:
   ```sh
   dotling vault export my-vault.bundle
   ```
   Then import it on the new machine and sync:
   ```sh
   dotling vault import my-vault.bundle
   dotling sync
   ```

5. **Editing encrypted files:**
   Use `dotling edit` to open an encrypted file in your `$EDITOR` without a manual decrypt/encrypt cycle:
   ```sh
   # Opens the decrypted content in $EDITOR, re-encrypts on save
   dotling edit ~/.ssh/config

   # Works with any tracked path: source path, target path, or partial name
   dotling edit ssh/config
   ```
   After editing, run `dotling sync` to push the re-encrypted changes to your deployed file.
   The editor is resolved in priority order: `$DOTLING_EDITOR``$VISUAL``$EDITOR``vim``nano`.

### Lifecycle Hooks

dotling supports executing hooks (shell commands) globally or per-entry during the sync process. Hooks can be used for actions like reloading your shell, compiling configurations, or running custom setup scripts.

#### Global Hooks
Global hooks run at the very beginning and very end of the `dotling sync` session:
- `init`: Command run during repository initialization.
- `before`: Command run before any entries are synced.
- `after`: Command run after all entries are successfully synced.

#### Entry-level Hooks
You can define hooks specific to individual tracked entries:
- `before`: Command run before this entry is pushed or pulled.
- `after`: Command run after this entry is successfully pushed or pulled.

#### Execution Context
Hooks are executed in the repository root directory. The following environment variables are populated to provide rich runtime context:
- `DOTLING_HOOK_TYPE`: Type of hook (`global_before`, `global_after`, `entry_before`, `entry_after`).
- `DOTLING_REPO_ROOT`: Absolute path to the dotfiles repository.
- `DOTLING_DRY_RUN`: `"true"` if running with `--dry-run`, otherwise `"false"`.
- `DOTLING_ENTRY_SOURCE`: (Entry hooks only) Repo-relative path of the entry's source file/folder.
- `DOTLING_ENTRY_TARGET`: (Entry hooks only) Target path of the entry's deployed file/folder.
- `DOTLING_ENTRY_ACTION`: (Entry hooks only) Current action being performed (`"push"` or `"pull"`).

#### Hook Trust System
To protect against malicious code in imported dotfile repositories, dotling prompts for user verification before running a hook for the first time:
```text
  ⚡ Untrusted hook detected (type: entry_before):
    echo "updating shell configuration"
    ? Do you want to run this hook? [y]es (once) / [n]o (skip) / [a]lways (trust) / [s]kip all >
```
Selecting `always` stores the Blake2s-256 hash of the command string in `~/.dotling/state/trusted_hooks` so it runs seamlessly on subsequent syncs.
- Pass `--allow-hooks` (or set `DOTLING_ALLOW_HOOKS=1`) to automatically execute all hooks without prompting.
- Pass `--no-hooks` (or set `DOTLING_NO_HOOKS=1`) to completely disable hook execution.

#### Hook Retry
If a hook command exits with a non-zero status, dotling automatically retries it up to **3 times** before aborting the sync. A warning is printed after each failed attempt so the failure is always visible.

### Automated Backups & Conflict Resolution

#### Backups
To protect your local environment from accidental data loss, dotling automatically backs up files before they are overwritten:
- Backups are stored in `~/.dotling/backups/<unix-seconds>/<repo-relative-source-path>`.
- Pass `--backup` to the sync command to always force a local backup before any push that would overwrite a file, even when there is no conflict.
- List backup sessions using `dotling backup list`.
- Prune old backups using `dotling backup clean [--keep-last N] [--older-than DAYS]`. By default, clean keeps the 10 most recent sessions.

#### Conflict Resolution & Three-way Merge
When sync detects a conflict between the repository and your local target, you can choose from the following interactive options:
- `[s]` Diff: Compare inline changes.
- `[k]` Keep Local: Overwrite the repository with your local file (pulls to repo).
- `[r]` Use Repo: Overwrite the local file with the repository version (pushes to local, backs up the local file first).
- `[m]` Merge: Performs a standard line-level **three-way merge**! It uses the last-in-sync snapshot at `~/.dotling/snapshots/` as the base, combining modifications from both the repo (ours) and local target (theirs). Non-overlapping changes are cleanly auto-merged, while overlapping conflicts are highlighted with standard git conflict markers:
  ```text
  <<<<<<< repo
  repo version content
  =======
  actual local content
  >>>>>>> actual
  ```
  The merge outcome is written back to both the local disk and the repository, resolving the conflict.

### Sync Fingerprints

Previously, encrypted entries had to be decrypted to verify their sync state. dotling v0.5.0 introduces lightweight Blake2s-256 sync fingerprints stored in `~/.dotling/fingerprints.toml`.
- After each successful sync, dotling records the content hashes of the `.enc` ciphertext and the local plaintext target.
- On subsequent `status` or `sync` checks, dotling compares current file hashes against the stored fingerprint.
- **Benefits:** You can run `dotling status` or `dotling sync --dry-run` to audit your system instantly, without entering your vault password. A password is only requested when actual file modifications need to be decrypted or re-encrypted!
- For copy-mode plain files, fingerprints track both repo source and target file hashes, enabling deterministic detection of which side has changed (`who_changed()`).

### Dotfile Templating

Some dotfiles contain machine-specific values — a hostname in a Nix flake, a username in a config, a path that differs per machine. dotling v0.6.0 introduces opt-in templating to handle this cleanly.

#### How it works

Any file tracked with `--template` is stored in the repo as `<name>.dtmpl`. On every `sync`, dotling renders the template and writes the output to the deploy target — the repo source is never deployed directly.

```sh
# 1. Set your machine-local variables (saved to ~/.dotling/vars.toml, never committed)
dotling vars set hostname "Macbook-Air-Ade"
dotling vars set primary_user "ade"

# 2. Add a file as a template
dotling add ~/.config/nix-darwin/flake.nix --template

# 3. On another machine, sync will detect missing vars and prompt for them
dotling sync
```

#### Template syntax

```nix
# ~/.config/nix-darwin/flake.nix.dtmpl
darwinConfigurations = {
  {{ var.hostname }} = darwin.lib.darwinSystem { ... };
};
```

```toml
# ~/.config/nix-darwin/configuration.nix.dtmpl
system.primaryUser = "{{ var.primary_user }}";
```

| Expression | Description |
|---|---|
| `{{ var.key }}` | User-defined variable (local or config default) |
| `{{ dotling.hostname }}` | Current machine hostname |
| `{{ dotling.username }}` | Current OS username |
| `{{ dotling.os }}` | `macos`, `linux`, or `windows` |
| `{{ dotling.arch }}` | `x86_64` or `aarch64` |
| `{{ dotling.home }}` | Home directory path |
| `{{ dotling.repo }}` | Dotfiles repo root path |
| `{{ env.VAR }}` | Environment variable |
| `{{ var.key \| upper }}` | Apply a filter (`upper`, `lower`, `trim`, `quote`, `squote`) |
| `{{ var.key \| default "fallback" }}` | Use a fallback if the variable is not set |
| `{{- expr -}}` | Strip surrounding whitespace |

#### Variable sources

Variables are resolved in priority order:

1. **Local store**`~/.dotling/vars.toml` (machine-specific, never committed)
2. **Config defaults**`[vars]` in `dotling.toml` (shared, committed)
3. **Built-ins**`dotling.*` (auto-populated from the machine)
4. **Environment**`env.*` (current process environment)

Shared defaults in `dotling.toml` act as documentation and fallbacks — use placeholders, not real values:

```toml
# dotling.toml
[vars]
hostname = "my-mac"       # placeholder — override in ~/.dotling/vars.toml
primary_user = "user"     # placeholder
```

#### Encrypted templates

Sensitive templates (e.g. a config containing tokens) can be both templated and encrypted:

```sh
dotling add ~/.config/secret.conf --template --encrypt
```

The pipeline on sync is: `vault decrypt → render with vars → deploy`.

#### `dotling vars` reference

```sh
dotling vars list                    # show all resolved variables
dotling vars set hostname "my-mac"   # set a machine-local variable
dotling vars get hostname            # print the resolved value
dotling vars unset hostname          # remove from local store
dotling vars check                   # validate all templates
dotling vars import ~/.env           # bulk-import from .env or TOML
dotling vars export                  # print as TOML (for new machines)
```

## Contributing

Contributions are welcome! Please:

1. Fork the repository
2. Create a feature branch (`git checkout -b feat/my-feature`)
3. Commit your changes (`git commit -m 'feat: add my feature'`)
4. Push to the branch (`git push origin feat/my-feature`)
5. Open a Pull Request

### Development

You can build and test this project using [just](https://github.com/casey/just) inside a Nix environment:

```sh
# Clone and build
git clone https://github.com/auricvex/dotling.git
cd dotling

# List all available recipes
nix develop --command just

# Build dotling
nix develop --command just build

# Run all tests
nix develop --command just test

# Run check and clippy lints
nix develop --command just check
nix develop --command just clippy

# Run formatting checks
nix develop --command just fmt-check

# Run the complete CI suite locally
nix develop --command just ci
```

## License

Licensed under either of:

- [MIT License]LICENSE-MIT
- [Apache License, Version 2.0]LICENSE-APACHE-2.0

at your option.