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
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module Gsd
7
+ module Plugins
8
+ # Plugin Installer - Instala e desinstala plugins
9
+ #
10
+ # Responsável por:
11
+ # - Instalar plugins de várias fontes
12
+ # - Desinstalar plugins
13
+ # - Gerenciar dependências
14
+ class Installer
15
+ attr_reader :plugins_dir, :loader
16
+
17
+ # Inicializa o Installer
18
+ #
19
+ # @param plugins_dir [String] Diretório de plugins
20
+ def initialize(plugins_dir: nil)
21
+ @plugins_dir = plugins_dir || File.join(Dir.home, '.gsd', 'plugins')
22
+ @loader = Loader.new(plugins_dir: @plugins_dir)
23
+ ensure_plugins_dir
24
+ end
25
+
26
+ # Instala plugin
27
+ #
28
+ # @param source [String] Fonte do plugin (path, URL, ou nome)
29
+ # @return [Boolean] true se sucesso
30
+ def install(source)
31
+ if source.start_with?('http://', 'https://')
32
+ install_from_url(source)
33
+ elsif File.directory?(source)
34
+ install_from_local(source)
35
+ else
36
+ install_from_marketplace(source)
37
+ end
38
+ end
39
+
40
+ # Desinstala plugin
41
+ #
42
+ # @param name [String] Nome do plugin
43
+ # @return [Boolean] true se sucesso
44
+ def uninstall(name)
45
+ return false unless installed?(name)
46
+
47
+ # Executa hook de uninstall se existir
48
+ run_uninstall_hook(name)
49
+
50
+ # Remove plugin
51
+ @loader.remove(name)
52
+
53
+ true
54
+ end
55
+
56
+ # Verifica se plugin está instalado
57
+ #
58
+ # @param name [String] Nome do plugin
59
+ # @return [Boolean] true se instalado
60
+ def installed?(name)
61
+ plugin_dir = File.join(@plugins_dir, name)
62
+ File.directory?(plugin_dir)
63
+ end
64
+
65
+ # Lista plugins instalados
66
+ #
67
+ # @return [Array<String>] Nomes dos plugins
68
+ def list_installed
69
+ Dir.children(@plugins_dir).select do |child|
70
+ File.directory?(File.join(@plugins_dir, child))
71
+ end
72
+ end
73
+
74
+ # Atualiza plugin
75
+ #
76
+ # @param name [String] Nome do plugin
77
+ # @return [Boolean] true se sucesso
78
+ def update(name)
79
+ return false unless installed?(name)
80
+
81
+ # Obtém info do plugin
82
+ plugin_info = read_plugin_info(name)
83
+ return false unless plugin_info
84
+
85
+ # Se tem URL de origem, atualiza
86
+ if plugin_info['source_url']
87
+ uninstall(name)
88
+ install(plugin_info['source_url'])
89
+ else
90
+ warn "[Installer] Plugin #{name} não tem URL de origem para atualização"
91
+ false
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Garante que diretório existe
98
+ #
99
+ # @return [void]
100
+ def ensure_plugins_dir
101
+ FileUtils.mkdir_p(@plugins_dir) unless File.directory?(@plugins_dir)
102
+ end
103
+
104
+ # Instala de URL
105
+ #
106
+ # @param url [String] URL do plugin
107
+ # @return [Boolean] true se sucesso
108
+ def install_from_url(url)
109
+ @loader.load_url(url)
110
+ end
111
+
112
+ # Instala de caminho local
113
+ #
114
+ # @param path [String] Caminho do plugin
115
+ # @return [Boolean] true se sucesso
116
+ def install_from_local(path)
117
+ @loader.load_local(path)
118
+ end
119
+
120
+ # Instala do marketplace
121
+ #
122
+ # @param name [String] Nome do plugin
123
+ # @return [Boolean] true se sucesso
124
+ def install_from_marketplace(name)
125
+ # TODO: Implementar marketplace integration
126
+ warn "[Installer] Marketplace não implementado ainda"
127
+ false
128
+ end
129
+
130
+ # Lê informações do plugin
131
+ #
132
+ # @param name [String] Nome do plugin
133
+ # @return [Hash, nil] Informações ou nil
134
+ def read_plugin_info(name)
135
+ plugin_file = File.join(@plugins_dir, name, 'plugin.json')
136
+ return nil unless File.exist?(plugin_file)
137
+
138
+ JSON.parse(File.read(plugin_file))
139
+ rescue => e
140
+ warn "[Installer] Error reading plugin info: #{e.message}"
141
+ nil
142
+ end
143
+
144
+ # Executa hook de uninstall
145
+ #
146
+ # @param name [String] Nome do plugin
147
+ # @return [void]
148
+ def run_uninstall_hook(name)
149
+ hook_file = File.join(@plugins_dir, name, 'hooks', 'uninstall.rb')
150
+ return unless File.exist?(hook_file)
151
+
152
+ require hook_file
153
+ rescue => e
154
+ warn "[Installer] Error running uninstall hook: #{e.message}"
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Gsd
6
+ module Plugins
7
+ # Plugin Loader - Carrega plugins de várias fontes
8
+ #
9
+ # Responsável por:
10
+ # - Carregar plugins locais
11
+ # - Carregar plugins de URL
12
+ # - Validar plugins
13
+ class Loader
14
+ attr_reader :plugins_dir
15
+
16
+ # Inicializa o Plugin Loader
17
+ #
18
+ # @param plugins_dir [String] Diretório de plugins
19
+ def initialize(plugins_dir: nil)
20
+ @plugins_dir = plugins_dir || File.join(Dir.home, '.gsd', 'plugins')
21
+ ensure_plugins_dir
22
+ end
23
+
24
+ # Carrega plugin local
25
+ #
26
+ # @param path [String] Caminho do plugin
27
+ # @return [Boolean] true se sucesso
28
+ def load_local(path)
29
+ return false unless File.directory?(path)
30
+
31
+ plugin_name = File.basename(path)
32
+ target_dir = File.join(@plugins_dir, plugin_name)
33
+
34
+ # Copia plugin para diretório de plugins
35
+ FileUtils.cp_r(path, target_dir)
36
+
37
+ # Valida plugin
38
+ valid?(plugin_name)
39
+ end
40
+
41
+ # Carrega plugin de URL
42
+ #
43
+ # @param url [String] URL do plugin (zip)
44
+ # @return [Boolean] true se sucesso
45
+ def load_url(url)
46
+ require 'open-uri'
47
+ require 'zip'
48
+
49
+ # Download do zip
50
+ temp_file = Tempfile.new(['plugin', '.zip'])
51
+ begin
52
+ URI.open(url) do |remote|
53
+ temp_file.write(remote.read)
54
+ end
55
+ temp_file.close
56
+
57
+ # Extrai zip
58
+ extract_plugin(temp_file.path)
59
+ ensure
60
+ temp_file.unlink
61
+ end
62
+ end
63
+
64
+ # Valida plugin
65
+ #
66
+ # @param name [String] Nome do plugin
67
+ # @return [Boolean] true se válido
68
+ def valid?(name)
69
+ plugin_dir = File.join(@plugins_dir, name)
70
+ return false unless File.directory?(plugin_dir)
71
+
72
+ # Verifica arquivos obrigatórios
73
+ has_plugin_rb = File.exist?(File.join(plugin_dir, 'plugin.rb'))
74
+ has_plugin_json = File.exist?(File.join(plugin_dir, 'plugin.json'))
75
+
76
+ has_plugin_rb && has_plugin_json
77
+ end
78
+
79
+ # Remove plugin
80
+ #
81
+ # @param name [String] Nome do plugin
82
+ # @return [Boolean] true se sucesso
83
+ def remove(name)
84
+ plugin_dir = File.join(@plugins_dir, name)
85
+ return false unless File.directory?(plugin_dir)
86
+
87
+ FileUtils.rm_rf(plugin_dir)
88
+ true
89
+ end
90
+
91
+ private
92
+
93
+ # Garante que diretório existe
94
+ #
95
+ # @return [void]
96
+ def ensure_plugins_dir
97
+ FileUtils.mkdir_p(@plugins_dir) unless File.directory?(@plugins_dir)
98
+ end
99
+
100
+ # Extrai plugin de zip
101
+ #
102
+ # @param zip_path [String] Caminho do zip
103
+ # @return [Boolean] true se sucesso
104
+ def extract_plugin(zip_path)
105
+ require 'zip'
106
+
107
+ Zip::File.open(zip_path) do |zip|
108
+ zip.each do |entry|
109
+ target_path = File.join(@plugins_dir, entry.name)
110
+ FileUtils.mkdir_p(File.dirname(target_path))
111
+ zip.extract(entry, target_path)
112
+ end
113
+ end
114
+
115
+ true
116
+ rescue => e
117
+ warn "[Plugin Loader] Error extracting plugin: #{e.message}"
118
+ false
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'gsd/plugins/base'
6
+
7
+ module Gsd
8
+ module Plugins
9
+ # Plugin Manager - Gerencia plugins instalados
10
+ #
11
+ # Responsável por:
12
+ # - Carregar plugins
13
+ # - Ativar/desativar plugins
14
+ # - Listar plugins
15
+ # - Gerenciar configurações
16
+ class Manager
17
+ attr_reader :plugins, :plugins_dir
18
+
19
+ # Inicializa o Plugin Manager
20
+ #
21
+ # @param plugins_dir [String] Diretório de plugins
22
+ def initialize(plugins_dir: nil)
23
+ @plugins_dir = plugins_dir || default_plugins_dir
24
+ @plugins = {}
25
+ @config = {}
26
+
27
+ ensure_plugins_dir
28
+ load_config
29
+ load_plugins
30
+ end
31
+
32
+ # Lista todos os plugins
33
+ #
34
+ # @return [Array<Hash>] Lista de plugins
35
+ def list
36
+ @plugins.values.map(&:info)
37
+ end
38
+
39
+ # Lista plugins ativos
40
+ #
41
+ # @return [Array<Hash>] Plugins ativos
42
+ def list_enabled
43
+ @plugins.values.select(&:enabled?).map(&:info)
44
+ end
45
+
46
+ # Lista plugins inativos
47
+ #
48
+ # @return [Array<Hash>] Plugins inativos
49
+ def list_disabled
50
+ @plugins.values.reject(&:enabled?).map(&:info)
51
+ end
52
+
53
+ # Obtém plugin por nome
54
+ #
55
+ # @param name [String] Nome do plugin
56
+ # @return [Base, nil] Plugin ou nil
57
+ def get(name)
58
+ @plugins[name]
59
+ end
60
+
61
+ # Ativa plugin
62
+ #
63
+ # @param name [String] Nome do plugin
64
+ # @return [Boolean] true se sucesso
65
+ def enable(name)
66
+ plugin = @plugins[name]
67
+ return false unless plugin
68
+
69
+ plugin.enable
70
+ save_config
71
+ true
72
+ end
73
+
74
+ # Desativa plugin
75
+ #
76
+ # @param name [String] Nome do plugin
77
+ # @return [Boolean] true se sucesso
78
+ def disable(name)
79
+ plugin = @plugins[name]
80
+ return false unless plugin
81
+
82
+ plugin.disable
83
+ save_config
84
+ true
85
+ end
86
+
87
+ # Recarrega todos os plugins
88
+ #
89
+ # @return [void]
90
+ def reload
91
+ @plugins.clear
92
+ load_plugins
93
+ end
94
+
95
+ # Retorna estatísticas de plugins
96
+ #
97
+ # @return [Hash] Estatísticas
98
+ def stats
99
+ {
100
+ total: @plugins.count,
101
+ enabled: list_enabled.count,
102
+ disabled: list_disabled.count
103
+ }
104
+ end
105
+
106
+ private
107
+
108
+ # Diretório padrão de plugins
109
+ #
110
+ # @return [String] Diretório padrão
111
+ def default_plugins_dir
112
+ File.join(Dir.home, '.gsd', 'plugins')
113
+ end
114
+
115
+ # Garante que diretório existe
116
+ #
117
+ # @return [void]
118
+ def ensure_plugins_dir
119
+ FileUtils.mkdir_p(@plugins_dir) unless File.directory?(@plugins_dir)
120
+ end
121
+
122
+ # Carrega configuração
123
+ #
124
+ # @return [void]
125
+ def load_config
126
+ config_file = File.join(@plugins_dir, 'config.json')
127
+ return unless File.exist?(config_file)
128
+
129
+ @config = JSON.parse(File.read(config_file))
130
+ end
131
+
132
+ # Salva configuração
133
+ #
134
+ # @return [void]
135
+ def save_config
136
+ config_file = File.join(@plugins_dir, 'config.json')
137
+ enabled_plugins = @plugins.select { |_, p| p.enabled? }.keys
138
+ File.write(config_file, JSON.pretty_generate({
139
+ enabled_plugins: enabled_plugins
140
+ }))
141
+ end
142
+
143
+ # Carrega plugins do diretório
144
+ #
145
+ # @return [void]
146
+ def load_plugins
147
+ return unless File.directory?(@plugins_dir)
148
+
149
+ Dir.each_child(@plugins_dir) do |child|
150
+ plugin_path = File.join(@plugins_dir, child)
151
+ next unless File.directory?(plugin_path)
152
+
153
+ plugin_file = File.join(plugin_path, 'plugin.rb')
154
+ next unless File.exist?(plugin_file)
155
+
156
+ load_plugin(plugin_file, child)
157
+ end
158
+ end
159
+
160
+ # Carrega um plugin específico
161
+ #
162
+ # @param plugin_file [String] Arquivo do plugin
163
+ # @param name [String] Nome do plugin
164
+ # @return [void]
165
+ def load_plugin(plugin_file, name)
166
+ require plugin_file
167
+
168
+ # Encontra classe do plugin
169
+ plugin_class = Gsd::Plugins.constants
170
+ .map { |c| Gsd::Plugins.const_get(c) }
171
+ .find { |c| c.is_a?(Class) && c < Gsd::Plugins::Base && c.name != 'Gsd::Plugins::Base' }
172
+
173
+ if plugin_class
174
+ plugin = plugin_class.new
175
+ @plugins[name] = plugin
176
+
177
+ # Ativa se estava ativo na config
178
+ if @config.dig('enabled_plugins')&.include?(name)
179
+ plugin.enable
180
+ end
181
+ end
182
+ rescue => e
183
+ warn "[Plugin Manager] Error loading plugin #{name}: #{e.message}"
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ module Gsd
8
+ module Plugins
9
+ # Marketplace Client - Cliente para marketplace de plugins
10
+ #
11
+ # Responsável por:
12
+ # - Buscar plugins no marketplace
13
+ # - Listar plugins disponíveis
14
+ # - Obter informações de plugins
15
+ class MarketplaceClient
16
+ attr_reader :base_url, :timeout
17
+
18
+ # Inicializa o Marketplace Client
19
+ #
20
+ # @param base_url [String] URL base do marketplace
21
+ # @param timeout [Integer] Timeout em segundos
22
+ def initialize(base_url: nil, timeout: 10)
23
+ @base_url = base_url || 'https://plugins.gsd-tools.com'
24
+ @timeout = timeout
25
+ end
26
+
27
+ # Lista todos os plugins
28
+ #
29
+ # @param page [Integer] Número da página
30
+ # @param per_page [Integer] Plugins por página
31
+ # @return [Array<Hash>] Lista de plugins
32
+ def list(page: 1, per_page: 20)
33
+ request('/api/v1/plugins', { page: page, per_page: per_page })
34
+ end
35
+
36
+ # Busca plugins por query
37
+ #
38
+ # @param query [String] Query de busca
39
+ # @param filters [Hash] Filtros adicionais
40
+ # @return [Array<Hash>] Plugins encontrados
41
+ def search(query, filters = {})
42
+ params = { q: query }.merge(filters)
43
+ request('/api/v1/plugins/search', params)
44
+ end
45
+
46
+ # Obtém informações de um plugin
47
+ #
48
+ # @param name [String] Nome do plugin
49
+ # @return [Hash, nil] Informações do plugin
50
+ def get_plugin(name)
51
+ request("/api/v1/plugins/#{name}")
52
+ rescue => e
53
+ warn "[Marketplace] Plugin not found: #{name}"
54
+ nil
55
+ end
56
+
57
+ # Lista categorias de plugins
58
+ #
59
+ # @return [Array<Hash>] Categorias
60
+ def categories
61
+ request('/api/v1/categories')
62
+ end
63
+
64
+ # Lista plugins por categoria
65
+ #
66
+ # @param category [String] Categoria
67
+ # @return [Array<Hash>] Plugins da categoria
68
+ def by_category(category)
69
+ request("/api/v1/categories/#{category}/plugins")
70
+ end
71
+
72
+ # Lista plugins mais populares
73
+ #
74
+ # @param limit [Integer] Limite de plugins
75
+ # @return [Array<Hash>] Plugins populares
76
+ def popular(limit: 10)
77
+ request('/api/v1/plugins/popular', { limit: limit })
78
+ end
79
+
80
+ # Lista plugins mais recentes
81
+ #
82
+ # @param limit [Integer] Limite de plugins
83
+ # @return [Array<Hash>] Plugins recentes
84
+ def recent(limit: 10)
85
+ request('/api/v1/plugins/recent', { limit: limit })
86
+ end
87
+
88
+ # Verifica se marketplace está disponível
89
+ #
90
+ # @return [Boolean] true se disponível
91
+ def available?
92
+ response = http.get('/api/v1/health')
93
+ response.code == '200'
94
+ rescue
95
+ false
96
+ end
97
+
98
+ private
99
+
100
+ # Faz requisição HTTP
101
+ #
102
+ # @param path [String] Path da API
103
+ # @param params [Hash] Parâmetros
104
+ # @return [Array, Hash] Resposta
105
+ def request(path, params = {})
106
+ uri = build_uri(path, params)
107
+ response = http.get(uri.path)
108
+
109
+ if response.code == '200'
110
+ JSON.parse(response.body)
111
+ else
112
+ raise MarketplaceError, "API error: #{response.code}"
113
+ end
114
+ end
115
+
116
+ # Constrói URI
117
+ #
118
+ # @param path [String] Path
119
+ # @param params [Hash] Parâmetros
120
+ # @return [URI] URI construída
121
+ def build_uri(path, params)
122
+ uri = URI("#{@base_url}#{path}")
123
+ uri.query = URI.encode_www_form(params) if params.any?
124
+ uri
125
+ end
126
+
127
+ # Retorna objeto HTTP
128
+ #
129
+ # @return [Net::HTTP] HTTP object
130
+ def http
131
+ uri = URI(@base_url)
132
+ http = Net::HTTP.new(uri.host, uri.port)
133
+ http.use_ssl = (uri.scheme == 'https')
134
+ http.read_timeout = @timeout
135
+ http
136
+ end
137
+
138
+ # Marketplace Error class
139
+ class MarketplaceError < StandardError; end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ module Plugins
5
+ # Plugin Sandbox - Isola plugins para segurança
6
+ #
7
+ # Responsável por:
8
+ # - Executar plugins em ambiente isolado
9
+ # - Limitar acesso a recursos
10
+ # - Prevenir ações maliciosas
11
+ class Sandbox
12
+ attr_reader :plugin, :restrictions
13
+
14
+ # Inicializa a Sandbox
15
+ #
16
+ # @param plugin [Base] Plugin para isolar
17
+ # @param restrictions [Hash] Restrições
18
+ def initialize(plugin, restrictions = {})
19
+ @plugin = plugin
20
+ @restrictions = {
21
+ allow_file_system: restrictions[:allow_file_system] || false,
22
+ allow_network: restrictions[:allow_network] || false,
23
+ allow_shell: restrictions[:allow_shell] || false,
24
+ allowed_paths: restrictions[:allowed_paths] || [],
25
+ max_memory: restrictions[:max_memory] || 100_000_000, # 100MB
26
+ max_time: restrictions[:max_time] || 5 # 5 seconds
27
+ }
28
+ end
29
+
30
+ # Executa plugin com segurança
31
+ #
32
+ # @param method [Symbol] Método para executar
33
+ # @param args [Array] Argumentos
34
+ # @return [Object] Resultado
35
+ def execute(method, *args)
36
+ validate_method(method)
37
+
38
+ # Executa com timeout
39
+ Timeout.timeout(@restrictions[:max_time]) do
40
+ # Executa com limitações
41
+ with_restrictions do
42
+ @plugin.send(method, *args)
43
+ end
44
+ end
45
+ rescue Timeout::Error => e
46
+ raise SecurityError, "Plugin execution timeout (max: #{@restrictions[:max_time]}s)"
47
+ rescue => e
48
+ raise SecurityError, "Plugin execution error: #{e.message}"
49
+ end
50
+
51
+ # Verifica se plugin é seguro
52
+ #
53
+ # @return [Boolean] true se seguro
54
+ def safe?
55
+ !@plugin.respond_to?(:dangerous_methods) || @plugin.dangerous_methods.empty?
56
+ end
57
+
58
+ # Retorna restrições aplicadas
59
+ #
60
+ # @return [Hash] Restrições
61
+ def applied_restrictions
62
+ @restrictions
63
+ end
64
+
65
+ private
66
+
67
+ # Valida método
68
+ #
69
+ # @param method [Symbol] Método
70
+ # @return [void]
71
+ def validate_method(method)
72
+ dangerous = [:system, :exec, :spawn, :popen, :` ]
73
+ if dangerous.include?(method) && !@restrictions[:allow_shell]
74
+ raise SecurityError, "Method #{method} is not allowed in sandbox"
75
+ end
76
+ end
77
+
78
+ # Executa com restrições
79
+ #
80
+ # @yield Bloco para executar
81
+ # @return [Object] Resultado
82
+ def with_restrictions
83
+ # Salva estado original
84
+ original_verbose = $VERBOSE
85
+
86
+ begin
87
+ # Aplica restrições
88
+ $VERBOSE = false if !@restrictions[:allow_warnings]
89
+
90
+ # Executa bloco
91
+ yield
92
+ ensure
93
+ # Restaura estado
94
+ $VERBOSE = original_verbose
95
+ end
96
+ end
97
+
98
+ # Verifica caminho permitido
99
+ #
100
+ # @param path [String] Caminho
101
+ # @return [Boolean] true se permitido
102
+ def allowed_path?(path)
103
+ return true if @restrictions[:allow_file_system]
104
+
105
+ @restrictions[:allowed_paths].any? do |allowed|
106
+ path.start_with?(allowed)
107
+ end
108
+ end
109
+
110
+ # Security Error class
111
+ class SecurityError < StandardError; end
112
+ end
113
+ end
114
+ end