#!/usr/bin/env python3 """ Configuration Migration Script for Void Bot Migrates from .env environment variables to config.yaml YAML configuration. """ import os import shutil from pathlib import Path import yaml from datetime import datetime def load_env_file(env_path=".env"): """Load environment variables from .env file.""" env_vars = {} if not os.path.exists(env_path): return env_vars try: with open(env_path, 'r', encoding='utf-8') as f: for line_num, line in enumerate(f, 1): line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Parse KEY=VALUE format if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip() # Remove quotes if present if value.startswith('"') and value.endswith('"'): value = value[1:-1] elif value.startswith("'") and value.endswith("'"): value = value[1:-1] env_vars[key] = value else: print(f"⚠️ Warning: Skipping malformed line {line_num} in .env: {line}") except Exception as e: print(f"❌ Error reading .env file: {e}") return env_vars def create_config_from_env(env_vars, existing_config=None): """Create YAML configuration from environment variables.""" # Start with existing config if available, otherwise use defaults if existing_config: config = existing_config.copy() else: config = {} # Ensure all sections exist if 'letta' not in config: config['letta'] = {} if 'bluesky' not in config: config['bluesky'] = {} if 'bot' not in config: config['bot'] = {} # Map environment variables to config structure env_mapping = { 'LETTA_API_KEY': ('letta', 'api_key'), 'BSKY_USERNAME': ('bluesky', 'username'), 'BSKY_PASSWORD': ('bluesky', 'password'), 'PDS_URI': ('bluesky', 'pds_uri'), } migrated_vars = [] for env_var, (section, key) in env_mapping.items(): if env_var in env_vars: config[section][key] = env_vars[env_var] migrated_vars.append(env_var) # Set some sensible defaults if not already present if 'timeout' not in config['letta']: config['letta']['timeout'] = 600 if 'pds_uri' not in config['bluesky']: config['bluesky']['pds_uri'] = "https://bsky.social" # Add bot configuration defaults if not present if 'fetch_notifications_delay' not in config['bot']: config['bot']['fetch_notifications_delay'] = 30 if 'max_processed_notifications' not in config['bot']: config['bot']['max_processed_notifications'] = 10000 if 'max_notification_pages' not in config['bot']: config['bot']['max_notification_pages'] = 20 return config, migrated_vars def backup_existing_files(): """Create backups of existing configuration files.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backups = [] # Backup existing config.yaml if it exists if os.path.exists("config.yaml"): backup_path = f"config.yaml.backup_{timestamp}" shutil.copy2("config.yaml", backup_path) backups.append(("config.yaml", backup_path)) # Backup .env if it exists if os.path.exists(".env"): backup_path = f".env.backup_{timestamp}" shutil.copy2(".env", backup_path) backups.append((".env", backup_path)) return backups def load_existing_config(): """Load existing config.yaml if it exists.""" if not os.path.exists("config.yaml"): return None try: with open("config.yaml", 'r', encoding='utf-8') as f: return yaml.safe_load(f) or {} except Exception as e: print(f"⚠️ Warning: Could not read existing config.yaml: {e}") return None def write_config_yaml(config): """Write the configuration to config.yaml.""" try: with open("config.yaml", 'w', encoding='utf-8') as f: # Write header comment f.write("# Void Bot Configuration\n") f.write("# Generated by migration script\n") f.write(f"# Created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write("# See config.yaml.example for all available options\n\n") # Write YAML content yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2) return True except Exception as e: print(f"❌ Error writing config.yaml: {e}") return False def main(): """Main migration function.""" print("🔄 Void Bot Configuration Migration Tool") print("=" * 50) print("This tool migrates from .env environment variables to config.yaml") print() # Check what files exist has_env = os.path.exists(".env") has_config = os.path.exists("config.yaml") has_example = os.path.exists("config.yaml.example") print("📋 Current configuration files:") print(f" - .env file: {'✅ Found' if has_env else '❌ Not found'}") print(f" - config.yaml: {'✅ Found' if has_config else '❌ Not found'}") print(f" - config.yaml.example: {'✅ Found' if has_example else '❌ Not found'}") print() # If no .env file, suggest creating config from example if not has_env: if not has_config and has_example: print("💡 No .env file found. Would you like to create config.yaml from the example?") response = input("Create config.yaml from example? (y/n): ").lower().strip() if response in ['y', 'yes']: try: shutil.copy2("config.yaml.example", "config.yaml") print("✅ Created config.yaml from config.yaml.example") print("📝 Please edit config.yaml to add your credentials") return except Exception as e: print(f"❌ Error copying example file: {e}") return else: print("👋 Migration cancelled") return else: print("ℹ️ No .env file found and config.yaml already exists or no example available") print(" If you need to set up configuration, see CONFIG.md") return # Load environment variables from .env print("🔍 Reading .env file...") env_vars = load_env_file() if not env_vars: print("⚠️ No environment variables found in .env file") return print(f" Found {len(env_vars)} environment variables") for key in env_vars.keys(): # Mask sensitive values if 'KEY' in key or 'PASSWORD' in key: value_display = f"***{env_vars[key][-4:]}" if len(env_vars[key]) > 4 else "***" else: value_display = env_vars[key] print(f" - {key}={value_display}") print() # Load existing config if present existing_config = load_existing_config() if existing_config: print("📄 Found existing config.yaml - will merge with .env values") # Create configuration print("🏗️ Building configuration...") config, migrated_vars = create_config_from_env(env_vars, existing_config) if not migrated_vars: print("⚠️ No recognized configuration variables found in .env") print(" Recognized variables: LETTA_API_KEY, BSKY_USERNAME, BSKY_PASSWORD, PDS_URI") return print(f" Migrating {len(migrated_vars)} variables: {', '.join(migrated_vars)}") # Show preview print("\n📋 Configuration preview:") print("-" * 30) # Show Letta section if 'letta' in config and config['letta']: print("🔧 Letta:") for key, value in config['letta'].items(): if 'key' in key.lower(): display_value = f"***{value[-8:]}" if len(str(value)) > 8 else "***" else: display_value = value print(f" {key}: {display_value}") # Show Bluesky section if 'bluesky' in config and config['bluesky']: print("🐦 Bluesky:") for key, value in config['bluesky'].items(): if 'password' in key.lower(): display_value = f"***{value[-4:]}" if len(str(value)) > 4 else "***" else: display_value = value print(f" {key}: {display_value}") print() # Confirm migration response = input("💾 Proceed with migration? This will update config.yaml (y/n): ").lower().strip() if response not in ['y', 'yes']: print("👋 Migration cancelled") return # Create backups print("💾 Creating backups...") backups = backup_existing_files() for original, backup in backups: print(f" Backed up {original} → {backup}") # Write new configuration print("✍️ Writing config.yaml...") if write_config_yaml(config): print("✅ Successfully created config.yaml") # Test the new configuration print("\n🧪 Testing new configuration...") try: from config_loader import get_config test_config = get_config() print("✅ Configuration loads successfully") # Test specific sections try: from config_loader import get_letta_config letta_config = get_letta_config() print("✅ Letta configuration valid") except Exception as e: print(f"⚠️ Letta config issue: {e}") try: from config_loader import get_bluesky_config bluesky_config = get_bluesky_config() print("✅ Bluesky configuration valid") except Exception as e: print(f"⚠️ Bluesky config issue: {e}") except Exception as e: print(f"❌ Configuration test failed: {e}") return # Success message and next steps print("\n🎉 Migration completed successfully!") print("\n📖 Next steps:") print(" 1. Run: python test_config.py") print(" 2. Test the bot: python bsky.py --test") print(" 3. If everything works, you can optionally remove the .env file") print(" 4. See CONFIG.md for more configuration options") if backups: print(f"\n🗂️ Backup files created:") for original, backup in backups: print(f" {backup}") print(" These can be deleted once you verify everything works") else: print("❌ Failed to write config.yaml") if backups: print("🔄 Restoring backups...") for original, backup in backups: try: if original != ".env": # Don't restore .env, keep it as fallback shutil.move(backup, original) print(f" Restored {backup} → {original}") except Exception as e: print(f" ❌ Failed to restore {backup}: {e}") if __name__ == "__main__": main()