fantasy-cli 1.2.6
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- metadata +231 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Gsd
|
|
7
|
+
module AI
|
|
8
|
+
# Config CLI - Comando para configurar API keys
|
|
9
|
+
#
|
|
10
|
+
# Uso:
|
|
11
|
+
# gsd ai config set openrouter <api-key>
|
|
12
|
+
# gsd ai config get openrouter
|
|
13
|
+
# gsd ai config list
|
|
14
|
+
class ConfigCLI
|
|
15
|
+
attr_reader :config_dir, :config_file
|
|
16
|
+
|
|
17
|
+
# Inicializa o config CLI
|
|
18
|
+
#
|
|
19
|
+
# @param cwd [String] Diretório de trabalho
|
|
20
|
+
def initialize(cwd: Dir.pwd)
|
|
21
|
+
@cwd = cwd
|
|
22
|
+
@config_dir = File.join(cwd, '.gsd')
|
|
23
|
+
@config_file = File.join(@config_dir, 'config.json')
|
|
24
|
+
|
|
25
|
+
ensure_config_dir
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Define uma API key
|
|
29
|
+
#
|
|
30
|
+
# @param provider [String] Nome do provider
|
|
31
|
+
# @param api_key [String] API key
|
|
32
|
+
# @return [void]
|
|
33
|
+
def set_key(provider, api_key)
|
|
34
|
+
config = load_config
|
|
35
|
+
config['api_keys'] ||= {}
|
|
36
|
+
config['api_keys'][provider] = api_key
|
|
37
|
+
save_config(config)
|
|
38
|
+
|
|
39
|
+
puts "✅ API key de #{provider} configurada!"
|
|
40
|
+
|
|
41
|
+
# Sugere exportar para variável de ambiente
|
|
42
|
+
env_var = "#{provider.upcase}_API_KEY"
|
|
43
|
+
puts "\nDica: Exporte para variável de ambiente:"
|
|
44
|
+
puts " export #{env_var}=#{api_key[0..10]}..."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Obtém uma API key
|
|
48
|
+
#
|
|
49
|
+
# @param provider [String] Nome do provider
|
|
50
|
+
# @return [String, nil] API key
|
|
51
|
+
def get_key(provider)
|
|
52
|
+
# Tenta variável de ambiente primeiro
|
|
53
|
+
env_var = "#{provider.upcase}_API_KEY"
|
|
54
|
+
return ENV[env_var] if ENV[env_var]
|
|
55
|
+
|
|
56
|
+
# Tenta config file
|
|
57
|
+
config = load_config
|
|
58
|
+
config['api_keys']&.[](provider)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Lista todas as API keys configuradas
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def list_keys
|
|
65
|
+
config = load_config
|
|
66
|
+
api_keys = config['api_keys'] || {}
|
|
67
|
+
|
|
68
|
+
# Adiciona variáveis de ambiente
|
|
69
|
+
env_keys = {
|
|
70
|
+
'anthropic' => ENV['ANTHROPIC_API_KEY'],
|
|
71
|
+
'openai' => ENV['OPENAI_API_KEY'],
|
|
72
|
+
'openrouter' => ENV['OPENROUTER_API_KEY'],
|
|
73
|
+
'ollama' => 'local (não requer key)',
|
|
74
|
+
'lmstudio' => 'local (não requer key)'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
puts "\n🔑 API Keys Configuradas\n"
|
|
78
|
+
puts "=" * 50
|
|
79
|
+
|
|
80
|
+
env_keys.each do |provider, key|
|
|
81
|
+
status = key ? '✅' : '❌'
|
|
82
|
+
masked = key ? "#{key[0..10]}..." : 'Não configurada'
|
|
83
|
+
puts "#{status} #{provider.ljust(12)} : #{masked}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
api_keys.each do |provider, key|
|
|
87
|
+
next if env_keys[provider] # Já listado
|
|
88
|
+
|
|
89
|
+
status = '✅ (config file)'
|
|
90
|
+
masked = "#{key[0..10]}..."
|
|
91
|
+
puts "#{status} #{provider.ljust(12)} : #{masked}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "=" * 50
|
|
95
|
+
puts "\nDica: Use variáveis de ambiente para maior segurança:"
|
|
96
|
+
puts " export OPENROUTER_API_KEY=sk-or-..."
|
|
97
|
+
puts " export ANTHROPIC_API_KEY=sk-ant-..."
|
|
98
|
+
puts " export OPENAI_API_KEY=sk-..."
|
|
99
|
+
puts
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Remove uma API key
|
|
103
|
+
#
|
|
104
|
+
# @param provider [String] Nome do provider
|
|
105
|
+
# @return [void]
|
|
106
|
+
def remove_key(provider)
|
|
107
|
+
config = load_config
|
|
108
|
+
if config['api_keys']&.delete(provider)
|
|
109
|
+
save_config(config)
|
|
110
|
+
puts "✅ API key de #{provider} removida!"
|
|
111
|
+
else
|
|
112
|
+
puts "⚠️ API key de #{provider} não encontrada!"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Verifica se provider está configurado
|
|
117
|
+
#
|
|
118
|
+
# @param provider [String] Nome do provider
|
|
119
|
+
# @return [Boolean] true se configurado
|
|
120
|
+
def configured?(provider)
|
|
121
|
+
!get_key(provider).nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Executa o comando config
|
|
125
|
+
#
|
|
126
|
+
# @param args [Array] Argumentos
|
|
127
|
+
# @return [void]
|
|
128
|
+
def run(args = [])
|
|
129
|
+
subcommand = args.shift
|
|
130
|
+
|
|
131
|
+
case subcommand
|
|
132
|
+
when 'set'
|
|
133
|
+
provider = args.shift
|
|
134
|
+
api_key = args.shift
|
|
135
|
+
|
|
136
|
+
unless provider && api_key
|
|
137
|
+
puts "❌ Uso: gsd ai config set <provider> <api-key>"
|
|
138
|
+
exit 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
set_key(provider, api_key)
|
|
142
|
+
|
|
143
|
+
when 'get'
|
|
144
|
+
provider = args.shift
|
|
145
|
+
|
|
146
|
+
unless provider
|
|
147
|
+
puts "❌ Uso: gsd ai config get <provider>"
|
|
148
|
+
exit 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
key = get_key(provider)
|
|
152
|
+
if key
|
|
153
|
+
puts "#{provider}: #{key[0..10]}..."
|
|
154
|
+
else
|
|
155
|
+
puts "❌ API key de #{provider} não configurada!"
|
|
156
|
+
puts "\nConfigure com:"
|
|
157
|
+
puts " gsd ai config set #{provider} <api-key>"
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
when 'list', nil
|
|
162
|
+
list_keys
|
|
163
|
+
|
|
164
|
+
when 'remove', 'rm'
|
|
165
|
+
provider = args.shift
|
|
166
|
+
|
|
167
|
+
unless provider
|
|
168
|
+
puts "❌ Uso: gsd ai config remove <provider>"
|
|
169
|
+
exit 1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
remove_key(provider)
|
|
173
|
+
|
|
174
|
+
when 'help', '--help', '-h'
|
|
175
|
+
print_help
|
|
176
|
+
|
|
177
|
+
else
|
|
178
|
+
puts "❌ Subcomando desconhecido: #{subcommand}"
|
|
179
|
+
print_help
|
|
180
|
+
exit 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
# Garante que diretório de config existe
|
|
187
|
+
#
|
|
188
|
+
# @return [void]
|
|
189
|
+
def ensure_config_dir
|
|
190
|
+
FileUtils.mkdir_p(@config_dir) unless File.directory?(@config_dir)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Carrega config do arquivo
|
|
194
|
+
#
|
|
195
|
+
# @return [Hash] Config
|
|
196
|
+
def load_config
|
|
197
|
+
return {} unless File.exist?(@config_file)
|
|
198
|
+
|
|
199
|
+
JSON.parse(File.read(@config_file))
|
|
200
|
+
rescue => e
|
|
201
|
+
warn "[Config] Erro ao carregar: #{e.message}"
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Salva config no arquivo
|
|
206
|
+
#
|
|
207
|
+
# @param config [Hash] Config
|
|
208
|
+
# @return [void]
|
|
209
|
+
def save_config(config)
|
|
210
|
+
File.write(@config_file, JSON.pretty_generate(config))
|
|
211
|
+
|
|
212
|
+
# Define permissões seguras (Unix)
|
|
213
|
+
File.chmod(0600, @config_file) if File.respond_to?(:chmod)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Imprime ajuda
|
|
217
|
+
#
|
|
218
|
+
# @return [void]
|
|
219
|
+
def print_help
|
|
220
|
+
puts <<~HELP
|
|
221
|
+
|
|
222
|
+
GSD AI Config - Gerencie suas API keys
|
|
223
|
+
|
|
224
|
+
USO:
|
|
225
|
+
gsd ai config <subcommand> [args]
|
|
226
|
+
|
|
227
|
+
SUBCOMANDOS:
|
|
228
|
+
set <provider> <key> Configurar API key
|
|
229
|
+
get <provider> Obter API key configurada
|
|
230
|
+
list Listar todas API keys
|
|
231
|
+
remove <provider> Remover API key
|
|
232
|
+
help Mostrar esta ajuda
|
|
233
|
+
|
|
234
|
+
EXEMPLOS:
|
|
235
|
+
gsd ai config set openrouter sk-or-v1-...
|
|
236
|
+
gsd ai config get openrouter
|
|
237
|
+
gsd ai config list
|
|
238
|
+
gsd ai config remove openrouter
|
|
239
|
+
|
|
240
|
+
PROVIDERS:
|
|
241
|
+
anthropic - Anthropic Claude
|
|
242
|
+
openai - OpenAI GPT
|
|
243
|
+
openrouter - OpenRouter (Qwen, Llama, etc.)
|
|
244
|
+
ollama - Ollama (local)
|
|
245
|
+
lmstudio - LM Studio (local)
|
|
246
|
+
|
|
247
|
+
VARIÁVEIS DE AMBIENTE:
|
|
248
|
+
OPENROUTER_API_KEY - OpenRouter API key
|
|
249
|
+
ANTHROPIC_API_KEY - Anthropic API key
|
|
250
|
+
OPENAI_API_KEY - OpenAI API key
|
|
251
|
+
|
|
252
|
+
HELP
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest/md5'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'gsd/state'
|
|
8
|
+
require 'gsd/phase'
|
|
9
|
+
require 'gsd/roadmap'
|
|
10
|
+
require 'gsd/ai/git_context'
|
|
11
|
+
|
|
12
|
+
module Gsd
|
|
13
|
+
module AI
|
|
14
|
+
# Context Management - Gerencia contexto do projeto para a IA
|
|
15
|
+
#
|
|
16
|
+
# Responsável por:
|
|
17
|
+
# - Carregar estado atual do projeto
|
|
18
|
+
# - Carregar phase atual
|
|
19
|
+
# - Carregar roadmap
|
|
20
|
+
# - Carregar todos e commits recentes
|
|
21
|
+
# - Cache com TTL e invalidação por hash
|
|
22
|
+
# - Persistência de contexto em disco
|
|
23
|
+
class Context
|
|
24
|
+
attr_reader :cwd, :cache_ttl
|
|
25
|
+
|
|
26
|
+
# Inicializa o contexto
|
|
27
|
+
#
|
|
28
|
+
# @param cwd [String] Diretório de trabalho
|
|
29
|
+
# @param cache_ttl [Integer] TTL do cache em segundos (default: 300)
|
|
30
|
+
# @param cache_dir [String] Diretório para cache persistente
|
|
31
|
+
def initialize(cwd: Dir.pwd, cache_ttl: 300, cache_dir: nil)
|
|
32
|
+
@cwd = cwd
|
|
33
|
+
@cache_ttl = cache_ttl
|
|
34
|
+
@cache_dir = cache_dir || File.join(@cwd, '.gsd', 'cache')
|
|
35
|
+
@cache = {}
|
|
36
|
+
@cache_timestamps = {}
|
|
37
|
+
@cache_hashes = {}
|
|
38
|
+
|
|
39
|
+
ensure_cache_dir
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Carrega todo o contexto do projeto
|
|
43
|
+
#
|
|
44
|
+
# @param force [Boolean] Forçar recarregamento (ignora cache)
|
|
45
|
+
# @return [Hash] Dados de contexto
|
|
46
|
+
def load(force: false)
|
|
47
|
+
if force || expired?
|
|
48
|
+
reload
|
|
49
|
+
else
|
|
50
|
+
# Verifica se o conteúdo mudou (hash-based cache)
|
|
51
|
+
current_hash = compute_context_hash
|
|
52
|
+
if @cache_hashes[:context] != current_hash
|
|
53
|
+
reload
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@cache
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Recarrega o contexto (invalida cache)
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash] Dados de contexto
|
|
63
|
+
def reload
|
|
64
|
+
log_debug("Reloading context...")
|
|
65
|
+
|
|
66
|
+
@cache = {}
|
|
67
|
+
@cache_timestamps[:loaded_at] = Time.now
|
|
68
|
+
|
|
69
|
+
@cache[:state] = load_state
|
|
70
|
+
@cache[:current_phase] = load_current_phase(@cache[:state])
|
|
71
|
+
@cache[:roadmap] = load_roadmap
|
|
72
|
+
@cache[:todos] = load_todos
|
|
73
|
+
@cache[:recent_commits] = load_recent_commits
|
|
74
|
+
@cache[:git] = load_git_context
|
|
75
|
+
@cache[:loaded_at] = @cache_timestamps[:loaded_at]&.iso8601
|
|
76
|
+
|
|
77
|
+
# Computa e salva hash do contexto
|
|
78
|
+
@cache_hashes[:context] = compute_context_hash
|
|
79
|
+
|
|
80
|
+
# Persiste cache em disco
|
|
81
|
+
save_cache_to_disk
|
|
82
|
+
|
|
83
|
+
log_debug("Context reloaded. Hash: #{@cache_hashes[:context][0..8]}")
|
|
84
|
+
|
|
85
|
+
@cache
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Retorna o tamanho do contexto em KB
|
|
89
|
+
#
|
|
90
|
+
# @return [Integer] Tamanho em KB
|
|
91
|
+
def size
|
|
92
|
+
JSON.generate(@cache).bytesize / 1024
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Verifica se o contexto é válido
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean] true se contexto é válido
|
|
98
|
+
def valid?
|
|
99
|
+
load
|
|
100
|
+
!@cache[:state].nil? && !@cache[:state].empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Retorna idade do cache em segundos
|
|
104
|
+
#
|
|
105
|
+
# @return [Integer] Idade em segundos
|
|
106
|
+
def cache_age
|
|
107
|
+
return 0 unless @cache_timestamps[:loaded_at]
|
|
108
|
+
|
|
109
|
+
(Time.now - @cache_timestamps[:loaded_at]).to_i
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Retorna informações do cache
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash] Informações do cache
|
|
115
|
+
def cache_info
|
|
116
|
+
{
|
|
117
|
+
loaded_at: @cache_timestamps[:loaded_at]&.iso8601,
|
|
118
|
+
age_seconds: cache_age,
|
|
119
|
+
ttl_seconds: @cache_ttl,
|
|
120
|
+
expired: expired?,
|
|
121
|
+
hash: @cache_hashes[:context],
|
|
122
|
+
size_kb: size
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Limpa o cache
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def clear_cache
|
|
130
|
+
@cache = {}
|
|
131
|
+
@cache_timestamps = {}
|
|
132
|
+
@cache_hashes = {}
|
|
133
|
+
delete_cache_from_disk
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Exporta contexto para JSON
|
|
137
|
+
#
|
|
138
|
+
# @return [String] JSON do contexto
|
|
139
|
+
def to_json
|
|
140
|
+
JSON.pretty_generate(@cache)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Importa contexto de JSON
|
|
144
|
+
#
|
|
145
|
+
# @param json [String] JSON do contexto
|
|
146
|
+
# @return [void]
|
|
147
|
+
def from_json(json)
|
|
148
|
+
@cache = JSON.parse(json, symbolize_names: true)
|
|
149
|
+
@cache_timestamps[:loaded_at] = Time.parse(@cache[:loaded_at]) if @cache[:loaded_at]
|
|
150
|
+
@cache_hashes[:context] = compute_context_hash
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
# Verifica se cache expirou
|
|
156
|
+
#
|
|
157
|
+
# @return [Boolean] true se expirado
|
|
158
|
+
def expired?
|
|
159
|
+
return true if @cache_ttl.nil? || @cache_ttl <= 0
|
|
160
|
+
return true unless @cache_timestamps[:loaded_at]
|
|
161
|
+
|
|
162
|
+
cache_age >= @cache_ttl
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Computa hash do contexto para detecção de mudanças
|
|
166
|
+
#
|
|
167
|
+
# @return [String] Hash MD5 do contexto
|
|
168
|
+
def compute_context_hash
|
|
169
|
+
content = ""
|
|
170
|
+
content += File.read(File.join(@cwd, '.planning', 'STATE.md')) rescue ""
|
|
171
|
+
content += Dir.glob(File.join(@cwd, '.planning', 'phases', '**', '*.md')).sort.map { |f| File.read(f) }.join("\n") rescue ""
|
|
172
|
+
content += File.read(File.join(@cwd, '.planning', 'ROADMAP.md')) rescue ""
|
|
173
|
+
|
|
174
|
+
Digest::MD5.hexdigest(content)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Garante que diretório de cache existe
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
180
|
+
def ensure_cache_dir
|
|
181
|
+
FileUtils.mkdir_p(@cache_dir) unless File.directory?(@cache_dir)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Salva cache em disco
|
|
185
|
+
#
|
|
186
|
+
# @return [void]
|
|
187
|
+
def save_cache_to_disk
|
|
188
|
+
cache_file = File.join(@cache_dir, 'context.json')
|
|
189
|
+
File.write(cache_file, to_json)
|
|
190
|
+
log_debug("Cache saved to #{cache_file}")
|
|
191
|
+
rescue => e
|
|
192
|
+
log_debug("Failed to save cache: #{e.message}")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Carrega cache do disco
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash,nil] Cache carregado ou nil
|
|
198
|
+
def load_cache_from_disk
|
|
199
|
+
cache_file = File.join(@cache_dir, 'context.json')
|
|
200
|
+
return nil unless File.exist?(cache_file)
|
|
201
|
+
|
|
202
|
+
json = File.read(cache_file)
|
|
203
|
+
JSON.parse(json, symbolize_names: true)
|
|
204
|
+
rescue => e
|
|
205
|
+
log_debug("Failed to load cache: #{e.message}")
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Deleta cache do disco
|
|
210
|
+
#
|
|
211
|
+
# @return [void]
|
|
212
|
+
def delete_cache_from_disk
|
|
213
|
+
cache_file = File.join(@cache_dir, 'context.json')
|
|
214
|
+
File.delete(cache_file) if File.exist?(cache_file)
|
|
215
|
+
rescue => e
|
|
216
|
+
log_debug("Failed to delete cache: #{e.message}")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Carrega estado do projeto
|
|
220
|
+
#
|
|
221
|
+
# @return [Hash] Dados do estado
|
|
222
|
+
def load_state
|
|
223
|
+
Gsd::State.json(cwd: @cwd)
|
|
224
|
+
rescue => e
|
|
225
|
+
warn "[Context] Erro ao carregar state: #{e.message}"
|
|
226
|
+
{}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Carrega phase atual
|
|
230
|
+
#
|
|
231
|
+
# @param state [Hash] Estado do projeto (opcional)
|
|
232
|
+
# @return [Hash] Dados da phase
|
|
233
|
+
def load_current_phase(state = nil)
|
|
234
|
+
state ||= @cache[:state] || load_state
|
|
235
|
+
phase_num = state&.dig('current_phase')
|
|
236
|
+
return nil unless phase_num
|
|
237
|
+
|
|
238
|
+
Gsd::Phase.find(phase_num, cwd: @cwd)
|
|
239
|
+
rescue => e
|
|
240
|
+
warn "[Context] Erro ao carregar phase: #{e.message}"
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Carrega roadmap
|
|
245
|
+
#
|
|
246
|
+
# @return [Hash] Dados do roadmap
|
|
247
|
+
def load_roadmap
|
|
248
|
+
Gsd::Roadmap.analyze(cwd: @cwd)
|
|
249
|
+
rescue => e
|
|
250
|
+
warn "[Context] Erro ao carregar roadmap: #{e.message}"
|
|
251
|
+
{}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Carrega todos
|
|
255
|
+
#
|
|
256
|
+
# @return [Array] Lista de todos
|
|
257
|
+
def load_todos
|
|
258
|
+
todos_path = File.join(@cwd, '.planning', 'todos')
|
|
259
|
+
return [] unless File.directory?(todos_path)
|
|
260
|
+
|
|
261
|
+
todos = []
|
|
262
|
+
Dir.glob(File.join(todos_path, '*.md')).each do |file|
|
|
263
|
+
content = File.read(file)
|
|
264
|
+
todos << {
|
|
265
|
+
file: File.basename(file),
|
|
266
|
+
content: content[0..500] # Primeiros 500 chars
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
todos
|
|
270
|
+
rescue => e
|
|
271
|
+
warn "[Context] Erro ao carregar todos: #{e.message}"
|
|
272
|
+
[]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Carrega commits recentes
|
|
276
|
+
#
|
|
277
|
+
# @return [Array] Lista de commits
|
|
278
|
+
def load_recent_commits
|
|
279
|
+
return [] unless git_available?
|
|
280
|
+
|
|
281
|
+
output = `git log -n 10 --oneline 2>/dev/null`
|
|
282
|
+
return [] if $?.exitstatus != 0
|
|
283
|
+
|
|
284
|
+
output.split("\n").map do |line|
|
|
285
|
+
parts = line.split(/\s+/, 2)
|
|
286
|
+
{
|
|
287
|
+
hash: parts[0],
|
|
288
|
+
message: parts[1]
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
rescue => e
|
|
292
|
+
warn "[Context] Erro ao carregar commits: #{e.message}"
|
|
293
|
+
[]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Carrega contexto do Git
|
|
297
|
+
#
|
|
298
|
+
# @return [Hash,nil] Dados do Git ou nil
|
|
299
|
+
def load_git_context
|
|
300
|
+
git = GitContext.new(cwd: @cwd)
|
|
301
|
+
git.gather
|
|
302
|
+
rescue => e
|
|
303
|
+
warn "[Context] Erro ao carregar git context: #{e.message}"
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Verifica se git está disponível
|
|
308
|
+
#
|
|
309
|
+
# @return [Boolean] true se git está disponível
|
|
310
|
+
def git_available?
|
|
311
|
+
@git_available ||= File.directory?(File.join(@cwd, '.git'))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Log de debug
|
|
315
|
+
#
|
|
316
|
+
# @param message [String] Mensagem de log
|
|
317
|
+
def log_debug(message)
|
|
318
|
+
return unless ENV['GSD_DEBUG']
|
|
319
|
+
|
|
320
|
+
puts "[Context] #{message}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|