diff --git a/lessy.py b/lessy.py new file mode 100755 index 00000000..09733189 --- /dev/null +++ b/lessy.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +import platform +import subprocess +import sys +from pathlib import Path + +import click + +OS = platform.system().lower() +SCRIPT_EXT = ".ps1" if OS == "windows" else ".sh" +SCRIPT_DIR = "Windows" if OS == "windows" else "Unix" +PROJECT_ROOT = Path(__file__).parent.absolute() +SCRIPTS_PATH = PROJECT_ROOT / "scripts" / SCRIPT_DIR + + +def get_script_path(command: str) -> Path: + return SCRIPTS_PATH / f"{command}{SCRIPT_EXT}" + + +def run_script(script_name: str, *args) -> int: + script_path = get_script_path(script_name) + + if not script_path.exists(): + click.secho(f"Error: Script '{script_name}' not found at {script_path}", fg="red", err=True) + return 1 + + if OS == "windows": + cmd = ["pwsh", "-File", str(script_path)] + else: + cmd = ["bash", str(script_path)] + + cmd.extend(args) + + try: + result = subprocess.run(cmd, cwd=PROJECT_ROOT) + return result.returncode + except FileNotFoundError: + shell_name = "PowerShell" if OS == "windows" else "bash" + click.secho(f"Error: {shell_name} not found. Please ensure it's installed.", fg="red", err=True) + return 127 + except KeyboardInterrupt: + click.secho("\nOperation cancelled by user.", fg="yellow") + return 130 + + +@click.group( + context_settings={"help_option_names": ["-h", "--help"]}, + invoke_without_command=True, +) +@click.pass_context +def cli(ctx): + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +@cli.command() +def install(): + return sys.exit(run_script("install")) + + +@cli.command() +def run(): + return sys.exit(run_script("run")) + + +@cli.command() +def restart(): + return sys.exit(run_script("restart")) + + +@cli.command() +@click.option("-r", "--report", type=click.Choice(["xml", "html"]), help="Generate coverage report (xml or html)") +def test(report): + args = [] + if report: + args.extend(["-r", report]) + return sys.exit(run_script("test", *args)) + + +@cli.command() +def uninstall(): + if click.confirm("This will remove all Docker containers, volumes, and generated files. Continue?"): + return sys.exit(run_script("uninstall")) + else: + click.secho("Uninstall cancelled.", fg="yellow") + return 0 + + +@cli.command() +def backup(): + return sys.exit(run_script("backup")) + + +@cli.command(name="generate-env") +def generate_env(): + return sys.exit(run_script("generate-environment-file")) + + +@cli.command(name="export-env") +def export_env(): + return sys.exit(run_script("export-environment-file")) + + +@cli.command(name="make-messages") +def make_messages(): + return sys.exit(run_script("make-messages")) + + +@cli.command(name="compile-messages") +def compile_messages(): + return sys.exit(run_script("compile-messages")) + + +@cli.command() +def info(): + click.echo(f"{'='*60}") + click.secho("lessy - eVibes Project CLI", fg="cyan", bold=True) + click.echo(f"{'='*60}") + click.echo(f"Operating System: {platform.system()} ({platform.release()})") + click.echo(f"Python Version: {platform.python_version()}") + click.echo(f"Architecture: {platform.machine()}") + click.echo(f"Project Root: {PROJECT_ROOT}") + click.echo(f"Scripts Directory: {SCRIPTS_PATH}") + click.echo(f"Script Extension: {SCRIPT_EXT}") + click.echo(f"{'='*60}") + + +if __name__ == "__main__": + cli() diff --git a/scripts/Unix/backup.sh b/scripts/Unix/backup.sh index 0f7c497c..a4c7bb7d 100644 --- a/scripts/Unix/backup.sh +++ b/scripts/Unix/backup.sh @@ -3,10 +3,21 @@ set -euo pipefail source ./scripts/Unix/starter.sh -echo "Starting database backup process..." -docker compose exec app uv run manage.py dbbackup -echo "Database backup created under ./dbbackup" +# Database backup +log_step "Starting database backup process..." +if ! docker compose exec app uv run manage.py dbbackup; then + log_error "Database backup failed" + exit 1 +fi +log_success "Database backup created under ./dbbackup" -echo "Starting media backup process..." -docker compose exec app uv run manage.py mediabackup -echo "Media backup created under ./dbbackup" +# Media backup +log_step "Starting media backup process..." +if ! docker compose exec app uv run manage.py mediabackup; then + log_error "Media backup failed" + exit 1 +fi +log_success "Media backup created under ./dbbackup" + +echo +log_result "Backup completed successfully!" diff --git a/scripts/Unix/compile-messages.sh b/scripts/Unix/compile-messages.sh new file mode 100755 index 00000000..ab229907 --- /dev/null +++ b/scripts/Unix/compile-messages.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/Unix/starter.sh + +if [ ! -f .env ]; then + log_warning ".env file not found. Exiting without running Docker steps." + exit 0 +fi + +# Check placeholders +log_step "Checking placeholders in PO files..." +if ! docker compose exec app uv run manage.py check_translated -l ALL -a ALL; then + log_error "PO files have placeholder issues" + exit 1 +fi +log_success "PO files have no placeholder issues!" + +# Compile messages +log_step "Compiling PO files into MO files..." +if ! docker compose exec app uv run manage.py compilemessages -l ar_AR -l cs_CZ -l da_DK -l de_DE -l en_GB -l en_US -l es_ES -l fa_IR -l fr_FR -l he_IL -l hi_IN -l hr_HR -l id_ID -l it_IT -l ja_JP -l kk_KZ -l ko_KR -l nl_NL -l no_NO -l pl_PL -l pt_BR -l ro_RO -l ru_RU -l sv_SE -l th_TH -l tr_TR -l vi_VN -l zh_Hans; then + log_error "Failed to compile messages" + exit 1 +fi +log_success "Compiled successfully!" + +echo +log_result "Translation compilation complete!" diff --git a/scripts/Unix/install.sh b/scripts/Unix/install.sh index 2c6db187..c9fb9025 100755 --- a/scripts/Unix/install.sh +++ b/scripts/Unix/install.sh @@ -4,41 +4,30 @@ set -euo pipefail source ./scripts/Unix/starter.sh if [ ! -f .env ]; then - echo ".env file not found. Exiting without running Docker steps." >&2 + log_warning ".env file not found. Exiting without running Docker steps." exit 0 fi -cpu_count=$(getconf _NPROCESSORS_ONLN) -if [ "$cpu_count" -lt 4 ]; then +# Check system requirements +if ! check_system_requirements 4 6 20; then exit 1 fi -if [ -f /proc/meminfo ]; then - mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') - total_mem_gb=$(awk "BEGIN {printf \"%.2f\", $mem_kb/1024/1024}") -else - total_mem_bytes=$(sysctl -n hw.memsize) - total_mem_gb=$(awk "BEGIN {printf \"%.2f\", $total_mem_bytes/1024/1024/1024}") -fi -if ! awk "BEGIN {exit !($total_mem_gb >= 6)}"; then +# Pull Docker images +log_step "Pulling images..." +if ! docker compose pull; then + log_error "Failed to pull Docker images" exit 1 fi +log_success "Images pulled successfully" -avail_kb=$(df -k . | tail -1 | awk '{print $4}') -free_gb=$(awk "BEGIN {printf \"%.2f\", $avail_kb/1024/1024}") -if ! awk "BEGIN {exit !($free_gb >= 20)}"; then +# Build Docker images +log_step "Building images..." +if ! docker compose build; then + log_error "Failed to build Docker images" exit 1 fi - -echo "System requirements met: CPU cores=$cpu_count, RAM=${total_mem_gb}GB, FreeDisk=${free_gb}GB" - -echo "Pulling images" -docker compose pull -echo "Images pulled successfully" - -echo "Building images" -docker compose build -echo "Images built successfully" +log_success "Images built successfully" echo -echo "You can now use run.sh script." +log_result "You can now use run.sh script or run: ./lessy.py run" diff --git a/scripts/Unix/make-messages.sh b/scripts/Unix/make-messages.sh new file mode 100755 index 00000000..3303ac67 --- /dev/null +++ b/scripts/Unix/make-messages.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/Unix/starter.sh + +if [ ! -f .env ]; then + log_warning ".env file not found. Exiting without running Docker steps." + exit 0 +fi + +# Remove old fuzzy entries +log_step "Remove old fuzzy entries..." +if ! docker compose exec app uv run manage.py fix_fuzzy; then + log_error "Failed to remove old fuzzy entries" + exit 1 +fi +log_success "Old fuzzy entries removed successfully!" + +# Update PO files +log_step "Updating PO files..." +if ! docker compose exec app uv run manage.py makemessages -l ar_AR -l cs_CZ -l da_DK -l de_DE -l en_GB -l en_US -l es_ES -l fa_IR -l fr_FR -l he_IL -l hi_IN -l hr_HR -l id_ID -l it_IT -l ja_JP -l kk_KZ -l ko_KR -l nl_NL -l no_NO -l pl_PL -l pt_BR -l ro_RO -l ru_RU -l sv_SE -l th_TH -l tr_TR -l vi_VN -l zh_Hans; then + log_error "Failed to update PO files" + exit 1 +fi +log_success "PO files updated successfully!" + +# Fix new fuzzy entries +log_step "Fixing new fuzzy entries..." +if ! docker compose exec app uv run manage.py fix_fuzzy; then + log_error "Failed to fix new fuzzy entries" + exit 1 +fi +log_success "New fuzzy entries fixed successfully!" + +# Translate with DeepL +log_step "Translating with DeepL..." +if ! docker compose exec app uv run manage.py deepl_translate -l ALL -a ALL; then + log_error "Translation failed" + exit 1 +fi +log_success "Translated successfully!" + +echo +log_result "You can now use compile-messages.sh script or run: ./lessy.py compile-messages" diff --git a/scripts/Unix/reboot.sh b/scripts/Unix/reboot.sh deleted file mode 100755 index 83b97565..00000000 --- a/scripts/Unix/reboot.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -source ./scripts/Unix/starter.sh - -echo "Shutting down..." -docker compose down -echo "Services were shut down successfully!" - -echo "Spinning services up..." -docker compose up -d --build --wait -echo "Services are up and healthy!" - -echo "Completing pre-run tasks..." -docker compose exec app uv run manage.py migrate --no-input --verbosity 0 -docker compose exec app uv run manage.py initialize -docker compose exec app uv run manage.py set_default_caches -docker compose exec app uv run manage.py search_index --rebuild -f -docker compose exec app uv run manage.py collectstatic --clear --no-input --verbosity 0 -echo "Pre-run tasks completed successfully!" - -echo "Cleaning up unused Docker data..." -docker system prune -f -echo "Unused Docker data cleaned successfully!" - -echo "All done! eVibes is up and running!" diff --git a/scripts/Unix/restart.sh b/scripts/Unix/restart.sh new file mode 100755 index 00000000..1fa17c97 --- /dev/null +++ b/scripts/Unix/restart.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/Unix/starter.sh + +# Shutdown services +log_step "Shutting down..." +if ! docker compose down; then + log_error "Failed to shut down services" + exit 1 +fi +log_success "Services were shut down successfully!" + +# Rebuild and start services +log_step "Spinning services up with rebuild..." +if ! docker compose up -d --build --wait; then + log_error "Failed to start services" + exit 1 +fi +log_success "Services are up and healthy!" + +# Run pre-run tasks +log_step "Completing pre-run tasks..." + +log_info " → Running migrations..." +if ! docker compose exec app uv run manage.py migrate --no-input --verbosity 0; then + log_error "Migrations failed" + exit 1 +fi + +log_info " → Initializing..." +if ! docker compose exec app uv run manage.py initialize; then + log_error "Initialization failed" + exit 1 +fi + +log_info " → Setting default caches..." +if ! docker compose exec app uv run manage.py set_default_caches; then + log_error "Cache setup failed" + exit 1 +fi + +log_info " → Rebuilding search index..." +if ! docker compose exec app uv run manage.py search_index --rebuild -f; then + log_error "Search index rebuild failed" + exit 1 +fi + +log_info " → Collecting static files..." +if ! docker compose exec app uv run manage.py collectstatic --clear --no-input --verbosity 0; then + log_error "Static files collection failed" + exit 1 +fi + +log_success "Pre-run tasks completed successfully!" + +# Cleanup +log_step "Cleaning up unused Docker data..." +if ! docker system prune -f; then + log_warning "Docker cleanup had issues, but continuing..." +fi +log_success "Unused Docker data cleaned successfully!" + +echo +log_result "All done! eVibes is up and running!" diff --git a/scripts/Unix/run.sh b/scripts/Unix/run.sh index c6246678..358e2d3a 100755 --- a/scripts/Unix/run.sh +++ b/scripts/Unix/run.sh @@ -3,36 +3,72 @@ set -euo pipefail source ./scripts/Unix/starter.sh -echo "Verifying all images are present..." +# Verify Docker images +log_step "Verifying all images are present..." if command -v jq >/dev/null 2>&1; then images=$(docker compose config --format json | jq -r '.services[].image // empty') if [ -n "$images" ]; then for image in $images; do if ! docker image inspect "$image" > /dev/null 2>&1; then - echo "Required images not found. Please run install.sh first." >&2 + log_error "Required images not found. Please run install.sh first." exit 1 fi - echo " - Found image: $image" + log_info " • Found image: $image" done fi else - echo "jq is not installed; skipping image verification step." + log_warning "jq is not installed; skipping image verification step." fi -echo "Spinning services up..." -docker compose up --no-build --detach --wait -echo "Services are up and healthy!" +# Start services +log_step "Spinning services up..." +if ! docker compose up --no-build --detach --wait; then + log_error "Failed to start services" + exit 1 +fi +log_success "Services are up and healthy!" -echo "Completing pre-run tasks..." -docker compose exec app uv run manage.py migrate --no-input --verbosity 0 -docker compose exec app uv run manage.py initialize -docker compose exec app uv run manage.py set_default_caches -docker compose exec app uv run manage.py search_index --rebuild -f -docker compose exec app uv run manage.py collectstatic --clear --no-input --verbosity 0 -echo "Pre-run tasks completed successfully!" +# Run pre-run tasks +log_step "Completing pre-run tasks..." -echo "Cleaning unused Docker data..." -docker system prune -f -echo "Unused Docker data cleaned successfully!" +log_info " → Running migrations..." +if ! docker compose exec app uv run manage.py migrate --no-input --verbosity 0; then + log_error "Migrations failed" + exit 1 +fi -echo "All done! eVibes is up and running!" +log_info " → Initializing..." +if ! docker compose exec app uv run manage.py initialize; then + log_error "Initialization failed" + exit 1 +fi + +log_info " → Setting default caches..." +if ! docker compose exec app uv run manage.py set_default_caches; then + log_error "Cache setup failed" + exit 1 +fi + +log_info " → Rebuilding search index..." +if ! docker compose exec app uv run manage.py search_index --rebuild -f; then + log_error "Search index rebuild failed" + exit 1 +fi + +log_info " → Collecting static files..." +if ! docker compose exec app uv run manage.py collectstatic --clear --no-input --verbosity 0; then + log_error "Static files collection failed" + exit 1 +fi + +log_success "Pre-run tasks completed successfully!" + +# Cleanup +log_step "Cleaning unused Docker data..." +if ! docker system prune -f; then + log_warning "Docker cleanup had issues, but continuing..." +fi +log_success "Unused Docker data cleaned successfully!" + +echo +log_result "All done! eVibes is up and running!" diff --git a/scripts/Unix/starter.sh b/scripts/Unix/starter.sh index 7ee42548..840bc2ac 100644 --- a/scripts/Unix/starter.sh +++ b/scripts/Unix/starter.sh @@ -7,18 +7,30 @@ else script_path="$0" fi script_dir="$(cd "$(dirname "$script_path")" && pwd -P)" +# Load shared utilities +# shellcheck source=../lib/utils.sh +source "$script_dir/../lib/utils.sh" + if [ ! -d "./evibes" ]; then - echo "❌ Please run this script from the project's root (where the 'evibes' directory lives)." >&2 + log_error "❌ Please run this script from the project's root (where the 'evibes' directory lives)." exit 1 fi art_path="$script_dir/../ASCII_ART_EVIBES" if [ ! -f "$art_path" ]; then - echo "❌ Could not find ASCII art at $art_path" >&2 + log_error "❌ Could not find ASCII art at $art_path" exit 1 fi -cat "$art_path" -echo -echo " by WISELESS TEAM" -echo +if is_interactive; then + # In interactive mode, show colorful banner + purple='\033[38;2;121;101;209m' + reset='\033[0m' + echo -e "${purple}$(cat "$art_path")${reset}" + echo + echo -e "${COLOR_GRAY} by WISELESS TEAM${COLOR_RESET}" + echo +else + # In non-interactive mode, show simple banner + echo "eVibes by WISELESS TEAM" +fi diff --git a/scripts/Unix/test.sh b/scripts/Unix/test.sh index f955dc39..f029c08a 100644 --- a/scripts/Unix/test.sh +++ b/scripts/Unix/test.sh @@ -4,12 +4,13 @@ set -euo pipefail source ./scripts/Unix/starter.sh report="" +omit_pattern='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' while [ "$#" -gt 0 ]; do case "$1" in --report|-r) if [ "${2-}" = "" ]; then - echo "Error: --report/-r requires an argument: xml or html" >&2 + log_error "Error: --report/-r requires an argument: xml or html" exit 1 fi report="$2" @@ -24,7 +25,7 @@ while [ "$#" -gt 0 ]; do shift ;; *) - echo "Unknown argument: $1" >&2 + log_error "Unknown argument: $1" echo "Usage: $0 [--report|-r xml|html]" >&2 exit 1 ;; @@ -33,18 +34,43 @@ done case "${report:-}" in "") - docker compose exec app uv run coverage erase - docker compose exec app uv run coverage run --source='.' --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' manage.py test + log_step "Running tests with coverage..." + + log_info " → Erasing previous coverage data..." + if ! docker compose exec app uv run coverage erase; then + log_error "Failed to erase coverage data" + exit 1 + fi + + log_info " → Running tests..." + if ! docker compose exec app uv run coverage run --source='.' --omit="$omit_pattern" manage.py test; then + log_error "Tests failed" + exit 1 + fi + + log_info " → Generating coverage report..." docker compose exec app uv run coverage report -m ;; xml) - docker compose exec app uv run coverage xml --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' + log_step "Generating XML coverage report..." + if docker compose exec app uv run coverage xml --omit="$omit_pattern"; then + log_success "XML coverage report generated" + else + log_error "Failed to generate XML coverage report" + exit 1 + fi ;; html) - docker compose exec app uv run coverage html --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' + log_step "Generating HTML coverage report..." + if docker compose exec app uv run coverage html --omit="$omit_pattern"; then + log_success "HTML coverage report generated" + else + log_error "Failed to generate HTML coverage report" + exit 1 + fi ;; *) - echo "Invalid report type: $report (expected xml or html)" >&2 + log_error "Invalid report type: $report (expected xml or html)" exit 1 ;; esac diff --git a/scripts/Unix/uninstall.sh b/scripts/Unix/uninstall.sh index 0c68de65..8e57da6c 100644 --- a/scripts/Unix/uninstall.sh +++ b/scripts/Unix/uninstall.sh @@ -3,21 +3,31 @@ set -euo pipefail source ./scripts/Unix/starter.sh -echo "Shutting down..." -docker compose down -echo "Services were shut down successfully!" +# Shutdown services +log_step "Shutting down..." +if ! docker compose down; then + log_error "Failed to shut down services" + exit 1 +fi +log_success "Services were shut down successfully!" -echo "Removing volumes..." -docker volume rm -f evibes_prometheus-data -docker volume rm -f evibes_es-data -echo "Volumes were removed successfully!" +# Remove volumes +log_step "Removing volumes..." +docker volume rm -f evibes_prometheus-data || log_warning "Failed to remove prometheus-data volume" +docker volume rm -f evibes_es-data || log_warning "Failed to remove es-data volume" +log_success "Volumes were removed successfully!" -echo "Cleaning up unused Docker data..." -docker system prune -a -f --volumes -echo "Unused Docker data cleaned successfully!" +# Cleanup Docker +log_step "Cleaning up unused Docker data..." +if ! docker system prune -a -f --volumes; then + log_warning "Docker cleanup had issues, but continuing..." +fi +log_success "Unused Docker data cleaned successfully!" -echo "Removing related files..." +# Remove local files +log_step "Removing related files..." rm -rf ./media ./static -echo "Related files removed successfully!" +log_success "Related files removed successfully!" -echo "Bye-bye, hope you return later!" +echo +log_result "Bye-bye, hope you return later!" diff --git a/scripts/Windows/backup.ps1 b/scripts/Windows/backup.ps1 index d98c27c4..cba82c2a 100644 --- a/scripts/Windows/backup.ps1 +++ b/scripts/Windows/backup.ps1 @@ -2,21 +2,32 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Host "Starting database backup process..." -ForegroundColor Magenta +# Database backup +Write-Step "Starting database backup process..." docker compose exec app uv run manage.py dbbackup if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Database backup failed" exit $LASTEXITCODE } -Write-Host "Database backup created under ./dbbackup" -ForegroundColor Green +Write-Success "Database backup created under ./dbbackup" -Write-Host "Starting media backup process..." -ForegroundColor Magenta +# Media backup +Write-Step "Starting media backup process..." docker compose exec app uv run manage.py mediabackup if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Media backup failed" exit $LASTEXITCODE } -Write-Host "Media backup created under ./dbbackup" -ForegroundColor Green \ No newline at end of file +Write-Success "Media backup created under ./dbbackup" + +Write-Result "" +Write-Result "Backup completed successfully!" \ No newline at end of file diff --git a/scripts/Windows/compile-messages.ps1 b/scripts/Windows/compile-messages.ps1 index ea0b5516..54a9a091 100644 --- a/scripts/Windows/compile-messages.ps1 +++ b/scripts/Windows/compile-messages.ps1 @@ -1,6 +1,10 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE @@ -8,20 +12,27 @@ if ($LASTEXITCODE -ne 0) { if (-not (Test-Path '.env')) { - Write-Warning ".env file not found. Exiting without running Docker steps." + Write-Warning-Custom ".env file not found. Exiting without running Docker steps." exit 0 } -Write-Host "Checking placeholders in PO files..." -ForegroundColor Magenta +# Check placeholders +Write-Step "Checking placeholders in PO files..." docker compose exec app uv run manage.py check_translated -l ALL -a ALL if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "PO files have placeholder issues" exit $LASTEXITCODE } -Write-Host "PO files have no placeholder issues!" -ForegroundColor Green +Write-Success "PO files have no placeholder issues!" -Write-Host "Compiling PO files into MO files..." -ForegroundColor Magenta +# Compile messages +Write-Step "Compiling PO files into MO files..." docker compose exec app uv run manage.py compilemessages -l ar_AR -l cs_CZ -l da_DK -l de_DE -l en_GB -l en_US -l es_ES -l fa_IR -l fr_FR -l he_IL -l hi_IN -l hr_HR -l id_ID -l it_IT -l ja_JP -l kk_KZ -l ko_KR -l nl_NL -l no_NO -l pl_PL -l pt_BR -l ro_RO -l ru_RU -l sv_SE -l th_TH -l tr_TR -l vi_VN -l zh_Hans if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to compile messages" exit $LASTEXITCODE } -Write-Host "Compiled successfully!" -ForegroundColor Green +Write-Success "Compiled successfully!" + +Write-Result "" +Write-Result "Translation compilation complete!" diff --git a/scripts/Windows/install.ps1 b/scripts/Windows/install.ps1 index 0df9aec8..fae13ba5 100644 --- a/scripts/Windows/install.ps1 +++ b/scripts/Windows/install.ps1 @@ -1,6 +1,10 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE @@ -8,39 +12,33 @@ if ($LASTEXITCODE -ne 0) { if (-not (Test-Path '.env')) { - Write-Warning ".env file not found. Exiting without running Docker steps." + Write-Warning-Custom ".env file not found. Exiting without running Docker steps." exit 0 } -$cpuCount = [Environment]::ProcessorCount -if ($cpuCount -lt 4) +# Check system requirements +if (-not (Test-SystemRequirements -MinCpu 4 -MinRamGB 6 -MinDiskGB 20)) { exit 1 } -$totalMemBytes = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory -$totalMemGB = [Math]::Round($totalMemBytes / 1GB, 2) -if ($totalMemGB -lt 6) -{ - exit 1 -} - -$currentDrive = Split-Path -Qualifier $PWD -$driveLetter = $currentDrive.Substring(0, 1) -$freeGB = [Math]::Round((Get-PSDrive -Name $driveLetter).Free / 1GB, 2) -if ($freeGB -lt 20) -{ - exit 1 -} - -Write-Host "System requirements met: CPU cores=$cpuCount, RAM=${totalMemGB}GB, FreeDisk=${freeGB}GB" -ForegroundColor Green - -Write-Host "Pulling images" -ForegroundColor Magenta +# Pull Docker images +Write-Step "Pulling images..." docker compose pull -Write-Host "Images pulled successfully" -ForegroundColor Green +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to pull Docker images" + exit $LASTEXITCODE +} +Write-Success "Images pulled successfully" -Write-Host "Building images" -ForegroundColor Magenta +# Build Docker images +Write-Step "Building images..." docker compose build -Write-Host "Images built successfully" -ForegroundColor Green -Write-Host "" -Write-Host "You can now use run.ps1 script." -ForegroundColor Cyan +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to build Docker images" + exit $LASTEXITCODE +} +Write-Success "Images built successfully" + +Write-Result "" +Write-Result "You can now use run.ps1 script or run: lessy.py run" diff --git a/scripts/Windows/make-messages.ps1 b/scripts/Windows/make-messages.ps1 index 2d91de00..4249ad42 100644 --- a/scripts/Windows/make-messages.ps1 +++ b/scripts/Windows/make-messages.ps1 @@ -1,6 +1,10 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE @@ -8,37 +12,45 @@ if ($LASTEXITCODE -ne 0) { if (-not (Test-Path '.env')) { - Write-Warning ".env file not found. Exiting without running Docker steps." + Write-Warning-Custom ".env file not found. Exiting without running Docker steps." exit 0 } -Write-Host "Remove old fuzzy entries..." -ForegroundColor Magenta +# Remove old fuzzy entries +Write-Step "Remove old fuzzy entries..." docker compose exec app uv run manage.py fix_fuzzy if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to remove old fuzzy entries" exit $LASTEXITCODE } -Write-Host "Old fuzzy entries removed successfully!" -ForegroundColor Green +Write-Success "Old fuzzy entries removed successfully!" -Write-Host "Updating PO files..." -ForegroundColor Magenta +# Update PO files +Write-Step "Updating PO files..." docker compose exec app uv run manage.py makemessages -l ar_AR -l cs_CZ -l da_DK -l de_DE -l en_GB -l en_US -l es_ES -l fa_IR -l fr_FR -l he_IL -l hi_IN -l hr_HR -l id_ID -l it_IT -l ja_JP -l kk_KZ -l ko_KR -l nl_NL -l no_NO -l pl_PL -l pt_BR -l ro_RO -l ru_RU -l sv_SE -l th_TH -l tr_TR -l vi_VN -l zh_Hans if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to update PO files" exit $LASTEXITCODE } -Write-Host "PO files updated successfully!" -ForegroundColor Green +Write-Success "PO files updated successfully!" -Write-Host "Fixing new fuzzy entries..." -ForegroundColor Magenta +# Fix new fuzzy entries +Write-Step "Fixing new fuzzy entries..." docker compose exec app uv run manage.py fix_fuzzy if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to fix new fuzzy entries" exit $LASTEXITCODE } -Write-Host "New fuzzy entries fixed successfully!" -ForegroundColor Green +Write-Success "New fuzzy entries fixed successfully!" -Write-Host "Translating with DeepL..." -ForegroundColor Magenta +# Translate with DeepL +Write-Step "Translating with DeepL..." docker compose exec app uv run manage.py deepl_translate -l ALL -a ALL if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Translation failed" exit $LASTEXITCODE } -Write-Host "Translated successfully!" -ForegroundColor Green +Write-Success "Translated successfully!" -Write-Host "" -Write-Host "You can now use compile-messages.ps1 script." -ForegroundColor Cyan +Write-Result "" +Write-Result "You can now use compile-messages.ps1 script or run: lessy.py compile-messages" diff --git a/scripts/Windows/reboot.ps1 b/scripts/Windows/reboot.ps1 deleted file mode 100644 index e5fcedeb..00000000 --- a/scripts/Windows/reboot.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -.\scripts\Windows\starter.ps1 -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} - - -Write-Host "Shutting down..." -ForegroundColor Magenta -docker compose down -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -Write-Host "Services were shut down successfully!" -ForegroundColor Green - -Write-Host "Spinning services up..." -ForegroundColor Magenta -docker compose up -d --build --wait -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -Write-Host "Services are up and healthy!" -ForegroundColor Green - -Write-Host "Completing pre-run tasks..." -ForegroundColor Magenta -docker compose exec app uv run manage.py migrate --no-input -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -docker compose exec app uv run manage.py initialize -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -docker compose exec app uv run manage.py set_default_caches -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -docker compose exec app uv run manage.py search_index --rebuild -f -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -docker compose exec app uv run manage.py collectstatic --clear --no-input -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -Write-Host "Pre-run tasks completed successfully!" -ForegroundColor Green - -Write-Host "Cleaning up unused Docker data..." -ForegroundColor Magenta -docker system prune -f -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} -Write-Host "Unused Docker data cleaned successfully!" -ForegroundColor Green - -Write-Host "All done! eVibes is up and running!" -ForegroundColor Cyan diff --git a/scripts/Windows/restart.ps1 b/scripts/Windows/restart.ps1 new file mode 100644 index 00000000..420a6d44 --- /dev/null +++ b/scripts/Windows/restart.ps1 @@ -0,0 +1,80 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + +.\scripts\Windows\starter.ps1 +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +# Shutdown services +Write-Step "Shutting down..." +docker compose down +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to shut down services" + exit $LASTEXITCODE +} +Write-Success "Services were shut down successfully!" + +# Rebuild and start services +Write-Step "Spinning services up with rebuild..." +docker compose up -d --build --wait +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to start services" + exit $LASTEXITCODE +} +Write-Success "Services are up and healthy!" + +# Run pre-run tasks +Write-Step "Completing pre-run tasks..." + +Write-Info " → Running migrations..." +docker compose exec app uv run manage.py migrate --no-input +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Migrations failed" + exit $LASTEXITCODE +} + +Write-Info " → Initializing..." +docker compose exec app uv run manage.py initialize +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Initialization failed" + exit $LASTEXITCODE +} + +Write-Info " → Setting default caches..." +docker compose exec app uv run manage.py set_default_caches +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Cache setup failed" + exit $LASTEXITCODE +} + +Write-Info " → Rebuilding search index..." +docker compose exec app uv run manage.py search_index --rebuild -f +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Search index rebuild failed" + exit $LASTEXITCODE +} + +Write-Info " → Collecting static files..." +docker compose exec app uv run manage.py collectstatic --clear --no-input +if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Static files collection failed" + exit $LASTEXITCODE +} + +Write-Success "Pre-run tasks completed successfully!" + +# Cleanup +Write-Step "Cleaning up unused Docker data..." +docker system prune -f +if ($LASTEXITCODE -ne 0) { + Write-Warning-Custom "Docker cleanup had issues, but continuing..." +} +Write-Success "Unused Docker data cleaned successfully!" + +Write-Result "" +Write-Result "All done! eVibes is up and running!" diff --git a/scripts/Windows/run.ps1 b/scripts/Windows/run.ps1 index adf0914b..b5d310a0 100644 --- a/scripts/Windows/run.ps1 +++ b/scripts/Windows/run.ps1 @@ -1,12 +1,17 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Host "Verifying all images are present…" -ForegroundColor Green +# Verify Docker images +Write-Step "Verifying all images are present…" $config = docker compose config --format json | ConvertFrom-Json @@ -21,50 +26,71 @@ foreach ($prop in $config.services.PSObject.Properties) $image = $svc.PSObject.Properties['image'].Value - if (-not (docker image inspect $image)) + if (-not (docker image inspect $image 2>$null)) { - Write-Error "Required images not found. Please run install.ps1 first." + Write-Error-Custom "Required images not found. Please run install.ps1 first." exit 1 } - Write-Host " • Found image: $image" + Write-Info " • Found image: $image" } -Write-Host "Spinning services up..." -ForegroundColor Magenta +# Start services +Write-Step "Spinning services up..." docker compose up --no-build --detach --wait if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to start services" exit $LASTEXITCODE } -Write-Host "Services are up and healthy!" -ForegroundColor Green +Write-Success "Services are up and healthy!" -Write-Host "Completing pre-run tasks..." -ForegroundColor Magenta +# Run pre-run tasks +Write-Step "Completing pre-run tasks..." + +Write-Info " → Running migrations..." docker compose exec app uv run manage.py migrate --no-input if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Migrations failed" exit $LASTEXITCODE } + +Write-Info " → Initializing..." docker compose exec app uv run manage.py initialize if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Initialization failed" exit $LASTEXITCODE } + +Write-Info " → Setting default caches..." docker compose exec app uv run manage.py set_default_caches if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Cache setup failed" exit $LASTEXITCODE } + +Write-Info " → Rebuilding search index..." docker compose exec app uv run manage.py search_index --rebuild -f if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Search index rebuild failed" exit $LASTEXITCODE } + +Write-Info " → Collecting static files..." docker compose exec app uv run manage.py collectstatic --clear --no-input if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Static files collection failed" exit $LASTEXITCODE } -Write-Host "Pre-run tasks completed successfully!" -ForegroundColor Green -Write-Host "Cleaning unused Docker data..." -ForegroundColor Magenta +Write-Success "Pre-run tasks completed successfully!" + +# Cleanup +Write-Step "Cleaning unused Docker data..." docker system prune -f if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + Write-Warning-Custom "Docker cleanup had issues, but continuing..." } -Write-Host "Unused Docker data cleaned successfully!" -ForegroundColor Green +Write-Success "Unused Docker data cleaned successfully!" -Write-Host "All done! eVibes is up and running!" -ForegroundColor Cyan +Write-Result "" +Write-Result "All done! eVibes is up and running!" diff --git a/scripts/Windows/starter.ps1 b/scripts/Windows/starter.ps1 index 7a847b58..01a517ba 100644 --- a/scripts/Windows/starter.ps1 +++ b/scripts/Windows/starter.ps1 @@ -1,22 +1,35 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Definition) '..\lib\utils.ps1' +. $utilsPath + if (-not (Test-Path -Path ".\evibes" -PathType Container)) { - Write-Host "❌ Please run this script from the project's root (where the 'evibes' directory lives)." -ForegroundColor Red + Write-Error-Custom "❌ Please run this script from the project's root (where the 'evibes' directory lives)." exit 1 } -$purple = "`e[38;2;121;101;209m" -$reset = "`e[0m" $artPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Definition) '..\ASCII_ART_EVIBES' if (-not (Test-Path $artPath)) { - Write-Host "❌ Could not find ASCII art at $artPath" -ForegroundColor Red + Write-Error-Custom "❌ Could not find ASCII art at $artPath" exit 1 } -Get-Content -Raw -Path $artPath | ForEach-Object { Write-Host "$purple$_$reset" } -Write-Host "`n by WISELESS TEAM`n" -ForegroundColor Gray +if (Test-Interactive) +{ + $purple = "`e[38;2;121;101;209m" + $reset = "`e[0m" + Get-Content -Raw -Path $artPath | ForEach-Object { Write-Host "$purple$_$reset" } + Write-Host "`n by WISELESS TEAM`n" -ForegroundColor Gray +} +else +{ + # In non-interactive mode, just show simple banner + Write-Output "eVibes by WISELESS TEAM" +} + exit 0 \ No newline at end of file diff --git a/scripts/Windows/test.ps1 b/scripts/Windows/test.ps1 index 4836104b..19f96791 100644 --- a/scripts/Windows/test.ps1 +++ b/scripts/Windows/test.ps1 @@ -6,33 +6,62 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + & .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +$omitPattern = 'storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' + if (-not $PSBoundParameters.ContainsKey('Report') -or [string]::IsNullOrWhiteSpace($Report)) { + Write-Step "Running tests with coverage..." + + Write-Info " → Erasing previous coverage data..." docker compose exec app uv run coverage erase - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to erase coverage data" + exit $LASTEXITCODE + } - docker compose exec app uv run coverage run --source='.' --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' manage.py test - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Info " → Running tests..." + docker compose exec app uv run coverage run --source='.' --omit=$omitPattern manage.py test + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Tests failed" + exit $LASTEXITCODE + } + Write-Info " → Generating coverage report..." docker compose exec app uv run coverage report -m exit $LASTEXITCODE } switch ($Report.ToLowerInvariant()) { 'xml' { - docker compose exec app uv run coverage xml --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' + Write-Step "Generating XML coverage report..." + docker compose exec app uv run coverage xml --omit=$omitPattern + if ($LASTEXITCODE -eq 0) { + Write-Success "XML coverage report generated" + } else { + Write-Error-Custom "Failed to generate XML coverage report" + } exit $LASTEXITCODE } 'html' { - docker compose exec app uv run coverage html --omit='storefront/*,monitoring/*,Dockerfiles/*,*/__init__.py,*/tests/*,*/migrations/*,manage.py,evibes/*' + Write-Step "Generating HTML coverage report..." + docker compose exec app uv run coverage html --omit=$omitPattern + if ($LASTEXITCODE -eq 0) { + Write-Success "HTML coverage report generated" + } else { + Write-Error-Custom "Failed to generate HTML coverage report" + } exit $LASTEXITCODE } default { - Write-Error "Invalid -Report/-r value '$Report'. Expected 'xml' or 'html'." + Write-Error-Custom "Invalid -Report/-r value '$Report'. Expected 'xml' or 'html'." exit 1 } } diff --git a/scripts/Windows/uninstall.ps1 b/scripts/Windows/uninstall.ps1 index b7491eb1..645060a1 100644 --- a/scripts/Windows/uninstall.ps1 +++ b/scripts/Windows/uninstall.ps1 @@ -1,39 +1,49 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Load shared utilities +$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1' +. $utilsPath + .\scripts\Windows\starter.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Host "Shutting down..." -ForegroundColor Magenta +# Shutdown services +Write-Step "Shutting down..." docker compose down if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Failed to shut down services" exit $LASTEXITCODE } -Write-Host "Services were shut down successfully!" -ForegroundColor Green +Write-Success "Services were shut down successfully!" -Write-Host "Removing volumes..." -ForegroundColor Magenta +# Remove volumes +Write-Step "Removing volumes..." docker volume remove -f evibes_prometheus-data if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + Write-Warning-Custom "Failed to remove prometheus-data volume" } docker volume remove -f evibes_es-data if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + Write-Warning-Custom "Failed to remove es-data volume" } -Write-Host "Volumes were removed successfully!" -ForegroundColor Green +Write-Success "Volumes were removed successfully!" -Write-Host "Cleaning up unused Docker data..." -ForegroundColor Magenta +# Cleanup Docker +Write-Step "Cleaning up unused Docker data..." docker system prune -a -f --volumes if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + Write-Warning-Custom "Docker cleanup had issues, but continuing..." } -Write-Host "Unused Docker data cleaned successfully!" -ForegroundColor Green +Write-Success "Unused Docker data cleaned successfully!" -Write-Host "Removing related files..." -ForegroundColor Magenta +# Remove local files +Write-Step "Removing related files..." Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./media Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./static -Write-Host "Related files removed successfully!" -ForegroundColor Green +Write-Success "Related files removed successfully!" -Write-Host "Bye-bye, hope you return later!" -ForegroundColor Red \ No newline at end of file +Write-Result "" +Write-Result "Bye-bye, hope you return later!" \ No newline at end of file diff --git a/scripts/lib/utils.ps1 b/scripts/lib/utils.ps1 new file mode 100644 index 00000000..b0923bc0 --- /dev/null +++ b/scripts/lib/utils.ps1 @@ -0,0 +1,298 @@ +# Shared utilities for Windows scripts +# Provides: colors, progress indicators, interactive detection + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Detect if running in interactive shell +function Test-Interactive +{ + # Check if both input and output are from terminal + # In non-interactive environments (CI/CD), this will be false + $isInteractive = [Environment]::UserInteractive -and + (-not [Console]::IsInputRedirected) -and + (-not [Console]::IsOutputRedirected) + return $isInteractive +} + +# Global spinner state +$script:SpinnerJob = $null +$script:SpinnerFrames = @('⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏') + +# Logging functions +function Write-Info +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Blue + } + else + { + Write-Output $Message + } +} + +function Write-Success +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Green + } + else + { + Write-Output $Message + } +} + +function Write-Warning-Custom +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Yellow + } + else + { + Write-Output "WARNING: $Message" + } +} + +function Write-Error-Custom +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Red + } + else + { + Write-Output "ERROR: $Message" *>&1 + } +} + +function Write-Step +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Magenta + } + else + { + Write-Output $Message + } +} + +function Write-Result +{ + param([string]$Message) + + if (Test-Interactive) + { + Write-Host $Message -ForegroundColor Cyan + } + else + { + Write-Output $Message + } +} + +# Spinner functions (only for interactive shells) +function Start-Spinner +{ + param([string]$Message = "Processing...") + + if (-not (Test-Interactive)) + { + Write-Output $Message + return + } + + $script:SpinnerJob = Start-Job -ScriptBlock { + param($Frames, $Msg) + + $i = 0 + while ($true) + { + $frame = $Frames[$i % $Frames.Length] + [Console]::SetCursorPosition(0, [Console]::CursorTop) + Write-Host -NoNewline "$frame $Msg" + Start-Sleep -Milliseconds 100 + $i++ + } + } -ArgumentList $script:SpinnerFrames, $Message +} + +function Stop-Spinner +{ + if ($script:SpinnerJob -ne $null) + { + Stop-Job -Job $script:SpinnerJob -ErrorAction SilentlyContinue + Remove-Job -Job $script:SpinnerJob -Force -ErrorAction SilentlyContinue + $script:SpinnerJob = $null + + if (Test-Interactive) + { + # Clear the spinner line + [Console]::SetCursorPosition(0, [Console]::CursorTop) + Write-Host (' ' * ([Console]::WindowWidth - 1)) -NoNewline + [Console]::SetCursorPosition(0, [Console]::CursorTop) + } + } +} + +function Update-Spinner +{ + param([string]$Message) + + if (-not (Test-Interactive)) + { + Write-Output $Message + return + } + + if ($script:SpinnerJob -ne $null) + { + Stop-Spinner + Start-Spinner -Message $Message + } +} + +# Execute command with progress indicator +function Invoke-WithProgress +{ + param( + [string]$Message, + [scriptblock]$ScriptBlock + ) + + if (Test-Interactive) + { + Start-Spinner -Message $Message + + try + { + $output = & $ScriptBlock 2>&1 + $exitCode = $LASTEXITCODE + + Stop-Spinner + + if ($exitCode -eq 0 -or $null -eq $exitCode) + { + Write-Success "✓ $Message - Done!" + } + else + { + Write-Error-Custom "✗ $Message - Failed!" + if ($output) + { + Write-Output $output + } + } + + return $exitCode + } + catch + { + Stop-Spinner + Write-Error-Custom "✗ $Message - Failed!" + throw + } + } + else + { + # Non-interactive: just show message and run + Write-Output "→ $Message" + + try + { + & $ScriptBlock + $exitCode = $LASTEXITCODE + + if ($exitCode -eq 0 -or $null -eq $exitCode) + { + Write-Output "✓ $Message - Done!" + } + else + { + Write-Output "✗ $Message - Failed!" + } + + return $exitCode + } + catch + { + Write-Output "✗ $Message - Failed!" + throw + } + } +} + +# Check system requirements +function Test-SystemRequirements +{ + param( + [int]$MinCpu = 4, + [int]$MinRamGB = 6, + [int]$MinDiskGB = 20 + ) + + Write-Step "Checking system requirements..." + + # Check CPU cores + $cpuCount = [Environment]::ProcessorCount + if ($cpuCount -lt $MinCpu) + { + Write-Error-Custom "Insufficient CPU cores: $cpuCount (minimum: $MinCpu)" + return $false + } + + # Check RAM + $totalMemBytes = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory + $totalMemGB = [Math]::Round($totalMemBytes / 1GB, 2) + if ($totalMemGB -lt $MinRamGB) + { + Write-Error-Custom "Insufficient RAM: ${totalMemGB}GB (minimum: ${MinRamGB}GB)" + return $false + } + + # Check disk space + $currentDrive = Split-Path -Qualifier $PWD + $driveLetter = $currentDrive.Substring(0, 1) + $freeGB = [Math]::Round((Get-PSDrive -Name $driveLetter).Free / 1GB, 2) + if ($freeGB -lt $MinDiskGB) + { + Write-Error-Custom "Insufficient disk space: ${freeGB}GB (minimum: ${MinDiskGB}GB)" + return $false + } + + Write-Success "System requirements met: CPU cores=$cpuCount, RAM=${totalMemGB}GB, FreeDisk=${freeGB}GB" + return $true +} + +# Confirm action +function Confirm-Action +{ + param([string]$Prompt = "Are you sure?") + + if (-not (Test-Interactive)) + { + # In non-interactive mode, assume yes + return $true + } + + $response = Read-Host "$Prompt [y/N]" + return $response -match '^[yY]([eE][sS])?$' +} + +# Ensure spinner cleanup on exit +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + Stop-Spinner +} diff --git a/scripts/lib/utils.sh b/scripts/lib/utils.sh new file mode 100644 index 00000000..2ad2426c --- /dev/null +++ b/scripts/lib/utils.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# Shared utilities for Unix scripts +# Provides: colors, progress indicators, interactive detection + +# Detect if running in interactive shell +is_interactive() { + [[ -t 0 && -t 1 ]] +} + +# Color definitions (only used in interactive mode) +if is_interactive && [[ "${NO_COLOR:-}" != "1" ]]; then + COLOR_RESET='\033[0m' + COLOR_RED='\033[0;31m' + COLOR_GREEN='\033[0;32m' + COLOR_YELLOW='\033[0;33m' + COLOR_BLUE='\033[0;34m' + COLOR_MAGENTA='\033[0;35m' + COLOR_CYAN='\033[0;36m' + COLOR_GRAY='\033[0;90m' + COLOR_BOLD='\033[1m' +else + COLOR_RESET='' + COLOR_RED='' + COLOR_GREEN='' + COLOR_YELLOW='' + COLOR_BLUE='' + COLOR_MAGENTA='' + COLOR_CYAN='' + COLOR_GRAY='' + COLOR_BOLD='' +fi + +# Logging functions +log_info() { + echo -e "${COLOR_BLUE}${1}${COLOR_RESET}" +} + +log_success() { + echo -e "${COLOR_GREEN}${1}${COLOR_RESET}" +} + +log_warning() { + echo -e "${COLOR_YELLOW}${1}${COLOR_RESET}" +} + +log_error() { + echo -e "${COLOR_RED}${1}${COLOR_RESET}" >&2 +} + +log_step() { + echo -e "${COLOR_MAGENTA}${1}${COLOR_RESET}" +} + +log_result() { + echo -e "${COLOR_CYAN}${1}${COLOR_RESET}" +} + +# Spinner animation (only for interactive shells) +SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') +SPINNER_PID="" + +start_spinner() { + local message="${1:-Processing...}" + + if ! is_interactive; then + echo "$message" + return + fi + + { + local i=0 + while true; do + printf "\r${COLOR_CYAN}${SPINNER_FRAMES[$i]}${COLOR_RESET} %s" "$message" + i=$(( (i + 1) % ${#SPINNER_FRAMES[@]} )) + sleep 0.1 + done + } & + SPINNER_PID=$! + + # Ensure spinner is cleaned up on script exit + trap "stop_spinner" EXIT INT TERM +} + +stop_spinner() { + if [[ -n "$SPINNER_PID" ]] && kill -0 "$SPINNER_PID" 2>/dev/null; then + kill "$SPINNER_PID" 2>/dev/null + wait "$SPINNER_PID" 2>/dev/null + SPINNER_PID="" + fi + + if is_interactive; then + printf "\r\033[K" # Clear the spinner line + fi +} + +update_spinner_message() { + local message="${1:-Processing...}" + + if ! is_interactive; then + echo "$message" + return + fi + + if [[ -n "$SPINNER_PID" ]] && kill -0 "$SPINNER_PID" 2>/dev/null; then + stop_spinner + start_spinner "$message" + fi +} + +# Execute command with progress indicator +run_with_progress() { + local message="$1" + shift + local cmd=("$@") + + if is_interactive; then + start_spinner "$message" + local output + local exit_code + + # Capture output and exit code + output=$("${cmd[@]}" 2>&1) + exit_code=$? + + stop_spinner + + if [[ $exit_code -eq 0 ]]; then + log_success "✓ $message - Done!" + else + log_error "✗ $message - Failed!" + echo "$output" + fi + + return $exit_code + else + # Non-interactive: just show message and run + echo "→ $message" + "${cmd[@]}" + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + echo "✓ $message - Done!" + else + echo "✗ $message - Failed!" + fi + + return $exit_code + fi +} + +# Check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Validate system requirements +check_system_requirements() { + local min_cpu="${1:-4}" + local min_ram_gb="${2:-6}" + local min_disk_gb="${3:-20}" + + log_step "Checking system requirements..." + + # Check CPU cores + local cpu_count + cpu_count=$(getconf _NPROCESSORS_ONLN) + + if [[ "$cpu_count" -lt "$min_cpu" ]]; then + log_error "Insufficient CPU cores: $cpu_count (minimum: $min_cpu)" + return 1 + fi + + # Check RAM + local total_mem_gb + if [[ -f /proc/meminfo ]]; then + local mem_kb + mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') + total_mem_gb=$(awk "BEGIN {printf \"%.2f\", $mem_kb/1024/1024}") + else + local total_mem_bytes + total_mem_bytes=$(sysctl -n hw.memsize 2>/dev/null || echo "0") + total_mem_gb=$(awk "BEGIN {printf \"%.2f\", $total_mem_bytes/1024/1024/1024}") + fi + + if ! awk "BEGIN {exit !($total_mem_gb >= $min_ram_gb)}"; then + log_error "Insufficient RAM: ${total_mem_gb}GB (minimum: ${min_ram_gb}GB)" + return 1 + fi + + # Check disk space + local avail_kb free_gb + avail_kb=$(df -k . | tail -1 | awk '{print $4}') + free_gb=$(awk "BEGIN {printf \"%.2f\", $avail_kb/1024/1024}") + + if ! awk "BEGIN {exit !($free_gb >= $min_disk_gb)}"; then + log_error "Insufficient disk space: ${free_gb}GB (minimum: ${min_disk_gb}GB)" + return 1 + fi + + log_success "System requirements met: CPU cores=$cpu_count, RAM=${total_mem_gb}GB, FreeDisk=${free_gb}GB" + return 0 +} + +# Confirm action (returns 0 for yes, 1 for no) +confirm() { + local prompt="${1:-Are you sure?}" + local response + + if ! is_interactive; then + # In non-interactive mode, assume yes + return 0 + fi + + read -r -p "$prompt [y/N]: " response + case "$response" in + [yY][eE][sS]|[yY]) + return 0 + ;; + *) + return 1 + ;; + esac +}