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 +3 -0
- goodfella/cli/__init__.py +1 -0
- goodfella/cli/app.py +144 -0
- goodfella/cli/commands.py +457 -0
- goodfella/cli/ui.py +33 -0
- goodfella/core/__init__.py +1 -0
- goodfella/core/config.py +71 -0
- goodfella/core/constants.py +63 -0
- goodfella/core/env.py +34 -0
- goodfella/knowledge/__init__.py +1 -0
- goodfella/knowledge/anti_patterns/anemic_domain.md +75 -0
- goodfella/knowledge/anti_patterns/god_class.md +49 -0
- goodfella/knowledge/anti_patterns/tight_coupling.md +80 -0
- goodfella/knowledge/rules/clean_architecture.md +44 -0
- goodfella/knowledge/rules/ddd.md +49 -0
- goodfella/knowledge/rules/hexagonal.md +43 -0
- goodfella/knowledge/rules/solid.md +54 -0
- goodfella/knowledge/rules.py +96 -0
- goodfella/llm/__init__.py +1 -0
- goodfella/llm/factory.py +73 -0
- goodfella/llm/memory.py +73 -0
- goodfella/rag/__init__.py +1 -0
- goodfella/rag/chunker.py +130 -0
- goodfella/rag/db.py +40 -0
- goodfella/rag/scanner.py +92 -0
- goodfella/rag/sync.py +90 -0
- goodfella-0.1.2.dist-info/METADATA +110 -0
- goodfella-0.1.2.dist-info/RECORD +31 -0
- goodfella-0.1.2.dist-info/WHEEL +4 -0
- goodfella-0.1.2.dist-info/entry_points.txt +2 -0
- goodfella-0.1.2.dist-info/licenses/LICENSE +12 -0
goodfella/__init__.py
ADDED
|
@@ -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."""
|