Coverage for src / moai_adk / cli / commands / doctor.py: 16.67%

132 statements  

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

1"""MoAI-ADK doctor command 

2 

3System diagnostics command: 

4- Check the Python version 

5- Verify Git installation 

6- Validate project structure 

7- Inspect language-specific tool chains 

8- Diagnose slash command loading issues (--check-commands) 

9 

10## Skill Invocation Guide (English-Only) 

11 

12### Related Skills 

13- **moai-foundation-langs**: For language toolchain verification and detection 

14 - Trigger: Use `--verbose` or `--fix` flag to inspect language-specific tools 

15 - Invocation: `Skill("moai-foundation-langs")` for detailed language stack analysis 

16 

17- **moai-foundation-trust**: For TRUST 5-principles verification after fixing tools 

18 - Trigger: After running doctor with `--fix` to validate improvements 

19 - Invocation: `Skill("moai-foundation-trust")` to verify code quality toolchain 

20 

21### When to Invoke Skills in Related Workflows 

221. **After doctor diagnosis**: 

23 - Run `Skill("moai-foundation-trust")` to validate that all TRUST tools are properly configured 

24 - Run `Skill("moai-foundation-langs")` to confirm language-specific toolchains 

25 

262. **When tools are missing** (`--fix` flag): 

27 - Use suggested fixes from doctor command 

28 - Follow up with `Skill("moai-foundation-langs")` to validate corrections 

29 

303. **Debugging slash command issues** (`--check-commands`): 

31 - Run `Skill("moai-cc-commands")` if commands fail to load 

32 - Check `.claude/commands/` directory structure and permissions 

33""" 

34 

35import json 

36from pathlib import Path 

37 

38import click 

39import questionary 

40from rich.console import Console 

41from rich.table import Table 

42 

43from moai_adk.core.project.checker import SystemChecker, check_environment 

44from moai_adk.core.project.detector import detect_project_language 

45 

46console = Console() 

47 

48 

49@click.command() 

50@click.option( 

51 "--verbose", 

52 "-v", 

53 is_flag=True, 

54 help="Show detailed tool versions and language detection", 

55) 

56@click.option("--fix", is_flag=True, help="Suggest fixes for missing tools") 

57@click.option("--export", type=click.Path(), help="Export diagnostics to JSON file") 

58@click.option("--check", type=str, help="Check specific tool only") 

59@click.option( 

60 "--check-commands", is_flag=True, help="Diagnose slash command loading issues" 

61) 

62def doctor( 

63 verbose: bool, 

64 fix: bool, 

65 export: str | None, 

66 check: str | None, 

67 check_commands: bool, 

68) -> None: 

69 """Check system requirements and project health 

70 

71 Verifies: 

72 - Python version (>= 3.13) 

73 - Git installation 

74 - Project structure (.moai directory) 

75 - Language-specific tool chains (20+ languages) 

76 """ 

77 try: 

78 # Handle --check-commands option first 

79 if check_commands: 

80 _check_slash_commands() 

81 return 

82 

83 console.print("[cyan]Running system diagnostics...[/cyan]\n") 

84 

85 # Run basic environment checks 

86 results = check_environment() 

87 diagnostics_data: dict = {"basic_checks": results} 

88 

89 # In verbose mode, verify language-specific toolchains 

90 if verbose or fix: 

91 language = detect_project_language() 

92 diagnostics_data["detected_language"] = language 

93 

94 if verbose: 

95 console.print( 

96 f"[dim]Detected language: {language or 'Unknown'}[/dim]\n" 

97 ) 

98 

99 if language: 

100 checker = SystemChecker() 

101 language_tools = checker.check_language_tools(language) 

102 diagnostics_data["language_tools"] = language_tools 

103 

104 if verbose: 

105 _display_language_tools(language, language_tools, checker) 

106 

107 # Specific tool check 

108 if check: 

109 _check_specific_tool(check) 

110 return 

111 

112 # Build the base results table 

113 table = Table(show_header=True, header_style="bold magenta") 

114 table.add_column("Check", style="dim", width=40) 

115 table.add_column("Status", justify="center") 

116 

117 for check_name, status in results.items(): 

118 icon = "✓" if status else "✗" 

119 color = "green" if status else "red" 

120 table.add_row(check_name, f"[{color}]{icon}[/{color}]") 

121 

122 console.print(table) 

123 

124 # In fix mode, suggest installation commands for missing tools 

125 if fix and "language_tools" in diagnostics_data: 

126 _suggest_fixes( 

127 diagnostics_data["language_tools"], 

128 diagnostics_data.get("detected_language"), 

129 ) 

130 

131 # When exporting, write diagnostics to JSON 

132 if export: 

133 _export_diagnostics(export, diagnostics_data) 

134 

135 # Summarize the overall result 

136 all_passed = all(results.values()) 

137 if all_passed: 

138 console.print("\n[green]✓ All checks passed[/green]") 

139 else: 

140 console.print("\n[yellow]⚠ Some checks failed[/yellow]") 

141 console.print( 

142 "[dim]Run [cyan]python -m moai_adk doctor --verbose[/cyan] for detailed diagnostics[/dim]" 

143 ) 

144 

145 except Exception as e: 

146 console.print(f"[red]✗ Diagnostic failed: {e}[/red]") 

147 raise 

148 

149 

150def _display_language_tools( 

151 language: str, tools: dict[str, bool], checker: SystemChecker 

152) -> None: 

153 """Display a table of language-specific tools (helper)""" 

154 table = Table( 

155 show_header=True, header_style="bold cyan", title=f"{language.title()} Tools" 

156 ) 

157 table.add_column("Tool", style="dim") 

158 table.add_column("Status", justify="center") 

159 table.add_column("Version", style="blue") 

160 

161 for tool, available in tools.items(): 

162 icon = "✓" if available else "✗" 

163 color = "green" if available else "red" 

164 version = checker.get_tool_version(tool) if available else "not installed" 

165 

166 table.add_row(tool, f"[{color}]{icon}[/{color}]", version or "") 

167 

168 console.print(table) 

169 console.print() 

170 

171 

172def _check_specific_tool(tool: str) -> None: 

173 """Check only a specific tool (helper)""" 

174 checker = SystemChecker() 

175 available = checker._is_tool_available(tool) 

176 version = checker.get_tool_version(tool) if available else None 

177 

178 if available: 

179 console.print(f"[green]✓ {tool} is installed[/green]") 

180 if version: 

181 console.print(f" Version: {version}") 

182 else: 

183 console.print(f"[red]✗ {tool} is not installed[/red]") 

184 

185 

186def _suggest_fixes(tools: dict[str, bool], language: str | None) -> None: 

187 """Suggest installation commands for missing tools (helper)""" 

188 missing_tools = [tool for tool, available in tools.items() if not available] 

189 

190 if not missing_tools: 

191 console.print("\n[green]✓ All tools are installed[/green]") 

192 return 

193 

194 console.print(f"\n[yellow]⚠ Missing {len(missing_tools)} tool(s)[/yellow]") 

195 

196 try: 

197 proceed = questionary.confirm( 

198 "Would you like to see install suggestions for missing tools?", 

199 default=True, 

200 ).ask() 

201 except Exception: 

202 proceed = True 

203 

204 if not proceed: 

205 console.print("[yellow]User skipped install suggestions[/yellow]") 

206 return 

207 

208 for tool in missing_tools: 

209 install_cmd = _get_install_command(tool, language) 

210 console.print(f" [red]✗[/red] {tool}") 

211 if install_cmd: 

212 console.print(f" Install: [cyan]{install_cmd}[/cyan]") 

213 

214 

215def _get_install_command(tool: str, language: str | None) -> str: 

216 """Return the install command for a given tool (helper)""" 

217 # Common tools with preferred package managers 

218 install_commands = { 

219 # Python tools (prefer uv) 

220 "pytest": "uv pip install pytest", 

221 "mypy": "uv pip install mypy", 

222 "ruff": "uv pip install ruff", 

223 # JavaScript tools 

224 "vitest": "npm install -D vitest", 

225 "biome": "npm install -D @biomejs/biome", 

226 "eslint": "npm install -D eslint", 

227 "jest": "npm install -D jest", 

228 } 

229 

230 return install_commands.get(tool, f"# Install {tool} for {language}") 

231 

232 

233def _export_diagnostics(export_path: str, data: dict) -> None: 

234 """Export diagnostic results to a JSON file (helper)""" 

235 try: 

236 output = Path(export_path) 

237 output.write_text(json.dumps(data, indent=2)) 

238 console.print(f"\n[green]✓ Diagnostics exported to {export_path}[/green]") 

239 except Exception as e: 

240 console.print(f"\n[red]✗ Failed to export diagnostics: {e}[/red]") 

241 

242 

243def _check_slash_commands() -> None: 

244 """Check slash command loading issues (helper)""" 

245 from moai_adk.core.diagnostics.slash_commands import diagnose_slash_commands 

246 

247 console.print("[cyan]Running slash command diagnostics...[/cyan]\n") 

248 

249 result = diagnose_slash_commands() 

250 

251 # Handle error case 

252 if "error" in result: 

253 console.print(f"[red]✗ {result['error']}[/red]") 

254 return 

255 

256 # Build results table 

257 table = Table(show_header=True, header_style="bold magenta") 

258 table.add_column("Command File", style="dim", width=40) 

259 table.add_column("Status", justify="center", width=10) 

260 table.add_column("Issues", style="yellow") 

261 

262 for detail in result["details"]: 

263 icon = "✓" if detail["valid"] else "✗" 

264 color = "green" if detail["valid"] else "red" 

265 issues = ", ".join(detail["errors"]) if detail["errors"] else "-" 

266 

267 table.add_row(detail["file"], f"[{color}]{icon}[/{color}]", issues) 

268 

269 console.print(table) 

270 console.print() 

271 

272 # Summary 

273 total = result["total_files"] 

274 valid = result["valid_commands"] 

275 

276 if valid == total and total > 0: 

277 console.print(f"[green]✓ {valid}/{total} command files are valid[/green]") 

278 elif total == 0: 

279 console.print("[yellow]⚠ No command files found in .claude/commands/[/yellow]") 

280 else: 

281 console.print( 

282 f"[yellow]⚠ Only {valid}/{total} command files are valid[/yellow]" 

283 ) 

284 console.print("[dim]Fix the issues above to enable slash commands[/dim]")