goodfella 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
goodfella/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Goodfella"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Goodfella CLI — Camada de interface do terminal."""
goodfella/cli/app.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Entry point principal do Goodfella CLI.
3
+
4
+ Este módulo contém a função `main()` que é registrada como console_script
5
+ no pyproject.toml, permitindo invocar o Goodfella diretamente via terminal:
6
+
7
+ $ goodfella
8
+
9
+ """
10
+
11
+
12
+ import sys
13
+ import logging
14
+ import warnings
15
+
16
+ # Suprime warnings e logs de bibliotecas de terceiros (ChromaDB, Langchain, etc)
17
+ warnings.filterwarnings("ignore")
18
+ logging.getLogger("chromadb").setLevel(logging.ERROR)
19
+ logging.getLogger("httpx").setLevel(logging.ERROR)
20
+
21
+ from langchain_core.messages import SystemMessage, HumanMessage
22
+
23
+ from goodfella.core.env import init_environment
24
+ from goodfella.rag.chunker import run_indexing_pipeline
25
+ from goodfella.knowledge.rules import sync_rules
26
+ from goodfella.llm.factory import get_llm
27
+ from goodfella.llm.memory import load_history, save_message, clear_history
28
+ from goodfella.cli.ui import console, show_spinner
29
+ from goodfella.cli.commands import handle_setup, handle_status, handle_refresh, handle_rebuild, handle_help, handle_review, handle_deep_review, handle_rule_add
30
+
31
+ def print_welcome():
32
+ console.print("\n[bold magenta]🎩 Goodfella AI Pair Programmer[/bold magenta]")
33
+ console.print("[info]Digite /help para ver a lista de comandos disponíveis.[/info]\n")
34
+
35
+ def main() -> None:
36
+ """Ponto de entrada principal do comando `goodfella`.
37
+
38
+ Inicializa o ambiente, sincroniza a base vetorial e
39
+ inicia o loop REPL interativo com o usuário.
40
+ """
41
+ try:
42
+ # 1. Setup do ambiente e Banco de Dados
43
+ init_environment()
44
+
45
+ with show_spinner("Sincronizando base de código e regras..."):
46
+ sync_rules()
47
+ run_indexing_pipeline()
48
+ # 2. Inicialização da Instância LangChain
49
+ llm = get_llm()
50
+
51
+ print_welcome()
52
+
53
+ # 3. Loop REPL Interativo
54
+ while True:
55
+ try:
56
+ user_input = console.input("[bold blue]❯[/bold blue] ")
57
+ except (KeyboardInterrupt, EOFError):
58
+ break
59
+
60
+ if not user_input.strip():
61
+ continue
62
+
63
+ cmd = user_input.strip().lower()
64
+ if cmd in ["/exit", "/quit"]:
65
+ break
66
+ elif cmd == "/clear":
67
+ console.clear()
68
+ print_welcome()
69
+ continue
70
+ elif cmd == "/reset":
71
+ clear_history()
72
+ console.print("[info]Histórico apagado.[/info]\n")
73
+ continue
74
+ elif cmd == "/setup":
75
+ handle_setup()
76
+ continue
77
+ elif cmd == "/status":
78
+ handle_status()
79
+ continue
80
+ elif cmd == "/refresh":
81
+ handle_refresh()
82
+ continue
83
+ elif cmd == "/rebuild":
84
+ handle_rebuild()
85
+ continue
86
+ elif cmd == "/help":
87
+ handle_help()
88
+ continue
89
+ elif cmd.startswith("/rule add"):
90
+ handle_rule_add()
91
+ continue
92
+
93
+ # Prepara a janela de contexto
94
+ history = load_history()
95
+
96
+ if cmd.startswith("/review"):
97
+ user_msg, sys_prompt = handle_review(cmd)
98
+ if not user_msg:
99
+ continue
100
+ user_input = user_msg
101
+ system_prompt = sys_prompt
102
+ elif cmd.startswith("/deep-review"):
103
+ user_msg, sys_prompt = handle_deep_review(cmd)
104
+ if not user_msg:
105
+ continue
106
+ user_input = user_msg
107
+ system_prompt = sys_prompt
108
+ else:
109
+ system_prompt = (
110
+ "Você é o Goodfella, um AI Pair Programmer local-first ultra focado em "
111
+ "engenharia de software pragmática. Responda sempre em português, de forma direta."
112
+ )
113
+
114
+ messages = [SystemMessage(content=system_prompt)]
115
+ messages.extend(history)
116
+ messages.append(HumanMessage(content=user_input))
117
+
118
+ console.print("\n[bold magenta]❖[/bold magenta] ", end="")
119
+ full_response = ""
120
+
121
+ try:
122
+ for chunk in llm.stream(messages):
123
+ print(chunk.content, end="", flush=True)
124
+ full_response += chunk.content
125
+ except Exception as e:
126
+ error_msg = str(e)
127
+ if "Connection refused" in error_msg or "Errno 111" in error_msg:
128
+ console.print("\n[danger]Erro: Não foi possível conectar ao Ollama (Connection refused).[/danger]")
129
+ console.print("[info]Dica: Verifique se o Ollama está rodando no seu terminal com 'ollama serve'.[/info]")
130
+ console.print("[info]Se deseja usar OpenAI ou Gemini, configure-os usando o comando /setup.[/info]")
131
+ else:
132
+ console.print(f"\n[danger]Erro de LLM: {error_msg}[/danger]")
133
+ continue
134
+ print("\n")
135
+
136
+ save_message("user", user_input)
137
+ save_message("ai", full_response)
138
+
139
+ except Exception as e:
140
+ console.print(f"\n[danger]Erro Fatal: {e}[/danger]")
141
+ sys.exit(1)
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,457 @@
1
+ """
2
+ Módulo responsável pelos comandos utilitários da CLI.
3
+ """
4
+
5
+ import shutil
6
+ import questionary
7
+ from rich.prompt import Prompt, Confirm
8
+ from pathlib import Path
9
+ from typing import Tuple, Optional
10
+
11
+ from goodfella.rag.scanner import scan_workspace
12
+
13
+ from goodfella.cli.ui import console, show_spinner
14
+ from goodfella.core.config import load_config, save_config, DEFAULT_CONFIG
15
+ from goodfella.core.env import init_environment
16
+ from goodfella.rag.db import get_client, get_collection, get_db_path
17
+ from goodfella.rag.chunker import run_indexing_pipeline
18
+ from goodfella.knowledge.rules import sync_rules, get_rules_directories
19
+
20
+ def handle_setup() -> None:
21
+ """
22
+ Inicia o assistente de configuração para definir provedor e chaves de API.
23
+ """
24
+ config = load_config()
25
+
26
+ console.print("\n[bold magenta]=== Configuração do Goodfella ===[/bold magenta]")
27
+
28
+ valid_providers = list(DEFAULT_CONFIG["models"].keys())
29
+ current_provider = config.get("provider", "ollama").lower()
30
+
31
+ default_idx = valid_providers.index(current_provider) if current_provider in valid_providers else 0
32
+
33
+ provider = questionary.select(
34
+ "Escolha o provedor padrão:",
35
+ choices=valid_providers,
36
+ default=valid_providers[default_idx]
37
+ ).ask()
38
+
39
+ if not provider:
40
+ console.print("[warning]Operação cancelada.[/warning]\n")
41
+ return
42
+
43
+ config["provider"] = provider
44
+
45
+ if provider != "ollama":
46
+ current_key = config["api_keys"].get(provider, "")
47
+ prompt_msg = f"Digite a API Key para {provider}"
48
+ if current_key:
49
+ prompt_msg += " (deixe em branco para manter a atual)"
50
+
51
+ key = Prompt.ask(prompt_msg, password=True, default="")
52
+ if key.strip():
53
+ config["api_keys"][provider] = key.strip()
54
+
55
+ save_config(config)
56
+ console.print("[success]Configuração salva com sucesso![/success]\n")
57
+
58
+ def handle_status() -> None:
59
+ """
60
+ Exibe o status do banco de dados, provedor e configuração atual.
61
+ """
62
+ config = load_config()
63
+ provider = config.get("provider", "ollama").lower()
64
+ has_key = bool(config["api_keys"].get(provider, ""))
65
+
66
+ console.print("\n[bold magenta]=== Status do Goodfella ===[/bold magenta]")
67
+ console.print(f"[info]Provedor Ativo:[/info] {provider}")
68
+
69
+ if provider != "ollama":
70
+ key_status = "[success]Configurada[/success]" if has_key else "[danger]Não Configurada[/danger]"
71
+ console.print(f"[info]API Key ({provider}):[/info] {key_status}")
72
+
73
+ try:
74
+ client = get_client()
75
+ col = get_collection(client)
76
+ count = col.count()
77
+ console.print(f"[info]Vetores na base local:[/info] {count}")
78
+ except Exception as e:
79
+ console.print(f"[danger]Erro ao acessar banco vetorial:[/danger] {e}")
80
+ console.print()
81
+
82
+ def handle_refresh() -> None:
83
+ """
84
+ Força a sincronização do banco vetorial local (RAG).
85
+ """
86
+ console.print()
87
+ with show_spinner("Sincronizando base de código e regras..."):
88
+ sync_rules()
89
+ run_indexing_pipeline()
90
+ console.print("[success]Sincronização concluída![/success]\n")
91
+
92
+ def handle_rebuild() -> None:
93
+ """
94
+ Apaga completamente o banco vetorial local e força uma recriação.
95
+ """
96
+ console.print("\n[bold red]ATENÇÃO:[/bold red] Isso apagará fisicamente o banco vetorial do projeto.")
97
+ console.print("Ele será reconstruído do zero, o que pode levar algum tempo.\n")
98
+
99
+ if Confirm.ask("Deseja realmente prosseguir?"):
100
+ db_path = get_db_path()
101
+ try:
102
+ if db_path.exists():
103
+ shutil.rmtree(db_path, ignore_errors=True)
104
+ console.print("[info]Banco apagado. Reconstruindo...[/info]")
105
+
106
+ # Recria o diretório se necessário e roda a pipeline
107
+ init_environment()
108
+ with show_spinner("Reconstruindo base vetorial..."):
109
+ sync_rules()
110
+ run_indexing_pipeline()
111
+ console.print("[success]Base reconstruída com sucesso![/success]\n")
112
+ except Exception as e:
113
+ console.print(f"[danger]Erro ao reconstruir base:[/danger] {e}\n")
114
+ else:
115
+ console.print("[info]Operação cancelada.[/info]\n")
116
+
117
+ def handle_help() -> None:
118
+ """
119
+ Exibe a lista de comandos disponíveis e suas descrições.
120
+ """
121
+ console.print("\n[bold magenta]=== Comandos Disponíveis ===[/bold magenta]")
122
+
123
+ commands = [
124
+ ("/help", "Exibe mensagem de ajuda com todos os comandos."),
125
+ ("/setup", "Configura provedor (Ollama, OpenAI, Gemini) e chaves de API."),
126
+ ("/status", "Mostra o status atual: provedor ativo, chaves e tamanho do banco vetorial."),
127
+ ("/refresh", "Força a sincronização dos arquivos do projeto com o banco vetorial local."),
128
+ ("/rebuild", "Apaga fisicamente o banco vetorial e reconstrói do zero (útil para corrupções)."),
129
+ ("/review [arquivos]", "Inicia revisão de código cruzada com regras arquiteturais via RAG."),
130
+ ("/deep-review", "Faz o bypass do RAG e empacota todo o projeto + regras para o LLM (O Curinga)."),
131
+ ("/rule add", "Adiciona uma nova regra ou anti-pattern ao projeto (local ou global)."),
132
+ ("/clear", "Limpa a tela do terminal."),
133
+ ("/reset", "Apaga o histórico de conversação atual."),
134
+ ("/exit ou /quit", "Encerra a aplicação.")
135
+ ]
136
+
137
+ for cmd, desc in commands:
138
+ console.print(f" [bold cyan]{cmd}[/bold cyan] - {desc}")
139
+ console.print()
140
+
141
+ def handle_review(cmd: str) -> Tuple[Optional[str], Optional[str]]:
142
+ """
143
+ Inicia o fluxo do comando /review.
144
+ - Se chamado sem argumentos, exibe menu interativo para escolha de arquivos.
145
+ - Se chamado com argumentos, pega os caminhos passados.
146
+ Lê os arquivos, busca regras no RAG usando fragmentos do código e injeta
147
+ isso no System Prompt retornado, junto com uma breve mensagem de usuário.
148
+ """
149
+ parts = cmd.split(maxsplit=1)
150
+ files_to_review = []
151
+
152
+ if len(parts) == 1:
153
+ valid_files = scan_workspace()
154
+ if not valid_files:
155
+ console.print("[warning]Nenhum arquivo encontrado no projeto para revisão.[/warning]")
156
+ return None, None
157
+
158
+ choices = [str(p.relative_to(Path.cwd())) for p in valid_files]
159
+ selected = questionary.checkbox(
160
+ "Selecione os arquivos para review (Espaço para marcar, Enter para confirmar):",
161
+ choices=choices
162
+ ).ask()
163
+
164
+ if not selected:
165
+ console.print("[info]Review cancelado.[/info]\n")
166
+ return None, None
167
+ files_to_review = selected
168
+ else:
169
+ raw_files = parts[1].replace(",", " ").split()
170
+ files_to_review = raw_files
171
+
172
+ code_contents = []
173
+ for f in files_to_review:
174
+ f_path = Path.cwd() / f
175
+ if f_path.exists() and f_path.is_file():
176
+ try:
177
+ content = f_path.read_text(encoding="utf-8")
178
+ code_contents.append(f"--- Arquivo: {f} ---\n{content}")
179
+ except Exception:
180
+ console.print(f"[warning]Erro ao ler {f}[/warning]")
181
+ else:
182
+ console.print(f"[warning]Arquivo {f} não encontrado.[/warning]")
183
+
184
+ if not code_contents:
185
+ return None, None
186
+
187
+ combined_code = "\n\n".join(code_contents)
188
+
189
+ client = get_client()
190
+ col = get_collection(client)
191
+
192
+ query_text = combined_code[:1000]
193
+
194
+ try:
195
+ results = col.query(
196
+ query_texts=[query_text],
197
+ n_results=5,
198
+ where={"is_rule": True}
199
+ )
200
+ if results and results.get("documents") and len(results["documents"]) > 0:
201
+ rules_context = "\n\n".join(results["documents"][0])
202
+ else:
203
+ rules_context = ""
204
+ except Exception as e:
205
+ console.print(f"[warning]Aviso: Falha ao buscar regras no RAG: {e}[/warning]")
206
+ rules_context = ""
207
+
208
+ system_prompt = (
209
+ "Você é o Goodfella, um AI Pair Programmer focado em engenharia de software pragmática.\n"
210
+ "Realize um Code Review estrito do código do projeto atual.\n\n"
211
+ "CÓDIGO-FONTE A SER REVISADO:\n"
212
+ f"{combined_code}\n\n"
213
+ "REGRAS DE ARQUITETURA E ANTI-PATTERNS (RAG):\n"
214
+ f"{rules_context}\n\n"
215
+ "Forneça sua análise com base estritamente nas regras listadas (se aplicável) e nas boas práticas.\n"
216
+ "Não se desculpe, seja direto e liste sugestões práticas de código."
217
+ )
218
+
219
+ user_message = f"/review {', '.join(files_to_review)}"
220
+
221
+ return user_message, system_prompt
222
+
223
+
224
+ def handle_rule_add() -> None:
225
+ """
226
+ Inicia o fluxo do comando /rule add.
227
+ Permite escolher o escopo (global ou local), o método de entrada
228
+ (digitação manual ou importação de arquivo .md) e sincroniza
229
+ a base de regras no ChromaDB.
230
+ """
231
+ import os
232
+ import tempfile
233
+ import subprocess
234
+
235
+ console.print("\n[bold magenta]=== Adicionar Regra / Anti-pattern ===[/bold magenta]")
236
+
237
+ # 1. Escopo
238
+ scope = questionary.select(
239
+ "Escolha o escopo da regra:",
240
+ choices=[
241
+ questionary.Choice("Local (apenas para o projeto atual)", "local"),
242
+ questionary.Choice("Global (para todos os projetos da máquina)", "global")
243
+ ]
244
+ ).ask()
245
+
246
+ if not scope:
247
+ console.print("[warning]Operação cancelada.[/warning]\n")
248
+ return
249
+
250
+ # 2. Tipo (Regra ou Anti-pattern)
251
+ doc_type = questionary.select(
252
+ "Selecione o tipo de diretriz:",
253
+ choices=[
254
+ questionary.Choice("Regra (Diretriz arquitetural recomendada)", "rules"),
255
+ questionary.Choice("Anti-pattern (Prática a ser evitada)", "anti_patterns")
256
+ ]
257
+ ).ask()
258
+
259
+ if not doc_type:
260
+ console.print("[warning]Operação cancelada.[/warning]\n")
261
+ return
262
+
263
+ rules_dirs = get_rules_directories()
264
+ base_dir = rules_dirs[1] if scope == "local" else rules_dirs[0]
265
+ target_dir = base_dir / doc_type
266
+
267
+ # 3. Método de entrada
268
+ method = questionary.select(
269
+ "Como deseja adicionar?",
270
+ choices=[
271
+ "Digitar manualmente (abre editor de texto)",
272
+ "Importar arquivo existente (.md)"
273
+ ]
274
+ ).ask()
275
+
276
+ if not method:
277
+ console.print("[warning]Operação cancelada.[/warning]\n")
278
+ return
279
+
280
+ rule_name = ""
281
+ rule_content = ""
282
+
283
+ if method == "Digitar manualmente (abre editor de texto)":
284
+ rule_name = Prompt.ask("Digite o nome da regra (ex: evitar_eval.md)").strip()
285
+ if not rule_name:
286
+ console.print("[warning]Operação cancelada por falta de nome.[/warning]\n")
287
+ return
288
+ if not rule_name.endswith(".md"):
289
+ rule_name += ".md"
290
+
291
+ initial_content = (
292
+ f"# {rule_name.replace('.md', '').replace('_', ' ').title()}\n\n"
293
+ "## Descrição\n"
294
+ "Descreva aqui a regra ou anti-pattern...\n\n"
295
+ "## Exemplos\n"
296
+ "Insira exemplos de código correto e incorreto...\n"
297
+ )
298
+
299
+ editors = [os.environ.get('EDITOR'), 'nano', 'vim', 'vi']
300
+ editors = [e for e in editors if e]
301
+
302
+ with tempfile.NamedTemporaryFile(suffix=".md", mode='w', delete=False, encoding='utf-8') as tf:
303
+ tf.write(initial_content)
304
+ tf_path = tf.name
305
+
306
+ success = False
307
+ for editor in editors:
308
+ try:
309
+ subprocess.call([editor, tf_path])
310
+ success = True
311
+ break
312
+ except FileNotFoundError:
313
+ continue
314
+
315
+ if not success:
316
+ console.print("[danger]Nenhum editor de texto encontrado (nano, vim, vi).[/danger]")
317
+ console.print("[info]Configure a variável de ambiente $EDITOR ou use a opção de importação de arquivo.[/info]\n")
318
+ try:
319
+ os.unlink(tf_path)
320
+ except OSError:
321
+ pass
322
+ return
323
+
324
+ rule_content = Path(tf_path).read_text(encoding="utf-8")
325
+ try:
326
+ os.unlink(tf_path)
327
+ except OSError:
328
+ pass
329
+
330
+ if not rule_content.strip() or rule_content == initial_content:
331
+ console.print("[warning]Nenhuma alteração detectada. Regra não salva.[/warning]\n")
332
+ return
333
+
334
+ else:
335
+ # Importar arquivo
336
+ path_str = Prompt.ask("Digite o caminho do arquivo .md a ser importado").strip()
337
+ if not path_str:
338
+ console.print("[warning]Operação cancelada.[/warning]\n")
339
+ return
340
+
341
+ import_path = Path(path_str).expanduser().resolve()
342
+ if not import_path.exists() or not import_path.is_file():
343
+ console.print("[danger]Arquivo não encontrado ou inválido.[/danger]\n")
344
+ return
345
+
346
+ if import_path.suffix.lower() != ".md":
347
+ console.print("[danger]Apenas arquivos Markdown (.md) são suportados.[/danger]\n")
348
+ return
349
+
350
+ try:
351
+ rule_content = import_path.read_text(encoding="utf-8")
352
+ except Exception as e:
353
+ console.print(f"[danger]Erro ao ler o arquivo: {e}[/danger]\n")
354
+ return
355
+
356
+ suggested_name = import_path.name
357
+ rule_name = Prompt.ask("Digite o nome de destino", default=suggested_name).strip()
358
+ if not rule_name:
359
+ rule_name = suggested_name
360
+ if not rule_name.endswith(".md"):
361
+ rule_name += ".md"
362
+
363
+ # Salvar no destino
364
+ try:
365
+ target_dir.mkdir(parents=True, exist_ok=True)
366
+ dest_path = target_dir / rule_name
367
+ dest_path.write_text(rule_content, encoding="utf-8")
368
+ console.print(f"[success]Regra salva em: {dest_path.relative_to(Path.cwd()) if scope == 'local' else dest_path}[/success]")
369
+ except Exception as e:
370
+ console.print(f"[danger]Erro ao salvar arquivo de regra: {e}[/danger]\n")
371
+ return
372
+
373
+ # Re-vetorizar (RAG Sync)
374
+ with show_spinner("Sincronizando novas regras no banco vetorial..."):
375
+ try:
376
+ sync_rules()
377
+ except Exception as e:
378
+ console.print(f"[danger]Erro na sincronização de regras: {e}[/danger]\n")
379
+ return
380
+
381
+ console.print("[success]Regras sincronizadas com sucesso![/success]\n")
382
+
383
+
384
+
385
+ def handle_deep_review(cmd: str) -> Tuple[Optional[str], Optional[str]]:
386
+ """
387
+ Inicia o fluxo do comando /deep-review ("Curinga da Nuvem").
388
+ Faz o bypass do ChromaDB e carrega fisicamente todo o repositório
389
+ mais os arquivos de regras (.md) em um único pacote gigantesco.
390
+ Alerta o usuário sobre custos e envia tudo no System Prompt.
391
+ """
392
+ console.print("\n[bold magenta]=== Iniciando Deep Review ===[/bold magenta]")
393
+ console.print("[info]Fazendo varredura completa do repositório (bypass RAG)...[/info]")
394
+
395
+ valid_files = scan_workspace()
396
+
397
+ if not valid_files:
398
+ console.print("[warning]Nenhum arquivo válido encontrado no projeto.[/warning]")
399
+ return None, None
400
+
401
+ code_contents = []
402
+ total_chars = 0
403
+
404
+ for f_path in valid_files:
405
+ try:
406
+ content = f_path.read_text(encoding="utf-8")
407
+ rel_path = str(f_path.relative_to(Path.cwd()))
408
+ formatted_content = f"--- Arquivo: {rel_path} ---\n{content}"
409
+ code_contents.append(formatted_content)
410
+ total_chars += len(formatted_content)
411
+ except Exception:
412
+ pass
413
+
414
+ rules_contents = []
415
+ rules_dirs = get_rules_directories()
416
+ for r_dir in rules_dirs:
417
+ if not r_dir.exists() or not r_dir.is_dir():
418
+ continue
419
+ for md_file in r_dir.glob("**/*.md"):
420
+ try:
421
+ content = md_file.read_text(encoding="utf-8")
422
+ rules_contents.append(f"--- Regra: {md_file.name} ---\n{content}")
423
+ total_chars += len(content)
424
+ except Exception:
425
+ pass
426
+
427
+ total_files = len(valid_files)
428
+ approx_tokens = total_chars // 4
429
+
430
+ console.print(f"\n[warning]O projeto contém {total_files} arquivos e ~{approx_tokens} tokens (incluindo as regras).[/warning]")
431
+ console.print("Este comando enviará TODO O REPOSITÓRIO como contexto para o LLM.")
432
+ console.print("Dependendo do provedor (ex: OpenAI, Anthropic, Gemini), isso pode [bold red]incorrer em altos custos[/bold red].")
433
+ console.print("Dica: Use LLMs que suportam 'Prompt Caching'.")
434
+
435
+ if not Confirm.ask("Deseja realmente prosseguir e realizar o Deep Review?"):
436
+ console.print("[info]Operação cancelada.[/info]\n")
437
+ return None, None
438
+
439
+ combined_code = "\n\n".join(code_contents)
440
+ combined_rules = "\n\n".join(rules_contents)
441
+
442
+ system_prompt = (
443
+ "Você é o Goodfella, um AI Pair Programmer Arquiteto Sênior.\n"
444
+ "Foi solicitado um DEEP REVIEW. Isso significa que você tem acesso integral a toda a base de código "
445
+ "deste projeto, além de todas as Regras de Arquitetura.\n\n"
446
+ "Sua missão é identificar gargalos arquiteturais severos, acoplamento indevido, e sugerir melhorias "
447
+ "sistêmicas de altíssimo nível. Relacione as diferentes partes do sistema.\n\n"
448
+ "REGRAS E BOAS PRÁTICAS DO PROJETO:\n"
449
+ f"{combined_rules}\n\n"
450
+ "CÓDIGO-FONTE INTEGRAL DO PROJETO:\n"
451
+ f"{combined_code}\n\n"
452
+ "Por favor, seja extremamente objetivo e foque em problemas estruturais e bad smells globais."
453
+ )
454
+
455
+ user_message = "/deep-review"
456
+
457
+ return user_message, system_prompt
goodfella/cli/ui.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Utilitários de interface de usuário (UI) e estilização usando a biblioteca Rich.
3
+ """
4
+
5
+ from contextlib import contextmanager
6
+ from typing import Generator
7
+
8
+ from rich.console import Console
9
+ from rich.theme import Theme
10
+
11
+ # Define um tema base
12
+ custom_theme = Theme({
13
+ "info": "dim white",
14
+ "warning": "magenta",
15
+ "danger": "bold red",
16
+ "success": "bold green",
17
+ })
18
+
19
+ # Console global compartilhado por todo CLI
20
+ console = Console(theme=custom_theme)
21
+
22
+ @contextmanager
23
+ def show_spinner(message: str) -> Generator[None, None, None]:
24
+ """
25
+ Context manager que exibe um spinner animado enquanto uma operação lenta
26
+ (ex: I/O de disco, ingestão RAG ou resposta do LLM) ocorre em background.
27
+
28
+ Exemplo de uso:
29
+ with show_spinner("Processando arquivos..."):
30
+ do_heavy_work()
31
+ """
32
+ with console.status(f"[bold cyan]{message}[/bold cyan]", spinner="dots"):
33
+ yield
@@ -0,0 +1 @@
1
+ """Goodfella Core — Camada de domínio e configuração."""