Coverage for src / moai_adk / core / migration / alfred_to_moai_migrator.py: 10.42%
192 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Alfred to Moai folder structure migration for MoAI-ADK
4Handles automatic migration from legacy alfred/ folders to new moai/ structure.
5- Creates backup before migration
6- Installs fresh moai/ templates from package
7- Deletes alfred/ folders
8- Updates settings.json Hook paths
9- Records migration status in config.json
10- Provides automatic rollback on failure
11"""
13import json
14import logging
15import shutil
16from datetime import datetime
17from pathlib import Path
18from typing import Optional
20from .backup_manager import BackupManager
22logger = logging.getLogger(__name__)
25class AlfredToMoaiMigrator:
26 """Handles automatic migration from Alfred to Moai folder structure"""
28 def __init__(self, project_root: Path):
29 """
30 Initialize Alfred to Moai migrator
32 Args:
33 project_root: Root directory of the project
34 """
35 self.project_root = Path(project_root)
36 self.claude_root = self.project_root / ".claude"
37 self.config_path = self.project_root / ".moai" / "config" / "config.json"
38 self.settings_path = self.claude_root / "settings.json"
39 self.backup_manager = BackupManager(project_root)
41 # Define folder paths
42 self.alfred_folders = {
43 "commands": self.claude_root / "commands" / "alfred",
44 "agents": self.claude_root / "agents" / "alfred",
45 "hooks": self.claude_root / "hooks" / "alfred",
46 }
48 self.moai_folders = {
49 "commands": self.claude_root / "commands" / "moai",
50 "agents": self.claude_root / "agents" / "moai",
51 "hooks": self.claude_root / "hooks" / "moai",
52 }
54 def _load_config(self) -> dict:
55 """
56 Load config.json
58 Returns:
59 Dictionary from config.json, or empty dict if not found
60 """
61 if not self.config_path.exists():
62 return {}
64 try:
65 return json.loads(self.config_path.read_text(encoding="utf-8"))
66 except Exception as e:
67 logger.warning(f"Failed to load config.json: {e}")
68 return {}
70 def _save_config(self, config: dict) -> None:
71 """
72 Save config.json
74 Args:
75 config: Configuration dictionary to save
77 Raises:
78 Exception: If save fails
79 """
80 self.config_path.parent.mkdir(parents=True, exist_ok=True)
81 self.config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False))
83 def needs_migration(self) -> bool:
84 """
85 Check if Alfred to Moai migration is needed
87 Returns:
88 True if migration is needed, False otherwise
89 """
90 # Check if already migrated
91 try:
92 config = self._load_config()
93 migration_state = config.get("migration", {}).get("alfred_to_moai", {})
94 if migration_state.get("migrated"):
95 logger.info("ℹ️ Alfred → Moai migration already completed")
96 if migration_state.get("timestamp"):
97 logger.info(f"Timestamp: {migration_state['timestamp']}")
98 return False
99 except Exception as e:
100 logger.debug(f"Config check error: {e}")
102 # Check if any alfred folder exists
103 has_alfred = any(folder.exists() for folder in self.alfred_folders.values())
105 if has_alfred:
106 detected = [
107 name
108 for name, folder in self.alfred_folders.items()
109 if folder.exists()
110 ]
111 logger.debug(f"Alfred folders detected: {', '.join(detected)}")
113 return has_alfred
115 def execute_migration(self, backup_path: Optional[Path] = None) -> bool:
116 """
117 Execute Alfred to Moai migration
119 Args:
120 backup_path: Path to use for backup (if None, creates new backup)
122 Returns:
123 True if migration successful, False otherwise
124 """
125 try:
126 logger.info("\n[1/5] Backing up project...")
128 # Step 1: Create or use existing backup
129 if backup_path is None:
130 try:
131 backup_path = self.backup_manager.create_backup(
132 "alfred_to_moai_migration"
133 )
134 logger.info(f"✅ Backup completed: {backup_path}")
135 except Exception as e:
136 logger.error("❌ Error: Backup failed")
137 logger.error(f"Cause: {str(e)}")
138 return False
139 else:
140 logger.info(f"✅ Using existing backup: {backup_path}")
142 # Step 2: Detect alfred folders
143 logger.info("\n[2/5] Alfred folders detected: ", end="")
144 alfred_detected = {
145 name: folder
146 for name, folder in self.alfred_folders.items()
147 if folder.exists()
148 }
150 if not alfred_detected:
151 logger.warning("No Alfred folders found - skipping migration")
152 return True
154 logger.info(", ".join(alfred_detected.keys()))
156 # Step 3: Verify moai folders exist (should be created in Phase 1)
157 logger.info("\n[3/5] Verifying Moai template installation...")
158 missing_moai = [
159 name
160 for name, folder in self.moai_folders.items()
161 if not folder.exists()
162 ]
164 if missing_moai:
165 logger.error(
166 f"❌ Missing Moai folders: {', '.join(missing_moai)}"
167 )
168 logger.error("Phase 1 implementation required first (package template moai structure)")
169 self._rollback_migration(backup_path)
170 return False
172 logger.info("✅ Moai templates installed")
174 # Step 4: Update settings.json hooks
175 logger.info("\n[4/5] Updating paths...")
176 try:
177 self._update_settings_json_hooks()
178 logger.info("✅ settings.json Hook paths updated")
179 except Exception as e:
180 logger.error("❌ Error: Failed to update settings.json")
181 logger.error(f"Cause: {str(e)}")
182 self._rollback_migration(backup_path)
183 return False
185 # Step 5: Delete alfred folders
186 logger.info("\n[5/5] Cleaning up...")
187 try:
188 self._delete_alfred_folders(alfred_detected)
189 logger.info("✅ Alfred folders deleted")
190 except Exception as e:
191 logger.error("❌ Error: Failed to delete Alfred folders")
192 logger.error(f"Cause: {str(e)}")
193 self._rollback_migration(backup_path)
194 return False
196 # Step 6: Verify migration
197 logger.info("\n[6/6] Verifying migration...")
198 if not self._verify_migration():
199 logger.error("❌ Migration verification failed")
200 self._rollback_migration(backup_path)
201 return False
203 logger.info("✅ Migration verification passed")
205 # Step 7: Record migration status
206 logger.info("\nRecording migration status...")
207 try:
208 self._record_migration_state(backup_path, len(alfred_detected))
209 logger.info("✅ Migration status recorded")
210 except Exception as e:
211 logger.warning(f"⚠️ Failed to record status: {str(e)}")
212 # Don't rollback for this, migration was successful
214 logger.info("\n✅ Alfred → Moai migration completed!")
215 return True
217 except Exception as e:
218 logger.error(f"\n❌ Unexpected error: {str(e)}")
219 if backup_path:
220 self._rollback_migration(backup_path)
221 return False
223 def _delete_alfred_folders(self, alfred_detected: dict) -> None:
224 """
225 Delete Alfred folders
227 Args:
228 alfred_detected: Dictionary of detected alfred folders
230 Raises:
231 Exception: If deletion fails
232 """
233 for name, folder in alfred_detected.items():
234 if folder.exists():
235 try:
236 shutil.rmtree(folder)
237 logger.debug(f"Deleted: {folder}")
238 except Exception as e:
239 raise Exception(f"Failed to delete {name} folder: {str(e)}")
241 def _update_settings_json_hooks(self) -> None:
242 """
243 Update settings.json to replace alfred paths with moai paths
245 Raises:
246 Exception: If update fails
247 """
248 if not self.settings_path.exists():
249 logger.warning(f"settings.json file missing: {self.settings_path}")
250 return
252 try:
253 # Read settings.json
254 with open(self.settings_path, "r", encoding="utf-8") as f:
255 settings_content = f.read()
257 # Replace all alfred references with moai
258 # Pattern: .claude/hooks/alfred/ → .claude/hooks/moai/
259 updated_content = settings_content.replace(
260 ".claude/hooks/alfred/", ".claude/hooks/moai/"
261 )
262 updated_content = updated_content.replace(
263 ".claude/commands/alfred/", ".claude/commands/moai/"
264 )
265 updated_content = updated_content.replace(
266 ".claude/agents/alfred/", ".claude/agents/moai/"
267 )
269 # Write back to file
270 with open(self.settings_path, "w", encoding="utf-8") as f:
271 f.write(updated_content)
273 # Verify JSON validity
274 with open(self.settings_path, "r", encoding="utf-8") as f:
275 json.load(f) # This will raise if JSON is invalid
277 logger.debug("settings.json update and verification completed")
279 except json.JSONDecodeError as e:
280 raise Exception(f"settings.json JSON format error: {str(e)}")
281 except Exception as e:
282 raise Exception(f"Failed to update settings.json: {str(e)}")
284 def _verify_migration(self) -> bool:
285 """
286 Verify migration was successful
288 Returns:
289 True if migration is valid, False otherwise
290 """
291 # Check moai folders exist
292 for name, folder in self.moai_folders.items():
293 if not folder.exists():
294 logger.error(f"❌ Missing Moai {name} folder: {folder}")
295 return False
297 # Check alfred folders are deleted
298 for name, folder in self.alfred_folders.items():
299 if folder.exists():
300 logger.warning(f"⚠️ Alfred {name} folder still exists: {folder}")
301 return False
303 # Check settings.json hooks paths (ignore pattern matching strings like "Bash(alfred:*)")
304 if self.settings_path.exists():
305 try:
306 with open(self.settings_path, "r", encoding="utf-8") as f:
307 settings_content = f.read()
309 # Only check for hooks/alfred paths, not pattern strings
310 if ".claude/hooks/alfred/" in settings_content or ".claude/commands/alfred/" in settings_content or ".claude/agents/alfred/" in settings_content:
311 logger.error("❌ settings.json still contains alfred hook paths")
312 return False
314 if "moai" not in settings_content.lower():
315 logger.warning("⚠️ No moai references in settings.json")
317 except Exception as e:
318 logger.error(f"❌ settings.json verification failed: {str(e)}")
319 return False
321 logger.debug("Migration verification completed")
322 return True
324 def _record_migration_state(self, backup_path: Path, folders_count: int) -> None:
325 """
326 Record migration state in config.json
328 Args:
329 backup_path: Path to the backup
330 folders_count: Number of folders migrated
332 Raises:
333 Exception: If recording fails
334 """
335 try:
336 config = self._load_config()
338 # Initialize migration section if not exists
339 if "migration" not in config:
340 config["migration"] = {}
342 config["migration"]["alfred_to_moai"] = {
343 "migrated": True,
344 "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
345 "folders_installed": 3, # commands, agents, hooks
346 "folders_removed": folders_count,
347 "backup_path": str(backup_path),
348 "package_version": self._get_package_version(),
349 }
351 self._save_config(config)
352 logger.debug("Migration state recorded in config.json")
354 except Exception as e:
355 raise Exception(f"Migration state recording failed: {str(e)}")
357 def _rollback_migration(self, backup_path: Path) -> None:
358 """
359 Rollback migration from backup
361 Args:
362 backup_path: Path to the backup to restore from
363 """
364 try:
365 logger.info("\n🔄 Starting automatic rollback...")
366 logger.info("[1/3] Restoring project...")
368 # Restore from backup
369 self.backup_manager.restore_backup(backup_path)
371 logger.info("✅ Project restored")
372 logger.info("[2/3] Resetting migration state...")
374 # Clear migration state in config
375 try:
376 config = self._load_config()
377 if "migration" in config and "alfred_to_moai" in config["migration"]:
378 del config["migration"]["alfred_to_moai"]
379 self._save_config(config)
380 except Exception as e:
381 logger.warning(f"⚠️ Failed to reset state: {str(e)}")
383 logger.info("✅ Rollback completed")
384 logger.info(
385 "💡 Tip: Run `moai-adk update` again after resolving the error"
386 )
388 except Exception as e:
389 logger.error(f"\n❌ Rollback failed: {str(e)}")
390 logger.error(
391 "⚠️ Manual recovery required: Please restore manually from backup: "
392 f"{backup_path}"
393 )
395 def _get_package_version(self) -> str:
396 """
397 Get current package version
399 Returns:
400 Version string
401 """
402 try:
403 config = self._load_config()
404 return config.get("moai", {}).get("version", "unknown")
405 except Exception:
406 return "unknown"