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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +700 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +154 -14
  6. data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +35 -43
  13. data/lib/cabriolet/cab/decompressor.rb +14 -19
  14. data/lib/cabriolet/cab/extractor.rb +140 -31
  15. data/lib/cabriolet/chm/command_handler.rb +227 -0
  16. data/lib/cabriolet/chm/compressor.rb +7 -3
  17. data/lib/cabriolet/chm/decompressor.rb +39 -21
  18. data/lib/cabriolet/chm/parser.rb +5 -2
  19. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  20. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  21. data/lib/cabriolet/cli/command_registry.rb +83 -0
  22. data/lib/cabriolet/cli.rb +356 -607
  23. data/lib/cabriolet/compressors/base.rb +1 -1
  24. data/lib/cabriolet/compressors/lzx.rb +241 -54
  25. data/lib/cabriolet/compressors/mszip.rb +35 -3
  26. data/lib/cabriolet/compressors/quantum.rb +34 -45
  27. data/lib/cabriolet/decompressors/base.rb +1 -1
  28. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  29. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  30. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  31. data/lib/cabriolet/decompressors/quantum.rb +3 -2
  32. data/lib/cabriolet/errors.rb +3 -0
  33. data/lib/cabriolet/file_entry.rb +156 -0
  34. data/lib/cabriolet/file_manager.rb +144 -0
  35. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  36. data/lib/cabriolet/hlp/compressor.rb +28 -238
  37. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  38. data/lib/cabriolet/hlp/parser.rb +52 -101
  39. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  40. data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
  41. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  42. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  43. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  44. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  45. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  46. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  47. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  48. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  49. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  50. data/lib/cabriolet/huffman/tree.rb +85 -1
  51. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  52. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  53. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  54. data/lib/cabriolet/lit/command_handler.rb +221 -0
  55. data/lib/cabriolet/lit/compressor.rb +633 -38
  56. data/lib/cabriolet/lit/decompressor.rb +518 -152
  57. data/lib/cabriolet/lit/parser.rb +670 -0
  58. data/lib/cabriolet/models/hlp_file.rb +130 -29
  59. data/lib/cabriolet/models/hlp_header.rb +105 -17
  60. data/lib/cabriolet/models/lit_header.rb +212 -25
  61. data/lib/cabriolet/models/szdd_header.rb +10 -2
  62. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  63. data/lib/cabriolet/oab/command_handler.rb +257 -0
  64. data/lib/cabriolet/oab/compressor.rb +17 -8
  65. data/lib/cabriolet/oab/decompressor.rb +41 -10
  66. data/lib/cabriolet/offset_calculator.rb +81 -0
  67. data/lib/cabriolet/plugin.rb +233 -0
  68. data/lib/cabriolet/plugin_manager.rb +453 -0
  69. data/lib/cabriolet/plugin_validator.rb +422 -0
  70. data/lib/cabriolet/system/io_system.rb +3 -0
  71. data/lib/cabriolet/system/memory_handle.rb +17 -4
  72. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  73. data/lib/cabriolet/szdd/compressor.rb +15 -11
  74. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  75. data/lib/cabriolet/version.rb +1 -1
  76. data/lib/cabriolet.rb +67 -17
  77. 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