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

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""" 

6 

7import re 

8import shutil 

9from dataclasses import dataclass, field 

10from enum import Enum 

11from pathlib import Path 

12from typing import Any, Dict, List, Optional 

13 

14from ..core.models import InstallationConfig 

15 

16 

17class ChatModeCompatibility(Enum): 

18 """Níveis de compatibilidade com GitHub Copilot.""" 

19 

20 FULLY_COMPATIBLE = "fully_compatible" 

21 MOSTLY_COMPATIBLE = "mostly_compatible" 

22 NEEDS_ADJUSTMENT = "needs_adjustment" 

23 INCOMPATIBLE = "incompatible" 

24 

25 

26@dataclass 

27class ChatModeInfo: 

28 """Informações sobre um ChatMode.""" 

29 

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 

41 

42 

43class ChatModeConfigurator: 

44 """ 

45 Configura ChatModes para GitHub Copilot. 

46 

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 """ 

54 

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" 

61 

62 self.discovered_chatmodes: List[ChatModeInfo] = [] 

63 self.installation_log: List[str] = [] 

64 

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 } 

82 

83 def discover_chatmodes( 

84 self, source_dir: Optional[Path] = None 

85 ) -> List[ChatModeInfo]: 

86 """ 

87 Descobre ChatModes disponíveis para configuração. 

88 

89 Args: 

90 source_dir: Diretório fonte dos chatmodes (opcional) 

91 

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] 

104 

105 chatmodes = [] 

106 

107 for search_path in search_paths: 

108 if not search_path.exists(): 

109 continue 

110 

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) 

117 

118 self.discovered_chatmodes = chatmodes 

119 msg = f"Descobertos {len(chatmodes)} chatmodes para configuração" 

120 self.installation_log.append(msg) 

121 return chatmodes 

122 

123 def _parse_chatmode_file(self, file_path: Path) -> Optional[ChatModeInfo]: 

124 """ 

125 Parseia arquivo de ChatMode e extrai informações. 

126 

127 Args: 

128 file_path: Caminho do arquivo 

129 

130 Returns: 

131 ChatModeInfo ou None se erro 

132 """ 

133 try: 

134 # Extrair nome do arquivo 

135 name = file_path.stem.replace(".chatmode", "") 

136 

137 # Determinar caminho alvo 

138 target_path = self.chatmodes_dir / file_path.name 

139 

140 # Ler e analisar conteúdo 

141 content = file_path.read_text(encoding="utf-8") 

142 

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") 

147 

148 # Validar compatibilidade 

149 compatibility, issues = self._validate_copilot_compatibility( 

150 content 

151 ) 

152 

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 ) 

163 

164 except Exception as e: 

165 msg = f"Erro ao parsear ChatMode {file_path}: {e}" 

166 self.installation_log.append(msg) 

167 return None 

168 

169 def _extract_chatmode_metadata(self, content: str) -> Dict[str, Any]: 

170 """ 

171 Extrai metadados do ChatMode. 

172 

173 Args: 

174 content: Conteúdo do arquivo 

175 

176 Returns: 

177 Dicionário com metadados 

178 """ 

179 metadata = {} 

180 

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() 

185 

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 ] 

192 

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 

200 

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() 

207 

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(",")] 

215 

216 return metadata 

217 

218 def _validate_copilot_compatibility( 

219 self, content: str 

220 ) -> tuple[ChatModeCompatibility, List[str]]: 

221 """ 

222 Valida compatibilidade com GitHub Copilot. 

223 

224 Args: 

225 content: Conteúdo do ChatMode 

226 

227 Returns: 

228 Tupla com nível de compatibilidade e lista de issues 

229 """ 

230 issues = [] 

231 

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) 

237 

238 if missing_headers: 

239 msg = f"Cabeçalhos obrigatórios faltando: {missing_headers}" 

240 issues.append(msg) 

241 

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 

247 

248 if missing_sections > 1: 

249 issues.append(f"Faltam {missing_sections} seções recomendadas") 

250 

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) 

257 

258 if compatibility_problems: 

259 issues.append(f"Elementos incompatíveis: {compatibility_problems}") 

260 

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 

270 

271 return compatibility, issues 

272 

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. 

280 

281 Args: 

282 chatmodes: Lista específica de chatmodes (opcional) 

283 force_install: Instalar mesmo com problemas de compatibilidade 

284 

285 Returns: 

286 Resultado da configuração 

287 """ 

288 if chatmodes is None: 

289 chatmodes = self.discovered_chatmodes 

290 

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 } 

299 

300 # Criar diretório de destino 

301 self._ensure_chatmodes_directory() 

302 

303 configured_chatmodes = [] 

304 skipped_chatmodes = [] 

305 failed_chatmodes = [] 

306 

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 

319 

320 # Copiar arquivo 

321 shutil.copy2(chatmode.source_path, chatmode.target_path) 

322 

323 # Configurar permissões 

324 self._set_chatmode_permissions(chatmode.target_path) 

325 

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) 

333 

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) 

338 

339 # Criar arquivo de configuração do GitHub Copilot se necessário 

340 if configured_chatmodes: 

341 self._create_copilot_config(configured_chatmodes) 

342 

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 } 

356 

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) 

361 

362 # Criar chatmodes com permissões apropriadas 

363 self.chatmodes_dir.mkdir(exist_ok=True) 

364 

365 # Configurar permissões (755 para compatibilidade GitHub) 

366 if hasattr(self.chatmodes_dir, "chmod"): 

367 self.chatmodes_dir.chmod(0o755) 

368 

369 def _set_chatmode_permissions(self, file_path: Path): 

370 """ 

371 Configura permissões apropriadas para arquivo ChatMode. 

372 

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) 

379 

380 def _create_copilot_config(self, configured_chatmodes: List[ChatModeInfo]): 

381 """ 

382 Cria arquivo de configuração para GitHub Copilot. 

383 

384 Args: 

385 configured_chatmodes: Lista de chatmodes configurados 

386 """ 

387 try: 

388 config_file = self.chatmodes_dir / ".copilot-config.json" 

389 

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 } 

405 

406 import json 

407 

408 with open(config_file, "w", encoding="utf-8") as f: 

409 json.dump(config_data, f, indent=2, ensure_ascii=False) 

410 

411 msg = ( 

412 f"Configuração do Copilot criada: " 

413 f"{len(configured_chatmodes)} chatmodes" 

414 ) 

415 self.installation_log.append(msg) 

416 

417 except Exception as e: 

418 msg = f"Erro ao criar configuração do Copilot: {e}" 

419 self.installation_log.append(msg) 

420 

421 def _get_compatibility_summary( 

422 self, chatmodes: List[ChatModeInfo] 

423 ) -> Dict[str, int]: 

424 """ 

425 Gera resumo de compatibilidade dos chatmodes. 

426 

427 Args: 

428 chatmodes: Lista de chatmodes 

429 

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 

439 

440 return summary 

441 

442 def validate_vscode_integration(self) -> Dict[str, Any]: 

443 """ 

444 Valida integração com VS Code. 

445 

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 } 

456 

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 

462 

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 

468 

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 

472 

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 ) 

483 

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 

487 

488 except Exception as e: 

489 validation["configuration_valid"] = False 

490 validation["issues"].append(f"Erro na validação: {e}") 

491 

492 return validation 

493 

494 def list_configured_chatmodes(self) -> List[Dict[str, Any]]: 

495 """ 

496 Lista chatmodes configurados. 

497 

498 Returns: 

499 Lista de chatmodes configurados 

500 """ 

501 configured = [] 

502 

503 if not self.chatmodes_dir.exists(): 

504 return configured 

505 

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 ) 

513 

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 ) 

528 

529 except Exception as e: 

530 msg = f"Erro ao processar {chatmode_file}: {e}" 

531 self.installation_log.append(msg) 

532 

533 return configured 

534 

535 def get_configuration_report(self) -> Dict[str, Any]: 

536 """ 

537 Gera relatório detalhado da configuração. 

538 

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 }