From 986c3ecafa01a579184b725ee3c8092cdaa8494c Mon Sep 17 00:00:00 2001 From: Marcelo Santos <117441129+marcelo-m7@users.noreply.github.com> Date: Sun, 31 Aug 2025 10:46:14 +0100 Subject: [PATCH] feat(ci): local pre-commit hooks + fix bats suite + GH Actions green --- .editorconfig | 3 + .github/workflows/{ci.yml => lint_test.yml} | 11 +- .pre-commit-config.yaml | 26 ++ .shellcheckrc | 2 +- Makefile | 17 +- README.md | 28 +- bin/mona | 327 +++++++++++++------- lib/common.sh | 1 + modules/coolify/bootstrap.sh | 2 +- modules/docker/install.sh | 5 +- modules/installer/docker.sh | 14 +- modules/installer/github.sh | 15 +- modules/installer/wizard.sh | 110 ++++--- modules/network/nmcli.sh | 10 +- modules/observability/cadvisor.sh | 2 +- modules/observability/node_exporter.sh | 2 +- 16 files changed, 390 insertions(+), 185 deletions(-) rename .github/workflows/{ci.yml => lint_test.yml} (59%) create mode 100644 .pre-commit-config.yaml mode change 100644 => 100755 bin/mona mode change 100644 => 100755 modules/coolify/bootstrap.sh mode change 100644 => 100755 modules/installer/docker.sh mode change 100644 => 100755 modules/installer/github.sh mode change 100644 => 100755 modules/installer/wizard.sh mode change 100644 => 100755 modules/network/nmcli.sh mode change 100644 => 100755 modules/observability/cadvisor.sh mode change 100644 => 100755 modules/observability/node_exporter.sh diff --git a/.editorconfig b/.editorconfig index c2b058b..660dec9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,6 @@ indent_size = 2 [*.sh] indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/lint_test.yml similarity index 59% rename from .github/workflows/ci.yml rename to .github/workflows/lint_test.yml index 1991c0e..cff3c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/lint_test.yml @@ -1,4 +1,4 @@ -name: CI +name: lint_test on: push: pull_request: @@ -10,15 +10,14 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install tools + - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y shellcheck curl - git clone https://github.com/bats-core/bats-core /tmp/bats - sudo /tmp/bats/install.sh /usr/local + sudo apt-get install -y shellcheck shfmt bats curl + pip install pre-commit editorconfig-checker - name: Lint run: make lint - - name: Tests + - name: Test env: MONA_NONINTERACTIVE: "1" run: make test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..94ce7d4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +default_install_hook_types: [pre-commit, pre-push] +default_stages: [commit, push] +repos: + - repo: local + hooks: + - id: shfmt + name: shfmt + entry: shfmt -i 2 -ci -sr + language: system + files: \.sh$ + - id: shellcheck + name: shellcheck + entry: shellcheck -x + language: system + files: \.sh$ + - id: editorconfig-checker + name: editorconfig-checker + entry: ec + language: system + pass_filenames: false + - id: bats + name: bats + entry: make test + language: system + pass_filenames: false + stages: [push] diff --git a/.shellcheckrc b/.shellcheckrc index bd5c0d1..d72aded 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -1,3 +1,3 @@ # Be strict but allow some practical patterns. -disable=SC1090,SC2155 +disable=SC1090,SC2155,SC1091,SC2317,SC2031 external-sources=true diff --git a/Makefile b/Makefile index 27ee41c..f8c4ea8 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,22 @@ SHELL := /bin/bash -.PHONY: lint test ci +.PHONY: install-dev fmt lint test ci + +install-dev: + sudo apt-get update -y + sudo apt-get install -y shellcheck shfmt bats curl + pip install --upgrade pre-commit editorconfig-checker + pre-commit install --install-hooks + pre-commit install --hook-type pre-push --install-hooks + +fmt: + shfmt -w bin modules recipes tests lint: - @command -v shellcheck >/dev/null || { echo 'Install shellcheck'; exit 1; } - shellcheck -x bin/mona modules/**/*.sh || true + shellcheck -x bin/mona modules/**/*.sh + shfmt -d bin modules recipes tests test: - @command -v bats >/dev/null || { echo 'Install bats-core'; exit 1; } bats -r tests ci: lint test diff --git a/README.md b/README.md index 099a76f..09bb365 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,36 @@ Use this as the starting point for Codex/agents to extend modules and recipes. # clone git clone monarepo && cd monarepo -# (optional) add simple curses as submodule (or rely on runtime downloader in bin/mona) -git submodule add https://github.com/metal3d/bashsimplecurses ui/bashsimplecurses -git submodule update --init --recursive +# install development dependencies and git hooks +make install-dev + +# run full test suite +make test # run TUI (recommended with sudo/root) sudo ./bin/mona ``` +### Local hooks + +This repository uses [pre-commit](https://pre-commit.com/) hooks. Formatting +(`shfmt`) and linting (`shellcheck`, `editorconfig-checker`) run on every +commit. The BATS tests run automatically on `git push`. Hooks are installed by +`make install-dev`. You can manually trigger them for all files with: + +```bash +pre-commit run --all-files +``` + +### Run CI tasks locally + +The same checks used in GitHub Actions can be executed locally: + +```bash +make lint +make test +``` + ### Non-interactive/CI flags - `--version` : prints version and exits - `--help` : prints help and exits diff --git a/bin/mona b/bin/mona old mode 100644 new mode 100755 index 991f190..7b6312a --- a/bin/mona +++ b/bin/mona @@ -2,19 +2,21 @@ # MonaRepo — Interactive TUI bootstrapper set -euo pipefail +# Determine repo root early so other scripts can source helpers safely +export MONA_BIN_PATH="$(readlink -f "$0")" +export MONA_DIR="$(cd "$(dirname "$MONA_BIN_PATH")/.." && pwd)" + # shellcheck source=../lib/common.sh -. "${MONA_DIR}/lib/common.sh" +. "$MONA_DIR/lib/common.sh" VERSION="0.4.0" # ------------------------------ Globals ------------------------------------ -export MONA_BIN_PATH="$(readlink -f "$0")" -export MONA_DIR="$(cd "$(dirname "$MONA_BIN_PATH")/.." && pwd)" export MONA_DRY_RUN="${MONA_DRY_RUN:-false}" export MONA_NONINTERACTIVE="${MONA_NONINTERACTIVE:-0}" # Arg parsing -show_help(){ +show_help() { cat </dev/null 2>&1; then curl -fsSL "https://raw.githubusercontent.com/metal3d/bashsimplecurses/master/simple_curses.sh" \ -o "$dst_dir/simple_curses.sh" || true @@ -57,7 +66,8 @@ if [[ ! -f "$SIMPLE_CURSES" ]]; then fi # ------------------------------- ASCII art ---------------------------------- -ascii_heart(){ cat <<'HEART' +ascii_heart() { + cat <<'HEART' ***** ***** *********** *********** *************************** @@ -88,48 +98,72 @@ MENU=( ) SEL=0 -list_recipes(){ - local rdir="${MONA_DIR}/recipes"; mkdir -p "$rdir" +list_recipes() { + local rdir="${MONA_DIR}/recipes" + mkdir -p "$rdir" find "$rdir" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' 2>/dev/null | sort } # ------------------------------ Flows --------------------------------------- -flow_quickstart(){ needs_root; echo; log "Quick Start"; echo +flow_quickstart() { + needs_root + echo + log "Quick Start" + echo flow_update_distro || true echo - log "Quick Start concluído."; pause_any + log "Quick Start concluído." + pause_any } -flow_update_distro(){ needs_root; echo; log "Atualização da distro"; echo - local pm; pm=$(pm_detect) +flow_update_distro() { + needs_root + echo + log "Atualização da distro" + echo + local pm + pm=$(pm_detect) case "$pm" in - apt) - apply "apt-get update -y" - apply "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y" - apply "apt-get autoremove -y" - ;; - dnf|yum) - apply "$pm -y upgrade" - ;; - pacman) - apply "pacman -Syu --noconfirm" - ;; - zypper) - apply "zypper --non-interactive refresh" - apply "zypper --non-interactive update" - ;; - *) - err "SO não suportado para upgrade."; return 1;; + apt) + apply "apt-get update -y" + apply "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y" + apply "apt-get autoremove -y" + ;; + dnf | yum) + apply "$pm -y upgrade" + ;; + pacman) + apply "pacman -Syu --noconfirm" + ;; + zypper) + apply "zypper --non-interactive refresh" + apply "zypper --non-interactive update" + ;; + *) + err "SO não suportado para upgrade." + return 1 + ;; esac - log "Upgrade finalizado."; pause_any + log "Upgrade finalizado." + pause_any } -flow_config_users(){ needs_root; echo; log "Configurar usuários e SSH"; echo +flow_config_users() { + needs_root + echo + log "Configurar usuários e SSH" + echo read -r -p "Novo usuário (ex.: mona): " username || true - [[ -n "${username:-}" ]] || { err "Usuário inválido"; return 1; } - read -r -p "Conceder sudo? [y/N]: " grant_sudo; grant_sudo=${grant_sudo:-N} - read -r -p "Adicionar ao grupo docker (se existir)? [y/N]: " add_docker; add_docker=${add_docker:-N} - read -r -p "Shell (padrão /bin/bash): " user_shell; user_shell=${user_shell:-/bin/bash} + [[ -n "${username:-}" ]] || { + err "Usuário inválido" + return 1 + } + read -r -p "Conceder sudo? [y/N]: " grant_sudo + grant_sudo=${grant_sudo:-N} + read -r -p "Adicionar ao grupo docker (se existir)? [y/N]: " add_docker + add_docker=${add_docker:-N} + read -r -p "Shell (padrão /bin/bash): " user_shell + user_shell=${user_shell:-/bin/bash} read -r -p "Cole a chave pública SSH (ENTER para pular): " ssh_pub || true if id -u "$username" >/dev/null 2>&1; then @@ -157,47 +191,67 @@ flow_config_users(){ needs_root; echo; log "Configurar usuários e SSH"; echo apply "sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config" apply "systemctl reload ssh || systemctl reload sshd || true" fi - log "Usuário/SSH configurados."; pause_any + log "Usuário/SSH configurados." + pause_any } -flow_config_hosts(){ needs_root; echo; log "Configurar hostname e /etc/hosts"; echo +flow_config_hosts() { + needs_root + echo + log "Configurar hostname e /etc/hosts" + echo read -r -p "Hostname (ex.: monynha-online-prod): " new_host || true - [[ -n "${new_host:-}" ]] || { err "Hostname inválido"; return 1; } + [[ -n "${new_host:-}" ]] || { + err "Hostname inválido" + return 1 + } read -r -p "FQDN (ex.: monynha.online): " new_fqdn || true apply "hostnamectl set-hostname '$new_host'" # /etc/hosts minimal if [[ -f /etc/hosts ]]; then apply "cp /etc/hosts /etc/hosts.bak.mona.$(date +%s)" if ! $MONA_DRY_RUN; then - awk -v h="$new_host" '!( ($2==h) || ($2 ~ h"\\.") )' /etc/hosts > /etc/hosts.mona || true + awk -v h="$new_host" '!( ($2==h) || ($2 ~ h"\\.") )' /etc/hosts >/etc/hosts.mona || true mv /etc/hosts.mona /etc/hosts || true fi apply "bash -lc 'echo 127.0.1.1 \"$new_host\" \"${new_host}.${new_fqdn:-localdomain}\" >> /etc/hosts'" fi - log "Hostname/hosts configurados."; pause_any + log "Hostname/hosts configurados." + pause_any } -flow_config_network(){ needs_root; echo; log "Configurar rede"; echo - local os_like=""; os_like=$( ( . /etc/os-release 2>/dev/null; echo "${ID_LIKE:-$ID}" ) || true ) +flow_config_network() { + needs_root + echo + log "Configurar rede" + echo + local os_like="" + os_like=$( ( + . /etc/os-release 2>/dev/null + echo "${ID_LIKE:-$ID}" + ) || true) if [[ "$os_like" == *"debian"* || "$os_like" == *"ubuntu"* ]]; then if command -v netplan >/dev/null 2>&1; then read -r -p "Interface (ex.: eth0): " ifc || true - read -r -p "Usar DHCP? [Y/n]: " use_dhcp; use_dhcp=${use_dhcp:-Y} + read -r -p "Usar DHCP? [Y/n]: " use_dhcp + use_dhcp=${use_dhcp:-Y} local netplan_yaml if [[ "${use_dhcp^^}" == Y* ]]; then - netplan_yaml=$(cat </dev/null || true" - if ! $MONA_DRY_RUN; then printf "%s\n" "$netplan_yaml" > /etc/netplan/99-mona.yaml; fi + if ! $MONA_DRY_RUN; then printf "%s\n" "$netplan_yaml" >/etc/netplan/99-mona.yaml; fi apply "netplan apply" log "Netplan aplicado." else @@ -237,23 +294,43 @@ YAML pause_any } -flow_run_recipe(){ needs_root; echo; log "Executar receita"; echo - local recipes; IFS=$'\n' read -r -d '' -a recipes < <(list_recipes && printf '\0') || true - if ((${#recipes[@]}==0)); then warn "Nenhuma receita encontrada em recipes/"; pause_any; return; fi - local i=1; for r in "${recipes[@]}"; do printf "%2d) %s\n" "$i" "$r"; ((i++)); done +flow_run_recipe() { + needs_root + echo + log "Executar receita" + echo + local recipes + IFS=$'\n' read -r -d '' -a recipes < <(list_recipes && printf '\0') || true + if ((${#recipes[@]} == 0)); then + warn "Nenhuma receita encontrada em recipes/" + pause_any + return + fi + local i=1 + for r in "${recipes[@]}"; do + printf "%2d) %s\n" "$i" "$r" + ((i++)) + done read -r -p "Escolha: " idx || true - if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx>=1 && idx<=${#recipes[@]} )); then - local choice="${recipes[idx-1]}" + if [[ "$idx" =~ ^[0-9]+$ ]] && ((idx >= 1 && idx <= ${#recipes[@]})); then + local choice="${recipes[idx - 1]}" log "Rodando recipes/$choice" if $MONA_DRY_RUN; then - log "(dry-run) bash \"${MONA_DIR}/recipes/${choice}\""; pause_any + log "(dry-run) bash \"${MONA_DIR}/recipes/${choice}\"" + pause_any else - bash "${MONA_DIR}/recipes/${choice}" || { err "Receita falhou"; pause_any; } + bash "${MONA_DIR}/recipes/${choice}" || { + err "Receita falhou" + pause_any + } fi fi } -flow_about(){ echo; log "MonaRepo — Monynha Softwares"; echo +flow_about() { + echo + log "MonaRepo — Monynha Softwares" + echo echo "Versão: $VERSION" echo "Repo: monarepo (base)" echo "UI: bashsimplecurses (vendorizado via submodule ou runtime download)" @@ -261,8 +338,7 @@ flow_about(){ echo; log "MonaRepo — Monynha Softwares"; echo pause_any } - -flow_project_installer(){ +flow_project_installer() { # shellcheck source=../modules/installer/wizard.sh . "${MONA_DIR}/modules/installer/wizard.sh" wizard_start @@ -271,70 +347,99 @@ flow_project_installer(){ # ---------------------------- TUI (Curses) ---------------------------------- SEPARATOR="────────────────────────────────────────────────────────────" -header_window(){ +header_window() { window " " "magenta" "100%" "center" - append "$(printf '\e[35m❤\e[0m') $(printf '\e[1mMonynha Softwares\e[0m') — $(printf '\e[1mMonaRepo\e[0m')" - addsep - while IFS= read -r line; do append " ${line}"; done < <(ascii_heart) + append "$(printf '\e[35m❤\e[0m') $(printf '\e[1mMonynha Softwares\e[0m') — $(printf '\e[1mMonaRepo\e[0m')" + addsep + while IFS= read -r line; do append " ${line}"; done < <(ascii_heart) endwin } -menu_window(){ +menu_window() { window "Escolha um fluxo / configuração" "blue" "100%" - local i - for i in "${!MENU[@]}"; do - if (( i == SEL )); then - append " → $(printf '\e[32m%s\e[0m' "${MENU[$i]}")" - else - append " ${MENU[$i]}" - fi - done - addsep - append "Use ↑/↓ para navegar, Enter para escolher, Q para sair." + local i + for i in "${!MENU[@]}"; do + if ((i == SEL)); then + append " → $(printf '\e[32m%s\e[0m' "${MENU[$i]}")" + else + append " ${MENU[$i]}" + fi + done + addsep + append "Use ↑/↓ para navegar, Enter para escolher, Q para sair." endwin } -status_window(){ +status_window() { window "Status" "yellow" "100%" - append "Host: $(hostname) | Usuário: $(whoami) | Dry-run: $MONA_DRY_RUN" - append "$SEPARATOR" + append "Host: $(hostname) | Usuário: $(whoami) | Dry-run: $MONA_DRY_RUN" + append "$SEPARATOR" endwin } -main(){ +main() { header_window menu_window status_window } -readKey(){ local k; IFS= read -rsn1 -t 0.2 k || true +readKey() { + local k + IFS= read -rsn1 -t 0.2 k || true if [[ "$k" == $'\e' ]]; then - local k2 k3; IFS= read -rsn1 -t 0.001 k2 || true; IFS= read -rsn1 -t 0.001 k3 || true + local k2 k3 + IFS= read -rsn1 -t 0.001 k2 || true + IFS= read -rsn1 -t 0.001 k3 || true k+="$k2$k3" fi printf '%s' "$k" } -update(){ - local key; key=$(readKey) +update() { + local key + key=$(readKey) case "$key" in - $'\e[A') (( SEL>0 )) && ((SEL--));; # Up - $'\e[B') (( SEL<${#MENU[@]}-1 )) && ((SEL++));; # Down - '') ;; - q|Q) exit 0;; - $'\n') - case "$SEL" in - 0) clear; flow_quickstart;; - 1) clear; flow_update_distro;; - 2) clear; flow_config_network;; - 3) clear; flow_config_users;; - 4) clear; flow_config_hosts;; - 5) clear; flow_run_recipe;; - 6) clear; flow_project_installer;; - 7) clear; flow_about;; - 8) exit 0;; - esac + $'\e[A') ((SEL > 0)) && ((SEL--)) ;; # Up + $'\e[B') ((SEL < ${#MENU[@]} - 1)) && ((SEL++)) ;; # Down + '') ;; + q | Q) exit 0 ;; + $'\n') + case "$SEL" in + 0) + clear + flow_quickstart + ;; + 1) + clear + flow_update_distro + ;; + 2) + clear + flow_config_network + ;; + 3) + clear + flow_config_users + ;; + 4) + clear + flow_config_hosts + ;; + 5) + clear + flow_run_recipe + ;; + 6) + clear + flow_project_installer + ;; + 7) + clear + flow_about ;; + 8) exit 0 ;; + esac + ;; esac } @@ -359,12 +464,12 @@ else echo "1) Quick Start 2) Upgrade 3) Rede 4) Users/SSH 5) Hosts 6) Receita 0) Sair" read -r -p "Escolha: " ch || true case "$ch" in - 1) flow_quickstart;; - 2) flow_update_distro;; - 3) flow_config_network;; - 4) flow_config_users;; - 5) flow_config_hosts;; - 6) flow_run_recipe;; - *) exit 0;; + 1) flow_quickstart ;; + 2) flow_update_distro ;; + 3) flow_config_network ;; + 4) flow_config_users ;; + 5) flow_config_hosts ;; + 6) flow_run_recipe ;; + *) exit 0 ;; esac fi diff --git a/lib/common.sh b/lib/common.sh index 31624e3..48a3a84 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # Common helpers for MonaRepo set -euo pipefail diff --git a/modules/coolify/bootstrap.sh b/modules/coolify/bootstrap.sh old mode 100644 new mode 100755 index e9d3026..f73dd4f --- a/modules/coolify/bootstrap.sh +++ b/modules/coolify/bootstrap.sh @@ -3,7 +3,7 @@ set -euo pipefail # shellcheck source=../../lib/common.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" -run_coolify_bootstrap(){ +run_coolify_bootstrap() { needs_root # Pasta padrão para Coolify deployments/volumes local base="/data/coolify" diff --git a/modules/docker/install.sh b/modules/docker/install.sh index 5a53706..e07c366 100644 --- a/modules/docker/install.sh +++ b/modules/docker/install.sh @@ -11,7 +11,10 @@ run_docker_install() { install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ -https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $VERSION_CODENAME) stable" \ +https://download.docker.com/linux/ubuntu $( + . /etc/os-release + echo $VERSION_CODENAME + ) stable" \ >/etc/apt/sources.list.d/docker.list apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin diff --git a/modules/installer/docker.sh b/modules/installer/docker.sh old mode 100644 new mode 100755 index 6099269..e49db67 --- a/modules/installer/docker.sh +++ b/modules/installer/docker.sh @@ -3,10 +3,13 @@ set -euo pipefail # shellcheck source=../../lib/common.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" -docker_pull_image(){ +docker_pull_image() { needs_root local image="$1" - [[ -n "$image" ]] || { err "Informe a imagem (ex.: nginx:latest)"; return 1; } + [[ -n "$image" ]] || { + err "Informe a imagem (ex.: nginx:latest)" + return 1 + } if ! command -v docker >/dev/null 2>&1; then err "Docker não instalado. Rode modules/docker/install.sh" return 1 @@ -14,10 +17,13 @@ docker_pull_image(){ apply "docker pull '$image'" } -docker_run_quick(){ +docker_run_quick() { needs_root local image="$1" name="${2:-mona-app}" port="${3:-}" - [[ -n "$image" ]] || { err "Informe a imagem (ex.: nginx:latest)"; return 1; } + [[ -n "$image" ]] || { + err "Informe a imagem (ex.: nginx:latest)" + return 1 + } local port_flag="" [[ -n "$port" ]] && port_flag="-p $port" if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then diff --git a/modules/installer/github.sh b/modules/installer/github.sh old mode 100644 new mode 100755 index db6f903..c4867c6 --- a/modules/installer/github.sh +++ b/modules/installer/github.sh @@ -5,7 +5,7 @@ set -euo pipefail GITHUB_API="https://api.github.com" -gh_list_org_repos(){ +gh_list_org_repos() { local org="${1:-Monynha-Softwares}" local per_page="${2:-100}" if ! command -v curl >/dev/null 2>&1; then @@ -15,11 +15,16 @@ gh_list_org_repos(){ # Nota: sem jq; parse simples de nomes. local hdr=() [[ -n "${GITHUB_TOKEN:-}" ]] && hdr+=(-H "Authorization: Bearer $GITHUB_TOKEN") - curl -fsSL "${hdr[@]}" "$GITHUB_API/orgs/$org/repos?per_page=$per_page" 2>/dev/null | \ + curl -fsSL "${hdr[@]}" "$GITHUB_API/orgs/$org/repos?per_page=$per_page" 2>/dev/null | + grep -o '"full_name"[^,]*' | cut -d '"' -f4 +} -gh_clone(){ +gh_clone() { local repo_url="$1" dest_dir="$2" branch="${3:-}" - [[ -n "$repo_url" && -n "$dest_dir" ]] || { err "Uso: gh_clone [branch]"; return 1; } + [[ -n "$repo_url" && -n "$dest_dir" ]] || { + err "Uso: gh_clone [branch]" + return 1 + } if [[ -d "$dest_dir/.git" ]]; then log "Repo já existe: $dest_dir ; fazendo pull --ff-only" $MONA_DRY_RUN || git -C "$dest_dir" pull --ff-only @@ -32,7 +37,7 @@ gh_clone(){ fi } -show_readme(){ +show_readme() { local dir="$1" [[ -n "$dir" ]] || return 0 local f="" diff --git a/modules/installer/wizard.sh b/modules/installer/wizard.sh old mode 100644 new mode 100755 index f56a600..b649ad7 --- a/modules/installer/wizard.sh +++ b/modules/installer/wizard.sh @@ -9,13 +9,13 @@ set -euo pipefail PROJECTS_BASE="${PROJECTS_BASE:-/opt/mona-projects}" -ensure_projects_dir(){ +ensure_projects_dir() { needs_root apply "mkdir -p '$PROJECTS_BASE'" } # Try to source simple_curses if TUI reader enabled -maybe_source_curses(){ +maybe_source_curses() { local sc="${MONA_DIR:-}/ui/bashsimplecurses/simple_curses.sh" if [[ "${MONA_USE_TUI_READER:-0}" == "1" && -f "$sc" ]]; then # shellcheck disable=SC1090 @@ -25,9 +25,12 @@ maybe_source_curses(){ return 1 } -tui_view_file(){ +tui_view_file() { local file="$1" - [[ -f "$file" ]] || { warn "Arquivo não encontrado: $file"; return 1; } + [[ -f "$file" ]] || { + warn "Arquivo não encontrado: $file" + return 1 + } if ! maybe_source_curses; then if command -v less >/dev/null 2>&1; then less -R "$file"; else cat "$file"; fi pause_any @@ -39,38 +42,44 @@ tui_view_file(){ local max=${#lines[@]} local page=20 - _draw(){ + _draw() { window "README (TUI) — ↑/↓ rola, Q sai" "blue" "100%" - local i; local end=$(( top + page )) - (( end > max )) && end=$max - for (( i=top; i max)) && end=$max + for ((i = top; i < end; i++)); do + append "${lines[$i]}" + done + addsep + append "Linhas: $((top + 1))–$end de $max" endwin } - main(){ _draw; } - readKey(){ local k; IFS= read -rsn1 -t 0.2 k || true + main() { _draw; } + readKey() { + local k + IFS= read -rsn1 -t 0.2 k || true if [[ "$k" == $'\e' ]]; then - local k2 k3; IFS= read -rsn1 -t 0.001 k2 || true; IFS= read -rsn1 -t 0.001 k3 || true + local k2 k3 + IFS= read -rsn1 -t 0.001 k2 || true + IFS= read -rsn1 -t 0.001 k3 || true k+="$k2$k3" fi printf '%s' "$k" } - update(){ - local key; key=$(readKey) + update() { + local key + key=$(readKey) case "$key" in - $'\e[A') (( top>0 )) && ((top--));; - $'\e[B') (( top+page 0)) && ((top--)) ;; + $'\e[B') ((top + page < max)) && ((top++)) ;; + q | Q) return 1 ;; esac } main_loop -t 1 || true } -compose_cmd_for(){ +compose_cmd_for() { local dir="$1" if command -v docker >/dev/null 2>&1; then if docker compose version >/dev/null 2>&1; then @@ -86,35 +95,41 @@ compose_cmd_for(){ return 1 } -env_interactive_from_example(){ +env_interactive_from_example() { local dir="$1" local src="$dir/.env.example" local dst="$dir/.env" - [[ -f "$src" ]] || { err ".env.example não encontrado"; return 1; } + [[ -f "$src" ]] || { + err ".env.example não encontrado" + return 1 + } echo "Gerando $dst a partir de $src (interativo). ENTER mantém o default, espaço em branco mantém vazio." [[ -f "$dst" ]] && cp "$dst" "$dst.bak.mona.$(date +%s)" - : > "$dst" + : >"$dst" while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" =~ ^\s*# ]] || [[ -z "$line" ]]; then - echo "$line" >> "$dst" + echo "$line" >>"$dst" continue fi # parse KEY=VALUE (tira aspas simples/duplas do default) - key="${line%%=*}"; def="${line#*=}" + key="${line%%=*}" + def="${line#*=}" def="${def%$'\r'}" - def="${def%\"}"; def="${def#\"}"; def="${def%\'}"; def="${def#\'}" + def="${def%\"}" + def="${def#\"}" + def="${def%\'}" + def="${def#\'}" read -r -p "$key [${def}]: " val if [[ -z "${val}" ]]; then val="${def}" fi - printf "%s=%s\n" "$key" "$val" >> "$dst" - done < "$src" + printf "%s=%s\n" "$key" "$val" >>"$dst" + done <"$src" log ".env gerado: $dst" } - # discover actions in a project checkout -discover_actions(){ +discover_actions() { local dir="$1" ACTIONS=() ACTION_CMDS=() @@ -193,14 +208,14 @@ discover_actions(){ fi } -run_actions_menu(){ +run_actions_menu() { local dir="$1" discover_actions "$dir" if [[ "${MONA_USE_TUI_READER:-0}" == "1" ]]; then tui_view_file "$dir/README.md" || true; else show_readme "$dir" || true; fi show_readme "$dir" || true - if ((${#ACTIONS[@]}==0)); then + if ((${#ACTIONS[@]} == 0)); then warn "Nenhuma ação automática detectada. Consulte o README e docs do projeto." pause_any return 0 @@ -214,8 +229,8 @@ run_actions_menu(){ done echo " 0) Voltar" read -r -p "Escolha uma ação para executar: " idx || true - if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx>=1 && idx<=${#ACTIONS[@]} )); then - local cmd="${ACTION_CMDS[idx-1]}" + if [[ "$idx" =~ ^[0-9]+$ ]] && ((idx >= 1 && idx <= ${#ACTIONS[@]})); then + local cmd="${ACTION_CMDS[idx - 1]}" local logfile="${MONA_LOG_DIR:-$HOME/.mona/logs}" mkdir -p "$logfile" logfile="$logfile/$(basename "$dir")-$(date +%Y%m%d-%H%M%S).log" @@ -230,20 +245,20 @@ run_actions_menu(){ fi } -wizard_start(){ +wizard_start() { echo "Tipo de instalação" echo " 1) GitHub (clonar repositório)" echo " 2) Docker (pull/run)" echo " 0) Voltar" read -r -p "Escolha: " choice || true case "$choice" in - 1) wizard_github ;; - 2) wizard_docker ;; - *) return 0 ;; + 1) wizard_github ;; + 2) wizard_docker ;; + *) return 0 ;; esac } -wizard_github(){ +wizard_github() { ensure_projects_dir read -r -p "Org/Usuário (ENTER p/ Monynha-Softwares): " org org="${org:-Monynha-Softwares}" @@ -251,22 +266,29 @@ wizard_github(){ gh_list_org_repos "$org" || true read -r -p "Informe URL do repositório (ou apenas nome para usar https://github.com/$org/NOME): " input - [[ -n "$input" ]] || { err "Entrada inválida"; return 1; } + [[ -n "$input" ]] || { + err "Entrada inválida" + return 1 + } local repo_url="$input" if [[ "$input" != http* ]]; then repo_url="https://github.com/$org/$input" fi read -r -p "Branch (ENTER para padrão): " branch - local name="${repo_url##*/}"; name="${name%.git}" + local name="${repo_url##*/}" + name="${name%.git}" local dest="${PROJECTS_BASE}/${name}" echo "Destino: $dest" gh_clone "$repo_url" "$dest" "$branch" run_actions_menu "$dest" } -wizard_docker(){ +wizard_docker() { read -r -p "Imagem (ex.: nginx:latest): " image - [[ -n "${image:-}" ]] || { err "Imagem inválida"; return 1; } + [[ -n "${image:-}" ]] || { + err "Imagem inválida" + return 1 + } read -r -p "Nome do container (ENTER p/ mona-app): " name name="${name:-mona-app}" read -r -p "Mapear porta (ex.: 8080:80) [ENTER para pular]: " port diff --git a/modules/network/nmcli.sh b/modules/network/nmcli.sh old mode 100644 new mode 100755 index 193c01b..cab87d1 --- a/modules/network/nmcli.sh +++ b/modules/network/nmcli.sh @@ -3,7 +3,7 @@ set -euo pipefail # shellcheck source=../../lib/common.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" -configure_nmcli(){ +configure_nmcli() { needs_root if ! command -v nmcli >/dev/null 2>&1; then err "nmcli não encontrado. Instale NetworkManager ou use Netplan." @@ -11,9 +11,13 @@ configure_nmcli(){ fi read -r -p "Interface (ex.: eth0): " ifc || true - [[ -n "${ifc:-}" ]] || { err "Interface inválida"; return 1; } + [[ -n "${ifc:-}" ]] || { + err "Interface inválida" + return 1 + } - read -r -p "Usar DHCP? [Y/n]: " use_dhcp; use_dhcp=${use_dhcp:-Y} + read -r -p "Usar DHCP? [Y/n]: " use_dhcp + use_dhcp=${use_dhcp:-Y} if [[ "${use_dhcp^^}" == Y* ]]; then apply "nmcli con mod \"$ifc\" ipv4.method auto || nmcli con add type ethernet ifname \"$ifc\" con-name \"$ifc\" ipv4.method auto" else diff --git a/modules/observability/cadvisor.sh b/modules/observability/cadvisor.sh old mode 100644 new mode 100755 index 33fd7fa..32fe1f7 --- a/modules/observability/cadvisor.sh +++ b/modules/observability/cadvisor.sh @@ -3,7 +3,7 @@ set -euo pipefail # shellcheck source=../../lib/common.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" -run_cadvisor(){ +run_cadvisor() { needs_root if ! command -v docker >/dev/null 2>&1; then err "Docker não instalado. Rode modules/docker/install.sh" diff --git a/modules/observability/node_exporter.sh b/modules/observability/node_exporter.sh old mode 100644 new mode 100755 index 4579d34..0e14e1e --- a/modules/observability/node_exporter.sh +++ b/modules/observability/node_exporter.sh @@ -3,7 +3,7 @@ set -euo pipefail # shellcheck source=../../lib/common.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" -run_node_exporter(){ +run_node_exporter() { needs_root if ! command -v docker >/dev/null 2>&1; then err "Docker não instalado. Rode modules/docker/install.sh"