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
« 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"""
5import os
6from dataclasses import dataclass
7from enum import Enum
8from typing import Any, Dict, List
10from ..core.exceptions import JTechInstallerException
11from ..core.models import InstallationConfig, InstallationType
14class DirectoryPermission(Enum):
15 """Permissões de diretório por sistema operacional."""
17 STANDARD = 0o755
18 RESTRICTED = 0o750
19 PUBLIC = 0o755
22@dataclass
23class DirectoryInfo:
24 """Informações sobre um diretório a ser criado."""
26 path: str
27 description: str
28 required: bool = True
29 permission: DirectoryPermission = DirectoryPermission.STANDARD
30 create_gitkeep: bool = False
33class StructureCreator:
34 """Criador da estrutura completa de diretórios do JTECH™ Core Framework."""
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 ]
149 def __init__(self, config: InstallationConfig, dry_run: bool = False):
150 """
151 Inicializa o criador de estrutura.
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] = []
163 def create_structure(self) -> Dict[str, Any]:
164 """
165 Cria toda a estrutura de diretórios necessária.
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 }
179 try:
180 for dir_info in self.JTECH_STRUCTURE:
181 try:
182 result = self._create_directory(dir_info)
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 )
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)
203 if dir_info.required:
204 results["success"] = False
206 # Criar arquivos .gitkeep necessários
207 if results["success"]:
208 self._create_gitkeep_files(results)
210 return results
212 except Exception as e:
213 raise JTechInstallerException(
214 f"Erro crítico na criação da estrutura: {e}"
215 )
217 def _create_directory(self, dir_info: DirectoryInfo) -> Dict[str, bool]:
218 """
219 Cria um diretório individual.
221 Args:
222 dir_info: Informações do diretório
224 Returns:
225 Dicionário com resultado da operação
226 """
227 full_path = self.config.project_path / dir_info.path
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 )
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}
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)
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)
253 return {"created": True, "existed": False}
255 def _should_create_in_brownfield(self, dir_info: DirectoryInfo) -> bool:
256 """
257 Determina se um diretório deve ser criado em projeto brownfield.
259 Args:
260 dir_info: Informações do diretório
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"]
268 if any(
269 dir_info.path.startswith(essential) for essential in essential_dirs
270 ):
271 return True
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()
280 return True
282 def _has_existing_structure(self) -> bool:
283 """
284 Verifica se já existe uma estrutura de projeto.
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 ]
301 for indicator in structure_indicators:
302 if (self.config.project_path / indicator).exists():
303 return True
305 return False
307 def _create_gitkeep_files(self, results: Dict[str, Any]) -> None:
308 """
309 Cria arquivos .gitkeep em diretórios vazios que precisam ser versionados.
311 Args:
312 results: Resultados da criação de diretórios
313 """
314 gitkeep_count = 0
316 for dir_info in self.JTECH_STRUCTURE:
317 if not dir_info.create_gitkeep:
318 continue
320 full_path = self.config.project_path / dir_info.path
321 gitkeep_file = full_path / ".gitkeep"
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 ):
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
337 if gitkeep_count > 0:
338 results["gitkeep_files_created"] = gitkeep_count
340 def validate_structure(self) -> Dict[str, Any]:
341 """
342 Valida se a estrutura foi criada corretamente.
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 }
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
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
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 ]
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 )
390 return validation_results
392 except Exception as e:
393 raise JTechInstallerException(
394 f"Erro durante validação da estrutura: {e}"
395 )
397 def get_structure_info(self) -> Dict[str, Any]:
398 """
399 Retorna informações sobre a estrutura que será criada.
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 }