diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f7edf1 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +# Package Manager Conversion Tool for Python LiveKit Agent +# This Makefile helps convert between different Python package managers + +.PHONY: help convert-to-pip convert-to-poetry convert-to-pipenv convert-to-pdm convert-to-hatch convert-to-uv rollback list-backups clean-backups + +# Default target shows help +help: + @echo "Package Manager Conversion Tool - Python" + @echo "=========================================" + @echo "" + @echo "⚠️ WARNING: Converting will reset Dockerfiles to LiveKit templates" + @echo " Any custom Dockerfile modifications will be lost!" + @echo "" + @echo "Available conversion targets:" + @echo " make convert-to-pip - Convert to pip (requirements.txt)" + @echo " make convert-to-poetry - Convert to Poetry" + @echo " make convert-to-pipenv - Convert to Pipenv" + @echo " make convert-to-pdm - Convert to PDM" + @echo " make convert-to-hatch - Convert to Hatch" + @echo " make convert-to-uv - Convert to UV" + @echo "" + @echo "Backup management:" + @echo " make rollback - Restore from backup (interactive if multiple)" + @echo " make list-backups - Show available backups" + @echo " make clean-backups - Remove all backup directories" + @echo "" + @echo "Notes:" + @echo " • Backups are saved as .backup.{package-manager}" + @echo " • Multiple conversions create multiple backups" + @echo " • Rollback is interactive when multiple backups exist" + @echo " • Lock files are NOT generated automatically - see instructions after conversion" + +convert-to-pip: + @bash scripts/convert-package-manager.sh pip + +convert-to-poetry: + @bash scripts/convert-package-manager.sh poetry + +convert-to-pipenv: + @bash scripts/convert-package-manager.sh pipenv + +convert-to-pdm: + @bash scripts/convert-package-manager.sh pdm + +convert-to-hatch: + @bash scripts/convert-package-manager.sh hatch + +convert-to-uv: + @bash scripts/convert-package-manager.sh uv + +rollback: + @bash scripts/rollback.sh $(PM) + +list-backups: + @echo "Available backups:" + @for dir in .backup.*; do \ + if [ -d "$$dir" ]; then \ + echo " $$dir"; \ + fi; \ + done 2>/dev/null || echo " No backups found" + +clean-backups: + @echo "Removing all backup directories..." + @rm -rf .backup.* 2>/dev/null || true + @echo "✔ All backups removed" \ No newline at end of file diff --git a/README.md b/README.md index 7e40a20..9424c59 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,78 @@ This project includes a complete suite of evals, based on the LiveKit Agents [te uv run pytest ``` +## Package Manager Conversion + +This project includes a tool to convert between different Python package managers. The default is UV for fast, modern dependency management, but you can switch to your preferred package manager. + +### Supported Package Managers +- **UV** (default) - Fast, modern Python package manager +- **pip** - Standard Python package manager (generates `requirements.txt`) +- **Poetry** - Dependency management with lock files +- **Pipenv** - Python packaging tool with virtual environments +- **PDM** - Modern Python package manager +- **Hatch** - Modern, extensible Python project manager + +### Converting to a Different Package Manager + +```bash +# Show available options +make help + +# Convert to pip +make convert-to-pip + +# Convert to Poetry +make convert-to-poetry + +# Convert to Pipenv +make convert-to-pipenv + +# Convert to PDM +make convert-to-pdm + +# Convert to Hatch +make convert-to-hatch + +# Rollback to previous package manager (interactive) +make rollback + +# Rollback to a specific package manager backup +make rollback PM=poetry +make rollback PM=uv +# Or use the script directly: +./scripts/rollback.sh poetry +``` + +**⚠️ Important Notes:** +- Converting will download the LiveKit Dockerfile templates and reset any custom Dockerfile modifications +- Your original files are backed up to `.backup.{package-manager}/` +- Lock files are NOT generated automatically - follow the instructions after conversion +- Multiple conversions create multiple backups +- Rollback supports both interactive mode (shows menu) and direct mode (specify package manager) + +## Building and Testing with Docker + +To test your agent in a production-like environment, build and run the Docker container locally: + +```bash +# Build the Docker image +docker build -t my-agent . + +# Run the container with environment variables (LiveKit variables are required, others as needed) +docker run --rm \ + -e LIVEKIT_URL=your-url \ + -e LIVEKIT_API_KEY=your-key \ + -e LIVEKIT_API_SECRET=your-secret \ + -e OPENAI_API_KEY=your-key \ + -e DEEPGRAM_API_KEY=your-key \ + -e CARTESIA_API_KEY=your-key \ + my-agent + +# Or use an env file +docker run --rm --env-file .env.local my-agent +``` + ## Using this template repo for your own project Once you've started your own project based on this repo, you should: diff --git a/scripts/convert-package-manager.sh b/scripts/convert-package-manager.sh new file mode 100755 index 0000000..77c95f0 --- /dev/null +++ b/scripts/convert-package-manager.sh @@ -0,0 +1,799 @@ +#!/bin/bash + +set -e + +# Configuration +GITHUB_BASE_URL="https://raw.githubusercontent.com/livekit/livekit-cli/refs/heads/main/pkg/agentfs/examples/" +PROGRAM_MAIN="src/agent.py" + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Source the parsing functions (includes detect_current_pm) +source "$(dirname "$0")/parse-dependencies.sh" + +# Get the target package manager from command line +TARGET_PM="$1" + +if [ -z "$TARGET_PM" ]; then + echo -e "${RED}Error: No package manager specified${NC}" + echo "Usage: $0 {pip|poetry|pipenv|pdm|hatch|uv}" + exit 1 +fi + +# Detect current package manager +CURRENT_PM=$(detect_current_pm) + +echo -e "${GREEN}✔${NC} Detected current package manager: ${YELLOW}$CURRENT_PM${NC}" + +# Create backup directory +BACKUP_DIR=".backup.$CURRENT_PM" +if [ "$CURRENT_PM" = "unknown" ]; then + BACKUP_DIR=".backup.original" +fi + +echo " Creating backup: $BACKUP_DIR/" + +# Extract extra tool sections BEFORE moving files +# This ensures we capture them from pyproject.toml if it exists +extra_sections="" +if [ -f "pyproject.toml" ]; then + extra_sections=$(extract_extra_pyproject_sections) +fi + +# Create backup +mkdir -p "$BACKUP_DIR" + +# Move most files to backup, but keep the source dependency file for reading +# We'll clean it up after conversion if needed +[ -f "Dockerfile" ] && mv "Dockerfile" "$BACKUP_DIR/" +[ -f ".dockerignore" ] && mv ".dockerignore" "$BACKUP_DIR/" + +# Handle dependency files based on current package manager +case "$CURRENT_PM" in + pip) + # Keep requirements.txt for reading, move everything else + [ -f "requirements.txt" ] && cp "requirements.txt" "$BACKUP_DIR/" + [ -f "requirements-dev.txt" ] && mv "requirements-dev.txt" "$BACKUP_DIR/" + [ -f "pyproject.toml" ] && mv "pyproject.toml" "$BACKUP_DIR/" + [ -f "Pipfile" ] && mv "Pipfile" "$BACKUP_DIR/" + ;; + pipenv) + # Keep Pipfile for reading, move everything else + [ -f "Pipfile" ] && cp "Pipfile" "$BACKUP_DIR/" + [ -f "Pipfile.lock" ] && mv "Pipfile.lock" "$BACKUP_DIR/" + [ -f "requirements.txt" ] && mv "requirements.txt" "$BACKUP_DIR/" + [ -f "pyproject.toml" ] && mv "pyproject.toml" "$BACKUP_DIR/" + ;; + poetry|pdm|hatch|uv|pyproject) + # Keep pyproject.toml for reading, move everything else + [ -f "pyproject.toml" ] && cp "pyproject.toml" "$BACKUP_DIR/" + [ -f "requirements.txt" ] && mv "requirements.txt" "$BACKUP_DIR/" + [ -f "requirements-dev.txt" ] && mv "requirements-dev.txt" "$BACKUP_DIR/" + [ -f "Pipfile" ] && mv "Pipfile" "$BACKUP_DIR/" + [ -f "Pipfile.lock" ] && mv "Pipfile.lock" "$BACKUP_DIR/" + ;; + *) + # Unknown, just copy everything to be safe + [ -f "pyproject.toml" ] && cp "pyproject.toml" "$BACKUP_DIR/" + [ -f "requirements.txt" ] && cp "requirements.txt" "$BACKUP_DIR/" + [ -f "Pipfile" ] && cp "Pipfile" "$BACKUP_DIR/" + ;; +esac + +# Always move lock files - they're never needed for reading +[ -f "poetry.lock" ] && mv "poetry.lock" "$BACKUP_DIR/" +[ -f "pdm.lock" ] && mv "pdm.lock" "$BACKUP_DIR/" +[ -f "uv.lock" ] && mv "uv.lock" "$BACKUP_DIR/" +[ -f "Pipfile.lock" ] && mv "Pipfile.lock" "$BACKUP_DIR/" + +echo "" +echo -e "${GREEN}✔${NC} Fetching $TARGET_PM templates from GitHub" + +# Download Dockerfile and dockerignore +DOCKERFILE_URL="$GITHUB_BASE_URL/python.$TARGET_PM.Dockerfile" +DOCKERIGNORE_URL="$GITHUB_BASE_URL/python.$TARGET_PM.dockerignore" + +curl -sL "$DOCKERFILE_URL" -o Dockerfile.tmp +if [ $? -ne 0 ]; then + echo -e "${RED}Error: Failed to download Dockerfile${NC}" + exit 1 +fi + +curl -sL "$DOCKERIGNORE_URL" -o .dockerignore +if [ $? -ne 0 ]; then + echo -e "${RED}Error: Failed to download .dockerignore${NC}" + exit 1 +fi + +# Replace template variable in Dockerfile +sed "s|{{\.ProgramMain}}|$PROGRAM_MAIN|g" Dockerfile.tmp > Dockerfile +rm Dockerfile.tmp + +echo " Downloaded: Dockerfile (from LiveKit template)" +echo " Downloaded: .dockerignore (from LiveKit template)" +echo "" +echo -e "${YELLOW}⚠️ Note: Dockerfile has been reset to LiveKit template version${NC}" +echo " Any custom modifications have been backed up" + +# Generate package manager specific files +echo "" +echo -e "${GREEN}✔${NC} Generating $TARGET_PM configuration" + +case "$TARGET_PM" in + pip) + # Generate requirements.txt from current project + # Get main dependencies + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + if [ ! -z "$main_deps" ]; then + # Process each dependency to fix version specifiers for requirements.txt format + while IFS= read -r dep; do + # Fix Poetry-style version specifiers + if [[ "$dep" == *"~"[0-9]* ]]; then + # Convert ~1.2 to ~=1.2 + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + # Convert ^1.2 to ~=1.2 + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + # Remove trailing * for any version + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + echo "$dep" + done <<< "$main_deps" > requirements.txt + + # Add dev dependencies as comments + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + if [ ! -z "$dev_deps" ]; then + echo "" >> requirements.txt + echo "# Development dependencies:" >> requirements.txt + while IFS= read -r dep; do + # Fix version specifiers for dev deps too + if [[ "$dep" == *"~"[0-9]* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + echo "# $dep" >> requirements.txt + done <<< "$dev_deps" + fi + + echo " Generated: requirements.txt" + + # Preserve tool configurations in a minimal pyproject.toml + # (using pre-extracted sections from before files were moved) + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + # Create minimal pyproject.toml with just tool configs + echo "[tool]" > pyproject.toml + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + echo " Preserved: pyproject.toml (tool configurations)" + else + # Clean up pyproject.toml if no tool configs to preserve + rm -f pyproject.toml + fi + else + echo -e "${YELLOW}Warning: No dependencies found to convert${NC}" + fi + ;; + + poetry) + # Parse dependencies BEFORE overwriting the file! + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + + # Generate pyproject.toml for Poetry + cat > pyproject.toml << 'EOF' +[tool.poetry] +name = "livekit-agent" +version = "0.1.0" +description = "LiveKit Agent" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.9" +EOF + + # Add main dependencies + if [ -n "$main_deps" ]; then + while IFS= read -r dep; do + # Handle dependencies with extras + if [[ "$dep" == *"["*"]"* ]]; then + pkg_name=$(echo "$dep" | sed 's/\[.*//') + extras=$(echo "$dep" | sed 's/.*\[\(.*\)\].*/\1/') + version=$(echo "$dep" | grep -oE '(~=|>=|<=|==|>|<)[0-9.]+' || echo "*") + + # Format for Poetry in pyproject.toml + if [ "$version" != "*" ]; then + # Convert ~= to ^ for Poetry + if [[ "$version" == "~="* ]]; then + version="^$(echo "$version" | sed 's/~=//')" + fi + # Format extras with proper quoting for Poetry + formatted_extras=$(echo "$extras" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed 's/.*/"&"/' | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') + echo "$pkg_name = {extras = [$formatted_extras], version = \"$version\"}" >> pyproject.toml + else + formatted_extras=$(echo "$extras" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed 's/.*/"&"/' | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') + echo "$pkg_name = {extras = [$formatted_extras]}" >> pyproject.toml + fi + else + # Regular dependencies + if [[ "$dep" =~ (~=?|>=?|<=?|==?|!=|\^)[0-9.] ]] || [[ "$dep" == *"*"* ]]; then + pkg_name=$(echo "$dep" | sed -E 's/(~=?|>=?|<=?|==?|!=|\^|\*).*//') + version=$(echo "$dep" | sed -E "s/^${pkg_name}//") + + # Convert version specifiers for Poetry + if [[ "$version" == "~="* ]]; then + version="^$(echo "$version" | sed 's/~=//')" + elif [[ "$version" == "~"* ]]; then + version="^$(echo "$version" | sed 's/~//')" + fi + + if [[ "$version" == "*" ]] || [ -z "$version" ]; then + echo "$pkg_name = \"*\"" >> pyproject.toml + else + echo "$pkg_name = \"$version\"" >> pyproject.toml + fi + else + echo "$dep = \"*\"" >> pyproject.toml + fi + fi + done <<< "$main_deps" + fi + + # Add dev dependencies (already parsed above) + echo "" >> pyproject.toml + echo "[tool.poetry.group.dev.dependencies]" >> pyproject.toml + if [ -n "$dev_deps" ]; then + while IFS= read -r dep; do + if [[ "$dep" =~ (~=?|>=?|<=?|==?|!=|\^)[0-9.] ]] || [[ "$dep" == *"*"* ]]; then + pkg_name=$(echo "$dep" | sed -E 's/(~=?|>=?|<=?|==?|!=|\^|\*).*//') + version=$(echo "$dep" | sed -E "s/^${pkg_name}//") + + if [[ "$version" == "~="* ]]; then + version="^$(echo "$version" | sed 's/~=//')" + elif [[ "$version" == "~"* ]]; then + version="^$(echo "$version" | sed 's/~//')" + fi + + if [[ "$version" == "*" ]] || [ -z "$version" ]; then + echo "$pkg_name = \"*\"" >> pyproject.toml + else + echo "$pkg_name = \"$version\"" >> pyproject.toml + fi + else + echo "$dep = \"*\"" >> pyproject.toml + fi + done <<< "$dev_deps" + fi + + # Add build system + echo "" >> pyproject.toml + echo "[build-system]" >> pyproject.toml + echo 'requires = ["poetry-core"]' >> pyproject.toml + echo 'build-backend = "poetry.core.masonry.api"' >> pyproject.toml + + # Add preserved extra tool sections + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + fi + + echo " Generated: pyproject.toml (Poetry format)" + ;; + + pipenv) + # Parse dependencies BEFORE overwriting the file! + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + + # Generate Pipfile from current project + # Start building the Pipfile + cat > Pipfile << 'EOF' +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +EOF + + # Add main dependencies (already parsed above) + if [ -n "$main_deps" ]; then + while IFS= read -r dep; do + # Handle dependencies with extras (e.g., livekit-agents[openai,silero]) + if [[ "$dep" == *"["*"]"* ]]; then + # Extract package name and extras + pkg_name=$(echo "$dep" | sed 's/\[.*//') + extras=$(echo "$dep" | sed 's/.*\[\(.*\)\].*/\1/') + # Look for version after the closing bracket - handle both ~= and ~ formats and ^ + version=$(echo "$dep" | grep -oE '(\^|~=?|>=|<=|==|>|<)[0-9.]+' || echo "*") + + if [ "$version" != "*" ]; then + # Handle Poetry's ~ format (convert to ~= for Pipfile) + if [[ "$version" == "~"* ]] && [[ "$version" != "~="* ]]; then + version=$(echo "$version" | sed 's/^~/~=/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$version" == "^"* ]]; then + version=$(echo "$version" | sed 's/^\^/~=/') + fi + # Format extras properly: split by comma, trim spaces, quote each + formatted_extras=$(echo "$extras" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed 's/.*/"&"/' | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') + echo "$pkg_name = {extras = [$formatted_extras], version = \"$version\"}" >> Pipfile + else + formatted_extras=$(echo "$extras" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed 's/.*/"&"/' | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') + echo "$pkg_name = {extras = [$formatted_extras]}" >> Pipfile + fi + else + # Handle regular dependencies + # Check for version specifiers (with or without =) + if [[ "$dep" =~ (~=?|>=?|<=?|==?|!=|\^)[0-9.] ]] || [[ "$dep" == *"*"* ]]; then + # Extract package name (everything before version specifier) + pkg_name=$(echo "$dep" | sed -E 's/(~=?|>=?|<=?|==?|!=|\^|\*).*//') + # Extract version (everything after package name) + version=$(echo "$dep" | sed -E "s/^${pkg_name}//") + + # Normalize version specifiers for Pipfile format + if [[ "$version" == "~"* ]] && [[ "$version" != "~="* ]]; then + # Convert ~1.2 to ~=1.2 + version=$(echo "$version" | sed 's/^~/~=/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$version" == "^"* ]]; then + version=$(echo "$version" | sed 's/^\^/~=/') + fi + + # Ensure no double equals (fix ~==) + version=$(echo "$version" | sed 's/~==/~=/') + + if [[ "$version" == "*" ]] || [ -z "$version" ]; then + echo "$pkg_name = \"*\"" >> Pipfile + else + echo "$pkg_name = \"$version\"" >> Pipfile + fi + else + # No version specified + echo "$dep = \"*\"" >> Pipfile + fi + fi + done <<< "$main_deps" + fi + + # Add dev dependencies (already parsed above) + echo "" >> Pipfile + echo "[dev-packages]" >> Pipfile + if [ -n "$dev_deps" ]; then + while IFS= read -r dep; do + # Check for version specifiers (with or without =) + if [[ "$dep" =~ (~=?|>=?|<=?|==?|!=|\^)[0-9.] ]] || [[ "$dep" == *"*"* ]]; then + # Extract package name (everything before version specifier) + pkg_name=$(echo "$dep" | sed -E 's/(~=?|>=?|<=?|==?|!=|\^|\*).*//') + # Extract version (everything after package name) + version=$(echo "$dep" | sed -E "s/^${pkg_name}//") + + # Normalize version specifiers for Pipfile format + if [[ "$version" == "~"* ]] && [[ "$version" != "~="* ]]; then + # Convert ~1.2 to ~=1.2 + version=$(echo "$version" | sed 's/^~/~=/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$version" == "^"* ]]; then + version=$(echo "$version" | sed 's/^\^/~=/') + fi + + # Ensure no double equals (fix ~==) + version=$(echo "$version" | sed 's/~==/~=/') + + if [[ "$version" == "*" ]] || [ -z "$version" ]; then + echo "$pkg_name = \"*\"" >> Pipfile + else + echo "$pkg_name = \"$version\"" >> Pipfile + fi + else + # No version specified + echo "$dep = \"*\"" >> Pipfile + fi + done <<< "$dev_deps" + fi + + # Add Python version requirement + # For now, use a sensible default - could be enhanced to detect from current Python + python_version="3.9" + echo "" >> Pipfile + echo "[requires]" >> Pipfile + echo "python_version = \"$python_version\"" >> Pipfile + + echo " Generated: Pipfile" + + # Preserve tool configurations in a minimal pyproject.toml + # (using pre-extracted sections from before files were moved) + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + # Create minimal pyproject.toml with just tool configs + echo "[tool]" > pyproject.toml + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + echo " Preserved: pyproject.toml (tool configurations)" + else + # Clean up pyproject.toml if no tool configs to preserve + rm -f pyproject.toml + fi + ;; + + pdm) + # Parse dependencies BEFORE overwriting the file! + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + + # Generate pyproject.toml for PDM + cat > pyproject.toml << 'EOF' +[project] +name = "livekit-agent" +version = "0.1.0" +description = "LiveKit Agent" +requires-python = ">=3.9" +dependencies = [ +EOF + + # Add main dependencies + if [ -n "$main_deps" ]; then + first=true + while IFS= read -r dep; do + # Clean up the dependency format + # Convert Poetry's ~1.2 to ~=1.2 for PDM + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + # Remove trailing * for any version + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$main_deps" + echo "" >> pyproject.toml + fi + + echo "]" >> pyproject.toml + echo "" >> pyproject.toml + echo "[tool.pdm]" >> pyproject.toml + echo "distribution = false" >> pyproject.toml + + # Add dev dependencies (already parsed above) + if [ -n "$dev_deps" ]; then + echo "" >> pyproject.toml + echo "[tool.pdm.dev-dependencies]" >> pyproject.toml + echo "dev = [" >> pyproject.toml + first=true + while IFS= read -r dep; do + # Clean up the dependency format + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$dev_deps" + echo "" >> pyproject.toml + echo "]" >> pyproject.toml + fi + + # Add build system + echo "" >> pyproject.toml + echo "[build-system]" >> pyproject.toml + echo 'requires = ["pdm-backend"]' >> pyproject.toml + echo 'build-backend = "pdm.backend"' >> pyproject.toml + + # Add preserved extra tool sections + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + fi + + echo " Generated: pyproject.toml (PDM format)" + ;; + + hatch) + # Parse dependencies BEFORE overwriting the file! + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + + # Generate pyproject.toml for Hatch + cat > pyproject.toml << 'EOF' +[project] +name = "livekit-agent" +version = "0.1.0" +description = "LiveKit Agent" +requires-python = ">=3.9" +dependencies = [ +EOF + + # Add main dependencies + if [ -n "$main_deps" ]; then + first=true + while IFS= read -r dep; do + # Clean up the dependency format + # Convert Poetry's ~1.2 to ~=1.2 for Hatch + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + # Remove trailing * for any version + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$main_deps" + echo "" >> pyproject.toml + fi + + echo "]" >> pyproject.toml + echo "" >> pyproject.toml + echo "[tool.hatch]" >> pyproject.toml + echo 'build.targets.wheel.packages = ["src"]' >> pyproject.toml + + # Add dev dependencies (already parsed above) + if [ -n "$dev_deps" ]; then + echo "" >> pyproject.toml + echo "[tool.hatch.envs.default]" >> pyproject.toml + echo "dependencies = [" >> pyproject.toml + first=true + while IFS= read -r dep; do + # Clean up the dependency format + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$dev_deps" + echo "" >> pyproject.toml + echo "]" >> pyproject.toml + fi + + # Add build system + echo "" >> pyproject.toml + echo "[build-system]" >> pyproject.toml + echo 'requires = ["hatchling"]' >> pyproject.toml + echo 'build-backend = "hatchling.build"' >> pyproject.toml + + # Add preserved extra tool sections + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + fi + + echo " Generated: pyproject.toml (Hatch format)" + ;; + + uv) + # Parse dependencies BEFORE overwriting the file! + main_deps=$(parse_dependencies "main" "$CURRENT_PM") + dev_deps=$(parse_dependencies "dev" "$CURRENT_PM") + + # Generate pyproject.toml for UV + cat > pyproject.toml << 'EOF' +[project] +name = "livekit-agent" +version = "0.1.0" +description = "LiveKit Agent" +requires-python = ">=3.9" +dependencies = [ +EOF + + # Add main dependencies + if [ -n "$main_deps" ]; then + first=true + while IFS= read -r dep; do + # Clean up the dependency format + # Convert Poetry's ~1.2 to ~=1.2 for UV + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + # Remove trailing * for any version + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$main_deps" + echo "" >> pyproject.toml + fi + + echo "]" >> pyproject.toml + + # Add dev dependencies (already parsed above) + if [ -n "$dev_deps" ]; then + echo "" >> pyproject.toml + echo "[dependency-groups]" >> pyproject.toml + echo "dev = [" >> pyproject.toml + first=true + while IFS= read -r dep; do + # Clean up the dependency format + if [[ "$dep" == *"~"[0-9]* ]] && [[ "$dep" != *"~="* ]]; then + dep=$(echo "$dep" | sed 's/\(~\)\([0-9]\)/~=\2/') + fi + # Convert Poetry's caret operator ^ to ~= + if [[ "$dep" == *"^"* ]]; then + dep=$(echo "$dep" | sed 's/\^/~=/') + fi + if [[ "$dep" == *"*" ]]; then + dep=$(echo "$dep" | sed 's/\*$//') + fi + + if [ "$first" = true ]; then + echo -n " \"$dep\"" >> pyproject.toml + first=false + else + echo "," >> pyproject.toml + echo -n " \"$dep\"" >> pyproject.toml + fi + done <<< "$dev_deps" + echo "" >> pyproject.toml + echo "]" >> pyproject.toml + fi + + # Add UV-specific configuration + echo "" >> pyproject.toml + echo "[tool.uv]" >> pyproject.toml + echo "package = false" >> pyproject.toml + + # Add preserved extra tool sections + if [ -n "$extra_sections" ] && [ "$extra_sections" != "{}" ]; then + format_preserved_toml_sections "$extra_sections" >> pyproject.toml + fi + + echo " Generated: pyproject.toml (UV format)" + ;; +esac + +echo " Entry point: $PROGRAM_MAIN" + +# Display instructions based on package manager +echo "" +echo "Next steps:" +echo " › Install $TARGET_PM:" + +case "$TARGET_PM" in + pip) + echo " # pip is usually pre-installed with Python" + echo "" + echo " › Install dependencies:" + echo " pip install -r requirements.txt" + echo "" + echo " › For reproducible builds, generate lock file:" + echo " pip freeze > requirements.lock" + ;; + poetry) + echo " curl -sSL https://install.python-poetry.org | python3 -" + echo "" + echo " › Generate lock file:" + echo " poetry lock" + echo "" + echo " › Install dependencies:" + echo " poetry install" + ;; + pipenv) + echo " pip install pipenv" + echo "" + echo " › Generate lock file:" + echo " pipenv lock" + echo "" + echo " › Install dependencies:" + echo " pipenv install" + ;; + pdm) + echo " pip install pdm" + echo "" + echo " › Generate lock file:" + echo " pdm lock" + echo "" + echo " › Install dependencies:" + echo " pdm install" + ;; + hatch) + echo " pip install hatch" + echo "" + echo " › Create environment:" + echo " hatch env create" + echo "" + echo " › Install dependencies:" + echo " hatch env run pip install -e ." + ;; + uv) + echo " curl -LsSf https://astral.sh/uv/install.sh | sh" + echo "" + echo " › Generate lock file:" + echo " uv lock" + echo "" + echo " › Install dependencies:" + echo " uv sync" + ;; +esac + +# Clean up source dependency files that are no longer needed +case "$TARGET_PM" in + pip|pipenv) + # These use requirements.txt/Pipfile, don't need the source pyproject.toml + # (unless it has tool configs, which we've already preserved above) + ;; + poetry|pdm|hatch|uv) + # These use pyproject.toml, clean up old pip/pipenv files + rm -f requirements.txt requirements-dev.txt Pipfile + ;; +esac + +echo "" +echo " › Test locally:" +echo " python $PROGRAM_MAIN dev" +echo "" +echo " › Build Docker image:" +echo " docker build -t my-agent ." +echo "" +echo "To rollback: make rollback" + +# List existing backups +BACKUP_COUNT=$(ls -d .backup.* 2>/dev/null | wc -l) +if [ $BACKUP_COUNT -gt 0 ]; then + echo "Existing backups: $(ls -d .backup.* | tr '\n' ' ')" +fi \ No newline at end of file diff --git a/scripts/detect-package-manager.sh b/scripts/detect-package-manager.sh new file mode 100755 index 0000000..aa6d8f3 --- /dev/null +++ b/scripts/detect-package-manager.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Detect the current Python package manager based on existing files + +detect_current_pm() { + # Check for various package manager files in order of specificity + + # UV (check for uv.lock, tool.uv, or dependency-groups in pyproject.toml) + if [ -f "uv.lock" ]; then + echo "uv" + return + fi + + # UV also uses [dependency-groups] or [tool.uv] in pyproject.toml + if [ -f "pyproject.toml" ]; then + if grep -q "\[tool\.uv\]" pyproject.toml 2>/dev/null || grep -q "\[dependency-groups\]" pyproject.toml 2>/dev/null; then + echo "uv" + return + fi + fi + + # Poetry (check for poetry.lock or poetry sections in pyproject.toml) + if [ -f "poetry.lock" ] || ([ -f "pyproject.toml" ] && grep -q "\[tool\.poetry\]" pyproject.toml 2>/dev/null); then + echo "poetry" + return + fi + + # PDM (check for pdm.lock or pdm sections in pyproject.toml) + if [ -f "pdm.lock" ] || ([ -f "pyproject.toml" ] && grep -q "\[tool\.pdm\]" pyproject.toml 2>/dev/null); then + echo "pdm" + return + fi + + # Hatch (check for hatch sections in pyproject.toml) + if [ -f "pyproject.toml" ] && grep -q "\[tool\.hatch\]" pyproject.toml 2>/dev/null; then + echo "hatch" + return + fi + + # Pipenv (check for Pipfile) + if [ -f "Pipfile" ]; then + echo "pipenv" + return + fi + + # Pip (check for requirements.txt) + if [ -f "requirements.txt" ]; then + echo "pip" + return + fi + + # Default to unknown if we have pyproject.toml but can't identify the tool + if [ -f "pyproject.toml" ]; then + echo "pyproject" + return + fi + + echo "unknown" +} + +# If script is executed directly, print the detected package manager +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + detect_current_pm +fi \ No newline at end of file diff --git a/scripts/parse-dependencies.sh b/scripts/parse-dependencies.sh new file mode 100644 index 0000000..4bfef33 --- /dev/null +++ b/scripts/parse-dependencies.sh @@ -0,0 +1,338 @@ +#!/bin/bash + +# Parsing functions for Python dependency files +# Can be sourced by conversion scripts or used standalone for testing + +# Source the detect script for package manager detection +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/detect-package-manager.sh" + +# Function to parse dependencies from pyproject.toml +parse_pyproject_dependencies() { + local dep_type="$1" # "main" or "dev" + + if [ ! -f "pyproject.toml" ]; then + return + fi + + python3 -c " +import sys +try: + import tomllib +except ImportError: + try: + import tomli as tomllib + except ImportError: + sys.exit(1) + +with open('pyproject.toml', 'rb') as f: + data = tomllib.load(f) + +dep_type = '$dep_type' + +# Check for different dependency locations based on the tool +dependencies = [] + +if dep_type == 'main': + # Standard location + if 'project' in data and 'dependencies' in data['project']: + dependencies.extend(data['project']['dependencies']) + # Poetry + if 'tool' in data and 'poetry' in data['tool'] and 'dependencies' in data['tool']['poetry']: + deps = data['tool']['poetry']['dependencies'] + for pkg, ver in deps.items(): + if pkg != 'python': + if isinstance(ver, dict): + if 'extras' in ver: + extras = ','.join(ver['extras']) + version = ver.get('version', '*') + dependencies.append(f'{pkg}[{extras}]{version}') + else: + version = ver.get('version', '*') + dependencies.append(f'{pkg}{version}') + else: + dependencies.append(f'{pkg}{ver}') + # PDM + if 'tool' in data and 'pdm' in data['tool'] and 'dependencies' in data['tool']['pdm']: + dependencies.extend(data['tool']['pdm']['dependencies']) +elif dep_type == 'dev': + # Standard location + if 'project' in data and 'optional-dependencies' in data['project']: + for group_deps in data['project']['optional-dependencies'].values(): + dependencies.extend(group_deps) + # Poetry + if 'tool' in data and 'poetry' in data['tool']: + poetry = data['tool']['poetry'] + if 'group' in poetry: + for group in poetry['group'].values(): + if 'dependencies' in group: + for pkg, ver in group['dependencies'].items(): + if isinstance(ver, dict): + version = ver.get('version', '*') + dependencies.append(f'{pkg}{version}') + else: + dependencies.append(f'{pkg}{ver}') + # Legacy dev-dependencies + if 'dev-dependencies' in poetry: + for pkg, ver in poetry['dev-dependencies'].items(): + if isinstance(ver, dict): + version = ver.get('version', '*') + dependencies.append(f'{pkg}{version}') + else: + dependencies.append(f'{pkg}{ver}') + # PDM + if 'tool' in data and 'pdm' in data['tool'] and 'dev-dependencies' in data['tool']['pdm']: + for group_deps in data['tool']['pdm']['dev-dependencies'].values(): + dependencies.extend(group_deps) + # Hatch + if 'tool' in data and 'hatch' in data['tool'] and 'envs' in data['tool']['hatch']: + if 'default' in data['tool']['hatch']['envs'] and 'dependencies' in data['tool']['hatch']['envs']['default']: + dependencies.extend(data['tool']['hatch']['envs']['default']['dependencies']) + # UV + if 'dependency-groups' in data: + for group_deps in data['dependency-groups'].values(): + dependencies.extend(group_deps) + +for dep in dependencies: + print(dep) +" 2>/dev/null +} + +# Function to parse dependencies from requirements.txt files +parse_requirements_dependencies() { + local dep_type="$1" + + if [ "$dep_type" = "main" ]; then + if [ -f "requirements.txt" ]; then + grep -v '^#' requirements.txt 2>/dev/null | grep -v '^$' || true + fi + elif [ "$dep_type" = "dev" ]; then + for dev_file in "requirements-dev.txt" "requirements.dev.txt" "dev-requirements.txt"; do + if [ -f "$dev_file" ]; then + grep -v '^#' "$dev_file" 2>/dev/null | grep -v '^$' || true + return + fi + done + fi +} + +# Function to parse dependencies from Pipfile +parse_pipfile_dependencies() { + local dep_type="$1" + + if [ ! -f "Pipfile" ]; then + return + fi + + python3 -c " +import re + +with open('Pipfile', 'r') as f: + content = f.read() + +dep_type = '$dep_type' +section = '[packages]' if dep_type == 'main' else '[dev-packages]' + +# Find the section +pattern = re.escape(section) + r'(.*?)(?:\n\[|\Z)' +match = re.search(pattern, content, re.DOTALL) +if not match: + exit(0) + +section_content = match.group(1) + +# Parse dependencies +for line in section_content.strip().split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + + # Parse different formats + if '=' in line: + parts = line.split('=', 1) + pkg_name = parts[0].strip() + version_part = parts[1].strip() + + # Handle extras format: {extras = [...], version = ...} + if '{' in version_part: + extras_match = re.search(r'extras\s*=\s*\[(.*?)\]', version_part) + version_match = re.search(r'version\s*=\s*\"(.*?)\"', version_part) + + if extras_match: + extras = re.sub(r'[\"\\']', '', extras_match.group(1)) + extras = ','.join([e.strip() for e in extras.split(',')]) + if version_match: + print(f'{pkg_name}[{extras}]{version_match.group(1)}') + else: + print(f'{pkg_name}[{extras}]') + elif version_match: + print(f'{pkg_name}{version_match.group(1)}') + else: + # Simple version string + version = version_part.strip('\"\\' ') + if version == '*': + print(pkg_name) + else: + print(f'{pkg_name}{version}') +" 2>/dev/null +} + +# Main router function that detects the file type and delegates to the appropriate parser +parse_dependencies() { + local dep_type="$1" # "main" or "dev" + local force_pm="$2" # Optional: force a specific package manager detection + + local current_pm + if [ -n "$force_pm" ]; then + current_pm="$force_pm" + else + current_pm=$(detect_current_pm) + fi + + case "$current_pm" in + pip) + parse_requirements_dependencies "$dep_type" + ;; + pipenv) + parse_pipfile_dependencies "$dep_type" + ;; + poetry|pdm|hatch|uv|pyproject) + parse_pyproject_dependencies "$dep_type" + ;; + *) + # Fallback: try each parser in order of likelihood + if [ -f "pyproject.toml" ]; then + parse_pyproject_dependencies "$dep_type" + elif [ -f "requirements.txt" ] || [ -f "requirements-dev.txt" ]; then + parse_requirements_dependencies "$dep_type" + elif [ -f "Pipfile" ]; then + parse_pipfile_dependencies "$dep_type" + fi + ;; + esac +} + +# Function to extract non-package-manager sections from pyproject.toml +# This preserves ALL tool configurations except package-manager specific ones +extract_extra_pyproject_sections() { + if [ ! -f "pyproject.toml" ]; then + return + fi + + python3 -c " +import sys +import json + +try: + import tomllib +except ImportError: + try: + import tomli as tomllib + except ImportError: + sys.exit(1) + +with open('pyproject.toml', 'rb') as f: + data = tomllib.load(f) + +# Define package-manager specific tool names that should NOT be preserved +# Everything else will be preserved +package_manager_tools = { + 'poetry', + 'pdm', + 'hatch', + 'uv', + 'pipenv', +} + +# Extract sections to preserve +preserved = {} + +# Preserve all tool sections except package manager ones +if 'tool' in data: + for tool_name, tool_config in data['tool'].items(): + if tool_name not in package_manager_tools: + if 'tool' not in preserved: + preserved['tool'] = {} + preserved['tool'][tool_name] = tool_config + +# Output as JSON for easy parsing in bash +print(json.dumps(preserved)) +" 2>/dev/null +} + +# Function to format preserved sections back into TOML format +format_preserved_toml_sections() { + local json_data="$1" + + if [ -z "$json_data" ] || [ "$json_data" = "{}" ]; then + return + fi + + python3 -c " +import sys +import json + +# Read JSON from stdin to avoid shell escaping issues +json_input = '''$json_data''' +data = json.loads(json_input) + +def format_value(value): + if isinstance(value, str): + # Check if string contains quotes or special characters + if '\"' in value or '\\n' in value or '\\\\' in value: + return f'\"\"\"\\n{value}\\n\"\"\"' + else: + return f'\"{value}\"' + elif isinstance(value, bool): + return 'true' if value else 'false' + elif isinstance(value, (int, float)): + return str(value) + elif isinstance(value, list): + items = [format_value(item) for item in value] + return '[' + ', '.join(items) + ']' + elif isinstance(value, dict): + # This is a table, handle separately + return None + else: + return f'\"{value}\"' + +def has_non_dict_values(data): + \"\"\"Check if a dictionary has any non-dict values\"\"\" + for value in data.values(): + if not isinstance(value, dict): + return True + return False + +def print_table(data, prefix='', section_header_printed=False): + # First, check if this table has any direct values + if has_non_dict_values(data): + # Print section header if not already printed + if not section_header_printed and prefix: + print(f'\\n[{prefix.rstrip(\".\")}]') + + # Print all non-dict values at this level + for key, value in data.items(): + if not isinstance(value, dict): + formatted = format_value(value) + if formatted is not None: + # Handle empty string keys specially + if key == '': + print(f'\"\" = {formatted}') + else: + print(f'{key} = {formatted}') + + # Then handle nested tables + for key, value in data.items(): + if isinstance(value, dict) and value: # Only process non-empty dicts + # Check if the nested table has any content worth printing + if has_non_dict_values(value) or any(isinstance(v, dict) and v for v in value.values()): + print_table(value, f'{prefix}{key}.', False) + +# Print tool sections +if 'tool' in data: + for tool_name, tool_config in data['tool'].items(): + # Only process tool if it has actual content + if tool_config: + print_table(tool_config, f'tool.{tool_name}.', False) +" 2>/dev/null +} \ No newline at end of file diff --git a/scripts/rollback.sh b/scripts/rollback.sh new file mode 100755 index 0000000..f242a35 --- /dev/null +++ b/scripts/rollback.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check for optional package manager argument +TARGET_PM="$1" + +if [ -n "$TARGET_PM" ]; then + # User specified a specific package manager backup to restore + BACKUP_DIR=".backup.$TARGET_PM" + + # Check if the specified backup exists + if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Error: No backup found for package manager '$TARGET_PM'${NC}" + echo "Available backups:" + for dir in .backup.*; do + if [ -d "$dir" ]; then + pm_name="${dir#.backup.}" + echo " - $pm_name" + fi + done 2>/dev/null || echo " None found" + exit 1 + fi + + echo -e "${GREEN}✔${NC} Rolling back from ${YELLOW}$BACKUP_DIR/${NC}" +else + # Original behavior: count backup directories and handle interactively + BACKUP_DIRS=($(ls -d .backup.* 2>/dev/null || true)) + BACKUP_COUNT=${#BACKUP_DIRS[@]} + + if [ $BACKUP_COUNT -eq 0 ]; then + echo -e "${RED}No backups found${NC}" + echo "Nothing to rollback" + exit 1 + elif [ $BACKUP_COUNT -eq 1 ]; then + # Automatic rollback from single backup + BACKUP_DIR="${BACKUP_DIRS[0]}" + echo -e "${GREEN}✔${NC} Rolling back from ${YELLOW}$BACKUP_DIR/${NC}" + else + # Interactive menu for multiple backups + echo "Multiple backups found:" + echo "" + + # Display options + for i in "${!BACKUP_DIRS[@]}"; do + dir="${BACKUP_DIRS[$i]}" + # Extract package manager name from backup directory + pm_name="${dir#.backup.}" + echo " $((i+1))) $pm_name (from $dir)" + done + + echo "" + read -p "Select backup to restore (1-$BACKUP_COUNT): " selection + + # Validate selection + if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $BACKUP_COUNT ]; then + echo -e "${RED}Invalid selection${NC}" + exit 1 + fi + + BACKUP_DIR="${BACKUP_DIRS[$((selection-1))]}" + echo "" + echo -e "${GREEN}✔${NC} Rolling back from ${YELLOW}$BACKUP_DIR/${NC}" + fi +fi + +# Perform rollback +if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Error: Backup directory not found: $BACKUP_DIR${NC}" + exit 1 +fi + +# Remove current package manager files (but keep backup directories) +echo " Cleaning current files..." +rm -f Dockerfile .dockerignore requirements.txt Pipfile Pipfile.lock poetry.lock pdm.lock uv.lock pyproject.toml + +# Restore files from backup +for file in "$BACKUP_DIR"/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + cp "$file" "./$filename" + echo " Restored: $filename" + fi +done + +echo "" +echo -e "${GREEN}✔${NC} Rollback complete!" +echo "" +echo "Note: Lock files and dependencies may need to be reinstalled" +echo "based on the restored package manager configuration." \ No newline at end of file