Coverage for src / moai_adk / core / project / initializer.py: 15.38%

104 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-20 20:52 +0900

1# SPEC: SPEC-CORE-PROJECT-001.md, SPEC-INIT-003.md 

2# TEST: tests/unit/test_project_initializer.py, tests/unit/test_init_reinit.py 

3"""Project Initialization Module 

4 

5Phase-based 5-step initialization process: 

61. Preparation: Backup and validation 

72. Directory: Create directory structure 

83. Resource: Copy template resources 

94. Configuration: Generate configuration files 

105. Validation: Verification and finalization 

11""" 

12# type: ignore 

13 

14import json 

15import stat 

16import time 

17from datetime import datetime 

18from pathlib import Path 

19 

20from moai_adk.core.project.phase_executor import PhaseExecutor, ProgressCallback # type: ignore 

21from moai_adk.core.project.validator import ProjectValidator 

22 

23 

24class InstallationResult: 

25 """Installation result""" 

26 

27 def __init__( 

28 self, 

29 success: bool, 

30 project_path: str, 

31 language: str, 

32 mode: str, 

33 locale: str, 

34 duration: int, 

35 created_files: list[str], 

36 errors: list[str] | None = None, 

37 ) -> None: 

38 self.success = success 

39 self.project_path = project_path 

40 self.language = language 

41 self.mode = mode 

42 self.locale = locale 

43 self.duration = duration 

44 self.created_files = created_files 

45 self.errors = errors or [] 

46 

47 

48class ProjectInitializer: 

49 """Project initializer (Phase-based)""" 

50 

51 def __init__(self, path: str | Path = ".") -> None: 

52 """Initialize 

53 

54 Args: 

55 path: Project root directory 

56 """ 

57 self.path = Path(path).resolve() 

58 self.validator = ProjectValidator() 

59 self.executor = PhaseExecutor(self.validator) 

60 

61 def _create_memory_files(self) -> list[str]: 

62 """Create runtime session and memory files (auto-generated per user/session) 

63 

64 Returns: 

65 List of created memory files 

66 

67 """ 

68 memory_dir = self.path / ".moai" / "memory" 

69 memory_dir.mkdir(parents=True, exist_ok=True) 

70 created_files = [] 

71 

72 # 1. project-notes.json - Project tracking notes (empty on init) 

73 project_notes = { 

74 "tech_debt": [], 

75 "performance_bottlenecks": [], 

76 "recent_patterns": { 

77 "frequent_file_edits": [], 

78 "test_failures": [], 

79 "git_operations": "daily commits, feature branches", 

80 }, 

81 "next_priorities": [], 

82 } 

83 project_notes_file = memory_dir / "project-notes.json" 

84 project_notes_file.write_text(json.dumps(project_notes, indent=2)) 

85 created_files.append(str(project_notes_file)) 

86 

87 # 2. session-hint.json - Last session state 

88 session_hint = { 

89 "last_command": None, 

90 "command_timestamp": None, 

91 "hours_ago": None, 

92 "active_spec": None, 

93 "current_branch": "main", 

94 } 

95 session_hint_file = memory_dir / "session-hint.json" 

96 session_hint_file.write_text(json.dumps(session_hint, indent=2)) 

97 created_files.append(str(session_hint_file)) 

98 

99 # 3. user-patterns.json - User preferences and expertise 

100 user_patterns = { 

101 "tech_preferences": {}, 

102 "expertise_signals": { 

103 "ask_question_skip_rate": 0.0, 

104 "custom_workflows": 0, 

105 "estimated_level": "beginner", 

106 }, 

107 "skip_questions": [], 

108 "last_updated": datetime.now().isoformat() + "Z", 

109 } 

110 user_patterns_file = memory_dir / "user-patterns.json" 

111 user_patterns_file.write_text(json.dumps(user_patterns, indent=2)) 

112 created_files.append(str(user_patterns_file)) 

113 

114 return created_files 

115 

116 def _create_user_settings(self) -> list[str]: 

117 """Create user-specific settings files (.claude/settings.local.json) 

118 

119 Returns: 

120 List of created settings files 

121 """ 

122 created_files = [] 

123 claude_dir = self.path / ".claude" 

124 claude_dir.mkdir(parents=True, exist_ok=True) 

125 

126 # Create settings.local.json 

127 settings_local = { 

128 "_meta": { 

129 "description": "User-specific Claude Code settings (gitignored - never commit)", 

130 "created_at": datetime.now().isoformat() + "Z", 

131 "note": "Edit this file to customize your local development environment", 

132 }, 

133 "enabledMcpjsonServers": ["context7"], # context7 is mandatory 

134 } 

135 

136 # Add companyAnnouncements in user's selected language 

137 try: 

138 import sys 

139 utils_dir = Path(__file__).parent.parent.parent / "templates" / ".claude" / "hooks" / "moai" / "shared" / "utils" 

140 

141 if utils_dir.exists(): 

142 sys.path.insert(0, str(utils_dir)) 

143 try: 

144 from announcement_translator import get_language_from_config, translate_announcements 

145 language = get_language_from_config(self.path) 

146 announcements = translate_announcements(language, self.path) 

147 settings_local["companyAnnouncements"] = announcements 

148 except Exception as e: 

149 print(f"[ProjectInitializer] Warning: Failed to add announcements: {e}") 

150 finally: 

151 sys.path.remove(str(utils_dir)) 

152 

153 except Exception as e: 

154 print(f"[ProjectInitializer] Warning: Announcement module not available: {e}") 

155 

156 settings_local_file = claude_dir / "settings.local.json" 

157 settings_local_file.write_text(json.dumps(settings_local, indent=2, ensure_ascii=False)) 

158 created_files.append(str(settings_local_file)) 

159 

160 return created_files 

161 

162 def initialize( 

163 self, 

164 mode: str = "personal", 

165 locale: str = "en", # Changed from "ko" to "en" (will be configurable in /alfred:0-project) 

166 language: str | None = None, 

167 custom_language: str | None = None, 

168 backup_enabled: bool = True, 

169 progress_callback: ProgressCallback | None = None, 

170 reinit: bool = False, 

171 ) -> InstallationResult: 

172 """Execute project initialization (5-phase process) 

173 

174 Args: 

175 mode: Project mode (personal/team) - Default: personal (configurable in /alfred:0-project) 

176 locale: Locale (ko/en/ja/zh/other) - Default: en (configurable in /alfred:0-project) 

177 language: Force language specification (auto-detect if None) - Will be detected in /alfred:0-project 

178 custom_language: Custom language name when locale="other" (user input) 

179 backup_enabled: Whether to enable backup 

180 progress_callback: Progress callback 

181 reinit: Reinitialization mode (v0.3.0, SPEC-INIT-003) 

182 

183 Returns: 

184 InstallationResult object 

185 

186 Raises: 

187 FileExistsError: If project is already initialized (when reinit=False) 

188 """ 

189 start_time = time.time() 

190 

191 try: 

192 # Prevent duplicate initialization (only when not in reinit mode) 

193 if self.is_initialized() and not reinit: 

194 raise FileExistsError( 

195 f"Project already initialized at {self.path}/.moai/\n" 

196 f"Use 'python -m moai_adk status' to check the current configuration." 

197 ) 

198 

199 # Use provided language or default to generic 

200 # Language detection now happens in /alfred:0-project via project-manager 

201 detected_language = language or "generic" 

202 

203 # Phase 1: Preparation (backup and validation) 

204 self.executor.execute_preparation_phase( 

205 self.path, backup_enabled, progress_callback 

206 ) 

207 

208 # Phase 2: Directory (create directories) 

209 self.executor.execute_directory_phase(self.path, progress_callback) 

210 

211 # Prepare config for template variable substitution (Phase 3) 

212 config = { 

213 "name": self.path.name, 

214 "mode": mode, 

215 "locale": locale, 

216 "language": detected_language, 

217 "description": "", 

218 "version": "0.1.0", 

219 "author": "@user", 

220 } 

221 

222 # Phase 3: Resource (copy templates with variable substitution) 

223 resource_files = self.executor.execute_resource_phase( 

224 self.path, config, progress_callback 

225 ) 

226 

227 # Post-Phase 3: Fix shell script permissions 

228 # git may not preserve file permissions, so explicitly set them 

229 scripts_dir = self.path / ".moai" / "scripts" 

230 if scripts_dir.exists(): 

231 for script_file in scripts_dir.glob("*.sh"): 

232 try: 

233 current_mode = script_file.stat().st_mode 

234 new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH 

235 # On Windows, chmod has limited effect, but we try anyway 

236 # or check os.name != 'nt' if strict behavior is needed. 

237 # For now, we just apply it and ignore errors if it fails. 

238 script_file.chmod(new_mode) 

239 except Exception: 

240 pass # Silently ignore permission errors 

241 

242 # Phase 4: Configuration (generate config.json) 

243 # Handle language configuration 

244 language_config = {} 

245 if locale == "other" and custom_language: 

246 language_config = { 

247 "conversation_language": "other", 

248 "conversation_language_name": custom_language, 

249 } 

250 elif locale in ["ko", "en", "ja", "zh"]: 

251 language_names = { 

252 "ko": "Korean", 

253 "en": "English", 

254 "ja": "Japanese", 

255 "zh": "Chinese", 

256 } 

257 language_config = { 

258 "conversation_language": locale, 

259 "conversation_language_name": language_names.get(locale, "English"), 

260 } 

261 else: 

262 # Default fallback 

263 language_config = { 

264 "conversation_language": locale, 

265 "conversation_language_name": "English", 

266 } 

267 

268 config_data: dict[str, str | bool | dict] = { 

269 "project": { 

270 "name": self.path.name, 

271 "mode": mode, 

272 "locale": locale, 

273 "language": detected_language, 

274 # Language detection metadata (will be updated by project-manager via /alfred:0-project) 

275 "language_detection": { 

276 "detected_language": detected_language, 

277 "detection_method": "cli_default", # Will be "context_aware" after /alfred:0-project 

278 "confidence": None, # Will be calculated by project-manager 

279 "markers": [], # Will be populated by project-manager 

280 "confirmed_by": None, # Will be "user" after project-manager confirmation 

281 }, 

282 }, 

283 "language": language_config, 

284 "constitution": { 

285 "enforce_tdd": True, 

286 "principles": { 

287 "simplicity": { 

288 "max_projects": 5, 

289 "notes": "Default recommendation. Adjust in .moai/config.json or via SPEC/ADR with documented rationale based on project size.", 

290 } 

291 }, 

292 "test_coverage_target": 90, 

293 }, 

294 "git_strategy": { 

295 "personal": { 

296 "auto_checkpoint": "disabled", 

297 "checkpoint_events": ["delete", "refactor", "merge", "script", "critical-file"], 

298 "checkpoint_type": "local-branch", 

299 "max_checkpoints": 10, 

300 "cleanup_days": 7, 

301 "push_to_remote": False, 

302 "auto_commit": True, 

303 "branch_prefix": "feature/SPEC-", 

304 "develop_branch": "develop", 

305 "main_branch": "main", 

306 "prevent_branch_creation": False, 

307 "work_on_main": False, 

308 }, 

309 "team": { 

310 "auto_pr": False, 

311 "develop_branch": "develop", 

312 "draft_pr": False, 

313 "feature_prefix": "feature/SPEC-", 

314 "main_branch": "main", 

315 "use_gitflow": True, 

316 "default_pr_base": "develop", 

317 "prevent_main_direct_merge": False, 

318 }, 

319 }, 

320 "session": { 

321 "suppress_setup_messages": False, 

322 "notes": "suppress_setup_messages: false enables SessionStart output. Set to true to suppress messages (show again after 7 days)", 

323 }, 

324 } 

325 config_files = self.executor.execute_configuration_phase( 

326 self.path, config_data, progress_callback 

327 ) 

328 

329 # Phase 5: Validation (verify and finalize) 

330 self.executor.execute_validation_phase(self.path, mode, progress_callback) 

331 

332 # Phase 6: Create runtime memory files (auto-generated per user/session) 

333 memory_files = self._create_memory_files() 

334 

335 # Phase 7: Create user settings (gitignored, user-specific) 

336 user_settings_files = self._create_user_settings() 

337 

338 # Generate result 

339 duration = int((time.time() - start_time) * 1000) # ms 

340 return InstallationResult( 

341 success=True, 

342 project_path=str(self.path), 

343 language=detected_language, 

344 mode=mode, 

345 locale=locale, 

346 duration=duration, 

347 created_files=resource_files + config_files + memory_files + user_settings_files, 

348 ) 

349 

350 except Exception as e: 

351 duration = int((time.time() - start_time) * 1000) 

352 return InstallationResult( 

353 success=False, 

354 project_path=str(self.path), 

355 language=language or "unknown", 

356 mode=mode, 

357 locale=locale, 

358 duration=duration, 

359 created_files=[], 

360 errors=[str(e)], 

361 ) 

362 

363 def is_initialized(self) -> bool: 

364 """Check if .moai/ directory exists 

365 

366 Returns: 

367 Whether initialized 

368 """ 

369 return (self.path / ".moai").exists() 

370 

371 

372def initialize_project( 

373 project_path: Path, progress_callback: ProgressCallback | None = None 

374) -> InstallationResult: 

375 """Initialize project (for CLI command) 

376 

377 Args: 

378 project_path: Project directory path 

379 progress_callback: Progress callback 

380 

381 Returns: 

382 InstallationResult object 

383 """ 

384 initializer = ProjectInitializer(project_path) 

385 return initializer.initialize(progress_callback=progress_callback)