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,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Execution
5
+ # Captured output from command execution
6
+ #
7
+ # Provides typed access to stdout and stderr with parsing utilities
8
+ class Output
9
+ attr_reader :raw_stdout, :raw_stderr, :exit_status
10
+
11
+ def initialize(stdout:, stderr:, exit_status:)
12
+ @raw_stdout = stdout
13
+ @raw_stderr = stderr
14
+ @exit_status = exit_status
15
+ end
16
+
17
+ # Get stdout as a string (stripped)
18
+ #
19
+ # @return [String] stripped stdout
20
+ def stdout
21
+ @raw_stdout.strip
22
+ end
23
+
24
+ # Get stderr as a string (stripped)
25
+ #
26
+ # @return [String] stripped stderr
27
+ def stderr
28
+ @raw_stderr.strip
29
+ end
30
+
31
+ # Get stdout lines as an array
32
+ #
33
+ # @return [Array<String>] stdout split by lines
34
+ def stdout_lines
35
+ @raw_stdout.split("\n")
36
+ end
37
+
38
+ # Get stderr lines as an array
39
+ #
40
+ # @return [Array<String>] stderr split by lines
41
+ def stderr_lines
42
+ @raw_stderr.split("\n")
43
+ end
44
+
45
+ # Check if stdout contains a pattern
46
+ #
47
+ # @param pattern [String, Regexp] pattern to search for
48
+ # @return [Boolean] true if pattern is found
49
+ def stdout_contains?(pattern)
50
+ if pattern.is_a?(Regexp)
51
+ @raw_stdout.match?(pattern)
52
+ else
53
+ @raw_stdout.include?(pattern.to_s)
54
+ end
55
+ end
56
+
57
+ # Check if stderr contains a pattern
58
+ #
59
+ # @param pattern [String, Regexp] pattern to search for
60
+ # @return [Boolean] true if pattern is found
61
+ def stderr_contains?(pattern)
62
+ if pattern.is_a?(Regexp)
63
+ @raw_stderr.match?(pattern)
64
+ else
65
+ @raw_stderr.include?(pattern.to_s)
66
+ end
67
+ end
68
+
69
+ # Check if stdout is empty
70
+ #
71
+ # @return [Boolean] true if stdout is empty
72
+ def stdout_empty?
73
+ @raw_stdout.strip.empty?
74
+ end
75
+
76
+ # Check if stderr is empty
77
+ #
78
+ # @return [Boolean] true if stderr is empty
79
+ def stderr_empty?
80
+ @raw_stderr.strip.empty?
81
+ end
82
+
83
+ # Get stdout length
84
+ #
85
+ # @return [Integer] byte length of stdout
86
+ def stdout_length
87
+ @raw_stdout.length
88
+ end
89
+
90
+ # Get stderr length
91
+ #
92
+ # @return [Integer] byte length of stderr
93
+ def stderr_length
94
+ @raw_stderr.length
95
+ end
96
+
97
+ # Check if command succeeded
98
+ #
99
+ # @return [Boolean] true if exit status is 0
100
+ def success?
101
+ @exit_status.zero?
102
+ end
103
+
104
+ # Check if command failed
105
+ #
106
+ # @return [Boolean] true if exit status is non-zero
107
+ def failure?
108
+ @exit_status != 0
109
+ end
110
+
111
+ # Convert to hash
112
+ #
113
+ # @return [Hash] output as hash
114
+ def to_h
115
+ {
116
+ stdout: @raw_stdout,
117
+ stderr: @raw_stderr,
118
+ exit_status: @exit_status,
119
+ success: success?,
120
+ stdout_lines: stdout_lines,
121
+ stderr_lines: stderr_lines
122
+ }
123
+ end
124
+
125
+ # String representation
126
+ #
127
+ # @return [String] summary string
128
+ def to_s
129
+ if success?
130
+ "Success (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
131
+ else
132
+ "Failed (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
133
+ end
134
+ end
135
+
136
+ # Inspect
137
+ #
138
+ # @return [String] inspection string
139
+ def inspect
140
+ "#<Ukiryu::Execution::Output exit=#{@exit_status} success=#{success?}>"
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Execution
5
+ # Result class for command execution
6
+ #
7
+ # Provides a rich, object-oriented interface to command execution results.
8
+ # Composes CommandInfo, Output, and ExecutionMetadata for a fully OOP design.
9
+ class Result
10
+ attr_reader :command_info, :output, :metadata
11
+
12
+ def initialize(command_info:, output:, metadata:)
13
+ @command_info = command_info
14
+ @output = output
15
+ @metadata = metadata
16
+ end
17
+
18
+ # Get the full command string
19
+ #
20
+ # @return [String] executed command
21
+ def command
22
+ @command_info.full_command
23
+ end
24
+
25
+ # Get the executable
26
+ #
27
+ # @return [String] executable path
28
+ def executable
29
+ @command_info.executable
30
+ end
31
+
32
+ # Get the executable name only
33
+ #
34
+ # @return [String] executable name
35
+ def executable_name
36
+ @command_info.executable_name
37
+ end
38
+
39
+ # Get raw stdout
40
+ #
41
+ # @return [String] raw stdout
42
+ def stdout
43
+ @output.raw_stdout
44
+ end
45
+
46
+ # Get raw stderr
47
+ #
48
+ # @return [String] raw stderr
49
+ def stderr
50
+ @output.raw_stderr
51
+ end
52
+
53
+ # Get exit status code
54
+ #
55
+ # @return [Integer] exit status
56
+ def status
57
+ @output.exit_status
58
+ end
59
+ alias exit_code status
60
+
61
+ # Get the exit code (alias for status)
62
+ #
63
+ # @return [Integer] exit status
64
+ def exit_status
65
+ @output.exit_status
66
+ end
67
+
68
+ # Get start time
69
+ #
70
+ # @return [Time] when command started
71
+ def started_at
72
+ @metadata.started_at
73
+ end
74
+
75
+ # Get finish time
76
+ #
77
+ # @return [Time] when command finished
78
+ def finished_at
79
+ @metadata.finished_at
80
+ end
81
+
82
+ # Get execution duration
83
+ #
84
+ # @return [Float, nil] duration in seconds
85
+ def duration
86
+ @metadata.duration
87
+ end
88
+
89
+ # Get execution duration (alias)
90
+ #
91
+ # @return [Float, nil] duration in seconds
92
+ def execution_time
93
+ @metadata.duration_seconds
94
+ end
95
+
96
+ # Check if the command succeeded
97
+ #
98
+ # @return [Boolean]
99
+ def success?
100
+ @output.success?
101
+ end
102
+
103
+ # Check if the command failed
104
+ #
105
+ # @return [Boolean]
106
+ def failure?
107
+ @output.failure?
108
+ end
109
+
110
+ # Get stdout as a stripped string
111
+ #
112
+ # @return [String] stripped stdout
113
+ def output
114
+ @output.stdout
115
+ end
116
+
117
+ # Get stderr as a stripped string
118
+ #
119
+ # @return [String] stripped stderr
120
+ def error_output
121
+ @output.stderr
122
+ end
123
+
124
+ # Get stdout lines
125
+ #
126
+ # @return [Array<String>] stdout split by lines
127
+ def stdout_lines
128
+ @output.stdout_lines
129
+ end
130
+
131
+ # Get stderr lines
132
+ #
133
+ # @return [Array<String>] stderr split by lines
134
+ def stderr_lines
135
+ @output.stderr_lines
136
+ end
137
+
138
+ # Check if stdout contains a pattern
139
+ #
140
+ # @param pattern [String, Regexp] pattern to search for
141
+ # @return [Boolean] true if pattern is found
142
+ def stdout_contains?(pattern)
143
+ @output.stdout_contains?(pattern)
144
+ end
145
+
146
+ # Check if stderr contains a pattern
147
+ #
148
+ # @param pattern [String, Regexp] pattern to search for
149
+ # @return [Boolean] true if pattern is found
150
+ def stderr_contains?(pattern)
151
+ @output.stderr_contains?(pattern)
152
+ end
153
+
154
+ # Get a hash representation of the result
155
+ #
156
+ # @return [Hash] result data as a hash
157
+ def to_h
158
+ {
159
+ command: @command_info.to_h,
160
+ output: @output.to_h,
161
+ metadata: @metadata.to_h,
162
+ success: success?,
163
+ status: status
164
+ }
165
+ end
166
+
167
+ # Get a JSON representation of the result
168
+ #
169
+ # @return [String] result data as JSON
170
+ def to_json(*args)
171
+ require 'json'
172
+ to_h.to_json(*args)
173
+ end
174
+
175
+ # String representation of the result
176
+ #
177
+ # @return [String] summary string
178
+ def to_s
179
+ if success?
180
+ "Success: #{command} (#{metadata.formatted_duration})"
181
+ else
182
+ "Failed: #{command} (exit: #{status}, #{metadata.formatted_duration})"
183
+ end
184
+ end
185
+
186
+ # Inspect
187
+ #
188
+ # @return [String] inspection string
189
+ def inspect
190
+ "#<Ukiryu::Execution::Result #{self}>"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'execution/command_info'
4
+ require_relative 'execution/output'
5
+ require_relative 'execution/metadata'
6
+ require_relative 'execution/result'
7
+
8
+ module Ukiryu
9
+ # Execution namespace for result models
10
+ #
11
+ # This namespace contains OOP models for command execution results,
12
+ # providing a clean separation between execution logic and result modeling.
13
+ module Execution
14
+ end
15
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runtime'
4
+ require_relative 'config'
5
+
6
+ module Ukiryu
7
+ # Simple thread-local storage for contexts
8
+ #
9
+ # @api private
10
+ class ExecutionContext
11
+ class ThreadLocal
12
+ def initialize
13
+ @key = "__ukiryu_execution_context_#{object_id}__"
14
+ end
15
+
16
+ def value
17
+ Thread.current[@key]
18
+ end
19
+
20
+ def value=(val)
21
+ Thread.current[@key] = val
22
+ end
23
+ end
24
+ end
25
+
26
+ # Execution context for dependency injection and execution-scoped configuration
27
+ #
28
+ # Provides a non-singleton alternative to Runtime.instance for better testability.
29
+ # Wraps runtime configuration and execution-specific options in a single object.
30
+ #
31
+ # @example Using ExecutionContext in production
32
+ # context = Ukiryu::ExecutionContext.current
33
+ # context.platform # => :macos
34
+ # context.shell # => :bash
35
+ # context.registry # => '/path/to/registry'
36
+ #
37
+ # @example Using ExecutionContext in tests
38
+ # context = Ukiryu::ExecutionContext.new(
39
+ # platform: :linux,
40
+ # shell: :zsh,
41
+ # registry_path: '/test/registry'
42
+ # )
43
+ # context.platform # => :linux
44
+ # context.shell # => :zsh
45
+ class ExecutionContext
46
+ # Thread-local storage for current context
47
+ @current_context = ThreadLocal.new
48
+
49
+ class << self
50
+ # Get the current execution context
51
+ #
52
+ # Creates a new context from the global Runtime if none is set.
53
+ #
54
+ # @return [ExecutionContext] the current context
55
+ def current
56
+ @current_context.value ||= from_runtime
57
+ end
58
+
59
+ # Set the current execution context
60
+ #
61
+ # @param context [ExecutionContext] the context to set
62
+ # @return [void]
63
+ def current=(context)
64
+ @current_context.value = context
65
+ end
66
+
67
+ # Execute a block with a temporary context
68
+ #
69
+ # @param context [ExecutionContext] the context to use
70
+ # @yield the block to execute
71
+ # @return [Object] the block's return value
72
+ def with_context(context)
73
+ old_context = @current_context.value
74
+ @current_context.value = context
75
+ yield
76
+ ensure
77
+ @current_context.value = old_context
78
+ end
79
+
80
+ # Create a context from the global Runtime
81
+ #
82
+ # @return [ExecutionContext] a new context with runtime values
83
+ def from_runtime
84
+ runtime = Runtime.instance
85
+ new(
86
+ platform: runtime.platform,
87
+ shell: runtime.shell,
88
+ registry_path: Registry.default_registry_path,
89
+ timeout: Config.timeout,
90
+ debug: Config.debug,
91
+ metrics: Config.metrics
92
+ )
93
+ end
94
+
95
+ # Reset the current context (mainly for testing)
96
+ #
97
+ # @api private
98
+ def reset_current!
99
+ @current_context.value = nil
100
+ end
101
+ end
102
+
103
+ # Platform (:macos, :linux, :windows)
104
+ #
105
+ # @return [Symbol] the platform
106
+ attr_reader :platform
107
+
108
+ # Shell (:bash, :zsh, :fish, :powershell, :cmd)
109
+ #
110
+ # @return [Symbol] the shell
111
+ attr_reader :shell
112
+
113
+ # Registry path for tool profiles
114
+ #
115
+ # @return [String, nil] the registry path
116
+ attr_reader :registry_path
117
+
118
+ # Execution timeout in seconds
119
+ #
120
+ # @return [Integer, nil] the timeout
121
+ attr_reader :timeout
122
+
123
+ # Debug mode enabled
124
+ #
125
+ # @return [Boolean] true if debug mode is enabled
126
+ attr_reader :debug
127
+
128
+ # Metrics collection enabled
129
+ #
130
+ # @return [Boolean] true if metrics are enabled
131
+ attr_reader :metrics
132
+
133
+ # User-defined options hash
134
+ #
135
+ # @return [Hash] additional options
136
+ attr_reader :options
137
+
138
+ # Create a new execution context
139
+ #
140
+ # @param platform [Symbol] the platform (:macos, :linux, :windows)
141
+ # @param shell [Symbol] the shell (:bash, :zsh, :fish, :powershell, :cmd)
142
+ # @param registry_path [String, nil] the registry path
143
+ # @param timeout [Integer, nil] execution timeout in seconds
144
+ # @param debug [Boolean] debug mode flag
145
+ # @param metrics [Boolean] metrics collection flag
146
+ # @param options [Hash] additional options
147
+ def initialize(platform: nil,
148
+ shell: nil,
149
+ registry_path: nil,
150
+ timeout: nil,
151
+ debug: false,
152
+ metrics: false,
153
+ options: {})
154
+ @platform = platform
155
+ @shell = shell
156
+ @registry_path = registry_path
157
+ @timeout = timeout
158
+ @debug = debug
159
+ @metrics = metrics
160
+ @options = options
161
+ end
162
+
163
+ # Get the shell class for this context
164
+ #
165
+ # @return [Class] the shell class
166
+ def shell_class
167
+ Shell.class_for(@shell)
168
+ end
169
+
170
+ # Check if running on a specific platform
171
+ #
172
+ # @param plat [Symbol] the platform to check
173
+ # @return [Boolean] true if running on the specified platform
174
+ def on_platform?(plat)
175
+ @platform == plat.to_sym
176
+ end
177
+
178
+ # Check if using a specific shell
179
+ #
180
+ # @param sh [Symbol] the shell to check
181
+ # @return [Boolean] true if using the specified shell
182
+ def using_shell?(sh)
183
+ @shell == sh.to_sym
184
+ end
185
+
186
+ # Check if running on Windows
187
+ #
188
+ # @return [Boolean] true if on Windows
189
+ def windows?
190
+ on_platform?(:windows)
191
+ end
192
+
193
+ # Check if running on macOS
194
+ #
195
+ # @return [Boolean] true if on macOS
196
+ def macos?
197
+ on_platform?(:macos)
198
+ end
199
+
200
+ # Check if running on Linux
201
+ #
202
+ # @return [Boolean] true if on Linux
203
+ def linux?
204
+ on_platform?(:linux)
205
+ end
206
+
207
+ # Check if using a Unix-like shell
208
+ #
209
+ # @return [Boolean] true if using bash, zsh, fish, or sh
210
+ def unix_shell?
211
+ %i[bash zsh fish sh].include?(@shell)
212
+ end
213
+
214
+ # Check if using a Windows shell
215
+ #
216
+ # @return [Boolean] true if using powershell or cmd
217
+ def windows_shell?
218
+ %i[powershell cmd].include?(@shell)
219
+ end
220
+
221
+ # Create a new context with merged options
222
+ #
223
+ # @param changes [Hash] the changes to merge
224
+ # @return [ExecutionContext] a new context with merged values
225
+ def merge(changes)
226
+ self.class.new(
227
+ platform: changes.fetch(:platform, @platform),
228
+ shell: changes.fetch(:shell, @shell),
229
+ registry_path: changes.fetch(:registry_path, @registry_path),
230
+ timeout: changes.fetch(:timeout, @timeout),
231
+ debug: changes.fetch(:debug, @debug),
232
+ metrics: changes.fetch(:metrics, @metrics),
233
+ options: @options.merge(changes.fetch(:options, {}))
234
+ )
235
+ end
236
+
237
+ # String representation
238
+ #
239
+ # @return [String] the context as a string
240
+ def to_s
241
+ "ExecutionContext(platform=#{@platform}, shell=#{@shell}, registry=#{@registry_path})"
242
+ end
243
+
244
+ # Inspect
245
+ #
246
+ # @return [String] the inspection string
247
+ def inspect
248
+ "#<#{self.class} #{self}>"
249
+ end
250
+ end
251
+ end