diff --git a/DEPENDENCY_UPDATE_SUMMARY.md b/DEPENDENCY_UPDATE_SUMMARY.md deleted file mode 100644 index 9a9a1fd..0000000 --- a/DEPENDENCY_UPDATE_SUMMARY.md +++ /dev/null @@ -1,136 +0,0 @@ -# Dependency Update Summary - -## Overview - -This document summarizes the dependency updates applied to align the refactored codebase with the latest stable versions from the main branch. - -## Updated Dependencies - -### Production Dependencies (requirements.txt) - -| Package | Old Version | New Version | Change Type | -|---------|------------|-------------|-------------| -| `uvicorn` | 0.27.0 | **0.40.0** | Minor version bump | -| `google-generativeai` | 0.3.2 | **0.8.6** | Significant minor update | -| `python-multipart` | 0.0.18 | **0.0.22** | Patch version | - -### Test Dependencies (requirements-test.txt) - -| Package | Old Version | New Version | Change Type | -|---------|------------|-------------|-------------| -| `pytest-asyncio` | 0.23.3 | **1.3.0** | **Major version bump** | -| `pytest-cov` | 4.1.0 | **7.0.0** | **Major version bump** | - -## Impact Analysis - -### 1. uvicorn (0.27.0 → 0.40.0) - -**Changes:** -- Performance improvements in ASGI server -- Better WebSocket handling -- Improved error messages - -**Code Impact:** ✅ None - Backward compatible -**Action Required:** None - -### 2. google-generativeai (0.3.2 → 0.8.6) - -**Changes:** -- Enhanced type hints -- New model capabilities (multimodal improvements) -- Better error handling -- Streaming improvements - -**Code Impact:** ✅ None - Our usage is compatible -**API Usage Verified:** -- `genai.configure(api_key=...)` - Compatible -- `genai.GenerativeModel('gemini-2.0-flash')` - Compatible -- No breaking changes in our use cases - -**Action Required:** None - -### 3. python-multipart (0.0.18 → 0.0.22) - -**Changes:** -- Security fixes -- Bug fixes in multipart parsing -- Performance improvements - -**Code Impact:** ✅ None - Internal library used by FastAPI -**Action Required:** None - -### 4. pytest-asyncio (0.23.3 → 1.3.0) ⚠️ Major Version - -**Breaking Changes:** -- Requires explicit `asyncio_mode` configuration -- Changes to fixture loop scope behavior - -**Code Impact:** ✅ Addressed -**Changes Made:** -- Added `asyncio_mode = auto` to `pytest.ini` -- Added `asyncio_default_fixture_loop_scope = function` for proper test isolation -- All async tests will now run with automatic mode detection - -**Action Required:** ✅ Complete - Configuration updated - -### 5. pytest-cov (4.1.0 → 7.0.0) ⚠️ Major Version - -**Changes:** -- Updated to support latest coverage.py -- Better integration with pytest 7+ -- Improved branch coverage reporting - -**Code Impact:** ✅ None - Compatible with our existing test setup -**Action Required:** None - -## Configuration Changes - -### pytest.ini - -**Added:** -```ini -asyncio_mode = auto -asyncio_default_fixture_loop_scope = function -``` - -**Purpose:** -- `asyncio_mode = auto`: Automatically detects async tests and runs them appropriately -- `asyncio_default_fixture_loop_scope = function`: Ensures each test gets a fresh event loop for proper isolation - -## Verification - -All updates have been verified: - -1. ✅ **Syntax Check**: All Python files compile without errors -2. ✅ **API Compatibility**: Google Gemini API usage verified compatible -3. ✅ **Test Configuration**: pytest.ini updated for pytest-asyncio 1.x -4. ✅ **No Breaking Changes**: All code patterns remain valid - -## Migration Notes - -If you're pulling these changes: - -1. **No code changes required** - All updates are backward compatible -2. **Run `pip install -r requirements.txt -r requirements-test.txt`** to update dependencies -3. **Tests will continue to work** with the updated pytest configuration - -## Security & Stability - -All updated versions are: -- ✅ Stable releases (not pre-release or beta) -- ✅ Widely adopted in production -- ✅ Include security fixes from previous versions -- ✅ Compatible with Python 3.11+ - -## References - -- [uvicorn Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) -- [google-generativeai Releases](https://github.com/google/generative-ai-python/releases) -- [pytest-asyncio Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/main/CHANGELOG.rst) -- [pytest-cov Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - ---- - -**Updated:** 2026-02-08 -**Status:** ✅ Complete and Verified -**Breaking Changes:** None (with configuration updates applied) diff --git a/FINAL_SUMMARY.md b/FINAL_SUMMARY.md deleted file mode 100644 index 22af6dc..0000000 --- a/FINAL_SUMMARY.md +++ /dev/null @@ -1,53 +0,0 @@ -# Comprehensive Refactoring - Final Summary - -## Executive Summary - -Successfully completed a comprehensive 4-phase refactoring of the MeticAI codebase, transforming a monolithic 7,230-line application into a well-organized, modular architecture while eliminating 300+ lines of duplicate code and updating all dependencies. - -## Metrics - -### Code Reduction -- **Main.py**: 7,230 → 1,325 lines (**82% reduction**) -- **Shell Scripts**: ~300 lines of duplication eliminated -- **Total Modularized**: 9,744 lines across 15 focused modules - -### Files Created -- **15 new modules**: 6 routes, 7 services, 2 utilities -- **1 config module**: Unified configuration management -- **1 shared library**: Shell script common functions -- **6 documentation files**: Comprehensive guides - -### Tests Updated -- **352 test functions** modernized for modular architecture -- **198 @patch decorators** updated to new module paths -- **72 service imports** added - -## Phase-by-Phase Summary - -### Phase 0: Naming Standardization ✅ -**Result**: Renamed coffee-relay → meticai-server (100+ occurrences) - -### Phase 1: Quick Wins & Shell Consolidation ✅ -**Result**: ~300 lines removed, quality improvements - -### Phase 2: Modularization ✅ -**Result**: 82% reduction, 15 focused modules created - -### Phase 3: Test Modernization ✅ -**Result**: 352 tests modernized, ready for coverage enhancement - -### Phase 4: Documentation & Configuration ✅ -**Result**: Unified config, comprehensive Docker documentation - -## Security - -- ✅ **0 security alerts** from CodeQL scan -- ✅ Documented Docker volume security risks -- ✅ All security patches in updated dependencies - -## Status - -✅ **ALL PHASES COMPLETE** -✅ Zero security alerts -✅ 100% backward compatible -✅ Ready for merge diff --git a/PHASE2_COMPLETION_REPORT.md b/PHASE2_COMPLETION_REPORT.md deleted file mode 100644 index 635bcad..0000000 --- a/PHASE2_COMPLETION_REPORT.md +++ /dev/null @@ -1,205 +0,0 @@ -# Phase 2 Modularization - Completion Report - -## Executive Summary - -Phase 2 of the MeticAI refactoring is **complete**. The monolithic 7,230-line `main.py` file has been successfully decomposed into 15 focused, testable modules. - -## Transformation Metrics - -### Before (Monolithic) -``` -main.py: 7,230 lines -- 121 functions -- 46 API endpoints -- Difficult to navigate -- High coupling -- Hard to test -``` - -### After (Modular) -``` -Total: 9,744 lines across 15 files - -main.py: 1,325 lines (82% reduction!) -- App setup only -- Route registration -- Lifecycle management - -API Routes: 6,284 lines (6 files, 46 endpoints) -Services: 2,017 lines (7 files, 48+ functions) -Utils: 117 lines (2 files) -``` - -## Module Breakdown - -### API Routes (6 modules, 46 endpoints) - -| Module | Lines | Endpoints | Responsibility | -|--------|-------|-----------|----------------| -| `api/routes/coffee.py` | 435 | 2 | Coffee bag analysis, profile creation | -| `api/routes/system.py` | 976 | 10 | Status, updates, logs, settings, version | -| `api/routes/history.py` | 290 | 6 | Profile history CRUD, migration | -| `api/routes/shots.py` | 818 | 7 | Shot data retrieval and analysis | -| `api/routes/profiles.py` | 2,953 | 11 | Profile images, import/export | -| `api/routes/scheduling.py` | 812 | 10 | Machine control, shot scheduling | - -### Services (7 modules, 48+ functions) - -| Module | Lines | Functions | Responsibility | -|--------|-------|-----------|----------------| -| `services/cache_service.py` | 238 | 14 | LLM, shot, and image caching | -| `services/settings_service.py` | 59 | 4 | Settings management | -| `services/history_service.py` | 155 | 7 | Profile history operations | -| `services/gemini_service.py` | 140 | 4 | AI/Gemini integration | -| `services/meticulous_service.py` | 131 | 4 | Espresso machine API | -| `services/analysis_service.py` | 1,147 | 15 | Shot analysis logic | -| `services/scheduling_state.py` | 145 | - | Scheduling state management | - -### Utilities (2 modules) - -| Module | Lines | Functions | Responsibility | -|--------|-------|-----------|----------------| -| `utils/file_utils.py` | 74 | 2 | Atomic writes, JSON conversion | -| `utils/sanitization.py` | 43 | 2 | Input sanitization | - -## Key Achievements - -### ✅ Code Organization -- **Clear separation of concerns** - Each module has a single responsibility -- **Domain-driven structure** - Modules organized by feature area -- **Easy navigation** - Developers can find code by functionality - -### ✅ Improved Testability -- **Isolated testing** - Each service/route can be tested independently -- **Mock-friendly** - Clear boundaries for dependency injection -- **Reduced setup** - Tests can import only what they need - -### ✅ Maintainability -- **Smaller files** - Largest route module is 2,953 lines vs 7,230 original -- **Focused modules** - Each file does one thing well -- **Better readability** - Less scrolling, clearer structure - -### ✅ Developer Experience -- **Faster IDE performance** - Smaller files load faster -- **Better IntelliSense** - Clearer import paths -- **Easier onboarding** - New developers can understand modules quickly - -### ✅ Zero Breaking Changes -- **100% backward compatible** - All API contracts preserved -- **Same endpoints** - No URL changes -- **Same behavior** - All functionality maintained - -## Technical Details - -### Import Strategy -All route modules use APIRouter: -```python -from fastapi import APIRouter -router = APIRouter() - -@router.post("/analyze_coffee") -async def analyze_coffee(...): - ... -``` - -### Main.py Registration -```python -from api.routes import coffee, system, history, shots, profiles, scheduling - -app.include_router(coffee.router) -app.include_router(system.router) -app.include_router(history.router) -app.include_router(shots.router) -app.include_router(profiles.router) -app.include_router(scheduling.router) -``` - -### Service Imports -Services are imported where needed: -```python -from services.cache_service import get_cached_llm_analysis -from services.gemini_service import get_vision_model -from services.analysis_service import perform_local_shot_analysis -``` - -## Code Quality Improvements - -During extraction, the following issues were fixed: -- ✅ Deprecated API usage (`app.on_event` → `lifespan`) -- ✅ Magic numbers replaced with named constants -- ✅ Missing function definitions added -- ✅ All docstrings preserved - -## Validation - -All code has been validated: -- ✅ **Syntax check** - All 15 files compile without errors -- ✅ **Import check** - All imports resolve correctly -- ✅ **Structure check** - Router registration verified - -## Next Steps (Phase 3) - -With the modularization complete, the foundation is in place for: - -1. **Test Coverage Enhancement** - - Update test imports to use new modules - - Add unit tests for each service module - - Add integration tests for route modules - - Target: 70%+ coverage (up from current 25%) - -2. **API Documentation** - - Add OpenAPI examples to route modules - - Document request/response schemas - - Add usage examples - -3. **Configuration Management** - - Create unified config module with pydantic-settings - - Consolidate scattered configuration - -4. **Performance Optimization** - - Profile the modular code - - Optimize imports - - Add caching where beneficial - -## Files Changed - -### Created (17 new files): -- `api/__init__.py` -- `api/routes/__init__.py` -- `api/routes/coffee.py` -- `api/routes/system.py` -- `api/routes/history.py` -- `api/routes/shots.py` -- `api/routes/profiles.py` -- `api/routes/scheduling.py` -- `services/__init__.py` -- `services/cache_service.py` -- `services/settings_service.py` -- `services/history_service.py` -- `services/gemini_service.py` -- `services/meticulous_service.py` -- `services/analysis_service.py` -- `utils/__init__.py` -- `utils/file_utils.py` -- `utils/sanitization.py` - -### Modified: -- `main.py` (7,230 → 1,325 lines) - -## Conclusion - -Phase 2 modularization has successfully transformed the MeticAI codebase from a monolithic architecture to a well-organized, modular system. The code is now: -- **More maintainable** - Smaller, focused modules -- **More testable** - Clear boundaries for testing -- **More scalable** - Easy to add new features -- **More approachable** - Easier for new developers - -The refactoring maintains 100% backward compatibility while dramatically improving code quality and developer experience. - ---- - -**Phase 2 Status**: ✅ **COMPLETE** -**Lines Refactored**: 9,744 lines across 15 modules -**Reduction**: 82% (main.py: 7,230 → 1,325 lines) -**Quality**: All files compile, all functionality preserved -**Next**: Phase 3 - Test Coverage Enhancement diff --git a/PHASE3_PLAN.md b/PHASE3_PLAN.md deleted file mode 100644 index 02cf7c5..0000000 --- a/PHASE3_PLAN.md +++ /dev/null @@ -1,119 +0,0 @@ -# Phase 3: Test Coverage Enhancement - Implementation Plan - -## Overview - -Phase 3 focuses on improving test coverage from the current ~25% to 70%+ on critical paths. With the modularized codebase from Phase 2, we can now test components in isolation. - -## Current State - -- **Test File**: `test_main.py` (7,362 lines, 352 test functions) -- **Current Coverage**: ~25% (369/1504 statements) -- **Target Coverage**: 70%+ on critical paths - -## Phase 3 Goals - -### 1. Update Test Imports ✅ PRIORITY 1 -- Update imports in `test_main.py` to use new modular structure -- Change from testing monolithic `main.py` to testing individual modules -- Ensure all existing tests still pass - -### 2. Add Service Module Tests 📝 PRIORITY 2 -Create dedicated test files for each service: -- `tests/test_cache_service.py` - Cache management (14 functions) -- `tests/test_settings_service.py` - Settings operations (4 functions) -- `tests/test_history_service.py` - History management (7 functions) -- `tests/test_gemini_service.py` - AI integration (4 functions) -- `tests/test_meticulous_service.py` - Machine API (4 functions) -- `tests/test_analysis_service.py` - Shot analysis (15 functions) - -### 3. Add Route Module Tests 📝 PRIORITY 3 -Create dedicated test files for each route module: -- `tests/test_routes_coffee.py` - Coffee analysis endpoints (2 endpoints) -- `tests/test_routes_system.py` - System endpoints (10 endpoints) -- `tests/test_routes_history.py` - History CRUD (6 endpoints) -- `tests/test_routes_shots.py` - Shot endpoints (7 endpoints) -- `tests/test_routes_profiles.py` - Profile management (11 endpoints) -- `tests/test_routes_scheduling.py` - Scheduling (10 endpoints) - -### 4. Add Utility Tests 📝 PRIORITY 4 -- `tests/test_file_utils.py` - File operations (2 functions) -- `tests/test_sanitization.py` - Input sanitization (2 functions) - -## Critical Path Focus Areas - -Based on the issue, focus testing on: -1. ✅ Shot analysis functions (`_analyze_stage_execution`, `_perform_local_shot_analysis`) -2. ✅ Profile image generation and processing -3. ✅ Scheduling system (recurring schedules, shot execution) -4. ✅ Machine API interaction layer -5. ✅ Cache management functions - -## Testing Strategy - -### Unit Tests -- Test each service/utility function in isolation -- Mock external dependencies (API calls, file I/O, database) -- Use pytest fixtures for common setup - -### Integration Tests -- Test route modules with full FastAPI TestClient -- Test end-to-end workflows -- Validate API contracts - -### Test Organization -- Follow existing patterns from `test_main.py` -- Group related tests into test classes -- Use descriptive test names -- Add docstrings explaining test purpose - -## Success Criteria - -- ✅ All existing tests pass after import updates -- ✅ 70%+ coverage on service modules -- ✅ 70%+ coverage on critical path functions -- ✅ All new tests pass in CI/CD -- ✅ No decrease in overall code quality - -## Implementation Steps - -### Step 1: Update Existing Test Imports -1. Analyze current test structure -2. Update imports to use new modules -3. Run tests and fix any failures -4. Commit working tests - -### Step 2: Add Service Tests -1. Start with cache_service (most functions) -2. Add analysis_service tests (critical path) -3. Add remaining service tests -4. Verify coverage improvements - -### Step 3: Add Route Tests -1. Start with coffee routes (entry point) -2. Add system routes (health checks) -3. Add remaining route tests -4. Ensure integration tests work - -### Step 4: Add Utility Tests -1. Test file_utils -2. Test sanitization -3. Verify edge cases - -### Step 5: Verify and Document -1. Run full test suite -2. Generate coverage report -3. Document coverage improvements -4. Update test documentation - -## Notes - -- Maintain existing test patterns and conventions -- Use pytest-asyncio for async tests -- Mock external services (Gemini API, Meticulous machine) -- Keep tests fast and independent -- Follow TDD principles for new features - ---- - -**Status**: Planning Complete - Ready to Begin Implementation -**Next**: Update test imports in `test_main.py` diff --git a/PHASE4_PLAN.md b/PHASE4_PLAN.md deleted file mode 100644 index 14d9bb9..0000000 --- a/PHASE4_PLAN.md +++ /dev/null @@ -1,125 +0,0 @@ -# Phase 4: Documentation & Configuration - Implementation Plan - -## Overview - -Phase 4 focuses on improving API documentation and creating a unified configuration management system. - -## Goals - -### 1. Add Comprehensive API Documentation -- Add detailed docstrings to all API endpoints -- Include parameter descriptions -- Document response schemas -- Add error response documentation -- Consider OpenAPI examples - -### 2. Create Unified Configuration Module -- Consolidate scattered configuration -- Use pydantic-settings for type-safe configuration -- Environment variables centralized -- Constants defined in one place -- Easy to test and mock - -### 3. Document Docker Compose Volume Mounts -- Document purpose of each volume mount -- Identify which are essential vs optional -- Security review of mounts (.git, docker.sock) -- Consider reducing where possible - -## Implementation Steps - -### Step 1: Create Unified Config Module - -Create `meticai-server/config.py` (simple class-based approach for no new dependencies): -```python -class Config: - # Environment-based configuration - GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") - METICULOUS_IP = os.environ.get("METICULOUS_IP") - - # Application constants - UPDATE_CHECK_INTERVAL = 7200 # 2 hours - MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 MB - - # Cache settings - LLM_CACHE_TTL_SECONDS = 259200 # 3 days - SHOT_CACHE_STALE_SECONDS = 3600 # 1 hour -``` - -Note: Using simple class instead of pydantic-settings to avoid adding new dependencies. - -### Step 2: Update Code to Use Config - -- Replace hardcoded constants with config.settings -- Update services to accept config as dependency -- Make testing easier with configurable values - -### Step 3: Add API Documentation - -For each route module, enhance docstrings: -```python -@router.post("/analyze_coffee") -async def analyze_coffee( - request: Request, - file: UploadFile = File(...) -): - """Analyze a coffee bag image using Google Gemini Vision AI. - - Args: - request: FastAPI request object - file: Coffee bag image (PNG, JPEG, or WEBP, max 10MB) - - Returns: - JSONResponse containing: - - analysis (str): Coffee identification and characteristics - - status (str): "success" - - timestamp (str): ISO 8601 timestamp - - Raises: - HTTPException(400): If file is too large or invalid format - HTTPException(500): If Gemini API fails - - Example: - POST /analyze_coffee - Content-Type: multipart/form-data - - Response: - { - "analysis": "Ethiopian Yirgacheffe, Light Roast, Floral Notes", - "status": "success", - "timestamp": "2026-02-08T14:47:00Z" - } - """ -``` - -### Step 4: Document Docker Volumes - -Create `DOCKER_VOLUMES.md` documenting each mount: - -| Mount | Purpose | Essential | Security Notes | -|-------|---------|-----------|----------------| -| `/var/run/docker.sock` | Container management | Yes | Risk: Full Docker access | -| `./.git` | Version tracking | No | Consider removing | -| `./data` | Persistent data | Yes | Safe | -| `./logs` | Log files | Yes | Safe | - -### Step 5: Verify and Test - -- Ensure config module works -- Test with environment variables -- Verify all imports work -- Update tests to use config - -## Success Criteria - -- ✅ All API endpoints have comprehensive docstrings -- ✅ Unified config module created -- ✅ All constants moved to config -- ✅ Docker volumes documented -- ✅ Tests pass with config changes -- ✅ Code review passes - ---- - -**Status**: Planning Complete - Ready to Implement -**Next**: Create config.py module diff --git a/README.md b/README.md index c0517a6..5a54953 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ *1. Take a photo or describe your coffee. Get a perfect espresso profile. Automatically.* *2. Understand your profiles, shot graphs by enabling shot comparison, analysis and AI-coaching* -[Get Started](#-quick-start) • [Features](#-what-it-does) • [Web Interface](#-using-meticai) • [Updates](UPDATE_GUIDE.md) +[Get Started](#-quick-start) • [Features](#-what-it-does) • [Web Interface](#-using-meticai) • [API](API.md) @@ -44,15 +44,13 @@ When I got my Meticulous, after a loooong wait, I was overwhelmed with the optio ### For Power Users - 🔌 **REST API** - Integrate with any automation system -- 🐳 **Self-Hosted** - Runs on Raspberry Pi or any Unix server +- 🐳 **Single Docker Container** - Simple deployment and updates - 🔓 **Open Source** - Customize and extend as you like -- 📡 **Update System** - One-command updates for all components +- 🔄 **Auto Updates** - Optional Watchtower integration ### Additional Features -- 🍎 **macOS Dock Integration** - Optional dock shortcut for quick access -- 📱 **QR Code Setup** - Easy mobile access during installation -- 🔄 **Automatic Updates** - Built-in update system with web UI support -- 🌍 **URL Integration** - Control via curl from any HTTP-capable device +- 📱 **iOS Shortcuts** - One-tap brewing from your iPhone +- 🌍 **Remote Access** - Optional Tailscale integration - 🔐 **Secure** - Self-hosted means your data stays private - 🎨 **Modern UI** - Built with React and shadcn/ui for a polished experience @@ -60,65 +58,79 @@ When I got my Meticulous, after a loooong wait, I was overwhelmed with the optio ### What You Need - ☑️ A **Meticulous Espresso Machine** (connected to your network) -- ☑️ A server to run MeticAI (Raspberry Pi, Mac, or Linux computer) +- ☑️ A server to run MeticAI (Raspberry Pi, Mac, Linux, or Windows with Docker) - ☑️ A **free Google Gemini API key** → [Get yours here](https://aistudio.google.com/app/apikey) (takes 30 seconds) ### Installation (5 minutes) -**Option 1: macOS Installer App** (Easiest for Mac users) +**Prerequisites:** +- Docker and Docker Compose installed ([Get Docker](https://docs.docker.com/get-docker/)) +- Git -Download and run the standalone installer app - **completely GUI-based, no terminal at all!** +**Recommended: Git Clone Method** -1. Download the installer: [MeticAI-Installer.dmg](https://github.com/hessius/MeticAI/releases/latest) *(coming soon)* -2. Open the DMG and drag "MeticAI Installer" to Applications -3. Launch the app and follow the graphical prompts +This is the safest and most transparent installation method: -The app will guide you through everything via dialogs: -- ✅ Checking prerequisites (with helpful install links) -- ✅ Choosing installation location -- ✅ Entering API key and IP addresses -- ✅ Background installation with progress feedback -- ✅ Auto-opens web interface when complete +```bash +# 1. Clone the repository (recommended: use a specific release tag when available) +git clone https://github.com/hessius/MeticAI.git +cd MeticAI -**No Terminal window - 100% graphical!** +# Optional: Checkout a specific release for stability +# git checkout v2.0.0 # (use when tagged releases are available) -[→ Learn more about the macOS installer](macos-installer/README.md) +# 2. Create .env file with your configuration +cat > .env << EOF +GEMINI_API_KEY=your_api_key_here +METICULOUS_IP=meticulous.local # or IP address like 192.168.1.100 +EOF -**Option 2: One-Line Install** (Recommended for terminal users) -```bash -curl -fsSL https://raw.githubusercontent.com/hessius/MeticAI/main/web_install.sh | bash +# 3. Start MeticAI +docker compose up -d ``` -That's it! The installer will: -- ✅ Check for and install prerequisites (git, docker, docker-compose) -- ✅ Detect and handle any existing MeticAI installations -- ✅ Stop and remove running MeticAI containers if found -- ✅ Guide you through setup (just paste your API key and machine IP) -- ✅ Download and start all services -- ✅ Show a QR code to access the web interface from your phone -- ✅ *[macOS only]* Optionally create a Dock icon for quick access +**Alternative: Direct Download (Advanced Users)** + +> ⚠️ **Security Warning**: Downloading and executing scripts or configuration files directly from the internet carries security risks. Only use this method if you trust the source and have verified the file contents. + +If you prefer not to clone the entire repository, you can download just the compose file: -**Option 3: Manual Install** ```bash -git clone https://github.com/hessius/MeticAI.git -cd MeticAI -./local-install.sh +# Create configuration directory +mkdir -p ~/.meticai && cd ~/.meticai + +# Download and inspect the compose file BEFORE running it +# Use a specific commit hash for reproducibility and security +# Find the latest commit at: https://github.com/hessius/MeticAI/commits/main +COMMIT_HASH="104d7c5" # Example: update this to your chosen commit +curl -fsSL "https://raw.githubusercontent.com/hessius/MeticAI/${COMMIT_HASH}/docker-compose.yml" -o docker-compose.yml + +# IMPORTANT: Review the downloaded file before proceeding +cat docker-compose.yml + +# Verify file integrity (optional but recommended) +# Compare the file hash with the one published in the release notes +sha256sum docker-compose.yml + +# Create .env file +cat > .env << EOF +GEMINI_API_KEY=your_api_key_here +METICULOUS_IP=meticulous.local # or IP address like 192.168.1.100 +EOF + +# Start MeticAI only after verifying the compose file +docker compose up -d ``` -After installation completes, scan the QR code with your phone or visit `http://YOUR_SERVER_IP:3550` in a browser! - -**Reinstalling or Upgrading?** +> **Best Practice**: Always review configuration files before running them, especially when downloaded from the internet. The git clone method above is recommended as it provides full transparency and version control. -If you already have MeticAI installed, the installer will: -- Detect existing containers and installation artifacts -- Offer to run the uninstall script first for a clean installation -- Allow you to continue anyway if you prefer to reuse existing configuration +### After Installation -For a clean reinstall, it's recommended to run `./uninstall.sh` first. +Open `http://YOUR_SERVER_IP:3550` in any browser to access the web interface! ### Need Help? -- 📖 [Detailed installation guide](TECHNICAL.md#manual-setup-alternative) -- 🔧 [Troubleshooting common issues](#troubleshooting) +- 📖 [Technical documentation](TECHNICAL.md) +- 🔧 [Troubleshooting](#troubleshooting) ## 📱 Using MeticAI @@ -139,229 +151,151 @@ For automation and integration: **With a photo:** ```bash -curl -X POST http://YOUR_IP:8000/analyze_and_profile \ +curl -X POST http://YOUR_IP:3550/api/v1/analyze_and_profile \ -F "file=@coffee_bag.jpg" ``` **With text preferences:** ```bash -curl -X POST http://YOUR_IP:8000/analyze_and_profile \ +curl -X POST http://YOUR_IP:3550/api/v1/analyze_and_profile \ -F "user_prefs=Bold and chocolatey" ``` **With both:** ```bash -curl -X POST http://YOUR_IP:8000/analyze_and_profile \ +curl -X POST http://YOUR_IP:3550/api/v1/analyze_and_profile \ -F "file=@coffee_bag.jpg" \ -F "user_prefs=Traditional extraction" ``` [→ Full API documentation](API.md) -### Advanced: iOS Shortcuts +### iOS Shortcuts For power users who want one-tap brewing from their iPhone, you can create custom shortcuts. [→ iOS Shortcuts setup guide](IOS_SHORTCUTS.md) -## 🔄 Keeping MeticAI Updated +## 🔄 Updating MeticAI -MeticAI has automatic updates built in! +MeticAI v2.0 uses Docker for simple updates: **Quick update:** ```bash -./update.sh +cd ~/.meticai +docker compose pull +docker compose up -d ``` -**Check without updating:** -```bash -./update.sh --check-only -``` - -The system automatically: -- ✅ Checks all components for updates -- ✅ Shows what's new -- ✅ Updates and rebuilds containers -- ✅ Can even update from the web interface! +**With Watchtower (automatic updates):** -[→ Full update system documentation](UPDATE_GUIDE.md) +If you enabled Watchtower during installation, MeticAI will automatically check for updates every 6 hours and update seamlessly. -## 🗑️ Uninstalling MeticAI +**Manual trigger via API:** -Need to remove MeticAI? We've got you covered with a clean uninstallation process. - -**Run the uninstaller:** +To use this endpoint, your Watchtower container must: +- be started with the HTTP API enabled (for example using `--http-api-update` and a token via `--http-api-token` or `WATCHTOWER_HTTP_API_TOKEN`), and +- publish its API port to the host (for example `-p 8080:8080` or `ports: ["8080:8080"]` in Docker Compose so that `http://localhost:8080` is reachable). ```bash -./uninstall.sh +curl -X POST http://localhost:8080/v1/update \ + -H "Authorization: Bearer YOUR_WATCHTOWER_TOKEN" ``` -The uninstaller will: -- ✅ Stop and remove all Docker containers -- ✅ Remove Docker images built by MeticAI -- ✅ Remove cloned repositories (meticulous-source, meticai-web) -- ✅ Remove configuration files (.env, settings) -- ✅ Remove macOS integrations (Dock shortcut, rebuild watcher) -- ✅ Ask about external dependencies (Docker, git, qrencode) - -**Safe by default:** -- External dependencies are **NOT** automatically removed -- You'll be asked to confirm before removing anything -- Summary shows what was removed and what was kept - -**Note:** The uninstaller doesn't remove Docker, git, or other tools unless you explicitly choose to do so. This is safe if you use these tools for other projects. - ---- - -## 🎨 What Makes MeticAI Special - -### The AI Barista Persona - -MeticAI doesn't just create recipes—it creates *experiences* with: - -**🎯 Witty Profile Names** -- "Slow-Mo Blossom" for gentle light roasts -- "Choco-Lot Going On" for bold chocolatey extractions -- "Warp Speed Espresso" for turbo shots - -**📊 Complete Guidance** -Every profile includes: -- ☕️ Recommended dose and grind settings -- 🌡️ Temperature recommendations -- 🔬 Scientific explanation of why it works -- ⚙️ Any special equipment notes - -**🚀 Modern Techniques** -Supports advanced espresso methods: -- Multi-stage extractions -- Pre-infusion and blooming -- Pressure profiling and flow control -- Turbo shots and more - -[→ See example profiles and dialogues](TECHNICAL.md#enhanced-barista-experience) - ---- - -## 📚 Additional Resources +## 🗑️ Uninstalling MeticAI -### For Users -- 📱 [iOS Shortcuts Setup Guide](IOS_SHORTCUTS.md) -- 🔄 [Update System Guide](UPDATE_GUIDE.md) -- 📊 [Logging & Diagnostics](LOGGING.md) -- 🔧 [Troubleshooting](#troubleshooting) +```bash +cd ~/.meticai +docker compose down -v # -v removes all volumes and data +rm -rf ~/.meticai +``` -### For Developers -- 🔌 [API Documentation](API.md) -- 🏗️ [Technical Architecture](TECHNICAL.md) -- 🧪 [Testing Guide](TEST_COVERAGE.md) -- 🔒 [Security Notes](SECURITY_FIXES.md) +**Note:** To verify volume names before removal, use `docker volume ls` ---- +## 🌐 Optional: Remote Access with Tailscale -## 🆘 Troubleshooting +Access MeticAI from anywhere using Tailscale: -### Installation Issues +1. Get an auth key from [Tailscale Admin](https://login.tailscale.com/admin/settings/keys) +2. Enable during installation, or add manually: -**Prerequisites not installing:** -- The script auto-detects your OS and installs what's needed -- On unsupported systems, manually install: git, docker, docker-compose -- See [TECHNICAL.md](TECHNICAL.md#manual-setup-alternative) for manual setup +```bash +cd ~/.meticai +echo "TAILSCALE_AUTHKEY=your_key_here" >> .env +docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d +``` -**Can't connect to machine:** -- Verify your Meticulous machine is on the network -- Check the IP address is correct in your `.env` file -- Ensure both devices are on the same network +## 🏗️ Architecture -### Usage Issues +MeticAI v2.0 runs as a single unified container: -**"Connection Failed" errors:** -- Make sure MeticAI is running: `docker ps` -- Check you're on the same network as the server -- Verify the IP address in your requests +``` +┌─────────────────────────────────────────────────────────┐ +│ MeticAI Container │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ nginx (:3550) │ │ +│ │ Web UI + API Reverse Proxy │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────┼───────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Relay │ │ MCP Server │ │ Gemini CLI │ │ +│ │ (FastAPI) │ │ (Meticulous)│ │ (AI) │ │ +│ │ :8000 │ │ :8080 │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` -**Profiles not appearing on machine:** -- Check the MCP server logs: `docker logs meticulous-mcp -f` -- Verify `METICULOUS_IP` in `.env` is correct -- Ensure the machine's API is accessible +**Optional sidecars:** +- **Tailscale** - Secure remote access +- **Watchtower** - Automatic container updates -**Poor coffee analysis:** -- Take photos in good lighting -- Ensure the label is clear and in focus -- Try adding text preferences to guide the AI +## 🛠️ Troubleshooting -### Getting Help +### Container won't start -**Check detailed logs:** ```bash -# View structured error logs -tail -f logs/meticai-server-errors.log | jq . - -# View all logs -tail -f logs/meticai-server.log | jq . - -# Or via API -curl "http://localhost:8000/api/logs?level=ERROR&lines=100" +# Check logs +cd ~/.meticai && docker compose logs -f -# See LOGGING.md for more details +# Check container status +docker compose ps ``` -**Check container logs:** -```bash -# All services -docker compose logs -f +### Can't connect to Meticulous machine -# Specific service -docker logs meticai-server -f -docker logs gemini-client -f -docker logs meticulous-mcp -f -``` +1. Verify the machine is on and connected to your network +2. Check the IP address in your `.env` file +3. Try using the IP address instead of `meticulous.local` -**Restart services:** -```bash -docker compose restart -``` +### API returns errors -**Full reset (recommended - uses wrapper script for correct permissions):** ```bash -./docker-up.sh +# Check relay logs specifically +docker compose logs meticai | grep -i error ``` -**Full reset (manual - may require permission fix on Linux):** +### Reset everything + ```bash -docker compose down -docker compose up -d --build -# If you used sudo, fix permissions: -sudo chown -R $(id -u):$(id -g) data logs meticulous-source meticai-web +cd ~/.meticai +docker compose down -v # -v removes volumes +docker compose pull +docker compose up -d ``` -For comprehensive troubleshooting and log analysis, see [LOGGING.md](LOGGING.md). - ---- +## 📄 License -## 🙏 Credits & Attribution +MIT License - see [LICENSE](LICENSE) for details. -MeticAI is built on the excellent [Meticulous MCP](https://github.com/twchad/meticulous-mcp) project by **twchad** and its [containerized fork](https://github.com/hessius/meticulous-mcp) (originally by **manonstreet**), which provides the essential interface for controlling the Meticulous Espresso Machine. +## 🙏 Acknowledgments -### Technology Stack -- **Google Gemini 2.0 Flash** - Vision AI and reasoning -- **FastAPI** - Backend API framework -- **Docker** - Containerization and deployment -- **React** - Web interface -- **Python** - Core application logic - -### Open Source - -MeticAI is open source and welcomes contributions! - -- 📖 [View the code on GitHub](https://github.com/hessius/MeticAI) -- 🐛 [Report issues](https://github.com/hessius/MeticAI/issues) -- 💡 [Contribute improvements](https://github.com/hessius/MeticAI/pulls) +- [Meticulous](https://meticulous.coffee/) for creating an amazing machine +- [Google Gemini](https://ai.google.dev/) for AI capabilities +- [meticulous-mcp](https://github.com/meticulous/meticulous-mcp) for machine communication ---
- -**Made with ☕️, ❤️, and 🤖** - -[Get Started](#-quick-start) • [Features](#-what-it-does) • [Documentation](#-additional-resources) - +Made with ☕ by @hessius
diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md deleted file mode 100644 index cbfe078..0000000 --- a/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,193 +0,0 @@ -# MeticAI Refactoring Summary - -This document summarizes the comprehensive refactoring work completed to improve code maintainability, reduce technical debt, and establish a foundation for future scalability. - -## Completed Phases - -### Phase 0: Repository Renaming ✅ - -**Objective**: Rename "coffee-relay" to "meticai-server" for naming consistency with MeticAI branding. - -**Changes Made**: -- Renamed directory: `coffee-relay/` → `meticai-server/` -- Updated Docker service name in `docker-compose.yml` -- Updated container name: `coffee-relay` → `meticai-server` -- Updated all Python logger names and log file references -- Updated all shell scripts (7 files) -- Updated all documentation (7 MD files + copilot instructions) -- Updated CI/CD workflows (`.github/workflows/tests.yml`) -- Updated macOS installer files - -**Impact**: -- 100+ occurrences of "coffee-relay" replaced with "meticai-server" -- Log files now named: `meticai-server.log` and `meticai-server-errors.log` -- Logger name updated to: `meticai-server` -- All syntax validated (Python, YAML, Bash) - -### Phase 1: Quick Wins ✅ - -**Objective**: Fix immediate code quality issues and eliminate code duplication. - -#### 1.1 Python Code Improvements - -**Type Hints Fix**: -- Fixed `_resolve_variable()` function at line 3852 -- Changed `tuple[any, str | None]` → `tuple[Any, str | None]` -- Added proper import: `from typing import Optional, Any` - -**Logging Fix**: -- Removed duplicate logger initialization (lines 36-38) -- Now uses single logger from `logging_config.setup_logging()` -- Eliminated potential configuration conflicts - -**Dependency Pinning**: -Updated `meticai-server/requirements.txt`: -``` -pyMeticulous>=0.1.0 → pyMeticulous==0.1.0 -zstandard>=0.22.0 → zstandard==0.22.0 -httpx>=0.26.0 → httpx==0.26.0 -``` - -#### 1.2 Shell Script Consolidation - -**Created Shared Library**: `scripts/lib/common.sh` - -**Functions Provided**: -- **Colors**: GREEN, BLUE, YELLOW, RED, NC -- **Logging**: log_info(), log_success(), log_error(), log_warning(), show_progress() -- **Docker Utilities**: check_docker(), check_docker_compose(), get_compose_command(), stop_containers() -- **Git Utilities**: checkout_latest_release() -- **System Utilities**: run_privileged(), check_prerequisites() -- **Validation**: validate_ip(), validate_not_empty() -- **File Operations**: check_file_exists(), ensure_directory() - -**Scripts Updated** (7 total): -1. `docker-up.sh` - 13 lines removed -2. `check-updates-on-start.sh` - 7 lines removed -3. `rebuild-watcher.sh` - 4 lines removed, updated logging -4. `uninstall.sh` - 80+ echo statements replaced -5. `web_install.sh` - 30+ logging calls updated -6. `update.sh` - 87 logging calls replaced -7. `local-install.sh` - 100+ logging calls updated - -**Impact**: -- **~300 lines of duplicate code eliminated** -- Centralized error handling and logging -- Consistent color coding across all scripts -- Easier maintenance and debugging -- All scripts pass syntax validation - -## Code Quality Metrics - -### Before Refactoring -- Type hints: 1 instance of unsafe `any` type -- Duplicate code: ~300 lines across shell scripts -- Naming: Inconsistent (coffee-relay vs MeticAI) -- Logging: Duplicate initialization -- Dependencies: 3 unpinned versions - -### After Refactoring -- Type hints: ✅ All use proper `Any` type -- Duplicate code: ✅ Eliminated via shared library -- Naming: ✅ Consistent (meticai-server) -- Logging: ✅ Single initialization point -- Dependencies: ✅ All versions pinned - -## Testing & Validation - -All changes have been validated: -- ✅ Python syntax check: All `.py` files compile successfully -- ✅ Bash syntax check: All `.sh` files pass `bash -n` validation -- ✅ YAML syntax check: `docker-compose.yml` passes `docker compose config` -- ✅ Shared library test: Functions work correctly with color output - -## Future Work (Not Yet Implemented) - -### Phase 2: Modularization -- Extract `meticai-server/main.py` (7,234 lines, 121 functions) into modules: - - `api/routes/` - Route handlers by domain - - `services/` - Business logic services - - `utils/` - Utility functions - - `models/` - Pydantic schemas - -### Phase 3: Test Coverage Enhancement -- Current coverage: ~25% (369/1504 statements) -- Target: 70%+ coverage on critical paths -- Focus areas: - - Shot analysis functions - - Profile image generation - - Scheduling system - - Machine API layer - - Cache management - -### Phase 4: Documentation & Configuration -- Add comprehensive API docstrings -- Create unified config module with pydantic-settings -- Document Docker volume mounts -- Update API.md with new structure - -## Migration Notes for Developers - -### If You're Working on Old Branches - -After this refactoring, you'll need to update: - -1. **Directory References**: - - Old: `coffee-relay/` - - New: `meticai-server/` - -2. **Container Names**: - - Old: `coffee-relay` - - New: `meticai-server` - -3. **Log File Names**: - - Old: `coffee-relay.log`, `coffee-relay-errors.log` - - New: `meticai-server.log`, `meticai-server-errors.log` - -4. **Shell Scripts**: - - All scripts now source `scripts/lib/common.sh` - - Use library functions instead of duplicate code - - Follow established patterns in updated scripts - -### For New Development - -1. **Python Code**: - - Use `from typing import Any` for generic types - - Never use lowercase `any` - - Pin all dependency versions - - Use logger from `logging_config.setup_logging()` - -2. **Shell Scripts**: - - Always source the common library - - Use `log_*()` functions for output - - Use library utilities instead of reimplementing - - Test with `bash -n scriptname.sh` - -3. **Naming**: - - Server component: `meticai-server` - - Web component: `meticai-web` - - Logger name: `meticai-server` - - Project name: MeticAI (capital A and I) - -## Contributors - -This refactoring was completed to address technical debt identified in issue #[issue_number]. - -Special thanks to the comprehensive analysis that identified: -- Monolithic code structure -- Code duplication patterns -- Inconsistent naming -- Missing type hints - -## Related Documentation - -- See [TECHNICAL.md](TECHNICAL.md) for technical architecture -- See [README.md](README.md) for usage and installation -- See [TEST_COVERAGE.md](TEST_COVERAGE.md) for testing guidelines -- See `.github/copilot-instructions.md` for AI assistant context - ---- - -*Last Updated: 2026-02-08* -*Refactoring Phases Completed: 0, 1* -*Refactoring Phases Remaining: 2, 3, 4* diff --git a/TEST_FIXES.md b/TEST_FIXES.md deleted file mode 100644 index 48bdd6f..0000000 --- a/TEST_FIXES.md +++ /dev/null @@ -1,278 +0,0 @@ -# Test Fixes Documentation - -## Overview -This document explains the test failures that occurred after recent bugfixes and how they were resolved without rolling back the improvements. - ---- - -## Latest Fix (January 2026) - Post-Caching Implementation - -### Problem Statement -After adding server-side LLM analysis caching, **19 Python tests were failing**: -- **5 LLM analysis tests**: Permission denied errors on `/app/data` -- **6 Barista persona tests**: TypeError when accessing NoneType -- **8 analyze_and_profile tests**: Status returning 'error' instead of 'success' - -### Root Causes - -#### 1. Hardcoded Data Paths -**Issue**: All data files used hardcoded `/app/data` paths which don't exist in test environment. - -**Root Cause**: Paths were defined as constants at module level: -```python -SETTINGS_FILE = Path("/app/data/settings.json") -HISTORY_FILE = Path("/app/data/profile_history.json") -LLM_CACHE_FILE = Path("/app/data/llm_analysis_cache.json") -SHOT_CACHE_FILE = Path("/app/data/shot_cache.json") -IMAGE_CACHE_DIR = Path("/app/data/image_cache") -``` - -When tests ran, these paths couldn't be created due to permission issues, causing errors. - -**Fix**: Made paths configurable via environment variables: -```python -# In main.py -TEST_MODE = os.environ.get("TEST_MODE") == "true" -if TEST_MODE: - DATA_DIR = Path(tempfile.gettempdir()) / "meticai_test_data" - DATA_DIR.mkdir(parents=True, exist_ok=True) -else: - DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) - -# Then use DATA_DIR for all paths -SETTINGS_FILE = DATA_DIR / "settings.json" -HISTORY_FILE = DATA_DIR / "profile_history.json" -# etc. -``` - -Created `conftest.py` to set environment variables before main.py imports: -```python -# conftest.py runs at import time before tests -os.environ["TEST_MODE"] = "true" -test_data_dir = tempfile.mkdtemp(prefix="meticai_test_") -os.environ["DATA_DIR"] = test_data_dir -``` - -#### 2. Missing Null Safety in Image Prompt Generation -**Issue**: `TypeError: 'NoneType' object is not subscriptable` in 6 barista tests. - -**Root Cause**: In `generate_profile_image()`, code accessed dictionary keys without checking if result was None: -```python -prompt_result = build_image_prompt_with_metadata(...) -full_prompt = prompt_result["prompt"] # TypeError if None! -``` - -**Fix**: Added null checking and safe access: -```python -prompt_result = build_image_prompt_with_metadata(...) - -if not prompt_result or not isinstance(prompt_result, dict): - raise HTTPException( - status_code=500, - detail="Failed to build image generation prompt" - ) - -full_prompt = prompt_result.get("prompt", "") -prompt_metadata = prompt_result.get("metadata", {}) -``` - -#### 3. LLM Analysis Cache Persistence -**Issue**: Tests expecting specific LLM behavior were hitting cached results from previous tests. - -**Root Cause**: The new LLM caching feature (with 3-day TTL) was persisting data across tests because all tests shared the same temp directory. - -**Fix**: Added `force_refresh=true` parameter to all LLM analysis tests: -```python -response = client.post("/api/shots/analyze-llm", data={ - "profile_name": "Test", - "shot_date": "2024-01-15", - "shot_filename": "shot.json", - "force_refresh": "true" # Bypass cache in tests -}) -``` - -#### 4. History File Mock Issue -**Issue**: Test `test_load_history_with_missing_file` expected empty list but got data from previous tests. - -**Root Cause**: Test was trying to mock `Path` class but the module-level constant `HISTORY_FILE` was already evaluated. Also, the file actually existed from previous tests. - -**Fix**: Changed test to mock `open()` to raise `FileNotFoundError`: -```python -@patch('main._ensure_history_file') -@patch('builtins.open') -def test_load_history_with_missing_file(self, mock_open_func, mock_ensure): - mock_open_func.side_effect = FileNotFoundError("File not found") - history = _load_history() - assert history == [] -``` - -### Changes Made - -#### File: `meticai-server/main.py` -- **Lines 39-47**: Added `DATA_DIR` configuration with test mode support -- **Lines 1016, 1235, 1266, 1337, 1413**: Updated all hardcoded paths to use `DATA_DIR` -- **Lines 2411-2426**: Added null safety checks for `prompt_result` in `generate_profile_image()` -- **Impact**: Enables tests to run with temporary directories, prevents NoneType errors - -#### File: `meticai-server/conftest.py` (new file) -- **Purpose**: Set up test environment variables before main.py is imported -- **Lines 10-16**: Configure TEST_MODE and DATA_DIR for all tests -- **Impact**: All tests automatically use temporary directories - -#### File: `meticai-server/test_main.py` -- **Lines 3497, 3520, 3563, 3611, 3666, 3682**: Added `force_refresh="true"` to 6 LLM analysis tests -- **Lines 1843-1856**: Fixed `test_load_history_with_missing_file` to mock `open()` instead of `Path` -- **Impact**: Tests properly bypass cache and handle file operations - -### Test Results - -#### Before Fixes -- **19 failures**, 160 passed -- Permission errors on `/app/data` -- NoneType subscript errors -- Unexpected cache hits - -#### After Fixes -- **0 failures**, 192 passed ✅ (test_main.py) -- **0 failures**, 19 passed ✅ (test_logging.py) -- **Total: 211 tests passing** - -### Benefits Added - -This fix maintains all recent improvements while adding: - -1. **Test Environment Isolation**: Tests use temporary directories, preventing conflicts -2. **Production Flexibility**: `DATA_DIR` can be configured via environment variable -3. **Robust Error Handling**: Null safety prevents crashes on edge cases -4. **Cache Control**: Tests can bypass cache when needed for deterministic behavior - -### Validation - -All fixes validated by: -1. Running full Python test suite (192 + 19 = 211 tests) -2. Verifying bash scripts have valid syntax -3. Confirming no production behavior changed -4. Ensuring proper separation of test and production environments - ---- - -## Previous Fix (Earlier) - Status and Update Endpoints -After merging several PRs with bugfixes and improvements (#32, #37, #39, #42, #45, #47), CI tests started failing: -- **10 Python tests failing** (all in `/status` and `/api/trigger-update` endpoints) -- **1 Bash test failing** (macOS dock shortcut prompt text) - -## Root Causes - -### 1. Bash Test Failure (test #32) -**Issue**: Test expected text "Would you like to add a MeticAI shortcut" but actual script had different text. - -**Root Cause**: PR #37 added macOS dock shortcut functionality with the prompt "Would you like to add MeticAI to your Dock?" but the test wasn't updated to match. - -**Fix**: Updated test to match actual script text. -```bash -# Before: -run grep -q "Would you like to add a MeticAI shortcut" "$SCRIPT_PATH" - -# After: -run grep -q "Would you like to add MeticAI to your Dock?" "$SCRIPT_PATH" -``` - -### 2. Python Tests - `/status` Endpoint (2 failures) -**Issue**: Tests expected subprocess calls but got different behavior. - -**Root Cause**: PR #32 changed `/status` endpoint implementation from running `update.sh --check-only` via subprocess to reading directly from `.versions.json` file. Tests were still mocking subprocess calls instead of file I/O. - -**Original Implementation** (what tests expected): -```python -result = subprocess.run(["bash", "update.sh", "--check-only"], ...) -# Parse stdout to determine update availability -``` - -**New Implementation** (what code actually does): -```python -version_file_path = Path("/app/.versions.json") -if version_file_path.exists(): - with open(version_file_path, 'r') as f: - version_data = json.load(f) -``` - -**Fix**: Updated tests to mock `Path.exists()` and `open()` instead of `subprocess.run()`. - -### 3. Python Tests - `/api/trigger-update` Endpoint (8 failures) -**Issue**: All tests returned 500 errors instead of expected 200/test-specific responses. - -**Root Cause**: Endpoint checks if `/app/update.sh` exists before running it: -```python -script_path = Path("/app/update.sh") -if not script_path.exists(): - raise HTTPException(status_code=500, detail={...}) -``` - -Tests were mocking `subprocess.run()` but not `Path.exists()`, so the check failed and raised a 500 error before subprocess was ever called. - -**Fix**: Added `@patch('main.Path')` to all affected tests and configured the mock to return a Path object where `.exists()` returns `True`. - -```python -# Before: -@patch('main.subprocess.run') -def test_trigger_update_success(self, mock_subprocess, client): - ... - -# After: -@patch('main.subprocess.run') -@patch('main.Path') -def test_trigger_update_success(self, mock_subprocess, mock_path_class, client): - mock_script_path = Mock() - mock_script_path.exists.return_value = True - mock_path_class.return_value = mock_script_path - ... -``` - -## Changes Made - -### File: `tests/test_local_install.bats` -- **Line 177**: Updated expected prompt text to match actual implementation -- **Impact**: Minimal - single line change - -### File: `meticai-server/test_main.py` -- **Line 13**: Added `mock_open` import for file mocking -- **TestStatusEndpoint (6 tests)**: - - Removed `subprocess.run` mocks - - Added `Path` and `open()` mocks - - Configured mocks to simulate file existence and JSON content -- **TestTriggerUpdateEndpoint (8 tests)**: - - Added `Path` mocks to all tests - - Configured `Path.exists()` to return `True` - - Maintained all other test logic unchanged -- **Impact**: Updated test mocking to match new implementation, no test coverage lost - -## Test Results - -### Before Fixes -- Python: 10 failures, 39 passing -- Bash: 1 failure, 50 passing - -### After Fixes -- Python: 0 failures, 49 passing ✅ -- Bash: 0 failures, 51 passing ✅ - -## Benefits Maintained - -All improvements from recent bugfixes remain intact: - -1. **Update System** (PR #32): Automatic repository switching, version tracking, API-accessible status -2. **macOS Dock Shortcut** (PR #37): Easy access to web app for macOS users -3. **Trigger Update API** (PR #39): Programmatic update triggering for web interfaces -4. **Installation Improvements** (PRs #42, #45, #47): Early .env detection, automatic IP discovery, network scanning, QR codes - -## Validation - -All fixes were validated by: -1. Running full Python test suite locally (49 tests) -2. Running full Bash test suite locally (51 tests) -3. Verifying no functionality was removed or degraded -4. Confirming all tests use proper mocking patterns - -## Conclusion - -The test failures were caused by tests not being updated to match improved implementations. By updating the test mocking to match the new behavior, all tests now pass while preserving all the improvements from recent bugfixes. diff --git a/UPDATE_GUIDE.md b/UPDATE_GUIDE.md deleted file mode 100644 index eccff71..0000000 --- a/UPDATE_GUIDE.md +++ /dev/null @@ -1,361 +0,0 @@ -# MeticAI Update System - -This document provides a quick reference for the MeticAI update system. - -## Quick Start - -```bash -# Check for updates -./update.sh --check-only - -# Apply updates -./update.sh - -# Auto-update (non-interactive, great for automation) -./update.sh --auto -``` - -## What Gets Updated? - -The update system manages three components: - -1. **MeticAI Main Repository** - The core application (version-based) -2. **Meticulous MCP** - The machine control protocol server (commit-based) -3. **MeticAI Web Interface** - The web-based user interface (version-based) - -## Features - -### 1. Update Checking -- Uses semantic versioning for MeticAI and MeticAI-web repos -- Compares local `VERSION` file with remote on GitHub -- Displays clear version numbers (e.g., v1.0.0 → v1.1.0) -- Non-invasive - doesn't modify anything in check-only mode - -### 2. Dependency Management -- Automatically clones missing dependencies -- Updates existing dependencies to latest version -- Preserves your `.env` configuration -- **Automatic repository switching** based on central configuration - -### 3. Automatic Repository Switching -- Repository URLs controlled centrally via `.update-config.json` -- Maintainers can switch all users to new repositories without manual intervention -- Automatic detection and switching during updates -- Backups created before switching -- Works seamlessly in both interactive and auto mode - -### 4. Container Management -- Optionally rebuilds Docker containers after updates -- Restarts services with updated code -- Handles docker compose versions automatically - -### 4. API Integration -- `/status` endpoint returns update availability -- JSON response with detailed version information -- CORS-enabled for web app integration -- Lightweight and fast - -### 5. Startup Notifications -- Automatically checks for updates on container start -- Displays notification banner if updates available -- Non-blocking background check -- Doesn't slow down startup - -## Command Reference - -### Update Script Options - -| Option | Description | -|--------|-------------| -| `--check-only` | Check for updates without applying them | -| `--auto` | Run non-interactively (auto-accept all prompts) | -| `--switch-mcp-repo` | Check and apply central repository configuration | -| `--help` | Show help message | - -**Note on Repository Switching:** The `--switch-mcp-repo` option is now primarily for manual checking. Repository switching happens **automatically** based on the central `.update-config.json` file when you run normal updates. - -### Examples - -**Check what updates are available:** -```bash -./update.sh --check-only -``` - -**Apply updates interactively:** -```bash -./update.sh -# You'll be asked: -# - Whether to apply updates -# - Whether to rebuild containers -``` - -**Apply updates automatically (CI/CD):** -```bash -./update.sh --auto -# No prompts, applies all updates and rebuilds -# Automatically switches repositories based on central config -``` - -**Check repository configuration:** -```bash -./update.sh --switch-mcp-repo -# Checks and applies central repository configuration -# Note: This happens automatically during normal updates -``` - -## Automatic Repository Switching - -### For Users - -Repository switching is now **fully automatic** for all dependencies. When you run `./update.sh`, the script: - -1. Fetches the central configuration from `.update-config.json` -2. Compares your current repository URLs with the preferred ones -3. Automatically switches if they differ (with confirmation in interactive mode) -4. Rebuilds containers with the new repositories - -You don't need to do anything special - just run your regular updates! - -### For Maintainers - -To switch all users to different repositories (e.g., when the fork merges upstream or dependencies change): - -1. **Edit `.update-config.json` in the main repository:** -```json -{ - "version": "1.1", - "description": "Central configuration for MeticAI update script", - "last_updated": "2026-01-13T21:38:00Z", - "repositories": { - "meticulous-mcp": { - "url": "https://github.com/meticulous/meticulous-mcp.git", - "description": "Meticulous MCP server for machine control" - }, - "meticai-web": { - "url": "https://github.com/your-org/MeticAI-web.git", - "description": "MeticAI web interface" - } - } -} -``` - -2. **Commit and push the change:** -```bash -git add .update-config.json -git commit -m "Switch to new repository URLs" -git push -``` - -3. **That's it!** When users run `./update.sh` or `./update.sh --auto`, they will: - - Fetch the updated configuration - - See notifications about any repository changes - - Automatically switch to the new repositories (with confirmation in interactive mode) - - Have their containers rebuilt with the new dependencies - -**Benefits:** -- Control all dependency repositories centrally -- No user intervention required (in auto mode) -- Safe backups created automatically -- Works with CI/CD pipelines -- Backward compatible with v1.0 config format - -## API Endpoint - -### GET /status - -Check for updates programmatically. - -**Request:** -```bash -curl http://YOUR_PI_IP:8000/status -``` - -**Response:** -```json -{ - "update_available": true, - "last_check": "2026-01-13T19:45:00Z", - "repositories": { - "meticai": { - "current_hash": "abc123...", - "last_updated": "2026-01-13T19:00:00Z" - }, - "meticulous-mcp": { - "current_hash": "def456...", - "repo_url": "https://github.com/hessius/meticulous-mcp.git", - "last_updated": "2026-01-13T18:00:00Z" - }, - "meticai-web": { - "current_hash": "ghi789...", - "last_updated": "2026-01-13T17:00:00Z" - } - } -} -``` - -## Version Tracking - -Updates are tracked in `.versions.json`: -- **Automatically generated** - Created on first run -- **Git-ignored** - Won't be committed to repository -- **JSON format** - Easy to parse programmatically -- **Timestamps** - Last check and update times -- **Commit hashes** - Precise version tracking - -## Troubleshooting - -### "Updates are available" but nothing changed - -This is expected when: -- Dependencies aren't installed yet (`meticulous-source` or `meticai-web` missing) -- Remote repository has new commits -- Repository URL has changed - -Solution: Run `./update.sh` without `--check-only` to apply updates. - -### Update script fails to pull changes - -Possible causes: -- Local modifications to dependency repositories -- Network connectivity issues -- Invalid git repository state - -Solutions: -1. Check network connection -2. Manually inspect dependency directories -3. Delete and re-clone: `rm -rf meticulous-source && ./update.sh` - -### Container rebuild fails - -If `docker compose up --build` fails: -- Check Docker is running -- Ensure you have sudo permissions (or are in docker group) -- Review Docker logs: `docker compose logs` - -### Version file is corrupted - -The update script handles this gracefully: -- Malformed JSON won't crash the script -- Script will create valid JSON on next successful run -- To reset: `rm .versions.json && ./update.sh --check-only` - -## Integration Examples - -### Web Application Polling - -```javascript -async function checkForUpdates() { - try { - const response = await fetch('http://YOUR_PI_IP:8000/status'); - const data = await response.json(); - - if (data.update_available) { - showNotification('Updates available!', 'Run ./update.sh to update'); - } - } catch (error) { - console.error('Failed to check for updates:', error); - } -} - -// Check every hour -setInterval(checkForUpdates, 3600000); -``` - -### Automated Updates (Cron) - -```bash -# Add to crontab to check daily at 3 AM -0 3 * * * cd /path/to/MeticAI && ./update.sh --check-only >> /var/log/meticai-updates.log 2>&1 -``` - -### CI/CD Integration - -```yaml -# Example GitHub Actions workflow -- name: Update dependencies - run: | - cd MeticAI - ./update.sh --auto -``` - -## Version-Based Updates - -MeticAI uses **semantic versioning** to control when users are notified about updates. Not every commit triggers an update notification - only when the maintainer explicitly bumps the version. - -### How It Works - -1. Each repository has a `VERSION` file containing the semantic version (e.g., `1.0.0`) -2. The update checker compares local `VERSION` with remote `VERSION` on GitHub -3. Users are only notified when the remote version is **greater than** their local version -4. Meticulous MCP (external dependency) still uses commit-based checking - -### For Maintainers: Releasing a New Version - -To release a new version that will trigger updates for all users: - -```bash -# 1. Update the VERSION file -echo "1.1.0" > VERSION - -# 2. Commit and push -git add VERSION -git commit -m "Release v1.1.0" -git push - -# 3. Optionally create a GitHub release/tag -git tag v1.1.0 -git push --tags -``` - -**For MeticAI-web**, do the same in the web repository: -```bash -cd meticai-web -echo "1.1.0" > VERSION -git add VERSION -git commit -m "Release v1.1.0" -git push -``` - -### Version Format - -- Use semantic versioning: `MAJOR.MINOR.PATCH` -- Examples: `1.0.0`, `1.1.0`, `2.0.0` -- The system compares versions numerically (e.g., `1.10.0` > `1.9.0`) - -## Best Practices - -1. **Check before updating**: Always run `--check-only` first -2. **Backup .env**: Your configuration is preserved, but backup is good practice -3. **Test after update**: Verify services are running after update -4. **Review changes**: Check what changed in updated repositories -5. **Monitor logs**: Use `docker compose logs -f` to monitor after rebuild - -## Security Considerations - -- Update script requires sudo for Docker operations -- Only clones from known repository URLs -- Doesn't modify `.env` file -- Creates backups when switching repositories -- All operations are logged in update output - -## Files Created - -| File | Purpose | Git Status | -|------|---------|------------| -| `update.sh` | Main update script | Tracked | -| `check-updates-on-start.sh` | Startup notification | Tracked | -| `VERSION` | Semantic version for releases | Tracked | -| `.versions.json` | Version tracking cache | Ignored | -| `tests/test_update.bats` | Test suite | Tracked | - -## Support - -If you encounter issues: -1. Check this document first -2. Review logs: `docker compose logs` -3. Run with verbose output: `bash -x update.sh --check-only` -4. Open an issue on GitHub with logs - -## License - -Same as MeticAI main project - see LICENSE file. diff --git a/VERSION b/VERSION index 26aaba0..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +2.0.0 diff --git a/check-updates-on-start.sh b/check-updates-on-start.sh deleted file mode 100755 index d38c998..0000000 --- a/check-updates-on-start.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -################################################################################ -# MeticAI - Startup Update Check -################################################################################ -# -# This script runs during container startup to check for updates and display -# a notification if updates are available. -# -################################################################################ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source common library -source "$SCRIPT_DIR/scripts/lib/common.sh" - -# Quick check for updates (non-interactive) -if [ -f "$SCRIPT_DIR/update.sh" ]; then - # Run update check in background to not slow down startup - { - sleep 5 # Wait for services to start - - # Run check-only mode - UPDATE_OUTPUT=$("$SCRIPT_DIR/update.sh" --check-only 2>&1) - - # Check if updates are available - if echo "$UPDATE_OUTPUT" | grep -q "Update available\|⚠\|Not installed"; then - echo "" - echo -e "${YELLOW}╔════════════════════════════════════════════════╗${NC}" - echo -e "${YELLOW}║ ║${NC}" - echo -e "${YELLOW}║ 📦 Updates Available for MeticAI! ║${NC}" - echo -e "${YELLOW}║ ║${NC}" - echo -e "${YELLOW}║ Run './update.sh' to update all components ║${NC}" - echo -e "${YELLOW}║ Or visit http://YOUR_IP:8000/docs ║${NC}" - echo -e "${YELLOW}║ and check the /status endpoint ║${NC}" - echo -e "${YELLOW}║ ║${NC}" - echo -e "${YELLOW}╚════════════════════════════════════════════════╝${NC}" - echo "" - fi - } & -fi diff --git a/docker-compose.old b/docker-compose.old deleted file mode 100644 index 825d7a4..0000000 --- a/docker-compose.old +++ /dev/null @@ -1,34 +0,0 @@ -services: - # 1. The Target Tool (Meticulous) - # We build this so it's available as an image on your system - meticulous: -# build: https://github.com/manonstreet/meticulous-mcp.git#main - build: ./meticulous-source - image: meticulous-mcp:latest - # We don't actually "run" this service as a daemon; - # Gemini will "run" it as a temporary command when needed. - entrypoint: ["tail", "-f", "/dev/null"] - - # 2. The Brain (Gemini CLI) - gemini-client: - build: ./gemini-client - container_name: gemini-client - volumes: - - /var/run/docker.sock:/var/run/docker.sock # CRITICAL: Allows it to run sibling containers - - ./gemini-client/settings.json:/root/.gemini/settings.json - env_file: - - .env - tty: true # Keeps the container running so you can attach to it - - # 3. The App (Coffee Relay) - coffee-relay: - build: ./coffee-relay - container_name: coffee-relay - ports: - - "8000:8000" - volumes: - # CRITICAL: Give it access to the host's Docker engine - - /var/run/docker.sock:/var/run/docker.sock - env_file: - - .env - restart: unless-stopped diff --git a/docker-up.sh b/docker-up.sh deleted file mode 100755 index 3042e0a..0000000 --- a/docker-up.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -################################################################################ -# MeticAI - Docker Compose Wrapper -################################################################################ -# -# Use this script instead of running `docker compose` directly. -# It ensures proper file permissions are maintained. -# -# USAGE: -# ./docker-up.sh # Build and start all containers -# ./docker-up.sh --no-build # Start without building -# ./docker-up.sh down # Stop all containers -# -################################################################################ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Source common library -source "$SCRIPT_DIR/scripts/lib/common.sh" - -# Ensure required files exist before Docker runs -prepare_files() { - # Ensure files exist (not directories) - [ -d ".versions.json" ] && rm -rf .versions.json - [ -d ".rebuild-needed" ] && rm -rf .rebuild-needed - [ ! -f ".versions.json" ] && echo '{}' > .versions.json - [ ! -f ".rebuild-needed" ] && touch .rebuild-needed - - # Pre-create directories - mkdir -p data logs -} - -# Fix permissions after Docker operations -fix_permissions() { - log_warning "Fixing file permissions..." - sudo chown -R "$(id -u):$(id -g)" data logs .versions.json .rebuild-needed \ - meticulous-source meticai-web 2>/dev/null || true - log_success "Permissions fixed" -} - -# Determine docker compose command -COMPOSE_CMD=$(get_compose_command) || exit 1 - -# Check if we need sudo -SUDO_PREFIX="" -if ! docker info &> /dev/null; then - if sudo docker info &> /dev/null; then - SUDO_PREFIX="sudo" - else - log_error "Cannot access Docker daemon" - exit 1 - fi -fi - -# Handle arguments -case "${1:-up}" in - down|stop) - $SUDO_PREFIX $COMPOSE_CMD down "$@" - ;; - *) - prepare_files - - if [[ "$1" == "--no-build" ]]; then - shift - $SUDO_PREFIX $COMPOSE_CMD up -d "$@" - else - $SUDO_PREFIX $COMPOSE_CMD up -d --build "$@" - fi - - # Fix permissions if we used sudo - if [ -n "$SUDO_PREFIX" ]; then - fix_permissions - fi - ;; -esac diff --git a/gemini-client/Dockerfile b/gemini-client/Dockerfile deleted file mode 100644 index 118f732..0000000 --- a/gemini-client/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM node:25-slim - -# Install curl so we can download the official script -RUN apt-get update && apt-get install -y curl git - -# Install the OFFICIAL Docker CLI (Latest) to match your host's API version -RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh - -# Install Gemini CLI -RUN npm install -g @google/gemini-cli - -# Install nanobanana extension for image generation -# Note: This requires a paid API key for image generation -# Clone to a known path so we can reference it in settings.json -RUN git clone https://github.com/gemini-cli-extensions/nanobanana.git /opt/nanobanana && \ - cd /opt/nanobanana && \ - npm install && \ - npm run build - -# Keep container alive -CMD ["tail", "-f", "/dev/null"] diff --git a/gemini-client/settings.json b/gemini-client/settings.json deleted file mode 100644 index b30bc21..0000000 --- a/gemini-client/settings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "theme": "GitHub", - "mcpServers": { - "meticulous": { - "command": "docker", - "args": [ - "exec", - "-i", - "meticulous-mcp-server", - "python3", - "-m", - "meticulous_mcp.server" - ] - }, - "nanobanana": { - "command": "node", - "args": ["/opt/nanobanana/mcp-server/dist/index.js"], - "env": {} - } - } -} diff --git a/gemini-client/settings.json.old b/gemini-client/settings.json.old deleted file mode 100644 index faad2f0..0000000 --- a/gemini-client/settings.json.old +++ /dev/null @@ -1,19 +0,0 @@ -{ - "theme": "GitHub", - "mcpServers": { - "meticulous": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", "METICULOUS_API_URL=http://192.168.50.168", - "-e", "PYTHONPATH=/app/meticulous-mcp/src:/app/pyMeticulous:/app/python-sdk/src", - "meticulous-mcp:latest", - "python", - "-u", - "/app/meticulous-mcp/run_server.py" - ] - } - } -} diff --git a/local-install.sh b/local-install.sh deleted file mode 100755 index 070f9a6..0000000 --- a/local-install.sh +++ /dev/null @@ -1,1632 +0,0 @@ -#!/bin/bash - -################################################################################ -# MeticAI - Interactive Installer -################################################################################ -# -# This script automates the installation of MeticAI, an autonomous AI agent -# that controls a Meticulous Espresso Machine using Google Gemini 2.0 Flash. -# -# USAGE: -# ./local-install.sh -# -# WHAT IT DOES: -# 1. Checks for prerequisites (Git, Docker) -# 2. Interactively collects configuration (API keys, IP addresses) -# 3. Creates .env file with your settings -# 4. Clones the required Meticulous MCP source repository -# 5. Builds and launches Docker containers -# -# REQUIREMENTS: -# - Git installed -# - Docker & Docker Compose installed -# - Google Gemini API key (get one at: https://aistudio.google.com/app/api-keys) -# - Meticulous Espresso Machine with known local IP address -# -################################################################################ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source common library -source "$SCRIPT_DIR/scripts/lib/common.sh" - -################################################################################ -# Non-Interactive Mode Support -################################################################################ -# Set METICAI_NON_INTERACTIVE=true to run without prompts. -# Required environment variables for non-interactive mode: -# - GEMINI_API_KEY: Your Google Gemini API key -# - METICULOUS_IP: IP address of your Meticulous machine -# - PI_IP: IP address of this server -# -# Optional environment variables: -# - METICAI_PROGRESS_FORMAT: Set to "platypus" for PROGRESS:X% format -# - SKIP_DOCK_SHORTCUT: Set to "true" to skip dock shortcut creation -# - SKIP_REBUILD_WATCHER: Set to "true" to skip watcher installation -# - SKIP_PREVIOUS_INSTALL_CHECK: Set to "true" to skip existing install check -# - FORCE_RECLONE: Set to "true" to force re-clone of repositories -################################################################################ - -# Progress output function (supports Platypus progress bar format) -show_progress() { - local message="$1" - local percent="${2:-}" - - if [ "$METICAI_PROGRESS_FORMAT" = "platypus" ]; then - if [ -n "$percent" ]; then - echo "PROGRESS:$percent" - fi - echo "$message" - else - echo -e "${YELLOW}$message${NC}" - fi -} - -# Check if running in non-interactive mode -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - # Validate required environment variables - if [ -z "$GEMINI_API_KEY" ]; then - echo "ERROR: GEMINI_API_KEY is required in non-interactive mode" >&2 - exit 1 - fi - if [ -z "$METICULOUS_IP" ]; then - echo "ERROR: METICULOUS_IP is required in non-interactive mode" >&2 - exit 1 - fi - if [ -z "$PI_IP" ]; then - echo "ERROR: PI_IP is required in non-interactive mode" >&2 - exit 1 - fi - - # Set defaults for non-interactive mode - SKIP_ENV_CREATION=false # Will create .env from environment variables - SKIP_PREVIOUS_INSTALL_CHECK="${SKIP_PREVIOUS_INSTALL_CHECK:-true}" - FORCE_RECLONE="${FORCE_RECLONE:-true}" - SKIP_DOCK_SHORTCUT="${SKIP_DOCK_SHORTCUT:-true}" - SKIP_REBUILD_WATCHER="${SKIP_REBUILD_WATCHER:-true}" - - show_progress "Running in non-interactive mode..." 5 -fi - -################################################################################ -# Sudo Helper Function -################################################################################ -# Some systems (like Puppy OS) are single-user and don't have sudo. -# This helper runs commands with sudo only when needed and available. -run_privileged() { - if [ "$(id -u)" -eq 0 ]; then - # Already root, no sudo needed - "$@" - elif command -v sudo &> /dev/null; then - # Not root but sudo is available - sudo "$@" - else - # Not root and no sudo - try anyway (will fail if permissions needed) - "$@" - fi -} - - - -# Only show banner in interactive mode -if [ "$METICAI_NON_INTERACTIVE" != "true" ]; then - log_info "=========================================" - log_info " ☕️ Barista AI Installer 🤖 " - log_info "=========================================" - echo "" -fi - -# Detect running MeticAI containers -detect_running_containers() { - if ! command -v docker &> /dev/null; then - return 1 # Docker not installed, no containers to detect - fi - - # Check for MeticAI-related containers (running or stopped) - local containers - containers=$(docker ps -a --format "{{.Names}}" 2>/dev/null | grep -E "(meticulous-mcp-server|gemini-client|meticai-server|meticai-web)" || true) - - if [ -n "$containers" ]; then - echo "$containers" - return 0 - else - return 1 - fi -} - -# Stop and remove MeticAI containers -stop_and_remove_containers() { - log_warning "Stopping and removing running MeticAI containers..." - - # Try docker compose down first (cleaner approach) - if [ -f "docker-compose.yml" ]; then - if docker compose down 2>/dev/null || docker-compose down 2>/dev/null; then - log_success "Containers stopped and removed via docker compose" - return 0 - fi - fi - - # Fallback: Remove containers individually - local containers - containers=$(docker ps -a --format "{{.Names}}" 2>/dev/null | grep -E "(meticulous-mcp-server|gemini-client|meticai-server|meticai-web)" || true) - - if [ -n "$containers" ]; then - while IFS= read -r container; do - log_warning " Stopping and removing: $container" - docker stop "$container" 2>/dev/null || true - docker rm "$container" 2>/dev/null || true - done < <(echo "$containers") - log_success "Individual containers stopped and removed" - fi -} - -# Detect previous MeticAI installation artifacts -detect_previous_installation() { - local found_items=() - local current_dir="$(pwd)" - - # Check for typical MeticAI installation artifacts - # Note: .update-config.json is a source file and NOT an installation artifact - [ -f ".env" ] && found_items+=(".env file at $current_dir/.env") - [ -d "meticulous-source" ] && found_items+=("meticulous-source directory at $current_dir/meticulous-source") - [ -d "meticai-web" ] && found_items+=("meticai-web directory at $current_dir/meticai-web") - [ -f ".versions.json" ] && found_items+=(".versions.json file at $current_dir/.versions.json") - [ -f ".rebuild-needed" ] && found_items+=(".rebuild-needed file at $current_dir/.rebuild-needed") - - # Check for macOS-specific installations - if [[ "$OSTYPE" == "darwin"* ]]; then - [ -d "/Applications/MeticAI.app" ] && found_items+=("macOS Dock shortcut at /Applications/MeticAI.app") - [ -f "$HOME/Library/LaunchAgents/com.meticai.rebuild-watcher.plist" ] && found_items+=("rebuild watcher service (launchd) at $HOME/Library/LaunchAgents/com.meticai.rebuild-watcher.plist") - fi - - # Check for Linux-specific installations (Raspberry Pi, etc.) - if [[ "$OSTYPE" == "linux"* ]]; then - [ -f "/etc/systemd/system/meticai-rebuild-watcher.path" ] && found_items+=("rebuild watcher service (systemd) at /etc/systemd/system/meticai-rebuild-watcher.path") - fi - - if [ ${#found_items[@]} -gt 0 ]; then - # Return items line-separated to handle items with spaces - printf '%s\n' "${found_items[@]}" - return 0 - else - return 1 - fi -} - -# Check for running containers and previous installations -# Skip in non-interactive mode if SKIP_PREVIOUS_INSTALL_CHECK is set -if [ "$SKIP_PREVIOUS_INSTALL_CHECK" != "true" ]; then - log_warning "Checking for existing MeticAI installations..." - echo "" - - CONTAINERS_FOUND="" - PREVIOUS_INSTALL_FOUND="" - - # Detect running containers - if CONTAINERS_FOUND=$(detect_running_containers); then - log_warning "Found running MeticAI containers:" - echo "$CONTAINERS_FOUND" | sed 's/^/ - /' - echo "" - fi - - # Detect previous installation artifacts - if PREVIOUS_INSTALL_FOUND=$(detect_previous_installation); then - log_warning "Found existing MeticAI installation artifacts:" - # Items are returned line-separated, so we can process them directly - echo "$PREVIOUS_INSTALL_FOUND" | while IFS= read -r item; do - echo " - $item" - done - echo "" - fi - - # If we found either containers or previous installation, offer uninstall - if [ -n "$CONTAINERS_FOUND" ] || [ -n "$PREVIOUS_INSTALL_FOUND" ]; then - log_warning "=========================================" - log_warning " Previous Installation Detected" - log_warning "=========================================" - echo "" - log_info "It looks like MeticAI may already be installed or partially installed." - echo "" - log_warning "Recommended actions:" - echo -e " 1) Run the uninstall script first to clean up: ${BLUE}./uninstall.sh${NC}" - echo -e " 2) Then run this installer again for a fresh installation" - echo "" - log_warning "Or:" - echo -e " 3) Continue anyway (may cause conflicts or use existing configuration)" - echo "" - - # Check if uninstall script exists - if [ -f "./uninstall.sh" ]; then - read -r -p "Would you like to run the uninstall script now? (y/n) [y]: " RUN_UNINSTALL /dev/null)" ]; then - log_success "Found existing data directory with profile history - will preserve it" -fi - -# In non-interactive mode, skip .env check prompts -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - # Non-interactive mode: always create new .env from environment variables - SKIP_ENV_CREATION=false -elif [ -f ".env" ]; then - log_warning "Found existing .env file (preserved from previous installation)." - echo "" - cat .env - echo "" - read -r -p "Do you want to use this existing configuration? (y/n) [y]: " USE_EXISTING_ENV /dev/null; then - log_warning "Installing qrencode..." - if brew install qrencode &> /dev/null; then - log_success "qrencode installed successfully." - return 0 - else - log_warning "Failed to install qrencode via Homebrew." - return 1 - fi - else - return 1 - fi - ;; - ubuntu|debian|raspbian) - log_warning "Installing qrencode..." - # Update package cache for more reliable installation - if run_privileged apt-get update &> /dev/null && run_privileged apt-get install -y qrencode &> /dev/null; then - log_success "qrencode installed successfully." - return 0 - else - log_warning "Failed to install qrencode." - return 1 - fi - ;; - fedora|rhel|centos) - log_warning "Installing qrencode..." - if command -v dnf &> /dev/null; then - if run_privileged dnf install -y qrencode &> /dev/null; then - log_success "qrencode installed successfully." - return 0 - fi - elif command -v yum &> /dev/null; then - if run_privileged yum install -y qrencode &> /dev/null; then - log_success "qrencode installed successfully." - return 0 - fi - fi - log_warning "Failed to install qrencode." - return 1 - ;; - arch|manjaro) - log_warning "Installing qrencode..." - # Use -S instead of -Sy to avoid slow database sync - if run_privileged pacman -S --noconfirm qrencode &> /dev/null; then - log_success "qrencode installed successfully." - return 0 - else - log_warning "Failed to install qrencode." - return 1 - fi - ;; - *) - return 1 - ;; - esac -} - -# Generate and display ASCII QR code for a URL -generate_qr_code() { - local url="$1" - - echo "" - log_success "=========================================" - log_success " 📱 Scan to Access Web App 📱 " - log_success "=========================================" - echo "" - - # Try to use qrencode if available (common on many Linux systems) - if command -v qrencode &> /dev/null; then - qrencode -t ansiutf8 "$url" 2>/dev/null - echo "" - log_warning "Scan the QR code above to open MeticAI Web App" - log_warning "Or visit directly: ${BLUE}${url}${NC}" - echo "" - return - fi - - # If qrencode not found, try to install it automatically - log_warning "QR code generator not found. Attempting to install..." - if install_qrencode && command -v qrencode &> /dev/null; then - # Installation succeeded, generate QR code - qrencode -t ansiutf8 "$url" 2>/dev/null - echo "" - log_warning "Scan the QR code above to open MeticAI Web App" - log_warning "Or visit directly: ${BLUE}${url}${NC}" - echo "" - return - fi - - # Try Python with qrcode library (if available) - if command -v python3 &> /dev/null; then - local python_result - python_result=$(python3 -c " -try: - import qrcode - qr = qrcode.QRCode() - qr.add_data('$url') - qr.print_ascii() - print('SUCCESS') -except: - print('FAILED') -" 2>/dev/null) - - if echo "$python_result" | grep -q "SUCCESS"; then - echo "$python_result" | grep -v "SUCCESS" - echo "" - log_warning "Scan the QR code above to open MeticAI Web App" - log_warning "Or visit directly: ${BLUE}${url}${NC}" - echo "" - return - fi - fi - - # Fallback: Show a simple box with the URL - log_warning "┌──────────────────────────────────────────────┐" - log_warning "│ │" - log_warning "│ Open this URL on your mobile device: │" - log_warning "│ │" - log_warning "│ ${BLUE}${url}${YELLOW}│" - log_warning "│ │" - log_warning "│ 💡 QR code not available on this system │" - log_warning "│ │" - log_warning "└──────────────────────────────────────────────┘" - echo "" -} - -# Install the rebuild watcher service on macOS for automatic web UI updates -install_rebuild_watcher() { - local script_dir="$1" - local watcher_script="${script_dir}/rebuild-watcher.sh" - - if [ ! -f "$watcher_script" ]; then - log_warning "Warning: rebuild-watcher.sh not found, skipping." - return 1 - fi - - log_warning "Installing rebuild watcher service..." - log_info "This enables fully automatic updates from the web interface." - - # Ensure the script is executable - chmod +x "$watcher_script" - - # Create empty signal files for Docker mount - touch "${script_dir}/.rebuild-needed" - touch "${script_dir}/.update-check-requested" - touch "${script_dir}/.update-requested" - touch "${script_dir}/.restart-requested" - touch "${script_dir}/.rebuild-watcher.log" - - # Configure git safe.directory for root (needed when systemd runs as root) - # This prevents "dubious ownership" errors - log_info "Configuring git safe directories for systemd..." - run_privileged git config --global --add safe.directory "${script_dir}" 2>/dev/null || true - run_privileged git config --global --add safe.directory "${script_dir}/meticulous-source" 2>/dev/null || true - run_privileged git config --global --add safe.directory "${script_dir}/meticai-web" 2>/dev/null || true - - # Run the install command - if "$watcher_script" --install; then - log_success "Rebuild watcher installed successfully" - log_info " Updates triggered from the web UI will now automatically rebuild containers." - return 0 - else - log_warning "Warning: Failed to install rebuild watcher service." - log_warning " You can install it manually later with: ./rebuild-watcher.sh --install" - return 1 - fi -} - -# Create macOS .app bundle for dock shortcut -create_macos_dock_shortcut() { - local url="$1" - local app_name="MeticAI" - local app_path="/Applications/${app_name}.app" - local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local icon_source="${script_dir}/resources/MeticAI.icns" - - # Validate URL format (basic check for http/https) - if [[ ! "$url" =~ ^https?:// ]]; then - log_error "Error: Invalid URL format. Skipping dock shortcut creation." - return 1 - fi - - echo "" - log_warning "Creating macOS application and adding to Dock..." - - # Create application bundle structure (may need elevated permissions for /Applications) - if ! mkdir -p "${app_path}/Contents/MacOS" 2>/dev/null; then - # Try with run_privileged (handles sudo/no-sudo) - run_privileged mkdir -p "${app_path}/Contents/MacOS" - run_privileged mkdir -p "${app_path}/Contents/Resources" - NEED_SUDO=true - else - mkdir -p "${app_path}/Contents/Resources" - NEED_SUDO=false - fi - - # Copy the icon if it exists - if [ -f "$icon_source" ]; then - if [ "$NEED_SUDO" = true ]; then - run_privileged cp "$icon_source" "${app_path}/Contents/Resources/MeticAI.icns" - else - cp "$icon_source" "${app_path}/Contents/Resources/MeticAI.icns" - fi - local icon_key="CFBundleIconFile - MeticAI" - else - local icon_key="" - fi - - # Create the executable script with properly escaped URL - # Using printf to avoid shell expansion issues - local script_content="#!/bin/bash -# MeticAI Web App Launcher -open \"${url}\" -" - if [ "$NEED_SUDO" = true ]; then - echo "$script_content" | run_privileged tee "${app_path}/Contents/MacOS/${app_name}" > /dev/null - run_privileged chmod +x "${app_path}/Contents/MacOS/${app_name}" - else - echo "$script_content" > "${app_path}/Contents/MacOS/${app_name}" - chmod +x "${app_path}/Contents/MacOS/${app_name}" - fi - - # Create Info.plist with icon reference if available - local plist_content=" - - - - CFBundleExecutable - ${app_name} - ${icon_key} - CFBundleIdentifier - com.meticai.webapp - CFBundleName - ${app_name} - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSMinimumSystemVersion - 10.15 - NSHighResolutionCapable - - -" - - if [ "$NEED_SUDO" = true ]; then - echo "$plist_content" | run_privileged tee "${app_path}/Contents/Info.plist" > /dev/null - else - echo "$plist_content" > "${app_path}/Contents/Info.plist" - fi - - log_success "Application created at: ${app_path}" - - # Add to Dock using defaults command - # First check if it's already in the Dock - local dock_plist="$HOME/Library/Preferences/com.apple.dock.plist" - if ! /usr/libexec/PlistBuddy -c "Print :persistent-apps" "$dock_plist" 2>/dev/null | grep -q "MeticAI"; then - # Add the app to the Dock - defaults write com.apple.dock persistent-apps -array-add " - tile-data - - file-data - - _CFURLString - file://${app_path}/ - _CFURLStringType - 15 - - - " - - # Restart the Dock to apply changes - killall Dock - - log_success "MeticAI added to your Dock" - else - log_warning " MeticAI is already in your Dock" - fi -} - -# Detect OS -detect_os() { - if [[ "$OSTYPE" == "darwin"* ]]; then - OS="macos" - elif [ -f /etc/os-release ]; then - # shellcheck disable=SC1091 - . /etc/os-release - OS=$ID - elif [ -f /etc/redhat-release ]; then - OS="rhel" - else - OS="unknown" - fi - echo "$OS" -} - -# Install git based on OS -install_git() { - local os - os=$(detect_os) - log_warning "Installing git..." - - case "$os" in - macos) - if command -v brew &> /dev/null; then - if brew install git; then - log_success "Git installed successfully." - else - log_error "Failed to install git via Homebrew." - exit 1 - fi - else - log_warning "Homebrew not found. Installing Homebrew first..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - - # Source Homebrew environment for both Intel and Apple Silicon Macs - if [ -f /opt/homebrew/bin/brew ]; then - eval "$(/opt/homebrew/bin/brew shellenv)" - elif [ -f /usr/local/bin/brew ]; then - eval "$(/usr/local/bin/brew shellenv)" - fi - - if command -v brew &> /dev/null && brew install git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - echo "Visit: https://git-scm.com/downloads" - exit 1 - fi - fi - ;; - ubuntu|debian|raspbian) - if run_privileged apt-get update && run_privileged apt-get install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - ;; - fedora|rhel|centos) - if command -v dnf &> /dev/null; then - if run_privileged dnf install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - elif command -v yum &> /dev/null; then - if run_privileged yum install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - else - log_error "No supported package manager found. Please install git manually." - exit 1 - fi - ;; - arch|manjaro) - if run_privileged pacman -Sy --noconfirm git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - ;; - *) - log_error "Unsupported OS for automatic installation. Please install git manually." - echo "Visit: https://git-scm.com/downloads" - exit 1 - ;; - esac -} - -# Install docker based on OS -install_docker() { - local os - os=$(detect_os) - log_warning "Installing Docker..." - - if [[ "$os" == "macos" ]]; then - log_warning "On macOS, Docker Desktop must be installed manually." - log_warning "Please visit: https://www.docker.com/products/docker-desktop" - log_warning "After installing Docker Desktop, make sure it's running and try again." - exit 1 - fi - - # Use Docker's official convenience script - if command -v curl &> /dev/null; then - curl -fsSL https://get.docker.com -o /tmp/get-docker.sh - run_privileged sh /tmp/get-docker.sh - rm /tmp/get-docker.sh - - # Add current user to docker group (only if not already root) - if [ "$(id -u)" -ne 0 ]; then - run_privileged usermod -aG docker "$USER" || true - fi - - # Start docker service - run_privileged systemctl enable docker || true - run_privileged systemctl start docker || true - - log_success "Docker installed successfully." - log_warning "Note: You may need to log out and back in for docker group changes to take effect." - else - log_error "curl is required to install Docker. Please install curl first." - exit 1 - fi -} - -# Install docker compose plugin -install_docker_compose() { - local os - os=$(detect_os) - log_warning "Installing Docker Compose..." - - if [[ "$os" == "macos" ]]; then - log_warning "On macOS, Docker Compose comes bundled with Docker Desktop." - log_warning "If Docker Desktop is installed and running, Docker Compose should be available." - if check_docker_compose; then - log_success "Docker Compose is available." - return 0 - else - log_error "Docker Compose not found. Please make sure Docker Desktop is installed and running." - log_warning "Visit: https://www.docker.com/products/docker-desktop" - exit 1 - fi - fi - - # Try to install docker-compose-plugin (preferred method) - case "$os" in - ubuntu|debian|raspbian) - if run_privileged apt-get update; then - if run_privileged apt-get install -y docker-compose-plugin; then - log_success "Docker Compose plugin installed." - fi - fi - ;; - fedora|rhel|centos) - if command -v dnf &> /dev/null; then - run_privileged dnf install -y docker-compose-plugin - elif command -v yum &> /dev/null; then - run_privileged yum install -y docker-compose-plugin - fi - ;; - *) - # Fallback to standalone docker-compose - log_warning "Installing standalone docker-compose..." - run_privileged curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - run_privileged chmod +x /usr/local/bin/docker-compose - ;; - esac - - if check_docker_compose; then - log_success "Docker Compose installed successfully." - else - log_error "Failed to install Docker Compose. Please install manually." - exit 1 - fi -} - -# Detect server IP address (cross-platform) -detect_ip() { - local detected_ip="" - - # Try macOS-specific methods first - if [[ "$OSTYPE" == "darwin"* ]]; then - # Try to get IP from default route - detected_ip=$(route -n get default 2>/dev/null | grep 'interface:' | awk '{print $2}' | xargs ipconfig getifaddr 2>/dev/null) - - if [ -z "$detected_ip" ]; then - # Fallback: Try ipconfig getifaddr for common interfaces - for interface in en0 en1 en2 en3 en4; do - detected_ip=$(ipconfig getifaddr "$interface" 2>/dev/null) - if [ -n "$detected_ip" ]; then - echo "$detected_ip" - return - fi - done - fi - - if [ -z "$detected_ip" ]; then - # Last resort: use ifconfig and parse - detected_ip=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1) - fi - else - # Linux: use hostname -I - detected_ip=$(hostname -I 2>/dev/null | awk '{print $1}') - fi - - echo "$detected_ip" -} - -# Portable timeout function that works on both macOS and Linux -# Usage: run_with_timeout [args...] -run_with_timeout() { - local timeout_secs="$1" - shift - - # Try GNU timeout first (works on Linux, available as gtimeout on macOS with coreutils) - if command -v timeout &>/dev/null; then - timeout "$timeout_secs" "$@" 2>/dev/null - return $? - elif command -v gtimeout &>/dev/null; then - gtimeout "$timeout_secs" "$@" 2>/dev/null - return $? - else - # Fallback: use perl alarm (available on macOS by default) - perl -e 'alarm shift; exec @ARGV' "$timeout_secs" "$@" 2>/dev/null - return $? - fi -} - -# Resolve .local hostname to IP address (cross-platform) -resolve_local_hostname() { - local hostname="$1" - local resolved_ip="" - - # Ensure hostname ends with .local - if [[ ! "$hostname" =~ \.local$ ]]; then - hostname="${hostname}.local" - fi - - # Method 1: macOS dscacheutil (most reliable for .local on macOS) - if command -v dscacheutil &>/dev/null; then - resolved_ip=$(dscacheutil -q host -a name "$hostname" 2>/dev/null | grep "^ip_address:" | head -1 | awk '{print $2}') - if [[ -n "$resolved_ip" ]]; then - echo "$resolved_ip" - return 0 - fi - fi - - # Method 2: getent (Linux) - if command -v getent &>/dev/null; then - resolved_ip=$(getent hosts "$hostname" 2>/dev/null | awk '{print $1}' | head -1) - if [[ -n "$resolved_ip" ]]; then - echo "$resolved_ip" - return 0 - fi - fi - - # Method 3: ping (works on both, but slower) - if command -v ping &>/dev/null; then - # macOS ping uses -t for timeout, Linux uses -W - if [[ "$OSTYPE" == "darwin"* ]]; then - resolved_ip=$(ping -c 1 -t 2 "$hostname" 2>/dev/null | grep -oE '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' | head -1 | tr -d '()') - else - resolved_ip=$(ping -c 1 -W 2 "$hostname" 2>/dev/null | grep -oE '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' | head -1 | tr -d '()') - fi - if [[ -n "$resolved_ip" ]]; then - echo "$resolved_ip" - return 0 - fi - fi - - return 1 -} - -# Scan network for Meticulous machines -# Returns list of "hostname,ip" pairs -scan_for_meticulous() { - local devices=() - - # Method 1: macOS dns-sd (Bonjour) - most reliable on macOS - if [[ "$OSTYPE" == "darwin"* ]] && command -v dns-sd &>/dev/null; then - # Browse for _http._tcp services and capture meticulous devices - local dns_sd_output - dns_sd_output=$(run_with_timeout 4 dns-sd -B _http._tcp local 2>/dev/null || true) - - # Parse dns-sd output to find meticulous devices - # Format: "Timestamp A/R Flags if Domain Service Type Instance Name" - if [ -n "$dns_sd_output" ]; then - while IFS= read -r line; do - # Look for lines containing "meticulous" (case insensitive) - if echo "$line" | grep -qi "meticulous"; then - # Extract the instance name (last field, may contain spaces) - # The format is: timestamp Add/Remove flags interface domain type instancename - local instance_name - instance_name=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf "%s", (i>7?" ":"") $i; print ""}') - - if [[ -n "$instance_name" ]]; then - # Resolve the hostname to IP using dscacheutil - local resolved_ip - resolved_ip=$(resolve_local_hostname "$instance_name") - - if [[ -n "$resolved_ip" ]]; then - devices+=("${instance_name}.local,$resolved_ip") - fi - fi - fi - done <<< "$dns_sd_output" - fi - fi - - # Method 2: Try avahi-browse (Linux mDNS/Bonjour) - if command -v avahi-browse &>/dev/null && [[ ${#devices[@]} -eq 0 ]]; then - # Scan for _http._tcp services with a timeout - local avahi_results - avahi_results=$(run_with_timeout 5 avahi-browse -a -t -r -p 2>/dev/null | grep -i meticulous || true) - - if [ -n "$avahi_results" ]; then - # Parse avahi output: format is =;interface;protocol;name;type;domain;hostname;address;port;txt - while IFS= read -r line; do - if [[ "$line" == =* ]]; then - local hostname=$(echo "$line" | cut -d';' -f7) - local ip=$(echo "$line" | cut -d';' -f8) - if [[ "$hostname" =~ meticulous ]] && [[ -n "$ip" ]]; then - devices+=("$hostname,$ip") - fi - fi - done <<< "$avahi_results" - fi - fi - - # Method 3: Try ARP cache (works on both Linux and macOS) - if [[ ${#devices[@]} -eq 0 ]]; then - if command -v arp &>/dev/null; then - # Get all IPs from ARP cache and try to resolve hostnames - local arp_ips - arp_ips=$(arp -a 2>/dev/null | grep -oE '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' | tr -d '()' || true) - - for ip in $arp_ips; do - # Try to get hostname via various methods - local hostname="" - - # macOS: try dscacheutil reverse lookup - if [[ "$OSTYPE" == "darwin"* ]] && command -v dscacheutil &>/dev/null; then - hostname=$(dscacheutil -q host -a ip_address "$ip" 2>/dev/null | grep "^name:" | head -1 | awk '{print $2}' || true) - fi - - # Linux: try getent - if [[ -z "$hostname" ]] && command -v getent &>/dev/null; then - hostname=$(getent hosts "$ip" 2>/dev/null | awk '{print $2}' || true) - fi - - # Fallback: nslookup - if [[ -z "$hostname" ]] && command -v nslookup &>/dev/null; then - hostname=$(nslookup "$ip" 2>/dev/null | grep 'name =' | awk '{print $4}' | sed 's/\.$//' || true) - fi - - # Fallback: host command - if [[ -z "$hostname" ]] && command -v host &>/dev/null; then - hostname=$(host "$ip" 2>/dev/null | grep 'domain name pointer' | awk '{print $5}' | sed 's/\.$//' || true) - fi - - # Check if hostname contains "meticulous" - if [[ -n "$hostname" ]] && echo "$hostname" | grep -qi "meticulous"; then - devices+=("$hostname,$ip") - fi - done - fi - fi - - # Method 4: Try scanning .local mDNS domain directly - if [[ ${#devices[@]} -eq 0 ]]; then - # Try common meticulous hostnames patterns - for name in meticulous meticulousmodelalmondmilklatte meticulousInspiringCoffeeGeek; do - local resolved_ip - resolved_ip=$(resolve_local_hostname "$name") - - if [[ -n "$resolved_ip" ]]; then - devices+=("${name}.local,$resolved_ip") - fi - done - fi - - # Return unique devices - if [ ${#devices[@]} -gt 0 ]; then - printf '%s\n' "${devices[@]}" | sort -u - fi -} - -# 1. Check for Prerequisites -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Checking prerequisites..." 10 -else - log_warning "[1/4] Checking and installing prerequisites..." -fi - -# Check and install git -if ! command -v git &> /dev/null; then - if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - # Auto-install in non-interactive mode - show_progress "Installing git..." 12 - install_git - else - log_error "Error: git is not installed." - read -r -p "Would you like to install git now? (y/n) [y]: " INSTALL_GIT /dev/null; then - if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - echo "ERROR: Docker is not installed. Please install Docker Desktop first." - exit 1 - else - log_error "Error: docker is not installed." - read -r -p "Would you like to install Docker now? (y/n) [y]: " INSTALL_DOCKER /dev/null; then - if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - # Skip qrencode in non-interactive mode - it's optional - : # no-op - else - log_warning "qrencode is not installed (used for QR code generation)." - read -r -p "Would you like to install qrencode now? (y/n) [y]: " INSTALL_QRENCODE .env -GEMINI_API_KEY=$GEMINI_KEY -METICULOUS_IP=$MET_IP -PI_IP=$PI_IP -EOF - show_progress "Configuration created" 25 -else - log_warning "[2/4] Configuration" - echo "We need to create a .env file with your specific settings." - echo "" - - # --- Gemini API Key --- - # Get your free API key at: https://aistudio.google.com/app/api-keys - echo "Get your free API key at: https://aistudio.google.com/app/api-keys" - read -r -p "Enter your Google Gemini API Key: " GEMINI_KEY .env -GEMINI_API_KEY=$GEMINI_KEY -METICULOUS_IP=$MET_IP -PI_IP=$PI_IP -EOF - log_success ".env file created." - echo "" -fi - -# 3. Setup Dependencies (The MCP Fork & Web App) -################################################################################ -# Clone the Meticulous MCP server fork required for machine communication -# Repository: https://github.com/hessius/meticulous-mcp.git -# And the MeticAI Web Interface -# Repository: https://github.com/hessius/MeticAI-web.git -################################################################################ -log_warning "[3/4] Setting up Meticulous Source and Web App..." - -# Clone MCP Source -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Setting up dependencies..." 30 -fi -if [ -d "meticulous-source" ]; then - if [ "$FORCE_RECLONE" = "true" ]; then - # Non-interactive mode: force re-clone - show_progress "Removing old meticulous-source..." 35 - rm -rf meticulous-source - show_progress "Cloning meticulous-source..." 40 - git clone https://github.com/hessius/meticulous-mcp.git meticulous-source - elif [ "$METICAI_NON_INTERACTIVE" != "true" ]; then - echo "Directory 'meticulous-source' already exists." - read -r -p "Do you want to delete it and re-clone the latest version? (y/n) [n]: " CLONE_CONFIRM meticai-web/public/config.json -{ - "serverUrl": "http://$PI_IP:8000" -} -WEBCONFIG -# Also create in meticai-web root for standalone docker-compose usage -cp meticai-web/public/config.json meticai-web/config.json -log_success "Web app configured." -echo "" - -# 4. Build and Launch -################################################################################ -# Stop any existing containers, then build and start the Docker services -# This includes: -# - meticai-server: FastAPI server for receiving requests -# - gemini-client: AI brain using Google Gemini 2.0 Flash -################################################################################ - -# Load .env values for display and web config generation -if [ -f ".env" ]; then - # Source the .env file to get the values - set -a # automatically export all variables - source .env - set +a - - # Ensure variables are set (fallback for any issues) - : ${PI_IP:="localhost"} - : ${MET_IP:="localhost"} -fi - -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Building and launching containers..." 60 -else - log_warning "[4/4] Building and Launching Containers..." - if [ "$(id -u)" -eq 0 ]; then - echo "Note: Running as root." - elif command -v sudo &> /dev/null; then - echo "Note: Running with sudo permissions." - else - echo "Note: Running without elevated permissions." - fi -fi - -# Ensure .versions.json exists as a file (not directory) before Docker mounts it -# Docker will create a directory if the file doesn't exist, causing mount errors -if [ -d ".versions.json" ]; then - if [ "$METICAI_NON_INTERACTIVE" != "true" ]; then - log_warning "Fixing .versions.json (was directory, converting to file)..." - fi - rm -rf .versions.json -fi -if [ ! -f ".versions.json" ]; then - echo '{}' > .versions.json -fi - -# Ensure .rebuild-needed exists as a file for the trigger-update endpoint -if [ -d ".rebuild-needed" ]; then - rm -rf .rebuild-needed -fi -if [ ! -f ".rebuild-needed" ]; then - touch .rebuild-needed -fi - -# Ensure .update-check-requested exists as a file for the check-updates endpoint -if [ -d ".update-check-requested" ]; then - rm -rf .update-check-requested -fi -if [ ! -f ".update-check-requested" ]; then - touch .update-check-requested -fi - -# Ensure .update-requested exists as a file for the trigger-update endpoint -if [ -d ".update-requested" ]; then - rm -rf .update-requested -fi -if [ ! -f ".update-requested" ]; then - touch .update-requested -fi - -# Ensure .restart-requested exists as a file for the restart endpoint -if [ -d ".restart-requested" ]; then - rm -rf .restart-requested -fi -if [ ! -f ".restart-requested" ]; then - touch .restart-requested -fi - -# Ensure .rebuild-watcher.log exists as a file for the watcher status endpoint -if [ -d ".rebuild-watcher.log" ]; then - rm -rf .rebuild-watcher.log -fi -if [ ! -f ".rebuild-watcher.log" ]; then - touch .rebuild-watcher.log -fi - -# Pre-create directories that Docker would otherwise create as root -# This ensures proper ownership for the current user -mkdir -p data logs - -# Final safety check: ensure config.json files are not directories -# This catches cases where a previous failed install left directories behind -if [ -d "meticai-web/config.json" ]; then - rm -rf meticai-web/config.json - echo '{"serverUrl": "http://'"$PI_IP"':8000"}' > meticai-web/config.json -fi -if [ -d "meticai-web/public/config.json" ]; then - rm -rf meticai-web/public/config.json - echo '{"serverUrl": "http://'"$PI_IP"':8000"}' > meticai-web/public/config.json -fi - -if [ "$METICAI_NON_INTERACTIVE" != "true" ]; then - log_success "Data directories created with correct ownership" -fi - -# Stop existing containers if running (safety net in case any were missed earlier) -# This handles edge cases where containers might have been started after detection -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Stopping existing containers..." 65 -fi - -# On macOS with Docker Desktop, we don't need sudo for docker commands -# On Linux, we may need sudo if user isn't in docker group (but not on single-user systems) -if [[ "$OSTYPE" == "darwin"* ]]; then - docker compose down 2>/dev/null || true -else - run_privileged docker compose down 2>/dev/null || true -fi - -# Build and start -if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Building containers (this may take a few minutes)..." 70 -fi - -# Determine the docker command to use -# On macOS: always use docker directly (Docker Desktop handles permissions) -# On Linux: use run_privileged to handle both sudo and non-sudo environments -if [[ "$OSTYPE" == "darwin"* ]]; then - DOCKER_CMD="docker compose" -else - # Build the command dynamically to handle sudo/no-sudo - if [ "$(id -u)" -eq 0 ]; then - DOCKER_CMD="docker compose" - elif command -v sudo &> /dev/null; then - DOCKER_CMD="sudo docker compose" - else - DOCKER_CMD="docker compose" - fi -fi - -if $DOCKER_CMD up -d --build; then - if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Finalizing installation..." 90 - fi - - # Fix ownership of any files created by Docker running as root - # This is needed on Linux because sudo docker compose creates files as root - # On macOS with Docker Desktop, this is usually not necessary but doesn't hurt - # On single-user systems running as root, this is a no-op - if [ "$METICAI_NON_INTERACTIVE" != "true" ]; then - echo "Fixing file ownership..." - fi - if [[ "$OSTYPE" != "darwin"* ]] && [ "$(id -u)" -ne 0 ]; then - run_privileged chown -R "$(id -u):$(id -g)" data logs .versions.json .rebuild-needed .update-check-requested .update-requested 2>/dev/null || true - # Also fix git directories in case they were affected - run_privileged chown -R "$(id -u):$(id -g)" meticulous-source meticai-web 2>/dev/null || true - fi - - if [ "$METICAI_NON_INTERACTIVE" = "true" ]; then - show_progress "Installation complete!" 100 - else - echo "" - log_success "=========================================" - log_success " 🎉 Installation Complete! 🎉 " - log_success "=========================================" - echo "" - fi - echo "Your Barista Agent is running." - echo "" - echo -e "👉 **Web Interface:** http://$PI_IP:3550" - echo -e "👉 **Relay API:** http://$PI_IP:8000" - echo -e "👉 **Meticulous:** http://$MET_IP (via Agent)" - echo "" - - # Display QR code for easy mobile access - generate_qr_code "http://$PI_IP:3550" - - # Offer macOS dock shortcut creation (check if /dev/tty is available for input) - if [[ "$OSTYPE" == "darwin"* ]] && [[ -c /dev/tty ]]; then - # Check if user wants to skip via environment variable - if [[ "${SKIP_DOCK_SHORTCUT}" != "true" ]]; then - echo "" - read -r -p "Would you like to add MeticAI to your Dock? (y/n) [y]: " CREATE_DOCK_SHORTCUT /dev/null; then - echo -e "${GREEN}✓ Platypus found${NC}" - echo -e "${YELLOW}Building app with Platypus...${NC}" - - # Build with Platypus - platypus_args=( - --name "$APP_NAME" - --bundle-identifier "$BUNDLE_ID" - --app-version "$VERSION" - --author "MeticAI" - --interface-type "Progress Bar" - --interpreter "/bin/bash" - --quit-after-execution - --overwrite - ) - - # Add icon if available - if [ -n "$ICON_FILE" ]; then - platypus_args+=(--app-icon "$ICON_FILE") - fi - - # Add script - platypus_args+=("$WRAPPER_SCRIPT" "$APP_PATH") - - # Execute platypus - if platypus "${platypus_args[@]}"; then - echo -e "${GREEN}✓ App built successfully with Platypus${NC}" - else - echo -e "${RED}Error: Platypus build failed${NC}" - exit 1 - fi - -else - echo -e "${YELLOW}Platypus not found. Building app manually...${NC}" - echo -e "${BLUE}Tip: Install Platypus for better app creation: brew install platypus${NC}" - echo "" - - # Manual .app bundle creation - echo "Creating app bundle structure..." - - # Remove old app if exists - rm -rf "$APP_PATH" - - # Create bundle structure - mkdir -p "$APP_PATH/Contents/MacOS" - mkdir -p "$APP_PATH/Contents/Resources" - - # Copy wrapper script as executable - cp "$WRAPPER_SCRIPT" "$APP_PATH/Contents/MacOS/${EXEC_NAME}" - chmod +x "$APP_PATH/Contents/MacOS/${EXEC_NAME}" - - # Copy icon if available - if [ -n "$ICON_FILE" ]; then - cp "$ICON_FILE" "$APP_PATH/Contents/Resources/AppIcon.icns" - ICON_KEY="CFBundleIconFile - AppIcon" - else - ICON_KEY="" - fi - - # Create Info.plist - cat > "$APP_PATH/Contents/Info.plist" < - - - - CFBundleExecutable - ${EXEC_NAME} - CFBundleIdentifier - ${BUNDLE_ID} - CFBundleName - ${APP_NAME} - CFBundleDisplayName - ${APP_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - ${VERSION} - CFBundleVersion - ${VERSION} - ${ICON_KEY} - LSMinimumSystemVersion - 10.15 - NSHighResolutionCapable - - LSApplicationCategoryType - public.app-category.utilities - LSUIElement - - - -EOF - - echo -e "${GREEN}✓ App bundle created manually${NC}" -fi - -# Verify the app was created -if [ ! -d "$APP_PATH" ]; then - echo -e "${RED}Error: App bundle was not created${NC}" - exit 1 -fi - -# Make the app executable (verify file exists first for better error handling) -if [ -f "$APP_PATH/Contents/MacOS/${EXEC_NAME}" ]; then - chmod +x "$APP_PATH/Contents/MacOS/${EXEC_NAME}" -else - echo -e "${YELLOW}Warning: Executable file not found at expected location${NC}" -fi - -echo "" -echo -e "${GREEN}=========================================${NC}" -echo -e "${GREEN} ✓ Build Complete!${NC}" -echo -e "${GREEN}=========================================${NC}" -echo "" -echo -e "App location: ${BLUE}$APP_PATH${NC}" -echo "" -echo -e "${YELLOW}Next steps:${NC}" -echo "1. Test the app: open \"$APP_PATH\"" -echo "2. Move to Applications: mv \"$APP_PATH\" /Applications/" -echo "3. Distribute: Create a DMG or ZIP file" -echo "" -echo -e "${YELLOW}To create a distributable DMG:${NC}" -echo " hdiutil create -volname \"MeticAI Uninstaller\" -srcfolder \"$APP_PATH\" -ov -format UDZO \"$OUTPUT_DIR/MeticAI-Uninstaller.dmg\"" -echo "" diff --git a/rebuild-watcher.sh b/rebuild-watcher.sh deleted file mode 100755 index f7bab67..0000000 --- a/rebuild-watcher.sh +++ /dev/null @@ -1,528 +0,0 @@ -#!/bin/bash - -################################################################################ -# MeticAI - Host Rebuild Watcher -################################################################################ -# -# This script watches for rebuild requests triggered by the update system -# running inside containers. When updates are pulled from inside a container, -# some containers (with host volume mounts) cannot be rebuilt from within. -# -# USAGE: -# ./rebuild-watcher.sh # Run once, check and rebuild if needed -# ./rebuild-watcher.sh --watch # Continuously watch (for launchd/cron) -# ./rebuild-watcher.sh --install # Install as launchd service (macOS) -# -# HOW IT WORKS: -# 1. The update system inside containers creates .rebuild-needed when updates -# are pulled but containers can't be rebuilt -# 2. This script detects that file and runs docker compose up -d --build -# 3. After successful rebuild, the flag file is removed -# -################################################################################ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source common library -source "$SCRIPT_DIR/scripts/lib/common.sh" -REBUILD_FLAG="$SCRIPT_DIR/.rebuild-needed" -UPDATE_CHECK_FLAG="$SCRIPT_DIR/.update-check-requested" -UPDATE_REQUESTED_FLAG="$SCRIPT_DIR/.update-requested" -RESTART_REQUESTED_FLAG="$SCRIPT_DIR/.restart-requested" -LOG_FILE="$SCRIPT_DIR/.rebuild-watcher.log" - -# Set up PATH for launchd environment (needed for Docker credential helpers) -export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" - -# Docker Desktop credential helper location -if [ -d "/Applications/Docker.app/Contents/Resources/bin" ]; then - export PATH="/Applications/Docker.app/Contents/Resources/bin:$PATH" -fi - -# Custom log function that writes to file and uses colors from common.sh -log() { - echo -e "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" -} - -# Ensure required files exist and have correct permissions before Docker operations -prepare_for_docker() { - cd "$SCRIPT_DIR" - - # Ensure required files exist as files (not directories) - for file in ".versions.json" ".rebuild-needed" ".update-check-requested" ".update-requested" ".restart-requested"; do - if [ -d "$SCRIPT_DIR/$file" ]; then - rm -rf "$SCRIPT_DIR/$file" - fi - done - - [ ! -f "$SCRIPT_DIR/.versions.json" ] && echo '{}' > "$SCRIPT_DIR/.versions.json" - [ ! -f "$SCRIPT_DIR/.rebuild-needed" ] && touch "$SCRIPT_DIR/.rebuild-needed" - [ ! -f "$SCRIPT_DIR/.update-check-requested" ] && touch "$SCRIPT_DIR/.update-check-requested" - [ ! -f "$SCRIPT_DIR/.update-requested" ] && touch "$SCRIPT_DIR/.update-requested" - [ ! -f "$SCRIPT_DIR/.restart-requested" ] && touch "$SCRIPT_DIR/.restart-requested" - - # Pre-create directories so Docker doesn't create them as root - mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs" -} - -# Fix permissions after Docker operations (needed when using sudo) -fix_permissions() { - cd "$SCRIPT_DIR" - - # Get the owner of the script directory (should be the real user) - local dir_owner - dir_owner=$(stat -c '%u:%g' "$SCRIPT_DIR" 2>/dev/null || stat -f '%u:%g' "$SCRIPT_DIR" 2>/dev/null) - - if [ -n "$dir_owner" ]; then - # Fix ownership of files that Docker might have created as root - for item in data logs .versions.json .rebuild-needed .update-check-requested .update-requested .restart-requested meticulous-source meticai-web; do - if [ -e "$SCRIPT_DIR/$item" ]; then - sudo chown -R "$dir_owner" "$SCRIPT_DIR/$item" 2>/dev/null || true - fi - done - fi -} - -# Handle full update request (triggered by trigger-update API endpoint) -do_full_update() { - log "${YELLOW}Full update requested by UI - pulling updates and rebuilding...${NC}" - - cd "$SCRIPT_DIR" - - if [ -x "$SCRIPT_DIR/update.sh" ]; then - if "$SCRIPT_DIR/update.sh" --auto 2>&1 | tee -a "$LOG_FILE"; then - log "${GREEN}✓ Full update completed${NC}" - else - log "${YELLOW}Update completed with warnings${NC}" - fi - else - log "${RED}update.sh not found or not executable${NC}" - fi - - # Clear the signal file - echo "" > "$UPDATE_REQUESTED_FLAG" -} - -check_full_update_needed() { - if [ -f "$UPDATE_REQUESTED_FLAG" ]; then - # File exists - check if it has non-whitespace content - if grep -q '[^[:space:]]' "$UPDATE_REQUESTED_FLAG" 2>/dev/null; then - return 0 - fi - fi - return 1 -} - -# Handle update check request (triggered by API endpoint) -do_update_check() { - log "${BLUE}Update check requested by API...${NC}" - - cd "$SCRIPT_DIR" - - if [ -x "$SCRIPT_DIR/update.sh" ]; then - if "$SCRIPT_DIR/update.sh" --check-only 2>&1 | tee -a "$LOG_FILE"; then - log "${GREEN}✓ Update check completed${NC}" - else - log "${YELLOW}Update check had issues but may have succeeded${NC}" - fi - else - log "${RED}update.sh not found or not executable${NC}" - fi - - # Clear the signal file - echo "" > "$UPDATE_CHECK_FLAG" -} - -check_update_check_needed() { - if [ -f "$UPDATE_CHECK_FLAG" ]; then - # File exists - check if it has non-whitespace content - if grep -q '[^[:space:]]' "$UPDATE_CHECK_FLAG" 2>/dev/null; then - return 0 - fi - fi - return 1 -} - -# Handle restart request (triggered by API endpoint) -do_restart() { - log "${BLUE}Restart requested by API...${NC}" - - cd "$SCRIPT_DIR" - - # Prepare files before Docker runs - prepare_for_docker - - # Check if docker compose is available - DOCKER_PATHS="/usr/local/bin/docker /opt/homebrew/bin/docker /Applications/Docker.app/Contents/Resources/bin/docker" - DOCKER_CMD="" - - for path in $DOCKER_PATHS; do - if [ -x "$path" ]; then - DOCKER_CMD="$path" - break - fi - done - - if [ -z "$DOCKER_CMD" ]; then - DOCKER_CMD=$(command -v docker 2>/dev/null) - fi - - if [ -z "$DOCKER_CMD" ] || ! "$DOCKER_CMD" compose version &> /dev/null; then - log "${RED}Error: Docker Compose not found${NC}" - echo "" > "$RESTART_REQUESTED_FLAG" - return 1 - fi - - # On Linux, we may need sudo for docker commands - SUDO_PREFIX="" - if [[ "$OSTYPE" == "linux"* ]]; then - if ! "$DOCKER_CMD" info &> /dev/null; then - SUDO_PREFIX="sudo" - fi - fi - - COMPOSE_CMD="$SUDO_PREFIX $DOCKER_CMD compose" - - log "Restarting containers..." - if $COMPOSE_CMD restart 2>&1 | tee -a "$LOG_FILE"; then - log "${GREEN}✓ Containers restarted successfully${NC}" - fix_permissions - else - log "${RED}✗ Failed to restart containers${NC}" - fi - - # Clear the signal file - echo "" > "$RESTART_REQUESTED_FLAG" -} - -check_restart_needed() { - if [ -f "$RESTART_REQUESTED_FLAG" ]; then - # File exists - check if it has non-whitespace content - if grep -q '[^[:space:]]' "$RESTART_REQUESTED_FLAG" 2>/dev/null; then - return 0 - fi - fi - return 1 -} - -do_rebuild() { - log "${YELLOW}Rebuild requested - starting container rebuild...${NC}" - - cd "$SCRIPT_DIR" - - # Prepare files before Docker runs - prepare_for_docker - - # Check if docker compose is available (use full paths for launchd compatibility) - # Common Docker paths on macOS - DOCKER_PATHS="/usr/local/bin/docker /opt/homebrew/bin/docker /Applications/Docker.app/Contents/Resources/bin/docker" - DOCKER_CMD="" - - for path in $DOCKER_PATHS; do - if [ -x "$path" ]; then - DOCKER_CMD="$path" - break - fi - done - - if [ -z "$DOCKER_CMD" ]; then - # Fallback to PATH lookup - DOCKER_CMD=$(command -v docker 2>/dev/null) - fi - - if [ -z "$DOCKER_CMD" ] || ! "$DOCKER_CMD" compose version &> /dev/null; then - log "${RED}Error: Docker Compose not found (tried: $DOCKER_PATHS)${NC}" - return 1 - fi - - # On Linux, we may need sudo for docker commands - SUDO_PREFIX="" - if [[ "$OSTYPE" == "linux"* ]]; then - if ! "$DOCKER_CMD" info &> /dev/null; then - SUDO_PREFIX="sudo" - fi - fi - - COMPOSE_CMD="$SUDO_PREFIX $DOCKER_CMD compose" - - # Rebuild containers - log "Rebuilding containers..." - if $COMPOSE_CMD up -d --build 2>&1 | tee -a "$LOG_FILE"; then - log "${GREEN}✓ Containers rebuilt successfully${NC}" - - # Fix permissions after Docker operations - fix_permissions - - # Clear the file contents but keep the file (Docker needs it to exist for mount) - echo "" > "$REBUILD_FLAG" - - # Update the versions file to clear update_available - if [ -x "$SCRIPT_DIR/update.sh" ]; then - "$SCRIPT_DIR/update.sh" --check-only &>/dev/null - fi - - return 0 - else - log "${RED}✗ Failed to rebuild containers${NC}" - return 1 - fi -} - -check_rebuild_needed() { - if [ -f "$REBUILD_FLAG" ]; then - # File exists - check if it has non-whitespace content - if grep -q '[^[:space:]]' "$REBUILD_FLAG" 2>/dev/null; then - return 0 - fi - fi - return 1 -} - -install_launchd() { - local plist_path="$HOME/Library/LaunchAgents/com.meticai.rebuild-watcher.plist" - local script_path="$SCRIPT_DIR/rebuild-watcher.sh" - - log_info "Installing launchd service..." - - # Create LaunchAgents directory if it doesn't exist - mkdir -p "$HOME/Library/LaunchAgents" - - # Create the plist file - cat > "$plist_path" < - - - - Label - com.meticai.rebuild-watcher - ProgramArguments - - $script_path - - WatchPaths - - $REBUILD_FLAG - $UPDATE_CHECK_FLAG - $UPDATE_REQUESTED_FLAG - $RESTART_REQUESTED_FLAG - - RunAtLoad - - StandardOutPath - $LOG_FILE - StandardErrorPath - $LOG_FILE - - -EOF - - # Load the service - launchctl unload "$plist_path" 2>/dev/null - launchctl load "$plist_path" - - log_success "Launchd service installed" - echo -e " Service will automatically rebuild containers when updates are triggered." - echo -e " Log file: $LOG_FILE" - echo "" - echo -e " To uninstall: launchctl unload $plist_path && rm $plist_path" -} - -uninstall_launchd() { - local plist_path="$HOME/Library/LaunchAgents/com.meticai.rebuild-watcher.plist" - - if [ -f "$plist_path" ]; then - launchctl unload "$plist_path" 2>/dev/null - rm -f "$plist_path" - log_success "Launchd service uninstalled" - else - log_warning "Service not installed" - fi -} - -################################################################################ -# Linux (systemd) Installation - For Raspberry Pi and other Linux systems -################################################################################ - -install_systemd() { - local service_dir="/etc/systemd/system" - local path_unit="$service_dir/meticai-rebuild-watcher.path" - local service_unit="$service_dir/meticai-rebuild-watcher.service" - local script_path="$SCRIPT_DIR/rebuild-watcher.sh" - - log_info "Installing systemd service..." - - # Check if we have sudo access - if ! sudo -n true 2>/dev/null; then - log_warning "This requires sudo access. You may be prompted for your password." - fi - - # Create the path unit (watches the files) - sudo tee "$path_unit" > /dev/null < /dev/null </dev/null - sudo systemctl disable meticai-rebuild-watcher.path 2>/dev/null - sudo systemctl stop meticai-rebuild-watcher.service 2>/dev/null - - # Remove unit files - if [ -f "$path_unit" ] || [ -f "$service_unit" ]; then - sudo rm -f "$path_unit" "$service_unit" - sudo systemctl daemon-reload - log_success "Systemd service uninstalled" - else - log_warning "Service not installed" - fi -} - -# Detect OS and call appropriate install/uninstall function -install_service() { - if [[ "$OSTYPE" == "darwin"* ]]; then - install_launchd - elif [[ "$OSTYPE" == "linux"* ]]; then - install_systemd - else - log_error "Unsupported OS: $OSTYPE" - echo "Supported: macOS (darwin), Linux" - exit 1 - fi -} - -uninstall_service() { - if [[ "$OSTYPE" == "darwin"* ]]; then - uninstall_launchd - elif [[ "$OSTYPE" == "linux"* ]]; then - uninstall_systemd - else - log_error "Unsupported OS: $OSTYPE" - exit 1 - fi -} - -show_help() { - echo "MeticAI Rebuild Watcher" - echo "" - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " (no args) Check once and rebuild if needed" - echo " --watch Continuously watch for rebuild requests" - echo " --install Install as system service (launchd on macOS, systemd on Linux)" - echo " --uninstall Remove system service" - echo " --status Check if rebuild is needed" - echo " --help Show this help message" - echo "" - echo "The rebuild watcher enables fully automatic updates from the web UI." - echo "When updates are triggered from inside a container, this service" - echo "completes the rebuild process on the host system." -} - -# Main -case "${1:-}" in - --watch) - log "Starting rebuild watcher (continuous mode)..." - while true; do - if check_full_update_needed; then - do_full_update - fi - if check_update_check_needed; then - do_update_check - fi - if check_restart_needed; then - do_restart - fi - if check_rebuild_needed; then - do_rebuild - fi - sleep 2 - done - ;; - --install) - install_service - ;; - --uninstall) - uninstall_service - ;; - --status) - if check_rebuild_needed; then - log_warning "Rebuild is needed" - cat "$REBUILD_FLAG" - exit 1 - else - log_success "No rebuild needed" - exit 0 - fi - ;; - --help|-h) - show_help - ;; - *) - # Default: check once for all signals - if check_full_update_needed; then - do_full_update - elif check_update_check_needed; then - do_update_check - elif check_restart_needed; then - do_restart - elif check_rebuild_needed; then - do_rebuild - else - log_success "No action needed" - fi - ;; -esac diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100755 index f1ba5ab..0000000 --- a/uninstall.sh +++ /dev/null @@ -1,607 +0,0 @@ -#!/bin/bash - -################################################################################ -# MeticAI - Uninstaller -################################################################################ -# -# This script removes MeticAI and its related files from your system. -# -# USAGE: -# ./uninstall.sh -# -# WHAT IT DOES: -# 1. Stops and removes all MeticAI Docker containers -# 2. Removes Docker images built by MeticAI -# 3. Removes cloned repositories (meticulous-source, meticai-web) -# 4. Removes configuration files (.env, settings) -# 5. Optionally removes macOS integrations (Dock shortcut, rebuild watcher) -# 6. Asks whether to remove external dependencies (Docker, git, etc.) -# -# NOTE: -# - External dependencies (Docker, git, qrencode) are NOT automatically -# removed unless you explicitly choose to do so -# - You can safely keep Docker and git if you use them for other projects -# -################################################################################ - -# Get the directory where this script is located (absolute path) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source common library -source "$SCRIPT_DIR/scripts/lib/common.sh" - -# Constants -WEB_INSTALL_URL="https://raw.githubusercontent.com/hessius/MeticAI/main/web_install.sh" - -echo -e "${RED}=========================================${NC}" -echo -e "${RED} ☕️ MeticAI Uninstaller 🗑️ ${NC}" -echo -e "${RED}=========================================${NC}" -echo "" -log_warning "This will remove MeticAI from your system." -echo "" - -# Ask for confirmation -read -r -p "Are you sure you want to uninstall MeticAI? (y/n) [n]: " CONFIRM /dev/null; then - # Preserve previous sudo state if it was true - [ "$prev_used_sudo" = "true" ] && USED_SUDO="true" - return 0 - # Try with sudo on Linux if regular command failed - elif [[ "$OSTYPE" != "darwin"* ]] && eval "sudo $cmd" 2>/dev/null; then - USED_SUDO="true" - return 0 - else - # Preserve previous sudo state if it was true - [ "$prev_used_sudo" = "true" ] && USED_SUDO="true" - return 1 - fi -} - -# 1. Stop and remove Docker containers -################################################################################ -log_warning "[1/7] Stopping and removing Docker containers..." - -if command -v docker &> /dev/null; then - # Only attempt docker compose commands if a compose file exists - if [ -f "docker-compose.yml" ] || [ -f "docker-compose.yaml" ] || [ -f "compose.yml" ] || [ -f "compose.yaml" ]; then - # Check if containers exist and stop them - if try_docker_command "docker compose ps -q" || try_docker_command "docker-compose ps -q"; then - # Stop and remove containers (supports both: docker compose down || docker-compose down) - CONTAINERS_REMOVED=false - if try_docker_command "docker compose down"; then - CONTAINERS_REMOVED=true - elif try_docker_command "docker-compose down"; then - CONTAINERS_REMOVED=true - fi - - if [ "$CONTAINERS_REMOVED" = true ]; then - if [ "$USED_SUDO" = "true" ]; then - log_success "Containers stopped and removed (with sudo)" - else - log_success "Containers stopped and removed" - fi - UNINSTALLED_ITEMS+=("Docker containers") - else - log_warning "Unable to stop containers - they are not running" - KEPT_ITEMS+=("Docker containers (not found)") - fi - else - log_warning "No containers found or not running" - KEPT_ITEMS+=("Docker containers (not found)") - fi - else - log_warning "No docker-compose.yml found" - KEPT_ITEMS+=("Docker containers (no compose file)") - fi -else - log_warning "Docker not found, skipping container cleanup" - KEPT_ITEMS+=("Docker containers (Docker not installed)") -fi - -# 2. Remove Docker images -################################################################################ -log_warning "[2/7] Removing Docker images..." - -if command -v docker &> /dev/null; then - # List MeticAI-related images (matches various naming patterns: meticai-, met-ai-, meticai-web-, etc.) - # Try without sudo first - if docker images &> /dev/null; then - IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep -E "(meticai|met-ai|meticai-server|gemini-client|meticulous-mcp|meticulous-source)" || true) - USED_SUDO_FOR_IMAGES=false - # If the docker command failed (permission denied), try with sudo on Linux - elif [[ "$OSTYPE" != "darwin"* ]] && sudo docker images &> /dev/null; then - IMAGES=$(sudo docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep -E "(meticai|met-ai|meticai-server|gemini-client|meticulous-mcp|meticulous-source)" || true) - USED_SUDO_FOR_IMAGES=true - else - IMAGES="" - USED_SUDO_FOR_IMAGES=false - fi - - if [ -n "$IMAGES" ]; then - echo "Found MeticAI images:" - echo "$IMAGES" - echo "" - read -r -p "Remove these Docker images? (y/n) [y]: " REMOVE_IMAGES /dev/null; then - log_success "Docker images removed (with sudo)" - UNINSTALLED_ITEMS+=("Docker images") - else - log_warning "Could not remove images" - KEPT_ITEMS+=("Docker images (removal failed)") - fi - # Try removing without sudo first - elif echo "$IMAGES" | xargs docker rmi -f 2>/dev/null; then - log_success "Docker images removed" - UNINSTALLED_ITEMS+=("Docker images") - # Try with sudo on Linux if regular command failed - elif [[ "$OSTYPE" != "darwin"* ]] && echo "$IMAGES" | xargs sudo docker rmi -f 2>/dev/null; then - log_success "Docker images removed (with sudo)" - UNINSTALLED_ITEMS+=("Docker images") - else - log_warning "Could not remove images" - KEPT_ITEMS+=("Docker images (removal failed)") - fi - else - log_warning "Keeping Docker images" - KEPT_ITEMS+=("Docker images (user choice)") - fi - else - log_warning "No MeticAI images found" - KEPT_ITEMS+=("Docker images (not found)") - fi -else - log_warning "Docker not found, skipping image cleanup" - KEPT_ITEMS+=("Docker images (Docker not installed)") -fi - -# 3. Remove cloned repositories -################################################################################ -log_warning "[3/7] Removing cloned repositories..." - -REMOVED_REPOS=0 - -if [ -d "meticulous-source" ]; then - rm -rf meticulous-source - log_success "Removed meticulous-source directory" - ((REMOVED_REPOS++)) -fi - -if [ -d "meticai-web" ]; then - rm -rf meticai-web - log_success "Removed meticai-web directory" - ((REMOVED_REPOS++)) -fi - -if [ $REMOVED_REPOS -gt 0 ]; then - UNINSTALLED_ITEMS+=("Cloned repositories ($REMOVED_REPOS)") -else - log_warning "No cloned repositories found" - KEPT_ITEMS+=("Cloned repositories (not found)") -fi - -# 4. Remove configuration files (preserving .env and data for reinstallation) -################################################################################ -log_warning "[4/7] Handling configuration files..." - -REMOVED_CONFIGS=0 - -# Preserve .env file for potential reinstallation -if [ -f ".env" ]; then - log_info "Preserving .env file for potential reinstallation" - log_info " (Contains your GEMINI_API_KEY, METICULOUS_IP, PI_IP settings)" - KEPT_ITEMS+=(".env file (preserved)") -fi - -# Preserve data directory (contains profile history) -if [ -d "data" ]; then - log_info "Preserving data/ directory (contains profile history)" - KEPT_ITEMS+=("data/ directory (preserved)") -fi - -# Preserve logs directory -if [ -d "logs" ]; then - log_info "Preserving logs/ directory" - KEPT_ITEMS+=("logs/ directory (preserved)") -fi - -if [ -f ".versions.json" ]; then - rm -f .versions.json - log_success "Removed .versions.json file" - ((REMOVED_CONFIGS++)) -fi - -# Note: .update-config.json is a source file and should not be removed - -if [ -f ".rebuild-needed" ]; then - rm -f .rebuild-needed - log_success "Removed .rebuild-needed file" - ((REMOVED_CONFIGS++)) -fi - -if [ -f ".rebuild-watcher.log" ]; then - rm -f .rebuild-watcher.log - log_success "Removed .rebuild-watcher.log file" - ((REMOVED_CONFIGS++)) -fi - -if [ $REMOVED_CONFIGS -gt 0 ]; then - UNINSTALLED_ITEMS+=("Configuration files ($REMOVED_CONFIGS)") -else - log_warning "No configuration files found" - KEPT_ITEMS+=("Configuration files (not found)") -fi - -# 5. Remove macOS integrations -################################################################################ -log_warning "[5/7] Removing macOS integrations..." - -REMOVED_MACOS=0 - -# Remove Dock shortcut -if [[ "$OSTYPE" == "darwin"* ]]; then - APP_PATH="/Applications/MeticAI.app" - if [ -d "$APP_PATH" ]; then - log_warning "Found MeticAI.app in Applications" - read -r -p "Remove MeticAI from Applications and Dock? (y/n) [y]: " REMOVE_APP /dev/null; then - log_success "Removed MeticAI.app" - ((REMOVED_MACOS++)) - else - # Need sudo - sudo rm -rf "$APP_PATH" 2>/dev/null - log_success "Removed MeticAI.app (with sudo)" - ((REMOVED_MACOS++)) - fi - - # Note: Removing the app from Dock programmatically is complex and risky - # The Dock will automatically remove the icon when it detects the app is missing - # User can also manually remove it by dragging the icon out of the Dock - log_warning "Note: MeticAI icon will disappear from Dock automatically or can be removed manually" - else - log_warning "Keeping MeticAI.app" - KEPT_ITEMS+=("macOS Dock shortcut (user choice)") - fi - fi - - # Uninstall rebuild watcher service - PLIST_PATH="$HOME/Library/LaunchAgents/com.meticai.rebuild-watcher.plist" - if [ -f "$PLIST_PATH" ]; then - log_warning "Found rebuild watcher service" - read -r -p "Remove rebuild watcher service? (y/n) [y]: " REMOVE_WATCHER /dev/null || true - rm -f "$PLIST_PATH" - log_success "Removed rebuild watcher service" - ((REMOVED_MACOS++)) - else - log_warning "Keeping rebuild watcher service" - KEPT_ITEMS+=("Rebuild watcher service (user choice)") - fi - fi - - if [ $REMOVED_MACOS -gt 0 ]; then - UNINSTALLED_ITEMS+=("macOS integrations ($REMOVED_MACOS)") - else - if [ ! -d "$APP_PATH" ] && [ ! -f "$PLIST_PATH" ]; then - log_warning "No macOS integrations found" - KEPT_ITEMS+=("macOS integrations (not found)") - fi - fi -else - log_warning "Not running on macOS, skipping" - KEPT_ITEMS+=("macOS integrations (not macOS)") -fi - -# Linux-specific (systemd) service cleanup -if [[ "$OSTYPE" == "linux"* ]]; then - log_warning "Checking for Linux systemd integrations..." - REMOVED_LINUX=0 - - # Check for systemd path unit - if [ -f "/etc/systemd/system/meticai-rebuild-watcher.path" ]; then - log_warning "Found rebuild watcher systemd service" - read -r -p "Remove rebuild watcher service? (y/n) [y]: " REMOVE_WATCHER /dev/null || true - sudo systemctl disable meticai-rebuild-watcher.path 2>/dev/null || true - sudo rm -f /etc/systemd/system/meticai-rebuild-watcher.path - sudo rm -f /etc/systemd/system/meticai-rebuild-watcher.service - sudo systemctl daemon-reload - log_success "Removed rebuild watcher systemd service" - ((REMOVED_LINUX++)) - else - log_warning "Keeping rebuild watcher service" - KEPT_ITEMS+=("Rebuild watcher service (user choice)") - fi - fi - - if [ $REMOVED_LINUX -gt 0 ]; then - UNINSTALLED_ITEMS+=("Linux integrations ($REMOVED_LINUX)") - fi -fi - -# 6. Ask about external dependencies -################################################################################ -log_warning "[6/7] External dependencies (optional)..." -echo "" -log_info "MeticAI can optionally remove external dependencies that were installed" -log_info "during setup. These are only removed if you explicitly choose to do so." -echo "" -log_warning "⚠️ WARNING: Only remove these if you don't use them for other projects!" -echo "" - -# Docker -if command -v docker &> /dev/null; then - log_warning "Docker is installed on this system" - read -r -p "Do you want to remove Docker? (y/n) [n]: " REMOVE_DOCKER /dev/null; then - sudo apt-get remove -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null || true - log_success "Docker removed" - UNINSTALLED_ITEMS+=("Docker") - elif command -v dnf &> /dev/null; then - sudo dnf remove -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null || true - log_success "Docker removed" - UNINSTALLED_ITEMS+=("Docker") - elif command -v yum &> /dev/null; then - sudo yum remove -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null || true - log_success "Docker removed" - UNINSTALLED_ITEMS+=("Docker") - else - log_warning "Could not determine package manager for Docker removal" - FAILED_ITEMS+=("Docker (unknown package manager)") - fi - fi - else - log_success "Keeping Docker" - KEPT_ITEMS+=("Docker (user choice)") - fi -else - KEPT_ITEMS+=("Docker (not installed)") -fi - -echo "" - -# Git -if command -v git &> /dev/null; then - log_warning "Git is installed on this system" - read -r -p "Do you want to remove git? (y/n) [n]: " REMOVE_GIT /dev/null; then - brew uninstall git 2>/dev/null || true - log_success "Git removed" - UNINSTALLED_ITEMS+=("git") - else - log_warning "Cannot remove git (not installed via Homebrew)" - KEPT_ITEMS+=("git (not managed by Homebrew)") - fi - elif command -v apt-get &> /dev/null; then - sudo apt-get remove -y git 2>/dev/null || true - log_success "Git removed" - UNINSTALLED_ITEMS+=("git") - elif command -v dnf &> /dev/null; then - sudo dnf remove -y git 2>/dev/null || true - log_success "Git removed" - UNINSTALLED_ITEMS+=("git") - elif command -v yum &> /dev/null; then - sudo yum remove -y git 2>/dev/null || true - log_success "Git removed" - UNINSTALLED_ITEMS+=("git") - else - log_warning "Could not determine package manager for git removal" - FAILED_ITEMS+=("git (unknown package manager)") - fi - else - log_success "Keeping git" - KEPT_ITEMS+=("git (user choice)") - fi -else - KEPT_ITEMS+=("git (not installed)") -fi - -echo "" - -# qrencode -if command -v qrencode &> /dev/null; then - log_warning "qrencode is installed on this system" - read -r -p "Do you want to remove qrencode? (y/n) [n]: " REMOVE_QRENCODE /dev/null; then - brew uninstall qrencode 2>/dev/null || true - log_success "qrencode removed" - UNINSTALLED_ITEMS+=("qrencode") - else - log_warning "Cannot remove qrencode (not installed via Homebrew)" - KEPT_ITEMS+=("qrencode (not managed by Homebrew)") - fi - elif command -v apt-get &> /dev/null; then - sudo apt-get remove -y qrencode 2>/dev/null || true - log_success "qrencode removed" - UNINSTALLED_ITEMS+=("qrencode") - elif command -v dnf &> /dev/null; then - sudo dnf remove -y qrencode 2>/dev/null || true - log_success "qrencode removed" - UNINSTALLED_ITEMS+=("qrencode") - elif command -v yum &> /dev/null; then - sudo yum remove -y qrencode 2>/dev/null || true - log_success "qrencode removed" - UNINSTALLED_ITEMS+=("qrencode") - else - log_warning "Could not determine package manager for qrencode removal" - FAILED_ITEMS+=("qrencode (unknown package manager)") - fi - else - log_success "Keeping qrencode" - KEPT_ITEMS+=("qrencode (user choice)") - fi -else - KEPT_ITEMS+=("qrencode (not installed)") -fi - -echo "" - -# 7. Summary -################################################################################ -log_warning "[7/7] Uninstallation complete!" -echo "" -echo -e "${GREEN}=========================================${NC}" -echo -e "${GREEN} Uninstallation Summary ${NC}" -echo -e "${GREEN}=========================================${NC}" -echo "" - -if [ ${#UNINSTALLED_ITEMS[@]} -gt 0 ]; then - echo -e "${GREEN}✓ Removed:${NC}" - for item in "${UNINSTALLED_ITEMS[@]}"; do - echo -e " - $item" - done - echo "" -fi - -if [ ${#KEPT_ITEMS[@]} -gt 0 ]; then - echo -e "${YELLOW}⊙ Kept:${NC}" - for item in "${KEPT_ITEMS[@]}"; do - echo -e " - $item" - done - echo "" -fi - -if [ ${#FAILED_ITEMS[@]} -gt 0 ]; then - echo -e "${RED}✗ Failed to remove:${NC}" - for item in "${FAILED_ITEMS[@]}"; do - echo -e " - $item" - done - echo "" -fi - -log_info "Thank you for using MeticAI! ☕️" -echo "" - -# Check if uninstall was called from an installer script -if [[ "$METICAI_CALLED_FROM_INSTALLER" == "true" ]]; then - echo -e "${GREEN}=========================================${NC}" - echo -e "${GREEN} Restart Installation Flow? ${NC}" - echo -e "${GREEN}=========================================${NC}" - echo "" - echo -e "${YELLOW}The uninstallation is complete.${NC}" - echo "" - - # Determine which install script to use - INSTALL_SCRIPT="./local-install.sh" - if [[ "$METICAI_INSTALL_METHOD" == "web_install.sh" ]]; then - INSTALL_SCRIPT="./web_install.sh" - fi - - echo -e "${BLUE}Would you like to restart the installation process now?${NC}" - echo -e " This will run: ${GREEN}$INSTALL_SCRIPT${NC}" - echo "" - read -r -p "Restart installation? (y/n) [y]: " RESTART_INSTALL /dev/null - else - echo "not-a-git-repo" - fi -} - -# Function to get remote commit hash -get_remote_hash() { - local dir="$1" - local branch="${2:-main}" - if [ -d "$dir/.git" ]; then - # Try to fetch first (may fail due to permissions) - local fetch_result - fetch_result=$(cd "$dir" && git fetch origin "$branch" 2>&1) - - # If fetch failed due to permissions, try ls-remote as fallback - if echo "$fetch_result" | grep -q "Permission denied"; then - local remote_url - remote_url=$(cd "$dir" && git config --get remote.origin.url 2>/dev/null) - if [ -n "$remote_url" ] && [ "$remote_url" != "unknown" ]; then - # Use ls-remote which doesn't require write access - git ls-remote "$remote_url" "refs/heads/$branch" 2>/dev/null | awk '{print $1}' - return - fi - fi - - # Try to get the remote hash - cd "$dir" && git rev-parse "origin/$branch" 2>/dev/null - else - echo "not-a-git-repo" - fi -} - -# Function to get current branch -get_current_branch() { - local dir="$1" - if [ -d "$dir/.git" ]; then - cd "$dir" && git rev-parse --abbrev-ref HEAD 2>/dev/null - else - echo "unknown" - fi -} - -# Function to get repository remote URL -get_remote_url() { - local dir="$1" - if [ -d "$dir/.git" ]; then - local url - url=$(cd "$dir" && git config --get remote.origin.url 2>/dev/null) - if [ -n "$url" ]; then - echo "$url" - else - echo "unknown" - fi - else - echo "unknown" - fi -} - -# Function to read local VERSION file -get_local_version() { - local dir="$1" - local version_file="$dir/VERSION" - if [ -f "$version_file" ]; then - cat "$version_file" | tr -d '[:space:]' - else - echo "0.0.0" - fi -} - -# Function to fetch remote VERSION file from GitHub -get_remote_version() { - local repo_owner="$1" - local repo_name="$2" - local branch="${3:-main}" - local url="https://raw.githubusercontent.com/${repo_owner}/${repo_name}/${branch}/VERSION" - - local version - if command -v curl &> /dev/null; then - version=$(curl -fsSL "$url" 2>/dev/null | tr -d '[:space:]') - elif command -v wget &> /dev/null; then - version=$(wget -qO- "$url" 2>/dev/null | tr -d '[:space:]') - fi - - if [ -n "$version" ] && [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then - echo "$version" - else - echo "0.0.0" - fi -} - -# Function to compare semantic versions -# Returns 0 if version1 > version2, 1 otherwise -version_greater_than() { - local version1="$1" - local version2="$2" - - # Handle edge cases - if [ "$version1" = "$version2" ]; then - return 1 - fi - if [ "$version1" = "0.0.0" ]; then - return 1 - fi - if [ "$version2" = "0.0.0" ]; then - return 0 - fi - - # Split versions into arrays - IFS='.' read -ra v1_parts <<< "$version1" - IFS='.' read -ra v2_parts <<< "$version2" - - # Compare each part - for i in 0 1 2; do - local v1_part="${v1_parts[$i]:-0}" - local v2_part="${v2_parts[$i]:-0}" - - # Remove any non-numeric suffix (e.g., -beta) - v1_part="${v1_part%%[!0-9]*}" - v2_part="${v2_part%%[!0-9]*}" - - if [ "$v1_part" -gt "$v2_part" ] 2>/dev/null; then - return 0 - elif [ "$v1_part" -lt "$v2_part" ] 2>/dev/null; then - return 1 - fi - done - - return 1 # Versions are equal -} - -# Function to fetch central configuration -fetch_central_config() { - # Try to fetch the central config file - if command -v curl &> /dev/null; then - curl -fsSL "$CONFIG_URL" -o "$LOCAL_CONFIG_FILE" 2>/dev/null - return $? - elif command -v wget &> /dev/null; then - wget -q -O "$LOCAL_CONFIG_FILE" "$CONFIG_URL" 2>/dev/null - return $? - else - return 1 - fi -} - -# Function to get preferred MCP repository URL from config -get_preferred_mcp_url() { - # Try to fetch latest config - fetch_central_config 2>/dev/null - - # Try to read from local config file - if [ -f "$LOCAL_CONFIG_FILE" ]; then - # Try new JSON structure first (v1.1+) - local preferred_url=$(grep -A 1 '"meticulous-mcp"' "$LOCAL_CONFIG_FILE" | grep '"url"' | sed 's/.*"url"[^"]*"\([^"]*\)".*/\1/') - - # Fallback to old structure (v1.0) - if [ -z "$preferred_url" ] || [ "$preferred_url" = "url" ]; then - preferred_url=$(grep '"mcp_repo_url"' "$LOCAL_CONFIG_FILE" | sed 's/.*"mcp_repo_url"[^"]*"\([^"]*\)".*/\1/') - fi - - if [ -n "$preferred_url" ] && [ "$preferred_url" != "url" ] && [ "$preferred_url" != "mcp_repo_url" ]; then - echo "$preferred_url" - return 0 - fi - fi - - # Fallback to fork URL if config not available - echo "$MCP_FORK_URL" - return 0 -} - -# Function to get preferred Web App repository URL from config -get_preferred_web_url() { - # Try to fetch latest config - fetch_central_config 2>/dev/null - - # Try to read from local config file - if [ -f "$LOCAL_CONFIG_FILE" ]; then - # Extract web app URL from new JSON structure (v1.1+) - local preferred_url=$(grep -A 1 '"meticai-web"' "$LOCAL_CONFIG_FILE" | grep '"url"' | sed 's/.*"url"[^"]*"\([^"]*\)".*/\1/') - - if [ -n "$preferred_url" ] && [ "$preferred_url" != "url" ]; then - echo "$preferred_url" - return 0 - fi - fi - - # Fallback to default URL if config not available - echo "$WEB_APP_URL" - return 0 -} - -# Function to check if MCP repository needs switching -check_and_switch_mcp_repo() { - if [ ! -d "$SCRIPT_DIR/meticulous-source" ]; then - # Not installed yet, will use preferred URL during installation - return 0 - fi - - local current_url=$(get_remote_url "$SCRIPT_DIR/meticulous-source") - local preferred_url=$(get_preferred_mcp_url) - - # Handle case where remote is not configured (unknown) - if [ "$current_url" = "unknown" ]; then - log_warning "MCP repository has no remote configured. Setting up remote..." - if cd "$SCRIPT_DIR/meticulous-source" && git remote add origin "$preferred_url" 2>/dev/null; then - log_success "Remote 'origin' configured to: $preferred_url" - git fetch origin 2>/dev/null || true - elif cd "$SCRIPT_DIR/meticulous-source" && git remote set-url origin "$preferred_url" 2>/dev/null; then - log_success "Remote 'origin' updated to: $preferred_url" - git fetch origin 2>/dev/null || true - fi - cd "$SCRIPT_DIR" - return 0 - fi - - # Normalize URLs for comparison (remove trailing slashes and .git) - local current_normalized=$(echo "$current_url" | sed 's/\.git$//' | sed 's/\/$//') - local preferred_normalized=$(echo "$preferred_url" | sed 's/\.git$//' | sed 's/\/$//') - - if [ "$current_normalized" != "$preferred_normalized" ]; then - log_warning "Repository switch detected!" - echo "Current: $current_url" - echo "Required: $preferred_url" - echo "" - - # Automatically switch in auto mode, or prompt in interactive mode - local should_switch=false - if [ "$AUTO_MODE" = true ]; then - should_switch=true - echo "Auto mode: switching automatically..." - else - read -r -p "Switch to required repository? (y/n) [y]: " SWITCH_CONFIRM /dev/null; then - log_success "Remote 'origin' configured to: $preferred_url" - git fetch origin 2>/dev/null || true - elif cd "$SCRIPT_DIR/meticai-web" && git remote set-url origin "$preferred_url" 2>/dev/null; then - log_success "Remote 'origin' updated to: $preferred_url" - git fetch origin 2>/dev/null || true - fi - cd "$SCRIPT_DIR" - return 0 - fi - - # Normalize URLs for comparison (remove trailing slashes and .git) - local current_normalized=$(echo "$current_url" | sed 's/\.git$//' | sed 's/\/$//') - local preferred_normalized=$(echo "$preferred_url" | sed 's/\.git$//' | sed 's/\/$//') - - if [ "$current_normalized" != "$preferred_normalized" ]; then - log_warning "Web App repository switch detected!" - echo "Current: $current_url" - echo "Required: $preferred_url" - echo "" - - # Automatically switch in auto mode, or prompt in interactive mode - local should_switch=false - if [ "$AUTO_MODE" = true ]; then - should_switch=true - echo "Auto mode: switching automatically..." - else - read -r -p "Switch to required repository? (y/n) [y]: " SWITCH_CONFIRM meticai-web/public/config.json < "$VERSION_FILE" </dev/null; then - log_warning "Warning: You have uncommitted changes. Stashing them..." - git stash push -m "Auto-stash before MeticAI update $(date +%Y-%m-%d_%H:%M:%S)" - local stashed=true - fi - - # Try fast-forward first, then rebase if needed - if git pull --ff-only origin "$current_branch" 2>/dev/null; then - log_success "MeticAI updated successfully" - [ "$stashed" = true ] && git stash pop 2>/dev/null || true - return 0 - fi - - # Fast-forward failed, try rebase - log_warning "Fast-forward not possible, attempting rebase..." - if git pull --rebase origin "$current_branch"; then - log_success "MeticAI updated successfully (with rebase)" - [ "$stashed" = true ] && git stash pop 2>/dev/null || true - return 0 - else - log_error "Failed to update MeticAI" - log_warning "You may have local changes that conflict with remote." - log_warning "Please resolve manually: git pull --rebase origin $current_branch" - [ "$stashed" = true ] && git stash pop 2>/dev/null || true - return 1 - fi -} - -# Function to update or install meticulous-mcp -update_mcp() { - if [ -d "$SCRIPT_DIR/meticulous-source" ]; then - log_warning "Updating Meticulous MCP..." - cd "$SCRIPT_DIR/meticulous-source" - - local current_branch=$(get_current_branch "$SCRIPT_DIR/meticulous-source") - if git pull origin "$current_branch"; then - log_success "Meticulous MCP updated successfully" - return 0 - else - log_error "Failed to update Meticulous MCP" - return 1 - fi - else - log_warning "Installing Meticulous MCP..." - cd "$SCRIPT_DIR" - - # Use preferred URL from central config - local preferred_url=$(get_preferred_mcp_url) - - if git clone "$preferred_url" meticulous-source; then - log_success "Meticulous MCP installed successfully" - return 0 - else - log_error "Failed to install Meticulous MCP" - return 1 - fi - fi -} - -# Function to update or install meticai-web -update_web() { - if [ -d "$SCRIPT_DIR/meticai-web" ]; then - log_warning "Updating MeticAI Web Interface..." - cd "$SCRIPT_DIR/meticai-web" - - local current_branch=$(get_current_branch "$SCRIPT_DIR/meticai-web") - if git pull origin "$current_branch"; then - log_success "MeticAI Web updated successfully" - return 0 - else - log_error "Failed to update MeticAI Web" - return 1 - fi - else - log_warning "Installing MeticAI Web Interface..." - cd "$SCRIPT_DIR" - - # Use preferred URL from central config - local preferred_url=$(get_preferred_web_url) - - if git clone "$preferred_url" meticai-web; then - log_success "MeticAI Web installed successfully" - - # Generate config.json if needed - if [ -f "$SCRIPT_DIR/.env" ]; then - # shellcheck disable=SC1091 - source "$SCRIPT_DIR/.env" - mkdir -p meticai-web/public - # Fix: Remove config.json if it's a directory (both locations) - if [ -d "meticai-web/config.json" ]; then - rm -rf meticai-web/config.json - fi - if [ -d "meticai-web/public/config.json" ]; then - rm -rf meticai-web/public/config.json - fi - cat > meticai-web/public/config.json < "$SCRIPT_DIR/.versions.json" - fi - - # Ensure .rebuild-needed exists as a file for the trigger-update endpoint - if [ -d "$SCRIPT_DIR/.rebuild-needed" ]; then - log_warning "Fixing .rebuild-needed (was directory, converting to file)..." - rm -rf "$SCRIPT_DIR/.rebuild-needed" - fi - if [ ! -f "$SCRIPT_DIR/.rebuild-needed" ]; then - touch "$SCRIPT_DIR/.rebuild-needed" - fi - - # Ensure .update-check-requested exists as a file - if [ -d "$SCRIPT_DIR/.update-check-requested" ]; then - rm -rf "$SCRIPT_DIR/.update-check-requested" - fi - if [ ! -f "$SCRIPT_DIR/.update-check-requested" ]; then - touch "$SCRIPT_DIR/.update-check-requested" - fi - - # Ensure .update-requested exists as a file - if [ -d "$SCRIPT_DIR/.update-requested" ]; then - rm -rf "$SCRIPT_DIR/.update-requested" - fi - if [ ! -f "$SCRIPT_DIR/.update-requested" ]; then - touch "$SCRIPT_DIR/.update-requested" - fi - - # Ensure .restart-requested exists as a file - if [ -d "$SCRIPT_DIR/.restart-requested" ]; then - rm -rf "$SCRIPT_DIR/.restart-requested" - fi - if [ ! -f "$SCRIPT_DIR/.restart-requested" ]; then - touch "$SCRIPT_DIR/.restart-requested" - fi - - # Check if docker compose is available - if docker compose version &> /dev/null; then - COMPOSE_CMD="docker compose" - elif command -v docker-compose &> /dev/null; then - COMPOSE_CMD="docker-compose" - else - log_error "Error: Docker Compose not found" - return 1 - fi - - # Determine if we need sudo (not needed inside containers or if user has docker access) - SUDO_PREFIX="" - if ! docker info &> /dev/null; then - # Docker not accessible, try with sudo - if command -v sudo &> /dev/null && sudo docker info &> /dev/null; then - SUDO_PREFIX="sudo" - else - log_error "Error: Cannot access Docker daemon" - return 1 - fi - fi - - # Check if we're running inside a container (to avoid stopping ourselves) - INSIDE_CONTAINER=false - if [ -f "/.dockerenv" ] || grep -q docker /proc/1/cgroup 2>/dev/null; then - INSIDE_CONTAINER=true - log_warning "Running inside container - will rebuild without stopping self" - fi - - if [ "$INSIDE_CONTAINER" = true ]; then - # When inside container, we can only rebuild containers that don't have - # host filesystem mounts (due to Docker Desktop path restrictions on macOS). - # Use explicit project name to match the host-started containers. - local PROJECT_NAME="meticai" - echo "Rebuilding containers..." - - # Note: gemini-client and meticai-web have volume mounts that reference - # host paths. When running docker compose from inside a container, these - # paths resolve incorrectly. Only meticulous-mcp can be rebuilt this way. - - # Build and restart meticulous-mcp (no problematic mounts) - echo " Building meticulous-mcp..." - if $SUDO_PREFIX $COMPOSE_CMD -p "$PROJECT_NAME" up -d --build --force-recreate --no-deps meticulous-mcp 2>&1; then - log_success " meticulous-mcp rebuilt" - else - log_warning " Warning: Failed to rebuild meticulous-mcp" - fi - - # Create a flag file to signal the host-side rebuild watcher - # The rebuild-watcher.sh script on the host will pick this up - local REBUILD_FLAG="$SCRIPT_DIR/.rebuild-needed" - cat > "$REBUILD_FLAG" </dev/null; then - log_success "Containers stopped" - else - log_warning "Warning: Failed to stop containers (they may not be running)" - fi - - # Pre-create directories so Docker doesn't create them as root - mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs" - - # Final safety check: ensure config.json files are not directories - # This catches cases where a previous failed install left directories behind - if [ -d "$SCRIPT_DIR/meticai-web/config.json" ]; then - rm -rf "$SCRIPT_DIR/meticai-web/config.json" - if [ -f "$SCRIPT_DIR/.env" ]; then - # shellcheck disable=SC1091 - source "$SCRIPT_DIR/.env" - echo '{"serverUrl": "http://'"$PI_IP"':8000"}' > "$SCRIPT_DIR/meticai-web/config.json" - else - echo '{"serverUrl": "http://localhost:8000"}' > "$SCRIPT_DIR/meticai-web/config.json" - fi - fi - if [ -d "$SCRIPT_DIR/meticai-web/public/config.json" ]; then - rm -rf "$SCRIPT_DIR/meticai-web/public/config.json" - if [ -f "$SCRIPT_DIR/.env" ]; then - # shellcheck disable=SC1091 - source "$SCRIPT_DIR/.env" - echo '{"serverUrl": "http://'"$PI_IP"':8000"}' > "$SCRIPT_DIR/meticai-web/public/config.json" - else - echo '{"serverUrl": "http://localhost:8000"}' > "$SCRIPT_DIR/meticai-web/public/config.json" - fi - fi - - # Rebuild and start - echo "Building and starting containers..." - if $SUDO_PREFIX $COMPOSE_CMD up -d --build; then - log_success "Containers rebuilt and started" - - # Fix permissions if we used sudo - if [ -n "$SUDO_PREFIX" ]; then - echo "Fixing file ownership..." - sudo chown -R "$(id -u):$(id -g)" "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs" \ - "$SCRIPT_DIR/.versions.json" "$SCRIPT_DIR/.rebuild-needed" \ - "$SCRIPT_DIR/meticulous-source" "$SCRIPT_DIR/meticai-web" 2>/dev/null || true - fi - - return 0 - else - log_error "Failed to rebuild containers" - return 1 - fi - fi -} - -# Function to update version file -update_version_file() { - cat > "$VERSION_FILE" < "$SCRIPT_DIR/.versions.json" - fi - if [ -d "$SCRIPT_DIR/.rebuild-needed" ]; then - rm -rf "$SCRIPT_DIR/.rebuild-needed" - fi - if [ ! -f "$SCRIPT_DIR/.rebuild-needed" ]; then - touch "$SCRIPT_DIR/.rebuild-needed" - fi - if [ -d "$SCRIPT_DIR/.update-check-requested" ]; then - rm -rf "$SCRIPT_DIR/.update-check-requested" - fi - if [ ! -f "$SCRIPT_DIR/.update-check-requested" ]; then - touch "$SCRIPT_DIR/.update-check-requested" - fi - if [ -d "$SCRIPT_DIR/.update-requested" ]; then - rm -rf "$SCRIPT_DIR/.update-requested" - fi - if [ ! -f "$SCRIPT_DIR/.update-requested" ]; then - touch "$SCRIPT_DIR/.update-requested" - fi - if [ -d "$SCRIPT_DIR/.restart-requested" ]; then - rm -rf "$SCRIPT_DIR/.restart-requested" - fi - if [ ! -f "$SCRIPT_DIR/.restart-requested" ]; then - touch "$SCRIPT_DIR/.restart-requested" - fi - - # Fix common permission issues with git directories - # This can happen when docker or sudo operations create files as root - for subdir in "meticulous-source" "meticai-web"; do - if [ -d "$SCRIPT_DIR/$subdir/.git" ]; then - # Check if current user can write to .git directory - if ! touch "$SCRIPT_DIR/$subdir/.git/.permission-test" 2>/dev/null; then - log_warning "Fixing permissions for $subdir..." - if command -v sudo &>/dev/null; then - sudo chown -R "$(id -u):$(id -g)" "$SCRIPT_DIR/$subdir" 2>/dev/null || true - fi - else - rm -f "$SCRIPT_DIR/$subdir/.git/.permission-test" 2>/dev/null - fi - fi - done - - # Initialize version tracking - initialize_versions_file - - # Handle MCP repository switch if requested (deprecated - now automatic) - if $SWITCH_MCP_REPO; then - log_warning "Note: Repository switching is now automatic based on central configuration." - log_warning "Checking for required repositories..." - echo "" - check_and_switch_mcp_repo - check_and_switch_web_repo - exit $? - fi - - # Check if repositories need switching (automatic based on central config) - check_and_switch_mcp_repo - check_and_switch_web_repo - - # Check for updates - if check_for_updates; then - # Updates are available - always save status to version file - update_version_file - - if $CHECK_ONLY; then - log_warning "Updates are available. Run without --check-only to apply them." - exit 0 - fi - - # Ask user if they want to proceed - if [ "$AUTO_MODE" = false ]; then - echo "" - read -r -p "Do you want to apply these updates? (y/n) [y]: " APPLY_UPDATES /dev/null 2>&1 || true - update_version_file - - if $update_success; then - echo "" - log_success "All updates applied successfully" - echo "" - - # Ask about rebuilding containers - if [ "$AUTO_MODE" = false ]; then - read -r -p "Do you want to rebuild and restart containers now? (y/n) [y]: " REBUILD /dev/null && pwd)" -else - SCRIPT_DIR="$(pwd)" -fi - -# Only source common.sh if it exists (won't exist during curl | bash) -if [ -f "$SCRIPT_DIR/scripts/lib/common.sh" ]; then - source "$SCRIPT_DIR/scripts/lib/common.sh" -else - # Fallback color definitions for when common.sh is not available - GREEN='\033[0;32m' - BLUE='\033[0;34m' - YELLOW='\033[1;33m' - RED='\033[0;31m' - NC='\033[0m' - - log_info() { echo -e "${BLUE}$1${NC}"; } - log_success() { echo -e "${GREEN}✓ $1${NC}"; } - log_error() { echo -e "${RED}✗ $1${NC}" >&2; } - log_warning() { echo -e "${YELLOW}⚠ $1${NC}"; } -fi - -# Configuration -REPO_URL="https://github.com/hessius/MeticAI.git" -INSTALL_DIR="MeticAI" -BRANCH="main" - -log_info "=========================================" -log_info " ☕️ MeticAI Remote Installer 🤖 " -log_info "=========================================" -echo "" - -# Detect if script is being run from a local git repository -# If local-install.sh exists in current directory, we're likely in the repo -if [ -f "./local-install.sh" ] && [ -d "./.git" ]; then - log_warning "Detected local repository installation." - log_warning "Running local-install.sh directly..." - echo "" - exec ./local-install.sh -fi - -# From here on, we're running in remote/web installation mode -log_warning "Remote installation mode detected." -echo "This will clone the MeticAI repository and run the installer." -echo "" - -# Ask user for installation location -log_warning "Where would you like to install MeticAI?" -echo "1) Current directory ($(pwd))" -echo "2) Home directory ($HOME)" -echo "3) Custom path" -read -r -p "Enter your choice (1/2/3) [1]: " LOCATION_CHOICE /dev/null; then - if brew install git; then - log_success "Git installed successfully." - else - log_error "Failed to install git via Homebrew." - exit 1 - fi - else - log_warning "Homebrew not found. Installing Homebrew first..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - - # Source Homebrew environment for both Intel and Apple Silicon Macs - if [ -f /opt/homebrew/bin/brew ]; then - eval "$(/opt/homebrew/bin/brew shellenv)" - elif [ -f /usr/local/bin/brew ]; then - eval "$(/usr/local/bin/brew shellenv)" - fi - - if command -v brew &> /dev/null && brew install git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - echo "Visit: https://git-scm.com/downloads" - exit 1 - fi - fi - ;; - ubuntu|debian|raspbian) - if sudo apt-get update && sudo apt-get install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - ;; - fedora|rhel|centos) - if command -v dnf &> /dev/null; then - if sudo dnf install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - elif command -v yum &> /dev/null; then - if sudo yum install -y git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - else - log_error "No supported package manager found. Please install git manually." - exit 1 - fi - ;; - arch|manjaro) - if sudo pacman -Sy --noconfirm git; then - log_success "Git installed successfully." - else - log_error "Failed to install git. Please install manually." - exit 1 - fi - ;; - *) - log_error "Unsupported OS for automatic installation. Please install git manually." - echo "Visit: https://git-scm.com/downloads" - exit 1 - ;; - esac -} - -# Check for curl (should exist if user ran this script via curl) -if ! command -v curl &> /dev/null; then - log_error "Error: curl is not installed." - echo "curl is required for remote installation." - echo "Please install curl and try again." - exit 1 -fi -log_success "curl found." - -# Check and install git if needed -if ! command -v git &> /dev/null; then - log_error "Error: git is not installed." - read -r -p "Would you like to install git now? (y/n) [y]: " INSTALL_GIT /dev/null)" ]; then - PRESERVED_FILES="${PRESERVED_FILES}data/ (profile history), " - fi - if [ -d "$INSTALL_DIR/logs" ]; then - PRESERVED_FILES="${PRESERVED_FILES}logs/, " - fi - - if [ -n "$PRESERVED_FILES" ]; then - log_success "Found preserved files from previous installation: ${PRESERVED_FILES%,*}" - fi - - read -r -p "Do you want to remove it and clone fresh? (y/n) [y]: " REMOVE_DIR /dev/null; then - log_success "Repository updated." - else - log_warning "Could not update repository. Continuing with existing version." - fi - - # Execute the local installer - if [ -f "./local-install.sh" ]; then - echo "" - log_success "Starting local installer..." - echo "" - exec ./local-install.sh - else - log_error "Error: local-install.sh not found in existing directory." - exit 1 - fi - fi -fi - -# Create parent directory if needed (for custom paths) -PARENT_DIR=$(dirname "$INSTALL_DIR") -if [ ! -d "$PARENT_DIR" ]; then - log_warning "Creating parent directory: $PARENT_DIR" - if mkdir -p "$PARENT_DIR"; then - log_success "Parent directory created." - else - log_error "Error: Failed to create parent directory." - echo "Please check permissions and try again." - exit 1 - fi -fi - -# Clone the repository -log_warning "Cloning MeticAI repository..." -if git clone -b "$BRANCH" "$REPO_URL" "$INSTALL_DIR"; then - log_success "Repository cloned successfully." - - # Checkout the latest stable release instead of main branch - # Use the common.sh version if available, otherwise skip - if [ -f "$INSTALL_DIR/scripts/lib/common.sh" ]; then - source "$INSTALL_DIR/scripts/lib/common.sh" - checkout_latest_release "$INSTALL_DIR" "MeticAI" - fi - - # Restore preserved files from previous installation - if [ -n "${TEMP_PRESERVE_DIR:-}" ] && [ -d "$TEMP_PRESERVE_DIR" ]; then - if [ -f "$TEMP_PRESERVE_DIR/.env" ]; then - cp "$TEMP_PRESERVE_DIR/.env" "$INSTALL_DIR/.env" - log_success "Restored .env file from previous installation" - fi - if [ -d "$TEMP_PRESERVE_DIR/data" ]; then - cp -r "$TEMP_PRESERVE_DIR/data" "$INSTALL_DIR/data" - log_success "Restored data/ directory (profile history) from previous installation" - fi - if [ -d "$TEMP_PRESERVE_DIR/logs" ]; then - cp -r "$TEMP_PRESERVE_DIR/logs" "$INSTALL_DIR/logs" - log_success "Restored logs/ directory from previous installation" - fi - rm -rf "$TEMP_PRESERVE_DIR" - fi -else - log_error "Error: Failed to clone repository." - echo "Please check your internet connection and try again." - # Clean up temp directory if cloning failed - if [ -n "${TEMP_PRESERVE_DIR:-}" ] && [ -d "$TEMP_PRESERVE_DIR" ]; then - rm -rf "$TEMP_PRESERVE_DIR" - fi - exit 1 -fi - -# Change to the cloned directory -cd "$INSTALL_DIR" || exit 1 - -# Verify local-install.sh exists -if [ ! -f "./local-install.sh" ]; then - log_error "Error: local-install.sh not found in cloned repository." - exit 1 -fi - -# Make sure local-install.sh is executable -chmod +x ./local-install.sh -# Also make uninstall.sh executable for later use -chmod +x ./uninstall.sh - -# Execute the local installer -echo "" -log_success "Starting local installer..." -log_info "Note: To uninstall MeticAI later, run './uninstall.sh' from $INSTALL_DIR" -echo "" -# Set environment variable to indicate we're in web install mode -export METICAI_INSTALL_METHOD="web_install.sh" -exec ./local-install.sh