From e5542de8181235935c418196c22afe7ddc9afbc8 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Wed, 15 Apr 2026 10:15:43 +0800 Subject: [PATCH] polish: CLI UX overhaul and rich .env.example metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI improvements: - Unicode status indicators (✔ ✘ ▶ ● ○ ⚠) and braille spinners - Animated spinner for docker pull/up operations - Project metadata parsed from .env.example (@name, @desc, @url, @port, @note) - Descriptions shown in list, deploy selection, and status views - Auto-generate passwords for secret fields (PASSWORD/TOKEN/AUTHKEY) - Confirmation prompt before deploy with project summary - Post-deploy access URL hint based on @port metadata - Divider lines for visual section separation - Helpful error messages with suggested commands - Command aliases: ls, st, ps, down, log, configure - Bash 3.2 compatible (no associative arrays) .env.example enrichment: - All projects now have @name, @desc, @url, @port metadata headers - Inline field descriptions shown as context during interactive config - Tailscale: @note hints for profile-based DERP deployment - Structured comments group related settings visually Installer: - Prerequisite check with per-tool status (✔/✘) - Quieter git operations - Cleaner post-install instructions --- automa | 619 ++++++++++++++++++++++++++------------- filesuite/.env.example | 15 +- forgejo/.env.example | 9 +- install.sh | 58 ++-- minecraft/.env.example | 16 +- nextcloud/.env.example | 16 +- tailscale/.env.example | 21 +- teamspeak/.env.example | 6 +- uptime-kuma/.env.example | 7 +- 9 files changed, 522 insertions(+), 245 deletions(-) diff --git a/automa b/automa index a379ff5..5c4cae9 100755 --- a/automa +++ b/automa @@ -1,55 +1,101 @@ #!/usr/bin/env bash # automa - interactive Docker Compose project deployer # -# Install & run: +# Quick start: # curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash # cd ~/automa && ./automa deploy set -euo pipefail -# ============================================================================ -# Constants -# ============================================================================ AUTOMA_VERSION="1.0.0" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Colors (disabled when not a terminal) +# ============================================================================ +# Terminal +# ============================================================================ if [[ -t 1 ]]; then - RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' - CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' + BLUE='\033[0;34m' CYAN='\033[0;36m' + BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' + COLS=$(tput cols 2>/dev/null || echo 80) else - RED='' GREEN='' YELLOW='' CYAN='' BOLD='' DIM='' NC='' + RED='' GREEN='' YELLOW='' BLUE='' CYAN='' + BOLD='' DIM='' NC='' + COLS=80 fi # ============================================================================ -# Helpers +# Output helpers # ============================================================================ -info() { echo -e "${GREEN}[+]${NC} $*"; } -warn() { echo -e "${YELLOW}[!]${NC} $*"; } -error() { echo -e "${RED}[-]${NC} $*" >&2; } +info() { echo -e " ${GREEN}\xe2\x9c\x94${NC} $*"; } +warn() { echo -e " ${YELLOW}\xe2\x9a\xa0${NC} $*"; } +error() { echo -e " ${RED}\xe2\x9c\x98${NC} $*" >&2; } +step() { echo -e " ${CYAN}\xe2\x96\xb6${NC} ${BOLD}$*${NC}"; } +dim() { echo -e " ${DIM}$*${NC}"; } -banner() { - echo "" - echo -e "${CYAN}${BOLD} automa${NC} ${DIM}v${AUTOMA_VERSION}${NC}" - echo -e " ${DIM}docker compose project deployer${NC}" - echo "" +divider() { + printf " ${DIM}" + printf '%.0s\xe2\x94\x80' $(seq 1 $(( (COLS - 4) / 3 + 1 )) ) + printf "${NC}\n" } +# Spinner for long operations +spinner() { + local pid=$1 msg="${2:-Working...}" + local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local i=0 + + while kill -0 "$pid" 2>/dev/null; do + printf "\r ${CYAN}%s${NC} %s" "${frames[$i]}" "$msg" + i=$(( (i + 1) % ${#frames[@]} )) + sleep 0.1 + done + + wait "$pid" + local rc=$? + printf "\r\033[2K" # clear line + return $rc +} + +run_with_spinner() { + local msg="$1"; shift + "$@" &>/dev/null & + local pid=$! + if spinner "$pid" "$msg"; then + info "$msg" + return 0 + else + error "$msg" + return 1 + fi +} + +# ============================================================================ +# Prerequisites +# ============================================================================ check_docker() { if ! command -v docker &>/dev/null; then - error "docker is required but not installed." - echo -e " ${DIM}Install: https://docs.docker.com/engine/install/${NC}" + echo "" + error "Docker is not installed" + echo "" + dim "Install Docker:" + dim " curl -fsSL https://get.docker.com | sh" + dim "" + dim "Or visit: https://docs.docker.com/engine/install/" + echo "" exit 1 fi if ! docker compose version &>/dev/null 2>&1; then - error "docker compose plugin is required." - echo -e " ${DIM}Install: https://docs.docker.com/compose/install/${NC}" + echo "" + error "Docker Compose plugin is not installed" + dim "Install: https://docs.docker.com/compose/install/" + echo "" exit 1 fi } # ============================================================================ -# Project discovery +# Project helpers # ============================================================================ PROJECTS=() @@ -60,120 +106,205 @@ discover_projects() { done } -project_exists() { - local name="$1" - [[ -f "$SCRIPT_DIR/$name/compose.yaml" ]] +project_exists() { [[ -f "$SCRIPT_DIR/$1/compose.yaml" ]]; } + +# Parse @key from .env.example header +project_meta() { + local slug="$1" key="$2" + local env_example="$SCRIPT_DIR/$slug/.env.example" + [[ -f "$env_example" ]] || return + while IFS= read -r line; do + if [[ "$line" =~ ^#\ @${key}\ (.+) ]]; then + echo "${BASH_REMATCH[1]}" + return + fi + [[ ! "$line" =~ ^# ]] && return # stop at first non-comment + done < "$env_example" } -project_dir() { - echo "$SCRIPT_DIR/$1" +# Collect all @note lines +project_notes() { + local slug="$1" + local env_example="$SCRIPT_DIR/$slug/.env.example" + [[ -f "$env_example" ]] || return + while IFS= read -r line; do + [[ "$line" =~ ^#\ @note\ (.+) ]] && echo "${BASH_REMATCH[1]}" + [[ ! "$line" =~ ^# ]] && return + done < "$env_example" } -# compose wrapper that auto-adds --env-file if .env exists -compose() { - local name="$1"; shift - local dir="$SCRIPT_DIR/$name" - local args=(-f "$dir/compose.yaml") - - if [[ -f "$dir/.env" ]]; then - args+=(--env-file "$dir/.env") +project_status() { + local slug="$1" + if [[ ! -f "$SCRIPT_DIR/$slug/.env" ]]; then + echo "not_configured" + elif compose "$slug" ps --status running 2>/dev/null | grep -q .; then + echo "running" + else + echo "stopped" fi +} +status_badge() { + case "$1" in + running) echo -e "${GREEN}\xe2\x97\x8f running${NC}" ;; + stopped) echo -e "${YELLOW}\xe2\x97\x8f stopped${NC}" ;; + not_configured) echo -e "${DIM}\xe2\x97\x8b not configured${NC}" ;; + esac +} + +# compose wrapper +compose() { + local slug="$1"; shift + local dir="$SCRIPT_DIR/$slug" + local args=(-f "$dir/compose.yaml") + [[ -f "$dir/.env" ]] && args+=(--env-file "$dir/.env") docker compose "${args[@]}" "$@" } +# Get access URL after deploy +access_hint() { + local slug="$1" + local port_var + port_var=$(project_meta "$slug" "port") + [[ -z "$port_var" ]] && return + + local env_file="$SCRIPT_DIR/$slug/.env" + [[ -f "$env_file" ]] || return + + local port_val + port_val=$(grep "^${port_var}=" "$env_file" 2>/dev/null | cut -d= -f2) + [[ -z "$port_val" ]] && return + + if [[ "$port_val" == *:* ]]; then + echo "http://${port_val}" + elif [[ "$port_val" == */* ]]; then + echo "$port_val" + else + echo "http://localhost:${port_val}" + fi +} + # ============================================================================ # Interactive .env configuration # ============================================================================ +generate_password() { + LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | head -c 24 || openssl rand -hex 12 +} + configure_env() { - local name="$1" - local dir="$SCRIPT_DIR/$name" + local slug="$1" + local dir="$SCRIPT_DIR/$slug" local env_example="$dir/.env.example" local env_file="$dir/.env" if [[ ! -f "$env_example" ]]; then - warn "No .env.example found, skipping configuration" + warn "No .env.example found, skipping" return 0 fi - # If .env already exists, ask what to do + local name + name=$(project_meta "$slug" "name") + name="${name:-$slug}" + + # Handle existing .env if [[ -f "$env_file" ]]; then echo "" - echo -e " ${YELLOW}.env already exists for ${BOLD}${name}${NC}" - echo -e " ${DIM}[k]eep current [r]econfigure [v]iew${NC}" + dim ".env already exists for ${BOLD}${name}${NC}" + echo "" + echo -e " ${BOLD}k${NC} Keep current configuration" + echo -e " ${BOLD}r${NC} Reconfigure from scratch" + echo -e " ${BOLD}v${NC} View current values" + echo "" while true; do - read -rp " > " choice - case "${choice,,}" in - k|keep|"") info "Keeping existing .env"; return 0 ;; - r|reconfigure) break ;; - v|view) + read -rp " Choose [k]: " choice + case "${choice:-k}" in + k) info "Keeping existing .env"; return 0 ;; + r) break ;; + v) echo "" + divider while IFS= read -r line; do - echo -e " ${DIM}${line}${NC}" + echo -e " ${DIM}${line}${NC}" done < "$env_file" + divider echo "" - echo -e " ${DIM}[k]eep [r]econfigure${NC}" ;; - *) echo -e " ${DIM}Enter k, r, or v${NC}" ;; + *) dim "Enter k, r, or v" ;; esac done fi echo "" - echo -e " ${BOLD}Configure: ${CYAN}${name}${NC}" - echo "" + step "Configure ${name}" + local desc + desc=$(project_meta "$slug" "desc") + local url + url=$(project_meta "$slug" "url") + [[ -n "$desc" ]] && dim "$desc" + [[ -n "$url" ]] && dim "Docs: ${url}" + + # Show notes + local has_notes=0 + while IFS= read -r note; do + [[ -z "$note" ]] && continue + [[ $has_notes -eq 0 ]] && echo "" + echo -e " ${YELLOW}!${NC} ${DIM}${note}${NC}" + has_notes=1 + done < <(project_notes "$slug") - # Show header comments from .env.example - while IFS= read -r line; do - [[ "$line" =~ ^#.* ]] && echo -e " ${DIM}${line}${NC}" || break - done < "$env_example" echo "" - echo -e " ${DIM}Enter values (blank = accept default in brackets)${NC}" + divider + dim "Press ${BOLD}Enter${NC}${DIM} to accept [default] values${NC}" echo "" local tmp_env tmp_env="$(mktemp)" - local pending_comment="" while IFS= read -r line; do # Blank line - if [[ -z "$line" ]]; then - [[ -n "$pending_comment" ]] && { echo "" ; pending_comment=""; } - continue - fi + [[ -z "$line" ]] && continue - # Comment line — show as hint for next variable + # Skip @metadata + [[ "$line" =~ ^#\ @(name|desc|url|port|note) ]] && continue + + # Comment → show as hint if [[ "$line" =~ ^#.* ]]; then - echo -e " ${DIM}${line}${NC}" - pending_comment="$line" + echo -e " ${DIM}${line#\# }${NC}" continue fi local key="${line%%=*}" local default="${line#*=}" - pending_comment="" - local val if [[ -n "$default" ]]; then - read -rp " ${key} [${default}]: " val + read -rp " ${BOLD}${key}${NC} [${DIM}${default}${NC}]: " val echo "${key}=${val:-$default}" >> "$tmp_env" else - # Required field — no default - while true; do - read -rp " ${key} (required): " val - if [[ -n "$val" ]]; then - break + # Required — check if it's a secret + if [[ "$key" =~ PASSWORD|SECRET|TOKEN|AUTHKEY ]]; then + echo -e " ${DIM}Leave blank to auto-generate${NC}" + read -rp " ${BOLD}${key}${NC}: " val + if [[ -z "$val" ]]; then + val=$(generate_password) + echo -e " ${DIM}Generated: ${val}${NC}" fi - echo -e " ${RED}This field cannot be empty${NC}" - done + else + while true; do + read -rp " ${BOLD}${key}${NC} ${RED}(required)${NC}: " val + [[ -n "$val" ]] && break + echo -e " ${RED}This field cannot be empty${NC}" + done + fi echo "${key}=${val}" >> "$tmp_env" fi done < "$env_example" + echo "" + divider + mv "$tmp_env" "$env_file" chmod 600 "$env_file" - echo "" - info ".env saved (chmod 600)" + info "Configuration saved" } # ============================================================================ @@ -182,6 +313,7 @@ configure_env() { cmd_list() { banner + check_docker discover_projects if [[ ${#PROJECTS[@]} -eq 0 ]]; then @@ -189,20 +321,18 @@ cmd_list() { return 1 fi - echo -e " ${BOLD}Available projects:${NC}" - echo "" - local i=1 - for p in "${PROJECTS[@]}"; do - local status="${DIM}not configured${NC}" - if [[ -f "$SCRIPT_DIR/$p/.env" ]]; then - if compose "$p" ps --status running 2>/dev/null | grep -q .; then - status="${GREEN}running${NC}" - else - status="${YELLOW}stopped${NC}" - fi - fi - printf " ${BOLD}%2d${NC} %-24s %b\n" "$i" "$p" "$status" + for slug in "${PROJECTS[@]}"; do + local st + st=$(project_status "$slug") + local badge + badge=$(status_badge "$st") + local desc + desc=$(project_meta "$slug" "desc") + + printf " ${BOLD}%2d${NC} %-20s %b\n" "$i" "$slug" "$badge" + [[ -n "$desc" ]] && echo -e " ${DIM}${desc}${NC}" + ((i++)) done echo "" @@ -218,27 +348,42 @@ cmd_deploy() { return 1 fi - # If arguments provided, deploy those directly + # Direct deploy if [[ $# -gt 0 ]]; then + local ok=0 fail=0 for name in "$@"; do - deploy_project "$name" + echo "" + if deploy_project "$name"; then ((ok++)); else ((fail++)); fi done + deploy_summary $ok $fail return fi - # Interactive selection - echo -e " ${BOLD}Select projects to deploy:${NC}" + # Interactive + step "Select projects to deploy" echo "" + local i=1 - for p in "${PROJECTS[@]}"; do - printf " ${BOLD}%2d${NC} %s\n" "$i" "$p" + for slug in "${PROJECTS[@]}"; do + local st + st=$(project_status "$slug") + local badge + badge=$(status_badge "$st") + local desc + desc=$(project_meta "$slug" "desc") + + printf " ${BOLD}%2d${NC} %-20s %b\n" "$i" "$slug" "$badge" + [[ -n "$desc" ]] && echo -e " ${DIM}${desc}${NC}" ((i++)) done - echo "" - echo -e " ${DIM}Enter numbers (space-separated), or 'q' to quit${NC}" - read -rp " > " selection - [[ "$selection" == "q" || -z "$selection" ]] && return 0 + echo "" + dim "Enter numbers separated by spaces, e.g. ${BOLD}1 3 5${NC}" + dim "Type ${BOLD}all${NC} to deploy everything, or ${BOLD}q${NC} to quit" + echo "" + read -rp " > " selection + + [[ -z "$selection" || "$selection" == "q" ]] && return 0 local selected=() if [[ "$selection" == "all" ]]; then @@ -248,83 +393,122 @@ cmd_deploy() { if [[ "$num" =~ ^[0-9]+$ ]] && ((num > 0 && num <= ${#PROJECTS[@]})); then selected+=("${PROJECTS[$((num-1))]}") else - warn "Invalid: $num (skipped)" + warn "Skipping invalid: $num" fi done fi - if [[ ${#selected[@]} -eq 0 ]]; then - return 0 - fi + [[ ${#selected[@]} -eq 0 ]] && return 0 + # Confirmation echo "" - info "Will deploy: ${selected[*]}" + divider + step "Will deploy:" + for s in "${selected[@]}"; do + local desc + desc=$(project_meta "$s" "desc") + echo -e " ${CYAN}\xe2\x96\xb6${NC} ${s} ${DIM}${desc:-}${NC}" + done echo "" + read -rp " Proceed? [Y/n] " confirm + [[ "${confirm:-y}" =~ ^[Nn] ]] && { echo ""; dim "Cancelled."; return 0; } + + divider local ok=0 fail=0 for name in "${selected[@]}"; do - if deploy_project "$name"; then - ((ok++)) - else - ((fail++)) - fi echo "" + if deploy_project "$name"; then ((ok++)); else ((fail++)); fi done - echo -e " ${BOLD}Done.${NC} ${GREEN}${ok} deployed${NC}" \ - "$( ((fail > 0)) && echo -e ", ${RED}${fail} failed${NC}" )" + deploy_summary $ok $fail } deploy_project() { - local name="$1" + local slug="$1" - if ! project_exists "$name"; then - error "Project not found: $name" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" + dim "Run ${BOLD}automa list${NC}${DIM} to see available projects${NC}" return 1 fi - echo -e " ${CYAN}── ${BOLD}${name}${NC} ${CYAN}──${NC}" + local name + name=$(project_meta "$slug" "name") + name="${name:-$slug}" + local desc + desc=$(project_meta "$slug" "desc") - configure_env "$name" + step "${name}" + [[ -n "$desc" ]] && dim "${desc}" - if [[ ! -f "$SCRIPT_DIR/$name/.env" ]]; then - error "No .env file — run: automa config $name" + configure_env "$slug" + + if [[ ! -f "$SCRIPT_DIR/$slug/.env" ]]; then + error "No .env — run: ${BOLD}automa config $slug${NC}" return 1 fi - info "Starting containers..." - if compose "$name" up -d 2>&1; then - info "${name} is up" - return 0 + echo "" + if run_with_spinner "Pulling images..." compose "$slug" pull; then + if run_with_spinner "Starting containers..." compose "$slug" up -d; then + local url + url=$(access_hint "$slug") + [[ -n "$url" ]] && dim "Access: ${BOLD}${url}${NC}" + return 0 + fi + fi + return 1 +} + +deploy_summary() { + local ok=$1 fail=$2 + echo "" + divider + echo "" + if [[ $fail -eq 0 ]]; then + info "${BOLD}All done!${NC} ${ok} project(s) deployed" else - error "${name} failed to start" - return 1 + warn "${BOLD}Done.${NC} ${GREEN}${ok} deployed${NC}, ${RED}${fail} failed${NC}" fi + echo "" + dim "Useful commands:" + dim " ${BOLD}automa status${NC}${DIM} — check running state${NC}" + dim " ${BOLD}automa logs${NC}${DIM} — view logs${NC}" + dim " ${BOLD}automa update${NC}${DIM} — pull & restart${NC}" + echo "" } cmd_stop() { - local name="${1:?Usage: automa stop }" - - if ! project_exists "$name"; then - error "Project not found: $name" + local slug="${1:-}" + if [[ -z "$slug" ]]; then + error "Usage: ${BOLD}automa stop ${NC}" + dim "Run ${BOLD}automa list${NC}${DIM} to see available projects${NC}" return 1 fi - info "Stopping ${name}..." - compose "$name" down - info "${name} stopped" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" + return 1 + fi + + run_with_spinner "Stopping ${slug}..." compose "$slug" down } cmd_logs() { - local name="${1:?Usage: automa logs }" + local slug="${1:-}" + if [[ -z "$slug" ]]; then + error "Usage: ${BOLD}automa logs ${NC}" + return 1 + fi shift - if ! project_exists "$name"; then - error "Project not found: $name" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" return 1 fi - compose "$name" logs -f "$@" + compose "$slug" logs -f "$@" } cmd_status() { @@ -332,86 +516,120 @@ cmd_status() { check_docker discover_projects - for p in "${PROJECTS[@]}"; do - echo -e " ${BOLD}${p}${NC}" - local output - output=$(compose "$p" ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null | tail -n +2) || true - if [[ -n "$output" ]]; then - while IFS= read -r line; do - echo -e " ${line}" - done <<< "$output" - else - echo -e " ${DIM}not running${NC}" + if [[ ${#PROJECTS[@]} -eq 0 ]]; then + warn "No projects found" + return 1 + fi + + for slug in "${PROJECTS[@]}"; do + local st name + st=$(project_status "$slug") + name=$(project_meta "$slug" "name") + name="${name:-$slug}" + + local badge + badge=$(status_badge "$st") + echo -e " ${BOLD}${name}${NC} ${badge}" + + if [[ "$st" == "running" ]]; then + compose "$slug" ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null \ + | tail -n +2 \ + | while IFS= read -r line; do + echo -e " ${DIM}${line}${NC}" + done fi echo "" done } cmd_restart() { - local name="${1:?Usage: automa restart }" - - if ! project_exists "$name"; then - error "Project not found: $name" + local slug="${1:-}" + if [[ -z "$slug" ]]; then + error "Usage: ${BOLD}automa restart ${NC}" return 1 fi - info "Restarting ${name}..." - compose "$name" restart - info "${name} restarted" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" + return 1 + fi + + run_with_spinner "Restarting ${slug}..." compose "$slug" restart } cmd_config() { - local name="${1:?Usage: automa config }" - - if ! project_exists "$name"; then - error "Project not found: $name" + local slug="${1:-}" + if [[ -z "$slug" ]]; then + error "Usage: ${BOLD}automa config ${NC}" return 1 fi - configure_env "$name" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" + return 1 + fi + + configure_env "$slug" } cmd_update() { - local name="${1:?Usage: automa update }" - - if ! project_exists "$name"; then - error "Project not found: $name" + local slug="${1:-}" + if [[ -z "$slug" ]]; then + error "Usage: ${BOLD}automa update ${NC}" return 1 fi - info "Pulling latest images for ${name}..." - compose "$name" pull - info "Recreating containers..." - compose "$name" up -d - info "${name} updated" + if ! project_exists "$slug"; then + error "Project not found: ${slug}" + return 1 + fi + + local name + name=$(project_meta "$slug" "name") + name="${name:-$slug}" + + echo "" + step "Updating ${name}" + run_with_spinner "Pulling latest images..." compose "$slug" pull + run_with_spinner "Recreating containers..." compose "$slug" up -d + echo "" + info "Update complete" + echo "" +} + +banner() { + echo "" + echo -e " ${BOLD}${CYAN}automa${NC} ${DIM}v${AUTOMA_VERSION}${NC}" + dim "Self-hosted Docker Compose deployer" + echo "" } cmd_help() { banner cat < [args] + ${BOLD}Usage${NC} + automa [options] - ${BOLD}Commands:${NC} - deploy [project...] Interactive deploy (or specify project names) - list List all available projects and status - status Show running containers per project - stop Stop a project (docker compose down) - restart Restart a project - logs Follow logs of a project - config (Re)configure .env for a project - update Pull latest images and recreate - help Show this help + ${BOLD}Commands${NC} + ${BOLD}deploy${NC} [project...] Deploy projects interactively or by name + ${BOLD}list${NC} List all projects and their status + ${BOLD}status${NC} Show running containers + ${BOLD}config${NC} Configure environment variables + ${BOLD}stop${NC} Stop a running project + ${BOLD}restart${NC} Restart a project + ${BOLD}update${NC} Pull latest images and recreate + ${BOLD}logs${NC} Follow container logs + ${BOLD}help${NC} Show this help message - ${BOLD}Examples:${NC} - automa deploy # interactive project selection - automa deploy forgejo filesuite # deploy specific projects - automa status # check all project status - automa logs forgejo # follow forgejo logs - automa update nextcloud # pull & restart nextcloud + ${BOLD}Examples${NC} + ${DIM}\$${NC} automa deploy ${DIM}# interactive selection${NC} + ${DIM}\$${NC} automa deploy forgejo nextcloud ${DIM}# deploy by name${NC} + ${DIM}\$${NC} automa status ${DIM}# overview dashboard${NC} + ${DIM}\$${NC} automa logs minecraft ${DIM}# follow logs${NC} - ${BOLD}Quick start:${NC} - curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash - cd ~/automa && ./automa deploy + ${BOLD}Quick start${NC} + ${DIM}\$${NC} curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash + ${DIM}\$${NC} cd ~/automa && ./automa deploy EOF } @@ -420,21 +638,26 @@ EOF # Main # ============================================================================ main() { - local cmd="${1:-help}" - shift 2>/dev/null || true + local cmd="${1:-}" + [[ -z "$cmd" ]] && { cmd_help; return; } + shift case "$cmd" in - deploy) cmd_deploy "$@" ;; - list|ls) cmd_list ;; - status) cmd_status ;; - stop) cmd_stop "$@" ;; - restart) cmd_restart "$@" ;; - logs) cmd_logs "$@" ;; - config) cmd_config "$@" ;; - update) cmd_update "$@" ;; - help|-h|--help) cmd_help ;; + deploy) cmd_deploy "$@" ;; + list|ls) cmd_list ;; + status|st|ps) cmd_status ;; + stop|down) cmd_stop "$@" ;; + restart) cmd_restart "$@" ;; + logs|log) cmd_logs "$@" ;; + config|configure) cmd_config "$@" ;; + update|upgrade) cmd_update "$@" ;; + help|-h|--help) cmd_help ;; version|-v|--version) echo "automa v${AUTOMA_VERSION}" ;; - *) error "Unknown command: $cmd"; cmd_help; exit 1 ;; + *) + error "Unknown command: ${cmd}" + dim "Run ${BOLD}automa help${NC}${DIM} for usage${NC}" + exit 1 + ;; esac } diff --git a/filesuite/.env.example b/filesuite/.env.example index 2b26d3f..1d76971 100644 --- a/filesuite/.env.example +++ b/filesuite/.env.example @@ -1,17 +1,20 @@ -# Filesuite - Cloudreve cloud storage + qBittorrent -# Both services share the same downloads directory +# @name Filesuite +# @desc Cloudreve cloud storage + qBittorrent downloader +# @url https://cloudreve.org +# @port CLOUDREVE_PORT TZ=Asia/Shanghai PUID=1000 PGID=1000 -# Shared downloads path (absolute path recommended) +# Shared download directory — both services read/write here +# Use an absolute path for external drives (e.g. /mnt/data/downloads) DOWNLOADS_DIR=./downloads -# Cloudreve +# Cloudreve — web file manager CLOUDREVE_PORT=5212 -CR_ENABLE_ARIA2=0 -# qBittorrent +# qBittorrent — torrent client QB_WEBUI_PORT=8090 +# BT listen port — must be forwarded in your router/firewall QB_BT_PORT=44773 diff --git a/forgejo/.env.example b/forgejo/.env.example index 4447e3e..2f26dd9 100644 --- a/forgejo/.env.example +++ b/forgejo/.env.example @@ -1,8 +1,11 @@ -# Forgejo - self-hosted Git service -# Docs: https://forgejo.org/docs/latest/admin/config-cheat-sheet/ +# @name Forgejo +# @desc Self-hosted Git service (Gitea fork) +# @url https://forgejo.org +# @port FORGEJO_HTTP_PORT +# Web and SSH access ports FORGEJO_HTTP_PORT=3000 FORGEJO_SSH_PORT=2223 -# Set this to your public URL when behind a reverse proxy +# Public URL — set this to your domain when behind a reverse proxy FORGEJO_ROOT_URL=http://localhost:3000 diff --git a/install.sh b/install.sh index 2cf56e8..24d5863 100755 --- a/install.sh +++ b/install.sh @@ -6,49 +6,61 @@ set -euo pipefail REPO="https://github.com/m1ngsama/automa.git" INSTALL_DIR="${AUTOMA_DIR:-$HOME/automa}" -RED='\033[0;31m' -GREEN='\033[0;32m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' +RED='\033[0;31m' GREEN='\033[0;32m' CYAN='\033[0;36m' +BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' -info() { echo -e "${GREEN}[+]${NC} $*"; } -error() { echo -e "${RED}[-]${NC} $*" >&2; } +info() { echo -e " ${GREEN}\xe2\x9c\x94${NC} $*"; } +error() { echo -e " ${RED}\xe2\x9c\x98${NC} $*" >&2; } +step() { echo -e " ${CYAN}\xe2\x96\xb6${NC} ${BOLD}$*${NC}"; } echo "" -echo -e "${CYAN}${BOLD} automa installer${NC}" +echo -e " ${BOLD}${CYAN}automa${NC}${BOLD} installer${NC}" echo "" # Check prerequisites +missing=0 for cmd in git docker; do - if ! command -v "$cmd" &>/dev/null; then - error "$cmd is required but not installed." - exit 1 + if command -v "$cmd" &>/dev/null; then + info "$cmd found" + else + error "$cmd is not installed" + missing=1 fi done -if ! docker compose version &>/dev/null 2>&1; then - error "docker compose plugin is required." - error "Install: https://docs.docker.com/compose/install/" +if docker compose version &>/dev/null 2>&1; then + info "docker compose plugin found" +else + error "docker compose plugin is not installed" + echo -e " ${DIM}Install: https://docs.docker.com/compose/install/${NC}" + missing=1 +fi + +if [[ $missing -eq 1 ]]; then + echo "" + error "Please install missing dependencies and try again" exit 1 fi +echo "" + # Clone or update if [[ -d "$INSTALL_DIR/.git" ]]; then - info "Updating existing installation..." - git -C "$INSTALL_DIR" pull --ff-only + step "Updating existing installation..." + git -C "$INSTALL_DIR" pull --ff-only --quiet + info "Updated" else - info "Cloning automa to ${INSTALL_DIR}..." - git clone "$REPO" "$INSTALL_DIR" + step "Installing to ${INSTALL_DIR}..." + git clone --quiet "$REPO" "$INSTALL_DIR" + info "Cloned" fi chmod +x "$INSTALL_DIR/automa" echo "" -info "Installed to ${INSTALL_DIR}" +echo -e " ${GREEN}${BOLD}Ready!${NC}" echo "" -echo -e " ${BOLD}Next steps:${NC}" -echo -e " cd ${INSTALL_DIR}" -echo -e " ./automa deploy" +echo -e " ${DIM}Get started:${NC}" +echo -e " ${BOLD}cd ${INSTALL_DIR}${NC}" +echo -e " ${BOLD}./automa deploy${NC}" echo "" diff --git a/minecraft/.env.example b/minecraft/.env.example index e755a1e..f66ee6c 100644 --- a/minecraft/.env.example +++ b/minecraft/.env.example @@ -1,11 +1,23 @@ -# Minecraft server (itzg/minecraft-server) -# Docs: https://docker-minecraft-server.readthedocs.io/ +# @name Minecraft +# @desc Fabric Minecraft server (itzg/minecraft-server) +# @url https://docker-minecraft-server.readthedocs.io +# @port MC_PORT TZ=Asia/Shanghai + +# Server type and version MC_TYPE=FABRIC MC_VERSION=1.21.1 + +# Memory allocation — adjust based on player count and mods MC_MEMORY=4G + +# Set to true for Mojang account verification MC_ONLINE_MODE=false + +# Ports MC_PORT=25565 RCON_PORT=25575 + +# RCON password for remote console access RCON_PASSWORD= diff --git a/nextcloud/.env.example b/nextcloud/.env.example index 920658d..c8fa010 100644 --- a/nextcloud/.env.example +++ b/nextcloud/.env.example @@ -1,17 +1,25 @@ -# Nextcloud with MariaDB + Redis -# Docs: https://hub.docker.com/_/nextcloud +# @name Nextcloud +# @desc Nextcloud private cloud with MariaDB + Redis +# @url https://nextcloud.com +# @port NC_PORT TZ=Asia/Shanghai + +# Web interface NC_PORT=8080 + +# Admin account — created on first startup NC_ADMIN_USER=admin NC_ADMIN_PASSWORD= + +# Trusted domains — space-separated list of domains/IPs that can access Nextcloud NC_TRUSTED_DOMAINS=localhost -# MariaDB +# MariaDB database MYSQL_DATABASE=nextcloud MYSQL_USER=nextcloud MYSQL_PASSWORD= MYSQL_ROOT_PASSWORD= -# Redis +# Redis cache REDIS_PASSWORD= diff --git a/tailscale/.env.example b/tailscale/.env.example index 954a75b..d721cd2 100644 --- a/tailscale/.env.example +++ b/tailscale/.env.example @@ -1,19 +1,28 @@ -# Tailscale + DERP relay server -# -# Deploy tailscale only: docker compose --profile tailscale up -d -# Deploy with DERP: docker compose --profile derp up -d +# @name Tailscale + DERP +# @desc Tailscale mesh VPN client with optional DERP relay +# @url https://tailscale.com/kb/1282/docker +# @note Deploy tailscale only: docker compose --profile tailscale up -d +# @note Deploy with DERP relay: docker compose --profile derp up -d TZ=Asia/Shanghai + +# Hostname shown in the Tailscale admin console TS_HOSTNAME= + +# Auth key — generate at https://login.tailscale.com/admin/settings/keys +# For headscale: generate via headscale CLI TS_AUTHKEY= -# For headscale: --advertise-tags=tag:container --login-server=https://your.headscale.host +# Extra arguments passed to tailscaled +# For headscale users, add: --login-server=https://your.headscale.host TS_EXTRA_ARGS=--advertise-tags=tag:container +# Networking mode: false = kernel (better performance), true = userspace TS_USERSPACE=false TS_FIREWALL_MODE=nftables -# DERP relay (only needed with --profile derp) +# DERP relay settings (only used with --profile derp) +# Public IP of this server — clients connect to this address DERP_HOST= DERP_PORT=443 STUN_PORT=3478 diff --git a/teamspeak/.env.example b/teamspeak/.env.example index 293d461..283aad8 100644 --- a/teamspeak/.env.example +++ b/teamspeak/.env.example @@ -1,3 +1,7 @@ -# TeamSpeak voice server +# @name TeamSpeak +# @desc TeamSpeak voice communication server +# @url https://teamspeak.com +# @port 9987/udp +# Server admin password — set on first run, used for ServerQuery TS3_ADMIN_PASSWORD= diff --git a/uptime-kuma/.env.example b/uptime-kuma/.env.example index ac0cc39..47efd58 100644 --- a/uptime-kuma/.env.example +++ b/uptime-kuma/.env.example @@ -1,4 +1,7 @@ -# Uptime Kuma - uptime monitoring -# Default binds to localhost only; change to 0.0.0.0:3001 for external access +# @name Uptime Kuma +# @desc Uptime monitoring dashboard +# @url https://github.com/louislam/uptime-kuma +# @port KUMA_PORT +# Bind address — default localhost only; use 0.0.0.0:3001 for external access KUMA_PORT=127.0.0.1:3001