ukiryu 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- data/.github/workflows/test.yml +0 -143
|
@@ -0,0 +1,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
|