@codebend3r/che 1.1.0 → 1.2.1
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 +58 -26
- package/bin/art.py +22 -20
- package/bin/branchsync.py +324 -0
- package/bin/che.py +208 -587
- package/bin/commands.py +219 -231
- package/bin/doctor.py +220 -0
- package/bin/drives/eject-all-drives.py +21 -38
- package/bin/drives/mount-all-drives.py +18 -62
- package/bin/drives/ping-nas.py +77 -105
- package/bin/files/compress-folders.py +7 -11
- package/bin/files/delete-by-ext.py +10 -14
- package/bin/files/delete-empty-folders.py +33 -37
- package/bin/files/delete-smb-files.py +12 -21
- package/bin/files/files-under-size.py +5 -11
- package/bin/files/find-largest-files.py +21 -25
- package/bin/files/list-permission.py +40 -0
- package/bin/files/make-alpha-dir.py +2 -0
- package/bin/git/all-actions.py +350 -253
- package/bin/git/checkout-my-branches.py +42 -63
- package/bin/git/clean-stale-branches.py +23 -48
- package/bin/git/prune-worktrees.py +12 -51
- package/bin/git/sync-all-branches.py +75 -297
- package/bin/git/update-local-branches.py +67 -73
- package/bin/gitutils.py +263 -0
- package/bin/install.py +393 -1182
- package/bin/machine.py +345 -0
- package/bin/media.py +127 -0
- package/bin/menu.py +437 -0
- package/bin/nas.py +102 -0
- package/bin/rcfile.py +380 -0
- package/bin/shellgen.py +503 -431
- package/bin/system/btop-launch.py +17 -13
- package/bin/system/update-brew.py +57 -68
- package/bin/tui.py +156 -153
- package/bin/utils.py +165 -237
- package/bin/video/delete-duplicate-videos.py +64 -62
- package/bin/video/detect-green-magenta-videos.py +105 -95
- package/bin/video/find-movie-by-year.py +3 -0
- package/bin/video/find-video-mkv-issues.py +48 -65
- package/bin/video/fix-codecs.py +25 -30
- package/bin/video/largest-tv-shows.py +60 -57
- package/bin/video/remove-metadata.py +3 -6
- package/bin/video/rename-video-file.py +74 -84
- package/bin/video/scan-videos-audio-language.py +16 -33
- package/bin/video/show-codecs.py +20 -24
- package/bin/video/validate-video-files.py +5 -8
- package/bin/video/video-list.py +17 -20
- package/package.json +4 -10
- package/shell/che.bash +33 -33
- package/shell/che.fish +7 -7
- package/shell/che.zsh +33 -33
- package/shell/completions/_che +19 -19
- package/shell/completions/che.bash +15 -15
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
169
|
-
if
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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"]:
|