From a3cf3f6fbacea85d94ce3d7232b0b879e5bb6c62 Mon Sep 17 00:00:00 2001 From: Marcelo Santos Date: Sun, 31 Aug 2025 10:09:39 +0100 Subject: [PATCH] Add project installer wizard and GitHub integration Introduces new modules for project installation via GitHub and Docker, including a TUI README viewer, interactive .env creation, and automatic action detection for common post-clone tasks. Updates documentation to describe installer features and roadmap, bumps version to 0.4.0, and adds a Bats test for the env_interactive_from_example function. --- AGENTS.md | 17 +++ README.md | 19 +++ bin/mona | 4 + modules/installer/github.sh | 54 +++++++ modules/installer/wizard.sh | 272 ++++++++++++++++++++++++++++++++++++ tests/installer_env.bats | 9 ++ 6 files changed, 375 insertions(+) create mode 100644 modules/installer/github.sh create mode 100644 modules/installer/wizard.sh create mode 100644 tests/installer_env.bats diff --git a/AGENTS.md b/AGENTS.md index fc368de..368cc5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,3 +28,20 @@ Extend MonaRepo by adding **modules** (idempotent bash units) and **recipes** (o - `set -euo pipefail` at top of every script - Use `trap` for cleanup if creating temp files - Prefer POSIX sh where feasible; when not, document Bashisms. +<<<<<<< Updated upstream +======= + + +## New Track: Project Installer +- Enhance GitHub listing (paginate, filter by forks/archived, support PAT if available). +- Teach the wizard to parse `README.md` for “Quick Start” sections and propose actions accordingly. +- Add TUI page viewer for README (fallback to `less`). +- Extend docker path with compose files and .env handling. + + +### Installer roadmap (next tasks) +- Parse `mona.yaml` playbooks (YAML subset) to define guided actions (title, command, confirm, rollback). +- Add `.env` schema validator (required keys, default values). +- Enhance README parser to auto-propose actions from “Quick Start” sections. +- Add multi-host inventory integration to run the same installer on several servers via SSH. +>>>>>>> Stashed changes diff --git a/README.md b/README.md index bed11eb..725ff2f 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,22 @@ MIT — see [LICENSE](LICENSE). - `recipes/coolify-node.sh` — base + docker + coolify bootstrap (network/dirs) - `recipes/monitoring-node.sh` — base + docker + node_exporter + cAdvisor - `recipes/supabase-node.sh` — base + docker (hook para Supabase) +<<<<<<< Updated upstream +======= + + +## Project Installer (Wizard) +Use the TUI option **“Instalar/Configurar Projeto (GitHub/Docker)”** to: +- List or select a GitHub repo (Monynha-Softwares by default) and clone it +- Or pull/run a Docker image quickly +- Then MonaRepo detects common post‑clone actions (e.g., copy `.env.example`, `docker compose up -d`, `npm/pnpm/yarn install`, `make setup`, `supabase start`) and lets you run them step‑by‑step while logging output. + +Logs are stored in `~/.mona/logs` (or `$MONA_LOG_DIR`). `--dry-run` prints commands without executing. + + +### Installer extras (v0.4.0) +- **GitHub PAT**: set `GITHUB_TOKEN` to avoid rate limits and access private repos. +- **Interactive `.env`**: when a project has `.env.example`, choose “Criar .env interativo” to fill values key-by-key. +- **Docker Compose advanced**: auto-detects `docker compose` (plugin) or `docker-compose`; offers `pull`, `up -d --remove-orphans`, `ps`, `logs -f`. +- **TUI README viewer** (experimental): set `MONA_USE_TUI_READER=1` to view README inside the TUI; otherwise falls back to `less`. +>>>>>>> Stashed changes diff --git a/bin/mona b/bin/mona index 57fc9a2..1cbabde 100644 --- a/bin/mona +++ b/bin/mona @@ -5,7 +5,11 @@ set -euo pipefail # shellcheck source=../lib/common.sh . "${MONA_DIR}/lib/common.sh" +<<<<<<< Updated upstream VERSION="0.2.0" +======= +VERSION="0.4.0" +>>>>>>> Stashed changes # ------------------------------ Globals ------------------------------------ export MONA_BIN_PATH="$(readlink -f "$0")" diff --git a/modules/installer/github.sh b/modules/installer/github.sh new file mode 100644 index 0000000..608c627 --- /dev/null +++ b/modules/installer/github.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail +# shellcheck source=../../lib/common.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" + +GITHUB_API="https://api.github.com" + +gh_list_org_repos(){ + local org="${1:-Monynha-Softwares}" + local per_page="${2:-100}" + if ! command -v curl >/dev/null 2>&1; then + err "curl não encontrado para consultar GitHub API." + return 1 + fi + # 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 | \ + grep -E '^\s*"name":\s*"' | sed -E 's/^\s*"name":\s*"([^"]+)".*/\1/' | sort -f -u +} + +gh_clone(){ + local repo_url="$1" dest_dir="$2" branch="${3:-}" + [[ -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 + else + if [[ -n "$branch" ]]; then + apply "git clone --branch '$branch' --depth 1 '$repo_url' '$dest_dir'" + else + apply "git clone --depth 1 '$repo_url' '$dest_dir'" + fi + fi +} + +show_readme(){ + local dir="$1" + [[ -n "$dir" ]] || return 0 + local f="" + for f in README.md README.txt README; do + if [[ -f "$dir/$f" ]]; then + if command -v less >/dev/null 2>&1; then + less -R "$dir/$f" + else + cat "$dir/$f" + pause_any + fi + return 0 + fi + done + warn "README não encontrado em $dir" + return 0 +} diff --git a/modules/installer/wizard.sh b/modules/installer/wizard.sh new file mode 100644 index 0000000..db6e68a --- /dev/null +++ b/modules/installer/wizard.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +set -euo pipefail +# shellcheck source=../../lib/common.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib/common.sh" +# shellcheck source=github.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/github.sh" +# shellcheck source=docker.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/docker.sh" + +PROJECTS_BASE="${PROJECTS_BASE:-/opt/mona-projects}" + +ensure_projects_dir(){ + needs_root + apply "mkdir -p '$PROJECTS_BASE'" +} + + +# Try to source simple_curses if TUI reader enabled +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 + source "$sc" + return 0 + fi + return 1 +} + +tui_view_file(){ + local file="$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 + return 0 + fi + local -a lines + mapfile -t lines <"$file" + local top=0 + local max=${#lines[@]} + local page=20 + + _draw(){ + window "README (TUI) — ↑/↓ rola, Q sai" "blue" "100%" + local i; local end=$(( top + page )) + (( end > max )) && end=$max + for (( i=top; i0 )) && ((top--));; + $'\e[B') (( top+page/dev/null 2>&1; then + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + return 0 + fi + if command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + return 0 + fi + fi + echo "" + return 1 +} + +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; } + 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" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^\s*# ]] || [[ -z "$line" ]]; then + echo "$line" >> "$dst" + continue + fi + # parse KEY=VALUE (tira aspas simples/duplas do default) + key="${line%%=*}"; def="${line#*=}" + def="${def%$'\r'}" + 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" + log ".env gerado: $dst" +} + + +# discover actions in a project checkout +discover_actions(){ + local dir="$1" + ACTIONS=() + ACTION_CMDS=() + + if [[ -f "$dir/.env.example" ]]; then + ACTIONS+=("Copiar .env.example → .env") + ACTION_CMDS+=("cp -n '$dir/.env.example' '$dir/.env'") + ACTIONS+=("Criar .env interativo (de .env.example)") + ACTION_CMDS+=("env_interactive_from_example '$dir'") + fi + + local ccmd + ccmd=$(compose_cmd_for "$dir") || true + if [[ -n "$ccmd" ]] && [[ -f "$dir/docker-compose.yml" || -f "$dir/docker-compose.yaml" ]]; then + ACTIONS+=("$ccmd pull") + ACTION_CMDS+=("cd '$dir' && $ccmd pull") + ACTIONS+=("$ccmd up -d --remove-orphans") + ACTION_CMDS+=("cd '$dir' && $ccmd up -d --remove-orphans") + ACTIONS+=("$ccmd ps") + ACTION_CMDS+=("cd '$dir' && $ccmd ps") + ACTIONS+=("$ccmd logs -f (Ctrl+C para sair)") + ACTION_CMDS+=("cd '$dir' && $ccmd logs -f") + fi + + if [[ -f "$dir/package.json" ]]; then + if command -v pnpm >/dev/null 2>&1; then + ACTIONS+=("pnpm install") + ACTION_CMDS+=("cd '$dir' && pnpm install") + elif command -v yarn >/dev/null 2>&1; then + ACTIONS+=("yarn install") + ACTION_CMDS+=("cd '$dir' && yarn install") + else + ACTIONS+=("npm install") + ACTION_CMDS+=("cd '$dir' && npm install") + fi + + # common scripts + if grep -q '"build"' "$dir/package.json"; then + ACTIONS+=("npm run build") + ACTION_CMDS+=("cd '$dir' && npm run build") + fi + if grep -q '"dev"' "$dir/package.json"; then + ACTIONS+=("npm run dev") + ACTION_CMDS+=("cd '$dir' && npm run dev") + fi + if grep -q '"start"' "$dir/package.json"; then + ACTIONS+=("npm start") + ACTION_CMDS+=("cd '$dir' && npm start") + fi + fi + + if [[ -x "$dir/scripts/setup.sh" ]]; then + ACTIONS+=("scripts/setup.sh") + ACTION_CMDS+=("cd '$dir' && ./scripts/setup.sh") + fi + + if [[ -f "$dir/Makefile" ]]; then + if grep -qE '(^|\s)up:' "$dir/Makefile"; then + ACTIONS+=("make up") + ACTION_CMDS+=("cd '$dir' && make up") + fi + if grep -qE '(^|\s)setup:' "$dir/Makefile"; then + ACTIONS+=("make setup") + ACTION_CMDS+=("cd '$dir' && make setup") + fi + fi + + if [[ -f "$dir/supabase/config.toml" ]] && command -v supabase >/dev/null 2>&1; then + ACTIONS+=("supabase start") + ACTION_CMDS+=("cd '$dir' && supabase start") + fi +} + +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 + + if ((${#ACTIONS[@]}==0)); then + warn "Nenhuma ação automática detectada. Consulte o README e docs do projeto." + pause_any + return 0 + fi + + echo "Ações sugeridas para $dir:" + local i=1 + for a in "${ACTIONS[@]}"; do + printf " %2d) %s\n" "$i" "$a" + ((i++)) + 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]}" + local logfile="${MONA_LOG_DIR:-$HOME/.mona/logs}" + mkdir -p "$logfile" + logfile="$logfile/$(basename "$dir")-$(date +%Y%m%d-%H%M%S).log" + log "Executando: $cmd" + if [[ "${MONA_DRY_RUN:-false}" == "true" ]]; then + log "(dry-run) $cmd" + else + bash -lc "$cmd" 2>&1 | tee "$logfile" + log "Logs: $logfile" + fi + pause_any + fi +} + +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 ;; + esac +} + +wizard_github(){ + ensure_projects_dir + read -r -p "Org/Usuário (ENTER p/ Monynha-Softwares): " org + org="${org:-Monynha-Softwares}" + echo "Listando repositórios públicos de $org (se possível)…" + 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; } + 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 dest="${PROJECTS_BASE}/${name}" + echo "Destino: $dest" + gh_clone "$repo_url" "$dest" "$branch" + run_actions_menu "$dest" +} + +wizard_docker(){ + read -r -p "Imagem (ex.: nginx:latest): " image + [[ -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 + docker_pull_image "$image" + docker_run_quick "$image" "$name" "$port" + log "Use: docker logs -f '$name'" + pause_any +} diff --git a/tests/installer_env.bats b/tests/installer_env.bats new file mode 100644 index 0000000..430375a --- /dev/null +++ b/tests/installer_env.bats @@ -0,0 +1,9 @@ +#!/usr/bin/env bats + +setup() { cd "$BATS_TEST_DIRNAME/.."; } + +@test "wizard provides env_interactive_from_example function" { + run bash -lc 'source modules/installer/wizard.sh; type -t env_interactive_from_example' + [ "$status" -eq 0 ] + [[ "$output" == "function" ]] +}