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
data/lib/gsd/ai/ui.rb
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsd
|
|
4
|
+
module AI
|
|
5
|
+
# UI Utilities - Utilitários de interface do usuário
|
|
6
|
+
#
|
|
7
|
+
# Responsável por:
|
|
8
|
+
# - Cores e formatação de texto
|
|
9
|
+
# - Spinners e animações
|
|
10
|
+
# - Barras de progresso
|
|
11
|
+
# - Tabelas e formatação
|
|
12
|
+
module UI
|
|
13
|
+
# Cores ANSI para terminal
|
|
14
|
+
module Colors
|
|
15
|
+
COLORS = {
|
|
16
|
+
reset: "\e[0m",
|
|
17
|
+
bold: "\e[1m",
|
|
18
|
+
italic: "\e[3m",
|
|
19
|
+
underline: "\e[4m",
|
|
20
|
+
blink: "\e[5m",
|
|
21
|
+
reverse: "\e[7m",
|
|
22
|
+
black: "\e[30m",
|
|
23
|
+
red: "\e[31m",
|
|
24
|
+
green: "\e[32m",
|
|
25
|
+
yellow: "\e[33m",
|
|
26
|
+
blue: "\e[34m",
|
|
27
|
+
magenta: "\e[35m",
|
|
28
|
+
cyan: "\e[36m",
|
|
29
|
+
white: "\e[37m",
|
|
30
|
+
gray: "\e[90m",
|
|
31
|
+
bright_red: "\e[91m",
|
|
32
|
+
bright_green: "\e[92m",
|
|
33
|
+
bright_yellow: "\e[93m",
|
|
34
|
+
bright_blue: "\e[94m",
|
|
35
|
+
bright_magenta: "\e[95m",
|
|
36
|
+
bright_cyan: "\e[96m",
|
|
37
|
+
bright_white: "\e[97m"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# Formata texto com cor
|
|
41
|
+
#
|
|
42
|
+
# @param text [String] Texto
|
|
43
|
+
# @param color [Symbol] Cor
|
|
44
|
+
# @return [String] Texto colorido
|
|
45
|
+
def self.colorize(text, color)
|
|
46
|
+
return text unless supports_color?
|
|
47
|
+
|
|
48
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Formata texto em negrito
|
|
52
|
+
#
|
|
53
|
+
# @param text [String] Texto
|
|
54
|
+
# @return [String] Texto em negrito
|
|
55
|
+
def self.bold(text)
|
|
56
|
+
colorize(text, :bold)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Formata texto em vermelho
|
|
60
|
+
#
|
|
61
|
+
# @param text [String] Texto
|
|
62
|
+
# @return [String] Texto em vermelho
|
|
63
|
+
def self.red(text)
|
|
64
|
+
colorize(text, :red)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Formata texto em verde
|
|
68
|
+
#
|
|
69
|
+
# @param text [String] Texto
|
|
70
|
+
# @return [String] Texto em verde
|
|
71
|
+
def self.green(text)
|
|
72
|
+
colorize(text, :green)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Formata texto em amarelo
|
|
76
|
+
#
|
|
77
|
+
# @param text [String] Texto
|
|
78
|
+
# @return [String] Texto em amarelo
|
|
79
|
+
def self.yellow(text)
|
|
80
|
+
colorize(text, :yellow)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Formata texto em azul
|
|
84
|
+
#
|
|
85
|
+
# @param text [String] Texto
|
|
86
|
+
# @return [String] Texto em azul
|
|
87
|
+
def self.blue(text)
|
|
88
|
+
colorize(text, :blue)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Formata texto em cyan
|
|
92
|
+
#
|
|
93
|
+
# @param text [String] Texto
|
|
94
|
+
# @return [String] Texto em cyan
|
|
95
|
+
def self.cyan(text)
|
|
96
|
+
colorize(text, :cyan)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Verifica se terminal suporta cores
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean] true se suporta
|
|
102
|
+
def self.supports_color?
|
|
103
|
+
return false if ENV['NO_COLOR']
|
|
104
|
+
return true if ENV['FORCE_COLOR']
|
|
105
|
+
return true if ENV['TERM'] && ENV['TERM'] != 'dumb'
|
|
106
|
+
|
|
107
|
+
$stdout.is_a?(IO) && $stdout.tty?
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Spinner para operações assíncronas
|
|
112
|
+
class Spinner
|
|
113
|
+
FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
|
|
114
|
+
|
|
115
|
+
attr_reader :message, :running
|
|
116
|
+
|
|
117
|
+
# Inicializa o spinner
|
|
118
|
+
#
|
|
119
|
+
# @param message [String] Mensagem para exibir
|
|
120
|
+
# @param color [Symbol] Cor do spinner
|
|
121
|
+
def initialize(message: 'Carregando...', color: :cyan)
|
|
122
|
+
@message = message
|
|
123
|
+
@color = color
|
|
124
|
+
@running = false
|
|
125
|
+
@frame_index = 0
|
|
126
|
+
@thread = nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Inicia o spinner
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
def start
|
|
133
|
+
return if @running
|
|
134
|
+
|
|
135
|
+
@running = true
|
|
136
|
+
@frame_index = 0
|
|
137
|
+
|
|
138
|
+
@thread = Thread.new do
|
|
139
|
+
while @running
|
|
140
|
+
print "\r#{colorize(FRAMES[@frame_index])} #{@message}"
|
|
141
|
+
$stdout.flush
|
|
142
|
+
@frame_index = (@frame_index + 1) % FRAMES.length
|
|
143
|
+
sleep(0.1)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Para o spinner
|
|
149
|
+
#
|
|
150
|
+
# @param success [Boolean] Sucesso ou erro
|
|
151
|
+
# @return [void]
|
|
152
|
+
def stop(success: true)
|
|
153
|
+
return unless @running
|
|
154
|
+
|
|
155
|
+
@running = false
|
|
156
|
+
@thread&.join
|
|
157
|
+
|
|
158
|
+
# Limpa a linha e mostra ícone final
|
|
159
|
+
icon = success ? '✓' : '✗'
|
|
160
|
+
color = success ? :green : :red
|
|
161
|
+
print "\r#{colorize(icon, color)} #{@message}"
|
|
162
|
+
puts
|
|
163
|
+
$stdout.flush
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Atualiza a mensagem
|
|
167
|
+
#
|
|
168
|
+
# @param message [String] Nova mensagem
|
|
169
|
+
# @return [void]
|
|
170
|
+
def update(message)
|
|
171
|
+
@message = message
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Executa um bloco com o spinner ativo
|
|
175
|
+
#
|
|
176
|
+
# @param message [String] Mensagem inicial
|
|
177
|
+
# @yield Bloco para executar
|
|
178
|
+
# @return [void]
|
|
179
|
+
def self.spin(message = 'Carregando...')
|
|
180
|
+
spinner = new(message: message)
|
|
181
|
+
spinner.start
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
yield
|
|
185
|
+
spinner.stop(success: true)
|
|
186
|
+
rescue => e
|
|
187
|
+
spinner.stop(success: false)
|
|
188
|
+
raise e
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Coloriza texto
|
|
195
|
+
#
|
|
196
|
+
# @param text [String] Texto
|
|
197
|
+
# @param color [Symbol] Cor
|
|
198
|
+
# @return [String] Texto colorido
|
|
199
|
+
def colorize(text, color = @color)
|
|
200
|
+
Colors.colorize(text, color)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Barra de progresso
|
|
205
|
+
class ProgressBar
|
|
206
|
+
attr_reader :total, :current
|
|
207
|
+
|
|
208
|
+
# Inicializa a barra de progresso
|
|
209
|
+
#
|
|
210
|
+
# @param total [Integer] Total de itens
|
|
211
|
+
# @param width [Integer] Largura da barra
|
|
212
|
+
# @param options [Hash] Opções
|
|
213
|
+
def initialize(total, options = {})
|
|
214
|
+
@total = total
|
|
215
|
+
@current = 0
|
|
216
|
+
@width = options[:width] || 40
|
|
217
|
+
@filled_char = options[:filled_char] || '█'
|
|
218
|
+
@empty_char = options[:empty_char] || '░'
|
|
219
|
+
@show_percent = options.fetch(:show_percent, true)
|
|
220
|
+
@show_count = options.fetch(:show_count, true)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Incrementa a barra
|
|
224
|
+
#
|
|
225
|
+
# @param count [Integer] Quantidade para incrementar
|
|
226
|
+
# @return [void]
|
|
227
|
+
def increment(count = 1)
|
|
228
|
+
@current += count
|
|
229
|
+
render
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Define o valor atual
|
|
233
|
+
#
|
|
234
|
+
# @param value [Integer] Valor atual
|
|
235
|
+
# @return [void]
|
|
236
|
+
def set(value)
|
|
237
|
+
@current = value
|
|
238
|
+
render
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Finaliza a barra
|
|
242
|
+
#
|
|
243
|
+
# @return [void]
|
|
244
|
+
def finish
|
|
245
|
+
@current = @total
|
|
246
|
+
render
|
|
247
|
+
puts
|
|
248
|
+
$stdout.flush
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Renderiza a barra
|
|
252
|
+
#
|
|
253
|
+
# @return [void]
|
|
254
|
+
def render
|
|
255
|
+
progress = @total > 0 ? @current.to_f / @total : 0
|
|
256
|
+
filled = (progress * @width).to_i
|
|
257
|
+
empty = @width - filled
|
|
258
|
+
|
|
259
|
+
bar = @filled_char * filled + @empty_char * empty
|
|
260
|
+
|
|
261
|
+
parts = []
|
|
262
|
+
parts << "[#{bar}]"
|
|
263
|
+
|
|
264
|
+
if @show_percent
|
|
265
|
+
parts << "#{(progress * 100).to_i}%"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
if @show_count
|
|
269
|
+
parts << "(#{@current}/#{@total})"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
print "\r#{parts.join(' ')}"
|
|
273
|
+
$stdout.flush
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Formatador de tabelas
|
|
278
|
+
class TableFormatter
|
|
279
|
+
attr_reader :headers, :rows
|
|
280
|
+
|
|
281
|
+
# Inicializa o formatador
|
|
282
|
+
#
|
|
283
|
+
# @param headers [Array<String>] Cabeçalhos
|
|
284
|
+
# @param rows [Array<Array<String>>] Linhas
|
|
285
|
+
def initialize(headers = [], rows = [])
|
|
286
|
+
@headers = headers
|
|
287
|
+
@rows = rows
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Adiciona uma linha
|
|
291
|
+
#
|
|
292
|
+
# @param row [Array<String>] Linha
|
|
293
|
+
# @return [void]
|
|
294
|
+
def add_row(row)
|
|
295
|
+
@rows << row
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Renderiza a tabela
|
|
299
|
+
#
|
|
300
|
+
# @return [String] Tabela formatada
|
|
301
|
+
def render
|
|
302
|
+
return '' if @headers.empty? && @rows.empty?
|
|
303
|
+
|
|
304
|
+
# Calcula larguras das colunas
|
|
305
|
+
all_rows = [@headers] + @rows
|
|
306
|
+
col_widths = all_rows.transpose.map do |col|
|
|
307
|
+
col.map { |cell| cell.to_s.length }.max
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Formata linhas
|
|
311
|
+
lines = []
|
|
312
|
+
|
|
313
|
+
# Header
|
|
314
|
+
header_line = @headers.zip(col_widths).map { |h, w| h.to_s.ljust(w) }.join(' │ ')
|
|
315
|
+
lines << header_line
|
|
316
|
+
|
|
317
|
+
# Separator
|
|
318
|
+
separator = col_widths.map { |w| '─' * w }.join('─┼─')
|
|
319
|
+
lines << separator
|
|
320
|
+
|
|
321
|
+
# Rows
|
|
322
|
+
@rows.each do |row|
|
|
323
|
+
row_line = row.zip(col_widths).map { |r, w| r.to_s.ljust(w) }.join(' │ ')
|
|
324
|
+
lines << row_line
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
lines.join("\n")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Imprime a tabela
|
|
331
|
+
#
|
|
332
|
+
# @return [void]
|
|
333
|
+
def print
|
|
334
|
+
puts render
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Cria e imprime uma tabela rapidamente
|
|
338
|
+
#
|
|
339
|
+
# @param headers [Array<String>] Cabeçalhos
|
|
340
|
+
# @param rows [Array<Array<String>>] Linhas
|
|
341
|
+
# @return [void]
|
|
342
|
+
def self.table(headers, rows)
|
|
343
|
+
formatter = new(headers, rows)
|
|
344
|
+
formatter.print
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Formatador de output
|
|
349
|
+
module Formatter
|
|
350
|
+
# Formata JSON com indentação e cores
|
|
351
|
+
#
|
|
352
|
+
# @param data [Object] Dados
|
|
353
|
+
# @return [String] JSON formatado
|
|
354
|
+
def self.format_json(data)
|
|
355
|
+
json = JSON.pretty_generate(data)
|
|
356
|
+
return json unless Colors.supports_color?
|
|
357
|
+
|
|
358
|
+
# Adiciona cores ao JSON
|
|
359
|
+
json.gsub(/"([^"]+)":/) { |m| Colors.cyan(m) }
|
|
360
|
+
.gsub(/: "([^"]+)"/) { |m| ": #{Colors.green('"' + $1 + '"')}" }
|
|
361
|
+
.gsub(/: (\d+)/) { |m| ": #{Colors.yellow($1)}" }
|
|
362
|
+
.gsub(/: (true|false)/) { |m| ": #{Colors.blue($1)}" }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Formata Markdown para terminal
|
|
366
|
+
#
|
|
367
|
+
# @param markdown [String] Markdown
|
|
368
|
+
# @return [String] Markdown formatado
|
|
369
|
+
def self.format_markdown(markdown)
|
|
370
|
+
return markdown unless Colors.supports_color?
|
|
371
|
+
|
|
372
|
+
# Headings
|
|
373
|
+
markdown = markdown.gsub(/^# (.+)$/) { |m| Colors.bold(Colors.blue(m)) }
|
|
374
|
+
markdown = markdown.gsub(/^## (.+)$/) { |m| Colors.bold(m) }
|
|
375
|
+
markdown = markdown.gsub(/^### (.+)$/) { |m| Colors.yellow(m) }
|
|
376
|
+
|
|
377
|
+
# Code blocks
|
|
378
|
+
markdown = markdown.gsub(/```(\w*)\n(.*?)```/m) do |m|
|
|
379
|
+
"\n" + Colors.gray(m) + "\n"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Inline code
|
|
383
|
+
markdown = markdown.gsub(/`([^`]+)`/) { |m| Colors.gray(m) }
|
|
384
|
+
|
|
385
|
+
# Bold
|
|
386
|
+
markdown = markdown.gsub(/\*\*([^*]+)\*\*/) { |m| Colors.bold($1) }
|
|
387
|
+
|
|
388
|
+
# Italic
|
|
389
|
+
markdown = markdown.gsub(/\*([^*]+)\*/) { |m| Colors.italic($1) }
|
|
390
|
+
|
|
391
|
+
markdown
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Trunca texto se for muito longo
|
|
395
|
+
#
|
|
396
|
+
# @param text [String] Texto
|
|
397
|
+
# @param max_length [Integer] Comprimento máximo
|
|
398
|
+
# @return [String] Texto truncado
|
|
399
|
+
def self.truncate(text, max_length: 100)
|
|
400
|
+
return text if text.length <= max_length
|
|
401
|
+
|
|
402
|
+
text[0..max_length - 3] + '...'
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Formata timestamp
|
|
406
|
+
#
|
|
407
|
+
# @param time [Time] Tempo
|
|
408
|
+
# @return [String] Tempo formatado
|
|
409
|
+
def self.format_time(time)
|
|
410
|
+
time.strftime('%Y-%m-%d %H:%M:%S')
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Formata duração em segundos
|
|
414
|
+
#
|
|
415
|
+
# @param seconds [Numeric] Segundos
|
|
416
|
+
# @return [String] Duração formatada
|
|
417
|
+
def self.format_duration(seconds)
|
|
418
|
+
if seconds < 60
|
|
419
|
+
"#{seconds.to_i}s"
|
|
420
|
+
elsif seconds < 3600
|
|
421
|
+
"#{(seconds / 60).to_i}m #{(seconds % 60).to_i}s"
|
|
422
|
+
else
|
|
423
|
+
"#{(seconds / 3600).to_i}h #{(seconds % 3600 / 60).to_i}m"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'gsd/buddy/gacha'
|
|
4
|
+
require 'gsd/buddy/stats'
|
|
5
|
+
require 'gsd/buddy/renderer'
|
|
6
|
+
|
|
7
|
+
module Gsd
|
|
8
|
+
module Buddy
|
|
9
|
+
# CLI - Interface de linha de comando para Buddy
|
|
10
|
+
#
|
|
11
|
+
# Comandos:
|
|
12
|
+
# gsd buddy get
|
|
13
|
+
# gsd buddy stats
|
|
14
|
+
# gsd buddy feed
|
|
15
|
+
# gsd buddy play
|
|
16
|
+
# gsd buddy evolve
|
|
17
|
+
# gsd buddy list
|
|
18
|
+
# gsd buddy pull
|
|
19
|
+
class CLI
|
|
20
|
+
attr_reader :buddies, :renderer
|
|
21
|
+
|
|
22
|
+
# Inicializa o CLI
|
|
23
|
+
#
|
|
24
|
+
# @param args [Array] Argumentos de linha de comando
|
|
25
|
+
def initialize(args = [])
|
|
26
|
+
@args = args
|
|
27
|
+
@buddies = []
|
|
28
|
+
@renderer = Renderer.new
|
|
29
|
+
@config_file = File.join(Dir.home, '.gsd', 'buddy.json')
|
|
30
|
+
load_buddies
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Executa o comando
|
|
34
|
+
#
|
|
35
|
+
# @return [Integer] Exit code
|
|
36
|
+
def run
|
|
37
|
+
command = @args.first
|
|
38
|
+
|
|
39
|
+
case command
|
|
40
|
+
when 'get'
|
|
41
|
+
cmd_get
|
|
42
|
+
when 'stats'
|
|
43
|
+
cmd_stats
|
|
44
|
+
when 'feed'
|
|
45
|
+
cmd_feed
|
|
46
|
+
when 'play'
|
|
47
|
+
cmd_play
|
|
48
|
+
when 'evolve'
|
|
49
|
+
cmd_evolve
|
|
50
|
+
when 'list'
|
|
51
|
+
cmd_list
|
|
52
|
+
when 'pull'
|
|
53
|
+
cmd_pull
|
|
54
|
+
when 'help', '--help', '-h', nil
|
|
55
|
+
print_help
|
|
56
|
+
0
|
|
57
|
+
else
|
|
58
|
+
warn "Unknown command: #{command}"
|
|
59
|
+
print_help
|
|
60
|
+
1
|
|
61
|
+
end
|
|
62
|
+
rescue => e
|
|
63
|
+
warn "Error: #{e.message}"
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Comando: get
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer] Exit code
|
|
72
|
+
def cmd_get
|
|
73
|
+
if @buddies.empty?
|
|
74
|
+
puts "You don't have any buddies yet!"
|
|
75
|
+
puts "\nPull your first buddy with:"
|
|
76
|
+
puts " gsd buddy pull"
|
|
77
|
+
return 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
buddy = @buddies.first
|
|
81
|
+
puts @renderer.render(buddy)
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Comando: stats
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer] Exit code
|
|
88
|
+
def cmd_stats
|
|
89
|
+
if @buddies.empty?
|
|
90
|
+
puts "No buddies found."
|
|
91
|
+
return 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@buddies.each do |buddy|
|
|
95
|
+
puts @renderer.render(buddy)
|
|
96
|
+
puts "\n" + '=' * 60 + "\n"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Comando: feed
|
|
103
|
+
#
|
|
104
|
+
# @return [Integer] Exit code
|
|
105
|
+
def cmd_feed
|
|
106
|
+
if @buddies.empty?
|
|
107
|
+
puts "No buddies to feed!"
|
|
108
|
+
return 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@buddies.each do |buddy|
|
|
112
|
+
buddy.stats.increment_interaction
|
|
113
|
+
puts "Fed #{buddy.name}! +10 EXP"
|
|
114
|
+
|
|
115
|
+
if buddy.stats.check_level_up
|
|
116
|
+
puts "🎉 #{buddy.name} leveled up to #{buddy.level}!"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
save_buddies
|
|
121
|
+
0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Comando: play
|
|
125
|
+
#
|
|
126
|
+
# @return [Integer] Exit code
|
|
127
|
+
def cmd_play
|
|
128
|
+
if @buddies.empty?
|
|
129
|
+
puts "No buddies to play with!"
|
|
130
|
+
return 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@buddies.each do |buddy|
|
|
134
|
+
buddy.stats.increment_interaction
|
|
135
|
+
puts "Played with #{buddy.name}! +10 EXP"
|
|
136
|
+
|
|
137
|
+
if buddy.stats.check_level_up
|
|
138
|
+
puts "🎉 #{buddy.name} leveled up to #{buddy.level}!"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
save_buddies
|
|
143
|
+
0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Comando: evolve
|
|
147
|
+
#
|
|
148
|
+
# @return [Integer] Exit code
|
|
149
|
+
def cmd_evolve
|
|
150
|
+
if @buddies.empty?
|
|
151
|
+
puts "No buddies to evolve!"
|
|
152
|
+
return 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@buddies.each do |buddy|
|
|
156
|
+
if buddy.stats.can_evolve?(buddy.stats.stage)
|
|
157
|
+
puts "🎉 #{buddy.name} evolved from #{buddy.stats.stage}!"
|
|
158
|
+
# Evolution logic would go here
|
|
159
|
+
else
|
|
160
|
+
puts "#{buddy.name} cannot evolve yet."
|
|
161
|
+
puts " Stage: #{buddy.stats.stage}"
|
|
162
|
+
puts " Interactions: #{buddy.stats.interactions}"
|
|
163
|
+
puts " Level: #{buddy.level}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Comando: list
|
|
171
|
+
#
|
|
172
|
+
# @return [Integer] Exit code
|
|
173
|
+
def cmd_list
|
|
174
|
+
if @buddies.empty?
|
|
175
|
+
puts "No buddies found."
|
|
176
|
+
return 0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
puts @renderer.render_list(@buddies)
|
|
180
|
+
0
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Comando: pull
|
|
184
|
+
#
|
|
185
|
+
# @return [Integer] Exit code
|
|
186
|
+
def cmd_pull
|
|
187
|
+
user_id = get_user_id
|
|
188
|
+
gacha = Gacha.new(user_id: user_id)
|
|
189
|
+
|
|
190
|
+
puts "Pulling buddy..."
|
|
191
|
+
result = gacha.pull
|
|
192
|
+
|
|
193
|
+
puts "\n🎉 New Buddy Obtained! 🎉"
|
|
194
|
+
puts "=" * 60
|
|
195
|
+
puts "Species: #{result[:name]}"
|
|
196
|
+
puts "Rarity: #{result[:rarity]}"
|
|
197
|
+
puts "ASCII: #{result[:ascii]}"
|
|
198
|
+
puts "\nBase Stats:"
|
|
199
|
+
result[:stats].each do |stat, value|
|
|
200
|
+
puts " #{stat.to_s.upcase}: #{value}/10"
|
|
201
|
+
end
|
|
202
|
+
puts "=" * 60
|
|
203
|
+
|
|
204
|
+
# Add buddy to collection
|
|
205
|
+
# In a real implementation, this would create a Buddy object
|
|
206
|
+
puts "\nBuddy added to your collection!"
|
|
207
|
+
|
|
208
|
+
0
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Obtém user ID
|
|
212
|
+
#
|
|
213
|
+
# @return [String] User ID
|
|
214
|
+
def get_user_id
|
|
215
|
+
ENV['USER'] || ENV['USERNAME'] || 'default-user'
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Carrega buddies do arquivo
|
|
219
|
+
#
|
|
220
|
+
# @return [void]
|
|
221
|
+
def load_buddies
|
|
222
|
+
return unless File.exist?(@config_file)
|
|
223
|
+
|
|
224
|
+
data = JSON.parse(File.read(@config_file))
|
|
225
|
+
# In a real implementation, this would load Buddy objects
|
|
226
|
+
@buddies = []
|
|
227
|
+
rescue => e
|
|
228
|
+
warn "Error loading buddies: #{e.message}"
|
|
229
|
+
@buddies = []
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Salva buddies no arquivo
|
|
233
|
+
#
|
|
234
|
+
# @return [void]
|
|
235
|
+
def save_buddies
|
|
236
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
|
237
|
+
data = @buddies.map { |b| b.to_h }
|
|
238
|
+
File.write(@config_file, JSON.pretty_generate(data))
|
|
239
|
+
rescue => e
|
|
240
|
+
warn "Error saving buddies: #{e.message}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Imprime ajuda
|
|
244
|
+
#
|
|
245
|
+
# @return [void]
|
|
246
|
+
def print_help
|
|
247
|
+
puts <<~HELP
|
|
248
|
+
|
|
249
|
+
GSD Buddy CLI - Tamagotchi-style companion system
|
|
250
|
+
|
|
251
|
+
USAGE:
|
|
252
|
+
gsd buddy <command>
|
|
253
|
+
|
|
254
|
+
COMMANDS:
|
|
255
|
+
get Get current buddy
|
|
256
|
+
stats View buddy stats
|
|
257
|
+
feed Feed your buddy
|
|
258
|
+
play Play with your buddy
|
|
259
|
+
evolve Evolve your buddy (if eligible)
|
|
260
|
+
list List all buddies
|
|
261
|
+
pull Pull a new buddy from gacha
|
|
262
|
+
help Show this help
|
|
263
|
+
|
|
264
|
+
EXAMPLES:
|
|
265
|
+
gsd buddy pull
|
|
266
|
+
gsd buddy get
|
|
267
|
+
gsd buddy feed
|
|
268
|
+
gsd buddy play
|
|
269
|
+
gsd buddy stats
|
|
270
|
+
gsd buddy list
|
|
271
|
+
gsd buddy evolve
|
|
272
|
+
|
|
273
|
+
RARITY:
|
|
274
|
+
Common (50%) - Pebblecrab, Codebug, Scriptling
|
|
275
|
+
Uncommon (30%) - Bytebird, Loopfox, Debugdeer
|
|
276
|
+
Rare (15%) - Testurtle, Refactoray, Mergebat
|
|
277
|
+
Epic (4%) - Deploydragon, Pipelineon, Commitcat
|
|
278
|
+
Legendary (1%) - Nebulynx, Quantumquail
|
|
279
|
+
|
|
280
|
+
HELP
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|