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
« 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
5Project initialization command (interactive/non-interactive):
6- Interactive Mode: Ask user for project settings
7- Non-Interactive Mode: Use defaults or CLI options
9## Skill Invocation Guide (English-Only)
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
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
212. **Before first SPEC creation**:
22 - Use `Skill("moai-core-language-detection")` to confirm language selection
243. **Project reinitialization** (`--force`):
25 - Skills automatically adapt to new project structure
26 - No manual intervention required
27"""
29import json
30from pathlib import Path
31from typing import Sequence
33import click
34from rich.console import Console
35from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn
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
46console = Console()
49def create_progress_callback(progress: Progress, task_ids: Sequence[TaskID]):
50 """Create progress callback
52 Args:
53 progress: Rich Progress object
54 task_ids: List of task IDs (one per phase)
56 Returns:
57 Progress callback function
58 """
60 def callback(message: str, current: int, total: int) -> None:
61 """Update progress
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)
72 return callback
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
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__)
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()
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]")
145 # 3. Check current directory mode
146 is_current_dir = path == "."
147 project_path = Path(path).resolve()
149 # Initialize variables
150 custom_language = None
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()
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 )
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")
183 console.print("\n[cyan]🚀 Starting installation...[/cyan]\n")
185 if locale is None:
186 locale = answers["locale"]
188 # 4. Check for reinitialization (SPEC-INIT-003 v0.3.0) - DEFAULT TO FORCE MODE
189 initializer = ProjectInitializer(project_path)
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")
202 # 5. Initialize project (Progress Bar with 5 phases)
203 # Always allow reinit (force mode by default)
204 is_reinit = initializer.is_initialized()
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
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)
226 # Update version and optimization flags
227 if "moai" not in config_data:
228 config_data["moai"] = {}
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__
244 if "project" not in config_data:
245 config_data["project"] = {}
246 config_data["project"]["optimized"] = False
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
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)
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 )
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")
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}/")
314 console.print(f"\n{separator}")
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")
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 )
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")
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()