Coverage for src / moai_adk / cli / commands / init.py: 13.21%

159 statements  

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

1# SPEC: SPEC-CLI-001.md, SPEC-INIT-003.md 

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

3"""MoAI-ADK init command 

4 

5Project initialization command (interactive/non-interactive): 

6- Interactive Mode: Ask user for project settings 

7- Non-Interactive Mode: Use defaults or CLI options 

8 

9## Skill Invocation Guide (English-Only) 

10 

11### Related Skills 

12- **moai-foundation-langs**: For language detection and stack configuration 

13 - Trigger: When language parameter is not specified (auto-detection) 

14 - Invocation: Called implicitly during project initialization for language matrix detection 

15 

16### When to Invoke Skills in Related Workflows 

171. **After project initialization**: 

18 - Run `Skill("moai-foundation-trust")` to verify project structure and toolchain 

19 - Run `Skill("moai-foundation-langs")` to validate detected language stack 

20 

212. **Before first SPEC creation**: 

22 - Use `Skill("moai-core-language-detection")` to confirm language selection 

23 

243. **Project reinitialization** (`--force`): 

25 - Skills automatically adapt to new project structure 

26 - No manual intervention required 

27""" 

28 

29import json 

30from pathlib import Path 

31from typing import Sequence 

32 

33import click 

34from rich.console import Console 

35from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn 

36 

37from moai_adk import __version__ 

38from moai_adk.cli.prompts import prompt_project_setup 

39from moai_adk.core.project.initializer import ProjectInitializer 

40from moai_adk.statusline.version_reader import ( 

41 VersionConfig, 

42 VersionReader, 

43) 

44from moai_adk.utils.banner import print_banner, print_welcome_message 

45 

46console = Console() 

47 

48 

49def create_progress_callback(progress: Progress, task_ids: Sequence[TaskID]): 

50 """Create progress callback 

51 

52 Args: 

53 progress: Rich Progress object 

54 task_ids: List of task IDs (one per phase) 

55 

56 Returns: 

57 Progress callback function 

58 """ 

59 

60 def callback(message: str, current: int, total: int) -> None: 

61 """Update progress 

62 

63 Args: 

64 message: Progress message 

65 current: Current phase (1-based) 

66 total: Total phases 

67 """ 

68 # Complete current phase (1-based index → 0-based) 

69 if 1 <= current <= len(task_ids): 

70 progress.update(task_ids[current - 1], completed=1, description=message) 

71 

72 return callback 

73 

74 

75@click.command() 

76@click.argument("path", type=click.Path(), default=".") 

77@click.option( 

78 "--non-interactive", 

79 "-y", 

80 is_flag=True, 

81 help="Non-interactive mode (use defaults)", 

82) 

83@click.option( 

84 "--mode", 

85 type=click.Choice(["personal", "team"]), 

86 default="personal", 

87 help="Project mode", 

88) 

89@click.option( 

90 "--locale", 

91 type=click.Choice(["ko", "en", "ja", "zh"]), 

92 default=None, 

93 help="Preferred language (ko/en/ja/zh, default: en)", 

94) 

95@click.option( 

96 "--language", 

97 type=str, 

98 default=None, 

99 help="Programming language (auto-detect if not specified)", 

100) 

101@click.option( 

102 "--force", 

103 is_flag=True, 

104 help="Force reinitialize without confirmation", 

105) 

106def init( 

107 path: str, 

108 non_interactive: bool, 

109 mode: str, 

110 locale: str, 

111 language: str | None, 

112 force: bool, 

113) -> None: 

114 """Initialize a new MoAI-ADK project 

115 

116 Args: 

117 path: Project directory path (default: current directory) 

118 non_interactive: Skip prompts and use defaults 

119 mode: Project mode (personal/team) 

120 locale: Preferred language (ko/en/ja/zh). Interactive mode supports additional languages. 

121 language: Programming language 

122 with_mcp: Install specific MCP servers (can be used multiple times) 

123 mcp_auto: Auto-install all recommended MCP servers 

124 force: Force reinitialize without confirmation 

125 """ 

126 try: 

127 # 1. Print banner with enhanced version info 

128 print_banner(__version__) 

129 

130 # 2. Enhanced version reading with error handling 

131 try: 

132 version_config = VersionConfig( 

133 cache_ttl_seconds=10, # Very short cache for CLI 

134 fallback_version=__version__, 

135 debug_mode=False, 

136 ) 

137 version_reader = VersionReader(version_config) 

138 current_version = version_reader.get_version() 

139 

140 # Log version info for debugging 

141 console.print(f"[dim]Current MoAI-ADK version: {current_version}[/dim]") 

142 except Exception as e: 

143 console.print(f"[yellow]⚠️ Version read error: {e}[/yellow]") 

144 

145 # 3. Check current directory mode 

146 is_current_dir = path == "." 

147 project_path = Path(path).resolve() 

148 

149 # Initialize variables 

150 custom_language = None 

151 

152 # 3. Interactive vs Non-Interactive 

153 if non_interactive: 

154 # Non-Interactive Mode 

155 console.print( 

156 f"\n[cyan]🚀 Initializing project at {project_path}...[/cyan]\n" 

157 ) 

158 project_name = project_path.name if is_current_dir else path 

159 locale = locale or "en" 

160 # Language detection happens in /alfred:0-project, so default to None here 

161 # This will become "generic" internally, but Summary will show more helpful message 

162 if not language: 

163 language = None 

164 else: 

165 # Interactive Mode 

166 print_welcome_message() 

167 

168 # Interactive prompt 

169 answers = prompt_project_setup( 

170 project_name=None if is_current_dir else path, 

171 is_current_dir=is_current_dir, 

172 project_path=project_path, 

173 initial_locale=locale, 

174 ) 

175 

176 # Override with prompt answers 

177 mode = answers["mode"] 

178 locale = answers["locale"] 

179 language = answers["language"] 

180 project_name = answers["project_name"] 

181 custom_language = answers.get("custom_language") 

182 

183 console.print("\n[cyan]🚀 Starting installation...[/cyan]\n") 

184 

185 if locale is None: 

186 locale = answers["locale"] 

187 

188 # 4. Check for reinitialization (SPEC-INIT-003 v0.3.0) - DEFAULT TO FORCE MODE 

189 initializer = ProjectInitializer(project_path) 

190 

191 if initializer.is_initialized(): 

192 # Always reinitialize without confirmation (force mode by default) 

193 if non_interactive: 

194 console.print( 

195 "\n[green]🔄 Reinitializing project (force mode)...[/green]\n" 

196 ) 

197 else: 

198 # Interactive mode: Simple notification 

199 console.print("\n[cyan]🔄 Reinitializing project...[/cyan]") 

200 console.print(" Backup will be created at .moai-backups/backup/\n") 

201 

202 # 5. Initialize project (Progress Bar with 5 phases) 

203 # Always allow reinit (force mode by default) 

204 is_reinit = initializer.is_initialized() 

205 

206 # Reinit mode: set config.json optimized to false (v0.3.1+) 

207 if is_reinit: 

208 # Migration: Remove old hook files (Issue #163) 

209 old_hook_files = [ 

210 ".claude/hooks/alfred/session_start__startup.py", # v0.8.0 deprecated 

211 ] 

212 for old_file in old_hook_files: 

213 old_path = project_path / old_file 

214 if old_path.exists(): 

215 try: 

216 old_path.unlink() # Remove old file 

217 except Exception: 

218 pass # Ignore removal failures 

219 

220 config_path = project_path / ".moai" / "config" / "config.json" 

221 if config_path.exists(): 

222 try: 

223 with open(config_path, "r", encoding="utf-8") as f: 

224 config_data = json.load(f) 

225 

226 # Update version and optimization flags 

227 if "moai" not in config_data: 

228 config_data["moai"] = {} 

229 

230 # Use enhanced version reader for consistent version handling 

231 try: 

232 version_config = VersionConfig( 

233 cache_ttl_seconds=5, # Very short cache for config update 

234 fallback_version=__version__, 

235 debug_mode=False, 

236 ) 

237 version_reader = VersionReader(version_config) 

238 current_version = version_reader.get_version() 

239 config_data["moai"]["version"] = current_version 

240 except Exception: 

241 # Fallback to package version 

242 config_data["moai"]["version"] = __version__ 

243 

244 if "project" not in config_data: 

245 config_data["project"] = {} 

246 config_data["project"]["optimized"] = False 

247 

248 with open(config_path, "w", encoding="utf-8") as f: 

249 json.dump(config_data, f, indent=2, ensure_ascii=False) 

250 except Exception: 

251 # Ignore read/write failures; config.json is regenerated during initialization 

252 pass 

253 

254 with Progress( 

255 SpinnerColumn(), 

256 TextColumn("[progress.description]{task.description}"), 

257 BarColumn(), 

258 TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), 

259 console=console, 

260 ) as progress: 

261 # Create 5 phase tasks 

262 phase_names = [ 

263 "Phase 1: Preparation and backup...", 

264 "Phase 2: Creating directory structure...", 

265 "Phase 3: Installing resources...", 

266 "Phase 4: Generating configurations...", 

267 "Phase 5: Validation and finalization...", 

268 ] 

269 task_ids = [progress.add_task(name, total=1) for name in phase_names] 

270 callback = create_progress_callback(progress, task_ids) 

271 

272 result = initializer.initialize( 

273 mode=mode, 

274 locale=locale, 

275 language=language, 

276 custom_language=custom_language, 

277 backup_enabled=True, 

278 progress_callback=callback, 

279 reinit=True, # Always allow reinit (force mode by default) 

280 ) 

281 

282 # 6. Output results 

283 if result.success: 

284 separator = "[dim]" + ("─" * 60) + "[/dim]" 

285 console.print( 

286 "\n[green bold]✅ Initialization Completed Successfully![/green bold]" 

287 ) 

288 console.print(separator) 

289 console.print("\n[cyan]📊 Summary:[/cyan]") 

290 console.print(f" [dim]📁 Location:[/dim] {result.project_path}") 

291 # Show language more clearly - "generic" means auto-detect 

292 language_display = ( 

293 "Auto-detect (use /alfred:0-project)" 

294 if result.language == "generic" 

295 else result.language 

296 ) 

297 console.print(f" [dim]🌐 Language:[/dim] {language_display}") 

298 console.print(f" [dim]🔧 Mode:[/dim] {result.mode}") 

299 console.print(f" [dim]🌍 Locale:[/dim] {result.locale}") 

300 console.print( 

301 f" [dim]📄 Files:[/dim] {len(result.created_files)} created" 

302 ) 

303 console.print(f" [dim]⏱️ Duration:[/dim] {result.duration}ms") 

304 

305 # Show backup info if reinitialized 

306 if is_reinit: 

307 backup_dir = project_path / ".moai-backups" 

308 if backup_dir.exists(): 

309 latest_backup = max( 

310 backup_dir.iterdir(), key=lambda p: p.stat().st_mtime 

311 ) 

312 console.print(f" [dim]💾 Backup:[/dim] {latest_backup.name}/") 

313 

314 console.print(f"\n{separator}") 

315 

316 # Show config merge notice if reinitialized 

317 if is_reinit: 

318 console.print("\n[yellow]⚠️ Configuration Status: optimized=false (merge required)[/yellow]") 

319 console.print() 

320 console.print("[cyan]What Happened:[/cyan]") 

321 console.print(" ✅ Template files updated to latest version") 

322 console.print(" 💾 Your previous settings backed up in: [cyan].moai-backups/backup/[/cyan]") 

323 console.print(" ⏳ Configuration merge required") 

324 console.print() 

325 console.print("[cyan]What is optimized=false?[/cyan]") 

326 console.print(" • Template version changed (you get new features)") 

327 console.print(" • Your previous settings are safe (backed up)") 

328 console.print(" • Next: Run /alfred:0-project to merge") 

329 console.print() 

330 console.print("[cyan]What Happens Next:[/cyan]") 

331 console.print(" 1. Run [bold]/alfred:0-project[/bold] in Claude Code") 

332 console.print(" 2. System intelligently merges old settings + new template") 

333 console.print(" 3. After successful merge → optimized becomes true") 

334 console.print(" 4. You're ready to continue developing\n") 

335 

336 console.print("\n[cyan]🚀 Next Steps:[/cyan]") 

337 if not is_current_dir: 

338 console.print( 

339 f" [blue]1.[/blue] Run [bold]cd {project_name}[/bold] to enter the project" 

340 ) 

341 console.print( 

342 " [blue]2.[/blue] Run [bold]/alfred:0-project[/bold] in Claude Code for full setup" 

343 ) 

344 console.print( 

345 " (Configure: mode, language, report generation, etc.)" 

346 ) 

347 else: 

348 console.print( 

349 " [blue]1.[/blue] Run [bold]/alfred:0-project[/bold] in Claude Code for full setup" 

350 ) 

351 console.print( 

352 " (Configure: mode, language, report generation, etc.)" 

353 ) 

354 

355 if not is_current_dir: 

356 console.print(" [blue]3.[/blue] Start developing with MoAI-ADK!\n") 

357 else: 

358 console.print(" [blue]2.[/blue] Start developing with MoAI-ADK!\n") 

359 else: 

360 console.print("\n[red bold]❌ Initialization Failed![/red bold]") 

361 if result.errors: 

362 console.print("\n[red]Errors:[/red]") 

363 for error in result.errors: 

364 console.print(f" [red]•[/red] {error}") 

365 console.print() 

366 raise click.ClickException("Installation failed") 

367 

368 except KeyboardInterrupt: 

369 console.print("\n\n[yellow]⚠ Initialization cancelled by user[/yellow]\n") 

370 raise click.Abort() 

371 except FileExistsError as e: 

372 console.print("\n[yellow]⚠ Project already initialized[/yellow]") 

373 console.print( 

374 "[dim] Use 'python -m moai_adk status' to check configuration[/dim]\n" 

375 ) 

376 raise click.Abort() from e 

377 except Exception as e: 

378 console.print(f"\n[red]✗ Initialization failed: {e}[/red]\n") 

379 raise click.ClickException(str(e)) from e 

380 finally: 

381 # Explicitly flush output buffer 

382 console.file.flush()