Coverage for src/jtech_installer/installer/structure.py: 94%

108 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-20 15:10 -0300

1""" 

2Criador de estrutura de diretórios para JTECH™ Installer 

3""" 

4 

5import os 

6from dataclasses import dataclass 

7from enum import Enum 

8from typing import Any, Dict, List 

9 

10from ..core.exceptions import JTechInstallerException 

11from ..core.models import InstallationConfig, InstallationType 

12 

13 

14class DirectoryPermission(Enum): 

15 """Permissões de diretório por sistema operacional.""" 

16 

17 STANDARD = 0o755 

18 RESTRICTED = 0o750 

19 PUBLIC = 0o755 

20 

21 

22@dataclass 

23class DirectoryInfo: 

24 """Informações sobre um diretório a ser criado.""" 

25 

26 path: str 

27 description: str 

28 required: bool = True 

29 permission: DirectoryPermission = DirectoryPermission.STANDARD 

30 create_gitkeep: bool = False 

31 

32 

33class StructureCreator: 

34 """Criador da estrutura completa de diretórios do JTECH™ Core Framework.""" 

35 

36 # Estrutura completa de diretórios do framework 

37 JTECH_STRUCTURE = [ 

38 DirectoryInfo( 

39 ".jtech-core", 

40 "Diretório raiz do framework JTECH™ Core", 

41 required=True, 

42 permission=DirectoryPermission.STANDARD, 

43 ), 

44 DirectoryInfo( 

45 ".jtech-core/agents", 

46 "Agentes especializados do framework", 

47 required=True, 

48 permission=DirectoryPermission.STANDARD, 

49 ), 

50 DirectoryInfo( 

51 ".jtech-core/templates", 

52 "Templates para geração de documentos", 

53 required=True, 

54 permission=DirectoryPermission.STANDARD, 

55 create_gitkeep=True, 

56 ), 

57 DirectoryInfo( 

58 ".jtech-core/workflows", 

59 "Workflows automatizados", 

60 required=True, 

61 permission=DirectoryPermission.STANDARD, 

62 create_gitkeep=True, 

63 ), 

64 DirectoryInfo( 

65 ".jtech-core/tasks", 

66 "Tarefas e automações específicas", 

67 required=True, 

68 permission=DirectoryPermission.STANDARD, 

69 ), 

70 DirectoryInfo( 

71 ".jtech-core/checklists", 

72 "Checklists para processos", 

73 required=True, 

74 permission=DirectoryPermission.STANDARD, 

75 create_gitkeep=True, 

76 ), 

77 DirectoryInfo( 

78 ".jtech-core/utils", 

79 "Utilitários e ferramentas auxiliares", 

80 required=True, 

81 permission=DirectoryPermission.STANDARD, 

82 create_gitkeep=True, 

83 ), 

84 DirectoryInfo( 

85 ".jtech-core/data", 

86 "Dados de configuração e cache", 

87 required=True, 

88 permission=DirectoryPermission.STANDARD, 

89 create_gitkeep=True, 

90 ), 

91 DirectoryInfo( 

92 ".jtech-core/agents-teams", 

93 "Agentes específicos por tipo de equipe", 

94 required=True, 

95 permission=DirectoryPermission.STANDARD, 

96 ), 

97 DirectoryInfo( 

98 ".jtech-core/chatmodes", 

99 "Definições de chatmodes para GitHub Copilot", 

100 required=True, 

101 permission=DirectoryPermission.STANDARD, 

102 ), 

103 DirectoryInfo( 

104 ".jtech-core/backups", 

105 "Backups e pontos de rollback", 

106 required=True, 

107 permission=DirectoryPermission.RESTRICTED, 

108 ), 

109 DirectoryInfo( 

110 ".github", 

111 "Configurações específicas do GitHub", 

112 required=True, 

113 permission=DirectoryPermission.STANDARD, 

114 ), 

115 DirectoryInfo( 

116 ".github/chatmodes", 

117 "ChatModes para integração com GitHub Copilot", 

118 required=True, 

119 permission=DirectoryPermission.STANDARD, 

120 ), 

121 DirectoryInfo( 

122 ".vscode", 

123 "Configurações do Visual Studio Code", 

124 required=False, 

125 permission=DirectoryPermission.STANDARD, 

126 ), 

127 DirectoryInfo( 

128 "docs", 

129 "Documentação do projeto", 

130 required=False, 

131 permission=DirectoryPermission.STANDARD, 

132 ), 

133 DirectoryInfo( 

134 "docs/architecture", 

135 "Documentação de arquitetura", 

136 required=False, 

137 permission=DirectoryPermission.STANDARD, 

138 create_gitkeep=True, 

139 ), 

140 DirectoryInfo( 

141 "docs/qa", 

142 "Documentação de quality assurance", 

143 required=False, 

144 permission=DirectoryPermission.STANDARD, 

145 create_gitkeep=True, 

146 ), 

147 ] 

148 

149 def __init__(self, config: InstallationConfig, dry_run: bool = False): 

150 """ 

151 Inicializa o criador de estrutura. 

152 

153 Args: 

154 config: Configuração da instalação 

155 dry_run: Se True, simula a criação sem criar os diretórios 

156 """ 

157 self.config = config 

158 self.dry_run = dry_run 

159 self.created_directories: List[str] = [] 

160 self.skipped_directories: List[str] = [] 

161 self.failed_directories: List[str] = [] 

162 

163 def create_structure(self) -> Dict[str, Any]: 

164 """ 

165 Cria toda a estrutura de diretórios necessária. 

166 

167 Returns: 

168 Dicionário com resultados da criação 

169 """ 

170 results = { 

171 "success": True, 

172 "created_directories": [], 

173 "skipped_directories": [], 

174 "failed_directories": [], 

175 "total_directories": len(self.JTECH_STRUCTURE), 

176 "errors": [], 

177 } 

178 

179 try: 

180 for dir_info in self.JTECH_STRUCTURE: 

181 try: 

182 result = self._create_directory(dir_info) 

183 

184 if result["created"]: 

185 results["created_directories"].append( 

186 { 

187 "path": dir_info.path, 

188 "description": dir_info.description, 

189 } 

190 ) 

191 elif result["existed"]: 

192 results["skipped_directories"].append( 

193 {"path": dir_info.path, "reason": "Already exists"} 

194 ) 

195 

196 except Exception as e: 

197 error_msg = ( 

198 f"Falha ao criar diretório {dir_info.path}: {e}" 

199 ) 

200 results["errors"].append(error_msg) 

201 results["failed_directories"].append(dir_info.path) 

202 

203 if dir_info.required: 

204 results["success"] = False 

205 

206 # Criar arquivos .gitkeep necessários 

207 if results["success"]: 

208 self._create_gitkeep_files(results) 

209 

210 return results 

211 

212 except Exception as e: 

213 raise JTechInstallerException( 

214 f"Erro crítico na criação da estrutura: {e}" 

215 ) 

216 

217 def _create_directory(self, dir_info: DirectoryInfo) -> Dict[str, bool]: 

218 """ 

219 Cria um diretório individual. 

220 

221 Args: 

222 dir_info: Informações do diretório 

223 

224 Returns: 

225 Dicionário com resultado da operação 

226 """ 

227 full_path = self.config.project_path / dir_info.path 

228 

229 # Verificar se já existe 

230 if full_path.exists(): 

231 if full_path.is_dir(): 

232 return {"created": False, "existed": True} 

233 else: 

234 raise JTechInstallerException( 

235 f"Conflito: {full_path} existe mas não é um diretório" 

236 ) 

237 

238 # Verificar modo brownfield 

239 if ( 

240 self.config.install_type == InstallationType.BROWNFIELD 

241 and not self._should_create_in_brownfield(dir_info) 

242 ): 

243 return {"created": False, "existed": False, "skipped": True} 

244 

245 # Criar diretório se não for dry run 

246 if not self.dry_run: 

247 full_path.mkdir(parents=True, exist_ok=True) 

248 

249 # Aplicar permissões (apenas em sistemas Unix) 

250 if os.name != "nt": # Não Windows 

251 os.chmod(full_path, dir_info.permission.value) 

252 

253 return {"created": True, "existed": False} 

254 

255 def _should_create_in_brownfield(self, dir_info: DirectoryInfo) -> bool: 

256 """ 

257 Determina se um diretório deve ser criado em projeto brownfield. 

258 

259 Args: 

260 dir_info: Informações do diretório 

261 

262 Returns: 

263 True se deve criar o diretório 

264 """ 

265 # Sempre criar diretórios essenciais do framework 

266 essential_dirs = [".jtech-core", ".github/chatmodes"] 

267 

268 if any( 

269 dir_info.path.startswith(essential) for essential in essential_dirs 

270 ): 

271 return True 

272 

273 # Não criar diretórios opcionais em brownfield se já existir estrutura 

274 optional_dirs = ["docs", ".vscode"] 

275 if any( 

276 dir_info.path.startswith(optional) for optional in optional_dirs 

277 ): 

278 return not self._has_existing_structure() 

279 

280 return True 

281 

282 def _has_existing_structure(self) -> bool: 

283 """ 

284 Verifica se já existe uma estrutura de projeto. 

285 

286 Returns: 

287 True se existe estrutura estabelecida 

288 """ 

289 structure_indicators = [ 

290 "src", 

291 "lib", 

292 "app", 

293 "components", 

294 "pages", 

295 "docs", 

296 "documentation", 

297 "README.md", 

298 "readme.md", 

299 ] 

300 

301 for indicator in structure_indicators: 

302 if (self.config.project_path / indicator).exists(): 

303 return True 

304 

305 return False 

306 

307 def _create_gitkeep_files(self, results: Dict[str, Any]) -> None: 

308 """ 

309 Cria arquivos .gitkeep em diretórios vazios que precisam ser versionados. 

310 

311 Args: 

312 results: Resultados da criação de diretórios 

313 """ 

314 gitkeep_count = 0 

315 

316 for dir_info in self.JTECH_STRUCTURE: 

317 if not dir_info.create_gitkeep: 

318 continue 

319 

320 full_path = self.config.project_path / dir_info.path 

321 gitkeep_file = full_path / ".gitkeep" 

322 

323 # Criar .gitkeep apenas se diretório estiver vazio 

324 if ( 

325 full_path.exists() 

326 and not gitkeep_file.exists() 

327 and not any(full_path.iterdir()) 

328 ): 

329 

330 if not self.dry_run: 

331 gitkeep_file.write_text( 

332 "# This file ensures the directory is tracked by Git\n" 

333 f"# Directory: {dir_info.description}\n" 

334 ) 

335 gitkeep_count += 1 

336 

337 if gitkeep_count > 0: 

338 results["gitkeep_files_created"] = gitkeep_count 

339 

340 def validate_structure(self) -> Dict[str, Any]: 

341 """ 

342 Valida se a estrutura foi criada corretamente. 

343 

344 Returns: 

345 Resultados da validação 

346 """ 

347 validation_results = { 

348 "valid": True, 

349 "missing_required": [], 

350 "permission_issues": [], 

351 "total_validated": 0, 

352 "errors": [], 

353 } 

354 

355 try: 

356 for dir_info in self.JTECH_STRUCTURE: 

357 validation_results["total_validated"] += 1 

358 full_path = self.config.project_path / dir_info.path 

359 

360 # Verificar existência de diretórios obrigatórios 

361 if dir_info.required and not full_path.exists(): 

362 validation_results["missing_required"].append( 

363 dir_info.path 

364 ) 

365 validation_results["valid"] = False 

366 

367 # Verificar permissões (apenas em sistemas Unix) 

368 if full_path.exists() and os.name != "nt": 

369 try: 

370 current_permissions = oct(full_path.stat().st_mode)[ 

371 -3: 

372 ] 

373 expected_permissions = oct(dir_info.permission.value)[ 

374 -3: 

375 ] 

376 

377 if current_permissions != expected_permissions: 

378 validation_results["permission_issues"].append( 

379 { 

380 "path": dir_info.path, 

381 "expected": expected_permissions, 

382 "actual": current_permissions, 

383 } 

384 ) 

385 except Exception as e: 

386 validation_results["errors"].append( 

387 f"Erro ao verificar permissões de {dir_info.path}: {e}" 

388 ) 

389 

390 return validation_results 

391 

392 except Exception as e: 

393 raise JTechInstallerException( 

394 f"Erro durante validação da estrutura: {e}" 

395 ) 

396 

397 def get_structure_info(self) -> Dict[str, Any]: 

398 """ 

399 Retorna informações sobre a estrutura que será criada. 

400 

401 Returns: 

402 Informações detalhadas da estrutura 

403 """ 

404 return { 

405 "total_directories": len(self.JTECH_STRUCTURE), 

406 "required_directories": len( 

407 [d for d in self.JTECH_STRUCTURE if d.required] 

408 ), 

409 "optional_directories": len( 

410 [d for d in self.JTECH_STRUCTURE if not d.required] 

411 ), 

412 "directories_with_gitkeep": len( 

413 [d for d in self.JTECH_STRUCTURE if d.create_gitkeep] 

414 ), 

415 "structure_details": [ 

416 { 

417 "path": dir_info.path, 

418 "description": dir_info.description, 

419 "required": dir_info.required, 

420 "permission": dir_info.permission.name, 

421 "create_gitkeep": dir_info.create_gitkeep, 

422 } 

423 for dir_info in self.JTECH_STRUCTURE 

424 ], 

425 }