Make PATH assemblrr a wrapper around the install CLI

Stop copying cli.sh and a lib subset to ~/.local/bin. Setup and upgrade
write a short wrapper that execs $INSTALL_DIR/cli.sh. Leftover PATH libs
are removed. update-cli now points at upgrade.
This commit is contained in:
soulis-1256 2026-08-16 16:20:22 +03:00
parent 962e322fdb
commit ea26dc794e
No known key found for this signature in database
9 changed files with 143 additions and 122 deletions

View file

@ -88,7 +88,6 @@ After install, the CLI is on your `PATH` as `assemblrr`. Run `assemblrr <command
| `backup /target/dir` | Snapshot configuration |
| `restore /backup.tar.gz` | Restore from a backup archive |
| `update-containers` | Pull latest images and restart (offers a backup first) |
| `update-cli` | Update the CLI to the latest version |
| `upgrade` | Upgrade from git `main` (or `--from DIR`): files, migrations, stack restart, service wiring |
| `upgrade --skip-stack` | Apply files and CLI without restarting containers |
| `upgrade --check` | Dry-run: show which managed files and migrations would run |

View file

@ -1,27 +1,44 @@
#!/bin/bash
set -euo pipefail
# assemblrr CLI — manages your media server
# This file is installed to ~/.local/bin/<APP_CLI_NAME> during setup
# assemblrr CLI — lives in the install tree (or repo bin/). PATH is a wrapper.
# Source shared library (safe_source, find_install_directory, logging, colors)
# CLI is installed to ~/.local/bin, lib/ lives next to it or in the install dir
_cli_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Locate lib directory (repo: lib/ sibling of bin/; installed: lib/ next to CLI)
# Leftover full copies in ~/.local/bin must not run; exec the install-tree CLI.
if [ "$_cli_self" = "$HOME/.local/bin" ] || [ "$_cli_self" = "/usr/local/bin" ]; then
_wrap_dir="${ASSEMBLRR_DIR:-}"
if [ -z "$_wrap_dir" ]; then
for _wrap_f in \
/opt/assemblrr/.assemblrr-config \
"$HOME/.assemblrr-config" \
"$HOME/assemblrr/.assemblrr-config"
do
if [ -f "$_wrap_f" ]; then
_wrap_dir=$(grep -E '^INSTALL_DIRECTORY=' "$_wrap_f" | head -1 || true)
_wrap_dir="${_wrap_dir#INSTALL_DIRECTORY=}"
_wrap_dir="${_wrap_dir%$'\r'}"
_wrap_dir="${_wrap_dir#\"}"
_wrap_dir="${_wrap_dir%\"}"
[ -n "$_wrap_dir" ] && break
fi
done
fi
if [ -n "${_wrap_dir:-}" ] && [ -f "$_wrap_dir/cli.sh" ]; then
exec bash "$_wrap_dir/cli.sh" "$@"
fi
echo "assemblrr: could not find the install. Run setup, or set ASSEMBLRR_DIR." >&2
exit 1
fi
# Locate lib (repo: sibling of bin/; install: lib/ next to cli.sh)
if [ -f "$_cli_self/../lib/core.sh" ]; then
_lib_dir="$_cli_self/../lib"
elif [ -f "$_cli_self/lib/core.sh" ]; then
_lib_dir="$_cli_self/lib"
else
# Fallback: find install dir first, then source from there
_tmp_install=$(grep -r "^INSTALL_DIRECTORY=" /opt/assemblrr/.assemblrr-config "$HOME/assemblrr/.assemblrr-config" "$HOME/.assemblrr-config" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' || true)
if [ -n "$_tmp_install" ] && [ -f "$_tmp_install/lib/core.sh" ]; then
_lib_dir="$_tmp_install/lib"
else
echo -e "\033[0;31mError: lib/core.sh not found. Re-run setup.\033[0m" >&2
exit 1
fi
echo -e "\033[0;31mError: lib/core.sh not found. Re-run setup.\033[0m" >&2
exit 1
fi
source "$_lib_dir/core.sh"
@ -92,7 +109,6 @@ declare -A COMMANDS=(
["backup"]="backs up to the destination location"
["restore"]="restores from a backup archive"
["update-containers"]="updates all containers"
["update-cli"]="updates the CLI script to the latest version"
["upgrade"]="upgrade install files from git (or --from DIR); runs migrations"
["logs"]="shows container logs (optionally specify service name)"
["health"]="checks health status of all services"
@ -129,7 +145,6 @@ show_help() {
echo " ${APP_CLI_NAME} logs # View all container logs"
echo " ${APP_CLI_NAME} logs jellyfin # View specific service logs"
echo " ${APP_CLI_NAME} health # Check health of all services"
echo " ${APP_CLI_NAME} update-cli # Update CLI to latest version"
echo " ${APP_CLI_NAME} upgrade # Upgrade from git main (backup first)"
echo " ${APP_CLI_NAME} upgrade --from DIR # Upgrade from a local source tree"
echo " ${APP_CLI_NAME} upgrade --check # Dry-run upgrade plan"
@ -309,18 +324,6 @@ backup_app() {
echo -e "\nBacking up ${APP_DISPLAY_NAME} to $destination..."
# Copy current CLI script and lib modules, create backup (skip copy if running from install dir)
local running_cli; running_cli="$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || echo "${_cli_self}/cli.sh")"
local target_cli; target_cli="$(realpath "$INSTALL_DIR/cli.sh" 2>/dev/null || echo "$INSTALL_DIR/cli.sh")"
if [ -f "$running_cli" ] && [ "$running_cli" != "$target_cli" ]; then
cp "$running_cli" "$INSTALL_DIR/cli.sh" 2>/dev/null || log_warning "Failed to backup CLI script"
fi
# Copy lib modules if they exist outside target dir
if [ -d "$HOME/.local/bin/lib" ] && [ "$(realpath "$HOME/.local/bin/lib" 2>/dev/null)" != "$(realpath "$INSTALL_DIR/lib" 2>/dev/null)" ]; then
cp -r "$HOME/.local/bin/lib" "$INSTALL_DIR/" 2>/dev/null || true
fi
# Run tar inside an alpine container bound to the INSTALL_DIR and the destination (excluding temp/cache and sockets)
if ! wait_while "Creating backup archive" run_docker run --rm \
-v "$INSTALL_DIR:/source" \
@ -482,16 +485,12 @@ uninstall_app() {
if [ -n "${APP_CLI_NAME:-}" ]; then
rm -f "$HOME/.local/bin/$APP_CLI_NAME" "/usr/local/bin/$APP_CLI_NAME" 2>/dev/null || true
fi
local _lib_module
if type list_cli_lib_modules >/dev/null 2>&1; then
while IFS= read -r _lib_module; do
[ -z "$_lib_module" ] && continue
rm -f "$HOME/.local/bin/lib/${_lib_module}.sh" 2>/dev/null || true
done < <(list_cli_lib_modules)
if type remove_stale_user_cli_libs >/dev/null 2>&1; then
remove_stale_user_cli_libs
else
rm -f "$HOME/.local/bin/lib/"*.sh 2>/dev/null || true
rmdir "$HOME/.local/bin/lib" 2>/dev/null || true
fi
rmdir "$HOME/.local/bin/lib" 2>/dev/null || true
# Setup writes this cheat-sheet under $HOME (outside the install dir)
# Legacy setup wrote ~/assemblrr_services.txt — remove if present
@ -944,38 +943,6 @@ check_health() {
return 0
}
update_cli() {
echo "Updating ${APP_DISPLAY_NAME} CLI..."
local tmp_dir
tmp_dir=$(mktemp -d)
if ! wait_while "Updating CLI from git" git clone --depth=1 "${APP_REPO_URL}" "$tmp_dir/assemblrr"; then
log_error "Failed to clone ${APP_REPO_URL}. Check your internet connection and repository access."
fi
mkdir -p "$HOME/.local/bin/lib"
if [ -f "$tmp_dir/assemblrr/lib/managed_files.sh" ]; then
# shellcheck source=/dev/null
source "$tmp_dir/assemblrr/lib/managed_files.sh"
fi
local _m
while IFS= read -r _m; do
[ -z "$_m" ] && continue
if [ -f "$tmp_dir/assemblrr/lib/${_m}.sh" ]; then
cp "$tmp_dir/assemblrr/lib/${_m}.sh" "$HOME/.local/bin/lib/${_m}.sh"
fi
done < <(list_cli_lib_modules)
cp "$tmp_dir/assemblrr/bin/cli.sh" "$HOME/.local/bin/$APP_CLI_NAME" && chmod +x "$HOME/.local/bin/$APP_CLI_NAME"
# Remove old system-wide install if it exists
[ -n "${APP_CLI_NAME:-}" ] && rm -f "/usr/local/bin/$APP_CLI_NAME" 2>/dev/null
if type ensure_local_bin_on_path >/dev/null 2>&1; then
ensure_local_bin_on_path
fi
log_success "CLI updated successfully!"
[ -n "$tmp_dir" ] && rm -rf "$tmp_dir"
}
main() {
local command=${1:-"--help"}
local destination=${2:-.}
@ -1020,7 +987,7 @@ main() {
update_containers "${@:2}"
;;
update-cli)
update_cli
log_info "The CLI is ${INSTALL_DIR}/cli.sh. Update with: ${APP_CLI_NAME} upgrade"
;;
upgrade)
if ! type upgrade_app >/dev/null 2>&1; then

View file

@ -569,23 +569,12 @@ EOF
# Usage: build_compose_args "$install_directory" "${setup_vpn,,}"
install_cli() {
# Copy from install directory (persistent), not SCRIPT_DIR (may be /tmp)
local cli_source="$install_directory/cli.sh"
if [ ! -f "$cli_source" ]; then
log_warning "CLI source missing at $cli_source — skip install"
if [ ! -f "$install_directory/cli.sh" ]; then
log_warning "CLI source missing at $install_directory/cli.sh — skip install"
return 1
fi
mkdir -p "$HOME/.local/bin/lib"
local _lib_module
while IFS= read -r _lib_module; do
[ -z "$_lib_module" ] && continue
if [ -f "$install_directory/lib/${_lib_module}.sh" ]; then
cp "$install_directory/lib/${_lib_module}.sh" "$HOME/.local/bin/lib/${_lib_module}.sh"
fi
done < <(list_cli_lib_modules)
cp "$cli_source" "$HOME/.local/bin/$APP_CLI_NAME" && chmod +x "$HOME/.local/bin/$APP_CLI_NAME"
ensure_local_bin_on_path
write_install_pointer "$install_directory" "${media_directory:-}"
install_user_cli_wrapper
log_debug "CLI installed to $HOME/.local/bin/$APP_CLI_NAME"
}

View file

@ -17,8 +17,7 @@ root-owned; plain `rm -rf` then fails — use the Docker removal step in Option
| Docker network | `assemblrr_network` |
| Install directory (config, secrets, compose files) | `~/assemblrr` (or the path you chose) |
| Media directory (your movies/TV/downloads) | `~/assemblrr-media` (or the path you chose) |
| CLI command | `~/.local/bin/assemblrr` |
| CLI library modules | `~/.local/bin/lib/*.sh` |
| CLI command | `~/.local/bin/assemblrr` (wrapper → install `cli.sh`) |
| Runtime config | `<install>/.assemblrr-config` (written when the install tree is first bootstrapped — early in setup, not only at the end) |
| Discovery pointer | `~/.assemblrr-config` (points at `INSTALL_DIRECTORY` so the CLI can find non-default paths) |
| Logs | `/tmp/assemblrr-*.log` |
@ -133,7 +132,7 @@ rm -rf ~/assemblrr-media # WARNING: this is your movies/TV/downloads
```bash
rm -f ~/.local/bin/assemblrr
rm -f ~/.local/bin/lib/*.sh
rm -f ~/.local/bin/lib/*.sh # leftover from older installs
rmdir ~/.local/bin/lib 2>/dev/null # only removed if now empty
rm -f ~/assemblrr_services.txt 2>/dev/null # legacy cheat-sheet
rm -f ~/.assemblrr-config 2>/dev/null # discovery pointer

View file

@ -398,6 +398,72 @@ probe_writable_path() {
return 0
}
# PATH entry (~/.local/bin/<cli>) is a wrapper that execs $INSTALL_DIR/cli.sh.
# Older installs copied cli.sh plus a lib/ subset there; those leftovers are removed.
remove_stale_user_cli_libs() {
local lib="$HOME/.local/bin/lib"
local m
[ -d "$lib" ] || return 0
for m in core branding compose vpn managed_files upgrade services ui config_edit; do
rm -f "$lib/${m}.sh"
done
rmdir "$lib" 2>/dev/null || true
}
write_user_cli_wrapper() {
local dest="$1"
mkdir -p "$(dirname "$dest")"
cat >"$dest" <<'EOF'
#!/bin/bash
set -euo pipefail
# PATH wrapper — runs the install-tree CLI.
_read_install_dir() {
local f="$1" line
[ -f "$f" ] || return 1
line=$(grep -E '^INSTALL_DIRECTORY=' "$f" | head -1) || return 1
line="${line#INSTALL_DIRECTORY=}"
line="${line%$'\r'}"
line="${line#\"}"
line="${line%\"}"
line="${line#\'}"
line="${line%\'}"
[ -n "$line" ] || return 1
printf '%s\n' "$line"
}
dir="${ASSEMBLRR_DIR:-}"
if [ -z "$dir" ]; then
for f in \
/opt/assemblrr/.assemblrr-config \
"${HOME}/.assemblrr-config" \
"${HOME}/assemblrr/.assemblrr-config"
do
if dir=$(_read_install_dir "$f"); then
break
fi
dir=""
done
fi
if [ -z "$dir" ] || [ ! -f "$dir/cli.sh" ]; then
echo "assemblrr: could not find the install. Run setup, or set ASSEMBLRR_DIR." >&2
exit 1
fi
exec bash "$dir/cli.sh" "$@"
EOF
chmod +x "$dest"
}
install_user_cli_wrapper() {
local dest="$HOME/.local/bin/${APP_CLI_NAME:-assemblrr}"
write_user_cli_wrapper "$dest"
remove_stale_user_cli_libs
ensure_local_bin_on_path
}
# Persist ~/.local/bin on PATH for bash, zsh, and fish.
# Does not create ~/.bash_profile (that would hide ~/.profile on login bash).
_append_path_line() {

View file

@ -68,18 +68,3 @@ EOF
done < <(find "$UPGRADE_SOURCE_ROOT/migrations" -type f -print0 2>/dev/null | sort -z)
fi
}
# Lib modules the user-facing CLI copies next to ~/.local/bin/assemblrr.
list_cli_lib_modules() {
cat <<'EOF'
core
branding
compose
vpn
managed_files
upgrade
services
ui
config_edit
EOF
}

View file

@ -235,25 +235,10 @@ write_upgrade_version() {
refresh_user_cli() {
local install_dir="$1"
mkdir -p "$HOME/.local/bin/lib"
local m
if ! type list_cli_lib_modules >/dev/null 2>&1; then
# shellcheck source=/dev/null
[ -f "$install_dir/lib/managed_files.sh" ] && source "$install_dir/lib/managed_files.sh"
fi
while IFS= read -r m; do
[ -z "$m" ] && continue
if [ -f "$install_dir/lib/${m}.sh" ]; then
cp "$install_dir/lib/${m}.sh" "$HOME/.local/bin/lib/${m}.sh"
fi
done < <(list_cli_lib_modules)
if [ -f "$install_dir/cli.sh" ]; then
cp "$install_dir/cli.sh" "$HOME/.local/bin/${APP_CLI_NAME:-assemblrr}"
chmod +x "$HOME/.local/bin/${APP_CLI_NAME:-assemblrr}"
fi
if type ensure_local_bin_on_path >/dev/null 2>&1; then
ensure_local_bin_on_path
if [ -n "$install_dir" ] && [ ! -f "$HOME/.assemblrr-config" ]; then
write_install_pointer "$install_dir" "${MEDIA_DIRECTORY:-}"
fi
install_user_cli_wrapper
}
# --- Validate compose after apply ---

View file

@ -176,4 +176,35 @@ HOME="$path_home" ensure_local_bin_on_path
assert_contains "$(cat "$path_home/.bash_profile")" '.local/bin' "appends existing .bash_profile"
assert_contains "$(cat "$path_home/.config/fish/config.fish")" '.local/bin' "writes fish config"
test_suite "PATH CLI wrapper"
wrap_home="$tmp/wrap-home"
wrap_inst="$tmp/wrap-install"
mkdir -p "$wrap_home/.local/bin/lib" "$wrap_inst"
printf '%s\n' '#!/bin/bash' 'echo real-cli' >"$wrap_inst/cli.sh"
chmod +x "$wrap_inst/cli.sh"
echo stale >"$wrap_home/.local/bin/lib/core.sh"
echo leftover >"$wrap_home/.local/bin/lib/config_edit.sh"
HOME="$wrap_home" APP_CLI_NAME=assemblrr install_user_cli_wrapper
assert_true "wrapper is executable" "[ -x \"$wrap_home/.local/bin/assemblrr\" ]"
assert_false "stale core.sh removed" "[ -f \"$wrap_home/.local/bin/lib/core.sh\" ]"
assert_false "stale config_edit.sh removed" "[ -f \"$wrap_home/.local/bin/lib/config_edit.sh\" ]"
assert_false "empty lib dir removed" "[ -d \"$wrap_home/.local/bin/lib\" ]"
assert_not_contains "$(cat "$wrap_home/.local/bin/assemblrr")" "find_install_directory" "wrapper is not the full CLI"
out=$(HOME="$wrap_home" ASSEMBLRR_DIR="$wrap_inst" "$wrap_home/.local/bin/assemblrr")
assert_eq "real-cli" "$out" "ASSEMBLRR_DIR runs install cli.sh"
printf 'INSTALL_DIRECTORY="%s"\r\n' "$wrap_inst" >"$wrap_home/.assemblrr-config"
out=$(HOME="$wrap_home" env -u ASSEMBLRR_DIR "$wrap_home/.local/bin/assemblrr")
assert_eq "real-cli" "$out" "CRLF pointer runs install cli.sh"
assert_failure "missing install fails" \
env HOME="$wrap_home" ASSEMBLRR_DIR="$tmp/no-such-install" "$wrap_home/.local/bin/assemblrr"
other="$tmp/wrap-other"
mkdir -p "$other"
printf '%s\n' '#!/bin/bash' 'echo other-cli' >"$other/cli.sh"
chmod +x "$other/cli.sh"
out=$(HOME="$wrap_home" ASSEMBLRR_DIR="$other" "$wrap_home/.local/bin/assemblrr")
assert_eq "other-cli" "$out" "ASSEMBLRR_DIR wins over pointer"
test_summary

View file

@ -111,8 +111,8 @@ assert_true "upgrade up -d --build --remove-orphans" "grep -q 'up -d --build --r
assert_true "upgrade wires with ASSEMBLRR_NONINTERACTIVE" "grep -q 'ASSEMBLRR_NONINTERACTIVE=1' \"$REPO_ROOT/lib/upgrade.sh\""
assert_true "upgrade accepts --skip-stack" "grep -q -- '--skip-stack' \"$REPO_ROOT/lib/upgrade.sh\""
assert_true "upgrade accepts --skip-wire" "grep -q -- '--skip-wire' \"$REPO_ROOT/lib/upgrade.sh\""
mods=$(list_cli_lib_modules)
assert_contains "$mods" "config_edit" "CLI modules include config_edit"
assert_contains "$mods" "ui" "CLI modules include ui"
refresh=$(sed -n '/^refresh_user_cli()/,/^}/p' "$REPO_ROOT/lib/upgrade.sh")
assert_contains "$refresh" "install_user_cli_wrapper" "refresh writes PATH wrapper"
assert_not_contains "$refresh" "list_cli_lib_modules" "refresh does not copy PATH libs"
test_summary