Skip to main content

Declarative, idempotent system configuration for Ubuntu/Kubuntu — hand it a recipe, it makes the machine comply.

Project description

🧑‍🍳 totchef

Write down how your machine should be set up. Run one command. It complies.

totchef is a declarative, idempotent system configurator for Ubuntu/Kubuntu. You write a recipe.toml — apt repos and packages, vendor CLIs, files in /etc, shell setup, per-app tweaks — and totchef makes the system match it. Run it on a fresh laptop to bootstrap; run it again next week to top up. It only touches what has actually drifted, so re-runs are cheap and safe.

Your tiny line cook: hand it a recipe and it works the whole kitchen — apt packages, vendor CLIs, /etc, per-app tweaks — plating only what isn't already done. "Yes, chef!"

totchef up

Install

totchef is distributed as a uv tool. If you don't have uv yet:

curl -LsSf https://astral.sh/uv/install.sh | sh

Then install (and later upgrade) totchef:

uv tool install totchef
uv tool upgrade totchef

This drops a totchef command on your PATH in its own isolated environment — no Python setup, nothing to pollute your system.


60-second quickstart

  1. Write a recipe.toml in your current directory:

    [apt_pkg]
    packages = ["ripgrep", "fd-find", "btop"]
    
    [url.uv]
    url = "https://astral.sh/uv/install.sh"
    update_action = ["self", "update"]
    
  2. Preview what would change — no root, no writes:

    totchef plan
    
  3. Apply it (escalates to root only for the steps that need it):

    totchef up
    

Run totchef up again any time. Already-satisfied steps report up-to-date and are skipped.


How an up run works

What happens when you run totchef up?

flowchart TD
    R["recipe.toml"] --> F["find and load<br/>(cwd → ~/.config → /etc)"]
    F --> V["validate against<br/>cook schemas"]
    V --> G["build dependency graph<br/>(topological order)"]
    G --> Q{"step needs root?"}
    Q -->|yes| Root["root cooks<br/>run one at a time"]
    Q -->|no| User["user cooks<br/>run concurrently<br/>(privilege dropped)"]
    Root --> S["system converged"]
    User --> S
    S --> Rep["report what changed"]

You launch totchef as yourself. It escalates to root only when a step needs it, and drops back to your user for everything else — so a vendor installer that wants your $HOME runs as you, while an apt transaction runs as root.


Writing a recipe

A recipe is a TOML file. Each section is handled by a cook — a small manager for one domain. The section name picks the cook: [apt_pkg] is cooked by the apt-package cook, [url.uv] by the URL-installer cook.

There are two section shapes:

  • Plain sections — one block of data, one unit of work. [apt_pkg] with a packages = [...] list is a single step.
  • Subtable sections[url.uv], [url.rustup], [file.nvidia_power] — each named entry is its own step, scheduled independently.

Two reserved fields

Any entry may carry two fields that totchef reads and then strips before the cook sees the rest:

Field Meaning
needs_root true runs this step as root; otherwise it runs as you. Grant it on the leaf entry that needs it, never on a subtable header — that would hand root to every entry under it. Most cooks default sensibly (apt/snap are root, vendor installers are not).
depends_on A list of steps that must finish first. Name an entry ("url.rustup"), a single-step section ("apt_pkg"), or a whole section ("apt_repo", which waits on all its entries). totchef topologically sorts the result; a cycle is a lint error.

Any entry may also carry a pre_hook (a guard: a non-zero exit skips the step) and a post_hook (a shell command run only when the step changed something) — on a versioned section like [bun] these gate and follow the whole sync, on a per-resource section like [file.<name>] they gate and follow each resource.

A temporary entry (a pinned workaround waiting on an upstream fix) may declare its expiry: remove_when is a shell probe (exit 0 means "the thing this entry waits on has happened") and remove_how the instruction for dismantling it. Every up and plan evaluates the probes as the invoking user; a fired watch puts remove_how in the end-of-run Action required block until the entry is deleted, while a failing probe (no network, missing tool) just keeps waiting silently.

Section defaults

In a subtable section, keys set on the header are inherited by every entry: lists union (the entry extends the shared list), scalars are overridden by the entry. Handy for sharing a common depends_on or a base feature list:

[desktop]
depends_on = ["apt_pkg"]
features = ["VaapiOnNvidiaGPUs", "WaylandLinuxDrmSyncobj"]

[desktop.brave]
desktop = "/usr/share/applications/brave-browser.desktop"
features = ["AcceleratedVideoEncoder"]   # → unions onto the two above

Validate before you run

totchef lint

Every entry is checked against its cook's schema (unknown keys are an error, not a silent typo), the dependency graph is checked for cycles, and needs_root placement is verified. totchef plan does all of that and then shows you the diff.


Built-in cooks

Run totchef cooks to see what's available on your machine. The ones that ship in the box:

Section Cooks Key fields
[url.<name>] vendor curl | bash installers url, bin, args, update_action, update_guard
[cargo] Rust crates via cargo-binstall packages
[uv] Python CLI tools in isolated venvs packages
[bun] global npm packages via bun add -g packages
[file.<name>] install a file with exact content path, source or content, mode, pre_hook, post_hook
[conf.<name>] own specific lines of a config file target, line or lines, pre_hook, post_hook
[bash.<name>] idempotent shell snippets current_state, desired_state, apply, pre_hook, post_hook
[apt_repo.<name>] third-party apt repos + keys (root) key_url, uris, suites, components, architectures, pin_priority
[apt_pkg] apt packages via nala (root) packages
[snap] snap packages (root) packages
[desktop.<app>] .desktop Exec= overrides desktop, features, switches
[chromium_flags.<app>] Chromium Local State / Electron argv.json local_state, argv_json, features
[settings.<app>] merge an env block into a JSON file settings_json, settings_env

A full, real-world recipe — a hybrid-GPU laptop with an eGPU, NVIDIA drivers, and browser tuning — lives in examples/totchef_recipe.toml.


A recipe repo

A recipe carries its assets and custom cooks beside it, all resolved relative to the recipe:

totchef_recipe.toml   the recipe
totchef_files/        assets a [file]/[local_bin]/... entry installs via `source`
totchef_cooks/        loose <section>_cook.py plugins (highly custom cooks, e.g. chezmoi)

(Each also accepts the totchef.toml / totchef-recipe.toml and totchef-files / totchef-cooks spellings.)

Where totchef looks for the recipe

In precedence order:

  1. --recipe PATH (or -r PATH) — a recipe file, or a repo directory holding one
  2. a recognized recipe name (totchef.toml, totchef_recipe.toml, totchef-recipe.toml) in the current directory, then walking up to /
  3. a recipe pinned by totchef init (saved to ~/.config/totchef/config.toml)

totchef where prints the path it would use, so you're never guessing.

Run totchef from anywhere

The example recipe's [bash.totchef] entry installs totchef to ~/.local/bin via uv tool install, and its post_hook runs totchef init to pin the recipe. After one totchef up, totchef up works from any directory.


Writing your own cook

totchef ships its built-in cooks through an entry-point group — and your cooks join the same way. Two routes, depending on how much ceremony you want.

A packaged plugin (shareable, versioned)

Subclass a cook base in your own package and register the section it serves:

# my_totchef_plugins/foo_cook.py
from totchef.cook_base import PackageListCook


class FooCook(PackageListCook): ...
# your package's pyproject.toml
[project.entry-points."totchef.cooks"]
foo = "my_totchef_plugins.foo_cook:FooCook"

Install it alongside totchef and [foo] becomes a usable section:

uv tool install totchef --with my-totchef-plugins

A local cook (no packaging, instant)

Drop a <section>_cook.py into your recipe's totchef_cooks/ (carried with the recipe) or into ~/.config/totchef/cooks/ (a per-machine hatch). It must define exactly one CookBase subclass; the filename (minus a _cook/_root_cook suffix) is the section it serves. A local cook shadows a built-in of the same name — this is how the example recipe ships chezmoi.

The two cook shapes

  • VersionedCook — for things with versions (packages). You implement list_requested / list_installed / find_latest / sync.
  • StateCook — for desired-state resources (files, settings). You implement get_current_state / get_desired_state / apply_resource. FileStateCook diffs by content hash for you.

Cooks only probe and act — they hold no diff logic. totchef owns every decision about what changed and what to run.


Commands

Command What it does
totchef up Apply the recipe; converge the system to it (escalates to root).
totchef plan Dry-run: probe and print what would change. No root, no writes.
totchef lint Validate the recipe against the cook schemas and exit.
totchef cooks List every available cook and the section it serves.
totchef where Print the recipe path that would be used.
totchef --version Print the version.

All recipe commands accept --recipe/-r PATH.

Every run also writes a full log to ~/.local/state/totchef/logs/totchef-<timestamp>.log (honors $XDG_STATE_HOME).

Set TOTCHEF_INLINE=1 to run every cook in the foreground — no fork, no sudo — with logs streamed straight to the terminal. Use it to debug a cook or to apply under an existing root shell.


One thing to know: convergence is create/update only

totchef drives resources toward their desired presence — it never prunes. Removing an entry from your recipe (or uninstalling its target) leaves prior artifacts in place: a written /etc drop-in, a repo's keyring, a .desktop override. Teardown is deliberate and manual, so a typo can't wipe your system.


Development

totchef lives in the zyplux monorepo as apps/totchef, with its tests under tests/totchef/. From the zyplux repo root:

Command What it does
just c Full gate across every package: install, knip, typecheck, lint, test.
just totchef Apply the example recipe (apps/totchef/examples/totchef_recipe.toml) to this machine.
uv run totchef plan --recipe apps/totchef/examples/totchef_recipe.toml Dry-run the example recipe.
uv run pytest tests/totchef Run just totchef's tests.

License: MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

totchef-0.0.1.tar.gz (55.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

totchef-0.0.1-py3-none-any.whl (73.9 kB view details)

Uploaded Python 3

File details

Details for the file totchef-0.0.1.tar.gz.

File metadata

  • Download URL: totchef-0.0.1.tar.gz
  • Upload date:
  • Size: 55.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for totchef-0.0.1.tar.gz
Algorithm Hash digest
SHA256 49179bdb8ab0f4eab3d56f8753ee927c724ed98fff58c591d25673bf15f6f82a
MD5 994cc59149af95f5c5e0ff43c1d41820
BLAKE2b-256 3035568fda324794bfc248690e84fe878186ba168aec87a61bf5f86a06f11652

See more details on using hashes here.

File details

Details for the file totchef-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: totchef-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 73.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for totchef-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a69a8d3cef89873222ca6942d3c92f97b0d0da3e025fc51df0965db07fb81020
MD5 b9ec1a8ad916937be7300ad21f85832c
BLAKE2b-256 7cdf25bc0a3edd8d8d86f80badd5cf8a0bd01ed3026844b02798c7b201976cbb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page