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
data/lib/ukiryu/executor.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require_relative 'execution'
|
|
6
|
+
require_relative 'shell'
|
|
5
7
|
|
|
6
8
|
module Ukiryu
|
|
7
9
|
# Command execution with platform-specific methods
|
|
@@ -22,7 +24,10 @@ module Ukiryu
|
|
|
22
24
|
# @option options [Hash] :env environment variables
|
|
23
25
|
# @option options [String] :cwd working directory
|
|
24
26
|
# @option options [Symbol] :shell shell to use (default: auto-detect)
|
|
25
|
-
# @
|
|
27
|
+
# @option options [String, IO] :stdin stdin input data (string or IO object)
|
|
28
|
+
# @option options [String] :tool_name tool name for exit code lookups
|
|
29
|
+
# @option options [String] :command_name command name for exit code lookups
|
|
30
|
+
# @return [Execution::Result] execution result with composed OOP classes
|
|
26
31
|
# @raise [TimeoutError] if command times out
|
|
27
32
|
# @raise [ExecutionError] if command fails
|
|
28
33
|
def execute(executable, args = [], options = {})
|
|
@@ -38,31 +43,38 @@ module Ukiryu
|
|
|
38
43
|
# Execute with timeout
|
|
39
44
|
timeout = options[:timeout] || 90
|
|
40
45
|
cwd = options[:cwd]
|
|
46
|
+
stdin = options[:stdin]
|
|
41
47
|
|
|
42
48
|
started_at = Time.now
|
|
43
49
|
begin
|
|
44
|
-
result =
|
|
50
|
+
result = if stdin
|
|
51
|
+
execute_with_stdin(command, env, timeout, cwd, stdin)
|
|
52
|
+
else
|
|
53
|
+
execute_with_timeout(command, env, timeout, cwd)
|
|
54
|
+
end
|
|
45
55
|
rescue Timeout::Error
|
|
46
|
-
|
|
56
|
+
Time.now
|
|
47
57
|
raise TimeoutError, "Command timed out after #{timeout} seconds: #{executable}"
|
|
48
58
|
end
|
|
49
59
|
finished_at = Time.now
|
|
50
60
|
|
|
51
|
-
# Create OOP result components
|
|
52
|
-
command_info = CommandInfo.new(
|
|
61
|
+
# Create OOP result components using Execution namespace
|
|
62
|
+
command_info = Execution::CommandInfo.new(
|
|
53
63
|
executable: executable,
|
|
54
64
|
arguments: args,
|
|
55
65
|
full_command: command,
|
|
56
|
-
shell: shell_name
|
|
66
|
+
shell: shell_name,
|
|
67
|
+
tool_name: options[:tool_name],
|
|
68
|
+
command_name: options[:command_name]
|
|
57
69
|
)
|
|
58
70
|
|
|
59
|
-
output = Output.new(
|
|
71
|
+
output = Execution::Output.new(
|
|
60
72
|
stdout: result[:stdout],
|
|
61
73
|
stderr: result[:stderr],
|
|
62
74
|
exit_status: result[:status]
|
|
63
75
|
)
|
|
64
76
|
|
|
65
|
-
metadata = ExecutionMetadata.new(
|
|
77
|
+
metadata = Execution::ExecutionMetadata.new(
|
|
66
78
|
started_at: started_at,
|
|
67
79
|
finished_at: finished_at,
|
|
68
80
|
timeout: timeout
|
|
@@ -70,10 +82,11 @@ module Ukiryu
|
|
|
70
82
|
|
|
71
83
|
# Check exit status
|
|
72
84
|
if result[:status] != 0 && !options[:allow_failure]
|
|
73
|
-
raise ExecutionError,
|
|
85
|
+
raise ExecutionError,
|
|
86
|
+
format_error(executable, command, result)
|
|
74
87
|
end
|
|
75
88
|
|
|
76
|
-
Result.new(
|
|
89
|
+
Execution::Result.new(
|
|
77
90
|
command_info: command_info,
|
|
78
91
|
output: output,
|
|
79
92
|
metadata: metadata
|
|
@@ -88,7 +101,7 @@ module Ukiryu
|
|
|
88
101
|
# @return [String, nil] the full path to the executable, or nil if not found
|
|
89
102
|
def find_executable(command, options = {})
|
|
90
103
|
# Try with PATHEXT extensions (Windows executables)
|
|
91
|
-
exts = ENV[
|
|
104
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
|
92
105
|
|
|
93
106
|
search_paths = Platform.executable_search_paths
|
|
94
107
|
search_paths.concat(options[:additional_paths]) if options[:additional_paths]
|
|
@@ -168,6 +181,56 @@ module Ukiryu
|
|
|
168
181
|
end
|
|
169
182
|
end
|
|
170
183
|
|
|
184
|
+
# Execute command with stdin input
|
|
185
|
+
#
|
|
186
|
+
# @param command [String] the command to execute
|
|
187
|
+
# @param env [Hash] environment variables
|
|
188
|
+
# @param timeout [Integer] timeout in seconds
|
|
189
|
+
# @param cwd [String, nil] working directory (nil for current directory)
|
|
190
|
+
# @param stdin_data [String, IO] stdin input data
|
|
191
|
+
# @return [Hash] execution result
|
|
192
|
+
def execute_with_stdin(command, env, timeout, cwd, stdin_data)
|
|
193
|
+
Timeout.timeout(timeout) do
|
|
194
|
+
execution = lambda do
|
|
195
|
+
Open3.popen3(env, command) do |stdin, stdout, stderr, wait_thr|
|
|
196
|
+
# Write stdin data
|
|
197
|
+
begin
|
|
198
|
+
if stdin_data.is_a?(IO)
|
|
199
|
+
IO.copy_stream(stdin_data, stdin)
|
|
200
|
+
elsif stdin_data.is_a?(String)
|
|
201
|
+
stdin.write(stdin_data)
|
|
202
|
+
end
|
|
203
|
+
rescue Errno::EPIPE
|
|
204
|
+
# Process closed stdin early (e.g., 'head' command)
|
|
205
|
+
ensure
|
|
206
|
+
stdin.close
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Read output
|
|
210
|
+
out = stdout.read
|
|
211
|
+
err = stderr.read
|
|
212
|
+
|
|
213
|
+
# Wait for process to complete
|
|
214
|
+
status = wait_thr.value
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
status: extract_status(status),
|
|
218
|
+
stdout: out,
|
|
219
|
+
stderr: err
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if cwd
|
|
225
|
+
Dir.chdir(cwd) do
|
|
226
|
+
execution.call
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
execution.call
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
171
234
|
# Extract exit status from Process::Status
|
|
172
235
|
#
|
|
173
236
|
# @param status [Process::Status] the process status
|
|
@@ -232,485 +295,5 @@ module Ukiryu
|
|
|
232
295
|
ERROR
|
|
233
296
|
end
|
|
234
297
|
end
|
|
235
|
-
|
|
236
|
-
# Execution command information
|
|
237
|
-
#
|
|
238
|
-
# Encapsulates details about the executed command
|
|
239
|
-
class CommandInfo
|
|
240
|
-
attr_reader :executable, :arguments, :full_command, :shell
|
|
241
|
-
|
|
242
|
-
def initialize(executable:, arguments:, full_command:, shell: nil)
|
|
243
|
-
@executable = executable
|
|
244
|
-
@arguments = arguments
|
|
245
|
-
@full_command = full_command
|
|
246
|
-
@shell = shell
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Get the executable name only
|
|
250
|
-
#
|
|
251
|
-
# @return [String] executable name
|
|
252
|
-
def executable_name
|
|
253
|
-
File.basename(@executable)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Get argument count
|
|
257
|
-
#
|
|
258
|
-
# @return [Integer] number of arguments
|
|
259
|
-
def argument_count
|
|
260
|
-
@arguments.count
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# String representation
|
|
264
|
-
#
|
|
265
|
-
# @return [String] command string
|
|
266
|
-
def to_s
|
|
267
|
-
@full_command
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Inspect
|
|
271
|
-
#
|
|
272
|
-
# @return [String] inspection string
|
|
273
|
-
def inspect
|
|
274
|
-
"#<Ukiryu::Executor::CommandInfo exe=#{executable_name.inspect} args=#{argument_count}>"
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Convert to hash
|
|
278
|
-
#
|
|
279
|
-
# @return [Hash] command info as hash
|
|
280
|
-
def to_h
|
|
281
|
-
{
|
|
282
|
-
executable: @executable,
|
|
283
|
-
executable_name: executable_name,
|
|
284
|
-
arguments: @arguments,
|
|
285
|
-
full_command: @full_command,
|
|
286
|
-
shell: @shell
|
|
287
|
-
}
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# Captured output from command execution
|
|
292
|
-
#
|
|
293
|
-
# Provides typed access to stdout and stderr with parsing utilities
|
|
294
|
-
class Output
|
|
295
|
-
attr_reader :raw_stdout, :raw_stderr, :exit_status
|
|
296
|
-
|
|
297
|
-
def initialize(stdout:, stderr:, exit_status:)
|
|
298
|
-
@raw_stdout = stdout
|
|
299
|
-
@raw_stderr = stderr
|
|
300
|
-
@exit_status = exit_status
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# Get stdout as a string (stripped)
|
|
304
|
-
#
|
|
305
|
-
# @return [String] stripped stdout
|
|
306
|
-
def stdout
|
|
307
|
-
@raw_stdout.strip
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Get stderr as a string (stripped)
|
|
311
|
-
#
|
|
312
|
-
# @return [String] stripped stderr
|
|
313
|
-
def stderr
|
|
314
|
-
@raw_stderr.strip
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Get stdout lines as an array
|
|
318
|
-
#
|
|
319
|
-
# @return [Array<String>] stdout split by lines
|
|
320
|
-
def stdout_lines
|
|
321
|
-
@raw_stdout.split("\n")
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# Get stderr lines as an array
|
|
325
|
-
#
|
|
326
|
-
# @return [Array<String>] stderr split by lines
|
|
327
|
-
def stderr_lines
|
|
328
|
-
@raw_stderr.split("\n")
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Check if stdout contains a pattern
|
|
332
|
-
#
|
|
333
|
-
# @param pattern [String, Regexp] pattern to search for
|
|
334
|
-
# @return [Boolean] true if pattern is found
|
|
335
|
-
def stdout_contains?(pattern)
|
|
336
|
-
if pattern.is_a?(Regexp)
|
|
337
|
-
@raw_stdout.match?(pattern)
|
|
338
|
-
else
|
|
339
|
-
@raw_stdout.include?(pattern.to_s)
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# Check if stderr contains a pattern
|
|
344
|
-
#
|
|
345
|
-
# @param pattern [String, Regexp] pattern to search for
|
|
346
|
-
# @return [Boolean] true if pattern is found
|
|
347
|
-
def stderr_contains?(pattern)
|
|
348
|
-
if pattern.is_a?(Regexp)
|
|
349
|
-
@raw_stderr.match?(pattern)
|
|
350
|
-
else
|
|
351
|
-
@raw_stderr.include?(pattern.to_s)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
# Check if stdout is empty
|
|
356
|
-
#
|
|
357
|
-
# @return [Boolean] true if stdout is empty
|
|
358
|
-
def stdout_empty?
|
|
359
|
-
@raw_stdout.strip.empty?
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Check if stderr is empty
|
|
363
|
-
#
|
|
364
|
-
# @return [Boolean] true if stderr is empty
|
|
365
|
-
def stderr_empty?
|
|
366
|
-
@raw_stderr.strip.empty?
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Get stdout length
|
|
370
|
-
#
|
|
371
|
-
# @return [Integer] byte length of stdout
|
|
372
|
-
def stdout_length
|
|
373
|
-
@raw_stdout.length
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Get stderr length
|
|
377
|
-
#
|
|
378
|
-
# @return [Integer] byte length of stderr
|
|
379
|
-
def stderr_length
|
|
380
|
-
@raw_stderr.length
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
# Check if command succeeded
|
|
384
|
-
#
|
|
385
|
-
# @return [Boolean] true if exit status is 0
|
|
386
|
-
def success?
|
|
387
|
-
@exit_status == 0
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Check if command failed
|
|
391
|
-
#
|
|
392
|
-
# @return [Boolean] true if exit status is non-zero
|
|
393
|
-
def failure?
|
|
394
|
-
@exit_status != 0
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# Convert to hash
|
|
398
|
-
#
|
|
399
|
-
# @return [Hash] output as hash
|
|
400
|
-
def to_h
|
|
401
|
-
{
|
|
402
|
-
stdout: @raw_stdout,
|
|
403
|
-
stderr: @raw_stderr,
|
|
404
|
-
exit_status: @exit_status,
|
|
405
|
-
success: success?,
|
|
406
|
-
stdout_lines: stdout_lines,
|
|
407
|
-
stderr_lines: stderr_lines
|
|
408
|
-
}
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
# String representation
|
|
412
|
-
#
|
|
413
|
-
# @return [String] summary string
|
|
414
|
-
def to_s
|
|
415
|
-
if success?
|
|
416
|
-
"Success (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
|
|
417
|
-
else
|
|
418
|
-
"Failed (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# Inspect
|
|
423
|
-
#
|
|
424
|
-
# @return [String] inspection string
|
|
425
|
-
def inspect
|
|
426
|
-
"#<Ukiryu::Executor::Output exit=#{@exit_status} success=#{success?}>"
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
# Execution metadata
|
|
431
|
-
#
|
|
432
|
-
# Provides timing and execution environment information
|
|
433
|
-
class ExecutionMetadata
|
|
434
|
-
attr_reader :started_at, :finished_at, :duration, :timeout
|
|
435
|
-
|
|
436
|
-
def initialize(started_at:, finished_at:, timeout: nil)
|
|
437
|
-
@started_at = started_at
|
|
438
|
-
@finished_at = finished_at
|
|
439
|
-
@timeout = timeout
|
|
440
|
-
@duration = calculate_duration
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# Calculate duration from start and finish times
|
|
444
|
-
#
|
|
445
|
-
# @return [Float, nil] duration in seconds
|
|
446
|
-
def calculate_duration
|
|
447
|
-
return nil unless @started_at && @finished_at
|
|
448
|
-
|
|
449
|
-
@finished_at - @started_at
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# Get execution duration in seconds
|
|
453
|
-
#
|
|
454
|
-
# @return [Float, nil] duration in seconds
|
|
455
|
-
def duration_seconds
|
|
456
|
-
@duration
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
# Get execution duration in milliseconds
|
|
460
|
-
#
|
|
461
|
-
# @return [Float, nil] duration in milliseconds
|
|
462
|
-
def duration_milliseconds
|
|
463
|
-
@duration ? @duration * 1000 : nil
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Check if execution timed out
|
|
467
|
-
#
|
|
468
|
-
# @return [Boolean] true if timeout was set and exceeded
|
|
469
|
-
def timed_out?
|
|
470
|
-
return false unless @timeout && @duration
|
|
471
|
-
|
|
472
|
-
@duration > @timeout
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# Format duration for display
|
|
476
|
-
#
|
|
477
|
-
# @return [String] formatted duration
|
|
478
|
-
def formatted_duration
|
|
479
|
-
return "N/A" unless @duration
|
|
480
|
-
|
|
481
|
-
if @duration < 1
|
|
482
|
-
"#{(@duration * 1000).round(2)}ms"
|
|
483
|
-
elsif @duration < 60
|
|
484
|
-
"#{@duration.round(3)}s"
|
|
485
|
-
else
|
|
486
|
-
minutes = @duration / 60
|
|
487
|
-
seconds = @duration % 60
|
|
488
|
-
"#{minutes.to_i}m#{seconds.round(1)}s"
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Convert to hash
|
|
493
|
-
#
|
|
494
|
-
# @return [Hash] metadata as hash
|
|
495
|
-
def to_h
|
|
496
|
-
{
|
|
497
|
-
started_at: @started_at,
|
|
498
|
-
finished_at: @finished_at,
|
|
499
|
-
duration: @duration,
|
|
500
|
-
duration_seconds: @duration,
|
|
501
|
-
duration_milliseconds: duration_milliseconds,
|
|
502
|
-
timeout: @timeout,
|
|
503
|
-
timed_out: timed_out?
|
|
504
|
-
}
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
# String representation
|
|
508
|
-
#
|
|
509
|
-
# @return [String] formatted string
|
|
510
|
-
def to_s
|
|
511
|
-
"duration: #{formatted_duration}"
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# Inspect
|
|
515
|
-
#
|
|
516
|
-
# @return [String] inspection string
|
|
517
|
-
def inspect
|
|
518
|
-
"#<Ukiryu::Executor::ExecutionMetadata duration=#{formatted_duration}>"
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
# Result class for command execution
|
|
523
|
-
#
|
|
524
|
-
# Provides a rich, object-oriented interface to command execution results.
|
|
525
|
-
# Composes CommandInfo, Output, and ExecutionMetadata for a fully OOP design.
|
|
526
|
-
class Result
|
|
527
|
-
attr_reader :command_info, :output, :metadata
|
|
528
|
-
|
|
529
|
-
# Initialize a new result
|
|
530
|
-
#
|
|
531
|
-
# @param command_info [CommandInfo] the command execution info
|
|
532
|
-
# @param output [Output] the captured output
|
|
533
|
-
# @param metadata [ExecutionMetadata] execution metadata
|
|
534
|
-
def initialize(command_info:, output:, metadata:)
|
|
535
|
-
@command_info = command_info
|
|
536
|
-
@output = output
|
|
537
|
-
@metadata = metadata
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
# Get the full command string
|
|
541
|
-
#
|
|
542
|
-
# @return [String] executed command
|
|
543
|
-
def command
|
|
544
|
-
@command_info.full_command
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# Get the executable
|
|
548
|
-
#
|
|
549
|
-
# @return [String] executable path
|
|
550
|
-
def executable
|
|
551
|
-
@command_info.executable
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
# Get the executable name only
|
|
555
|
-
#
|
|
556
|
-
# @return [String] executable name
|
|
557
|
-
def executable_name
|
|
558
|
-
@command_info.executable_name
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# Get raw stdout
|
|
562
|
-
#
|
|
563
|
-
# @return [String] raw stdout
|
|
564
|
-
def stdout
|
|
565
|
-
@output.raw_stdout
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
# Get raw stderr
|
|
569
|
-
#
|
|
570
|
-
# @return [String] raw stderr
|
|
571
|
-
def stderr
|
|
572
|
-
@output.raw_stderr
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
# Get exit status code
|
|
576
|
-
#
|
|
577
|
-
# @return [Integer] exit status
|
|
578
|
-
def status
|
|
579
|
-
@output.exit_status
|
|
580
|
-
end
|
|
581
|
-
alias exit_code status
|
|
582
|
-
|
|
583
|
-
# Get the exit code (alias for status)
|
|
584
|
-
#
|
|
585
|
-
# @return [Integer] exit status
|
|
586
|
-
def exit_status
|
|
587
|
-
@output.exit_status
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
# Get start time
|
|
591
|
-
#
|
|
592
|
-
# @return [Time] when command started
|
|
593
|
-
def started_at
|
|
594
|
-
@metadata.started_at
|
|
595
|
-
end
|
|
596
|
-
|
|
597
|
-
# Get finish time
|
|
598
|
-
#
|
|
599
|
-
# @return [Time] when command finished
|
|
600
|
-
def finished_at
|
|
601
|
-
@metadata.finished_at
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
# Get execution duration
|
|
605
|
-
#
|
|
606
|
-
# @return [Float, nil] duration in seconds
|
|
607
|
-
def duration
|
|
608
|
-
@metadata.duration
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
# Get execution duration (alias)
|
|
612
|
-
#
|
|
613
|
-
# @return [Float, nil] duration in seconds
|
|
614
|
-
def execution_time
|
|
615
|
-
@metadata.duration_seconds
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
# Check if the command succeeded
|
|
619
|
-
#
|
|
620
|
-
# @return [Boolean]
|
|
621
|
-
def success?
|
|
622
|
-
@output.success?
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
# Check if the command failed
|
|
626
|
-
#
|
|
627
|
-
# @return [Boolean]
|
|
628
|
-
def failure?
|
|
629
|
-
@output.failure?
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
# Get stdout as a stripped string
|
|
633
|
-
#
|
|
634
|
-
# @return [String] stripped stdout
|
|
635
|
-
def output
|
|
636
|
-
@output.stdout
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
# Get stderr as a stripped string
|
|
640
|
-
#
|
|
641
|
-
# @return [String] stripped stderr
|
|
642
|
-
def error_output
|
|
643
|
-
@output.stderr
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
# Get stdout lines
|
|
647
|
-
#
|
|
648
|
-
# @return [Array<String>] stdout split by lines
|
|
649
|
-
def stdout_lines
|
|
650
|
-
@output.stdout_lines
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
# Get stderr lines
|
|
654
|
-
#
|
|
655
|
-
# @return [Array<String>] stderr split by lines
|
|
656
|
-
def stderr_lines
|
|
657
|
-
@output.stderr_lines
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
# Check if stdout contains a pattern
|
|
661
|
-
#
|
|
662
|
-
# @param pattern [String, Regexp] pattern to search for
|
|
663
|
-
# @return [Boolean] true if pattern is found
|
|
664
|
-
def stdout_contains?(pattern)
|
|
665
|
-
@output.stdout_contains?(pattern)
|
|
666
|
-
end
|
|
667
|
-
|
|
668
|
-
# Check if stderr contains a pattern
|
|
669
|
-
#
|
|
670
|
-
# @param pattern [String, Regexp] pattern to search for
|
|
671
|
-
# @return [Boolean] true if pattern is found
|
|
672
|
-
def stderr_contains?(pattern)
|
|
673
|
-
@output.stderr_contains?(pattern)
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
# Get a hash representation of the result
|
|
677
|
-
#
|
|
678
|
-
# @return [Hash] result data as a hash
|
|
679
|
-
def to_h
|
|
680
|
-
{
|
|
681
|
-
command: @command_info.to_h,
|
|
682
|
-
output: @output.to_h,
|
|
683
|
-
metadata: @metadata.to_h,
|
|
684
|
-
success: success?,
|
|
685
|
-
status: status
|
|
686
|
-
}
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
# Get a JSON representation of the result
|
|
690
|
-
#
|
|
691
|
-
# @return [String] result data as JSON
|
|
692
|
-
def to_json(*args)
|
|
693
|
-
require 'json'
|
|
694
|
-
to_h.to_json(*args)
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
# String representation of the result
|
|
698
|
-
#
|
|
699
|
-
# @return [String] summary of the result
|
|
700
|
-
def to_s
|
|
701
|
-
if success?
|
|
702
|
-
"Success (#{@command_info.executable_name}, status: #{status}, duration: #{@metadata.formatted_duration})"
|
|
703
|
-
else
|
|
704
|
-
"Failed (#{@command_info.executable_name}, status: #{status}, duration: #{@metadata.formatted_duration})"
|
|
705
|
-
end
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
# Inspect the result (for debugging)
|
|
709
|
-
#
|
|
710
|
-
# @return [String] detailed inspection string
|
|
711
|
-
def inspect
|
|
712
|
-
"#<Ukiryu::Executor::Result exe=#{@command_info.executable_name.inspect} status=#{status} duration=#{@metadata.formatted_duration}>"
|
|
713
|
-
end
|
|
714
|
-
end
|
|
715
298
|
end
|
|
716
299
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Extractors
|
|
5
|
+
# Base class for definition extraction strategies
|
|
6
|
+
#
|
|
7
|
+
# Each extraction strategy implements a different approach to
|
|
8
|
+
# extracting tool definitions from CLI tools.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclasses must implement the `extract` method
|
|
11
|
+
class BaseExtractor
|
|
12
|
+
# Initialize the extractor
|
|
13
|
+
#
|
|
14
|
+
# @param tool_name [String, Symbol] the tool name
|
|
15
|
+
# @param options [Hash] extraction options
|
|
16
|
+
def initialize(tool_name, options = {})
|
|
17
|
+
@tool_name = tool_name
|
|
18
|
+
@options = options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Extract definition from the tool
|
|
22
|
+
#
|
|
23
|
+
# Subclasses must implement this method
|
|
24
|
+
#
|
|
25
|
+
# @return [String, nil] the YAML definition or nil if extraction failed
|
|
26
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
|
27
|
+
def extract
|
|
28
|
+
raise NotImplementedError, "#{self.class} must implement #extract"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if this extractor can extract from the tool
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] true if extraction is possible
|
|
34
|
+
def available?
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #available?"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Execute a command and capture output
|
|
41
|
+
#
|
|
42
|
+
# @param command [Array<String>] the command to execute
|
|
43
|
+
# @return [Hash] result with :stdout, :stderr, :exit_status keys
|
|
44
|
+
def execute_command(command)
|
|
45
|
+
require 'open3'
|
|
46
|
+
|
|
47
|
+
stdout, stderr, status = Open3.capture3(*command)
|
|
48
|
+
{
|
|
49
|
+
stdout: stdout,
|
|
50
|
+
stderr: stderr,
|
|
51
|
+
exit_status: status.exitstatus
|
|
52
|
+
}
|
|
53
|
+
rescue Errno::ENOENT
|
|
54
|
+
# Command not found
|
|
55
|
+
{
|
|
56
|
+
stdout: '',
|
|
57
|
+
stderr: 'Command not found',
|
|
58
|
+
exit_status: 127
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|