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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. 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