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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +456 -0
  4. data/bin/gsd +8 -0
  5. data/bin/gsd-core-darwin-amd64 +0 -0
  6. data/bin/gsd-core-darwin-arm64 +0 -0
  7. data/bin/gsd-core-linux-amd64 +0 -0
  8. data/bin/gsd-core-linux-arm64 +0 -0
  9. data/bin/gsd-core-windows-amd64.exe +0 -0
  10. data/bin/gsd-core-windows-arm64.exe +0 -0
  11. data/bin/gsd-core.exe +0 -0
  12. data/lib/gsd/agents/coordinator.rb +195 -0
  13. data/lib/gsd/agents/task_manager.rb +158 -0
  14. data/lib/gsd/agents/worker.rb +162 -0
  15. data/lib/gsd/agents.rb +30 -0
  16. data/lib/gsd/ai/chat.rb +486 -0
  17. data/lib/gsd/ai/cli.rb +248 -0
  18. data/lib/gsd/ai/command_parser.rb +97 -0
  19. data/lib/gsd/ai/commands/base.rb +42 -0
  20. data/lib/gsd/ai/commands/clear.rb +20 -0
  21. data/lib/gsd/ai/commands/context.rb +30 -0
  22. data/lib/gsd/ai/commands/cost.rb +30 -0
  23. data/lib/gsd/ai/commands/export.rb +42 -0
  24. data/lib/gsd/ai/commands/help.rb +61 -0
  25. data/lib/gsd/ai/commands/model.rb +67 -0
  26. data/lib/gsd/ai/commands/reset.rb +22 -0
  27. data/lib/gsd/ai/config.rb +256 -0
  28. data/lib/gsd/ai/context.rb +324 -0
  29. data/lib/gsd/ai/cost_tracker.rb +361 -0
  30. data/lib/gsd/ai/git_context.rb +169 -0
  31. data/lib/gsd/ai/history.rb +384 -0
  32. data/lib/gsd/ai/providers/anthropic.rb +429 -0
  33. data/lib/gsd/ai/providers/base.rb +282 -0
  34. data/lib/gsd/ai/providers/lmstudio.rb +279 -0
  35. data/lib/gsd/ai/providers/ollama.rb +336 -0
  36. data/lib/gsd/ai/providers/openai.rb +396 -0
  37. data/lib/gsd/ai/providers/openrouter.rb +429 -0
  38. data/lib/gsd/ai/reference_resolver.rb +225 -0
  39. data/lib/gsd/ai/repl.rb +349 -0
  40. data/lib/gsd/ai/streaming.rb +438 -0
  41. data/lib/gsd/ai/ui.rb +429 -0
  42. data/lib/gsd/buddy/cli.rb +284 -0
  43. data/lib/gsd/buddy/gacha.rb +148 -0
  44. data/lib/gsd/buddy/renderer.rb +108 -0
  45. data/lib/gsd/buddy/species.rb +190 -0
  46. data/lib/gsd/buddy/stats.rb +156 -0
  47. data/lib/gsd/buddy.rb +28 -0
  48. data/lib/gsd/cli.rb +455 -0
  49. data/lib/gsd/commands.rb +198 -0
  50. data/lib/gsd/config.rb +183 -0
  51. data/lib/gsd/error.rb +188 -0
  52. data/lib/gsd/frontmatter.rb +123 -0
  53. data/lib/gsd/go/bridge.rb +173 -0
  54. data/lib/gsd/history.rb +76 -0
  55. data/lib/gsd/milestone.rb +75 -0
  56. data/lib/gsd/output.rb +184 -0
  57. data/lib/gsd/phase.rb +102 -0
  58. data/lib/gsd/plugins/base.rb +92 -0
  59. data/lib/gsd/plugins/cli.rb +330 -0
  60. data/lib/gsd/plugins/config.rb +164 -0
  61. data/lib/gsd/plugins/hooks.rb +132 -0
  62. data/lib/gsd/plugins/installer.rb +158 -0
  63. data/lib/gsd/plugins/loader.rb +122 -0
  64. data/lib/gsd/plugins/manager.rb +187 -0
  65. data/lib/gsd/plugins/marketplace.rb +142 -0
  66. data/lib/gsd/plugins/sandbox.rb +114 -0
  67. data/lib/gsd/plugins/search.rb +131 -0
  68. data/lib/gsd/plugins/validator.rb +157 -0
  69. data/lib/gsd/plugins.rb +48 -0
  70. data/lib/gsd/profile.rb +127 -0
  71. data/lib/gsd/research.rb +85 -0
  72. data/lib/gsd/roadmap.rb +90 -0
  73. data/lib/gsd/skills/bundled/commit.md +58 -0
  74. data/lib/gsd/skills/bundled/debug.md +28 -0
  75. data/lib/gsd/skills/bundled/explain.md +41 -0
  76. data/lib/gsd/skills/bundled/plan.md +42 -0
  77. data/lib/gsd/skills/bundled/verify.md +26 -0
  78. data/lib/gsd/skills/loader.rb +189 -0
  79. data/lib/gsd/state.rb +102 -0
  80. data/lib/gsd/template.rb +106 -0
  81. data/lib/gsd/tools/ask_user_question.rb +179 -0
  82. data/lib/gsd/tools/base.rb +204 -0
  83. data/lib/gsd/tools/bash.rb +246 -0
  84. data/lib/gsd/tools/file_edit.rb +297 -0
  85. data/lib/gsd/tools/file_read.rb +199 -0
  86. data/lib/gsd/tools/file_write.rb +153 -0
  87. data/lib/gsd/tools/glob.rb +202 -0
  88. data/lib/gsd/tools/grep.rb +227 -0
  89. data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
  90. data/lib/gsd/tools/gsd_phase.rb +140 -0
  91. data/lib/gsd/tools/gsd_roadmap.rb +108 -0
  92. data/lib/gsd/tools/gsd_state.rb +143 -0
  93. data/lib/gsd/tools/gsd_template.rb +157 -0
  94. data/lib/gsd/tools/gsd_verify.rb +159 -0
  95. data/lib/gsd/tools/registry.rb +103 -0
  96. data/lib/gsd/tools/task.rb +235 -0
  97. data/lib/gsd/tools/todo_write.rb +290 -0
  98. data/lib/gsd/tools/web.rb +260 -0
  99. data/lib/gsd/tui/app.rb +366 -0
  100. data/lib/gsd/tui/auto_complete.rb +79 -0
  101. data/lib/gsd/tui/colors.rb +111 -0
  102. data/lib/gsd/tui/command_palette.rb +126 -0
  103. data/lib/gsd/tui/header.rb +38 -0
  104. data/lib/gsd/tui/input_box.rb +199 -0
  105. data/lib/gsd/tui/spinner.rb +40 -0
  106. data/lib/gsd/tui/status_bar.rb +51 -0
  107. data/lib/gsd/tui.rb +17 -0
  108. data/lib/gsd/validator.rb +216 -0
  109. data/lib/gsd/verify.rb +175 -0
  110. data/lib/gsd/version.rb +5 -0
  111. data/lib/gsd/workstream.rb +91 -0
  112. 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