@danolez/docker-util 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fatunmbi Daniel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # docker-util
2
+
3
+ Small interactive helpers for people who run a lot of Docker Compose stacks
4
+ on one machine. You pick projects from a list instead of typing names, and
5
+ anything that is still running is left alone.
6
+
7
+ ```
8
+ docker-list Show compose projects and containers outside compose
9
+ docker-stop Pick running compose projects to stop, then pick which to delete
10
+ docker-prune Clean stopped containers, unused images, volumes and build cache
11
+ docker-help Show the command list
12
+ ```
13
+
14
+ ## Install
15
+
16
+ Run it once without installing:
17
+
18
+ ```sh
19
+ npx @danolez/docker-util list
20
+ npx @danolez/docker-util stop
21
+ npx @danolez/docker-util prune --dry-run
22
+ ```
23
+
24
+ Or install it so the `docker-*` commands are on your PATH:
25
+
26
+ ```sh
27
+ npm install -g @danolez/docker-util
28
+ ```
29
+
30
+ Straight from GitHub works too, no registry needed:
31
+
32
+ ```sh
33
+ npx github:danolez1/docker-util help
34
+ ```
35
+
36
+ Works on macOS and Linux. It needs `bash` (the 3.2 that ships with macOS is
37
+ fine) and the `docker` CLI. Node is only used to install it.
38
+
39
+ ## Picking things
40
+
41
+ Every choice is a menu. Nothing asks you to type a name or a number.
42
+
43
+ The tool uses [fzf](https://github.com/junegunn/fzf) if it is installed, then
44
+ [gum](https://github.com/charmbracelet/gum), and otherwise falls back to its
45
+ own arrow-key menu:
46
+
47
+ | key | action |
48
+ | --- | --- |
49
+ | up / down (or k / j) | move |
50
+ | space | mark or unmark (multi-select menus) |
51
+ | enter | confirm |
52
+ | q or esc | abort, nothing is changed |
53
+
54
+ In fzf and gum, multi-select menus start with a `(none)` row. Confirm on it
55
+ to pick nothing. Yes/No questions default to No.
56
+
57
+ Force a picker with `DOCKER_UTIL_PICKER=fzf|gum|builtin`.
58
+
59
+ ## docker-stop
60
+
61
+ 1. Lists every compose project with at least one running container. Mark the
62
+ ones to stop.
63
+ 2. Stops them, then lists the same projects again so you can mark which ones
64
+ to delete. Deleting removes their containers and networks.
65
+ 3. If you deleted anything, asks whether to keep or delete the project
66
+ volumes. Keeping them is the first option.
67
+
68
+ `docker-stop --dry-run` shows the commands without running them.
69
+
70
+ ## docker-prune
71
+
72
+ Removes stopped containers, unused images, anonymous volumes and dangling
73
+ build cache. Compose projects with a running container are always kept, and
74
+ so are the images they use.
75
+
76
+ Before removing anything it asks:
77
+
78
+ - which stopped compose projects to keep,
79
+ - which unused named volumes to delete (none by default, since fixed-name
80
+ volumes often hold data you care about),
81
+ - whether to go ahead with the plan it prints.
82
+
83
+ ```
84
+ -n, --dry-run Show what would be removed, remove nothing
85
+ -y, --yes Skip prompts: remove all stopped projects, keep all named volumes
86
+ --keep-image REGEX Never remove images whose repo:tag matches REGEX (repeatable)
87
+ --cache-cap SIZE Build cache to keep, e.g. 10GB (default 20GB)
88
+ --tmp-prefix PREFIX Also delete $TMPDIR/PREFIX* dirs older than 24h (repeatable)
89
+ --no-mole Do not offer 'mo clean' afterwards
90
+ ```
91
+
92
+ If [Mole](https://github.com/tw93/Mole) is installed, it offers to run
93
+ `mo clean` at the end.
94
+
95
+ ### Config
96
+
97
+ Defaults can live in `~/.config/docker-util/config` (or `$DOCKER_UTIL_CONFIG`).
98
+ It is plain shell:
99
+
100
+ ```sh
101
+ # Images you can't pull again, e.g. mirrors of deleted upstream tags
102
+ KEEP_IMAGE_PATTERN='^(registry\.example\.com/|myorg/)'
103
+ CACHE_CAP=20GB
104
+ # Scratch dirs your test suites leave in $TMPDIR
105
+ TMP_PREFIXES="myapp-test- e2e-run-"
106
+ TMP_MAX_AGE_MIN=1440
107
+ ```
108
+
109
+ `DOCKER_UTIL_KEEP_IMAGES`, `DOCKER_UTIL_CACHE_CAP` and
110
+ `DOCKER_UTIL_TMP_PREFIXES` override the file, and flags override both.
111
+
112
+ ## Development
113
+
114
+ ```sh
115
+ npm test # runs test/run.sh against a stub docker, no daemon needed
116
+ npm run lint # shellcheck
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ self="$0"
5
+ while [ -L "$self" ]; do
6
+ link=$(readlink "$self")
7
+ case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
8
+ done
9
+ # shellcheck source=../lib/common.sh
10
+ . "$(dirname "$self")/../lib/common.sh"
11
+
12
+ case "${1:-}" in -v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;; esac
13
+
14
+ cat <<EOF
15
+ docker-util $DOCKER_UTIL_VERSION
16
+
17
+ docker-list Show compose projects and containers outside compose
18
+ docker-stop Pick running compose projects to stop, then pick which to delete
19
+ docker-prune Clean stopped containers, unused images, volumes and build cache
20
+ (running compose projects are never touched)
21
+ docker-help Show this list
22
+
23
+ Each command also runs as 'docker-util <list|stop|prune|help>'.
24
+ Add --help to any command for its options. --dry-run works on stop and prune.
25
+
26
+ Picker in use: $(picker) (fzf, then gum, then a built-in arrow-key menu)
27
+ EOF
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # Compose projects and loose containers at a glance.
3
+ set -uo pipefail
4
+
5
+ self="$0"
6
+ while [ -L "$self" ]; do
7
+ link=$(readlink "$self")
8
+ case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
9
+ done
10
+ # shellcheck source=../lib/common.sh
11
+ . "$(dirname "$self")/../lib/common.sh"
12
+
13
+ case "${1:-}" in
14
+ "") ;;
15
+ -h|--help)
16
+ cat <<EOF
17
+ docker-list $DOCKER_UTIL_VERSION
18
+ Show compose projects (running, stopped, partial) and containers outside compose.
19
+
20
+ Usage: docker-list
21
+ EOF
22
+ exit 0 ;;
23
+ -v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
24
+ *) echo "docker-list: unknown option '$1'" >&2; exit 2 ;;
25
+ esac
26
+
27
+ require_docker
28
+
29
+ rows=$(docker ps -a --format '{{.Label "com.docker.compose.project"}} {{.State}} {{.Names}} {{.Image}} {{.Status}}')
30
+
31
+ bold "Compose projects"
32
+ printf '%s\n' "$rows" | awk -F'\t' '
33
+ $1 != "" {
34
+ total[$1]++
35
+ if ($2 == "running") up[$1]++
36
+ }
37
+ END {
38
+ n = 0
39
+ for (p in total) {
40
+ if (up[p] == total[p]) state = "running"
41
+ else if (up[p] > 0) state = "partial"
42
+ else state = "stopped"
43
+ printf " %-32s %-8s %d/%d up\n", p, state, up[p], total[p]
44
+ n++
45
+ }
46
+ if (n == 0) print " none"
47
+ }' | sort
48
+
49
+ echo
50
+ bold "Containers outside compose"
51
+ printf '%s\n' "$rows" | awk -F'\t' '
52
+ $1 == "" && $3 != "" { printf " %-32s %-8s %-30s %s\n", $3, $2, $4, $5; n++ }
53
+ END { if (n == 0) print " none" }'
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bash
2
+ # Interactive Docker cleanup that never touches running compose stacks.
3
+ set -uo pipefail
4
+
5
+ self="$0"
6
+ while [ -L "$self" ]; do
7
+ link=$(readlink "$self")
8
+ case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
9
+ done
10
+ # shellcheck source=../lib/common.sh
11
+ . "$(dirname "$self")/../lib/common.sh"
12
+
13
+ CONFIG_FILE="${DOCKER_UTIL_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/docker-util/config}"
14
+ KEEP_IMAGE_PATTERN=""
15
+ CACHE_CAP="20GB"
16
+ TMP_PREFIXES=""
17
+ TMP_MAX_AGE_MIN=1440
18
+ MOLE=1
19
+
20
+ # shellcheck source=/dev/null
21
+ [ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE"
22
+ KEEP_IMAGE_PATTERN="${DOCKER_UTIL_KEEP_IMAGES:-$KEEP_IMAGE_PATTERN}"
23
+ CACHE_CAP="${DOCKER_UTIL_CACHE_CAP:-$CACHE_CAP}"
24
+ TMP_PREFIXES="${DOCKER_UTIL_TMP_PREFIXES:-$TMP_PREFIXES}"
25
+
26
+ usage() {
27
+ cat <<EOF
28
+ docker-prune $DOCKER_UTIL_VERSION
29
+ Clean up stopped containers, unused images, volumes and build cache
30
+ without touching running compose projects.
31
+
32
+ Usage: docker-prune [options]
33
+
34
+ Options:
35
+ -n, --dry-run Show what would be removed, remove nothing
36
+ -y, --yes Skip prompts: remove all stopped projects, keep all named volumes
37
+ --keep-image REGEX Never remove images whose repo:tag matches REGEX (repeatable)
38
+ --cache-cap SIZE Build cache to keep, e.g. 10GB (default: $CACHE_CAP)
39
+ --tmp-prefix PREFIX Also delete \$TMPDIR/PREFIX* dirs older than 24h (repeatable)
40
+ --no-mole Do not offer 'mo clean' afterwards
41
+ -h, --help Show this help
42
+ -v, --version Show version
43
+
44
+ Config file: $CONFIG_FILE
45
+ EOF
46
+ }
47
+
48
+ add_pattern() { KEEP_IMAGE_PATTERN="${KEEP_IMAGE_PATTERN:+$KEEP_IMAGE_PATTERN|}$1"; }
49
+ need_arg() { [ -n "${2:-}" ] || { echo "docker-prune: $1 needs a value" >&2; exit 2; }; }
50
+
51
+ while [ $# -gt 0 ]; do
52
+ case "$1" in
53
+ -n|--dry-run) DRY_RUN=1 ;;
54
+ -y|--yes) ASSUME_YES=1 ;;
55
+ --keep-image) need_arg "$1" "${2:-}"; add_pattern "$2"; shift ;;
56
+ --keep-image=*) add_pattern "${1#*=}" ;;
57
+ --cache-cap) need_arg "$1" "${2:-}"; CACHE_CAP="$2"; shift ;;
58
+ --cache-cap=*) CACHE_CAP="${1#*=}" ;;
59
+ --tmp-prefix) need_arg "$1" "${2:-}"; TMP_PREFIXES="$TMP_PREFIXES $2"; shift ;;
60
+ --tmp-prefix=*) TMP_PREFIXES="$TMP_PREFIXES ${1#*=}" ;;
61
+ --no-mole) MOLE=0 ;;
62
+ -h|--help) usage; exit 0 ;;
63
+ -v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
64
+ *) echo "docker-prune: unknown option '$1'" >&2; usage >&2; exit 2 ;;
65
+ esac
66
+ shift
67
+ done
68
+
69
+ aborted() { echo "Aborted, nothing removed." >&2; exit 130; }
70
+
71
+ require_docker
72
+
73
+ bold "Docker usage before"
74
+ docker system df
75
+
76
+ running_projects=$(compose_projects)
77
+ all_projects=$(compose_projects -a)
78
+ stopped_projects=$(comm -23 <(printf '%s\n' "$all_projects") <(printf '%s\n' "$running_projects"))
79
+
80
+ echo
81
+ bold "Running compose projects (always kept)"
82
+ # shellcheck disable=SC2086
83
+ printf ' %s\n' ${running_projects:-none}
84
+
85
+ kept_projects=""
86
+ if [ -n "$stopped_projects" ]; then
87
+ echo
88
+ kept_projects=$(pick_many "Stopped compose projects to KEEP" "$stopped_projects") || aborted
89
+ fi
90
+ protected_projects=$(printf '%s\n%s\n' "$running_projects" "$kept_projects" | sed '/^$/d' | sort -u)
91
+ doomed_projects=$(comm -23 <(printf '%s\n' "$stopped_projects" | sed '/^$/d') <(printf '%s\n' "$protected_projects"))
92
+
93
+ doomed_containers=""
94
+ while IFS=$'\t' read -r id name project; do
95
+ [ -z "$id" ] && continue
96
+ [ -n "$project" ] && in_list "$project" "$protected_projects" && continue
97
+ doomed_containers+="$id $name ${project:-(no compose project)}"$'\n'
98
+ done < <(docker ps -a --filter status=exited --filter status=created --filter status=dead \
99
+ --format '{{.ID}}\t{{.Names}}\t{{.Label "com.docker.compose.project"}}')
100
+
101
+ container_ids=$(printf '%s' "$doomed_containers" | cut -f1 | sed '/^$/d')
102
+ # Named volumes are only deleted when picked; fixed-name ones can hold backups.
103
+ unused_volumes=""
104
+ while IFS= read -r vol; do
105
+ [ -z "$vol" ] && continue
106
+ # Anonymous volumes are handled by `volume prune` below.
107
+ [[ "$vol" =~ ^[0-9a-f]{64}$ ]] && continue
108
+ users=$(docker ps -aq --filter volume="$vol" | grep -vxF -f <(printf '%s\n' "$container_ids"))
109
+ [ -z "$users" ] && unused_volumes+="$vol"$'\n'
110
+ done < <(docker volume ls --format '{{.Name}}')
111
+ unused_volumes=$(printf '%s' "$unused_volumes" | sed '/^$/d')
112
+
113
+ doomed_volumes=""
114
+ if [ -n "$unused_volumes" ]; then
115
+ echo
116
+ doomed_volumes=$(pick_many "Unused named volumes to DELETE (may be backups)" "$unused_volumes") || aborted
117
+ fi
118
+
119
+ echo
120
+ bold "Plan"
121
+ echo "Stopped projects to remove:"
122
+ # shellcheck disable=SC2086
123
+ printf ' %s\n' ${doomed_projects:-none}
124
+ echo "Stopped containers to remove:"
125
+ printf '%s' "${doomed_containers:- none
126
+ }" | awk -F'\t' 'NF>1 {printf " %s (%s)\n", $2, $3} NF<=1 {print}'
127
+ echo "Named volumes to remove:"
128
+ # shellcheck disable=SC2086
129
+ printf ' %s\n' ${doomed_volumes:-none}
130
+ echo "Also: anonymous unused volumes, dangling images, unused images"
131
+ echo " (except images of kept projects${KEEP_IMAGE_PATTERN:+ and $KEEP_IMAGE_PATTERN}),"
132
+ echo " build cache trimmed to $CACHE_CAP."
133
+ [ -n "${TMP_PREFIXES// /}" ] && echo " \$TMPDIR dirs older than ${TMP_MAX_AGE_MIN}m matching:$TMP_PREFIXES"
134
+
135
+ if [ "$DRY_RUN" = 0 ] && ! confirm "Proceed?"; then
136
+ echo "Nothing removed."
137
+ exit 0
138
+ fi
139
+
140
+ echo
141
+ bold "Removing"
142
+ # shellcheck disable=SC2086
143
+ [ -n "$container_ids" ] && run docker rm -v $container_ids >/dev/null
144
+ for vol in $doomed_volumes; do
145
+ run docker volume rm "$vol" >/dev/null
146
+ done
147
+ run docker volume prune -f
148
+
149
+ # Containers pending removal are excluded so a dry run lists the same images a real run removes.
150
+ used_images=$(docker ps -aq | grep -vxF -f <(printf '%s\n' "$container_ids") |
151
+ xargs docker inspect 2>/dev/null --format '{{.Image}}' | sort -u)
152
+ while IFS=$'\t' read -r id ref; do
153
+ [ -z "$id" ] && continue
154
+ in_list "$id" "$used_images" && continue
155
+ # An empty regex matches everything, so it must mean "keep nothing extra".
156
+ [ -n "$KEEP_IMAGE_PATTERN" ] && [[ "$ref" =~ $KEEP_IMAGE_PATTERN ]] && continue
157
+ [ "$ref" = "<none>:<none>" ] && continue
158
+ # Digest-pinned pulls have no tag, so only their ID addresses them.
159
+ [[ "$ref" == *:"<none>" ]] && ref="$id"
160
+ run docker image rm "$ref" >/dev/null 2>&1
161
+ done < <(docker image ls --no-trunc --format '{{.ID}}\t{{.Repository}}:{{.Tag}}')
162
+ run docker image prune -f >/dev/null
163
+
164
+ # --keep-storage was renamed to --max-used-space in newer buildx.
165
+ if docker builder prune --help 2>&1 | grep -q -- --max-used-space; then
166
+ run docker builder prune -af --max-used-space "$CACHE_CAP"
167
+ else
168
+ run docker builder prune -af --keep-storage "$CACHE_CAP"
169
+ fi
170
+
171
+ tmp_root="${TMPDIR:-/tmp}"
172
+ for prefix in $TMP_PREFIXES; do
173
+ find "$tmp_root" -maxdepth 1 -type d -name "${prefix}*" -mmin +"$TMP_MAX_AGE_MIN" -print0 2>/dev/null |
174
+ while IFS= read -r -d '' dir; do run rm -rf "$dir"; done
175
+ done
176
+
177
+ echo
178
+ bold "Docker usage after"
179
+ docker system df
180
+
181
+ if [ "$MOLE" = 1 ] && command -v mo >/dev/null 2>&1; then
182
+ echo
183
+ if [ "$DRY_RUN" = 1 ]; then
184
+ dim " would offer: mo clean (macOS caches, logs, temp)"
185
+ elif [ "$ASSUME_YES" = 0 ] && confirm "Also clean macOS caches and temp with Mole (mo clean)?"; then
186
+ mo clean
187
+ fi
188
+ fi
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # Stop running compose projects picked from a list, then optionally delete them.
3
+ set -uo pipefail
4
+
5
+ self="$0"
6
+ while [ -L "$self" ]; do
7
+ link=$(readlink "$self")
8
+ case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
9
+ done
10
+ # shellcheck source=../lib/common.sh
11
+ . "$(dirname "$self")/../lib/common.sh"
12
+
13
+ usage() {
14
+ cat <<EOF
15
+ docker-stop $DOCKER_UTIL_VERSION
16
+ Pick running compose projects to stop, then pick which of those to delete.
17
+
18
+ Usage: docker-stop [options]
19
+
20
+ Options:
21
+ -n, --dry-run Show what would happen, change nothing
22
+ -h, --help Show this help
23
+ -v, --version Show version
24
+ EOF
25
+ }
26
+
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ -n|--dry-run) DRY_RUN=1 ;;
30
+ -h|--help) usage; exit 0 ;;
31
+ -v|--version) echo "$DOCKER_UTIL_VERSION"; exit 0 ;;
32
+ *) echo "docker-stop: unknown option '$1'" >&2; usage >&2; exit 2 ;;
33
+ esac
34
+ shift
35
+ done
36
+
37
+ aborted() { echo "Aborted, nothing $1." >&2; exit 130; }
38
+ by_project() { printf 'label=com.docker.compose.project=%s' "$1"; }
39
+
40
+ require_docker
41
+
42
+ running=$(compose_projects)
43
+ if [ -z "$running" ]; then
44
+ echo "No running compose projects."
45
+ exit 0
46
+ fi
47
+
48
+ chosen=$(pick_many "Compose projects to STOP" "$running") || aborted "stopped"
49
+ if [ -z "$chosen" ]; then
50
+ echo "Nothing stopped."
51
+ exit 0
52
+ fi
53
+
54
+ bold "Stopping"
55
+ for project in $chosen; do
56
+ echo " $project"
57
+ # shellcheck disable=SC2046
58
+ run docker stop $(docker ps -q --filter "$(by_project "$project")") >/dev/null
59
+ done
60
+
61
+ echo
62
+ doomed=$(pick_many "Stopped projects to also DELETE (containers and networks)" "$chosen") || aborted "deleted"
63
+ if [ -z "$doomed" ]; then
64
+ echo "Stopped, nothing deleted."
65
+ exit 0
66
+ fi
67
+
68
+ # Volumes hold the data, so wiping them is a separate, explicit choice.
69
+ volumes=$(pick_one "Volumes of deleted projects" "Keep volumes" "Delete volumes too") || aborted "deleted"
70
+
71
+ echo
72
+ bold "Deleting"
73
+ for project in $doomed; do
74
+ echo " $project"
75
+ filter=$(by_project "$project")
76
+ ids=$(docker ps -aq --filter "$filter")
77
+ # shellcheck disable=SC2086
78
+ [ -n "$ids" ] && run docker rm -v $ids >/dev/null
79
+ nets=$(docker network ls -q --filter "$filter")
80
+ # shellcheck disable=SC2086
81
+ [ -n "$nets" ] && run docker network rm $nets >/dev/null
82
+ if [ "$volumes" = "Delete volumes too" ]; then
83
+ vols=$(docker volume ls -q --filter "$filter")
84
+ # shellcheck disable=SC2086
85
+ [ -n "$vols" ] && run docker volume rm $vols >/dev/null
86
+ fi
87
+ done
88
+ echo "Done."
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Single entry point so `npx @danolez/docker-util <cmd>` reaches every command.
3
+ set -uo pipefail
4
+
5
+ self="$0"
6
+ while [ -L "$self" ]; do
7
+ link=$(readlink "$self")
8
+ case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
9
+ done
10
+ dir="$(dirname "$self")"
11
+
12
+ cmd="${1:-help}"
13
+ [ $# -gt 0 ] && shift
14
+ case "$cmd" in
15
+ list|stop|prune|help) exec "$dir/docker-$cmd" "$@" ;;
16
+ -h|--help) exec "$dir/docker-help" ;;
17
+ -v|--version) exec "$dir/docker-help" --version ;;
18
+ *) echo "docker-util: unknown command '$cmd'" >&2; "$dir/docker-help" >&2; exit 2 ;;
19
+ esac
package/lib/common.sh ADDED
@@ -0,0 +1,128 @@
1
+ # shellcheck shell=bash
2
+ # Shared by every docker-util command so they all pick things the same way.
3
+
4
+ # shellcheck disable=SC2034
5
+ DOCKER_UTIL_VERSION="1.0.0"
6
+ NONE="(none)"
7
+ DRY_RUN="${DRY_RUN:-0}"
8
+ ASSUME_YES="${ASSUME_YES:-0}"
9
+
10
+ bold() { printf '\033[1m%s\033[0m\n' "$*"; }
11
+ dim() { printf '\033[2m%s\033[0m\n' "$*"; }
12
+ # fd 3 keeps dry-run output visible past callers that silence the real command.
13
+ exec 3>&1
14
+ run() {
15
+ if [ "$DRY_RUN" = 1 ]; then dim " would run: $*" >&3; else "$@"; fi
16
+ }
17
+
18
+ in_list() { printf '%s\n' "$2" | grep -qxF -- "$1"; }
19
+
20
+ require_docker() {
21
+ command -v docker >/dev/null 2>&1 || { echo "docker not found on PATH." >&2; exit 1; }
22
+ docker info >/dev/null 2>&1 || { echo "Docker daemon not reachable." >&2; exit 1; }
23
+ }
24
+
25
+ # Opened once so every menu in a run shares one read offset (lets tests script keystrokes).
26
+ { exec 4<"${DOCKER_UTIL_TTY:-/dev/tty}"; } 2>/dev/null || true
27
+
28
+ picker() {
29
+ local want="${DOCKER_UTIL_PICKER:-auto}"
30
+ if [ "$want" = auto ]; then
31
+ if command -v fzf >/dev/null 2>&1; then want=fzf
32
+ elif command -v gum >/dev/null 2>&1; then want=gum
33
+ else want=builtin; fi
34
+ fi
35
+ echo "$want"
36
+ }
37
+
38
+ # Arrow-key menu for machines without fzf or gum. Args: multi(0|1) prompt items...
39
+ _menu() {
40
+ local multi="$1" prompt="$2"; shift 2
41
+ local opts=("$@") n=$# cur=0 i key rest mark ptr hint drawn=0
42
+ local sel=()
43
+ [ "$n" -eq 0 ] && return 0
44
+ # Runs in a $() subshell, so this only restores the cursor for this menu.
45
+ trap 'printf "\033[?25h" >&2' EXIT
46
+ for ((i = 0; i < n; i++)); do sel[i]=0; done
47
+ hint="up/down move, enter select, q abort"
48
+ [ "$multi" = 1 ] && hint="up/down move, space mark, enter confirm, q abort"
49
+ printf '\033[?25l\033[1m%s\033[0m \033[2m%s\033[0m\n' "$prompt" "$hint" >&2
50
+ while :; do
51
+ [ "$drawn" = 1 ] && printf '\033[%dA' "$n" >&2
52
+ for ((i = 0; i < n; i++)); do
53
+ ptr=" "; [ "$i" = "$cur" ] && ptr="> "
54
+ mark=""
55
+ if [ "$multi" = 1 ]; then mark="[ ] "; [ "${sel[i]}" = 1 ] && mark="[x] "; fi
56
+ printf '\033[2K%s%s%s\n' "$ptr" "$mark" "${opts[i]}" >&2
57
+ done
58
+ drawn=1
59
+ IFS= read -rsn1 key 2>/dev/null <&4 || key=q
60
+ if [ "$key" = $'\033' ]; then
61
+ rest=""
62
+ IFS= read -rsn2 -t 1 rest 2>/dev/null <&4
63
+ case "$rest" in "[A") key=k ;; "[B") key=j ;; *) key=q ;; esac
64
+ fi
65
+ case "$key" in
66
+ k) cur=$(((cur - 1 + n) % n)) ;;
67
+ j) cur=$(((cur + 1) % n)) ;;
68
+ " ") [ "$multi" = 1 ] && sel[cur]=$((1 - sel[cur])) ;;
69
+ "") break ;;
70
+ q) printf '\033[?25h' >&2; return 130 ;;
71
+ esac
72
+ done
73
+ printf '\033[?25h' >&2
74
+ if [ "$multi" = 1 ]; then
75
+ for ((i = 0; i < n; i++)); do [ "${sel[i]}" = 1 ] && printf '%s\n' "${opts[i]}"; done
76
+ else
77
+ printf '%s\n' "${opts[cur]}"
78
+ fi
79
+ return 0
80
+ }
81
+
82
+ # Newline-separated items in, picked items out. Non-zero only on abort.
83
+ pick_many() {
84
+ local prompt="$1" items="$2" picked rc line
85
+ local arr=()
86
+ [ -z "$items" ] && return 0
87
+ [ "$ASSUME_YES" = 1 ] && return 0
88
+ case "$(picker)" in
89
+ # fzf and gum return the highlighted row on a bare ENTER, so (none) is the only way to pick nothing.
90
+ fzf)
91
+ picked=$(printf '%s\n%s\n' "$NONE" "$items" | fzf --multi --prompt="$prompt> " \
92
+ --height=40% --reverse --header="TAB to mark, ENTER to confirm, ESC to abort")
93
+ rc=$? ;;
94
+ gum)
95
+ picked=$(printf '%s\n%s\n' "$NONE" "$items" | gum choose --no-limit --header="$prompt")
96
+ rc=$? ;;
97
+ *)
98
+ while IFS= read -r line; do [ -n "$line" ] && arr+=("$line"); done <<<"$items"
99
+ [ ${#arr[@]} -eq 0 ] && return 0
100
+ picked=$(_menu 1 "$prompt" "${arr[@]}")
101
+ rc=$? ;;
102
+ esac
103
+ [ "$rc" -ne 0 ] && return 130
104
+ printf '%s\n' "$picked" | grep -vxF -- "$NONE" | sed '/^$/d'
105
+ return 0
106
+ }
107
+
108
+ # Items as arguments, one picked item out. Non-zero only on abort.
109
+ pick_one() {
110
+ local prompt="$1"; shift
111
+ case "$(picker)" in
112
+ fzf) printf '%s\n' "$@" | fzf --prompt="$prompt> " --height=40% --reverse \
113
+ --header="ENTER to select, ESC to abort" ;;
114
+ gum) gum choose --header="$prompt" "$@" ;;
115
+ *) _menu 0 "$prompt" "$@" ;;
116
+ esac
117
+ }
118
+
119
+ confirm() {
120
+ local answer
121
+ [ "$ASSUME_YES" = 1 ] && return 0
122
+ answer=$(pick_one "$1" No Yes) || return 1
123
+ [ "$answer" = Yes ]
124
+ }
125
+
126
+ compose_projects() {
127
+ docker ps "$@" --format '{{.Label "com.docker.compose.project"}}' | sed '/^$/d' | sort -u
128
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@danolez/docker-util",
3
+ "version": "1.0.0",
4
+ "description": "Interactive Docker helpers: list, stop and prune compose projects without typing names",
5
+ "bin": {
6
+ "docker-util": "bin/docker-util",
7
+ "docker-list": "bin/docker-list",
8
+ "docker-stop": "bin/docker-stop",
9
+ "docker-prune": "bin/docker-prune",
10
+ "docker-help": "bin/docker-help"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/"
15
+ ],
16
+ "scripts": {
17
+ "test": "bash test/run.sh",
18
+ "lint": "shellcheck bin/* lib/common.sh test/run.sh test/stub/docker"
19
+ },
20
+ "keywords": [
21
+ "docker",
22
+ "compose",
23
+ "prune",
24
+ "stop",
25
+ "cleanup",
26
+ "cli",
27
+ "fzf"
28
+ ],
29
+ "os": [
30
+ "darwin",
31
+ "linux"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/danolez1/docker-util.git"
36
+ },
37
+ "homepage": "https://github.com/danolez1/docker-util#readme",
38
+ "bugs": "https://github.com/danolez1/docker-util/issues",
39
+ "author": "Fatunmbi Daniel",
40
+ "license": "MIT",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }