diff --git a/scripts/easy-docker/README.md b/scripts/easy-docker/README.md index d1ad7408..f6f0c4d1 100644 --- a/scripts/easy-docker/README.md +++ b/scripts/easy-docker/README.md @@ -10,15 +10,18 @@ bash easy-docker.sh ## Dependencies -- `gum` is used for the TUI -- The script checks required dependencies on startup -- Missing dependencies are installed automatically when possible +- `gum` is used for the TUI and is installed automatically when possible +- `docker` CLI is required and checked on startup +- `docker compose` (Compose v2 command) is required and checked on startup +- Docker Desktop includes Compose v2 by default; on Linux Engine-only setups you may need the `docker-compose-plugin` package +- Docker daemon must be running before the TUI starts +- Required docker commands are validated (`docker ps/exec/inspect/cp` and `docker compose config/up/down/logs/exec/pull/ps`) - If package manager installation for `gum` fails, the script can use a GitHub binary fallback ## Options - `-h`, `--help` - Shows usage and exits without starting the TUI -- `--no-github-binary-fallback` +- `--no-installation-fallback` - Disables GitHub binary fallback for `gum` - If package manager installation fails, the script exits with manual installation guidance diff --git a/scripts/easy-docker/lib/app/options.sh b/scripts/easy-docker/lib/app/options.sh new file mode 100755 index 00000000..7b010ff7 --- /dev/null +++ b/scripts/easy-docker/lib/app/options.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +print_usage() { + cat <<'USAGE' +Usage: bash easy-docker.sh [options] + +Options: + --no-installation-fallback Disable installation fallback prompt + -h, --help Show this help +USAGE +} + +parse_cli_options() { + local result_var="${1}" + local disable_installation_fallback=0 + shift + + while [ "$#" -gt 0 ]; do + case "$1" in + --no-installation-fallback) + disable_installation_fallback=1 + ;; + -h | --help) + print_usage + return 2 + ;; + *) + echo "Unknown option: $1" + print_usage + return 1 + ;; + esac + shift + done + + printf -v "${result_var}" "%s" "${disable_installation_fallback}" + return 0 +} diff --git a/scripts/easy-docker/lib/app/run.sh b/scripts/easy-docker/lib/app/run.sh new file mode 100755 index 00000000..874ae91c --- /dev/null +++ b/scripts/easy-docker/lib/app/run.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +run_easy_docker_app() { + local action="" + local local_env_action="" + + enter_alt_screen + render_main_screen 1 + + while true; do + local_env_action="" + action="$(show_main_menu || true)" + + if [ -z "${action}" ]; then + return 0 + fi + + case "${action}" in + "Environment check") + local_env_action="$(show_environment_status || true)" + case "${local_env_action}" in + "Back to main menu" | "") + render_main_screen 1 + ;; + "Exit and close easy-docker") + return 0 + ;; + *) + show_warning_message "Unknown environment action: ${local_env_action}" + sleep 1 + ;; + esac + ;; + "Exit") + return 0 + ;; + *) + show_warning_message "Unknown action: ${action}" + sleep 1 + ;; + esac + done +} diff --git a/scripts/easy-docker/lib/app/screen.sh b/scripts/easy-docker/lib/app/screen.sh new file mode 100755 index 00000000..ce293994 --- /dev/null +++ b/scripts/easy-docker/lib/app/screen.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +ALT_SCREEN_ACTIVE=0 + +enter_alt_screen() { + if [ -t 1 ] && command_exists tput; then + tput smcup || true + tput civis || true + ALT_SCREEN_ACTIVE=1 + fi +} + +leave_alt_screen() { + if [ "${ALT_SCREEN_ACTIVE}" = "1" ] && command_exists tput; then + tput cnorm || true + tput rmcup || true + ALT_SCREEN_ACTIVE=0 + fi +} diff --git a/scripts/easy-docker/lib/checks/docker.sh b/scripts/easy-docker/lib/checks/docker.sh new file mode 100755 index 00000000..1b703f7f --- /dev/null +++ b/scripts/easy-docker/lib/checks/docker.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +docker_compose_available() { + docker compose version >/dev/null 2>&1 +} + +docker_daemon_running() { + docker info >/dev/null 2>&1 +} + +docker_supports_command() { + docker "$@" --help >/dev/null 2>&1 +} + +get_missing_docker_commands() { + local missing=() + local subcommand="" + + for subcommand in ps exec inspect cp; do + if ! docker_supports_command "${subcommand}"; then + missing+=("docker ${subcommand}") + fi + done + + for subcommand in config up down logs exec pull ps; do + if ! docker_supports_command compose "${subcommand}"; then + missing+=("docker compose ${subcommand}") + fi + done + + if [ "${#missing[@]}" -eq 0 ]; then + return 0 + fi + + printf '%s\n' "${missing[@]}" + return 1 +} + +format_missing_commands_list() { + local missing_commands="${1}" + local missing_list="" + + missing_list="$(printf '%s' "${missing_commands}" | tr '\n' ',')" + missing_list="${missing_list%,}" + missing_list="${missing_list#,}" + printf '%s\n' "${missing_list}" +} + +ensure_docker() { + local missing_commands="" + local missing_list="" + + if ! command_exists docker; then + echo "docker is not installed." + print_docker_install_guidance + return 1 + fi + + if ! docker_compose_available; then + echo "docker compose (Compose v2 command) is not available." + print_docker_compose_install_guidance + return 1 + fi + + if ! docker_daemon_running; then + echo "docker daemon is not running." + print_docker_daemon_start_guidance + return 1 + fi + + if ! missing_commands="$(get_missing_docker_commands)"; then + missing_list="$(format_missing_commands_list "${missing_commands}")" + echo "Missing required docker commands: ${missing_list}" + print_docker_command_support_guidance + return 1 + fi + + return 0 +} diff --git a/scripts/easy-docker/lib/core/commands.sh b/scripts/easy-docker/lib/core/commands.sh new file mode 100755 index 00000000..6933bf03 --- /dev/null +++ b/scripts/easy-docker/lib/core/commands.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +command_exists() { + command -v "${1}" >/dev/null 2>&1 || command -v "${1}.exe" >/dev/null 2>&1 +} + +run_with_privileges() { + if command_exists sudo; then + sudo "$@" + return + fi + + "$@" +} + +copy_binary() { + local source_path="${1}" + local target_path="${2}" + + if command_exists install; then + install -m 0755 "${source_path}" "${target_path}" + return $? + fi + + cp "${source_path}" "${target_path}" && chmod +x "${target_path}" +} + +copy_binary_with_privileges() { + local source_path="${1}" + local target_path="${2}" + + if command_exists install; then + run_with_privileges install -m 0755 "${source_path}" "${target_path}" + return $? + fi + + run_with_privileges cp "${source_path}" "${target_path}" && + run_with_privileges chmod +x "${target_path}" +} diff --git a/scripts/easy-docker/lib/core/messages.sh b/scripts/easy-docker/lib/core/messages.sh new file mode 100755 index 00000000..a2fd3fde --- /dev/null +++ b/scripts/easy-docker/lib/core/messages.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +print_manual_gum_install_guidance() { + echo "Install gum manually: https://github.com/charmbracelet/gum#installation" + echo "If installed into ~/.local/bin, add it to PATH first." +} + +print_docker_install_guidance() { + echo "Install Docker first: https://docs.docker.com/get-started/get-docker/" +} + +print_docker_compose_install_guidance() { + echo "This script requires Docker Compose v2 via the 'docker compose' command." + echo "Docker Desktop includes it by default." + echo "On Linux Engine-only setups, install the Docker Compose CLI plugin package (commonly 'docker-compose-plugin')." + echo "Setup docs:" + echo "https://docs.docker.com/compose/install/" + echo "Note: this script uses 'docker compose' (Compose v2), not the old standalone 'docker-compose'." +} + +print_docker_daemon_start_guidance() { + echo "Start the Docker daemon/service and retry." + echo "If you use Docker Desktop, ensure it is running." +} + +print_docker_command_support_guidance() { + echo "Update Docker to a recent version and ensure Compose v2 is available as 'docker compose'." + echo "Standard 'docker' and 'docker compose' commands are required." +} diff --git a/scripts/easy-docker/lib/env.sh b/scripts/easy-docker/lib/env.sh deleted file mode 100755 index d6c7c70c..00000000 --- a/scripts/easy-docker/lib/env.sh +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env bash - -command_exists() { - command -v "${1}" >/dev/null 2>&1 || command -v "${1}.exe" >/dev/null 2>&1 -} - -run_with_privileges() { - if command_exists sudo; then - sudo "$@" - return - fi - - "$@" -} - -copy_binary() { - local source_path="${1}" - local target_path="${2}" - - if command_exists install; then - install -m 0755 "${source_path}" "${target_path}" - return $? - fi - - cp "${source_path}" "${target_path}" && chmod +x "${target_path}" -} - -copy_binary_with_privileges() { - local source_path="${1}" - local target_path="${2}" - - if command_exists install; then - run_with_privileges install -m 0755 "${source_path}" "${target_path}" - return $? - fi - - run_with_privileges cp "${source_path}" "${target_path}" && - run_with_privileges chmod +x "${target_path}" -} - -detect_gum_platform() { - local raw_os="" - local raw_arch="" - - raw_os="$(uname -s 2>/dev/null || echo unknown)" - raw_arch="$(uname -m 2>/dev/null || echo unknown)" - - case "${raw_os}" in - Linux*) - GUM_OS="Linux" - ;; - Darwin*) - GUM_OS="Darwin" - ;; - MINGW* | MSYS* | CYGWIN* | Windows_NT) - GUM_OS="Windows" - ;; - *) - return 1 - ;; - esac - - case "${raw_arch}" in - x86_64 | amd64) - GUM_ARCH="x86_64" - ;; - aarch64 | arm64) - GUM_ARCH="arm64" - ;; - armv7l | armv7) - GUM_ARCH="armv7" - ;; - *) - return 1 - ;; - esac - - return 0 -} - -get_os_aliases() { - local os_name="${1}" - local os_lower="" - - os_lower="$(printf '%s' "${os_name}" | tr '[:upper:]' '[:lower:]')" - - if [ "${os_lower}" = "${os_name}" ]; then - printf '%s\n' "${os_name}" - return - fi - - printf '%s\n%s\n' "${os_name}" "${os_lower}" -} - -get_arch_aliases() { - case "${1}" in - x86_64) - printf '%s\n%s\n' "x86_64" "amd64" - ;; - arm64) - printf '%s\n%s\n' "arm64" "aarch64" - ;; - armv7) - printf '%s\n%s\n' "armv7" "armv7l" - ;; - *) - printf '%s\n' "${1}" - ;; - esac -} - -get_gum_asset_candidates() { - local release_version="${1}" - local os_alias="" - local arch_alias="" - local ext="" - - while IFS= read -r os_alias; do - while IFS= read -r arch_alias; do - for ext in tar.gz zip; do - printf 'gum_%s_%s_%s.%s\n' "${release_version}" "${os_alias}" "${arch_alias}" "${ext}" - done - done < <(get_arch_aliases "${GUM_ARCH}") - done < <(get_os_aliases "${GUM_OS}") -} - -extract_gum_asset() { - local asset_path="${1}" - local extract_dir="${2}" - - mkdir -p "${extract_dir}" - - case "${asset_path}" in - *.tar.gz) - if ! command_exists tar; then - echo "tar is required to extract gum tar.gz assets." - return 1 - fi - tar -xzf "${asset_path}" -C "${extract_dir}" - ;; - *.zip) - if ! command_exists unzip; then - echo "unzip is required to extract gum zip assets." - return 1 - fi - unzip -q "${asset_path}" -d "${extract_dir}" - ;; - *) - return 1 - ;; - esac -} - -find_gum_binary() { - local search_dir="${1}" - local found_path="" - - found_path="$( - find "${search_dir}" -type f \( -name "gum" -o -name "gum.exe" \) 2>/dev/null | - head -n 1 - )" - - if [ -n "${found_path}" ]; then - printf '%s\n' "${found_path}" - return 0 - fi - - return 1 -} - -install_gum_from_github_release() { - local release_version="" - local asset_name="" - local downloaded_asset_path="" - local download_url="" - local tmp_dir="" - local extract_dir="" - local target_dir="" - local gum_binary_path="" - local target_binary_name="gum" - - if ! command_exists curl; then - return 1 - fi - - if ! detect_gum_platform; then - echo "Unsupported platform for automatic GitHub fallback." - return 1 - fi - - release_version="$( - curl -fsSL "https://api.github.com/repos/charmbracelet/gum/releases/latest" | - sed -n 's/.*"tag_name":[[:space:]]*"v\([^"]*\)".*/\1/p' | - head -n 1 - )" - - if [ -z "${release_version}" ]; then - return 1 - fi - - tmp_dir="$(mktemp -d)" - extract_dir="${tmp_dir}/extract" - - while IFS= read -r asset_name; do - download_url="https://github.com/charmbracelet/gum/releases/download/v${release_version}/${asset_name}" - if curl -fsSL "${download_url}" -o "${tmp_dir}/${asset_name}"; then - downloaded_asset_path="${tmp_dir}/${asset_name}" - break - fi - done < <(get_gum_asset_candidates "${release_version}") - - if [ -z "${downloaded_asset_path}" ]; then - rm -rf "${tmp_dir}" - return 1 - fi - - if ! extract_gum_asset "${downloaded_asset_path}" "${extract_dir}"; then - rm -rf "${tmp_dir}" - return 1 - fi - - gum_binary_path="$(find_gum_binary "${extract_dir}" || true)" - - if [ -z "${gum_binary_path}" ]; then - rm -rf "${tmp_dir}" - return 1 - fi - - if [[ "${gum_binary_path}" == *.exe ]]; then - target_binary_name="gum.exe" - fi - - if [ "${GUM_OS}" != "Windows" ] && [ -w "/usr/local/bin" ]; then - target_dir="/usr/local/bin" - if copy_binary "${gum_binary_path}" "${target_dir}/${target_binary_name}"; then - rm -rf "${tmp_dir}" - return 0 - fi - fi - - if [ "${GUM_OS}" != "Windows" ] && command_exists sudo; then - if copy_binary_with_privileges "${gum_binary_path}" "/usr/local/bin/${target_binary_name}"; then - rm -rf "${tmp_dir}" - return 0 - fi - fi - - target_dir="${HOME}/.local/bin" - mkdir -p "${target_dir}" - if copy_binary "${gum_binary_path}" "${target_dir}/${target_binary_name}"; then - rm -rf "${tmp_dir}" - return 0 - fi - - rm -rf "${tmp_dir}" - return 1 -} - -install_gum_with_package_manager() { - local pm_attempted=0 - - if command_exists brew; then - pm_attempted=1 - if brew install gum; then - return 0 - fi - fi - - if command_exists apt-get; then - pm_attempted=1 - if run_with_privileges apt-get update && run_with_privileges apt-get install -y gum; then - return 0 - fi - fi - - if command_exists dnf; then - pm_attempted=1 - if run_with_privileges dnf install -y gum; then - return 0 - fi - fi - - if command_exists pacman; then - pm_attempted=1 - if run_with_privileges pacman -Sy --noconfirm gum; then - return 0 - fi - fi - - if command_exists zypper; then - pm_attempted=1 - if run_with_privileges zypper --non-interactive install gum; then - return 0 - fi - fi - - if command_exists winget; then - pm_attempted=1 - if winget install --id Charmbracelet.Gum -e --accept-source-agreements --accept-package-agreements; then - return 0 - fi - fi - - if command_exists choco; then - pm_attempted=1 - if choco install gum -y; then - return 0 - fi - fi - - if [ "${pm_attempted}" -eq 0 ]; then - echo "No supported package manager was found." - else - echo "Package manager installation did not succeed." - fi - - return 1 -} - -should_use_github_fallback() { - local answer="" - - if [ ! -t 0 ]; then - echo "GitHub fallback prompt requires an interactive terminal." - return 1 - fi - - printf "Use GitHub binary fallback for gum? [y/N]: " - read -r answer - - case "${answer}" in - y | Y | yes | YES) - return 0 - ;; - *) - return 1 - ;; - esac -} - -ensure_gum() { - local disable_github_binary_fallback="${1:-0}" - - if command_exists gum; then - return 0 - fi - - echo "gum is not installed. Trying package manager installation..." - - if install_gum_with_package_manager; then - hash -r - fi - - if command_exists gum; then - echo "gum was installed successfully." - return 0 - fi - - if [ "${disable_github_binary_fallback}" = "1" ]; then - echo "GitHub binary fallback is disabled." - echo "Install gum manually: https://github.com/charmbracelet/gum#installation" - echo "If installed into ~/.local/bin, add it to PATH first." - exit 1 - fi - - if should_use_github_fallback; then - echo "Trying GitHub release fallback..." - if install_gum_from_github_release; then - hash -r - fi - else - echo "GitHub fallback was not selected." - echo "Install gum manually: https://github.com/charmbracelet/gum#installation" - echo "If installed into ~/.local/bin, add it to PATH first." - exit 1 - fi - - if command_exists gum; then - echo "gum was installed successfully." - return 0 - fi - - echo "Automatic installation failed." - echo "Install gum manually: https://github.com/charmbracelet/gum#installation" - echo "If installed into ~/.local/bin, add it to PATH first." - exit 1 -} diff --git a/scripts/easy-docker/lib/install/gum/assets.sh b/scripts/easy-docker/lib/install/gum/assets.sh new file mode 100755 index 00000000..8cfd9f00 --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/assets.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +get_gum_asset_candidates() { + local release_version="${1}" + local gum_os="${2}" + local gum_arch="${3}" + local os_alias="" + local arch_alias="" + local ext="" + + while IFS= read -r os_alias; do + while IFS= read -r arch_alias; do + for ext in tar.gz zip; do + printf 'gum_%s_%s_%s.%s\n' "${release_version}" "${os_alias}" "${arch_alias}" "${ext}" + done + done < <(get_arch_aliases "${gum_arch}") + done < <(get_os_aliases "${gum_os}") +} + +extract_gum_asset() { + local asset_path="${1}" + local extract_dir="${2}" + + mkdir -p "${extract_dir}" + + case "${asset_path}" in + *.tar.gz) + if ! command_exists tar; then + echo "tar is required to extract gum tar.gz assets." + return 1 + fi + tar -xzf "${asset_path}" -C "${extract_dir}" + ;; + *.zip) + if ! command_exists unzip; then + echo "unzip is required to extract gum zip assets." + return 1 + fi + unzip -q "${asset_path}" -d "${extract_dir}" + ;; + *) + return 1 + ;; + esac +} + +find_gum_binary() { + local search_dir="${1}" + local found_path="" + + found_path="$( + find "${search_dir}" -type f \( -name "gum" -o -name "gum.exe" \) 2>/dev/null | + head -n 1 + )" + + if [ -n "${found_path}" ]; then + printf '%s\n' "${found_path}" + return 0 + fi + + return 1 +} + +fetch_latest_gum_release_version() { + local api_payload="" + local tag_name="" + + api_payload="$(curl -fsSL "https://api.github.com/repos/charmbracelet/gum/releases/latest")" || return 1 + + if command_exists jq; then + tag_name="$(printf '%s' "${api_payload}" | jq -r '.tag_name // empty')" + else + tag_name="$(printf '%s' "${api_payload}" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" + fi + + tag_name="${tag_name#v}" + if [ -z "${tag_name}" ]; then + return 1 + fi + + printf '%s\n' "${tag_name}" +} diff --git a/scripts/easy-docker/lib/install/gum/ensure.sh b/scripts/easy-docker/lib/install/gum/ensure.sh new file mode 100755 index 00000000..b7587a26 --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/ensure.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +should_use_github_fallback() { + local answer="" + + if [ ! -t 0 ]; then + echo "GitHub fallback prompt requires an interactive terminal." + return 1 + fi + + printf "Use GitHub binary fallback for gum? [y/N]: " + read -r answer + + case "${answer}" in + y | Y | yes | YES) + return 0 + ;; + *) + return 1 + ;; + esac +} + +ensure_gum() { + local disable_installation_fallback="${1:-0}" + + if command_exists gum; then + return 0 + fi + + echo "gum is not installed. Trying package manager installation..." + + if install_gum_with_package_manager; then + hash -r + fi + + if command_exists gum; then + echo "gum was installed successfully." + return 0 + fi + + if [ "${disable_installation_fallback}" = "1" ]; then + echo "Installation fallback is disabled." + print_manual_gum_install_guidance + return 1 + fi + + if should_use_github_fallback; then + echo "Trying GitHub release fallback..." + if install_gum_from_github_release; then + hash -r + fi + else + echo "GitHub fallback was not selected." + print_manual_gum_install_guidance + return 1 + fi + + if command_exists gum; then + echo "gum was installed successfully." + return 0 + fi + + echo "Automatic installation failed." + print_manual_gum_install_guidance + return 1 +} diff --git a/scripts/easy-docker/lib/install/gum/github_release.sh b/scripts/easy-docker/lib/install/gum/github_release.sh new file mode 100755 index 00000000..d157afd0 --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/github_release.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +cleanup_gum_tmp_dir() { + local tmp_dir="${1:-}" + + if [ -n "${tmp_dir}" ] && [ -d "${tmp_dir}" ]; then + rm -rf "${tmp_dir}" + fi +} + +install_gum_from_github_release() { + local release_version="" + local asset_name="" + local asset_path="" + local download_url="" + local tmp_dir="" + local extract_dir="" + local target_dir="" + local gum_binary_path="" + local target_binary_name="gum" + local gum_os="" + local gum_arch="" + + if ! command_exists curl; then + echo "curl is required for the GitHub fallback." + return 1 + fi + + if ! read -r gum_os gum_arch < <(detect_gum_platform); then + echo "Unsupported platform for automatic GitHub fallback." + return 1 + fi + + release_version="$(fetch_latest_gum_release_version || true)" + if [ -z "${release_version}" ]; then + echo "Could not determine latest gum release version." + return 1 + fi + + tmp_dir="$(mktemp -d 2>/dev/null || true)" + if [ -z "${tmp_dir}" ] || [ ! -d "${tmp_dir}" ]; then + echo "Failed to create temporary directory for gum installation." + return 1 + fi + extract_dir="${tmp_dir}/extract" + + while IFS= read -r asset_name; do + asset_path="${tmp_dir}/${asset_name}" + download_url="https://github.com/charmbracelet/gum/releases/download/v${release_version}/${asset_name}" + + if ! curl -fsSL "${download_url}" -o "${asset_path}"; then + continue + fi + + rm -rf "${extract_dir}" + mkdir -p "${extract_dir}" + + if ! extract_gum_asset "${asset_path}" "${extract_dir}"; then + continue + fi + + gum_binary_path="$(find_gum_binary "${extract_dir}" || true)" + if [ -n "${gum_binary_path}" ]; then + break + fi + done < <(get_gum_asset_candidates "${release_version}" "${gum_os}" "${gum_arch}") + + if [ -z "${gum_binary_path}" ]; then + cleanup_gum_tmp_dir "${tmp_dir}" + echo "No compatible gum binary was found in GitHub release assets." + return 1 + fi + + if [[ "${gum_binary_path}" == *.exe ]]; then + target_binary_name="gum.exe" + fi + + if [ "${gum_os}" != "Windows" ] && [ -w "/usr/local/bin" ]; then + target_dir="/usr/local/bin" + if copy_binary "${gum_binary_path}" "${target_dir}/${target_binary_name}"; then + cleanup_gum_tmp_dir "${tmp_dir}" + return 0 + fi + fi + + if [ "${gum_os}" != "Windows" ] && command_exists sudo; then + if copy_binary_with_privileges "${gum_binary_path}" "/usr/local/bin/${target_binary_name}"; then + cleanup_gum_tmp_dir "${tmp_dir}" + return 0 + fi + fi + + if [ -n "${HOME:-}" ]; then + target_dir="${HOME}/.local/bin" + mkdir -p "${target_dir}" + if copy_binary "${gum_binary_path}" "${target_dir}/${target_binary_name}"; then + cleanup_gum_tmp_dir "${tmp_dir}" + return 0 + fi + fi + + cleanup_gum_tmp_dir "${tmp_dir}" + echo "Failed to install gum binary from GitHub release." + return 1 +} diff --git a/scripts/easy-docker/lib/install/gum/load.sh b/scripts/easy-docker/lib/install/gum/load.sh new file mode 100755 index 00000000..d3983a57 --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/load.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +load_gum_install_modules() { + local gum_lib_dir="" + gum_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # shellcheck source=scripts/easy-docker/lib/install/gum/platform.sh + source "${gum_lib_dir}/platform.sh" + # shellcheck source=scripts/easy-docker/lib/install/gum/assets.sh + source "${gum_lib_dir}/assets.sh" + # shellcheck source=scripts/easy-docker/lib/install/gum/package_manager.sh + source "${gum_lib_dir}/package_manager.sh" + # shellcheck source=scripts/easy-docker/lib/install/gum/github_release.sh + source "${gum_lib_dir}/github_release.sh" + # shellcheck source=scripts/easy-docker/lib/install/gum/ensure.sh + source "${gum_lib_dir}/ensure.sh" +} + +load_gum_install_modules diff --git a/scripts/easy-docker/lib/install/gum/package_manager.sh b/scripts/easy-docker/lib/install/gum/package_manager.sh new file mode 100755 index 00000000..8d6884c1 --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/package_manager.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +install_gum_with_package_manager() { + local pm_attempted=0 + + if command_exists brew; then + pm_attempted=1 + if brew install gum; then + return 0 + fi + fi + + if command_exists apt-get; then + pm_attempted=1 + if run_with_privileges apt-get update && run_with_privileges apt-get install -y gum; then + return 0 + fi + fi + + if command_exists dnf; then + pm_attempted=1 + if run_with_privileges dnf install -y gum; then + return 0 + fi + fi + + if command_exists pacman; then + pm_attempted=1 + if run_with_privileges pacman -Sy --noconfirm gum; then + return 0 + fi + fi + + if command_exists zypper; then + pm_attempted=1 + if run_with_privileges zypper --non-interactive install gum; then + return 0 + fi + fi + + if command_exists winget; then + pm_attempted=1 + if winget install --id Charmbracelet.Gum -e --accept-source-agreements --accept-package-agreements; then + return 0 + fi + fi + + if command_exists choco; then + pm_attempted=1 + if choco install gum -y; then + return 0 + fi + fi + + if [ "${pm_attempted}" -eq 0 ]; then + echo "No supported package manager was found." + else + echo "Package manager installation did not succeed." + fi + + return 1 +} diff --git a/scripts/easy-docker/lib/install/gum/platform.sh b/scripts/easy-docker/lib/install/gum/platform.sh new file mode 100755 index 00000000..5f145aac --- /dev/null +++ b/scripts/easy-docker/lib/install/gum/platform.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +detect_gum_platform() { + local raw_os="" + local raw_arch="" + local gum_os="" + local gum_arch="" + + raw_os="$(uname -s 2>/dev/null || echo unknown)" + raw_arch="$(uname -m 2>/dev/null || echo unknown)" + + case "${raw_os}" in + Linux*) + gum_os="Linux" + ;; + Darwin*) + gum_os="Darwin" + ;; + MINGW* | MSYS* | CYGWIN* | Windows_NT) + gum_os="Windows" + ;; + *) + return 1 + ;; + esac + + case "${raw_arch}" in + x86_64 | amd64) + gum_arch="x86_64" + ;; + aarch64 | arm64) + gum_arch="arm64" + ;; + armv7l | armv7) + gum_arch="armv7" + ;; + *) + return 1 + ;; + esac + + printf '%s %s\n' "${gum_os}" "${gum_arch}" + return 0 +} + +get_os_aliases() { + local os_name="${1}" + local os_lower="" + + os_lower="$(printf '%s' "${os_name}" | tr '[:upper:]' '[:lower:]')" + + if [ "${os_lower}" = "${os_name}" ]; then + printf '%s\n' "${os_name}" + return + fi + + printf '%s\n%s\n' "${os_name}" "${os_lower}" +} + +get_arch_aliases() { + case "${1}" in + x86_64) + printf '%s\n%s\n' "x86_64" "amd64" + ;; + arm64) + printf '%s\n%s\n' "arm64" "aarch64" + ;; + armv7) + printf '%s\n%s\n' "armv7" "armv7l" + ;; + *) + printf '%s\n' "${1}" + ;; + esac +} diff --git a/scripts/easy-docker/lib/ui.sh b/scripts/easy-docker/lib/ui/screens.sh similarity index 70% rename from scripts/easy-docker/lib/ui.sh rename to scripts/easy-docker/lib/ui/screens.sh index 097ba93c..9b2a391d 100755 --- a/scripts/easy-docker/lib/ui.sh +++ b/scripts/easy-docker/lib/ui/screens.sh @@ -8,7 +8,7 @@ render_main_screen() { clear fi - header_text="$(printf "Easy Frappe Docker\nMinimal TUI bootstrap")" + header_text="$(printf "Easy Frappe Docker\nManage Docker setups quickly and easily")" gum style \ --border rounded \ @@ -31,16 +31,20 @@ show_main_menu() { show_environment_status() { local docker_status="not installed" - local gum_status="installed" + local docker_daemon_status="not running" local status_text="" if command_exists docker; then docker_status="installed" + + if docker_daemon_running; then + docker_daemon_status="running" + fi fi render_main_screen 1 >&2 - status_text="$(printf "Environment status\n\n- gum: %s\n- docker: %s" "${gum_status}" "${docker_status}")" + status_text="$(printf "Environment status\n\n- docker: %s\n- docker daemon: %s" "${docker_status}" "${docker_daemon_status}")" gum style \ --border rounded \ @@ -58,3 +62,8 @@ show_environment_status() { "Back to main menu" \ "Exit and close easy-docker" } + +show_warning_message() { + local message="${1}" + gum style --foreground 214 "${message}" +} diff --git a/scripts/easy-docker/main.sh b/scripts/easy-docker/main.sh index b196542c..626b9b8f 100755 --- a/scripts/easy-docker/main.sh +++ b/scripts/easy-docker/main.sh @@ -3,106 +3,43 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=scripts/easy-docker/lib/env.sh -source "${SCRIPT_DIR}/lib/env.sh" -# shellcheck source=scripts/easy-docker/lib/ui.sh -source "${SCRIPT_DIR}/lib/ui.sh" +# shellcheck source=scripts/easy-docker/lib/core/commands.sh +source "${SCRIPT_DIR}/lib/core/commands.sh" +# shellcheck source=scripts/easy-docker/lib/core/messages.sh +source "${SCRIPT_DIR}/lib/core/messages.sh" +# shellcheck source=scripts/easy-docker/lib/install/gum/load.sh +source "${SCRIPT_DIR}/lib/install/gum/load.sh" +# shellcheck source=scripts/easy-docker/lib/checks/docker.sh +source "${SCRIPT_DIR}/lib/checks/docker.sh" +# shellcheck source=scripts/easy-docker/lib/ui/screens.sh +source "${SCRIPT_DIR}/lib/ui/screens.sh" +# shellcheck source=scripts/easy-docker/lib/app/screen.sh +source "${SCRIPT_DIR}/lib/app/screen.sh" +# shellcheck source=scripts/easy-docker/lib/app/options.sh +source "${SCRIPT_DIR}/lib/app/options.sh" +# shellcheck source=scripts/easy-docker/lib/app/run.sh +source "${SCRIPT_DIR}/lib/app/run.sh" -print_usage() { - cat <<'USAGE' -Usage: bash easy-docker.sh [options] - -Options: - --no-github-binary-fallback Disable GitHub binary fallback prompt - -h, --help Show this help -USAGE -} - -DISABLE_GITHUB_BINARY_FALLBACK=0 - -while [ "$#" -gt 0 ]; do - case "$1" in - --no-github-binary-fallback) - DISABLE_GITHUB_BINARY_FALLBACK=1 - ;; - -h | --help) - print_usage +disable_installation_fallback=0 +if parse_cli_options disable_installation_fallback "$@"; then + : +else + parse_status=$? + if [ "${parse_status}" -eq 2 ]; then exit 0 - ;; - *) - echo "Unknown option: $1" - print_usage - exit 1 - ;; - esac - shift -done - -ensure_gum "${DISABLE_GITHUB_BINARY_FALLBACK}" - -ALT_SCREEN_ACTIVE=0 - -enter_alt_screen() { - if [ -t 1 ] && command -v tput >/dev/null 2>&1; then - tput smcup || true - tput civis || true - ALT_SCREEN_ACTIVE=1 fi -} + exit "${parse_status}" +fi -leave_alt_screen() { - if [ "${ALT_SCREEN_ACTIVE}" = "1" ] && command -v tput >/dev/null 2>&1; then - tput cnorm || true - tput rmcup || true - ALT_SCREEN_ACTIVE=0 - fi -} +if ! ensure_gum "${disable_installation_fallback}"; then + exit 1 +fi -cleanup_screen() { - leave_alt_screen -} +if ! ensure_docker; then + exit 1 +fi -cleanup_and_exit() { - exit 0 -} +trap 'leave_alt_screen; exit 0' INT TERM +trap 'leave_alt_screen' EXIT -trap cleanup_and_exit INT TERM -trap cleanup_screen EXIT - -enter_alt_screen - -render_main_screen 1 - -while true; do - local_env_action="" - action="$(show_main_menu || true)" - - if [ -z "${action}" ]; then - cleanup_and_exit - fi - - case "${action}" in - "Environment check") - local_env_action="$(show_environment_status || true)" - case "${local_env_action}" in - "Back to main menu" | "") - render_main_screen 1 - ;; - "Exit and close easy-docker") - cleanup_and_exit - ;; - *) - gum style --foreground 214 "Unknown environment action: ${local_env_action}" - sleep 1 - ;; - esac - ;; - "Exit") - cleanup_and_exit - ;; - *) - gum style --foreground 214 "Unknown action: ${action}" - sleep 1 - ;; - esac -done +run_easy_docker_app