totchef 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- totchef-0.0.1/PKG-INFO +319 -0
- totchef-0.0.1/README.md +304 -0
- totchef-0.0.1/pyproject.toml +56 -0
- totchef-0.0.1/src/totchef/__init__.py +5 -0
- totchef-0.0.1/src/totchef/__main__.py +6 -0
- totchef-0.0.1/src/totchef/cli.py +316 -0
- totchef-0.0.1/src/totchef/cook_base.py +251 -0
- totchef-0.0.1/src/totchef/cook_runner.py +605 -0
- totchef-0.0.1/src/totchef/cooks/__init__.py +1 -0
- totchef-0.0.1/src/totchef/cooks/apt_pkg_root_cook.py +170 -0
- totchef-0.0.1/src/totchef/cooks/apt_repo_root_cook.py +159 -0
- totchef-0.0.1/src/totchef/cooks/bash_cook.py +51 -0
- totchef-0.0.1/src/totchef/cooks/bin_cook_base.py +108 -0
- totchef-0.0.1/src/totchef/cooks/bun_cook.py +123 -0
- totchef-0.0.1/src/totchef/cooks/cargo_cook.py +101 -0
- totchef-0.0.1/src/totchef/cooks/chromium_flags_cook.py +127 -0
- totchef-0.0.1/src/totchef/cooks/conf_cook.py +71 -0
- totchef-0.0.1/src/totchef/cooks/desktop_cook.py +118 -0
- totchef-0.0.1/src/totchef/cooks/file_cook.py +58 -0
- totchef-0.0.1/src/totchef/cooks/local_bin_cook.py +10 -0
- totchef-0.0.1/src/totchef/cooks/settings_cook.py +56 -0
- totchef-0.0.1/src/totchef/cooks/skills_cook.py +435 -0
- totchef-0.0.1/src/totchef/cooks/snap_root_cook.py +102 -0
- totchef-0.0.1/src/totchef/cooks/url_cook.py +148 -0
- totchef-0.0.1/src/totchef/cooks/usr_local_bin_root_cook.py +11 -0
- totchef-0.0.1/src/totchef/cooks/usr_local_sbin_root_cook.py +11 -0
- totchef-0.0.1/src/totchef/cooks/uv_cook.py +97 -0
- totchef-0.0.1/src/totchef/harness.py +161 -0
- totchef-0.0.1/src/totchef/log_pump.py +29 -0
- totchef-0.0.1/src/totchef/logs.py +208 -0
- totchef-0.0.1/src/totchef/recipe.py +138 -0
- totchef-0.0.1/src/totchef/recipe_graph.py +144 -0
- totchef-0.0.1/src/totchef/recipe_types.py +4 -0
- totchef-0.0.1/src/totchef/registry.py +118 -0
- totchef-0.0.1/src/totchef/removal_watch.py +114 -0
- totchef-0.0.1/src/totchef/schema_lint.py +98 -0
- totchef-0.0.1/src/totchef/shell.py +130 -0
- totchef-0.0.1/src/totchef/terminal.py +261 -0
totchef-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: totchef
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Declarative, idempotent system configuration for Ubuntu/Kubuntu — hand it a recipe, it makes the machine comply.
|
|
5
|
+
Keywords: configuration,idempotent,ubuntu,apt,declarative,provisioning
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Dist: loguru>=0.7.3
|
|
8
|
+
Requires-Dist: pydantic>=2.13.4
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
10
|
+
Requires-Dist: rich>=15.0.0
|
|
11
|
+
Requires-Dist: toon-format>=0.9.0b1
|
|
12
|
+
Requires-Dist: typer>=0.26.8
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# 🧑🍳 totchef
|
|
17
|
+
|
|
18
|
+
**Write down how your machine should be set up. Run one command. It complies.**
|
|
19
|
+
|
|
20
|
+
`totchef` is a declarative, idempotent system configurator for Ubuntu/Kubuntu. You
|
|
21
|
+
write a `recipe.toml` — apt repos and packages, vendor CLIs, files in `/etc`,
|
|
22
|
+
shell setup, per-app tweaks — and `totchef` makes the system match it. Run it on a
|
|
23
|
+
fresh laptop to bootstrap; run it again next week to top up. It only touches what
|
|
24
|
+
has actually drifted, so re-runs are cheap and safe.
|
|
25
|
+
|
|
26
|
+
> Your tiny line cook: hand it a recipe and it works the whole kitchen — apt
|
|
27
|
+
> packages, vendor CLIs, `/etc`, per-app tweaks — plating only what isn't already
|
|
28
|
+
> done. *"Yes, chef!"*
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
totchef up
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
`totchef` is distributed as a [uv](https://docs.astral.sh/uv/) tool. If you don't
|
|
39
|
+
have `uv` yet:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then install (and later upgrade) `totchef`:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
uv tool install totchef
|
|
49
|
+
uv tool upgrade totchef
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This drops a `totchef` command on your `PATH` in its own isolated environment — no
|
|
53
|
+
Python setup, nothing to pollute your system.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 60-second quickstart
|
|
58
|
+
|
|
59
|
+
1. Write a `recipe.toml` in your current directory:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[apt_pkg]
|
|
63
|
+
packages = ["ripgrep", "fd-find", "btop"]
|
|
64
|
+
|
|
65
|
+
[url.uv]
|
|
66
|
+
url = "https://astral.sh/uv/install.sh"
|
|
67
|
+
update_action = ["self", "update"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. Preview what would change — no root, no writes:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
totchef plan
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
3. Apply it (escalates to root only for the steps that need it):
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
totchef up
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run `totchef up` again any time. Already-satisfied steps report `up-to-date` and
|
|
83
|
+
are skipped.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## How an `up` run works
|
|
88
|
+
|
|
89
|
+
*What happens when you run `totchef up`?*
|
|
90
|
+
|
|
91
|
+
```mermaid
|
|
92
|
+
flowchart TD
|
|
93
|
+
R["recipe.toml"] --> F["find and load<br/>(cwd → ~/.config → /etc)"]
|
|
94
|
+
F --> V["validate against<br/>cook schemas"]
|
|
95
|
+
V --> G["build dependency graph<br/>(topological order)"]
|
|
96
|
+
G --> Q{"step needs root?"}
|
|
97
|
+
Q -->|yes| Root["root cooks<br/>run one at a time"]
|
|
98
|
+
Q -->|no| User["user cooks<br/>run concurrently<br/>(privilege dropped)"]
|
|
99
|
+
Root --> S["system converged"]
|
|
100
|
+
User --> S
|
|
101
|
+
S --> Rep["report what changed"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You launch `totchef` as yourself. It escalates to root only when a step needs it,
|
|
105
|
+
and drops back to your user for everything else — so a vendor installer that wants
|
|
106
|
+
your `$HOME` runs as you, while an apt transaction runs as root.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Writing a recipe
|
|
111
|
+
|
|
112
|
+
A recipe is a TOML file. Each **section** is handled by a **cook** — a small
|
|
113
|
+
manager for one domain. The section name picks the cook: `[apt_pkg]` is cooked by
|
|
114
|
+
the apt-package cook, `[url.uv]` by the URL-installer cook.
|
|
115
|
+
|
|
116
|
+
There are two section shapes:
|
|
117
|
+
|
|
118
|
+
- **Plain sections** — one block of data, one unit of work. `[apt_pkg]` with a
|
|
119
|
+
`packages = [...]` list is a single step.
|
|
120
|
+
- **Subtable sections** — `[url.uv]`, `[url.rustup]`, `[file.nvidia_power]` — each
|
|
121
|
+
named entry is its own step, scheduled independently.
|
|
122
|
+
|
|
123
|
+
### Two reserved fields
|
|
124
|
+
|
|
125
|
+
Any entry may carry two fields that `totchef` reads and then strips before the cook
|
|
126
|
+
sees the rest:
|
|
127
|
+
|
|
128
|
+
| Field | Meaning |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `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). |
|
|
131
|
+
| `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. |
|
|
132
|
+
|
|
133
|
+
Any entry may also carry a `pre_hook` (a guard: a non-zero exit **skips** the step)
|
|
134
|
+
and a `post_hook` (a shell command run **only when the step changed something**) —
|
|
135
|
+
on a versioned section like `[bun]` these gate and follow the whole sync, on a
|
|
136
|
+
per-resource section like `[file.<name>]` they gate and follow each resource.
|
|
137
|
+
|
|
138
|
+
A temporary entry (a pinned workaround waiting on an upstream fix) may declare its
|
|
139
|
+
expiry: `remove_when` is a shell probe (exit 0 means "the thing this entry waits on
|
|
140
|
+
has happened") and `remove_how` the instruction for dismantling it. Every `up` and
|
|
141
|
+
`plan` evaluates the probes as the invoking user; a fired watch puts `remove_how`
|
|
142
|
+
in the end-of-run `Action required` block until the entry is deleted, while a
|
|
143
|
+
failing probe (no network, missing tool) just keeps waiting silently.
|
|
144
|
+
|
|
145
|
+
### Section defaults
|
|
146
|
+
|
|
147
|
+
In a subtable section, keys set on the header are inherited by every entry: lists
|
|
148
|
+
**union** (the entry extends the shared list), scalars are **overridden** by the
|
|
149
|
+
entry. Handy for sharing a common `depends_on` or a base feature list:
|
|
150
|
+
|
|
151
|
+
```toml
|
|
152
|
+
[desktop]
|
|
153
|
+
depends_on = ["apt_pkg"]
|
|
154
|
+
features = ["VaapiOnNvidiaGPUs", "WaylandLinuxDrmSyncobj"]
|
|
155
|
+
|
|
156
|
+
[desktop.brave]
|
|
157
|
+
desktop = "/usr/share/applications/brave-browser.desktop"
|
|
158
|
+
features = ["AcceleratedVideoEncoder"] # → unions onto the two above
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Validate before you run
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
totchef lint
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Every entry is checked against its cook's schema (unknown keys are an error, not a
|
|
168
|
+
silent typo), the dependency graph is checked for cycles, and `needs_root`
|
|
169
|
+
placement is verified. `totchef plan` does all of that and then shows you the diff.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Built-in cooks
|
|
174
|
+
|
|
175
|
+
Run `totchef cooks` to see what's available on your machine. The ones that ship in
|
|
176
|
+
the box:
|
|
177
|
+
|
|
178
|
+
| Section | Cooks | Key fields |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `[url.<name>]` | vendor `curl \| bash` installers | `url`, `bin`, `args`, `update_action`, `update_guard` |
|
|
181
|
+
| `[cargo]` | Rust crates via `cargo-binstall` | `packages` |
|
|
182
|
+
| `[uv]` | Python CLI tools in isolated venvs | `packages` |
|
|
183
|
+
| `[bun]` | global npm packages via `bun add -g` | `packages` |
|
|
184
|
+
| `[file.<name>]` | install a file with exact content | `path`, `source` or `content`, `mode`, `pre_hook`, `post_hook` |
|
|
185
|
+
| `[conf.<name>]` | own specific lines of a config file | `target`, `line` or `lines`, `pre_hook`, `post_hook` |
|
|
186
|
+
| `[bash.<name>]` | idempotent shell snippets | `current_state`, `desired_state`, `apply`, `pre_hook`, `post_hook` |
|
|
187
|
+
| `[apt_repo.<name>]` | third-party apt repos + keys (root) | `key_url`, `uris`, `suites`, `components`, `architectures`, `pin_priority` |
|
|
188
|
+
| `[apt_pkg]` | apt packages via `nala` (root) | `packages` |
|
|
189
|
+
| `[snap]` | snap packages (root) | `packages` |
|
|
190
|
+
| `[desktop.<app>]` | `.desktop` `Exec=` overrides | `desktop`, `features`, `switches` |
|
|
191
|
+
| `[chromium_flags.<app>]` | Chromium Local State / Electron `argv.json` | `local_state`, `argv_json`, `features` |
|
|
192
|
+
| `[settings.<app>]` | merge an env block into a JSON file | `settings_json`, `settings_env` |
|
|
193
|
+
|
|
194
|
+
A full, real-world recipe — a hybrid-GPU laptop with an eGPU, NVIDIA drivers, and
|
|
195
|
+
browser tuning — lives in [`examples/totchef_recipe.toml`](examples/totchef_recipe.toml).
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## A recipe repo
|
|
200
|
+
|
|
201
|
+
A recipe carries its assets and custom cooks beside it, all resolved relative to the recipe:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
totchef_recipe.toml the recipe
|
|
205
|
+
totchef_files/ assets a [file]/[local_bin]/... entry installs via `source`
|
|
206
|
+
totchef_cooks/ loose <section>_cook.py plugins (highly custom cooks, e.g. chezmoi)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
(Each also accepts the `totchef.toml` / `totchef-recipe.toml` and `totchef-files` / `totchef-cooks` spellings.)
|
|
210
|
+
|
|
211
|
+
## Where `totchef` looks for the recipe
|
|
212
|
+
|
|
213
|
+
In precedence order:
|
|
214
|
+
|
|
215
|
+
1. `--recipe PATH` (or `-r PATH`) — a recipe file, or a repo directory holding one
|
|
216
|
+
2. a recognized recipe name (`totchef.toml`, `totchef_recipe.toml`, `totchef-recipe.toml`) in the current directory, then walking up to `/`
|
|
217
|
+
3. a recipe pinned by `totchef init` (saved to `~/.config/totchef/config.toml`)
|
|
218
|
+
|
|
219
|
+
`totchef where` prints the path it would use, so you're never guessing.
|
|
220
|
+
|
|
221
|
+
## Run `totchef` from anywhere
|
|
222
|
+
|
|
223
|
+
The example recipe's `[bash.totchef]` entry installs totchef to `~/.local/bin` via
|
|
224
|
+
`uv tool install`, and its `post_hook` runs `totchef init` to pin the recipe. After one
|
|
225
|
+
`totchef up`, `totchef up` works from any directory.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Writing your own cook
|
|
230
|
+
|
|
231
|
+
`totchef` ships its built-in cooks through an entry-point group — and your cooks
|
|
232
|
+
join the same way. Two routes, depending on how much ceremony you want.
|
|
233
|
+
|
|
234
|
+
### A packaged plugin (shareable, versioned)
|
|
235
|
+
|
|
236
|
+
Subclass a cook base in your own package and register the section it serves:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
# my_totchef_plugins/foo_cook.py
|
|
240
|
+
from totchef.cook_base import PackageListCook
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class FooCook(PackageListCook): ...
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```toml
|
|
247
|
+
# your package's pyproject.toml
|
|
248
|
+
[project.entry-points."totchef.cooks"]
|
|
249
|
+
foo = "my_totchef_plugins.foo_cook:FooCook"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Install it alongside `totchef` and `[foo]` becomes a usable section:
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
uv tool install totchef --with my-totchef-plugins
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### A local cook (no packaging, instant)
|
|
259
|
+
|
|
260
|
+
Drop a `<section>_cook.py` into your recipe's `totchef_cooks/` (carried with the recipe) or
|
|
261
|
+
into `~/.config/totchef/cooks/` (a per-machine hatch). It must define exactly one `CookBase`
|
|
262
|
+
subclass; the filename (minus a `_cook`/`_root_cook` suffix) is the section it serves. A
|
|
263
|
+
local cook **shadows** a built-in of the same name — this is how the example recipe ships
|
|
264
|
+
`chezmoi`.
|
|
265
|
+
|
|
266
|
+
### The two cook shapes
|
|
267
|
+
|
|
268
|
+
- **`VersionedCook`** — for things with versions (packages). You implement
|
|
269
|
+
`list_requested` / `list_installed` / `find_latest` / `sync`.
|
|
270
|
+
- **`StateCook`** — for desired-state resources (files, settings). You implement
|
|
271
|
+
`get_current_state` / `get_desired_state` / `apply_resource`. `FileStateCook`
|
|
272
|
+
diffs by content hash for you.
|
|
273
|
+
|
|
274
|
+
Cooks only *probe* and *act* — they hold no diff logic. `totchef` owns every
|
|
275
|
+
decision about what changed and what to run.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Commands
|
|
280
|
+
|
|
281
|
+
| Command | What it does |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `totchef up` | Apply the recipe; converge the system to it (escalates to root). |
|
|
284
|
+
| `totchef plan` | Dry-run: probe and print what would change. No root, no writes. |
|
|
285
|
+
| `totchef lint` | Validate the recipe against the cook schemas and exit. |
|
|
286
|
+
| `totchef cooks` | List every available cook and the section it serves. |
|
|
287
|
+
| `totchef where` | Print the recipe path that would be used. |
|
|
288
|
+
| `totchef --version` | Print the version. |
|
|
289
|
+
|
|
290
|
+
All recipe commands accept `--recipe/-r PATH`.
|
|
291
|
+
|
|
292
|
+
Every run also writes a full log to `~/.local/state/totchef/logs/totchef-<timestamp>.log` (honors `$XDG_STATE_HOME`).
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## One thing to know: convergence is create/update only
|
|
299
|
+
|
|
300
|
+
`totchef` drives resources toward their desired *presence* — it never prunes.
|
|
301
|
+
Removing an entry from your recipe (or uninstalling its target) leaves prior
|
|
302
|
+
artifacts in place: a written `/etc` drop-in, a repo's keyring, a `.desktop`
|
|
303
|
+
override. Teardown is deliberate and manual, so a typo can't wipe your system.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Development
|
|
308
|
+
|
|
309
|
+
`totchef` lives in the [zyplux](https://github.com/zyplux/zyplux) monorepo as
|
|
310
|
+
`apps/totchef`, with its tests under `tests/totchef/`. From the zyplux repo root:
|
|
311
|
+
|
|
312
|
+
| Command | What it does |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `just c` | Full gate across every package: install, knip, typecheck, lint, test. |
|
|
315
|
+
| `just totchef` | Apply the example recipe (`apps/totchef/examples/totchef_recipe.toml`) to this machine. |
|
|
316
|
+
| `uv run totchef plan --recipe apps/totchef/examples/totchef_recipe.toml` | Dry-run the example recipe. |
|
|
317
|
+
| `uv run pytest tests/totchef` | Run just totchef's tests. |
|
|
318
|
+
|
|
319
|
+
License: MIT.
|
totchef-0.0.1/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# 🧑🍳 totchef
|
|
2
|
+
|
|
3
|
+
**Write down how your machine should be set up. Run one command. It complies.**
|
|
4
|
+
|
|
5
|
+
`totchef` is a declarative, idempotent system configurator for Ubuntu/Kubuntu. You
|
|
6
|
+
write a `recipe.toml` — apt repos and packages, vendor CLIs, files in `/etc`,
|
|
7
|
+
shell setup, per-app tweaks — and `totchef` makes the system match it. Run it on a
|
|
8
|
+
fresh laptop to bootstrap; run it again next week to top up. It only touches what
|
|
9
|
+
has actually drifted, so re-runs are cheap and safe.
|
|
10
|
+
|
|
11
|
+
> Your tiny line cook: hand it a recipe and it works the whole kitchen — apt
|
|
12
|
+
> packages, vendor CLIs, `/etc`, per-app tweaks — plating only what isn't already
|
|
13
|
+
> done. *"Yes, chef!"*
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
totchef up
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
`totchef` is distributed as a [uv](https://docs.astral.sh/uv/) tool. If you don't
|
|
24
|
+
have `uv` yet:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then install (and later upgrade) `totchef`:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv tool install totchef
|
|
34
|
+
uv tool upgrade totchef
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This drops a `totchef` command on your `PATH` in its own isolated environment — no
|
|
38
|
+
Python setup, nothing to pollute your system.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 60-second quickstart
|
|
43
|
+
|
|
44
|
+
1. Write a `recipe.toml` in your current directory:
|
|
45
|
+
|
|
46
|
+
```toml
|
|
47
|
+
[apt_pkg]
|
|
48
|
+
packages = ["ripgrep", "fd-find", "btop"]
|
|
49
|
+
|
|
50
|
+
[url.uv]
|
|
51
|
+
url = "https://astral.sh/uv/install.sh"
|
|
52
|
+
update_action = ["self", "update"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2. Preview what would change — no root, no writes:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
totchef plan
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
3. Apply it (escalates to root only for the steps that need it):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
totchef up
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run `totchef up` again any time. Already-satisfied steps report `up-to-date` and
|
|
68
|
+
are skipped.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## How an `up` run works
|
|
73
|
+
|
|
74
|
+
*What happens when you run `totchef up`?*
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
flowchart TD
|
|
78
|
+
R["recipe.toml"] --> F["find and load<br/>(cwd → ~/.config → /etc)"]
|
|
79
|
+
F --> V["validate against<br/>cook schemas"]
|
|
80
|
+
V --> G["build dependency graph<br/>(topological order)"]
|
|
81
|
+
G --> Q{"step needs root?"}
|
|
82
|
+
Q -->|yes| Root["root cooks<br/>run one at a time"]
|
|
83
|
+
Q -->|no| User["user cooks<br/>run concurrently<br/>(privilege dropped)"]
|
|
84
|
+
Root --> S["system converged"]
|
|
85
|
+
User --> S
|
|
86
|
+
S --> Rep["report what changed"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
You launch `totchef` as yourself. It escalates to root only when a step needs it,
|
|
90
|
+
and drops back to your user for everything else — so a vendor installer that wants
|
|
91
|
+
your `$HOME` runs as you, while an apt transaction runs as root.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Writing a recipe
|
|
96
|
+
|
|
97
|
+
A recipe is a TOML file. Each **section** is handled by a **cook** — a small
|
|
98
|
+
manager for one domain. The section name picks the cook: `[apt_pkg]` is cooked by
|
|
99
|
+
the apt-package cook, `[url.uv]` by the URL-installer cook.
|
|
100
|
+
|
|
101
|
+
There are two section shapes:
|
|
102
|
+
|
|
103
|
+
- **Plain sections** — one block of data, one unit of work. `[apt_pkg]` with a
|
|
104
|
+
`packages = [...]` list is a single step.
|
|
105
|
+
- **Subtable sections** — `[url.uv]`, `[url.rustup]`, `[file.nvidia_power]` — each
|
|
106
|
+
named entry is its own step, scheduled independently.
|
|
107
|
+
|
|
108
|
+
### Two reserved fields
|
|
109
|
+
|
|
110
|
+
Any entry may carry two fields that `totchef` reads and then strips before the cook
|
|
111
|
+
sees the rest:
|
|
112
|
+
|
|
113
|
+
| Field | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `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). |
|
|
116
|
+
| `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. |
|
|
117
|
+
|
|
118
|
+
Any entry may also carry a `pre_hook` (a guard: a non-zero exit **skips** the step)
|
|
119
|
+
and a `post_hook` (a shell command run **only when the step changed something**) —
|
|
120
|
+
on a versioned section like `[bun]` these gate and follow the whole sync, on a
|
|
121
|
+
per-resource section like `[file.<name>]` they gate and follow each resource.
|
|
122
|
+
|
|
123
|
+
A temporary entry (a pinned workaround waiting on an upstream fix) may declare its
|
|
124
|
+
expiry: `remove_when` is a shell probe (exit 0 means "the thing this entry waits on
|
|
125
|
+
has happened") and `remove_how` the instruction for dismantling it. Every `up` and
|
|
126
|
+
`plan` evaluates the probes as the invoking user; a fired watch puts `remove_how`
|
|
127
|
+
in the end-of-run `Action required` block until the entry is deleted, while a
|
|
128
|
+
failing probe (no network, missing tool) just keeps waiting silently.
|
|
129
|
+
|
|
130
|
+
### Section defaults
|
|
131
|
+
|
|
132
|
+
In a subtable section, keys set on the header are inherited by every entry: lists
|
|
133
|
+
**union** (the entry extends the shared list), scalars are **overridden** by the
|
|
134
|
+
entry. Handy for sharing a common `depends_on` or a base feature list:
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
[desktop]
|
|
138
|
+
depends_on = ["apt_pkg"]
|
|
139
|
+
features = ["VaapiOnNvidiaGPUs", "WaylandLinuxDrmSyncobj"]
|
|
140
|
+
|
|
141
|
+
[desktop.brave]
|
|
142
|
+
desktop = "/usr/share/applications/brave-browser.desktop"
|
|
143
|
+
features = ["AcceleratedVideoEncoder"] # → unions onto the two above
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Validate before you run
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
totchef lint
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Every entry is checked against its cook's schema (unknown keys are an error, not a
|
|
153
|
+
silent typo), the dependency graph is checked for cycles, and `needs_root`
|
|
154
|
+
placement is verified. `totchef plan` does all of that and then shows you the diff.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Built-in cooks
|
|
159
|
+
|
|
160
|
+
Run `totchef cooks` to see what's available on your machine. The ones that ship in
|
|
161
|
+
the box:
|
|
162
|
+
|
|
163
|
+
| Section | Cooks | Key fields |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `[url.<name>]` | vendor `curl \| bash` installers | `url`, `bin`, `args`, `update_action`, `update_guard` |
|
|
166
|
+
| `[cargo]` | Rust crates via `cargo-binstall` | `packages` |
|
|
167
|
+
| `[uv]` | Python CLI tools in isolated venvs | `packages` |
|
|
168
|
+
| `[bun]` | global npm packages via `bun add -g` | `packages` |
|
|
169
|
+
| `[file.<name>]` | install a file with exact content | `path`, `source` or `content`, `mode`, `pre_hook`, `post_hook` |
|
|
170
|
+
| `[conf.<name>]` | own specific lines of a config file | `target`, `line` or `lines`, `pre_hook`, `post_hook` |
|
|
171
|
+
| `[bash.<name>]` | idempotent shell snippets | `current_state`, `desired_state`, `apply`, `pre_hook`, `post_hook` |
|
|
172
|
+
| `[apt_repo.<name>]` | third-party apt repos + keys (root) | `key_url`, `uris`, `suites`, `components`, `architectures`, `pin_priority` |
|
|
173
|
+
| `[apt_pkg]` | apt packages via `nala` (root) | `packages` |
|
|
174
|
+
| `[snap]` | snap packages (root) | `packages` |
|
|
175
|
+
| `[desktop.<app>]` | `.desktop` `Exec=` overrides | `desktop`, `features`, `switches` |
|
|
176
|
+
| `[chromium_flags.<app>]` | Chromium Local State / Electron `argv.json` | `local_state`, `argv_json`, `features` |
|
|
177
|
+
| `[settings.<app>]` | merge an env block into a JSON file | `settings_json`, `settings_env` |
|
|
178
|
+
|
|
179
|
+
A full, real-world recipe — a hybrid-GPU laptop with an eGPU, NVIDIA drivers, and
|
|
180
|
+
browser tuning — lives in [`examples/totchef_recipe.toml`](examples/totchef_recipe.toml).
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## A recipe repo
|
|
185
|
+
|
|
186
|
+
A recipe carries its assets and custom cooks beside it, all resolved relative to the recipe:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
totchef_recipe.toml the recipe
|
|
190
|
+
totchef_files/ assets a [file]/[local_bin]/... entry installs via `source`
|
|
191
|
+
totchef_cooks/ loose <section>_cook.py plugins (highly custom cooks, e.g. chezmoi)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
(Each also accepts the `totchef.toml` / `totchef-recipe.toml` and `totchef-files` / `totchef-cooks` spellings.)
|
|
195
|
+
|
|
196
|
+
## Where `totchef` looks for the recipe
|
|
197
|
+
|
|
198
|
+
In precedence order:
|
|
199
|
+
|
|
200
|
+
1. `--recipe PATH` (or `-r PATH`) — a recipe file, or a repo directory holding one
|
|
201
|
+
2. a recognized recipe name (`totchef.toml`, `totchef_recipe.toml`, `totchef-recipe.toml`) in the current directory, then walking up to `/`
|
|
202
|
+
3. a recipe pinned by `totchef init` (saved to `~/.config/totchef/config.toml`)
|
|
203
|
+
|
|
204
|
+
`totchef where` prints the path it would use, so you're never guessing.
|
|
205
|
+
|
|
206
|
+
## Run `totchef` from anywhere
|
|
207
|
+
|
|
208
|
+
The example recipe's `[bash.totchef]` entry installs totchef to `~/.local/bin` via
|
|
209
|
+
`uv tool install`, and its `post_hook` runs `totchef init` to pin the recipe. After one
|
|
210
|
+
`totchef up`, `totchef up` works from any directory.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Writing your own cook
|
|
215
|
+
|
|
216
|
+
`totchef` ships its built-in cooks through an entry-point group — and your cooks
|
|
217
|
+
join the same way. Two routes, depending on how much ceremony you want.
|
|
218
|
+
|
|
219
|
+
### A packaged plugin (shareable, versioned)
|
|
220
|
+
|
|
221
|
+
Subclass a cook base in your own package and register the section it serves:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# my_totchef_plugins/foo_cook.py
|
|
225
|
+
from totchef.cook_base import PackageListCook
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class FooCook(PackageListCook): ...
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```toml
|
|
232
|
+
# your package's pyproject.toml
|
|
233
|
+
[project.entry-points."totchef.cooks"]
|
|
234
|
+
foo = "my_totchef_plugins.foo_cook:FooCook"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Install it alongside `totchef` and `[foo]` becomes a usable section:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
uv tool install totchef --with my-totchef-plugins
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### A local cook (no packaging, instant)
|
|
244
|
+
|
|
245
|
+
Drop a `<section>_cook.py` into your recipe's `totchef_cooks/` (carried with the recipe) or
|
|
246
|
+
into `~/.config/totchef/cooks/` (a per-machine hatch). It must define exactly one `CookBase`
|
|
247
|
+
subclass; the filename (minus a `_cook`/`_root_cook` suffix) is the section it serves. A
|
|
248
|
+
local cook **shadows** a built-in of the same name — this is how the example recipe ships
|
|
249
|
+
`chezmoi`.
|
|
250
|
+
|
|
251
|
+
### The two cook shapes
|
|
252
|
+
|
|
253
|
+
- **`VersionedCook`** — for things with versions (packages). You implement
|
|
254
|
+
`list_requested` / `list_installed` / `find_latest` / `sync`.
|
|
255
|
+
- **`StateCook`** — for desired-state resources (files, settings). You implement
|
|
256
|
+
`get_current_state` / `get_desired_state` / `apply_resource`. `FileStateCook`
|
|
257
|
+
diffs by content hash for you.
|
|
258
|
+
|
|
259
|
+
Cooks only *probe* and *act* — they hold no diff logic. `totchef` owns every
|
|
260
|
+
decision about what changed and what to run.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Commands
|
|
265
|
+
|
|
266
|
+
| Command | What it does |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `totchef up` | Apply the recipe; converge the system to it (escalates to root). |
|
|
269
|
+
| `totchef plan` | Dry-run: probe and print what would change. No root, no writes. |
|
|
270
|
+
| `totchef lint` | Validate the recipe against the cook schemas and exit. |
|
|
271
|
+
| `totchef cooks` | List every available cook and the section it serves. |
|
|
272
|
+
| `totchef where` | Print the recipe path that would be used. |
|
|
273
|
+
| `totchef --version` | Print the version. |
|
|
274
|
+
|
|
275
|
+
All recipe commands accept `--recipe/-r PATH`.
|
|
276
|
+
|
|
277
|
+
Every run also writes a full log to `~/.local/state/totchef/logs/totchef-<timestamp>.log` (honors `$XDG_STATE_HOME`).
|
|
278
|
+
|
|
279
|
+
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.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## One thing to know: convergence is create/update only
|
|
284
|
+
|
|
285
|
+
`totchef` drives resources toward their desired *presence* — it never prunes.
|
|
286
|
+
Removing an entry from your recipe (or uninstalling its target) leaves prior
|
|
287
|
+
artifacts in place: a written `/etc` drop-in, a repo's keyring, a `.desktop`
|
|
288
|
+
override. Teardown is deliberate and manual, so a typo can't wipe your system.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
`totchef` lives in the [zyplux](https://github.com/zyplux/zyplux) monorepo as
|
|
295
|
+
`apps/totchef`, with its tests under `tests/totchef/`. From the zyplux repo root:
|
|
296
|
+
|
|
297
|
+
| Command | What it does |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `just c` | Full gate across every package: install, knip, typecheck, lint, test. |
|
|
300
|
+
| `just totchef` | Apply the example recipe (`apps/totchef/examples/totchef_recipe.toml`) to this machine. |
|
|
301
|
+
| `uv run totchef plan --recipe apps/totchef/examples/totchef_recipe.toml` | Dry-run the example recipe. |
|
|
302
|
+
| `uv run pytest tests/totchef` | Run just totchef's tests. |
|
|
303
|
+
|
|
304
|
+
License: MIT.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "totchef"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Declarative, idempotent system configuration for Ubuntu/Kubuntu — hand it a recipe, it makes the machine comply."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = [
|
|
9
|
+
"configuration",
|
|
10
|
+
"idempotent",
|
|
11
|
+
"ubuntu",
|
|
12
|
+
"apt",
|
|
13
|
+
"declarative",
|
|
14
|
+
"provisioning",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"loguru>=0.7.3",
|
|
18
|
+
"pydantic>=2.13.4",
|
|
19
|
+
"pyyaml>=6.0.3",
|
|
20
|
+
"rich>=15.0.0",
|
|
21
|
+
"toon-format>=0.9.0b1",
|
|
22
|
+
"typer>=0.26.8",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
totchef = "totchef.cli:main"
|
|
27
|
+
|
|
28
|
+
# Built-in cooks register through the same entry-point group third-party plugins use:
|
|
29
|
+
# `pip install`/`uv tool install --with` a package that adds to [project.entry-points."totchef.cooks"]
|
|
30
|
+
# and its sections become available with no change to totchef itself.
|
|
31
|
+
[project.entry-points."totchef.cooks"]
|
|
32
|
+
url = "totchef.cooks.url_cook:UrlCook"
|
|
33
|
+
cargo = "totchef.cooks.cargo_cook:CargoCook"
|
|
34
|
+
uv = "totchef.cooks.uv_cook:UvCook"
|
|
35
|
+
bun = "totchef.cooks.bun_cook:BunCook"
|
|
36
|
+
skills = "totchef.cooks.skills_cook:SkillsCook"
|
|
37
|
+
file = "totchef.cooks.file_cook:FileCook"
|
|
38
|
+
conf = "totchef.cooks.conf_cook:ConfCook"
|
|
39
|
+
usr_local_bin = "totchef.cooks.usr_local_bin_root_cook:UsrLocalBinCook"
|
|
40
|
+
usr_local_sbin = "totchef.cooks.usr_local_sbin_root_cook:UsrLocalSbinCook"
|
|
41
|
+
local_bin = "totchef.cooks.local_bin_cook:LocalBinCook"
|
|
42
|
+
bash = "totchef.cooks.bash_cook:BashCook"
|
|
43
|
+
apt_repo = "totchef.cooks.apt_repo_root_cook:AptRepoCook"
|
|
44
|
+
apt_pkg = "totchef.cooks.apt_pkg_root_cook:AptPkgCook"
|
|
45
|
+
snap = "totchef.cooks.snap_root_cook:SnapCook"
|
|
46
|
+
desktop = "totchef.cooks.desktop_cook:DesktopCook"
|
|
47
|
+
chromium_flags = "totchef.cooks.chromium_flags_cook:ChromiumFlagsCook"
|
|
48
|
+
settings = "totchef.cooks.settings_cook:SettingsCook"
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
52
|
+
build-backend = "uv_build"
|
|
53
|
+
|
|
54
|
+
[tool.uv.build-backend]
|
|
55
|
+
module-name = "totchef"
|
|
56
|
+
module-root = "src"
|