Coverage for src/jtech_installer/installer/chatmodes.py: 93%
204 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-20 15:10 -0300
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-20 15:10 -0300
1#!/usr/bin/env python3
2"""
3Configurador de ChatModes para GitHub Copilot
4Copia e configura chatmodes para integração com VS Code e GitHub Copilot.
5"""
7import re
8import shutil
9from dataclasses import dataclass, field
10from enum import Enum
11from pathlib import Path
12from typing import Any, Dict, List, Optional
14from ..core.models import InstallationConfig
17class ChatModeCompatibility(Enum):
18 """Níveis de compatibilidade com GitHub Copilot."""
20 FULLY_COMPATIBLE = "fully_compatible"
21 MOSTLY_COMPATIBLE = "mostly_compatible"
22 NEEDS_ADJUSTMENT = "needs_adjustment"
23 INCOMPATIBLE = "incompatible"
26@dataclass
27class ChatModeInfo:
28 """Informações sobre um ChatMode."""
30 name: str
31 source_path: Path
32 target_path: Path
33 description: str = ""
34 version: str = "1.0"
35 compatibility: ChatModeCompatibility = (
36 ChatModeCompatibility.FULLY_COMPATIBLE
37 )
38 metadata: Dict[str, Any] = field(default_factory=dict)
39 validation_issues: List[str] = field(default_factory=list)
40 installed: bool = False
43class ChatModeConfigurator:
44 """
45 Configura ChatModes para GitHub Copilot.
47 Responsável por:
48 - Descobrir arquivos *.chatmode.md
49 - Validar compatibilidade com GitHub Copilot
50 - Copiar para .github/chatmodes/
51 - Configurar permissões apropriadas
52 - Verificar integração com VS Code
53 """
55 def __init__(self, config: InstallationConfig):
56 """Inicializa o configurador de ChatModes."""
57 self.config = config
58 self.project_path = Path(config.project_path)
59 self.github_dir = self.project_path / ".github"
60 self.chatmodes_dir = self.github_dir / "chatmodes"
62 self.discovered_chatmodes: List[ChatModeInfo] = []
63 self.installation_log: List[str] = []
65 # Padrões para validação de compatibilidade
66 self.copilot_patterns = {
67 "required_headers": [
68 r"^#\s+.+", # Título principal
69 r"description:", # Descrição
70 ],
71 "recommended_sections": [
72 r"##\s+(role|context|instructions)",
73 r"##\s+(capabilities|limitations)",
74 r"##\s+(examples|usage)",
75 ],
76 "compatibility_issues": [
77 r"@[a-zA-Z]+\s*\(", # Funções não suportadas
78 r"\${[^}]+}", # Variáveis não suportadas
79 r"<script", # Scripts não permitidos
80 ],
81 }
83 def discover_chatmodes(
84 self, source_dir: Optional[Path] = None
85 ) -> List[ChatModeInfo]:
86 """
87 Descobre ChatModes disponíveis para configuração.
89 Args:
90 source_dir: Diretório fonte dos chatmodes (opcional)
92 Returns:
93 Lista de chatmodes descobertos
94 """
95 if source_dir is None:
96 # Procurar chatmodes no próprio projeto
97 search_paths = [
98 self.project_path / "chatmodes",
99 self.project_path / ".github" / "chatmodes",
100 self.project_path / "agents" / "chatmodes",
101 ]
102 else:
103 search_paths = [source_dir]
105 chatmodes = []
107 for search_path in search_paths:
108 if not search_path.exists():
109 continue
111 # Buscar arquivos *.chatmode.md
112 chatmode_pattern = "**/*.chatmode.md"
113 for chatmode_file in search_path.glob(chatmode_pattern):
114 chatmode_info = self._parse_chatmode_file(chatmode_file)
115 if chatmode_info:
116 chatmodes.append(chatmode_info)
118 self.discovered_chatmodes = chatmodes
119 msg = f"Descobertos {len(chatmodes)} chatmodes para configuração"
120 self.installation_log.append(msg)
121 return chatmodes
123 def _parse_chatmode_file(self, file_path: Path) -> Optional[ChatModeInfo]:
124 """
125 Parseia arquivo de ChatMode e extrai informações.
127 Args:
128 file_path: Caminho do arquivo
130 Returns:
131 ChatModeInfo ou None se erro
132 """
133 try:
134 # Extrair nome do arquivo
135 name = file_path.stem.replace(".chatmode", "")
137 # Determinar caminho alvo
138 target_path = self.chatmodes_dir / file_path.name
140 # Ler e analisar conteúdo
141 content = file_path.read_text(encoding="utf-8")
143 # Extrair metadados
144 metadata = self._extract_chatmode_metadata(content)
145 description = metadata.get("description", f"ChatMode - {name}")
146 version = metadata.get("version", "1.0")
148 # Validar compatibilidade
149 compatibility, issues = self._validate_copilot_compatibility(
150 content
151 )
153 return ChatModeInfo(
154 name=name,
155 source_path=file_path,
156 target_path=target_path,
157 description=description,
158 version=str(version),
159 compatibility=compatibility,
160 metadata=metadata,
161 validation_issues=issues,
162 )
164 except Exception as e:
165 msg = f"Erro ao parsear ChatMode {file_path}: {e}"
166 self.installation_log.append(msg)
167 return None
169 def _extract_chatmode_metadata(self, content: str) -> Dict[str, Any]:
170 """
171 Extrai metadados do ChatMode.
173 Args:
174 content: Conteúdo do arquivo
176 Returns:
177 Dicionário com metadados
178 """
179 metadata = {}
181 # Extrair título
182 title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
183 if title_match:
184 metadata["title"] = title_match.group(1).strip()
186 # Extrair descrição (várias formas possíveis)
187 desc_patterns = [
188 r"^description:\s*(.+)$",
189 r"^##\s*description\s*\n(.+?)(?=\n##|\n#|\Z)",
190 r"^\*\*description:\*\*\s*(.+)$",
191 ]
193 for pattern in desc_patterns:
194 desc_match = re.search(
195 pattern, content, re.MULTILINE | re.IGNORECASE
196 )
197 if desc_match:
198 metadata["description"] = desc_match.group(1).strip()
199 break
201 # Extrair versão
202 version_match = re.search(
203 r"^version:\s*(.+)$", content, re.MULTILINE | re.IGNORECASE
204 )
205 if version_match:
206 metadata["version"] = version_match.group(1).strip()
208 # Extrair tags/categorias
209 tags_match = re.search(
210 r"^tags:\s*(.+)$", content, re.MULTILINE | re.IGNORECASE
211 )
212 if tags_match:
213 tags_str = tags_match.group(1).strip()
214 metadata["tags"] = [tag.strip() for tag in tags_str.split(",")]
216 return metadata
218 def _validate_copilot_compatibility(
219 self, content: str
220 ) -> tuple[ChatModeCompatibility, List[str]]:
221 """
222 Valida compatibilidade com GitHub Copilot.
224 Args:
225 content: Conteúdo do ChatMode
227 Returns:
228 Tupla com nível de compatibilidade e lista de issues
229 """
230 issues = []
232 # Verificar cabeçalhos obrigatórios
233 missing_headers = []
234 for pattern in self.copilot_patterns["required_headers"]:
235 if not re.search(pattern, content, re.MULTILINE | re.IGNORECASE):
236 missing_headers.append(pattern)
238 if missing_headers:
239 msg = f"Cabeçalhos obrigatórios faltando: {missing_headers}"
240 issues.append(msg)
242 # Verificar seções recomendadas
243 missing_sections = 0
244 for pattern in self.copilot_patterns["recommended_sections"]:
245 if not re.search(pattern, content, re.MULTILINE | re.IGNORECASE):
246 missing_sections += 1
248 if missing_sections > 1:
249 issues.append(f"Faltam {missing_sections} seções recomendadas")
251 # Verificar problemas de compatibilidade
252 compatibility_problems = []
253 for pattern in self.copilot_patterns["compatibility_issues"]:
254 matches = re.findall(pattern, content, re.IGNORECASE)
255 if matches:
256 compatibility_problems.extend(matches)
258 if compatibility_problems:
259 issues.append(f"Elementos incompatíveis: {compatibility_problems}")
261 # Determinar nível de compatibilidade
262 if not issues:
263 compatibility = ChatModeCompatibility.FULLY_COMPATIBLE
264 elif len(issues) == 1 and "seções recomendadas" in issues[0]:
265 compatibility = ChatModeCompatibility.MOSTLY_COMPATIBLE
266 elif missing_headers or compatibility_problems:
267 compatibility = ChatModeCompatibility.INCOMPATIBLE
268 else:
269 compatibility = ChatModeCompatibility.NEEDS_ADJUSTMENT
271 return compatibility, issues
273 def configure_chatmodes(
274 self,
275 chatmodes: Optional[List[ChatModeInfo]] = None,
276 force_install: bool = False,
277 ) -> Dict[str, Any]:
278 """
279 Configura ChatModes para GitHub Copilot.
281 Args:
282 chatmodes: Lista específica de chatmodes (opcional)
283 force_install: Instalar mesmo com problemas de compatibilidade
285 Returns:
286 Resultado da configuração
287 """
288 if chatmodes is None:
289 chatmodes = self.discovered_chatmodes
291 if not chatmodes:
292 return {
293 "success": False,
294 "error": "Nenhum ChatMode descoberto para configuração",
295 "configured_count": 0,
296 "skipped_count": 0,
297 "failed_count": 0,
298 }
300 # Criar diretório de destino
301 self._ensure_chatmodes_directory()
303 configured_chatmodes = []
304 skipped_chatmodes = []
305 failed_chatmodes = []
307 for chatmode in chatmodes:
308 try:
309 # Verificar compatibilidade
310 incompatible = (
311 chatmode.compatibility
312 == ChatModeCompatibility.INCOMPATIBLE
313 )
314 if not force_install and incompatible:
315 skipped_chatmodes.append(chatmode)
316 msg = f"Pulado (incompatível): {chatmode.name}"
317 self.installation_log.append(msg)
318 continue
320 # Copiar arquivo
321 shutil.copy2(chatmode.source_path, chatmode.target_path)
323 # Configurar permissões
324 self._set_chatmode_permissions(chatmode.target_path)
326 chatmode.installed = True
327 configured_chatmodes.append(chatmode)
328 msg = (
329 f"Configurado: {chatmode.name} "
330 f"({chatmode.compatibility.value})"
331 )
332 self.installation_log.append(msg)
334 except Exception as e:
335 failed_chatmodes.append(chatmode)
336 msg = f"Falha na configuração de {chatmode.name}: {e}"
337 self.installation_log.append(msg)
339 # Criar arquivo de configuração do GitHub Copilot se necessário
340 if configured_chatmodes:
341 self._create_copilot_config(configured_chatmodes)
343 return {
344 "success": len(failed_chatmodes) == 0,
345 "configured_count": len(configured_chatmodes),
346 "skipped_count": len(skipped_chatmodes),
347 "failed_count": len(failed_chatmodes),
348 "configured_chatmodes": [cm.name for cm in configured_chatmodes],
349 "skipped_chatmodes": [cm.name for cm in skipped_chatmodes],
350 "failed_chatmodes": [cm.name for cm in failed_chatmodes],
351 "compatibility_summary": self._get_compatibility_summary(
352 chatmodes
353 ),
354 "log": self.installation_log.copy(),
355 }
357 def _ensure_chatmodes_directory(self):
358 """Garante que diretório de chatmodes existe com permissões."""
359 # Criar .github se não existir
360 self.github_dir.mkdir(exist_ok=True)
362 # Criar chatmodes com permissões apropriadas
363 self.chatmodes_dir.mkdir(exist_ok=True)
365 # Configurar permissões (755 para compatibilidade GitHub)
366 if hasattr(self.chatmodes_dir, "chmod"):
367 self.chatmodes_dir.chmod(0o755)
369 def _set_chatmode_permissions(self, file_path: Path):
370 """
371 Configura permissões apropriadas para arquivo ChatMode.
373 Args:
374 file_path: Caminho do arquivo
375 """
376 if hasattr(file_path, "chmod"):
377 # Permissão 644 (leitura para todos, escrita para dono)
378 file_path.chmod(0o644)
380 def _create_copilot_config(self, configured_chatmodes: List[ChatModeInfo]):
381 """
382 Cria arquivo de configuração para GitHub Copilot.
384 Args:
385 configured_chatmodes: Lista de chatmodes configurados
386 """
387 try:
388 config_file = self.chatmodes_dir / ".copilot-config.json"
390 config_data = {
391 "version": "1.0",
392 "chatmodes": [
393 {
394 "name": cm.name,
395 "file": cm.target_path.name,
396 "description": cm.description,
397 "compatibility": cm.compatibility.value,
398 "version": cm.version,
399 }
400 for cm in configured_chatmodes
401 ],
402 "configured_at": None, # Seria preenchido com timestamp
403 "total_chatmodes": len(configured_chatmodes),
404 }
406 import json
408 with open(config_file, "w", encoding="utf-8") as f:
409 json.dump(config_data, f, indent=2, ensure_ascii=False)
411 msg = (
412 f"Configuração do Copilot criada: "
413 f"{len(configured_chatmodes)} chatmodes"
414 )
415 self.installation_log.append(msg)
417 except Exception as e:
418 msg = f"Erro ao criar configuração do Copilot: {e}"
419 self.installation_log.append(msg)
421 def _get_compatibility_summary(
422 self, chatmodes: List[ChatModeInfo]
423 ) -> Dict[str, int]:
424 """
425 Gera resumo de compatibilidade dos chatmodes.
427 Args:
428 chatmodes: Lista de chatmodes
430 Returns:
431 Resumo por nível de compatibilidade
432 """
433 summary = {}
434 for compatibility in ChatModeCompatibility:
435 count = len(
436 [cm for cm in chatmodes if cm.compatibility == compatibility]
437 )
438 summary[compatibility.value] = count
440 return summary
442 def validate_vscode_integration(self) -> Dict[str, Any]:
443 """
444 Valida integração com VS Code.
446 Returns:
447 Resultado da validação
448 """
449 validation = {
450 "vscode_detected": False,
451 "copilot_extension_available": False,
452 "chatmodes_accessible": False,
453 "configuration_valid": True,
454 "issues": [],
455 }
457 try:
458 # Verificar se VS Code está configurado no projeto
459 vscode_dir = self.project_path / ".vscode"
460 if vscode_dir.exists():
461 validation["vscode_detected"] = True
463 # Verificar configurações específicas
464 settings_file = vscode_dir / "settings.json"
465 if settings_file.exists():
466 # Aqui poderia verificar configurações específicas
467 pass
469 # Verificar se diretório de chatmodes está acessível
470 if self.chatmodes_dir.exists() and self.chatmodes_dir.is_dir():
471 validation["chatmodes_accessible"] = True
473 # Verificar se há chatmodes configurados
474 chatmode_files = list(self.chatmodes_dir.glob("*.chatmode.md"))
475 if not chatmode_files:
476 validation["issues"].append(
477 "Nenhum ChatMode encontrado em .github/chatmodes/"
478 )
479 else:
480 validation["issues"].append(
481 "Diretório .github/chatmodes/ não encontrado"
482 )
484 # Nota: Verificação real da extensão Copilot exigiria
485 # acesso ao VS Code, que não é possível em ambiente de teste
486 validation["copilot_extension_available"] = None # Indeterminado
488 except Exception as e:
489 validation["configuration_valid"] = False
490 validation["issues"].append(f"Erro na validação: {e}")
492 return validation
494 def list_configured_chatmodes(self) -> List[Dict[str, Any]]:
495 """
496 Lista chatmodes configurados.
498 Returns:
499 Lista de chatmodes configurados
500 """
501 configured = []
503 if not self.chatmodes_dir.exists():
504 return configured
506 for chatmode_file in self.chatmodes_dir.glob("*.chatmode.md"):
507 try:
508 content = chatmode_file.read_text(encoding="utf-8")
509 metadata = self._extract_chatmode_metadata(content)
510 compatibility, issues = self._validate_copilot_compatibility(
511 content
512 )
514 configured.append(
515 {
516 "name": chatmode_file.stem.replace(".chatmode", ""),
517 "file": chatmode_file.name,
518 "description": metadata.get("description", ""),
519 "version": metadata.get("version", "1.0"),
520 "compatibility": compatibility.value,
521 "issues": issues,
522 "size": chatmode_file.stat().st_size,
523 "path": str(
524 chatmode_file.relative_to(self.project_path)
525 ),
526 }
527 )
529 except Exception as e:
530 msg = f"Erro ao processar {chatmode_file}: {e}"
531 self.installation_log.append(msg)
533 return configured
535 def get_configuration_report(self) -> Dict[str, Any]:
536 """
537 Gera relatório detalhado da configuração.
539 Returns:
540 Relatório de configuração
541 """
542 return {
543 "discovered_chatmodes_count": len(self.discovered_chatmodes),
544 "configured_chatmodes_count": len(
545 self.list_configured_chatmodes()
546 ),
547 "chatmodes_directory": str(self.chatmodes_dir),
548 "vscode_integration": self.validate_vscode_integration(),
549 "compatibility_summary": self._get_compatibility_summary(
550 self.discovered_chatmodes
551 ),
552 "configuration_log": self.installation_log,
553 "chatmodes_details": self.list_configured_chatmodes(),
554 }