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.
Files changed (38) hide show
  1. totchef-0.0.1/PKG-INFO +319 -0
  2. totchef-0.0.1/README.md +304 -0
  3. totchef-0.0.1/pyproject.toml +56 -0
  4. totchef-0.0.1/src/totchef/__init__.py +5 -0
  5. totchef-0.0.1/src/totchef/__main__.py +6 -0
  6. totchef-0.0.1/src/totchef/cli.py +316 -0
  7. totchef-0.0.1/src/totchef/cook_base.py +251 -0
  8. totchef-0.0.1/src/totchef/cook_runner.py +605 -0
  9. totchef-0.0.1/src/totchef/cooks/__init__.py +1 -0
  10. totchef-0.0.1/src/totchef/cooks/apt_pkg_root_cook.py +170 -0
  11. totchef-0.0.1/src/totchef/cooks/apt_repo_root_cook.py +159 -0
  12. totchef-0.0.1/src/totchef/cooks/bash_cook.py +51 -0
  13. totchef-0.0.1/src/totchef/cooks/bin_cook_base.py +108 -0
  14. totchef-0.0.1/src/totchef/cooks/bun_cook.py +123 -0
  15. totchef-0.0.1/src/totchef/cooks/cargo_cook.py +101 -0
  16. totchef-0.0.1/src/totchef/cooks/chromium_flags_cook.py +127 -0
  17. totchef-0.0.1/src/totchef/cooks/conf_cook.py +71 -0
  18. totchef-0.0.1/src/totchef/cooks/desktop_cook.py +118 -0
  19. totchef-0.0.1/src/totchef/cooks/file_cook.py +58 -0
  20. totchef-0.0.1/src/totchef/cooks/local_bin_cook.py +10 -0
  21. totchef-0.0.1/src/totchef/cooks/settings_cook.py +56 -0
  22. totchef-0.0.1/src/totchef/cooks/skills_cook.py +435 -0
  23. totchef-0.0.1/src/totchef/cooks/snap_root_cook.py +102 -0
  24. totchef-0.0.1/src/totchef/cooks/url_cook.py +148 -0
  25. totchef-0.0.1/src/totchef/cooks/usr_local_bin_root_cook.py +11 -0
  26. totchef-0.0.1/src/totchef/cooks/usr_local_sbin_root_cook.py +11 -0
  27. totchef-0.0.1/src/totchef/cooks/uv_cook.py +97 -0
  28. totchef-0.0.1/src/totchef/harness.py +161 -0
  29. totchef-0.0.1/src/totchef/log_pump.py +29 -0
  30. totchef-0.0.1/src/totchef/logs.py +208 -0
  31. totchef-0.0.1/src/totchef/recipe.py +138 -0
  32. totchef-0.0.1/src/totchef/recipe_graph.py +144 -0
  33. totchef-0.0.1/src/totchef/recipe_types.py +4 -0
  34. totchef-0.0.1/src/totchef/registry.py +118 -0
  35. totchef-0.0.1/src/totchef/removal_watch.py +114 -0
  36. totchef-0.0.1/src/totchef/schema_lint.py +98 -0
  37. totchef-0.0.1/src/totchef/shell.py +130 -0
  38. 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.
@@ -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"
@@ -0,0 +1,5 @@
1
+ """totchef — declarative, idempotent system configuration. Hand it a recipe; it makes the system comply."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("totchef")
@@ -0,0 +1,6 @@
1
+ """`python -m totchef` and the frozen single-file binary's entry point (see `just build`)."""
2
+
3
+ from totchef.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()