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,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
|