From ea26dc794e69398a698f4e0362c8ef71d00a7d1f Mon Sep 17 00:00:00 2001 From: soulis-1256 Date: Sun, 16 Aug 2026 16:20:22 +0300 Subject: [PATCH] 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. --- README.md | 1 - bin/cli.sh | 101 +++++++++++++------------------------ bin/setup.sh | 19 ++----- docs/uninstall.md | 5 +- lib/core.sh | 66 ++++++++++++++++++++++++ lib/managed_files.sh | 15 ------ lib/upgrade.sh | 21 ++------ tests/unit/test_core.sh | 31 ++++++++++++ tests/unit/test_upgrade.sh | 6 +-- 9 files changed, 143 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 4789ce0..b88c90e 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ After install, the CLI is on your `PATH` as `assemblrr`. Run `assemblrr 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 diff --git a/bin/setup.sh b/bin/setup.sh index 55e7fd5..55ad3e9 100644 --- a/bin/setup.sh +++ b/bin/setup.sh @@ -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" } diff --git a/docs/uninstall.md b/docs/uninstall.md index 0aa6226..4b9225c 100644 --- a/docs/uninstall.md +++ b/docs/uninstall.md @@ -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 | `/.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 diff --git a/lib/core.sh b/lib/core.sh index 26702d4..d39bcae 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -398,6 +398,72 @@ probe_writable_path() { return 0 } +# PATH entry (~/.local/bin/) 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() { diff --git a/lib/managed_files.sh b/lib/managed_files.sh index 51447ea..5c8e537 100644 --- a/lib/managed_files.sh +++ b/lib/managed_files.sh @@ -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 -} diff --git a/lib/upgrade.sh b/lib/upgrade.sh index 2cd133e..d43d364 100644 --- a/lib/upgrade.sh +++ b/lib/upgrade.sh @@ -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 --- diff --git a/tests/unit/test_core.sh b/tests/unit/test_core.sh index b6b7fcc..4d10470 100755 --- a/tests/unit/test_core.sh +++ b/tests/unit/test_core.sh @@ -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 diff --git a/tests/unit/test_upgrade.sh b/tests/unit/test_upgrade.sh index e706943..f3f0774 100644 --- a/tests/unit/test_upgrade.sh +++ b/tests/unit/test_upgrade.sh @@ -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