@codebend3r/che 1.0.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/bin/che.py +869 -0
  4. package/bin/commands.py +872 -0
  5. package/bin/drives/eject-all-drives.py +203 -0
  6. package/bin/drives/mount-all-drives.py +140 -0
  7. package/bin/drives/ping-nas.py +242 -0
  8. package/bin/files/compress-folders.py +188 -0
  9. package/bin/files/delete-by-ext.py +108 -0
  10. package/bin/files/delete-empty-folders.py +136 -0
  11. package/bin/files/delete-smb-files.py +105 -0
  12. package/bin/files/files-under-size.py +132 -0
  13. package/bin/files/find-largest-files.py +102 -0
  14. package/bin/files/make-alpha-dir.py +63 -0
  15. package/bin/git/all-actions.py +545 -0
  16. package/bin/git/checkout-my-branches.py +147 -0
  17. package/bin/git/clean-stale-branches.py +161 -0
  18. package/bin/git/prune-worktrees.py +143 -0
  19. package/bin/git/sync-all-branches.py +368 -0
  20. package/bin/git/update-local-branches.py +195 -0
  21. package/bin/install.py +1515 -0
  22. package/bin/shellgen.py +782 -0
  23. package/bin/system/btop-launch.py +114 -0
  24. package/bin/system/update-brew.py +146 -0
  25. package/bin/tui.py +648 -0
  26. package/bin/utils.py +836 -0
  27. package/bin/video/delete-duplicate-videos.py +313 -0
  28. package/bin/video/detect-green-magenta-videos.py +268 -0
  29. package/bin/video/find-movie-by-year.py +84 -0
  30. package/bin/video/find-video-mkv-issues.py +242 -0
  31. package/bin/video/fix-codecs.py +209 -0
  32. package/bin/video/largest-tv-shows.py +175 -0
  33. package/bin/video/remove-metadata.py +126 -0
  34. package/bin/video/rename-video-file.py +329 -0
  35. package/bin/video/scan-videos-audio-language.py +110 -0
  36. package/bin/video/show-codecs.py +111 -0
  37. package/bin/video/validate-video-files.py +114 -0
  38. package/bin/video/video-list.py +120 -0
  39. package/package.json +54 -0
  40. package/shell/che.bash +382 -0
  41. package/shell/che.fish +282 -0
  42. package/shell/che.zsh +389 -0
  43. package/shell/completions/_che +146 -0
  44. package/shell/completions/che.bash +107 -0
  45. package/shell/completions/che.fish +371 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CJ Rivas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,424 @@
1
+ <div align="center">
2
+
3
+ # `che`
4
+
5
+ **A collection of helper scripts shared amongst computers, called by name from
6
+ your shell, and browsable through one menu.**
7
+
8
+ [![CI](https://github.com/codebend3r/che/actions/workflows/ci.yml/badge.svg)](https://github.com/codebend3r/che/actions/workflows/ci.yml)
9
+ [![npm](https://img.shields.io/npm/v/%40codebend3r%2Fche)](https://www.npmjs.com/package/@codebend3r/che)
10
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)
11
+ ![Dependencies: stdlib only](https://img.shields.io/badge/dependencies-stdlib%20only-brightgreen)
12
+ ![Shells: zsh, bash, fish](https://img.shields.io/badge/shells-zsh%20%7C%20bash%20%7C%20fish-8A2BE2)
13
+
14
+ <img src="menu.png" alt="The che menu: commands grouped by category on the left, details for the selected command on the right" width="850">
15
+
16
+ </div>
17
+
18
+ The scripts are Python, standard library only. The names you type
19
+ (`delete-by-ext`, `sync-all-branches`, `show-codecs`, …) are shell functions
20
+ generated from one manifest, `bin/commands.py`, and installed into whichever
21
+ shells you use.
22
+
23
+ ```
24
+ che browse everything: type to filter, Enter to run
25
+ che <command> [args…] run one directly
26
+ delete-by-ext --path=… or just call it by name
27
+ ```
28
+
29
+ <!-- che:count -->
30
+ 33 commands across 5 categories, plus 6 built into `che` itself
31
+ <!-- /che:count -->
32
+
33
+ ## Highlights
34
+
35
+ - **One manifest, everything generated.** Wrappers, completions, the menu, the
36
+ doctor report and the tables in this README all come from `bin/commands.py`;
37
+ `bun run ci` fails if any of them drift.
38
+ - **Destructive commands preview by default.** Every deleter has a `-dr` twin
39
+ that only prints, and the menu opens on the dry-run.
40
+ - **Standard library only.** A bare `python3` 3.12+ runs everything; no venv,
41
+ no `pip install`, on any machine the repo is cloned to.
42
+ - **Your dotfiles stay yours.** The installer writes a few lines between
43
+ markers, backs every rc file up first, and never touches anything outside
44
+ the markers.
45
+ - **`che doctor` knows the whole story.** Interpreter, generated files, rc
46
+ blocks, external tools, alias collisions — one command, non-zero on failure.
47
+
48
+ ## Install
49
+
50
+ From a clone:
51
+
52
+ ```sh
53
+ git clone https://github.com/codebend3r/che.git ~/Developer/git/che
54
+ ~/Developer/git/che/install.sh
55
+ ```
56
+
57
+ Or in one line, which clones for you (into `~/Developer/git/shell-functions`,
58
+ or wherever `CHE_HOME` points):
59
+
60
+ ```sh
61
+ curl -fsSL https://raw.githubusercontent.com/codebend3r/che/main/install.sh | bash
62
+ ```
63
+
64
+ Or from npm, if Node is around:
65
+
66
+ ```sh
67
+ npm install -g @codebend3r/che
68
+ che # offers to install the wrappers on first run
69
+ ```
70
+
71
+ The wrappers point back at wherever the package landed, so install it for real
72
+ rather than through `npx`, whose cache is temporary. Update that kind of install
73
+ with `npm install -g @codebend3r/che@latest`; `che update` only pulls a git
74
+ clone, and says so.
75
+
76
+ `install.sh` finds a suitable python3, then hands over to `bin/install.py`,
77
+ which opens a first-run wizard: it lists the shells it found and the rc files
78
+ it would touch, asks whether to add a `che` on `PATH` and whether to keep the
79
+ completion chimes, offers to remove hand-written wrappers left over from before
80
+ the installer existed, and reports any missing external tool (offering to
81
+ `brew install` them when Homebrew is present).
82
+
83
+ <div align="center">
84
+ <img src="first-run.png" alt="The first run screen: che is not installed in this shell yet, offering to install, open the menu, or quit" width="700">
85
+ <br>
86
+ <em>Running <code>che</code> before installing offers to set itself up.</em>
87
+ </div>
88
+
89
+ Skip the wizard:
90
+
91
+ ```sh
92
+ ./install.sh --yes # into the shells it detects
93
+ ./install.sh --yes --shells=zsh,fish # or the ones you name
94
+ ./install.sh --dry-run # report what would change, write nothing
95
+ ./install.sh --yes --replace-legacy # also remove pre-che hand-written wrappers
96
+ ./install.sh --print # print the rc block, install nothing
97
+ ```
98
+
99
+ Every rc file is copied to `~/.config/che/backups/` before it is edited.
100
+
101
+ ### What lands where
102
+
103
+ | Thing | Where |
104
+ | --- | --- |
105
+ | The rc block, six or seven lines between markers | `~/.zshrc` (or `$ZDOTDIR/.zshrc`), `~/.bashrc` and `~/.bash_profile` (whichever exist), `~/.config/fish/config.fish`, and `~/.profile` only if you ask for `sh` |
106
+ | The wrappers themselves | `shell/che.zsh`, `shell/che.bash`, `shell/che.fish` in this repo |
107
+ | Tab completions | `shell/completions/`, sourced by the same wrapper file |
108
+ | A `che` executable, for cron and scripts that never read an rc file | `~/.local/bin/che` |
109
+ | What you answered | `~/.config/che/config.json` |
110
+
111
+ The block itself is small on purpose, so upgrading che never rewrites your
112
+ dotfiles:
113
+
114
+ ```zsh
115
+ # >>> che shell functions >>>
116
+ # Managed by `che install` (che 1.0.0). Edits here are overwritten.
117
+ export CHE_HOME=/Users/you/Developer/git/shell-functions
118
+ export CHE_PYTHON=/opt/homebrew/bin/python3
119
+ [ -r /Users/you/Developer/git/shell-functions/shell/che.zsh ] && . /Users/you/Developer/git/shell-functions/shell/che.zsh
120
+ case ":$PATH:" in *":/Users/you/.local/bin:"*) ;; *) PATH="/Users/you/.local/bin:$PATH" ;; esac
121
+ # <<< che shell functions <<<
122
+ ```
123
+
124
+ Everything between those markers belongs to `che install` and is rewritten on
125
+ every install. Everything outside them is yours and is never touched.
126
+
127
+ Then reload:
128
+
129
+ ```sh
130
+ exec zsh # or exec bash / exec fish
131
+ ```
132
+
133
+ **Requires Python 3.12+** and nothing else to run the scripts. The installer
134
+ picks the newest suitable interpreter it can find and records its path in the
135
+ block, so an old `/usr/bin/python3` does not matter as long as some newer
136
+ python3 exists. `brew install python@3.13` if none does.
137
+
138
+ ### Uninstall
139
+
140
+ ```sh
141
+ che uninstall # remove the block, the shim and the completions
142
+ che uninstall --purge # also delete ~/.config/che
143
+ ```
144
+
145
+ The repo itself is left alone; delete the directory to finish.
146
+
147
+ ## Using it
148
+
149
+ `che` on its own opens the menu. Anything that can preview **previews by
150
+ default**, so pressing Enter on `delete-by-ext` shows you what it would delete
151
+ rather than deleting it.
152
+
153
+ Type to filter — the list narrows as you go, and the right pane always shows
154
+ the selected command's summary, script, required tools and flags:
155
+
156
+ <div align="center">
157
+ <img src="search.png" alt="The menu filtered by the search term branch, showing the five matching commands" width="850">
158
+ </div>
159
+
160
+ <!-- che:keys -->
161
+ | Key | Does |
162
+ | --- | --- |
163
+ | `↑ ↓ / j k` | move |
164
+ | `PgUp PgDn / g G` | jump |
165
+ | `⏎` | run (asks for arguments) |
166
+ | `x` | run with no arguments |
167
+ | `d` | toggle dry-run for this command |
168
+ | `h` | show the command's own --help |
169
+ | `/` | search, Esc to clear |
170
+ | `?` | this list |
171
+ | `q` | quit |
172
+ <!-- /che:keys -->
173
+
174
+ Every command is also a plain shell function, so `type delete-by-ext` shows
175
+ exactly what it runs, and the destructive ones have a `-dr` twin that only
176
+ prints:
177
+
178
+ ```sh
179
+ delete-by-ext --path=/Volumes/Media --ext=nfo,txt # deletes
180
+ delete-by-ext-dr --path=/Volumes/Media --ext=nfo # shows what it would delete
181
+ ```
182
+
183
+ `che <command>` runs the same thing with the same environment the wrapper would
184
+ have used, and `che help <command>` prints that script's own `--help`.
185
+
186
+ ### Commands
187
+
188
+ <!-- che:commands -->
189
+ | Command | Does | Preview |
190
+ | --- | --- | --- |
191
+ | **Git** | *Branch hygiene, worktrees and GitHub Actions* | |
192
+ | `all-actions` | GitHub Actions status for every open PR you authored | |
193
+ | `all-actions-watch` | Same as all-actions, refreshed on an interval | |
194
+ | `checkout-my-branches` | Check out recent remote branches you authored that aren't local yet | |
195
+ | `clean-stale-branches` | Delete local branches whose upstream is gone | `clean-stale-branches-dr` |
196
+ | `prune-worktrees` | Remove every linked worktree, then prune stale admin records | `prune-worktrees-dr` |
197
+ | `sync-all-branches` | Push every worktree, collapse the pushed ones, then tidy branches | `sync-all-branches-dr` |
198
+ | `update-from-origin` | Same as sync-all-branches but never pushes: fetch, collapse, rebase | |
199
+ | `update-local-branches` | Rebase every local branch that has an upstream onto origin | `--dry-run` |
200
+ | **Video** | *Codecs, renaming, dedupe and metadata* | |
201
+ | `show-codecs` | Report media files outside the Direct Play codec/container set | |
202
+ | `fix-codecs` | Re-encode media to h265/aac mp4 so it Direct Plays | `fix-codecs-dr` |
203
+ | `find-video-mkv-issues` | Scan MKV files and estimate Plex direct-play compatibility | |
204
+ | `validate-video-files` | Check .mp4/.mkv files decode by playing one frame with mpv | |
205
+ | `scan-videos-audio-language` | Print the audio language tags of every video under a path | |
206
+ | `remove-metadata` | Strip all metadata from video files recursively | none |
207
+ | `rename-video-file` | Title-case video filenames (and optionally their folders) | `--dry-run` |
208
+ | `delete-duplicate-videos` | Delete duplicate MKV/MP4 files under a root directory | `delete-duplicate-videos-dr` |
209
+ | `video-list` | List .mp4/.mkv files under a path with human-readable sizes | |
210
+ | `detect-green-magenta-videos` | Detect videos with the green/magenta chroma artifact | |
211
+ | `find-movie-by-year` | Find movie folders whose name ends with "(YYYY)" | |
212
+ | `largest-tv-shows` | Rank TV show folders in a library by total size on disk | |
213
+ | **Files** | *Delete, compress and inspect by size or extension* | |
214
+ | `delete-by-ext` | Delete files under a path matching a set of extensions | `delete-by-ext-dr` |
215
+ | `delete-empty-folders` | Delete truly-empty directories under a path, cascading upwards | `delete-empty-folders-dr` |
216
+ | `delete-smb-files` | Delete .smbdelete* files left behind by an SMB share | `delete-smb-files-dr` |
217
+ | `files-under-size` | Find video files at or under a size threshold | `files-under-size-dr` |
218
+ | `find-largest-files` | List the largest files under a path, biggest first | |
219
+ | `make-alpha-dir` | Create '#' and A-Z bucket folders under a parent directory | |
220
+ | `compress-folders` | Zip every immediate subfolder of a path at max compression | `--dry-run` |
221
+ | `list-permission` | Show ownership and mode of a volume under /Volumes | |
222
+ | **Drives** | *Mount, eject and keep-alive for the NAS volumes* | |
223
+ | `mount-all-drives` | Mount all NAS drives over SMB via AppleScript | |
224
+ | `eject-all-drives` | Eject all NAS volumes from /Volumes | `eject-all-drives-dr` |
225
+ | `ping-nas` | Keep-alive pinger that remounts a NAS drive that dropped off | |
226
+ | **System** | *Homebrew, monitoring and the machine itself* | |
227
+ | `update-brew` | Update Homebrew, upgrade formulae and casks, then clean up | `update-brew-dr` |
228
+ | `btop` | Launch btop with a gruvbox theme matching the macOS appearance | |
229
+ <!-- /che:commands -->
230
+
231
+ Two commands have no `-dr` twin. `remove-metadata` cannot preview at all, since
232
+ exiftool rewrites in place. `rename-video-file` does not need one: it already
233
+ defaults to `--dry-run=true`, so the real rename is the one that needs
234
+ `--dry-run=false`.
235
+
236
+ ### Managing the install
237
+
238
+ <!-- che:builtins -->
239
+ | Command | Does |
240
+ | --- | --- |
241
+ | `che install` | Install the shell wrappers into your shell startup files |
242
+ | `che update` | Pull the latest scripts and refresh the installed wrappers |
243
+ | `che doctor` | Check the install, the interpreter and every external tool |
244
+ | `che uninstall` | Remove the wrappers, the shim and the completions |
245
+ | `che list` | List every command, one per line |
246
+ | `che completions` | Print the completion script for a shell |
247
+ <!-- /che:builtins -->
248
+
249
+ `che doctor` checks the interpreter, the repo, whether the generated files are
250
+ current, whether each rc file has the block, whether `che` is on `PATH`, every
251
+ external tool, and anything shadowing a wrapper. It exits non-zero on a real
252
+ failure, so it works in a health check. `che doctor --json` for a machine.
253
+
254
+ <div align="center">
255
+ <img src="doctor.png" alt="che doctor output: green checks for python, repo and generated files, with problems and warnings each followed by the command that fixes them" width="850">
256
+ <br>
257
+ <em>Every problem <code>che doctor</code> reports comes with the command that fixes it.</em>
258
+ </div>
259
+
260
+ ### Environment
261
+
262
+ | Variable | Effect |
263
+ | --- | --- |
264
+ | `CHE_HOME` | Where this repo lives. Set by the block; the wrapper file falls back to its own location. |
265
+ | `CHE_PYTHON` | Interpreter the wrappers use. Set by the block to the one the installer verified. |
266
+ | `CHE_SOUNDS` | `0` turns off the `playsound-N` chime after every command. |
267
+ | `DRY_RUN` | What the wrappers set for the destructive commands. Exporting it yourself changes nothing for them, since the wrapper sets it explicitly on every call; it only reaches a script invoked directly. |
268
+ | `NO_COLOR` | Honoured by every script and by the menu. |
269
+
270
+ ## How it fits together
271
+
272
+ `bin/commands.py` is the single source of truth. The wrappers, the completions,
273
+ the menu, the `doctor` report and the tables in this README are all generated
274
+ from it, and `bun run ci` fails if any of them drift.
275
+
276
+ ### Adding a command
277
+
278
+ 1. Write the script under `bin/<category>/` and `chmod +x` it.
279
+ 2. Add a `Command(...)` to `bin/commands.py`: name, summary, script, sound,
280
+ whether it can preview, which external tools it needs, its flags, and the
281
+ two or three questions the menu should ask.
282
+ 3. `bun run generate` to rewrite `shell/` and this README's tables, and commit
283
+ the result.
284
+ 4. `che install` on each machine, or `che update`, which does both.
285
+
286
+ There is no step where you hand-edit a wrapper into `~/.zshrc`, and no step
287
+ where you copy one machine's rc file to another.
288
+
289
+ ### Layout
290
+
291
+ ```
292
+ bin/
293
+ che.py the dispatcher and the interactive menu
294
+ commands.py the command manifest: the source of truth for everything
295
+ install.py installer, first-run wizard, doctor, self-update
296
+ shellgen.py renders the wrappers, completions and README tables
297
+ tui.py terminal primitives: raw input, frame buffer, prompts
298
+ utils.py shared library: logging, arg parsing, subprocess, files
299
+ drives/ mount, eject, and keep-alive for the NAS volumes
300
+ files/ delete/compress/inspect files by extension or size
301
+ git/ branch hygiene, worktrees, GitHub Actions status
302
+ system/ Homebrew updates, btop launcher
303
+ video/ codec inspection, renaming, dedupe, metadata
304
+ shell/ GENERATED wrappers and completions, do not edit
305
+ test/ pytest suites
306
+ install.sh bootstrap for a machine with nothing on it
307
+ .zshrc a shim that sources shell/che.zsh, kept for old setups
308
+ ```
309
+
310
+ ## CLI conventions
311
+
312
+ Every script follows the same shape, described in full at the top of
313
+ `bin/utils.py`:
314
+
315
+ - **Long flags only**, `--name=value` for values.
316
+ - **Booleans** accept a bare `--flag` *and* `--flag=true|false` (plus
317
+ `yes|no`, `1|0`, `on|off`). A bare `--flag` never swallows the next argument.
318
+ - **`--help`** on every script.
319
+ - **Destructive scripts default to dry-run.** `delete-by-ext`,
320
+ `delete-empty-folders`, `delete-smb-files`, `files-under-size`, `fix-codecs`,
321
+ `delete-duplicate-videos` and `prune-worktrees` read `DRY_RUN` and preview
322
+ unless told otherwise; the generated wrapper sets `DRY_RUN=false` for the
323
+ real thing and the `-dr` twin sets it back to `true`.
324
+ - **A symlinked `--path` is refused.** `find` never followed a symlinked start
325
+ point, so pointing a delete tool at `~/media -> /Volumes/...` used to be a
326
+ silent no-op. Pass the real path to act on it.
327
+
328
+ ## Syncing branches and worktrees
329
+
330
+ ```sh
331
+ sync-all-branches # push every worktree, collapse the pushed ones, then sync
332
+ sync-all-branches-dr # preview only
333
+ update-from-origin # same, but --no-push: only collapses already-pushed worktrees
334
+ ```
335
+
336
+ A worktree is a temporary workspace. Once its commits are on origin the
337
+ directory holds nothing the remote doesn't, so `sync-all-branches` pushes it,
338
+ removes the directory, and leaves the branch behind as an ordinary local
339
+ branch. That also clears the `already used by worktree` pin, which is what
340
+ otherwise makes `main` un-checkoutable and stale branches un-deletable.
341
+
342
+ A worktree is only collapsed when it is **clean** *and* **fully pushed**.
343
+ Anything else is kept and reported with the reason.
344
+
345
+ ## External tools
346
+
347
+ The scripts shell out to these where relevant and fail with an install hint
348
+ when one is missing. `che doctor` reports which are present and what needs
349
+ them.
350
+
351
+ <!-- che:tools -->
352
+ | Tool | Needed by |
353
+ | --- | --- |
354
+ | `brew` | `update-brew` |
355
+ | `btop` | `btop` |
356
+ | `diskutil` | `eject-all-drives` |
357
+ | `exiftool` | `remove-metadata` |
358
+ | `ffmpeg` | `fix-codecs` |
359
+ | `ffprobe` | `show-codecs`, `find-video-mkv-issues`, `scan-videos-audio-language` |
360
+ | `gh` | `all-actions`, `all-actions-watch` |
361
+ | `git` | `all-actions`, `all-actions-watch`, `checkout-my-branches`, `clean-stale-branches`, `prune-worktrees`, `sync-all-branches`, `update-from-origin`, `update-local-branches`, `update` |
362
+ | `mpv` | `validate-video-files` |
363
+ | `osascript` | `mount-all-drives` |
364
+ | (not a binary) | `detect-green-magenta-videos` needs OpenCV in ~/.venvs/green-magenta (override with $DETECT_GM_PYTHON) |
365
+ <!-- /che:tools -->
366
+
367
+ `diskutil` and `osascript` ship with macOS. The rest are one `brew install`
368
+ away, and `che install --install-deps` will do it for you.
369
+
370
+ `playsound-N`, the chime a wrapper plays when it finishes, is defined outside
371
+ this repo. When it is absent the wrappers skip it silently; `CHE_SOUNDS=0`
372
+ turns the chimes off everywhere.
373
+
374
+ `zip`, `bc`, `jq`, `perl`, `awk` and `sort` are no longer needed: `zipfile`,
375
+ integer maths, `json`, `re` and `sorted()` cover what they were doing.
376
+
377
+ ## Running tests
378
+
379
+ This repo uses [Bun](https://bun.sh) as its task runner and
380
+ [uv](https://docs.astral.sh/uv/) to supply the dev tooling.
381
+
382
+ ```sh
383
+ bun run test # pytest
384
+ bun run generate # rewrite shell/ and this README's tables from the manifest
385
+ bun run ci # ruff + syntax + exec bits + shebangs + generated files + pytest
386
+ ```
387
+
388
+ Neither `pytest` nor `ruff` needs to be installed globally: the scripts shell
389
+ out to `uv run --with-requirements requirements-dev.txt`, which builds an
390
+ ephemeral environment per invocation. Versions are pinned exactly in
391
+ `requirements-dev.txt`.
392
+
393
+ (Note: plain `bun test` invokes Bun's built-in JS/TS test runner, which doesn't
394
+ apply here, so always use `bun run test`.)
395
+
396
+ ## Troubleshooting
397
+
398
+ **A command still runs the old thing.** An alias beats a function in both zsh
399
+ and bash. The wrappers stash a colliding alias, define themselves, then put the
400
+ alias back, so your alias keeps winning at the prompt exactly as before; run
401
+ `\name` (backslash) to reach the wrapper, or remove the alias. `che doctor`
402
+ lists every collision it finds.
403
+
404
+ **`che: command not found` in a script or cron job.** Those never read an rc
405
+ file. Use `~/.local/bin/che`, which the installer puts on `PATH`.
406
+
407
+ **A wrapper is missing after an update.** `che update` regenerates and
408
+ reinstalls; `che doctor` says whether `shell/` is stale or the block is out of
409
+ date.
410
+
411
+ **Old hand-written wrappers are still in your rc file.** `che doctor` counts
412
+ them; `che install --replace-legacy` removes them, their helpers and the lines
413
+ that called them, after a backup.
414
+
415
+ ## History
416
+
417
+ These scripts were bash until August 2026. `git log` has the shell versions;
418
+ the migration commit explains each deliberate behavioural change, and anywhere
419
+ the Python intentionally differs from the bash it replaced, there is a comment
420
+ at that line saying so.
421
+
422
+ The wrappers were hand-maintained in this repo's `.zshrc` until `che` arrived,
423
+ with the documented workflow being to copy them into `~/.zshrc` by hand. That
424
+ is what `che install --replace-legacy` cleans up.