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

1""" 

2Alfred to Moai folder structure migration for MoAI-ADK 

3 

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""" 

12 

13import json 

14import logging 

15import shutil 

16from datetime import datetime 

17from pathlib import Path 

18from typing import Optional 

19 

20from .backup_manager import BackupManager 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class AlfredToMoaiMigrator: 

26 """Handles automatic migration from Alfred to Moai folder structure""" 

27 

28 def __init__(self, project_root: Path): 

29 """ 

30 Initialize Alfred to Moai migrator 

31 

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) 

40 

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 } 

47 

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 } 

53 

54 def _load_config(self) -> dict: 

55 """ 

56 Load config.json 

57 

58 Returns: 

59 Dictionary from config.json, or empty dict if not found 

60 """ 

61 if not self.config_path.exists(): 

62 return {} 

63 

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 {} 

69 

70 def _save_config(self, config: dict) -> None: 

71 """ 

72 Save config.json 

73 

74 Args: 

75 config: Configuration dictionary to save 

76 

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)) 

82 

83 def needs_migration(self) -> bool: 

84 """ 

85 Check if Alfred to Moai migration is needed 

86 

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}") 

101 

102 # Check if any alfred folder exists 

103 has_alfred = any(folder.exists() for folder in self.alfred_folders.values()) 

104 

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)}") 

112 

113 return has_alfred 

114 

115 def execute_migration(self, backup_path: Optional[Path] = None) -> bool: 

116 """ 

117 Execute Alfred to Moai migration 

118 

119 Args: 

120 backup_path: Path to use for backup (if None, creates new backup) 

121 

122 Returns: 

123 True if migration successful, False otherwise 

124 """ 

125 try: 

126 logger.info("\n[1/5] Backing up project...") 

127 

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}") 

141 

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 } 

149 

150 if not alfred_detected: 

151 logger.warning("No Alfred folders found - skipping migration") 

152 return True 

153 

154 logger.info(", ".join(alfred_detected.keys())) 

155 

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 ] 

163 

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 

171 

172 logger.info("✅ Moai templates installed") 

173 

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 

184 

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 

195 

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 

202 

203 logger.info("✅ Migration verification passed") 

204 

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 

213 

214 logger.info("\n✅ Alfred → Moai migration completed!") 

215 return True 

216 

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 

222 

223 def _delete_alfred_folders(self, alfred_detected: dict) -> None: 

224 """ 

225 Delete Alfred folders 

226 

227 Args: 

228 alfred_detected: Dictionary of detected alfred folders 

229 

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)}") 

240 

241 def _update_settings_json_hooks(self) -> None: 

242 """ 

243 Update settings.json to replace alfred paths with moai paths 

244 

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 

251 

252 try: 

253 # Read settings.json 

254 with open(self.settings_path, "r", encoding="utf-8") as f: 

255 settings_content = f.read() 

256 

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 ) 

268 

269 # Write back to file 

270 with open(self.settings_path, "w", encoding="utf-8") as f: 

271 f.write(updated_content) 

272 

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 

276 

277 logger.debug("settings.json update and verification completed") 

278 

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)}") 

283 

284 def _verify_migration(self) -> bool: 

285 """ 

286 Verify migration was successful 

287 

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 

296 

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 

302 

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() 

308 

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 

313 

314 if "moai" not in settings_content.lower(): 

315 logger.warning("⚠️ No moai references in settings.json") 

316 

317 except Exception as e: 

318 logger.error(f"❌ settings.json verification failed: {str(e)}") 

319 return False 

320 

321 logger.debug("Migration verification completed") 

322 return True 

323 

324 def _record_migration_state(self, backup_path: Path, folders_count: int) -> None: 

325 """ 

326 Record migration state in config.json 

327 

328 Args: 

329 backup_path: Path to the backup 

330 folders_count: Number of folders migrated 

331 

332 Raises: 

333 Exception: If recording fails 

334 """ 

335 try: 

336 config = self._load_config() 

337 

338 # Initialize migration section if not exists 

339 if "migration" not in config: 

340 config["migration"] = {} 

341 

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 } 

350 

351 self._save_config(config) 

352 logger.debug("Migration state recorded in config.json") 

353 

354 except Exception as e: 

355 raise Exception(f"Migration state recording failed: {str(e)}") 

356 

357 def _rollback_migration(self, backup_path: Path) -> None: 

358 """ 

359 Rollback migration from backup 

360 

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...") 

367 

368 # Restore from backup 

369 self.backup_manager.restore_backup(backup_path) 

370 

371 logger.info("✅ Project restored") 

372 logger.info("[2/3] Resetting migration state...") 

373 

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)}") 

382 

383 logger.info("✅ Rollback completed") 

384 logger.info( 

385 "💡 Tip: Run `moai-adk update` again after resolving the error" 

386 ) 

387 

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 ) 

394 

395 def _get_package_version(self) -> str: 

396 """ 

397 Get current package version 

398 

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"