@codebend3r/che 1.0.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 +65 -25
  2. package/bin/art.py +123 -0
  3. package/bin/che.py +209 -568
  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 -1188
  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 +508 -428
  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/bin/doctor.py ADDED
@@ -0,0 +1,220 @@
1
+ """``che doctor``: every check on the install, as data first and a table second.
2
+
3
+ :func:`diagnose` collects the checks as :class:`Check` records, so the table
4
+ and ``--json`` can never disagree. Nothing here changes anything; each problem
5
+ comes with the command that fixes it.
6
+
7
+ Library module: no shebang, not executable.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import shutil
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ import shellgen
18
+ from art import print_logo
19
+ from commands import REPO, VERSION, all_commands, required_binaries
20
+ from machine import (
21
+ INSTALL_HINTS,
22
+ PYTHON_HINT,
23
+ SHELLS,
24
+ ShellSpec,
25
+ default_shell_selection,
26
+ find_python,
27
+ load_record,
28
+ login_shell,
29
+ python_ok,
30
+ shim_on_path,
31
+ shim_path,
32
+ tildify,
33
+ update_command,
34
+ version_text,
35
+ )
36
+ from rcfile import ALIAS, find_block, find_legacy, find_shadowing, read_rc
37
+ from tui import palette
38
+ from utils import add_bool_flag, build_parser, note, success, warning
39
+
40
+ OK, WARN, FAIL = "ok", "warn", "fail"
41
+
42
+ ALIAS_FIX = "the alias wins at the prompt; run `\\name` for the wrapper, or remove the alias"
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Check:
47
+ """One line of the report: what was checked, how it stands, and the fix."""
48
+
49
+ name: str
50
+ status: str # OK, WARN or FAIL
51
+ detail: str
52
+ fix: str = ""
53
+ # A passing check that is noise unless asked for, like a tool that is present.
54
+ quiet: bool = False
55
+
56
+ def to_json(self) -> dict[str, str]:
57
+ return {"name": self.name, "status": self.status, "detail": self.detail, "fix": self.fix}
58
+
59
+
60
+ def diagnose() -> list[Check]:
61
+ """Collect every check as data, so both the table and --json use one source."""
62
+ record = load_record()
63
+
64
+ python, version = find_python()
65
+ checks = [
66
+ Check(
67
+ "python",
68
+ OK if python_ok(version) else FAIL,
69
+ f"{version_text(version)} ({python})",
70
+ PYTHON_HINT,
71
+ ),
72
+ # Informational: doctor runs from this repo's own che.py, so a repo
73
+ # without one could never have got this far.
74
+ Check("repo", OK, str(REPO)),
75
+ ]
76
+
77
+ try:
78
+ stale = shellgen.sync_generated(dry_run=True)
79
+ except ValueError as exc:
80
+ checks.append(Check("generated files", WARN, str(exc), "restore the markers in README.md"))
81
+ else:
82
+ detail = f"{len(stale)} out of date: {', '.join(map(str, stale))}" if stale else "current"
83
+ checks.append(Check("generated files", WARN if stale else OK, detail, "che install"))
84
+
85
+ recorded = record.version if record else ""
86
+ checks.append(
87
+ Check(
88
+ "install record",
89
+ OK if recorded == VERSION else (WARN if record else FAIL),
90
+ f"config {recorded or 'missing'} vs che {VERSION}",
91
+ "che install",
92
+ )
93
+ )
94
+
95
+ # Only the shells this install actually targets. Reporting on every shell
96
+ # present would flag ~/.profile as "missing" on any machine with /bin/sh,
97
+ # which is every machine.
98
+ watched = {*(record.shells if record else default_shell_selection()), login_shell()}
99
+ for spec in SHELLS.values():
100
+ if spec.name in watched:
101
+ checks += [_rc_check(spec, rc_file) for rc_file in spec.rc_files]
102
+
103
+ shim = shim_path()
104
+ if shim.exists() and shim_on_path():
105
+ checks.append(Check("che on PATH", OK, str(shim)))
106
+ elif shim.exists():
107
+ detail = f"{shim} (PATH picks it up on your next shell)"
108
+ checks.append(Check("che on PATH", WARN, detail, f"exec {login_shell() or 'zsh'}"))
109
+ else:
110
+ checks.append(Check("che on PATH", WARN, "not installed", "che install"))
111
+
112
+ for binary, users in required_binaries().items():
113
+ found = shutil.which(binary)
114
+ used_by = ", ".join(users[:3]) + (f" +{len(users) - 3}" if len(users) > 3 else "")
115
+ checks.append(
116
+ Check(
117
+ f"tool: {binary}",
118
+ OK if found else WARN,
119
+ found or f"missing - needed by {used_by}",
120
+ INSTALL_HINTS.get(binary, ""),
121
+ quiet=True,
122
+ )
123
+ )
124
+
125
+ missing = [
126
+ command.name for command in all_commands() if command.script and not command.path.exists()
127
+ ]
128
+ checks.append(
129
+ Check(
130
+ "scripts",
131
+ OK if not missing else FAIL,
132
+ f"{len(all_commands())} commands"
133
+ + (f", missing: {', '.join(missing)}" if missing else ""),
134
+ update_command(),
135
+ )
136
+ )
137
+
138
+ return checks
139
+
140
+
141
+ def _rc_check(spec: ShellSpec, rc_file: Path) -> Check:
142
+ """Whether ``rc_file`` has the block, and whatever in it competes with the block."""
143
+ name = f"{spec.name}: {tildify(rc_file)}"
144
+ try:
145
+ text = read_rc(rc_file)
146
+ except OSError as exc:
147
+ return Check(name, FAIL, f"unreadable ({exc.strerror or exc})", "make it readable")
148
+
149
+ has_block = find_block(text) is not None
150
+ legacy = find_legacy(text)
151
+ shadowing = find_shadowing(text)
152
+
153
+ if has_block:
154
+ status = WARN if (legacy or shadowing) else OK
155
+ else:
156
+ status = WARN if spec.opt_in else FAIL
157
+
158
+ detail = "block installed" if has_block else "no che block"
159
+ if legacy:
160
+ detail += f", {len(legacy)} legacy wrappers"
161
+ if shadowing:
162
+ detail += ", shadowed: " + ", ".join(f"{wrapper} ({how})" for wrapper, how in shadowing[:3])
163
+
164
+ if legacy:
165
+ fix = "che install --replace-legacy"
166
+ elif any(how == ALIAS for _, how in shadowing):
167
+ fix = ALIAS_FIX
168
+ elif shadowing:
169
+ fix = "move your definition above the che block"
170
+ else:
171
+ fix = "che install"
172
+ return Check(name, status, detail, fix)
173
+
174
+
175
+ def doctor(*, as_json: bool = False, verbose: bool = False) -> int:
176
+ checks = diagnose()
177
+ failures = sum(1 for check in checks if check.status == FAIL)
178
+
179
+ if as_json:
180
+ payload = {"version": VERSION, "checks": [check.to_json() for check in checks]}
181
+ print(json.dumps(payload, indent=2))
182
+ return 1 if failures else 0
183
+
184
+ colors = palette()
185
+ marks = {
186
+ OK: f"{colors.green}✓{colors.reset}",
187
+ WARN: f"{colors.yellow}!{colors.reset}",
188
+ FAIL: f"{colors.red}✗{colors.reset}",
189
+ }
190
+ print_logo(f"doctor - che {VERSION}")
191
+ width = max(len(check.name) for check in checks) + 2
192
+
193
+ for check in checks:
194
+ if check.status == OK and check.quiet and not verbose:
195
+ continue
196
+ print(
197
+ f" {marks[check.status]} {check.name:<{width}} {colors.grey}{check.detail}{colors.reset}"
198
+ )
199
+ if check.status != OK and check.fix:
200
+ print(f" {colors.grey}→ {check.fix}{colors.reset}")
201
+
202
+ warnings = sum(1 for check in checks if check.status == WARN)
203
+ print()
204
+ if failures:
205
+ warning(f"{failures} problem(s), {warnings} warning(s).")
206
+ elif warnings:
207
+ note(f"All good, with {warnings} warning(s).")
208
+ else:
209
+ success("Everything checks out.")
210
+ return 1 if failures else 0
211
+
212
+
213
+ def doctor_main(argv: list[str] | None = None) -> int:
214
+ parser = build_parser(
215
+ prog="che doctor", description="🩺 Check the install and everything it depends on."
216
+ )
217
+ add_bool_flag(parser, "--json", help="Machine-readable output")
218
+ add_bool_flag(parser, "--verbose", help="Include checks that passed")
219
+ args = parser.parse_args(argv)
220
+ return doctor(as_json=args.json, verbose=args.verbose)
@@ -30,12 +30,15 @@ from pathlib import Path
30
30
 
31
31
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
32
32
 
33
+ from nas import NasDrive, select_drives, warn_unknown_drive
33
34
  from utils import (
34
35
  add_bool_flag,
35
36
  build_parser,
37
+ dry_run_complete,
38
+ footer_rule,
39
+ heading,
36
40
  info,
37
41
  log,
38
- note,
39
42
  require_binary,
40
43
  run,
41
44
  run_cli,
@@ -45,14 +48,6 @@ from utils import (
45
48
 
46
49
  __version__ = "3.3.0"
47
50
 
48
- DRIVES: tuple[tuple[str, str], ...] = (
49
- ("Meleys", "192.168.50.2"),
50
- ("Vermithor", "192.168.50.3"),
51
- ("Caraxes", "192.168.50.4"),
52
- ("Syrax", "192.168.50.5"),
53
- ("Vhagar", "192.168.50.6"),
54
- )
55
-
56
51
 
57
52
  def eject_volume(label: str, *, dry_run: bool, force: bool, quiet: bool) -> str:
58
53
  """Unmount ``/Volumes/<label>``. Returns "ejected", "skipped" or "failed"."""
@@ -90,19 +85,17 @@ def eject_volume(label: str, *, dry_run: bool, force: bool, quiet: bool) -> str:
90
85
  return "ejected"
91
86
 
92
87
 
93
- def reset_finder_state(
94
- selected: list[tuple[str, str]], *, clear_favorites: bool, only: str
95
- ) -> None:
88
+ def reset_finder_state(selected: list[NasDrive], *, reset_lists: bool) -> None:
96
89
  """Drop Finder's server-level entries so the sidebar stops resurrecting them."""
97
90
  log("🪪 Asking Finder to drop any server-level entries...")
98
- for name, _ in selected:
91
+ for drive in selected:
99
92
  run(
100
93
  [
101
94
  "osascript",
102
95
  "-e",
103
96
  'tell application "Finder" to try',
104
97
  "-e",
105
- f'eject disk "{name}"',
98
+ f'eject disk "{drive.name}"',
106
99
  "-e",
107
100
  "end try",
108
101
  ],
@@ -110,9 +103,7 @@ def reset_finder_state(
110
103
  capture=True,
111
104
  )
112
105
 
113
- # Skipped when --only is set: resetlist is all-or-nothing and would nuke
114
- # the other drives' favorites too.
115
- if not only and clear_favorites:
106
+ if reset_lists:
116
107
  log("🧽 Resetting Finder Favorite Servers + Recent Hosts lists...")
117
108
  for list_name in (
118
109
  "com.apple.LSSharedFileList.FavoriteServers",
@@ -138,54 +129,44 @@ def main(argv: list[str] | None = None) -> int:
138
129
  add_bool_flag(
139
130
  parser,
140
131
  "--no-force",
141
- dest="no_force",
142
132
  allow_value=False,
143
133
  help="Use 'diskutil unmount' instead of 'diskutil unmount force'",
144
134
  )
145
135
  add_bool_flag(
146
136
  parser,
147
137
  "--no-clear-favorites",
148
- dest="no_clear_favorites",
149
138
  allow_value=False,
150
139
  help="Don't reset Finder's Favorite Servers / Recent Hosts lists",
151
140
  )
152
141
  add_bool_flag(parser, "--quiet", help="Only log unmounts and failures")
153
142
  args = parser.parse_args(argv)
154
143
 
155
- force = not args.no_force
156
- clear_favorites = not args.no_clear_favorites
157
-
158
144
  # Only gate on diskutil for a real run: the shell version's dry-run never
159
145
  # invoked it, so previewing worked anywhere.
160
146
  if not args.dry_run:
161
147
  require_binary("diskutil", hint="This script is macOS-only.")
162
148
 
163
- header_suffix = " 🌵 (dry run)" if args.dry_run else ""
164
- note("═══════════════════════════════════════════")
165
- note(f"📤 eject-all-drives{header_suffix}")
166
- note("═══════════════════════════════════════════")
149
+ heading("📤 eject-all-drives", dry_run=args.dry_run)
167
150
 
168
- selected = [(n, ip) for n, ip in DRIVES if not args.only or n.lower() == args.only.lower()]
169
- if args.only and not selected:
170
- # Warn, but keep the shell version's exit code and control flow: an
171
- # unknown --only printed a 0/0 summary and still exited 0.
172
- warning(f"⚠️ No drive named '{args.only}'. Known: {', '.join(n for n, _ in DRIVES)}")
151
+ selected = select_drives(args.only)
152
+ if not selected:
153
+ warn_unknown_drive(args.only)
173
154
 
155
+ force = not args.no_force
174
156
  counts = {"ejected": 0, "skipped": 0, "failed": 0}
175
157
 
176
- for name, ip in selected:
177
- info(f"🐉 {name} ({ip})")
158
+ for drive in selected:
159
+ info(f"🐉 {drive.name} ({drive.ip})")
178
160
  # macOS re-mounts of the same share land at "<name>-1" or at the IP.
179
- for label in (name, f"{name}-1", ip):
161
+ for label in (drive.name, f"{drive.name}-1", drive.ip):
180
162
  counts[eject_volume(label, dry_run=args.dry_run, force=force, quiet=args.quiet)] += 1
181
163
 
182
- print()
183
- note("─────────────────────────────────────────────")
164
+ footer_rule()
184
165
 
185
166
  if args.dry_run:
186
167
  success(f"🌵 Would eject: {counts['ejected']}")
187
168
  info(f"⏭️ Skipped: {counts['skipped']} (not mounted)")
188
- log("💡 Dry run complete. Nothing was actually ejected.")
169
+ dry_run_complete("ejected")
189
170
  return 0
190
171
 
191
172
  success(f"✅ Ejected: {counts['ejected']}")
@@ -194,7 +175,9 @@ def main(argv: list[str] | None = None) -> int:
194
175
  warning(f"❌ Failed: {counts['failed']}")
195
176
  return 1
196
177
 
197
- reset_finder_state(selected, clear_favorites=clear_favorites, only=args.only)
178
+ # The lists are left alone under --only: resetlist is all-or-nothing and
179
+ # would nuke the other drives' favorites too.
180
+ reset_finder_state(selected, reset_lists=not args.only and not args.no_clear_favorites)
198
181
  log("🎉 All clean! 🧹✨")
199
182
  return 0
200
183
 
@@ -19,20 +19,20 @@ Options:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- import os
23
22
  import sys
24
23
  from pathlib import Path
25
24
 
26
25
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
27
26
 
27
+ from nas import is_mounted, mount_smb, select_drives, smb_url, warn_unknown_drive
28
28
  from utils import (
29
29
  add_bool_flag,
30
30
  build_parser,
31
+ footer_rule,
32
+ heading,
31
33
  info,
32
34
  log,
33
- note,
34
35
  require_binary,
35
- run,
36
36
  run_cli,
37
37
  success,
38
38
  warning,
@@ -40,29 +40,6 @@ from utils import (
40
40
 
41
41
  __version__ = "4.1.0"
42
42
 
43
- SMB_USER = "crivas"
44
-
45
- # (name, ip) - the share name is assumed to match the drive name.
46
- DRIVES: tuple[tuple[str, str], ...] = (
47
- ("Meleys", "192.168.50.2"),
48
- ("Vermithor", "192.168.50.3"),
49
- ("Caraxes", "192.168.50.4"),
50
- ("Syrax", "192.168.50.5"),
51
- ("Vhagar", "192.168.50.6"),
52
- )
53
-
54
-
55
- def is_mounted(name: str) -> bool:
56
- """True when something is actually mounted at ``/Volumes/<name>``.
57
-
58
- The shell version grepped ``mount`` output for `` on /Volumes/<name> (``;
59
- ``os.path.ismount`` asks the kernel directly. The two agree for every
60
- healthy mount. The one real difference: ``ismount`` lstats the mount root,
61
- so on a wedged SMB mount it can block for the server timeout where reading
62
- the mount table cannot.
63
- """
64
- return os.path.ismount(f"/Volumes/{name}")
65
-
66
43
 
67
44
  def mount_drive(name: str, host: str, *, quiet: bool) -> str:
68
45
  """Mount one drive. Returns "mounted", "skipped" or "failed"."""
@@ -71,24 +48,12 @@ def mount_drive(name: str, host: str, *, quiet: bool) -> str:
71
48
  info(f"⏭️ {name} — already mounted")
72
49
  return "skipped"
73
50
 
74
- url = f"smb://{SMB_USER}@{host}/{name}"
75
- log(f"🔗 Mounting {name} ({url})...")
76
-
77
- completed = run(
78
- ["osascript", "-e", f'mount volume "{url}"'],
79
- check=False,
80
- capture=True,
81
- )
82
- if completed.returncode == 0:
83
- success(f" ✅ {name} mounted 🐉")
84
- return "mounted"
51
+ log(f"🔗 Mounting {name} ({smb_url(name, host)})...")
52
+ if not mount_smb(name, host):
53
+ return "failed"
85
54
 
86
- warning(f" ❌ mount failed for {url}")
87
- stderr = (completed.stderr or "").strip()
88
- if stderr:
89
- warning(f" {stderr}")
90
- warning(f" Check that Keychain has a password for '{SMB_USER}@{host}'.")
91
- return "failed"
55
+ success(f" ✅ {name} mounted 🐉")
56
+ return "mounted"
92
57
 
93
58
 
94
59
  def main(argv: list[str] | None = None) -> int:
@@ -105,27 +70,18 @@ def main(argv: list[str] | None = None) -> int:
105
70
 
106
71
  require_binary("osascript", hint="This script is macOS-only.")
107
72
 
108
- note("═══════════════════════════════════════════")
109
- note("💿 mount-all-drives")
110
- note("═══════════════════════════════════════════")
73
+ heading("💿 mount-all-drives")
74
+
75
+ selected = select_drives(args.only)
76
+ if not selected:
77
+ warn_unknown_drive(args.only)
111
78
 
112
79
  counts = {"mounted": 0, "skipped": 0, "failed": 0}
113
- matched = False
114
-
115
- for name, ip in DRIVES:
116
- if args.only and name.lower() != args.only.lower():
117
- continue
118
- matched = True
119
- host = ip if args.use_ip else name
120
- counts[mount_drive(name, host, quiet=args.quiet)] += 1
121
-
122
- if args.only and not matched:
123
- # Warn, but keep the shell version's exit code: an unknown --only
124
- # printed a 0/0 summary and still exited 0.
125
- warning(f"⚠️ No drive named '{args.only}'. Known: {', '.join(n for n, _ in DRIVES)}")
126
-
127
- print()
128
- note("─────────────────────────────────────────────")
80
+ for drive in selected:
81
+ host = drive.host(use_ip=args.use_ip)
82
+ counts[mount_drive(drive.name, host, quiet=args.quiet)] += 1
83
+
84
+ footer_rule()
129
85
  success(f"✅ Mounted: {counts['mounted']}")
130
86
  info(f"⏭️ Skipped: {counts['skipped']} (already mounted)")
131
87
  if counts["failed"]: