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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ class Config
5
+ # Type converter for environment variable values
6
+ # Converts string ENV values to appropriate Ruby types
7
+ class TypeConverter
8
+ BOOLEAN_VALUES = {
9
+ 'true' => true,
10
+ '1' => true,
11
+ 'yes' => true,
12
+ 'false' => false,
13
+ '0' => false,
14
+ 'no' => false
15
+ }.freeze
16
+
17
+ class << self
18
+ def convert(attribute, value)
19
+ return nil if value.nil? || value.empty?
20
+
21
+ type = EnvSchema.type_for(attribute)
22
+ case type
23
+ when :boolean
24
+ convert_boolean(value)
25
+ when :integer
26
+ convert_integer(value)
27
+ when :symbol
28
+ convert_symbol(value)
29
+ when :string
30
+ value
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def convert_boolean(value)
39
+ normalized = value.to_s.downcase
40
+ return BOOLEAN_VALUES[normalized] if BOOLEAN_VALUES.key?(normalized)
41
+
42
+ raise ArgumentError,
43
+ "Invalid boolean value: #{value}. " \
44
+ "Valid values: #{BOOLEAN_VALUES.keys.join(', ')}"
45
+ end
46
+
47
+ def convert_integer(value)
48
+ Integer(value)
49
+ rescue ArgumentError
50
+ raise ArgumentError, "Invalid integer value: #{value}"
51
+ end
52
+
53
+ def convert_symbol(value)
54
+ value.to_sym
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config/env_provider'
4
+ require_relative 'config/override_resolver'
5
+
6
+ module Ukiryu
7
+ # Global configuration for Ukiryu
8
+ # Provides unified configuration across CLI, Ruby API, and programmatic interfaces
9
+ #
10
+ # Configuration priority (highest to lowest):
11
+ # 1. CLI options (passed at runtime)
12
+ # 2. Environment variables (UKIRYU_*)
13
+ # 3. Programmatic configuration (Config.configure)
14
+ # 4. Default values
15
+ #
16
+ # @example Configure programmatically
17
+ # Ukiryu::Config.configure do |config|
18
+ # config.timeout = 30
19
+ # config.debug = true
20
+ # config.format = :json
21
+ # end
22
+ #
23
+ # @example Configure via environment variables
24
+ # export UKIRYU_TIMEOUT=60
25
+ # export UKIRYU_DEBUG=true
26
+ # export UKIRYU_FORMAT=json
27
+ #
28
+ # @example Configure via CLI options
29
+ # ukiryu exec ping host=example.com --format json --timeout 30
30
+ class Config
31
+ class << self
32
+ def instance
33
+ @instance ||= new
34
+ end
35
+
36
+ # Configure Ukiryu with a block
37
+ # @yield [config] The configuration instance
38
+ # @return [Config] The configuration instance
39
+ def configure
40
+ yield instance if block_given?
41
+ instance
42
+ end
43
+
44
+ # Reset configuration to defaults
45
+ def reset!
46
+ @instance = new
47
+ end
48
+
49
+ # Delegate to instance
50
+ def method_missing(method, ...)
51
+ instance.send(method, ...)
52
+ end
53
+
54
+ def respond_to_missing?(method, include_private = false)
55
+ instance.respond_to?(method) || super
56
+ end
57
+ end
58
+
59
+ # @!attribute [r] resolver
60
+ # @return [OverrideResolver] The resolver for configuration values
61
+ attr_reader :resolver
62
+
63
+ def initialize
64
+ @resolver = build_resolver
65
+ end
66
+
67
+ # Reset configuration to defaults
68
+ def reset!
69
+ @resolver = build_resolver
70
+ end
71
+
72
+ # Execution timeout in seconds
73
+ # @return [Integer, nil] timeout in seconds, or nil for no timeout
74
+ def timeout
75
+ @resolver.resolve(:timeout)
76
+ end
77
+
78
+ # Set execution timeout
79
+ # @param value [Integer] timeout in seconds
80
+ def timeout=(value)
81
+ @resolver.set_programmatic(:timeout, value)
82
+ end
83
+
84
+ # Debug mode flag
85
+ # @return [Boolean] true if debug mode is enabled
86
+ def debug
87
+ @resolver.resolve(:debug)
88
+ end
89
+
90
+ # Set debug mode
91
+ # @param value [Boolean] debug mode flag
92
+ def debug=(value)
93
+ @resolver.set_programmatic(:debug, value)
94
+ end
95
+
96
+ # Dry run flag
97
+ # @return [Boolean] true if dry run is enabled
98
+ def dry_run
99
+ @resolver.resolve(:dry_run)
100
+ end
101
+
102
+ # Set dry run mode
103
+ # @param value [Boolean] dry run flag
104
+ def dry_run=(value)
105
+ @resolver.set_programmatic(:dry_run, value)
106
+ end
107
+
108
+ # Output format
109
+ # @return [Symbol] output format (:yaml, :json, :table)
110
+ def format
111
+ @resolver.resolve(:format)
112
+ end
113
+
114
+ # Set output format
115
+ # @param value [Symbol] output format
116
+ def format=(value)
117
+ @resolver.set_programmatic(:format, value)
118
+ end
119
+
120
+ # Output file path
121
+ # @return [String, nil] output file path, or nil for stdout
122
+ def output
123
+ @resolver.resolve(:output)
124
+ end
125
+
126
+ # Set output file path
127
+ # @param value [String] output file path
128
+ def output=(value)
129
+ @resolver.set_programmatic(:output, value)
130
+ end
131
+
132
+ # Registry path
133
+ # @return [String, nil] path to tool registry
134
+ def registry
135
+ @resolver.resolve(:registry)
136
+ end
137
+
138
+ # Set registry path
139
+ # @param value [String] path to tool registry
140
+ def registry=(value)
141
+ @resolver.set_programmatic(:registry, value)
142
+ end
143
+
144
+ # Tool search paths (comma-separated)
145
+ # @return [String, nil] comma-separated search paths
146
+ def search_paths
147
+ @resolver.resolve(:search_paths)
148
+ end
149
+
150
+ # Set search paths
151
+ # @param value [String] comma-separated search paths
152
+ def search_paths=(value)
153
+ @resolver.set_programmatic(:search_paths, value)
154
+ end
155
+
156
+ # Use color in output
157
+ # @return [Boolean] true if colors should be used
158
+ def use_color
159
+ @resolver.resolve(:use_color)
160
+ end
161
+
162
+ # Set color usage
163
+ # @param value [Boolean] color usage flag
164
+ def use_color=(value)
165
+ @resolver.set_programmatic(:use_color, value)
166
+ end
167
+
168
+ # Check if colors are disabled
169
+ # Returns true if use_color is explicitly false or if NO_COLOR is set
170
+ # @return [Boolean] true if colors should be disabled
171
+ def colors_disabled?
172
+ use_color == false
173
+ end
174
+
175
+ # Metrics collection flag
176
+ # @return [Boolean] true if metrics should be collected
177
+ def metrics
178
+ @resolver.resolve(:metrics)
179
+ end
180
+
181
+ # Set metrics collection
182
+ # @param value [Boolean] metrics flag
183
+ def metrics=(value)
184
+ @resolver.set_programmatic(:metrics, value)
185
+ end
186
+
187
+ # Shell to use for command execution
188
+ # @return [Symbol, nil] shell symbol (:bash, :zsh, :fish, :powershell, :cmd) or nil for auto-detect
189
+ def shell
190
+ @resolver.resolve(:shell)
191
+ end
192
+
193
+ # Set shell
194
+ # @param value [Symbol, String] shell symbol or string
195
+ def shell=(value)
196
+ @resolver.set_programmatic(:shell, value&.to_sym)
197
+ end
198
+
199
+ # Set CLI option (highest priority)
200
+ # @param key [Symbol] option key
201
+ # @param value [Object] option value
202
+ def set_cli_option(key, value)
203
+ @resolver.set_cli(key, value)
204
+ end
205
+
206
+ # Get configuration as hash
207
+ # @return [Hash] configuration values
208
+ def to_h
209
+ {
210
+ timeout: timeout,
211
+ debug: debug,
212
+ dry_run: dry_run,
213
+ metrics: metrics,
214
+ shell: shell,
215
+ format: format,
216
+ output: output,
217
+ registry: registry,
218
+ search_paths: search_paths,
219
+ use_color: use_color
220
+ }
221
+ end
222
+
223
+ private
224
+
225
+ def build_resolver
226
+ defaults = {
227
+ timeout: nil,
228
+ debug: false,
229
+ dry_run: false,
230
+ metrics: false,
231
+ shell: nil,
232
+ format: :yaml,
233
+ output: nil,
234
+ registry: nil,
235
+ search_paths: nil,
236
+ use_color: nil # nil means auto-detect
237
+ }
238
+
239
+ env = EnvProvider.load_all
240
+
241
+ OverrideResolver.new(
242
+ defaults: defaults,
243
+ programmatic: {},
244
+ env: env,
245
+ cli: {}
246
+ )
247
+ end
248
+ end
249
+ end
data/lib/ukiryu/errors.rb CHANGED
@@ -17,6 +17,9 @@ module Ukiryu
17
17
  class ProfileNotFoundError < Error; end
18
18
  class ProfileLoadError < Error; end
19
19
 
20
+ # Definition loading errors
21
+ class LoadError < Error; end
22
+
20
23
  # Tool errors
21
24
  class ToolNotFoundError < Error; end
22
25
  class ExecutableNotFoundError < Error; end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executor'
4
+ require_relative 'platform'
5
+
6
+ module Ukiryu
7
+ # Executable locator for finding tool executables
8
+ #
9
+ # This module provides centralized executable location logic with:
10
+ # - Custom search paths from tool profiles
11
+ # - Shell-specific path extensions (Windows PATHEXT)
12
+ # - Fallback to system PATH
13
+ # - Proper OOP design (no duplicated code)
14
+ #
15
+ # @example Finding an executable
16
+ # path = ExecutableLocator.find(
17
+ # tool_name: 'imagemagick',
18
+ # aliases: ['magick'],
19
+ # search_paths: ['/opt/homebrew/bin/magick'],
20
+ # platform: :macos
21
+ # )
22
+ module ExecutableLocator
23
+ class << self
24
+ # Find an executable by name with search paths
25
+ #
26
+ # @param tool_name [String] the primary tool name
27
+ # @param aliases [Array<String>] alternative names to try
28
+ # @param search_paths [Array<String>, Models::SearchPaths] custom search paths or SearchPaths model
29
+ # @param platform [Symbol] the platform (defaults to Runtime.platform)
30
+ # @return [String, nil] the executable path or nil if not found
31
+ def find(tool_name:, aliases: [], search_paths: [], platform: nil)
32
+ platform ||= Runtime.instance.platform
33
+
34
+ # Convert SearchPaths model to array if needed
35
+ paths = normalize_search_paths(search_paths, platform)
36
+
37
+ # Try primary name first
38
+ exe = try_find(tool_name, paths)
39
+ return exe if exe
40
+
41
+ # Try aliases
42
+ aliases.each do |alias_name|
43
+ exe = try_find(alias_name, paths)
44
+ return exe if exe
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ # Find an executable in the system PATH
51
+ #
52
+ # @param command [String] the command or executable name
53
+ # @param additional_paths [Array<String>] additional search paths
54
+ # @return [String, nil] the full path to the executable, or nil if not found
55
+ def find_in_path(command, additional_paths: [])
56
+ # Try with PATHEXT extensions (Windows executables)
57
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
58
+
59
+ search_paths = Platform.executable_search_paths
60
+ search_paths.concat(additional_paths) if additional_paths
61
+ search_paths.uniq!
62
+
63
+ search_paths.each do |dir|
64
+ exts.each do |ext|
65
+ exe = File.join(dir, "#{command}#{ext}")
66
+ return exe if File.executable?(exe) && !File.directory?(exe)
67
+ end
68
+ end
69
+
70
+ nil
71
+ end
72
+
73
+ private
74
+
75
+ # Normalize search paths to array format
76
+ #
77
+ # @param search_paths [Array<String>, Models::SearchPaths] the search paths
78
+ # @param platform [Symbol] the platform
79
+ # @return [Array<String>] normalized array of paths
80
+ def normalize_search_paths(search_paths, platform)
81
+ return [] unless search_paths
82
+
83
+ # If it's a SearchPaths model, get platform-specific paths
84
+ return search_paths.for_platform(platform) || [] if search_paths.is_a?(Models::SearchPaths)
85
+
86
+ # Already an array
87
+ search_paths
88
+ end
89
+
90
+ # Try to find an executable by name
91
+ #
92
+ # @param command [String] the command name
93
+ # @param search_paths [Array<String>] custom search paths
94
+ # @return [String, nil] the executable path or nil
95
+ def try_find(command, search_paths)
96
+ # Check custom search paths first (both absolute paths and glob patterns)
97
+ search_paths.each do |path_pattern|
98
+ # Handle glob patterns
99
+ if path_pattern.include?('*')
100
+ Dir.glob(path_pattern).each do |expanded|
101
+ return expanded if File.executable?(expanded) && !File.directory?(expanded)
102
+ end
103
+ # Handle absolute paths
104
+ elsif File.executable?(path_pattern) && !File.directory?(path_pattern)
105
+ return path_pattern
106
+ end
107
+ end
108
+
109
+ # Fall back to PATH
110
+ find_in_path(command)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Execution
5
+ # Execution command information
6
+ #
7
+ # Encapsulates details about the executed command
8
+ class CommandInfo
9
+ attr_reader :executable, :arguments, :full_command, :shell, :tool_name, :command_name
10
+
11
+ def initialize(executable:, arguments:, full_command:, shell: nil, tool_name: nil, command_name: nil)
12
+ @executable = executable
13
+ @arguments = arguments
14
+ @full_command = full_command
15
+ @shell = shell
16
+ @tool_name = tool_name
17
+ @command_name = command_name
18
+ end
19
+
20
+ # Get the executable name only
21
+ #
22
+ # @return [String] executable name
23
+ def executable_name
24
+ File.basename(@executable)
25
+ end
26
+
27
+ # Get argument count
28
+ #
29
+ # @return [Integer] number of arguments
30
+ def argument_count
31
+ @arguments.count
32
+ end
33
+
34
+ # String representation
35
+ #
36
+ # @return [String] command string
37
+ def to_s
38
+ @full_command
39
+ end
40
+
41
+ # Inspect
42
+ #
43
+ # @return [String] inspection string
44
+ def inspect
45
+ "#<Ukiryu::Execution::CommandInfo exe=#{executable_name.inspect} args=#{argument_count}>"
46
+ end
47
+
48
+ # Convert to hash
49
+ #
50
+ # @return [Hash] command info as hash
51
+ def to_h
52
+ {
53
+ executable: @executable,
54
+ executable_name: executable_name,
55
+ tool_name: @tool_name,
56
+ command_name: @command_name,
57
+ arguments: @arguments,
58
+ full_command: @full_command,
59
+ shell: @shell
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Execution
5
+ # Execution metadata
6
+ #
7
+ # Provides timing and execution environment information
8
+ class ExecutionMetadata
9
+ attr_reader :started_at, :finished_at, :duration, :timeout
10
+
11
+ def initialize(started_at:, finished_at:, timeout: nil)
12
+ @started_at = started_at
13
+ @finished_at = finished_at
14
+ @timeout = timeout
15
+ @duration = calculate_duration
16
+ end
17
+
18
+ # Calculate duration from start and finish times
19
+ #
20
+ # @return [Float, nil] duration in seconds
21
+ def calculate_duration
22
+ return nil unless @started_at && @finished_at
23
+
24
+ @finished_at - @started_at
25
+ end
26
+
27
+ # Get execution duration in seconds
28
+ #
29
+ # @return [Float, nil] duration in seconds
30
+ def duration_seconds
31
+ @duration
32
+ end
33
+
34
+ # Get execution duration in milliseconds
35
+ #
36
+ # @return [Float, nil] duration in milliseconds
37
+ def duration_milliseconds
38
+ @duration ? @duration * 1000 : nil
39
+ end
40
+
41
+ # Check if execution timed out
42
+ #
43
+ # @return [Boolean] true if timeout was set and exceeded
44
+ def timed_out?
45
+ return false unless @timeout && @duration
46
+
47
+ @duration > @timeout
48
+ end
49
+
50
+ # Format duration for display
51
+ #
52
+ # @return [String] formatted duration
53
+ def formatted_duration
54
+ return 'N/A' unless @duration
55
+
56
+ if @duration < 1
57
+ "#{(@duration * 1000).round(2)}ms"
58
+ elsif @duration < 60
59
+ "#{@duration.round(3)}s"
60
+ else
61
+ minutes = @duration / 60
62
+ seconds = @duration % 60
63
+ "#{minutes.to_i}m#{seconds.round(1)}s"
64
+ end
65
+ end
66
+
67
+ # Convert to hash
68
+ #
69
+ # @return [Hash] metadata as hash
70
+ def to_h
71
+ {
72
+ started_at: @started_at,
73
+ finished_at: @finished_at,
74
+ duration: @duration,
75
+ duration_seconds: @duration,
76
+ duration_milliseconds: duration_milliseconds,
77
+ timeout: @timeout,
78
+ timed_out: timed_out?
79
+ }
80
+ end
81
+
82
+ # String representation
83
+ #
84
+ # @return [String] formatted string
85
+ def to_s
86
+ "duration: #{formatted_duration}"
87
+ end
88
+
89
+ # Inspect
90
+ #
91
+ # @return [String] inspection string
92
+ def inspect
93
+ "#<Ukiryu::Execution::ExecutionMetadata duration=#{formatted_duration}>"
94
+ end
95
+ end
96
+ end
97
+ end