Coverage for src/jtech_installer/installer/asset_copier.py: 56%

111 statements  

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

1""" 

2Asset copier for JTECH™ Installer - handles copying framework files 

3""" 

4 

5import hashlib 

6import shutil 

7from pathlib import Path 

8from typing import Callable, Dict, List, Optional 

9 

10from jtech_installer.core.exceptions import FileOperationError 

11from jtech_installer.core.models import ( 

12 AssetInfo, 

13 InstallationConfig, 

14 InstallationProgress, 

15 TeamType, 

16) 

17 

18 

19class AssetCopier: 

20 """Gerencia cópia de assets do framework JTECH™ Core""" 

21 

22 # Mapeamento de arquivos por tipo de equipe 

23 TEAM_AGENT_MAPPING = { 

24 TeamType.ALL: [ 

25 "jtech-master.md", 

26 "jtech-orchestrator.md", 

27 "analyst.md", 

28 "pm.md", 

29 "po.md", 

30 "architect.md", 

31 "dev.md", 

32 "qa.md", 

33 "ux-expert.md", 

34 "sm.md", 

35 ], 

36 TeamType.FULLSTACK: [ 

37 "jtech-orchestrator.md", 

38 "analyst.md", 

39 "pm.md", 

40 "ux-expert.md", 

41 "architect.md", 

42 "po.md", 

43 "dev.md", 

44 ], 

45 TeamType.NO_UI: [ 

46 "jtech-orchestrator.md", 

47 "analyst.md", 

48 "pm.md", 

49 "architect.md", 

50 "dev.md", 

51 "qa.md", 

52 ], 

53 TeamType.IDE_MINIMAL: ["pm.md", "architect.md", "dev.md"], 

54 } 

55 

56 def __init__( 

57 self, 

58 config: InstallationConfig, 

59 dry_run: bool = False, 

60 progress_callback: Optional[ 

61 Callable[[InstallationProgress], None] 

62 ] = None, 

63 ): 

64 self.config = config 

65 self.dry_run = dry_run 

66 self.progress_callback = progress_callback 

67 self._determine_source_path() 

68 

69 def _determine_source_path(self) -> None: 

70 """Determina o caminho fonte do framework""" 

71 if self.config.framework_source_path: 

72 self.framework_source = self.config.framework_source_path 

73 else: 

74 # Usar o framework do projeto atual como fonte 

75 current_project = Path(__file__).parent.parent.parent.parent.parent 

76 self.framework_source = current_project / ".jtech-core" 

77 

78 if not self.framework_source.exists(): 

79 raise FileOperationError( 

80 f"Framework source não encontrado: {self.framework_source}" 

81 ) 

82 

83 def copy_agents(self) -> List[AssetInfo]: 

84 """Copia agentes especializados baseado no tipo de equipe""" 

85 agent_files = self.TEAM_AGENT_MAPPING.get( 

86 self.config.team_type, self.TEAM_AGENT_MAPPING[TeamType.FULLSTACK] 

87 ) 

88 

89 assets_copied = [] 

90 source_agents_dir = self.framework_source / "agents" 

91 target_agents_dir = self.config.project_path / ".jtech-core" / "agents" 

92 

93 if not source_agents_dir.exists(): 

94 raise FileOperationError( 

95 f"Diretório de agentes não encontrado: {source_agents_dir}" 

96 ) 

97 

98 # Criar diretório de destino se não existir 

99 if not self.dry_run: 

100 target_agents_dir.mkdir(parents=True, exist_ok=True) 

101 

102 total_files = len(agent_files) 

103 

104 for i, agent_file in enumerate(agent_files): 

105 source_file = source_agents_dir / agent_file 

106 target_file = target_agents_dir / agent_file 

107 

108 if source_file.exists(): 

109 # Calcular progresso 

110 if self.progress_callback: 

111 progress = InstallationProgress( 

112 current_phase="Copiando agentes", 

113 total_phases=5, 

114 current_phase_number=2, 

115 files_processed=i, 

116 total_files=total_files, 

117 current_file=agent_file, 

118 ) 

119 self.progress_callback(progress) 

120 

121 # Copiar arquivo 

122 asset_info = self._copy_file(source_file, target_file) 

123 assets_copied.append(asset_info) 

124 else: 

125 # Log warning mas continue 

126 print(f"⚠️ Agente não encontrado: {source_file}") 

127 

128 return assets_copied 

129 

130 def copy_chatmodes(self) -> List[AssetInfo]: 

131 """Copia arquivos chatmode para .github/chatmodes/""" 

132 assets_copied = [] 

133 source_chatmodes_dir = self.framework_source / "chatmodes" 

134 target_chatmodes_dir = ( 

135 self.config.project_path / ".github" / "chatmodes" 

136 ) 

137 

138 if not source_chatmodes_dir.exists(): 

139 # Tentar localização alternativa 

140 source_chatmodes_dir = ( 

141 self.framework_source.parent / ".github" / "chatmodes" 

142 ) 

143 

144 if not source_chatmodes_dir.exists(): 

145 print(f"⚠️ Diretório chatmodes não encontrado, pulando...") 

146 return assets_copied 

147 

148 # Criar diretório de destino 

149 if not self.dry_run: 

150 target_chatmodes_dir.mkdir(parents=True, exist_ok=True) 

151 

152 # Copiar todos os arquivos .chatmode.md 

153 chatmode_files = list(source_chatmodes_dir.glob("*.chatmode.md")) 

154 

155 for i, source_file in enumerate(chatmode_files): 

156 target_file = target_chatmodes_dir / source_file.name 

157 

158 if self.progress_callback: 

159 progress = InstallationProgress( 

160 current_phase="Copiando chatmodes", 

161 total_phases=5, 

162 current_phase_number=3, 

163 files_processed=i, 

164 total_files=len(chatmode_files), 

165 current_file=source_file.name, 

166 ) 

167 self.progress_callback(progress) 

168 

169 asset_info = self._copy_file(source_file, target_file) 

170 assets_copied.append(asset_info) 

171 

172 return assets_copied 

173 

174 def copy_templates_and_workflows(self) -> List[AssetInfo]: 

175 """Copia templates e workflows""" 

176 assets_copied = [] 

177 

178 # Diretórios a copiar 

179 directories_to_copy = [ 

180 ("templates", "templates"), 

181 ("workflows", "workflows"), 

182 ("tasks", "tasks"), 

183 ("checklists", "checklists"), 

184 ("utils", "utils"), 

185 ("data", "data"), 

186 ] 

187 

188 for source_dir, target_dir in directories_to_copy: 

189 source_path = self.framework_source / source_dir 

190 target_path = self.config.project_path / ".jtech-core" / target_dir 

191 

192 if source_path.exists(): 

193 assets = self._copy_directory(source_path, target_path) 

194 assets_copied.extend(assets) 

195 

196 return assets_copied 

197 

198 def copy_core_config(self) -> Optional[AssetInfo]: 

199 """Copia e adapta core-config.yml""" 

200 source_config = self.framework_source / "core-config.yml" 

201 target_config = ( 

202 self.config.project_path / ".jtech-core" / "core-config.yml" 

203 ) 

204 

205 if source_config.exists(): 

206 return self._copy_file(source_config, target_config) 

207 return None 

208 

209 def _copy_file(self, source: Path, target: Path) -> AssetInfo: 

210 """Copia um arquivo individual com verificação""" 

211 try: 

212 # Calcular checksum do arquivo fonte 

213 checksum = ( 

214 self._calculate_checksum(source) if source.exists() else None 

215 ) 

216 

217 if not self.dry_run: 

218 # Criar diretório pai se necessário 

219 target.parent.mkdir(parents=True, exist_ok=True) 

220 

221 # Copiar arquivo 

222 shutil.copy2(source, target) 

223 

224 # Verificar se foi copiado corretamente 

225 if not target.exists(): 

226 raise FileOperationError( 

227 f"Falha ao copiar {source} -> {target}" 

228 ) 

229 

230 return AssetInfo( 

231 source_path=source, 

232 target_path=target, 

233 file_type=source.suffix, 

234 checksum=checksum, 

235 ) 

236 

237 except Exception as e: 

238 raise FileOperationError(f"Erro ao copiar {source}: {e}") 

239 

240 def _copy_directory(self, source: Path, target: Path) -> List[AssetInfo]: 

241 """Copia um diretório completo""" 

242 assets_copied = [] 

243 

244 if not self.dry_run: 

245 target.mkdir(parents=True, exist_ok=True) 

246 

247 # Copiar todos os arquivos do diretório 

248 for source_file in source.rglob("*"): 

249 if source_file.is_file(): 

250 relative_path = source_file.relative_to(source) 

251 target_file = target / relative_path 

252 

253 asset_info = self._copy_file(source_file, target_file) 

254 assets_copied.append(asset_info) 

255 

256 return assets_copied 

257 

258 def _calculate_checksum(self, file_path: Path) -> str: 

259 """Calcula checksum SHA256 de um arquivo""" 

260 sha256_hash = hashlib.sha256() 

261 with open(file_path, "rb") as f: 

262 for chunk in iter(lambda: f.read(4096), b""): 

263 sha256_hash.update(chunk) 

264 return sha256_hash.hexdigest() 

265 

266 def copy_all(self) -> Dict[str, List[AssetInfo]]: 

267 """Copia todos os assets necessários""" 

268 all_assets = { 

269 "agents": self.copy_agents(), 

270 "chatmodes": self.copy_chatmodes(), 

271 "templates_workflows": self.copy_templates_and_workflows(), 

272 } 

273 

274 # Copiar core-config separadamente 

275 core_config = self.copy_core_config() 

276 if core_config: 

277 all_assets["core_config"] = [core_config] 

278 

279 return all_assets