@codebend3r/che 1.1.0 → 1.2.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 (53) hide show
  1. package/README.md +38 -24
  2. package/bin/art.py +22 -20
  3. package/bin/che.py +208 -587
  4. package/bin/commands.py +220 -231
  5. package/bin/doctor.py +220 -0
  6. package/bin/drives/eject-all-drives.py +21 -38
  7. package/bin/drives/mount-all-drives.py +18 -62
  8. package/bin/drives/ping-nas.py +77 -105
  9. package/bin/files/compress-folders.py +7 -11
  10. package/bin/files/delete-by-ext.py +10 -14
  11. package/bin/files/delete-empty-folders.py +33 -37
  12. package/bin/files/delete-smb-files.py +12 -21
  13. package/bin/files/files-under-size.py +5 -11
  14. package/bin/files/find-largest-files.py +21 -25
  15. package/bin/files/list-permission.py +40 -0
  16. package/bin/files/make-alpha-dir.py +2 -0
  17. package/bin/git/all-actions.py +350 -253
  18. package/bin/git/checkout-my-branches.py +42 -63
  19. package/bin/git/clean-stale-branches.py +23 -48
  20. package/bin/git/prune-worktrees.py +12 -51
  21. package/bin/git/sync-all-branches.py +54 -283
  22. package/bin/git/update-local-branches.py +67 -73
  23. package/bin/gitutils.py +263 -0
  24. package/bin/install.py +393 -1182
  25. package/bin/machine.py +345 -0
  26. package/bin/media.py +127 -0
  27. package/bin/menu.py +437 -0
  28. package/bin/nas.py +102 -0
  29. package/bin/rcfile.py +380 -0
  30. package/bin/shellgen.py +503 -431
  31. package/bin/system/btop-launch.py +17 -13
  32. package/bin/system/update-brew.py +57 -68
  33. package/bin/tui.py +156 -153
  34. package/bin/utils.py +165 -237
  35. package/bin/video/delete-duplicate-videos.py +64 -62
  36. package/bin/video/detect-green-magenta-videos.py +105 -95
  37. package/bin/video/find-movie-by-year.py +3 -0
  38. package/bin/video/find-video-mkv-issues.py +48 -65
  39. package/bin/video/fix-codecs.py +25 -30
  40. package/bin/video/largest-tv-shows.py +60 -57
  41. package/bin/video/remove-metadata.py +3 -6
  42. package/bin/video/rename-video-file.py +74 -84
  43. package/bin/video/scan-videos-audio-language.py +16 -33
  44. package/bin/video/show-codecs.py +20 -24
  45. package/bin/video/validate-video-files.py +5 -8
  46. package/bin/video/video-list.py +17 -20
  47. package/package.json +4 -10
  48. package/shell/che.bash +33 -33
  49. package/shell/che.fish +7 -7
  50. package/shell/che.zsh +33 -33
  51. package/shell/completions/_che +19 -19
  52. package/shell/completions/che.bash +15 -15
  53. package/shell/completions/che.fish +34 -50
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  **A collection of helper scripts shared amongst computers, called by name from
6
6
  your shell, and browsable through one menu.**
7
7
 
8
- [![CI](https://github.com/codebend3r/che/actions/workflows/ci.yml/badge.svg)](https://github.com/codebend3r/che/actions/workflows/ci.yml)
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)
@@ -79,8 +79,8 @@ git clone https://github.com/codebend3r/che.git ~/Developer/git/che
79
79
  ~/Developer/git/che/install.sh
80
80
  ```
81
81
 
82
- Or in one line, which clones for you (into `~/Developer/git/shell-functions`,
83
- or wherever `CHE_HOME` points):
82
+ Or in one line, which clones for you (into `~/Developer/git/che`, or wherever
83
+ `CHE_HOME` points):
84
84
 
85
85
  ```sh
86
86
  curl -fsSL https://raw.githubusercontent.com/codebend3r/che/main/install.sh | bash
@@ -103,7 +103,9 @@ which opens a first-run wizard: it lists the shells it found and the rc files
103
103
  it would touch, asks whether to add a `che` on `PATH` and whether to keep the
104
104
  completion chimes, offers to remove hand-written wrappers left over from before
105
105
  the installer existed, and reports any missing external tool (offering to
106
- `brew install` them when Homebrew is present).
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.
107
109
 
108
110
  <div align="center">
109
111
  <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">
@@ -118,7 +120,7 @@ Skip the wizard:
118
120
  ./install.sh --yes --shells=zsh,fish # or the ones you name
119
121
  ./install.sh --dry-run # report what would change, write nothing
120
122
  ./install.sh --yes --replace-legacy # also remove pre-che hand-written wrappers
121
- ./install.sh --print # print the rc block, install nothing
123
+ ./install.sh --print # print the exact rc block, install nothing
122
124
  ```
123
125
 
124
126
  Every rc file is copied to `~/.config/che/backups/` before it is edited.
@@ -139,9 +141,9 @@ dotfiles:
139
141
  ```zsh
140
142
  # >>> che shell functions >>>
141
143
  # Managed by `che install` (che 1.0.0). Edits here are overwritten.
142
- export CHE_HOME=/Users/you/Developer/git/shell-functions
144
+ export CHE_HOME=/Users/you/Developer/git/che
143
145
  export CHE_PYTHON=/opt/homebrew/bin/python3
144
- [ -r /Users/you/Developer/git/shell-functions/shell/che.zsh ] && . /Users/you/Developer/git/shell-functions/shell/che.zsh
146
+ [ -r /Users/you/Developer/git/che/shell/che.zsh ] && . /Users/you/Developer/git/che/shell/che.zsh
145
147
  case ":$PATH:" in *":/Users/you/.local/bin:"*) ;; *) PATH="/Users/you/.local/bin:$PATH" ;; esac
146
148
  # <<< che shell functions <<<
147
149
  ```
@@ -164,7 +166,7 @@ python3 exists. `brew install python@3.13` if none does.
164
166
 
165
167
  ```sh
166
168
  che uninstall # remove the block, the shim and the completions
167
- che uninstall --purge # also delete ~/.config/che
169
+ che uninstall --purge # also delete ~/.config/che and the menu's input history
168
170
  ```
169
171
 
170
172
  The repo itself is left alone; delete the directory to finish.
@@ -214,13 +216,13 @@ have used, and `che help <command>` prints that script's own `--help`.
214
216
  | Command | Does | Preview |
215
217
  | --- | --- | --- |
216
218
  | **Git** | *Branch hygiene, worktrees and GitHub Actions* | |
217
- | `all-actions` | GitHub Actions status for every open PR you authored | |
219
+ | `all-actions` | GitHub Actions status for every open PR and every default branch | |
218
220
  | `all-actions-watch` | Same as all-actions, refreshed on an interval | |
219
221
  | `checkout-my-branches` | Check out recent remote branches you authored that aren't local yet | |
220
222
  | `clean-stale-branches` | Delete local branches whose upstream is gone | `clean-stale-branches-dr` |
221
223
  | `prune-worktrees` | Remove every linked worktree, then prune stale admin records | `prune-worktrees-dr` |
222
- | `sync-all-branches` | Push every worktree, collapse the pushed ones, then tidy branches | `sync-all-branches-dr` |
223
- | `update-from-origin` | Same as sync-all-branches but never pushes: fetch, collapse, rebase | |
224
+ | `sync-all-branches` | Delete merged branches, rebase the rest, check out new ones; branches only | `sync-all-branches-dr` |
225
+ | `update-from-origin` | Alias of sync-all-branches | `--dry-run` |
224
226
  | `update-local-branches` | Rebase every local branch that has an upstream onto origin | `--dry-run` |
225
227
  | **Video** | *Codecs, renaming, dedupe and metadata* | |
226
228
  | `show-codecs` | Report media files outside the Direct Play codec/container set | |
@@ -316,9 +318,16 @@ where you copy one machine's rc file to another.
316
318
  ```
317
319
  bin/
318
320
  art.py the wordmark and the face beside it, for every screen
319
- che.py the dispatcher and the interactive menu
321
+ che.py the dispatcher, and the loop that runs what the menu picks
320
322
  commands.py the command manifest: the source of truth for everything
321
- install.py installer, first-run wizard, doctor, self-update
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
322
331
  shellgen.py renders the wrappers, completions and README tables
323
332
  tui.py terminal primitives: raw input, frame buffer, prompts
324
333
  utils.py shared library: logging, arg parsing, subprocess, files
@@ -351,22 +360,27 @@ Every script follows the same shape, described in full at the top of
351
360
  point, so pointing a delete tool at `~/media -> /Volumes/...` used to be a
352
361
  silent no-op. Pass the real path to act on it.
353
362
 
354
- ## Syncing branches and worktrees
363
+ ## Syncing branches
355
364
 
356
365
  ```sh
357
- sync-all-branches # push every worktree, collapse the pushed ones, then sync
366
+ sync-all-branches # delete merged branches, rebase the rest, check out new ones
358
367
  sync-all-branches-dr # preview only
359
- update-from-origin # same, but --no-push: only collapses already-pushed worktrees
368
+ update-from-origin # alias of sync-all-branches
360
369
  ```
361
370
 
362
- A worktree is a temporary workspace. Once its commits are on origin the
363
- directory holds nothing the remote doesn't, so `sync-all-branches` pushes it,
364
- removes the directory, and leaves the branch behind as an ordinary local
365
- branch. That also clears the `already used by worktree` pin, which is what
366
- otherwise makes `main` un-checkoutable and stale branches un-deletable.
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`.
367
375
 
368
- A worktree is only collapsed when it is **clean** *and* **fully pushed**.
369
- Anything else is kept and reported with the reason.
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.
370
384
 
371
385
  ## External tools
372
386
 
@@ -408,7 +422,7 @@ This repo uses [Bun](https://bun.sh) as its task runner and
408
422
  ```sh
409
423
  bun run test # pytest
410
424
  bun run generate # rewrite shell/ and this README's tables from the manifest
411
- bun run ci # ruff + syntax + exec bits + shebangs + generated files + pytest
425
+ bun run ci # ruff + exec bits + shebangs + pytest (stale generated files fail it)
412
426
  ```
413
427
 
414
428
  Neither `pytest` nor `ruff` needs to be installed globally: the scripts shell
package/bin/art.py CHANGED
@@ -56,28 +56,20 @@ INDENT = " "
56
56
  GAP = 1
57
57
 
58
58
 
59
- def logo_lines() -> list[str]:
60
- return LOGO.strip("\n").splitlines()
59
+ LOGO_LINES: tuple[str, ...] = tuple(LOGO.strip("\n").splitlines())
60
+ FACE_LINES: tuple[str, ...] = tuple(FACE.strip("\n").splitlines())
61
61
 
62
+ HEIGHT = len(FACE_LINES)
63
+ WIDTH = len(INDENT) + max(len(line) for line in FACE_LINES)
62
64
 
63
- def face_lines() -> list[str]:
64
- return FACE.strip("\n").splitlines()
65
65
 
66
+ def _paint(lines: Sequence[str], colors: tui.Palette, indent: str = "") -> list[str]:
67
+ return [f"{colors.bright_magenta}{indent}{line}{colors.reset}" for line in lines]
66
68
 
67
- HEIGHT = len(face_lines())
68
- WIDTH = len(INDENT) + max(len(line) for line in face_lines())
69
69
 
70
-
71
- def wordmark(colors: tui.Palette | None = None) -> list[str]:
70
+ def wordmark(colors: tui.Palette) -> list[str]:
72
71
  """The block letters, coloured the way every screen prints them."""
73
- colors = colors or tui.palette()
74
- return [f"{colors.bright_magenta}{line}{colors.reset}" for line in logo_lines()]
75
-
76
-
77
- def face(colors: tui.Palette | None = None) -> list[str]:
78
- """The face, indented to line up with the wordmark and in the same colour."""
79
- colors = colors or tui.palette()
80
- return [f"{colors.bright_magenta}{INDENT}{line}{colors.reset}" for line in face_lines()]
72
+ return _paint(LOGO_LINES, colors)
81
73
 
82
74
 
83
75
  def compose(left: Sequence[str], right: Sequence[str], *, gap: int = GAP) -> list[str]:
@@ -105,17 +97,27 @@ def fits(right: Sequence[str], columns: int) -> bool:
105
97
  return WIDTH + GAP + right_width <= columns
106
98
 
107
99
 
108
- def banner(
109
- right: Sequence[str], *, columns: int | None = None, colors: tui.Palette | None = None
110
- ) -> list[str]:
100
+ def banner(right: Sequence[str], *, colors: tui.Palette, columns: int | None = None) -> list[str]:
111
101
  """The face with ``right`` beside it, or ``right`` alone when it would not fit.
112
102
 
113
103
  ``right`` is normally the wordmark plus whatever a screen wants under it (a
114
104
  subtitle, the command count). It comes back untouched, indent and all, on a
115
105
  terminal too narrow for both, so every caller keeps working at 40 columns.
106
+ The face is indented to line up with the wordmark and painted in its colour.
116
107
  """
117
108
  if columns is None:
118
109
  columns = tui.size()[0]
119
110
  if not fits(right, columns):
120
111
  return list(right)
121
- return compose(face(colors), right)
112
+ return compose(_paint(FACE_LINES, colors, INDENT), right)
113
+
114
+
115
+ def print_logo(subtitle: str = "") -> None:
116
+ """The face beside the wordmark, with ``subtitle`` under the letters."""
117
+ colors = tui.palette()
118
+ right = wordmark(colors)
119
+ if subtitle:
120
+ right.append(f" {colors.grey}{subtitle}{colors.reset}")
121
+ for line in banner(right, colors=colors):
122
+ print(line)
123
+ print()