Coverage for src / moai_adk / core / project / checker.py: 29.51%
61 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"""System requirements validation module.
3Checks whether required and optional tools are installed.
4"""
6import platform
7import shutil
8import subprocess
9import sys
10from pathlib import Path
13class SystemChecker:
14 """Validate system requirements."""
16 REQUIRED_TOOLS: dict[str, str] = {
17 "git": "git --version",
18 "python": "python3 --version",
19 }
21 OPTIONAL_TOOLS: dict[str, str] = {
22 "gh": "gh --version",
23 "docker": "docker --version",
24 }
26 LANGUAGE_TOOLS: dict[str, dict[str, list[str]]] = {
27 "python": {
28 "required": ["python3", "pip"],
29 "recommended": ["pytest", "mypy", "ruff"],
30 "optional": ["black", "pylint"],
31 },
32 "typescript": {
33 "required": ["node", "npm"],
34 "recommended": ["vitest", "biome"],
35 "optional": ["typescript", "eslint"],
36 },
37 "javascript": {
38 "required": ["node", "npm"],
39 "recommended": ["jest", "eslint"],
40 "optional": ["prettier", "webpack"],
41 },
42 "java": {
43 "required": ["java", "javac"],
44 "recommended": ["maven", "gradle"],
45 "optional": ["junit", "checkstyle"],
46 },
47 "go": {
48 "required": ["go"],
49 "recommended": ["golangci-lint", "gofmt"],
50 "optional": ["delve", "gopls"],
51 },
52 "rust": {
53 "required": ["rustc", "cargo"],
54 "recommended": ["rustfmt", "clippy"],
55 "optional": ["rust-analyzer", "cargo-audit"],
56 },
57 "dart": {
58 "required": ["dart"],
59 "recommended": ["flutter", "dart_test"],
60 "optional": ["dartfmt", "dartanalyzer"],
61 },
62 "swift": {
63 "required": ["swift", "swiftc"],
64 "recommended": ["xcrun", "swift-format"],
65 "optional": ["swiftlint", "sourcekit-lsp"],
66 },
67 "kotlin": {
68 "required": ["kotlin", "kotlinc"],
69 "recommended": ["gradle", "ktlint"],
70 "optional": ["detekt", "kotlin-language-server"],
71 },
72 "csharp": {
73 "required": ["dotnet"],
74 "recommended": ["msbuild", "nuget"],
75 "optional": ["csharpier", "roslyn"],
76 },
77 "php": {
78 "required": ["php"],
79 "recommended": ["composer", "phpunit"],
80 "optional": ["psalm", "phpstan"],
81 },
82 "ruby": {
83 "required": ["ruby", "gem"],
84 "recommended": ["bundler", "rspec"],
85 "optional": ["rubocop", "solargraph"],
86 },
87 "elixir": {
88 "required": ["elixir", "mix"],
89 "recommended": ["hex", "dialyzer"],
90 "optional": ["credo", "ex_unit"],
91 },
92 "scala": {
93 "required": ["scala", "scalac"],
94 "recommended": ["sbt", "scalatest"],
95 "optional": ["scalafmt", "metals"],
96 },
97 "clojure": {
98 "required": ["clojure", "clj"],
99 "recommended": ["leiningen", "clojure.test"],
100 "optional": ["cider", "clj-kondo"],
101 },
102 "haskell": {
103 "required": ["ghc", "ghci"],
104 "recommended": ["cabal", "stack"],
105 "optional": ["hlint", "haskell-language-server"],
106 },
107 "c": {
108 "required": ["gcc", "make"],
109 "recommended": ["clang", "cmake"],
110 "optional": ["gdb", "valgrind"],
111 },
112 "cpp": {
113 "required": ["g++", "make"],
114 "recommended": ["clang++", "cmake"],
115 "optional": ["gdb", "cppcheck"],
116 },
117 "lua": {
118 "required": ["lua"],
119 "recommended": ["luarocks", "busted"],
120 "optional": ["luacheck", "lua-language-server"],
121 },
122 "ocaml": {
123 "required": ["ocaml", "opam"],
124 "recommended": ["dune", "ocamlformat"],
125 "optional": ["merlin", "ocp-indent"],
126 },
127 }
129 def check_all(self) -> dict[str, bool]:
130 """Validate every tool.
132 Returns:
133 Dictionary mapping tool names to availability.
134 """
135 result = {}
137 # Check required tools
138 for tool, command in self.REQUIRED_TOOLS.items():
139 result[tool] = self._check_tool(command)
141 # Check optional tools
142 for tool, command in self.OPTIONAL_TOOLS.items():
143 result[tool] = self._check_tool(command)
145 return result
147 def _check_tool(self, command: str) -> bool:
148 """Check an individual tool.
150 Args:
151 command: Command to run (e.g., "git --version").
153 Returns:
154 True when the tool is available.
155 """
156 if not command:
157 return False
159 try:
160 # Extract the tool name (first token)
161 tool_name = command.split()[0]
162 # Determine availability via shutil.which
163 return shutil.which(tool_name) is not None
164 except Exception:
165 return False
167 def check_language_tools(self, language: str | None) -> dict[str, bool]:
168 """Validate toolchains by language.
170 Args:
171 language: Programming language name (e.g., "python", "typescript").
173 Returns:
174 Dictionary mapping tool names to availability.
175 """
176 # Guard clause: no language specified
177 if not language:
178 return {}
180 language_lower = language.lower()
182 # Guard clause: unsupported language
183 if language_lower not in self.LANGUAGE_TOOLS:
184 return {}
186 # Retrieve tool configuration for the language
187 tools_config = self.LANGUAGE_TOOLS[language_lower]
189 # Evaluate tools by category and collect results
190 result: dict[str, bool] = {}
191 for category in ["required", "recommended", "optional"]:
192 tools = tools_config.get(category, [])
193 for tool in tools:
194 result[tool] = self._is_tool_available(tool)
196 return result
198 def _is_tool_available(self, tool: str) -> bool:
199 """Check whether a tool is available (helper).
201 Args:
202 tool: Tool name.
204 Returns:
205 True when the tool is available.
206 """
207 return shutil.which(tool) is not None
209 def get_tool_version(self, tool: str | None) -> str | None:
210 """Retrieve tool version information.
212 Args:
213 tool: Tool name (for example, "python3", "node").
215 Returns:
216 Version string or None when the tool is unavailable.
217 """
218 # Guard clause: unspecified or unavailable tool
219 if not tool or not self._is_tool_available(tool):
220 return None
222 try:
223 # Call the tool with --version to obtain the version string
224 result = subprocess.run(
225 [tool, "--version"],
226 capture_output=True,
227 text=True,
228 timeout=2, # 2-second timeout to respect performance constraints
229 check=False,
230 )
232 # Return the version when the command succeeds
233 if result.returncode == 0 and result.stdout:
234 return self._extract_version_line(result.stdout)
236 return None
238 except (subprocess.TimeoutExpired, OSError):
239 # Gracefully handle timeout and OS errors
240 return None
242 def _extract_version_line(self, version_output: str) -> str:
243 """Extract the first line from version output (helper).
245 Args:
246 version_output: Output captured from the --version command.
248 Returns:
249 First line containing version information.
250 """
251 return version_output.strip().split("\n")[0]
254def check_environment() -> dict[str, bool]:
255 """Validate the overall environment (used by the CLI doctor command).
257 Returns:
258 Mapping from check description to boolean status.
259 """
260 return {
261 "Python >= 3.13": sys.version_info >= (3, 13),
262 "Git installed": shutil.which("git") is not None,
263 "Project structure (.moai/)": Path(".moai").exists(),
264 "Config file (.moai/config/config.json)": Path(
265 ".moai/config/config.json"
266 ).exists(),
267 }
270def get_platform_specific_message(unix_message: str, windows_message: str) -> str:
271 """Return platform-specific message.
273 Args:
274 unix_message: Message for Unix/Linux/macOS.
275 windows_message: Message for Windows.
277 Returns:
278 Platform-appropriate message.
280 Examples:
281 >>> get_platform_specific_message("chmod 755 .moai", "Check directory permissions")
282 'chmod 755 .moai' # on Unix/Linux/macOS
283 >>> get_platform_specific_message("chmod 755 .moai", "Check directory permissions")
284 'Check directory permissions' # on Windows
285 """
286 if platform.system() == "Windows":
287 return windows_message
288 return unix_message
291def get_permission_fix_message(path: str) -> str:
292 """Get platform-specific permission fix message.
294 Args:
295 path: Path to fix permissions for.
297 Returns:
298 Platform-specific fix instructions.
299 """
300 if platform.system() == "Windows":
301 return f"Run with administrator privileges or verify permissions in the properties of the '{path}' directory"
302 return f"Run 'chmod 755 {path}' and try again"