↑ subaud · local-first · MIT  

Declarative machine setup you can read before you run

kempt

One manifest describes the machine you want — kempt shows you every change before it makes it.

A config repo is one kempt.toml plus the files it references. kempt parses it, shows you a plan, and applies only what you approve. The primitive set is closed and there is no exec/script escape hatch — every action is a built-in, versioned primitive, so the whole plan is reviewable before anything on your machine changes.

what it's for

You should be able to read a setup before you run it.

Machine setup today is imperative: a dotfiles installer, a bundle file, a shell script you pipe into sh and hope. You run it and find out what it did afterward — there is no honest way to read the full set of changes first, because the script decides as it goes. kempt inverts that. The setup is declarative — a manifest of the state you want, not a sequence of commands — so kempt can inspect the machine, compute the difference, and show you the exact plan before it touches anything. Nothing changes until you approve the diff.

declarative, not imperative

The manifest describes the end state — this software present, these files linked, this service loaded — not the steps to get there. kempt derives the steps, and derives them against your machine as it actually is right now.

plan before apply

kempt plan is read-only. It prints every delta apply would make and changes nothing. What you read is what apply does — there is no branch the plan can't see, because every action is a known primitive.

no escape hatch

There is no exec, no run, no script hook — not as a discouraged option, but as an absent one. A manifest can only invoke kempt's built-in primitives, so a plan can never hide an arbitrary command.

idempotent by contract

Every step computes current vs desired and no-ops on a match. Running apply twice is safe; converging a mostly-set-up machine is cheap; the plan is honest about how little is actually left to do.

the manifest

One kempt.toml, plus the files it references.

A config repo is a single kempt.toml and the config files it points at. The manifest is TOML and declares spec = 1; a parser rejects spec values it doesn't know rather than mis-parse a manifest from the future. Packages are made of ordered steps and run in dependency (needs) order. Crucially, a manifest describes what — the safety class of each action (software, files, read-only) lives in kempt's own code, never in the manifest, so a config can't declare its own installs "safe."

Profiles are named package sets — personas like developer or minimalnot conditionals. The only conditional in the manifest is only = { os, arch } on a package or a step, and it is always visible in plan output.

primitives

A closed set of primitives.

Every action a manifest can take is one of the primitives below — that is the whole vocabulary. The set is deliberately closed: when a real capability gap appears, the answer is a new versioned primitive in kempt, not a script hook. That is what keeps a plan fully reviewable. Each primitive carries a fixed safety class — software, files, or read-only — assigned by kempt, not the manifest.

install

brew · winget · apt · npm · pi

Cross-platform backends; kempt selects the applicable ones at plan time. Backends are additive — npm/pi install on any host with the runtime, alongside the OS backend. npm and pi entries can be pinned as name@version; kempt then converges to that exact version.

github-release

download

github-release templates the asset with {os}/{arch}, sha256-verifies against checksums.txt, and installs atomically. download fetches from a domain distribution instead of GitHub, verifying a .sha256 sidecar via the same stage-and-atomic-move path.

git-clone

service

git-clone pins a ref or branch. service renders a launchd plist (macOS) or systemd --user unit (Linux) and does a cmp-before-reload, so an unchanged service never restarts.

files

symlink · json-merge · toml-merge · line-in-file

symlink links a repo-relative source (backup = true moves a real file to .bak first). json-merge and toml-merge do an additive, idempotent deep merge; line-in-file ensures a line or block.

verify

command-exists · http-ok · version-current

Read-only checks that touch nothing: is a command on PATH (command-exists / command-exists-any), does a URL return 2xx (http-ok), does a symlink point where it should, has a GitHub release drifted (version-current).

no exec, ever

— absent by design —

There is no primitive that runs an arbitrary command, and there is no plan to add one. A notes field can surface post-install hints in plan output, but notes are documentation only — they run nothing.

plan & apply

Read the diff, then apply it.

The core loop is two commands. kempt plan inspects the machine and prints the deltas apply would make — read-only, it changes nothing. kempt apply converges the machine to the manifest, applying only the changes you approved (it prompts unless you pass -yes). Installs stage to a temp file and land with an atomic mv; services are cmp-compared before reload so unchanged ones never restart.

plan

kempt plan

The read-only diff. Prints every delta with its primitive and safety class, and honors only = { os, arch } visibly. Pass -os/-arch to dry-plan for another platform without inspecting the local machine.

apply

kempt apply -yes

Converges to the manifest, applying only approved changes. Atomic installs, and cmp-before-reload services so nothing restarts without cause.

the update split

kempt update · kempt refresh

update pulls the repo, self-updates the binary, and converges. refresh recomputes pending changes and — with auto-apply enabled — applies files-class changes only, never software.

verify

kempt verify

Runs the manifest's declared read-only checks — the same verify steps that appear in a plan — to confirm the machine still matches, touching nothing.

install

Install, point it at a repo, plan, apply.

The installer downloads the latest release for your platform, verifies its checksum fail-closed, and drops the kempt binary in ~/.local/bin (override with KEMPT_INSTALL_DIR). Then point kempt at your config repo, read the plan, and apply it.

# 1. install the binary (fail-closed checksum, installs to ~/.local/bin)
$ curl -fsSL https://kempt.tools/install.sh | sh
#    or build from source:  go install ./cmd/kempt

# 2. point kempt at your config repo
$ kempt init <repo-url>

# 3. read the plan, then apply only what you approve
$ kempt plan
$ kempt apply

Binaries are served from the kempt.tools domain under the download family-URL contract; GitHub releases remain as a durable backing store. See the spec for the manifest schema and the full primitive set.