From 1356348d79602a145581d58357b7127a0f3cb518 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sat, 28 Feb 2026 01:42:50 +0800 Subject: [PATCH] feat: add interactive setup.sh wizard Discovers all deployable modules from services/ automatically. Grouped menu by role (vps / homeserver / any) with descriptions. Env resolution priority: 1. pre-filled .env in local infra checkout (--infra-dir) 2. .env.example from infra (interactive fill) 3. .env.example bundled in automa (interactive fill, no infra needed) Usage: ./setup.sh # fully interactive ./setup.sh --infra-dir /path/to/infra # use pre-filled .env files ./setup.sh --dry-run # preview without deploying Also add .env.example with role/description metadata to each service module so setup.sh can build the menu and prompt for values without requiring an infra checkout. --- services/email/.env.example | 7 + services/frp/client/.env.example | 6 + services/frp/server/.env.example | 6 + services/nginx/.env.example | 8 + services/shadowsocks/client/.env.example | 7 + services/shadowsocks/server/.env.example | 6 + setup.sh | 337 +++++++++++++++++++++++ 7 files changed, 377 insertions(+) create mode 100644 services/email/.env.example create mode 100644 services/frp/client/.env.example create mode 100644 services/frp/server/.env.example create mode 100644 services/nginx/.env.example create mode 100644 services/shadowsocks/client/.env.example create mode 100644 services/shadowsocks/server/.env.example create mode 100755 setup.sh diff --git a/services/email/.env.example b/services/email/.env.example new file mode 100644 index 0000000..cf2c41a --- /dev/null +++ b/services/email/.env.example @@ -0,0 +1,7 @@ +# role: vps +# description: Postfix + Dovecot + OpenDKIM + SpamAssassin full email stack + +DOMAIN=your-domain.com +MAIL_HOST=mail.your-domain.com +SERVER_IP=x.x.x.x +MAIL_USER=contact diff --git a/services/frp/client/.env.example b/services/frp/client/.env.example new file mode 100644 index 0000000..0f033b8 --- /dev/null +++ b/services/frp/client/.env.example @@ -0,0 +1,6 @@ +# role: homeserver +# description: FRP client (frpc) — tunnels local services through VPS + +FRP_SERVER_ADDR=your-vps-ip +FRP_SERVER_PORT=7000 +FRP_TOKEN= diff --git a/services/frp/server/.env.example b/services/frp/server/.env.example new file mode 100644 index 0000000..87a31cc --- /dev/null +++ b/services/frp/server/.env.example @@ -0,0 +1,6 @@ +# role: vps +# description: FRP server (frps) — public entry point for reverse tunnels + +FRP_TOKEN= +FRP_WEB_PASSWORD= +FRP_BIND_PORT=7000 diff --git a/services/nginx/.env.example b/services/nginx/.env.example new file mode 100644 index 0000000..a87f6a5 --- /dev/null +++ b/services/nginx/.env.example @@ -0,0 +1,8 @@ +# role: vps +# description: Nginx web server and reverse proxy vhosts + +DOMAIN=your-domain.com +BLOG_DOMAIN=blog.your-domain.com +CHAN_DOMAIN=chan.your-domain.com +MAIL_DOMAIN=mail.your-domain.com +GIT_DOMAIN=git.your-domain.com diff --git a/services/shadowsocks/client/.env.example b/services/shadowsocks/client/.env.example new file mode 100644 index 0000000..4d65676 --- /dev/null +++ b/services/shadowsocks/client/.env.example @@ -0,0 +1,7 @@ +# role: homeserver +# description: sslocal + privoxy proxy chain (SOCKS5 :1080, HTTP :8118) + +SS_SERVER=your-vps-ip +SS_PORT=41268 +SS_PASSWORD= +SS_METHOD=2022-blake3-aes-256-gcm diff --git a/services/shadowsocks/server/.env.example b/services/shadowsocks/server/.env.example new file mode 100644 index 0000000..65d3a34 --- /dev/null +++ b/services/shadowsocks/server/.env.example @@ -0,0 +1,6 @@ +# role: vps +# description: Shadowsocks-Rust server (GFW-resistant proxy) + +SS_PORT=41268 +SS_PASSWORD= +SS_METHOD=2022-blake3-aes-256-gcm diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..7909fd9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# Interactive installer for infra services. +# +# Usage: +# ./setup.sh # fully interactive +# ./setup.sh --infra-dir PATH # use pre-filled .env files from local infra checkout +# ./setup.sh --dry-run # show what would be deployed, no changes +# INFRA_DIR=/path/to/infra ./setup.sh # same as --infra-dir via env var + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/bin/lib/common.sh" + +# ============================================================================ +# Args +# ============================================================================ +INFRA_DIR="${INFRA_DIR:-}" +DRY_RUN=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --infra-dir) INFRA_DIR="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + *) log_error "Unknown argument: $1"; exit 1 ;; + esac +done + +# ============================================================================ +# Banner +# ============================================================================ +echo "" +echo " ┌─────────────────────────────────────┐" +echo " │ automa setup │" +echo " │ interactive infrastructure deploy │" +echo " └─────────────────────────────────────┘" +echo "" + +# ============================================================================ +# Prerequisites (skipped in dry-run) +# ============================================================================ +check_prerequisites() { + log_info "Checking prerequisites..." + local missing=0 + for cmd in curl wget systemctl envsubst; do + if ! command -v "$cmd" &>/dev/null; then + log_warn " missing: $cmd" + missing=1 + fi + done + if [[ "$missing" -eq 1 ]]; then + log_error "Install missing tools before continuing." + exit 1 + fi + log_info "Prerequisites OK." +} + +[[ "$DRY_RUN" -eq 0 ]] && check_prerequisites + +# ============================================================================ +# Infra dir resolution +# ============================================================================ +if [[ -n "$INFRA_DIR" ]]; then + if [[ ! -d "$INFRA_DIR" ]]; then + log_error "INFRA_DIR does not exist: $INFRA_DIR" + exit 1 + fi + log_info "Using infra dir: $INFRA_DIR" +else + echo "" + echo " No --infra-dir specified." + echo " Point to a local infra checkout (with pre-filled .env files) for auto-config," + echo " or leave blank to enter all values interactively." + echo "" + read -rp " Path to local infra repo [blank = interactive mode]: " infra_input + if [[ -n "$infra_input" ]]; then + infra_input="${infra_input/#\~/$HOME}" + if [[ -d "$infra_input" ]]; then + INFRA_DIR="$infra_input" + log_info "Using infra dir: $INFRA_DIR" + else + log_warn "Directory not found, continuing in interactive mode." + fi + fi +fi + +# ============================================================================ +# Module discovery +# Parallel arrays indexed by position: MODULES[i], ROLES[i], DESCS[i] +# MENU_MODS[menu_number] = module_path (1-based; index 0 = "") +# ============================================================================ +MODULES=() +ROLES=() +DESCS=() +MENU_MODS=("") # index 0 unused — menu numbers start at 1 + +discover_modules() { + while IFS= read -r deploy; do + local mod env_ex role desc r d + mod="$(dirname "$deploy" | sed "s|^$SCRIPT_DIR/services/||")" + env_ex="$SCRIPT_DIR/services/$mod/.env.example" + + role="any" + desc="$mod" + + if [[ -f "$env_ex" ]]; then + # grep returns 1 if no match — use || true to avoid pipefail exit + r="$(grep '^# role:' "$env_ex" | head -1 | sed 's/^# role: *//' || true)" + d="$(grep '^# description:' "$env_ex" | head -1 | sed 's/^# description: *//' || true)" + [[ -n "$r" ]] && role="$r" + [[ -n "$d" ]] && desc="$d" + fi + + MODULES+=("$mod") + ROLES+=("$role") + DESCS+=("$desc") + done < <(find "$SCRIPT_DIR/services" -name "deploy.sh" | sort) +} + +discover_modules + +# ============================================================================ +# Interactive menu (grouped by role) +# Populates MENU_MODS as a side effect. +# ============================================================================ +print_menu() { + local printed_any=0 + + for role_group in "vps" "homeserver" "any"; do + local label header_printed=0 + case "$role_group" in + vps) label="VPS services" ;; + homeserver) label="Home server services" ;; + any) label="Any machine" ;; + esac + + for i in "${!MODULES[@]}"; do + [[ "${ROLES[$i]}" != "$role_group" ]] && continue + + if [[ "$header_printed" -eq 0 ]]; then + echo " $label:" + header_printed=1 + printed_any=1 + fi + + local menu_idx=${#MENU_MODS[@]} + MENU_MODS+=("${MODULES[$i]}") + printf " [%2d] %-30s %s\n" "$menu_idx" "${MODULES[$i]}" "${DESCS[$i]}" + done + + [[ "$header_printed" -eq 1 ]] && echo "" + done + + if [[ "$printed_any" -eq 0 ]]; then + log_error "No deployable modules found." + exit 1 + fi +} + +print_menu + +echo " Enter module numbers to deploy (space-separated, e.g. \"1 3\")." +echo " Press Enter to select all, or type \"none\" to exit." +echo "" +read -rp " > " selection + +if [[ "$selection" == "none" ]]; then + echo " Exiting." + exit 0 +fi + +SELECTED_MODULES=() + +if [[ -z "$selection" ]]; then + SELECTED_MODULES=("${MODULES[@]}") +else + for num in $selection; do + if [[ "$num" =~ ^[0-9]+$ ]] \ + && [[ "$num" -gt 0 ]] \ + && [[ "$num" -lt "${#MENU_MODS[@]}" ]]; then + SELECTED_MODULES+=("${MENU_MODS[$num]}") + else + log_warn "Unknown module number: $num (skipped)" + fi + done +fi + +if [[ ${#SELECTED_MODULES[@]} -eq 0 ]]; then + log_error "No valid modules selected." + exit 1 +fi + +echo "" +log_info "Selected: ${SELECTED_MODULES[*]}" + +# ============================================================================ +# Env resolution +# Priority: +# 1. $INFRA_DIR/services/$mod/.env (pre-filled, use as-is) +# 2. $INFRA_DIR/services/$mod/.env.example (prompt, fill from infra template) +# 3. $SCRIPT_DIR/services/$mod/.env.example (prompt, fill from automa template) +# Prints the resolved .env path to stdout. +# ============================================================================ +fill_env_interactive() { + # All user-visible output → stderr so caller can capture stdout for the path. + local env_example="$1" + local output="$2" + + printf "" > "$output" + echo " Fill in values (Enter = accept default shown in [brackets]):" >&2 + echo "" >&2 + + while IFS= read -r line; do + if [[ "$line" =~ ^#\ (role|description): ]]; then + continue + fi + if [[ -z "$line" ]]; then + continue + fi + if [[ "$line" =~ ^# ]]; then + printf " %s\n" "$line" >&2 + continue + fi + + local key default hint val + key="${line%%=*}" + default="${line#*=}" + hint="" + + if [[ -n "$default" && "$default" != "your-"* && "$default" != "x.x.x.x" ]]; then + hint=" [$default]" + fi + + read -rp " $key$hint: " val <&0 + printf "%s=%s\n" "$key" "${val:-$default}" >> "$output" + done < "$env_example" +} + +resolve_env_for_module() { + # Prints resolved .env path to stdout; all messages → stderr. + local mod="$1" + local dry_run="${2:-0}" + local infra_mod_dir="" + + if [[ -n "$INFRA_DIR" ]]; then + infra_mod_dir="$INFRA_DIR/services/$mod" + fi + + # Case 1: pre-filled .env in infra — use directly + if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env" ]]; then + log_info " Using pre-filled .env from infra" >&2 + printf "%s" "$infra_mod_dir/.env" + return 0 + fi + + # Determine .env.example template source + local env_example="" + if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env.example" ]]; then + env_example="$infra_mod_dir/.env.example" + elif [[ -f "$SCRIPT_DIR/services/$mod/.env.example" ]]; then + env_example="$SCRIPT_DIR/services/$mod/.env.example" + else + log_error " No .env.example found for: $mod" >&2 + return 1 + fi + + # Dry-run: skip interactive fill, just return path to the example + if [[ "$dry_run" -eq 1 ]]; then + printf "%s" "$env_example" + return 0 + fi + + local tmp_env + tmp_env="$(mktemp /tmp/automa-env-XXXXXX)" + fill_env_interactive "$env_example" "$tmp_env" + printf "%s" "$tmp_env" +} + +# ============================================================================ +# Deployment +# ============================================================================ +DEPLOYED=() +FAILED=() + +for mod in "${SELECTED_MODULES[@]}"; do + echo "" + echo " ── $mod ──" + + local_env="" + if ! local_env="$(resolve_env_for_module "$mod" "$DRY_RUN")"; then + FAILED+=("$mod") + continue + fi + + deploy_script="$SCRIPT_DIR/services/$mod/deploy.sh" + if [[ ! -x "$deploy_script" ]]; then + log_error " deploy.sh not found or not executable: $deploy_script" + FAILED+=("$mod") + continue + fi + + if [[ "$DRY_RUN" -eq 1 ]]; then + log_info " [dry-run] $mod → $deploy_script (env: $local_env)" + DEPLOYED+=("$mod") + continue + fi + + if INFRA_DIR="$(dirname "$local_env")" bash "$deploy_script"; then + DEPLOYED+=("$mod") + else + log_error " Deployment failed: $mod" + FAILED+=("$mod") + fi +done + +# ============================================================================ +# Summary +# ============================================================================ +echo "" +echo " ┌─────────────────────────────────────┐" +echo " │ Summary │" +echo " └─────────────────────────────────────┘" + +if [[ ${#DEPLOYED[@]} -gt 0 ]]; then + echo "" + log_info "Deployed (${#DEPLOYED[@]}):" + for m in "${DEPLOYED[@]}"; do echo " ✓ $m"; done +fi + +if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "" + log_error "Failed (${#FAILED[@]}):" + for m in "${FAILED[@]}"; do echo " ✗ $m"; done + exit 1 +fi + +echo "" +log_info "Done."