@codebend3r/che 1.2.1 → 1.2.3

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` | Update every branch and worktree, preserving locks and local work | `sync-all-branches-dr` |
260
+ | `sync-all-branches` | Update every branch and worktree, pruning what has been merged | `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,127 +320,61 @@ 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
288
-
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
323
+ ### Syncing branches
346
324
 
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 # update branches and worktrees, keeping locks and local work
367
- sync-all-branches-dr # preview cached remote refs; no fetch or changes
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
- Fetches once and rebases every local branch onto its upstream, falling back to
372
- `origin/<branch>` when no upstream is configured. Branches in existing
373
- worktrees, including locked ones, update **inside their own checkout**. Dirty
374
- tracked and untracked work is temporarily stashed and restored with its staged
375
- state; unrelated existing stashes survive. Nested registered worktrees are
376
- excluded from their parent's stash. Dirty submodules and existing Git operations
377
- are reported and left alone.
378
-
379
- It **never pushes, hard resets or deletes local branches**, and preserves every
380
- existing worktree directory, lock and checkout, including the caller's branch.
381
- Unoccupied branches use temporary worktrees that are removed after updating.
382
- New origin branches authored by you become local tracking branches without
383
- switching HEAD. Missing upstreams are reported, not treated as proof of a merge;
384
- use `clean-stale-branches` separately when you want branch cleanup.
385
-
386
- Rebase conflicts are aborted and reported while other branches continue. If
387
- restoring WIP conflicts, the exact recovery stash is retained and its ID and
388
- worktree path are printed. A blocked branch makes the command exit nonzero even
389
- when other branches updated. Local-only branches are reported without inventing
390
- a remote target. Ahead counts may remain because local commits are kept; clearing
391
- them would require pushing or discarding work. Avoid concurrent edits or Git
392
- operations while the sync temporarily saves WIP.
393
-
394
- The `-dr` preview does not fetch and only inspects cached remote refs. It changes
395
- no branches, working files, locks or stashes. Legacy `--no-push` and
396
- `--keep-locked` flags are still accepted for compatibility.
397
-
398
- Like the other branch scripts (`clean-stale-branches`, `update-local-branches`,
399
- `checkout-my-branches`, `prune-worktrees`), sync retains che's existing policy of
400
- pointing `core.hooksPath` at `/dev/null` for its Git commands so bookkeeping does
401
- not run repository hooks.
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
+ - **No local change is discarded.** It never pushes. A branch is only moved
341
+ or deleted when Git can prove its changes already exist elsewhere:
342
+ - A diverged branch whose changes are all in its upstream (a stale copy of
343
+ a branch rebased and force-pushed from another machine, typically after
344
+ its parent was squash-merged) moves to the upstream instead of replaying
345
+ commits that would conflict.
346
+ - A branch whose upstream is gone is deleted, with its clean worktree (a
347
+ lock is dropped first), once it is proven merged into `origin/main`,
348
+ `master`, `dev` or `develop`: contained in it, its content already there
349
+ (squash merges included), or a merged GitHub PR. The tip SHA is printed so
350
+ you can restore it. A missing upstream alone is not treated as merged: an
351
+ unproven one is listed as `Upstream gone` and kept. A merged branch whose
352
+ worktree is dirty, mid-operation or your current checkout is kept and
353
+ reported.
354
+ - **Failures are isolated.** A rebase conflict is aborted and reported while
355
+ other branches continue, and any blocked branch makes the command exit
356
+ non-zero. If restoring work conflicts, the recovery stash ID and worktree
357
+ path are printed.
358
+ - New remote branches you authored become local tracking branches.
359
+
360
+ Like every branch-touching script in `bin/git/`, it disables repository hooks
361
+ for its own Git commands. The legacy `--no-push` and `--keep-locked` flags are
362
+ still accepted and ignored.
363
+
364
+ ## Configuration
365
+
366
+ | Variable | Effect |
367
+ | --- | --- |
368
+ | `CHE_HOME` | Where the repository lives. Set by the rc block; the wrapper file falls back to its own location. |
369
+ | `CHE_PYTHON` | The interpreter the wrappers use. Set by the rc block to the one the installer verified. |
370
+ | `CHE_SOUNDS` | Set to `0` to turn off the `playsound-N` chime after each command. |
371
+ | `DRY_RUN` | Set explicitly by the wrappers on every call, so exporting it only affects scripts run directly. |
372
+ | `NO_COLOR` | Disables color in every script and in the menu. |
402
373
 
403
374
  ## External tools
404
375
 
405
- The scripts shell out to these where relevant and fail with an install hint
406
- when one is missing. `che doctor` reports which are present and what needs
407
- them.
376
+ Scripts that shell out to an external tool fail with an install hint when it
377
+ is missing, and `che doctor` reports which are present.
408
378
 
409
379
  <!-- che:tools -->
410
380
  | Tool | Needed by |
@@ -416,67 +386,119 @@ them.
416
386
  | `ffmpeg` | `fix-codecs` |
417
387
  | `ffprobe` | `show-codecs`, `find-video-mkv-issues`, `scan-videos-audio-language` |
418
388
  | `gh` | `all-actions`, `all-actions-watch` |
419
- | `git` | `all-actions`, `all-actions-watch`, `checkout-my-branches`, `clean-stale-branches`, `prune-worktrees`, `sync-all-branches`, `update-from-origin`, `update-local-branches`, `update` |
389
+ | `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` |
420
390
  | `mpv` | `validate-video-files` |
421
391
  | `osascript` | `mount-all-drives` |
422
392
  | (not a binary) | `detect-green-magenta-videos` needs OpenCV in ~/.venvs/green-magenta (override with $DETECT_GM_PYTHON) |
423
393
  <!-- /che:tools -->
424
394
 
425
- `diskutil` and `osascript` ship with macOS. The rest are one `brew install`
426
- away, and `che install --install-deps` will do it for you.
395
+ `diskutil` and `osascript` ship with macOS. The rest are a `brew install`
396
+ away, and `che install --install-deps` installs them for you.
427
397
 
428
- `playsound-N`, the chime a wrapper plays when it finishes, is defined outside
429
- this repo. When it is absent the wrappers skip it silently; `CHE_SOUNDS=0`
430
- turns the chimes off everywhere.
398
+ `playsound-N`, the chime played when a wrapper finishes, is defined outside
399
+ this repository; the wrappers skip it silently when it is absent.
431
400
 
432
- `zip`, `bc`, `jq`, `perl`, `awk` and `sort` are no longer needed: `zipfile`,
433
- integer maths, `json`, `re` and `sorted()` cover what they were doing.
401
+ ## Development
434
402
 
435
- ## Running tests
403
+ `bin/commands.py` is the single source of truth. The wrappers, completions,
404
+ menu, doctor report and this README's tables are all generated from it.
436
405
 
437
- This repo uses [Bun](https://bun.sh) as its task runner and
438
- [uv](https://docs.astral.sh/uv/) to supply the dev tooling.
406
+ ### Adding a command
439
407
 
440
- ```sh
441
- bun run test # pytest
442
- bun run generate # rewrite shell/ and this README's tables from the manifest
443
- bun run ci # ruff + exec bits + shebangs + pytest (stale generated files fail it)
408
+ 1. Write the script under `bin/<category>/` and `chmod +x` it.
409
+ 2. Add a `Command(...)` to `bin/commands.py`: name, summary, script, sound,
410
+ preview style, required tools, flags, and the two or three questions the
411
+ menu should ask.
412
+ 3. Run `bun run generate` to rewrite `shell/` and the README tables, and
413
+ commit the result.
414
+ 4. Run `che update` on each machine.
415
+
416
+ Conventions for new scripts are documented at the top of `bin/utils.py` and in
417
+ [`CLAUDE.md`](CLAUDE.md).
418
+
419
+ ### Project layout
420
+
421
+ ```
422
+ bin/
423
+ che.py dispatcher, and the loop that runs what the menu picks
424
+ commands.py command manifest: the source of truth for everything
425
+ menu.py the menu's model, keys and drawing
426
+ shellgen.py renders wrappers, completions and README tables
427
+ install.py installer, first-run wizard, self-update, uninstall
428
+ doctor.py `che doctor`
429
+ rcfile.py rc file edits: the che block and legacy wrappers
430
+ machine.py shells, rc paths, python discovery, install record
431
+ tui.py terminal primitives: raw input, frame buffer, prompts
432
+ art.py the wordmark and banner
433
+ utils.py shared library: logging, arg parsing, subprocess, files
434
+ gitutils.py git helpers: worktrees, main branch, hook skipping
435
+ branchsync.py per-branch sync used by sync-all-branches
436
+ media.py ffprobe helpers for the video scripts
437
+ nas.py NAS drive table and SMB mounting
438
+ git/ video/ files/ drives/ system/ the scripts, by category
439
+ shell/ GENERATED wrappers and completions (do not edit)
440
+ test/ pytest suites
441
+ install.sh bootstrap for a bare machine
444
442
  ```
445
443
 
446
- Neither `pytest` nor `ruff` needs to be installed globally: the scripts shell
447
- out to `uv run --with-requirements requirements-dev.txt`, which builds an
448
- ephemeral environment per invocation. Versions are pinned exactly in
444
+ ### Testing
445
+
446
+ The repository uses [Bun](https://bun.sh) as its task runner and
447
+ [uv](https://docs.astral.sh/uv/) for dev tooling, so neither `pytest` nor
448
+ `ruff` needs to be installed globally. Versions are pinned in
449
449
  `requirements-dev.txt`.
450
450
 
451
- (Note: plain `bun test` invokes Bun's built-in JS/TS test runner, which doesn't
452
- apply here, so always use `bun run test`.)
451
+ ```sh
452
+ bun run test # pytest
453
+ bun run generate # rewrite shell/ and the README tables from the manifest
454
+ bun run ci # ruff, exec bits, shebangs, pytest; fails on stale generated files
455
+ ```
456
+
457
+ > [!TIP]
458
+ > Use `bun run test`, not `bun test`, which runs Bun's built-in JavaScript
459
+ > test runner instead.
453
460
 
454
461
  ## Troubleshooting
455
462
 
456
- **A command still runs the old thing.** An alias beats a function in both zsh
457
- and bash. The wrappers stash a colliding alias, define themselves, then put the
458
- alias back, so your alias keeps winning at the prompt exactly as before; run
459
- `\name` (backslash) to reach the wrapper, or remove the alias. `che doctor`
460
- lists every collision it finds.
463
+ <details>
464
+ <summary><strong>A command still runs the old version</strong></summary>
461
465
 
462
- **`che: command not found` in a script or cron job.** Those never read an rc
463
- file. Use `~/.local/bin/che`, which the installer puts on `PATH`.
466
+ An alias beats a function in zsh and bash. The wrappers preserve any colliding
467
+ alias, so it keeps winning at the prompt. Run `\name` to reach the wrapper, or
468
+ remove the alias. `che doctor` lists every collision.
464
469
 
465
- **A wrapper is missing after an update.** `che update` regenerates and
466
- reinstalls; `che doctor` says whether `shell/` is stale or the block is out of
467
- date.
470
+ </details>
468
471
 
469
- **Old hand-written wrappers are still in your rc file.** `che doctor` counts
470
- them; `che install --replace-legacy` removes them, their helpers and the lines
471
- that called them, after a backup.
472
+ <details>
473
+ <summary><strong><code>che: command not found</code> in a script or cron job</strong></summary>
474
+
475
+ Non-interactive shells never read an rc file. Use `~/.local/bin/che`, which
476
+ the installer puts on `PATH`.
477
+
478
+ </details>
479
+
480
+ <details>
481
+ <summary><strong>A wrapper is missing after an update</strong></summary>
482
+
483
+ `che update` regenerates and reinstalls the wrappers. `che doctor` reports
484
+ whether `shell/` is stale or the rc block is out of date.
485
+
486
+ </details>
487
+
488
+ <details>
489
+ <summary><strong>Old hand-written wrappers are still in an rc file</strong></summary>
490
+
491
+ `che doctor` counts them, and `che install --replace-legacy` removes them,
492
+ along with their helpers and the lines that called them, after a backup.
493
+
494
+ </details>
472
495
 
473
496
  ## History
474
497
 
475
- These scripts were bash until August 2026. `git log` has the shell versions;
476
- the migration commit explains each deliberate behavioural change, and anywhere
477
- the Python intentionally differs from the bash it replaced, there is a comment
478
- at that line saying so.
498
+ These scripts were written in bash until August 2026, when they were ported to
499
+ Python. Every intentional behavioural difference from the bash originals is
500
+ commented at the line where it occurs, and `git log` holds the shell versions.
501
+
502
+ ## License
479
503
 
480
- The wrappers were hand-maintained in this repo's `.zshrc` until `che` arrived,
481
- with the documented workflow being to copy them into `~/.zshrc` by hand. That
482
- is what `che install --replace-legacy` cleans up.
504
+ [MIT](LICENSE) © CJ Rivas