@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.
- package/README.md +65 -25
- package/bin/art.py +123 -0
- package/bin/che.py +209 -568
- package/bin/commands.py +220 -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 +54 -283
- package/bin/git/update-local-branches.py +67 -73
- package/bin/gitutils.py +263 -0
- package/bin/install.py +393 -1188
- 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 +508 -428
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""๐ฟ Check out recent remote branches authored by you that aren't yet local.
|
|
3
3
|
|
|
4
|
+
Each one becomes a local branch tracking its remote. HEAD stays where it is.
|
|
5
|
+
|
|
4
6
|
Usage:
|
|
5
7
|
checkout-my-branches.py [--author=EMAIL] [--limit=N]
|
|
6
8
|
|
|
@@ -12,44 +14,40 @@ Options:
|
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
14
16
|
|
|
15
|
-
import re
|
|
16
17
|
import sys
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
20
21
|
|
|
22
|
+
from gitutils import fetch_remotes, for_each_ref, local_branches, skip_git_hooks
|
|
21
23
|
from utils import (
|
|
22
24
|
build_parser,
|
|
25
|
+
heading,
|
|
23
26
|
info,
|
|
24
27
|
log,
|
|
25
|
-
|
|
28
|
+
parse_count,
|
|
26
29
|
require_binary,
|
|
27
30
|
run,
|
|
28
31
|
run_cli,
|
|
29
|
-
run_lines,
|
|
30
32
|
run_output,
|
|
31
33
|
success,
|
|
32
34
|
warning,
|
|
33
35
|
)
|
|
34
36
|
|
|
35
|
-
__version__ = "3.
|
|
37
|
+
__version__ = "3.1.0"
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
def main(argv: list[str] | None = None) -> int:
|
|
39
|
-
default_author = run_output(["git", "config", "user.email"], check=False).strip()
|
|
40
|
-
|
|
41
41
|
parser = build_parser(
|
|
42
42
|
prog="checkout-my-branches",
|
|
43
43
|
description="๐ฟ Check out recent remote branches you authored that aren't yet local.",
|
|
44
44
|
)
|
|
45
45
|
parser.add_argument(
|
|
46
46
|
"--author",
|
|
47
|
-
default=
|
|
47
|
+
default="",
|
|
48
48
|
metavar="EMAIL",
|
|
49
49
|
help="Author email to match (default: git config user.email)",
|
|
50
50
|
)
|
|
51
|
-
# Kept as a string, not `type=int`: argparse's own int conversion would
|
|
52
|
-
# own the failure and exit before the shell version's message could print.
|
|
53
51
|
parser.add_argument(
|
|
54
52
|
"--limit",
|
|
55
53
|
default="100",
|
|
@@ -58,80 +56,61 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
58
56
|
)
|
|
59
57
|
args = parser.parse_args(argv)
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
warning("โ No author email โ pass --author=EMAIL or set git config user.email")
|
|
63
|
-
return 1
|
|
64
|
-
|
|
65
|
-
if not re.fullmatch(r"[0-9]+", args.limit) or int(args.limit) < 1:
|
|
66
|
-
warning(f"โ --limit must be a positive integer (got: {args.limit})")
|
|
67
|
-
return 1
|
|
68
|
-
limit = int(args.limit)
|
|
59
|
+
limit = parse_count(args.limit, "--limit", minimum=1)
|
|
69
60
|
|
|
70
61
|
require_binary("git")
|
|
62
|
+
# Read after the check for git, so a missing git says so rather than
|
|
63
|
+
# reporting that no author email is set.
|
|
64
|
+
author = args.author or run_output(["git", "config", "user.email"], check=False).strip()
|
|
65
|
+
if not author:
|
|
66
|
+
warning("โ No author email โ pass --author=EMAIL or set git config user.email")
|
|
67
|
+
return 1
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
# Nothing here checks out, but fetch and branch still fire a repo's
|
|
70
|
+
# reference-transaction hook.
|
|
71
|
+
skip_git_hooks()
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
warning("โ Working tree is dirty. Commit or stash before running. ๐งบ")
|
|
78
|
-
return 1
|
|
73
|
+
heading("๐ฟ checkout-my-branches")
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
run(["git", "fetch", "--all", "--prune", "--quiet"])
|
|
75
|
+
fetch_remotes()
|
|
82
76
|
|
|
83
|
-
info(f"๐ฆ Scanning {limit} most recent remote branches for author ๐ค {
|
|
77
|
+
info(f"๐ฆ Scanning {limit} most recent remote branches for author ๐ค {author}...")
|
|
84
78
|
|
|
85
79
|
checked_out = 0
|
|
86
80
|
skipped = 0
|
|
87
81
|
not_mine = 0
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"--sort=-committerdate",
|
|
101
|
-
"--format=%(authoremail)%01%(refname:short)%01%(symref)",
|
|
102
|
-
"refs/remotes/origin",
|
|
103
|
-
]
|
|
83
|
+
local = {branch for (branch,) in local_branches()}
|
|
84
|
+
|
|
85
|
+
# One pass: author email + branch name + symref target, newest first.
|
|
86
|
+
# %(symref) is non-empty for symbolic refs like origin/HEAD, which must be
|
|
87
|
+
# skipped - it is a pointer, not a branch anyone authored.
|
|
88
|
+
refs = for_each_ref(
|
|
89
|
+
"refs/remotes/origin",
|
|
90
|
+
"authoremail",
|
|
91
|
+
"refname:lstrip=3",
|
|
92
|
+
"symref",
|
|
93
|
+
sort="-committerdate",
|
|
104
94
|
)
|
|
105
95
|
|
|
106
|
-
for
|
|
107
|
-
|
|
108
|
-
if len(parts) < 3:
|
|
109
|
-
continue
|
|
110
|
-
author_email, remote_branch, symref = parts[0], parts[1], parts[2]
|
|
111
|
-
|
|
112
|
-
if not remote_branch or symref:
|
|
96
|
+
for author_email, branch, symref in refs[:limit]:
|
|
97
|
+
if symref:
|
|
113
98
|
continue
|
|
114
99
|
|
|
115
|
-
|
|
116
|
-
if author_email != args.author:
|
|
100
|
+
if author_email.removeprefix("<").removesuffix(">") != author:
|
|
117
101
|
not_mine += 1
|
|
118
102
|
continue
|
|
119
103
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
continue # malformed entry โ don't touch
|
|
123
|
-
|
|
124
|
-
exists = run(
|
|
125
|
-
["git", "show-ref", "--verify", "--quiet", f"refs/heads/{local_branch}"],
|
|
126
|
-
check=False,
|
|
127
|
-
capture=True,
|
|
128
|
-
)
|
|
129
|
-
if exists.returncode == 0:
|
|
130
|
-
info(f"โญ๏ธ Already local: {local_branch}")
|
|
104
|
+
if branch in local:
|
|
105
|
+
info(f"โญ๏ธ Already local: {branch}")
|
|
131
106
|
skipped += 1
|
|
132
107
|
else:
|
|
133
|
-
log(f"๐ Checking out: {
|
|
134
|
-
|
|
108
|
+
log(f"๐ Checking out: {branch}")
|
|
109
|
+
# `git branch --track`, not `checkout -b`: switching to each new
|
|
110
|
+
# branch left HEAD on the last one, undoing the park on main that
|
|
111
|
+
# sync-all-branches runs this after. The full ref name cannot be
|
|
112
|
+
# mistaken for a local branch called `origin/<branch>`.
|
|
113
|
+
run(["git", "branch", "--track", branch, f"refs/remotes/origin/{branch}"])
|
|
135
114
|
checked_out += 1
|
|
136
115
|
|
|
137
116
|
print()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""๐งน Delete local branches whose upstream is gone (the remote branch was deleted).
|
|
3
3
|
|
|
4
|
-
Protected branches and
|
|
4
|
+
Protected branches are never deleted, and neither is a branch checked out in a
|
|
5
|
+
worktree - the current one included. Those are reported instead.
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
7
8
|
clean-stale-branches.py [--dry-run] [--protect=BRANCH1,BRANCH2,...]
|
|
@@ -23,62 +24,41 @@ from pathlib import Path
|
|
|
23
24
|
|
|
24
25
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
25
26
|
|
|
27
|
+
from gitutils import fetch_remotes, local_branches, skip_git_hooks, worktree_branch_map
|
|
26
28
|
from utils import (
|
|
27
29
|
add_bool_flag,
|
|
28
30
|
build_parser,
|
|
31
|
+
dry_run_complete,
|
|
29
32
|
env_bool,
|
|
33
|
+
heading,
|
|
30
34
|
info,
|
|
31
|
-
log,
|
|
32
|
-
note,
|
|
33
35
|
require_binary,
|
|
34
36
|
run,
|
|
35
37
|
run_cli,
|
|
36
|
-
run_lines,
|
|
37
38
|
success,
|
|
38
39
|
warning,
|
|
39
|
-
worktree_branch_map,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
__version__ = "3.
|
|
42
|
+
__version__ = "3.2.0"
|
|
43
43
|
|
|
44
44
|
DEFAULT_PROTECTED = ("main", "master", "dev", "develop", "staging")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
"""The checked-out branch, or "" when HEAD is detached."""
|
|
49
|
-
completed = run(
|
|
50
|
-
["git", "symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
51
|
-
check=False,
|
|
52
|
-
capture=True,
|
|
53
|
-
)
|
|
54
|
-
return (completed.stdout or "").strip()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def stale_branches(protected: set[str], skip: str) -> list[str]:
|
|
47
|
+
def stale_branches(protected: set[str]) -> list[str]:
|
|
58
48
|
"""Local branches whose upstream is marked ``[gone]``.
|
|
59
49
|
|
|
60
50
|
``[gone]`` means the remote branch was deleted, which is what a merged and
|
|
61
51
|
tidied-up PR leaves behind. It does *not* mean merged - a closed-unmerged
|
|
62
52
|
PR looks identical here, same as in the shell version.
|
|
53
|
+
|
|
54
|
+
The current branch gets no special case: its worktree pins it like any
|
|
55
|
+
other checked-out branch, so ``main`` reports it alongside those.
|
|
63
56
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
["git", "for-each-ref", "--format=%(refname:short)%01%(upstream:track)", "refs/heads/"]
|
|
70
|
-
):
|
|
71
|
-
branch, _, track = line.partition("\x01")
|
|
72
|
-
if not branch:
|
|
73
|
-
continue
|
|
74
|
-
if branch in protected:
|
|
75
|
-
continue
|
|
76
|
-
if branch == skip:
|
|
77
|
-
note(f"โญ๏ธ Skipping current branch: {branch}")
|
|
78
|
-
continue
|
|
79
|
-
if "[gone]" in track:
|
|
80
|
-
found.append(branch)
|
|
81
|
-
return found
|
|
57
|
+
return [
|
|
58
|
+
branch
|
|
59
|
+
for branch, track in local_branches("upstream:track")
|
|
60
|
+
if branch not in protected and "[gone]" in track
|
|
61
|
+
]
|
|
82
62
|
|
|
83
63
|
|
|
84
64
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -101,22 +81,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
101
81
|
args = parser.parse_args(argv)
|
|
102
82
|
|
|
103
83
|
require_binary("git")
|
|
84
|
+
skip_git_hooks()
|
|
104
85
|
|
|
105
86
|
protected = set(DEFAULT_PROTECTED)
|
|
106
87
|
protected.update(name for name in args.protect.split(",") if name)
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
110
|
-
note(f"๐งน clean-stale-branches{header_suffix}")
|
|
111
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
89
|
+
heading("๐งน clean-stale-branches", dry_run=args.dry_run)
|
|
112
90
|
|
|
113
|
-
|
|
114
|
-
# Not wrapped: the shell version ran this bare under `set -e`, so a fetch
|
|
115
|
-
# failure exited with git's own status (128), not a flattened 1.
|
|
116
|
-
run(["git", "fetch", "--all", "--prune", "--quiet"])
|
|
91
|
+
fetch_remotes()
|
|
117
92
|
|
|
118
93
|
info("๐ Scanning local branches for gone upstreams...")
|
|
119
|
-
stale = stale_branches(protected
|
|
94
|
+
stale = stale_branches(protected)
|
|
120
95
|
|
|
121
96
|
if not stale:
|
|
122
97
|
success("โจ No stale branches. All clean! ๐")
|
|
@@ -124,8 +99,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
124
99
|
|
|
125
100
|
# A branch checked out in a worktree cannot be deleted โ git refuses with
|
|
126
101
|
# "already used by worktree". Split those out so one pinned branch doesn't
|
|
127
|
-
# abort the whole run
|
|
128
|
-
#
|
|
102
|
+
# abort the whole run. Worktrees are the user's, so they are reported and
|
|
103
|
+
# left alone, never removed to free the branch.
|
|
129
104
|
# One `git worktree list` for the whole set, not one per branch as the
|
|
130
105
|
# shell's per-branch `worktree_for_branch` did.
|
|
131
106
|
pinned_by_branch = worktree_branch_map()
|
|
@@ -136,7 +111,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
136
111
|
warning(f"โญ๏ธ {len(pinned)} stale branch(es) are checked out in a worktree:")
|
|
137
112
|
for branch, path in pinned:
|
|
138
113
|
warning(f" โข ๐ฟ {branch} (in {path})")
|
|
139
|
-
info("๐ก
|
|
114
|
+
info("๐ก Switch off or remove those worktrees, and the next run deletes their branches.")
|
|
140
115
|
|
|
141
116
|
if not deletable:
|
|
142
117
|
success("โจ Nothing left to delete. ๐")
|
|
@@ -146,7 +121,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
146
121
|
warning(f"๐ต {len(deletable)} stale branch(es) would be deleted:")
|
|
147
122
|
for branch in deletable:
|
|
148
123
|
warning(f" โข ๐ชฆ {branch}")
|
|
149
|
-
|
|
124
|
+
dry_run_complete("deleted")
|
|
150
125
|
return 0
|
|
151
126
|
|
|
152
127
|
for branch in deletable:
|
|
@@ -22,24 +22,22 @@ from pathlib import Path
|
|
|
22
22
|
|
|
23
23
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
24
24
|
|
|
25
|
+
from gitutils import park_main_worktree, skip_git_hooks, worktrees
|
|
25
26
|
from utils import (
|
|
26
27
|
add_bool_flag,
|
|
27
28
|
build_parser,
|
|
29
|
+
dry_run_complete,
|
|
28
30
|
env_bool,
|
|
29
|
-
|
|
31
|
+
heading,
|
|
30
32
|
info,
|
|
31
|
-
log,
|
|
32
|
-
note,
|
|
33
33
|
require_binary,
|
|
34
34
|
run,
|
|
35
35
|
run_cli,
|
|
36
|
-
run_output,
|
|
37
36
|
success,
|
|
38
37
|
warning,
|
|
39
|
-
worktree_paths,
|
|
40
38
|
)
|
|
41
39
|
|
|
42
|
-
__version__ = "1.
|
|
40
|
+
__version__ = "1.3.0"
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -61,60 +59,23 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
61
59
|
args = parser.parse_args(argv)
|
|
62
60
|
|
|
63
61
|
require_binary("git")
|
|
62
|
+
# Parking the main worktree is a `git checkout`, which fires post-checkout.
|
|
63
|
+
skip_git_hooks()
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
if inside.returncode != 0:
|
|
67
|
-
warning("โ Not inside a git working tree.")
|
|
68
|
-
return 1
|
|
69
|
-
|
|
70
|
-
header_suffix = " ๐ต (dry run)" if args.dry_run else ""
|
|
71
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
72
|
-
note(f"๐ฟ prune-worktrees{header_suffix}")
|
|
73
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
65
|
+
heading("๐ฟ prune-worktrees", dry_run=args.dry_run)
|
|
74
66
|
|
|
75
67
|
# Work from the main worktree: removing a worktree fails while standing
|
|
76
|
-
# inside it
|
|
77
|
-
|
|
78
|
-
if not paths:
|
|
79
|
-
warning("โ Could not determine main worktree.")
|
|
80
|
-
return 1
|
|
81
|
-
|
|
82
|
-
main_wt = Path(paths[0])
|
|
83
|
-
linked = [p for p in paths[1:] if Path(p) != main_wt]
|
|
84
|
-
|
|
85
|
-
main_branch = find_main_branch(main_wt)
|
|
86
|
-
if not main_branch:
|
|
87
|
-
warning("โ Neither 'main' nor 'master' exists locally โ refusing to prune.")
|
|
88
|
-
return 1
|
|
89
|
-
|
|
90
|
-
current = run_output(
|
|
91
|
-
["git", "symbolic-ref", "--quiet", "--short", "HEAD"], check=False, cwd=main_wt
|
|
92
|
-
).strip()
|
|
93
|
-
|
|
94
|
-
if current != main_branch:
|
|
95
|
-
# Only block on tracked-file changes: untracked files (including linked
|
|
96
|
-
# worktrees nested under the main worktree) won't be touched by checkout.
|
|
97
|
-
dirty = run_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=main_wt)
|
|
98
|
-
if dirty:
|
|
99
|
-
warning("โ Main worktree has uncommitted changes โ commit or stash before pruning. ๐งบ")
|
|
100
|
-
return 1
|
|
101
|
-
if args.dry_run:
|
|
102
|
-
info(
|
|
103
|
-
f"๐ต Would switch main worktree to ๐ฟ {main_branch} (currently on {current or 'detached'})"
|
|
104
|
-
)
|
|
105
|
-
else:
|
|
106
|
-
info(f"๐ Switching main worktree to ๐ฟ {main_branch} (from {current or 'detached'})")
|
|
107
|
-
run(["git", "checkout", "--quiet", main_branch], cwd=main_wt)
|
|
108
|
-
else:
|
|
109
|
-
info(f"๐ Main worktree already on ๐ฟ {main_branch}")
|
|
68
|
+
# inside it.
|
|
69
|
+
main_wt = park_main_worktree("prune", dry_run=args.dry_run)
|
|
110
70
|
|
|
111
71
|
info("๐ Listing worktrees...")
|
|
72
|
+
linked = [worktree.path for worktree in worktrees(main_wt)[1:]]
|
|
112
73
|
|
|
113
74
|
if not linked:
|
|
114
75
|
success("โจ No linked worktrees. Running prune anyway... ๐")
|
|
115
76
|
if args.dry_run:
|
|
116
77
|
run(["git", "worktree", "prune", "--dry-run", "--verbose"], check=False, cwd=main_wt)
|
|
117
|
-
|
|
78
|
+
dry_run_complete("pruned")
|
|
118
79
|
else:
|
|
119
80
|
run(["git", "worktree", "prune", "--verbose"], cwd=main_wt)
|
|
120
81
|
success("โจ Pruned stale worktree records. ๐")
|
|
@@ -125,7 +86,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
125
86
|
for path in linked:
|
|
126
87
|
warning(f" โข ๐ชฆ {path}")
|
|
127
88
|
info("๐ Would also run: git worktree prune --verbose")
|
|
128
|
-
|
|
89
|
+
dry_run_complete("removed")
|
|
129
90
|
return 0
|
|
130
91
|
|
|
131
92
|
remove_args = ["--force"] if args.force else []
|