Coverage for src / moai_adk / core / command_helpers.py: 20.55%
73 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"""
2Command Helper Utilities
4Provides helper functions for commands to interact with ContextManager
5and perform common operations like context extraction and validation.
6"""
8import json
9import os
10from datetime import datetime, timezone
11from typing import Any, Dict, List, Optional
13# Conditional import of ContextManager
14try:
15 from moai_adk.core.context_manager import (
16 ContextManager,
17 validate_and_convert_path,
18 validate_no_template_vars,
19 )
21 CONTEXT_MANAGER_AVAILABLE = True
22except ImportError:
23 CONTEXT_MANAGER_AVAILABLE = False
26def extract_project_metadata(project_root: str) -> Dict[str, Any]:
27 """
28 Extract project metadata from config.json.
30 Args:
31 project_root: Root directory of the project
33 Returns:
34 Dictionary containing project metadata
36 Raises:
37 FileNotFoundError: If config.json doesn't exist
38 json.JSONDecodeError: If config.json is invalid
39 """
40 config_path = os.path.join(project_root, ".moai", "config", "config.json")
42 if not os.path.exists(config_path):
43 raise FileNotFoundError(f"Config file not found: {config_path}")
45 with open(config_path, "r", encoding="utf-8") as f:
46 config = json.load(f)
48 # Extract key metadata
49 metadata = {
50 "project_name": config.get("project", {}).get("name", "Unknown"),
51 "mode": config.get("project", {}).get("mode", "personal"),
52 "owner": config.get("project", {}).get("owner", "@user"),
53 "language": config.get("language", {}).get("conversation_language", "en"),
54 "tech_stack": [], # To be detected separately
55 }
57 return metadata
60def detect_tech_stack(project_root: str) -> List[str]:
61 """
62 Detect primary tech stack from project structure.
64 Checks for common project indicator files.
66 Args:
67 project_root: Root directory of the project
69 Returns:
70 List of detected languages/frameworks
71 """
72 indicators = {
73 "pyproject.toml": "python",
74 "package.json": "javascript",
75 "go.mod": "go",
76 "Cargo.toml": "rust",
77 "pom.xml": "java",
78 "Gemfile": "ruby",
79 }
81 tech_stack = []
83 for indicator_file, language in indicators.items():
84 if os.path.exists(os.path.join(project_root, indicator_file)):
85 tech_stack.append(language)
87 # Default to python if nothing detected
88 if not tech_stack:
89 tech_stack.append("python")
91 return tech_stack
94def build_phase_result(
95 phase_name: str,
96 status: str,
97 outputs: Dict[str, Any],
98 files_created: List[str],
99 next_phase: Optional[str] = None,
100) -> Dict[str, Any]:
101 """
102 Build standardized phase result dictionary.
104 Args:
105 phase_name: Name of the phase (e.g., "0-project")
106 status: Phase status (completed/error/interrupted)
107 outputs: Dictionary of phase outputs
108 files_created: List of created files (absolute paths)
109 next_phase: Optional next phase name
111 Returns:
112 Standardized phase result dictionary
113 """
114 phase_result = {
115 "phase": phase_name,
116 "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
117 "status": status,
118 "outputs": outputs,
119 "files_created": files_created,
120 }
122 if next_phase:
123 phase_result["next_phase"] = next_phase
125 return phase_result
128def validate_phase_files(relative_paths: List[str], project_root: str) -> List[str]:
129 """
130 Validate and convert relative file paths to absolute paths.
132 Handles errors gracefully by logging warnings and skipping invalid paths.
134 Args:
135 relative_paths: List of relative file paths
136 project_root: Project root directory
138 Returns:
139 List of validated absolute paths
140 """
141 if not CONTEXT_MANAGER_AVAILABLE:
142 # Fallback: simple absolute path conversion
143 return [os.path.abspath(os.path.join(project_root, p)) for p in relative_paths]
145 absolute_paths = []
147 for rel_path in relative_paths:
148 try:
149 abs_path = validate_and_convert_path(rel_path, project_root)
150 absolute_paths.append(abs_path)
151 except (ValueError, FileNotFoundError) as e:
152 # Log warning but continue processing
153 print(f"Warning: Could not validate path '{rel_path}': {e}")
155 return absolute_paths
158def _prepare_phase_data(
159 phase_name: str,
160 status: str,
161 outputs: Dict[str, Any],
162 absolute_paths: List[str],
163 next_phase: Optional[str],
164) -> Dict[str, Any]:
165 """
166 Prepare phase data for saving.
168 Args:
169 phase_name: Name of the phase
170 status: Phase status
171 outputs: Phase outputs
172 absolute_paths: List of absolute file paths
173 next_phase: Optional next phase
175 Returns:
176 Phase data dictionary ready for saving
177 """
178 phase_data = build_phase_result(
179 phase_name=phase_name,
180 status=status,
181 outputs=outputs,
182 files_created=absolute_paths,
183 next_phase=next_phase,
184 )
186 # Validate no unsubstituted template variables
187 phase_json = json.dumps(phase_data)
188 validate_no_template_vars(phase_json)
190 return phase_data
193def _validate_and_save(context_mgr: Any, phase_data: Dict[str, Any]) -> str:
194 """
195 Validate and save phase data.
197 Args:
198 context_mgr: ContextManager instance
199 phase_data: Phase data to save
201 Returns:
202 Path to saved file
203 """
204 saved_path = context_mgr.save_phase_result(phase_data)
205 print(f"✓ Phase context saved: {os.path.basename(saved_path)}")
206 return saved_path
209def save_command_context(
210 phase_name: str,
211 project_root: str,
212 outputs: Dict[str, Any],
213 files_created: List[str],
214 next_phase: Optional[str] = None,
215 status: str = "completed",
216) -> Optional[str]:
217 """
218 Save command phase context using ContextManager.
220 This is a convenience wrapper for commands to save phase results
221 with proper error handling.
223 Args:
224 phase_name: Name of the phase (e.g., "0-project")
225 project_root: Project root directory
226 outputs: Phase-specific outputs
227 files_created: List of relative file paths
228 next_phase: Optional next phase recommendation
229 status: Phase status (default: "completed")
231 Returns:
232 Path to saved file, or None if save failed
233 """
234 if not CONTEXT_MANAGER_AVAILABLE:
235 print("Warning: ContextManager not available. Phase context not saved.")
236 return None
238 try:
239 context_mgr = ContextManager(project_root)
240 absolute_paths = validate_phase_files(files_created, project_root)
241 phase_data = _prepare_phase_data(
242 phase_name, status, outputs, absolute_paths, next_phase
243 )
244 return _validate_and_save(context_mgr, phase_data)
246 except Exception as e:
247 print(f"Warning: Failed to save phase context: {e}")
248 print("Command execution continues normally.")
249 return None
252def load_previous_phase(project_root: str) -> Optional[Dict[str, Any]]:
253 """
254 Load the most recent phase result.
256 Args:
257 project_root: Project root directory
259 Returns:
260 Phase result dictionary, or None if unavailable
261 """
262 if not CONTEXT_MANAGER_AVAILABLE:
263 return None
265 try:
266 context_mgr = ContextManager(project_root)
267 return context_mgr.load_latest_phase()
268 except Exception as e:
269 print(f"Warning: Could not load previous phase: {e}")
270 return None