dek 0.1.28

Declarative environment setup and scripted workflows from TOML
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# dek

Declarative environment setup. One TOML, any machine.

## Install

```bash
cargo install dek
# or
cargo binstall dek

# setup completions
dek setup
```

## Usage

```bash
dek apply              # apply ./dek.toml or ./dek/
dek check              # dry-run, show what would change
dek plan               # list items (no state check)
dek run <name>         # run a command from config
dek test               # test in container
dek exec <cmd>         # run command in test container
dek bake               # bake into standalone binary
```

All commands have short aliases: `a`pply, `c`heck, `p`lan, `r`un, `t`est, `dx` (exec).

Config is loaded from: `./dek.toml`, `./dek/`, or `$XDG_CONFIG_HOME/dek/` (fallback).

## Config

```toml
# Packages
[package.os]  # auto-detects: pacman, apt, brew
items = ["curl", "git", "htop"]

[package.apt]
items = ["build-essential"]

[package.pacman]  # falls back to yay for AUR packages
items = ["base-devel", "yay"]

[package.cargo]
items = ["bat", "eza", "ripgrep"]

[package.go]
items = ["github.com/junegunn/fzf@latest"]

[package.npm]
items = ["prettier", "typescript"]

[package.pip]
items = ["httpie", "tldr"]

[package.pipx]
items = ["poetry", "black"]

[package.webi]
items = ["jq", "yq"]

# Systemd services
[[service]]
name = "docker"
state = "active"
enabled = true

# User services (systemctl --user, no sudo)
[[service]]
name = "syncthing"
state = "active"
enabled = true
scope = "user"

# Files
[file.copy]
"dotfiles/.zshrc" = "~/.zshrc"

[file.fetch]
"https://raw.githubusercontent.com/user/repo/main/.bashrc" = "~/.bashrc"
"https://example.com/config.json" = { path = "~/.config/app/config.json", ttl = "1h" }

[file.symlink]
"~/dotfiles/nvim" = "~/.config/nvim"

[file.ensure_line]
"~/.bashrc" = ["export PATH=$HOME/.local/bin:$PATH"]

# Structured line management
[[file.line]]
path = "/etc/needrestart/needrestart.conf"
line = "$nrconf{restart} = 'l';"
original = "#$nrconf{restart} = 'i';"
mode = "replace"

[[file.line]]
path = "/etc/ssh/sshd_config"
line = "PermitRootLogin no"
original_regex = "^#?PermitRootLogin\\s+"
mode = "replace"

# Shell
[alias]
la = "ls -larth"
g = "git"

[env]
EDITOR = "nvim"

# System
timezone = "Europe/Istanbul"
hostname = "workstation"

# Scripts (installed to ~/.local/bin)
[script]
cleanup = "scripts/cleanup.sh"

# Custom commands
[[command]]
name = "setup-db"
check = "psql -c 'SELECT 1 FROM pg_database WHERE datname=mydb'"
apply = "createdb mydb"

# Assertions
[[assert]]
name = "dotty up to date"
check = "git -C ~/dotty fetch -q && test $(git -C ~/dotty rev-list --count HEAD..@{upstream}) -eq 0"
message = "dotty has remote changes"

[[assert]]
name = "note conflicts"
foreach = "rg --files ~/Sync/vault 2>/dev/null | grep conflict | sed 's|.*/||'"

[[assert]]
name = "stow"
foreach = "for p in common nvim tmux; do stow -d ~/dotty -n -v $p 2>&1 | grep -q LINK && echo $p; done"
```

## File Fetch

Download files from URLs. Results are cached at `~/.cache/dek/url/`. Use `ttl` to control cache expiry:

```toml
[file.fetch]
# Cache forever (re-fetches only when cache is cleared)
"https://raw.githubusercontent.com/user/repo/main/.bashrc" = "~/.bashrc"

# Cache for 1 hour — re-fetches if older
"https://example.com/config.json" = { path = "~/.config/app/config.json", ttl = "1h" }
```

Supported TTL units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days). Can be combined: `1h30m`.

## Vars

Runtime variables defined in `meta.toml`, set in the process environment before anything runs. Available to all providers, commands, scripts — locally and remotely.

```toml
# meta.toml
[vars]
APP_NAME = "myapp"
DEPLOY_DIR = "/opt/default"

# Scoped by @label — applied when that label is selected
[vars."@staging"]
DEPLOY_DIR = "/opt/staging"
DB_HOST = "staging-db"

[vars."@production"]
DEPLOY_DIR = "/opt/production"
DB_HOST = "prod-db"

# Scoped by config key
[vars.deploy]
NOTIFY = "true"
```

Base vars are always set. Scoped vars overlay when their selector is active:

```bash
dek apply @staging    # APP_NAME=myapp DEPLOY_DIR=/opt/staging DB_HOST=staging-db
dek apply @production # APP_NAME=myapp DEPLOY_DIR=/opt/production DB_HOST=prod-db
dek apply deploy      # APP_NAME=myapp DEPLOY_DIR=/opt/default NOTIFY=true
```

Vars are inherited by all child processes, so `[[command]]` check/apply, `[script]`, and remote `dek apply` all see them.

## Cache Key

Skip steps when a value hasn't changed since last successful apply. Works on `[[command]]`, `[[service]]`, and `[[file.line]]`.

**`cache_key`** — a string value (supports `$VAR` expansion):

```toml
[[command]]
name = "generate test data"
check = "test -f /opt/test/input.csv"
apply = "generate-data.sh"
cache_key = "$INPUT_FILE_SIZE_MB"  # only re-runs when size changes
```

**`cache_key_cmd`** — a command whose stdout is the cache key:

```toml
[[command]]
name = "deploy jar"
check = "test -f /opt/dpi/jar/dpi.jar"
apply = "cp build/dpi.jar /opt/dpi/jar/"
cache_key_cmd = "sha256sum build/dpi.jar"  # only re-deploys when jar changes
```

Cache state is stored in `~/.cache/dek/state/`. The provider's `check` always runs — if the state is missing (e.g. file deleted), apply runs regardless of cache. The cache key only skips the apply when check already passes and the key is unchanged. After a successful apply (or when check passes with a new key), the current value is stored.

## Assertions

Assertions are check-only items — they report issues but don't change anything. Two modes:

**check** — pass if command exits 0:

```toml
[[assert]]
name = "docker running"
check = "docker info >/dev/null 2>&1"
message = "docker daemon is not running"
stdout = "some regex"  # optional: also match stdout
```

**foreach** — each stdout line is a finding (zero lines = pass):

```toml
[[assert]]
name = "stow packages"
foreach = "for p in common nvim; do stow -n -v $p 2>&1 | grep -q LINK && echo $p; done"
```

In `dek check`, assertions show as `✓`/`✗`. In `dek apply`, failing assertions show as issues (not "changed") and don't block other items.

## Conditional Execution

Any item supports `run_if` — a shell command that gates execution (skip if non-zero):

```toml
[package.pacman]
items = ["base-devel"]
run_if = "command -v pacman"

[[assert]]
name = "desktop stow"
run_if = "echo $(uname -n) | grep -qE 'marko|bender'"
foreach = "..."

[meta]
run_if = "test -d /etc/apt"  # skip entire config file
```

## Package:Binary Syntax

When package and binary names differ:

```toml
[package.cargo]
items = ["ripgrep:rg", "fd-find:fd", "bottom:btm"]
```

Installs `ripgrep`, checks for `rg` in PATH.

## Split Config

```
dek/
├── meta.toml           # project metadata + defaults
├── banner.txt          # optional banner (shown on apply/help)
├── inventory.ini       # remote hosts (one per line)
├── 00-packages.toml
├── 10-services.toml
├── 20-dotfiles.toml
└── optional/
    └── extra.toml      # only applied when explicitly selected
```

Files merged alphabetically. Use `dek apply extra` to include optional configs.

### meta.toml

```toml
name = "myproject"
description = "Project deployment"
version = "1.0"
defaults = ["@setup", "@deploy"]     # default selectors for apply
inventory = "../devops/inventory.ini" # custom inventory path

[test]
image = "ubuntu:22.04"
keep = true
```

### Labels & Selectors

Tag configs with labels for grouped selection:

```toml
# 10-deps.toml
[meta]
name = "Dependencies"
labels = ["setup"]

[package.os]
items = ["curl", "git"]
```

```toml
# 20-deploy.toml
[meta]
name = "Deploy"
labels = ["deploy"]

[file.copy]
"app.jar" = "/opt/app/app.jar"
```

```bash
dek apply @setup          # only configs labeled "setup"
dek apply @deploy         # only configs labeled "deploy"
dek apply @setup tools    # @label refs and config keys can be mixed
dek apply                 # uses meta.toml defaults (or all main configs if no defaults)
```

When `defaults` is set in `meta.toml`, a bare `dek apply` applies only those selectors. Without `defaults`, it applies all non-optional configs (backward compatible).

## Run Commands

Define reusable commands:

```toml
[run.deploy]
description = "Deploy the application"
deps = ["os.rsync"]
cmd = "rsync -av ./dist/ server:/var/www/"

[run.backup]
description = "Backup database"
script = "scripts/backup.sh"  # relative to config dir

[run.restart]
cmd = "systemctl restart myapp"
confirm = true                 # prompt before running

[run.logs]
cmd = "journalctl -fu myapp"
tty = true                     # interactive, uses ssh -t
```

```bash
dek run              # list available commands
dek run deploy       # run command
dek run backup arg1  # args passed via $@
```

### Remote Run

Run commands on remote hosts without deploying dek — just SSH the command directly:

```bash
dek run restart -t server1        # single host
dek run restart -r 'app-*'        # multi-host (parallel)
dek run logs -t server1           # tty command (interactive)
```

- **`-t`** — single host, prints output directly. With `tty: true`, uses `ssh -t` for interactive commands.
- **`-r`** — multi-host from inventory, runs in parallel with progress spinners. `tty: true` commands are rejected (can't attach TTY to multiple hosts).
- **`confirm: true`** — prompts `[y/N]` before running (works both locally and remotely).

## Remote

Apply to remote hosts via SSH:

```bash
dek apply -t user@host
dek check -t server1
```

Use `-q`/`--quiet` to suppress banners (auto-enabled for multi-host). Use `--color always|never|auto` to control colored output.

### Multi-host with Inventory

Ansible-style `inventory.ini` (one host per line, `[groups]` and `;comments` ignored):

```ini
# inventory.ini
[web]
web-01
web-02
web-03

[db]
db-master
```

```bash
dek apply -r 'web-*'    # glob pattern (-r is short for --remotes)
dek apply -r '*'         # all hosts
```

Hosts are deployed in parallel. Override inventory path in `meta.toml`:

```toml
inventory = "../devops/inventory.ini"
```

### Deploy Workflow

Use `[[artifact]]` to build locally before shipping to remotes or baking:

```toml
[[artifact]]
name = "app.jar"
build = "mvn package -DskipTests -q"
watch = ["src", "pom.xml"]              # skip build if unchanged
src = "target/app-1.0.jar"              # build output
dest = "artifacts/app.jar"              # placed in config for shipping

[file.copy]
"artifacts/app.jar" = "/opt/app/app.jar"

[[service]]
name = "app"
state = "active"
```

```bash
dek apply -r 'app-*'
# 1. Builds artifact locally (skips if watch hash unchanged)
# 2. Packages config + artifact into tarball
# 3. Ships to all app-* hosts in parallel
# 4. Copies jar, restarts service

dek bake -o myapp
# Artifact is built and included in the baked binary
```

Artifacts are resolved before any config processing — they work with `apply`, `apply -r`, and `bake`.

Freshness can be determined two ways:
- **`watch`** — list of files/directories to hash (path + size + mtime). Build is skipped when the hash matches the previous run. Best for source trees.
- **`check`** — shell command that exits 0 if the artifact is fresh. Use for custom logic (e.g., `test target/app.jar -nt pom.xml`).

**`deps`** — local dependencies needed before build. Ensures build tools exist on the machine running the build:

```toml
[[artifact]]
name = "app.jar"
build = "mvn package -DskipTests -q"
deps = ["apt.default-jdk:java", "apt.maven:mvn"]
src = "target/app-1.0.jar"
dest = "artifacts/app.jar"
```

Format: `"package:binary"` — installs `package` if `binary` isn't in PATH. Prefix with package manager (`apt.`, `pacman.`, `brew.`) to force a specific one, or omit for auto-detection (`os.`).

## Inline

Quick installs without a config file:

```bash
dek os.htop os.git cargo.bat
dek pip.httpie npm.prettier
```

## Test

Bakes config into the binary and runs it in a container. The baked `dek` inside the container is fully functional — `apply`, `list`, `run` all work.

```bash
dek test                     # bake + create container + apply + shell
dek test @core               # only apply @core labeled configs
dek test -i ubuntu tools     # custom image + specific configs
dek test                     # (second run) rebake + apply + shell (container kept)
dek test -f                  # force new container (remove + recreate)
dek test -a                  # attach to running container (no rebuild)
dek test -r                  # remove container after exit
```

Containers are kept by default and named `dek-test-{name}` (from `meta.toml` name or directory). On subsequent runs, dek rebakes the binary, copies it into the existing container, reapplies config, and drops into a shell — installed packages and files persist.

### Exec

Run commands directly in the test container:

```bash
dek exec ls /opt/app         # run a command
dek dx cat /etc/os-release   # dx is a short alias
dek dx dek run version       # run dek commands inside
dek dx dek list              # list configs in container
```

Configure defaults in `meta.toml`:

```toml
[test]
image = "ubuntu:22.04"
```

CLI flags override meta.toml (`-i/--image`, `-r/--rm`).

## Completions

Dynamic completions for configs, @labels, and run commands.

### Manual

```bash
dek setup              # auto-detect shell, install completions
dek completions zsh    # raw output (pipe to file yourself)
```

### Via dek config

Add to any config (e.g., `15-shell.toml`):

```toml
[[command]]
name = "dek completions"
cmd = "dek setup"
check = "dek _complete check"
```

Completions support all aliases (`a`, `c`, `p`, `r`, `t`, `dx`) and dynamically complete config keys, `@labels`, and run command names from whatever config is in the current directory.

## Bake

Embed config into a standalone binary:

```bash
dek bake ./dek -o mysetup
./mysetup              # show help with available configs
./mysetup apply        # apply all
./mysetup run deploy   # run commands
```