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.
- checksums.yaml +4 -4
- data/lib/ukiryu/cache.rb +6 -0
- data/lib/ukiryu/cache_registry.rb +64 -0
- data/lib/ukiryu/cli_commands/base_command.rb +6 -5
- data/lib/ukiryu/cli_commands/config_command.rb +7 -10
- data/lib/ukiryu/cli_commands/register_command.rb +27 -18
- data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
- data/lib/ukiryu/command_builder.rb +83 -50
- data/lib/ukiryu/config.rb +13 -2
- data/lib/ukiryu/debug.rb +20 -9
- data/lib/ukiryu/definition/loader.rb +3 -3
- data/lib/ukiryu/errors.rb +37 -37
- data/lib/ukiryu/executable_locator.rb +40 -16
- data/lib/ukiryu/extractors/base_extractor.rb +2 -1
- data/lib/ukiryu/extractors/help_parser.rb +3 -0
- data/lib/ukiryu/logger.rb +51 -0
- data/lib/ukiryu/models/implementation_index.rb +2 -1
- data/lib/ukiryu/models/implementation_version.rb +18 -1
- data/lib/ukiryu/models/interface.rb +2 -1
- data/lib/ukiryu/models/run_environment.rb +0 -2
- data/lib/ukiryu/models/semantic_version.rb +174 -0
- data/lib/ukiryu/models/stage_metrics.rb +0 -1
- data/lib/ukiryu/register.rb +473 -232
- data/lib/ukiryu/shell/powershell.rb +209 -89
- data/lib/ukiryu/shell/sh.rb +4 -1
- data/lib/ukiryu/shell.rb +60 -2
- data/lib/ukiryu/tool/command_resolution.rb +2 -1
- data/lib/ukiryu/tool/executable_discovery.rb +14 -15
- data/lib/ukiryu/tool/loader.rb +543 -0
- data/lib/ukiryu/tool/version_detection.rb +1 -3
- data/lib/ukiryu/tool.rb +79 -87
- data/lib/ukiryu/tool_index.rb +127 -62
- data/lib/ukiryu/tools/base.rb +4 -2
- data/lib/ukiryu/type.rb +26 -15
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu.rb +1 -1
- data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
- data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/tool_helper.rb +2 -0
- data/spec/ukiryu/definition/loader_spec.rb +2 -2
- data/spec/ukiryu/executor_spec.rb +6 -3
- data/spec/ukiryu/models/execution_report_spec.rb +3 -2
- data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
- data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
- data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
- data/spec/ukiryu/shell/powershell_spec.rb +286 -51
- data/spec/ukiryu/tool/loader_spec.rb +148 -0
- data/spec/ukiryu/tool_index_spec.rb +110 -18
- data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
- data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
- data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
- metadata +14 -2
- 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?
|