@codebend3r/che 1.2.0 → 1.2.2

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.
package/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  # `che`
4
4
 
5
- **A collection of helper scripts shared amongst computers, called by name from
6
- your shell, and browsable through one menu.**
5
+ **Personal helper scripts, shared across machines, called by name from your
6
+ shell and browsable through one menu.**
7
7
 
8
8
  [![CI](https://github.com/codebend3r/che/actions/workflows/sanity-check.yml/badge.svg)](https://github.com/codebend3r/che/actions/workflows/sanity-check.yml)
9
9
  [![npm](https://img.shields.io/npm/v/%40codebend3r%2Fche)](https://www.npmjs.com/package/@codebend3r/che)
10
10
  ![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)
11
11
  ![Dependencies: stdlib only](https://img.shields.io/badge/dependencies-stdlib%20only-brightgreen)
12
12
  ![Shells: zsh, bash, fish](https://img.shields.io/badge/shells-zsh%20%7C%20bash%20%7C%20fish-8A2BE2)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)
13
14
 
14
15
  </div>
15
16
 
@@ -40,72 +41,104 @@ your shell, and browsable through one menu.**
40
41
  <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">
41
42
  </div>
42
43
 
43
- The scripts are Python, standard library only. The names you type
44
- (`delete-by-ext`, `sync-all-branches`, `show-codecs`, …) are shell functions
45
- generated from one manifest, `bin/commands.py`, and installed into whichever
46
- shells you use.
44
+ <br>
47
45
 
48
- ```
49
- che browse everything: type to filter, Enter to run
50
- che <command> [args…] run one directly
51
- delete-by-ext --path=… or just call it by name
52
- ```
46
+ `che` is a set of standalone Python scripts for git housekeeping, media
47
+ libraries, file cleanup, NAS drives and system upkeep. Each one is installed
48
+ as a shell function in zsh, bash and fish, and all of them can be browsed,
49
+ filtered and run from a full-screen terminal menu.
53
50
 
54
51
  <!-- che:count -->
55
- 33 commands across 5 categories, plus 6 built into `che` itself
52
+ 34 commands across 5 categories, plus 6 built into `che` itself
56
53
  <!-- /che:count -->
57
54
 
58
- ## Highlights
55
+ ## Contents
56
+
57
+ - [Features](#features)
58
+ - [Quick start](#quick-start)
59
+ - [Installation](#installation)
60
+ - [Usage](#usage)
61
+ - [Commands](#commands)
62
+ - [Configuration](#configuration)
63
+ - [External tools](#external-tools)
64
+ - [Development](#development)
65
+ - [Troubleshooting](#troubleshooting)
66
+ - [License](#license)
67
+
68
+ ## Features
59
69
 
60
70
  - **One manifest, everything generated.** Wrappers, completions, the menu, the
61
- doctor report and the tables in this README all come from `bin/commands.py`;
62
- `bun run ci` fails if any of them drift.
63
- - **Destructive commands preview by default.** Every deleter has a `-dr` twin
64
- that only prints, and the menu opens on the dry-run.
65
- - **Standard library only.** A bare `python3` 3.12+ runs everything; no venv,
66
- no `pip install`, on any machine the repo is cloned to.
71
+ doctor report and the tables in this README all come from
72
+ `bin/commands.py`. CI fails if any of them drift.
73
+ - **Safe by default.** Destructive commands preview unless told otherwise, and
74
+ each has a `-dr` twin that only prints. The menu opens on the dry run.
75
+ - **Standard library only.** Any Python 3.12+ runs everything. No virtualenv,
76
+ no `pip install`.
67
77
  - **Your dotfiles stay yours.** The installer writes a few lines between
68
- markers, backs every rc file up first, and never touches anything outside
69
- the markers.
70
- - **`che doctor` knows the whole story.** Interpreter, generated files, rc
71
- blocks, external tools, alias collisions — one command, non-zero on failure.
78
+ markers, backs up every rc file first, and never touches anything outside
79
+ them.
80
+ - **Self-diagnosing.** `che doctor` checks the interpreter, generated files, rc
81
+ blocks, external tools and alias collisions, and exits non-zero on failure.
72
82
 
73
- ## Install
83
+ ## Quick start
74
84
 
75
- From a clone:
85
+ ```sh
86
+ curl -fsSL https://raw.githubusercontent.com/codebend3r/che/main/install.sh | bash
87
+ exec $SHELL
88
+ ```
89
+
90
+ ```sh
91
+ che # browse everything: type to filter, Enter to run
92
+ che <command> [args…] # run one command directly
93
+ delete-by-ext --path=… # or call any command by name
94
+ ```
95
+
96
+ ## Installation
97
+
98
+ ### Requirements
99
+
100
+ - **Python 3.12 or newer.** The installer picks the newest suitable interpreter
101
+ it can find and records its path, so an old system `python3` is fine as long
102
+ as a newer one exists (`brew install python@3.13` if not).
103
+ - **zsh, bash or fish.**
104
+ - Some commands need external tools such as `ffmpeg` or `gh`; see
105
+ [External tools](#external-tools).
106
+
107
+ ### Install methods
108
+
109
+ **From a clone**
76
110
 
77
111
  ```sh
78
112
  git clone https://github.com/codebend3r/che.git ~/Developer/git/che
79
113
  ~/Developer/git/che/install.sh
80
114
  ```
81
115
 
82
- Or in one line, which clones for you (into `~/Developer/git/che`, or wherever
83
- `CHE_HOME` points):
116
+ **One-liner** (clones into `~/Developer/git/che`, or `$CHE_HOME` if set)
84
117
 
85
118
  ```sh
86
119
  curl -fsSL https://raw.githubusercontent.com/codebend3r/che/main/install.sh | bash
87
120
  ```
88
121
 
89
- Or from npm, if Node is around:
122
+ **From npm**
90
123
 
91
124
  ```sh
92
125
  npm install -g @codebend3r/che
93
- che # offers to install the wrappers on first run
126
+ che # offers to install the shell wrappers on first run
94
127
  ```
95
128
 
96
- The wrappers point back at wherever the package landed, so install it for real
97
- rather than through `npx`, whose cache is temporary. Update that kind of install
98
- with `npm install -g @codebend3r/che@latest`; `che update` only pulls a git
99
- clone, and says so.
129
+ > [!NOTE]
130
+ > Install the npm package globally rather than running it through `npx`: the
131
+ > wrappers point back at the package's location, and the `npx` cache is
132
+ > temporary. Update with `npm install -g @codebend3r/che@latest`; `che update`
133
+ > only works in a git clone.
134
+
135
+ ### First run
100
136
 
101
- `install.sh` finds a suitable python3, then hands over to `bin/install.py`,
102
- which opens a first-run wizard: it lists the shells it found and the rc files
103
- it would touch, asks whether to add a `che` on `PATH` and whether to keep the
104
- completion chimes, offers to remove hand-written wrappers left over from before
105
- the installer existed, and reports any missing external tool (offering to
106
- `brew install` them when Homebrew is present). A question already answered on
107
- the command line (`--no-sounds`, `--no-path-shim`, `--replace-legacy`,
108
- `--install-deps`) is not asked again.
137
+ `install.sh` finds a suitable Python and hands over to a short wizard. It
138
+ lists the shells it found and the rc files it would touch, asks whether to put
139
+ `che` on `PATH` and whether to keep the completion chimes, offers to remove
140
+ hand-written wrappers from before the installer existed, and reports missing
141
+ external tools (offering to `brew install` them when Homebrew is present).
109
142
 
110
143
  <div align="center">
111
144
  <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">
@@ -113,29 +146,31 @@ the command line (`--no-sounds`, `--no-path-shim`, `--replace-legacy`,
113
146
  <em>Running <code>che</code> before installing offers to set itself up.</em>
114
147
  </div>
115
148
 
116
- Skip the wizard:
149
+ ### Unattended install
150
+
151
+ Any question answered on the command line is not asked again
152
+ (`--no-sounds`, `--no-path-shim`, `--replace-legacy`, `--install-deps`).
117
153
 
118
154
  ```sh
119
- ./install.sh --yes # into the shells it detects
120
- ./install.sh --yes --shells=zsh,fish # or the ones you name
121
- ./install.sh --dry-run # report what would change, write nothing
122
- ./install.sh --yes --replace-legacy # also remove pre-che hand-written wrappers
123
- ./install.sh --print # print the exact rc block, install nothing
155
+ ./install.sh --yes # install into every detected shell
156
+ ./install.sh --yes --shells=zsh,fish # or only the shells you name
157
+ ./install.sh --dry-run # report what would change, write nothing
158
+ ./install.sh --yes --replace-legacy # also remove pre-che hand-written wrappers
159
+ ./install.sh --print # print the rc block and exit
124
160
  ```
125
161
 
126
- Every rc file is copied to `~/.config/che/backups/` before it is edited.
127
-
128
- ### What lands where
162
+ ### What gets installed
129
163
 
130
- | Thing | Where |
164
+ | What | Where |
131
165
  | --- | --- |
132
- | 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` |
133
- | The wrappers themselves | `shell/che.zsh`, `shell/che.bash`, `shell/che.fish` in this repo |
134
- | Tab completions | `shell/completions/`, sourced by the same wrapper file |
135
- | A `che` executable, for cron and scripts that never read an rc file | `~/.local/bin/che` |
136
- | What you answered | `~/.config/che/config.json` |
137
-
138
- The block itself is small on purpose, so upgrading che never rewrites your
166
+ | The rc block | `~/.zshrc` (or `$ZDOTDIR/.zshrc`), `~/.bashrc` and `~/.bash_profile` (whichever exist), `~/.config/fish/config.fish`, and `~/.profile` only when `sh` is requested |
167
+ | Shell wrappers | `shell/che.zsh`, `shell/che.bash`, `shell/che.fish` in the repo |
168
+ | Tab completions | `shell/completions/`, sourced by the wrapper file |
169
+ | A `che` executable for cron and scripts | `~/.local/bin/che` |
170
+ | Your installer answers | `~/.config/che/config.json` |
171
+ | rc file backups | `~/.config/che/backups/` |
172
+
173
+ The rc block is deliberately small, so upgrading `che` never rewrites your
139
174
  dotfiles:
140
175
 
141
176
  ```zsh
@@ -148,37 +183,28 @@ case ":$PATH:" in *":/Users/you/.local/bin:"*) ;; *) PATH="/Users/you/.local/bin
148
183
  # <<< che shell functions <<<
149
184
  ```
150
185
 
151
- Everything between those markers belongs to `che install` and is rewritten on
152
- every install. Everything outside them is yours and is never touched.
153
-
154
- Then reload:
155
-
156
- ```sh
157
- exec zsh # or exec bash / exec fish
158
- ```
159
-
160
- **Requires Python 3.12+** and nothing else to run the scripts. The installer
161
- picks the newest suitable interpreter it can find and records its path in the
162
- block, so an old `/usr/bin/python3` does not matter as long as some newer
163
- python3 exists. `brew install python@3.13` if none does.
186
+ Everything between the markers is rewritten on every install; everything
187
+ outside them is left alone. Reload your shell afterwards with `exec zsh`
188
+ (or `exec bash`, `exec fish`).
164
189
 
165
- ### Uninstall
190
+ ### Updating and uninstalling
166
191
 
167
192
  ```sh
168
- che uninstall # remove the block, the shim and the completions
193
+ che update # pull the latest scripts and refresh the wrappers
194
+ che uninstall # remove the rc block, the shim and the completions
169
195
  che uninstall --purge # also delete ~/.config/che and the menu's input history
170
196
  ```
171
197
 
172
- The repo itself is left alone; delete the directory to finish.
198
+ Uninstalling leaves the repository in place; delete the directory to finish.
173
199
 
174
- ## Using it
200
+ ## Usage
175
201
 
176
- `che` on its own opens the menu. Anything that can preview **previews by
177
- default**, so pressing Enter on `delete-by-ext` shows you what it would delete
178
- rather than deleting it.
202
+ ### The menu
179
203
 
180
- Type to filter — the list narrows as you go, and the right pane always shows
181
- the selected command's summary, script, required tools and flags:
204
+ Run `che` with no arguments to open the menu. Type to filter; the right pane
205
+ shows the selected command's summary, script, required tools and flags.
206
+ Anything that can preview **previews by default**, so pressing Enter on
207
+ `delete-by-ext` shows what it would delete rather than deleting it.
182
208
 
183
209
  <div align="center">
184
210
  <img src="search.png" alt="The menu filtered by the search term branch, showing the five matching commands" width="850">
@@ -198,19 +224,28 @@ the selected command's summary, script, required tools and flags:
198
224
  | `q` | quit |
199
225
  <!-- /che:keys -->
200
226
 
201
- Every command is also a plain shell function, so `type delete-by-ext` shows
202
- exactly what it runs, and the destructive ones have a `-dr` twin that only
203
- prints:
227
+ ### From the shell
228
+
229
+ Every command is a plain shell function, so `type delete-by-ext` shows exactly
230
+ what it runs. Destructive commands have a `-dr` twin that only prints:
204
231
 
205
232
  ```sh
206
233
  delete-by-ext --path=/Volumes/Media --ext=nfo,txt # deletes
207
234
  delete-by-ext-dr --path=/Volumes/Media --ext=nfo # shows what it would delete
208
235
  ```
209
236
 
210
- `che <command>` runs the same thing with the same environment the wrapper would
211
- have used, and `che help <command>` prints that script's own `--help`.
237
+ `che <command>` runs the same script with the same environment its wrapper
238
+ uses, and `che help <command>` prints that script's `--help`.
239
+
240
+ All scripts share the same CLI conventions:
241
+
242
+ - **Long flags only**, with `=` for values: `--path=/foo`, `--ext=nfo,txt`.
243
+ - **Booleans** accept a bare `--flag` or `--flag=true|false` (also
244
+ `yes|no`, `1|0`, `on|off`). A bare flag never swallows the next argument.
245
+ - **`--help`** on every script.
246
+ - **A symlinked `--path` is refused.** Pass the real path to act on it.
212
247
 
213
- ### Commands
248
+ ## Commands
214
249
 
215
250
  <!-- che:commands -->
216
251
  | Command | Does | Preview |
@@ -220,8 +255,9 @@ have used, and `che help <command>` prints that script's own `--help`.
220
255
  | `all-actions-watch` | Same as all-actions, refreshed on an interval | |
221
256
  | `checkout-my-branches` | Check out recent remote branches you authored that aren't local yet | |
222
257
  | `clean-stale-branches` | Delete local branches whose upstream is gone | `clean-stale-branches-dr` |
258
+ | `hard-reset-all-branches` | Check out every local branch and hard reset it to origin/<branch> | `hard-reset-all-branches-dr` |
223
259
  | `prune-worktrees` | Remove every linked worktree, then prune stale admin records | `prune-worktrees-dr` |
224
- | `sync-all-branches` | Delete merged branches, rebase the rest, check out new ones; branches only | `sync-all-branches-dr` |
260
+ | `sync-all-branches` | Update every branch and worktree, preserving locks and local work | `sync-all-branches-dr` |
225
261
  | `update-from-origin` | Alias of sync-all-branches | `--dry-run` |
226
262
  | `update-local-branches` | Rebase every local branch that has an upstream onto origin | `--dry-run` |
227
263
  | **Video** | *Codecs, renaming, dedupe and metadata* | |
@@ -255,12 +291,11 @@ have used, and `che help <command>` prints that script's own `--help`.
255
291
  | `btop` | Launch btop with a gruvbox theme matching the macOS appearance | |
256
292
  <!-- /che:commands -->
257
293
 
258
- Two commands have no `-dr` twin. `remove-metadata` cannot preview at all, since
259
- exiftool rewrites in place. `rename-video-file` does not need one: it already
260
- defaults to `--dry-run=true`, so the real rename is the one that needs
261
- `--dry-run=false`.
294
+ Two commands have no `-dr` twin. `remove-metadata` cannot preview, since
295
+ exiftool rewrites files in place. `rename-video-file` already defaults to
296
+ `--dry-run=true`, so the real rename is the one that needs `--dry-run=false`.
262
297
 
263
- ### Managing the install
298
+ ### Built-in commands
264
299
 
265
300
  <!-- che:builtins -->
266
301
  | Command | Does |
@@ -273,10 +308,11 @@ defaults to `--dry-run=true`, so the real rename is the one that needs
273
308
  | `che completions` | Print the completion script for a shell |
274
309
  <!-- /che:builtins -->
275
310
 
276
- `che doctor` checks the interpreter, the repo, whether the generated files are
277
- current, whether each rc file has the block, whether `che` is on `PATH`, every
278
- external tool, and anything shadowing a wrapper. It exits non-zero on a real
279
- failure, so it works in a health check. `che doctor --json` for a machine.
311
+ `che doctor` checks the interpreter, the repository, whether generated files
312
+ are current, whether each rc file has the block, whether `che` is on `PATH`,
313
+ every external tool, and anything shadowing a wrapper. It exits non-zero on a
314
+ real failure, so it can serve as a health check; `che doctor --json` gives
315
+ machine-readable output.
280
316
 
281
317
  <div align="center">
282
318
  <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">
@@ -284,109 +320,50 @@ failure, so it works in a health check. `che doctor --json` for a machine.
284
320
  <em>Every problem <code>che doctor</code> reports comes with the command that fixes it.</em>
285
321
  </div>
286
322
 
287
- ### Environment
323
+ ### Syncing branches
288
324
 
289
- | Variable | Effect |
290
- | --- | --- |
291
- | `CHE_HOME` | Where this repo lives. Set by the block; the wrapper file falls back to its own location. |
292
- | `CHE_PYTHON` | Interpreter the wrappers use. Set by the block to the one the installer verified. |
293
- | `CHE_SOUNDS` | `0` turns off the `playsound-N` chime after every command. |
294
- | `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. |
295
- | `NO_COLOR` | Honoured by every script and by the menu. |
296
-
297
- ## How it fits together
298
-
299
- `bin/commands.py` is the single source of truth. The wrappers, the completions,
300
- the menu, the `doctor` report and the tables in this README are all generated
301
- from it, and `bun run ci` fails if any of them drift.
302
-
303
- ### Adding a command
304
-
305
- 1. Write the script under `bin/<category>/` and `chmod +x` it.
306
- 2. Add a `Command(...)` to `bin/commands.py`: name, summary, script, sound,
307
- whether it can preview, which external tools it needs, its flags, and the
308
- two or three questions the menu should ask.
309
- 3. `bun run generate` to rewrite `shell/` and this README's tables, and commit
310
- the result.
311
- 4. `che install` on each machine, or `che update`, which does both.
312
-
313
- There is no step where you hand-edit a wrapper into `~/.zshrc`, and no step
314
- where you copy one machine's rc file to another.
315
-
316
- ### Layout
317
-
318
- ```
319
- bin/
320
- art.py the wordmark and the face beside it, for every screen
321
- che.py the dispatcher, and the loop that runs what the menu picks
322
- commands.py the command manifest: the source of truth for everything
323
- doctor.py `che doctor`: every check on the install
324
- gitutils.py shared git helpers: worktrees, main branch, hook skipping
325
- install.py installer, first-run wizard, self-update, uninstall
326
- machine.py shells, rc file paths, python, config: facts about the machine
327
- media.py shared ffprobe helpers for the video scripts
328
- menu.py the menu's list, keys and drawing
329
- nas.py the NAS drive table and SMB mounting, for the drive scripts
330
- rcfile.py edits to rc files: the che block and old hand-written wrappers
331
- shellgen.py renders the wrappers, completions and README tables
332
- tui.py terminal primitives: raw input, frame buffer, prompts
333
- utils.py shared library: logging, arg parsing, subprocess, files
334
- drives/ mount, eject, and keep-alive for the NAS volumes
335
- files/ delete/compress/inspect files by extension or size
336
- git/ branch hygiene, worktrees, GitHub Actions status
337
- system/ Homebrew updates, btop launcher
338
- video/ codec inspection, renaming, dedupe, metadata
339
- shell/ GENERATED wrappers and completions, do not edit
340
- test/ pytest suites
341
- install.sh bootstrap for a machine with nothing on it
342
- .zshrc a shim that sources shell/che.zsh, kept for old setups
343
- ```
344
-
345
- ## CLI conventions
346
-
347
- Every script follows the same shape, described in full at the top of
348
- `bin/utils.py`:
349
-
350
- - **Long flags only**, `--name=value` for values.
351
- - **Booleans** accept a bare `--flag` *and* `--flag=true|false` (plus
352
- `yes|no`, `1|0`, `on|off`). A bare `--flag` never swallows the next argument.
353
- - **`--help`** on every script.
354
- - **Destructive scripts default to dry-run.** `delete-by-ext`,
355
- `delete-empty-folders`, `delete-smb-files`, `files-under-size`, `fix-codecs`,
356
- `delete-duplicate-videos` and `prune-worktrees` read `DRY_RUN` and preview
357
- unless told otherwise; the generated wrapper sets `DRY_RUN=false` for the
358
- real thing and the `-dr` twin sets it back to `true`.
359
- - **A symlinked `--path` is refused.** `find` never followed a symlinked start
360
- point, so pointing a delete tool at `~/media -> /Volumes/...` used to be a
361
- silent no-op. Pass the real path to act on it.
362
-
363
- ## Syncing branches
325
+ `sync-all-branches` (alias `update-from-origin`) fetches once and rebases
326
+ every local branch onto its upstream, falling back to `origin/<branch>`.
364
327
 
365
328
  ```sh
366
- sync-all-branches # delete merged branches, rebase the rest, check out new ones
367
- sync-all-branches-dr # preview only
368
- update-from-origin # alias of sync-all-branches
329
+ sync-all-branches # update every branch and worktree
330
+ sync-all-branches-dr # preview against cached remote refs; no fetch, no changes
369
331
  ```
370
332
 
371
- Only branches change. The sync deletes local branches whose upstream is gone,
372
- `pull --rebase`s every branch with an upstream, and creates local tracking
373
- branches for the remote branches you authored that aren't local yet, without
374
- switching to them. It ends with the main clone on `main`.
333
+ - **Worktrees update in place.** Branches checked out in other worktrees,
334
+ including locked ones, rebase inside their own checkout. Unoccupied branches
335
+ use temporary worktrees, so your current checkout never switches.
336
+ - **Local work is preserved.** Uncommitted tracked and untracked changes are
337
+ stashed and restored with their staged state; existing stashes are
338
+ untouched. Dirty submodules and in-progress Git operations are reported and
339
+ left alone.
340
+ - **Nothing is destroyed.** It never pushes, hard resets or deletes branches.
341
+ Missing upstreams are reported, not treated as merged; use
342
+ `clean-stale-branches` for cleanup.
343
+ - **Failures are isolated.** A rebase conflict is aborted and reported while
344
+ other branches continue, and any blocked branch makes the command exit
345
+ non-zero. If restoring work conflicts, the recovery stash ID and worktree
346
+ path are printed.
347
+ - New remote branches you authored become local tracking branches.
348
+
349
+ Like every branch-touching script in `bin/git/`, it disables repository hooks
350
+ for its own Git commands. The legacy `--no-push` and `--keep-locked` flags are
351
+ still accepted and ignored.
352
+
353
+ ## Configuration
375
354
 
376
- It **never pushes**, **never touches a worktree** and **never runs the repo's
377
- git hooks**. A branch checked out in a linked worktree is rebased in place
378
- there when that worktree is clean; a stale one is reported and kept, since git
379
- won't delete a branch a worktree has checked out. The branch scripts
380
- (`clean-stale-branches`, `update-local-branches`, `checkout-my-branches`,
381
- `prune-worktrees`, `sync-all-branches`) point `core.hooksPath` at `/dev/null`
382
- for every git they run, so a lefthook or husky pre-commit, pre-push or
383
- post-checkout never fires on a checkout or a rebase.
355
+ | Variable | Effect |
356
+ | --- | --- |
357
+ | `CHE_HOME` | Where the repository lives. Set by the rc block; the wrapper file falls back to its own location. |
358
+ | `CHE_PYTHON` | The interpreter the wrappers use. Set by the rc block to the one the installer verified. |
359
+ | `CHE_SOUNDS` | Set to `0` to turn off the `playsound-N` chime after each command. |
360
+ | `DRY_RUN` | Set explicitly by the wrappers on every call, so exporting it only affects scripts run directly. |
361
+ | `NO_COLOR` | Disables color in every script and in the menu. |
384
362
 
385
363
  ## External tools
386
364
 
387
- The scripts shell out to these where relevant and fail with an install hint
388
- when one is missing. `che doctor` reports which are present and what needs
389
- them.
365
+ Scripts that shell out to an external tool fail with an install hint when it
366
+ is missing, and `che doctor` reports which are present.
390
367
 
391
368
  <!-- che:tools -->
392
369
  | Tool | Needed by |
@@ -398,67 +375,119 @@ them.
398
375
  | `ffmpeg` | `fix-codecs` |
399
376
  | `ffprobe` | `show-codecs`, `find-video-mkv-issues`, `scan-videos-audio-language` |
400
377
  | `gh` | `all-actions`, `all-actions-watch` |
401
- | `git` | `all-actions`, `all-actions-watch`, `checkout-my-branches`, `clean-stale-branches`, `prune-worktrees`, `sync-all-branches`, `update-from-origin`, `update-local-branches`, `update` |
378
+ | `git` | `all-actions`, `all-actions-watch`, `checkout-my-branches`, `clean-stale-branches`, `hard-reset-all-branches`, `prune-worktrees`, `sync-all-branches`, `update-from-origin`, `update-local-branches`, `update` |
402
379
  | `mpv` | `validate-video-files` |
403
380
  | `osascript` | `mount-all-drives` |
404
381
  | (not a binary) | `detect-green-magenta-videos` needs OpenCV in ~/.venvs/green-magenta (override with $DETECT_GM_PYTHON) |
405
382
  <!-- /che:tools -->
406
383
 
407
- `diskutil` and `osascript` ship with macOS. The rest are one `brew install`
408
- away, and `che install --install-deps` will do it for you.
384
+ `diskutil` and `osascript` ship with macOS. The rest are a `brew install`
385
+ away, and `che install --install-deps` installs them for you.
409
386
 
410
- `playsound-N`, the chime a wrapper plays when it finishes, is defined outside
411
- this repo. When it is absent the wrappers skip it silently; `CHE_SOUNDS=0`
412
- turns the chimes off everywhere.
387
+ `playsound-N`, the chime played when a wrapper finishes, is defined outside
388
+ this repository; the wrappers skip it silently when it is absent.
413
389
 
414
- `zip`, `bc`, `jq`, `perl`, `awk` and `sort` are no longer needed: `zipfile`,
415
- integer maths, `json`, `re` and `sorted()` cover what they were doing.
390
+ ## Development
416
391
 
417
- ## Running tests
392
+ `bin/commands.py` is the single source of truth. The wrappers, completions,
393
+ menu, doctor report and this README's tables are all generated from it.
418
394
 
419
- This repo uses [Bun](https://bun.sh) as its task runner and
420
- [uv](https://docs.astral.sh/uv/) to supply the dev tooling.
395
+ ### Adding a command
396
+
397
+ 1. Write the script under `bin/<category>/` and `chmod +x` it.
398
+ 2. Add a `Command(...)` to `bin/commands.py`: name, summary, script, sound,
399
+ preview style, required tools, flags, and the two or three questions the
400
+ menu should ask.
401
+ 3. Run `bun run generate` to rewrite `shell/` and the README tables, and
402
+ commit the result.
403
+ 4. Run `che update` on each machine.
404
+
405
+ Conventions for new scripts are documented at the top of `bin/utils.py` and in
406
+ [`CLAUDE.md`](CLAUDE.md).
407
+
408
+ ### Project layout
421
409
 
422
- ```sh
423
- bun run test # pytest
424
- bun run generate # rewrite shell/ and this README's tables from the manifest
425
- bun run ci # ruff + exec bits + shebangs + pytest (stale generated files fail it)
426
410
  ```
411
+ bin/
412
+ che.py dispatcher, and the loop that runs what the menu picks
413
+ commands.py command manifest: the source of truth for everything
414
+ menu.py the menu's model, keys and drawing
415
+ shellgen.py renders wrappers, completions and README tables
416
+ install.py installer, first-run wizard, self-update, uninstall
417
+ doctor.py `che doctor`
418
+ rcfile.py rc file edits: the che block and legacy wrappers
419
+ machine.py shells, rc paths, python discovery, install record
420
+ tui.py terminal primitives: raw input, frame buffer, prompts
421
+ art.py the wordmark and banner
422
+ utils.py shared library: logging, arg parsing, subprocess, files
423
+ gitutils.py git helpers: worktrees, main branch, hook skipping
424
+ branchsync.py per-branch sync used by sync-all-branches
425
+ media.py ffprobe helpers for the video scripts
426
+ nas.py NAS drive table and SMB mounting
427
+ git/ video/ files/ drives/ system/ the scripts, by category
428
+ shell/ GENERATED wrappers and completions (do not edit)
429
+ test/ pytest suites
430
+ install.sh bootstrap for a bare machine
431
+ ```
432
+
433
+ ### Testing
427
434
 
428
- Neither `pytest` nor `ruff` needs to be installed globally: the scripts shell
429
- out to `uv run --with-requirements requirements-dev.txt`, which builds an
430
- ephemeral environment per invocation. Versions are pinned exactly in
435
+ The repository uses [Bun](https://bun.sh) as its task runner and
436
+ [uv](https://docs.astral.sh/uv/) for dev tooling, so neither `pytest` nor
437
+ `ruff` needs to be installed globally. Versions are pinned in
431
438
  `requirements-dev.txt`.
432
439
 
433
- (Note: plain `bun test` invokes Bun's built-in JS/TS test runner, which doesn't
434
- apply here, so always use `bun run test`.)
440
+ ```sh
441
+ bun run test # pytest
442
+ bun run generate # rewrite shell/ and the README tables from the manifest
443
+ bun run ci # ruff, exec bits, shebangs, pytest; fails on stale generated files
444
+ ```
445
+
446
+ > [!TIP]
447
+ > Use `bun run test`, not `bun test`, which runs Bun's built-in JavaScript
448
+ > test runner instead.
435
449
 
436
450
  ## Troubleshooting
437
451
 
438
- **A command still runs the old thing.** An alias beats a function in both zsh
439
- and bash. The wrappers stash a colliding alias, define themselves, then put the
440
- alias back, so your alias keeps winning at the prompt exactly as before; run
441
- `\name` (backslash) to reach the wrapper, or remove the alias. `che doctor`
442
- lists every collision it finds.
452
+ <details>
453
+ <summary><strong>A command still runs the old version</strong></summary>
443
454
 
444
- **`che: command not found` in a script or cron job.** Those never read an rc
445
- file. Use `~/.local/bin/che`, which the installer puts on `PATH`.
455
+ An alias beats a function in zsh and bash. The wrappers preserve any colliding
456
+ alias, so it keeps winning at the prompt. Run `\name` to reach the wrapper, or
457
+ remove the alias. `che doctor` lists every collision.
446
458
 
447
- **A wrapper is missing after an update.** `che update` regenerates and
448
- reinstalls; `che doctor` says whether `shell/` is stale or the block is out of
449
- date.
459
+ </details>
450
460
 
451
- **Old hand-written wrappers are still in your rc file.** `che doctor` counts
452
- them; `che install --replace-legacy` removes them, their helpers and the lines
453
- that called them, after a backup.
461
+ <details>
462
+ <summary><strong><code>che: command not found</code> in a script or cron job</strong></summary>
463
+
464
+ Non-interactive shells never read an rc file. Use `~/.local/bin/che`, which
465
+ the installer puts on `PATH`.
466
+
467
+ </details>
468
+
469
+ <details>
470
+ <summary><strong>A wrapper is missing after an update</strong></summary>
471
+
472
+ `che update` regenerates and reinstalls the wrappers. `che doctor` reports
473
+ whether `shell/` is stale or the rc block is out of date.
474
+
475
+ </details>
476
+
477
+ <details>
478
+ <summary><strong>Old hand-written wrappers are still in an rc file</strong></summary>
479
+
480
+ `che doctor` counts them, and `che install --replace-legacy` removes them,
481
+ along with their helpers and the lines that called them, after a backup.
482
+
483
+ </details>
454
484
 
455
485
  ## History
456
486
 
457
- These scripts were bash until August 2026. `git log` has the shell versions;
458
- the migration commit explains each deliberate behavioural change, and anywhere
459
- the Python intentionally differs from the bash it replaced, there is a comment
460
- at that line saying so.
487
+ These scripts were written in bash until August 2026, when they were ported to
488
+ Python. Every intentional behavioural difference from the bash originals is
489
+ commented at the line where it occurs, and `git log` holds the shell versions.
490
+
491
+ ## License
461
492
 
462
- The wrappers were hand-maintained in this repo's `.zshrc` until `che` arrived,
463
- with the documented workflow being to copy them into `~/.zshrc` by hand. That
464
- is what `che install --replace-legacy` cleans up.
493
+ [MIT](LICENSE) © CJ Rivas