ukiryu 0.1.6 → 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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ukiryu/cache.rb +6 -0
  3. data/lib/ukiryu/cache_registry.rb +64 -0
  4. data/lib/ukiryu/cli_commands/base_command.rb +6 -5
  5. data/lib/ukiryu/cli_commands/config_command.rb +7 -10
  6. data/lib/ukiryu/cli_commands/register_command.rb +27 -18
  7. data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
  8. data/lib/ukiryu/command_builder.rb +83 -50
  9. data/lib/ukiryu/config.rb +13 -2
  10. data/lib/ukiryu/debug.rb +20 -9
  11. data/lib/ukiryu/definition/loader.rb +3 -3
  12. data/lib/ukiryu/errors.rb +37 -37
  13. data/lib/ukiryu/executable_locator.rb +40 -16
  14. data/lib/ukiryu/extractors/base_extractor.rb +2 -1
  15. data/lib/ukiryu/extractors/help_parser.rb +3 -0
  16. data/lib/ukiryu/logger.rb +51 -0
  17. data/lib/ukiryu/models/implementation_index.rb +2 -1
  18. data/lib/ukiryu/models/implementation_version.rb +18 -1
  19. data/lib/ukiryu/models/interface.rb +2 -1
  20. data/lib/ukiryu/models/run_environment.rb +0 -2
  21. data/lib/ukiryu/models/semantic_version.rb +174 -0
  22. data/lib/ukiryu/models/stage_metrics.rb +0 -1
  23. data/lib/ukiryu/register.rb +473 -232
  24. data/lib/ukiryu/shell/powershell.rb +209 -89
  25. data/lib/ukiryu/shell/sh.rb +4 -1
  26. data/lib/ukiryu/shell.rb +60 -2
  27. data/lib/ukiryu/tool/command_resolution.rb +2 -1
  28. data/lib/ukiryu/tool/executable_discovery.rb +14 -15
  29. data/lib/ukiryu/tool/loader.rb +543 -0
  30. data/lib/ukiryu/tool/version_detection.rb +1 -3
  31. data/lib/ukiryu/tool.rb +79 -87
  32. data/lib/ukiryu/tool_index.rb +127 -62
  33. data/lib/ukiryu/tools/base.rb +4 -2
  34. data/lib/ukiryu/type.rb +26 -15
  35. data/lib/ukiryu/version.rb +1 -1
  36. data/lib/ukiryu.rb +1 -1
  37. data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
  38. data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
  39. data/spec/spec_helper.rb +10 -6
  40. data/spec/support/tool_helper.rb +2 -0
  41. data/spec/ukiryu/definition/loader_spec.rb +2 -2
  42. data/spec/ukiryu/executor_spec.rb +6 -3
  43. data/spec/ukiryu/models/execution_report_spec.rb +3 -2
  44. data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
  45. data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
  46. data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
  47. data/spec/ukiryu/shell/powershell_spec.rb +286 -51
  48. data/spec/ukiryu/tool/loader_spec.rb +148 -0
  49. data/spec/ukiryu/tool_index_spec.rb +110 -18
  50. data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
  51. data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
  52. data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
  53. metadata +14 -2
  54. data/lib/ukiryu/register_auto_manager.rb +0 -342
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ class Tool
5
+ # Handles loading of tools using the ImplementationIndex architecture
6
+ #
7
+ # This module extracts the complex loading pipeline from the main Tool class:
8
+ # 1. Load ImplementationIndex from register
9
+ # 2. Load Interface definition
10
+ # 3. Detect implementation and version
11
+ # 4. Load ImplementationVersion
12
+ # 5. Convert to ToolDefinition for compatibility
13
+ #
14
+ # @api private
15
+ module Loader
16
+ class << self
17
+ # Load a tool using the new ImplementationIndex architecture
18
+ #
19
+ # @param name [String, Symbol] the tool name
20
+ # @param options [Hash] loading options
21
+ # @return [Tool, nil] the tool instance or nil if not using new architecture
22
+ def load_with_implementation_index(name, options = {})
23
+ require_relative '../version_scheme_resolver'
24
+
25
+ # Try to load ImplementationIndex
26
+ index = Register.load_implementation_index(name, options)
27
+ return nil unless index
28
+
29
+ # Load Interface
30
+ interface = Register.load_interface(index.interface, options)
31
+ return nil unless interface
32
+
33
+ # Detect implementation and version
34
+ impl_spec = detect_implementation_and_version(index, name, options)
35
+ return nil unless impl_spec
36
+
37
+ # Load ImplementationVersion
38
+ impl_version = Register.load_implementation_version(
39
+ name,
40
+ impl_spec[:implementation_name],
41
+ impl_spec[:file],
42
+ options
43
+ )
44
+ return nil unless impl_version
45
+
46
+ # Convert to old ToolDefinition format for compatibility
47
+ tool_definition = convert_to_tool_definition(
48
+ name,
49
+ interface,
50
+ impl_version,
51
+ impl_spec[:implementation_name],
52
+ impl_spec[:version], # Pass detected version
53
+ options
54
+ )
55
+ return nil unless tool_definition
56
+
57
+ # Create tool instance
58
+ Tool.new(tool_definition, options)
59
+ end
60
+
61
+ # Detect implementation and version from ImplementationIndex
62
+ #
63
+ # @param index [Models::ImplementationIndex] the implementation index
64
+ # @param tool_name [String] the tool name for executable lookup
65
+ # @param options [Hash] options including platform and shell
66
+ # @return [Hash, nil] hash with :implementation_name, :version, :file or nil
67
+ def detect_implementation_and_version(index, tool_name, options = {})
68
+ # Try each implementation in order
69
+ index.implementations.each do |impl|
70
+ result = try_implementation(impl, tool_name, options)
71
+ return result if result
72
+ end
73
+
74
+ # If no implementation matched, use the first one's default
75
+ fallback_to_default(index)
76
+ end
77
+
78
+ private
79
+
80
+ # Try to detect version from a single implementation
81
+ #
82
+ # @param impl [Hash] implementation definition
83
+ # @param tool_name [String] the tool name
84
+ # @param options [Hash] options
85
+ # @return [Hash, nil] implementation spec or nil
86
+ def try_implementation(impl, tool_name, options)
87
+ detection = impl[:detection] || impl['detection']
88
+ detection_result = run_detection_command(detection, tool_name, options)
89
+ return nil unless detection_result
90
+
91
+ # Extract version using pattern
92
+ pattern = detection[:pattern] || detection['pattern']
93
+ version = extract_version_from_pattern(detection_result, pattern)
94
+
95
+ # If detection succeeded but no version was extracted, check if pattern matched
96
+ if version.nil? && pattern
97
+ has_pattern = detection_result.match?(Regexp.new(pattern))
98
+ return nil unless has_pattern
99
+ end
100
+
101
+ # Resolve version scheme and find matching spec
102
+ build_implementation_spec(impl, version)
103
+ end
104
+
105
+ # Build implementation spec from detected version
106
+ #
107
+ # @param impl [Hash] implementation definition
108
+ # @param version [String, nil] detected version
109
+ # @return [Hash, nil] implementation spec or nil
110
+ def build_implementation_spec(impl, version)
111
+ require_relative '../version_scheme_resolver'
112
+ version_scheme = impl[:version_scheme] || impl['version_scheme']
113
+ scheme = VersionSchemeResolver.resolve(version_scheme)
114
+ versions = impl[:versions] || impl['versions']
115
+
116
+ if version.nil?
117
+ # Use default version spec
118
+ build_default_spec(impl, versions)
119
+ else
120
+ # Find matching version spec for detected version
121
+ version_spec = find_matching_version_spec(versions, version, scheme)
122
+ if version_spec
123
+ {
124
+ implementation_name: impl[:name] || impl['name'],
125
+ version: version,
126
+ file: version_spec[:file] || version_spec['file']
127
+ }
128
+ end
129
+ end
130
+ end
131
+
132
+ # Build default spec when no version detected
133
+ #
134
+ # @param impl [Hash] implementation definition
135
+ # @param versions [Array] version specs
136
+ # @return [Hash] default implementation spec
137
+ def build_default_spec(impl, versions)
138
+ impl_default = impl[:default] || impl['default']
139
+ version_spec = if impl_default
140
+ versions.find { |v| v[:file] == impl_default || v['file'] == impl_default } || versions.last
141
+ else
142
+ versions.find { |v| v[:default] || v['default'] } || versions.last
143
+ end
144
+ {
145
+ implementation_name: impl[:name] || impl['name'],
146
+ version: nil,
147
+ file: version_spec[:file] || version_spec['file'] || impl_default
148
+ }
149
+ end
150
+
151
+ # Fallback to first implementation's default
152
+ #
153
+ # @param index [Models::ImplementationIndex] the implementation index
154
+ # @return [Hash, nil] fallback spec or nil
155
+ def fallback_to_default(index)
156
+ return nil if index.implementations.empty?
157
+
158
+ impl = index.implementations.first
159
+ versions = impl[:versions] || impl['versions']
160
+ build_default_spec(impl, versions)
161
+ end
162
+
163
+ # Run detection command for an implementation
164
+ #
165
+ # @param detection [Hash] detection configuration
166
+ # @param tool_name [String] the tool name for executable lookup
167
+ # @param options [Hash] options
168
+ # @return [String, nil] command output or nil
169
+ def run_detection_command(detection, tool_name, options = {})
170
+ command = detection[:command] || detection['command']
171
+ return nil unless command
172
+
173
+ cmd = Array(command)
174
+
175
+ # Support multiple executables for detection
176
+ executables = detection[:executables] || detection['executables']
177
+ if executables
178
+ # Try each executable until one succeeds
179
+ Array(executables).each do |executable|
180
+ full_cmd = [executable] + cmd
181
+ result = try_execute_command(full_cmd, options)
182
+ return result if result
183
+ end
184
+ nil
185
+ else
186
+ executable = detection[:executable] || detection['executable'] || options[:executable] || tool_name.to_s
187
+ full_cmd = [executable] + cmd
188
+ try_execute_command(full_cmd, options)
189
+ end
190
+ end
191
+
192
+ # Try executing a command and return stdout on success
193
+ #
194
+ # @param cmd [Array] command parts
195
+ # @param options [Hash] options
196
+ # @return [String, nil] stdout or stderr (if command failed but has output) or nil
197
+ def try_execute_command(cmd, options = {})
198
+ require_relative '../executor'
199
+ require_relative '../shell'
200
+ result = Executor.execute(
201
+ cmd.first,
202
+ cmd.drop(1),
203
+ env: options[:env],
204
+ shell: options[:shell] || Shell.detect,
205
+ timeout: 5,
206
+ allow_failure: true
207
+ )
208
+ if result.success?
209
+ result.stdout.scrub('')
210
+ elsif !result.stderr.to_s.strip.empty?
211
+ result.stderr.scrub('')
212
+ end
213
+ rescue StandardError
214
+ nil
215
+ end
216
+
217
+ # Extract version from command output using pattern
218
+ #
219
+ # @param output [String] command output
220
+ # @param pattern [String] regex pattern
221
+ # @return [String, nil] extracted version or nil
222
+ def extract_version_from_pattern(output, pattern)
223
+ return nil unless output && pattern
224
+
225
+ scrubbed_output = output.scrub('')
226
+ match = scrubbed_output.match(Regexp.new(pattern))
227
+ return nil unless match
228
+
229
+ match[1]
230
+ end
231
+
232
+ # Find matching version spec using versionian
233
+ #
234
+ # @param versions [Array<Hash>] version specs
235
+ # @param detected_version [String] detected version
236
+ # @param scheme [Versionian::VersionScheme] versionian scheme
237
+ # @return [Hash, nil] matching version spec or nil
238
+ def find_matching_version_spec(versions, detected_version, scheme)
239
+ require 'versionian'
240
+
241
+ versions.each do |version_spec|
242
+ range_type = determine_range_type(version_spec)
243
+ next unless range_type
244
+
245
+ range = build_version_range(version_spec, range_type, scheme)
246
+ return version_spec if range&.matches?(detected_version)
247
+ end
248
+ nil
249
+ end
250
+
251
+ # Determine the range type from a version spec
252
+ #
253
+ # @param version_spec [Hash] version specification
254
+ # @return [Symbol, nil] range type (:equals, :before, :after, :between) or nil
255
+ def determine_range_type(version_spec)
256
+ if version_spec[:equals] || version_spec['equals']
257
+ :equals
258
+ elsif version_spec[:before] || version_spec['before']
259
+ :before
260
+ elsif version_spec[:after] || version_spec['after']
261
+ :after
262
+ elsif version_spec[:between] || version_spec['between']
263
+ :between
264
+ end
265
+ end
266
+
267
+ # Build a VersionRange from a version spec
268
+ #
269
+ # @param version_spec [Hash] version specification
270
+ # @param range_type [Symbol] range type
271
+ # @param scheme [Versionian::VersionScheme] version scheme
272
+ # @return [Versionian::VersionRange, nil] version range or nil
273
+ def build_version_range(version_spec, range_type, scheme)
274
+ case range_type
275
+ when :equals
276
+ boundary = version_spec[:equals] || version_spec['equals']
277
+ Versionian::VersionRange.new(:equals, scheme, version: boundary)
278
+ when :before
279
+ boundary = version_spec[:before] || version_spec['before']
280
+ Versionian::VersionRange.new(:before, scheme, version: boundary)
281
+ when :after
282
+ boundary = version_spec[:after] || version_spec['after']
283
+ Versionian::VersionRange.new(:after, scheme, version: boundary)
284
+ when :between
285
+ between = version_spec[:between] || version_spec['between']
286
+ from = between[:from] || between['from']
287
+ to = between[:to] || between['to']
288
+ Versionian::VersionRange.new(:between, scheme, from: from, to: to)
289
+ end
290
+ end
291
+
292
+ # Convert ImplementationVersion to ToolDefinition for compatibility
293
+ #
294
+ # @param tool_name [String] tool name
295
+ # @param interface [Models::Interface] interface
296
+ # @param impl_version [Models::ImplementationVersion] implementation version
297
+ # @param implementation_name [String] implementation name
298
+ # @param detected_version [String, nil] detected version
299
+ # @param options [Hash] options
300
+ # @return [ToolDefinition] converted tool definition
301
+ def convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version,
302
+ options = {})
303
+ require_relative '../models/tool_definition'
304
+ require_relative '../models/platform_profile'
305
+
306
+ # Select compatible execution profile
307
+ profile = impl_version.compatible_profile(
308
+ platform: options[:platform] || Platform.detect,
309
+ shell: options[:shell] || Shell.detect
310
+ )
311
+
312
+ return nil unless profile
313
+
314
+ # Debug output for profile selection
315
+ Logger.debug("Selected profile name: #{profile[:name] || profile['name']}",
316
+ category: :executable)
317
+ Logger.debug("Profile inherits: #{profile[:inherits] || profile['inherits']}",
318
+ category: :executable)
319
+ Logger.debug("Profile executable_name: #{profile[:executable_name] || profile['executable_name']}",
320
+ category: :executable)
321
+ Logger.debug("Profile has commands: #{(profile[:commands] || profile['commands']).inspect[0..100]}",
322
+ category: :executable)
323
+ Logger.debug("Full profile keys: #{profile.keys.inspect}", category: :executable)
324
+
325
+ # Build tool name (append implementation name for non-default)
326
+ specific_tool_name = build_tool_name(tool_name, implementation_name)
327
+ version = detected_version || impl_version.version
328
+
329
+ tool_def = Models::ToolDefinition.new(
330
+ name: specific_tool_name,
331
+ version: version,
332
+ display_name: impl_version.display_name || "#{interface.name} #{implementation_name} #{version}",
333
+ implements: Array(interface.name),
334
+ profiles: [convert_profile_to_platform_profile(profile, interface.actions)],
335
+ version_detection: impl_version.version_detection,
336
+ aliases: impl_version.aliases || []
337
+ )
338
+
339
+ tool_def.resolve_inheritance!
340
+ tool_def
341
+ end
342
+
343
+ # Build specific tool name with implementation suffix
344
+ #
345
+ # @param tool_name [String] base tool name
346
+ # @param implementation_name [String] implementation name
347
+ # @return [String] specific tool name
348
+ def build_tool_name(tool_name, implementation_name)
349
+ if implementation_name && implementation_name != 'default'
350
+ "#{tool_name}_#{implementation_name}"
351
+ else
352
+ tool_name
353
+ end
354
+ end
355
+
356
+ # Convert ExecutionProfile to PlatformProfile object
357
+ #
358
+ # @param profile [Models::ExecutionProfile, Hash] execution profile
359
+ # @param actions [Array<Hash>] interface actions
360
+ # @return [PlatformProfile] platform profile object
361
+ def convert_profile_to_platform_profile(profile, actions)
362
+ require_relative '../models/platform_profile'
363
+ require_relative '../models/command_definition'
364
+
365
+ profile_data = extract_profile_data(profile)
366
+ profile_commands = profile.respond_to?(:commands) ? profile.commands : (profile[:commands] || profile['commands'] || [])
367
+
368
+ # Debug output for profile loading issues (especially Windows)
369
+ Logger.debug("profile class: #{profile.class}", category: :executable)
370
+ Logger.debug("profile_data: #{profile_data.inspect}", category: :executable)
371
+ Logger.debug("profile_commands class: #{profile_commands.class}", category: :executable)
372
+ Logger.debug("profile_commands empty?: #{profile_commands.respond_to?(:empty?) ? profile_commands.empty? : 'N/A'}",
373
+ category: :executable)
374
+ Logger.debug("profile_commands count: #{profile_commands.respond_to?(:size) ? profile_commands.size : 'N/A'}",
375
+ category: :executable)
376
+ Logger.debug("profile_commands: #{profile_commands.inspect[0..500]}", category: :executable)
377
+ Logger.debug("actions keys: #{actions.keys.inspect if actions.respond_to?(:keys)}", category: :executable)
378
+
379
+ # Build command definitions
380
+ interface_commands_hash = build_interface_commands_hash(actions)
381
+ command_definitions = build_command_definitions(profile_commands, interface_commands_hash)
382
+
383
+ Models::PlatformProfile.new(
384
+ **profile_data,
385
+ commands: command_definitions
386
+ )
387
+ end
388
+
389
+ # Extract profile data from profile object or hash
390
+ #
391
+ # @param profile [Models::ExecutionProfile, Hash] profile
392
+ # @return [Hash] profile data
393
+ def extract_profile_data(profile)
394
+ if profile.is_a?(Hash)
395
+ {
396
+ name: profile[:name] || profile['name'],
397
+ display_name: profile[:display_name] || profile['display_name'],
398
+ platforms: profile[:platforms] || profile['platforms'],
399
+ shells: profile[:shells] || profile['shells'],
400
+ option_style: profile[:option_style] || profile['option_style'],
401
+ inherits: profile[:inherits] || profile['inherits'],
402
+ executable_name: profile[:executable_name] || profile['executable_name']
403
+ }
404
+ else
405
+ {
406
+ name: profile.name,
407
+ display_name: profile.display_name,
408
+ platforms: profile.platforms,
409
+ shells: profile.shells,
410
+ option_style: profile.option_style,
411
+ inherits: profile.respond_to?(:inherits) ? profile.inherits : nil,
412
+ executable_name: profile.respond_to?(:executable_name) ? profile.executable_name : nil
413
+ }
414
+ end
415
+ end
416
+
417
+ # Build interface commands hash from actions
418
+ #
419
+ # @param actions [Array<Hash>] interface actions
420
+ # @return [Hash] commands hash keyed by name
421
+ def build_interface_commands_hash(actions)
422
+ hash = {}
423
+ convert_actions_to_array(actions || []).each do |cmd|
424
+ cmd_name = cmd[:name] || cmd['name']
425
+ hash[cmd_name] = cmd
426
+ end
427
+ hash
428
+ end
429
+
430
+ # Build command definitions from profile and interface data
431
+ #
432
+ # @param profile_commands [Array] profile commands
433
+ # @param interface_commands_hash [Hash] interface commands
434
+ # @return [Array<CommandDefinition>] command definitions
435
+ def build_command_definitions(profile_commands, interface_commands_hash)
436
+ if profile_commands.nil? || profile_commands.empty?
437
+ # Use interface actions directly
438
+ interface_commands_hash.map do |_cmd_name, cmd_hash|
439
+ convert_hash_to_command_definition(cmd_hash)
440
+ end
441
+ else
442
+ # Merge profile commands with interface actions
443
+ profile_commands.map do |cmd_hash|
444
+ cmd_name = cmd_hash[:name] || cmd_hash['name'] || cmd_hash[:subcommand] || cmd_hash['subcommand']
445
+ interface_cmd = interface_commands_hash[cmd_name]
446
+ merged_cmd_hash = interface_cmd ? deep_merge_hashes(interface_cmd, cmd_hash) : cmd_hash
447
+ convert_hash_to_command_definition(merged_cmd_hash)
448
+ end
449
+ end
450
+ end
451
+
452
+ # Convert hash to CommandDefinition object
453
+ #
454
+ # @param cmd_hash [Hash] command definition hash
455
+ # @return [CommandDefinition] command definition object
456
+ def convert_hash_to_command_definition(cmd_hash)
457
+ require_relative '../models/command_definition'
458
+
459
+ post_options_data = cmd_hash['post_options'] || cmd_hash[:post_options]
460
+
461
+ Models::CommandDefinition.new(
462
+ name: cmd_hash['name'] || cmd_hash[:name],
463
+ description: cmd_hash['description'] || cmd_hash[:description],
464
+ usage: cmd_hash['usage'] || cmd_hash[:usage],
465
+ subcommand: cmd_hash['subcommand'] || cmd_hash[:subcommand],
466
+ belongs_to: cmd_hash['belongs_to'] || cmd_hash[:belongs_to],
467
+ cli_flag: cmd_hash['cli_flag'] || cmd_hash[:cli_flag],
468
+ standalone_executable: cmd_hash['standalone_executable'] || cmd_hash[:standalone_executable] || false,
469
+ aliases: cmd_hash['aliases'] || cmd_hash[:aliases] || [],
470
+ use_env_vars: cmd_hash['use_env_vars'] || cmd_hash[:use_env_vars] || [],
471
+ implements: cmd_hash['implements'] || cmd_hash[:implements] || [],
472
+ options: cmd_hash['options'] || cmd_hash[:options],
473
+ flags: cmd_hash['flags'] || cmd_hash[:flags],
474
+ arguments: cmd_hash['arguments'] || cmd_hash[:arguments],
475
+ post_options: post_options_data,
476
+ env_vars: cmd_hash['env_vars'] || cmd_hash[:env_vars],
477
+ exit_codes: cmd_hash['exit_codes'] || cmd_hash[:exit_codes]
478
+ )
479
+ end
480
+
481
+ # Deep merge two hashes (second hash takes precedence)
482
+ #
483
+ # @param base [Hash] base hash
484
+ # @param override [Hash] override hash
485
+ # @return [Hash] merged hash
486
+ def deep_merge_hashes(base, override)
487
+ base.merge(override) do |_key, old_val, new_val|
488
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
489
+ deep_merge_hashes(old_val, new_val)
490
+ elsif new_val.nil?
491
+ old_val
492
+ else
493
+ new_val
494
+ end
495
+ end
496
+ end
497
+
498
+ # Convert actions hash to array format
499
+ #
500
+ # @param actions_data [Hash, Array] actions data
501
+ # @return [Array<Hash>] array of command definitions
502
+ def convert_actions_to_array(actions_data)
503
+ return [] if actions_data.nil? || actions_data.empty?
504
+
505
+ if actions_data.is_a?(Hash)
506
+ actions_data.map do |command_name, command_def|
507
+ command_def = command_def.to_h
508
+ command_def['name'] ||= command_name.to_s
509
+ command_def
510
+ end
511
+ else
512
+ actions_data.map do |command_def|
513
+ flatten_signature(command_def.to_h)
514
+ end
515
+ end
516
+ end
517
+
518
+ # Flatten signature from command definition
519
+ #
520
+ # @param command_def [Hash] command definition
521
+ # @return [Hash] flattened command definition
522
+ def flatten_signature(command_def)
523
+ signature = command_def[:signature] || command_def['signature']
524
+ return command_def unless signature
525
+
526
+ signature.each do |key, value|
527
+ if [:inputs, 'inputs'].include?(key) && value.is_a?(Hash)
528
+ value.each do |nested_key, nested_value|
529
+ target_key = nested_key.to_s == 'inputs' ? 'arguments' : nested_key.to_s
530
+ command_def[target_key.to_sym] = nested_value unless [:signature, 'signature'].include?(nested_key)
531
+ end
532
+ else
533
+ command_def[key] = value unless [:signature, 'signature'].include?(key)
534
+ end
535
+ end
536
+ command_def.delete(:signature)
537
+ command_def.delete('signature')
538
+ command_def
539
+ end
540
+ end
541
+ end
542
+ end
543
+ end
@@ -26,9 +26,7 @@ module Ukiryu
26
26
  return nil unless vd
27
27
 
28
28
  # Check for new detection_methods array format
29
- if vd.respond_to?(:detection_methods) && vd.detection_methods && !vd.detection_methods.empty?
30
- return detect_version_with_detection_methods(vd.detection_methods)
31
- end
29
+ return detect_version_with_detection_methods(vd.detection_methods) if vd.respond_to?(:detection_methods) && vd.detection_methods && !vd.detection_methods.empty?
32
30
 
33
31
  # Legacy format: command-based detection
34
32
  return nil if vd.command.nil? || vd.command.empty?