ukiryu 0.1.0 → 0.1.1
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/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- data/.github/workflows/test.yml +0 -143
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lazy load ToolMetadata only when needed
|
|
4
|
+
autoload :ToolMetadata, File.expand_path('models/tool_metadata', __dir__)
|
|
5
|
+
|
|
6
|
+
module Ukiryu
|
|
7
|
+
# Index for fast tool lookup by interface and alias
|
|
8
|
+
#
|
|
9
|
+
# This class maintains cached mappings for:
|
|
10
|
+
# - Interfaces to tools (multiple tools can implement one interface)
|
|
11
|
+
# - Aliases to tool names
|
|
12
|
+
# - Registry change detection via mtime
|
|
13
|
+
#
|
|
14
|
+
# Built once and cached for the lifetime of the process.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class ToolIndex
|
|
18
|
+
class << self
|
|
19
|
+
# Get the singleton instance
|
|
20
|
+
#
|
|
21
|
+
# @return [ToolIndex] the index instance
|
|
22
|
+
def instance
|
|
23
|
+
@instance ||= new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Reset the index (mainly for testing)
|
|
27
|
+
def reset
|
|
28
|
+
@instance = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Initialize the index
|
|
33
|
+
#
|
|
34
|
+
# @param registry_path [String] the path to the tool registry
|
|
35
|
+
def initialize(registry_path: Ukiryu::Registry.default_registry_path)
|
|
36
|
+
@registry_path = registry_path
|
|
37
|
+
@interface_to_tools = {} # interface => [tool_names]
|
|
38
|
+
@alias_to_tool = {} # alias => tool_name
|
|
39
|
+
@built = false
|
|
40
|
+
@cache_key = nil # Registry state for change detection
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find tool metadata by interface name
|
|
44
|
+
# Returns the first available tool for this interface
|
|
45
|
+
#
|
|
46
|
+
# @param interface_name [Symbol] the interface to look up
|
|
47
|
+
# @return [ToolMetadata, nil] the tool metadata or nil if not found
|
|
48
|
+
def find_by_interface(interface_name)
|
|
49
|
+
build_index_if_needed
|
|
50
|
+
|
|
51
|
+
tool_names = @interface_to_tools[interface_name]
|
|
52
|
+
return nil unless tool_names
|
|
53
|
+
|
|
54
|
+
# Try each tool implementing this interface until we find one that loads
|
|
55
|
+
tool_names.each do |tool_name|
|
|
56
|
+
metadata = load_metadata_for_tool(tool_name)
|
|
57
|
+
return metadata if metadata
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Find all tools that implement an interface
|
|
64
|
+
#
|
|
65
|
+
# @param interface_name [Symbol] the interface to look up
|
|
66
|
+
# @return [Array<String>] list of tool names implementing this interface
|
|
67
|
+
def find_all_by_interface(interface_name)
|
|
68
|
+
build_index_if_needed
|
|
69
|
+
|
|
70
|
+
@interface_to_tools[interface_name] || []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Find tool by alias
|
|
74
|
+
#
|
|
75
|
+
# @param alias_name [String] the alias to look up
|
|
76
|
+
# @return [String, nil] the tool name or nil if not found
|
|
77
|
+
def find_by_alias(alias_name)
|
|
78
|
+
build_index_if_needed
|
|
79
|
+
|
|
80
|
+
@alias_to_tool[alias_name.to_sym]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get all tools in the index
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash{Symbol => Array<String>}] mapping of interface to tool names
|
|
86
|
+
def all_tools
|
|
87
|
+
build_index_if_needed
|
|
88
|
+
|
|
89
|
+
@interface_to_tools.dup
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if the index needs rebuilding due to registry changes
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean] true if rebuild is needed
|
|
95
|
+
def stale?
|
|
96
|
+
return true unless @built
|
|
97
|
+
|
|
98
|
+
current_cache_key = build_cache_key
|
|
99
|
+
@cache_key != current_cache_key
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Update the registry path
|
|
103
|
+
#
|
|
104
|
+
# @param new_path [String] the new registry path
|
|
105
|
+
def registry_path=(new_path)
|
|
106
|
+
return if @registry_path == new_path
|
|
107
|
+
|
|
108
|
+
@registry_path = new_path
|
|
109
|
+
@built = false # Rebuild index with new path
|
|
110
|
+
@cache_key = nil
|
|
111
|
+
@interface_to_tools = {}
|
|
112
|
+
@alias_to_tool = {}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Build index only if needed (lazy loading)
|
|
118
|
+
def build_index_if_needed
|
|
119
|
+
build_index if stale?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Build cache key for registry change detection
|
|
123
|
+
# Uses mtime of registry directory + file count for fast comparison
|
|
124
|
+
#
|
|
125
|
+
# @return [String] the cache key
|
|
126
|
+
def build_cache_key
|
|
127
|
+
current_path = registry_path
|
|
128
|
+
return 'empty' unless current_path
|
|
129
|
+
|
|
130
|
+
tools_dir = File.join(current_path, 'tools')
|
|
131
|
+
return 'no-tools-dir' unless Dir.exist?(tools_dir)
|
|
132
|
+
|
|
133
|
+
# Use directory mtime and file count for change detection
|
|
134
|
+
mtime = File.mtime(tools_dir).to_s
|
|
135
|
+
file_count = Dir.glob(File.join(tools_dir, '*', '*.yaml')).size
|
|
136
|
+
|
|
137
|
+
"#{mtime}-#{file_count}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get the current registry path
|
|
141
|
+
#
|
|
142
|
+
# @return [String, nil] the registry path
|
|
143
|
+
def registry_path
|
|
144
|
+
@registry_path ||= Ukiryu::Registry.default_registry_path
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Build the index by scanning tool directories
|
|
148
|
+
# This is done once and cached
|
|
149
|
+
def build_index
|
|
150
|
+
current_path = registry_path
|
|
151
|
+
return unless current_path
|
|
152
|
+
|
|
153
|
+
tools_dir = File.join(current_path, 'tools')
|
|
154
|
+
return unless Dir.exist?(tools_dir)
|
|
155
|
+
|
|
156
|
+
# Clear existing indexes
|
|
157
|
+
@interface_to_tools.clear
|
|
158
|
+
@alias_to_tool.clear
|
|
159
|
+
|
|
160
|
+
# Scan all tool directories for metadata
|
|
161
|
+
Dir.glob(File.join(tools_dir, '*', '*.yaml')).each do |file|
|
|
162
|
+
# Load only the top-level keys (metadata) without full parsing
|
|
163
|
+
hash = YAML.safe_load(File.read(file), permitted_classes: [Symbol])
|
|
164
|
+
next unless hash
|
|
165
|
+
|
|
166
|
+
tool_name = File.basename(File.dirname(file))
|
|
167
|
+
tool_sym = tool_name.to_sym
|
|
168
|
+
|
|
169
|
+
# Index by interface (multiple tools can implement one interface)
|
|
170
|
+
implements = hash['implements']&.to_sym
|
|
171
|
+
if implements
|
|
172
|
+
@interface_to_tools[implements] ||= []
|
|
173
|
+
@interface_to_tools[implements] << tool_sym unless @interface_to_tools[implements].include?(tool_sym)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Index by alias
|
|
177
|
+
aliases = hash['aliases']
|
|
178
|
+
if aliases && aliases.respond_to?(:each)
|
|
179
|
+
aliases.each do |alias_name|
|
|
180
|
+
@alias_to_tool[alias_name.to_sym] = tool_sym
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
# Skip files that can't be parsed
|
|
185
|
+
warn "Warning: Failed to parse #{file}: #{e.message}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
@cache_key = build_cache_key
|
|
189
|
+
@built = true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Load metadata for a specific tool
|
|
193
|
+
#
|
|
194
|
+
# @param tool_name [Symbol, String] the tool name
|
|
195
|
+
# @return [ToolMetadata, nil] the tool metadata
|
|
196
|
+
def load_metadata_for_tool(tool_name)
|
|
197
|
+
yaml_content = load_yaml_for_tool(tool_name)
|
|
198
|
+
return nil unless yaml_content
|
|
199
|
+
|
|
200
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
201
|
+
return nil unless hash
|
|
202
|
+
|
|
203
|
+
ToolMetadata.from_hash(hash, tool_name: tool_name.to_s, registry_path: registry_path)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Load YAML content for a specific tool
|
|
207
|
+
#
|
|
208
|
+
# @param tool_name [Symbol, String] the tool name
|
|
209
|
+
# @return [String, nil] the YAML content
|
|
210
|
+
def load_yaml_for_tool(tool_name)
|
|
211
|
+
current_path = registry_path
|
|
212
|
+
return nil unless current_path
|
|
213
|
+
|
|
214
|
+
# Search for version files
|
|
215
|
+
pattern = File.join(current_path, 'tools', tool_name.to_s, '*.yaml')
|
|
216
|
+
files = Dir.glob(pattern).sort
|
|
217
|
+
|
|
218
|
+
# Return the latest version
|
|
219
|
+
files.last ? File.read(files.last) : nil
|
|
220
|
+
rescue StandardError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../runtime'
|
|
4
|
+
require_relative '../command_builder'
|
|
5
|
+
require_relative 'class_generator'
|
|
6
|
+
require_relative 'executable_finder'
|
|
7
|
+
|
|
8
|
+
module Ukiryu
|
|
9
|
+
# Tools namespace for tool-specific classes
|
|
10
|
+
#
|
|
11
|
+
# This namespace contains dynamically generated classes for each tool,
|
|
12
|
+
# providing a fully OOP interface for working with command-line tools.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# Ukiryu::Tools::Imagemagick.new.tap do |tool|
|
|
16
|
+
# options = tool.options_for(:convert)
|
|
17
|
+
# options.set(inputs: ["image.png"], resize: "50%")
|
|
18
|
+
# options.run
|
|
19
|
+
# end
|
|
20
|
+
module Tools
|
|
21
|
+
# Abstract base class for all tool-specific classes
|
|
22
|
+
#
|
|
23
|
+
# This class provides the common interface that all tool classes inherit from.
|
|
24
|
+
# Tool-specific classes are dynamically generated by Tools::Generator.
|
|
25
|
+
#
|
|
26
|
+
# @abstract
|
|
27
|
+
class Base
|
|
28
|
+
include Ukiryu::CommandBuilder
|
|
29
|
+
class << self
|
|
30
|
+
# Get the tool name symbol
|
|
31
|
+
#
|
|
32
|
+
# @return [Symbol] the tool name
|
|
33
|
+
attr_reader :tool_name
|
|
34
|
+
|
|
35
|
+
# Get the tool definition model
|
|
36
|
+
#
|
|
37
|
+
# @return [Models::ToolDefinition] the tool definition
|
|
38
|
+
attr_reader :tool_definition
|
|
39
|
+
|
|
40
|
+
# Get the platform profile model
|
|
41
|
+
#
|
|
42
|
+
# @return [Models::PlatformProfile] the platform profile
|
|
43
|
+
attr_reader :platform_profile
|
|
44
|
+
|
|
45
|
+
# Get the options class for a specific command
|
|
46
|
+
#
|
|
47
|
+
# @param command_name [Symbol] the command name
|
|
48
|
+
# @return [Class] the options class for this command
|
|
49
|
+
def options_class_for(command_name)
|
|
50
|
+
command_name = command_name.to_sym
|
|
51
|
+
@options_classes ||= {}
|
|
52
|
+
return @options_classes[command_name] if @options_classes[command_name]
|
|
53
|
+
|
|
54
|
+
# Get command from platform profile model
|
|
55
|
+
command_def = @platform_profile.command(command_name.to_s)
|
|
56
|
+
raise ArgumentError, "Unknown command: #{command_name}" unless command_def
|
|
57
|
+
|
|
58
|
+
# Generate the options class using ClassGenerator
|
|
59
|
+
options_class = ClassGenerator.generate_options_class(self, command_name, command_def)
|
|
60
|
+
@options_classes[command_name] = options_class
|
|
61
|
+
options_class
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Alias for options_class_for (more idiomatic)
|
|
65
|
+
#
|
|
66
|
+
# @param command_name [Symbol] the command name
|
|
67
|
+
# @return [Class] the options class for this command
|
|
68
|
+
def options_for(command_name)
|
|
69
|
+
options_class_for(command_name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get the action class for a specific command
|
|
73
|
+
#
|
|
74
|
+
# @param command_name [Symbol] the command name
|
|
75
|
+
# @return [Class] the action class for this command
|
|
76
|
+
def action_class_for(command_name)
|
|
77
|
+
command_name = command_name.to_sym
|
|
78
|
+
@action_classes ||= {}
|
|
79
|
+
return @action_classes[command_name] if @action_classes[command_name]
|
|
80
|
+
|
|
81
|
+
# Get command from platform profile model
|
|
82
|
+
command_def = @platform_profile.command(command_name.to_s)
|
|
83
|
+
raise ArgumentError, "Unknown command: #{command_name}" unless command_def
|
|
84
|
+
|
|
85
|
+
# Generate the action class using ClassGenerator
|
|
86
|
+
action_class = ClassGenerator.generate_action_class(self, command_name, command_def)
|
|
87
|
+
@action_classes[command_name] = action_class
|
|
88
|
+
action_class
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Alias for action_class_for
|
|
92
|
+
#
|
|
93
|
+
# @param command_name [Symbol] the command name
|
|
94
|
+
# @return [Class] the action class for this command
|
|
95
|
+
def action_for(command_name)
|
|
96
|
+
action_class_for(command_name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get version information for this tool
|
|
100
|
+
#
|
|
101
|
+
# @return [Version, nil] version object if tool is available
|
|
102
|
+
def version
|
|
103
|
+
return nil unless available?
|
|
104
|
+
|
|
105
|
+
new.version
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if the tool is available on this system
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean] true if the tool can be executed
|
|
111
|
+
def available?
|
|
112
|
+
@available ||= begin
|
|
113
|
+
executable = ExecutableFinder.find_executable(@tool_definition.name.to_s, @tool_definition)
|
|
114
|
+
!executable.nil?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get the response class for a command
|
|
119
|
+
#
|
|
120
|
+
# @param command_name [Symbol] the command name
|
|
121
|
+
# @return [Class] the response class
|
|
122
|
+
def response_class_for(command_name)
|
|
123
|
+
command_name = command_name.to_sym
|
|
124
|
+
@response_classes ||= {}
|
|
125
|
+
return @response_classes[command_name] if @response_classes[command_name]
|
|
126
|
+
|
|
127
|
+
# Get command from platform profile model
|
|
128
|
+
command_def = @platform_profile.command(command_name.to_s)
|
|
129
|
+
raise ArgumentError, "Unknown command: #{command_name}" unless command_def
|
|
130
|
+
|
|
131
|
+
# Generate the response class using ClassGenerator
|
|
132
|
+
response_class = ClassGenerator.generate_response_class(self, command_name, command_def)
|
|
133
|
+
@response_classes[command_name] = response_class
|
|
134
|
+
response_class
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Initialize the tool instance
|
|
139
|
+
#
|
|
140
|
+
# Sets up shell and platform instance variables for CommandBuilder
|
|
141
|
+
def initialize
|
|
142
|
+
runtime = Ukiryu::Runtime.instance
|
|
143
|
+
@platform = runtime.platform
|
|
144
|
+
@shell = runtime.shell
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get the tool name for this instance
|
|
148
|
+
#
|
|
149
|
+
# @return [Symbol] the tool name
|
|
150
|
+
def tool_name
|
|
151
|
+
self.class.tool_name
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get the tool name from the tool definition
|
|
155
|
+
#
|
|
156
|
+
# @return [String] the tool name
|
|
157
|
+
def name
|
|
158
|
+
self.class.tool_definition.name
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get the tool definition (for backward compatibility)
|
|
162
|
+
#
|
|
163
|
+
# @return [Models::ToolDefinition] the tool definition
|
|
164
|
+
def tool_definition
|
|
165
|
+
self.class.tool_definition
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get the platform profile (for backward compatibility)
|
|
169
|
+
#
|
|
170
|
+
# @return [Models::PlatformProfile] the platform profile
|
|
171
|
+
def platform_profile
|
|
172
|
+
self.class.platform_profile
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get a new options instance for a command
|
|
176
|
+
#
|
|
177
|
+
# @param command_name [Symbol] the command name
|
|
178
|
+
# @return [Options::Base] a new options instance
|
|
179
|
+
def options_for(command_name)
|
|
180
|
+
self.class.options_for(command_name).new
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get the options class for a command
|
|
184
|
+
#
|
|
185
|
+
# @param command_name [Symbol] the command name
|
|
186
|
+
# @return [Class] the options class
|
|
187
|
+
def options_class_for(command_name)
|
|
188
|
+
self.class.options_for(command_name)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get a new action instance for a command
|
|
192
|
+
#
|
|
193
|
+
# @param command_name [Symbol] the command name
|
|
194
|
+
# @return [Action::Base] a new action instance
|
|
195
|
+
def action_for(command_name)
|
|
196
|
+
self.class.action_for(command_name).new(self)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Get the action class for a command
|
|
200
|
+
#
|
|
201
|
+
# @param command_name [Symbol] the command name
|
|
202
|
+
# @return [Class] the action class
|
|
203
|
+
def action_class_for(command_name)
|
|
204
|
+
self.class.action_for(command_name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get version information
|
|
208
|
+
#
|
|
209
|
+
# @return [String, nil] version string if tool is available
|
|
210
|
+
def version
|
|
211
|
+
return @version if defined?(@version)
|
|
212
|
+
|
|
213
|
+
@version = detect_version
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Check if the tool is available
|
|
217
|
+
#
|
|
218
|
+
# @return [Boolean] true if the tool can be executed
|
|
219
|
+
def available?
|
|
220
|
+
self.class.available?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Execute a command with options object
|
|
224
|
+
#
|
|
225
|
+
# @param command_name [Symbol] the command to execute
|
|
226
|
+
# @param options [Object] the options object
|
|
227
|
+
# @return [Response::Base] the execution response
|
|
228
|
+
def execute(command_name, options)
|
|
229
|
+
require_relative '../executor'
|
|
230
|
+
require_relative '../options_builder'
|
|
231
|
+
|
|
232
|
+
command_name = command_name.to_sym
|
|
233
|
+
# Get command from platform profile model
|
|
234
|
+
command_def = self.class.platform_profile.command(command_name.to_s)
|
|
235
|
+
|
|
236
|
+
raise ArgumentError, "Unknown command: #{command_name}" unless command_def
|
|
237
|
+
|
|
238
|
+
# Convert options to hash
|
|
239
|
+
params = if options.is_a?(Hash)
|
|
240
|
+
options.transform_keys(&:to_sym)
|
|
241
|
+
else
|
|
242
|
+
Ukiryu::OptionsBuilder.to_hash(options)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Build arguments
|
|
246
|
+
args = build_args(command_def, params)
|
|
247
|
+
|
|
248
|
+
# Find executable using ExecutableFinder
|
|
249
|
+
executable = ExecutableFinder.find_executable(self.class.tool_definition.name.to_s, self.class.tool_definition)
|
|
250
|
+
|
|
251
|
+
# Get shell
|
|
252
|
+
shell_sym = Ukiryu::Runtime.instance.shell
|
|
253
|
+
|
|
254
|
+
# Build environment variables (including execution mode env vars)
|
|
255
|
+
env = build_execution_env(command_def, params)
|
|
256
|
+
|
|
257
|
+
# Execute
|
|
258
|
+
result = Ukiryu::Executor.execute(
|
|
259
|
+
executable,
|
|
260
|
+
args,
|
|
261
|
+
env: env,
|
|
262
|
+
timeout: self.class.tool_definition.timeout || 90,
|
|
263
|
+
shell: shell_sym
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Build response
|
|
267
|
+
build_response(command_name, result)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
# Build command arguments from parameters
|
|
273
|
+
#
|
|
274
|
+
# This method extends the CommandBuilder implementation to handle
|
|
275
|
+
# extra_args for manual option injection.
|
|
276
|
+
#
|
|
277
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
278
|
+
# @param params [Hash] the parameters
|
|
279
|
+
# @return [Array<String>] the command arguments
|
|
280
|
+
def build_args(command_def, params)
|
|
281
|
+
# Call CommandBuilder's build_args first
|
|
282
|
+
args = super
|
|
283
|
+
|
|
284
|
+
# Handle extra_args for manual option injection
|
|
285
|
+
# This allows users to pass options not defined in the YAML profile
|
|
286
|
+
if params[:extra_args]
|
|
287
|
+
extra = params[:extra_args]
|
|
288
|
+
shell_sym = Ukiryu::Runtime.instance.shell
|
|
289
|
+
|
|
290
|
+
if extra.is_a?(Array)
|
|
291
|
+
shell_class = Ukiryu::Shell.class_for(shell_sym)
|
|
292
|
+
shell_instance = shell_class.new
|
|
293
|
+
extra.each { |arg| args << shell_instance.escape(arg.to_s) }
|
|
294
|
+
else
|
|
295
|
+
args << extra.to_s
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
args
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Build execution environment including execution mode
|
|
303
|
+
#
|
|
304
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
305
|
+
# @param params [Hash] the parameters
|
|
306
|
+
# @return [Hash] environment variables
|
|
307
|
+
def build_execution_env(command_def, params)
|
|
308
|
+
env = build_env_vars(command_def, params)
|
|
309
|
+
|
|
310
|
+
# Handle execution modes
|
|
311
|
+
execution_mode = command_def.execution_mode
|
|
312
|
+
|
|
313
|
+
case execution_mode
|
|
314
|
+
when 'headless', :headless
|
|
315
|
+
# Set headless environment variables
|
|
316
|
+
platform = Ukiryu::Runtime.instance.platform
|
|
317
|
+
case platform
|
|
318
|
+
when :macos, :linux
|
|
319
|
+
env['DISPLAY'] ||= '' # Disable X11 display
|
|
320
|
+
when :windows
|
|
321
|
+
# Windows doesn't need special handling for headless
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
env
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Check if command needs batch processing flag for headless mode
|
|
329
|
+
#
|
|
330
|
+
# @param command_def [Models::CommandDefinition] the command definition
|
|
331
|
+
# @return [Boolean] true if batch processing flag should be added
|
|
332
|
+
def needs_batch_process_flag?(command_def)
|
|
333
|
+
execution_mode = command_def.execution_mode
|
|
334
|
+
['headless', :headless].include?(execution_mode)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Build a response object
|
|
338
|
+
#
|
|
339
|
+
# @param command_name [Symbol] the command name
|
|
340
|
+
# @param result [Executor::Result] the execution result
|
|
341
|
+
# @return [Response::Base] the response object
|
|
342
|
+
def build_response(command_name, result)
|
|
343
|
+
require_relative '../response/base'
|
|
344
|
+
|
|
345
|
+
# Get the response class for this command
|
|
346
|
+
response_class = self.class.response_class_for(command_name)
|
|
347
|
+
|
|
348
|
+
# Create response instance
|
|
349
|
+
response_class.new(result)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Detect the tool version
|
|
353
|
+
#
|
|
354
|
+
# @return [String, nil] the detected version string
|
|
355
|
+
def detect_version
|
|
356
|
+
vd = self.class.tool_definition.version_detection
|
|
357
|
+
return nil unless vd
|
|
358
|
+
|
|
359
|
+
cmd = vd.command || '--version'
|
|
360
|
+
|
|
361
|
+
executable = ExecutableFinder.find_executable(self.class.tool_definition.name.to_s, self.class.tool_definition)
|
|
362
|
+
return nil unless executable
|
|
363
|
+
|
|
364
|
+
require_relative '../executor'
|
|
365
|
+
shell_sym = Ukiryu::Runtime.instance.shell
|
|
366
|
+
|
|
367
|
+
result = Ukiryu::Executor.execute(executable, [cmd], shell: shell_sym)
|
|
368
|
+
return nil unless result.success?
|
|
369
|
+
|
|
370
|
+
# Convert pattern string to regex if needed
|
|
371
|
+
pattern = if vd.pattern.is_a?(String)
|
|
372
|
+
Regexp.new(vd.pattern)
|
|
373
|
+
else
|
|
374
|
+
vd.pattern || /(\d+\.\d+)/
|
|
375
|
+
end
|
|
376
|
+
match = result.stdout.match(pattern) || result.stderr.match(pattern)
|
|
377
|
+
match[1] if match
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|