|
| 1 | +""" |
| 2 | +V1 Configuration Detection and Analysis |
| 3 | +""" |
| 4 | + |
| 5 | +import json |
| 6 | +import logging |
| 7 | +from pathlib import Path |
| 8 | +from typing import Dict, List, Optional |
| 9 | + |
| 10 | +from mcpm.utils.config import DEFAULT_CONFIG_DIR |
| 11 | + |
| 12 | +logger = logging.getLogger(__name__) |
| 13 | + |
| 14 | + |
| 15 | +class V1ConfigDetector: |
| 16 | + """Detects and analyzes v1 configuration files""" |
| 17 | + |
| 18 | + def __init__(self, config_dir: Optional[Path] = None): |
| 19 | + self.config_dir = Path(config_dir) if config_dir else Path(DEFAULT_CONFIG_DIR) |
| 20 | + self.config_file = self.config_dir / "config.json" |
| 21 | + self.profiles_file = self.config_dir / "profiles.json" |
| 22 | + |
| 23 | + def has_v1_config(self) -> bool: |
| 24 | + """Check if v1 configuration files exist with actual v1 content""" |
| 25 | + # Check for legacy profiles file (clear indicator of v1) |
| 26 | + if self.profiles_file.exists(): |
| 27 | + return True |
| 28 | + |
| 29 | + # Check config file for v1-specific content |
| 30 | + if self.config_file.exists(): |
| 31 | + try: |
| 32 | + with open(self.config_file) as f: |
| 33 | + config = json.load(f) |
| 34 | + |
| 35 | + # Look for v1-specific content (not just the keys with None values) |
| 36 | + v1_indicators = [ |
| 37 | + config.get("active_client"), |
| 38 | + config.get("active_target"), |
| 39 | + config.get("stashed_servers"), |
| 40 | + config.get("router"), |
| 41 | + config.get("share") |
| 42 | + ] |
| 43 | + |
| 44 | + # Return True if any v1 indicator has actual content (not None/empty) |
| 45 | + return any(indicator for indicator in v1_indicators) |
| 46 | + |
| 47 | + except (json.JSONDecodeError, IOError): |
| 48 | + pass |
| 49 | + |
| 50 | + return False |
| 51 | + |
| 52 | + def detect_v1_features(self) -> Dict[str, bool]: |
| 53 | + """Detect which v1 features are present in the config""" |
| 54 | + features = { |
| 55 | + "active_target": False, |
| 56 | + "stashed_servers": False, |
| 57 | + "router_config": False, |
| 58 | + "share_status": False, |
| 59 | + "legacy_profiles": False, |
| 60 | + } |
| 61 | + |
| 62 | + # Check main config file |
| 63 | + if self.config_file.exists(): |
| 64 | + try: |
| 65 | + with open(self.config_file) as f: |
| 66 | + config = json.load(f) |
| 67 | + |
| 68 | + # Check for actual v1 content, not just the presence of keys with null values |
| 69 | + features["active_target"] = any(config.get(key) for key in ["active_client", "active_target"]) |
| 70 | + features["stashed_servers"] = bool(config.get("stashed_servers")) |
| 71 | + features["router_config"] = bool(config.get("router")) |
| 72 | + features["share_status"] = bool(config.get("share")) |
| 73 | + |
| 74 | + except (json.JSONDecodeError, IOError) as e: |
| 75 | + logger.warning(f"Failed to read v1 config file: {e}") |
| 76 | + |
| 77 | + # Check profiles file |
| 78 | + if self.profiles_file.exists(): |
| 79 | + features["legacy_profiles"] = True |
| 80 | + |
| 81 | + return features |
| 82 | + |
| 83 | + def analyze_v1_config(self) -> Dict[str, any]: |
| 84 | + """Analyze v1 configuration and return migration info""" |
| 85 | + analysis = { |
| 86 | + "config_found": False, |
| 87 | + "profiles_found": False, |
| 88 | + "server_count": 0, |
| 89 | + "profile_count": 0, |
| 90 | + "stashed_count": 0, |
| 91 | + "active_target": None, |
| 92 | + "router_enabled": False, |
| 93 | + "share_active": False, |
| 94 | + "profiles": {}, |
| 95 | + "clients_with_stashed": [], |
| 96 | + } |
| 97 | + |
| 98 | + # Analyze main config |
| 99 | + if self.config_file.exists(): |
| 100 | + try: |
| 101 | + with open(self.config_file) as f: |
| 102 | + config = json.load(f) |
| 103 | + |
| 104 | + analysis["config_found"] = True |
| 105 | + analysis["active_target"] = config.get("active_target") |
| 106 | + analysis["router_enabled"] = bool(config.get("router")) |
| 107 | + share_config = config.get("share", {}) |
| 108 | + analysis["share_active"] = bool(share_config and share_config.get("url")) |
| 109 | + |
| 110 | + # Count stashed servers |
| 111 | + stashed = config.get("stashed_servers", {}) or {} |
| 112 | + analysis["stashed_count"] = sum(len(servers) for servers in stashed.values()) |
| 113 | + analysis["clients_with_stashed"] = list(stashed.keys()) |
| 114 | + |
| 115 | + except (json.JSONDecodeError, IOError) as e: |
| 116 | + logger.warning(f"Failed to analyze v1 config file: {e}") |
| 117 | + |
| 118 | + # Analyze profiles |
| 119 | + if self.profiles_file.exists(): |
| 120 | + try: |
| 121 | + with open(self.profiles_file) as f: |
| 122 | + profiles = json.load(f) |
| 123 | + |
| 124 | + analysis["profiles_found"] = True |
| 125 | + analysis["profile_count"] = len(profiles) |
| 126 | + analysis["profiles"] = {name: len(servers) for name, servers in profiles.items()} |
| 127 | + analysis["server_count"] = sum(analysis["profiles"].values()) |
| 128 | + |
| 129 | + except (json.JSONDecodeError, IOError) as e: |
| 130 | + logger.warning(f"Failed to analyze v1 profiles file: {e}") |
| 131 | + |
| 132 | + return analysis |
| 133 | + |
| 134 | + def get_v1_profiles(self) -> Dict[str, List[Dict]]: |
| 135 | + """Get v1 profiles for migration""" |
| 136 | + if not self.profiles_file.exists(): |
| 137 | + return {} |
| 138 | + |
| 139 | + try: |
| 140 | + with open(self.profiles_file) as f: |
| 141 | + return json.load(f) |
| 142 | + except (json.JSONDecodeError, IOError) as e: |
| 143 | + logger.error(f"Failed to read v1 profiles: {e}") |
| 144 | + return {} |
| 145 | + |
| 146 | + def get_stashed_servers(self) -> Dict[str, Dict[str, Dict]]: |
| 147 | + """Get stashed servers for migration""" |
| 148 | + if not self.config_file.exists(): |
| 149 | + return {} |
| 150 | + |
| 151 | + try: |
| 152 | + with open(self.config_file) as f: |
| 153 | + config = json.load(f) |
| 154 | + return config.get("stashed_servers", {}) |
| 155 | + except (json.JSONDecodeError, IOError) as e: |
| 156 | + logger.error(f"Failed to read stashed servers: {e}") |
| 157 | + return {} |
| 158 | + |
| 159 | + def backup_v1_configs(self) -> List[Path]: |
| 160 | + """Create backups of v1 config files in system backup directory""" |
| 161 | + import shutil |
| 162 | + from datetime import datetime |
| 163 | + |
| 164 | + # Create backup directory with timestamp |
| 165 | + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| 166 | + backup_dir = self.config_dir / "backups" / f"v1_migration_{timestamp}" |
| 167 | + backup_dir.mkdir(parents=True, exist_ok=True) |
| 168 | + |
| 169 | + backed_up = [] |
| 170 | + |
| 171 | + for config_file in [self.config_file, self.profiles_file]: |
| 172 | + if config_file.exists(): |
| 173 | + backup_path = backup_dir / config_file.name |
| 174 | + try: |
| 175 | + shutil.copy2(config_file, backup_path) |
| 176 | + backed_up.append(backup_path) |
| 177 | + logger.info(f"Backed up {config_file} to {backup_path}") |
| 178 | + except IOError as e: |
| 179 | + logger.error(f"Failed to backup {config_file}: {e}") |
| 180 | + |
| 181 | + # Create a README file explaining the backup |
| 182 | + readme_path = backup_dir / "README.md" |
| 183 | + try: |
| 184 | + with open(readme_path, "w") as f: |
| 185 | + f.write(f"""# MCPM v1 Configuration Backup |
| 186 | +
|
| 187 | +This directory contains your MCPM v1 configuration files that were automatically backed up during migration to v2. |
| 188 | +
|
| 189 | +**Backup Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} |
| 190 | +
|
| 191 | +## Files Backed Up |
| 192 | +
|
| 193 | +- `config.json`: Main v1 configuration (active targets, stashed servers, router settings) |
| 194 | +- `profiles.json`: v1 profile definitions (if existed) |
| 195 | +
|
| 196 | +## Restoring from Backup |
| 197 | +
|
| 198 | +If you need to restore any v1 settings: |
| 199 | +
|
| 200 | +1. **Manual Review**: Open the JSON files to see your old configuration |
| 201 | +2. **Individual Settings**: Copy specific values you need back to v2 config |
| 202 | +3. **Full Restore**: Replace current config files with these backups (not recommended) |
| 203 | +
|
| 204 | +## v1 vs v2 Differences |
| 205 | +
|
| 206 | +- **Active Targets**: No longer needed - use profiles as tags instead |
| 207 | +- **Stashed Servers**: Managed directly - enable/disable servers as needed |
| 208 | +- **Router Daemon**: Replaced with direct execution and sharing features |
| 209 | +
|
| 210 | +## Support |
| 211 | +
|
| 212 | +If you need help understanding or migrating specific v1 settings: |
| 213 | +- Run `mcpm --help` to see v2 commands |
| 214 | +- Visit: https://github.com/pathintegral-institute/mcpm.sh/issues |
| 215 | +""") |
| 216 | + backed_up.append(readme_path) |
| 217 | + except IOError as e: |
| 218 | + logger.warning(f"Failed to create backup README: {e}") |
| 219 | + |
| 220 | + return backed_up |
0 commit comments