dek
Declarative environment setup. One TOML, any machine.
Install
# or
# setup completions
Usage
All commands have short aliases: apply, check, plan, run, test, dx (exec).
Config is loaded from: ./dek.toml, ./dek/, or $XDG_CONFIG_HOME/dek/ (fallback).
Config
# Packages
[] # auto-detects: pacman, apt, brew
= ["curl", "git", "htop"]
[]
= ["build-essential"]
[] # falls back to yay for AUR packages
= ["base-devel", "yay"]
[]
= ["bat", "eza", "ripgrep"]
[]
= ["github.com/junegunn/fzf@latest"]
[]
= ["prettier", "typescript"]
[]
= ["httpie", "tldr"]
[]
= ["poetry", "black"]
[]
= ["jq", "yq"]
# Systemd services
[[]]
= "docker"
= "active"
= true
# User services (systemctl --user, no sudo)
[[]]
= "syncthing"
= "active"
= true
= "user"
# Files
[]
= "~/.zshrc"
[]
= "~/.config/nvim"
[]
= ["export PATH=$HOME/.local/bin:$PATH"]
# Structured line management
[[]]
= "/etc/needrestart/needrestart.conf"
= "$nrconf{restart} = 'l';"
= "#$nrconf{restart} = 'i';"
= "replace"
[[]]
= "/etc/ssh/sshd_config"
= "PermitRootLogin no"
= "^#?PermitRootLogin\\s+"
= "replace"
# Shell
[]
= "ls -larth"
= "git"
[]
= "nvim"
# System
= "Europe/Istanbul"
= "workstation"
# Scripts (installed to ~/.local/bin)
[]
= "scripts/cleanup.sh"
# Custom commands
[[]]
= "setup-db"
= "psql -c 'SELECT 1 FROM pg_database WHERE datname=mydb'"
= "createdb mydb"
# Assertions
[[]]
= "dotty up to date"
= "git -C ~/dotty fetch -q && test $(git -C ~/dotty rev-list --count HEAD..@{upstream}) -eq 0"
= "dotty has remote changes"
[[]]
= "note conflicts"
= "rg --files ~/Sync/vault 2>/dev/null | grep conflict | sed 's|.*/||'"
[[]]
= "stow"
= "for p in common nvim tmux; do stow -d ~/dotty -n -v $p 2>&1 | grep -q LINK && echo $p; done"
Assertions
Assertions are check-only items — they report issues but don't change anything. Two modes:
check — pass if command exits 0:
[[]]
= "docker running"
= "docker info >/dev/null 2>&1"
= "docker daemon is not running"
= "some regex" # optional: also match stdout
foreach — each stdout line is a finding (zero lines = pass):
[[]]
= "stow packages"
= "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):
[]
= ["base-devel"]
= "command -v pacman"
[[]]
= "desktop stow"
= "echo $(uname -n) | grep -qE 'marko|bender'"
= "..."
[]
= "test -d /etc/apt" # skip entire config file
Package:Binary Syntax
When package and binary names differ:
[]
= ["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
= "myproject"
= "Project deployment"
= "1.0"
= ["@setup", "@deploy"] # default selectors for apply
= "../devops/inventory.ini" # custom inventory path
[]
= "ubuntu:22.04"
= true
Labels & Selectors
Tag configs with labels for grouped selection:
# 10-deps.toml
[]
= "Dependencies"
= ["setup"]
[]
= ["curl", "git"]
# 20-deploy.toml
[]
= "Deploy"
= ["deploy"]
[]
= "/opt/app/app.jar"
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:
[]
= "Deploy the application"
= ["os.rsync"]
= "rsync -av ./dist/ server:/var/www/"
[]
= "Backup database"
= "scripts/backup.sh" # relative to config dir
[]
= "systemctl restart myapp"
= true # prompt before running
[]
= "journalctl -fu myapp"
= true # interactive, uses ssh -t
Remote Run
Run commands on remote hosts without deploying dek — just SSH the command directly:
-t— single host, prints output directly. Withtty: true, usesssh -tfor interactive commands.-r— multi-host from inventory, runs in parallel with progress spinners.tty: truecommands 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:
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):
# inventory.ini
[web]
web-01
web-02
web-03
[db]
db-master
Hosts are deployed in parallel. Override inventory path in meta.toml:
= "../devops/inventory.ini"
Deploy Workflow
Use [[artifact]] to build locally before shipping to remotes or baking:
[[]]
= "app.jar"
= "mvn package -DskipTests -q"
= ["src", "pom.xml"] # skip build if unchanged
= "target/app-1.0.jar" # build output
= "artifacts/app.jar" # placed in config for shipping
[]
= "/opt/app/app.jar"
[[]]
= "app"
= "active"
# 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
# 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).
Inline
Quick installs without a config file:
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.
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:
Configure defaults in meta.toml:
[]
= "ubuntu:22.04"
CLI flags override meta.toml (-i/--image, -r/--rm).
Bake
Embed config into a standalone binary: