cabriolet 0.1.2 → 0.2.0
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 +4 -4
- data/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +34 -45
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +67 -17
- metadata +33 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# Base class for all Cabriolet plugins
|
|
5
|
+
#
|
|
6
|
+
# Plugins extend Cabriolet's functionality by providing custom compression
|
|
7
|
+
# algorithms, format handlers, or other enhancements. All plugins must
|
|
8
|
+
# inherit from this base class and implement required methods.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclass and implement {#metadata} and {#setup} to create
|
|
11
|
+
# a plugin
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a simple plugin
|
|
14
|
+
# class MyPlugin < Cabriolet::Plugin
|
|
15
|
+
# def metadata
|
|
16
|
+
# {
|
|
17
|
+
# name: "my-plugin",
|
|
18
|
+
# version: "1.0.0",
|
|
19
|
+
# author: "Your Name",
|
|
20
|
+
# description: "Adds custom compression algorithm",
|
|
21
|
+
# cabriolet_version: "~> 0.1"
|
|
22
|
+
# }
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# def setup
|
|
26
|
+
# register_algorithm(:custom, CustomAlgorithm,
|
|
27
|
+
# category: :compressor)
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
class Plugin
|
|
31
|
+
# Plugin states
|
|
32
|
+
STATES = %i[discovered loaded active failed disabled].freeze
|
|
33
|
+
|
|
34
|
+
# @return [Symbol] Current plugin state
|
|
35
|
+
attr_reader :state
|
|
36
|
+
|
|
37
|
+
# Initialize a new plugin
|
|
38
|
+
#
|
|
39
|
+
# @param manager [PluginManager] The plugin manager instance
|
|
40
|
+
def initialize(manager = nil)
|
|
41
|
+
@manager = manager
|
|
42
|
+
@state = :discovered
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get plugin metadata
|
|
46
|
+
#
|
|
47
|
+
# @abstract Must be implemented by subclasses
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash] Plugin metadata containing:
|
|
50
|
+
# @option return [String] :name Plugin name (required)
|
|
51
|
+
# @option return [String] :version Plugin version (required)
|
|
52
|
+
# @option return [String] :author Plugin author (required)
|
|
53
|
+
# @option return [String] :description Plugin description (required)
|
|
54
|
+
# @option return [String] :cabriolet_version Compatible Cabriolet
|
|
55
|
+
# version (required)
|
|
56
|
+
# @option return [String] :homepage Plugin homepage URL (optional)
|
|
57
|
+
# @option return [String] :license Plugin license (optional)
|
|
58
|
+
# @option return [Array<String>] :dependencies Plugin dependencies
|
|
59
|
+
# (optional)
|
|
60
|
+
# @option return [Array<String>] :tags Search tags (optional)
|
|
61
|
+
# @option return [Hash] :provides What the plugin provides (optional)
|
|
62
|
+
#
|
|
63
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
64
|
+
#
|
|
65
|
+
# @example Minimal metadata
|
|
66
|
+
# def metadata
|
|
67
|
+
# {
|
|
68
|
+
# name: "my-plugin",
|
|
69
|
+
# version: "1.0.0",
|
|
70
|
+
# author: "Your Name",
|
|
71
|
+
# description: "Plugin description",
|
|
72
|
+
# cabriolet_version: "~> 0.1"
|
|
73
|
+
# }
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example Full metadata
|
|
77
|
+
# def metadata
|
|
78
|
+
# {
|
|
79
|
+
# name: "advanced-plugin",
|
|
80
|
+
# version: "2.0.0",
|
|
81
|
+
# author: "Developer",
|
|
82
|
+
# description: "Advanced features",
|
|
83
|
+
# cabriolet_version: ">= 0.1.0",
|
|
84
|
+
# homepage: "https://example.com",
|
|
85
|
+
# license: "MIT",
|
|
86
|
+
# dependencies: ["other-plugin >= 1.0"],
|
|
87
|
+
# tags: ["compression", "algorithm"],
|
|
88
|
+
# provides: { algorithms: [:custom], formats: [:special] }
|
|
89
|
+
# }
|
|
90
|
+
# end
|
|
91
|
+
def metadata
|
|
92
|
+
raise NotImplementedError,
|
|
93
|
+
"#{self.class} must implement metadata method"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Setup the plugin
|
|
97
|
+
#
|
|
98
|
+
# Called when the plugin is loaded. Use this method to register
|
|
99
|
+
# algorithms, formats, or perform other initialization tasks.
|
|
100
|
+
#
|
|
101
|
+
# @abstract Must be implemented by subclasses
|
|
102
|
+
#
|
|
103
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
104
|
+
#
|
|
105
|
+
# @example Register an algorithm
|
|
106
|
+
# def setup
|
|
107
|
+
# register_algorithm(:myalgo, MyAlgorithm,
|
|
108
|
+
# category: :compressor)
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# @example Register multiple items
|
|
112
|
+
# def setup
|
|
113
|
+
# register_algorithm(:algo1, Algo1, category: :compressor)
|
|
114
|
+
# register_algorithm(:algo2, Algo2, category: :decompressor)
|
|
115
|
+
# register_format(:myformat, MyFormatHandler)
|
|
116
|
+
# end
|
|
117
|
+
def setup
|
|
118
|
+
raise NotImplementedError,
|
|
119
|
+
"#{self.class} must implement setup method"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Activate the plugin
|
|
123
|
+
#
|
|
124
|
+
# Called when the plugin is activated. Override to perform actions
|
|
125
|
+
# when the plugin becomes active.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
128
|
+
#
|
|
129
|
+
# @example Add hooks on activation
|
|
130
|
+
# def activate
|
|
131
|
+
# puts "#{metadata[:name]} activated"
|
|
132
|
+
# # Additional activation logic...
|
|
133
|
+
# end
|
|
134
|
+
def activate
|
|
135
|
+
# Default implementation does nothing
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Deactivate the plugin
|
|
139
|
+
#
|
|
140
|
+
# Called when the plugin is deactivated. Override to perform cleanup
|
|
141
|
+
# when the plugin is deactivated.
|
|
142
|
+
#
|
|
143
|
+
# @return [void]
|
|
144
|
+
#
|
|
145
|
+
# @example Cleanup on deactivation
|
|
146
|
+
# def deactivate
|
|
147
|
+
# # Cleanup resources...
|
|
148
|
+
# puts "#{metadata[:name]} deactivated"
|
|
149
|
+
# end
|
|
150
|
+
def deactivate
|
|
151
|
+
# Default implementation does nothing
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Cleanup the plugin
|
|
155
|
+
#
|
|
156
|
+
# Called when the plugin is unloaded. Override to perform final
|
|
157
|
+
# cleanup tasks.
|
|
158
|
+
#
|
|
159
|
+
# @return [void]
|
|
160
|
+
#
|
|
161
|
+
# @example Final cleanup
|
|
162
|
+
# def cleanup
|
|
163
|
+
# # Release resources...
|
|
164
|
+
# # Close connections...
|
|
165
|
+
# end
|
|
166
|
+
def cleanup
|
|
167
|
+
# Default implementation does nothing
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
protected
|
|
171
|
+
|
|
172
|
+
# Register a compression or decompression algorithm
|
|
173
|
+
#
|
|
174
|
+
# @param type [Symbol] Algorithm type identifier
|
|
175
|
+
# @param klass [Class] Algorithm class
|
|
176
|
+
# @param options [Hash] Registration options
|
|
177
|
+
# @option options [Symbol] :category Required - :compressor or
|
|
178
|
+
# :decompressor
|
|
179
|
+
# @option options [Integer] :priority Algorithm priority (default: 0)
|
|
180
|
+
# @option options [Symbol, nil] :format Format restriction (optional)
|
|
181
|
+
#
|
|
182
|
+
# @return [void]
|
|
183
|
+
#
|
|
184
|
+
# @raise [PluginError] If manager is not available
|
|
185
|
+
#
|
|
186
|
+
# @example Register a compressor
|
|
187
|
+
# register_algorithm(:myalgo, MyCompressor,
|
|
188
|
+
# category: :compressor, priority: 10)
|
|
189
|
+
#
|
|
190
|
+
# @example Register a format-specific decompressor
|
|
191
|
+
# register_algorithm(:special, SpecialDecompressor,
|
|
192
|
+
# category: :decompressor, format: :cab)
|
|
193
|
+
def register_algorithm(type, klass, **options)
|
|
194
|
+
raise PluginError, "Plugin manager not available" unless @manager
|
|
195
|
+
|
|
196
|
+
Cabriolet.algorithm_factory.register(type, klass, **options)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Register a format handler
|
|
200
|
+
#
|
|
201
|
+
# @param format [Symbol] Format identifier
|
|
202
|
+
# @param handler [Class] Format handler class
|
|
203
|
+
#
|
|
204
|
+
# @return [void]
|
|
205
|
+
#
|
|
206
|
+
# @raise [PluginError] If manager is not available
|
|
207
|
+
#
|
|
208
|
+
# @example Register a format handler
|
|
209
|
+
# register_format(:myformat, MyFormatHandler)
|
|
210
|
+
def register_format(format, handler)
|
|
211
|
+
raise PluginError, "Plugin manager not available" unless @manager
|
|
212
|
+
|
|
213
|
+
# Format registration will be implemented when format registry exists
|
|
214
|
+
# For now, store in manager's format registry
|
|
215
|
+
@manager.register_format(format, handler)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Update plugin state
|
|
219
|
+
#
|
|
220
|
+
# @param new_state [Symbol] New state (must be in STATES)
|
|
221
|
+
#
|
|
222
|
+
# @return [void]
|
|
223
|
+
#
|
|
224
|
+
# @raise [ArgumentError] If state is invalid
|
|
225
|
+
def update_state(new_state)
|
|
226
|
+
unless STATES.include?(new_state)
|
|
227
|
+
raise ArgumentError, "Invalid state: #{new_state}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
@state = new_state
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Cabriolet
|
|
7
|
+
# Manages plugin lifecycle and registry for Cabriolet
|
|
8
|
+
#
|
|
9
|
+
# The PluginManager is a thread-safe singleton that handles plugin
|
|
10
|
+
# discovery, loading, activation, and deactivation. It maintains plugin
|
|
11
|
+
# states and resolves dependencies.
|
|
12
|
+
#
|
|
13
|
+
# @example Access the plugin manager
|
|
14
|
+
# manager = Cabriolet::PluginManager.instance
|
|
15
|
+
# manager.discover_plugins
|
|
16
|
+
# manager.load_plugin("my-plugin")
|
|
17
|
+
# manager.activate_plugin("my-plugin")
|
|
18
|
+
#
|
|
19
|
+
# @example Using global accessor
|
|
20
|
+
# Cabriolet.plugin_manager.discover_plugins
|
|
21
|
+
# Cabriolet.plugin_manager.list_plugins(state: :active)
|
|
22
|
+
class PluginManager
|
|
23
|
+
include Singleton
|
|
24
|
+
|
|
25
|
+
# @return [Hash] Plugin registry by name
|
|
26
|
+
attr_reader :plugins
|
|
27
|
+
|
|
28
|
+
# @return [Hash] Format registry
|
|
29
|
+
attr_reader :formats
|
|
30
|
+
|
|
31
|
+
# Initialize the plugin manager
|
|
32
|
+
def initialize
|
|
33
|
+
@plugins = {}
|
|
34
|
+
@formats = {}
|
|
35
|
+
@mutex = Mutex.new
|
|
36
|
+
@config = load_config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Discover available plugins
|
|
40
|
+
#
|
|
41
|
+
# Searches for plugins in gem paths using the pattern
|
|
42
|
+
# 'cabriolet/plugins/**/*.rb'. Discovered plugins are added to the
|
|
43
|
+
# registry in :discovered state.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<String>] List of discovered plugin names
|
|
46
|
+
#
|
|
47
|
+
# @example Discover plugins
|
|
48
|
+
# manager.discover_plugins
|
|
49
|
+
# #=> ["plugin1", "plugin2"]
|
|
50
|
+
def discover_plugins
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
plugin_files = Gem.find_files("cabriolet/plugins/**/*.rb")
|
|
53
|
+
|
|
54
|
+
plugin_files.each do |file|
|
|
55
|
+
load_plugin_file(file)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
warn "Failed to load plugin from #{file}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@plugins.keys
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Register a plugin instance
|
|
65
|
+
#
|
|
66
|
+
# Adds a plugin to the registry. The plugin must be a valid Plugin
|
|
67
|
+
# instance with proper metadata.
|
|
68
|
+
#
|
|
69
|
+
# @param plugin_instance [Plugin] Plugin instance to register
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] True if registered successfully
|
|
72
|
+
#
|
|
73
|
+
# @raise [PluginError] If plugin is invalid
|
|
74
|
+
#
|
|
75
|
+
# @example Register a plugin
|
|
76
|
+
# plugin = MyPlugin.new(manager)
|
|
77
|
+
# manager.register(plugin)
|
|
78
|
+
def register(plugin_instance)
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
unless plugin_instance.is_a?(Plugin)
|
|
81
|
+
raise PluginError,
|
|
82
|
+
"Plugin must inherit from Cabriolet::Plugin"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validate plugin
|
|
86
|
+
validation = PluginValidator.validate(plugin_instance.class)
|
|
87
|
+
unless validation[:valid]
|
|
88
|
+
raise PluginError,
|
|
89
|
+
"Plugin validation failed: #{validation[:errors].join(', ')}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
meta = plugin_instance.metadata
|
|
93
|
+
name = meta[:name]
|
|
94
|
+
|
|
95
|
+
if @plugins.key?(name)
|
|
96
|
+
raise PluginError, "Plugin '#{name}' already registered"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@plugins[name] = {
|
|
100
|
+
instance: plugin_instance,
|
|
101
|
+
metadata: meta,
|
|
102
|
+
state: :discovered,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Load a plugin
|
|
110
|
+
#
|
|
111
|
+
# Loads and validates a discovered plugin. Calls the plugin's setup
|
|
112
|
+
# method and transitions to :loaded state.
|
|
113
|
+
#
|
|
114
|
+
# @param name [String] Plugin name
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] True if loaded successfully
|
|
117
|
+
#
|
|
118
|
+
# @raise [PluginError] If plugin not found or load fails
|
|
119
|
+
#
|
|
120
|
+
# @example Load a plugin
|
|
121
|
+
# manager.load_plugin("my-plugin")
|
|
122
|
+
def load_plugin(name)
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
entry = @plugins[name]
|
|
125
|
+
raise PluginError, "Plugin '#{name}' not found" unless entry
|
|
126
|
+
|
|
127
|
+
if %i[loaded active].include?(entry[:state])
|
|
128
|
+
return true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
begin
|
|
132
|
+
plugin = entry[:instance]
|
|
133
|
+
|
|
134
|
+
# Check dependencies
|
|
135
|
+
check_dependencies!(entry[:metadata])
|
|
136
|
+
|
|
137
|
+
# Call setup
|
|
138
|
+
plugin.setup
|
|
139
|
+
plugin.send(:update_state, :loaded)
|
|
140
|
+
entry[:state] = :loaded
|
|
141
|
+
|
|
142
|
+
true
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
plugin.send(:update_state, :failed)
|
|
145
|
+
entry[:state] = :failed
|
|
146
|
+
entry[:error] = e.message
|
|
147
|
+
raise PluginError, "Failed to load plugin '#{name}': #{e.message}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Activate a plugin
|
|
153
|
+
#
|
|
154
|
+
# Activates a loaded plugin. Calls the plugin's activate method and
|
|
155
|
+
# transitions to :active state.
|
|
156
|
+
#
|
|
157
|
+
# @param name [String] Plugin name
|
|
158
|
+
#
|
|
159
|
+
# @return [Boolean] True if activated successfully
|
|
160
|
+
#
|
|
161
|
+
# @raise [PluginError] If plugin not found or not loaded
|
|
162
|
+
#
|
|
163
|
+
# @example Activate a plugin
|
|
164
|
+
# manager.activate_plugin("my-plugin")
|
|
165
|
+
def activate_plugin(name)
|
|
166
|
+
@mutex.synchronize do
|
|
167
|
+
entry = @plugins[name]
|
|
168
|
+
raise PluginError, "Plugin '#{name}' not found" unless entry
|
|
169
|
+
|
|
170
|
+
if entry[:state] == :active
|
|
171
|
+
return true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
unless entry[:state] == :loaded
|
|
175
|
+
raise PluginError,
|
|
176
|
+
"Plugin '#{name}' must be loaded before activation"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
plugin = entry[:instance]
|
|
181
|
+
plugin.activate
|
|
182
|
+
plugin.send(:update_state, :active)
|
|
183
|
+
entry[:state] = :active
|
|
184
|
+
|
|
185
|
+
true
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
plugin.send(:update_state, :failed)
|
|
188
|
+
entry[:state] = :failed
|
|
189
|
+
entry[:error] = e.message
|
|
190
|
+
raise PluginError,
|
|
191
|
+
"Failed to activate plugin '#{name}': #{e.message}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Deactivate a plugin
|
|
197
|
+
#
|
|
198
|
+
# Deactivates an active plugin. Calls the plugin's deactivate method
|
|
199
|
+
# and transitions back to :loaded state.
|
|
200
|
+
#
|
|
201
|
+
# @param name [String] Plugin name
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean] True if deactivated successfully
|
|
204
|
+
#
|
|
205
|
+
# @raise [PluginError] If plugin not found
|
|
206
|
+
#
|
|
207
|
+
# @example Deactivate a plugin
|
|
208
|
+
# manager.deactivate_plugin("my-plugin")
|
|
209
|
+
def deactivate_plugin(name)
|
|
210
|
+
@mutex.synchronize do
|
|
211
|
+
entry = @plugins[name]
|
|
212
|
+
raise PluginError, "Plugin '#{name}' not found" unless entry
|
|
213
|
+
|
|
214
|
+
if entry[:state] != :active
|
|
215
|
+
return true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
plugin = entry[:instance]
|
|
220
|
+
plugin.deactivate
|
|
221
|
+
plugin.send(:update_state, :loaded)
|
|
222
|
+
entry[:state] = :loaded
|
|
223
|
+
|
|
224
|
+
true
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
entry[:error] = e.message
|
|
227
|
+
raise PluginError,
|
|
228
|
+
"Failed to deactivate plugin '#{name}': #{e.message}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# List plugins
|
|
234
|
+
#
|
|
235
|
+
# Returns plugin information, optionally filtered by state.
|
|
236
|
+
#
|
|
237
|
+
# @param state [Symbol, nil] Optional state filter (:discovered,
|
|
238
|
+
# :loaded, :active, :failed, :disabled)
|
|
239
|
+
#
|
|
240
|
+
# @return [Hash] Plugin information keyed by name
|
|
241
|
+
#
|
|
242
|
+
# @example List all plugins
|
|
243
|
+
# manager.list_plugins
|
|
244
|
+
# #=> { "plugin1" => {...}, "plugin2" => {...} }
|
|
245
|
+
#
|
|
246
|
+
# @example List only active plugins
|
|
247
|
+
# manager.list_plugins(state: :active)
|
|
248
|
+
# #=> { "active-plugin" => {...} }
|
|
249
|
+
def list_plugins(state: nil)
|
|
250
|
+
@mutex.synchronize do
|
|
251
|
+
if state.nil?
|
|
252
|
+
@plugins.transform_values do |entry|
|
|
253
|
+
{
|
|
254
|
+
metadata: entry[:metadata],
|
|
255
|
+
state: entry[:state],
|
|
256
|
+
error: entry[:error],
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
else
|
|
260
|
+
@plugins.select { |_, entry| entry[:state] == state }
|
|
261
|
+
.transform_values do |entry|
|
|
262
|
+
{
|
|
263
|
+
metadata: entry[:metadata],
|
|
264
|
+
state: entry[:state],
|
|
265
|
+
error: entry[:error],
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Get a plugin by name
|
|
273
|
+
#
|
|
274
|
+
# @param name [String] Plugin name
|
|
275
|
+
#
|
|
276
|
+
# @return [Plugin, nil] Plugin instance or nil if not found
|
|
277
|
+
#
|
|
278
|
+
# @example Get a plugin
|
|
279
|
+
# plugin = manager.plugin("my-plugin")
|
|
280
|
+
def plugin(name)
|
|
281
|
+
@mutex.synchronize do
|
|
282
|
+
@plugins[name]&.dig(:instance)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Check if a plugin is active
|
|
287
|
+
#
|
|
288
|
+
# @param name [String] Plugin name
|
|
289
|
+
#
|
|
290
|
+
# @return [Boolean] True if plugin is active
|
|
291
|
+
#
|
|
292
|
+
# @example Check plugin status
|
|
293
|
+
# manager.plugin_active?("my-plugin") #=> true
|
|
294
|
+
def plugin_active?(name)
|
|
295
|
+
@mutex.synchronize do
|
|
296
|
+
@plugins[name]&.dig(:state) == :active
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Register a format handler
|
|
301
|
+
#
|
|
302
|
+
# Called by plugins to register format handlers. This is used
|
|
303
|
+
# internally by Plugin#register_format.
|
|
304
|
+
#
|
|
305
|
+
# @param format [Symbol] Format identifier
|
|
306
|
+
# @param handler [Class] Handler class
|
|
307
|
+
#
|
|
308
|
+
# @return [void]
|
|
309
|
+
#
|
|
310
|
+
# @api private
|
|
311
|
+
def register_format(format, handler)
|
|
312
|
+
@mutex.synchronize do
|
|
313
|
+
@formats[format] = handler
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Get format handler
|
|
318
|
+
#
|
|
319
|
+
# @param format [Symbol] Format identifier
|
|
320
|
+
#
|
|
321
|
+
# @return [Class, nil] Handler class or nil
|
|
322
|
+
#
|
|
323
|
+
# @example Get format handler
|
|
324
|
+
# handler = manager.format_handler(:myformat)
|
|
325
|
+
def format_handler(format)
|
|
326
|
+
@mutex.synchronize do
|
|
327
|
+
@formats[format]
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
private
|
|
332
|
+
|
|
333
|
+
# Load configuration from ~/.cabriolet/plugins.yml
|
|
334
|
+
#
|
|
335
|
+
# @return [Hash] Configuration hash
|
|
336
|
+
def load_config
|
|
337
|
+
config_path = File.expand_path("~/.cabriolet/plugins.yml")
|
|
338
|
+
return {} unless File.exist?(config_path)
|
|
339
|
+
|
|
340
|
+
YAML.load_file(config_path) || {}
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
warn "Failed to load plugin config: #{e.message}"
|
|
343
|
+
{}
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Load a plugin file
|
|
347
|
+
#
|
|
348
|
+
# @param file [String] Plugin file path
|
|
349
|
+
#
|
|
350
|
+
# @return [void]
|
|
351
|
+
def load_plugin_file(file)
|
|
352
|
+
require file
|
|
353
|
+
|
|
354
|
+
# After requiring, plugin classes should auto-register
|
|
355
|
+
# This is a convention - plugins call register in their class body
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Check plugin dependencies
|
|
359
|
+
#
|
|
360
|
+
# @param metadata [Hash] Plugin metadata
|
|
361
|
+
#
|
|
362
|
+
# @return [void]
|
|
363
|
+
#
|
|
364
|
+
# @raise [PluginError] If dependencies not met
|
|
365
|
+
def check_dependencies!(metadata)
|
|
366
|
+
dependencies = metadata[:dependencies] || []
|
|
367
|
+
|
|
368
|
+
dependencies.each do |dep|
|
|
369
|
+
dep_name, dep_version = parse_dependency(dep)
|
|
370
|
+
|
|
371
|
+
unless @plugins.key?(dep_name)
|
|
372
|
+
raise PluginError,
|
|
373
|
+
"Missing dependency: #{dep_name}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
dep_entry = @plugins[dep_name]
|
|
377
|
+
unless %i[loaded active].include?(dep_entry[:state])
|
|
378
|
+
raise PluginError,
|
|
379
|
+
"Dependency '#{dep_name}' not loaded"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
if dep_version
|
|
383
|
+
actual_version = dep_entry[:metadata][:version]
|
|
384
|
+
unless version_satisfies?(actual_version, dep_version)
|
|
385
|
+
raise PluginError,
|
|
386
|
+
"Dependency '#{dep_name}' version mismatch: " \
|
|
387
|
+
"need #{dep_version}, have #{actual_version}"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Parse dependency string
|
|
394
|
+
#
|
|
395
|
+
# @param dep [String] Dependency string (e.g., "plugin >= 1.0")
|
|
396
|
+
#
|
|
397
|
+
# @return [Array<String, String>] [name, version_requirement]
|
|
398
|
+
def parse_dependency(dep)
|
|
399
|
+
parts = dep.split
|
|
400
|
+
name = parts[0]
|
|
401
|
+
version = parts[1..].join(" ") if parts.length > 1
|
|
402
|
+
|
|
403
|
+
[name, version]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Check if version satisfies requirement
|
|
407
|
+
#
|
|
408
|
+
# @param version [String] Actual version
|
|
409
|
+
# @param requirement [String] Version requirement
|
|
410
|
+
#
|
|
411
|
+
# @return [Boolean] True if satisfied
|
|
412
|
+
def version_satisfies?(version, requirement)
|
|
413
|
+
# Simple version check - can be enhanced with gem version logic
|
|
414
|
+
return true if requirement.nil?
|
|
415
|
+
|
|
416
|
+
# Parse requirement (e.g., ">= 1.0", "~> 2.0", "= 1.5")
|
|
417
|
+
if requirement.start_with?(">=")
|
|
418
|
+
min_version = requirement.sub(">=", "").strip
|
|
419
|
+
compare_versions(version, min_version) >= 0
|
|
420
|
+
elsif requirement.start_with?("~>")
|
|
421
|
+
# Pessimistic version constraint
|
|
422
|
+
base = requirement.sub("~>", "").strip
|
|
423
|
+
compare_versions(version, base) >= 0
|
|
424
|
+
elsif requirement.start_with?("=")
|
|
425
|
+
exact = requirement.sub("=", "").strip
|
|
426
|
+
version == exact
|
|
427
|
+
else
|
|
428
|
+
true
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Compare two version strings
|
|
433
|
+
#
|
|
434
|
+
# @param v1 [String] Version 1
|
|
435
|
+
# @param v2 [String] Version 2
|
|
436
|
+
#
|
|
437
|
+
# @return [Integer] -1, 0, or 1
|
|
438
|
+
def compare_versions(v1, v2)
|
|
439
|
+
parts1 = v1.split(".").map(&:to_i)
|
|
440
|
+
parts2 = v2.split(".").map(&:to_i)
|
|
441
|
+
|
|
442
|
+
[parts1.length, parts2.length].max.times do |i|
|
|
443
|
+
p1 = parts1[i] || 0
|
|
444
|
+
p2 = parts2[i] || 0
|
|
445
|
+
|
|
446
|
+
return -1 if p1 < p2
|
|
447
|
+
return 1 if p1 > p2
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
0
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|